Friday, 30 September 2016

ATG Endeca Integration | Write log [Logging API] requests while using Assembler API

In the case we want to use endeca logging and reporting. The first thing is we need to write log entries in LogServer's log file. Here I am going to explain, how to write log request to log file with assembler API.


Below are steps : 

1. Create class which extends RequestEventListener.
2. Create global scoped component of this class.
3. Add this component in assemblerEventListeners property of NucleusAssemblerFactory.

Below is the example : 

1. Create class which extends RequestEventListener.

=========================================================================
package com.shop.endeca.assembler.logserver;

import atg.servlet.ServletUtil;
import com.endeca.infront.assembler.ContentItem;
import com.endeca.infront.assembler.event.request.RequestEvent;
import com.endeca.infront.assembler.event.request.RequestEventListener;
import com.endeca.infront.navigation.event.NavigationEventWrapper;
import com.endeca.logging.*;
import atg.nucleus.logging.ApplicationLogging;
import atg.nucleus.logging.ClassLoggingFactory;


public class LogServerAdapter extends RequestEventListener
{

    private static ApplicationLogging mLogger =     ClassLoggingFactory.getFactory().getLoggerForClass(LogServerAdapter.class);
   
    private static LogConnection Logconn;

    private LogConnection getLogconn() {
        Logconn=new LogConnection(getLogServer(),getPortNumber());
        return Logconn;
    }

    public void setLogconn(LogConnection conn) {
        this.Logconn =Logconn;
    }
   
    // Endeca server hostname where endeca log server is hosted.
    private String logServer;

    public  String getLogServer() {
        return logServer;
    }

    public void setLogServer(String logServer) {
        this.logServer = logServer;
    }

    // Endeca Log Sever port number
    private Integer portNumber;


    public Integer getPortNumber() {
        return portNumber;
    }

    public  void  setPortNumber(Integer portNumber) {
        this.portNumber = portNumber;
    }

    /**
     * @return ApplicationLogging object for logger.
     */
    private ApplicationLogging getLogger() {
        return mLogger;
    }


    @Override
    public void handleAssemblerRequestEvent(RequestEvent assemblerReqEnt, ContentItem conItemPar) {
       
        try
        {
            LogEntry handleLog = new LogEntry();
            Logconn=getLogconn();
           
           NavigationEventWrapper eventNavigation = new NavigationEventWrapper(assemblerReqEnt);
            if (assemblerReqEnt.getSessionId() != null) {
                handleLog.putString("SESSION_ID", assemblerReqEnt.getSessionId());

            }


            if (eventNavigation.getAutocorrectTo() != null) {
                handleLog.putString("AUTOCORRECT_TO", eventNavigation.getAutocorrectTo());
            }


            if (eventNavigation.getDimensions() != null) {
                handleLog.putList("DIMS", eventNavigation.getDimensions());
            }


            if (eventNavigation.getDimensionValues() != null) {
                handleLog.putList("DVALS", eventNavigation.getDimensionValues());
            }


            if (eventNavigation.getEneTime() != null) {
                handleLog.put("ENE_TIME", eventNavigation.getEneTime());
            }


            if (eventNavigation.getNumRecords() != null) {
                handleLog.put("NUM_RECORDS", eventNavigation.getNumRecords());
            }

            if (eventNavigation.getRecordNames() != null) {
                handleLog.putList("RECORD_NAMES", eventNavigation.getRecordNames());
            }


            if (eventNavigation.getRequestType() != null) {
                handleLog.putString("TYPE", eventNavigation.getRequestType().toString());
            }


            if (eventNavigation.getSearchKey() != null) {
                handleLog.putString("SEARCH_KEY", eventNavigation.getSearchKey());
            }

            if (eventNavigation.getNumRefinements() != null) {
                handleLog.put("NUMREFINEMENTS", eventNavigation.getNumRefinements());
            }


            if (eventNavigation.getSortKeys() != null) {
                handleLog.putList("SORT_KEY", eventNavigation.getSortKeys());
            }


            if (eventNavigation.getSpotlights() != null) {
                handleLog.putList("MERCH_RULES", eventNavigation.getSpotlights());
            }


            if (eventNavigation.getSearchMode() != null) {
                handleLog.putString("SEARCH_MODE", eventNavigation.getSearchMode());
            }


            if (eventNavigation.getSearchTerms() != null) {
                handleLog.putString("SEARCH_TERMS", eventNavigation.getSearchTerms());
            }


            Logconn.logAsynchronously(handleLog);


            if(getLogger().isLoggingDebug())
            {
                getLogger().logDebug("reporting log is updated.");
            }

        }
        catch (LogException e)
        {
            if (getLogger().isLoggingError())
            {
                getLogger().logError("There is some LogException"+e.getMessage());
            }
        }catch(Exception e)
        {
            if (getLogger().isLoggingError())
            {
                getLogger().logError("There is some other exception"+e.getMessage());
            }
           
        }
    }
}

