Introducing the SecurityHeadersPolicy in Alfresco Share

A few weeks back we added a new security filter to Alfresco Share. It is a regular Java Servlet filter that applies http response headers to incoming requests to Alfresco Share. The headers that are returned are defined in a config section called SecurityHeadersPolicy in alfresco-security-config.xml. We did this mainly to improve mitigation of clickjacking attacks but we also added 2 headers to tighten up the security in Internet Explorer after a tip from Jens Goldhammer at FME.

To read more about clickjacking please visit OWASP’s page on the subject:
https://www.owasp.org/index.php/Clickjacking

Now let’s take a look at the default config for Alfresco Enterprise 4.1.4 (and on Community on HEAD), as you can see 3 headers are added by default:

<config evaluator="string-compare" condition="SecurityHeadersPolicy">
  <headers>
    <header>
      <name>X-Frame-Options</name>
      <value>SAMEORIGIN</value>
    </header>
    <header>
      <name>X-Content-Type-Options</name>
      <value>nosniff</value>
    </header>
    <header>
      <name>X-XSS-Protection</name>
      <value>1; mode=block</value>
    </header>
  </headers>
</config>

Lets take a look at what implications these headers will have on browsers…

X-Frame-Options

Adding this header to an http response will tell the browser if Share pages are allowed to be put inside iframes or not. In our default config we have set this to SAMEORIGIN which means that Share pages are only allowed to be “(i)framed” inside Share or other webapps that happen to live under the same domain. In other words it will i.e. be possible to include http://www.acme.com/share inside an iframe on http://www.acme.com/portal.

If you do not want this to be allowed you can override the config and set the header to return DENY instead, you can do that by placing the following config in your share-config-custom.xml file:

<config evaluator="string-compare" condition="SecurityHeadersPolicy">
  <headers>
    <header>
      <name>X-Frame-Options</name>
      <value>DENY</value>
    </header>
  </headers>
</config>

If you instead want to open up the possibility to include Share inside iframes on any domain you can do this by using the following config instead. Note! This is not recommended since it will open up the possibility of Alfresco Share being a target of a clickjacking attack.

<config evaluator="string-compare" condition="SecurityHeadersPolicy">
  <headers>
    <header>
      <name>X-Frame-Options</name>
      <enabled>false</enabled>
    </header>
  </headers>
</config>

Note! Since we are on the subject of iframes, feel free to read this blog post about improved mitigation of phishing attacks in Alfresco Share by the introduction of the new IFramePolicy. It will decide which urls that Alfresco Share will allow to be put inside an iframe in the Alfresco Share client.

X-Content-Type-Options

This is only valid for Internet Explorer. Older version of Internet Explorer (8 and below) will *help* developers by sniffing the content of a returned resource and then execute the content as the content type that IE thinks the resource has, instead of the content type the server returned. To stop IE from doing this we are returning nosniff in the header.

X-XSS-Protection

Yep IE trying to *help* again. Instead of trusting the developer to do a good job of mitigating XSS attacks IE just can’t keep its fingers away from your code. If it finds some code that looks suspicious (XSS code) it will manipulate (a.k.a. “sanitize” it) to become safe and then execute the sanitized code. So what’s bad about that? Well IE’s “sanitization” logic can be used by an attacker to actually introduce an XSS flaw on your site. As a “solution” to this IE introduced the X-XSS-Protection header.

By default Alfresco Share will return “1; mode=block” for this header, which means that if IE *thinks* it has found an XSS attack it will not execute the code (instead of sanitizing it and running it which is the default).

It is also possible to set it to “0” which means that IE shall not even try and inspect the code for XSS attacks.

Adding additional headers

Adding additional headers to the config is also supported. Lets take the Strict-Transport-Security header as an example, it is used to force your browser to only allow https and not http communication. It is not provided by default in Alfresco Share but can be added by placing the following code inside your share-config-custom.xml file:

<config evaluator="string-compare" condition="SecurityHeadersPolicy">
  <headers>
    <header>
      <name>Strict-Transport-Security</name>
      <value>max-age=31536000</value>
    </header>
  </headers>
</config>

I hope you have enjoyed this blog post, if you have any questions please add a comment below!

Introducing the IFramePolicy in Alfresco Share

A few weeks back we added a new config section called IFramePolicy into the alfresco-security-config.xml file. This is a config section that describes which pages that Alfresco Share should allow to be “(i)framed”, in other words be included inside Alfresco Share within an iframe. It is available in Alfresco Enterprise 4.1.4 and also for Community on HEAD.

The reason we added this config is to improve mitigation of phishing attacks. To read more about phishing attacks please visit OWASP’s page on the subject.
https://www.owasp.org/index.php/Phishing

So how does this concern you? Well if you are a developer and you have code that creates iframe’s you should honour the config before creating the iframe. If you are a system administrator you are probably interested in overriding the default config because it allows *any* pages to be iframed.

Let’s start by taking a look at the default configuration defined in share-security-config.xml.

<config evaluator="string-compare" condition="IFramePolicy">
  <!-- 
    Local Share pages/resources are governed by the same-domain element which 
    can be set to "allow" or "deny" 
  -->
  <same-domain>allow</same-domain> 
  <!--
    Add a list of <url> elements inside this element to form a whitelist of 
    allowed domains. The check will assert that the url used for the <iframe> 
    starts with the value of one of the <url> elements.
  -->
  <cross-domain>
    <!--
      Allow all domains by default, it is recommended to override this 
      setting and instead keep a whitelist of the domains that you trust to be 
      included on Share pages.
    -->
    <url>*</url>
  </cross-domain>
</config> 

Honouring the IFramePolicy in your code

Every developer creating custom code for Alfresco Share should honour the IFramePolicy config. It is very simple and all you need to do is to add the following code snipped to your code:

if (Alfresco.util.IFramePolicy && 
    !Alfresco.util.IFramePolicy.isUrlAllowed(iFrameSrcUrl))
{
   // TODO: Display error message saying the IFramePolicy doesn't allow this url
}
else
{
  // TODO: Display the iframe just like you did before
}

First we check if the IFramePolicy is there, we do this to make sure our code will continue to work in older Alfresco Share versions that don’t have an IFramePolicy. Then we check if the url that we are about to display is trusted by the IFramePolicy config. If it isn’t we display a friendly error message telling the user how to proceed.

Creating a whitelist of trusted domains

As an administrator you probably want to override the default configuration to keep your Alfresco Share installation as safe as possible. This is very simple to do, simply:

Copy the following code and add it to your share-config-custom.xml file:

<config evaluator="string-compare" condition="IFramePolicy" replace="true">
  <cross-domain>
    <url>http://www.trusted-domain-1.com/</url>
    <url>http://www.trusted-domain-2.com/</url>
  </cross-domain>
</config>

As you can see we have overriden/replaced the IFramePolicy‘s <cross-domain> element to not include the default <url>*</url> but instead multiple <url> element each specifying the urls to trust.

The url check will be done using a “startswith” comparison (not a regexp) meaning you can, if you like, only allow certain pages on a domain to be trusted, i.e. you could add a <url> element like below:

<url>http://www.partly-trusted-domain.com/but/only/urls/from/here</url>

Note! Avoid adding a url with only the protocol and domain that doesn’t end with a front slash (“/”), since http://www.my-proxy-server.com.evil-server.se/phishing-attack.html obviously starts with http://www.my-proxy-server.com but not http://www.my-proxy-server.com/ .

That’s it, I hope you enjoyed the blog post, if you have any questions please add a comment.

Introducing the CSRFPolicy in Alfresco Share

A few weeks back we added a CSRF filter to Share. It is now available on Alfresco Enterprise 4.1.4 and also to the Community on HEAD.

This blog post will be about how the new filter affects your current Alfresco Share installation or your custom Share extension module. In most cases the new filter will actually not affect your installation or custom code at all, however there might be a few edge cases where you need to change your code slightly. The post will also describe how you can configure the CSRF filter to work behind one or more proxies, how to run it with 3rd party plugins behaving badly, how it can be used to stop specific repository services from being accessible directly from the browser through Share’s proxy and also how to turn off the filter.

To learn more about CSRF feel free to read OWASP’s introduction article about CSRF: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)

Note! This post misspells the word “referer” during the entire post to honour the spec that introduced the Referer header with the incorrect spelling :-)

How does the filter work?

The filter is implemented in a class named org.alfresco.web.site.servlet.CSRFFilter that reads a config section in share-security-config.xml named CSRFPolicy which will describe how and when the filter shall mitigate CSRF, summarized as:

  • Each logged in user will receive a secret CSRF token.
  • The token will be communicated to the browser using a cookie named Alfresco-CSRF-Token.
  • When a logged in user performs a POST, PUT or DELETE http request against Alfresco Share the token MUST be passed in the request using one of the following methods:
    • As a custom http request header named Alfresco-CSRF-Token
    • As a url parameter named Alfresco-CSRF-Token.
      Note! Most often the header will be required, but in certain circumstances a header cannot be used and only then the token may be passed using a url parameter. The default config only accepts the url parameter when the Content-Type header starts with multipart/.
  • Every time the logged in user goes to a new Share page the token will be renewed.
  • The filter will also check the Referer and Original http request headers matches the current domain (if present in the request).

To dig into the config, take a look at the latests revision http://svn.alfresco.com/repos/alfresco-open-mirror/alfresco/HEAD/root/projects/slingshot/config/alfresco/share-security-config.xml. A detailed description of all available options can be found at the bottom of this post.

Do you need to tweak your code?

If your custom code only is reading data using GET requests you will not have to do anything.

If your custom code only is using the standard Alfresco.util.Ajaxalfresco/core/CoreXhr or Alfresco.forms.Form javascript classes when creating/updating/deleting data you will also not have to do anything. Everything will be handled for you by:

  • Alfresco.util.Ajax & alfresco/core/CoreXhr – will automatically take the token from the cookie and add it as a request header for every request.
  • Alfresco.forms.Form – will automatically take the token from the cookie and add it as a url parameter to when submitting an multipart/form-data request.
    (When submitting a form as JSON the Alfresco.util.Ajax will be used internally)

Note! In the unlikely event that you do need to change your code make sure to:

  • Always read the token from the cookie just before the request is sent.
    Reason being that if you have multiple tabs opened in your browser one tab could, if accessing a new page, refresh the token in the session. If another tab then had saved its token in a variable it would suddenly be stale. But if all tabs always read the cookie just before submission they will always get the latest token even if the user changed pages in another tab. To read the token from the cookie simply use 
    Alfresco.util.CSRFPolicy.getToken().
  • Always check if the Alfresco.util.CSRFPolicy object exists before using it
    That way your code will work in all versions of Alfresco Share.

