Sunday, 16 December 2012

Portals, Portlets and Alfresco


Portlets are self-contained web applications that are meant to be deployed in portals or, to be more precise, portlet containers.
All major application server vendors have extended their products with a portlet container, and the open-source world provides us with some implementations, too. To mention a few, we have Jetspeed, GateIn, Liferay...
In theory portlets should be able to deploy in any portlet container, but in practice we're developing portlets having in mind the concrete portal implementation we're working with. Often we need to add to our portlets some vendor-specific config files that make them integrate nicely in that particular portal and, as a side effect, make them useless for any other.
In this little example I have stuck to the portlet specification, kept the code portal-free and let the container inject whatever the portlet needs to run in it. And it works, at least in GateIn, my chosen portal. Later I will try to drop it in Liferay and see what happens; I'll let you know the results in another post.

Let's build a portlet that calls an Alfresco webscript to pull some data, and displays the results.

Alfresco

First we need an instance of Alfresco up and running. If you don't have it yet, download the latest community edition from here and install it. The default settings will almost do for our purposes, you don't even need to create a site for this example to work.
The only configuration items we need to change are the authentication settings since we need to ensure that Alfresco accepts remote calls. We achieve this by enabling external authentication. Edit alfresco-global.properties (in $ALFRESCO_HOME/tomcat/shared/classes) to include the following:

authentication.chain=external1:external,alfrescoNtlm1:alfrescoNtlm
external.authentication.enabled=true
external.authentication.proxyUserName=

If you have already an authentication.chain setup you only need to add external1:external to your current value. Note also that we have intentionally left empty the proxyUserName; this tells the external authentication subsystem that the user accessing Alfresco is the one specified in a header parameter in the remote request, as you will see when we develop the portlet. For more details about external authentication parameters see Alfresco docs.

Portlet

Now let's make this portlet. I have already mentioned GateIn, but there are some more pieces in this little demo. Let me show you the techonology stack we're going to work on:
  • Eclipse IDE.
  • Maven. Let the experts do the build.
  • commons-httpclient. We're going to call an Alfresco webscript via HTTP, remember?
  • Jackson. The webscript returns JSON, and Jackson parses JSON like hell.
  • JSF. Why not? It's the technology I'm using at work right now.
  • GateIn. It comes bundled either with Tomcat or JBoss. Take JBoss, which can talk JSF without the need of any additional libraries.

GateIn installation