=========================================================================

2. Create global scoped component of this class.

=========================================================================
 # /atg/endeca/assembler/logserver/LogServerAdapter
$class=com.shop.endeca.assembler.logserver.LogServerAdapter

$scope=global
portNumber=16010
logServer=localhost

=========================================================================

3. Add this component in assemblerEventListeners property of NucleusAssemblerFactory.

=========================================================================
 assemblerEventListeners+=\
    /atg/endeca/assembler/logserver/LogServerAdapter

=========================================================================

Wednesday, 14 September 2016

Resolved | Workbench Error org.apache.sling.api.SlingException

Today while accessing Oracle Commerce Workbench got below exception on browser.
=========================================================================
The requested URL /ifcr/sites/Store.html resulted in an error in /apps/endeca/site/html.jsp. Exception: org.apache.sling.api.SlingException: at org.apache.sling.scripting.jsp.jasper.servlet.JspServletWrapper.handleJspExceptionIntern al (JspServletWrapper.java:560) atorg.apache.sling.scripting.jsp.jasper.servlet.JspServletWrapper.handleJspException (JspServletWrapper.java:496)
=========================================================================

Solution : Restart the Endeca ToolsAndFrameworks

Wednesday, 7 September 2016

ATG BCC | Disable(hide) Promotion Template in template selection screen

Sometimes business is not utilizing all the available promotion templates in BCC. So it is better to hide these non-required templates in template selection screen in BCC.

Here are the steps : 


1. Copy pmdt file for promotion to disable in \atg\registry\data\promotiontemplates\item directory of "Versioned" module of your application.You can find existing templates at DCS\config\config.jar\config\atg\registry\atg\registry\data\promotiontemplates.

2. Add available-in-ui="false" attribute in ui-description tag of pmdt.

3. Rebuild and Redeploy the BCC ear. Now this template won't be available in template selection screen.

In below pmdt  SpendY GetGiftWithPurchase promotion is disabled.

==========================================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE template PUBLIC "-//Art Technology Group, Inc.//DTD PMDT 1.0//EN" "http://www.atg.com/dtds/pmdt/pmdt_1.0.dtd">
<template item-type="Item Discount"
                    author="Sean McIntyre"
                    last-modified-by="Sean McIntyre"
                    creation-date="11/02/2011">
  <ui-description display-name-resource="template.gwp.spendYGetGiftWithPurchase.title" 

             resource-bundle="atg.remote.promotion.template.Resources" available-in-ui="false">
  </ui-description>
 </template>

==========================================================================

Sunday, 4 September 2016

ATG | Internationalizing the repository items

Here I am going to explain internationalization "best-practice" strategy provided by ATG.

Here are the steps :

1. Identify properties for translation in given item-descriptor. In below example title,content, location.

2. Now create new properties in base item-descriptor [article]. These properties will point to existing properties database column. For example we have created contentDefault property to refer long_description column. This column was initially referred by content property. Name these properties in such a way so that these can bee easily differentiate from derived property [postfix property name with Default].

3. Create the helper item descriptor to store the translated data. This item-descriptor will contain all the translated properties from main item-descriptor. Here articleTranslation used for this purpose.

4. Now add map type property in base item-descriptor which is map of locale to translated item [defined in above step]. In below example translations property is used to map locale to articleTranslation.

