Commits

Stefan Saasen  committed a43a00c

Simple Sinatra application to browse a Confluence space.

* Java <-> Ruby bridge servlet
* Properly configure the JRuby ScriptingContainer to load ruby files from the classpath.
* Set the LOAD_PATH based on the bundled RubyGems using a simple index file
that is generated at build time.
* Integrate Maven and JRuby Rake to install the required RubyGem libraries.

  • Participants
  • Parent commits 03002dd

Comments (0)

Files changed (35)

+src/main/rubygems/*
+source :rubygems
+
+gem "sinatra"
+gem "rack"
+
+group :test do
+    gem 'shoulda'
+    gem 'shoulda-context'
+end

File Gemfile.lock

+GEM
+  remote: http://rubygems.org/
+  specs:
+    rack (1.4.1)
+    rack-protection (1.2.0)
+      rack
+    shoulda (3.0.1)
+      shoulda-context (~> 1.0.0)
+      shoulda-matchers (~> 1.0.0)
+    shoulda-context (1.0.0)
+    shoulda-matchers (1.0.0)
+    sinatra (1.3.2)
+      rack (~> 1.3, >= 1.3.6)
+      rack-protection (~> 1.2)
+      tilt (~> 1.3, >= 1.3.3)
+    tilt (1.3.3)
+
+PLATFORMS
+  java
+  ruby
+
+DEPENDENCIES
+  rack
+  shoulda
+  shoulda-context
+  sinatra
+JRuby Example Plugin
+====================
+
+This is an example Alassian plugin using JRuby and Java.
+
+The plugin shows:
+
+* How to embed the JRuby runtime (via the JRuby Embed API),
+* How to execute Ruby scripts that are part of the plugin (the example runs
+  a [Sinatra app](http://www.sinatrarb.com/) that lists spaces and shows
+  Confluence pages.
+* How to set variables from Java to be used in Ruby scripts
+* How to work with the output of Ruby scripts
+* How to automatically install and bundle Ruby libraries (RubyGems)
+
+![Ruby Sinatra app running in Confluence](http://ssaasen.bitbucket.org/atlassian-jruby-example-plugin/images/connie-sinatra-3.png)
+
+
+---
+
+*This plugin is for demonstration purposes. Hence it is incomplete and not
+production ready.*
+
+---
+
+
+
+Development
+===========
+
+Prerequisite:
+
+Make sure that you have installed the [Atlassian Plugin SDK](https://developer.atlassian.com/display/DOCS/Installing+the+Atlassian+Plugin+SDK) and that the various `atlas-*` commands are available on your `PATH`.
+
+
+Clone the repository and start Confluence from within the plugin project
+
+    atlas-debug
+
+The first time this command is executed, the required RubyGem libraries will be
+automatically downloaded and copied to the `src/main/ruby/rubygems` directory.
+
+The Maven project is set up to copy the Ruby scripts in `src/main/ruby/**` into
+your plugin.
+
+The list of RubyGems is defined in the `required.gems` property. Feel free to
+change this list to add the RubyGems you need for your own implementation.
+
+    <required.gems>sinatra jruby-rack rio nokogiri</required.gems>
+
+After updating the list, the new RubyGems can be downloaded by running:
+
+    mvn -Prubygems process-resources
+
+Note: The build process creates an index file `META-INF/gemspec.index` that is used
+by the `ScriptingContainerProviderImpl` to correctly set the Ruby `LOAD_PATH`
+so that all the RubyGems can be read from the classpath.
+
+
+Running Tests
+=============
+
+At the moment the Ruby tests need to be run manually (they are not executed as
+part of the Maven build yet).
+
+With `jruby` available on your `PATH` run (see http://jruby.org/getting-started to get started):
+
+    jruby -S bundle install
+    jruby -S rake test
+
+References
+==========
+
+https://github.com/jruby/jruby/wiki/JRubyCompiler
+
+https://github.com/jruby/jruby/wiki/FAQs#Calling_Into_Java
+
+https://github.com/jruby/jruby/wiki/GeneratingJavaClasses
+
+http://www.sinatrarb.com/

File README.txt

-You have successfully created a plugin using the Confluence plugin archetype!
-
-Here are the SDK commands you'll use immediately:
-
-* atlas-run   -- installs this plugin into Confluence and starts it on http://localhost:1990/confluence
-* atlas-debug -- same as atlas-run, but allows a debugger to attach at port 5005
-* atlas-cli   -- after atlas-run or atlas-debug, opens a Maven command line window:
-                 - 'pi' reinstalls the plugin into the running Confluence instance
-* atlas-help  -- prints description for all commands in the SDK
-
-Full documentation is always available at:
-
-https://developer.atlassian.com/display/DOCS/Developing+with+the+Atlassian+Plugin+SDK
+require 'rake'
+require 'rake/testtask'
+
+Rake::TestTask.new do |t|
+  t.libs << "test"
+  t.test_files = FileList['src/test/ruby/*test.rb']
+  t.verbose = true
+end
+
+desc "Generate gem index in order to set up the load path properly"
+task :generate_gem_index do
+  index_file = ENV['index_file']
+  gem_path = ENV['custom_gem_path']
+  raise "rake generate_gem_index index_file=PATH_TO_INDEX_FILE" unless index_file && gem_path
+  specs = "#{gem_path}/specifications"
+  specs = Dir["#{specs}/*.gemspec"].map {|path| File.basename(path).gsub(/\.gemspec$/, '')}
+  File.open(index_file, 'wb') do |f|
+    f << specs.join("\n")
+  end
+end
+
     <packaging>atlassian-plugin</packaging>
 
     <dependencies>
+
+        <dependency>
+            <groupId>org.jruby</groupId>
+            <artifactId>jruby-complete</artifactId>
+            <version>${jruby-complete.version}</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-context</artifactId>
+            <version>2.5.6.SEC02</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.osgi</groupId>
+            <artifactId>spring-osgi-core</artifactId>
+            <version>1.2.1</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.core</artifactId>
+            <version>4.2.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.atlassian.plugins</groupId>
+            <artifactId>atlassian-plugins-osgi-spring-extender</artifactId>
+            <version>${plugins.version}</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>com.atlassian.util.concurrent</groupId>
+            <artifactId>atlassian-util-concurrent</artifactId>
+            <version>2.2.0</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>servlet-api</artifactId>
+            <version>2.5</version>
+            <scope>provided</scope>
+        </dependency>
+
+
         <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
             <plugin>
                 <groupId>com.atlassian.maven.plugins</groupId>
                 <artifactId>maven-confluence-plugin</artifactId>
-                <version>3.7.3</version>
+                <version>${amps.version}</version>
                 <extensions>true</extensions>
                 <configuration>
                     <productVersion>${confluence.version}</productVersion>
                     <productDataVersion>${confluence.data.version}</productDataVersion>
+                    <jvmArgs>-Xmx1536m -XX:MaxPermSize=512m -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled</jvmArgs>
+                    <enableFastdev>false</enableFastdev>
+                    <instructions>
+                        <Import-Package>
+                            javax.servlet;version="[2.3,3.0)",
+                            org.springframework.osgi*,
+                            org.springframework.osgi.context,
+                            org.springframework.scripting.jruby.*,
+                            org.springframework.scripting.config.*,
+                            org.springframework.core*;version="[2.5.0,3.0)",
+                            org.springframework.beans*;version="[2.5.0,3.0)",
+                            org.springframework.scripting*,
+                            org.springframework.context*;version="[2.5.0,3.0)",
+                            org.springframework.util*;version="[2.5.0,3.0)",
+                            !bsh,
+                            !groovy.lang,
+                            !com.kenai.jnr.x86asm,
+                            *;resolution:=optional
+                        </Import-Package>
+
+                        <!--
+                        Used as a last resort to allow Ruby scripts to load Java classes that are not referred to in the Java code.
+                        Due to https://jira.codehaus.org/browse/JRUBY-6519 the more suitable OSGiScriptingContainer can't be used, hence this workaround.
+                        -->
+                        <DynamicImport-Package>
+                            *
+                        </DynamicImport-Package>
+                    </instructions>
+                    <compressResources>false</compressResources>
                 </configuration>
             </plugin>
             <plugin>
                     <target>1.6</target>
                 </configuration>
             </plugin>
+            <plugin>
+                <groupId>org.jruby.plugins</groupId>
+                <artifactId>jruby-rake-plugin</artifactId>
+                <version>${jruby-complete.version}</version>
+                <executions>
+                    <execution>
+                        <!-- use ruby to generate an index file for the additional RubyGems added to the jruby-complete.jar -->
+                        <id>generate-gem-specification-index</id>
+                        <phase>process-resources</phase>
+                        <goals>
+                            <goal>rake</goal>
+                        </goals>
+                        <configuration>
+                            <args>
+                                generate_gem_index
+                                index_file=${gem.spec.index}
+                                custom_gem_path=${gem.path}
+                            </args>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
         </plugins>
+
+        <resources>
+            <!-- mvn process-resources -->
+            <resource>
+                <filtering>false</filtering>
+                <directory>src/main/ruby</directory>
+            </resource>
+            <resource>
+                <filtering>false</filtering>
+                <directory>src/main/rubygems/gems</directory>
+                <targetPath>rubygems</targetPath>
+                <includes>
+                    <include>*/lib/**/*</include>
+                </includes>
+            </resource>
+            <resource>
+                <directory>src/main/resources</directory>
+            </resource>
+        </resources>
     </build>
 
     <properties>
