Author Archive

December 17, 2018

AEM Rockstar is back for a 3rd year!

 

Submissions Closed for 2019

 

…and just like that, we are back! Join us in Las Vegas at Adobe Summit 2019 for one of the most popular sessions from 2017 & 2018.

When Darin & Kaushal came up with the idea of AEMRockstar, they had one goal in mind: Give the vast AEM community a platform to show off their chops! If the last two years are any indication, the AEM Community loves AEMRockstar, and so do we!

Want to be up to date with all things AEM Rockstar?

Follow/add

Format Refresher

Below is a quick refresher on how things work.

  1. We ask that you submit your AEM (really, Adobe Experience Cloud) tips & tricks using this FORM before January 27, 2019. Submit as many ideas as you want.
  2. After submissions close, the AEMRockstar team will diligently go through each of the submissions and pick our semi finalists.
  3. We then setup a quick call with each of the semi finalist to go over their submission, let you ask questions and go over the session details.
  4. After having talked to all the semi finalists, we pick the finalists who get invited to Las Vegas to present their submission, including a live demo to the session attendees & a judging panel of Adobe heavy hitters, including last years winner, Brett Birschbach, should he choose to accept his mission.
  5. We pay for your Summit pass, but you are responsible for travel costs and handling all the fame that comes with being an AEMRockstar!

2018 Recap

  • We had over 100+ submissions and the following individuals were selected as finalists.
    1. Brett Birschbach
    2. Bryan Williams
    3. Joost van Dun
    4. Conrad Woeltge
  • Over 300 people attended the session at Adobe Summit
  • Our esteemed judging panel was
    1. Jean-Michel Pittet – VP, Engineering, AEM
    2. Jay Dettling – VP, Partners
    3. Robert Anderson – Director, AEM Practice, ACS
    4. Dan Klco – 2017 AEMRockstar winner
  • And they chose Brett and his submission of AEM Remote Assets as the winner. You can read more about his submission here. Brett walked away with a swanky new Oculus Rift!
  • Survey callouts
    • 80% of the attendees would recommend AEMRockstar to their peers.
    • Over 88% said that it either met or exceeded their expectations.
    • 76% said that the session was excellent or above average.
  • Social Media highlights

Tips for submission

  • https://aemrockstar2019.attendease.com/submission-guide/
  • The live demos last year were a HIT! Please think about how your submission demoes.
  • Submissions that cover multiple Adobe Experience Cloud offerings tend to do better with the varied audience and the judges.
  • Are you solving a problem that others deal with it as well? You’d be surprised how common the problem you are facing is and how you can help the community
  • Would it make people go “WOW!”?
  • Using older tools/versions are not favored by the judges (avoid classic UI if you want to win)
  • Look at past year’s submissions and winners
  • Lastly, KISS!

To be considered, all you have to do is SUBMIT your idea. We are looking for all things Adobe Experience Cloud! When you submit, please make sure you provide your complete shipping address, that ways we can send you the brand new AEMRockstar sticker. We had quite a few stickers returned last year.

The fine print

  • Adobe employees are not eligible to submit.
  • The finalists are responsible for paying for their own travel and lodging. The AEMRockstar team only pays for your Summit pass.
  • Last year’s (2018) finalists are not eligible to submit.

 

Submissions Closed for 2019

5:00 AM Permalink
March 26, 2018

Integrating Microsoft Azure Active Directory OAuth with AEM. Author and publish

On a recent project we were asked to implement an OAuth integration with AEM using Microsoft Azure AD as the server and use it on both the author and publish instances.

AEM OOTB provides Facebook and Twitter OAuth providers and Cloud Service configurations. More details for AEM 6.3 can be found here.

But, since we needed the OAuth to work on the author instance as well, the Cloud Service way won’t work. Our publish use case was to disable anonymous access and only allow access via OAuth, we stayed away from using Cloud Services for publish instances as well.

