May 13, 2020
Estimated Post Reading Time ~

AEM Components

Most java code in AEM that does any processing will be a component. While the annotation doesn't specifically give you much in the way of additional functionality, it will bind the piece of code into the Components section of the OSGI system console so that you can check/change any of the values in the properties. It also allows you to write a configuration file to update the properties file at deploy time.
Within any OSGi Component, you can reference another component whether that is a simple component or a service
@Component(metatype = true, label = "A scheduled task", description = "Simple demo for cron-job like task with properties")
The component tag is generally used with services, servlets, and other processing components.
It has the following options
  • name
    • The name of the component
    • Defaults to the fully qualified name of the java class
  • label
    • Adds metadata for the component's title
  • description
    • Adds metadata for the components description
  • enabled
    • If the component is enabled when deployed or not
    • A component that is not enabled cannot be used
    • Defaults to true
  • factory
    • If the component is a factory for other components
  • immediate
    • If the component will be activated immediately
    • defaults to false
  • metatype
    • If the metatype service data is generated in the metatype.xml file for the component
  • ds
    • If the declarative services configuration is written for the component
  • specVersion
    • The version of the declarative services against which the component was written
  • createPid
    • If the component should create a PID
    • Defaults to true
  • configurationFactory
    • Sets the metatype factory pid property for non-factory components
    • Defaults to false
  • policy
    • The configuration policy
    • Defaults to ConfigurationPolicy.OPTIONAL
      • If the configuration is available it will be used, the component will still be activated without configuration
    • ConfigurationPolicy.IGNORE
      • Doesn't require any configuration
    • ConfigurationPolicy.REQUIRED
      • Will not activate the component without configuration settings
  • configurationPid
    • The PID of the configuration
