WebSphere Portal 8.5: Customize Ephox Editor

We have been encouraging our clients to use Ephox Editor as their default WCM rich text editor ever since IBM has acquired Ephox Editor OEM licence. Hence it is important for us to know how to:

  • Customize Ephox Editor default menu and toolbar items (for example change the default font size from “pt” to “px”)
  • Apply our custom theme css classes styles in Ephox Editor

Most of the customization will be taking place at the config file, it is located at  <wp_profile>/installedApps/<cell>/wcm.ear/editor-editlive-config.war/config/config.xml.jsp.

Steps to customize Ephox Editor default editor settings:

  1. Read Setting Menu and Toolbar Items tutorial guide first.
  2. Edit the config file accordingly.
  3. Restart the server.

 

Steps to include custom theme css in Ephox Editor:

  1. Read Using CSS in the Applet tutorial guide first.
  2. Edit the config file accordingly.
  3. Restart server.

 

Find out your Class Jar File

Below is the code to find out your class jar file:

CodeSource src = MyClass.class.getProtectionDomain().getCodeSource();

Query Service in WebSphere Portal WCM API

QueryService is a good addition to WCM API. Other than helping to keep the API neat, it also provides a way for developers to do “OR” condition (Disjunction class).

Below is a simple example on how to do a “Like” search on content’s title
(Do take note that Selectors.titleLike method is case-sensitive (as of 8.5) <- hope they will change this in future)


Repository repository = WCM_API.getRepository();
Workspace ws = repository.getWorkspace();
ws.login();

QueryService queryService = ws.getQueryService();
Query query = queryService.createQuery(Content.class);
query.addSelector(Selectors.titleLike("%a%")); //searching for contents' title containing "a"

query.addSort(Sorts.byPublishDate(SortDirection.DESCENDING)); // sort by publish date
ResultIterator resultIter = queryService.execute(query);
while (resultIter.hasNext()) {
 Content content = (Content) resultIter.next();
 // process...
}

But we noticed that there are performance deficits when we try to traverse 400 to 500 contents (~11 sec to complete). The deficit occurs when we call resultIter.next(), which we believe that the API is trying to clone the content’s internal document (including its respective elements).

If you only need to read the contents, maybe you can try to retrieve content using the traditional Workspace.getById(DocumentId<T> id, boolean asReference, boolean loadElements) method (this effectively reduce the time taken to 1 sec on our side).


Repository repository = WCM_API.getRepository();
Workspace ws = repository.getWorkspace();
ws.login();

QueryService queryService = ws.getQueryService();
Query query = queryService.createQuery(Content.class);
query.returnIds(); // get the query services to return DocumentId instead of Document objects
query.addSelector(Selectors.titleLike("%a%")); //searching for contents' title containing "a"

query.addSort(Sorts.byPublishDate(SortDirection.DESCENDING)); // sort by publish date
ResultIterator resultIter = queryService.execute(query);
while (resultIter.hasNext()) {
 Content content = (Content) ws.getById(((DocumentId) resultIter.next()),true,false); // only retrieve a reference Content without loading its elements
 // process...
}

 

Disjunction (Our favourite “OR” condition)

Below is a code snippet on how to retrieve contents based on “OR” categories condition (Disjunction).


Disjunction orCondForCats = new Disjunction();
// tranverse contents if they contains one of the selected categories
for (int i = 0, len = selectedList.size(); i < len; i++) {
 orCondForCats.add(ProfileSelectors.categoriesContains(selectedList.get(i)));
}
query.addSelector(orCondForCats);

WebSphere Portal: Using WCM_API for Selected Virtual Portal

I was writing a live-feed migration portlet (AJAX) for my client when I realized that calling Workspace in Servlet is actually pointing to the base portal (which somehow makes sense). So I went back to the WCM API to check for updates and I found Repository.generateVPContextFromHostname and Repository.executeInVP. The methods look promising but I have no idea how to call it until I found Philip Cheshire’s blog. Credits goes to him!

