Author Archive: sakagarw

Transporting assets across systems/environments in CM 3.0

Correspondence Management Solution 3.0 provides this great ability to transport, via an intuitive user interface, not just the entire set of assets across environments, but also selective assets. This is one of the uber cool enhancements from how the same is achieved in the previous version of the solution, where this was somewhat limited and cumbersome.

Right from the Manage Assets user interface itself, you can now select any asset or set of (possibly related) assets, that you wish to take over or move to another environment (or physical server), and export them as a ZIP. This ZIP can then be imported over to the desired target system, and voila!…you’ll have your assets in place. Indeed! it is as simple as this.

Please take a moment to read through the published documentation to guide you through the step by step process.

Adobe Correspondence Management Solution 3.0 – Top 10 items to look out for!

If you have not already got a chance to explore the various uber cool features of the all new Correspondence Management Solution 3.0, here are my top 10 ones that you should look out for…

1. With the all new Create Correspondence (aka Document Composer) UI, composing Letters was never so easy! . The new interface provides an easy, intuitive way of composing Letters. With a lot more controls available, such as indentation adjustments, new line, free text, one can design/create a Letter exactly as required.

2. Highlighting of the currently selected asset in the PDF Preview when composing a Letter. Wouldn’t it be a great composing experience if selecting an asset on the flex app. automatically takes you to the respective page, exactly where your content lies within the PDF!? …and then highlight the selected content as well as the target in which the content lies, so you don’t have to search/scroll-over for your content within the PDF. The solution now brings along this amazingly convenient experience for the users.


3. Working on multiple assets at a time. With the all new Manage Assets interface, one can now work on multiple assets (possibly, related to each other) at a time, by virtue of each asset/editor being opened in a new tab in the Editors view. Of course, you can also switch between tabs when working with multiple assets. Here’s a snapshot:

4. Content Preview is yet another amazing addition to the asset authoring experience, wherein you can hover over your asset (in all views that present a list of assets) and see a Preview of the asset content and metadata, be it Texts, Images, Lists, Conditions, etc. So, you no longer need to go back and open the asset editor to see what’s in it. Use the Preview experience to identify the desired asset!

5. Creating numbered and bulleted list content. You can now easily design numbered and bulleted content, by authoring List assets using the List Editor. You can control indentation on paragraph(s) (or even images), specify custom prefix/suffix characters, and much more…

6. The all new Rich Text Editor, that has great text formatting capabilities that includes styling such as Bold/Italic/Underline, Font controls, letter Spacing, line height, Margin controls, Alignment controls. The editor also allows creating advanced bulleted and numbered content, using the appropriate toolbar controls.

Spell Check (English) is another great feature that enhances the text authoring experience.

7. Ability to Publish assets and enhanced version management, with the ability to create different versions of an asset, view previous versions, revert back to last published version, etc. See this post for more on publishing assets.

8. Import/ Export of selective assets is now possible using CM 3.0 via the Manage Assets interface itself. One can select the assets to be exported and simply press the “Export Assets” button. The exported ZIP can then be imported on any other system, using the “Import Assets” action on the Manage Assets interface.
Import/Export of all assets is also now possible right from the Manage Assets (Admin) interface itself, with a single click of a button (rather than the cumbersome steps in Contentspace, as in ES 2.5).

9. CM 3.0 introduces the ability to author Tables (dynamic or static) within your correspondence.

Here’s the Fund Allocation table in the Welcome Kit letter (that is part of the CM sample assets).

10. The Asset Dependencies Browser is an excellent tool/interface to view the dependencies of an asset, and be able to generate a report out of the same.

Note : Users can further drill down into the related assets by double-clicking on the asset, which will then show the dependencies for that asset. One can switch back-n-forth using the breadcrumbs on the top bar.

These are just 10 key features that you just cannot afford to miss. There is a lot more to the solution, the details of which can be seen on this post.

Solutions and the Application Context

Solutions over ADEP have introduced the concept of an application context (aka app context), which can be seen as a unique identifier, that various server side modules use to identify the execution context (from the current execution thread) and process requests in context of that solution. For instance, when persisting content/assets onto the CRX repository, the platform’s persistence module (official known as Platform Content) uses the current (invoking) app context to determine where to store the content/assets, and what configurations to use (which would typically be different for different solutions). See snapshot below, indicating the solution specific content areas.

