hross: February 2008 Archives

I love the ALUI Server API. It's robust, fairly easy to use, cross-platform, and powerful. Unfortunately, it still has its limitations. One of the biggest limitations is the same limitation that plagues most object models built on top of a database layer: its inability to be a database.

No matter how brilliant the design of the object hierarchy, there will always be situations where running a SQL query to get information would be a lot simpler. Recently, I ran into one of those situations and was lucky enough to know how to circumvent the "rules". What follows is an analysis of that situation.

As usual, this post has me thinking I should just put "this is not supported" in my blog description.

The Problem

I was recently asked, "How can I get a list of all the portlets currently in use in my portal?" In other words, could I come up with a table of pages, their parent communities, and the portlets on them for the entire portal.

Using the server API, creating such a table is possible via the following logic:

  1. Loop through all communities in the portal
  2. For each community, get a list of pages
  3. For each page, get a list of portlets.
  4. Look up each portlet name and ID.

While this is certainly do-able, you don't need big O notation to see the embedded for/each statements, large volume of data, and potential for this code to chew up a ton of CPU cycles and make quite a few database queries. In a production environment, this just doesn't seem feasible.

But oh, if I only had access to the database. I could write a complex query that would join a few tables together and give me what I want. One simple SQL statement. Here is the statement I would write to produce the described table:

SELECT pagegadgets.GADGETID, gadgets.NAME AS GADGETNAME,
communities.NAME AS COMMUNITYNAME, pages.NAME AS PAGENAME FROM 
PTPAGEGADGETS pagegadgets LEFT JOIN PTPAGES pages 
    ON pagegadgets.PAGEID=pages.OBJECTID 
INNER JOIN PTGADGETS gadgets 
    ON pagegadgets.GADGETID=gadgets.OBJECTID 
INNER JOIN PTCOMMUNITIES communities 
    ON pages.FOLDERID = communities.FOLDERID 
ORDER BY gadgets.NAME, communities.NAME, pages.NAME ASC

Some of you are probably thinking, "How about I just create a remote portlet that directly connects to the portal database?", which you can do, and some customers have. However, you lose the ability to combine this table with other API code, lose the portable nature of Sever API libraries, and the cross platform capability to execute the query.

How about I show you a way to use the Server API instead?

Casting to an Internal Session

It turns out that this process is much easier than you think. The first part involves getting an internal session object. An internal session is simply a back end class that we aren't expected to use. It provides a lot of goodies that aren't available to normal server API IPTSession objects. To get an internal session, we simply need to know about it. This means the following imports:

import com.plumtree.server.impl.core.PTSession;

import com.plumtree.server.impl.core.InternalSession;

and casting an IPTSession object like so:

InternalSession iSession = ((PTSession) session).GetInternalSession();

Congratulations. You have an internal session object. Intellisense will show you all kinds of undocumented goodies related to this object. Given the post topic, today we are mainly interested in the database querying ability...

Running a Query

I could write something long and witty to explain the rest of the code, but it hardly seems necessary. Here is the entire source (Java) for a tag which does as described. The SQL is hard coded into the example:

package com.bea.services.tags;

import com.plumtree.openkernel.db.IOKDBCursor;
import com.plumtree.openkernel.db.IOKDBResultSet;
import com.plumtree.openkernel.db.IOKDBRow;
import com.plumtree.portaluiinfrastructure.tags.ATag;
import com.plumtree.portaluiinfrastructure.tags.TagType;
import com.plumtree.portaluiinfrastructure.tags.metadata.*;
import com.plumtree.server.*;
import com.plumtree.server.impl.core.InternalSession;
import com.plumtree.server.impl.core.PTSession;
import com.plumtree.taskapi.portalui.TaskAPIUIUser;
import com.plumtree.uiinfrastructure.activityspace.AActivitySpace;
import com.plumtree.xpshared.htmlconstructs.PTStyleClass;
import com.plumtree.xpshared.htmlelements.*;

public class PortletLocationTag extends ATag {

    public static final ITagMetaData TAG = new TagMetaData("portletlist",
            "This tag lists portlet locations.");

