Monday, October 06, 2008

Hacking SAP Portal with a Javascript/CSS Service

One of the more, erm, "interesting features" of SAP Portal is the lack of ability to directly access the HTML HEAD tag and insert SCRIPT and LINK tags to your own CSS and Javascripts. Well it's not impossible to do, but SAP doesn't offer this out of the box straight away. Probably because they don't want you breaking things.

But lets say you want to use the Dojo Toolkit or YUI on a new hip AbstractPortalComponent. But you don't want to download and host the scripts locally. You wish to use AOLs CDN or Yahoo's CDN to load the javascript. It's faster and solid in terms of reliability. How can you accomplish this?

The answer is, you need to write a new service to access the HTML HEAD.

Create a service inside NWDS and call it, HtmlHeadService. NWDS will create an interface for the service and an implementation. Go to IHtmlHeadService and insert the following method signatures:

package com.portal.htmlheadservice;

import com.sapportals.portal.prt.service.IService;
import com.sapportals.portal.prt.component.IPortalComponentRequest;

public interface IHtmlHeadService extends IService {

public static final String KEY = "HtmlHeadService";

public void addScript(IPortalComponentRequest request, String scriptURL, String type);
public void addJS(IPortalComponentRequest request, String jsURL);
public void addLink(IPortalComponentRequest request, String linkURL, String type, String rel);
public void addCSSLink(IPortalComponentRequest request, String linkURL);
}

This is pretty simple so far. Next look at the HtmlHeadService object:

package com.portal.htmlheadservice;

import com.sapportals.portal.prt.service.IServiceContext;
import com.sapportals.portal.prt.logger.ILogger;
import com.sapportals.portal.prt.runtime.IPortalConstants;
import com.sapportals.portal.prt.component.IPortalComponentRequest;
import com.sapportals.portal.prt.pom.IPortalNode;
import com.sapportals.portal.prt.connection.PortalHtmlResponse;
import com.sapportals.portal.prt.connection.IPortalResponse;
import com.sapportals.portal.prt.util.html.HtmlDocument;
import com.sapportals.portal.prt.util.html.HtmlHead;
import com.sapportals.portal.prt.util.html.HtmlScript;
import com.sapportals.portal.prt.util.html.HtmlLink;

public class HtmlHeadService implements IHtmlHeadService{

private IServiceContext mm_serviceContext;
private ILogger mm_logger;

public void init(IServiceContext serviceContext) {
mm_serviceContext = serviceContext;
mm_logger = serviceContext.getLogger(IPortalConstants.SERVICE_LOGGER);
mm_logger.info(this, "Initialization of HtmlHeadAccessor");
}

public void afterInit() {
mm_logger.info(this, "After Initialization of HtmlHeadAccessor");
}

public void configure(com.sapportals.portal.prt.service.IServiceConfiguration configuration) {}

public void destroy() {}

public void release() {}

public IServiceContext getContext() {
return mm_serviceContext;
}

public String getKey() {
return KEY;
}

public void addLink(IPortalComponentRequest request, String linkURL, String type, String rel) {
HtmlHead docHead = getHtmlHead(request);
if (docHead != null) {
HtmlLink link = new HtmlLink(linkURL);
link.setType(type);
link.setRel(rel);
docHead.addElement(link);
} else {
mm_logger.severe("Could not get HtmlHead from PortalResponse");
}
}

public void addCSSLink(IPortalComponentRequest request, String linkURL) {
addLink(request, linkURL, "text/css", "stylesheet");
}

public void addScript(IPortalComponentRequest request, String scriptURL, String type) {
HtmlHead docHead = getHtmlHead(request);
if (docHead != null) {
HtmlScript script = new HtmlScript();
script.setSrc(scriptURL);
script.setType(type);
docHead.addElement(script);
} else {
mm_logger.severe("Could not get HtmlHead from PortalResponse");
}
}

public void addJS(IPortalComponentRequest request, String jsURL) {
addScript(request, jsURL, "text/javascript");
}

/* This contains the deprecated method getHtmlDocument(). If this fails, check
* the Web Page Composer based service cssService. It uses the exact same
* method. If this is failing, it should be failing.
*/
private HtmlHead getHtmlHead(IPortalComponentRequest request) {
HtmlHead docHead = null;
IPortalNode node = request.getNode().getPortalNode();
IPortalResponse resp = (IPortalResponse) node.getValue(IPortalResponse.class.getName());
try {
PortalHtmlResponse htmlResp = (PortalHtmlResponse) resp;
HtmlDocument doc = htmlResp.getHtmlDocument();
docHead = doc.getHead();
} catch (Exception cce) {
mm_logger.severe("Exception found: " + cce.getMessage());
cce.printStackTrace(System.err);
}
return docHead;
}
}


Here's the meat of the matter. What does this code do? It uses some undocumented objects to gain access to an HtmlDocument object. This object gives you full access to the entire web page. In this case we're just grabbing a head, you could do much more if you so choose.

So what about the deprecated method getHtmlDocument(), seems bad. Well, with the exception of the fact that SAP is using the exact same method in the recently released Web Page Composer tool, I wouldn't be worried. WPC uses this method to grab its style sheets and javascripts from the KM repositiory. The cool thing is, the code can be repurposed to place anything you like into the page.

How to finalize the service? It needs a ton of SharingReferences in the portalapp.xml file to make it go. This is probably more than it needs, but cssService was using this exact string:

"connection,usermanagement, knowledgemanagement, landscape, htmlb, exportalJCOclient, exportal"

With this service you can easily create a PortalComponent that accesses external stylesheets and javascripts to give your portal that custom look and feel that it's been lacking. Some folks have used this method to change the Portal Title and other features as well. Thanks to Darrell Merryweather at SAP for the inspiration.

2 comments:

Zheng An(Joanna) said...

This is a nice article. I am tryin to implement it myself following your article. What I essentially try to accomplish is to get the HTMLDocument of the page on which the component iview is stored. I built the iview with the component following your approach. But for some reason, I can not get anything from the document body. Can you please share your thoughts?
Many thanks

Mike said...

Hi Joanna,

I assume you're doing this by working without the service, but instead placing it all inside the iView to try to get it working?

I would suggest placing a lot of debugging code in the iView to track when you have the HTMLDocument, outputting if it's null, etc.

Unfortunately I am not working with SAP Portal at this time. So these are the best ideas I have off the top of my head.

--Mike

ShareThis