Showing posts with label AEM. Show all posts
Showing posts with label AEM. Show all posts

Saturday, November 14, 2020

Custom Tab Component in AEM

Although the best way to implement tab component in AEM is via inheriting it from core tab component, but there could be certain scenarios where we need to use a custom Tab component. In such scenarios, we can easily create a tab component by using a multifield for addition of n number of tabs and by providing a responsivegrid/parsys for adding the component inside each tab. But, the problem arises when we need to rearrange tabs after authoring or deleting any tab, the challenge is to maintain the components authored for each tab. In this article, we will be discussing different ways to create the custom Tab component.


To begin with, lets create a custom tab component which have the below files/nodes inside it:


Next, we can start by taking any HTML for tabs component, for this article I have used a pretty basic HTML from W3School. We can separate the HTML, JS and CSS files based on standard practice and can place the JS and CSS to the concerned clientlib folders. Once, we are done with this we can try including this component in any page where it is allowed and we will be able to see the static markup getting loaded with tab functionalities.

Next, we will be creating a dialog for it and will try to make it authorable. Observe the below xml code for dialog:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/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="Autopace Tabs"
    sling:resourceType="cq/gui/components/authoring/dialog"
    extraClientlibs="[autopace.tabs]">
    <content
        jcr:primaryType="nt:unstructured"
        sling:resourceType="granite/ui/components/coral/foundation/container">
        <items jcr:primaryType="nt:unstructured">
            <tabs
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/tabs"
                maximized="{Boolean}true">
                <items jcr:primaryType="nt:unstructured">
                    <tabitems
                        jcr:primaryType="nt:unstructured"
                        jcr:title="Items"
                        sling:resourceType="granite/ui/components/coral/foundation/container"
                        margin="{Boolean}true">
                        <items jcr:primaryType="nt:unstructured">
                            <columns
                                jcr:primaryType="nt:unstructured"
                                sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns"
                                margin="{Boolean}true">
                                <items jcr:primaryType="nt:unstructured">
                                    <column
                                        jcr:primaryType="nt:unstructured"
                                        sling:resourceType="granite/ui/components/coral/foundation/container">
                                        <items jcr:primaryType="nt:unstructured">
	                                        <tabItems
	                                            jcr:primaryType="nt:unstructured"
	                                            sling:resourceType="granite/ui/components/coral/foundation/form/multifield"
	                                            composite="{Boolean}true"
	                                            fieldDescription="Click 'Add Field' to add new item."
	                                            fieldLabel="Tab Items">
	                                            <field
	                                                jcr:primaryType="nt:unstructured"
	                                                sling:resourceType="granite/ui/components/coral/foundation/container"
	                                                name="./tabItems">
	                                                <items jcr:primaryType="nt:unstructured">
	                                                    <title
	                                                    	granite:class="tab-title"
	                                                        jcr:primaryType="nt:unstructured"
	                                                        sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
	                                                        fieldLabel="Title"
	                                                        name="./title"/>
	                                                    <tabId
	                                                    	granite:class="tab-id"
	                                                        jcr:primaryType="nt:unstructured"
	                                                        sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
	                                                        name="./tabId"/>
	                                                </items>
	                                            </field>
	                                        </tabItems>
                                        </items>
                                    </column>
                                </items>
                            </columns>
                        </items>
                    </tabitems>
                </items>
            </tabs>
        </items>
    </content>
</jcr:root>


In the above XML code, if we observe closely, we will see that in the multifield , we have two fields, one is for title and the other is for tabId. The trick here is if we provide an option to author tabId for each tab added to the component, we can use that tabId to name the parsys/responsivegrid present under that particular tab. For example: if we have added "Tab1 Title" as title with id name as "tab1-id", then we can use "tab1-id" to create the parsys/responsivegrid node for that particular tab and all the components added inside that Tab parsys/responsivegrid will be added under a node with name "tab1-id". In that way, even if we change the positions of tabs later, we will be able to render the current components added for the tab as those will be present under node "tab1-id".

Now lets have a look at the HTML code for the same and try to understand the ID login:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<sly data-sly-use.tabs="org.autopace.autopace.core.models.AutopaceTabs" />
<sly data-sly-test="${wcmmode.edit}" data-emptytext="Autopace Tabs" class="cq-placeholder" />

<h2>Tabs</h2>
<div class="tab">
	<sly data-sly-list.item="${tabs.tabItems}">
	<button class="tablinks" onclick="openCity(event, '${item.title @ context='html'}')">${item.title}</button>
	</sly>
</div>

<sly data-sly-list.item="${tabs.tabItems}">
<div id="${item.title}" class="tabcontent">
	<sly data-sly-resource="${item.tabId @ resourceType='wcm/foundation/components/responsivegrid'}"/>
</div>
</sly>


We can observe that from line #11 to line #15, we are running a loop to create the responsive grid for each tab added to the component. In line #13, we can observe that "item.tabId" is the name for each tab's responsive grid. we just need to make sure that the tabId authored is unique and we can manage the components added inside. Also, if the author uses the same Id for more than one tab, then the components displayed for all those tabs will be same. Also, In line #1, we can see that we have used the sling model, below is the sling model code snippet for reference:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package org.autopace.autopace.core.models;

import java.util.List;

import javax.inject.Inject;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.Via;

import com.adobe.cq.export.json.ExporterConstants;

@Model(adaptables = SlingHttpServletRequest.class, adapters = {
		AutopaceTabs.class }, resourceType = AutopaceTabs.RESOURCE_TYPE_TABS, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class AutopaceTabs {

	public static final String RESOURCE_TYPE_TABS = "autopace/components/content/tabs";

	@Inject
	@Via("resource")
	private List<AutopaceTabsItem> tabItems;

	public List<AutopaceTabsItem> getTabItems() {
		return tabItems;
	}

}


 Below is the code for AutopaceTabsItem.java which is acting like a bean class for each tab item:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package org.autopace.autopace.core.models;

import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;

import com.adobe.cq.export.json.ExporterConstants;

@Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class AutopaceTabsItem {

	@ValueMapValue
	private String title;

	@ValueMapValue
	private String tabId;

	public String getTitle() {
		return title;
	}

	public String getTabId() {
		return tabId;
	}

}


So far we discussed, how we can create a authorable custom tab component and can retain the authored components intact to there individual tabs by using the tabId. Now lets try to automate this process so that authoring of Id is not required. We will be writing our custom JS code which will assign an ID to each tab i.e. to each addition to the multifield.

If we observe again in the XML provided for dialog above, we will see that at line #6, I have used an extraClientlib, this Clientlib will be called whenever we will be authoring the dialog. Lets create a Clientlib with autopace.tabs category and add the below JS to it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(function($) {
	$.validator.register({
		selector : "input.tab-title",
		validate : validate,
		show : show,
		clear : clear
	});
	function validate($el) {
		var multifieldItem = $el.closest('.coral3-Multifield-item');
		var val = Math.floor(1000 + Math.random() * 9000);
		var tabName = "tabpar_" + val;
		var parsysNameField = $(multifieldItem).find("input.tab-id");
		if (!parsysNameField.val()) {
			parsysNameField.val(tabName);
		}

		return;
	}
	function show($el, message) {
	}
	function clear($el) {
	}
}(jQuery));


The above JS will help us auto-populate an ID for each addition to multifield. It will make sure that whenever authors click on ADD in the multifield, the tabId field is auto-populated and author needs to only provide a title for the tabs. In this JS, we are creating a random Number at line #10. This random number is appended to "tabpar_" at line #11 and then this newly created ID is getting assigned to the tabId field of each addition at line #14. Also, the class name used at line #3 i.e. "tab-title" and at line #12 i.e. "tab-id" are the custom classes added to the dialog fields in the XML at line #43 and #49 respectively to search those fields via JS.

Note: This JS is designed and tested with the structure for Coral3 dialog fields and might need some modifications for other field inheritance.

Next, what we can do is the we can hide this tabId field for each addition as the tabID is now getting auto-populated and will only be used to maintain the components added to each tab. There is no use of this ID to authors. Hence, to hide this field we can include a CSS to the same Clientlib i.e. "autopace.tabs". Below is the CSS file which I have used:

1
2
3
input.tab-id {
	display:none !important;
}


This file will help us hide the field whenever authors will try to author the tab component. This is how it will look if someone is trying to author it:



Once we save this dialog, we can keep switching to preview/edit mode and start adding the components to each tab. All the components added to each tab will be added under a separate node in CRXDE with the node name as the tabID which is generated via our JS:



This way we can create a custom Tab component. But, there is one drawback of this approach, say we need to add 3 tabs say Tab1, Tab2 and Tab3. We added them and authored the content inside each one of them. So the other 3 nodes with the tabId corresponding to each tab will be created. Now let us suppose we need to delete a tab say Tab2 later. In this case we can go to the dialog box and can delete Tab2 easily and the component will also render just fine. But the node created for storing Tab2 components with tabId will not be deleted from CRXDE. It will result in creating stale nodes in CRXDE. 

Although, this is an edge case scenario and is of not much harm. But if it is very important to fix it based on the usage of this component, we can write a custom service to read all the tabId from the tabs available and can check if any extra node is available in CRXDE. The service can be used to delete the extra nodes (if any).

Thursday, July 30, 2020

JMX in AEM

We can invoke/consume a custom service in AEM in many ways. One way of calling it is via JMX MBean. With the JMX we can invoke any piece of code on demand by just a click on the invoke button. Also, we can pass as many parameters as required to be used inside our code. In this blog, we will create a custom JMX to call a service that simply prints a confirmation in the log.

I have already created a TestService interface and TestServiceImpl class which have only one method and it just prints a confirmation in the logger. Our JMX class will call the method in the service class and eventually the confirmation will be printed on the invoke of JMX. To Begin let's create a TestJMX interface:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package org.autopace.core.jmx;

import com.adobe.granite.jmx.annotation.Description;
import com.adobe.granite.jmx.annotation.Name;

@Description("Test JMX for Autopace")
public interface TestJMX {

 @Description("Trigger Test Service")
 String triggerTestService(@Name("anyParameter") @Description("This is any parameter which we need as Input.") String anyParameter);

 @Description("Test Flag")
 String getFlag();

}

In the above code, we have used @Description annotation on top of interface TestJMX and for both the methods in the interface. This is used to give a better understanding to the authors of what each method does and what JMX does as a whole. Further, if there are any parameters which are required for any method then we can pass then with the help of @Name annotation and better description can be added with @Description annotation as done in the line:

String triggerTestService(
@Name("anyParameter") @Description("This is any parameter which we need as Input.") String any parameter);

This is how it looks to the authors who are trying to invoke the JMX:



Next, we need to create the implementation class for our JMX, let's have a look at the below code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package org.autopace.core.jmx;

import javax.management.NotCompliantMBeanException;

import org.autopace.core.services.TestService;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.adobe.granite.jmx.annotation.AnnotatedStandardMBean;


@Component(property = {"jmx.objectname=org.autopace.core.jmx:type=AutoPaceTestJMX", "pattern=/.*" })
public class TestJMXImpl extends AnnotatedStandardMBean implements TestJMX {

 public TestJMXImpl() throws NotCompliantMBeanException {
  super(TestJMX.class);
 }

 private static final Logger LOG = LoggerFactory.getLogger(TestJMXImpl.class);

 @Reference
 private TestService testService;

 boolean flag;

 @Override
 public String triggerTestService(String anyParameter) {

  if (flag) {
   LOG.info("Service is already Running");
   return "Service already Running";
  }

  try {
   LOG.info("Triggering Test Service via JMX");
   testService.printInLog(anyParameter);
   return "Test Service Successfully Triggered via JMX.";
  } catch (Exception e) {
   LOG.error("Some Exception occured while triggereing Test Service via JMX", e);
  } finally {
   flag = false;
  }

  return "Something went wrong while triggering Test Service via JMX.";
 }

 @Override
 public String getFlag() {
  if (flag) {
   return "Service is already running.";
  }
  return "Service is not running, we can invoke the triggerTestService Method";
 }

}

To register the JMX, I have used @Component annotation with two properties in which jmx.objectname=org.autopace.core.jmx: AutoPaceTestJMX is the name that will come in the JMX console.

Also, we have extended AnnotatedStandardMBean and created a public constructor that throws NotCompliantMBeanException.

Next, I used the @Reference annotation to use the TestService methods. Now we have implemented both the methods which we have created in TestJMX. The first method is used to trigger the TestService triggerTestService(String anyParameter) method and the second one is just a flag which tells us if the JMX service is currently invoked or not.

To clarify, let's say if we invoked a JMX and the service takes half an hour to complete. Now, we do not have any option to restrict the invoke button in between and authors can click on invoke multiple times. To prevent this scenario, we have created a flag that is set to true as soon as the jmx is invoked and it only returns to false once the service is complete. Hence, if anyone clicks on the invoke again while service is running, the flag will be true and the service method will not be called, and in our case "Service already Running" will be printed.

The getter for the flag is our second method. This is helpful when an author lands to jmx console. Based on this flag, authors can check if the service is currently active or not. Also, if we add a getter method, the variable comes under Attributes and we do not need to invoke any method to check the value. Simply landing on the JMX console will print the getter value.

If a method is added to JMX, then it comes under operations and we can invoke the methods and can pass any parameters as per requirement.

Once we have the TestJMX and TestJMXImpl ready, we need to deploy the build and follow the below steps to invoke the JMX:

Step 1: Login to <Host>:<Port>/system/console/jmx.

Step 2: Search for the JMX Name used while registering JMX. In our case, it is AutoPaceTestJMX.




Step 3: Click on the AutoPaceTestJMX, it will show you the attributes and operations available. Check the flag value to see if any method is invoked or not. If nothing is active, click on any method to invoke it.



Step 4: Now, we have an option to pass any parameter while invoking the method. Pass any parameter and click on invoke. Once the service, will be completed, it will return us "Test Service Successfully Triggered via JMX." as coded in our TestJMXImpl.



In our case, the Method in TestService simply prints an info log with the parameter passed. If you have a similar service, you can verify your execution in logs.

In this way, we can write a JMX which can help in execution of any piece of code on demand. One use case would be to write a JMX to trigger a service which is usually triggered via Scheduler. JMX will help in giving authors the flexibility and they do not need to change the cron expression everytime.

Monday, July 6, 2020

AEM Scheduler with Config Factory

In this article, we will try to explain how to create an AEM scheduler with multiple configs so that the job can be scheduled for different cron expressions. This scenario will be helpful in case we need to run the same job at a different type with some different parameters. For Example, we can have a scenario where we need to run the same job to create the sitemap but the locales are different, hence we need different schedulers for the same.


For creating an AEM Scheduler we will be using R6 annotations. We will start by creating a schedulerConfiguration.java using @ObjectClassDefinition and @AttributeDefinition annotations. Please have a look at the below code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@ObjectClassDefinition(name = "Configuration of the Scheduler")
public @interface SchedulerConfiguration {

	@AttributeDefinition(
	name = "Name of the scheduler",
	type = AttributeType.STRING)
	String name();

	@AttributeDefinition(
	name = "Cron Expression of the scheduler",
	type = AttributeType.STRING)
	String expression();	

	@AttributeDefinition(
	name = "Whether or not to run the scheduler concurrently",
	type = AttributeType.BOOLEAN)
	String concurrent();

	@AttributeDefinition(
	name = "Whether or not the scheduler is enabled",
	type = AttributeType.BOOLEAN)
	String enabled();

