AEM 6 - Search and Download DAM Assets in Adobe CC Products

Goal


This post is on developing a Adobe CC HTML Extension to login to AEM 6, search and download Adobe CC assets (eg. Adobe Illustrator 18.1 files) uploaded to AEM DAM from CC products like Illustrator, InDesign, Photoshop...

CC HTML Extensions ( short guide and samples ) run on Common Extensibility Platform (CEP) of Adobe Illustrator 18.1, Adobe InDesign 10.1 etc. (the best part is, if you are not/little interacting with the host in extensions, without any changes they can run on various Adobe CC products). For example (though i've not tried) there are very good changes the sample extension developed in this post will run with minor changes (like adding an entry in CSXS/manifest.xml) in PhotoShop...

For downloading AEM assets (images, content fragments) and place them on InDesign pages check this post

Demo | AI 18.1 IDSN 10.1 CC HTML Extension | Extension Source  |  AEM 6 Package Install

I just put together the pieces, thanks to my fantastic adobe colleagues and internet for quick code snippets


Login panel in Illustrator ( click Window -> Extensions -> Search AEM )





AEM Asset Search in Illustrator





Login Panel in InDesign





Asset Search in InDesign





Solution


AEM CRX Updates

1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder config.author and node org.apache.sling.security.impl.ReferrerFilter of type sling:OsgiConfig and set the allow.empty property to true (Package Install). This setting allows the CEP panel (explained in next steps) to post credentials successfully to AEM. It's an observation that Chromium Embedded of CEP doesn't send referer header with requests (a bug in CEP?). To circumvent the problem we set allow.empty to true (it may be a security issue in publish environments; this CEP extension connects to and downloads assets from author instance, so set the configuration in config.author folder)





CC HTML Extension

1) Create the following folder structure (Download source). This folder structure is installed as extension. The lib folder (containing helper files for creating the extension) is excluded from final executable created

                           search-aem
                                   css
                                         style.css
                                   CSXS
                                         manifest.xml
                                   html
                                         search-aem.html
                                   js
                                          CSInterface-5.2.js
                                          login-search.js
                                   .debug
                                   build.xml


2)  Use ant for assembling the extension for dev and distribution purposes. Ant targets to copy files to extensions folder and create installer.


Develop & Test
         
              a. Create CEP/extensions folder in user's home if it doesn't exist. On my windows it's C:\Users\nalabotu\AppData\Roaming\Adobe\CEP\extensions and MAC /Library/Application Support/Adobe/CEP/extensions

              b. Set PlayerDebugMode to  ( for debugging extensions the flag PlayerDebugMode needs to be set to 1)

                    On Windows, Open registry (Run -> regedit), HKEY_CURRENT_USER\Software\Adobe\CSXS.5



                    On local MAC (/Users/nalabotu/Library/Preferences/com.adobe.CSXS.5.plist)



              c. Create a .debug file in extension root. Add following xml; extension debugger will be available on http://localhost:8098/ when the extension panel is open in Illustrator 18.1. Extension id should be the same id added in CSXS/manifest.xml (next steps)

<?xml version="1.0" encoding="UTF-8"?>
<ExtensionList>
    <Extension Id="com.experience.aem.cep.search">
        <HostList>
           <Host Name="ILST" Port="8098"/>
        </HostList>
    </Extension>
</ExtensionList>
         
              d. For development purposes we can use the following ant target (in build.xml) to copy files over to user's extensions folder eg. C:\Users\nalabotu\AppData\Roaming\Adobe\CEP\extensions

    <target name="copy">
        <delete dir="${live.extensions.dir}" />

        <mkdir dir="${live.extensions.dir}"/>

        <copy todir="${live.extensions.dir}" failonerror="false">
            <fileset dir="${basedir}">
                <exclude name="*.xml"/>
                <exclude name="*.iml"/>
                <exclude name="lib/**"/>
                <exclude name="temp/**"/>
                <exclude name="docs/**"/>
            </fileset>
        </copy>
    </target>

               e. Executing ant target copy, copies the extension files to extensions folder (eg. C:\Users\nalabotu\AppData\Roaming\Adobe\CEP\extensions)

               f. To debug the extension, open CC product (here Adobe Illustrator 18.1), make sure extension panel (Search AEM is open), open chrome browser and access http://localhost:8098/. Check log files, for example on my local C:\Users\nalabotu\AppData\Local\Temp\csxs5-IDSN.log, C:\Users\nalabotu\AppData\Local\Temp\csxs5-ILST.log, C:\Users\nalabotu\AppData\Local\Temp\cef_debug.log; For MAC the log files are in /Users/<user>/Library/Logs/CSXS