Below is a list of classes we implemented

  • Scribe classes
    • Azure AD OAuth API
      • AEM uses Scribe, this class extends the scribejava DefaultApi20 class.
      • After writing this class, I submitted a PR to the scribe java project and it’s now been merged . You can use this code as reference to create the same in your AEM code base.
    • Azure AD OAuth Service
      • Also included in the PR to the scribejava project
  • Thats all for the non AEM classes, below are all the AEM related classes.
    • Azure AD Provider
    • Azure AD Login Selector
      • This class is responsible for redirecting the user to the OAuth server login page, a URL with the format similar to http://localhost:4502/j_security_check?provider=xxxxxx&configid=my-granite-oauth-configid&state=/aem/start.html
      • The Service Ranking for this class needs to be higher than the OOTB LoginSelector
      • In the requestCredentials method, you will need get the client ID and the config ID from your Granite OAuth Provider configuration. You can use the ProviderConfigManager and the ProdviderConfig classes for this.
        • for example, providerConfigManager.getProviderConfig(my-granite-oauth-configid);
      • We had to ignore some URLs for being redirected as well, for example, if the request is for below URLs, you don’t want it redirected to the OAuth login screen
        • /libs/granite/core/content/login.html
        • /libs/granite/csrf/token.json
        • /callback/j_security_check
      • This class also replaces the need for having a Cloud Service configuration, but it only works if you want every AEM request to go through this auth provider. If you have a use case similar to allowing a “Social Login”, you will need to create your own Cloud Service configuration.
      • To create a custom Cloud Service configuration, follow the docs and reference the OOTB Facebook and Twitter Cloud Service configurations to create your own.
      • Don’t forget to include the state request parameter in your redirect URI to ensure the users are redirected to the appropriate page after successful login.

On the OAuth app side, the reply URL needs to be of the format https://host:port/callback/j_securitycheck.

As always, please let me know if there are any questions.

12:44 PM Permalink
January 3, 2018

Using a JSON file with the Dropdown widget on the Metadata Schema

Have to use where you need to setup a metadata schema with a dropdown widget where the options needed to come from a JSON file? Keep on reading…

First, the JSON file.

The JSON file needs to follow the same structure as below.

{
 "options": [
 {
 "value": "en-gb",
 "text": "English-United Kingdom"
 },
 {
 "value": "en-us",
 "text": "English-United States"
 },
 {
 "value": "fr-ca",
 "text": "French-Canada"
 },
 {
 "value": "fr-fr",
 "text": "French-France"
 },
 {
 "value": "de-de",
 "text": "German-Germany"
 },
 {
 "value": "es-mx",
 "text": "Spanish-Mexico"
 },
 {
 "value": "es-es",
 "text": "Spanish-Spain"
 }
 ]
}

There needs to be an “options” array and each element in the array needs to have a “value” and a “text”. The “text” value is what is displayed to the end user and the “value” value is what is saved in JCR.

Second, where to store the file.

You can upload this file to AEM as an Asset or upload it somewhere as a nt:file. Where its uploaded will define how the widget on the metadata schema needs to be configured.

Lastly, how to configure the widget.

The dropdown widget on the metadata schema lets you define a “JSON Path” property. This property needs to point to the nt:file node of the JSON file.

What does this mean?

If you have uploaded the JSON file as an Asset, this value needs to be set to the original rendition of the asset. For example /content/dam/myproject/myfile.json/jcr:content/renditions/original. If you have upload this file as an nt:file, the path would be something like /etc/myconfigurations/myfile.json.

At the time of writing this blog, there is a known issue where if the content of the JSON file changes, you need to open the schema again and hit save for the new changes to populate. 

12:05 PM Permalink
November 7, 2016

Do you consider yourself an AEM Rockstar?

Its time to make your AEM knowledge get you the fame and recognition you deserve!


 

aem_rockstar

EDIT (1/23/2017) : The submission for 2017 AEM Rockstar is now closed. If you have submitted an entry, we will soon be in touch. If you missed out, please consider submitting next year. Please register for the session at Summit to see the top 5 tip. Session details are http://bit.ly/2kbauhJ!

At Adobe Summit 2017, we’re introducing a brand new session where you get to be a rock star! We’re looking for the top tips & tricks from AEM marketers and technologists that we can feature in a fun, fast-paced, and informative breakout session.

Do you want to be on a Summit stage and share your own AEM tip? If you’re chosen as one of our presenters, you’ll receive a free pass (valued at $1895) to Summit on March 19-23, 2017 in Las Vegas*, and the opportunity to win cool prizes like a Drone, Surface Book, or who knows what! And don’t forget about the bragging rights.

To be considered, all you have to do is submit your tip using this online form. We’re looking for all things AEM. Sites, Assets, Mobile, Forms, anything and EVERYTHING! We want tips that you believe will show your fellow AEM marketers and technologists new things about AEM and help them to do their jobs more effectively and efficiently. Ultimately, your judges will be the session attendees, including leaders from the AEM Product and Marketing teams, as they vote for their favorite tips. Don’t be on the fence about submitting a tip — you might be surprised how useful things can be to others.

We’ll be screening your tips based on how innovative, practical, and valuable they are as well as how broadly they could be used by AEM marketers and technologists at other companies in different industries. We’ll select up to 5 finalists who will come to Las Vegas and present their tip to an audience. This is a great opportunity to attend Summit and showcase your talents to the world!

We are giving away some swag to everyone who submits an idea as well. Every person who submits an idea gets an AEM Rockstar sticker!

aem-rockstar-sticker

All submissions must be submitted by 1/22/2017. Soon after that we’ll reach out to all the applicants with next steps.

Go to AEM Rockstars Application Form

*You will be responsible for travel expenses. 
12:15 PM Permalink
October 4, 2013

Custom Component: Manual List Widget

Requirement: The users want the ability to manually choose a list of of items from a source list of items.

Example: A list of all the articles in the system is the source list and the users want to select the most important ones to be listed on the home page of a site.

To achieve this, I created a custom ExtJs widget.

  • The users are able to double click on an item on the left (source) list to move the said item to the right list
  • They can manually order the items on the right list via drag and drop.
  • They can double click the item on the right list to remove it from the list
  • They are also able to pass tags to limit the results (your custom servlet that returns the JSON object to be consumed by the widget will need to handle this use case)

Below are the steps to use this widget in your implementation.

1. Paste the widget code from below into a JS file in your CQ implementation.

//Create a new class based on existing CompositeField
CQ.form.GridtoGrid = CQ.Ext.extend(CQ.CustomContentPanel, {
    secondGrid: null,
    firstGrid: null,
    columnModel: null,
    reader: null,
    layout: 'fit',
    constructor : function(config){

        var dataSrcUrl;

        if( config.dataSrcUrl != null ){
            dataSrcUrl = config.dataSrcUrl;
        }

        var jsonReaderConfig;
        if( config.jsonReaderConfig != null ){
            jsonReaderConfig = config.jsonReaderConfig;
        }

        var sortDirection;
        if( config.sortDirection != null ){
            sortDirection = config.sortDirection;
        }

        var sortField;
        if( config.sortField != null ){
            sortField = config.sortField;
        }

        var columnModelConfig;
        if( config.columnModelConfig != null ){
            columnModelConfig = config.columnModelConfig;
        }

        console.log( dataSrcUrl );
        console.log( sortDirection );
        console.log( sortField );
        console.log( CQ.Ext.util.JSON.decode( jsonReaderConfig ) );
        console.log( CQ.Ext.util.JSON.decode( columnModelConfig ) );

        var proxy = new CQ.Ext.data.HttpProxy( {url:  dataSrcUrl} );

        reader = new CQ.Ext.data.JsonReader( {}, CQ.Ext.util.JSON.decode( jsonReaderConfig ) );

        var sourceStore = new CQ.Ext.data.Store(
            {
                proxy: proxy,
                sortInfo: {
                    field: sortField,
                    direction: sortDirection
                },
                reader: reader
            }
        );
        sourceStore.load();

        columnModel = new CQ.Ext.grid.ColumnModel( CQ.Ext.util.JSON.decode( columnModelConfig ) );

        firstGrid = new CQ.Ext.grid.GridPanel({
            listeners: {
                rowdblclick: function (grid, index){

                    //On DoubleClick, get the record from the sourceStore using the index.
                    var moveRecord = firstGrid.getStore().getAt( index );

                    //Add the record to the destinationStore.
                    secondGrid.getStore().add( moveRecord );
                }
            },
            store            : sourceStore,
            name             : 'sourceGrid',
            colModel          : columnModel,
            stripeRows       : true,
            title            : 'Latest Articles',
            margins          : '0 2 0 0',
            x: 0,
            y:100,
            height: 500

        });

        //destination store
        var destinationStore = new CQ.Ext.data.Store({
            reader: reader
        });

        //destination grid
        secondGrid = new CQ.Ext.grid.GridPanel({
            listeners: {
                rowdblclick: function (grid, index){
                    //On DoubleClick, get the record from the sourceStore using the index.
                    var moveRecord = secondGrid.getStore().getAt( index );

                    //Remove the record from the destinationStore.
                    secondGrid.getStore().remove( moveRecord );
                }
            },
            name: 'destinationGrid',
            store: destinationStore,
            colModel: columnModel,
            stripeRows       : true,
            title            : 'Displayed Articles',
            enableDragDrop: true,
            hideHeaders: true,
            ddGroup: 'ddGroup',
            margins          : '0 0 0 3',
            x: 450,
            y: 100,
            height: 500
        });

        //Set the dropZone for the second grid so that users can manually re-order the list.
        secondGrid.on('render', function() {

            secondGrid.dropZone = new CQ.Ext.dd.DropZone(secondGrid.getView().scroller, {
                ddGroup: 'ddGroup',

                //If the mouse is over a grid row, return that node. This is
                //provided as the "target" parameter in all "onNodeXXXX" node event handling functions
                getTargetFromEvent: function(e) {
                    return e.getTarget(secondGrid.getView().rowSelector);
                },

                //On entry into a target node, highlight that node.
                onNodeEnter : function(target, dd, e, data){
                    CQ.Ext.fly(target).addClass('my-row-highlight-class');
                },

                //On exit from a target node, unhighlight that node.
                onNodeOut : function(target, dd, e, data){
                    CQ.Ext.fly(target).removeClass('my-row-highlight-class');
                },

                //While over a target node, return the default drop allowed class which
                //places a "tick" icon into the drag proxy.
                onNodeOver : function(target, dd, e, data){
                    return CQ.Ext.dd.DropZone.prototype.dropAllowed;
                },

                //On NodeDrop event
                onNodeDrop : function(target, dd, e, data){

                    var targetRowIndex = secondGrid.getView().findRowIndex(target);
                    var sourceRowIndex = data.rowIndex;

                    if( targetRowIndex == false ){
                        return false;
                    }

                    if( targetRowIndex == sourceRowIndex ){
                        return false;
                    }

                    var ds = secondGrid.getStore();

                    var sourceRecord = ds.getAt(sourceRowIndex);

                    ds.insert( targetRowIndex, sourceRecord );

                    //Reconfigure the destination list to new store.
                    secondGrid.reconfigure ( ds, columnModel );

                    return true;
                }
            });
        })

        //Tag Inputbox
        var tagInputField = new CQ.tagging.TagInputField({
            listeners:{
                addtag: function( inputField, tag ){

                    var proxyUrl = firstGrid.getStore().proxy.url;
                    var tagId = tag.tagID;

                    var tagsIndex = proxyUrl.indexOf( '?tags=' );

                    if( tagsIndex > -1 ){
                        proxyUrl = proxyUrl + "," + tagId;
                    } else {
                        proxyUrl = proxyUrl + "?tags=" + tagId;
                    }
                    //alert( "Add Tag: " + proxyUrl );
                    var updatedSourceStore = new CQ.Ext.data.Store({
                        url: proxyUrl,
                        reader: reader
                    });

                    updatedSourceStore.load();
                    firstGrid.reconfigure( updatedSourceStore, columnModel );
                },
                removetag: function( inputField, tag ){
                    var proxyUrl = firstGrid.getStore().proxy.url;
                    var tagId = tag.tagID;

                    var splitUrl = new Array();
                    splitUrl = proxyUrl.split( '?tags=' );

                    //alert( "Remove Tag 1 : " + proxyUrl );

                    if( splitUrl[1].indexOf( "," + tagId ) > -1 ){
                        splitUrl[1] = splitUrl[1].replace( "," + tagId, "" );
                    } else {
                        splitUrl[1] = splitUrl[1].replace( tagId , "" );
                    }

                    if( splitUrl[1].indexOf( ',' ) == 0 ){
                        splitUrl[1] = splitUrl[1].substring(1);
                    }

                    if( splitUrl[1] != null && splitUrl[1].length > 0 ){
                        proxyUrl = splitUrl[0] + '?tags=' + splitUrl[1];
                    } else {
                        proxyUrl = splitUrl[0];
                    }
                    //alert( "Remove Tag 2 : " + proxyUrl );

                    var updatedSourceStore = new CQ.Ext.data.Store({
                        url: proxyUrl,
                        reader: reader
                    });

                    updatedSourceStore.load();
                    firstGrid.reconfigure( updatedSourceStore, columnModel );
                }

            },
            title: 'Tag Properties',
            name: './tags',
            x: 250,
            y: 0,
            width: 400
        });
        CQ.form.GridtoGrid.superclass.constructor.call(this, config);

        this.selectionForm = new CQ.Ext.Panel({
            name         : this.name,
            width        : 'auto',
            height       : 600,
            border       : false,
            layout       : {
                type: 'absolute',
                padding: 0
            },
            items        : [
                tagInputField,
                firstGrid,
                secondGrid
            ]
        });
        this.add(this.selectionForm);

    },
    submitPanel: function() {

        var store = secondGrid.getStore();
        var storeData = store.data;
        var storeFields = store.fields;
        var params = {};

        if( storeData.length > 0 ){
            for(var i = 0; i < storeData.length; i++) {
                for(var j = 0; j < storeFields.length; j++){
                    var name = storeFields.item(j).name;
                    if (!params[name]) {
                        params[name] = new Array();
                    }

                    if( name == 'headline' ){
                        var headline = storeData.get(i).get(name);
                        if( headline.indexOf ( '_' ) == headline.length - 1 ){
                            params[name] = params[name].concat( storeData.get(i).get(name));
                        } else {
                            params[name] = params[name].concat( storeData.get(i).get(name) + "_");
                        }
                    } else {
                        params[name] = params[name].concat( storeData.get(i).get(name));
                    }
                }
            }
        }

        var serverResponse = CQ.utils.HTTP.post(this.urlPost, null, params, this);
        return CQ.utils.HTTP.isOk(serverResponse);
    },
    loadContent: function() {

        loadStore = null;

        if (!this.content) {
            var url = CQ.HTTP.externalize(this.urlGet);
            loadStore = new CQ.data.SlingStore({
                url: url+'.infinity.json'
            });
        } else if (this.content instanceof CQ.Ext.data.Store) {
            loadStore = this.content;
        }

        loadStore.load({
            callback: this.processRecords,
            scope: this
        });

    },
    processRecords: function(){

        var tagString = loadStore.getAt(0).get('tags');
        var sourceProxyUrl = firstGrid.getStore().proxy.url;

        if( tagString != undefined && sourceProxyUrl.indexOf( tagString ) == -1 ){
            sourceProxyUrl = sourceProxyUrl + "?tags=" + tagString;
        }
        //alert( "Process Records: " + sourceProxyUrl );
        var updatedSourceStore = new CQ.Ext.data.Store({
            url: sourceProxyUrl,
            reader: reader
        });

        updatedSourceStore.load();
        firstGrid.reconfigure( updatedSourceStore, columnModel );

        var tempHeadlineData = loadStore.getAt(0).get('headline');
        if( tempHeadlineData != undefined ){
            tempHeadlineData = "" + tempHeadlineData;
            var headlineData = new Array();
            headlineData = tempHeadlineData.split('_,');

            var tempPathData = loadStore.getAt(0).get('path');
            var tempDateData = loadStore.getAt(0).get('date');

            var secondGridData = [];

            for( var i = 0; i < headlineData.length; i++ ){
                var headline = headlineData[i].replace('_', '');
                secondGridData.push({headline: headline, path: tempPathData[i], date: tempDateData[i]});
            }

            //destination store
            var destinationStore = new CQ.Ext.data.Store({
                reader: reader
            });

            destinationStore.loadData( secondGridData );

            secondGrid.reconfigure( destinationStore, columnModel );
        }

    }
});
CQ.Ext.reg("gridtogrid", CQ.form.GridtoGrid);

2. Create a component in CQ and in the dialog add a widget node with the following properties.

Screen Shot 2013-10-04 at 12.18.59 PM

  • xtype : gridtogrid –> this matches to what you register the widget as in the javascript.
  • dataSrcUrl –> This is the URL which returns a JSON object that is consumed by the JSONReader in the widget JS
  • jsonReaderConfig –> Configuration for the JSON reader object
    • example: [{name: ‘headline’, mapping: ‘headline’},{name:’date’, mapping:’date’}]
  • columnModelConfig –> Configuration for the column model to be used in the left grid.
    • example: [{header: ‘Headline’, width: 500, sortable: false, dataIndex: ‘headline’},{header: ‘Date’, width: 200, sortable: false, dataIndex: ‘date’, hidden: true}]
  • sortField –> The name of the field that the data should be sorted on
  • sortDirection –> ASC/DESC
  • name –> name of property on the content node to store the destination list under. It will stored as a String[].

Screen Shot 2013-10-04 at 12.28.30 PM

3. Add the component on your page and double click to open the dialog. You should now see a list on the left and can populate the right based on the capabilities described above (double clicks).

Screen Shot 2013-10-04 at 12.39.13 PM

 

 

4. An example servlet that returns a JSON object to be consumed by the widget above is below

final ValueMap attributes = resource.adaptTo(ValueMap.class);
try{
TidyJSONWriter writer = new TidyJSONWriter(out);
String headline = "Headline Not Set";
Calendar date = null;
String stringDate = null;
SimpleDateFormat sdf = new SimpleDateFormat( "yyyy-MM-dd" );
String path = null;

String tagsParameter = request.getParameter( "tags" );

if( tagsParameter != null ){
String[] tags = tagsParameter.split( "," );

NodeIterator articleList = Headlines.getArticleListByTags( currentNode.getSession(), tags, 25 );

if( articleList != null && articleList.getSize() > 0 ){
writer.array();
for( NodeIterator ni = articleList; articleList.hasNext(); ){
Node article = articleList.nextNode();

//Get Headline
try{
headline = article.getProperty( "headline" ).getValue().getString();
} catch (Exception e){
log.info( "headline doesn't exist, using static value instead" );
}

//Get Date
Node contentNode = currentNode.getSession().getNode( article.getPath().replace( "/contentpar/articleBody", "" ) );
try{
date = contentNode.getProperty( "cq:lastModified" ).getValue().getDate();
} catch (Exception e){
log.info( "cq:lastModified doesn't exist, using jcr:created instead" );
date = contentNode.getProperty( "jcr:created" ).getValue().getDate();
}

//Convert date to String
if( date != null ){
stringDate = sdf.format( date.getTime() );
} else {
log.info( "Date Object was null" );
}

//Get Path
path = article.getPath().replace("/jcr:content/contentpar/articleBody","");
writer.object();
writer.key("headline").value( headline );
writer.key("path").value( path );
writer.key("date").value(stringDate);
writer.endObject();
}
writer.endArray();
}

} else {
NodeIterator articleList = Headlines.getArticleListByModifiedDate( currentNode.getSession(), 25 );

if( articleList != null && articleList.getSize() > 0 ){
writer.array();
for( NodeIterator ni = articleList; articleList.hasNext(); ){
Node article = articleList.nextNode();
Node bodyNode = currentNode.getSession().getNode( article.getPath() + "/contentpar/articleBody" );

//Get Headline
try{
headline = bodyNode.getProperty("headline").getValue().getString();
} catch( Exception e ){
log.info( "headline doesn't exist, using static value instead" );
}

//Get lastModifiedDate
try{
date = article.getProperty( "cq:lastModified" ).getValue().getDate();
} catch (Exception e){
log.info( "cq:lastModified doesn't exist, using jcr:created instead" );
date = article.getProperty( "jcr:created" ).getValue().getDate();
}

if( date != null ){
stringDate = sdf.format( date.getTime() );
} else {
log.info( "Date Object was null" );
}

//Get Path
path = bodyNode.getPath().replace("/jcr:content/contentpar/articleBody","");

writer.object();
writer.key("headline").value( headline );
writer.key("path").value( path );
writer.key("date").value(stringDate);
writer.endObject();

}
writer.endArray();
}
}

} catch (Exception e){
log.info( e.getMessage() );
}

I can post a video of the widget in action if this blog post isn’t descriptive  enough. As always, please post comments/questions and I will try to get back to you as soon as possible.

12:46 PM Permalink
  • Authors

  • Archives

  • Developer Resources