    public static final OptionalTagAttribute PORTLETID = new OptionalTagAttribute(
            "portletId", "A specific portlet ID to query by.",
            AttributeType.STRING, "");
    
    public static final OptionalTagAttribute MAXROWS = new OptionalTagAttribute(
            "maxRows", "The maximum number of rows in the query.",
            AttributeType.STRING, "500");

    private static final String PORTLET_LOCATION_QUERY = 
        "SELECT pagegadgets.GADGETID, gadgets.NAME AS GADGETNAME, "
            + "communities.NAME AS COMMUNITYNAME, "
            + "pages.NAME AS PAGENAME FROM "
            + "PTPAGEGADGETS pagegadgets "
            + "LEFT JOIN PTPAGES pages ON "
            + "pagegadgets.PAGEID=pages.OBJECTID "
            + "INNER JOIN PTGADGETS gadgets ON "
            + "pagegadgets.GADGETID=gadgets.OBJECTID "
            + "INNER JOIN PTCOMMUNITIES communities ON "
            + "pages.FOLDERID = communities.FOLDERID ";
    
    private static final String WHERE_PORTLET_ID = "WHERE gadgets.OBJECTID = ";
    private static final String ORDER_BY_GADGET = " ORDER BY gadgets.NAME, communities.NAME, pages.NAME ASC";
    private static final String ORDER_BY_COMMUNITY = " ORDER BY communities.NAME, pages.NAME ASC";

    public ATag Create() {
        return new PortletLocationTag();
    }

    public TagType GetTagType() {
        return TagType.SIMPLE;
    }

    public HTMLElement DisplayTag() {

        if (!hasAdminAccess()) {
            return null;
        } // they don't have access

        // create a table for our result set
        HTMLTable result = new HTMLTable();
        result.SetWidth(CommonHTMLStrings.ONE_HUNDRED_PERCENT);
        result.SetBorder(CommonHTMLStrings.ZERO);
        result.SetCellPadding(CommonHTMLStrings.ONE);
        result.SetCellSpacing(CommonHTMLStrings.ONE);

        // get the user session
        IPTSession session = getSession();
        InternalSession iSession = ((PTSession) session).GetInternalSession();

        // run the query as a cursor
        String query = PORTLET_LOCATION_QUERY;
        
        // build the query based on tag options
        if (getPortletId() > 0) {
            query += WHERE_PORTLET_ID + getPortletId() + ORDER_BY_COMMUNITY;
        } else {
            query += ORDER_BY_GADGET;
        }
        
        // open the cursor and run it
        IOKDBCursor cursor = iSession.CreateCursor(query);
        int maxRows = getMaxRows();
        IOKDBResultSet results = cursor.Open(maxRows);
        
        // build the table of results
        HTMLTableRow tableRow = new HTMLTableRow();
        tableRow.SetStyleClass(PTStyleClass.LIST_SORT_HEADER_BG);

        HTMLTableCell tableCell = new HTMLTableCell();
        tableCell.SetVAlign(CommonHTMLStrings.MIDDLE);
        tableCell.SetStyleClass(PTStyleClass.LIST_SORT_HEADER);
        tableCell.AddInnerHTMLString("<b>Portlet Name</b>");
        tableRow.AddInnerHTMLElement(tableCell);
        
        tableCell = new HTMLTableCell();
        tableCell.SetVAlign(CommonHTMLStrings.MIDDLE);
        tableCell.SetStyleClass(PTStyleClass.LIST_SORT_HEADER);
        tableCell.AddInnerHTMLString("<b>Community Name</b>");
        tableRow.AddInnerHTMLElement(tableCell);
        
        tableCell = new HTMLTableCell();
        tableCell.SetVAlign(CommonHTMLStrings.MIDDLE);
        tableCell.SetStyleClass(PTStyleClass.LIST_SORT_HEADER);
        tableCell.AddInnerHTMLString("<b>Page Name</b>");
        tableRow.AddInnerHTMLElement(tableCell);
        
        result.AddInnerHTMLElement(tableRow);
        
        // output the results
        for (int i = 0; i < results.GetNumRows(); i++) {
            

            
            IOKDBRow dbrow = results.GetRow(i);
            tableRow = new HTMLTableRow();
            
            // row coloring
            if ((0 == i) || (((i + 1) / 2) == (i / 2))) {
                // it's even
                tableRow.SetStyleClass(PTStyleClass.LIST_ITEM_TWO_BG);
            } else {
                // it's odd
                tableRow.SetStyleClass(PTStyleClass.LIST_ITEM_ONE_BG);
            }

            tableCell = new HTMLTableCell();
            tableCell.SetVAlign(CommonHTMLStrings.MIDDLE);
            tableCell.AddInnerHTMLString(dbrow.GetString("GADGETNAME"));
            tableRow.AddInnerHTMLElement(tableCell);

            tableCell = new HTMLTableCell();
            tableCell.SetVAlign(CommonHTMLStrings.MIDDLE);
            tableCell.AddInnerHTMLString(dbrow.GetString("COMMUNITYNAME"));
            tableRow.AddInnerHTMLElement(tableCell);
            
            tableCell = new HTMLTableCell();
            tableCell.SetVAlign(CommonHTMLStrings.MIDDLE);
            tableCell.AddInnerHTMLString(dbrow.GetString("PAGENAME"));
            tableRow.AddInnerHTMLElement(tableCell);
            
            result.AddInnerHTMLElement(tableRow);
        }

        return result;
    }

