Tag: Web Scripts

Book Review: Learning Alfresco Web Scripts

Packt Publishing recently sent me a copy of Learning Alfresco Web Scripts by Ramesh Chauhan to review in exchange for my thoughts on the book which I’ll share with you now…

Web Scripts are an essential part of Alfresco. If you are extending or customizing the platform and you have time to learn only one thing about it, web scripts might very well be that thing. The reason is that they are key to so much else you might want to do such as integrating Alfresco with a third party system or customizing Alfresco Share, which is, at its core, comprised of web scripts.

Most technical books on Alfresco give some attention to web scripts, but this one dives into the details. After reading it, you’ll know how to do simple things with web scripts and you’ll have some idea of how to do more complex work beyond understanding the Model-View-Controller basics.

While the comprehensiveness is a good thing, I think it also presents a bit of a challenge which comes down to this: Who is this book for? If it is for beginners, it needs a lot more examples and could cut back on a lot of the technical detail. If it is for experts, point to existing sources for the basics and drill deeper on the interesting technical topics.

I did see a couple of bad practices in the book. First, in the chapter on Java-backed web scripts (Ch. 6) the author provides an example Spring configuration XML file that injects the lower-cased Alfresco beans into the web script class. This may sound like a trivial nitpick, but it’s actually a big no-no that I see repeatedly. If you have good reason for using the unsecured, internal-only, lower-cased beans, explain it. Otherwise, stick to the public, secured, upper-cased beans so that beginners don’t pick up a bad habit.

Similarly, there is a part that discusses where web script files can live in the Alfresco WAR. The author does point out that the extension directory is “preferred” but I don’t think this is worded strongly enough. Those other locations should have been left out entirely or maybe it could have said, “Place your files only in the extension directory. While these other locations may technically work, you should never use them.”

I was happy to see the chapter on the Maven SDK and the discussion of AMPs. And I think putting the Eclipse details later was a good idea, as one of the features of web scripts is that you don’t have to use Java or an IDE of any kind if you don’t want to. The book doesn’t cover Surf, Aikau, or Share customization in any detail, and I think that was also a good decision as those areas are too fluid at the moment and because the singular focus on web scripts is one of the book’s assets.

Overall, Learning Alfresco Web Scripts is a very thorough and comprehensive treatment of an important technical topic for Alfresco developers.

Updated tutorial: Introduction to Alfresco Web Scripts

I have published a new version of my Introduction to Web Scripts tutorial. This is a major revision that refactors the tutorial to leverage the Alfresco Maven SDK and AMPs. In addition, I have done a little bit of reorganization to improve clarity and a lot of wordsmithing to make the tutorial more consistent with the others in the Alfresco Developer Series.

By the end of this tutorial, you will know how to:

  • Extend Alfresco with your own custom RESTful API.
  • Write web scripts that respond to GET, POST, and DELETE requests over HTTP/S and return data in both HTML and JSON.
  • Use the web scripts console to display documentation and debug info on your custom and out-of-the-box web scripts.
  • Make AJAX calls to your custom web scripts.

The tutorial assumes you already know how to use the Alfresco Maven SDK. If you don’t, take a look this tutorial.

The tutorial text and all of the source code related to it are on GitHub. If you see problems or opportunities for improvement, please fork the project and send me a pull request.

Tip: How to move an Alfresco category

Alfresco has a category hierarchy that can be used to categorize content. Unlike tags, which are not in a hierarchy and can be created by anyone, categories must be managed by an administrator. But even an administrator cannot move a category from one parent to another through the user interface. When it is time to reorganize your categories, how do you do it?

It’s actually pretty easy to do this with code. Your first thought might be, “Well, a category is a node and server-side JavaScript can work with nodes, so I’ll just write a quick script to do the move,” and who could blame you–that’s a well-reasoned line of thinking. But if you look at the source for the current JavaScript API ScriptNode object’s “move” method, you’ll see that it assumes that the association it is creating is “cm:contains”. Categories are associated with an association called “cm:subcategories”, not “cm:contains”. So, at least in this release, JavaScript can’t help us.