-        <confluence.version>4.0</confluence.version>
+        <confluence.version>4.1.1</confluence.version>
         <confluence.data.version>3.5</confluence.data.version>
-        <amps.version>3.7.3</amps.version>
+        <amps.version>3.8</amps.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <plugins.version>2.7.0</plugins.version>
+        <!--<archive>${project.build.directory}/classes/META-INF/lib/jruby-complete-${jruby-complete.version}.jar</archive>-->
+        <jruby-complete.version>1.6.7</jruby-complete.version>
+
+        <required.gems>sinatra jruby-rack rio nokogiri</required.gems>
+        <gem.path>${basedir}/src/main/rubygems</gem.path>
+        <gem.spec.index>${basedir}/target/classes/META-INF/gemspec.index</gem.spec.index>
+        <gem.options>--no-rdoc --no-ri --install-dir=${gem.path}</gem.options>
     </properties>
 
+    <profiles>
+        <profile>
+            <id>rubygems</id>
+            <activation>
+                <activeByDefault>false</activeByDefault>
+                <file>
+                    <!-- Note: local properties are not interpolated! -->
+                    <missing>src/main/rubygems/gems</missing>
+                </file>
+            </activation>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.jruby.plugins</groupId>
+                        <artifactId>jruby-rake-plugin</artifactId>
+                        <version>${jruby-complete.version}</version>
+                        <executions>
+                            <execution>
+                                <id>install-gems</id>
+                                <phase>generate-resources</phase>
+                                <goals>
+                                    <goal>install-gems</goal>
+                                </goals>
+                                <configuration>
+                                    <gems>${gem.options} ${required.gems}</gems>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+        <profile>
+            <id>idea</id>
+            <dependencies>
+                <dependency>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring</artifactId>
+                    <version>2.5.6.SEC02</version>
+                    <scope>compile</scope>
+                </dependency>
+                <dependency>
+                    <groupId>com.atlassian.util.concurrent</groupId>
+                    <artifactId>atlassian-util-concurrent</artifactId>
+                    <version>2.2.0</version>
+                    <scope>compile</scope>
+                </dependency>
+                <dependency>
+                    <groupId>com.atlassian.confluence</groupId>
+                    <artifactId>confluence</artifactId>
+                    <version>${confluence.version}</version>
+                    <scope>compile</scope>
+                </dependency>
+            </dependencies>
+        </profile>
+    </profiles>
+
 </project>

File src/main/java/com/atlassian/example/ExampleMacro.java

-package com.atlassian.example;
-
-import java.util.Map;
-import java.util.List;
-import java.util.Iterator;
-
-import com.atlassian.renderer.RenderContext;
-import com.atlassian.renderer.v2.macro.BaseMacro;
-import com.atlassian.renderer.v2.macro.MacroException;
-import com.atlassian.renderer.v2.RenderMode;
-import com.atlassian.confluence.pages.PageManager;
-import com.atlassian.confluence.pages.Page;
-import com.atlassian.confluence.spaces.SpaceManager;
-import com.atlassian.confluence.user.AuthenticatedUserThreadLocal;
-import com.atlassian.user.User;
-import com.opensymphony.util.TextUtils;
-
-/**
- * This very simple macro shows you the very basic use-case of displaying *something* on the Confluence page where it is used.
- * Use this example macro to toy around, and then quickly move on to the next example - this macro doesn't
- * really show you all the fun stuff you can do with Confluence.
- */
-public class ExampleMacro extends BaseMacro
-{
-
-    // We just have to define the variables and the setters, then Spring injects the correct objects for us to use. Simple and efficient.
-    // You just need to know *what* you want to inject and use.
-
-    private final PageManager pageManager;
-    private final SpaceManager spaceManager;
-
-    public ExampleMacro(PageManager pageManager, SpaceManager spaceManager)
-    {
-        this.pageManager = pageManager;
-        this.spaceManager = spaceManager;
-    }
-
-    public boolean isInline()
-    {
-        return false;
-    }
-
-    public boolean hasBody()
-    {
-        return false;
-    }
-
-    public RenderMode getBodyRenderMode()
-    {
-        return RenderMode.NO_RENDER;
-    }
-
-    /**
-     * This method returns XHTML to be displayed on the page that uses this macro
-     * we just do random stuff here, trying to show how you can access the most basic
-     * managers and model objects. No emphasis is put on beauty of code nor on
-     * doing actually useful things :-)
-     */
-    public String execute(Map params, String body, RenderContext renderContext)
-            throws MacroException
-    {
-
-        // in this most simple example, we build the result in memory, appending HTML code to it at will.
-        // this is something you absolutely don't want to do once you start writing plugins for real. Refer
-        // to the next example for better ways to render content.
-        StringBuffer result = new StringBuffer();
-
-        // get the currently logged in user and display his name
-        User user = AuthenticatedUserThreadLocal.getUser();
-        if (user != null)
-        {
-            String greeting = "Hello " + TextUtils.htmlEncode(user.getFullName()) + "<br><br>";
-            result.append(greeting);
-        }
-
-        //get the pages added in the last 55 days to the DS space ("Demo Space"), and display them
-        List list = pageManager.getRecentlyAddedPages(55, "DS");
-        result.append("Some stats for the Demo space: <br> ");
-        for (Iterator i = list.iterator(); i.hasNext();)
-        {
-            Page page = (Page) i.next();
-            int numberOfChildren = page.getChildren().size();
-            String pageWithChildren = "Page " + TextUtils.htmlEncode(page.getTitle()) + " has " + numberOfChildren + " children <br> ";
-            result.append(pageWithChildren);
-        }
-
-        // and show the number of all spaces in this installation.
-        String spaces = "<br>Altogether, this installation has " + spaceManager.getAllSpaces().size() + " spaces. <br>";
-        result.append(spaces);
-
-        // this concludes our little demo. Now you should understand the basics of code injection use in Confluence, and how
-        // to get a really simple macro running.
-
-        return result.toString();
-    }
-
-}

