Handling events using ResourceChangeListener AEM 6.3 - annotations

Can someone please help me in understanding how to implement a ResourceChangeListener and handle events using osgi R6 annotations?
I saw a similar post with no answer.AEM 6.3 - Creating Event handler using OSGi R6 annotations

The code snippet below registers a ResourceChangeListener with OSGI R6 annotations. The comments inline have the explanation.
import java.util.List;
import org.apache.sling.api.resource.observation.ResourceChange;
import org.apache.sling.api.resource.observation.ResourceChangeListener;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
#Component(
service = ResourceChangeListener.class,
property = {
// filter the notifications by path in the repo. Can be array and supports globs
ResourceChangeListener.PATHS+"="+"/content",
//The type of change you want to listen to.
//Possible values at https://sling.apache.org/apidocs/sling9/org/apache/sling/api/resource/observation/ResourceChange.ChangeType.html.
ResourceChangeListener.CHANGES+"="+"ADDED",
ResourceChangeListener.CHANGES+"="+"REMOVED",
ResourceChangeListener.CHANGES+"="+"CHANGED"
//PS: If you want to declare multiple values for a prop, you repeat it in OSGI R6 annotations.
//https://stackoverflow.com/questions/41243873/osgi-r6-service-component-annotations-property-list#answer-41248826
}
)
public class SampleResourceChangeListener implements ResourceChangeListener{ // Use ExternalResourceChangeListener to listen for changes that happen in a different node
public static final Logger LOGGER = LoggerFactory.getLogger(SampleResourceChangeListener.class);
//This method will be called with paths and types of change that occurred
//The task in this should be fast. In case it takes more time, trigger a sling job from here.
//The listener can be blacklisted if it's slow [it does for event handler, should be same for ResourceListener IMHO]
#Override
public void onChange(List<ResourceChange> list) {
list.forEach((change) -> {
LOGGER.info(change.getPath());
LOGGER.info(change.getType().toString());
//the methods to identify the property that changed are deprecated.
});
}
}
Some reference implementations within Sling
https://github.com/apache/sling-org-apache-sling-discovery-commons/blob/4f7d7ca3224239d52798cc8418ec8283f5eddc9e/src/main/java/org/apache/sling/discovery/commons/providers/spi/base/IdMapService.java
https://github.com/apache/sling-org-apache-sling-scripting-java/blob/89c28859a7df17a40eaaf2c26ee2433c98830204/src/main/java/org/apache/sling/scripting/java/impl/JavaScriptEngineFactory.java

Related

Why double inject an Android Application in Dagger?

Looking at the source code for the Android Architecture Components sample GithubBrowerSample, I don't understand the point of double injecting the githubApp.
Wouldn't the inject method be enough? Why do it need both of them in the same sentence?
public interface AppComponent {
#Component.Builder
interface Builder {
#BindsInstance Builder application(Application application);
AppComponent build();
}
void inject(GithubApp githubApp);
}
And they use it like:
public static void init(GithubApp githubApp) {
DaggerAppComponent.builder().application(githubApp)
.build().inject(githubApp);
As Thomas Broyer described, you've got two separate directions to set up: You want the Dagger graph to know how to get to your Application instance, and you want to get access to certain bindings out of the dependency graph.
The #BindsInstance line in the Builder creates a binding for Application set to the instance you pass in. It sounds like you understand this part.
However, after you've created your Component, presumably you want to use it. Let's say you want to get fully-injected instances of classes Dep1, Dep2, and Dep3 out of your graph. One way you could do this is to create methods on your Component that get the instances:
#Singleton #Component(/* ... */) interface AppComponent {
// [builder snipped out here]
fun getDep1(): Dep1
fun getDep2(): Dep2
fun getDep3(): Dep3
}
And then you call those as part of your App creation.
var appComponent = DaggerAppComponent.builder().application(githubApp).build()
var dep1 = appComponent.getDep1()
var dep2 = appComponent.getDep2()
var dep3 = appComponent.getDep3()
// Use dep1, dep2, and dep3 here.
However, you can also create a single-arg method, which is typically a void method called inject. This populates all of the #Inject-annotated fields and calls all of the #Inject-annotated methods on the instance you pass in. If GitHubApp has #Inject-annotated-fields (and it does), the call to inject lets you skip defining all of the getters on the Component. That reduces all of the above code to:
DaggerAppComponent.builder().application(githubApp)
.build().inject(githubApp)
...which is what you see in the demo.
The #BindsInstance tells Dagger that it should inject the application into whichever #Inject Application it finds in the dependency graph.
The second asks Dagger to inject dependencies into it's #Inject-annotated fields and methods. This the root of the dependency graph.
Technically, the component method can be called as many times as you like, while the builder method can only be called once.