Now let’s take a look however at the scenarios when you might need to tweak your code or change the default configuration.

1. You are making an XMLHttpRequest with method POST, PUT or DELETE without using the Alfresco.util.Ajax or alfresco/core/CoreXhr classes

Perhaps you are using the native XmlHttpRequest object or a 3rd party library such as jQuery. If that is the case you will need to add code looking something like this to pass the token:

if (Alfresco.util.CSRFPolicy && Alfresco.util.CSRFPolicy.isFilterEnabled())
{
   xhrHeadersObject[Alfresco.util.CSRFPolicy.getHeader()] = Alfresco.util.CSRFPolicy.getToken();
} 

Or if your using YAHOO.util.DataSource to load data with POST requests your code shall look like this:

if (Alfresco.util.CSRFPolicy && Alfresco.util.CSRFPolicy.isFilterEnabled())
{
   yuiDataSource.connMgr.initHeader(Alfresco.util.CSRFPolicy.getHeader(), Alfresco.util.CSRFPolicy.getToken(), false);
}

2. You are making a form upload with enctype multipart/form-data without using Alfresco.forms.Form

When uploading a file by submitting a form with enctype multipart/form-data it is not possible to set a header on the request, the reason is not because of the enctype specifically but due to the fact that its not possible to set a header on any form submission in the browser. Therefor you need to pass the token as a url parameter instead. If you are using the Alfresco.forms.Form class  this will be handled for you automatically but otherwise you have to add the token as a url parameter using code looking something like this:

if (Alfresco.util.CSRFPolicy && Alfresco.util.CSRFPolicy.isFilterEnabled())
{
   url += "?" + Alfresco.util.CSRFPolicy.getParameter() + "=" + encodeURIComponent(Alfresco.util.CSRFPolicy.getToken());
}

3. You are using a flash movie inside Share to send http requests with method POST

If you are using a flash movie to upload files (it uses the flash.net.FileReference ActionScript class which will perform a multipart/form-data request) make sure to add the token as a url parameter in your Javascript before passing in the url to the flash movie. If your Flash movie is performing application/json or other text based POST requests (it uses the flash.net.URLRequest and/or flash.net.navigateToURL ActionScript classes and methods) then make sure to pass in the token and the name of the header so it can be set from the flash movie.

Note! Flash cannot use ActionScript to directly get hold of the token since it may not read the browser cookies or read http response headers. It is possible however to use the flash.external.ExternalInterface ActionScript class to call a custom javascript method you have included on the page.

4. You are writing a non-browser client, i.e. a mobile app

Such app should be targeted against the repo in which there is no CSRF filter, meaning you don’t have to do anything.

That’s it! If your don’t have code matching any of the described patterns you should not have to modify your code in any way. Please continue reading to be aware of other scenarios when you might be required to alter the default configuration.

Another system is sending POST requests to your Alfresco Share server

If there is a scenario in your system environment where servers from other domains actually shall be allowed to POST requests you will need to reconfigure the CSRFPolicy config in your share-config-custom.xml file to not check for a token or a Referer or Origin header. To do so simply:

  1. Copy the entire CSRFPolicy config in share-security-config.xml 
  2. Paste it into your share-config-custom.xml file and make sure it is replacing the old config section:
    <config evaluator="string-compare" condition="CSRFPolicy" replace="true">
  3. Place the following snippet as the first child to the <filter> element:
<rule>
   <request>
      <method>POST</method>
      <path>/page/trusted/call/1|/page/trusted/call/2</path>
   </request>
   <action name="assertReferer">
      <param name="always">false</param>
      <param name="referer">https://www.trustedserver.com/.*</param>
   </action>
   <action name="assertOrigin">
      <param name="always">false</param>
      <param name="origin">https://www.trustedserver.com</param>
   </action>
</rule>

The CSRF filter will compare the incoming request with all the rules’ request elements to find one that match, when it does it will invoke all the defined actions for that rule and then let the normal Share processing begin. In this case it means that if the external page was POST:ing to /page/trusted/call/1 or /page/trusted/call/2 this (and no other) rule will be used and its actions will run. The first action will assert that full page url in the Referer header (if present) equals https://www.trustedserver.com/.* and that the protocol and domain in the Origin header (if present) equals https://www.trustedserver.com .

I am running Alfresco Share behind one or more proxy server(s) and I get errors…

If you have placed Alfresco Share behind one or more proxy servers that rewrites all the urls before Alfresco Share is reached you might see errors in your log about the Referer or Origin headers not matching the current server. If that is the case see if its possible to also rewrite the Origin and Referer headers if they a) are present and b) match your proxy servers domain.
Note! When doing so make sure to not use a start-with-like-comparison without ending the proxy server’s domain with a front slash (“/”), since http://www.my-proxy-server.com.evil-server.se/csrf.html obviously starts with http://www.my-proxy-server.com but not http://www.my-proxy-server.com/ .

If that is not possible you will have reconfigure the filter to accept requests from specific domains by simply:

  1. Copy the entire CSRFPolicy config in share-security-config.xml
  2. Paste it into your share-config-custom.xml file and make sure it is replacing the old config section:<config evaluator="string-compare" condition="CSRFPolicy" replace="true">
  3. Modify every referer action to accept additional urls by changing:
    <action name="assertReferer">
       <param name="always">false</param>
    </action>

    …to…

    <action name="assertReferer">
       <param name="always">false</param>
       <param name="referer">https://www.proxyserver1.com/.*|https://www.proxyserver2.com/.*</param>
    </action>
  4. Modify every origin action to accept additional urls by changing:
    <action name="assertOrigin">
       <param name="always">false</param>
    </action>

    …to…

    <action name="assertOrigin">
       <param name="always">false</param>
       <param name="origin">https://www.proxyserver1.com|https://www.proxyserver2.com</param>
    </action>

The Referer will contain the entire url from which the request was submitted but the Origin will only include the protocol and domain (hence the .* wildcard at the end of the referer parameter).

In case you wondered, the current domain will continue to be accepted, meaning that you can login and use Share using its own “internal” domain.

 

I just want to disable the filter!

There is no real reason why you should need to turn off the filter if it is configured correctly and you are running a standard Share installation. However if you have installed a 3rd party plugin that is not using the Alfresco provided classes for sending XMLHttpRequests or submitting forms you should contact the plugin developer and ask him to read this blog post so he/she can update the plugin. You will then have to make a decision to either uninstall your plugin OR lower the security level in the filter and not check for tokens anymore (at least until a new version of the plugin has been released). To stop checking for tokens, but continuing to check the Origin and Referer headers when available for logged in users, just add the following code snippet in your share-config-custom.xml file:

<config evaluator="string-compare" condition="CSRFPolicy" replace="true">
   <filter>
      <rule>
         <request>
            <method>POST|PUT|DELETE</method>
            <session>
               <attribute name="_alf_USER_ID">.*</attribute>
            </session>
         </request>
         <action name="assertReferer">
            <param name="always">false</param>
         </action>
         <action name="assertOrigin">
            <param name="always">false</param>
         </action>
      </rule>
   </filter>
</config>

If you have custom code that fails for the reason mentioned in the previous section make sure to fix them instead of disabling the filter. It shouldn’t take long.

If you still, for what ever reason, want to disable the filter just add the following code snippet in your share-config-custom.xml file:

<config evaluator="string-compare" condition="CSRFPolicy" replace="true">
   <filter/>
</config>

 

I have a repository webscript or service that I don’t want to be accessible through Share’s proxy…

This is a bonus feature of having a configurable CSRF filter, that it can be used to completely block certain services in the repository. Perhaps you have an API that only shall be accessible from a) other clients than Share OR b) from server side Java or Javascript code running on the Share server (rather than in the browser as a Java Applet or client side Javascript).

If that is the case you can add the urls to those services to the CSRF filter  and make sure it throws an error when they are accessed. To do this simply:

  1. Copy the entire CSRFPolicy config in share-security-config.xml
  2. Paste it into your share-config-custom.xml file and make sure it is replacing the old config section:
    <config evaluator="string-compare" condition="CSRFPolicy" replace="true">
  3. Add the following code snippet as the first child to the <filter> element:
    <rule>
       <request>
          <path>/proxy/alfresco/acme/special/services/.*</path>
       </request>
       <action name="throwError">
          <param name="message">It is not allowed to access this url from your browser</param>
       </action>
    </rule>

 

A detailed description of the CSRFPolicy configuration

The next code snippet is will give you a detailed description of all available options in the CSRFPolicy configuration. It is probably only worth reading in case you’re really interested or have run into trouble.

Cheers and thanks for reading this far!