Distribute & Install

                a. To distribute the extension for installation on user's CC products, create a ZXP.

                b. ZXP should be CA (Certificate authority ) signed or self signed.

                c. To create a self signed key and certificate in pkcs12 format, use the following openssl commands (A sample certificate experience-aem-cep.p12 is available in
                    extension source, lib folder, password is experience-aem)

                    openssl req -x509 -days 365 -newkey rsa:2048 -keyout experience-aem-cep-key.pem -out experience-aem-cep-cert.pem

                    openssl pkcs12 -export -in  experience-aem-cep-cert.pem -inkey experience-aem-cep-key.pem -out experience-aem-cep.p12

                d. A signing toolkit ZXPSignCmd (available in extension source or can be downloaded at adobe labs ). It's different on MAC (the one available in source is Windows executable)

                e. Use following ant target zxp (in build.xml) to create the extension installer search-aem.zxp in extension source/temp folder

    <!--
    Use this target for creating zxp, installed using Adobe Extension Manager
    The ZXPSignCmd library is different for MAC, the following targets use windows library
    -->
    <target name="zxp" if="isWindows">
        <delete dir="${basedir}/temp" />

        <mkdir dir="${basedir}/temp" />

        <copy todir="${basedir}/temp" failonerror="false">
            <fileset dir="${basedir}">
                <exclude name="*.xml"/>
                <exclude name="*.iml"/>
                <exclude name="lib/**"/>
                <exclude name="temp/**"/>
                <exclude name="docs/**"/>
                <exclude name=".debug"/>
            </fileset>
        </copy>

        <exec executable="${basedir}/lib/ZXPSignCmd">
            <arg line="-sign ${basedir}/temp" />
            <arg value="${basedir}/temp/search-aem.zxp" />
            <arg value="${basedir}/lib/experience-aem-cep.p12" />
            <arg value="experience-aem" />  <!-- cert password -->
        </exec>
    </target>

                   f.  To install the extension, use Adobe Extension Manager CC. Click Install on top right, browse to zxp folder and select search-aem.zxp. If the certificate used is not
                       CA signed (ie. a self signed) Extension Manager alerts with warning...




                       g. The installed extension. Open Adobe Illustrator 18.1 (Window -> Extensions) or InDesign 10.1 (Window -> Extensions) and you should see the Search AEM menu item






3) The most important part of the extension is CSXS/manifest.xml. The following xml fragment of manifest says this extension runs on Illustrator 18.1 and InDesign 10.1

        <HostList>
            <Host Name="ILST" Version="18.1"/>
            <Host Name="IDSN" Version="10.1"/>
            <!-- Add other CC products here -->
        </HostList>

4) The extension declaration with id com.experience.aem.cep.search (provided in .debug file for debugging in browser) <MainPath>./html/search-aem.html</MainPath> instructs Illustrator to load the html file html/search-aem.html when extension is opened by clicking Window -> Extensions -> Search AEM

        <Extension Id="com.experience.aem.cep.search">
            <DispatchInfo>
                <Resources>
                    <MainPath>./html/search-aem.html</MainPath>
                </Resources>
                <Lifecycle>
                    <AutoVisible>true</AutoVisible>
                </Lifecycle>
                <UI>
                    <Type>Panel</Type>
                    <Menu>Search AEM</Menu>
                    <Geometry>
                        <Size>
                            <Height>400</Height>
                            <Width>350</Width>
                        </Size>
                    </Geometry>
                </UI>
            </DispatchInfo>
        </Extension>

4) For AEM developers, developing the actual extension should be pretty easy, its standard html, css, javascript

