ShowTable of Contents
Introduction
It is often helpful, for various reasons, to use JavaScript
TM within IBM Lotus Web Content Management (hereafter called "WCM") component design. WCM sorts search results by relevancy or date. However, the date by which results are sorted is not necessarily the last modified date; instead, it's a different date transparent to WCM. In this article, we use JavaScript to add functionality that provides a mechanism to sort WCM search results by the last modified date of the content that's being returned.
To do this, we use JSON to create a JavaScript object from the results returned by a WCM search component. We don't get into a technical discussion of what JSON is, as there are many resources available for this. Instead, we present a full working example that can be modified for different uses.
Basic setup
In our basic setup we have a piece of WCM content that is responsible for processing search queries for WCM content. This content then leverages a WCM search component, as well as a couple of WCM JSP components. All the necessary component designs, and the JSPs for the JSP components, are included in this article. We also break down the search component design, since it leverages the JSON object.
The WCM components that we create are:
- Search – JSON. A WCM search component.
- HTML – SearchResultsJSON. A WCM HTML component responsible for marking up the search results.
- JSP – renderSearchResultsJSON.jsp. A WCM JSP component responsible for building the JSON object based on the results from the Search – JSON search component.
- HTML – SearchForm. A WCM HTML component responsible for generating the search form.
Also included here is the export of the library containing the basic WCM setup necessary, including workflow, workflow action, workflow stage, site, site area, authoring template, presentation template, and content.
Before detailing the WCM assets, however, we need to create an IBM WebSphere® Portal page to display our search and results and give it a friendly URL mapping, to allow easy access for users.
Creating a WebSphere Portal page
To do this:
- Log into WebSphere Portal server and navigate to the Portal Administration page.
- Under Portal User Interface, click Manage Pages.
- Navigate down to where you want to create the page, and click New Page (see figure 1).
Figure 1. Creating a new page