Do note that objects taken out from executeInVP method will not be able to access their repository and all actions will be performed as if the item belonged to the base portal workspace. For example, I tried to take out the Workspace from executeInVP before I realized that the taken Workspace is “referencing” back to the default repository.

Below illustrate a simple example on how to retrieve Authoring Template’s DocumentId from a specific Virtual Portal

In Our Servlet:


@WebServlet("/ProcessCircular")
public class ProcessCircular extends HttpServlet {
 public ProcessCircular() {
  super();
  try {
   Repository repository = WCM_API.getRepository();
   VirtualPortalContext vpc = repository.generateVPContextFromHostname(MigrationScriptPortlet.VIRUTAL_PORTAL_HOSTNAME);

   // retrieve AT document id
   VPScopedActionAT vpsAT = new VPScopedActionAT();
   repository.executeInVP(vpc, vpsAT);
   DocumentId atId = vpsAT.docId; // this is the part where we take object out from executeInVP
  } catch (WCMException e) {
   // ...
  }
 }
}

 

Then we proceed to create VPScopedActionAT that implements VirtualPortalScopedAction:

public class VPScopedActionAT implements VirtualPortalScopedAction {
 public DocumentId docId;

 @Override
 public void run() throws WCMException {
  Repository repository = WCM_API.getRepository();
  Workspace ws = repository.getSystemWorkspace();
  ws.setCurrentDocumentLibrary(ws.getDocumentLibrary(MigrationScriptPortlet.CONTENT_LIB));
  DocumentLibrary docLib = ws.getDocumentLibrary("Corporate");

  DocumentIdIterator atIter = ws.findByName(DocumentTypes.AuthoringTemplate, MigrationScriptPortlet.CIRCULAR_AT);
  if (atIter.hasNext()) {
   docId = atIter.next();
  }
  repository.endWorkspace();
 }
}

Where is WebSphere Portal Jars?

Below is the jars file location for WebSphere Portal 8.5 SPI.

ilwwcm-api.jar 
Reside at <PortalServer>/wcm/prereq.wcm/wcm/shared/app/ilwwcm-api.jar
It is responsible for the following classes:

  • com.ibm.workplace.wcm.api.*

wp.model.api.jar 
Reside at <PortalServer>/base/wp.model.api/shared/app/wp.model.api.jar
It is responsible for the following classes:

  • com.ibm.portal.state.accessors.pagemode.*

wp.resolver.friendly.api.jar
Reside at <PortalServer>/base/wp.resolver/wp.resolver.friendly.api/shared/app/wp.resolver.friendly.api.jar
It is responsible for the following classes:

  • com.ibm.portal.resolver.friendly.*

wp.services.api.jar
Reside at <PortalServer>/base/wp.services.api/shared/app/wp.services.api.jar
It is responsible for the following classes:

  • com.ibm.portal.services.project.*

Just drop us a comment if you need to find out the jar location of any WebSphere Portal classes.

WebSphere Portal EJPPD0015E: Portlet application manager failed

Scenario

You have created your portlet using RAD and the portlet has been deployed to WebSphere Portal. For some reasons, you need to remove the portlet (not update) and re-deployed the portlet again. Immediately you encountered “EJPPD0015E: Portlet application manager failed” exception in your SystemOut.log file.

Then you begin to trace and discover that the exception occurs at EMF2DOMAdapterImpl_ERROR_0:

Caused by: java.lang.IllegalStateException: EMF2DOMAdapterImpl_ERROR_0
at org.eclipse.wst.common.internal.emf.resource.EMF2DOMAdapterImpl.handleInvalidMultiNodes(EMF2DOMAdapterImpl.java:1402)
at org.eclipse.wst.common.internal.emf.resource.EMF2DOMAdapterImpl.findDOMNode(EMF2DOMAdapterImpl.java:1389)
at org.eclipse.wst.common.internal.emf.resource.EMF2DOMAdapterImpl.primUpdateMOFFeature(EMF2DOMAdapterImpl.java:1572)
at org.eclipse.wst.common.internal.emf.resource.EMF2DOMAdapterImpl.updateMOFFeature(EMF2DOMAdapterImpl.java:1992)
at org.eclipse.wst.common.internal.emf.resource.EMF2DOMAdapterImpl.primUpdateMOF(EMF2DOMAdapterImpl.java:1010)
at org.eclipse.wst.common.internal.emf.resource.EMF2DOMAdapterImpl.updateMOF(EMF2DOMAdapterImpl.java:986)
at org.eclipse.wst.common.internal.emf.resource.EMF2DOMAdapterImpl.primUpdateMOFMultiFeature(EMF2DOMAdapterImpl.java:489)
at org.eclipse.wst.common.internal.emf.resource.EMF2DOMAdapterImpl.updateMOFRootFeature(EMF2DOMAdapterImpl.java:1039)
at org.eclipse.wst.common.internal.emf.resource.EMF2DOMAdapterImpl.primUpdateMOF(EMF2DOMAdapterImpl.java:1006)
at org.eclipse.wst.common.internal.emf.resource.EMF2DOMAdapterImpl.updateMOF(EMF2DOMAdapterImpl.java:986)
at org.eclipse.wst.common.internal.emf.resource.EMF2DOMRenderer.doLoad(EMF2DOMRenderer.java:331)
at org.eclipse.wst.common.internal.emf.resource.TranslatorResourceImpl.basicDoLoad(TranslatorResourceImpl.java:633)
... 78 more

If that is the reason, then you might just be in luck. Go to your WEB-INF/web.xml and ensure that the web.xml file does not contains multiple nodes of the following:

  • session-config
  • welcome-file-list
  • jsp-config
  • login-config
  • locale-encoding-mapping-list

Access Portlet Session as Anonymous User In WebSphere Portal

By default, WebSphere Portal does not enable session tracking for anonymous users, hence the following request.getPortletSession() code snippet will always return null for anonymous users:

private static SideNavigationPortletSessionBean getSessionBean(PortletRequest request) {
 PortletSession session = request.getPortletSession();
 if (session == null)
 return null; // <-- will always return null for anonymous users
 SideNavigationPortletSessionBean sessionBean = (SideNavigationPortletSessionBean) session.getAttribute(SESSION_BEAN);
 if (sessionBean == null) {
  sessionBean = new SideNavigationPortletSessionBean();
  session.setAttribute(SESSION_BEAN, sessionBean);
 }
 return sessionBean;
}

And this set the team wondering on how does the out of the box IBM portlets like Sitemap portlet (PA_SearchSitemapPort.ear) transports the information to JSP? We de-compiled the sitemap.jar and found out that they are transporting the information through the render parameters which is something that matches the paragraph found in Accessing the portlet session on the anonymous page:

If you need to enable session tracking across requests for non-authenticated users, you can do so by setting the public.session parameter in the portal Navigator service configuration or by setting the com.ibm.portal.public.session container run time option in a JSR 286 portlet deployment descriptor. Note that this may result in significantly increased memory consumption. … Instead of using these options, portlets that need to maintain interaction state even for non-authenticated users should use render parameters to keep this information instead of the portlet session, as recommended by the Java Portlet Specification.

 

How to pass information using render parameters?

  1. Under doView method, set the information to RenderRequest’s parameters
    public void doView(RenderRequest request, RenderResponse response) throws PortletException, IOException {
     // ...
    
     request.setAttribute("rootPage", rootPage);
    
     // Invoke the JSP to render
     PortletRequestDispatcher rd = getPortletContext().getRequestDispatcher(JSP_FOLDER + VIEW_JSP);
     rd.include(request, response);
    }
  2. At JSP, retrieve the information as follows:
    <jsp:useBean id="rootElement" class="sg.xxx.xxx.sidenavigation.model.PageElement" scope="request"/>
    <%
     PageElement rootPage = (PageElement)request.getAttribute("rootPage");
     if (rootPage != null) {
      // logic ...
     }
    %>
    

How to Merge PDF Files with PDFBox in Java?

Merging PDF files in java is made easier with Apache PDFBox.

The codes below illustrate how to sort and merge all PDF files found in a particular directory according by their last modified date:

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Comparator;

import org.apache.pdfbox.exceptions.COSVisitorException;
import org.apache.pdfbox.util.PDFMergerUtility;