5. Change the definitions of the translatable properties in the existing item types. The new definitions should specify that each translatable property is a derived property. Here atg.repository.dp.LanguageTranslation class used to implement the derivation.

Below mechanism is used to find the values of translatable property.
  •  Use the current locale to look up a corresponding baseTypeTranslation [articleTranslation] item in the translations property map. The property derivation attempts to find a best match. First, it searches the locale keys for a match on the entire locale with a variant, then it searches for a match on the locale without a variant, and finally it searches on just the language code.
  •  If a baseTypeTranslation [articleTranslation] item exists for the current locale, use its value for the property.
  • If a baseTypeTranslation [articleTranslation] item does not exist for the current locale, or its value for the property is null, use the translatablePropertyDefault [stored in base item-descriptor] value instead.
Below is the example of internationalization : Here we have article repository item [with 3 properties title,content, location]. Here articleTranslation item-descriptor is used to translate these properties.
==========================================================================
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<gsa-template>

    <!--///////////////////////////////-->
    <!--     article                         -->
    <!--///////////////////////////////-->
   
    <item-descriptor name="article" display-property="titleDefault" display-name="article">
   
        <table name="shop_article" type="primary" id-column-name="article_id">
            <property name="id" data-type="string" column-name="article_id"  display-name="article_id"/>
            <property name="titleDefault" data-type="string" column-name="title" 

                     display-name="titleDefault"/>   
            <property name="contentDefault" data-type="big string" column-name="long_description" 

                    display-name="contentDefault"/>
            <property name="locationDefault" data-type="string" column-name="article_location" 

                    display-name="location"/>   
        </table>
       
        <property name="title" data-type="string" writable="false" hidden="true" queryable="true" 

                    display-name="title">
            <derivation user-method="atg.repository.dp.LanguageTranslation">
                <expression>titleDefault</expression>
            </derivation>
            <attribute name="defaultProperty" value="titleDefault" />
            <attribute name="defaultLocale" value="en" />
        </property>
       
        <property name="content" data-type="big string" writable="false" hidden="true" queryable="true"
            display-name="content">
            <derivation user-method="atg.repository.dp.LanguageTranslation">
                <expression>contentDefault</expression>
            </derivation>
            <attribute name="defaultProperty" value="contentDefault" />
            <attribute name="defaultLocale" value="en" />
        </property>
       
        <property name="location" data-type="string" writable="false" hidden="true" queryable="true"
            display-name="location">
            <derivation user-method="atg.repository.dp.LanguageTranslation">
                <expression>locationDefault</expression>
            </derivation>
            <attribute name="defaultProperty" value="locationDefault" />
            <attribute name="defaultLocale" value="en" />
        </property>
       
        <table name="shop_article_translations" type="multi" multi-column-name="locale" 

                     id-column-names="article_id">
            <property name="translations" display-name="Translations" 

                    column-name="translation_id"      data-type="map"  
                    component-item-type="articleTranslation"  display-name="translations">
            </property>
        </table>
       
    </item-descriptor>
   
    <!--///////////////////////////////-->
    <!--     articleTranslation      -->
    <!--///////////////////////////////-->

    <item-descriptor name="articleTranslation" id-space-name="articleTranslation" 

         display-name="articleTranslation"  display-property="title">
       
        <table name="shop_article_xlate" type="primary" id-column-name="translation_id">
            <property name="title" column-name="title" data-type="string"  display-name="Title" />
            <property name="content" data-type="big string" column-name="long_description" 

                      display-name="content"/>
            <property name="location" data-type="string" column-name="article_location"

                     display-name="location" />
        </table>
       
    </item-descriptor>
</gsa-template>

==========================================================================

Advantages of this approach : 
  • Applications can switch between international and non-international modes without requiring any JSP  page changes. 
  • No database schema changes are required to add additional languages.

Friday, 19 August 2016

Hidden Power of ATG | Service Cache

As we all know the importance of cache in web application performance. In the case of ATG most of the developers  are aware of repository and cache droplet.


While working on one of the Oracle commerce application. I came across new cache from ATG (called service cache). 

Below are the limitations of the above mentioned caches [repository/droplet].

