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

November 9th, 2011 by Erik Winlöf

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

October 12th, 2011 by Erik Winlöf

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

November 23rd, 2010 by Erik Winlöf
.
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

October 15th, 2010 by Erik Winlöf
.
.
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

June 4th, 2010 by Erik Winlöf
.
.
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.
.

Alfresco Home | Legal | Privacy | Accessibility | Site Map | RSS  RSS

© 2012 Alfresco Software, Inc. All Rights Reserved.