public class test {
 public static void main(String[] args) throws IOException, COSVisitorException {
  int maxPdf = 1000; // use this to troubleshoot java/lang/OutOfMemoryError exception
  String pdfDirPath = "C:\\circulars_and_notices\\pdfs\\port_marine_notices";

  File pdfDir = new File(pdfDirPath);
  if (pdfDir.isDirectory()) {
   // proceed to crawl thru the folder and merge the pdf according to last mod date
   File[] pdfs = pdfDir.listFiles();
   int cnt = pdfs.length;

   if (cnt > 0) {
    // sort the pdfs by last mod date in desc order
    Arrays.sort(pdfs, new Comparator() {
    public int compare(File f1, File f2) {
     return Long.compare(f2.lastModified(), f1.lastModified());
     }
    });

    if (maxPdf != 0 && maxPdf < cnt) {
     cnt = maxPdf;
    }

    // start add merging sources
    PDFMergerUtility pdfMerger = new PDFMergerUtility();

    // set destination
    pdfMerger.setDestinationFileName("C:\\mergeDocs.pdf");

    // add in pdf source
    for (int i = 0; i < cnt; i++) {
     File file = pdfs[i];
     pdfMerger.addSource(file);
    }

    // merge pdfs
    pdfMerger.mergeDocuments();

   } else {
    System.out.println("Target directory is empty.");
   }
  } else {
   System.out.println("Target is not a directory (" + pdfDirPath + ").");
  }
 }
}

The codes above should works fine in most scenarios. But if you are merging large PDFs files like in my case, then the chances of encountering “java/lang/OutOfMemoryError” exception is high. Of course a quick solution is to increase heap size but this will only be a temporary solution.

Lucky for us, PDFBox offers another alternative way of merging PDFs by storing the PDF streams into a temp file. See below code for illustrations:

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.List;

import org.apache.pdfbox.exceptions.COSVisitorException;
import org.apache.pdfbox.io.RandomAccessFile;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink;
import org.apache.pdfbox.util.PDFMergerUtility;

public class test {
 public static void main(String[] args) throws IOException, COSVisitorException {
  int maxPdf = 1500; // use this to troubleshoot java/lang/OutOfMemoryError exception
  String pdfDirPath = "C:\\circulars_and_notices\\pdfs\\port_marine_notices";

  File pdfDir = new File(pdfDirPath);
  if (pdfDir.isDirectory()) {
   Date startTime = new Date();
   System.out.println("Start time: " + startTime.toString());

   // proceed to crawl thru the folder and merge the pdf according to last mod date
   File[] pdfs = pdfDir.listFiles();
   int cnt = pdfs.length;

   if (cnt > 0) {
    // sort the pdfs by last mod date in desc order
    Arrays.sort(pdfs, new Comparator<File>() {
     public int compare(File f1, File f2) {
      return Long.compare(f2.lastModified(), f1.lastModified());
     }
    });

    if (maxPdf != 0 && maxPdf < cnt) {
     cnt = maxPdf;
    }

    // create a temp file for temp pdf stream storage
    String tempFileName = (new Date()).getTime() + "_temp";
    File tempFile = new File("C:\\" + tempFileName);

    // proceed to merge
    PDDocument desPDDoc = null;
    PDFMergerUtility pdfMerger = new PDFMergerUtility();
    try {
     // traverse the files
     boolean hasCloneFirstDoc = false;
     for (int i = 0; i < cnt; i++) {
      File file = pdfs[i];
      PDDocument doc = null;
      try {
       if (hasCloneFirstDoc) {
        doc = PDDocument.load(file);
        pdfMerger.appendDocument(desPDDoc, doc);
       } else {
        desPDDoc = PDDocument.load(file, new RandomAccessFile(tempFile, "rw"));
        hasCloneFirstDoc = true;
       }
      } catch (IOException ioe) {
       System.out.println("Invalid PDF detected: " + file.getName());
       ioe.printStackTrace();
      } finally {
       if (doc != null) {
        doc.close();
       }
      }
     }

     System.out.println("Merging and saving the PDF to its destination");
     desPDDoc.save("C:\\mergeDoc.pdf");

     Date endTime = new Date();
     System.out.println("Process Completed: " + endTime);
     long timeTakenInSec = endTime.getTime() - startTime.getTime();
     System.out.println("Time taken: " + (timeTakenInSec / 1000) + " secs " + (timeTakenInSec % 1000) + " ms");

    } catch (IOException | COSVisitorException e) {
     e.printStackTrace(); // will encounter issues if it is more than 850 pdfs!!
    } finally {
     try {
      if (desPDDoc != null) {
       desPDDoc.close();
      }
     } catch (IOException ioe) {
      ioe.printStackTrace();
     }
     tempFile.delete();
    }
   } else {
    System.out.println("Target directory is empty.");
   }
  } else {
   System.out.println("Target is not a directory (" + pdfDirPath + ").");
  }
 }
}

