Sunday, March 2, 2014

Adding Custom search results in WebCenter Search

WebCenter provides a Search task-flow, which can be used to search in all or specific webcenter services. Common services supported by WebCenter are Announcement, Discussion, Events, Related Resources, Documents, Page, People Connection, Tags, Blogs etc. 

Webcenter global serach can by default perform search in above mentioned services.


Our aim here is to perform webcenter search in other places as well. For example we can have data in our custom table and we want that also to appear in webcenter search. For this example I will try to show data from Employee table in webcenter search.

 Here are the steps
1. Have employee table, populate data in that table.
2. Have ADF Model (EO/VO/AM) which can query Employee table.
3. Add bindings to a page
4. Register your service
5. Implement QueryManager and QueryExecuter classes.
6. Create a page EmployeeDetailPg.jspx which should show the detail of an employee.
7. Use webcenter provided Search task-flow to perform global search

============================================================
1. Have Employee table and populate data in that table: For this demo I am using HR.EMPLOYEES table.

2. Have ADF Model (EO/VO/AM) which can query employee table:
 
   a. VO should have a view criteria as 'SearchEmployee', which can search employee data based on first_name, middle_name, last_name, email, phone_number as




   b. Add this VO in AM as View Instance.


  c. Create EmployeeResourceRow class which represents every employee row in search.

    package com.san.model.search.employee;

import java.io.Serializable;

public class EmployeeResourceRow implements Serializable{
    private static final long serialVersionUID = 1090909090909L;
    private String resourceId;
    private String title;
    private String creator;
    private String lastModified;
    private int size;
    /**Constructor to create an instance of ResourceRow.
     * @param resourceId
     * @param title
     * @param creator
     * @param lastModified
     * @param size
     */
    public EmployeeResourceRow(String resourceId, String title,    String creator, String lastModified,    int size){
        this.resourceId = resourceId;
        this.title = title;
        this.creator = creator;
        this.lastModified = lastModified;
        this.size = size;
    }

    public void setResourceId(String resourceId) {
        this.resourceId = resourceId;
    }

    public String getResourceId() {
        return resourceId;
    }


   .... other getters/sertters

}

  d. Generate AMImpl class and add a method getSearchResult, which should query employeeVO and create EmployeeResourceRow based on each vo row.


    public List getSearchResult(String keyword){
       
      
        ViewObjectImpl vo = this.getEmployeeVO();
        if(keyword != null){
            vo.appendViewCriteria(vo.getViewCriteria("SearchEmployee"));
            vo.setNamedWhereClauseParam("bind_searchKeyword", keyword);
        }
        vo.executeQuery();

        List searchResultList = new ArrayList();
        while (vo.hasNext()) {
            Row resRow = vo.next();
            String resourceId = (String)resRow.getAttribute("EmployeeId").toString();
            String title = (String)resRow.getAttribute("FirstName");
            searchResultList.add(new EmployeeResourceRow(resourceId, title, null, null, 0));
        }
        return searchResultList;
    }