Luckily, Alfresco’s Java API is up to the task. When you call the NodeService’s moveNode method you have full control over the new relationship that gets created.

For stuff like this it is often handy to whip up a little Java-backed web script. All the web script needs is the source node reference that needs to be moved and a target node reference to move the source node reference to. Here’s the class with debug and parameter checking removed:

public class MoveCategory extends DeclarativeWebScript {

  // Dependencies
  private NodeService nodeService;

  @Override
  protected Map<String, Object> executeImpl(WebScriptRequest req, Status status) {

    final String sourceNodeRefString = req.getParameter("sourceNodeRef");
    final String targetNodeRefString = req.getParameter("targetNodeRef");

    // snag the nodes
    NodeRef sourceNodeRef = new NodeRef(sourceNodeRefString);
    String sourceName = (String) nodeService.getProperty(
                                   sourceNodeRef,
                                   ContentModel.PROP_NAME);
    NodeRef targetNodeRef = new NodeRef(targetNodeRefString);
    String targetName = (String) nodeService.getProperty(
                                   targetNodeRef,
                                   ContentModel.PROP_NAME);

    // move the source node to the target
    nodeService.moveNode(sourceNodeRef,
                         targetNodeRef,
                         ContentModel.ASSOC_SUBCATEGORIES,
                         QName.createQName(
                           NamespaceService.CONTENT_MODEL_1_0_URI,
                           sourceName));

    // set up the model
    Map<String, Object> model = new HashMap<String, Object>();
    model.put("sourceNodeRef", sourceNodeRefString);
    model.put("sourceName", sourceName);
    model.put("targetNodeRef", targetNodeRefString);
    model.put("targetName", targetName);

    return model;
  }

  public void setNodeService(NodeService nodeService) {
    this.nodeService = nodeService;
  }

}

If you need to see how to wire up the web script with Spring configuration, or you want to see the rest of the web script files (like the descriptor & view), you can take a look at this zipped up Eclipse project.

I should mention that in my web script descriptor, I configured the web script to require an authenticated user, but I set it to run as admin. That gives non-admin users the ability to move categories around using this web script. You may or may not want to do that.

Also, because this is just an example, I’m using the zip overlay method of deployment. For production, you should be using AMPs.

For more Java-backed web script examples, see this wiki page. And if you would prefer to use Maven to develop your Java-backed web scripts, take a look at this project on Google Code.

Improving Alfresco Share performance by using getChildren

I had a client that was seeing response time in the neighborhood of several seconds for the Alfresco Share document library and data list pages across all of his sites. The client’s Share install had just over 1,000 Share sites. The volume of the data lists in each site was insignificant. This is the story of how we resolved the issue, but note that the resolution may not be appropriate for everyone in all cases.

The Symptoms

The client was seeing slow performance of the document library and data list pages in Share. They noticed that the folder tree in the document library view responded quickly but the actual document list itself took a long time to render. This was happening for all users in all sites.

Looking for a quick resolution, even if that meant solving the symptom but not necessarily the underlying cause, we decided to see if we could optimize the repository tier web script that returns the document library contents to see if we could get it to perform a little closer to what we were seeing with the folder tree. I made a copy of the repository tier’s doclist.get web script into our project’s extension directory and started tweaking.

The Bunny Trail

First, I’ll fess up to a mistaken assumption I had: I thought that everything in Alfresco always went through Lucene. I knew that separating out full-text index searches and property searches into Lucene queries and DB queries, respectively, was on the roadmap, but I had it in my head that in 3.4, even a call to something like ScriptNode.getChildren() was ultimately a Lucene index hit. If everything is a Lucene hit, I figured, there had to be a different reason for the folder list control to perform so much better than the document library list.

So, instead of starting with what, in hindsight, would have yielded the most bang for the buck, I started tuning what turned out to be little things. For example, our app didn’t use favorites, so I removed any references to the preferences service. Our app didn’t allow users to check out documents so out went any logic that dealt with that. Goodbye, Google Docs code blocks. Adios, type and aspect checking for types and aspects our app doesn’t support. Farewell, filters. I hardcoded permissions to avoid the lookup. I set created by and modified by values to empty strings to avoid the lookup to the person object. I jettisoned anything that wasn’t crucial to simply producing a list of the folder contents. All of this did speed up the repository-tier web script, but only a little bit. I needed an order of magnitude improvement.

