Chapter 4. Portal Server Tutorials

MORE NEEDED HERE.

4.1. How to Create Independent Portlets

This Tutorial will demonstrate how to use the CCM Portal Server API. First, development of a simple 'Hello World' Portlet will be demonstrated. This exercise will be followed by a more sophisticated portlet that stores its message in the database and provides an interface for a user to set and modify its message.

To begin, every Portlet needs a domain object representation. This is done by extending the Portlet base class com.arsdigita.portal.Portlet:

import com.arsdigita.portal.Portlet;
import com.arsdigita.persistence.DataObject;
import com.arsdigita.bebop.portal.AbstractPortletRenderer;
import com.arsdigita.portlets.ui.portlet.HelloWorldPortletRenderer;

public class HelloWorldPortlet extends Portlet {
   public static final String BASE_DATA_OBJECT_TYPE =
     "com.arsdigita.portlets.HelloWorldPortlet";
   public HelloWorldPortlet(DataObject dataObject) {
     super(dataObject);
   }   
   protected String getBaseDataObjectType() {
     return BASE_DATA_OBJECT_TYPE;
   }
   protected AbstractPortletRenderer doGetPortletRenderer() {
     return new HelloWorldPortletRenderer(this);
   }
}

The BASE_DATA_OBJECT_TYPE is an identifier for our class and is used by the CCM Core Persistence layer. Every domain object in the CCM Core system has an object type.

The class above has every thing we need for our Portlet Domain Object. You will notice, though, that we need to provide one more class -- we need an HelloWorldPortletRenderer that extends AbstractPortletRenderer. Let's look at one:

import com.arsdigita.bebop.PageState;
import com.arsdigita.xml.Element;
import com.arsdigita.portal.Portlet;
import com.arsdigita.portlets.SayAnythingPortlet;
import com.arsdigita.bebop.portal.AbstractPortletRenderer;
public class HelloWorldPortletRenderer extends AbstractPortletRenderer {   
    private HelloWorldPortlet m_portlet;   
    public HelloWorldPortletRenderer(HelloWorldPortlet portlet) {
        m_portlet = portlet;   
    }   
    public void generateBodyXML(PageState state, Element parent) {
        Element content = 
           parent.newChildElement("portlet:helloworld",
                                  "http://www.arsdigita.com/portlet/1.0");
           content.setText("Hello World!");   
    }
}

The important part of this class is the generateXML method. The Portal framework code iterates through each portlet in the current portal and calls each portlet's generateXML() method, passing a Document Object Model(DOM) Element to each for the aggregation of each portlet's output.

Portlets that output a static text message are of limited use...a more useful portlet is one that allows the user to set the message, and even to modify it at will. Let's see what we need to modify in our portlet class above to provide this feature:

import com.arsdigita.portal.Portlet;
import com.arsdigita.persistence.DataObject;
import com.arsdigita.bebop.portal.AbstractPortletRenderer;
import com.arsdigita.portlets.ui.portlet.SayAnythingPortletRenderer;
public class SayAnythingPortlet extends Portlet { 
    public static final String BASE_DATA_OBJECT_TYPE =
                        "com.arsdigita.portlets.SayAnythingPortlet";
    public static final String CONTENT = "content";
    public SayAnythingPortlet(DataObject dataObject) {
        super(dataObject);   
    }   
    protected String getBaseDataObjectType() {
        return BASE_DATA_OBJECT_TYPE;   
    }   
    public String getContent() {
        return (String)get(CONTENT);
    }   
    public void setContent(String text) {
        set(CONTENT, text);   
    }   
    protected AbstractPortletRenderer doGetPortletRenderer() {
        return new SayAnythingPortletRenderer(this);  
    }
}

Notice that this class is very similar to the original, except for the setter and getter methods. These methods call the get and set methods on the parent of this class, the com.arsdigita.domain.DomainObject. These methods are how the string 'text' is written to the persistence layer, and how the stored string is later retrieved. In order to allow the user to determine the value of the string, and to subsequently save the value in the database, we will need a data model description file (pdl file), and we will need a ui. First lets look at the pdl file for our portlet:

model com.arsdigita.portlets;
import com.arsdigita.portal.Portlet;

object type SayAnythingPortlet extends Portlet {
  String[0..1] content = portlet_say_anything.content VARCHAR(4000);
  reference key (portlet_say_anything.portlet_id);
}

The above pdl file defines a String named content as a VARCHAR with a size of 4000 chars. It maps the Domain realm (the String) through persistance to the database realm. Note that the name 'content' is the same literal used in the Portlet Domain class as an argument to the 'set()' method inside the setContent() call.