File src/main/java/com/atlassian/plugins/polyglot/jrubyexample/bridge/ScriptingContainerProvider.java

+package com.atlassian.plugins.polyglot.jrubyexample.bridge;
+
+import org.jruby.embed.ScriptingContainer;
+
+public interface ScriptingContainerProvider
+{
+    ScriptingContainer getScriptingContainer();
+}

File src/main/java/com/atlassian/plugins/polyglot/jrubyexample/bridge/ScriptingContainerProviderImpl.java

+package com.atlassian.plugins.polyglot.jrubyexample.bridge;
+
+import com.atlassian.confluence.core.ConfluenceSystemProperties;
+import com.atlassian.util.concurrent.LazyReference;
+import com.atlassian.util.concurrent.Supplier;
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringUtils;
+import org.jruby.embed.LocalContextScope;
+import org.jruby.embed.LocalVariableBehavior;
+import org.jruby.embed.ScriptingContainer;
+import org.jruby.embed.osgi.OSGiScriptingContainer;
+import org.jruby.util.KCode;
+import org.osgi.framework.BundleContext;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.osgi.context.BundleContextAware;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Nullable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.jruby.RubyInstanceConfig.CompileMode;
+
+@Component
+public class ScriptingContainerProviderImpl implements ScriptingContainerProvider, DisposableBean {
+
+    private final Supplier<ScriptingContainer> engine = new LazyReference<ScriptingContainer>() {
+        @Override
+        protected ScriptingContainer create() throws Exception {
+            // Unfortunately we can't use the more suitable OSGiScriptingContainer
+            // here, due to https://jira.codehaus.org/browse/JRUBY-6519
+            ScriptingContainer container = new ScriptingContainer(LocalContextScope.CONCURRENT,
+                    LocalVariableBehavior.TRANSIENT);
+            container.setHomeDirectory("classpath:/META-INF/jruby.home");
+            container.setCompileMode(ConfluenceSystemProperties.isDevMode() ?
+                    CompileMode.OFF :
+                    CompileMode.JIT);
+            container.setKCode(KCode.UTF8);
+
+            // Define Ruby LOAD_PATH and add the Rubygems lib/ directories to it
+            List<String> gemLoadPaths = resolveGemLoadPaths();
+            List<String> loadPaths = new ArrayList<String>(container.getLoadPaths());
+            loadPaths.addAll(gemLoadPaths);
+            container.setLoadPaths(loadPaths);
+            return container;
+        }
+    };
+
+    @Override
+    public ScriptingContainer getScriptingContainer() {
+        return engine.get();
+    }
+
+    private List<String> resolveGemLoadPaths() {
+        return Lists.transform(resolveGemlist(), new Function<String, String>() {
+            @Override
+            public String apply(@Nullable String s) {
+                if (StringUtils.isNotEmpty(s)) {
+                    return "classpath:/rubygems/" + s + "/lib";
+                }
+                return null;
+            }
+        });
+    }
+
+    private List<String> resolveGemlist() {
+        InputStream is = getClass().getClassLoader().getResourceAsStream("/META-INF/gemspec.index");
+        try {
+            return IOUtils.readLines(is);
+        } catch (IOException e) {
+            return Collections.emptyList();
+        } finally {
+            IOUtils.closeQuietly(is);
+        }
+    }
+
+    @Override
+    public void destroy() throws Exception {
+        getScriptingContainer().terminate();
+    }
+
+}

File src/main/java/com/atlassian/plugins/polyglot/jrubyexample/servlet/ExampleRubyServlet.java

+package com.atlassian.plugins.polyglot.jrubyexample.servlet;
+
+import com.atlassian.plugins.polyglot.jrubyexample.bridge.ScriptingContainerProvider;
+import org.jruby.embed.PathType;
+import org.jruby.embed.ScriptingContainer;
+
+import javax.script.*;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.*;
+import java.util.List;
+
+/**
+ * Simple Servlet that shows how to execute a Ruby script.
+ */
+public class ExampleRubyServlet extends HttpServlet {
+
+    private final ScriptingContainerProvider scriptingContainerProvider;
+
+    public ExampleRubyServlet(ScriptingContainerProvider scriptingContainerProvider) {
+        this.scriptingContainerProvider = scriptingContainerProvider;
+    }
+
+
+    @Override
+    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+        resp.setContentType("text/plain");
+
+        final PrintWriter writer = resp.getWriter();
+        if (null == req.getParameter("url")) {
+            writer.write("Param 'url' is missing");
+            return;
+        }
+
+        ScriptingContainer container = scriptingContainerProvider.getScriptingContainer();
+
+        container.put("@url", req.getParameter("url"));
+        container.setWriter(writer);
+        container.runScriptlet(PathType.CLASSPATH, "/do_http_request.rb");
+    }
+
+}

File src/main/java/com/atlassian/plugins/polyglot/jrubyexample/servlet/SinatraAdapterServlet.java

