AEM 6540 - Using Asset Selector with DMS7 Dynamic Renditions in external CMS or thirdypary sites

Goal


Assuming AEM Assets is the image repository and the usecase is to make use of these assets managed in AEM, in an external CMS (not AEM sites) or a thirdparty website, Assets Selector provides a nice interface for browse/search & selecting the assets

Combined with Dynamic Media Smart Crop, you can generate dynamic rendition urls (scene7 delivery urls) for creating intelligent responsive crops for various device breakpoints, replacing the need for static renditions (eg. doing manual cropping using photoshop)...

For setting up AEM with Dynamic Media Scene7 check this post

Demo | Source Code | Github



Solution


1) Assuming AEM is setup with Dynamic Media Scene7 (check this post for how to) create the Image Profiles by accessing Tools > Assets > Image Profiles or http://localhost:4502/mnt/overlay/dam/gui/content/processingprofilepage/imageprocessingprofiles.html/conf/global/settings/dam/adminui-extension/imageprofile

2) Click Create to create an Image Profile,  give it a name, select the type as Smart Crop, add Responsive Image Crop breakpoints and click Save



3) Optionally create few processing profiles for customer specific purposes (only one image processing profile can be active on a folder ). The profiles are shown in asset selector page created to show image dynamic renditions...



4) Assign the Image Profile to a folder by selecting it and clicking Apply Processing Profile to Folder(s)



5) For accessing and showing the profiles in an external page, create read only authentication handler apps.experienceaem.assets.EAEMReadonlyAuthenticationHandler with the following code

package apps.experienceaem.assets;

import org.apache.commons.lang3.StringUtils;
import org.apache.sling.auth.core.spi.AuthenticationHandler;
import org.apache.sling.auth.core.spi.AuthenticationInfo;
import org.apache.sling.jcr.api.SlingRepository;
import org.apache.sling.jcr.resource.api.JcrResourceConstants;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Session;
import javax.jcr.SimpleCredentials;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

import static org.osgi.framework.Constants.SERVICE_RANKING;

@Component(
        service = { AuthenticationHandler.class, Filter.class },
        immediate = true,
        property = {
            SERVICE_RANKING + ":Integer=" + 9999,
            AuthenticationHandler.PATH_PROPERTY + "=/content/dam",
            AuthenticationHandler.PATH_PROPERTY + "=/conf",
            "service.description=Experience AEM Ready only Authentication Handler",
            "sling.filter.scope=REQUEST"
        })
@Designate(ocd = EAEMReadonlyAuthenticationHandler.Configuration.class)
public class EAEMReadonlyAuthenticationHandler implements AuthenticationHandler, Filter  {
    private static final Logger log = LoggerFactory.getLogger(EAEMReadonlyAuthenticationHandler.class);

    private static String AUTH_TYPE_ASSET_SELECTOR = "EAEM_ASSET_SELECTOR";

    private static final String SESSION_REQ_ATTR = EAEMReadonlyAuthenticationHandler.class.getName() + ".session";

    private String readOnlyUser = "";

    @Reference
    private SlingRepository repository;

    @Reference(target = "(service.pid=com.day.crx.security.token.impl.impl.TokenAuthenticationHandler)")
    private AuthenticationHandler wrappedAuthHandler;

    @Activate
    protected void activate(final Configuration config) {
        readOnlyUser = config.read_only_user();
    }

    public AuthenticationInfo extractCredentials(HttpServletRequest request, HttpServletResponse response) {
        AuthenticationInfo authInfo = null;

        String authType = request.getParameter("authType");

        if(StringUtils.isEmpty(authType) || !authType.equals(AUTH_TYPE_ASSET_SELECTOR)){
            return wrappedAuthHandler.extractCredentials(request, response);
        }

        Session adminSession = null;

        try{
            adminSession = repository.loginAdministrative(null);

            Session userSession = adminSession.impersonate(new SimpleCredentials(readOnlyUser, new char[0]));

            request.setAttribute(SESSION_REQ_ATTR, userSession);

            authInfo = new AuthenticationInfo(AUTH_TYPE_ASSET_SELECTOR);

            authInfo.put(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, userSession);
        }catch(Exception e){
            log.error("Error could not create session for authType - " + authType, e);
            return AuthenticationInfo.FAIL_AUTH;
        }finally {
            if (adminSession != null) {
                adminSession.logout();
            }
        }

        return authInfo;
    }