Below is our UI, a Form, that the user will use to enter the value for 'content'.

import com.arsdigita.kernel.ResourceType;
import com.arsdigita.portal.Portlet;
import com.arsdigita.bebop.portal.PortletConfigFormSection;
import com.arsdigita.bebop.portal.PortletSelectionModel;
import com.arsdigita.portlets.SayAnythingPortlet;
import com.arsdigita.bebop.Form;
import com.arsdigita.bebop.event.FormProcessListener;
import com.arsdigita.bebop.Label;
import com.arsdigita.bebop.event.FormSectionEvent;
import com.arsdigita.bebop.FormProcessException;
import com.arsdigita.bebop.form.TextArea;
import com.arsdigita.bebop.SaveCancelSection;
import com.arsdigita.bebop.parameters.StringParameter;
import com.arsdigita.bebop.ColumnPanel;
import com.arsdigita.bebop.PageState;
import com.arsdigita.bebop.RequestLocal;
import com.arsdigita.bebop.event.FormInitListener;
import com.arsdigita.bebop.form.TextField;
import com.arsdigita.bebop.parameters.NotNullValidationListener;
import com.arsdigita.bebop.parameters.StringInRangeValidationListener;
import com.arsdigita.bebop.event.ActionEvent;
import com.arsdigita.bebop.event.ActionListener;
public class SayAnythingPortletEditor extends PortletConfigFormSection {
    private TextArea m_content;
    public SayAnythingPortletEditor(ResourceType resType, 
                                  RequestLocal parentAppRL) {
        super(resType, parentAppRL);
    }
    public SayAnythingPortletEditor(RequestLocal application) {
        super(application);
    }

The addWidgets method builds the Form UI. It is called by the superclass.

Notice that a TextArea is instantiated and its bounds established. Next, two validationListeners are set on the TextArea, to make certain that the contents are not null, and that it does not exceed 4000 characters (the limit we set for the database VARCHAR datatype in the PDL file. If either of these Listeners fire, the form will be displayed with corrective action highlighted in red. A label for "Content:" is also added to the form for instructional purposes.

protected void addWidgets() {
    super.addWidgets();
    m_content = new TextArea(new StringParameter("content"));
    m_content.setRows(10);
    m_content.setCols(35);
    m_content.addValidationListener(new NotNullValidationListener());
    m_content.addValidationListener(new StringInRangeValidationListener(1, 4000));
    add(new Label("Content:", Label.BOLD), ColumnPanel.RIGHT);
    add(m_content);
}

Ths next method, initWidgets, checks to see if the portlet exists yet, and if it does, pre-loads the Text Area with the current content value.

protected void initWidgets(PageState state, Portlet portlet) 
                                     throws FormProcessException {
    super.initWidgets(state, portlet);
    if (portlet != null) {
        SayAnythingPortlet myportlet = (SayAnythingPortlet)portlet;
    m_content.setValue(state, myportlet.getContent());
}

The final method for this class is where the form is processed. Note that the setter method on our Portlet class is called with the contents of the TextArea -- insuring that the user's content is written to the database via the persistence layer.

   
protected void processWidgets(PageState state, Portlet portlet) 
                                             throws FormProcessException {
  super.processWidgets(state, portlet);
  SayAnythingPortlet myportlet = (SayAnythingPortlet)portlet;
  myportlet.setContent((String)m_content.getValue(state));   
}

The final class needed for our new Portlet, is the renderer. Notice how the generateXML() calls now uses the stored text value by calling the Portlets getContent() call, and using this value is the argument to the DOM element's setText() method.

import com.arsdigita.bebop.PageState;
import com.arsdigita.xml.Element;
import com.arsdigita.portal.Portlet;
import com.arsdigita.portlets.SayAnythingPortlet;
import com.arsdigita.bebop.portal.AbstractPortletRenderer;

public class SayAnythingPortletRenderer extends AbstractPortletRenderer {

    private SayAnythingPortlet m_portlet; 

    public SayAnythingPortletRenderer(SayAnythingPortlet portlet) {
        m_portlet = portlet;
    }   
    public void generateBodyXML(PageState state, Element parent) {
        Element content = parent.newChildElement("portlet:sayanything",
                                       "http://www.arsdigita.com/portlet/1.0");
        content.setText(m_portlet.getContent());   
    }
}

This completes the SayAnything Portlet, but we still need to initialize this portlet type at server startup. For this, we will need an Initializer...