Friday, August 29, 2008

XSLT in SAP Portal's Knowledge Management

One of the features of SAP's Portal application is a Knowledge Management library. Think of it as a JSR-170 application that's not JSR-170 compliant.

One of the challenges of working with this library is the lack of meaningful documentation. It's difficult to parse exactly how it works by just looking at the javadocs. There are some examples of what you can do, but they require strange configurations and occasionally bouncing the Portal. Considering a bounce can take 20-30 minutes rather than seconds, that's not an ideal situation.

Let's examine an idea that the UI/Usability designer has had on my current project. He wanted to simply drop XML into the Portal and use XSLT to give the look and feel he was looking for on individual pages.

Seems like a reasonable request. After much searching, I found this document on how to do that within SAP's KM. Go ahead, give it a read. Seems straight forward except for the bouncing of the server and the fact that it is focused more on XML documents rather than XML for the sake of having HTML look proper within the portal. If you've spend any time with Firebug looking at Portal output, you'll understand where I'm coming from.

Needless to say, this seemed highly difficult to actually implement. I don't want to have to bounce a server for each page we develop or each mistake we might make with the XSLT. Velocity of development would be far too slow.

Therefore I set off on a journey to figure out just how the KM APIs work. I ended up with the following code:


import com.sapportals.portal.prt.component.*;
import com.sapportals.wcm.repository.*;
import com.sapportals.wcm.util.uri.*;
import com.sapportals.wcm.util.usermanagement.*;


import org.jdom.*;
import org.jdom.input.*;
import org.jdom.output.*;
import org.jdom.transform.*;

import javax.xml.transform.stream.*;
import javax.xml.transform.*;

public class KmXmlTransformer extends AbstractPortalComponent {

public void doContent(IPortalComponentRequest request, IPortalComponentResponse response) {
IPortalComponentProfile profile = request.getComponentContext().getProfile();
String xmlDocument = profile.getProperty("XmlDocumentPath");
String xsltDocument = profile.getProperty("XsltDocumentPath");

try {
com.sap.security.api.IUser user=(com.sap.security.api.IUser)request.getUser();
com.sapportals.portal.security.usermanagement.IUser epUser = WPUMFactory.getUserFactory().getEP5User(user);
ResourceContext ctx= new ResourceContext(epUser);

RID xmlRid=RID.getRID(xmlDocument);
IResource xmlResource = (ResourceFactory.getInstance().getResource(xmlRid, ctx));

RID xslRid=RID.getRID(xsltDocument);
IResource xslResource = (ResourceFactory.getInstance().getResource(xslRid, ctx));

SAXBuilder builder = new SAXBuilder();

Document docXml = builder.build(xmlResource.getContent().getInputStream());
Document resultDoc = null;

TransformerFactory transformerFactory = TransformerFactory.newInstance();
Templates stylesheet =
transformerFactory.newTemplates(new StreamSource(xslResource.getContent().getInputStream()));
Transformer xslTransformer = stylesheet.newTransformer();

JDOMResult jdRes = new JDOMResult();
JDOMSource jdSrc = new JDOMSource(docXml);
xslTransformer.transform(jdSrc, jdRes);

resultDoc = jdRes.getDocument();

XMLOutputter outputter = new XMLOutputter(Format.getPrettyFormat());
outputter.output(resultDoc, response.getWriter());

} catch (Exception e) {
e.printStackTrace(System.err);
}
}
}
So what does this do? It uses JDOM and an XSLT engine to take an XML file in the repository and transform it with an XSLT file in the repository. It uses properties (XmlDocumentPath, XsltDocumentPath) to define where in the KM those files are. These are configurable so that you can simply reuse this object and just modify the properties to choose different files.

There are some issues with the code. Obviously, it's limited to a single transform in its current form. It also uses a deprecated API in the first three lines of the try block. com.sapportals.portal.security.usermanagement.IUser is a deprecated class. Unfortunately you can't create a ResourceContext without one. Nice professionalism by SAP to not offer an alternative.

Other than those limitations, it works pretty darn well. The only question left to analyze is how well this scales.

Saturday, July 26, 2008

Updating Javascript in SAP Portal

Quick reminder to those of you who've followed my posts on Google Analytics.

Be sure that if you're updating a Javascript file that you take the time to update the version of the file. SAP Portal does this using a query string like construct after the javascript file for its OOTB components.