3. Enter SearchResultsDisplay for the name and Unique name, then click OK.
4. Navigate to Portal Settings > URL Mapping, and click New Context.
5. Enter SearchResultsDisplay for the label, click OK.
6. Next to the SearchResultsDisplay link, click the Globe icon to edit the mapping.
7. Navigate to the Portal page you created, select the radio button, and click OK
(see figure 2).
Figure 2. Newly created page
Finally, we deploy a WCM Local Rendering Portlet to this page:
- Navigate to the new page and then click Edit Page Layout.
- Click the Add Portlet button, and add a WCM Local Rendering Portlet to it.
- Once we have the search content created, we will come back and add that content to this page.
Creating a WebSphere Portal Search Collection
To provide the functionality to crawl the WCM content and return the results within WCM, we must create a Search Collection. To do this:
- Log into WebSphere Portal server and navigate to the Portal Administration page.
- Under Search Administration, select Manage Search.
- Click Search Collection, then the New Collection button (see figure 3).
Figure 3. Creating a new collection
4. For the Location and Name of collection, enter CMKTestCollection.
5. Under Select Summarizer, make sure to select Automatic. (This is because we'll be using the summary of each item within the search component design.) Click OK.
That's all we need for now. Let's proceed to walk through the WCM assets for this sample.
Search – JSON
This is a WCM search component, used to process the search query from the request by SearchLibrary/SearchResultsSite/SearchResultsSiteArea/SearchResults. It then marks up the results in a particular way, to be used by the JSP component, JSP
– rendersearchresultsjson.jsp. The design is shown in listing 1.
Listing 1. Design for Search – JSON
url!!!!!<AttributeResource attributeName="url" separator=","/>:::parentcontentpath!!!!!<AttributeResource attributeName="parentcontentpath" separator=","/>:::name!!!!!<AttributeResource attributeName="title" separator=","/>:::summary!!!!!<AttributeResource attributeName="summary" separator=","/>:::parentid!!!!!com.ibm.workplace.wcm.api.WCM_SiteArea/<IDCmpnt context="autoFill" type="sitearea" field="title"/>/<IDCmpnt context="autoFill" type="sitearea" field="id"/>/PUBLISHED:::parentname!!!!!<IDCmpnt context="autoFill" type="sitearea" field="title"/>:::lastmod!!!!!<HistoryCmpnt context="autoFill" type="content" field="lastmodifieddate" format="DATE_MEDIUM"/>:::authors!!!!!<IDCmpnt context="autoFill" type="content" field="authors" separator=","/>
The important parts of this code are:
- !!!!! is used when the jsp processes the results. It uses this to split up the results into specific items.
- ::: is used when the jsp processes the results. It uses this to split up the results into specific items.
You end up with name value pairs; for instance, “url” is the name, and value is the result of the tag:
<AttributeResource attributeName="url" separator=","/>
These attributes are later used by the JSP to create the JavaScript object that's used by HTML – SearchResultsJSON to generate the HTML of the results.
For the rest of the search component design, enter the following for Separator:
WCM_SEPARATOR
This is used by the JSP component to split each of the search results later.
For the no-results design, enter the following:
{
identifier: 'name',
label: 'name'
items: []}
Thus, when there are no results, the JavaScript object does not throw JavaScript exceptions.
Finally, for the search collection drop-down selection, select the search collection you created above.
HTML – SearchResultsJSON
This HTML component is used to generate the html for the results. It creates a JavaScript object based on the results from the JSP – renderSearchResultsJSON.jsp results. The jsp is responsible for generating the JavaScript information based on the results returned by Search – JSON.
There is a lot of HTML involved in this component; let's break down the design into pieces.
Listing 2 shows the code used for the design of the paging results when the search results are rendered.
Listing 2. Design of paging results
<style type="text/css">
@nowiki@1paginationNumber {
border:1px solid;
border-color:black;
padding:4px;
text-decoration:none;
}
</style>
The code in listing 3 calls the component HTML – SearchForm, which basically renders the search form in the results page.
Listing 3. Call the HTML– SearchForm
<!-- dynamically create the search form using JSP component -->
<Component name=”HTML - SearchForm”/><br>
<span>
Listing 4 is the code to create the JavaScript data object from which we will retrieve the results. It calls the jsp – rendersearchresultsjson.jsp to get its information. The object is searchResultsJSON, which is basically a JSON object, which we then use to pull data from the result set and render it out. Note that, in the typeMap we are using lastmod as the “primary key”. That way, the results can be sorted by the last modified date (which is stored in lastmod in the JSON object by the jsp).
Listing 4. Create JavaScript data object
<script>
dojo.require("dojo.data.ItemFileReadStore");
// store the current page so we know to disable a link
var currentPage = 0;
// retrieve the JSON objects from the JSP that parses the search results
var searchResultsJSON = new dojo.data.ItemFileReadStore({data:<Component name="jsp - rendersearchresultsjson.jsp"/>,typeMap: {
"lastmod": {
type: Date,
deserialize: function(value){
return dojo.date.stamp.fromISOString(value);
}
}
}});
Listing 5 sets up the JavaScript init method, which gets called when the page is loaded. It includes some variables and some methods. The variables are used throughout, and the sortAttributes is used to return the results sorted by the last modified date.
Listing 5. Set up JavaScript init method
// the init method will add all the necessary methods to the page
function init() {
// the page size is the # of items to display per page
var pageSize = 10;
// request used for retrieval
var request = null;
// whether or not we're out of items
var outOfItems = false;
// total items returned by the query
var totalItems = -1
// the sort attributes to use for the query
var sortAttributes = [{attribute: "lastmod", descending: true}];
The clearOldList method (see listing 6) is called whenever items are re-retrieved from the JSON object, basically when the paging is used. It clears the current items being displayed in the results, as well as sets up the display of the number of results returned for the query.
Listing 6. clearOldList method
// clearOldList method is called in the onBegin in order to clear the table and
// populate the display of the number of items that matched the search
var clearOldList = function(size, request)
{
var list = dojo.byId("resultDisplay");
if(list) {
while(list.firstChild)
{
list.removeChild(list.firstChild);
}
}
// totalItems come from the size param from the onBegin
totalItems = size;
// searchTerm is a div defined in the JSP that creates the search form
// this code sets the number of results and the search query
var searchTermDiv = dojo.byId("searchTerm");
searchTermDiv.innerHTML = totalItems+' - '+searchQuery;
}
Listing 7 shows the method that is invoked when a page number link is clicked. Essentially, it retrieves the items from the JSON object for the page that has been clicked. It also sets the necessary values on the request object for the retrieval, such as the number of items to return (which is 10, unless there are less than 10 items left), the value at which to start the retrieval, and the sort parameter.
Listing 7. Method invoked when page number link is clicked
// Define a function that will be connected to a 'number' link
var gotoPageNumber = function(newPageNumber) {
currentPage = newPageNumber;
// since the page numbers are 1,2,3 etc
// and the array is 0 based
// we want (page# -1 )* pageSize
// for 1 for example
// (1 -1 ) * 10 gives us 0 for 0 through 9
// (2 -1 ) * 10 gives us 10 for 10 through 19
// no more above, the values are passed as 0-x now, not 1
var startValue = (newPageNumber) * pageSize;
request.onBegin = clearOldList;
request.sort = sortAttributes;
request.start = startValue;
// if there are less than pageSize on this page, have to set the count
if((totalItems - startValue) < pageSize)
{
request.count = (totalItems - startValue);
}
else
{
request.count = pageSize;
}
searchResultsJSON.fetch(request);
}; // end function
The onNext and onPrevious methods (see listing 8) are attached to the Next and Previous buttons in the paging results. It basically does the same thing as the method to get by page number, but in this case the page number is generated based on your current page.
Listing 8. onNext and onPrevious methods
//Define a function that will be connected to a 'next' button
var onNext = function() {
if(!outOfItems) {
currentPage = currentPage +1;
var newPageNumber = currentPage;
var startValue = (newPageNumber) * pageSize;
request.onBegin = clearOldList;
request.sort = sortAttributes;
request.start = startValue;
// if there are less than pageSize on this page, have to set the count
if((totalItems - startValue) < pageSize)
{
request.count = (totalItems - startValue);
}
else
{
request.count = pageSize;
}
searchResultsJSON.fetch(request);
} // end if
}; // end function
//Connect this function to the onClick event of the 'next' button
//Done through dojo.connect() generally.
//Define a function will be connected to a 'previous' button.
var onPrevious = function(){
if (request.start > 0){
currentPage = currentPage -1;
var newPageNumber = currentPage;
var startValue = (newPageNumber) * pageSize;
request.onBegin = clearOldList;
request.sort = sortAttributes;
request.start = startValue;
// if there are less than pageSize on this page, have to set the count
if((totalItems - startValue) < pageSize)
{
request.count = (totalItems - startValue);
}
else
{
request.count = pageSize;
}
searchResultsJSON.fetch(request);
} // end if
}; // end function
The itemsLoaded method (see listing 9) is responsible for writing the html for the results to the page. Basically, it renders the links for the search results, as well as the paging numbers and the Next/Previous buttons. To render the links, it parses through the JavaScript JSON object and generates the information for each individual result by generating html tags in the DOM object and pushing the information to the tags. It then creates the links for previous and next page, as well as numbered links for the paging information.
Listing 9. itemsLoaded method
//Define how we handle the items when we get them
// this is the code that writes the results to the page
var itemsLoaded = function(items, request) {
var list = dojo.byId("resultDisplay");
if (items.length < pageSize){
//We have found all the items and are at the end of our set.
outOfItems = true;
} // end if
else{
outOfItems = false;
} // end else
var i;
for (i = 0; i < items.length; i++) {
var item = items[i];
var ptag = document.createElement("P");
// create the link
var anchor = document.createElement("A");
// this is the code that changes the link if its
// intended for an AJPERES request. We want those in a new window
// now we want to change the URL from the PDF to the content that contains the PDF instead
var linkTextText = searchResultsJSON.getValue(item, "name");
var hrefWCMValue = searchResultsJSON.getValue(item, "url");
var hrefPortalValue = '/wps/myportal/SearchResultsDisplay?WCM_GLOBAL_CONTEXT='+hrefWCMValue;
if(hrefWCMValue.indexOf("AJPERES") > 0)
{
var linkSplit = hrefPortalValue.split("WCM_GLOBAL_CONTEXT=");
hrefPortalValue = linkSplit[1];
anchor.setAttribute("target","_new");
}
anchor.setAttribute("href",hrefPortalValue);
var linkText = document.createTextNode(linkTextText);
anchor.appendChild(linkText);
ptag.appendChild(anchor);
// add the summary display
ptag.appendChild(document.createTextNode(' - '+searchResultsJSON.getValue(item, "summary")));
ptag.appendChild(document.createElement("BR"));
// create the link to the parent. We'll build this
// from the URL to the item parent
anchor = document.createElement("A");
hrefValue = '/wps/myportal/SearchResultsDisplay?WCM_GLOBAL_CONTEXT='+searchResultsJSON.getValue(item, "parentid");
linkText = document.createTextNode(searchResultsJSON.getValue(item, "parentname"));
anchor.setAttribute("href",hrefValue);
anchor.appendChild(linkText);
ptag.appendChild(anchor);
// create the display for last modified date and the author names
var dateSpan = document.createElement("span");
dateSpan.className="date";
var dateText = document.createTextNode(searchResultsJSON.getValue(item, "lastmod")+','+searchResultsJSON.getValue(item, "authors"));
dateSpan.appendChild(dateText);
ptag.appendChild(dateSpan);
ptag.appendChild(document.createElement("br"));
list.appendChild(ptag);
} // end for
// now add the number links
var x=0;
// while the x value * the page size is less than total items, add another link
var resultsElement = document.createElement("div");
resultsElement.className = "SSPagingResults";
var hasResults = false;
// check for previous button
if(currentPage != 0 && totalItems > pageSize)
{
var prevPageLink = document.createElement("A");
var prevPageLinkText = document.createTextNode("Previous Page");
var previousPage = currentPage -1;
prevPageLink.setAttribute("href","javascript:gotoPageNumber("+previousPage+")");
prevPageLink.appendChild(prevPageLinkText);
prevPageLink.id="paginationNumber";
resultsElement.appendChild(prevPageLink);
resultsElement.appendChild(document.createTextNode(' '));
}
while((x+1)*pageSize <= totalItems)
{
hasResults = true;
var pageLink = document.createElement("A");
var pageLinkText = document.createTextNode(x+1);
pageLink.setAttribute("href","javascript:gotoPageNumber("+x+")");
pageLink.appendChild(pageLinkText);
if(currentPage != (x))
{
pageLink.id="paginationNumber";
}
resultsElement.appendChild(pageLink);
resultsElement.appendChild(document.createTextNode(' '));
x++;
}
// now check for next page button
if((currentPage+1)*pageSize < totalItems)
{
var nextPageLink = document.createElement("A");
var nextPageLinkText = document.createTextNode("Next Page");
var nextPage = currentPage + 1;
nextPageLink.setAttribute("href","javascript:gotoPageNumber("+nextPage+")");
nextPageLink.appendChild(nextPageLinkText);
nextPageLink.id="paginationNumber";
resultsElement.appendChild(nextPageLink);
resultsElement.appendChild(document.createTextNode(' '));
}
if(hasResults)
{
list.appendChild(resultsElement);
}
} // end function
Once all that is created, we append the DOM information to the html, using this line:
list.appendChild(resultsElement);
where the list is this:
var list = dojo.byId("resultDisplay");
which is the resultDisplay div in the html.
The remaining code information is shown in listing 10. This code sets the field by which to sort the results, as well as the process used to compare the fields. In this case, we use Date comparisons for the last modified date information from each result. Finally, we retrieve the initial results and then push the init method to be invoked when the page is loaded.
Listing 10. Set the fields by which to sort the results
//Callback for if the lookup fails.
function fetchFailed(error, request) {
alert("lookup failed.");
alert(error);
}
// create the mechanism for comparing the last mod date
searchResultsJSON.comparatorMap = {};
searchResultsJSON.comparatorMap["lastmod"] = function(a,b) {
var ret = 0;
var date1 = new Date(a);
var date2 = new Date(b)
if (date1 > date2)
{
ret = 1
}
if (date1 < date2)
{
ret = -1
}
return ret;
};
//Do the initial request. Without a query, it should just select all items. The start and count limit the number returned.
request = searchResultsJSON.fetch({onBegin: clearOldList,onComplete: itemsLoaded, start: 0, count: pageSize, sort: sortAttributes});
// set the gotoPageNumber method to the window so the onclick for the numbered links works
window.gotoPageNumber = gotoPageNumber;
}
//Set the init function to run when dojo loading and page parsing has completed.
dojo.addOnLoad(init);
</script>
<span id="resultDisplay">
</span>
JSP – renderSearchResultsJSON.jsp
This JSP component is configured to the /jsp/html/renderSearchResultsJSON.jsp, which is stored in the WCM Local Rendering Portlet. This JSP processes the results from the Search component Search –JSON. It's called by the HTML – SearchResultsJSON to get the necessary JavaScript object. The jsp is included, but here's the important part:
ContentComponent searchComp;
Content thisContent = context.getContent();
searchComp = thisContent.getComponentByReference("SearchCompRef");
This code gets the Search Component from the content, which has it populated in a ComponentReference named SearchCompRef. The rest of the JSP just renders this search component and creates the necessary JSON object from the results.
HTML – SearchForm
This HTML component holds the HTML for the search form:
<form method="post" action="?WCM_GLOBAL_CONTEXT=/wps/wcm/myconnect/SearchLibrary/SearchResultsSite/SearchResultsSiteArea/SearchResults" id="searchform" name="searchform" >
<input type="text" name="search_query"/>
<input type="submit" value="Search"/>
</form>
The important piece here is:
action="?WCM_GLOBAL_CONTEXT=/wps/wcm/myconnect/SearchLibrary/SearchResultsSite/SearchResultsSiteArea/SearchResults"
This tells the form to submit to the current page and to push the portlet to display the search results.
Conclusion
Finally, now that all of the WCM assets are created, the last step is to make one or more of your sites searchable. To do this:
- Navigate to the WCM site within the WCM Authoring portlet.
- Edit the site, marking the site as searchable (see figure 4).
- Select the CMKTestCollection collection we created earlier, and enter the admin username and password; save the site.
Figure 4. Making a site searchable
So now the search collection will be updated with a URL to crawl the WCM content. Once the collection has finished crawling the content, you can start issuing searches against the collection to see your results.
The design that's attached to this article is fairly simple and is used merely to prove the concept. Although it does provide client-side paging and the ability to sort the results by a parameter that's not usually used to sort by, the functionality provided is pretty limited. It can, however, be easily incorporated into the Dojo JavaScript libraries to provide many different functional changes.
If you do not want to create all the WCM artifacts from scratch, a WCM library export has also been provided as part of the attachment. Here are the high-level steps to import this library:
- Download the library.zip file that's included in the .zip file attached to this document.
- Extract it to a temporary location on your Portal Server filesystem.
- Modify the /WPHOME/wp_profile/PortalServer/wcm/config/wcm60_conf.xml file, setting the value for ImportDir to point to the directory where you extracted the library.zip.
- Modify the same file for WebLib, setting it to “SearchLibrary”.
- Run the ConfigEngine.sh/.bat import-wcm-library task.
- Restart WebSphere Portal.