5) The following code in search-aem.html provides extension view. It loads angular, underscore from cdns. CSInterface-5.2.js is a helper CEP file to interact with host (Illustrator or InDesign) to run native code, for example in later steps we code a simple script to open downloaded file )

<!DOCTYPE html>
<html lang="en" ng-app="SearchAEM">

<head>
    <meta charset="utf-8">

    <title>Login Search AEM</title>

    <link rel="stylesheet" href="../css/style.css">
</head>

<body>
<div ng-controller="pc">
    <div class="sign-in" ng-show="showLogin">
        <div>
            <h3>DIGITAL ASSET MANAGEMENT</h3>
        </div>
        <div>
            <label>User Name</label>
            <input type="text" ng-model="j_username" ng-enter="login()"/>
        </div>
        <div>
            <label>Password</label>
            <input type="password" ng-model="j_password" ng-enter="login()"/>
        </div>
        <div>
            <label>DAM Host</label>
            <input type="text" value="{{damHost}}" ng-model="damHost" ng-enter="login()"/>
        </div>

        <button type="button" ng-click="login()">Sign In</button>
    </div>

    <div ng-show="!showLogin">
        <div class="top-left">
            <input type="text" placeholder="Enter search text" ng-model="term" ng-enter="search()"/>
        </div>
        <div class="top-right">
            <button type="button" ng-click="search()">Search</button>
        </div>
        <div class="results">
            <div class="result-block" ng-repeat="result in results" ng-click="download(result)">
                <div>
                    <img ng-src="{{result.imgPath}}"/>
                </div>
                <div>
                    {{result.name}}
                </div>
            </div>
        </div>
    </div>
</div>

<script src="http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.3.9/angular.js"></script>
<script src="../js/CSInterface-5.2.js"></script>
<script src="../js/login-search.js"></script>
</body>
</html>

6) The js/login-search.js provides necessary login/search/download from AEM functionality. Discussing Angular or Underscore logic is beyond the scope of this post; JS frameworks are not mandatory for developing CC HTML extensions, if you know HTML, CSS, Javascript that's good enough

'use strict';