    private int getPortletId() {
        try {
            return Integer.parseInt(GetTagAttributeAsString(PORTLETID));
        } catch (Exception ex) {
            return -1;
        }
    }

    private int getMaxRows() {
        try {
            return Integer.parseInt(GetTagAttributeAsString(MAXROWS));
        } catch (Exception ex) {
            return 0;
        }
    }
    
    private IPTSession getSession() {
        return (IPTSession) GetEnvironment().GetUserSession();
    }

    private boolean hasAdminAccess() {
        return TaskAPIUIUser.HasAdminLinkAccess((AActivitySpace) this
                .GetEnvironment());
    }
}

Caveat Emptor

As Uncle Ben would say, "With great power comes great responsibility." The power I gave you above may also allow you to perform INSERT's, UPDATE's and DELETE's, which I strongly caution against. Not only that, but the InternalSession object doesn't perform all those nifty security checks that happen when we use a normal session (notice the hasAdminAccess function), so make sure you either do your own authentication, or limit the amount of information you provide.

Happy querying.

Customizing the Collaboration Calendar

| | Comments (0) | TrackBacks (0)

As is the case with most COTS products, there is a ton of cool functionality in the ALUI product. As a programmer and certified perfectionist, I am always looking at that cool functionality and thinking, "gee, if I could only tweak it to do XYZ, it would be perfect". Apparently, a lot of my clients are thinking the same thing.

Such is the case with the out-of-the-box Collaboration calendar. The calendar itself is great: it allows addition of events, tasks, month/date scrolling and it all works without a full page refresh. It would probably take me months to perfect that kind of bug-free functionality if I wrote my own. Add to that its already existing project integration features, and you've got a great portlet.

But here's the problem... the calendar limits users to projects on their My Page or those associated with a specific Community. And although it's a "feature", users have to specifically select certain projects they want displayed. There is no automatic way for me to control the Calendar's behavior or make it display one specific project.

From a usability standpoint, this is less than ideal, especially if I want to use some of the cool Collaboration EDK features to automate project use and creation. It sure would be great if I had a calendar portlet that would only display a calendar from 1 project.

mycalendar 

Figure 1 - The My Calendar Portlet and somewhat confusing drop down.

Since I don't normally ask questions in my blog that I can't answer, the point of this post is to show you how you I managed to come up with a one project calendar, and maintain existing Collaboration functionality. As always, here is the obligatory "This is not supported" disclaimer:

This is not supported.

But I did it anyway.

Defining Our New Calendar