+package com.atlassian.plugins.polyglot.jrubyexample.servlet;
+
+import com.atlassian.confluence.user.AuthenticatedUserThreadLocal;
+import com.atlassian.plugin.webresource.UrlMode;
+import com.atlassian.plugin.webresource.WebResourceManager;
+import com.atlassian.plugins.polyglot.jrubyexample.bridge.ScriptingContainerProvider;
+import org.apache.commons.lang.StringUtils;
+import org.jruby.embed.EmbedEvalUnit;
+import org.jruby.embed.PathType;
+import org.jruby.embed.ScriptingContainer;
+import org.jruby.exceptions.RaiseException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.StopWatch;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.security.Principal;
+import java.util.Collections;
+
+public class SinatraAdapterServlet extends HttpServlet {
+
+    private static final Logger log = LoggerFactory.getLogger(SinatraAdapterServlet.class);
+
+    private ServletConfig servletConfig;
+    private EmbedEvalUnit rackServletHandler;
+    private ScriptingContainer container;
+    private String servletName = "";
+    private String webResourceTags = "";
+    private final WebResourceManager webResourceManager;
+    private final ScriptingContainerProvider containerProvider;
+
+    @Autowired
+    public SinatraAdapterServlet(ScriptingContainerProvider containerProvider, @SuppressWarnings("SpringJavaAutowiringInspection") WebResourceManager webResourceManager) {
+        this.containerProvider = containerProvider;
+        this.webResourceManager = webResourceManager;
+    }
+
+    @Override
+    public void init(ServletConfig config) {
+        this.servletConfig = config;
+        servletName = StringUtils.defaultString(config.getInitParameter("servletName"), "");
+
+        final StopWatch stopWatch = new StopWatch();
+        stopWatch.start("Initialization - getScriptingContainer");
+        container = containerProvider.getScriptingContainer();
+        stopWatch.stop();
+
+        stopWatch.start("Require RubyGems");
+        container.runScriptlet("require 'rubygems'");
+        stopWatch.stop();
+
+        // parse closes the InputStream!
+        stopWatch.start("Parse servlet_handler.rb");
+        rackServletHandler = container.parse(PathType.CLASSPATH, "/jruby_integration/rack_servlet_handler.rb");
+        stopWatch.stop();
+
+        stopWatch.start("Include resources");
+        Writer sw = new StringWriter();
+        webResourceManager.includeResources(Collections.singletonList("com.atlassian.example.jrubyplugin:jruby-plugin-resources"), sw, UrlMode.AUTO);
+        webResourceTags = sw.toString();
+        stopWatch.stop();
+
+        log.debug("Stopwatch: {}", stopWatch.prettyPrint());
+    }
+
+
+    @Override
+    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+        try {
+            final Writer w = response.getWriter();
+            container.setOutput(w);
+
+            container.put("$resource_tags", webResourceTags);
+            container.put("$context_path", request.getContextPath());
+            container.put("$servlet_context", servletConfig.getServletContext());
+
+            container.put("@config", servletConfig);
+            container.put("@handler", rackServletHandler.run());
+            container.put("@request", new HttpServletRequestWrapper(request) {
+                @Override
+                public String getPathInfo() {
+                    return super.getPathInfo().replaceFirst(servletName, "").replaceFirst("//", "");
+                }
+            });
+
+            Object rackResponse = container.runScriptlet(PathType.CLASSPATH,
+                    "/jruby_integration/handle_request.rb");
+
+            String body = container.callMethod(rackResponse, "getBody", String.class);
+            response.setContentLength(body.getBytes().length);
+            w.write(body);
+            container.clear();
+
+        } catch (RaiseException re) {
+            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, re.getLocalizedMessage());
+        }
+    }
+
+}

File src/main/resources/META-INF/spring/pluginContext.xml

+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:context="http://www.springframework.org/schema/context"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans
+               http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
+               http://www.springframework.org/schema/context
+               http://www.springframework.org/schema/context/spring-context-2.5.xsd"
+       default-autowire="autodetect">
+    <context:component-scan base-package="com.atlassian.plugins.polyglot" />
+</beans>

File src/main/resources/atlassian-plugin.xml

     <plugin-info>
         <description>${project.description}</description>
         <version>${project.version}</version>
-        <vendor name="${project.organization.name}" url="${project.organization.url}" />
+        <vendor name="${project.organization.name}" url="${project.organization.url}"/>
     </plugin-info>
 
-    <macro name="jrubyplugin" class="com.atlassian.example.ExampleMacro" key="my-macro">
-    <!-- TODO: Add macro description -->
-    <!-- <description></description> -->
-    </macro>
+    <resource type="i18n" name="i18n" location="com.atlassian.plugins.polyglot.jruby.strings"/>
+
+    <resource name="img/" type="download" location="/img"/>
+
+    <web-resource key="jruby-plugin-resources" name="JRuby Example Resources">
+        <resource type="download" name="underscore.js" location="/js/underscore-min.js"/>
+
+        <resource type="download" name="bootstrap.css" location="/css/bootstrap.min.css"/>
+    </web-resource>
+
+    <web-item key="confluence-sinatra-link" name="Link to Confluence on Sinatra"
+              section="system.browse"
+              weight="1000">
+        <description key="links.global.linkto.sinatra.desc">Confluence on Sinatra</description>
+        <label key="links.global.linkto.sinatra.label"/>
+        <link linkId="sinatraHome">/plugins/servlet/sinatra/</link>
+    </web-item>
+
+    <web-item key="confluence-example-servlet-link" name="Ruby Example Script output"
+              section="system.browse"
+              weight="1000">
+        <description key="links.global.linkto.example.servlet.desc">Simple example servlet</description>
+        <label key="links.global.linkto.example.servlet.label"/>
+        <link linkId="sinatraHome">/plugins/servlet/jruby?url=http://www.example.com</link>
+    </web-item>
+
+    <servlet name="Sinatra Servlet" key="jrubySinatraServlet"
+             class="com.atlassian.plugins.polyglot.jrubyexample.servlet.SinatraAdapterServlet">
+        <description>Using Sinatra apps</description>
+        <!-- /CONTEXT_PATH/plugins/servlet/sinatra -->
+        <url-pattern>/sinatra*</url-pattern>
+        <init-param>
+            <param-name>servletName</param-name>
+            <param-value>sinatra</param-value>
+        </init-param>
+    </servlet>
+
+    <servlet name="Scripting Servlet" key="jrubyExampleScriptingServlet"
+             class="com.atlassian.plugins.polyglot.jrubyexample.servlet.ExampleRubyServlet">
+        <description>Simple example scripting servlet</description>
+        <!-- /CONTEXT_PATH/plugins/servlet/jruby -->
+        <url-pattern>/jruby</url-pattern>
+    </servlet>
+
 </atlassian-plugin>

File src/main/resources/com/atlassian/plugins/polyglot/jruby/strings.properties

+links.global.linkto.sinatra.label=Confluence on Sinatra
+links.global.linkto.example.servlet.label=Example Ruby Script Servlet

File src/main/resources/com/atlassian/plugins/polyglot/jruby/strings_de_DE.properties

+links.global.linkto.sinatra.label=Confluence mit Sinatra
+links.global.linkto.example.servlet.label=Beispiel Ruby Skript Servlet

File src/main/resources/css/bootstrap.css