You can do a similar construct for your files by simply embedding a version into the file name. Following the previous GA example, you can simply update the file name to be ga-split-2.0.1.js

Of course, the next step is to update the PortalComponent to pull in the correct version of the file.

So why do you need to do this? Depending upon how your Portal and Load Balancers are setup with caching and expires headers, you won't push the correct version of the javascript file to the browser unless you update the file name! Why? It's common practice to set Expires headers in the far future and set the browser to cache the javascript file. If your setup is doing that, then any changes you make the the original JS file will not be pulled in unless your users happened to clear their browser cache. Since the chance of your entire user community pulling that off is miniscule, the only way to force them to get the new version of the file is to update the file name!

Also, be sure you head over and look at what Spyvee did to inspire these posts over at NetweaverCentral.

Thursday, July 17, 2008

Enhanced Google Analytics in SAP Portal

If you happened to follow my post on integrating Google Analytics with SAP Portal, and attempted to implement it, you may have found some challenges with the reports. More specifically:
  • If you're using the Light Framework (or derivation), all of your URLs are unreadable. They don't describe what is going on in the page since the Portal uses GUIDs as a URL parameter to gather the appropriate page.
  • If you're using the Default Framework (or derivation), you only show hits on your entry point. Which is great for gathering browser information, but not so much for following user activity.
  • In order to resolve this problem, you decide to add the Analytics iView to other pages in your Portal. Now all of your URLs are really unreadable. In fact, you will find that you receive multiple URLs for the same page, where the only difference is the windowID in the query string. This makes the data flat out unusable.
So, what to do?

There is a single fix that resolves both issues. The fix involves asking Portal where in the Navigation Tree you are. First, add in some imports to your code:

import com.sapportals.portal.navigation.INavigationNode;
import com.sapportals.portal.navigation.NavigationEventsHelperService;
import com.sapportals.portal.prt.runtime.PortalRuntime;
import com.sapportals.portal.prt.pom.IEvent;


In order to use these, you'll need to get the following JARs and import them into your project:
  • com.sap.portal.navigation.api_service_api.jar
  • com.sap.portal.navigation.helperservice_api.jar
  • com.sap.portal.navigation.helperservice_core.jar
One of the methods you can override in an AbstractPortalComponent is doOnNodeReady(). This method is called once the PortalNode has been constructed. At this point, the node can ask the Portal for information. The method is implemented as follows:

    
protected void doOnNodeReady(IPortalComponentRequest request,IEvent arg1) {
// Get the service to access the Navigation information
NavigationEventsHelperService helperService =(NavigationEventsHelperService) PortalRuntime.getRuntimeResources().getService("com.sap.portal.navigation.helperservice.navigation_events_helper");
// Get your current location in the navigation tree
INavigationNode navTargetNode = helperService.getCurrentLaunchNavNode(request);
StringBuffer fullPath = new StringBuffer(navTargetNode.getTitle(Locale.ENGLISH));
// After stashing the title of the node, get the node's parent and loop
// until you've reached the top node. Stash each parent's name and build
// a navigation "path" for use later.
INavigationNode aParent = helperService.getParentNode(navTargetNode, request);
while (aParent != null && !aParent.getTitle(Locale.ENGLISH).equals("")) {
fullPath.insert(0, aParent.getTitle(Locale.ENGLISH) + "/");
aParent = helperService.getParentNode(aParent, request);
}
// store the path in a member variable that can be used inside doContent()
pageTitle = fullPath.toString();
}

Once you've created this path, you can then use it to track the page properly. Inside ga-split-2.js, you should remove the final line which calls pageTracker._trackPageview() Instead, you'll create a set of response.write() calls to use the pageTitle object and write a new snippet of code on each specific page.

The end of doContent will look as follows:


response.include(request, googleAnalyticsDataResource2);
response.write("<script type=\"text/javascript\">\n");
response.write("pageTracker._trackPageview(\""+ pageTitle +"\");\n");
response.write("</script>");

response.setContentType(PortalComponentContentType.HTML);


How to use the enhancements:

If you're in the light framework, it will just work. You can keep the code at the framework level and it will work on every page in the portal. If you're in the default framework, you'll need to add the code to each page that you want to track. You may want to remove the code from the framework and just track pages. The resulting reports will be far more readable and much better for your business users and portal sponsors who would likely be consuming the data (and pretty graphs) that Google Analytics provides.

ShareThis