<config evaluator="string-compare" condition="CSRFPolicy">

  <!--
    (Mandatory) Only 1 client element is allowed. 
    Describes what names are used to communicate the token back and forth 
    between the server and the client.
  -->
  <client>

    <!--
      (Mandatory) A client element must have exactly 1 cookie element.
      Name of the cookie that will hold the token, used by the client side to 
      grab the value.
    -->
    <cookie>

    <!--
      (Mandatory) A client element must have exactly 1 header element.
      Name of the custom Http header to place the token in when sending a request
    -->
    <header/>

    <!--
      (Mandatory) A client element must have exactly 1 parameter element.
      Name of the parameter to place the token in when sending a request
    -->
    <parameter/>
  </client>

  <!--
    (Mandatory) Only 1 filter element is allowed.
    The filter will look for 1 rule with a matching request and execute its 
    actions (if any). An empty filter element means the CSRF filter is disabled, 
    in other words will allow all requests to pass.
  -->
  <filter>

    <!--
      (Optional) Zero or more rule elements are allowed.
      A rule contains a description of a request and a set of actions to execute.
    -->
    <rule>

      <!-- (Mandatory) A rule element must have exactly 1 request element -->
      <request>

        <!--
          (Optional) A request element may have exactly 1 method element.
          Holds a regular expression that will be matched against the request's 
          method.
        -->
        <method/>

        <!--
          (Optional) A request element may have exactly 1 path element.
          Holds a regular expression that will be matched against the request's 
          "share path", i.e. /page/start-workflow or /proxy/alfresco/api/people
        -->
        <path/>

        <!--
          (Optional) A request element may have any number of header elements.
          Holds a regular expression that will be matched by the header specified 
          by the name attribute.
        -->
        <header name=""/>

        <!-- (Optional) A request element may have exactly 1 session element -->
        <session>
          <!--
            (Optional) A session may have multipe attribute elements.
            Holds a regular expression that will be matched by the session 
            attribute specified by the name attribute. A closed attribute element 
            indicates that the session attribute does not exist. 
            I.e. <attribute name="Alfresco-CSRFToken"/> means that the token has 
            not yet been created.
          -->
          <attribute name=""/>
        </session>

      </request>

      <!--
        (Optional) A rule element may have multiple action elements.
        Below is a list of all available actions:
      -->

      <!-- Generate the token -->
      <action name="generateToken">
        <!--
          (Mandatory) An "generateToken" action may have exactly 1 "session" 
          param.
          Holds the name of the session attribute in which to place the token, 
          shall match the client element's session element above.
        -->
        <param name="session"/>

        <!--
          (Mandatory) A "generateToken" action may have exactly 1 "cookie" param.
          Holds the name of the cookie in which to place the token, shall match 
          the client element's cookie element above.
        -->
        <param name="cookie"/>
      </action>

      <!-- Clear the token value -->
      <action name="clearToken">
        <!--
          (Mandatory) A "clearToken" action may have exactly 1 "session" param.
          Holds the name of the session attribute which value shall be cleared,
          shall match the client element's session element above.
        -->
        <param name="session"/>

        <!--
          (Mandatory) A "clearToken" action may have exactly 1 "session" param.
          Holds the name of the cookie which value shall be cleared, shall match
          the client element's cookie element above.
        -->
        <param name="cookie"/>
      </action>

      <!-- 
        Assert the request's Referer header matches the current domain. 
        If not an error will be thrown. 
      -->
      <action name="assertReferer">
        <!--
          (Mandatory) An "assertReferer" action may have exactly 1 "always" param.
          Decides when to compare the incoming requests Referer header to the 
          current domain, if set to:
          - true: Always compare, even when no Referer header was provided in the 
                  request.
          - false: Only compare if a Referer header was provided in the request
        -->
        <param name="always"/>

        <!--
          (Optional) An "assertReferer" action may have 0 or 1 "referer" param.
          Holds a regular expression that will be matched against the incoming 
          Referer header if the incoming Referer header does not match Share's domain.
        -->
        <param name="referer"/>
      </action>

      <!-- 
        Assert the requets's Origin header matches the current domain. 
        If not an error will be thrown. 
      -->
      <action name="assertOrigin"> 
        <!--
          (Mandatory) An "assertOrigin" action may have exactly 1 "always" param.
          Decides when to compare the incoming requests Origin header to the 
          current domain, if set to:
          - true: Always compare, even when no Origin header was provided in the 
                 request.
          - false: only compare if a Origin header was provided in the request
        -->
        <param name="always"/>

        <!--
          (Optional) An "assertOrigin" action may have 0 or 1 "origin" param.
          Holds a regular expression that will be matched against the incoming 
          Origin header if the incoming Origin header does not match Share's 
          domain.
        -->
        <param name="origin"/>
      </action>

      <!-- Will throw an error -->
      <action name="throwError">

        <!--
          (Optional) A "throwError" action may have exactly 1 "message" param.
          Holds the error message that will be used when throwing the error.
        -->
        <param name="message"/>
      </action>
    </rule>

  </filter>

</config>

 

 

Add, remove or replace components on Share’s Document Details page

Introduction

.
In Alfresco Community 4.0.a we included a lot of customization improvements to Share. One of them was to be able to swap out components on a page. This means that you can:
  • Add new components to a page
  • Remove a components from a page
  • Replace a component on a page
This post will show how you can do this using 1 single config file that you later will be able to deploy and undeploy (without restarting the server) using the module deployment ui in Share. In this blog post we will use Share’s “Document details” page to show these features, but once you have done this tutorial you will be able to switch out components on any page (except the dashboards which instead can be changed using Share’s “Customize dasboard” ui).
.
First lets take a look at the out of the box panels available on the Document Details page in Alfresco Community 4.0.a (the image to the left) and how the panels will look after we have applied the customization in this tutorial (the image to the right).
.
In short we will:
.
  1. Create a custom “Acme Sample” document panel component
  2. Declare an extension module xml file for Acme which will.
    • Add the new “Acme Sample” panel component above the “Actions” panel.
    • Remove the “Tags” panel component.
    • Replace the “Permissions” panel with the “Acme Sample” document panel.
  3. Package your extension as a .jar
  4. Deploy extension on your server
(In other words the “Acme Sample” document panel will appear 2 times)
.
Note! If you want the full example source code, use this link: acme-test-extension.zip. Don’t forget to rename it from .zip to .jar before placing it in TOMCAT_HOME/shared/lib!

Create a custom “Sample” document panel for Acme

.
Create a minimal document details panel by first creating the following directoy structure /alfresco/site-webscripts/acme/components/document-details/ and inside it create the following files.

sample.get.desc.xml

<webscript>
   <shortname>Acme Test Sample</shortname>
   <description>Acme Test Document sample panel</description>
   <url>/acme/components/document-details/sample</url>
</webscript>

sample.get.html.ftl

<#assign el=args.htmlid?js_string/>
<div class="document-details-panel">
  <h2 id="${el}-heading" class="thin dark">${msg("heading")}</h2>
  <div>${msg("text")}</div>
  <script type="text/javascript">//<![CDATA[
    Alfresco.util.createTwister("${el}-heading", "DocumentMetadata");
  //]]></script>
</div>

sample.get.properties

heading=Acme Sample
text=This is a demo

Declare an extension module xml

.

Create the following directory structure: /alfresco/site-data/extensions/.

Inside that directory create the following file:

acme-test-extension.xml

<extension>
   <modules>
      <module>
         <id>Acme :: Test - Document Details Page</id>
         <evaluator type="default.extensibility.evaluator"/>
         <components>
            <!-- SWAP COMPONENT CODE GOES HERE -->
         </components>
      </module>
   </modules>