	@AttributeDefinition(
	name = "Any other distinct parameter for the job",
	type = AttributeType.STRING)
	String distinctParam();
}

In here, we have defined 5 fields for the configuration, you can add additional fields based on the requirement.

  • The first field is for the scheduler name and we are assuming it should be configured uniquely for each configuration added. 
  • The second is for the scheduler cron expression and it can be set as per the requirement. 
  • The third field indicates if the job for each scheduler configured can run concurrently or not. This means if you have a scheduler for 1 minute and the job starts executing, the next job will only run once the old one is finished (if the concurrent is unchecked).
  • The fourth indicates if the scheduler is active or not.
  • The last field is for any distinct parameter which we may need to use in our job. For Ex: We may end up with a scenario where we need to distinct locale value for each scheduler configuration.
Once we have the scheduler configuration ready, we can start writing the AddScheduler.java. In this, we need to use the @Designate and @Activate annotations. Also, we need to understand different scenarios which could arise with multiple configurations, and what methods will be called from AddScheduler.java on the execution of all the scenarios. What I see is there could be three scenarios:

  1. We have a possibility to add a new configuration - When a new configuration is added, only the method denoted by @Activate will be called.
  2. We have a possibility to remove an old existing configuration - When an old configuration is removed, only the method denoted by @Deactivate will be called.
  3. We have a possibility to modify an old existing configuration - When an old configuration is modified, first @Deactivate will be called followed by @Activate.
Now, let's have a look at the code and then we will deep dive and see how the above scenarios are handled:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Component(service = Runnable.class, immediate = true)
@Designate(ocd = SchedulerConfiguration.class, factory = true)
public class AddScheduler implements Runnable {
	
        private static final Logger LOG = LoggerFactory.getLogger(AddScheduler.class);
	
        @Reference
	private Scheduler scheduler;

	@Override
	public void run(){
		//Do Nothing
	}

	@Activate
	protected void activate(SchedulerConfiguration config){
		//Write a JOB
		final Runnable job = () -> {
			//Do something using the distinct parameter.
			LOG.info("Inside Run Method for the scheduler with 
                        name: {} and distinctParam: {}", config.name(), 
                        config.distinctParam());
		}
		SchedulerOptions options = scheduler.EXPR(config.expression());
		options.canRunConcurrently(config.concurrent());
		options.name(config.name());
		if(config.enabled()){
			LOG.info("Adding Schduler with Name:{}", config.name());
			scheduler.schedule(job, options);
		}
	}

	@Deactivate
	protected void deactivate(SchedulerConfiguration config){
		LOG.info("Removing Scheduler with name: {}", config.name());
		scheduler.unschedule(config.name());
	}
}

In this, we have created AddScheduler by implementing Runnable class and have used scheduler API to schedule and unschedule the schedulers. Also, if we observe closely, we have used "factory=true" in the @Designate which makes our configuration as config factory. Now, let's try to understand each scenario:
  1. When we add any new configuration, then the code flow does not go to @Deactivate and only execute whatever is inside @Activate. This means it creates a job and then uses the scheduler expression, name, and concurrent property of the config to create the scheduler options. Further, it checks if the scheduler is enabled or not and if the scheduler is enabled, it will scheduler our scheduler based on the values in the configuration.
  2. When we remove any old configuration, the code flow only goes to @Deactivate. This means if the configuration is removed/deleted, the scheduler is unscheduled based on the scheduler's name. That is why we have assumed that the configurations should be added with a unique name.
  3. When any configuration is modified, then the code flow goes to @Deactivate first and then to the @Activate. That means if any value is modified then we first unschedule the old scheduler with the old configuration values, and then we move on to @Activate where it creates a fresh job with new values of the parameter configured and schedules a new scheduler with the new values of the parameter. 
In this way, this code will take care of all our scenarios discussed above.

Contact Form

Name

Email *

Message *

Categories

Latest Post

Memory Management in JAVA

One of the most important features of Java is its memory management. Since Java uses automatic memory management, it becomes superior to tho...

Popular Posts