The Lightbulb

Next, I did what I should have done initially: Add some simple log statements to see which part of the code was taking the longest to execute. Of course, it was the query. As it turns out, it is much, much faster to ask a node for its children (which is what the treenode web script does) than it is to do a Lucene search with a PARENT clause that yields the same result set (which is what the doclist web script does). On a dev machine with a small dataset, you don’t notice the difference. But on our integration and prod servers the difference is huge.

The Fix

The Share document library page uses a YUI data table to produce the list of documents for the currently selected folder. The data table is bound to a web script that lives on the repository tier that is responsible for returning the requested data as JSON. Out-of-the-box, the repository tier web script that returns the document list calls a function called getFilterParams which is responsible for setting up a bunch of query predicates based on the document library filter the user has selected in the Share UI. The script then asks the filterParams object for the Lucene query it needs to run to return the document list. It then uses the search service to invoke the query and return the results.

My optimization was to bypass building and executing the query completely because, in our case, we don’t care about filters. All we want is the list of children in the current folder, and ScriptNode already has a function to do that called getChildren. So instead of performing a Lucene search, we ask the current “root node” for its children. We then iterate over the results and filter out a couple of content types that otherwise would have been excluded had we used the Lucene query instead of getting all children.

Oh man, that did it. The document library went from rendering in 6+ seconds to rendering in less than 1 second.

I gave the data lists web script the same treatment. In that case, our customized Share app still makes use of filters, so the “getChildren bypass” is only used when the “All” filter is selected. When any other filter is selected the original out-of-the-box Lucene query is used.

Now, again, I completely acknowledge that we may have succeeded in speeding up performance for those two cases, but failed to resolve the underlying issue, and addressing that may result in a system-wide performance boost, but it was good to get the quick fix in place and it should be easy enough to revert if and when we resolve the underlying index issue, if one exists.

Here’s a code snippet from the custom doclist.get.js controller if you are curious:


if (parsedArgs.path == "")
{
    parentNode = parsedArgs.rootNode;
}
else
{
    parentNode = parsedArgs.rootNode.childByNamePath(parsedArgs.path);
}      
// We are iterating over the parent node's children instead of iterating
// over search results... 
for each (node in parentNode.getChildren())
{
   try
   {
      // ...so we need to filter out some system types that would have otherwise been
      // filtered out by the lucene query
      if (node.typeShort == "cm:systemfolder" || node.typeShort == "cm:thumbnail")
      {
         // do nothing. we don't want these.
      }
      else if (node.isContainer || node.typeShort == "app:folderlink")
      {
         folderNodes.push(node);
      }
      else
      {
         documentNodes.push(node);
      }
   }
   catch (e)
   {
      // Possibly an old indexed node - ignore it
   }
}

Book Review: Alfresco 3 Web Services

I’ve just finished reading Alfresco 3 Web Services, by Ugo Cei and Piergiorgio Lucidi, which Packt sent me to read and comment on.

Alfresco 3 Web Services is meant for developers with a very specific focus: Remotely talking to Alfresco. That might mean communicating via SOAP-based Web Services, RESTful web services (Web Scripts), or either protocol through CMIS. Whichever you choose, Ugo and Piergiorgio have you covered. I really like that the authors set out to write such a focused piece and stuck to it–it allows them to go deep on their topic and keeps the chapter length and total book length digestable.

The book is written very clearly and follows a logical progression. It starts out with SOAP-based services (first 5 chapters), including a chapter on .NET, and then moves on to web scripts (3 chapters). After that, the authors discuss AtomPub, which is an interesting, but not critical, background for the remaining 5 chapters of the book which focus on CMIS.