You only need to be careful with the ports if you are running Alfresco and GateIn on the same machine. If that's the case, edit the files standalone.xml (there are several) in $GATEIN_HOME/standalone/configuration to change the port numbers. Alternatively you can change Alfresco settings in $ALFRESCO_HOME/tomcat/conf/server.xml.
To launch GeteIn execute standalone.sh (or .bat if you're on Windows) from $GATEIN_HOME/bin.
When you have your portlet ready just drop the .war file into $GATEIN_HOME/standalone/deployments and it will autodeploy.

Maven

As always, it is easy to build a portlet project with Maven. We have a maven-archetype-portlet to get us started. It generates, along with the folder structure that we need, some Jetspeed-specific folders and files that you can simply remove since they're not needed in our example.
If you don't want to get involved in maven archetypes (shame on you), just grab my code from here.
Liferay provides some portlet archetypes, too. Even one for JSF!

Portlet construction

My portlet is very simple. It just calls an Alfresco webscript that returns the list of user groups in JSON format, and then the view displays the list. The webscript I'm calling is /alfresco/wcservice/api/groups.

Portlet descriptor

Let's say that a portlet is a portlet because it has a portlet.xml file.
The maven-archetype-portlet generates a sample descriptor for us; we just have to use it as a template to make the one we need. This is the important bit in mine:
javax.portlet.faces.GenericFacesPortlet

 javax.portlet.faces.defaultViewId.view
 /xhtml/groups.xhtml

There's no much to explain. The portlet class is not ours, the container provides it. On the other hand, the view is something we must implement, as you may expect.

Faces config

faces-config.xml is not generated by maven-archetype-portlet.
In my example, it only declares a managed bean: the backing bean for the view.

 groupsBean
 ie.paco.alfresco_portlet.GroupsBean
 view

Backing bean

GroupsBean.java is the bean linked to the view file groups.xhtml.
It doesn't do anything really especial. Just calls Alfresco webscript and transforms the JSON response into a Java Map, which is then stored in a field called groups.
private static final String ALFRESCO_GROUPS_URL = "http://192.168.145.166:8080/alfresco/wcservice/api/groups";

private Map<String, Object> groups = null;

public GroupsBean() {
 HttpClient client = new HttpClient();
 GetMethod method = new GetMethod(ALFRESCO_GROUPS_URL);
 method.addRequestHeader("X-Alfresco-Remote-User", "admin");
 try {
  // Execute the method.
  int statusCode = client.executeMethod(method);

  if (statusCode != HttpStatus.SC_OK) {
    LOGGER.error("Method failed: " + method.getStatusLine());
  }

  // Read the response body.
  String response = method.getResponseBodyAsString();

  // Deal with the response.
  LOGGER.debug(response);
  ObjectMapper mapper = new ObjectMapper();
  groups = mapper.readValue(response, new TypeReference<Map<String, Object>>(){});

   } catch (HttpException e) {
  LOGGER.error("Fatal protocol violation:", e);
   } catch (IOException e) {
  LOGGER.error("Fatal transport error:", e);
   } finally {
  // Release the connection.
  method.releaseConnection();
   }  
}

Note that the webscript URL is hard-coded in a constant. You need to edit that to make it work in your environment.
Note also how the user is passed in the request header. You may need to edit that line, too, if your Alfresco username is different.
The JSON response from the webscript is going to be something like this:
{
 "data": [
  {
     "authorityType": "GROUP",
     "shortName": "ALFRESCO_ADMINISTRATORS",
     "fullName": "GROUP_ALFRESCO_ADMINISTRATORS",
     "displayName": "ALFRESCO_ADMINISTRATORS",
     "url": "/api/groups/ALFRESCO_ADMINISTRATORS"
  },
  {
     "authorityType": "GROUP",
     "shortName": "EMAIL_CONTRIBUTORS",
     "fullName": "GROUP_EMAIL_CONTRIBUTORS",
     "displayName": "EMAIL_CONTRIBUTORS",
     "url": "/api/groups/EMAIL_CONTRIBUTORS"
  }
 ]
}
So the groups Map in the backing been is going to end up with a single entry with key "data" and a list of group objects as value. The group objects are going to be also Maps.

The view

You can't get it any simpler:

 

Alfresco User Groups

It iterates over the list of groups in the backing bean and outputs their property "shortName".
This is what my portlet looks like in a GateIn page:

Source code

You can get the source code from here.

 What's next?

Well, this one has been sooooo simple, hasn't it? I'm sure you're missing some action.
So now that we're not scared of portals, portlets and Alfresco, let's enable some clicking on this portlet and make things happen. I will share my work in the next post.

The blank verse

Remember that this section has nothing to do whith technology or consulting. Anyway, this conversation may be of interest:

"It's hard to find a decent room in Castleknock, Fergal. It seems that everybody wants to live there. Offers last just minutes; everytime I call, the room is gone. Browsing the ads in daft.ie is a hard work, and sometimes the unexpectedness strikes you."

"the unexpectedness! Your English, Pepe."

"Yeah, I know. My English...
"Listen to this one I found yesterday, for example: '... single female in her thirties offers a room to let in... blah blah blah Castleknock. More than a lodger I'm looking for someone who wants to relax and feel at home... blah blah blah...'
"...Intriguing".

"Is that room gone, too?"

"I don't know, but that room is out of question. It seems to me that just by answering this ad I would commit myself to no less than... being happy. That's big deal. An exhausting task."

"I think you're reading the message the wrong way, Pepe"

"I've read and re-read 'the message', as you say, several times and thought about it for hours trying to find different interpretations."

"Don't tell me you always arrive to the same conclusion."

"Not the same, but alike. They range from 'nightmare' to 'hell'".

Fergal chuckles. "How's that?"

"Listen, it's very simple." Pepe takes his lecture tone "That room is not my room. That house is not my house. That's not my home. I don't want to feel at home, I don't want to feel comfortable, I don't want to be nice, and I'm ready to pay for not having to."

"So you finally admit it. You are antisocial."

"What's wrong with being antisocial?"

"Well, that question is wrong, too."

"Anyway, that's not all. Listen to this: 'I'm clean and tidy but not over the top.'"

"That's the same lady?"

"That's her. Same lady, same ad.
"What the heck does she mean?"

"What do you think she means, Pepe?"

"I don't know what she means, I'm asking you. But I do know what those words mean: 'she - is - dirty.'"

"For God's sake, Pepe. She's not saying that. She's clean although not to the highest degree."

"Highest degree? There's only one degree of cleanness and many degrees of dirtiness, everybody knows that."

"That's a sophism."

"No, it's true. Listen, Fergal, what would you think of me if I said 'I'm honest but not over the top.'?"

"That's different."

"Because you say so."