Catching AEM Content installation event

The use case here is to get the information of content with type cq:Page and dam:Asset that is being installed/updated in the JCR using package manager in AEM.
I would like to know if there are any event listener or API's that can be used for this purpose.
Is there a way to know if the content was installed/updated using the package manager?
AEM 6.2 is being used here.
Thanks!!
To detect Resource changes, you need to create a ResourceChangeListener
Here is an example based on ACS commons SampleResourceChangeListener but restricted to ADDED and CHANGED events.
import org.apache.felix.scr.annotations.*;
import org.apache.sling.api.resource.observation.ExternalResourceChangeListener;
import org.apache.sling.api.resource.observation.ResourceChange;
import org.apache.sling.api.resource.observation.ResourceChangeListener;
import org.apache.sling.event.jobs.JobManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* The Sling Resource Change Listener is the preferred method for listening for Resource Change events in AEM.
* This is preferred over the Sling Resource Event Listener, or the JCR Event Handler approaches.
*
* ResourceChangeListener Javadoc:
* - https://docs.adobe.com/docs/en/aem/6-2/develop/ref/javadoc/org/apache/sling/api/resource/observation/ResourceChangeListener.html
*
* Note: To listen for External events, implements the ExternalResourceChangeListener. If ONLY local events are in scope, implement only the ResourceChangeListener.
*/
#Component(
label = "Sample - Resource Change Listener",
description = "A sample implementation of the Sling Resource Change Listener",
metatype = true
)
#Properties({
// Scope the paths as tightly as possible based on your use-case.
#Property(
label = "Paths",
description = "[ Required ] A list of resource paths this listener will listen for change events.",
name = ResourceChangeListener.PATHS,
value = {"/content"}
),
// Scope the types as tightly as possible based on your use-case.
// If This property is not provided, ALL ChangeTypes will be accepted.
// Available values are defined on: ResourceChange.ChangeType
#Property(
label = "Change Types",
description = "[ Optional ] The change event types this listener will listener for. ",
name = ResourceChangeListener.CHANGES,
value = {"ADDED", "CHANGED"}
)
})
#Service
public class SampleResourceChangeListener implements ResourceChangeListener, ExternalResourceChangeListener {
private static final Logger log = LoggerFactory.getLogger(SampleResourceChangeListener.class);
#Reference
private JobManager jobManager;
public void onChange(#Nonnull List<ResourceChange> changes) {
// Iterate over the ResourceChanges and process them
for (final ResourceChange change : changes) {
// Process each change quickly; Do not do long-running work in the Resource Change Listener.
// If expensive/long-running work is required to handle the event, create a Sling Job to perform that work.
if (change.isExternal()) {
// Since this implements BOTH the ResourceChangeListener AND ExternalResourceChangeListener
// we can conditionally handle local vs external events.
}
switch (change.getType()) {
case ADDED:
log.debug("Change Type ADDED: {}", change);
if (change.getAddedPropertyNames().contains("someProperty")) {
// Do some work
// In this case we will pass some some data from the Event to a custom job via a custom Job topic.
final Map<String, Object> props = new HashMap<String, Object>();
props.put("path", change.getPath());
props.put("userId", change.getUserId());
jobManager.addJob("com/adobe/acs/commons/samples/somePropertyAdded", props);
}
break;
case CHANGED:
log.debug("Change Type CHANGED: {}", change);
if (change.getChangedPropertyNames().contains("someOtherProperty")) {
// Do some other work
}
break;
default:
// Do nothing
}
}
}
}
Alternatively, you can use a JcrEventListener, again ACS has a good example here: SampleJcrEventListener.java
I don't know of a way to detect if the cause of the add/update is package manager. However, if you want to execute some code before a package is installed, you can use InstallHook. I have not tried this before.

Flyway Spring Boot Autowired Beans with JPA Dependency