Note that the storage location is /content/apps/cm for Correspondence Management, and /content/apps/icr for Integrated Content Review, which happen to be the app contexts for the two solutions.

Since it is essential for the server to identify the execution context, if you do not set or establish the application context before you make calls to the solution APIs, you will encounter a server error that says :  “Unable to fetch application context“. To set the app context, use one of the two methods:

App context in your Flex application

If you are invoking a solution API from a flex application, ensure that you set the app context using:


var appToken:AsyncToken = com.adobe.ep.ux.content.services.search.lccontent.LCCQueryServiceFactory.getInstance().setAppContextService("/content/apps/cm"); // setting app context for the CM solution
appToken.addResponder(new mx.rpc.Responder(<your success handler>, <your fault handler>));

App context in your Java application

If you are invoking a solution API from a java based application, ensure that you set the app context using:

com.adobe.livecycle.content.appcontext.AppContextManager.setCurrentAppContext("/content/apps/cm"); // setting app context for the CM solution

 

The app context concept is also used (or rather leveraged) in other scenarios such as driving solution specific Building Block (BB) configurations. Since a Building Block is meant to be reusable across solutions, it exposes certain configurations that can be different for different solutions. Hence, the BB needs to behave differently depending upon the context in which it is being used or invoked. Below is an example where the Data Dictionary BB is used by two solutions – CM and ICR – and has configurations specific to each solution, lying within the solution’s specific app context - /apps/cm for CM and /apps/icr for ICR.

Publishing assets in the Correspondence Management Solution 3.0

The Correspondence Management Solution 3.0 introduces the concept of publishing assets, and deprecates the concept of activation (which was used in the 2.5.x version of the solution).

The solution is typically configured on two separate (ADEP Experience Server) instances – an Author instance and a Publish instance (see the installation and configuration documentation on how to configure the solution over the two instances).

The Author instance is one on which assets/templates are created and managed (using the Manage Assets interface). One can also Preview the Letter templates created on the Author instance, which launches the Create Correspondence interface in a document preview mode.

The Publish instance is one on which the final correspondence is created by agents (using the Create Correspondence interface), using the designed letter templates and published assets. The Manage Assets interface is also available on this instance, but with restricted actions.

Once done with the asset authoring, they must be marked Ready to Publish, indicating the same to the persona (which would typically be different from the one creating/editing the asset) responsible for publishing the asset, who can then go ahead and publish the asset. Note that when publishing an asset, all assets related to this asset should be in Published or Ready To Publish state. Assets that are in Ready To Publish state are published automatically, while the ones that are already Published, are ignored. If any related asset is in a Modified state, the publish operation is aborted, and hence cannot proceed until all related assets are marked Ready to Publish. The understanding behind this behavior is that there could be different persona involved in creation of various related assets, and it is essential that each one of them marks the respective asset ready to publish (indicating completion) before they can really be published.

Here’s a state diagram for various asset states, and how they transition on various actions:

On publishing an asset, a new version of the asset gets created on the Author instance, and the asset is immediately ‘replicated’ over to the Publish instance, which always has a single (head) version of an asset.

Also note that any related Data dictionaries are not published automatically when assets that use it are published. You are required to explicitly publish data dictionaries.

Read more on asset publishing and versioning here.

Common ADEP URLs

As part of your journey into the Adobe Digital Enterprise Platform (Experience Server), there are certain parts/applications of the platform that you may need access to quite frequently.

Here is a list of some of those with a short description of what they can be used for (assuming that your Experience Server is running on localhost and port 4502 – if you have a different server URL, the links should change accordingly):

 

Bookmark the above links and enjoy development over ADEP!

Building OSGi bundles for ADEP

When building custom enterprise applications and solutions over the Adobe Digital Enterprise Platform, there is often a need to build custom OSGi bundles that deploy/expose services to be used within the application (typically by the UI layer). Such bundles are deployed in the platform’s OSGi container (Apache Felix).

Here’s a sample Java project, that can be used as a reference or a template to help you build your own OSGi bundle within minutes.

