How to build a Wicket AJAX Portlet in Liferay’s Eclipse IDE

Built Using

IDE: Eclipse Java EE IDE for Web Developers.
Version: Helios Release
Build id: 20100617-1415

Liferay Portal 6.0.4
Liferay Plugins SDK 6.0.4
Liferay Eclipse IDE Tools Version 1.0.1.v201007090414
Wicket 1.4.10

Background

The purpose of this document is to provide a relatively easy step-by-step process to create an AJAX capable Wicket Portlet in Liferay’s Eclipse IDE. This work started with us using the Wicket portlet examples as a base from which we extracted the smallest slice of code neccesary to get a standalone portlet (the examples are somewhat standalone but all the portlet descriptor files including web.xml have everything lumped together). Our goal was to take this and incorporate it into the Liferay IDE project setup. Along the way, we found a few caveats and issues with solutions residing in various disparate locations on the web. Where possible we have included links for your reference. Any input, feedback is greatly appreciated and encouraged; especially any other caveats or issues found in your travels.

A sample project is attached (sample-portlet) to this blog posting for your reference that includes the steps implemented as described below.

How To

  1. Create a new Liferay plug-in project. For project or display name, avoid hyphens or any non-alpha characters just to be safe. It appears as though Liferay strips hyphens from the portlet name in web.xml when the WAR file is deployed (among other things) and this causes issues. This how-to uses ‘sample’ as the portlet name. An important thing to note is that when you provide the portlet name, the Liferay plug-in project appends ‘-portlet’ to it. So in our case, we specify ‘sample’ and the project that gets created has ‘sample-portlet’ as its name.

You can change this to your portlet name (keeping with the guidelines above). Two very critical things that are not obvious:

  • portletName, wicketFilterPath and filter-mapping/url-patterns must all match
  • These names must NOT contain hyphens or any non-alpha characters (same as above to be on the safe side).

A MAJOR caveat: The project builder will automatically pre-populate fields in the portlet.xml and liferay-portlet.xml with the ‘-portlet’ suffix. The hyphen MUST be removed manually.

This will make more sense once you follow the steps below and see these tags and their contents. Refer to http://issues.liferay.com/browse/LPS-1911if you need more context. Raymond Auge and the other folks on this thread provide some good background on this issue.

2. Add the following JARS to the WEB-INF/lib folder in the project:

  • wicket-1.4.10.jar
  • slf4j-api-1.5.8.jar
  • slf4j-log4j12-1.5.8.jar

3. In the portlet.xml make the following checks/adjustments:

  • ‘portlet-name’ or ‘display-name’ do not contain any hyphens or any non-alpha characters (display-name may not be an issue).
  • ‘portlet-class’ should be ‘org.apache.wicket.protocol.http.portlet.WicketPortlet’
  • Following ‘portlet-class’ remove the ‘init-param’ that has ‘view-jsp’ and ‘/view.jsp’ as the name/valur pair and add the following (where ‘sample’ is your portlet name):
<init-param>
	<name>wicketFilterPath</name>
	<value>/sample</value>
</init-param>
<supports>
	<mime-type>text/html</mime-type>
	<portlet-mode>VIEW</portlet-mode>
	<portlet-mode>EDIT</portlet-mode>
</supports>
<supports>
	<mime-type>text/xml</mime-type>
	<portlet-mode>VIEW</portlet-mode>
	<portlet-mode>EDIT</portlet-mode>
</supports>

4. Add the following to your web.xml:

<context-param>
		<param-name>configuration</param-name>
		<param-value>deployment</param-value>
	</context-param>

	<filter>
		<filter-name>HelloWorldApplication</filter-name>
		<filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class>
		<init-param>
		  <param-name>detectPortletContext</param-name>
		  <param-value>true</param-value>
		</init-param>
		<init-param>
		  <param-name>applicationClassName</param-name>
		  <param-value>ca.intelliware.helloworld.app.HelloWorldApplication</param-value>
		</init-param>
	</filter>

	<filter-mapping>
		<filter-name>HelloWorldApplication</filter-name>
		<url-pattern>/sample/*</url-pattern>
		<dispatcher>FORWARD</dispatcher>
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>INCLUDE</dispatcher>
	</filter-mapping>
  • You can remove the ‘jsp-config’ tag and its children.

5. The following can be fetched from the attached zip.

Under the docroot/WEB-INF/src source folder create the following packages (this is really up to you as long as you make sure the class name matches in the ‘applicationClassName’ parameter under the HelloWorldApplication filter declaration):

  • ca.intelliware.helloworld.app
  • ca.intelliware.helloworld.pages

6. Add the following classes to the ‘ca.intelliware.helloworld.app’ package. These were pulled from the example WAR file and I’m sure things can be simplified futrther. Thank you, Mr. Locke and Mr. Gravener for these base classes and your efforts.

HelloWorldApplication.java:

package ca.intelliware.helloworld.app;

import org.apache.wicket.Page;

import ca.intelliware.helloworld.pages.HelloWorld;

public class HelloWorldApplication extends WicketExampleApplication {
    public HelloWorldApplication() {
    }

    @Override
    public Class<? extends Page> getHomePage() {
        return HelloWorld.class;
    }
}

WicketExampleApplication.java:

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ca.intelliware.helloworld.app;

import org.apache.wicket.Request;
import org.apache.wicket.RequestCycle;
import org.apache.wicket.Response;
import org.apache.wicket.protocol.http.WebApplication;
import org.apache.wicket.protocol.http.WebRequest;
import org.apache.wicket.settings.ISecuritySettings;
import org.apache.wicket.util.crypt.ClassCryptFactory;
import org.apache.wicket.util.crypt.NoCrypt;

/**
 * Wicket Example Application class.
 *
 * @author Jonathan Locke
 */
public abstract class WicketExampleApplication extends WebApplication
{
	/**
	 * prevent wicket from launching a java application window on the desktop
	 * <br/>
	 * once someone uses awt-specific classes java will automatically do so and allocate a window
	 * unless you tell java to run in 'headless-mode'
	 */
	static
	{
		System.setProperty("java.awt.headless", "true");
	}

	/**
	 * Constructor.
	 */
	public WicketExampleApplication()
	{
	}

	/**
	 * @see org.apache.wicket.protocol.http.WebApplication#init()
	 */
	@Override
	protected void init()
	{
		// WARNING: DO NOT do this on a real world application unless
		// you really want your app's passwords all passed around and
		// stored in unencrypted browser cookies (BAD IDEA!)!!!

		// The NoCrypt class is being used here because not everyone
		// has the java security classes required by Crypt installed
		// and we want them to be able to run the examples out of the
		// box.
		getSecuritySettings().setCryptFactory(
			new ClassCryptFactory(NoCrypt.class, ISecuritySettings.DEFAULT_ENCRYPTION_KEY));

		getDebugSettings().setDevelopmentUtilitiesEnabled(true);
	}

	/**
	 *
	 * @see org.apache.wicket.protocol.http.WebApplication#newRequestCycle(org.apache.wicket.Request,
	 *      org.apache.wicket.Response)
	 */
	@Override
	public final RequestCycle newRequestCycle(Request request, Response response)
	{
		return new WicketExampleRequestCycle(this, (WebRequest)request, response);
	}
}

WicketExampleRequestCycle.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ca.intelliware.helloworld.app;

import org.apache.wicket.Page;
import org.apache.wicket.Response;
import org.apache.wicket.protocol.http.BufferedWebResponse;
import org.apache.wicket.protocol.http.PageExpiredException;
import org.apache.wicket.protocol.http.WebApplication;
import org.apache.wicket.protocol.http.WebRequest;
import org.apache.wicket.protocol.http.WebRequestCycle;
import org.apache.wicket.protocol.http.servlet.AbortWithWebErrorCodeException;

/**
 * Handles the PageExpiredException so that the SourcesPage can recover from a session expired.
 *
 * @author rgravener
 */
public class WicketExampleRequestCycle extends WebRequestCycle
{
	/**
	 * Construct.
	 *
	 * @param application
	 * @param request
	 * @param response
	 */
	public WicketExampleRequestCycle(WebApplication application, WebRequest request,
		Response response)
	{
		super(application, request, response);
	}

	/**
	 * @see org.apache.wicket.RequestCycle#onRuntimeException(org.apache.wicket.Page,
	 *      java.lang.RuntimeException)
	 */
	@Override
	public Page onRuntimeException(final Page page, final RuntimeException e)
	{
		final Throwable cause;
		if (e.getCause() != null)
		{
			cause = e.getCause();
		}
		else
		{
			cause = e;
		}

		if (cause instanceof PageExpiredException)
		{
			handlePageExpiredException((PageExpiredException)cause);
		}
		return super.onRuntimeException(page, e);
	}