</extension>
When we write the code to swap components we will use 3 elements (<scope>, <region-id> & <source-id>) to identify “where” our new components shall be added. But instead of diving into a detailed description of how pages, templates & regions work in Share I will just give you an extremely cut down version:
.
Each page (document-details.xml) uses a template (document-details.ftl) in which regions are declared (<@region id=”document-actions” scope=”template”/> into which one or more webscript components can be inserted.
.
If you didn’t understand any of this, don’t worry. All you really need is to identify the values for the 3 elements, something that can be done using a Share developer tool called SurfBug, just do the following:
.
  1. Go to http://localhost:8080/share/page/surfBugStatus
  2. Click the “Enable SurfBug” button
  3. Navigate to the “Document Details” (you should now see each component being surrounded by a red border)
  4. To identify the region-id, source-id & scope for the “Actions” panel, simply click on the “Actions” panel.
  5. You should now see a popup displaying information about the “Actions” panel, you will find the values you are looking for under the “Component Details” section.

.

If you haven’t got a server running you can see the popup displayed bu SurfBug in the image below:

.

In the sections below I will show you how to use these values to add, remove and replace components on the page.

Add a new custom panel component above the “Actions” panel.

Place the following code inside the place-holder-comment in the acme-test-extension.xml file mentioned above.

<component>
   <scope>template</scope>
   <region-id>document-actions</region-id>
   <source-id>document-details</source-id>
   <sub-components>
      <sub-component id="acme-sample" index="-1">
         <evaluations>
            <evaluation id="acme-test-addSample">
               <url>/acme/components/document-details/sample</url>
            </evaluation>
         </evaluations>
      </sub-component>
   </sub-components>
</component>

As you can we are using the 3 values to identify the region in which to add our sample component.

We then declare a <sub-component> element, you can declare as many as you like and therefore add as many components as you like. Note that we give the <sub-component> a unique id. The cool thing about that is that the id can be referenced from another extension module (which then can remove it). Therefor make sure you namespace your id with a company name.

The <evaluations> & <evaluation> elements are there to make it possible for a <subcomponent> to have multiple outcomes (the outcome being the <url> element pointing to the webscript to use). The outcome depends on which <evaluation> that returned true. In this example we are only using one <evaluation> element and it has no <evaluator> child elements, meaning we are actually not “evaluating” anything and that the <evaluation> therefor always will “win” (be the the onw who decides the outcome).

Note that the <url> element is pointing to our Acme “Sample” panel we created earlier.

Remove the “Tags” panel component

Place the following code inside the place-holder-comment in the acme-test-extension.xml file mentioned above.

<!-- Remove Tag Panel -->
<component>
   <scope>template</scope>
   <region-id>document-tags</region-id>
   <source-id>document-details</source-id>
   <sub-components>
      <sub-component id="default">
         <evaluations>
            <evaluation id="acme-test-removeTags">
               <render>false</render>
            </evaluation>
         </evaluations>
      </sub-component>
   </sub-components>
</component>
If you use SurfBug and this time click on the “Tags” panel you can see from where the values for our first 3 elements are coming from. More interesting though is that we have set the id of the <sub-component> to “default”. The reason for this is that we are actually not creating a new <sub-component> here, instead we are referencing the “Tags” panel component.
.
The reason it is called “default” is because when it was added to the page it didn’t provide an explicit id. The <sub-component> id is also something you can find by using SurfBug, just look at the “ID” field under the “Sub-Component Details”, the part after the “#” character is the id.
.
This means that what we are doing is to extend the “default” <sub-component> (the “Tags” panel) with a new <evaluation> element, and since it has no <evaluator> child elements it will always “win” and decide the outcome. The outcome in this case though is NOT a <url> element, instead it is <render>false</render> which tells Share that no component at all shall be rendered.

Replace the “Permissions” panel with the “Sample” document panel.

Place the following code inside the place-holder-comment in the acme-test-extension.xml file mentioned above.

<!-- Replace Permissions with Sample Panel -->
<component>
   <scope>template</scope>
   <region-id>document-permissions</region-id>
   <source-id>document-details</source-id>
   <sub-components>
      <sub-component id="default">
         <evaluations>
            <evaluation id="acme-test-replaceWithSample">
               <url>/acme/components/document-details/sample</url>
            </evaluation>
         </evaluations>
      </sub-component>
   </sub-components>
</component>

In this example we have actually not introduced any new concepts, its just a combination of the previous two examples.

We use the “default” id to add in a new <evaluation> to  the “Permissions” panel. Since the <evaluation> has no <evaluator> child elements it will always “win” and decide the outcome, in this case a <url> element pointing to Acme’s “Sample” panel component.

Package your extension as a .jar

As the last piece of the puzzle lets make sure to package everything up in a jar file so you can share it with your colleagues or easily deploy it on servers. Simply do the following:

  1. Use your terminal/dos window and change into the directory containing the directory structure you have created.
    • Note! it shall only contain the directory “alfresco”
  2. Type the following command to jar it up:
    • jar cvf acme-test-extension.jar *

Deploy extension on your server

Now you have a nice extension packaged as a jar, so lets go ahead and use it, simply…

  1. Place it in your TOMCAT_HOME/shared/lib
  2. Restart your server
  3. Go to the deplyment ui http://localhost:8080/share/page/modules/deploy and make sure to enable your module by adding it and then click the “Apply” button.
  4. Go to the document details page and you should now see 2 “Sample” panels but no “Tags” or “Permissions” panels.

I hope you enjoyed this blog post. If there was something you didn’t understand, don’t hesitate to add a comment. Once you get it you will find its a really easy and powerful way of extending your Share installation.

Cheers,

:: Erik

Create pages in Alfresco Share using new “out of the box” templates

Introduction

This blog post explains how you can speed up the development of creating pages in Alfresco Share by using a couple of  ”out of the box templates”.

The idea is that when creating a “simple” page in Share with 1, 2 or 3 column you shouldn’t have create your own template-instance.xml and template.ftl file. Instead you can re-use one of the 6 out of the box templates described below. That means that you only need to define 1 page.xml-definition in which you tell which webscripts to display inside each region.

The out of the box templates described below are using “page scoped” regions which means that they can be configured from within a page.xml file. They also make use of a new Spring Surf feature that makes it possible to associate multiple webscripts to a single template region, a feature that was made available in Alfresco Community 4.0.a release.

Note! The actual out of the box templates were made available on HEAD in revision 31068. – They are thereby NOT included in the 4.0a release, so to use them you need to either download the Alfresco Community 4.0.b release OR (to use them in Alfresco Community 4.0.a) do the following…

  1. Download the alfresco-share-out-of-the-box-templates-r31068-lib.zip patch (which contains the 6 out of the box templates)
  2. Rename it from .zip to .jar
  3. Place it in your <SHARE_TOMCAT_HOME>/shared/classes directory
  4. Restart your server

Now we will take a look at how the new templates look and then give an example of how to use them.

.

The templates

Below you can see the 6 templates in action. I have created sample pages using them to make it easy for you to understand the structure of each template.

I am using the components/title/simple-title.get webscript to display the blue title (i.e. “TEST 1 COLUMN”) and a custom webscript created for this blog post (components/test/echo.get available in the source code for this blog post) to display the “Echo strings” (i.e. “Echo: FIRST”).

To test out them out yourself just:

  1. Download the sample source code alfresco-share-out-of-the-box-templates-test-pages-source.zip (which contains the test pages & the Echo webscript).
  2. Rename it from .zip to .jar.
  3. Place it in your <SHARE_TOMCAT_HOME>/shared/classes directory.
  4. Restart your server.
  5. Follow the links below to view each template.

.

1-column (regions: title & column)

http://localhost:8081/share/page/test-1-column

.

2-columns (regions: title, left-column & right-column)

http://localhost:8081/share/page/test-2-columns

.

2-columns-narrow-left (regions: title, left-column & right-column)

http://localhost:8081/share/page/test-2-columns-narrow-left

.

2-columns-narrow-right (regions: title, left-column & right-column)

http://localhost:8081/share/page/test-2-columns-narrow-right

.

2-columns-resizable (regions: title, left-column & right-column)

http://localhost:8081/share/page/test-2-columns-resizable

.

3-columns (regions: title, left-column, middle-column & right-column)

http://localhost:8081/share/page/test-3-columns

.

How to create a page that uses one of the templates

We will create a page named “test-1-column” that will use the 1-column template and will be reachable at http://localhost:8081/share/page/my-1-column-test.

As you can see below the 1-column template contains 2 “page scoped” regions (title & column) that therefore can be configured from within a  page.

To get the result as above do the following:

  1. Create the following directory stucture <SHARE_TOMCAT_HOME>/shared/classes/config/alfresco/site-data/pages
  2. Create a new page in the directory above and name it to my-1-column-test.xml
  3. In my-1-column-test.xml reference the webscripts you want to include in each region, i.e. using the code below.

<SHARE_TOMCAT_HOME>/shared/classes/config/alfresco/site-data/pages/my-1-column-test.xml

<?xml version='1.0' encoding='UTF-8'?>
<page>
   <title>Test 1 column</title>
   <description>Test 1 column</description>
   <template-instance>1-column</template-instance>
   <authentication>user</authentication>
   <components>

      <!-- Title -->
      <component>
         <region-id>title</region-id>
         <url>/components/title/simple-title</url>
         <properties>
            <title>TEST 1 COLUMN</title>
         </properties>
      </component>

      <!-- Column -->
      <component>
         <region-id>column</region-id>
         <sub-components>
            <sub-component id="first">
               <url>/components/test/echo</url>
               <properties>
                  <echo>FIRST</echo>
               </properties>
            </sub-component>
            <sub-component id="second">
               <url>/components/test/echo</url>
               <properties>
                  <echo>SECOND</echo>
               </properties>
            </sub-component>
         </sub-components>
      </component>

   </components>
</page>

First note that we use the <template-instance> element to define that the 1-column template shall be used.

Then note that the syntax used to “bind in” the webscripts into a region are different in the two regions.

In the “title” region we only need one webscript and therefor we use the same old syntax that has been used in Share since day 1.

In the “column” region, however, we want to add in multiple webscripts into the same region. We therefore  have to use the new <sub-components>-syntax to get the job done (available since the Alfresco Share Community 4.0a release). Using this syntax means that you can add in as many webscripts as you like in 1 region. Note! Make sure you make the “id” attribute in the <sub-component> element “unique per region”.

To test your page make sure you have installed alfresco-share-out-of-the-box-templates-test-pages-source.zip as described earlier, if so just point your browser to: http://localhost:8081/share/page/my-1-column-test

.

Further reading

More about the new syntax for “binding in” webscripts into a region can be found here:

Display .pdf files in your browser using pdf2swf & Alfresco’s WebPreview.swf

.
Hi,
.
There was a request in the forums for how to use the WebPreviewer (found on Share’s document details page) outside of Alfresco.
I made a simple example that displays the current functionality that can be downloaded from here.
.
Note! The folder must be placed on a webserver, otherwise Flash Player will stop the external .swf from loading due to security violations.
Otherwise all you need to do is point your browser to the index.html file.
.
To test with your own documents simply generate a .swf from a .pdf by running pdf2swf with the following command …
%> pdf2swf -T 9 -s poly2bitmap,subpixels=72 SomeFile.pdf -o SomeFile.swf.
… and don’t forget to change the “url” parameter in the “index.html” example page ;-)
.
The pdf2swf utility can be downloaded from http://www.swftools.org/
.
PS. We will enhance the WebPreviewer to support text searches in the document & text selection w copy to clipboard in fortcoming versions.
.

Spring Surf Development #2 – Secure your Spring WebScript REST API

.
.
In the previous blog post we added a WebScript that listed the different safaris offered by the Safari company. Definitively no company secret in other words. But what if we want to add a WebScript that lists internal company information in a manager section of the website or let a registered user buy a safari? Then we need to add security to our REST API. In this blog post we will continue and build upon the Safari use case using Spring WebScripts. We will:
  1. Explain the basics of WebScript authentication
  2. Secure our Safari REST API by writing an authentication factory that maps Spring WebScript authentication roles to the Safari’s company’s user groups.
  3. Write 3 new secured webscripts: Create Trip, Create Booking & List User’s Bookings that we can use in future when we create the Safari web shop ui.
  4. Test that the WebScripts are secured

If you are only interested to get a quick glance at WebScript security, and don’t want to bother about all the Safari use case code, consider only reading the following sections: “Spring WebScript Security Basics”,  “Securing the Spring WebScript runtime” and possibly “Using HTTP Sessions in Spring WebScripts?”.

If you want to download the sample code you can do that from: Safari – Spring WebScripts REST API sample #2.

If .

Spring WebScript Security Basics

.

Spring WebScripts can be hosted inside a number of different runtime environments:

  • Servlet Runtime (HTTP Access)
  • JSR-168 Runtime (Portlet Access)
  • JSF Runtime (JSF Component Access)

.As you have probaly guessed the Safari REST API is using the Servlet Runtime, something we declared by defining the WebScriptServlet in safari-rest/src/main/webapp/WEB-INF/web.xml in the last blog post. All runtimes support 4 roles or levels of authentication which is declared in the <authentication> element in each webscript’s decriptor file (some-webscript.get.desc.xml):

  • none” – No authentication is required (default)
  • guest” – No authentication is required but the webscript could get access to additional read services
  • user” – Authentication is required
  • admin” – Authentication is required by a user with the admin role

The default authentication level, if the <authentication> element is omitted, is “none”..


The “use case” for the Safari Company

.

The Safari company currently only has 2 persons in its system:
  • erik” – A registered user and therefore a member of the “customer” group.
  • roy” – An employee and therefore a member of the “manager” group but sometimes also buys trips and therefore is a member of the “customer” group as well.
.
The class that will handle users, groups and authentication is the IdentityService. First register it as a Spring bean inside safari-rest/src/main/webapp/WEB-INF/config/web-application-context.xml next to our old TravelService from the previous blog post.
<!-- Safari Services -->
<bean id="travelService" class="com.safari.core.travel.TravelService"/>
<bean id="identityService" class="com.safari.core.travel.IdentityService"/
.
Then create the actual implementation in safari-core/src/main/java/com/safari/core/travel/ and make it look like below:.
package com.safari.core.travel;

import com.safari.core.SafariContext;

import java.util.ArrayList;
import java.util.List;

/**
 * In memory service for demo and testing purposes
 */
public class IdentityService {

   public static final String MANAGER = "manager";
   public static final String CUSTOMER = "customer";

   public boolean authenticate(String username, String password) {
      // Perform simple example authentication
      if (username.equals(password)) {
         SafariContext.setCurrentUser(username);
         return true;
      }
      return false;
   }

   public List<String> getUsers() {
      List<String> users = new ArrayList<String>();
      users.add("erik"); // A registered customer
      users.add("roy"); // Sales manager for the company who sometimes buy trips
      return users;
   }