The project essentially consists of the following:

  • Sample Service (com.adobe.adep.sample.ADEPSampleService), and its implementation, that is exposed by this bundle.

 

  • osgi-context.xml – configuration file (snippet below) that defines the service bean, and uses Spring (Blueprint) to expose it as an OSGi service, available over Flex and HTTP remoting.

 

  • BND tool/library and the supporting configuration file (snippet below) – to help generate an OSGi bundle.

  • ANT based scripts – to build the final deployable bundle.

 

To build the above project and generate your OSGi bundle, simply import the project in Eclipse and run the associated build.xml file. Or, use ANT over command line to do the same. The final deployable bundle archive (JAR) is generated in the dist directory of your project root directory.

This can now be deployed over the ADEP OSGi container, the URL of which would typically be http://server:port/system/console/bundles. Your deployed bundle would be listed on the console as shown below, once deployed successfully:

 

Of course, this is just a sample that illustrates how easy it is to build your OSGi bundles for ADEP. You could extend this to define more complex, real life services within the bundle and get yourself going in minutes…

ADEP Solutions and Document Server

The Adobe Digital Enterprise Platform consists of two technology stacks – the Experience Server and the Document Server. The combined architecture provides services required for various Customer Experience Solutions.

The Experience Server needs to be integrated (configured) with the Document Services to leverage services that address multiple enterprise applications requirements around content creation, management, protection, distribution and enterprise workflows. The below video not only shows the steps to configure your Document Server instance with your Experience server, but also talks about building web applications that use Document Services.

 

 

You may also check out the published documentation on the exact steps to be performed.

Read/Write content node(s) or files from/into the CRX repository

Almost all of the content (either as specific types or unstructured) is stored in the CRX repository, as part of the ADEP platform. You may also want to add your custom content into the repository as part of a larger process or workflow, and then retrieve the same (maybe at a later stage).

While you could do this using classic JCR APIs, the Adobe Digital Enterprise Platform provides convenient APIs (via the delivered SDKs) to do so.

Below is a sample CRXFileManager that performs the following using these APIs:

  • Read nodes/files from CRX
  • Write nodes/files into CRX
  • Read a list of nodes/files from CRX, given a folder node

Here’s how the CRXFileManager looks like:


import java.io.BufferedInputStream;
import java.io.InputStream;


import java.util.ArrayList;
import java.util.List;

import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;


import org.apache.commons.io.IOUtils;
import org.apache.sling.api.resource.ResourceResolver;


import org.springframework.beans.factory.annotation.Autowired;


import com.adobe.livecycle.content.File
import com.adobe.livecycle.content.repository.FileService;
import com.adobe.livecycle.content.sling.ResourceResolverHolder;


public class CRXFileManager {

@Autowired
private FileService fileService;

/**
* Write content (given as input stream) in repository and returns the path.
*/
public String createContentInCRXRepository(InputStream inputStream, String mimeType)
{
try
{
   File file = fileService.createFile(inputStream, mimeType, null, null);
   Session currentSession = getSession();
currentSession.save();
return file.getTempPath();
}
catch (Exception e)
{
// log or throw
}
}


/**
* Read content (given the absolute path) from the repository.
*/
public static byte[] retrieveContentFromCRXRepository(String absPath)
{
byte[] result = null;
try
{
Node ntFileNode = getSession().getNode(absPath);
Node ntResourceNode = ntFileNode.getNode("jcr:content");
InputStream is = ntResourceNode.getProperty("jcr:data").getBinary().getStream();
BufferedInputStream bin = new BufferedInputStream(is);
result = IOUtils.toByteArray(bin);
bin.close();
is.close();
}
catch (Exception ex)
{
// log or throw
}
return result;
}

/**
* Read list of nodes (given the absolute folder path) from the repository.
*/
public static List<Node> retrieveNodesFromCRXRepository(String absFolderPath)
{
List<Node> nodes = new ArrayList<Node>();
try
{
Node ntFolderNode = getSession().getNode(absFolderPath);
NodeIterator ni = ntFileNode.getNodes();
while (ni.hasNext()) {
nodes.add((Node) ni.next(););
}
}
catch (Exception ex)
{
// log or throw
}
return nodes;
}


private static Session getSession() throws RepositoryException {
ResourceResolver resolver = ResourceResolverHolder.getResourceResolver();
return resolver.adaptTo(Session.class);
}

}