[21/4/2014] We have encountered org.apache.pdfbox.exceptions.COSVisitorException: java.lang.NullPointerException when we tried to merge large number of PDFs (<850 pdfs) at once. Hence we decided to revise our codes by merge our PDFs in smaller quantities before merging them as one.

How to Access SharePoint Web Services via Javascript?

Been tasked by my boss with another tight deadline project (understand client’s requirement and developed 2 mobile app within 1 month). Both of the apps required to read data from SharePoint Web Services using Javascript.

Before we proceed, it is important to know what are the types of SharePoint web services that are available to us? Click here for a list of SharePoint services.

Next, we need to understand how to read and call web services’ methods. In the example below, we will demonstrate how we derived and constructed our SOAP message to retrieve list’s items (API: List.GetListItems method):

  1. Always wrapped our SOAP message block with the following header and footer:
    Header

    <soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'>
     <soapenv:Header/>
     <soapenv:Body xmlns='http://schemas.microsoft.com/sharepoint/soap/'>
    

    Footer

     </soapenv:Body>
    </soapenv:Envelope>
    
  2. Construct the web service’s method and parameters exactly as shown in MSDN. In our example, we will input listName and sort the result according to the title:
    <GetListItems>
     <listName>{A4E4AA50-CB01-48C4-9D24-DD6404CD70C1}</listName>
     <query>
      <Query>
       <OrderBy>
        <FieldRef Ascending='TRUE' Name='Title'/>
       </OrderBy>
      </Query>
     </query>
    </GetListItems>
    

Lastly wrapped the SOAP message that you form above and sent it thru using jQuery.ajax method (example like the one below):

$.ajax({
 type: "POST",
 url: url /* the url will be as stated in MSDN Web Reference, for all list related services, it will be http://<Site>/_vti_bin/Lists.asmx*/,
 data: q.join("") /* the soap message that we have construct earlier*/,
 contentType: "text/xml; charset=utf-8",
 dataType: "html" /* can be xml */,
 success: function(xdata){
  result.css("color","green").html($('<div/>').text(xdata).html());
 },
 error: function(a,b,c){
  result.css("color","red").html("Unable to connect ("+c+").");
 }
});

WebSphere Portal: AuthorizationException Is Thrown While Calling Workspace getById Method

Below looks like an innocent block of codes written to retrieve WCM items:

 Content content = null;
 Workspace ws=WCM_API.getRepository().getWorkspace();
 DocumentIdIterator iter = ws.findByPath(path, Workspace.WORKFLOWSTATUS_ALL);

 if(iter.hasNext()) {
  DocumentId docId = iter.nextId();
  if (docId.isOfType(DocumentTypes.Content)) {
   content = (Content)ws.getById(docId,true,true);
  }
 }

But as soon as you try to retrieve WCM items with “User” access, the following “AuthorizationException” will be thrown (even though you are able to view the items in WCM Authoring Portlet):

  • com.aptrix.pluto.security.AuthorizationException
  • com.ibm.workplace.wcm.api.exceptions.AuthorizationException

This is because getById method by default required min “Contributor” rights. In order to resolve the issue, call Workspace.useUserAccess(true) before you call the getById method.