Components are allowed to have properties that can be defined in the OSGI configuration section, or deployed as an xml file named the same as the package and class name. i.e the following is the xml file org.apache.sling.commons.log.LogManager.factory.config-apps.xml from the config folder of the app (this will be discussed in the ui.apps section)
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
xmlns:jcr="http://www.jcp.org/jcr/1.0" jcr:primaryType="sling:OsgiConfig"
org.apache.sling.commons.log.file="logs/project-toolbox.log"
org.apache.sling.commons.log.level="info"
org.apache.sling.commons.log.names="[com.ibm.ix.toolbox.core]"
org.apache.sling.commons.log.pattern="\{0,date,yyyy-MM-dd HH:mm:ss.SSS} {4} [{3}] {5}" />
It is possible to have the configuration as a singleton so that every app that sends configuration would overwrite the others, or as an appendment such as the logger one above.
Properties can be defined in multiple ways using the felix scr properties annotation,
At class level
@Properties({
@Property(name = "service.description", value = "Replication Service"),
@Property(name = "service.vendor", value = "Sample"),
@Property(name = "process.label", value = "Replication"),
@Property(name = "secondsBeforeReplication", longValue=30 })
Adding properties at class level doesn't bind them to any particular variable in the class and you would need to read the properties from the config when the component is updated/activated
@Activate
protected void activate(final Map<String, Object> config) {
secondsBeforeReplication = PropertiesUtil.toLong(config.get("secondsBeforeReplication"), 0);
}
however it is also possible to bind a parameter straight to a variable itself
@Property(value = "append-version", propertyPrivate = true) private String PIPELINE_TYPE = "pipeline.type";
While this has been documented in a few places, I have never used it myself and always do class level properties.
One of the nice things with configuration, as I mentioned, you can have multiple configurations deployed by different apps and have them all run at the same time, however, your component will need to handle the different configurations and understand how to work with the different ones.
The following is an example of a multi-app configuration to allow for multiple apps to define their external addresses. I will explain each of the classes in this service starting with the interface
package com.sample.core.services;
import org.apache.sling.api.resource.Resource;

public interface Externalizer {
String getAuthor(final Resource page);
String getDispatcher(final Resource page);
}
The interface is pretty basic and defines how the service will work
package com.sample.core.services.impl;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Modified;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.felix.scr.annotations.References;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.api.resource.Resource;

import com.sample.core.services.Externalizer;

@Component(immediate = true,
label = "Externalizer Service",
description = "Custom AEM service for getting external urls",
metatype = true, enabled = true)
@Service
@Properties({
@Property(name = "path", label = "Path", description = "The path these settings are for"),
@Property(name = "author", label = "Author", description = "The author url"),
@Property(name = "dispatcher", label = "Dispatcher", description = "The dispatcher url") })
@References({
@Reference(name = "amendment", referenceInterface = ExternalizerConfigAmendment.class,
cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE, policy = ReferencePolicy.DYNAMIC, updated = "updateAmendment") })
public class ExternalizerService implements Externalizer {
private static final String SERVICE_ID = "service.id";
private static final String HTTP_LOCALHOST = "http://localhost";
private static final String HTTP_LOCALHOST_4502 = "http://localhost:4502";
private Map<Long, ExternalizerConfigAmendment> amendments = new HashMap<>();
private List<ExternalizerConfigAmendment> sortedMappings = new ArrayList<>();

@Activate
@Modified
public void configure(final Map>String, Object< config) {
synchronized (this.amendments) {
this.updateMappings();
}
}

private void updateMappings() {
sortedMappings = new ArrayList><();
for (final ExternalizerConfigAmendment amendment : this.amendments.values()) {
sortedMappings.add(amendment);
}
Collections.sort(sortedMappings);
}

@Override
public String getAuthor(Resource page) {
for (ExternalizerConfigAmendment config : sortedMappings) {
if (config.isValidFor(page.getPath())) {
return config.getAuthor();
}
}
return HTTP_LOCALHOST_4502;
}

@Override
public String getDispatcher(Resource page) {
for (ExternalizerConfigAmendment config : sortedMappings) {
if (config.isValidFor(page.getPath())) {
return config.getDispatcher();
}
} return HTTP_LOCALHOST;
}

protected void bindAmendment(final ExternalizerConfigAmendment amendment, final Map<String, Object> props) {
final Long key = (Long) props.get(SERVICE_ID);
synchronized (this.amendments) {
amendments.remove(key);
amendments.put(key, amendment);
this.updateMappings();
}
}

protected void unbindAmendment(final ExternalizerConfigAmendment amendment, final Map<String, Object> props) {
final Long key = (Long) props.get(SERVICE_ID);
synchronized (this.amendments) {
if (amendments.remove(key) != null) {
this.updateMappings();
}
}
}

protected void updateAmendment(final ExternalizerConfigAmendment amendment, final Map<String, Object> props) {
this.bindAmendment(amendment, props);
}
}
As you can see there isn't much in there that is amazing. It is the Reference annotation that tells the component that it will get it's reference's from another class
This has the following configuration
  • name
    • The name of the configuration.
    • It is possible to have multiple configurations for different implementations or factory components
  • referenceInterface
    • The interface or class that implements the configuration
  • cardinality
    • Defines the referential cardinality of configurations
    • Defaults to ReferenceCardinality.MANDATORY_UNARY
    • ReferenceCardinality
      • OPTIONAL_UNARY
        • 0..1 reference
        • No service required to be available for the reference to be satisfied. Only a single service is available through this interface
      • MANDATORY_UNARY
        • 1..1 reference
        • At least one service must be available for the reference to be satisfied. Only a single service is available through this reference.
      • OPTIONAL_MULTIPLE
        • 0..n reference
        • No service required to be available for the reference to be satisfied. All matching services are available through this reference.
      • MANDATORY_MULTIPLE
        • 1..n reference
        • At least one service must be available for the reference to be satisfied. All matching services are available through this reference.
    • policy
      • Defines the dynamic reference policy, this controls how the reference configuration is activated/deactivated
      • Defaults to ReferencePolicy.STATIC
      • ReferencePolicy
        • STATIC
          • The component will be deactivated and re-activated if the service comes and/or goes away
        • DYNAMIC
          • The service will be made available to the component as it comes and goes
    • policyOption
      • Defines the options for the reference
      • Defaults to
        • ReferencePolicyOption.RELUCTANT
      • ReferencePolicyOption
        • RELUCTANT
          • The reluctant policy option is the default policy option. When a new target service for a reference becomes available, references having the reluctant policy option for the static policy or the dynamic policy with a unary cardinality will ignore the new target service. References having the dynamic policy with a multiple cardinalities will bind the new target service
        • GREEDY
          • When a new target service for a reference becomes available, references having the greedy policy option will bind the new target service
    • target
      • The service target filter to select specific services to be made available
    • bind
      • The method to be used for binding the reference configuration
      • It defaults to the name created by the string bind and the reference name i.e bindAmendment in the example above
    • unbind
      • The method to be used for unbinding the reference configuration
      • It defaults to the name created by the string unbind and the reference name i.e unbindAmendment in the example above
    • updated
      • The method to be used for updating the reference configuration
    • strategy
      • The reference strategy
      • Defaults to ReferenceStrategy.EVENT
      • ReferenceStrategy
        • EVENT
          • The reference is updated on events using the bind and unbind methods
        • LOOKUP
          • The reference is looked up via the component context
In the class above you can see the bind/unbind and updated methods are updating a map of the amendments using the service id property to define each amendment's uniqueness. In this class, we have also defined the properties so that it can have default values, however as you can see these have not been used. We sort the amendments so that they are processed in the correct order. The sorting order is handled by the amendments class itself
The amendments class itself is a very simple service that really only handles the parameters, as well as having a method to determine if it is the one that should be used.
package com.sample.core.services.impl;

import java.util.Map;

import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.ConfigurationPolicy;
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.commons.osgi.PropertiesUtil;

@Component(metatype = true,
name = "com.sample.core.services.impl.ExternalizerService.amended",
label = "Externalizer Config Amendment",
description = "An amendment to the externalizer",
configurationFactory = true, policy = ConfigurationPolicy.REQUIRE)
@Service(value = ExternalizerConfigAmendment.class)

@Properties({
@Property(name = "serviceRanking", label = "Ranking", description = "Amendments are processed in order of their ranking, an amendment with a higher ranking has precedence over a mapping with a lower ranking."),
@Property(name = "path", label = "Path", description = "The path these settings are for"),
@Property(name = "author", label = "Author", description = "The author url"),
@Property(name = "dispatcher", label = "Dispatcher", description = "The dispatcher url") })
public class CastrolExternalizerConfigAmendment implements Comparable<CastrolExternalizerConfigAmendment> {
private String path;
private String author;
private String dispatcher;
private int serviceRanking;

@Activate
protected void activate(final Map<String, Object> config) {
author = PropertiesUtil.toString(config.get("author"),"");
dispatcher = PropertiesUtil.toString(config.get("dispatcher"),"");
path = PropertiesUtil.toString(config.get("path"),"");
serviceRanking = PropertiesUtil.toInteger(config.get("serviceRanking"), 0);
}

public boolean isValidFor(String path) {
return path.startsWith(this.path);
}

public String getAuthor() {
return author;
}

public String getDispatcher() {
return dispatcher;
}

@Override
public int hashCode() {
return path.hashCode();
}

@Override
public boolean equals(Object o) {
if (o instanceof CastrolExternalizerConfigAmendment) {
return ((CastrolExternalizerConfigAmendment)o).path.equals(path);
}
return false;
}

@Override
public int compareTo(CastrolExternalizerConfigAmendment o) {
if (this.serviceRanking > o.serviceRanking) {
return -1;
} else if (this.serviceRanking < o.serviceRanking) {
return 1;
}
return this.hashCode()-o.hashCode();
}
}
As you can see other than retrieving the properties in the activation method there isn't much that this class needs to do. In this implementation we have added the serviceRanking field to allow for sorting of the configuration.

Component Annotations

There are a few annotations that are used in all components (services/servlets)
  • Activate
    • Defines the method that is called at activation of the component
  • Deactivate
    • Defines the method that is called at deactivation of the component
  • Modified
    • Defines the method that is called if the component is modified


By aem4beginner

No comments:

Post a Comment

If you have any doubts or questions, please let us know.