I am using Flyway 5.0.5 and I am unable to create a java (SpringJdbcMigration) with autowired properties... They end up null.
The closest thing I can find is this question: Spring beans are not injected in flyway java based migration
The answer mentions it being fixed in Flyway 5 but the links are dead.
What am I missing?
I struggled with this for a long time due to my JPA dependency. I am going to edit the title of my question slightly to reflect this...
#Autowired beans are instantiated from the ApplicationContext. We can create a different bean that is ApplicationContextAware and use that to "manually wire" our beans for use in migrations.
A quite clean approach can be found here. Unfortunately, this throws an uncaught exception (specifically, ApplicationContext is null) when using JPA. Luckily, we can solve this by using the #DependsOn annotation and force flyway to run after the ApplicationContext has been set.
First we'll need the SpringUtility from avehlies/spring-beans-flyway2 above.
package com.mypackage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
#Component
public class SpringUtility implements ApplicationContextAware {
#Autowired
private static ApplicationContext applicationContext;
public void setApplicationContext(final ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
/*
Get a class bean from the application context
*/
public static <T> T getBean(final Class clazz) {
return (T) applicationContext.getBean(clazz);
}
/*
Return the application context if necessary for anything else
*/
public static ApplicationContext getContext() {
return applicationContext;
}
}
Then, configure a flywayInitializer with a #DependsOn for springUtility. I extended the FlywayAutoConfiguration here hoping to keep the autoconfiguration functionality. This mostly seems to have worked for me, except that turning off flyway in my gradle.build file no longer works, so I had to add the #Profile("!integration") to prevent it from running during my tests. Other than that the autoconfiguration seems to work for me but admittedly I've only run one migration. Hopefully someone will correct me if I am wrong.
package com.mypackage;
import org.flywaydb.core.Flyway;
import org.springframework.boot.autoconfigure.flyway.FlywayMigrationInitializer;
import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.DependsOn;
import com.mypackage.SpringUtility;
#Configuration
#Profile("!integration")
class MyFlywayConfiguration extends FlywayConfiguration {
#Primary
#Bean(name = "flywayInitializer")
#DependsOn("springUtility")
public FlywayMigrationInitializer flywayInitializer(Flyway flyway){
return super.flywayInitializer(flyway);
//return new FlywayMigrationInitializer(flyway, null);
}
}
And just to complete the example, here is a migration:
package db.migration;
import org.flywaydb.core.api.migration.spring.BaseSpringJdbcMigration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import com.mypackage.repository.AccountRepository;
import com.mypackage.domain.Account;
import com.mypackage.SpringUtility;
import java.util.List;
public class V2__account_name_ucase_firstname extends BaseSpringJdbcMigration {
private AccountRepository accountRepository = SpringUtility.getBean(AccountRepository.class);
public void migrate(JdbcTemplate jdbcTemplate) throws Exception {
List<Account> accounts = accountRepository.findAll();
for (Account account : accounts) {
String firstName = account.getFirstName();
account.setFirstName(firstName.substring(0, 1).toUpperCase() + firstName.substring(1));
account = accountRepository.save(account);
}
}
}
Thanks to avehlies on github, Andy Wilkinson on stack overflow and OldIMP on github for helping me along the way.
In case you are using more recent versions of Flyway, then extend BaseJavaMigration instead of BaseSpringJdbcMigration as the later is deprecated. Also, take a look at the below two comments by the user Wim Deblauwe.
The functionality hasn't made it into Flyway yet. It's being tracked by this issue. At the time of writing that issue is open and assigned to the 5.1.0 milestone.
Seems the updated answer provided by #mararn1618 is under documented on the official documentation, so I will provide a working setup here. Thanks to #mararn1618 for guiding in that direction.
Disclaimer, it's written in Kotlin :)
First you need a configuration for loading the migration classes, in Spring Boot (and perhaps Spring) you need either an implementation of FlywayConfigurationCustomizer or a setup of FlywayAutoConfiguration.FlywayConfiguration. Only the first is tested, but both should work
Configuration a, tested
import org.flywaydb.core.api.configuration.FluentConfiguration
import org.flywaydb.core.api.migration.JavaMigration
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.flyway.FlywayConfigurationCustomizer
import org.springframework.context.ApplicationContext
import org.springframework.stereotype.Component
#Component
class MyFlywayConfiguration #Autowired constructor(
val applicationContext: ApplicationContext
) : FlywayConfigurationCustomizer {
override fun customize(configuration: FluentConfiguration?) {
val migrationBeans = applicationContext.getBeansOfType(JavaMigration::class.java)
val migrationBeansAsArray = migrationBeans.values.toTypedArray()
configuration?.javaMigrations(*migrationBeansAsArray)
}
}
Configuration option B, untested, but should also work
import org.flywaydb.core.api.migration.JavaMigration
import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration
import org.springframework.boot.autoconfigure.flyway.FlywayConfigurationCustomizer
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
#Configuration
class MyFlywayConfiguration : FlywayAutoConfiguration.FlywayConfiguration() {
#Bean
fun flywayConfigurationCustomizer(applicationContext: ApplicationContext): FlywayConfigurationCustomizer {
return FlywayConfigurationCustomizer { flyway ->
val p = applicationContext.getBeansOfType(JavaMigration::class.java)
val v = p.values.toTypedArray()
flyway.javaMigrations(*v)
}
}
}
And with that you can just write your migrations as almost any other Spring bean:
import org.flywaydb.core.api.migration.BaseJavaMigration
import org.flywaydb.core.api.migration.Context
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
#Component
class V7_1__MyMigration #Autowired constructor(
) : BaseJavaMigration() {
override fun migrate(context: Context?) {
TODO("go crazy, mate, now you can import beans, but be aware of circular dependencies")
}
}
Side notes:
Be careful of circular dependencies, your migrations can most likely not depend on repositories (also makes sense, you are preparing them, after all)
Make sure your migrations are located where Spring scans for classes. So if you want to place them in the namespace db/migrations, you need to ensure that Spring scans that location
I haven't tested, but it's likely one should be cautious with mixing the path for these migrations and the locations where Flyway scans for migrations
Current flyway 6.5.5 version is released and back from 6.0.0 I believe support for spring beans is provided.
You can directly autowire spring beans into your Java based migrations (using #autowired), But the hunch is your Migration class also should be managed by Spring to resolve dependency.
There is a cool and simple way for it, by overriding default behavior of Flyway, check out https://reflectoring.io/database-migration-spring-boot-flyway/
the article clearly answers your question with code snippets.
If you are using deltaspike you can use BeanProvider to get a reference to your DAO.
Change your DAO code:
public static UserDao getInstance() {
return BeanProvider.getContextualReference(UserDao.class, false, new DaoLiteral());
}
Then in your migration method:
UserDao userdao = UserDao.getInstance();
And there you've got your reference.
(referenced from: Flyway Migration with java)

Filter request URL before any processing in CQ5.6

In my CQ5.6 application,. as soon as the user hits a URL, I need to edit it using a certain parameters. All this must happen before Sling starts processing the URL.
I basically need to convert the URL like:
www.mysite.fr --> converts to --> /content/mysite/fr/
and so on....
I understand I'll need to create an OSGi bundle for this, but which API should I use to ensure that the URL is filtered by my class first and then catered by
Sling. ?
if you want a code-based solution for multiple websites (and you don't want to manage /etc/map) you can setup your own Filter:
package your.package;
import org.apache.felix.scr.annotations.*;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.RequestDispatcher;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.osgi.service.component.ComponentContext;
#Component(immediate=true, enabled=true)
#Service(value=Filter.class)
#Properties({
#Property(name="sling.filter.scope", value="REQUEST", propertyPrivate=true),
#Property(name="service.ranking", intValue=-10000, propertyPrivate=true)
})
public class YourFilter implements Filter {
private static final Logger log = LoggerFactory.getLogger(ProductSEOFilter.class);
#Activate
protected void activate(ComponentContext ctx) throws Exception {
}
#Deactivate
protected void deactivate() throws Exception {
}
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws java.io.IOException, ServletException {
String lang = "en";
// 1. get domain and path
// 2. check if your conditions are met
// 3. extract language from domain
// 4. internal redirect
RequestDispatcher dispatch = request.getRequestDispatcher("/content/mysite/" + lang);
dispatch.forward(request, response);
}
public void destroy() {
}
}
you don't need to bother checking for and passing querystrings--those are carried on in the dispatcher. it only needs a new url to forward to.
You can do this via Sling URL Mapping without the need for a filter. The simpliest way to achieve this is to create a node under the /etc/map directory with a resource type of sling:Mapping & called www.mysite.fr.
This then takes a property of sling:internalRedirect — if an incoming request matches the node name, this property is appended to the path to continue with internal resource resolution.
<map>
<http jcr:primaryType="sling:OrderedFolder">
<www.mysite.fr
jcr:primaryType="sling:Mapping"
sling:internalRedirect="/content/mysite/fr"/>
</http>
</map>
The above will ensure any request coming to www.mysite.fr is resolved to www.mysite.fr/content/mysite/fr.
You can also pattern matching based on regex properties rather than names & include port numbers or schemes too. The full documentation is available on the Sling website.