   public List<String> getGroups(String user) {
      List<String> groups = new ArrayList<String>();
      if (user.equals("erik")) {
         groups.add(CUSTOMER);
      }
      else if (user.equals("roy")) {
         groups.add(MANAGER);
         groups.add(CUSTOMER);
      }
      return groups;
   }

}
Notice that the IdentityService “saves” the current username, if the authentication was successful, by calling SafariContext.setCurrentUser(username) which will make the username available elsewhere in the application. The implementation of SafariContext will be really simple and use the ThreadLocal class to make sure the correct username only is available for the current request/thread. Implement the SafariContext class in safari-core/src/main/java/com/safari/core/ and make it look like below:
package com.safari.core;

public abstract class SafariContext {

  static ThreadLocal<String> authenticatedUserNameThreadLocal = new ThreadLocal<String>();

  public static void setCurrentUser(String userName) {
    authenticatedUserNameThreadLocal.set(userName);
  }

  public static String getCurrentUser() {
    return authenticatedUserNameThreadLocal.get();
  }
}
So now we have the IdentityService that will handle the authentication (it will actually just do a silly authentication by making sure the username also is the password) and if so store the username so it is available from other parts of the application by doing a simple SafariContext.getCurrentUser().
.
But somehow we will need to make the IdentityService’s authenticate method get called by the webscripts runtime and we also somehow must match the Safari company’s groups (“customer” & “manager”) against the webscript “roles” (“none”, “guest”, “user” & “admin”). To do this we need to add our own custom authenticator factory to the webscripts runtime. (Don’t worry it’s very easy)

Securing the Spring WebScript runtime

As mentioned before the Safari REST API was configured to use the WebScript Servlet Runtime. To add authentication to it simply add the “authenticator” init-param to the WebScriptServlet and safari-rest/src/main/webapp/WEB-INF/web.xml like below:

<!-- Spring WebScripts -->
<servlet>
   <servlet-name>WebScriptServlet</servlet-name>
   <servlet-class>org.springframework.extensions.webscripts.servlet.WebScriptServlet</servlet-class>
   <init-param>
      <param-name>authenticator</param-name>
      <param-value>webscripts.authenticator.safari</param-value>
   </init-param>
</servlet>
.
The value “webscripts.authenticator.safari” is a spring bean reference to the authenticator factory we haven’t created yet. The job for it will be to  to decode the authentication headers from the HTTP request and decide if the username and the password matches (authentication) and if so decide if the user’s privilige matches or exceeds what the privelige that the webscript require (authorization).

This is done by simply extending the org.springframework.extensions.webscripts.AbstractBasicHttpAuthenticatorFactory class (which will do the work of decoding the HTTP headers for us) and respond to its 2 abstract methods: doAuthenticate(String username, String password) and doAuthorize(String username, RequiredAuthentication role).
Name it SafariBasicHttpAuthenticatorFactory, place it in safari-rest/src/main/java/com/safari/rest/auth/ and make it look like below:

package com.safari.rest.auth;

import com.safari.core.travel.IdentityService;
import org.springframework.extensions.webscripts.AbstractBasicHttpAuthenticatorFactory;
import org.springframework.extensions.webscripts.Description;
import org.springframework.extensions.webscripts.WebScriptRequest;
import org.springframework.extensions.webscripts.WebScriptResponse;

import java.util.ArrayList;
import java.util.List;

public class SafariBasicHttpAuthenticatorFactory extends AbstractBasicHttpAuthenticatorFactory
{

   private IdentityService identityService;

   /**
    * The Safari Identity Service which will perform the authentication.
    *
    * @param identityService The safari identity service
    */
   public void setIdentityService(IdentityService identityService) {
      this.identityService = identityService;
   }

   /**
    * Performs an authentication check using the Identity Service.
    *
    * @param username The username of the user
    * @param password THe password the user tries to log in with
    * @return true if the username and pass word matched a user in the system
    */
   public boolean doAuthenticate(String username, String password) {
      return identityService.authenticate(username, password);
   }

   /**
    * Translate the logged in users groups with the WebScript security
    * role model and decide if user is allowed to execute the WebScript.
    *
    * @param username The username of the logged in user
    * @param role The authority role that is required for the user to execute the webscript
    * @return true if user is authorized to execute the WebScript
    */
   public boolean doAuthorize(String username, Description.RequiredAuthentication role)
   {
      List<String> grantedGroupIds = new ArrayList<String>();
      if (role == Description.RequiredAuthentication.user) {
         /**
          * This method is called after doAuthenticate which means
          * the login was successful and the request was done by a user.
          */
         grantedGroupIds.add(IdentityService.CUSTOMER);
      }
      else if (role == Description.RequiredAuthentication.admin) {
         // Check if user is member of the admin group.
         grantedGroupIds.add(IdentityService.MANAGER);
      }
      if (grantedGroupIds.size() == 0) {
         // No group membership is required for the user.
         return true;
      }
      else {
         // Certain group membership is required user.
         List<String> userGroups = identityService.getGroups(username);
         for (String group : userGroups)
         {
            for (String grantedGroupId : grantedGroupIds) {
               if (group.equals(grantedGroupId)) {
                  return true;
               }
            }
         }
      }
      return false;
   }
}
Then define it as the “webscripts.authenticator.safari” bean in safari-rest/src/main/webapp/WEB-INF/web-application-context.xml:
<!-- Add authentication and authorization support for webscripts (used by the WebScriptServlet) -->
<bean id="webscripts.authenticator.safari" class="com.safari.rest.auth.SafariBasicHttpAuthenticatorFactory">
   <property name="identityService" ref="identityService"/>
</bean>
.
This is basically it! What will happen is that the doAuthenticate(username, password) method will be called if the authentication role for the webscript is “user” or “admin” (remember that “none” and “guest” didn’t require any authentication). If the authentication is successful doAuthorize(username, role) will also be called in which the mapping between webscript authentication roles and safari groups happen.
.
As you might have noticed, if you tried to compile, the AbstractBasicHttpAuthenticatorFactory class is missing. To add it to your project simply modify the safari-rest/pom.xml maven file from the last tuorial:
<!--
   Temporary include the "spring-surf" artifact to get the AbstractBasicHttpAuthenticatorFactory.
   When updating to RC2 or the final 1.0.0 release it will have moved to "spring-webscripts".
-->
<dependency>
   <groupId>org.springframework.extensions.surf</groupId>
   <artifactId>spring-surf</artifactId>
   <version>1.0.0-RC1</version>
</dependency>

<!-- Include the Spring WebScripts runtime -->
<dependency>
   <groupId>org.springframework.extensions.surf</groupId>
   <artifactId>spring-webscripts</artifactId>
   <version>1.0.0-RC1</version>
</dependency>
<!-- Include the Spring WebScript API so we can browse and list our webscripts on the server -->
<dependency>
   <groupId>org.springframework.extensions.surf</groupId>
   <artifactId>spring-webscripts-api</artifactId>
   <version>1.0.0-RC1</version>
</dependency>
So by now you should be ready to build and redploy the entire project by changing into your projects top level directory do:
mvn -f safari-root/pom.xml clean install org.codehaus.mojo:tomcat-maven-plugin:1.0-beta-1:redeploy
If you got an error try “deploy” rather than “redeploy” since the webapp might have been removed since you did the previous tutorial.
To test that we actually have succeded to secure the our WebScripts point your browser to http://localhost:8080/safari-rest/service/index which then shall make your browser display a login dialog. This url is pointing to the pages that lists all deployed webscripts, and in the last tutorial they were publically available since we hadn’t secured the runtime. If you try to login with “erik” (as both username and password) it shall NOT work, since he only is a “user”. But if you try to login with “roy” (as both username and password) it should work.
By now we are actually finished with the security part of this tutorial so lets do a quick recap what we did:
  1. Told the WebScriptServlet in web.xml to use our authenticator: “webscripts.authenticator.safari”.
  2. Defined the authenticator bean in web-application-config.xml.
  3. Implemented the authenticator by extending AbstractBasicHttpAuthenticatorFactory and respond to doAuthenticate(username, password) and doAuthorize(username, role).

… the rest is up to you and how your app is being implemented, perhaps you want to pass in the username and roles to another environment?

.

Using HTTP Sessions in Spring WebScripts?

What could be worth pointing out is that the only place where the authentication details are stored, using this approach, is in the request headers and that currently no sessions are used, in other words ideal for scaling. However if you want to use the HttpSession it’s of course possible to access it both in a regular webscript and in your custom made authenticator class. Simply modify you authentication factory with the changes below:

public class SafariBasicHttpAuthenticatorFactoryWithSession extends AbstractBasicHttpAuthenticatorFactory
{
   ...

   // Add a place to store the request and response when the authenticator is created
   private WebScriptRequest req;
   private WebScriptResponse res;

   ...

   @Override
   public Authenticator create(WebScriptServletRequest req, WebScriptServletResponse res)
   {
   	// Override the create method so we get a change to store the request & response
      this.req = req;
      this.res = res;
      return new BasicHttpAuthenticator(req, res);
   }

   @Override
   public boolean doAuthenticate(String username, String password) {
      if (identityService.authenticate(username, password)) {
      	// ...so we can access the HttpSession after a successful login
         this.req.getHttpServletRequest().getSession().setAttribute("safari.username", username);
         return true;
      }
      return false;
   }

   ...

}
…and to later access the session attribute it in the WebScript:
public class SafariWebScriptWithSessionGet extends SafariWebScript
{
   @Override
   protected Map<String, Object> executeImpl(WebScriptRequest req, Status status, Cache cache) {
      String bar = (String) ((WebScriptServletRequest)req).getHttpServletRequest().getSession().getAttribute("safari.username");
      …
   }
}

.

Creating 3 secured WebScripts

The rest of this tutorial will be about creating a couple of secured WebScripts. We will use these WebScripts in future blog posts when we build a Spring Surf web application that will use them to display Safari trips and bookings.

First create a simple Booking pojo in safari-core/src/main/java/com/safari/core/travel/Booking.java that looks like below:

package com.safari.core.travel;

public class Booking {

   private int id;
   private String username;
   private Trip trip;

   public Booking () {}

   public int getId(){ return id; }
   public void setId(int id) { this.id = id; }

   public String getUsername() { return username; }
   public void setUsername(String username) { this.username = username; }