    public boolean requestCredentials(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException {
        return wrappedAuthHandler.requestCredentials(httpServletRequest, httpServletResponse);
    }

    public void dropCredentials(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException {
        wrappedAuthHandler.dropCredentials(httpServletRequest, httpServletResponse);
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                            throws IOException, ServletException {
        Session userSession = (Session) request.getAttribute(EAEMReadonlyAuthenticationHandler.SESSION_REQ_ATTR);

        if (userSession != null) {
            request.removeAttribute(EAEMReadonlyAuthenticationHandler.SESSION_REQ_ATTR);
        }

        chain.doFilter(request, response);

        if (userSession != null) {
            userSession.logout();
        }
    }

    public void init(FilterConfig filterConfig) throws ServletException {
    }

    public void destroy() {
    }

    @ObjectClassDefinition(name = "EAEMReadonlyAuthenticationHandler Configuration")
    public @interface Configuration {

        @AttributeDefinition(
                name = "Read only user",
                description = "Read only user for accessing asset metadata json",
                type = AttributeType.STRING)
        String read_only_user() default "eaem-read-only-user";
    }
}

6) The authentication handler provides readonly access to nodes in paths /content/dam and /conf, when the url contains query parameter authType=EAEM_ASSET_SELECTOR.It needs a read only user to impersonate, so create the user eg. eaem-read-only-user, provide read only permission on /content/dam and /conf eg. http://localhost:4502/security/permissions.html/principal/eaem-read-only-user?filter=user



7) Configure the read only user in the EAEMReadonlyAuthenticationHandler osgi properties http://localhost:4502/system/console/configMgr/apps.experienceaem.assets.EAEMReadonlyAuthenticationHandler



8) Authentication handler uses repository.loginAdministrative(null) to get an admin session. To allow administrative login in code whitelist the bundle (best practice is to get a session using service user)



9) Since the calls to asset metadata in AEM are made from an external site, set the CORS policy to allow origin Access-Control-Allow-Origin



10) Create the standalone page for loading asset selector and showing dynamic renditions eg. eaem-asset-selector-dynamic-renditions\asset-selector\asset-selector.html. It can be written using any javascript framework (eg. just jQuery), the following sample uses CoralUI

<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,user-scalable=no,initial-scale = 1.0,maximum-scale = 1.0">
    <title>Experience AEM Asset Selector</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore-min.js"></script>
    <script src="https://helpx.adobe.com/experience-manager/6-4/sites/developing/using/reference-materials/coral-ui/coralui3/js/libs/moment.js"></script>
    <script src="https://helpx.adobe.com/experience-manager/6-4/sites/developing/using/reference-materials/coral-ui/coralui3/js/libs/jquery.js"></script>
    <script src="https://helpx.adobe.com/experience-manager/6-4/sites/developing/using/reference-materials/coral-ui/coralui3/js/coral.js"></script>
    <script src="asset-selector.js"></script>
    <link rel="stylesheet" type="text/css"
          href="https://helpx.adobe.com/experience-manager/6-4/sites/developing/using/reference-materials/coral-ui/coralui3/css/coral.css">
    <link rel="stylesheet" type="text/css" href="asset-selector.css">
</head>
<body>
<div>
    <h1 class="coral-Heading" style="text-align: center">AEM Dynamic Renditions Selector</h1>
</div>
<div>
    <h4 class="coral-Heading">
        AEM Asset selector lets you search for, filter, and browse assets within Adobe Experience Manager (AEM) Assets. Combined with Dynamic Media Smart Crop for Responsive images, you can select AEM assets in third party tools without the need to create manually cropped static renditions...
        <br><br>
        Enter the AEM URL eg. http://localhost:4502. Select Image profile, click on the button below to open AEM asset picker and select an asset...
        <br><br>
        <font color="red"> If you see a blank image the asset might not have been published in AEM or the asset was uploaded before applying a image processing profile to the folder in AEM</font>
    </h4>