CQ5 - displaying a CQ.Notification in the frontend, when a workflow finished

I implemented workflows, but it would be nice to know if there are hooks provided by the client library which allow to hook in. When a workflow was triggered and finished, a CQ.Notification should be displayed. Or do i need to implement a polling library by myself?
As far as I know, there is no built-in CQ area to see when something is done, aside from looking here:
http://yoursite.com:port/libs/cq/workflow/content/console.html
Once there, you can go to the 'Instances' tab and see what's happening.
In one application that I worked on, we ended up writing our own method that sends notifications to us based on one of our workflows (our workflow ties into it - from the workflow models area, you can set your process to be a servlet that you've put into CQ). Here is the main piece of code from our servlet that catches the process being active, and then calls our methods to email us based on what it finds:
import com.day.cq.workflow.WorkflowException;
import com.day.cq.workflow.WorkflowSession;
import com.day.cq.workflow.exec.WorkItem;
import com.day.cq.workflow.exec.WorkflowData;
import com.day.cq.workflow.exec.WorkflowProcess;
import com.day.cq.workflow.metadata.MetaDataMap;
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.osgi.framework.Constants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import java.util.Arrays;
public class YourServletName implements WorkflowProcess {
#Override
public void execute(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap args) throws WorkflowException {
session = workflowSession.getSession();
final WorkflowData data = workItem.getWorkflowData();
String type = data.getPayloadType();
String[] argStrings = args.get("PROCESS_ARGS", ARG_UPDATED).split(",");
String reason = argStrings[0];
String baseUrl = argStrings[1];
try {
if (type.equals(TYPE_JCR_PATH) && data.getPayload() != null) {
String resourcePath = data.getPayload().toString();
logger.info("Send Notification that {} has been {}.", resourcePath, reason.toLowerCase());
if (resourcePath != null && !resourcePath.isEmpty()) {
ResourceInfo resourceInfo = new ResourceInfo(resourcePath, baseUrl);
sendEmail(resourceInfo, reason);
}
}
} catch (EmailException ex) {
logger.warn("Failed to send Email");
throw new WorkflowException(ex);
} catch (MailingException ex) {
logger.warn("Failed to send Email");
throw new WorkflowException(ex);
}
}
}
You can find more info in the documentation for Extending Workflow Functionality.
Look at the first code block on that page, and that will give you the best idea of how you can implement a custom workflow handler.
EDIT
If you want to see it on the front-end, you could do an AJAX call to get the JSON list of currently running workflows - you can hit this url:
http://localhost:4502/etc/workflow/instances.RUNNING.json
Then you could loop through them and see if yours is in there. This isn't very nice though, since they are all just listed by IDs. I would instead suggest using the querybuilder, or again, just doing an AJAX GET. This is one example:
1_group.0_path=/etc/workflow/instances
2_group.0_type=cq:Workflow
0_group.property.2_value=COMPLETED
0_group.property=status
0_group.property.and=true
3_group.property=modelId
3_group.property.2_value=/etc/workflow/models/your-model-name/jcr:content/model
3_group.property.and=true
Then the URL would look something like this:
http://yoursiteurl:port/libs/cq/search/content/querydebug.html?_charset_=UTF-8&query=http%3A%2F%2Fyoursiteurl%3Aport%3F%0D%0A1_group.0_path%3D%2Fetc%2Fworkflow%2Finstances%0D%0A2_group.0_type%3Dcq%3AWorkflow%0D%0A0_group.property.2_value%3DRUNNING%0D%0A0_group.property%3Dstatus%0D%0A0_group.property.and%3Dtrue%0D%0A3_group.property%3DmodelId%0D%0A3_group.property.2_value%3D%2Fetc%2Fworkflow%2Fmodels%2Fyour-model-name%2Fjcr%3Acontent%2Fmodel%0D%0A3_group.property.and%3Dtrue
It's ugly, but it gets you the results you need, and then you can parse them to get further information you need.