The discussion on web scripts covers both Java and JavaScript controllers, but JavaScript definitely gets more attention. Something developers new to the platform will find helpful is the chapter on FreeMarker. Most of Alfresco’s documentation and the books that are out there (including mine) relegate the FreeMarker API to appendices or just show examples and assume you’ll look up details later when you need it. Because it is typically such a key component of web scripts and other aspects of Alfresco, it was a good call to include it in the book.

The book comes with several small examples and a couple of tie-it-together examples. None of it runs out-of-the-box without some tweaking which isn’t a surprise given how rapidly things are changing in this area, particularly with regard to CMIS. At the time the book was written, OpenCMIS, the Java API for CMIS available as part of Apache Chemistry, had not been officially released. As I write this, the Chemistry team is about to release their second tagged build. The differences aren’t significant enough to cause confusion–most readers will be able to fidget with the import statements and the other changes required to get the sample code running.

I thought there was a little too much time devoted to SOAP-based Web Services, but that’s just personal preference and the fact that nearly all of my clients over the last four years have gone the RESTful route. The authors note that others may have the same preference and they make it easy to skip over those chapters if the reader wants to.

Although the chapters logically progress and build on each other, the code samples don’t–for the most part, each chapter’s code samples are self-contained. For example, I thought it was kind of strange that the code built in Chapter 13 for the CMIS Web Services binding examples isn’t used in Chapter 14 to build the CMIS Wiki example.

Many Alfresco implementation teams are divided into at least back-end and front-end teams and when the project is large enough, you can definitely have people focused on the middle. This book is perfect for that middle-ware team or for anyone who’s got a handle on the back- and front-ends and just needs to learn how to stitch them together. Nice job, Ugo and Piergiorgio.

Spring, Roo, and Alfresco Too: What Alfresco Gave to Spring and Why

Surf Logo

You’ll recall from my community event takeaways post in November that Alfresco announced plans around Surf, the Apache license, and Spring but the details were foggy at the time. This week, Alfresco and SpringSource announced that Surf, Web Scripts, and Web Studio have been donated to the Spring open source community under the Apache 2.0 license.

What is Surf?

Surf is a lightweight web application development framework. At a very high-level, Surf is essentially Alfresco Web Scripts (an MVC framework for binding URLs to server-side JavaScript/Java and Freemarker-based views) plus some page layout constructs and some built-in objects for connecting to and authenticating with remote HTTP end points, including Alfresco (See also “Alfresco UI Options” and “Surf Code Camp“).

Why Spring Surf Makes Sense

Alfresco’s team collaboration application, Alfresco Share, is built on top of the Surf framework and clients and partners, including Optaros, have built solutions on top of Surf. But so far, our experience has been that we probably could have built solutions faster using a different framework. One of the reasons is because you often can’t do everything you need to with Surf alone–it lacks services that would normally be provided by a broader framework. Your choice is either to re-create what’s missing or bolt on something that exists. So that’s the first reason why Surf becoming part of Spring makes sense. Spring is already a mature and widely-adopted framework. It’s much better to make Surf and Web Scripts part of an established framework (and community) than to try to grow Surf into a full-featured framework.

The second reason is more strategic. Alfresco sees a future dominated by CMIS (See “Getting Started with CMIS“). They want to be the go-to CMIS platform. From a repository perspective, they’ve been very active on this front. But development tools are going to be important, and although part of the beauty of CMIS is that it is tool-agnostic, I think SpringSource and Alfresco would obviously be pleased if their framework became a very natural and productive way to build CMIS apps.

Third, Alfresco doesn’t necessarily want to spend a lot of time on tools and frameworks if it doesn’t have to. Look at how much time Web Studio has languished in Community limbo–it’s clearly not a priority. If Surf catches on in the broader Spring community maybe Web Studio has a chance to turn into something. My guess is that SpringSource would prefer all development to take place within STS, its Eclipse-based IDE. Maybe Web Studio will get sucked into that somehow.

So what is Roo?