Of course, you would need to have the ADEP SDK setup in your eclipse. Specifically, you would need the lc-content-api.jar library from the ADEP SDK (found under \etc\aep\sdks\riaservices\riacore\10.0.0.0\java).

Auditing actions in the Correspondence Management solution

The Correspondence Management solution allows a number of actions, such as Create, Edit/Update, Revert, etc., over assets within the solution. Here is an approach to maintain an audit trail for each such action performed on an asset.

Before you go ahead with this approach, ensure that you have your development environment setup for the solution.

Overview

The overall approach used here is to register an AuditHandler that is registered against each action, such that it is invoked (from within the infrastructure) when the corresponding actions are performed over an asset. The handler can then have any custom implementation for managing the audit log, varying from as simple as a message in a log file to more complex management such as a database tuple.

Implementation

Here are the specific steps you need to take to enable the above.

  1. Create an AuditHandler class, that implements the com.adobe.livecycle.content.repository.action.ActionHandler interface, in the CorrespondenceManagementSolutionTemplate/Services project. You are then required to implement the preProcess() and postProcess() methods which are called before and after an action in performed respectively. One may choose to perform the audit logging in any one (or maybe both) of these methods. Here’s how your AuditHandler skeleton would look like:

    package com.adobe.icc; 
    
    import java.util.Map;
    import com.adobe.icc.dbforms.obj.Asset;
    import com.adobe.livecycle.content.repository.action.ActionHandler;
    import com.adobe.livecycle.content.repository.action.ActionType; 
    
    /**
     * Custom audit handler that audits actions in the CM solution.
     */
    public class AuditHandler implements ActionHandler { 
    
        private static final String PREFIX = "PREFIX";
        /**
         * This method is invoked just prior to the action being performed.
         */
        public void preProcess(String nodeType, ActionType actionType, Object obj, Map<String, Object> arg1)
        {
            Asset asset = (Asset) obj; // obj is the Asset object, and can hence be cast.
            // perform your audit logging here - for e.g., a log statement - logger.info("Asset being created [" + asset.getName() + "] by user : " + SecurityContextHolder.getContext().getAuthentication().getName());
        } 
    
        /**
         * This method is invoked just after the action has been performed.
         */
        public void postProcess(String nodeType, ActionType actionType, Object obj, Map<String, Object> arg1) {
            // perform your audit logging here -  - for e.g., a log statement - logger.info("Asset created [" + asset.getName() + "] by user : " + SecurityContextHolder.getContext().getAuthentication().getName());
        }
    }
  2. Add the following entry to the <bp:blueprint> section of CorrespondenceManagementSolutionTemplate\Services\resources\META-INF\spring\osgi-context.xmlfile:
    
    <bp:reference interface="com.adobe.livecycle.content.repository.RepositoryService" />
    
    
  3. Update the com.adobe.icc.bootstrap.AssetDefinitionDeployer class, under CorrespondenceManagementSolutionTemplate\Services\src\com\adobe\icc\bootstrap, to add a private member variable repositoryService of type com.adobe.livecycle.content.repository.RepositoryService:
    
    @Autowired
    private RepositoryService repositoryService;
    
    
  4. Register your handler for the necessary actions. To do so, add the following code to the afterPropertiesSet()method:
    
    List<ActionHandler> handlers = new ArrayList<ActionHandler>();
    handlers = new ArrayList<ActionHandler>();
    handlers.add(new AuditHandler()); 
    
    // registering the handler against the CREATE action of a Text asset.
    repositoryService.addActionHandlers(ActionHandlerService.APP_ROOT_GLOBAL, TextModule.class.getName(), ActionType.CREATE, handlers);
    
    // registering the handler against the EDIT/UPDATE action of a Text asset.
    repositoryService.addActionHandlers(ActionHandlerService.APP_ROOT_GLOBAL, TextModule.class.getName(), ActionType.UPDATE, handlers); 
    
    // register for other actions, as applicable
  5. Rebuild and redeploy the Solution template to get your changes in affect. For information on rebuilding and redeploying, see Building and deploying the CM Solution Template

Dynamic/Runtime Log level configuration

There is often a need to modify/change the log level of your (J2EE) application or parts of your application, without a server restart. These may be done for various purposes, debugging being one very obvious need.