(function () {
    var underscore = angular.module('underscore', []);

    underscore.factory('_', function () {
        return window._;
    });

    var cep = angular.module('cep', []);

    cep.factory('cep', ['$window', function ($window) {
        return $window.cep;
    }]);

    cep.service('csi', CSInterface);

    var aem = angular.module('aem', ['underscore', 'cep']);

    aem.service('login', [ '$http' , '_', function ($http, _) {
        return {
            login: function (username, password, damHost) {
                var jSecurityCheck = damHost + "/libs/granite/core/content/login.html/j_security_check";

                var data = {
                    j_username: username,
                    j_password: password,
                    j_validate: true
                };

                return $http.post(jSecurityCheck, data, {
                    headers: {
                        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
                    },
                    transformRequest: function(obj) {
                        var params = [];

                        angular.forEach(obj, function(value, key){
                            params.push(encodeURIComponent(key) + "=" + encodeURIComponent(value));
                        });

                        return params.join("&");
                    }
                });
            }
        }
    }]);

    aem.factory('search', [ '_', '$http', function (_, $http) {
        return function (defaults) {
            this.aem = "http://localhost:4502";
            this.params = _.extend({}, defaults);
            this.numPredicates = 0;

            this.host = function(aem){
                this.aem = aem;
                return this;
            };

            this.fullText = function (value) {
                if (!value) {
                    return this;
                }

                this.params[this.numPredicates + '_fulltext'] = value;
                this.numPredicates++;

                return this;
            };

            this.http = function(){
                var builder = this;

                return $http({
                    method: 'GET',
                    url: builder.aem + "/bin/querybuilder.json",
                    params: builder.params
                });
            }
        }
    }]);

    var app = angular.module('SearchAEM', ['aem']);

    app.directive('ngEnter', function () {
        return function(scope, element, attrs) {
            element.bind("keydown keypress", function(event) {
                if (event.which === 13) {
                    scope.$apply(function() {
                        scope.$eval(attrs.ngEnter);
                    });

                    event.preventDefault();
                }
            });
        };
    });

    app.controller('pc', [ '$scope', 'login', 'search', '$http', 'csi', 'cep',
        function ($scope, login, search, $http, csi, cep) {
            $scope.damHost = "localhost:4502";
            $scope.showLogin = true;

            $scope.login = function () {
                if (!$scope.j_username || !$scope.j_password || !$scope.damHost) {
                    alert("Enter credentials");
                    return;
                }

                $scope.damHost = $scope.damHost.trim();

                if ($scope.damHost.indexOf("http://") == -1) {
                    $scope.damHost = "http://" + $scope.damHost;
                }

                login.login($scope.j_username, $scope.j_password, $scope.damHost)
                    .success(function (data) {
                        $scope.showLogin = false;
                    })
                    .error(function () {
                        alert("Trouble logging-in, Invalid Credentials?")
                    })
            };

            var searchDefaults = {
                'path': "/content/dam",
                'type': 'dam:Asset',
                'orderby': '@jcr:content/jcr:lastModified',
                'orderby.sort': 'desc',
                'p.hits': 'full',
                'p.nodedepth': 2,
                'p.limit': 25,
                'p.offset': 0
            };

            $scope.search = function () {
                if (!$scope.term) {
                    alert("Enter search term");
                    return;
                }

                $scope.results = [];

                var mapHit = function(hit) {
                    var result;

                    result = {};

                    result.name = hit["jcr:path"].substring(hit["jcr:path"].lastIndexOf("/") + 1);
                    result.url = $scope.damHost + hit["jcr:path"];
                    result.imgPath = $scope.damHost + hit["jcr:path"] + "/jcr:content/renditions/cq5dam.thumbnail.140.100.png";

                    return result;
                };

                new search(searchDefaults).host($scope.damHost)
                        .fullText($scope.term)
                        .http()
                        .then(function(resp) {
                    $scope.results = _.compact(_.map(resp.data.hits, mapHit));
                });
            };

            $scope.download = function(result){
                $http.get(result.url, {
                    responseType: "blob"
                }).success(function(data) {
                    var reader = new FileReader();

                    reader.onload = function() {
                        var filePath = csi.getSystemPath(SystemPath.MY_DOCUMENTS)
                                            + "/" + result.name;
                        cep.fs.writeFile(filePath, reader.result.split(',')[1], cep.encoding.Base64);

                        csi.evalScript("(function(){app.open(new File('" + filePath + "'));})();", function(){
                            alert("File " + result.name + " downloaded as " + filePath)
                        });
                    };

                    reader.readAsDataURL(data);
                }).error(function() {
                    alert("Error downloading file");
                });
            };
    }]);
}());

7) The following script at #183, executes app.open call to open the downloaded file in Illustrator or InDesign. It's a simple script, provided inline to the evalScript() function; for developing sleek panels with complex extend script logic (Extendscript is similar to javascript, used to code CC extensions) you can use <ScriptPath></ScriptPath> in CSXS/manifest.xml (not explained in this post). app is a global variable available in host (Illustrator or InDesign)

                        csi.evalScript("(function(){app.open(new File('" + filePath + "'));})();", function(){
                            alert("File " + result.name + " downloaded as " + filePath)
                        });

8) A note on using JQuery in CC HTML Extensions. If you get error $ is undefined when executing jquery selectors, add window.module=undefined in html. I am not aware of the details behind NodeJS and JQuery ($) not able to co-exist, but having window.module=undefined makes NodeJS unavailable in extension panel and a developer can code using JQuery

    <script type="text/javascript">
        window.module = undefined;
    </script>



8 comments:

  1. This comment has been removed by a blog administrator.

    ReplyDelete
    Replies
    1. This comment has been removed by a blog administrator.

      Delete
  2. This comment has been removed by a blog administrator.

    ReplyDelete
  3. This comment has been removed by a blog administrator.

    ReplyDelete
  4. This comment has been removed by a blog administrator.

    ReplyDelete
  5. This comment has been removed by a blog administrator.

    ReplyDelete
  6. This comment has been removed by a blog administrator.

    ReplyDelete