One of the things mentioned as part of the Spring Surf announcement is that Spring Roo integration is included. Spring Roo is pretty new so you might be wondering what that is. It’s pretty cool, actually. Basically, it’s a productivity tool for people who are building Spring apps. If you’ve ever worked with frameworks like Ruby on Rails, Grails, or Django, one of the first things you learn is how to use the command-line project scaffolding tools. Those tools make it easy for you to spin up and configure your project. Spring Roo is similar–it gives you a shell and a bunch of commands for things like setting up persistence, adding unit tests, and configuring security.

Spring Roo is extensible which is where Surf comes in. Let’s say you’ve created a Spring project and you want to use Surf as part of that project. All you have to do is go into your Roo shell and type “surf addon install”. No monkeying with the web.xml. No hunting for JAR files. It just happens. Next, suppose you want to add some Surf pages. Type “surf page create –id ‘SomeOtherPage’ –templateInstance home” and the XML is created for you in the right place (yes, the shell provides keyboard assist and hints so you don’t have to remember those commands).

Roo is definitely better appreciated by seeing it or trying it yourself. Michael Uzquiano did a short screencast showing the Spring Roo Surf extension. If you want to try Roo out yourself, go through Ben Alex’s “Getting Started with Spring Roo” posts.

Learn More

The bottom-line is that Surf becoming part of the Spring community is a good thing. You should check it out. The official Spring Surf page is the place to start. That’s where you’ll find the SVN URL, binary downloads, and links to other resources. There’s also going to be a webinar in January if you want to learn more.

Keeping your Alfresco web scripts DRY

One of my teammates thoroughly drenched our under-the-desk development server with a giant cup of coffee once. Somehow, disaster was avoided, although the client’s carpet had a nice coffee-stain outline of the server’s footprint long after the app rolled out which afforded the rest of us endless opportunities to mercilessly haze the clumsy coder.

Keeping your Alfresco web scripts DRY is actually not about making your developers use spill-proof travel mugs, or better yet, virtual machines. DRY is a coding philosophy or principle that stands for Don’t Repeat Yourself. There are all kinds of resources available that go into more detail, and the principle is more broad than simply avoiding duplicate code, but that’s what I want to focus on here.

There are three techniques you should be using to avoid repeating yourself when writing web scripts: web script configuration, JavaScript libraries accessed via import, and FreeMarker macros accessed via import.

Web Script Configuration

Added in 3.0, a web script configuration file is an XML file that contains arbitrary settings for your web script. It’s accessible from both your controller and your view. Building on the hello world web script from the Alfresco Developer Guide, you could add a configuration script named “helloworld.get.config.xml” that contained:


<properties>
<title>Hello World</title>
</properties>

You could then access the “title” element from a JavaScript controller using the built-in E4X library:

var s = new XML(config.script);
logger.log(s.title);

And, you could also grab the title from the FreeMarker view:

<title>${config.script["properties"]["title"]}</title>

The web script configuration lets you separate your configuration from your controller logic. And because you can get to it from both the controller and the view, you don’t have to stick configuration info into your model.

This example showed a script-specific configuration, but global configuration is also possible. See the Alfresco Wiki Page on Web Scripts for more details.

JavaScript Import

If your web script controllers are written in JavaScript, at some point you will find yourself writing JavaScript functions that you’d like to share across multiple web scripts. A common example is logic that builds a query string, executes the query, and returns the results. You don’t want to repeat the code that does that across multiple controllers. That’s what the import statement is for.

The syntax is easy:

<import resource="classpath:alfresco/extension/scripts/status.js">

This needs to be the first line of your JavaScript controller file. This example shows a JavaScript library being imported from the classpath, but you can also import from the repository by name or by node reference. See the Alfresco JavaScript API Wiki Page for more details.

FreeMarker Import

Of the three this is the one I see ignored most often. Let’s take the Optaros-developed microblogging component for Share. Its basic data entity is called a “Status” object. So web scripts on the repository tier return JSON that might have a single Status object or a list of Status objects. That’s two different web scripts and two different views, but the difference between a list of objects and a single object is really just the list-related wrapper–in both cases, the individual Status object JSON is identical. I’ve seen people simply copy-and-paste the FreeMarker from the single-object template to the list template and then just wrap that with the list markup. That’s bad. If you ever change how a Status object is structured, you’ve got to change it in (at least) two places. (Or it’ll be someone that comes after you that has to do it which makes it even worse. If you aren’t following the analogy, your redundant code is the coffee stain).