1. Cache Droplet : Accessible through JSP pages. This caches the generated html fragments which includes whites space, this results in increased cache size [comparatively big cache].
2. Repository Cache : Applicable when accessing Database through repository layer.

This cache [Service Cache] is useful in number of scenarios like when you want to access database directly but still want to cache the results. You can also cache data like from other systems like third party response of some call or result of Endeca presentation API.

Here I am going to explain to configure this cache for caching the results in droplet call.

1. Create Cache Key Class : This class will represent the cache key object. Optionally you can directly create the string cache key in droplet itself. But this approach is useful when cache key is complex [includes language/local/segments]. This is also helpful to easily extend the cache key in future [add more fields]. Here you need to override the equals and hashCode methods [Internally used to compare cache keys].

=========================================================================
public class DropletCacheKey {

private String mLanguage;
private String mSegment;
private String mKey; 

public String getLanguage() {
        return mLanguage;
    }

public void setLanguage(String pLanguage) {
        this.mLanguage = pLanguage;
    }

public String getSegment() {
        return mSegment;
    }

public void setSegment(String pSegment) {
        this.mSegment = pSegment;
    }    

 public String getKey() {
        return mKey;
    }

public void setKey(String pKey) {
        this.mKey = pKey;
    }

 private boolean isSameRecordKey(DropletCacheKey pObject) {
        return (this.mKey == null && pObject.getKey() == null) ||
        (this.mKey!= null && pObject.getKey()!= null && pObject.getKey().equals(this.mKey)); 
    }

private boolean isSameRecordLanguage(DropletCacheKey pObject) {
        return (this.mLanguage == null && pObject.getLanguage() == null) ||
                  (this.mLanguage!= null && pObject.getLanguage()!= null
                  && pObject.getLanguage().equals(this.mLanguage)); 
    }

private boolean isSameRecordSegment(DropletCacheKey pObject) {
        return (this.mSegment == null && pObject.getSegment() == null) ||
                  (this.mSegment!= null && pObject.getSegment()!= null 
                 &&   pObject.getSegment().equals(this.mSegment)); 
    }

@Override
    public boolean equals(Object pObject){
        if(pObject instanceof
DropletCacheKey){
           
DropletCacheKey cacheKey = (DropletCacheKey) pObject;
            return (isSameRecordKey(
cacheKey)
                      && isSameRecordLanguage(cacheKey) && isSameRecordSegment(cacheKey));
        }
        return false;
    }

@Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("KEY : [ ");
        builder.append(mKey).append("|");
        builder.append(mLanguage).append("|");
        builder.append(mSegment);
        builder.append(" ]");
        return builder.toString();
    }

@Override
public int hashCode() {
        int PRIME = 31;
        int result = 1;
        result = PRIME * result + ((StringUtils.isBlank(mKey)) ? 0 : mKey.hashCode());
        result = PRIME * result + ((StringUtils.isBlank(mLanguage)) ? 0 : mLanguage.hashCode());
        result = PRIME * result + ((StringUtils.isBlank(mSegment)) ? 0 : mSegment.hashCode());
        return result;
    }
}
=========================================================================

2. Create Cache Adapter Class : This class contains the business logic to fetch actual result.

=========================================================================
public class DropletCacheAdapter extends GenericService implements CacheAdapter {

@Override
public Object getCacheElement(Object pKey) throws Exception {
     if (pKey != null) {
        // Write business logic here to fetch actual result and return the same
     }
}

@Override
    public int getCacheElementSize(Object pArg0, Object pArg1) {
        return 0;
    }

@Override
    public Object[] getCacheElements(Object[] pKeys) throws Exception {
        vlogDebug("Entering Object[] getCacheElements(Object[] pKeys) : " );
        if (pKeys != null) {
            int keyLength = pKeys.length;
            vlogDebug("Key Length :: " + keyLength);
            Object elements[] = new Object[keyLength];
            for (int i = 0; i < keyLength; i++) {
                elements[i] = getCacheElement(pKeys[i]);
            }
            return elements;
        } else {
            return null;
        }
    }

@Override
    public int getCacheKeySize(Object pArg0) {
        return 0;
    }

@Override
    public void removeCacheElement(Object pArg0, Object pArg1) {
    }
}
========================================================================= 
3. Create Cache Adapter Component : Configure cache adapter component using above class.