	/**
	 * Checks to see if the request was ajax based. If so we send a 404 so that the
	 * org.apache.wicket.ajax.IAjaxCallDecorator failure script is executed.
	 *
	 * @param e
	 */
	private void handlePageExpiredException(final PageExpiredException e)
	{
		Response response = getOriginalResponse();
		if (response instanceof BufferedWebResponse)
		{
			BufferedWebResponse bufferedWebResponse = (BufferedWebResponse)response;
			if (bufferedWebResponse.isAjax())
			{
				// If there is a better way to figure out if SourcesPage was the request, we should
				// do that.
				throw new AbortWithWebErrorCodeException(404);
			}
		}
	}
}

6. Add the following to the ‘ca.intelliware.helloworld.pages’ package.

AnotherPage.java

package ca.intelliware.helloworld.pages;

import org.apache.wicket.PageParameters;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.basic.Label;

public class AnotherPage extends WebPage {

	public AnotherPage() {
		add(new Label("message", "Yes!"));
	}

	public AnotherPage(PageParameters params) {
		add(new Label("message", params.getString("display")));

	}

}

HelloWorld.java

package ca.intelliware.helloworld.pages;

import java.io.Serializable;

import org.apache.wicket.PageParameters;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.RequiredTextField;
import org.apache.wicket.markup.html.form.SubmitLink;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.model.CompoundPropertyModel;
import org.apache.wicket.model.PropertyModel;

public class HelloWorld extends WebPage {

	public class FormBean implements Serializable {

		private static final long serialVersionUID = -630600987317343730L;

		private String fieldValue;

		private String getFieldValue() {
			return fieldValue;
		}

		@SuppressWarnings("unused")
		private void setFieldValue(String fieldValue) {
			this.fieldValue = fieldValue;
		}

	}

	private int counter = 1;

	public HelloWorld() {
		final Label counterLabel = new Label("counter", new PropertyModel<HelloWorld>(this, "counter"));
		add(counterLabel);
		counterLabel.setOutputMarkupId(true);
		add(new AjaxLink("counterLabelLink") {

			@Override
			public void onClick(AjaxRequestTarget target) {
				counter++;
				target.addComponent(counterLabel);
			}

		});
		add(new Label("message", "Hello World!"));
		Form<FormBean> form = new Form<FormBean>("form", new CompoundPropertyModel<FormBean>(new FormBean()) ){
			private static final long serialVersionUID = -4286799072934065298L;

			@Override
			protected void onSubmit() {
				PageParameters params = new PageParameters();
				params.add("display", getModelObject().getFieldValue());
				setResponsePage(AnotherPage.class, params);
			}
		};

		add(form);
		form.add(new RequiredTextField<FormBean>("fieldValue"));
	}

}

AnotherPage.html

<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
    <title>Another Page!</title>
</head>
<body>
  	Hello!!!
    <span wicket:id="message" id="message">Message goes here</span>

 </body>
</html>

HelloWorld.html

<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
    <title>Hello World!</title>
</head>
<body>
    <span wicket:id="message" id="message">Message goes here</span>
	<span wicket:id="counter" id="counter">Counter</span>
	<a wicket:id="counterLabelLink" id="counterLabelLink">Click me!</a>
    <form wicket:id="form">
    		<h2>Form</h2>
	    	<dl>
				<dt>Value: </dt>
				<dd><input type="text" wicket:id="fieldValue" /></dd>
			</dl>
			<input type="submit" value="Go" id="fieldSubmit"/>
	</form>

</body>
</html>

7. At this point, you should see an error free console. You can now add the portlet via the ‘Add and Remove…’ option in your Liferay Server (in the Servers tab). Simply add it to the ‘Configured’ list. Now when you start Liferay or make any changes your portlet will be synchronized. Once the Liferay tomcat instance is started, you should be able to find the portlet under the ‘sample’ category named as ‘Sample’. Add and enjoy.

It's only fair to share...
Share on FacebookGoogle+Tweet about this on TwitterShare on LinkedIn

Leave a Reply