Instead of duplicating the logic, use a FreeMarker import and define a macro that formats the object. Then you can call the macro any time you need your object formatted. Here’s how it works for Status.

A FreeMarker file called “status.lib.ftl” contains the macros that format Status objects. It lives with the rest of the web script files and looks like this:


<#assign datetimeformat="EEE, dd MMM yyyy HH:mm:ss zzz">
<#--
Renders a status node as a JSON object
-->
<#macro statusJSON status>
<#escape x as jsonUtils.encodeJSONString(x)>
{
"siteId" : "${status.properties["optStatus:siteId"]!''}",
"user" : "${status.properties["optStatus:user"]!''}",
"message" : "${status.properties["optStatus:message"]!''}",
"prefix" : "${status.properties["optStatus:prefix"]!''}",
"mood" : "${status.properties["optStatus:mood"]!''}",
"complete" : ${(status.properties["optStatus:complete"]!'false')?string},
"created" : "${status.properties["cm:created"]?string(datetimeformat)}",
"modified" : "${status.properties["cm:modified"]?string(datetimeformat)}"
}
</#escape>
</#macro>
<#--
Renders a status node as HTML
-->
<#macro statusHTML status>
SiteID: ${status.properties["optStatus:siteId"]!''}<br />
User: ${status.properties["optStatus:user"]!''}<br />
Message: ${status.properties["optStatus:message"]!''}<br />
Prefix: ${status.properties["optStatus:prefix"]!''}<br />
Mood: ${status.properties["optStatus:mood"]!''}<br />
Complete: ${(status.properties["optStatus:complete"]!'false')?string}<br />
Created: ${status.properties["cm:created"]?string(datetimeformat)}<br />
Modified: ${status.properties["cm:modified"]?string(datetimeformat)}<br />
</#macro>

There’s one macro for JSON and another for HTML. It still bothers me that the same basic structure is repeated, but at least they are both in the same file and it feels better knowing that this is the one and only place where the data structure for a Status object is defined.

The FreeMarker that returns Status objects as JSON resides in status.get.json.ftl and looks like this:

<#import "status.lib.ftl" as statusLib/>
{
"items" : [
<#list results as result>
<@statusLib.statusJSON status=result />
<#if result_has_next>,</#if>
</#list>
]
}

You can see the import statement followed by the JSON that sets up the list, and then the call to the FreeMarker macro to format each Status object in the list.

When someone posts a new Status, the new Status object is returned. Rather than repeat the JSON structure for a Status object, the status.post.json.ftl view simply calls the same macro that got called from the GET:

<#import "status.lib.ftl" as statusLib/>
{
"status" : <@statusLib.statusJSON status=result />
}

Now if the data structure for a Status object ever needs to change, it only has to be changed in one place.

Don’t Repeat Yourself

Take a look at your web scripts. Eliminate your duplicate code. And keep a lid on your mocha frappuccino.

Webinar: Developer’s Intro to Alfresco Part 3: Web Scripts & Surf

Part 3 of the “Developer’s Introduction to Alfresco” webinars is today at 12 Eastern (GMT – 5). I’ll be talking about web scripts, Surf, and CMIS. The format will be the same as the first two parts: you’ll watch a pre-recorded presentation for about 30 minutes, then I’ll do at least a 30 minute live Q&A session.

Also, I know I’ve been slow in getting this posted, but the Part Two presentation and Q&A recordings are available at Alfresco’s on-demand events page.

Alfresco Surf Code Camp: Do-It-Yourself

Did you miss the Alfresco Surf Code Camps? I’ve got you covered. With Alfresco’s blessing (they wrote most of the content, after all) I’ve uploaded the Optaros Alfresco 3.0 Surf Code Camp instructor presentations and class labs to slideshare.net.