The way I want my "new" calendar portlet to operate is as follows:

  1. I pass it a Project ID as a querystring parameter via the web service URL and it displays the project I want. I can then duplicate this web service to display different projects.
  2. I pass it a Project ID via a session variable and it displays the project ID contained in the session variable. In this way I can have a dynamic calendar portlet that can be manipulated by other portlets on the page.

Cracking Open the Product

The tools and techniques I used to hack into the product are the same as those from my previous post regarding Publisher. As such, refer to that post for downloads and specifics.

In the case of the calendar, I simply looked at the My Calendar Web Service and then browsed to the proper JSP in the collab.war. Next, I took a look at /calendar/portlets/calendar.jsp to see how I could modify it to suit my purposes. What I came up with was the following calendar.jsp.

How It Works

As with past posts, if you just want to get this working, skip to the Installation section of the post.

If you review the source in the JSP, you will note a couple of things:

  • The JSP itself looks for a querystring parameter called projectIdPrefName. If the querystring does not exist, the JSP behaves as a normal My Calendar or Community Calendar. This allows for normal collaboration operation in all cases except my custom case.
  • If the querystring parameter does exist, the JSP uses the EDK to load the value stored in the Session Preference specified by the value of projectIdPrefName. In other words, the querystring parameter tells the JSP what session preference to load a project ID from. This means I could create multiple web services with different preference values, allowing me to have multiple dynamic calendars on a single page or in a single portal.
  • The JSP then creates a custom request wrapper (more on this in a minute) and a brand new instance of a CalendarControl object (the piece that displays the Calendar). The first argument to the constructor is set to 0, which basically tricks the Calendar into thinking it is being displayed in Project Explorer as part of a stand-alone project (one project only).
  • The rest of the JSP is the original source, except for the piece that makes AJAX postback requests, which had to be slightly modified to take into account the new Calendar type.

As I mentioned, the only other piece of custom code in this solution is the HttpRequest wrapper (called CollabRequestWrapper). The reason this is necessary is that a CalendarControl object with type 0 will only display one project, but it will read that project ID from the querystring. Thus, we must trick the control into thinking the project ID passed as a session variable is on the querystring. The CollabReqeustWrapper does just this (see source in the jar file for details).

Installation

Here is a copy of the JSP.

Here is a copy of the JAR.

To install, follow these instructions:

  1. Stop the Collaboration service.
  2. Replace the file /calendar/portlets/calendar.jsp with the included calendar.jsp in the $PT_HOME\ptcollab\4.2\webapp\ptcollab.war file by unzipping and rezipping it (use WinRAR, WinZip or the jar utility that comes with Java).
  3. Add requestwrapper.jar to the folder WEB-INF\lib in $PT_HOME\ptcollab\6.4\webapp\ptcollab.war file by unzipping and rezipping it.
  4. Start the Collaboration service.

At this point, normal Collaboration behavior should be unchanged. To use the customization, do the following:

  1. Create a copy of the My Calendar web service in portal administration.
  2. Rename the new web service and edit the base URL as follows:

    /do/calendar?type=my&projectPrefName=<session preference name>
    Where <session preference name> is a session preference that contains the project ID whose calendar should be displayed.

  3. Add a session preference to the Web Service (Edit the Web Service, go to the Preferences section) with the name <session preference name> from step 3.
  4. Create a new portlet based on the updated web service.
  5. Set session preferences to control project display in the new portlet, as desired, or append &projID=<project ID> to the web service URL.

Finished Product

modcalendar 

Figure 2 - The new Calendar Portlet displaying a single project.

As you can see from the above, we now have a calendar that displays only a single project without the drop down and is completely customizable via standard ALUI portal mechanisms.

About this Archive

This page is a archive of recent entries written by hross in February 2008.

hross: January 2008 is the previous archive.

hross: April 2008 is the next archive.

Blogroll


Integryst

Function1

Fabien Sanglier

Bill Benac

Jordan Rose

Chris Bucchere

Robert Herrera

Nanek Blog Aggregator

Spartan Java




if you'd like to be listed here.




I don't blog about non-tech issues here, but you can check my Google Reader Shared Items if you want to know what I'm currently interested in.

Categories