Here’s a simple way to achieve the same, wherein we once you have deployed the EAR (if your WAR is wrapped within it) or WAR, you can simply place a JSP file at the root folder of your WAR file, and viola!, you have a custom log level configuration console.

In a typical JBOSS installation this would be placed under :   <jboss home>\server\<profile>\tmp\deploy\<EAR – if wrapped within>\<WAR>

In a typical WAS installation this would be placed under : <WebSphere Installation Directory>\AppServer\profiles\<profile>\installedApps\<node name>\<EAR – if wrapped within>\<WAR>

Of course, you should have configured the log4j in your application properly.

Here is the configureLogging.jsp:


<%@ page language="java" contentType="text/html;charset=UTF-8"%>
<%@ page import="org.apache.log4j.Level"%>
<%@ page import="org.apache.log4j.LogManager"%>
<%@ page import="org.apache.log4j.Logger"%>
<%@ page import="java.util.HashMap"%>
<%@ page import="java.util.Enumeration"%>
<%@ page import="java.util.Set"%>
<%@ page import="java.util.Arrays"%>
<html>
<head>
<title>Log Level Configuration</title>
<style type="text/css">
<!--
#content {
    margin: 0px;
    padding: 0px;
    text-align: center;
    background-color: green;
    border: 1px solid #000;
    width: 100%;
}

body {
    position: relative;
    margin: 10px;
    padding: 0px;
    color: #333;
}

h1 {
    margin-top: 20px;
    font: 1.5em Verdana, Arial, Helvetica sans-serif;
}

h2 {
    margin-top: 10px;
    font: 0.75em Verdana, Arial, Helvetica sans-serif;
    text-align: left;
}

a,a:link,a:visited,a:active {
    color: blue;
    text-decoration: none;
    text-transform: uppercase;
}

table {
    width: 100%;
    background-color: #000;
    padding: 3px;
    border: 0px;
}

th {
    font-size: 0.75em;
    background-color: #eee;
    color: #000;
    padding-left: 5px;
    text-align: center;
    border: 1px solid #eee;
    white-space: nowrap;
}

td {
    font-size: 0.75em;
    background-color: #fff;
    white-space: nowrap;
}

td.center {
    font-size: 0.75em;
    background-color: #fff;
    text-align: center;
    white-space: nowrap;
}

.filterForm {
    font-size: 0.9em;
    background-color: #000;
    color: #fff;
    padding-left: 5px;
    text-align: left;
    border: 1px solid #000;
    white-space: nowrap;
}

.filterText {
    font-size: 0.75em;
    background-color: #ccc;
    color: #000;
    text-align: left;
    border: 1px solid #ccc;
    white-space: nowrap;
}