You might start by looking at the agenda to get an idea of the order you should progress through the lecture and labs. Then, move on to the introduction. Use the agenda to guide you through the rest.

The labs will be a little bit more painful than they were in-person. That’s because for the in-person camps, we used a virtual machine image that had everything pre-installed. For the DIY Code Camp, you’ll need to set this up yourself. To approximate what was on the image:

  1. Install the Alfresco-Tomcat bundle.
  2. Install a second Tomcat instance. This will be your Surf tier.
  3. Build a fresh Surf war. It’s in the “web framework” project in the source code. It will produce a WAR called alfwf.war.
  4. The labs will refer to “assets.zip”. I had an assets.zip file for just about every lab. For this setup, I’ve just got one zip, which is the entire solution source available for download. So when you see that, you’ll have to pick through the solution to find the file dependencies. Sorry.

The image we used for the class ran on 3.0 Labs from head circa mid-November. I believe people have had success running on 3.0 Enterprise. I haven’t tested on Labs 3 Stable. If someone tries it please post a comment here to let us know your degree of success.

The Code Camp doesn’t cover Web Studio. I’ll leave that up to someone else–I’ll be happy to link to it.

UPDATE: Making you find your own dependencies for the labs was lame. I had a few extra minutes so I pulled them into a Code Camp Assets file organized by lab/walkthrough. Now you’ve got no excuse.

Running Alfresco web scripts as Liferay portlets

I’ve seen a lot of Liferay and Alfresco forum posts from people having trouble getting Alfresco running within a Liferay portal. Once that’s done, people usually want to invoke Alfresco web scripts as portlets without requiring a separate single sign-on (SSO) infrastructure. Some people have pointed to the Alfresco wiki (Deploying 2.1 WAR Liferay 4.3). That is a helpful reference but it isn’t the full story. Here are some notes that may help.

1. Download the Liferay 4.3.6 + Tomcat 5.5 JDK5 bundle. I had mixed results with the latest release 4.4.2. You may be tempted to try to download the WAR-only distribution and configure it in your existing Tomcat instance. In this case, save yourself the time and headache and get the bundle. Fool with the WAR distribution later.

2. Unpack the Liferay distribution and fire it up. Make sure you can log in as the test@liferay.com (password: test) user to validate that all is well with the Liferay install.

2a. Create a test user. (“Create Account” on the Liferay login screen). Remember the email address. This will matter shortly. For this discussion I’ll assume Foo User with a screen name of fuser and an email address of fuser@foo.com. Make sure you create a home directory. In this example, we’ll call it “fuser”.

2b. Verify that you can log in as your test user.

3. Shut down the server.

4. Download Alfresco 2.1.2 Enterprise, WAR only. Alfresco 2.1.1 has a known issue (AWC-1686) with the way authentication is handled for web scripts in the context of Liferay so make sure you are using 2.1.2.

5. Expand the Alfresco 2.1.2 WAR into the Tomcat webapps/alfresco directory (which you’ll have to create the first time). If you are tweaking the install (such as pointing to a specific MySQL database, using something other than MySQL, pointing to a different data directory, etc.) make sure you have copied your good set of extensions into Tomcat’s shared/classes/alfresco/extension directory.

6. Copy the MySQL connector into Tomcat’s common/lib directory.

7. Start Tomcat. When it comes up, you’ll have Liferay running and you’ll have Alfresco running, but Liferay doesn’t yet know about Alfresco. Verify that you can log in to Alfresco as admin.

7a. While you are here, create a test user account. You need to create a user account that has an email address that matches the test user account you created in Liferay. In this example you created Foo User with a screen name of fuser and an email address of fuser@foo.com so you need to create an Alfresco user with the same settings. You’ll log in to Alfresco as fuser. You’ll log in to Liferay as fuser@foo.com.

7b. Verify that you can log in to Alfresco as fuser.

8. Shut down Tomcat.

9. Now you need to configure Alfresco as a Liferay plug-in. This involves adding four files to Alfresco’s WEB-INF directory: liferay-display.xml, liferay-plugin-package.xml, liferay-portlet.xml, and portlet.xml. Why aren’t these available in the Alfresco source or on the wiki? Apparently someone tried to address this at some point because there is a link on the wiki but it is broken. Until that’s addressed, I’ve put them here.