3. Add bindings (Drop AM method getSearchResult on any page's  page-definition file): We are going to use this page def files binding-container to execute AM method. For that purpose you can create a test page and add getSearchResult method on its Page-Def file. Page-Def will look something like

 <bindings>
    <methodAction id="getSearchResult"
                  InstanceName="HRAppModuleDataControl.dataProvider"
                  DataControl="HRAppModuleDataControl"
                  RequiresUpdateModel="true" Action="invokeMethod"
                  MethodName="getSearchResult" IsViewObjectMethod="false"
                  ReturnName="data.HRAppModuleDataControl.methodResults.getSearchResult_HRAppModuleDataControl_dataProvider_getSearchResult_result">
      <NamedData NDName="keyword" NDType="java.lang.String"/>
    </methodAction>
  </bindings>

Open databinding.cpx file and note down page-def entry.


 <pageMap>
        <page path="/oracle/webcenter/portalapp/pages/PortalSearchPg.jspx"
                    usageId="com_san_ui_PortalSearchPgPageDef"/>

</pageMap>
 <pageDefinitionUsages>
         <page id="com_san_ui_PortalSearchPgPageDef"
          path="oracle.webcenter.portalapp.pages.PortalSearchPgPageDef"/>

</pageDefinitionUsages>

usageid com_san_ui_PortalSearchPgPageDef will be used to execute AM method programatically.


4. Register your service:
To register a service in webcenter you need to add following entry in service-definition.xml file. This file is located under <application-folder>/.adf/META-INF
 <?xml version="1.0" encoding="UTF-8" ?>
<definitions xmlns="http://xmlns.oracle.com/webcenter/framework/service">

 <service-definition id="com.san.searchapp.employee" version="11.1.1.0.0">
   <resource-view urlRewriterClass="com.san.portal.search.employee.EmployeeDetailURL">
        <parameters>
            <parameter name="resourceId" value="{0}"/>
        </parameters>
    </resource-view>
        <!--resource-bundle-class>mycompany.myproduct.myapp.mycomponent.resource.ComponentMessageBundle</resource-bundle-class-->
    <name-key>Employee Search</name-key>
    <description-key>Employee Search</description-key>
    <!--icon>/mycompany/myproduct/myapp/mycomponent/component.png</icon-->
    <search-definition xmlns="http://xmlns.oracle.com/webcenter/search"
                       id="com.san.searchapp.employee.query" version="11.1.1.0.0">
      <query-manager-class>com.san.portal.search.employee.EmployeeQueryManager</query-manager-class>
    </search-definition>
  </service-definition>
</definitions> 

NOTE down we have mentioned a query-manager class com.san.portal.search.employee.EmployeeQueryManager. 
We need to implement this class and few other class to make it working. Whenever user performs a search, webcenter will look for all services registered in service-definition.xml file and if any query-manager-class is specified, it will use that class to start search operation for that service. 


5. Implement QueryManager and QueryExecuter classes: 
   a. Write a query manager class: Query manager class is a class which is specified while registering a service. This class should implement oracle.webcenter.search.QueryManager. QueryManager will force you to implement a method createRowQueryHandler. This is the method, which will be executed by webcenter framework when a user performs search.
      Here is the code.

package com.san.portal.search.employee;

import java.util.List;

import oracle.webcenter.search.QName;
import oracle.webcenter.search.Query;
import oracle.webcenter.search.QueryExecutor;
import oracle.webcenter.search.QueryManager;
import oracle.webcenter.search.Row;

public class EmployeeQueryManager  implements QueryManager{
    public EmployeeQueryManager() {
        super();
    }
   
    public QueryExecutor<Row> createRowQueryHandler(Query query,
    List<QName> columns)
    {
        return new EmployeeQueryExecutor(query, columns);
       
    }
}

b.  Query  Executor class:
QueryManager (EmployeeQueryManager) returns a QueryExecutor. We need to create a class EmployeeQueryExecutor which implements QueryExecutor. Class should look as

package com.san.portal.search.employee;
import com.san.model.search.employee.EmployeeResourceRow;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import java.util.Map;
import java.util.logging.Level;
import oracle.adf.model.BindingContext;
import oracle.adf.model.binding.DCBindingContainer;

import oracle.adf.share.logging.ADFLogger;
import oracle.jbo.uicli.binding.JUCtrlActionBinding;
import oracle.webcenter.framework.resource.ResourceRow;
import oracle.webcenter.search.ComplexPredicate;
import oracle.webcenter.search.QName;
import oracle.webcenter.search.Query;
import oracle.webcenter.search.QueryExecutor;
import oracle.webcenter.search.QueryHandler;
import oracle.webcenter.search.QueryResult;
import oracle.webcenter.search.Row;
import oracle.webcenter.search.TextPredicate;

public class EmployeeQueryExecutor implements QueryExecutor<Row>{
   private static final int PRESET_ESTIMATED_RESULT_COUNT = 20;
    private static final int PRESET_VALUES_BATCH_SIZE = 5;
    private static final int MAX_RESULT_LIMIT = 100;
    private int m_resultStart;
    private int m_resultLimit;
    private Query m_query;
    private List<QName> m_columns;
    private Map<String, String> m_properties;
    private static ADFLogger _logger =   ADFLogger.createADFLogger(EmployeeQueryExecutor.class);
   
    public EmployeeQueryExecutor(Query query, List<QName> columns)
    {
        m_query = query;
        m_columns = columns;
        m_properties = new HashMap<String, String>();
        m_properties.put(QueryHandler.TITLE, "Sample");
    }
   
   
  
    private void createRows(List<Row> rows, int batchRepeat)
    {
        Map param = new HashMap();        

        ComplexPredicate cp = (ComplexPredicate)m_query.getPredicate();
        TextPredicate tp = (TextPredicate)cp.getChildren().get(0);
        String searchQuery = tp.getTextQueryString();
        if(searchQuery == null)
            return;
        param.put("keyword",searchQuery);       
   
        BindingContext bc = BindingContext.getCurrent();
                DCBindingContainer dc =  bc.findBindingContainer("com_san_ui_PortalSearchPgPageDef");
                JUCtrlActionBinding lBinding =(JUCtrlActionBinding)dc.findCtrlBinding("getSearchResult");
                lBinding.getParamsMap().putAll(param);
                lBinding.invoke();
                List<EmployeeResourceRow> allResources = (List<EmployeeResourceRow>)lBinding.getResult();

               
       
        
        Iterator it = allResources.iterator();
       
        int i = 1;
        while(it.hasNext()){
            if(i > m_resultStart){
                EmployeeResourceRow resRow = (EmployeeResourceRow) it.next();
                String resourceId = (String)resRow.getResourceId();
                String title = (String)resRow.getTitle();
                String creator = (String)resRow.getCreator();
                String lastModifiedOn = (String)resRow.getLastModified();
                rows.add(new EmployeeRow(resourceId,title,creator,lastModifiedOn,null));
              
            }
            else{
                it.next();
            }
           
            i++;
              
           
        }   
    }
   
   
    public QueryResult<Row> execute()    {
        List<Row> rows = new ArrayList<Row>();
        int realLimit = Math.min(MAX_RESULT_LIMIT, m_resultLimit);
        
        createRows(rows, (realLimit / PRESET_VALUES_BATCH_SIZE));
        // Create the QueryResult
        QueryResult<Row> ret = new EmployeeQueryResult(rows.iterator());
        ret.getProperties().put(QueryResult.ESTIMATED_RESULT_COUNT,
                                new Integer(rows.size()).toString());
        return ret;
    }
   
    /**
    * Remember the result start.
    */
    public void setResultStart(int resultStart){
      m_resultStart = resultStart;
    }
    

    public void setResultLimit(int resultLimit){
        m_resultLimit = resultLimit;
    }
   
    public Map<String, String> getProperties(){
        return m_properties;
    }
}

Highlighted text shows how to execute a binding method using java code. I have shown executing a binding because this way we can execute anything for which we can create a binding. It might be AM method, a webservice call, a java method call etc etc.

c. Create EmployeeRow class which should implement Row.

package com.san.portal.search.employee;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;

import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.logging.Level;
import static oracle.webcenter.search.AttributeConstants.DCMI_CREATOR;
import static oracle.webcenter.search.AttributeConstants.DCMI_EXTENT;
import static oracle.webcenter.search.AttributeConstants.DCMI_IDENTIFIER;
import static oracle.webcenter.search.AttributeConstants.DCMI_MODIFIED;
import static oracle.webcenter.search.AttributeConstants.DCMI_TITLE;
import static oracle.webcenter.search.AttributeConstants.WPSAPI_ICON_PATH;

import oracle.adf.share.logging.ADFLogger;
import oracle.webcenter.search.QName;
import oracle.webcenter.search.Row;

public class EmployeeRow implements Row{
    private Map<QName, Object> m_storage = new HashMap<QName, Object>();
    private DateFormat m_dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
    private static ADFLogger _logger =   ADFLogger.createADFLogger(EmployeeRow.class);
   
    EmployeeRow(String id, String title,    String creator, String lastModified,    Long size)
    {
               m_storage.put(DCMI_IDENTIFIER, id);
        // Only if you have an external URL to go to
        // m_storage.put(DCMI_URI, "http://www.externalurl.com");
        m_storage.put(DCMI_TITLE, title);
        m_storage.put(DCMI_CREATOR, creator);
        // The below path points to a path accessible in the classpath to an icon
        m_storage.put(WPSAPI_ICON_PATH, "/adf/webcenter/search_qualifier.png"); //we can set human icon here
        m_storage.put(DCMI_EXTENT, size);
        try
        { 
            if(lastModified != null){
                Date date = m_dateFormat.parse(lastModified);
                Calendar cal = Calendar.getInstance();
                cal.setTime(date);
                m_storage.put(DCMI_MODIFIED, cal);
            }
          
        }
        catch (ParseException e)
        {
            e.printStackTrace();
           
        }
       
       
      
    }
    public Object getObject(QName columnName)
    {
            return m_storage.get(columnName);
    }
    public Iterator<QName> getColumns()
    {
            return m_storage.keySet().iterator();
    }
}

d. Create EmployeeQueryResult class which should extend WrapperQueryResult

package com.san.portal.search.employee;

import java.util.Iterator;

import oracle.webcenter.search.Row;
import oracle.webcenter.search.util.WrapperQueryResult;

public class EmployeeQueryResult extends WrapperQueryResult<Row> {
    public EmployeeQueryResult(Iterator<Row> rows)
    {    super(rows);
    }
}





e. Create a class EmployeeDetailURL which should implement ResourceUrlRewriter

package com.san.portal.search.employee;

import oracle.webcenter.framework.resource.ResourceUrlRewriter;

public class EmployeeDetailURL implements ResourceUrlRewriter{
    private String resourceId;
    private String resourceType;
    private String resourceTitle;
    public EmployeeDetailURL() {
        super();
    }

    public String rewriteUrl(String url) {
        return "/faces/oracle/webcenter/portalapp/pages/EmployeeDetailPg.jspx" + "?resourceId=" + url ;
    }

    public void setResourceId(String resourceId) {
        this.resourceId = resourceId;
    }

    public String getResourceId() {
        return resourceId;
    }

    public void setResourceType(String resourceType) {
        this.resourceType = resourceType;
    }

    public String getResourceType() {
        return resourceType;
    }

    public void setResourceTitle(String resourceTitle) {
        this.resourceTitle = resourceTitle;
    }

    public String getResourceTitle() {
        return resourceTitle;
    }
}



6. Create a page EmployeeDetailPg.jspx which should show the detail of an employee:
 When user performs a search, he will see a list of employees. From these employees user can select one to see more details. We have already specified our page EmployeeDetailPg.jspx  in class
EmployeeDetailURL.java. Now we need to create this jspx page. Its just a normal page. To get employee-id which it needs to show you can use expression #{param.resourceId}. There could be multiple ways to implement this page. 
   a. Develop a task-flow which takes employee-id as input and show details of that employee. Drop such task flow on this page and pass it #{param.resourceId} as employee-id
   b. Directly create a form on this page which shows detail of an employee from a VO. Add first binding executable as setCurrentRowWithKeyValue and pass value as #{param.resourceId}



7. User Search task-flow to perform global search
Add webcenter provided search task-flow on your page (or template) where you want to show query box.

Page entry:
<af:region value="#{bindings.searchtoolbar1.regionModel}" id="r1"/>
PageDef entry:
<taskFlow id="searchtoolbar1"
              taskFlowId="/oracle/webcenter/search/controller/taskflows/localToolbarSearch.xml#search-toolbar"
              activation="deferred"
              xmlns="http://xmlns.oracle.com/adf/controller/binding">
      <parameters>
        <parameter id="serviceIds" value="com.san.searchapp.employee"/>
      </parameters>
    </taskFlow>





NOTE: I have not tested above implementation with webcenter secured enterprise search enabled. If your implementation is not usig SES you need to make extra configuration in adf-config.xml file.

Comment out following line

 <!--crawl-properties fullCrawlInterval="P5D" enableWcServicesCrawl="true"
                      enableWcDiscussionsCrawl="true" enableWcUcmCrawl="true"/-->



Other Issues faced:


Error: 
<SearchServiceAPI> <execute> oracle.webcenter.concurrent.TimeoutException occurred during Search result transfer of service with identifier "com.san.searchapp.employee" with message "Execution timedout
            <SearchResultsBean> <collectResults> Execution of the search query did not complete within the time limit for executor com.san.searchapp.employee.

  Solution: 
Increase timeout <execution-properties timeoutMs="300000" prepareTimeoutMs="100000"/> in adf-config.xml



Disclaimer: Any views or opinions presented in this blog are solely those of the author and do not necessarily represent those of the company.