=========================================================================
# /com/cache/DropletCacheAdapter
 $class=com.cache.DropletCacheAdapter 
 $scope=global
cache=/atg/service/cache/Cache
=========================================================================
 
4. Create Cache Component : Now its time to configure cache component

=========================================================================
 # /com/cache/DropletCache
$class=atg.service.cache.Cache
$scope=global

# injecting caching adapter
cacheAdapter=
/com/cache/DropletCacheAdapter
#Configure below values as per your application need
maximumCacheEntries=10000
maximumCacheSize=10485760
maximumEntryLifetime=43200000
=========================================================================
 
5. Inject Cache component as property in droplet component : This is required as we need to refer cache inside the droplet code.

=========================================================================
 $class=com.search.droplet.StoreDroplet
$scope=global
dropletCache=
/com/cache/DropletCache
#inject other required component
=========================================================================

6. Update Droplet code to use this cache : Here first of all you need to create cachekey object based on input parameter [in this case language, segment and key]. Then invoke the get method on cache component [injected in above step].

=========================================================================
 public class StoreDroplet extends DynamoServlet {    

    private Cache  mDropletCache;

    public Cache
getDropletCache() {
        return
mDropletCache;
    }

   public void
setDropletCache(Cache pDropletCache) {
        this.
mDropletCache= pDropletCache;
    }

public void service(DynamoHttpServletRequest pRequest, DynamoHttpServletResponse pResponse) throws ServletException, IOException { 

// Create cache key using custom method here you need to write method that will return populated object
// of type DropletCacheKey you can modify definition of this object [class] as per your need.

// next fetch the result from cache using this key. This will automatically take care of adding object to cache 
// if already not done. And fetch object from cache if it is available there.
// Here you can also type cast the result. This type cast must be same as retuned type object of //getCacheElement method of cache adapter.
getDropletCache.get(dropletCacheKey);

//further you can utilize the retuned object 

}

}
=========================================================================

Note : You can monitor this cache from dyn/admin.

Monday, 15 August 2016

ATG request pipeline servlets vs J2EE servlets

Below are two major differences between ATG Request pipeline servlets and J2EE servlets.

1. ATG servlets exist in the servlet pipeline, which executes before the request reaches the J2EE web container. J2EE servlets are executed by the web container.

2. ATG servlets themselves determine the order in which they execute. The application deployment descriptor web.xml describes the order and conditions in which J2EE servlets execute.

Wednesday, 10 August 2016

ATG | Configure request handling pipeline servlets [Customize request handling pipeline]

Below are the two common ways to configure [create] request handling pipeline servlets in ATG.

1. Extend atg.servlet.pipeline.PipelineableServletImpl.
2. Extend atg.servlet.pipeline.InsertableServletImpl.

1. Extend atg.servlet.pipeline.PipelineableServletImpl 

Follow below steps to configure servlet using PipelineableServletImpl.
  • Extend atg.servlet.pipeline.PipelineableServletImpl.
  • Define the servlet as a globally scoped Nucleus component.
  • Reset the previous servlet’s nextServlet property to point to the new servlet.
  • Set the new servlet’s nextServlet property to point to the next servlet in the pipeline.
  • Add the servlet’s path to the initialServices property of /atg/dynamo/servlet/Initial. 

 2. Extend atg.servlet.pipeline.InsertableServletImpl

Follow below steps to configure servlet using InsertableServletImpl.
  • Extend atg.servlet.pipeline.InsertableServletImpl.
  • Define the servlet as a globally scoped Nucleus component.
  • Set the insertAfterServlet property of your servlet to point to the path of the pipeline servlet you want your servlet to follow. 
    For example, you can insert a servlet after StoreServlet as follows :
    insertAfterServlet=/atg/dynamo/servlet/dafpipeline/StoreServlet
  • Add the servlet’s path to the initialServices property of /atg/dynamo/servlet/Initial
Advantage of second approach is you no need to make any change in the existing servlets.