</div>
<div>
    <div style="padding: 0 0 15px 0">
        <input is="coral-textfield" type="text" value="http://localhost:4502"
               id="aem-host-url" size="100" style="width: 30rem">
    </div>
</div>
<div>
    <label>Select Image Profile</label>

    <div class="coral-RadioGroup" id="dyn-image-profiles-grp">
    </div>
</div>
<div>
    <label>Select a Dynamic Rendition</label>
    <div class="coral-RadioGroup" id="dyn-renditions-grp">
    </div>
</div>
<div>
    <button is="coral-button" id="open-asset-picker" style="text-align: center" variant="primary">
        Open Asset Selector
    </button>
</div>
<div>
    <h4 class="coral-Heading">Asset Info : </h4>

    <div id="asset-info">No asset selected</div>

    <img id="aem-image" src=""/>
</div>
</body>
</html>

11) Add the necessary JS code in eaem-asset-selector-dynamic-renditions\asset-selector\asset-selector.js to load the asset picker, select an asset and show the dynamic renditions

(function(){
    var ASSET_SELECTOR_ID = "aem-asset-selector",
        AUTH_TYPE_ASSET_SELECTOR = "authType=EAEM_ASSET_SELECTOR",
        ASSET_SELECTOR_URL = "/aem/assetpicker",
        IMAGE_PROFILES_URL = "/conf/global/settings/dam/adminui-extension/imageprofile.1.json",
        assetsSelected, imageProfiles = {};

    $(document).ready(initComponents);

    function addHostAndAuthType(url){
        if(!url || url.includes(AUTH_TYPE_ASSET_SELECTOR)){
            return url;
        }

        var host = $("#aem-host-url").val();

        if(!url.startsWith(host)){
            url = $("#aem-host-url").val() + url;
        }

        if(url.includes("?")){
            return (url + "&" + AUTH_TYPE_ASSET_SELECTOR);
        }

        return (url + "?" + AUTH_TYPE_ASSET_SELECTOR);
    }

    function loadImageProfiles(){
        $("#dyn-image-profiles-grp").html("");

        $("#dyn-renditions-grp").html("");

        $.ajax(addHostAndAuthType(IMAGE_PROFILES_URL)).done(handler).fail(function(){
            alert("Error getting image profile. Check AEM host value")
        });

        function handler(data){
            if(!data["jcr:primaryType"]){
                alert("Error getting image profiles");
                return;
            }

            _.each(data, function(imgProfile, key){
                if(key.startsWith("jcr:") || ( imgProfile["crop_type"] != "crop_smart" ) ){
                    return;
                }

                var crops = [];

                _.each(imgProfile["banner"].split("|"), function(crop){
                    crops.push(crop.substring(0,crop.indexOf(",")));
                });

                imageProfiles[key] = crops;
            });

            addImageProfilesRadioGroup(imageProfiles);
        }
    }

    function addImageProfilesRadioGroup(){
        var html = "";

        _.each(imageProfiles, function(crops, profileName){
            html = html + '<coral-radio name="dyn-image-profiles" value="' + profileName + '">' + profileName + '</coral-radio>';
        });

        $("#dyn-image-profiles-grp").html(html);

        $("[name='dyn-image-profiles']").change(addSmartCropsRadioGroup);
    }

    function addSmartCropsRadioGroup(){
        var selProfile = $("input[name='dyn-image-profiles']:checked").val();

        var html = '<coral-radio name="dyn-renditions" value="original" checked>Original</coral-radio>';

        _.each(imageProfiles, function(crops, profileName){
            if(profileName != selProfile){
                return;
            }

            _.each(crops, function(crop){
                html = html + '<coral-radio name="dyn-renditions" value="' + crop + '">' + crop + '</coral-radio>';
            });
        });

        $("#dyn-renditions-grp").html(html);

        $("[name='dyn-renditions']").change(setDynamicRenditionImage);

        setDynamicRenditionImage();
    }

    function createDialog(){
        var selUrl = $("#aem-host-url").val() + ASSET_SELECTOR_URL,
            html = "<iframe width='1500px' height='800px' frameBorder='0' src='" + selUrl + "'></iframe>";

        var dialog = new Coral.Dialog().set({
            id: ASSET_SELECTOR_ID,
            content: {
                innerHTML: html
            }
        });

        document.body.appendChild(dialog);
    }

    function hideDialog() {
        var dialog = document.querySelector('#' + ASSET_SELECTOR_ID);

        dialog.hide();
    }

    function showDialog(){
        var dialog = document.querySelector('#' + ASSET_SELECTOR_ID);

        dialog.show();

        var $dialog = $(dialog);

        adjustHeader($dialog);
    }

    function adjustHeader($dialog){
        $dialog.find(".coral3-Dialog-header").remove().find(".coral3-Dialog-footer").remove();
    }

    function registerReceiveDataListener(handler) {
        if (window.addEventListener) {
            window.addEventListener("message", handler, false);
        } else if (window.attachEvent) {
            window.attachEvent("onmessage", handler);
        }
    }

    function receiveMessage(event) {
        var message = JSON.parse(event.data);

        if(message.config.action == "close"){
            hideDialog();
            return;
        }

        assetsSelected = message.data;

        setAssetInfo("original");

        $("#aem-image").attr("src", assetsSelected[0].url);

        $("input[name='dyn-renditions']")[0].checked = true;

        var dialog = document.querySelector('#' + ASSET_SELECTOR_ID);

        dialog.hide();
    }

    function setAssetInfo(dynRendition, dynRendUrl){
        var assetInfo = "Path - " + assetsSelected[0].path;

        if(dynRendition != "original"){
            assetInfo = assetInfo + "<BR><BR>" + "Dynamic Media URL - <a href='" + dynRendUrl+ "' target='_blank'>" + dynRendUrl + "</a>";
        }

        $("#asset-info").html(assetInfo);
    }

    function setDynamicRenditionImage(){
        var dynRendition = $("input[name='dyn-renditions']:checked").val(),
            $aemImage = $("#aem-image");

        if(!$aemImage.attr("src")){
            return;
        }

        if(dynRendition == "original"){
            $aemImage.attr("src", addHostAndAuthType(assetsSelected[0].url));

            setAssetInfo(dynRendition);

            return;
        }

        $.ajax(addHostAndAuthType(assetsSelected[0].url + ".2.json")).done(handler);

        function handler(data){
            if(!data["jcr:content"]){
                alert("Error getting asset metadata");
                return;
            }

            var metadata = data["jcr:content"]["metadata"],
                dynRendUrl = metadata["dam:scene7Domain"] + "is/image/" + metadata["dam:scene7File"] + ":" + dynRendition;

            $aemImage.attr("src", dynRendUrl);

            setAssetInfo(dynRendition, dynRendUrl);
        }
    }

    function initComponents(){
        loadImageProfiles();

        registerReceiveDataListener(receiveMessage);

        $("#open-asset-picker").click(function(){
            createDialog();

            showDialog();
        });

        $("#aem-host-url").change(loadImageProfiles);
    }
}());


12) The asset picker url /aem/assetpicker is loaded in an iFrame added in a Coral.Dialog. when user selects an asset, the picker fires message event, captured by the receiveMessage() function and selected assets specific data is extracted from event object...

13) The dynamic rendition of an asset in AEM asset details eg. http://localhost:4502/assetdetails.html/content/dam/experience-aem/room-green.jpg



14) The asset picker shown in an external site/page eg. file:///C:/dev/projects/eaem-extensions/eaem-65-extensions/eaem-asset-selector-dynamic-renditions/asset-selector/asset-selector.html



15) Selected AEM asset dynamic renditions shown in the external site / page


16) Test the image responsiveness using embed code provided in AEM asset details page and adding it in a standalone html


1 comment:

  1. PPCexpo is now making life easier for PPC managers and data analysts by simplifying data analysis with their Pareto Chart.

    The cumulative line in the Pareto Chart runs diagonally downwards through the chart. This is used to add the percentage values of each bar, starting with the top bar.

    ReplyDelete