+/*!
+ * Bootstrap v2.0.1
+ *
+ * Copyright 2012 Twitter, Inc
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Designed and built with all the love in the world @twitter by @mdo and @fat.
+ */
+.clearfix {
+  *zoom: 1;
+}
+.clearfix:before, .clearfix:after {
+  display: table;
+  content: "";
+}
+.clearfix:after {
+  clear: both;
+}
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+nav,
+section {
+  display: block;
+}
+audio, canvas, video {
+  display: inline-block;
+  *display: inline;
+  *zoom: 1;
+}
+audio:not([controls]) {
+  display: none;
+}
+html {
+  font-size: 100%;
+  -webkit-text-size-adjust: 100%;
+  -ms-text-size-adjust: 100%;
+}
+a:focus {
+  outline: thin dotted #333;
+  outline: 5px auto -webkit-focus-ring-color;
+  outline-offset: -2px;
+}
+a:hover, a:active {
+  outline: 0;
+}
+sub, sup {
+  position: relative;
+  font-size: 75%;
+  line-height: 0;
+  vertical-align: baseline;
+}
+sup {
+  top: -0.5em;
+}
+sub {
+  bottom: -0.25em;
+}
+img {
+  max-width: 100%;
+  height: auto;
+  border: 0;
+  -ms-interpolation-mode: bicubic;
+}
+button,
+input,
+select,
+textarea {
+  margin: 0;
+  font-size: 100%;
+  vertical-align: middle;
+}
+button, input {
+  *overflow: visible;
+  line-height: normal;
+}
+button::-moz-focus-inner, input::-moz-focus-inner {
+  padding: 0;
+  border: 0;
+}
+button,
+input[type="button"],
+input[type="reset"],
+input[type="submit"] {
+  cursor: pointer;
+  -webkit-appearance: button;
+}
+input[type="search"] {
+  -webkit-appearance: textfield;
+  -webkit-box-sizing: content-box;
+  -moz-box-sizing: content-box;
+  box-sizing: content-box;
+}
+input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button {
+  -webkit-appearance: none;
+}
+textarea {
+  overflow: auto;
+  vertical-align: top;
+}
+body {
+  margin: 0;
+  font-family: sans-serif;
+  font-size: 13px;
+  line-height: 16px;
+  color: #333333;
+  background-color: #ffffff;
+}
+a {
+  color: #326ca6;
+  text-decoration: none;
+}
+a:hover {
+  color: #029dd4;
+  text-decoration: underline;
+}
+.row {
+  margin-left: -8;
+  *zoom: 1;
+}
+.row:before, .row:after {
+  display: table;
+  content: "";
+}
+.row:after {
+  clear: both;
+}
+[class*="span"] {
+  float: left;
+  margin-left: 8;
+}
+.span1 {
+  width: 72px;
+}
+.span2 {
+  width: 152px;
+}
+.span3 {
+  width: 232px;
+}
+.span4 {
+  width: 312px;
+}
+.span5 {
+  width: 392px;
+}
+.span6 {
+  width: 472px;
+}
+.span7 {
+  width: 552px;
+}
+.span8 {
+  width: 632px;
+}
+.span9 {
+  width: 712px;
+}
+.span10 {
+  width: 792px;
+}
+.span11 {
+  width: 872px;
+}
+.span12, .container {
+  width: 952px;
+}
+.offset1 {
+  margin-left: 88px;
+}
+.offset2 {
+  margin-left: 168px;
+}
+.offset3 {
+  margin-left: 248px;
+}
+.offset4 {
+  margin-left: 328px;
+}
+.offset5 {
+  margin-left: 408px;
+}
+.offset6 {
+  margin-left: 488px;
+}
+.offset7 {
+  margin-left: 568px;
+}
+.offset8 {
+  margin-left: 648px;
+}
+.offset9 {
+  margin-left: 728px;
+}
+.offset10 {
+  margin-left: 808px;
+}
+.offset11 {
+  margin-left: 888px;
+}
+.row-fluid {
+  width: 100%;
+  *zoom: 1;
+}
+.row-fluid:before, .row-fluid:after {
+  display: table;
+  content: "";
+}
+.row-fluid:after {
+  clear: both;
+}
+.row-fluid > [class*="span"] {
+  float: left;
+  margin-left: 2.127659574%;
+}
+.row-fluid > [class*="span"]:first-child {
+  margin-left: 0;
+}
+.row-fluid > .span1 {
+  width: 6.382978723%;
+}
+.row-fluid > .span2 {
+  width: 14.89361702%;
+}
+.row-fluid > .span3 {
+  width: 23.404255317%;
+}
+.row-fluid > .span4 {
+  width: 31.914893614%;
+}
+.row-fluid > .span5 {
+  width: 40.425531911%;
+}
+.row-fluid > .span6 {
+  width: 48.93617020799999%;
+}
+.row-fluid > .span7 {
+  width: 57.446808505%;
+}
+.row-fluid > .span8 {
+  width: 65.95744680199999%;
+}
+.row-fluid > .span9 {
+  width: 74.468085099%;
+}
+.row-fluid > .span10 {
+  width: 82.97872339599999%;
+}
+.row-fluid > .span11 {
+  width: 91.489361693%;
+}
+.row-fluid > .span12 {
+  width: 99.99999998999999%;
+}
+.container {
+  width: 952px;
+  margin-left: auto;
+  margin-right: auto;
+  *zoom: 1;
+}
+.container:before, .container:after {
+  display: table;
+  content: "";
+}
+.container:after {
+  clear: both;
+}
+.container-fluid {
+  padding-left: 8;
+  padding-right: 8;
+  *zoom: 1;
+}
+.container-fluid:before, .container-fluid:after {
+  display: table;
+  content: "";
+}
+.container-fluid:after {
+  clear: both;
+}
+p {
+  margin: 0 0 8px;
+  font-family: sans-serif;
+  font-size: 13px;
+  line-height: 16px;
+}
+p small {
+  font-size: 11px;
+  color: #999999;
+}
+.lead {
+  margin-bottom: 16px;
+  font-size: 20px;
+  font-weight: 200;
+  line-height: 24px;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+  margin: 0;
+  font-weight: bold;
+  color: #333333;
+  text-rendering: optimizelegibility;
+}
+h1 small,
+h2 small,
+h3 small,
+h4 small,
+h5 small,
+h6 small {
+  font-weight: normal;
+  color: #999999;
+}
+h1 {
+  font-size: 30px;
+  line-height: 32px;
+}
+h1 small {
+  font-size: 18px;
+}
+h2 {
+  font-size: 24px;
+  line-height: 32px;
+}
+h2 small {
+  font-size: 18px;
+}
+h3 {
+  line-height: 24px;
+  font-size: 18px;
+}
+h3 small {
+  font-size: 14px;
+}
+h4, h5, h6 {
+  line-height: 16px;
+}
+h4 {
+  font-size: 14px;
+}
+h4 small {
+  font-size: 12px;
+}
+h5 {
+  font-size: 12px;
+}
+h6 {
+  font-size: 11px;
+  color: #999999;
+  text-transform: uppercase;
+}
+.page-header {
+  padding-bottom: 15px;
+  margin: 16px 0;
+  border-bottom: 1px solid #eeeeee;
+}
+.page-header h1 {
+  line-height: 1;
+}
+ul, ol {
+  padding: 0;
+  margin: 0 0 8px 25px;
+}
+ul ul,
+ul ol,
+ol ol,
+ol ul {
+  margin-bottom: 0;
+}
+ul {
+  list-style: disc;
+}
+ol {
+  list-style: decimal;
+}
+li {
+  line-height: 16px;
+}
+ul.unstyled, ol.unstyled {
+  margin-left: 0;
+  list-style: none;
+}
+dl {
+  margin-bottom: 16px;
+}
+dt, dd {
+  line-height: 16px;
+}
+dt {
+  font-weight: bold;
+}
+dd {
+  margin-left: 8px;
+}
+hr {
+  margin: 16px 0;
+  border: 0;
+  border-top: 1px solid #eeeeee;
+  border-bottom: 1px solid #ffffff;
+}
+strong {
+  font-weight: bold;
+}
+em {
+  font-style: italic;
+}
+.muted {
+  color: #999999;
+}
+abbr {
+  font-size: 90%;
+  text-transform: uppercase;
+  border-bottom: 1px dotted #ddd;
+  cursor: help;
+}
+blockquote {
+  padding: 0 0 0 15px;
+  margin: 0 0 16px;
+  border-left: 5px solid #eeeeee;
+}
+blockquote p {
+  margin-bottom: 0;
+  font-size: 16px;
+  font-weight: 300;
+  line-height: 20px;
+}
+blockquote small {
+  display: block;
+  line-height: 16px;
+  color: #999999;
+}
+blockquote small:before {
+  content: '\2014 \00A0';
+}
+blockquote.pull-right {
+  float: right;
+  padding-left: 0;
+  padding-right: 15px;
+  border-left: 0;
+  border-right: 5px solid #eeeeee;
+}
+blockquote.pull-right p, blockquote.pull-right small {
+  text-align: right;
+}
+q:before,
+q:after,
+blockquote:before,
+blockquote:after {
+  content: "";
+}
+address {
+  display: block;
+  margin-bottom: 16px;
+  line-height: 16px;
+  font-style: normal;
+}
+small {
+  font-size: 100%;
+}
+cite {
+  font-style: normal;
+}
+code, pre {
+  padding: 0 3px 2px;
+  font-family: Menlo, Monaco, "Courier New", monospace;
+  font-size: 12px;
+  color: #333333;
+  -webkit-border-radius: 3px;
+  -moz-border-radius: 3px;
+  border-radius: 3px;
+}
+code {
+  padding: 3px 4px;
+  color: #d14;
+  background-color: #f7f7f9;
+  border: 1px solid #e1e1e8;
+}
+pre {
+  display: block;
+  padding: 7.5px;
+  margin: 0 0 8px;
+  font-size: 12px;
+  line-height: 16px;
+  background-color: #f5f5f5;
+  border: 1px solid #ccc;
+  border: 1px solid rgba(0, 0, 0, 0.15);
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+  white-space: pre;
+  white-space: pre-wrap;
+  word-break: break-all;
+  word-wrap: break-word;
+}
+pre.prettyprint {
+  margin-bottom: 16px;
+}
+pre code {
+  padding: 0;
+  color: inherit;
+  background-color: transparent;
+  border: 0;
+}
+.pre-scrollable {
+  max-height: 340px;
+  overflow-y: scroll;
+}
+.label {
+  padding: 2px 4px 3px;
+  font-size: 11.049999999999999px;
+  font-weight: bold;
+  color: #ffffff;
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+  background-color: #999999;
+  -webkit-border-radius: 3px;
+  -moz-border-radius: 3px;
+  border-radius: 3px;
+}
+.label:hover {
+  color: #ffffff;
+  text-decoration: none;
+}
+.label-important {
+  background-color: #b94a48;
+}
+.label-important:hover {
+  background-color: #953b39;
+}
+.label-warning {
+  background-color: #f89406;
+}
+.label-warning:hover {
+  background-color: #c67605;
+}
+.label-success {
+  background-color: #468847;
+}
+.label-success:hover {
+  background-color: #356635;
+}
+.label-info {
+  background-color: #3a87ad;
+}
+.label-info:hover {
+  background-color: #2d6987;
+}
+table {
+  max-width: 100%;
+  border-collapse: collapse;
+  border-spacing: 0;
+}
+.table {
+  width: 100%;
+  margin-bottom: 16px;
+}
+.table th, .table td {
+  padding: 8px;
+  line-height: 16px;
+  text-align: left;
+  vertical-align: top;
+  border-top: 1px solid #ddd;
+}
+.table th {
+  font-weight: bold;
+}
+.table thead th {
+  vertical-align: bottom;
+}
+.table thead:first-child tr th, .table thead:first-child tr td {
+  border-top: 0;
+}
+.table tbody + tbody {
+  border-top: 2px solid #ddd;
+}
+.table-condensed th, .table-condensed td {
+  padding: 4px 5px;
+}
+.table-bordered {
+  border: 1px solid #ddd;
+  border-collapse: separate;
+  *border-collapse: collapsed;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+}
+.table-bordered th + th,
+.table-bordered td + td,
+.table-bordered th + td,
+.table-bordered td + th {
+  border-left: 1px solid #ddd;
+}
+.table-bordered thead:first-child tr:first-child th, .table-bordered tbody:first-child tr:first-child th, .table-bordered tbody:first-child tr:first-child td {
+  border-top: 0;
+}
+.table-bordered thead:first-child tr:first-child th:first-child, .table-bordered tbody:first-child tr:first-child td:first-child {
+  -webkit-border-radius: 4px 0 0 0;
+  -moz-border-radius: 4px 0 0 0;
+  border-radius: 4px 0 0 0;
+}
+.table-bordered thead:first-child tr:first-child th:last-child, .table-bordered tbody:first-child tr:first-child td:last-child {
+  -webkit-border-radius: 0 4px 0 0;
+  -moz-border-radius: 0 4px 0 0;
+  border-radius: 0 4px 0 0;
+}
+.table-bordered thead:last-child tr:last-child th:first-child, .table-bordered tbody:last-child tr:last-child td:first-child {
+  -webkit-border-radius: 0 0 0 4px;
+  -moz-border-radius: 0 0 0 4px;
+  border-radius: 0 0 0 4px;
+}
+.table-bordered thead:last-child tr:last-child th:last-child, .table-bordered tbody:last-child tr:last-child td:last-child {
+  -webkit-border-radius: 0 0 4px 0;
+  -moz-border-radius: 0 0 4px 0;
+  border-radius: 0 0 4px 0;
+}
+.table-striped tbody tr:nth-child(odd) td, .table-striped tbody tr:nth-child(odd) th {
+  background-color: #f9f9f9;
+}
+.table tbody tr:hover td, .table tbody tr:hover th {
+  background-color: #f5f5f5;
+}
+table .span1 {
+  float: none;
+  width: 56px;
+  margin-left: 0;
+}
+table .span2 {
+  float: none;
+  width: 136px;
+  margin-left: 0;
+}
+table .span3 {
+  float: none;
+  width: 216px;
+  margin-left: 0;
+}
+table .span4 {
+  float: none;
+  width: 296px;
+  margin-left: 0;
+}
+table .span5 {
+  float: none;
+  width: 376px;
+  margin-left: 0;
+}
+table .span6 {
+  float: none;
+  width: 456px;
+  margin-left: 0;
+}
+table .span7 {
+  float: none;
+  width: 536px;
+  margin-left: 0;
+}
+table .span8 {
+  float: none;
+  width: 616px;
+  margin-left: 0;
+}
+table .span9 {
+  float: none;
+  width: 696px;
+  margin-left: 0;
+}
+table .span10 {
+  float: none;
+  width: 776px;
+  margin-left: 0;
+}
+table .span11 {
+  float: none;
+  width: 856px;
+  margin-left: 0;
+}
+table .span12 {
+  float: none;
+  width: 936px;
+  margin-left: 0;
+}
+form {
+  margin: 0 0 16px;
+}
+fieldset {
+  padding: 0;
+  margin: 0;
+  border: 0;
+}
+legend {
+  display: block;
+  width: 100%;
+  padding: 0;
+  margin-bottom: 24px;
+  font-size: 19.5px;
+  line-height: 32px;
+  color: #333333;
+  border: 0;
+  border-bottom: 1px solid #eee;
+}
+legend small {
+  font-size: 12px;
+  color: #999999;
+}
+label,
+input,
+button,
+select,
+textarea {
+  font-size: 13px;
+  font-weight: normal;
+  line-height: 16px;
+}
+input,
+button,
+select,
+textarea {
+  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+}
+label {
+  display: block;
+  margin-bottom: 5px;
+  color: #333333;
+}
+input,
+textarea,
+select,
+.uneditable-input {
+  display: inline-block;
+  width: 210px;
+  height: 16px;
+  padding: 4px;
+  margin-bottom: 9px;
+  font-size: 13px;
+  line-height: 16px;
+  color: #555555;
+  border: 1px solid #ccc;
+  -webkit-border-radius: 3px;
+  -moz-border-radius: 3px;
+  border-radius: 3px;
+}
+.uneditable-textarea {
+  width: auto;
+  height: auto;
+}
+label input, label textarea, label select {
+  display: block;
+}
+input[type="image"], input[type="checkbox"], input[type="radio"] {
+  width: auto;
+  height: auto;
+  padding: 0;
+  margin: 3px 0;
+  *margin-top: 0;
+  /* IE7 */
+
+  line-height: normal;
+  cursor: pointer;
+  -webkit-border-radius: 0;
+  -moz-border-radius: 0;
+  border-radius: 0;
+  border: 0 \9;
+  /* IE9 and down */
+
+}
+input[type="image"] {
+  border: 0;
+}
+input[type="file"] {
+  width: auto;
+  padding: initial;
+  line-height: initial;
+  border: initial;
+  background-color: #ffffff;
+  background-color: initial;
+  -webkit-box-shadow: none;
+  -moz-box-shadow: none;
+  box-shadow: none;
+}
+input[type="button"], input[type="reset"], input[type="submit"] {
+  width: auto;
+  height: auto;
+}
+select, input[type="file"] {
+  height: 28px;
+  /* In IE7, the height of the select element cannot be changed by height, only font-size */
+
+  *margin-top: 4px;
+  /* For IE7, add top margin to align select with labels */
+
+  line-height: 28px;
+}
+input[type="file"] {
+  line-height: 18px \9;
+}
+select {
+  width: 220px;
+  background-color: #ffffff;
+}
+select[multiple], select[size] {
+  height: auto;
+}
+input[type="image"] {
+  -webkit-box-shadow: none;
+  -moz-box-shadow: none;
+  box-shadow: none;
+}
+textarea {
+  height: auto;
+}
+input[type="hidden"] {
+  display: none;
+}
+.radio, .checkbox {
+  padding-left: 18px;
+}
+.radio input[type="radio"], .checkbox input[type="checkbox"] {
+  float: left;
+  margin-left: -18px;
+}
+.controls > .radio:first-child, .controls > .checkbox:first-child {
+  padding-top: 5px;
+}
+.radio.inline, .checkbox.inline {
+  display: inline-block;
+  padding-top: 5px;
+  margin-bottom: 0;
+  vertical-align: middle;
+}
+.radio.inline + .radio.inline, .checkbox.inline + .checkbox.inline {
+  margin-left: 10px;
+}
+input, textarea {
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+  -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+  -webkit-transition: border linear 0.2s, box-shadow linear 0.2s;
+  -moz-transition: border linear 0.2s, box-shadow linear 0.2s;
+  -ms-transition: border linear 0.2s, box-shadow linear 0.2s;
+  -o-transition: border linear 0.2s, box-shadow linear 0.2s;
+  transition: border linear 0.2s, box-shadow linear 0.2s;
+}
+input:focus, textarea:focus {
+  border-color: rgba(82, 168, 236, 0.8);
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
+  -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
+  outline: 0;
+  outline: thin dotted \9;
+  /* IE6-9 */
+
+}
+input[type="file"]:focus,
+input[type="radio"]:focus,
+input[type="checkbox"]:focus,
+select:focus {
+  -webkit-box-shadow: none;
+  -moz-box-shadow: none;
+  box-shadow: none;
+  outline: thin dotted #333;
+  outline: 5px auto -webkit-focus-ring-color;
+  outline-offset: -2px;
+}
+.input-mini {
+  width: 60px;
+}
+.input-small {
+  width: 90px;
+}
+.input-medium {
+  width: 150px;
+}
+.input-large {
+  width: 210px;
+}
+.input-xlarge {
+  width: 270px;
+}
+.input-xxlarge {
+  width: 530px;
+}
+input[class*="span"],
+select[class*="span"],
+textarea[class*="span"],
+.uneditable-input {
+  float: none;
+  margin-left: 0;
+}
+input.span1, textarea.span1, .uneditable-input.span1 {
+  width: 62px;
+}
+input.span2, textarea.span2, .uneditable-input.span2 {
+  width: 142px;
+}
+input.span3, textarea.span3, .uneditable-input.span3 {
+  width: 222px;
+}
+input.span4, textarea.span4, .uneditable-input.span4 {
+  width: 302px;
+}
+input.span5, textarea.span5, .uneditable-input.span5 {
+  width: 382px;
+}
+input.span6, textarea.span6, .uneditable-input.span6 {
+  width: 462px;
+}
+input.span7, textarea.span7, .uneditable-input.span7 {
+  width: 542px;
+}
+input.span8, textarea.span8, .uneditable-input.span8 {
+  width: 622px;
+}
+input.span9, textarea.span9, .uneditable-input.span9 {
+  width: 702px;
+}
+input.span10, textarea.span10, .uneditable-input.span10 {
+  width: 782px;
+}
+input.span11, textarea.span11, .uneditable-input.span11 {
+  width: 862px;
+}
+input.span12, textarea.span12, .uneditable-input.span12 {
+  width: 942px;
+}
+input[disabled],
+select[disabled],
+textarea[disabled],
+input[readonly],
+select[readonly],
+textarea[readonly] {
+  background-color: #f5f5f5;
+  border-color: #ddd;
+  cursor: not-allowed;
+}
+.control-group.warning > label, .control-group.warning .help-block, .control-group.warning .help-inline {
+  color: #c09853;
+}
+.control-group.warning input, .control-group.warning select, .control-group.warning textarea {
+  color: #c09853;
+  border-color: #c09853;
+}
+.control-group.warning input:focus, .control-group.warning select:focus, .control-group.warning textarea:focus {
+  border-color: #a47e3c;
+  -webkit-box-shadow: 0 0 6px #dbc59e;
+  -moz-box-shadow: 0 0 6px #dbc59e;
+  box-shadow: 0 0 6px #dbc59e;
+}
+.control-group.warning .input-prepend .add-on, .control-group.warning .input-append .add-on {
+  color: #c09853;
+  background-color: #fcf8e3;
+  border-color: #c09853;
+}
+.control-group.error > label, .control-group.error .help-block, .control-group.error .help-inline {
+  color: #b94a48;
+}
+.control-group.error input, .control-group.error select, .control-group.error textarea {
+  color: #b94a48;
+  border-color: #b94a48;
+}
+.control-group.error input:focus, .control-group.error select:focus, .control-group.error textarea:focus {
+  border-color: #953b39;
+  -webkit-box-shadow: 0 0 6px #d59392;
+  -moz-box-shadow: 0 0 6px #d59392;
+  box-shadow: 0 0 6px #d59392;
+}
+.control-group.error .input-prepend .add-on, .control-group.error .input-append .add-on {
+  color: #b94a48;
+  background-color: #f2dede;
+  border-color: #b94a48;
+}
+.control-group.success > label, .control-group.success .help-block, .control-group.success .help-inline {
+  color: #468847;
+}
+.control-group.success input, .control-group.success select, .control-group.success textarea {
+  color: #468847;
+  border-color: #468847;
+}
+.control-group.success input:focus, .control-group.success select:focus, .control-group.success textarea:focus {
+  border-color: #356635;
+  -webkit-box-shadow: 0 0 6px #7aba7b;
+  -moz-box-shadow: 0 0 6px #7aba7b;
+  box-shadow: 0 0 6px #7aba7b;
+}
+.control-group.success .input-prepend .add-on, .control-group.success .input-append .add-on {
+  color: #468847;
+  background-color: #dff0d8;
+  border-color: #468847;
+}
+input:focus:required:invalid, textarea:focus:required:invalid, select:focus:required:invalid {
+  color: #b94a48;
+  border-color: #ee5f5b;
+}
+input:focus:required:invalid:focus, textarea:focus:required:invalid:focus, select:focus:required:invalid:focus {
+  border-color: #e9322d;
+  -webkit-box-shadow: 0 0 6px #f8b9b7;
+  -moz-box-shadow: 0 0 6px #f8b9b7;
+  box-shadow: 0 0 6px #f8b9b7;
+}
+.form-actions {
+  padding: 15px 20px 16px;
+  margin-top: 16px;
+  margin-bottom: 16px;
+  background-color: #f5f5f5;
+  border-top: 1px solid #ddd;
+}
+.uneditable-input {
+  display: block;
+  background-color: #ffffff;
+  border-color: #eee;
+  -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
+  -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
+  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
+  cursor: not-allowed;
+}
+:-moz-placeholder {
+  color: #999999;
+}
+::-webkit-input-placeholder {
+  color: #999999;
+}
+.help-block {
+  display: block;
+  margin-top: 5px;
+  margin-bottom: 0;
+  color: #999999;
+}
+.help-inline {
+  display: inline-block;
+  *display: inline;
+  /* IE7 inline-block hack */
+
+  *zoom: 1;
+  margin-bottom: 9px;
+  vertical-align: middle;
+  padding-left: 5px;
+}
+.input-prepend, .input-append {
+  margin-bottom: 5px;
+  *zoom: 1;
+}
+.input-prepend:before,
+.input-append:before,
+.input-prepend:after,
+.input-append:after {
+  display: table;
+  content: "";
+}
+.input-prepend:after, .input-append:after {
+  clear: both;
+}
+.input-prepend input,
+.input-append input,
+.input-prepend .uneditable-input,
+.input-append .uneditable-input {
+  -webkit-border-radius: 0 3px 3px 0;
+  -moz-border-radius: 0 3px 3px 0;
+  border-radius: 0 3px 3px 0;
+}
+.input-prepend input:focus,
+.input-append input:focus,
+.input-prepend .uneditable-input:focus,
+.input-append .uneditable-input:focus {
+  position: relative;
+  z-index: 2;
+}
+.input-prepend .uneditable-input, .input-append .uneditable-input {
+  border-left-color: #ccc;
+}
+.input-prepend .add-on, .input-append .add-on {
+  float: left;
+  display: block;
+  width: auto;
+  min-width: 16px;
+  height: 16px;
+  margin-right: -1px;
+  padding: 4px 5px;
+  font-weight: normal;
+  line-height: 16px;
+  color: #999999;
+  text-align: center;
+  text-shadow: 0 1px 0 #ffffff;
+  background-color: #f5f5f5;
+  border: 1px solid #ccc;
+  -webkit-border-radius: 3px 0 0 3px;
+  -moz-border-radius: 3px 0 0 3px;
+  border-radius: 3px 0 0 3px;
+}
+.input-prepend .active, .input-append .active {
+  background-color: #a9dba9;
+  border-color: #46a546;
+}
+.input-prepend .add-on {
+  *margin-top: 1px;
+  /* IE6-7 */
+
+}
+.input-append input, .input-append .uneditable-input {
+  float: left;
+  -webkit-border-radius: 3px 0 0 3px;
+  -moz-border-radius: 3px 0 0 3px;
+  border-radius: 3px 0 0 3px;
+}
+.input-append .uneditable-input {
+  border-left-color: #eee;
+  border-right-color: #ccc;
+}
+.input-append .add-on {
+  margin-right: 0;
+  margin-left: -1px;
+  -webkit-border-radius: 0 3px 3px 0;
+  -moz-border-radius: 0 3px 3px 0;
+  border-radius: 0 3px 3px 0;
+}
+.input-append input:first-child {
+  *margin-left: -160px;
+}
+.input-append input:first-child + .add-on {