.filterButton {
    font-size: 0.75em;
    background-color: brown;
    color: white;
    padding-left: 5px;
    padding-right: 5px;
    text-align: center;
    border: 1px solid #ccc;
    width: 100px;
    white-space: nowrap;
}
–>
</style>
</head>
<body onLoad=”javascript:document.logFilterForm.logNameFilter.focus();”>

    <%
        String containsFilter = “Contains”;
        String beginsWithFilter = “Begins With”;

        String[] logLevels = { “debug”, “info”, “warn”, “error”, “fatal”,
                “off” };
        String targetOperation = (String) request.getParameter(“operation”);
        String targetLogger = (String) request.getParameter(“logger”);
        String targetLogLevel = (String) request
                .getParameter(“newLogLevel”);
        String logNameFilter = (String) request
                .getParameter(“logNameFilter”);
        String logNameFilterType = (String) request
                .getParameter(“logNameFilterType”);
    %>
    <div id=”content”>
        <h1>Log Level Configuration</h1>

        <div>

            <form action=”configureLogging.jsp” name=”logFilterForm”>
                Filter Loggers:&nbsp;&nbsp; <input name=”logNameFilter” type=”text”
                    size=”50″ value=”<%=(logNameFilter == null ? “” : logNameFilter)%>”
                    class=”filterText” /> <input name=”logNameFilterType” type=”submit”
                    value=”<%=beginsWithFilter%>” />&nbsp; <input
                    name=”logNameFilterType” type=”submit” value=”<%=containsFilter%>”
                    class=”filterButton” />&nbsp; <input name=”logNameClear”
                    type=”button” value=”Clear”
                    onmousedown=’javascript:document.logFilterForm.logNameFilter.value=””;’ />
                <input name=”logNameReset” type=”reset” value=”Reset”
                    class=”filterButton” />

                <param name=”operation” value=”changeLogLevel” />
            </form>
        </div>

        <table cellspacing=”1″>
            <tr>
                <th width=”25%”>Logger</th>

                <th width=”25%”>Parent Logger</th>
                <th width=”15%”>Current Level</th>

                <th width=”35%”>Change Log Level To</th>
            </tr>

            <%
                Enumeration loggers = LogManager.getCurrentLoggers();

                HashMap loggersMap = new HashMap(128);
                Logger rootLogger = LogManager.getRootLogger();

                if (!loggersMap.containsKey(rootLogger.getName())) {

                    loggersMap.put(rootLogger.getName(), rootLogger);
                }

                while (loggers.hasMoreElements()) {
                    Logger logger = (Logger) loggers.nextElement();

                    if (logNameFilter == null || logNameFilter.trim().length() == 0) {

                        loggersMap.put(logger.getName(), logger);
                    } else if (containsFilter.equals(logNameFilterType)) {

                        if (logger.getName().toUpperCase()
                                .indexOf(logNameFilter.toUpperCase()) >= 0) {

                            loggersMap.put(logger.getName(), logger);
                        }

                    } else {
                        // Either was no filter in IF, contains filter in ELSE IF, or begins with in ELSE
                        if (logger.getName().startsWith(logNameFilter)) {

                            loggersMap.put(logger.getName(), logger);
                        }

                    }
                }
                Set loggerKeys = loggersMap.keySet();

                String[] keys = new String[loggerKeys.size()];

                keys = (String[]) loggerKeys.toArray(keys);

                Arrays.sort(keys, String.CASE_INSENSITIVE_ORDER);
                for (int i = 0; i < keys.length; i++) {

                    Logger logger = (Logger) loggersMap.get(keys[i]);

                    // MUST CHANGE THE LOG LEVEL ON LOGGER BEFORE GENERATING THE LINKS AND THE
                    // CURRENT LOG LEVEL OR DISABLED LINK WON’T MATCH THE NEWLY CHANGED VALUES
                    if (“changeLogLevel”.equals(targetOperation)
                            && targetLogger.equals(logger.getName())) {

                        Logger selectedLogger = (Logger) loggersMap
                                .get(targetLogger);

                        selectedLogger.setLevel(Level.toLevel(targetLogLevel));
                    }

                    String loggerName = null;
                    String loggerEffectiveLevel = null;
                    String loggerParent = null;
                    if (logger != null) {
                        loggerName = logger.getName();
                        loggerEffectiveLevel = String.valueOf(logger
                                .getEffectiveLevel());
                        loggerParent = (logger.getParent() == null ? null : logger
                                .getParent().getName());

                    }
            %>
            <tr>
                <td><%=loggerName%></td>

                <td><%=loggerParent%></td>
                <td><%=loggerEffectiveLevel%></td>
                <td>
                    <%
                        for (int cnt = 0; cnt < logLevels.length; cnt++) {

                                String url = “configureLogging.jsp?operation=changeLogLevel&logger=”
                                        + loggerName
                                        + “&newLogLevel=”
                                        + logLevels[cnt]
                                        + “&logNameFilter=”
                                        + (logNameFilter != null ? logNameFilter : “”)
                                        + “&logNameFilterType=”
                                        + (logNameFilterType != null ? logNameFilterType
                                                : “”);

                                if (logger.getLevel() == Level.toLevel(logLevels[cnt])
                                        || logger.getEffectiveLevel() == Level
                                                .toLevel(logLevels[cnt])) {
                    %> [<%=logLevels[cnt].toUpperCase()%>] <%
                        } else {
                    %> <a href='<%=url%>’>[<%=logLevels[cnt]%>]</a>&nbsp; <%
     }
         }
 %>
                </td>
            </tr>

            <%
                }
            %>
        </table>
    </div>
</body>
</html>

Once you copy this JSP under root of your web app, you can access it by entering http://[host]:[port]/[context root]/configureLogging.jsp

You should see something like this on your screen: