Quantcast
Channel: Experiencing Adobe Experience Manager (AEM, CQ)
Viewing all 513 articles
Browse latest View live

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

$
0
0

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 that sample extension developed in this post will run with minor changes (like adding an entry in CSXS/manifest.xml) in PhotoShop...

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





Distribute & Install

                a. To distribute the extension for installation on user's CC products you need 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
};

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;
}

//couldn't find a better way to check login success - success, fail both return 200
var isLoginFailure = function(html) {
return !html || (html.indexOf('class="coral-App"') == -1);
};

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

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>




AEM 6 SP1 - Rendition Maker Sample

$
0
0

Goal


A Sample OSGI Servlet demonstrating Rendition Maker. Creates a 250 X 250 thumbnail rendition

Source Code | Package Install

Install the OSGI Servlet apps.experienceaem.rms.SampleRenditionMaker and access /bin/experienceaem/rms with path ( eg. http://localhost:4502/bin/experience-aem/rms?path=/content/dam/Product/Desert.jpg)

Sample generated - http://localhost:4502/content/dam/Product/Desert.jpg/jcr:content/renditions/cq5dam.thumbnail.250.250.png




Servlet Source


package apps.experienceaem.rms;

import com.day.cq.dam.api.Asset;
import com.day.cq.dam.api.Rendition;
import com.day.cq.dam.api.renditions.RenditionMaker;
import com.day.cq.dam.api.renditions.RenditionTemplate;
import com.day.cq.dam.api.thumbnail.ThumbnailConfig;
import com.day.cq.dam.commons.thumbnail.ThumbnailConfigImpl;
import org.apache.felix.scr.annotations.*;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.commons.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Node;
import javax.jcr.Session;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.List;

@Component(metatype = true, label = "Experience AEM Sample Rendition Maker")
@Service
@Properties({
@Property(name = "sling.servlet.methods", value = {"GET"}, propertyPrivate = true),
@Property(name = "sling.servlet.paths", value = "/bin/experience-aem/rms", propertyPrivate = true),
@Property(name = "sling.servlet.extensions", value = "json", propertyPrivate = true)})
public class SampleRenditionMaker extends SlingAllMethodsServlet {
private static final Logger log = LoggerFactory.getLogger(SampleRenditionMaker.class);

@Reference
private RenditionMaker renditionMaker;

protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException,IOException {
JSONObject json = new JSONObject();

try {
ResourceResolver resolver = request.getResourceResolver();
String path = request.getParameter("path");

if (path == null) {
json.put("error", "Empty path");
return;
}

Session session = resolver.adaptTo(Session.class);
Asset asset = resolver.getResource(path).adaptTo(Asset.class);

RenditionTemplate[] templates = createRenditionTemplates(asset);

List<Rendition> renditionList = renditionMaker.generateRenditions(asset, templates);

session.save();

json.put("success", "Created - " + renditionList.get(0).getPath());

json.write(response.getWriter());
} catch (Exception e) {
log.error("Error processing request", e);
}
}

private RenditionTemplate[] createRenditionTemplates(Asset asset) {
ThumbnailConfig[] thumbnails = new ThumbnailConfig[1];
thumbnails[0] = new ThumbnailConfigImpl(250,250,false);

RenditionTemplate[] templates = new RenditionTemplate[thumbnails.length];

for (int i = 0; i < thumbnails.length; i++) {
ThumbnailConfig thumb = thumbnails[i];

templates[i] = renditionMaker.createThumbnailTemplate(asset,thumb.getWidth(),
thumb.getHeight(),thumb.doCenter());
}

return templates;
}
}

AEM 6 SP1 - Touch UI Rich Text Editor Plugin To Upper Case

$
0
0

Goal


Create a Touch UI RTE (Rich Text Editor) plugin for converting text to upper case. Here we use css style attribute text-transform:uppercase to convert any selected text in editor to upper case

Demo shows dialog of foundation text component modified to add the plugin config. This is just for demonstration only (on Geometrixx pages), ideally the foundation components should never be altered...

A Sample classic UI RTE Extension is available here

Demo | Package Install


Full Screen View






Inline View





Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/rte-touch-ui-to-upper-case

2) Create node /apps/rte-touch-ui-to-upper-case/clientlib of type cq:ClientLibraryFolder and add a String property categories with value rte.coralui2

3) Create file (nt:file) /apps/rte-touch-ui-to-upper-case/clientlib/css.txt and add

                       to-upper.css

4) Create file (nt:file) /apps/rte-touch-ui-to-upper-case/clientlib/to-upper.css and add the following code. Content is available in font-family: AdobeIcons

.eam-touchui-to-upper-case::before {
content: "\f294";
}

5) Create file (nt:file) /apps/rte-touch-ui-to-upper-case/clientlib/js.txt and add

                       to-upper.js

6) Create file (nt:file) /apps/rte-touch-ui-to-upper-case/clientlib/to-upper.js and add the following code.

(function(){
var ExperienceAEM = {
TUC_UI_SETTING: "touchuitouppercase#touchuitouppercase",
TUC_FEATURE: "touchuitouppercase"
};

//extend toolbar builder to register upper case styles
ExperienceAEM.CuiToolbarBuilder = new Class({
toString: "EAEMCuiToolbarBuilder",

extend: CUI.rte.ui.cui.CuiToolbarBuilder,

//add uppercase toolbar icon to the existing set
_getUISettings: function(options) {
var uiSettings = this.superClass._getUISettings(options);

//inline toolbar
var items = uiSettings["inline"]["popovers"]["format"].items;

if(items.indexOf(ExperienceAEM.TUC_UI_SETTING) == -1){
items.push(ExperienceAEM.TUC_UI_SETTING);
}

//fullscreen toolbar
items = uiSettings["fullscreen"]["toolbar"];

if(items.indexOf(ExperienceAEM.TUC_UI_SETTING) == -1){
items.splice(3, 0, ExperienceAEM.TUC_UI_SETTING);
}

if(!this._getClassesForCommand(ExperienceAEM.TUC_UI_SETTING)){
this.registerAdditionalClasses(ExperienceAEM.TUC_UI_SETTING, "coral-Icon eam-touchui-to-upper-case");
}

return uiSettings;
}
});

ExperienceAEM.ToolkitImpl = new Class({
toString: "EAEMToolkitImpl",

extend: CUI.rte.ui.cui.ToolkitImpl,

createToolbarBuilder: function() {
return new ExperienceAEM.CuiToolbarBuilder();
}
});

CUI.rte.ui.ToolkitRegistry.register("cui", ExperienceAEM.ToolkitImpl);

ExperienceAEM.TouchUIUpperCasePlugin = new Class({
toString: "TouchUIUpperCasePlugin",

extend: CUI.rte.plugins.Plugin,

pickerUI: null,

getFeatures: function() {
return [ ExperienceAEM.TUC_FEATURE ];
},

initializeUI: function(tbGenerator) {
var plg = CUI.rte.plugins;

if (this.isFeatureEnabled(ExperienceAEM.TUC_FEATURE)) {
this.pickerUI = tbGenerator.createElement(ExperienceAEM.TUC_FEATURE, this, true, "To Upper Case");
tbGenerator.addElement("format", plg.Plugin.SORT_FORMAT, this.pickerUI, 120);
}
},

execute: function(id) {
this.editorKernel.relayCmd(id);
},

//to mark the uppercase icon selected/deselected
updateState: function(selDef) {
var hasUC = this.editorKernel.queryState(ExperienceAEM.TUC_FEATURE, selDef);

if (this.pickerUI != null) {
this.pickerUI.setSelected(hasUC);
}
},

notifyPluginConfig: function(pluginConfig) {
pluginConfig = pluginConfig || { };

var defaults = {
"tooltips": {
"touchuitouppercase": {
"title": "To Upper Case",
"text": "To Upper Case"
}
}
};

CUI.rte.Utils.applyDefaults(pluginConfig, defaults);

this.config = pluginConfig;
}
});

CUI.rte.plugins.PluginRegistry.register(ExperienceAEM.TUC_FEATURE,ExperienceAEM.TouchUIUpperCasePlugin);

ExperienceAEM.UpperCaseCmd = new Class({
toString: "UpperCaseCmd",

extend: CUI.rte.commands.Command,

isCommand: function(cmdStr) {
return (cmdStr.toLowerCase() == ExperienceAEM.TUC_FEATURE);
},

getProcessingOptions: function() {
var cmd = CUI.rte.commands.Command;
return cmd.PO_SELECTION | cmd.PO_BOOKMARK | cmd.PO_NODELIST;
},

_getTagObject: function() {
return {
"tag": "span",
"attributes": {
"style" : "text-transform:uppercase"
}
};
},

execute: function(execDef) {
var selection = execDef.selection;

if (!selection) {
return;
}

var nodeList = execDef.nodeList;

if (!nodeList) {
return;
}

var common = CUI.rte.Common;
var context = execDef.editContext;

var tagObj = this._getTagObject();

var tags = common.getTagInPath(context, selection.startNode, tagObj.tag, tagObj.attributes);

if (tags == null) {
nodeList.surround(execDef.editContext, tagObj.tag, tagObj.attributes);
} else {
nodeList.removeNodesByTag(execDef.editContext, tagObj.tag, tagObj.attributes, true);
}
},

queryState: function(selectionDef, cmd) {
var common = CUI.rte.Common;
var context = selectionDef.editContext;

var selection = selectionDef.selection;
var tagObj = this._getTagObject();

return (common.getTagInPath(context, selection.startNode, tagObj.tag, tagObj.attributes) != null);
}
});

CUI.rte.commands.CommandRegistry.register(ExperienceAEM.TUC_FEATURE, ExperienceAEM.UpperCaseCmd);
})();

AEM 6 - Classic UI Restrict User from Creating Pages with Same Title

$
0
0

Goal


Extend Classic UI Create Page Dialog to restrict user from creating pages with same title in a path. Create button is disabled when user opens the dialog and enabled only when user enters a unique title

Demo | Package Install






Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/classic-ui-no-page-duplication

2) Create node /apps/classic-ui-no-page-duplication/clientlib of type cq:ClientLibraryFolder and add a String property categories with value cq.widgets

3) Create file (nt:file) /apps/classic-ui-no-page-duplication/clientlib/js.txt and add

                       check-page-name.js

4) Create file (nt:file) /apps/classic-ui-no-page-duplication/clientlib/check-page-name.js and add the following code.

(function(){
//the original create page dialog fn
var cqCreatePageDialog = CQ.wcm.Page.getCreatePageDialog;

//override ootb function
CQ.wcm.Page.getCreatePageDialog = function(parentPath){
//create dialog by executing the product function
var dialog = cqCreatePageDialog(parentPath);

try{
//disable create until page title gets validated
var createButton = dialog.buttons[0];
createButton.setDisabled(true);

//make necessary UI changes to the dialog created above
var panel = dialog.findBy(function(comp){
return comp["jcr:primaryType"] == "cq:Panel";
}, dialog);

if(!panel || !panel.length){
return;
}

panel = panel[0];

//get title field
var titleField = panel.findBy(function(comp){
return comp["fieldLabel"] == "Title";
}, panel);

if(!titleField || !titleField.length){
return;
}

titleField = titleField[0];

titleField.on('change', function(t, nvalue){
//when user enters title, search CRX if a title with same wording exists
$.ajax({
type: "GET",
url: "/bin/querybuilder.json",
data: {
"path": parentPath,
"0_property": "jcr:title",
"0_property.value": nvalue
}
}).done(function(data){
if(data && data.hits && data.hits.length > 0){
CQ.Ext.Msg.alert("Error", "Page " + nvalue + " exists in path " + parentPath);
return;
}

//not a duplicate, enable create button
createButton.setDisabled(false);
})
})
}catch(err){
console.log("Error executing CQ.wcm.Page.getCreatePageDialog override");
}

return dialog;
}
})();


AEM 6 SP1 - TouchUI Richtext Editor Color Picker Plugin

$
0
0

Goal


Create a Color Picker Plugin (CUI.Colorpicker) for the Touch UI Rich Text Editor (RTE). A Color Picker plugin for Classic UI RTE is available here

Demo shows dialog of foundation text component (/libs/foundation/components/text/dialog/items/tab1/items/text/rtePlugins) modified to add the color picker config. This is just for demonstration only (on Geometrixx pages), ideally the foundation components should never be altered...

Please leave a comment if you find bug / fix...

Demo | Package Install


Picker Configuration

Configure the palette colors in rtePlugins touchuicolorpicker node property colors as String[]






Inline View






Full Screen View





Color applied to text wrapped in span tag




Text in CRX node





Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/rte-touch-ui-color-picker

2) Create clientlib (type cq:ClientLibraryFolder/apps/rte-touch-ui-color-picker/clientlib and set property categories of String type to rte.coralui2

3) Create file ( type nt:file ) /apps/rte-touch-ui-color-picker/clientlib/css.txt, add the following

                         color-picker.css

4) Create file (type nt:file) /apps/rte-touch-ui-color-picker/clientlib/color-picker.css, add the following code. This is palette button (available in font-family: font-family: AdobeIcons)

.eaem-touchui-color-picker::before {
content: "\f156";
}

5) Create file ( type nt:file ) /apps/rte-touch-ui-color-picker/clientlib/js.txt, add the following

                         color-picker.js

6) Create file (type nt:file) /apps/rte-touch-ui-color-picker/clientlib/color-picker.js, add the following code

(function(){
var ExperienceAEM = {
TCP_UI_SETTING: "touchuicolorpicker#touchuicolorpicker",
TCP_FEATURE: "touchuicolorpicker",
TCP_DIALOG: "touchuicolorpickerdialog"
};

ExperienceAEM.CuiToolbarBuilder = new Class({
toString: "EAEMCuiToolbarBuilder",

extend: CUI.rte.ui.cui.CuiToolbarBuilder,

_getUISettings: function(options) {
var uiSettings = this.superClass._getUISettings(options);

var items = uiSettings["inline"]["popovers"]["format"].items;

if(items.indexOf(ExperienceAEM.TCP_UI_SETTING) == -1){
items.push(ExperienceAEM.TCP_UI_SETTING);
}

items = uiSettings["fullscreen"]["toolbar"];

if(items.indexOf(ExperienceAEM.TCP_UI_SETTING) == -1){
items.splice(3, 0, ExperienceAEM.TCP_UI_SETTING);
}

//add the color picker css to ui settings of toolbar
if(!this._getClassesForCommand(ExperienceAEM.TCP_UI_SETTING)){
this.registerAdditionalClasses(ExperienceAEM.TCP_UI_SETTING, "coral-Icon eaem-touchui-color-picker");
}

return uiSettings;
}
});

//the popover dialog
ExperienceAEM.ColorPickerDialog = new Class({
extend: CUI.rte.ui.cui.AbstractBaseDialog,

toString: "EAEMColorPickerDialog",

initialize: function(config) {
//exec function passes the color value to plugin command
this.exec = config.execute;
},

getDataType: function() {
return ExperienceAEM.TCP_DIALOG;
},

attach: function(config, $container, editorKernel) {
this.superClass.attach.call(this,config, $container, editorKernel);

var self = this;

//to removed previously selected color
this.$dialog.on("click.rte-dialog", "button[data-type=\"delete\"]",
function(e) {
self.colorPicker.$element.removeAttr("value");
self.apply();
e.stopPropagation();
}
);
},

apply: function() {
this.hide();

if(!this.colorPicker){
return;
}

var $selection = this.colorPicker.$element.find(".colorpicker-holder").find(".selection");

var hexCode = $selection.length > 0 ? $selection.find("span:last-child").html() : undefined;

//pass the color value to command
this.exec(hexCode);
},

cancel: function() {
this.hide();
}
});

//extend the CUI dialog manager to register color picker dialog
ExperienceAEM.DialogManager = new Class({
toString: "EAEMDialogManager",

extend: CUI.rte.ui.cui.CuiDialogManager,

create: function(dialogId, config) {
if(dialogId !== ExperienceAEM.TCP_DIALOG){
return this.superClass.create.call(this,dialogId, config);
}

var context = this.editorKernel.getEditContext();
var $container = CUI.rte.UIUtils.getUIContainer($(context.root));

var dialog = new ExperienceAEM.ColorPickerDialog();
dialog.attach(config, $container, this.editorKernel);

return dialog;
}
});

//extend CUI toolkit impl to create instances of extended toolbar builder and dialog manager
ExperienceAEM.ToolkitImpl = new Class({
toString: "EAEMCuiToolbarBuilder",

extend: CUI.rte.ui.cui.ToolkitImpl,

createToolbarBuilder: function() {
return new ExperienceAEM.CuiToolbarBuilder();
},

createDialogManager: function(editorKernel) {
return new ExperienceAEM.DialogManager(editorKernel);
}
});

CUI.rte.ui.ToolkitRegistry.register("cui", ExperienceAEM.ToolkitImpl);

//the color picker plugin for touch ui
ExperienceAEM.TouchUIColorPickerPlugin = new Class({
toString: "TouchUIColorPickerPlugin",

extend: CUI.rte.plugins.Plugin,

pickerUI: null,

getFeatures: function() {
return [ ExperienceAEM.TCP_FEATURE ];
},

initializeUI: function(tbGenerator) {
var plg = CUI.rte.plugins;

//add the color picker
if (this.isFeatureEnabled(ExperienceAEM.TCP_FEATURE)) {
this.pickerUI = tbGenerator.createElement(ExperienceAEM.TCP_FEATURE, this, true, "Color Picker");
tbGenerator.addElement("format", plg.Plugin.SORT_FORMAT, this.pickerUI, 140);
}
},

//executes when user clicks on color picker button to open the picker dialog
execute: function(id, value, envOptions) {
var ek = this.editorKernel;
var dm = ek.getDialogManager();

if (dm.isShown(this.dialog)) {
dm.hide(this.dialog);
return;
}

var dialogConfig = {
execute: function(value) {
ek.relayCmd(id, value);
},
parameters : {
"command": ExperienceAEM.TCP_UI_SETTING
}
};

//create or get existing dialog
this.dialog = dm.create(ExperienceAEM.TCP_DIALOG, dialogConfig);

dm.prepareShow(this.dialog);

dm.show(this.dialog);

if(!this.dialog.colorPicker){
//default colors if the colors are not configured for plugin in crx
var colors = { "White" : "FFFFFF", "Yellow" : "FFFF00" };

if(this.config.colors){
colors = this.config.colors;
}

var options = {
element : $('[data-rte-dialog="' + ExperienceAEM.TCP_DIALOG + '"] .coral-ColorPicker'),
config : {
colors: colors,
displayModes : {
"freestylePalette" : true,
"edit" : false
}
}
};

//create the picker
this.dialog.colorPicker = new CUI.Colorpicker(options);
}

var context = envOptions.editContext;

var selection = CUI.rte.Selection.createProcessingSelection(context);
var tag = CUI.rte.Common.getTagInPath(context, selection.startNode, "span" );

//get existing color to initialize picker
var color = $(tag).css("color");

if(color){
this.dialog.colorPicker._setColor(color);
}
},

updateState: function(selDef) {
var hasColorPicker = this.editorKernel.queryState(ExperienceAEM.TCP_FEATURE, selDef);

if (this.pickerUI != null) {
this.pickerUI.setSelected(hasColorPicker);
}
}
});

CUI.rte.plugins.PluginRegistry.register(ExperienceAEM.TCP_FEATURE,
ExperienceAEM.TouchUIColorPickerPlugin);

//the command for making text colored
ExperienceAEM.ColorPickerCmd = new Class({
toString: "ColorPickerCmd",

extend: CUI.rte.commands.Command,

isCommand: function(cmdStr) {
return (cmdStr.toLowerCase() == ExperienceAEM.TCP_FEATURE);
},

getProcessingOptions: function() {
var cmd = CUI.rte.commands.Command;
return cmd.PO_SELECTION | cmd.PO_BOOKMARK | cmd.PO_NODELIST;
},

_getTagObject: function(color) {
return {
"tag": "span",
"attributes": {
"style" : "color: " + color
}
};
},

execute: function(execDef) {
var selection = execDef.selection;

if (!selection) {
return;
}

var nodeList = execDef.nodeList;

if (!nodeList) {
return;
}

var common = CUI.rte.Common;
var context = execDef.editContext;

var tagObj = this._getTagObject(execDef.value);

//if no color value passed, assume delete and remove color
if(!execDef.value){
nodeList.removeNodesByTag(execDef.editContext, tagObj.tag, undefined, true);
return;
}

var tags = common.getTagInPath(context, selection.startNode, tagObj.tag);

//remove existing color before adding new color
if (tags != null) {
nodeList.removeNodesByTag(execDef.editContext, tagObj.tag, undefined, true);
}

nodeList.surround(execDef.editContext, tagObj.tag, tagObj.attributes);
},

queryState: function(selectionDef) {
var common = CUI.rte.Common;
var context = selectionDef.editContext;

var selection = selectionDef.selection;
var tagObj = this._getTagObject();

return (common.getTagInPath(context, selection.startNode, tagObj.tag, tagObj.attributes) != null);
}
});

CUI.rte.commands.CommandRegistry.register(ExperienceAEM.TCP_FEATURE, ExperienceAEM.ColorPickerCmd);

//returns the picker dialog html
//Handlebars doesn't do anything useful here, but the framework expects a template
var cpTemplate = function(){
CUI.rte.Templates["dlg-" + ExperienceAEM.TCP_DIALOG] =
Handlebars.compile('<div data-rte-dialog="' + ExperienceAEM.TCP_DIALOG + '" class="coral--dark coral-Popover coral-RichText-dialog">'
+ '<div class="coral-RichText-dialog-columnContainer">'
+ '<div class="coral-RichText-dialog-column">'
+ '<label class="coral-Form-fieldlabel">Select color </label>'
+ '</div>'
+ '<div class="coral-RichText-dialog-column">'
+ '<span class="coral-Form-field coral-ColorPicker">'
+ '<button class="coral-ColorPicker-button coral-MinimalButton" type="button"></button>'
+ '</span>'
+ '</div>'
+ '<div class="coral-RichText-dialog-column">'
+ '<button data-type="apply" class="coral-RichText-dialogButton coral-Icon coral-Icon--check coral-Icon--sizeS coral-RichText--white coral-Button--primary"></button>'
+ '</div>'
+ '<div class="coral-RichText-dialog-column">'
+ '<button data-type="cancel" class="coral-RichText-dialogButton coral-Icon coral-Icon--close coral-Icon--sizeS coral-RichText--white"></button>'
+ '</div>'
+ '<div class="coral-RichText-dialog-column">'
+ '<button data-type="delete" class="coral-RichText-dialogButton coral-Icon coral-Icon--delete coral-Icon--sizeS coral-RichText--white coral-Button--warning"></button>'
+ '</div>'
+ '</div>'
+ '</div>');
};

cpTemplate();
})();


7) Add any text component with RichText editor and in the rtePlugins path of dialog add touchuicolorpicker node to enable color picker plugin

AEM - Browser Extension to Open Publish Page in Author

$
0
0

Goal


Develop simple Chrome and Firefox browser Access and Edit Extension to open a published page in author instance. Assuming author is on default port http://localhost:4502 and publish on http://localhost:4503, this extension allows user to open a published page, for authoring in a new browser tab. Eg. if user has published page http://localhost:4503/content/geometrixx-outdoors/en.html open in a browser tab, clicking on the extension opens Classic UI http://localhost:4502/cf#/content/geometrixx-outdoors/en.html in new tab (if not logged in on author instance, the login page is shown first)

Demo

A very useful chrome extension that every AEM developer should have is available on chrome web store


Chrome





Firefox






Develop for Chrome


1) Create a folder C:\dev\open-page-in-author-browser-ext\chrome

2) Create file C:\dev\open-page-in-author-browser-ext\chrome\manifest.json with following data. Script eaem.js is the runtime for extension with necessary JS logic to open a new tab

{
"manifest_version": 2,
"name": "Access and Edit",
"description": "Access and Editing Tool for Adobe Experience Manager",
"version": "1.0",

"browser_action": {
"default_icon": "pencil.png"
},

"permissions": [
"tabs",
"http://*/*",
"https://*/*"
],

"background": {
"scripts": ["eaem.js"]
}
}

3) Add the pencil icon to C:\dev\open-page-in-author-browser-ext\chrome

4) Create file C:\dev\open-page-in-author-browser-ext\chrome\eaem.js and add following code

(function(){
var authorPrefix = 'http://localhost:4502';

chrome.browserAction.onClicked.addListener(function(tab) {
chrome.tabs.getSelected(null, function(tab){
var parser = document.createElement('a');
parser.href = tab.url;

var path = "";

if(parser.pathname.indexOf("/cf") == 0){
path = parser.pathname + parser.hash + parser.search;
}else{
path = "/cf#" + parser.pathname + parser.search;
}

path = authorPrefix + path;

chrome.tabs.create({url : path});
});
});
})();

5) Chrome enables only web store extensions (and this extension is not web store worthy :) ); to use this extension use chrome developer mode and load it




6) A small pencil icon should appear on the top right of chrome; open any page on publish in a tab eg. http://localhost:4503/content/geometrixx-outdoors/en/women.html, click on the pencil and chrome opens the same page on author in new tab (a login page if user was not already authenticated)

8) Download code as zip


Develop for Firefox


1) Create a folder C:\dev\open-page-in-author-browser-ext\firefox

2) Create file C:\dev\open-page-in-author-browser-ext\firefox\install.rdf with following xml. Just bragging about yourself :)

<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>fake@experience-aem.blogspot.com</em:id>
<em:name>Access and Edit</em:name>
<em:description>Access and Editing Tool for Adobe Experience Manager</em:description>
<em:version>1.0</em:version>
<em:creator>Experience AEM</em:creator>
<em:homepageURL>http://experience-aem.blogspot.com</em:homepageURL>
<em:type>2</em:type>
<em:targetApplication>
<Description>
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
<em:minVersion>4.0</em:minVersion>
<em:maxVersion>14.*</em:maxVersion>
</Description>
</em:targetApplication>
</Description>
</RDF>

3) Create file C:\dev\open-page-in-author-browser-ext\firefox\chrome.manifest with following content, overlaying the browser chrome for adding your extension pencil button; the extension will be available in eaem folder

content eaem content/

overlay chrome://browser/content/browser.xul chrome://eaem/content/eaem.xul

4) Create file C:\dev\open-page-in-author-browser-ext\firefox\content\pencil.png

5) Create file C:\dev\open-page-in-author-browser-ext\firefox\content\eaem.properties with author hostname property (read in extension JS logic)

                     author.prefix = http://localhost:4502

6) Create file C:\dev\open-page-in-author-browser-ext\firefox\content\eaem.xul, overlay file assembling the extension (adding pencil, click action etc.)

<?xml version="1.0"?>
<overlay id="eaem-browser-overlay"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">

<script type="application/x-javascript" src="chrome://eaem/content/eaem.js" />

<stringbundleset id="stringbundleset">
<stringbundle id="eaem-string-bundle"
src="chrome://eaem/content/eaem.properties" />
</stringbundleset>

<toolbarpalette id="BrowserToolbarPalette">
<toolbarbutton id="eaem-navbar-button"
class="toolbarbutton-1 chromeclass-toolbar-additional"
label="Access Edit"
image="chrome://eaem/content/pencil.png"
tooltiptext="Access and Editing"
oncommand="ExperienceAEM.AccessAndEditing.open(event)">
</toolbarbutton>
</toolbarpalette>

</overlay>


7) Create file C:\dev\open-page-in-author-browser-ext\firefox\content\eaem.js with necessary logic to open page in new tab

if ("undefined" == typeof(ExperienceAEM)) {
var ExperienceAEM = {};
};

ExperienceAEM.AccessAndEditing = {
init : function () {
var navbar = document.getElementById("nav-bar");
var newset = navbar.currentSet + ',eaem-navbar-button';
navbar.currentSet = newset;
navbar.setAttribute("currentset", newset );
document.persist("nav-bar", "currentset");
},

open: function(aEvent) {
var bundle = document.getElementById("eaem-string-bundle");

if(!bundle){
window.alert("Missing 'eaem.properties'");
return;
}

var hostPrefix = bundle.getString("author.prefix");

if(!hostPrefix){
window.alert("Missing author prefix 'author.prefix' in 'eaem.properties'");
return;
}

var loc = window.content.location;
var path = "";

if(loc.pathname.indexOf("/cf") == 0){
path = loc.pathname + loc.hash + loc.search;
}else{
path = "/cf#" + loc.pathname + loc.search;
}

window.BrowserOpenTab();
window.content.location.href = hostPrefix + path;
}
};

window.addEventListener("load", function(){ ExperienceAEM.AccessAndEditing.init(); }, false);

8) To build the extension, use standard jar command, at C:\dev\open-page-in-author-browser-ext

                     jar cvf eaem_access_edit.xpi -C firefox .

9) Download the extension as zip

AEM CQ 6 - Debugging AEM JSPs with IntelliJ IDEA 12

$
0
0

Goal


This post is on Debugging AEM Jsps ( or custom component jsps ) for better understanding of request flow and rendering, eg. textfield component used in cq:dialog

Product Jsps are available in CRX; so we'll start with a new project in Intellij, sync code from CRX and set up debugging... At the end of this article you should be able to debug the textfield component jsp...

Check AEM Documentation for more information on setting up AEM/CQ projects in Intellij IDEA




Sync code from CRX


1) Open IntelliJ IDEA, click File -> New Project... select Java Module and give project name (aem) and location (C:\dev\code\projects\aem)




2) Create folders META-INF/vault under aem project folder, file META-INF/vault/filter.xml





3) Add the filter for checking out /libs/granite/ui from CRX (using vault). The filter.xml

<?xml version="1.0" encoding="UTF-8"?>
<workspaceFilter version="1.0">
<filter root="/libs/granite/ui"/>
</workspaceFilter>

4) To add vault commands in IntelliJ check this post; vault is not really necessary here, if you are not interested in setting up vault, go to CRXDE Lite (http://localhost:4502/crx/de) create a package with filter /libs/granite/ui, download and unzip to the folder created above (C:\dev\code\projects\aem). In the following pic, code was checked out using vault commands







Configure Source Code


5) The code was checked-out; open module settings and add jcr_root to sources






6) Open any checked out file (eg. C:\dev\code\projects\aem\jcr_root\libs\granite\ui\components\foundation\form\multifield\render.jsp) and you can see red markings all over, as no dependent libraries were added...






7) At this point open Dependencies tab of module, create a Global Library and go wild, if you have enough memory just add all maven dependency jars available in your local repo (or be sane, pick up the necessary jars and add them to your library)






8) Goto IntelliJ Settings -> Plugins -> Install JetBrains plugin and add JSR 45 Integration





Setup Debugger


9) Goto Intellij -> Run -> Edit Configurations -> + (Add New Configuration) -> JSR 45 Compatible Server -> Remote






10) Give the debugger a name CQ, remove any Before Launch steps (as we are not really building anything)






11) Add Application server Generic, any start page say Geometrixx English and leave everything default






12) Click on tab Startup/Connection, Debug. Change the port number or leave it default (in the below pic it was changed to 5005)






13) Open the module and add a Web facet






14) Remove the default Web Resource Directory to configure jcr_root






15) Add the project module sub folder jcr_root (C:\dev\code\projects\aem\jcr_root) as Web Resource Directory









16) Copy the debug arguments from Step 12 and add it in your CQ start script

       java -Xdebug -Xrunjdwp:transport=dt_socket,address=5005,suspend=n,server=y -XX:MaxPermSize=512m -Xmx1024M -jar cq-quickstart-6.0.0.jar -nofork






17) Restart CQ


18) Return to IntelliJ IDEA when CQ is up, open component jsp to debug (eg. C:\dev\code\projects\aem\jcr_root\libs\granite\ui\components\foundation\form\textfield\render.jsp) set a debug point, select debugger CQ (created in step 12 above) and click on bug icon to connect the debugger. Open any dialog with a textfield component say Page Properties dialog of Geometrixx english http://localhost:4502/editor.html/content/geometrixx/en.html and the code execution should stop at debug point in textfield component jsp









AEM 6 SP2 - Touch UI Multi Field Component

$
0
0

Goal


Create a component with Touch UI (Coral UI) Dialog and Multifield (granite/ui/components/foundation/form/multifield) widget. Here we extend product multifield and create a composite multifield  /apps/touch-ui-multi-field-panel/multifield , configure it with two form fields, a Textfield (granite/ui/components/foundation/form/textfield), Pathbrowser (granite/ui/components/foundation/form/pathbrowser). End result is a simple Dashboard Component

A Classic UI Multifield configuration is available here

Demo | Package Install


Touch UI Dialog





Dialog Structure in CRX





Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create a folder (nt:folder) /apps/touch-ui-multi-field-panel

2) Create a component (cq:Component) /apps/touch-ui-multi-field-panel/sample-multi-field. Check this post on how to create a CQ component

3) Add the following xml for /apps/touch-ui-multi-field-panel/sample-multi-field/dialog created. Ideally a cq:Dialog should be configured with necessary widgets for displaying dialog in Classic UI. This post is on Touch UI dialogs so keep it simple (node required for opening the touch ui dialog created in next step)

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="cq:Dialog"
title="Multi Field"
xtype="dialog"/>


4) Create a nt:unstructured node /apps/touch-ui-multi-field-panel/sample-multi-field/cq:dialog with following xml (the dialog UI as xml)

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="nt:unstructured"
jcr:title="Multifield TouchUI Component"
sling:resourceType="cq/gui/components/authoring/dialog"
helpPath="en/cq/current/wcm/default_components.html#Text">
<content
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container">
<layout
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/>
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container">
<items jcr:primaryType="nt:unstructured">
<fieldset
jcr:primaryType="nt:unstructured"
jcr:title="Sample Dashboard"
sling:resourceType="granite/ui/components/foundation/form/fieldset">
<layout
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/>
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container">
<items jcr:primaryType="nt:unstructured">
<dashboard
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/textfield"
fieldDescription="Enter Dashboard name"
fieldLabel="Dashboard name"
name="./dashboard"/>
<pages
jcr:primaryType="nt:unstructured"
sling:resourceType="/apps/touch-ui-multi-field-panel/multifield"
class="full-width"
fieldDescription="Click '+' to add a new page"
fieldLabel="URLs">
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/fieldset"
name="./items">
<layout
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"
method="absolute"/>
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container">
<items jcr:primaryType="nt:unstructured">
<page
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/textfield"
fieldDescription="Enter Page Name"
fieldLabel="Page Name"
name="./page"/>
<path
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/pathbrowser"
fieldDescription="Select Path"
fieldLabel="Path"
name="./path"
rootPath="/content"/>
</items>
</column>
</items>
</field>
</pages>
</items>
</column>
</items>
</fieldset>
</items>
</column>
</items>
</content>
</jcr:root>


5) The composite multifield node /apps/touch-ui-multi-field-panel/sample-multi-field/cq:dialog/content/items/column/items/fieldset/items/column/items/pages is of extended multifield type /apps/touch-ui-multi-field-panel/multifield (created in next steps)

6) Different layouts can be used to structure the multifield, here we use granite/ui/components/foundation/layouts/fixedcolumns layout

7) Create folder (sling:Folder) /apps/touch-ui-multi-field-panel/multifield

8) Create file (nt:file) /apps/touch-ui-multi-field-panel/multifield/multifield.jsp (for rendering the widget) and add following code. Inline JS was added for simplicity, its always recommended to create a clientlib (cq:ClientLibraryFolder) for javascript code

<%@ page import="com.adobe.granite.ui.components.Config" %>
<%@ page import="org.slf4j.Logger" %>
<%@ page import="org.slf4j.LoggerFactory" %>
<%@ page import="com.adobe.granite.ui.components.Value" %>
<%@ page import="org.apache.commons.lang3.StringUtils" %>
<%@include file="/libs/granite/ui/global.jsp" %>

<%--include ootb multifield--%>
<sling:include resourceType="/libs/granite/ui/components/foundation/form/multifield"/>

<%!
private final Logger mLog = LoggerFactory.getLogger(this.getClass());
%>

<%
Config mCfg = cmp.getConfig();

Resource mField = mCfg.getChild("field");

if (mField == null) {
mLog.warn("Field node doesn't exist");
return;
}

ValueMap mVM = mField.adaptTo(ValueMap.class);

String mName = mVM.get("name", "");

if ("".equals(mName)) {
mLog.warn("name property doesn't exist on field node");
return;
}

Value mValue = ((ComponentHelper) cmp).getValue();

//get the values added in multifield
String[] mItems = mValue.get(mName, String[].class);
%>

<script>
(function () {
//function to add values into multifield widgets. The values are stored in CRX by collectDataFromFields() as json
//eg. {"page":"English","path":"/content/geometrixx/en"}
var addDataInFields = function () {
var mValues = [ <%= StringUtils.join(mValue.get(mName, String[].class), ",") %> ],
mName = '<%=mName%>',
$fieldSets = $("[class='coral-Form-fieldset'][data-name='" + mName + "']");

var record, $fields, $field, name;

$fieldSets.each(function (i, fieldSet) {
$fields = $(fieldSet).find("[name]");

record = mValues[i];

if (!record) {
return;
}

$fields.each(function (j, field) {
$field = $(field);

name = $field.attr("name");

if (!name) {
return;
}

//strip ./
if (name.indexOf("./") == 0) {
name = name.substring(2);
}

$field.val(record[name]);
});
});
};

//collect data from widgets in multifield and POST them to CRX as JSON
var collectDataFromFields = function(){
$(document).on("click", ".cq-dialog-submit", function () {
var $form = $(this).closest("form.foundation-form"), mName = '<%=mName%>';

//get all the input fields of multifield
var $fieldSets = $("[class='coral-Form-fieldset'][data-name='" + mName + "']");

var record, $fields, $field, name;

$fieldSets.each(function (i, fieldSet) {
$fields = $(fieldSet).find("[name]");

record = {};

$fields.each(function (j, field) {
$field = $(field);

name = $field.attr("name");

if (!name) {
return;
}

//strip ./
if (name.indexOf("./") == 0) {
name = name.substring(2);
}

record[name] = $field.val();

//remove the field, so that individual values are not POSTed
$field.remove();
});

if ($.isEmptyObject(record)) {
return;
}

//add the record JSON in a hidden field as string
$('<input />').attr('type', 'hidden')
.attr('name', mName)
.attr('value', JSON.stringify(record))
.appendTo($form);
});
});
};

$(document).ready(function () {
addDataInFields();
collectDataFromFields();
});
})();
</script>


9) sling:include at #9 includes the product multifield  /libs/granite/ui/components/foundation/form/multifield after which the code necessary for creating a composite multifield is added

10) The function collectDataFromFields() at #80 registers a click listener on dialog submit, to collect the multifield form data, create a json to group it and add in a hidden field before data is POSTed to CRX. The function addDataInFields() at #44 reads json data added previously and fills the multifield fields, when dialog is reopened. The extension uses simple jquery calls to get and set data; based on the widget type more code may be necessary for supporting complex Granite UI widgets (granite/ui/components/foundation/form)

11) Create the component jsp /apps/touch-ui-multi-field-panel/sample-multi-field/sample-multi-field.jsp for rendering data entered in dialog

<%@ page import="org.apache.sling.commons.json.JSONObject" %>
<%@ page import="java.io.PrintWriter" %>
<%@include file="/libs/foundation/global.jsp" %>
<%@page session="false" %>

<div style="display: block; border-style: solid; border-width: 1px; margin: 10px; padding: 10px">
<b>Multi Field Sample Dashboard</b>

<%
try {
Property property = null;

if (currentNode.hasProperty("items")) {
property = currentNode.getProperty("items");
}

if (property != null) {
JSONObject obj = null;
Value[] values = null;

if (property.isMultiple()) {
values = property.getValues();
} else {
values = new Value[1];
values[0] = property.getValue();
}

for (Value val : values) {
obj = new JSONObject(val.getString());
%>
Page : <b><%= obj.get("page") %></b>,
URL : <b><a href="<%= obj.get("path") %>.html" target="_blank"><%= obj.get("path") %></a></b>

<%
}
} else {
%>
Add values in dialog
<%
}
} catch (Exception e) {
e.printStackTrace(new PrintWriter(out));
}
%>

</div>



AEM 6 SP2 - Add Column to List View of Touch UI (Coral UI)

$
0
0

Goal


Add a column Path, to the Sites List View (the other two views are Card and Column) of Touch UI. Check this post for adding a column to the SiteAdmin grid of Classic UI

Demo | Package Install




Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/touchui-add-column-to-sites-list

2) Create clientlib (type cq:ClientLibraryFolder/apps/touchui-add-column-to-sites-list/clientlib and set a property categories of String type to granite.ui.foundation.admin

3) Create file ( type nt:file ) /apps/touchui-add-column-to-sites-list/clientlib/css.txt, add the following

                         add-path-column.css

4) Create file ( type nt:file ) /apps/touchui-add-column-to-sites-list/clientlib/add-path-column.css, add the following code (style for Path column)

.list .card-page .eaem-path-column{
position:relative;
float:left;
width: 32%;
}

5) Create file ( type nt:file ) /apps/touchui-add-column-to-sites-list/clientlib/js.txt, add the following

                         add-path-column.js

6) Create file ( type nt:file ) /apps/touchui-add-column-to-sites-list/clientlib/add-path-column.js, add the following code

(function (document, $) {
"use strict";

var FOUNDATION_LAYOUT_LIST = ".foundation-layout-list";
var SITE_ADMIN_CHILD_PAGES = "cq-siteadmin-admin-childpages";
var TITLE_SELECTOR = "[data-title='Title']";
var ARTICLE_TITLE_SELECTOR = ".label .main";

$(document).on("foundation-mode-change", function(e, mode, group){
//not on sites list, may be assets, return
if((group != SITE_ADMIN_CHILD_PAGES) || (mode == "selection") ){
return;
}

//group is cq-siteadmin-admin-childpages for sites
var $collection = $(".foundation-collection[data-foundation-mode-group=" + group + "]");

if (!$collection.is(FOUNDATION_LAYOUT_LIST)) {
return;
}

//adjust the width of title column to accommodate Path column
$(".list .card-page .main").css("width", "33%");

var $hTitle = $("." + SITE_ADMIN_CHILD_PAGES).find("header").find(TITLE_SELECTOR);

//add Path column to header
$hTitle.after( $("<div/>").attr('class', 'eaem-path-column')
.attr('data-sort-selector', ".label .eaem-path-column h4")
.attr('data-title', "Path").html("Path"));

var $pathElement;

$("article").each(function(index, article){
var $article = $(article);

$pathElement = $("<div/>").attr('class', 'eaem-path-column')
.append($("<h4/>").attr("class", "foundation-collection-item-title")
.html($article.data("path")));

//insert path after title
$article.find(ARTICLE_TITLE_SELECTOR).after($pathElement);
})
});
})(document, jQuery);



AEM 6 - Filter Specific Nodes and Properties from Activation (Replication)

$
0
0

Goal


Filter any renditions other than thumbnail.48.48.png from Activation. In addition to filtering renditions, restrict activation of properties with specific namespace eaem. A sample Replication Content Filter implementation for restrictive activation

Thank you Ravi Kiran for the suggestion

Demo | Package Install | Source Code


Image Node in Author CRX





Image Node in Publish CRX

                            Nodes renditions/cq5dam.thumbnail.319.319.png, renditions/cq5dam.thumbnail.140.100.png, renditions/original NOT activated
                            Property eaem:department NOT activated






Solution


1) Create a sample namespace eaem in Author CRX using the namespace editor (http://localhost:4502/crx/explorer/ui/namespace_editor.jsp). Check this post for more information on creating namespaces



2) Code and install a OSGI servlet apps.experienceaem.repfilter.NodePropertyReplicationFilterFactory (check this on post creating OSGI services)

package apps.experienceaem.repfilter;

import com.day.cq.replication.ReplicationAction;
import com.day.cq.replication.ReplicationActionType;
import com.day.cq.replication.ReplicationContentFilter;
import com.day.cq.replication.ReplicationContentFilterFactory;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
import java.util.List;

@Component
@Service
public class NodePropertyReplicationFilterFactory implements ReplicationContentFilterFactory {

private static final Logger log = LoggerFactory.getLogger(NodePropertyReplicationFilterFactory.class);

private static final ReplicationContentFilter FILTER = new NodePropertyReplicationContentFilter();

/**
* Filter executes on content activation
*
* @param action The {@link ReplicationAction} to consider.
*
* @return
*/
public ReplicationContentFilter createFilter(final ReplicationAction action) {
return action.getType() == ReplicationActionType.ACTIVATE ? FILTER : null;
}

/**
* Filters out some renditions and properties from activation
*/
private static class NodePropertyReplicationContentFilter implements ReplicationContentFilter {
private static String ALLOWED_RENDITION = "thumbnail.48.48.png";
private static String RESTRICTED_PREFIX = "eaem:";

/**
* Filter any renditions other than thumbnail.48.48.png while Activation
*
* @param node The {@link Node} to check.
* @return
*/
public boolean accepts(final Node node) {
try {
String parentName = node.getParent().getName();

if (!parentName.equals("renditions")) {
return true;
}

String nodeName = node.getName();

return nodeName.endsWith(ALLOWED_RENDITION);
} catch (RepositoryException e) {
log.error("Error with filtering ", e);
}

return true;
}

/**
* Any property with prefix eaem: is not activated
*
* @param property The {@link Property} to check.
* @return
*/
public boolean accepts(final Property property) {
try {
String name = property.getName();

return !name.startsWith(RESTRICTED_PREFIX);
} catch (RepositoryException e) {
log.error("Error with filtering ", e);
}

return true;
}

public boolean allowsDescent(final Node node) {
return true;
}

public List<String> getFilteredPaths() {
return null;
}
}
}

AEM 6 SP2 - Add Open Page in Touch UI Button to Classic UI Sidekick

$
0
0

Goal


Add a button to the bottom toolbar of Classic UI Sidekick to Open page in Touch UI. Check this useful chrome browser extension for switching between Touch and Classic UI

Demo | Package Install






Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/sidekick-open-page-in-touch-ui

2) Create node /apps/sidekick-open-page-in-touch-ui/clientlib of type cq:ClientLibraryFolder and add a String property categories with value cq.widgets

3) For button icon, create file (nt:file) /apps/sidekick-open-page-in-touch-ui/clientlib/css.txt and add

                       open-touch-ui.css

4) Add the following code in /apps/sidekick-open-page-in-touch-ui/clientlib/open-touch-ui.css

#CQ .cq-sidekick .x-window-bbar .cq-sidekick-open-touch-ui {
background-image:url(touch-ui.png);
}

5) Get the touch-ui.png from package and add it to /apps/sidekick-open-page-in-touch-ui/clientlib

6) Create file (nt:file) /apps/sidekick-open-page-in-touch-ui/clientlib/js.txt and add

                       open-touch-ui.js

7) Create file (nt:file) /apps/sidekick-open-page-in-touch-ui/clientlib/open-touch-ui.js and add the following code

CQ.Ext.ns("ExperienceAEM");

ExperienceAEM.Sidekick = {
OPEN_TOUCH_UI: "experience-aem-sk-button-open-touch-ui",

//add the button to sidekick bottom bar
addTouchUIButton: function(sk){
var bbar = sk.getBottomToolbar();
var oButton = bbar.getComponent(0);

//if the sidekick is reloaded, remove existing and add a fresh one
if(oButton.getId() == this.OPEN_TOUCH_UI){
bbar.remove(oButton, true);
}

oButton = new CQ.Ext.Button({
id: this.OPEN_TOUCH_UI,
iconCls: "cq-sidekick-open-touch-ui",
tooltip: {
title: "Touch UI",
text: "Open page in Touch UI"
},
handler: function() {
var win = CQ.WCM.isContentWindow(window) ? window.parent : window;
win.open("/editor.html" + sk.getPath() + ".html","_blank");
},
scope: sk
});

//add the button as first component in bottom toolbar
bbar.insert(0, oButton );
}
};

(function(){
var E = ExperienceAEM.Sidekick;

if( ( window.location.pathname == "/cf" ) || ( window.location.pathname.indexOf("/content") == 0)){
//when the sidekick is ready CQ fires sidekickready event
CQ.WCM.on("sidekickready", function(sk){
//after the sidekick content is loaded, add button
sk.on("loadcontent", function(){
E.addTouchUIButton(sk);
});
});
}
})();




AEM 6 SP2 - Touch UI Dialog Before Submit Confirmation

$
0
0

Goal


Show Confirmation when user submits a component dialog. Here we ask user to check the Title (./jcr:title) before submit

Demo | Package Install






Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/touch-ui-sample-form-before-submit

2) Create clientlib (type cq:ClientLibraryFolder/apps/touch-ui-sample-form-before-submit/clientlib and set a property categories of String type to cq.authoring.dialog

3) Create file ( type nt:file ) /apps/touch-ui-sample-form-before-submit/clientlib/js.txt, add the following

                         dialog-before-submit.js

6) Create file ( type nt:file ) /apps/touch-ui-sample-form-before-submit/clientlib/dialog-before-submit.js, add the following code

(function (document, $, ns) {
"use strict";

$(document).on("click", ".cq-dialog-submit", function (e) {
e.stopPropagation();
e.preventDefault();

var $form = $(this).closest("form.foundation-form"),
title = $form.find("[name='./jcr:title']").val(),
message, clazz = "coral-Button ";

if(!title){
message = "Title is empty. Are you sure?";
clazz = clazz + "coral-Button--warning";
}else{
message = "Title is '" + title + "'. Submit?";
clazz = clazz + "coral-Button--primary";
}

ns.ui.helpers.prompt({
title: Granite.I18n.get("Confirm"),
message: message,
actions: [{
id: "CANCEL",
text: "CANCEL",
className: "coral-Button"
},{
id: "SUBMIT",
text: "SUBMIT",
className: clazz
}
],
callback: function (actionId) {
if (actionId === "SUBMIT") {
$form.submit();
}
}
});
});
})(document, Granite.$, Granite.author);

AEM 6 SP2 - Touch UI Sample Dialog Ready (Open) Event Listener

$
0
0

Goal


A simple dialog-ready event listener fired when a Touch UI dialog is opened (listener top aligns field labels)

Package Install


Product






Extension





Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/touch-ui-sample-dialog-ready

2) Create clientlib (type cq:ClientLibraryFolder/apps/touch-ui-sample-dialog-ready/clientlib and set a property categories of String type to cq.authoring.dialog

3) Create file ( type nt:file ) /apps/touch-ui-sample-dialog-ready/clientlib/js.txt, add the following

                         labels.js

4) Create file ( type nt:file ) /apps/touch-ui-sample-dialog-ready/clientlib/labels.js, add the following code

(function ($, $document) {
"use strict";

$document.on("dialog-ready", function() {
$(".coral-Form-fieldlabel").css("float", "none");
});
})($, $(document));

AEM 6 SP2 - Classic UI Tags Widget Show Sub Folders as Namespaces

$
0
0

Goal


Extend Tags Widget (CQ.tagging.TagInputField, xtype: tags) and register as xtype eaem-tags (ExperienceAEM.TagInputField) to show different tag subfolders as namespaces. The tagsBasePath config option of CQ.tagging.TagInputField allows user to configure a single tag subfolder path as base path (children of base path are shown as namespace tabs in widget). This extension allows a user to pick different subfolder paths as namespaces (widget tabs)

Demo modifies foundation page dialog for demonstration purposes; For better code quality never alter /libs/foundation/components. Code here is just a quick sample, make sure its thoroughly tested before adding in projects....

Demo | Package Install


Product




Extension

                  Widget in Page Properties




                    Configuration in CRX



Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/classic-ui-tags-base-path-folders

2) Create node /apps/classic-ui-tags-base-path-folders/clientlib of type cq:ClientLibraryFolder and add a String property categories with value cq.tagging

3) Create file (nt:file) /apps/classic-ui-tags-base-path-folders/clientlib/js.txt and add

                       tags.js

4) Add the following code in /apps/classic-ui-tags-base-path-folders/clientlib/tags.js

CQ.Ext.ns("ExperienceAEM");

//extend CQ.tagging.TagInputField and register as eaem-tags
ExperienceAEM.TagInputField = CQ.Ext.extend(CQ.tagging.TagInputField, {
//eaemTagsBasePaths: [ "/etc/tags/geometrixx-media", "/etc/tags/marketing/interest/business" ],
eaemTagsBasePaths: null, // any namespace or subfolders of namespace passed as array

//Iterates base paths and adds each path as namespace
loadTagNamespaces: function() {
this.tagNamespaces = {};

if(!this.eaemTagsBasePaths || $.isEmptyObject(this.eaemTagsBasePaths)){
ExperienceAEM.TagInputField.superclass.loadTagNamespaces.call(this);
return;
}

CQ.Ext.each(this.eaemTagsBasePaths, function(tUrl) {
var pUrl = tUrl.substring(0, tUrl.lastIndexOf("/"));

//load each base path
var tagJson = this.loadJson(pUrl + CQ.tagging.TAG_LIST_JSON_SUFFIX + "?count=false");

if (tagJson && tagJson.tags) {
CQ.Ext.each(tagJson.tags, function(t) {
if(t.path === tUrl){
this.tagNamespaces[t.name] = t;
}
}, this);
}
}, this);

this.setupPopupMenu();

this.tagNamespacesLoaded = true;
},

setupPopupMenu: function() {
ExperienceAEM.TagInputField.superclass.setupPopupMenu.call(this);

if(!this.eaemTagsBasePaths || $.isEmptyObject(this.eaemTagsBasePaths)){
return;
}

var panel, treePanel, path, nsName;

//adjust the tree panel roots to load eaemTagsBasePaths data
CQ.Ext.each(this.namespacesTabPanel, function(tabPanel) {
for(var i = 0; i < tabPanel.items.length; i++){
panel = tabPanel.items.get(i);
treePanel = panel.items.get(0);

nsName = treePanel.root.attributes.name;
nsName = nsName.substring(nsName.lastIndexOf("/") + 1);

path = this.tagNamespaces[nsName].path;

treePanel.getLoader().path = path.substring(0, path.lastIndexOf("/"));
treePanel.root.attributes.name = path.substring(1);
}
}, this);
}
});

CQ.Ext.reg("eaem-tags", ExperienceAEM.TagInputField);

5) Make sure a String[] value is added for config option eaemTagsBasePaths. Each path is shown as panel in widget

AEM 6 SP2 - Touch UI ( Coral UI ) Nested Mutlifield ( Multi Multifield )

$
0
0

Goal


Create a Touch UI Nested multifield (or Multi multifield). The nested one is configured with two form fields, a Textfield (granite/ui/components/foundation/form/textfield), Pathbrowser  (granite/ui/components/foundation/form/pathbrowser )

Remember, it's always recommended to minimize the  number of UI extensions in projects, given a choice, a better (or simple) design of the dialog i guess would be without a nested multifield

A Composite Touch UI multifield is available here

Demo | Package Install


Nested Multifield





Stored as JSON in CRX





Component Rendering





Dialog in CRX






Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create a folder (nt:folder) /apps/touch-ui-nested-multi-field-panel

2) Create a component (cq:Component) /apps/touch-ui-nested-multi-field-panel/sample-nested-multi-fieldCheck this post on how to create a CQ component

3) Add the following xml for /apps/touch-ui-nested-multi-field-panel/sample-nested-multi-field/dialog created. Ideally a cq:Dialog should be configured with necessary widgets for displaying dialog in Classic UI. This post is on Touch UI so keep it simple (node required for opening the touch ui dialog created in next step)

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="cq:Dialog"
title="Multi Field"
xtype="dialog"/>

4) Create a nt:unstructured node /apps/touch-ui-nested-multi-field-panel/sample-nested-multi-field/cq:dialog with following xml (the dialog UI as xml)

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="nt:unstructured"
jcr:title="Multifield TouchUI Component"
sling:resourceType="cq/gui/components/authoring/dialog"
helpPath="en/cq/current/wcm/default_components.html#Text">
<content
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container">
<layout
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/>
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container">
<items jcr:primaryType="nt:unstructured">
<fieldset
jcr:primaryType="nt:unstructured"
jcr:title="Sample Dashboard"
sling:resourceType="granite/ui/components/foundation/form/fieldset">
<layout
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/>
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container">
<items jcr:primaryType="nt:unstructured">
<dashboard
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/textfield"
fieldDescription="Enter Dashboard name"
fieldLabel="Dashboard name"
name="./dashboard"/>
<countries
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/multifield"
class="full-width"
fieldDescription="Click '+' to add a new page"
fieldLabel="Countries">
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/fieldset"
eaem-nested=""
name="./countries">
<layout
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"
method="absolute"/>
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container">
<items jcr:primaryType="nt:unstructured">
<country
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/textfield"
fieldDescription="Name of Country"
fieldLabel="Country Name"
name="./country"/>
<states
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/multifield"
class="full-width"
fieldDescription="Click '+' to add a new page"
fieldLabel="States">
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/fieldset"
name="./states">
<layout
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"
method="absolute"/>
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container">
<items jcr:primaryType="nt:unstructured">
<state
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/textfield"
fieldDescription="Name of State"
fieldLabel="State Name"
name="./state"/>
<path
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/pathbrowser"
fieldDescription="Select Path"
fieldLabel="Path"
name="./path"
rootPath="/content"/>
</items>
</column>
</items>
</field>
</states>
</items>
</column>
</items>
</field>
</countries>
</items>
</column>
</items>
</fieldset>
</items>
</column>
</items>
</content>
</jcr:root>

5) Line 45 in above dialog xml marks this multifield as nested by setting a no value flag eaem-nested

6) Different layouts can be used to structure the multifield, here we use granite/ui/components/foundation/layouts/fixedcolumns layout

7) Create a clientlib (cq:ClientLibraryFolder) /apps/touch-ui-nested-multi-field-panel/clientlib with categories property as String with value cq.authoring.dialog and dependencies as String[] with value underscore

8) Create file (nt:file) /apps/touch-ui-nested-multi-field-panel/clientlib/js.txt with

                nested-multifield.js

9) Create file (nt:file) /apps/touch-ui-nested-multi-field-panel/clientlib/nested-multifield.js with following code

(function () {
var DATA_EAEM_NESTED = "data-eaem-nested";
var CFFW = ".coral-Form-fieldwrapper";

//reads multifield data from server, creates the nested composite multifields and fills them
var addDataInFields = function () {
$(document).on("dialog-ready", function() {
var mName = $("[" + DATA_EAEM_NESTED + "]").data("name");

if(!mName){
return;
}

//strip ./
mName = mName.substring(2);

var $fieldSets = $("[" + DATA_EAEM_NESTED + "][class='coral-Form-fieldset']"),
$form = $fieldSets.closest("form.foundation-form");

var actionUrl = $form.attr("action") + ".json";

var postProcess = function(data){
if(!data || !data[mName]){
return;
}

var mValues = data[mName], $field, name;

if(_.isString(mValues)){
mValues = [ JSON.parse(mValues) ];
}

_.each(mValues, function (record, i) {
if (!record) {
return;
}

if(_.isString(record)){
record = JSON.parse(record);
}

_.each(record, function(rValue, rKey){
$field = $($fieldSets[i]).find("[name='./" + rKey + "']");

if(_.isArray(rValue) && !_.isEmpty(rValue)){
fillNestedFields( $($fieldSets[i]).find("[data-init='multifield']"), rValue);
}else{
$field.val(rValue);
}
});
});
};

//creates & fills the nested multifield with data
var fillNestedFields = function($multifield, valueArr){
_.each(valueArr, function(record, index){
$multifield.find(".js-coral-Multifield-add").click();

//a setTimeout may be needed
_.each(record, function(value, key){
var $field = $($multifield.find("[name='./" + key + "']")[index]);
$field.val(value);
})
})
};

$.ajax(actionUrl).done(postProcess);
});
};

var fillValue = function($field, record){
var name = $field.attr("name");

if (!name) {
return;
}

//strip ./
if (name.indexOf("./") == 0) {
name = name.substring(2);
}

record[name] = $field.val();

//remove the field, so that individual values are not POSTed
$field.remove();
};

//for getting the nested multifield data as js objects
var getRecordFromMultiField = function($multifield){
var $fieldSets = $multifield.find("[class='coral-Form-fieldset']");

var records = [], record, $fields, name;

$fieldSets.each(function (i, fieldSet) {
$fields = $(fieldSet).find("[name]");

record = {};

$fields.each(function (j, field) {
fillValue($(field), record);
});

if(!$.isEmptyObject(record)){
records.push(record)
}
});

return records;
};

//collect data from widgets in multifield and POST them to CRX as JSON
var collectDataFromFields = function(){
$(document).on("click", ".cq-dialog-submit", function () {
var $form = $(this).closest("form.foundation-form");

var mName = $("[" + DATA_EAEM_NESTED + "]").data("name");
var $fieldSets = $("[" + DATA_EAEM_NESTED + "][class='coral-Form-fieldset']");

var record, $fields, $field, name, $nestedMultiField;

$fieldSets.each(function (i, fieldSet) {
$fields = $(fieldSet).children().children(CFFW);

record = {};

$fields.each(function (j, field) {
$field = $(field);

//may be a nested multifield
$nestedMultiField = $field.find("[data-init='multifield']");

if($nestedMultiField.length == 0){
fillValue($field.find("[name]"), record);
}else{
name = $nestedMultiField.find("[class='coral-Form-fieldset']").data("name");

if(!name){
return;
}

//strip ./
name = name.substring(2);

record[name] = getRecordFromMultiField($nestedMultiField);
}
});

if ($.isEmptyObject(record)) {
return;
}

//add the record JSON in a hidden field as string
$('<input />').attr('type', 'hidden')
.attr('name', mName)
.attr('value', JSON.stringify(record))
.appendTo($form);
});
});
};

$(document).ready(function () {
addDataInFields();
collectDataFromFields();
});

//extend otb multifield for adjusting event propagation when there are nested multifields
//for working around the nested multifield add and reorder
CUI.Multifield = new Class({
toString: "Multifield",
extend: CUI.Multifield,

construct: function (options) {
this.script = this.$element.find(".js-coral-Multifield-input-template:last");
},

_addListeners: function () {
this.superClass._addListeners.call(this);

//otb coral event handler is added on selector .js-coral-Multifield-add
//any nested multifield add click events are propagated to the parent multifield
//to prevent adding a new composite field in both nested multifield and parent multifield
//when user clicks on add of nested multifield, stop the event propagation to parent multifield
this.$element.on("click", ".js-coral-Multifield-add", function (e) {
e.stopPropagation();
});

this.$element.on("drop", function (e) {
e.stopPropagation();
});
}
});

CUI.Widget.registry.register("multifield", CUI.Multifield);
})();

10) The function collectDataFromFields() registers a click listener on dialog submit, to collect the multifield form data, create a json to group it and add in a hidden field before data is POSTed to CRX. The function addDataInFields() reads json data added previously and fills the multifield & nested multifield fields, when dialog is opened. Extension uses simple jquery & underscore js calls to get and set data; based on the widget type more code may be necessary for supporting complex Granite UI widgets (granite/ui/components/foundation/form)

11) Create the component jsp /apps/touch-ui-nested-multi-field-panel/sample-nested-multi-field/sample-nested-multi-field.jsp for rendering data entered in dialog

<%@ page import="org.apache.sling.commons.json.JSONObject" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="org.apache.sling.commons.json.JSONArray" %>
<%@include file="/libs/foundation/global.jsp" %>
<%@page session="false" %>

<div style="display: block; border-style: solid; border-width: 1px; margin: 10px; padding: 10px">
<b>Countries and States</b>

<%
try {
Property property = null;

if (currentNode.hasProperty("countries")) {
property = currentNode.getProperty("countries");
}

if (property != null) {
JSONObject country = null, state = null;
Value[] values = null;

if (property.isMultiple()) {
values = property.getValues();
} else {
values = new Value[1];
values[0] = property.getValue();
}

for (Value val : values) {
country = new JSONObject(val.getString());
%>
Country : <b><%= country.get("country") %></b>
<%
if (country.has("states")) {
JSONArray states = (JSONArray) country.get("states");

if (states != null) {
for (int index = 0, length = states.length(); index < length; index++) {
state = (JSONObject) states.get(index);
%>
<div style="padding-left: 25px">
<a href="<%= state.get("path") %>.html" target="_blank">
<%= state.get("state") %> - <%= state.get("path") %>
</a>
</div>
<%
}
}

}
}
} else {
%>
Add values in dialog
<%
}
} catch (Exception e) {
e.printStackTrace(new PrintWriter(out));
}
%>
</div>



AEM 6 SP2 - Disable Search Boxes in CRXDE Lite

$
0
0
This is an unconventional way of extending CRXDE Lite; for desperate situations only

Goal


Disable search fields in CRXDE Lite. This extension does not disable Tools -> Query. Users may want to disable repository wide search in lite to stop random performance degrading searches on CRX

Demo | Package install



Solution


Follow the two steps below to extend CRXDE Lite and add necessary JS to disable search. First step is Not Upgrade-Proof, so when CQ is upgraded, the first step may have to be repeated

Step 1 - Update CRXDE Lite Jar

All we do in this step is copy (back it up just in case if something goes wrong) the serialized CRXDE lite jar, open it and add a small chunk of JS code so that any extensions we code are loaded by the added JS logic when lite is opened in browser.

1) Access bundles console http://localhost:4502/system/console/bundles and find the CRXDE Support bundle





2) Search for the serialized bundle on filesystem and copy it to a temp location (take a backup before modifying). On my AEM 6 SP2 its available in author\crx-quickstart\launchpad\installer (rsrc-com.adobe.granite.crxde-lite-1.0.66-CQ600-B0001.jar-1415034571045.ser)

3) Rename the copied .ser file to .jar (eg. rsrc-com.adobe.granite.crxde-lite-1.0.66-CQ600-B0001.jar-1415034571045.ser -> rsrc-com.adobe.granite.crxde-lite-1.0.66-CQ600-B0001.jar)

4) Open the jar using zip executable (say winrar), open file docroot\js\start.js in any text editor and add following code at the end. Save file and a winrar confirmation should popup asking if the jar should be updated with saved file.

Ext.onReady(function() {
var loadLiteExtns = function(){
Ext.Ajax.request({
url: "/apps/ext-crxde-lite/files.txt",
success: function(response, options) {
var js = response.responseText;

if(!js){
return;
}

js = js.split("\n");

Ext.each(js, function(jsPath) {
Ext.Ajax.request({
url: jsPath,
success: function(response, options) {
eval(response.responseText);
}
});
});
}
});
};

loadLiteExtns();
});


5) In the above steps we add necessary code to load the extension files entered in /apps/ext-crxde-lite/files.txt. So whenever a new CRXDE Lite extension is needed a new line with extension file path can be added in /apps/ext-crxde-lite/files.txt

6) Access http://localhost:4502/system/console/bundles, click Install/Update... to upload and update CQ with the new CRXDE Support jar having necessary code to load the CRXDE Lite extension files.

Step 2 - Add extension files in CRX

In this step we add the JS file containing logic to disable search fields

1) Access http://localhost:4502/crx/de

2) Create node /apps/ext-crxde-lite of type nt:folder

3) Create node /apps/ext-crxde-lite/files.txt of type nt:file and add the following line. The logic added in Step 1 reads this file for loading JS extension files added as paths

                                 /apps/ext-crxde-lite/disable-search.js

4) Create node /apps/ext-crxde-lite/disable-search.js of type nt:file and add the following code

Ext.onReady(function(){
var INTERVAL = setInterval(function(){
var searchField = Ext.getCmp(CRX.ide.REPO_PATH_ID);

if(searchField){
clearInterval(INTERVAL);

searchField.setDisabled(true);
}
}, 250);

var SB_INTERVAL = setInterval(function(){
var homePanel = Ext.getCmp("editors");

if(homePanel){
clearInterval(SB_INTERVAL);

homePanel.findByType("panel")[0].setDisabled(true);
}
}, 250);
});





AEM 6 SP2 - Extend Site Admin Page Properties Show Submit Confirmation

$
0
0

Goal


Extend Site Admin console Page Properties, add submit event listener to show confirmation for page title property

Demo | Package Install

A similar listener on dialog (cq:dialog) save is here

http://localhost:4502/libs/wcm/core/content/sites/properties.html?item=/content/geometrixx/en/products




Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/touch-ui-extend-site-page-properties

2) Create clientlib (type cq:ClientLibraryFolder/apps/touch-ui-extend-site-page-properties/clientlib and set a property categories of String type to cq.siteadmin.admin.properties

3) Create file ( type nt:file ) /apps/touch-ui-extend-site-page-properties/clientlib/js.txt, add the following

                         missing-page-title.js

4) Create file ( type nt:file ) /apps/touch-ui-extend-site-page-properties/clientlib/missing-page-title.js, add the following code

(function($document, $) {
"use strict";

//form id defined in /libs/wcm/core/content/sites/properties/jcr:content/body/content/content
var PROPERTIES_FORM = "propertiesform";

$document.on("foundation-contentloaded", function(){
$(".foundation-content-current").on('click', "button[type='submit'][form='" + PROPERTIES_FORM + "']", function(e){
var $propertiesForm = $("#" + PROPERTIES_FORM);

if($propertiesForm.length == 0){
return;
}

e.preventDefault();
e.stopPropagation();

var title = $propertiesForm.find("[name='./pageTitle']").val(),
message, warning = false;

var fui = $(window).adaptTo("foundation-ui");

if(!title){
message = "Page title is empty. Are you sure?";
warning = true;
}else{
message = "Page title is '" + title + "'. Submit?";
}

fui.prompt("Confirm", message, "notice",
[{
id: "CANCEL",
text: "CANCEL",
className: "coral-Button"
},{
id: "SUBMIT",
text: "SUBMIT",
warning: warning,
primary: !warning
}
],function (actionId) {
if (actionId === "SUBMIT") {
$propertiesForm.submit();
}
}
);
});
});
})($(document), Granite.$);

AEM 6 SP2 - Servlet for overlaying libs path in apps

$
0
0

Goal


Sample servlet for creating overlay of a /libs path in /apps. Overlays can be used for overriding/extending otb implementation. It's no exaggeration almost all CQ projects may have used overlays at some point for extending product functionality. This servlet checks for type of each node under /libs (of given path) and creates similar path in /apps

Package Install

A sample request http://localhost:4502/bin/experience-aem/create/overlay?path=/libs/wcm/core/content/sites/jcr:content/body/content/content/items/childpages/layout creates /apps/wcm/core/content/sites/jcr:content/body/content/content/items/childpages/layout





Add copyChildren=true to the request URL to copy (not create) the last node from libs


Solution


Create an OSGI servlet with the following code

package apps.experienceaem;

import com.day.cq.commons.jcr.JcrUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.commons.json.JSONObject;

import javax.jcr.Node;
import javax.jcr.Session;
import javax.servlet.ServletException;
import java.io.IOException;

@Component(metatype = true, label = "Experience AEM Overlay Creator")
@Service
@Properties({
@Property(name = "sling.servlet.methods", value = {"GET"}, propertyPrivate = true),
@Property(name = "sling.servlet.paths", value = "/bin/experience-aem/create/overlay", propertyPrivate = true),
@Property(name = "sling.servlet.extensions", value = "json", propertyPrivate = true)})
public class OverlayPathCreatorServlet extends SlingAllMethodsServlet {
protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException,IOException {
String path = request.getParameter("path");
String copyChildren = request.getParameter("copyChildren");

String srcHierarchy = "/libs", desHierarchy = "/apps";

JSONObject json = new JSONObject();

try {
ResourceResolver resolver = request.getResourceResolver();

if (StringUtils.isEmpty(path) || !path.trim().startsWith("/libs")) {
json.put("error", "Path should start with /libs");
response.getWriter().print(json);
return;
}

Session session = resolver.adaptTo(Session.class);
Resource destResource = null, srcResource = null, parentDestResource = null;

Node srcNode = null; String token;
String tokens[] = path.substring("/libs".length()).split("/");

for(int index = 0; index < tokens.length; index++){
token = tokens[index];

if(StringUtils.isEmpty(token)){
continue;
}

desHierarchy = desHierarchy + "/" + token;
srcHierarchy = srcHierarchy + "/" + token;

destResource = resolver.getResource(desHierarchy);

if(destResource != null){
continue;
}

srcResource = resolver.getResource(srcHierarchy);

if(srcResource == null){
throw new ServletException("Error finding resource - " + srcHierarchy);
}

srcNode = srcResource.adaptTo(Node.class);

if(index == (tokens.length - 1) && "true".equalsIgnoreCase(copyChildren)){
JcrUtil.copy(srcNode, parentDestResource.adaptTo(Node.class), null);
}else{
JcrUtil.createPath(desHierarchy, srcNode.getPrimaryNodeType().getName(), session);
parentDestResource = resolver.getResource(desHierarchy);
}
}

session.save();

json.put("success", "Created " + desHierarchy);

response.getWriter().print(json);
} catch (Exception e) {
throw new ServletException("Error creating - " + desHierarchy, e);
}
}
}


AEM 6 SP2 - Touch UI Site Admin Console Default to List View

$
0
0

Goal


When a user accesses Sites Admin console first time (http://localhost:4502/sites.html) the results are shown as cards by default (user can then select choice of view, stored in cookie cq.sites.childpages.layoutId for any future requests). This post is on setting the default view to List

Demo | Package Install

               List View




Solution


1) Create overlay of /libs/wcm/core/content/sites/jcr:content/body/content/content/items/childpages/layout in /apps. Use overlay creator helper servlet for creating the structure in /apps

2) Add a property layout on /apps/wcm/core/content/sites/jcr:content/body/content/content/items/childpages/layout with the following value (when cookie cq.sites.childpages.layoutId is null, select view type as list)

${state["cq.sites.childpages.layoutId"].string == null ? "list" : state["cq.sites.childpages.layoutId"].string}

An other way to select default view is by reordering the view nodes under /libs/wcm/core/content/sites/jcr:content/body/content/content/items/childpages/layout/layouts in /apps overlay



AEM 6 SP2 - View (Tail) CQ Log Files

$
0
0

Goal


Quick post on creating a Tail Log servlet for examining the logs on remote CQ instances; if you are debugging issues on a friend's CQ instance or QA, deploying this servlet could be helpful. Ootb, logs can be viewed by logging into felix console (http://localhost:4502/system/console/status-slinglogs) or doing a remote login (ssh)

Demo | Package Install | Source Code

View error log : http://localhost:4502/bin/experience-aem/tail/log

View specific log (eg. access.log) : http://localhost:4502/bin/experience-aem/tail/log?log=access

Clear: Clears the current log view

Color Line Begin - First few characters of the line are colored (next fetch)

Start Line At - Cut out the first few characters of line, say timestamp (next fetch)

Line Min Length - Only the lines with length greater than entered number are returned (next fetch)

Line Max Length - Only the lines with length less than entered number are returned (next fetch)

Trim to Size - Cut the line to specified size (next fetch)

If Line Contains - Get line only if it contains the entered string (next fetch)

Not If Line Contains - Get line only if it does not contain entered string (next fetch)



Solution


Create an OSGI servlet apps.experienceaem.taillogs.TailLogsServlet with the following code

package apps.experienceaem.taillogs;

import org.apache.commons.lang3.StringUtils;
import org.apache.felix.scr.annotations.*;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.commons.json.JSONObject;
import org.apache.sling.settings.SlingSettingsService;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletException;
import java.io.*;
import java.util.Dictionary;
import org.apache.commons.lang3.StringEscapeUtils;

@Component(
metatype = true,
label = "Experience AEM Tail Logs",
description = "Experience AEM Tail Logs Servlet")
@Service
@Properties({
@Property(name = "sling.servlet.methods", value = {"GET", "POST"}, propertyPrivate = true),
@Property(name = "sling.servlet.paths", value = "/bin/experience-aem/tail/log", propertyPrivate = true)})
public class TailLogsServlet extends SlingAllMethodsServlet {
private final Logger LOG = LoggerFactory.getLogger(getClass());

@Reference
protected SlingSettingsService slingSettings;

@Property(name = "logs.path", label = "Logs folder location",
value = "",
description = "Absolute path of log files")
private static final String LOG_PATH = "log.path";

@Property(name = "bytes.to.read", label = "Initial Bytes to Read",
value = "2048",
description = "The initial bytes to read")
private static final String DEFAULT_BYTES_TO_READ = "bytes.to.read";

@Property(name = "refresh.interval.millis", label = "Refresh Interval",
value = "5000",
description = "Log textarea refresh interval in millis")
private static final String REFRESH_INTERVAL = "refresh.interval.millis";

private String logFolderPath = null;
private String bytesToRead = null;
private String refreshInterval = null;

@Activate
protected void activate(final ComponentContext context) {
Dictionary<String, Object> props = context.getProperties();
Object prop = props.get(LOG_PATH);

if(prop == null){
logFolderPath = slingSettings.getSlingHomePath() + File.separator + "logs" + File.separator;
}else{
logFolderPath = String.valueOf(prop);
}

bytesToRead = String.valueOf(props.get(DEFAULT_BYTES_TO_READ));
refreshInterval = String.valueOf(props.get(REFRESH_INTERVAL));

LOG.info("Logs path : " + logFolderPath + ", Initial bytes to read : " + bytesToRead + ", Refresh interval : " + refreshInterval);
}

private void addLastNBytes(SlingHttpServletRequest request, SlingHttpServletResponse response, String logName)
throws Exception{
String filePointer = request.getParameter("pointer");
String startLineAt = request.getParameter("startLineAt");
String lineMinLength = request.getParameter("lineMinLength");
String lineMaxLength = request.getParameter("lineMaxLength");
String lineContains = request.getParameter("lineContains");
String notLineContains = request.getParameter("notLineContains");
String colorLineBegin = request.getParameter("colorLineBegin");
String trimToSize = request.getParameter("trimToSize");

File file = new File(logFolderPath + logName);

long _filePointer = -1, len;
int btr = Integer.parseInt(this.bytesToRead);

if(StringUtils.isEmpty(filePointer)){
_filePointer = file.length() - btr;
}else{
_filePointer = Long.parseLong(filePointer);

len = file.length();

//roll over or log clean
if( len < _filePointer){
_filePointer = len - btr;
}
}

if(_filePointer < 0){
_filePointer = 0;
}

StringBuilder sb = new StringBuilder();

//based on //http://www.jibble.org/jlogtailer.php
RandomAccessFile raf = new RandomAccessFile(file, "r");

try{
raf.seek(_filePointer);

String line = null; int startAt = 0;

if(StringUtils.isNotEmpty(startLineAt)){
startAt = Integer.parseInt(startLineAt);
}

while ((line = raf.readLine()) != null) {
if(startAt > 0 ){
if(line.length() > startAt){
line = line.substring(startAt);
}else{
continue; //skip lines shorter than desired
}
}

if(StringUtils.isNotEmpty(lineMinLength) && line.length() < Integer.parseInt(lineMinLength)){
continue;
}

if(StringUtils.isNotEmpty(lineMaxLength) && line.length() > Integer.parseInt(lineMaxLength)){
continue;
}

if(StringUtils.isNotEmpty(lineContains) && !line.contains(lineContains)){
continue;
}

if(StringUtils.isNotEmpty(notLineContains) && line.contains(notLineContains)){
continue;
}

if(StringUtils.isNotEmpty(trimToSize)){
line = line.substring(0, Integer.parseInt(trimToSize));
}

if(StringUtils.isNotEmpty(colorLineBegin) && colorLineBegin.equals("true")){
int length = (line.length() < 10) ? 1 : 10;
line = "<span style='color:red;font-weight:bold'>" + StringEscapeUtils.escapeHtml4(line.substring(0,length))
+ "</span>" + StringEscapeUtils.escapeHtml4(line.substring(length));
}

sb.append(line);
}

_filePointer = raf.getFilePointer();

raf.close();
}catch(Exception e){
raf.close();
throw new ServletException("Error reading file - " + logName);
}

PrintWriter pw = response.getWriter();

if(StringUtils.isEmpty(filePointer)){
response.setContentType("text/html");

String interval = Integer.parseInt(this.refreshInterval)/1000 + " secs";

pw.write("<html><head><title>CQ Tail Log</title><script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.js'></script><style>input[type='text']{width:50px}</style></head><body><div style='border: 1px solid; padding: 5px; height: 780px; overflow: scroll;'><code contenteditable='true' id='logData'>" + sb.toString() + "</code></div>");
pw.write("<div>Log file : " + (logFolderPath + logName) + "</div>");
pw.write("<div>Refreshing : <span id=status style='color:red'>" + interval + "</span> ");
pw.write("| <input type=button value='pause' onclick=\"eaemTL.paused = !eaemTL.paused; this.value=eaemTL.paused ? 'resume' : 'pause'; $('#status').html(eaemTL.paused ? 'paused' : eaemTL.interval)\"/> ");
pw.write("| <input type=button value='clear' onclick=\"$('#logData').html('');\"/> ");
pw.write("| Color Line Begin : <input type=checkbox onchange='eaemTL.colorLineBegin=this.checked; updateTextArea()'/> ");
pw.write("| Font Size : <input type=text onchange=\"$('#logData').css('font-size', this.value)\"> Px ");
pw.write("| Start Line At : <input type=text onchange='eaemTL.startLineAt=this.value; updateTextArea()'> ");
pw.write("| Line Min Length : <input type=text onchange='eaemTL.lineMinLength=this.value; updateTextArea()'> ");
pw.write("| Line Max Length : <input type=text onchange='eaemTL.lineMaxLength=this.value; updateTextArea()'> ");
pw.write("| Trim to Size : <input type=text onchange='eaemTL.trimToSize=this.value; updateTextArea()'> </div>");
pw.write("<div>If Line Contains : <input type=text onchange='eaemTL.lineContains=this.value; updateTextArea()' style='width:600px'>     ");
pw.write(" |     Not If Line Contains : <input type=text onchange='eaemTL.notLineContains=this.value; updateTextArea()' style='width:600px'></div> ");
pw.write("<script type='text/javascript'>var eaemTL = { log: '" + logName + "', pointer : " + _filePointer + ", paused : false, interval : '" + interval + "' }; var $logData = $('#logData');");
pw.write("function updateTextArea() { if(eaemTL.paused){return;} $.ajax( { url: '/bin/experience-aem/tail/log', data: eaemTL } ).done(function(data){ if(data.log){$logData.html($logData.html() + data.log)}; eaemTL.pointer = data.pointer});} setInterval(updateTextArea, 5000);</script></body></html>");
}else{
response.setContentType("application/json");

JSONObject json = new JSONObject();
json.put("log", sb.toString());
json.put("pointer", _filePointer);

json.write(pw);
}
}

/**
* @param request
* @param response
* @throws javax.servlet.ServletException
* @throws java.io.IOException
*/
protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException, IOException {
try{
String logName = request.getParameter("log");

if(StringUtils.isEmpty(logName)){
logName = "error";
}

if(!logName.endsWith(".log")){
logName = logName + ".log";
}

addLastNBytes(request, response, logName);
}catch(Exception e){
LOG.warn("Error tailing logs servlet", e);
}
}
}

Viewing all 513 articles
Browse latest View live




Latest Images