   public Trip getTrip() { return trip; }
   public void setTrip(Trip trip) { this.trip = trip; }

}
Then open the TravelService class that we implemeted in the previous tutorial, located in safari-core/src/main/java/com/safari/core/travel/, and update it so it looks like below:
package com.safari.core.travel;

import com.safari.core.SafariContext;

import java.util.ArrayList;
import java.util.List;

/**
 * In memory service for testing purposes
 */
public class TravelService {

   private static int tripIndex = 0;
   private static List<Trip> trips = new ArrayList<Trip>();
   private static List<Booking> bookings = new ArrayList<Booking>();

   public TravelService() {
      // Bootstrap data
      trips.add(new Trip(++tripIndex, "Masai Mara Adventurer"));
      trips.add(new Trip(++tripIndex, "Serengeti Explorer"));
      trips.add(new Trip(++tripIndex, "Kruger Wildlife"));
   }

   /**
    * Returns a public list of all available trips
    *
    * @return a list of trips
    */
   public List<Trip> getTrips() {
      return trips;
   }

   /**
    * Finds a trip by id
    *
    * @param tripId The trip id to look for
    * @return the trip if found otherwise null
    */
   public Trip getTrip(int tripId) {
      // Find the trip
      for (Trip trip : trips) {
         if (trip.getId() == tripId) {
            return trip;
         }
      }
      return null;
   }

   /**
    * Administration method to create new trips
    *
    * @param trip The trip to create
    * @return The created trip with a unique id
    */
   public Trip createTrip(Trip trip) {
      // Give trip a unique id and add it
      trip.setId(++tripIndex);
      trips.add(trip);
      return trip;
   }

   /**
    * Lets the current user create a booking for a certain trip
    *
    * @param tripId The trip to create a booking for
    */
   public Booking createBooking(int tripId) {
      // Check that the trip exists
      Trip trip = getTrip(tripId);
      if (trip == null) {
         throw new IllegalArgumentException("Cannot book trip with id '" + tripId + "' since it doesn't exist");
      }

      // Create booking and return it
      Booking booking = new Booking();
      booking.setTrip(trip);
      booking.setUsername(SafariContext.getCurrentUser());
      bookings.add(booking);
      return booking;
   }

   /**
    * Returns a list of the current users bookings
    */
   public List<Booking> getBookings() {
      List<Booking> userBookings = new ArrayList<Booking>();
      for (Booking booking : bookings) {
         if (booking.getUsername().equals(SafariContext.getCurrentUser())) {
            userBookings.add(booking);
         }
      }
      return userBookings;
   }

}
The code should be pretty self explanatory, what could be worth pointing out is that the getBookings() method uses the SafariContext.getCurrentUser() when it makes sure that only the current users bookings are returned.
No let’s create a new webscript that will create a  Trip and only should be available to administrators. Create the descriptor file as safari-rest/src/main/resources/webscripts/com/safari/travel/trip.post.desc.xml. It’s just like any other webscript except that the <authentication> element is set to contain the value of “admin”.
<webscript>
   <shortname>Create Trip</shortname>
   <description>Creates a trip</description>
   <url>/travel/trip</url>
   <format default="json">argument</format>
   <authentication>admin</authentication>
</webscript>

Now let’s continue with the admin WebScript for creating trips and implement the Java controller:

package com.safari.rest.api.travel;

import com.safari.core.travel.Trip;
import com.safari.rest.api.SafariWebScript;
import org.json.JSONException;
import org.json.JSONObject;
import org.springframework.extensions.webscripts.Cache;
import org.springframework.extensions.webscripts.Status;
import org.springframework.extensions.webscripts.WebScriptException;
import org.springframework.extensions.webscripts.WebScriptRequest;

import java.util.HashMap;
import java.util.Map;

public class TripPost extends SafariWebScript
{
   protected Map<String, Object> executeImpl(WebScriptRequest req, Status status, Cache cache) {
      try {
         JSONObject json = new JSONObject(req.getContent().getContent());
         Trip trip = new Trip();
         trip.setName(json.getString("name"));
         trip = getTravelService().createTrip(trip);
         Map<String, Object> model = new HashMap<String, Object>();
         model.put("trip", trip);
         return model;
      }
      catch (JSONException e) {
         throw new WebScriptException(Status.STATUS_BAD_REQUEST, "The request body contains badly formatted json");
      } catch (Exception e) {
         throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR, "Trip could not be created: " + e.getMessage());
      }
   }
}
Note that the webscript expects the request body to contain JSON, its in other words not a regular HTML Form post that is being submitted. This is an approach that we will use for all of our webscripts. Also note that we are throwing a special exception if the request isn’t correctly formatted: Status.STATUS_BAD_REQUEST this exception actually maps to the HTTP status code 400. When you create your webscript make sure to use the HTTP status codes to signal to the client what went wrong.Below is the response template that shall be defined in safari-rest/src/main/resources/webscripts/com/safari/travel/trip.post.json.ftl. It will return the new trip and the id it was assigned when “persisted” in the service:
<#escape x as jsonUtils.encodeJSONString(x)>
{
   "id": ${trip.id},
   "name": "${trip.name}"
}
</#escape>
.
Below comes the code for 2 new webscripts for the Safari customers (in other words “users”). First one that allows a user to create a booking and then one that lists the users bookings.

Create safari-rest/src/main/resources/webscripts/com/safari/travel/booking.post.desc.xml and make it look like below and note that the <authentication> element is set to “user”:

<webscript>
   <shortname>Create Booking</shortname>
   <description>Creates a booking</description>
   <url>/travel/booking</url>
   <format default="json">argument</format>
   <authentication>user</authentication>
</webscript>
Then create the Java controller in safari-rest/src/main/java/com/safari/rest/api/travel/BookingPost.java that also expects a json request body.
package com.safari.rest.api.travel;

import com.safari.core.travel.Booking;
import com.safari.rest.api.SafariWebScript;
import org.json.JSONObject;
import org.springframework.extensions.webscripts.Cache;
import org.springframework.extensions.webscripts.Status;
import org.springframework.extensions.webscripts.WebScriptException;
import org.springframework.extensions.webscripts.WebScriptRequest;

import java.util.HashMap;
import java.util.Map;

public class BookingPost extends SafariWebScript
{
   @Override
   protected Map<String, Object> executeImpl(WebScriptRequest req, Status status, Cache cache)
   {
      // Make sure tripId is a number and create the booking
      try {
         JSONObject json = new JSONObject(req.getContent().getContent());
         Booking booking = getTravelService().createBooking(json.getInt("tripId"));
         Map<String, Object> model = new HashMap<String, Object>();
         model.put("booking", booking);
         return model;
      }
      catch (NumberFormatException nfe) {
         throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Parameter tripId is mandatory and must contain an integer value");
      } catch (Exception e) {
         throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR, "Trip could not be booked: " + e.getMessage());
      }
   }
}
Now create the response template in safari-rest/src/main/resources/webscripts/com/safari/travel/booking.post.json.ftl and note how easy it is to render the nested the Trip object inside the Booking:
<#escape x as jsonUtils.encodeJSONString(x)>
{
   "id": ${booking.id},
   "username": "${booking.username}",
   "trip": {
      "id": "${booking.trip.id}",
      "name": "${booking.trip.name}"
   }
}
</#escape>
.
Now we are almost there, go ahead and create the second webscript that lists the user’s bookings, define it in safari-rest/src/main/resources/webscripts/com/safari/travel/bookings.get.desc.xml:
<webscript>
   <shortname>List User's Bookings</shortname>
   <description>Lists the logged in user's bookings</description>
   <url>/travel/bookings</url>
   <format default="json">argument</format>
   <authentication>user</authentication>
</webscript>
Implement its Java controller in safari-rest/src/main/java/com/safari/rest/api/travel/BookingsGet.java:
package com.safari.rest.api.travel;

import com.safari.rest.api.SafariWebScript;
import org.springframework.extensions.webscripts.Cache;
import org.springframework.extensions.webscripts.Status;
import org.springframework.extensions.webscripts.WebScriptRequest;

import java.util.HashMap;
import java.util.Map;