10. Remove the portlet-api-lib.jar file from Alfresco’s WEB-INF/lib directory.

11. Re-package alfresco.war. It is now ready to hand over to Liferay.

12. Start Tomcat.

13. Find your Liferay deploy directory. If you are running out-of-the-box on Linux, Liferay’s “deploy” directory is called liferay/deploy and it resides in the home directory of the user who started Tomcat. I’m running it as root so my Liferay deploy directory is /root/liferay/deploy.

14. Copy the alfresco.war you just created into the deploy directory. Watch the log. You should see Liferay working on the WAR. He’s finding the plug-in config files and essentially deploying the Alfresco portlets.

15. Now log in to Liferay using the Liferay admin account (test@liferay.com). Go to a page, then use the global navigation dropdown to select “Add Content”. The list of portlets should appear and you should see the “Alfresco” category. If you don’t, look at the log because something is amiss. Add the My Spaces portlet to the page. You may see an error at this point but ignore it. The problem is you probably don’t have a user in Alfresco that has an email address of “test@liferay.com”, which is the currently-logged in user.

16. Log out.

17. Log in as your test user that exists in both Alfresco and Liferay (fuser@foo.com).

18. Go to the page. You should see the “My Spaces” portlet. You should be able to upload content, create spaces, etc.

Exposing your own web scripts as portlets

All Alfresco web scripts are automatically exposed as JSR-168 portlets, including the ones you create. To add your web scripts as portlets, first make sure you have authentication set to “user” and transaction set to “required” in your web script’s descriptor. Then, update portlet.xml, liferay-portlet.xml, and liferay-display.xml. Follow the pattern that’s in those files already and you’ll be fine. For example, if you deploy the Hello World web script from my web script tutorial, you need to add a new portlet to portlet.xml with a “scriptUrl” like: /alfresco/168s/someco/helloworld?name=jeff. Then you update liferay-portlet.xml and liferay-display.xml with the new portlet name or portlet ID.

Single sign-on with no single sign-on?

The web script runtime has a JSR-168 authenticator. So when your web scripts get invoked by the portlet, the current credentials are passed in. That’s why your web script can run without requiring an additional sign in. Prior to this being put in place, people had to implement Yale CAS (or an equivalent) to get SSO between Liferay and Alfresco web scripts.

What’s not covered in these instructions is that you’ll probably want to (1) configure both Alfresco and Liferay to authenticate against LDAP and (2) change the configuration of either Alfresco or Liferay to use the same credential (either username or email address) for both systems so that if you do have users logging in to both, they don’t have to remember that one requires the full email address but the other doesn’t.

Troubleshooting

If you see one of the Alfresco portlets displaying “Data is not currently available” or somesuch, try hitting
Alfresco in another tab. Log in, then log out. Then go back to the
portal and open the page again. It should work now. I’m not sure what’s going on there. I think it may have to do with me switching back-and-forth between Liferay instances (4.3.2 versus 4.4.2) so maybe you won’t see it.

Open issues

You may see an error like this:

21:22:15,965 WARN [BaseDeployer:1038] Unable to format /usr/local/bin/liferay-4.3.6/temp/20080408212212978/WEB-INF/faces-config-jbpm.xml: Error on line 5 of document file:///usr/local/bin/liferay-4.3.6/temp/20080408212212978/WEB-INF/faces-config-jbpm.xml : A ‘)’ is required in the declaration of element type “application”. Nested exception: A ‘)’ is required in the declaration of element type “application”.

I haven’t chased that down yet. I’ll update this post with a comment when I find out. I’m sure fixing that will also fix the problem that you’ll see if you try to start an advanced workflow from a piece of content displayed in the My Spaces portlet.

I was also seeing an error when trying to use the “Add Content” link in the straight Alfresco client. I think it is JSF-related. Again, I’ll update this post with a comment when it is resolved (or when I find a Jira ticket).