Monday, October 13, 2008

TrExMa for Firefox 0.6.2

TrophyManager.com players. TrExMa 0.6.2 is finally available. It should resolve most of the issues in getting a TrExMa rating since the update of TM's web site.

http://code.google.com/p/trexma-for-firefox/

Wednesday, October 08, 2008

SAP Portal Javascript/CSS Service Enhanced

The service was enhanced today.

What I discovered whilst doing more testing is that the service inserted the content very early in the lifecycle of the Portal.

So lets say you inserted your CSS which fixes a bunch of SAPisms you can't fix using the theme editor. Using the service you'd get output that looked like this:

<link href="/irj/portalapps/com.sap.portal.design.portaldesigndata/themes/portal/tillerTheme/glbl/glbl_nn7.css?7.0.15.0.25" rel="stylesheet"/>
<link href="/irj/portalapps/com.sap.portal.design.portaldesigndata/themes/portal/tillerTheme/prtl_std/prtl_std_nn7.css?7.0.15.0.25" rel="stylesheet"/>

<!-- EPCF: BOB Core -->
<meta content="text/javascript" http-equiv="Content-Script-Type"/>
<script src="/irj/portalapps/com.sap.portal.epcf.loader/script/optimize/js13_epcf.js?7.00001502"/>
<script>
<!--
// Snipped 30 lines of script
</script>
<!-- EPCF: EOB Core -->

<!-- HTML Business for Java, 645_VAL_REL, 477869, Tue Feb 26 13:23:36 EST 2008 -->
<!-- HTMLB: begin VARS -->
<script language="JavaScript">
ur_system = {doc : window.document , mimepath :"/irj/portalapps/com.sap.portal.design.urdesigndata/themes/portal/tillerTheme/common/", stylepath : "/irj/portalapps/com.sap.portal.design.urdesigndata/themes/portal/tillerTheme/ur/", emptyhoverurl : "/irj/portalapps/com.sap.portal.htmlb/jslib/emptyhover.html", is508 : false, dateformat : 1, domainrelaxing : "MINIMAL"};
</script>
<!-- HTMLB: end VARS -->
<link type="text/css" href="http://your/css.css" rel="stylesheet"/>
a bunch of scripts


Oops, you're not after the theme at all. You'd have to use !important all over the place. How does one resolve this?

You enhance the service. In this case I used an example found by decompiling the LAFService, which is the actual theme service. It provided examples on how to implement and use new IResource and IResourceInformation objects. Here's the ExternalResource IResource object:


import com.sapportals.portal.prt.resource.IResource;
import com.sapportals.portal.prt.resource.IResourceInformation;
import java.io.Serializable;

/**
* Describes a resource which resides outside of the Portal landscape
* Such resources could be external Javascript toolkits or CSS pages
*
*/
public class ExternalResource implements IResource, Serializable {
private IResourceInformation mm_resInfo;

public ExternalResource(){}

public IResourceInformation getResourceInformation(){
return mm_resInfo;
}

public void init(IResourceInformation resourceInformation) {
mm_resInfo = resourceInformation;
}

public boolean isAvailable() {
return mm_resInfo != null;
}

}

Doesn't do much does it. All of the work is in the init method and using the ExternalResourceInformation object which is an IResourceInformation:


import com.sapportals.portal.prt.component.IPortalComponentRequest;
import com.sapportals.portal.prt.resource.IResourceInformation;
import java.io.Serializable;

/**
*
* Describes the ResourceInformation required by a resource that lives
* outside of the Portal Landscape.
*/
public class ExternalResourceInformation implements IResourceInformation, Serializable {

private String mm_type;
private String mm_fileName;
private boolean mm_useFileName;
private String mm_URL;

/**
* @param resourceType - you should be using the static types defined in IResource.
* @param URL - The URL to the resource you're trying to add
*/
public ExternalResourceInformation(String resourceType, String URL){
mm_type = resourceType;
mm_URL = URL;
}
/* (non-Javadoc)
* @see com.sapportals.portal.prt.resource.IResourceInformation#getComponent()
*/
public String getComponent() {
return "theNameOfYourService";
}

/* (non-Javadoc)
* @see com.sapportals.portal.prt.resource.IResourceInformation#getType()
*/
public String getType() {
return mm_type;
}

/* (non-Javadoc)
* @see com.sapportals.portal.prt.resource.IResourceInformation#getSource()
*/
public String getSource() {
return "";
}

/* (non-Javadoc)
* @see com.sapportals.portal.prt.resource.IResourceInformation#getURL(com.sapportals.portal.prt.component.IPortalComponentRequest)
*/
public String getURL(IPortalComponentRequest arg0) {
return getURL();
}

public String getURL() {
return mm_URL;
}
}

All of the work here is done in the constructor. You just provide the URL and it will pass that to the IResource which will be included in the PortalResponse.

What's amazing about this is how easy this actually was. What's more amazing is how you need to decompile things to really understand how this works. Most IResources are BaseResource objects. Those objects are more complex since they need to ask the portal to build a URL to the resource you're attempting to include. Therefore, using this method must be faster and lighter on the portal itself as well as the browser.

One more thing to do. Enhance the service objects:

A new method signature in our interface:

public IResource getExternalCssResource(String cssURL);


New methods in our implementation:

public IResource getExternalCssResource(String cssURL) {
IResource ret = null;
ret = getResource(IResource.CSS, cssURL);
return ret;
}

private IResource getResource(String URL) {
IResource er = new ExternalResource();
IResourceInformation ri = new ExternalResourceInformation(resourceType, URL);
er.init(ri);
return er;
}

That's it.

Now, if you create a simple "footer" portal component which does nothing but insert your IResource, you can have your CSS at the bottom of your page.


IPortalComponentResponse componentResponse = (IPortalComponentResponse)pageContext.getAttribute(javax.servlet.jsp.PageContext.RESPONSE);
IHtmlHeadService htmlHeadService =
(IHtmlHeadService) PortalRuntime.getRuntimeResources().getService("com.scotts.tiller.portal.layouts.htmlheadservice.HtmlHeadService");
IResource res = htmlHeadService.getExternalScriptResource("http://your/css.css");
componentResponse.include(componentRequest, res);


Now your stylesheet will appear after your portal theme.

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.

ShareThis