public class BookingsGet extends SafariWebScript
{
   @Override
   protected Map<String, Object> executeImpl(WebScriptRequest req, Status status, Cache cache)
   {
      // Create the template model and fill it with trips
      Map<String, Object> model = new HashMap<String, Object>();
      model.put("bookings", getTravelService().getBookings());
      return model;
   }
}
Now create the response template that lists the bookings in safari-rest/src/main/resources/webscripts/com/safari/travel/bookings.get.json.ftl:
<#escape x as jsonUtils.encodeJSONString(x)>
[<#list bookings as booking>
   {
      "id": ${booking.id},
      "username": "${booking.username}",
      "trip": {
         "id": "${booking.trip.id}",
         "name": "${booking.trip.name}"
      }
   }<#if booking_has_next>,</#if>
</#list>]
</#escape>
Finally, since they are java backed webscripts, we need to defined them as spring beans in safari-rest/src/main/webapp/WEB-INF/config/web-application-context.xm, place them under the TripsGet webscript so the whole list of webscripts look like below:
<!-- Safari WebScripts / REST API calls (id follows pattern with "webscript." + package + webscript name + method -->
<bean id="webscript.com.safari.travel.trips.get"
      class="com.safari.rest.api.travel.TripsGet"
      parent="safariWebscript">
</bean>

<bean id="webscript.com.safari.travel.trip.post"
      class="com.safari.rest.api.travel.TripPost"
      parent="safariWebscript">
</bean>

<bean id="webscript.com.safari.travel.booking.post"
      class="com.safari.rest.api.travel.BookingPost"
      parent="safariWebscript">
</bean>

<bean id="webscript.com.safari.travel.bookings.get"
      class="com.safari.rest.api.travel.BookingsGet"
      parent="safariWebscript">
</bean>
Thats it, we now have 4 webscripts and the Safari REST API is complete. Now lets go ahead and test it the calls.

Testing you WebScripts

First build the entire project and redploy the rest api using:

mvn -f safari-root/pom.xml clean install org.codehaus.mojo:tomcat-maven-plugin:1.0-beta-1:redeploy
Now, to create a trip you will need to be logged in as admin and post a request with json on the request body. If you don’t want to write some javascript that does this you can probably test it from your IDE. If not, there are a great variety of tools, such as curl (command line) or RESTClient (a free FireFox addon that is simple to use).
.
What ever tool you choose create a request like below to create a trip:
POST http://localhost:8080/safari-rest/service/travel/trip
{
  "name": "Spring Safari"
}
If you hadn’t logged in do it with “roy”:”roy” and you shall get a response like below:
{
   "id": 4,
   "name": "Spring Safari"
}
To list all of the Safari company’s trips do a:
GET http://localhost:8080/safari-rest/service/travel/trips
That shall give  you a list where your new trip is included. To go further and create a booking make sure to clear your cookies so you’re not logged into the rest api. Then go ahead and make a request like below and login as “erik”:”erik”
POST http://localhost:8080/safari-rest/service/travel/booking
{
  "tripId": 4
}
Which shall return:
{
   "id": 0,
   "username": "roy",
   "trip": {
      "id": 4,
      "name": "Spring Safari"
   }
}
To later list erik’s bookings create a request like below:
GET http://localhost:8080/safari-rest/service/travel/bookings
Which shall return:
[
   {
      "id": 0,
      "username": "roy",
      "trip": {
         "id": 4,
         "name": "Spring Safari"
      }
   }
]
Ok, that’s it. Hopefully you have found this blog post interesting. Next post will be about creating a Safari web application using Spring Surf.

Spring Surf Development #1 – Creating a REST API with Spring WebScripts

.
.
A couple weeks ago I implemented 2 web applications and a REST API for a new open sourced BPM / BPMN 2.0 engine named Activiti. Feel free to visit http://www.activiti.org where you may download the distribution (Apache licensed) and play around with it. The actual BPM engine is just a simple jar file having the benefit of therefore being deployable in virtually any JAVA environment. The whole architecture for the REST API and web applications was built using the Spring Surf framework.
.
Spring Surf is the name of a collection of script centric frameworks used to create web applications (Spring Surf) and REST APIs (Spring WebScripts). The project page can be found at http://www.springsource.org/extensions/se-surf while documentation and downloads can be found at http://www.springsurf.org/.
.
I will write a series of blog posts introducing the Spring Surf framework by writing an example application for a “Safari” company. The application will be built using Maven and will contain of 4 modules:
.
  • safari-root – The root module containing documentation and from which the project is built.
  • safari-core – The core business logic packaged into a .jar file.
  • safari-rest – The Safari JSON REST API packaged as a deployable .war-file.
  • safari-shop – The Safari Web Shop packaged as a deployable .war file.
.
There will be 4 blog posts and they will be organized as below:
.
  1. Setting up the Maven project and creating the Safari REST API using Spring WebScript.
  2. Securing the REST API.
  3. Setting up the Safari Web Shop using Spring Surf.
  4. Securing the Safari Web Shop and connecting it with the REST API.

.

In this first post we will: install Maven, create a maven project with the root, core & rest modules, add  Spring WebScripts’ dependencies, install Tomcat and the tomcat-maven-plugin, create a service tier for the “Safari” company and create the Safari REST API. Now lets go ahead and do it.

Note that if you want the complete project you can download it from here: Safari – Spring WebScripts REST API sample and if you want to take a closer look at the Activiti source code to learn more about Spring WebScripts after this blog post you can do that here http://svn.codehaus.org/activiti/activiti/trunk/.

.

Install maven & setup the project structure

.

Visit http://maven.apache.org/download.html and download the latest Maven 2 release. When downloaded unzip it and make sure to set the following environment variables:

M2_HOME=<path-to-the-expanded-folder>
MAVEN_OPTS=-Xmx512m
Also make sure you set your PATH variable to include “${M2_HOME}/bin”.
.
Create a directory named “Safari” for your project and change into that directory with your terminal.
Then type the following to create the safari-root module.
mvn archetype:create \
    -DgroupId=com.safari \
    -DartifactId=safari-root \
    -DarchetypeArtifactId=maven-archetype-site-simple
You have now created a directory named “safari-root” that contains a pom.xml file which is where you instruct Maven on how to compile, package and deploy its modules. The safari-root module will not contain any source code but it may contain user documentation since we created it using the maven-archetype-site-simple archetype. To read further about Maven and archetypes please visit http://maven.apache.org/archetype/index.html.
Now lets go ahead and create the business logic module by typing:
.
mvn archetype:create \
   -DgroupId=com.safari \
   -DartifactId=safari-core \
   -DpackageName=com.safari.core \
   -DarchetypeGroupId=org.apache.maven.archetypes
…and a web module for the rest api by typing (note the archetype used is maven-archetype-webapp)….
.
mvn archetype:create \
   -DarchetypeGroupId=org.apache.maven.archetypes \
   -DarchetypeArtifactId=maven-archetype-webapp \
   -DgroupId=com.safari \
   -DartifactId=safari-rest
You have now created 2 new folders (safari-core & safari-rest) that shall be located next to the safari-root folder.
To make the new modules child modules to safari-root enter the following xml snippet in safari-root/pom.xml after the <properties> element:
<modules>
  <module>../safari-core</module>
  <module>../safari-rest</module>
</modules>
You also need to add the following xml snippet in your pom.xml files (located under safari-core & safari-rest) after their <url> elements:
<parent>
   <groupId>com.safari</groupId>
   <artifactId>safari-root</artifactId>
   <version>1.0-SNAPSHOT</version>
</parent>
Now lets try and build the whole project by typing:
mvn -f safari-root/pom.xml clean install
Since we have added safari-core & safari-rest as sub modules to safari-root they will also be built when building safari-root. And since we instructed maven to “install” the modules they will end up in your local repository which by default is located in the “.m2″ directory inside the current users home directory. Feel free to browse around in the repository and note that we have already created a .jar file (repository/com/safari/safari-core/1.0-SNAPSHOT/safari-core-1.0-SNAPSHOT.jar) & and a .war file (repository/com/safari/safari-rest/1.0-SNAPSHOT/safari-rest-1.0-SNAPSHOT.war). The “1.0-SNAPSHOT” is set by Maven automatically since we haven’t set the version ourselves and to reflect that we are just building the files for development rather than for a release.
.

Adding the Spring WebScripts runtime

.
So far we only have an empty project structure so lets go ahead and add the Spring WebScripts runtime to our project. First add the maven repository where the Spring WebScript releases are located by pasting the following snippet into safari-root/pom.xml after the <modules> element:
<repositories>
   <repository>
      <id>spring-extensions-milestone</id>
      <name>Spring Extensions Milestone Repository</name>
      <url>http://extensions.springframework.org/milestone</url>
   </repository>
</repositories>
Since the safari-rest module is dependent upon Spring WebScripts (which is dependent upon Spring) we need to instruct maven to download those dependencies. We also need to instruct maven to include the safari-core.jar file in the safari-rest.war file so we can use the business logic from the REST API. We do that by adding the following xml-snippet to safari-rest/pom.xml inside the <dependencies> element:
<!-- Make the Safari Core API available to our the safari REST API -->
<dependency>
   <groupId>com.safari</groupId>
   <artifactId>safari-core</artifactId>
   <version>1.0-SNAPSHOT</version>
</dependency>
<!-- Use servlet.jar for compiling but not for packaging since it will be provided by the server -->
<dependency>
   <groupId>javax.servlet</groupId>
   <artifactId>servlet-api</artifactId>
   <version>2.5</version>
   <scope>provided</scope>
</dependency>
<!-- Include the Spring Core Framework-->
<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-core</artifactId>
   <version>3.0.1.RELEASE</version>
</dependency>
<!-- Include the Spring WebScripts runtime -->
<dependency>
   <groupId>org.springframework.extensions.surf</groupId>
   <artifactId>spring-webscripts</artifactId>
   <version>1.0.0.M3</version>
</dependency>
<!-- Include the Spring WebScript API so we can browse and list our webscripts on the server -->
<dependency>
   <groupId>org.springframework.extensions.surf</groupId>
   <artifactId>spring-webscripts-api</artifactId>
   <version>1.0.0.M3</version>
</dependency>
To ensure Spring and Spring WebScripts are initialised we need to configure the safari-rest/src/main/webapp/WEB-INF/web.xml file to look like below. First we tell the Spring framework to start up and use the config file (WEB-INF/web-application-context.xml) and then we instruct the WebScriptServlet to handle all URL requests starting with “/service/*” which means that all REST API calls will start with “http://localhost:8080/activiti-rest/service/”. We also set the session timeout so we don’t need to worry about server having different default settings.
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
   <display-name>Safari REST API</display-name>

   <!-- Spring Application Context location -->
   <context-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/web-application-context.xml</param-value>
      <description>Spring config file location</description>
   </context-param>

   <!-- Spring Context Loader listener -->
   <listener>
      <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
   </listener>

   <!-- Spring WebScripts -->
   <servlet>
      <servlet-name>WebScriptServlet</servlet-name>
      <servlet-class>org.springframework.extensions.webscripts.servlet.WebScriptServlet</servlet-class>
   </servlet>

   <!-- The WebScript Servlet -->
   <servlet-mapping>
      <servlet-name>WebScriptServlet</servlet-name>
      <url-pattern>/service/*</url-pattern>
   </servlet-mapping>

   <!-- Session configuration -->
   <session-config>
      <session-timeout>30</session-timeout>
   </session-config>
</web-app>
Of course we also need to create the web-application-context.xml file inside the safari-rest/src/main/webapp/WEB-INF directory. Make sure it looks like the following so that the Spring WebScript runtime will get the framework configuration files it needs.
<?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">
   <!-- Import Spring WebScripts Framework config files-->
   <import resource="classpath*:org/springframework/extensions/webscripts/*-context.xml" />
</beans>
Now try and rebuild your project once again by running:
mvn -f safari-root/pom.xml clean install
If you have done everything correctly you should get a successful build. To confirm that the Spring WebScripts are properly setup we need to deploy it to a server. So lets go ahead and install Tomcat so we can deploy the safari-rest module to it…
.

Install and setup tomcat

.
Download the latest Tomcat 6 release from http://tomcat.apache.org/. To make things easy for us we will let maven handle the deployment to the Tomcat server by using the tomcat-maven-plugin. The tomcat-maven-plugin will use the Tomcat manager application to deploy our rest api, but when it does it expects
certain users and roles to be setup inside Tomcat. To add the required users and roles configure
${TOMCAT_HOME}/conf/tomcat-users.xml to look like below:
<?xml version='1.0' encoding='utf-8'?>
<tomcat-users>
   <role rolename="tomcat"/>
   <role rolename="manager"/>
   <user username="tomcat" password="tomcat" roles="tomcat,manager"/>
   <user username="admin" password="" roles="tomcat,manager"/>
</tomcat-users>
Now you may start your tomcat using ${TOMCAT_HOME}/bin/startup.sh (or startup.bat if your on windows).
To install the tomcat-maven-plugin you need to add the following xml snippet to safari-root/pom.xml after the <repositories> element:
<build>
   <plugins>
      <plugin>
      <groupId>org.codehaus.mojo</groupId>
      <artifactId>tomcat-maven-plugin</artifactId>
      <configuration>
         <url>http://localhost:8080/manager</url>
      </configuration>
      </plugin>
   </plugins>
</build>
Once this is done you’re ready to deploy the safari-rest module by invoking:
mvn -f safari-root/pom.xml clean install org.codehaus.mojo:tomcat-maven-plugin:1.0-beta-1:deploy
If you have done everything correctly you can now point your browser to http://localhost:8080/safari-rest/service/index which will show the “dashboard” for the Spring Webscripts environment. As you know we haven’t created any webscripts of our own but the pages that you are accessing are actually webscripts that return html (instead of JSON) and gives the user the possibility to browse, reload and test webscripts. If you for example click the “Browse by Web Script URI” link you will see all the URL’s that are possible to invoke. Once you have configured webscripts of your own they will also show up in this list.
.
It is also useful to pay attention to the “Refresh Webscripts” button, if you make changes in your webscripts and copy them over to a web application deployed as an exploded directory you may reload the Spring WebScript runtime by clicking this button, so the changes are picked up without you needing to restart the server.
.

Add some business logic

.
Before we write our webscripts we will create a simple service class and POJO for the core api so we can list the trips that the Safari company offers. First create a directory named “travel” in safari-core/src/main/java/com/safari/core and then create Trip.java file like below in the safari-core/src/main/java/com/safari/core/travel directory.
package com.safari.core.travel;

public class Trip {

 private int id;
 private String name;

 public Trip (int id, String name) {
    super();
    this.id = id;
    this.name = name;
 }

 public int getId(){ return id; }
 public void setId(int id) { this.id = id; }

 public String getName() { return name; }
 public void setName(String name) { this.name = name; }

}
Then go ahead and create a TravelService.java like below in the same directory.
package com.safari.core.travel;

import java.util.ArrayList;
import java.util.List;

public class TravelService {

   public List<Trip> getTrips() {
      List<Trip> trips = new ArrayList<Trip>();
      trips.add(new Trip(1, "Masai Mara Adventurer"));
      trips.add(new Trip(2, "Serengeti Explorer"));
      trips.add(new Trip(3, "Kruger Wildlife"));
      return trips;
   }

}
Finally make sure we make Spring manage the service class by adding the following xml snippet in
safari-rest/src/main/webapp/WEB-INF/web-application-context.xml after the <import> element:
<!-- Safari Services -->
<bean id="travelService" class="com.safari.core.travel.TravelService"/>
Now if you try to compile this you will probably get an error complaining about the use of generics requiring a Java source level of 5 or higher. To change the compilation source level in maven add the following xml snippet in safari-root/pom.xml inside the <plugins> element (which is inside the <build> element).
<plugin>
   <artifactId>maven-compiler-plugin</artifactId>
   <configuration>
      <source>1.5</source>
      <target>1.5</target>
   </configuration>
</plugin>
Lets try to compile this so we know we are on top of things by invoking the following command:
mvn -f safari-root/pom.xml clean install
If the compilation worked for you, you are ready for the actual REST api development.
.
.

Writing our first webscript (our first REST API service)

.
First let’s explain really quickly what a webscript actually is. A webscript can be described as a set of files that together form a component that receives a request, reacts to it by invoking some logic (using Java, JavaScript, Groovy) and afterwards returns a response in a suitable format (JSON, HTML, XML, Atom or whatever format that is suitable). In other words each REST API call will be implemented as a separate webscript.
A webscript MAY consist of the following files:
.
  • trips.get.desc.xml - The webscript descriptor in xml format, telling us which URL:s that shall be handled by this webscript
  • trips.get.js – The logic written in JavaScript (note the .js-suffix) that loads the trips and prepares it for the response template
  • trips.get.json.ftl - The response template that written as Freemarker template (note the “.ftl” suffix) returning JSON data (note the “.json”)
  • trips.get.config.xml – An xml configuration file
  • trips.get.properties – The default i18n messages ready for use by the JavaScript logic and the response templates
  • trips.get.sv_SE.properties – Language & Locale specific i18n messages.
As you can see all the files have similar file names, this is because Spring WebScripts uses naming convention instead of configuration to indicate which files that shall “grouped” together and become a webscript component. The “.get” part of the file names indicate that this webscript shall be invoked only if the URL was called using the GET HTTP method. In other words if somebody would POST an html address form towards a URL that is handled by a webscript that webscript would be named something like address.post.desc.xml.
It is common practice to use the following pattern when implementing a REST API:
.
  • GET – used for retreiving data
  • POST – Used for creating new data
  • PUT – Used for updating existing data
  • DELETE – Used for deleting data
In our case we will choose a slightly different path than the one described as above when implementing our first webscript. To make the example really simple we will neither use the .get.properties or the config.xml files and we will use Java instead of JavaScript for the logic. Our webscript will then consist of the following 3 files:
.
  • trips.get.desc.xml
  • TripsGet.java
  • trips.get.json.ftl
First we create the trips.get.desc.xml descriptor as described below in the
safari-rest/src/main/resources/webscripts/com/safari/travel directory (Note that you will need to create the directories). The descriptor will get picked up by the webscripts runtime and the webscript will be registered to handle GET HTTP Requests against the “/travel/trips” url. In other words to invoke this webscript you would need to type http://localhost:8080/safari-rest/service/travel/trips in your web browser’s address bar.
<webscript>
  <shortname>Trips</shortname>
  <description>Lists all trips</description>
  <url>/travel/trips</url>
  <format default="json">argument</format>
</webscript>
Then we implement the logic by creating a TripsGet.java as below in the safari-rest/src/main/java/com/safari/rest/api/travel directory (again you will need to create the directories, all the way form the “main” directory).
package com.safari.rest.api.travel;

import com.safari.rest.api.SafariWebScript;
import org.springframework.extensions.webscripts.Cache;
import org.springframework.extensions.webscripts.Status;
import org.springframework.extensions.webscripts.WebScriptRequest;

import java.util.HashMap;
import java.util.Map;

public class TripsGet extends SafariWebScript
{
   @Override
   protected Map<String, Object> executeImpl(WebScriptRequest req, Status status, Cache cache)
   {
      // Create the template model and fill it with trips
      Map<String, Object> model = new HashMap<String, Object>();
      model.put("trips", getTravelService().getTrips());
      return model;
   }
}
The executeImpl method serves as an entry point for Java backed webscript so this is where the logic shall be defined. Looking at the code you can see that it creates a Map named model, this is where the logic places all the object that the response template shall render. Since this webscript is a webscript responding to a GET HTTP request it doesn’t perform any updates against the core api, but if it would, this would be the place to do it.
What you can see however is that it uses a method called getTravelService() and that it extends another class named SafariWebScript.
.
SafariWebscript is our base webscript class suitable for helper methods and easy access to services and other resources. Therefore, make sure you also create SafariWebScript.java as below, in the safari-rest/src/main/java/com/safari/rest/api directory. As you can see this class extends the DeclarativeWebScript class which is required for Java backed webscripts.
package com.safari.rest.api;

import com.safari.core.travel.TravelService;
import org.springframework.extensions.webscripts.*;

public class SafariWebScript extends DeclarativeWebScript {

   protected TravelService travelService;

   public void setTravelService(TravelService travelService) {
      this.travelService = travelService;
   }

   public TravelService getTravelService() {
      return this.travelService;
   }

}
Now we will render the response in JSON format using Freemarker. To do that create trips.get.json.ftl in the same directory as the descriptor file (safari-rest/src/main/resources/webscripts/com/safari/travel).
<#escape x as jsonUtils.encodeJSONString(x)>
[
   <#list trips as trip>
   {
      "id": ${trip.id},
      "name": "${trip.name}"
   }<#if trip_has_next>,</#if>
   </#list>
]
</#escape>
The <#escape> tag is a Freemarker helper utility for making sure the json attribute values are escaped.
The <#list> tag makes the Freemarker iterate over the trips so each one can be rendered.
The <#if> tag uses the Freemarker “_has_next” built in to decided if a “,” shall be rendered depending on if its the last trip or not. To read more about Freemarker visit http://freemarker.sourceforge.net/
Now we only have one last thing to do before we can deploy the REST API to the server.
.
When writing webscripts using JavaScript for the logic we would have been ready by now, but since we are writing Java backed webscripts in which the Java classes actually are Spring beans, we need to define the Java classes/beans in our Spring configuration file. Add the following xml snippets in the bottom of safari-rest/src/main/webapp/WEB-INF/web-application-context.xml
<!-- Safari WebScript base helper class -->
<bean id="safariWebscript"
      class="com.safari.rest.api.SafariWebScript"
      parent="webscript">
   <property name="travelService" ref="travelService"/>
</bean>

<!-- Safari WebScripts / REST API calls -->
<bean id="webscript.com.safari.travel.trips.get"
      class="com.safari.rest.api.travel.TripsGet"
      parent="safariWebscript">
</bean>
Note the bean id for the TripsGet class. It follows a naming convention used to let the framework understand that this Java bean is part of the “trips.get”-webscript component. First it must start with “webscript.”, the next part shall be the package name (“com.safari.travel”) followed by a dot (“.”) and the webscript name (“trips”) followed by a dot (“.”) and the http method (“get”). Note also that this where we instruct Spring to always insert the TravelService into our webscript base helper class. In case you’re wondering where the parent bean “webscript” is defined you can find it in one of the many Spring WebScript framework config files that you imported a couple of rows up in this file, namely spring-webscripts-application-context.xml.
.
Now you are ready to redeploy your REST API which now contains your first REST API services. Do that by invoking the following command (note the “redeploy” instead of the “deploy” in the end):
mvn -f safari-root/pom.xml clean install org.codehaus.mojo:tomcat-maven-plugin:1.0-beta-1:redeploy
You are now ready to invoke your first webscript by pointing your browser to http://localhost:8080/safari-rest/service/travel/trips where you should get the available trips as a result. Note that your browser might open the result in a text editor or external program.
.
Hopefully you have found this post interesting and can see that Spring WebScripts are an exciting development alternative for rapidly implementing REST API’s. The next part, about securing the REST API, will be posted in roughly 6 weeks.
.