adobe experience manager, aem

Updating AEM content with Sling Pipes

You are still updating content manually? Try out Sling pipes.

Several weeks ago I saw some tweet how Adobe guys are using Sling pipes for updating their AEM content, so I was curious and I try it out.

Let's see what I have find out...

Sling pipes is simple tool for executing CRUD operations over resources in AEM repository. But is it powerful enough to replace your groovy scripts? Let's discover it in following examples.

Introduction

Just to get your attention, let's say in past you have made some bad decision during development of some of your component and now you need to update all content with that component. With Sling Pipes that is very easy.

Example

1
2
3
4
5
plumber.newPipe(resourceResolver)
  .echo("/content/yoursite")
  .$("nt:unstructured[sling:resourceType=projectName/components/content/imageSlider]")
  .write("autoplay", true)
  .run()

5 lines? Yes, you can set autoplay property to true in all your content with imageSlider component in 5 lines, and I'm pretty much sure you can't do that so simply with your groovy code, or you can do it manually in crx/de for whole day...

So what the hell is happening in this code, what are this methods?!

I will explain it later in more details, but in tl;dr;

  • plumber is OSGi service and to start of all this magic you need to pass resourceResolver object to newPipe method. After that you need to call pipes (methods) to achieve what you want.
  • echo("path") is used to receive root resource on specific path.
  • $(expression) is wrapper of Sling query expressions and I'm using it here to filter out specific resources with imageSlider component
  • write(conf) is pipe to write/update resources/properties, in this case I'm creating or updating autoplay property with value true
  • run() is used to execute code

Still interested? Let me explain you Sling pipes in more details.

About Sling Pipes

Sling pipes is a tool set for doing extract - transform - load operations by chaining proven code blocks. A sling pipe is essentially a sling resource stream, encapsulating a well-known sling operations.

There are 3 types of pipes:

  • reader pipes - used to get resources
  • writer pipes - for writing/updating resources in repository, depending on configuration and input
  • logical pipes - they refer to other pipes, chaining them or using their results in a general way

Reader pipes

Used to get resources.

Base pipe: echo(path)

Used to receive resource on given path

Let's say you want to get "/content/we-retail" resource.

1
2
3
plumber.newPipe(resourceResolver)
  .echo("/content/we-retail")
  .run()

XPathPipe: xpath(expr)

Used to receive resource with given xpath query

Let's say you want to get all nt:unstructured resources with heroimage component under "/content/we-retail".

1
2
3
plumber.newPipe(resourceResolver)
  .xpath("/jcr:root/content/we-retail//element(*, nt:unstructured)[(@sling:resourceType = 'weretail/components/content/heroimage')]")
  .run()

Sling Query Pipes

Wrapper for executing Sling Query methods in sling pipe way.

  • Find Pipe: $(expr)
  • Children Pipe: children(expr)
  • Siblings Pipe: siblings(expr)
  • Parent Pipe: parent()
  • Closest Pipe: closest(expr)

Here is more details about Sling Query

Same example as previous one, let's say you want to get all resources with heroimage component under "/content/we-retail".

1
2
3
4
plumber.newPipe(resourceResolver)
  .echo("/content/we-retail")
  .$("nt:unstructured[sling:resourceType=weretail/components/content/heroimage]")
  .run()

Writer pipes

Used to modify resources.

Write Pipe: write(conf)

Used to write given nodes and properties to current input resource

Let's say you want to create/update property "foo" with value "bar" and property "foo2" with value "bar2".

Please let me know is it possible to write different values except strings, like long, date, boolean?

1
2
3
4
plumber.newPipe(resourceResolver)
  .echo("/content/we-retail")
  .write('foo','bar','foo2','bar2')
  .run()

PathPipe: mkdir(expr)

Used to create resource on given location.

Let's you want to create resource "master" on "/content/we-retail/master" location. This will create "sling:Folder" node.

Please let me know if is it possible to specify different jcr:primaryType?

1
2
3
plumber.newPipe(resourceResolver)
  .mkdir("/content/we-retail/master")
  .run()

MovePipe: mv(expr)

Moves resource or property to target path

Following example will move master resource from "/content/we-retail/master" to "/content/we-retail/language-master".

1
2
3
4
plumber.newPipe(resourceResolver)
  .echo("/content/we-retail/master")
  .mv("/content/we-retail/language-master")
  .run()

RemovePipe: rm()

Removes resource or property on specific location.

Following example will remove master resource on given path "/content/we-retail/master".

1
2
3
4
plumber.newPipe(resourceResolver)
  .echo("/content/we-retail/master")
  .rm()
  .run()

PackagePipe: pkg(expr)

Creates a package and add current resource as a filter.

Following example will package all we retail content

1
2
3
4
plumber.newPipe(resourceResolver)
  .echo("/content/we-retail")
  .pkg("/etc/packages/we-retail-content.zip")
  .run()

How to use it in your AEM project

Okey, how to run all this in your AEM project? You can configure and execute a pipes with java, groovy console, http, or jmx

Dependencies

You need to add "org.apache.sling.query" & "org.apache.sling.pipes" as dependencies and you need to embedded them as part of your bundle or install it as separate bundles (my recommended way).

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
    <groupId>org.apache.sling</groupId>
    <artifactId>org.apache.sling.query</artifactId>
    <version>4.0.2</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>org.apache.sling</groupId>
    <artifactId>org.apache.sling.pipes</artifactId>
    <version>3.1.0</version>
    <scope>provided</scope>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<plugin>
  <groupId>org.apache.jackrabbit</groupId>
  <artifactId>filevault-package-maven-plugin</artifactId>
  <extensions>true</extensions>
  <configuration>
      <allowIndexDefinitions>true</allowIndexDefinitions>
      <packageType>mixed</packageType>
      <group>pipes</group>
      <embeddeds>
          <!--sling pipes related-->
          <embedded>
              <groupId>org.apache.sling</groupId>
              <artifactId>org.apache.sling.query</artifactId>
              <target>/apps/pipes-vendor-packages/application/install</target>
          </embedded>
          <embedded>
              <groupId>org.apache.sling</groupId>
              <artifactId>org.apache.sling.pipes</artifactId>
              <target>/apps/pipes-vendor-packages/application/install</target>
          </embedded>
      </embeddeds>
  </configuration>
</plugin>

Code execution

You can write in groovy console.

1
2
3
4
5
6
7
def plumber = getService("org.apache.sling.pipes.Plumber")

plumber.newPipe(resourceResolver)
  .echo("/content/yoursite")
  .$("nt:unstructured[sling:resourceType=projectName/components/content/imageSlider]")
  .write("autoplay", true)
  .run()

You can write in you Java class.

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
@Component(service = SlingPipesExamples.class, immediate = true)
public class SlingPipesExamples {

    private static final Logger logger = LoggerFactory.getLogger(SlingPipesExamples.class);

    private static final String SERVICE_USER = "sling-pipes-service-user";

    @Reference
    private ResourceResolverFactory resourceResolverFactory;

    @Reference
    private Plumber plumber;

    public void example() {
        try (final ResourceResolver resourceResolver = this.resourceResolverFactory.getServiceResourceResolver(this.getAuthenticationInfo())) {
            PipeBuilder pipeBuilder = this.plumber.newPipe(resourceResolver);
            // your logic with pipes
        } catch (final LoginException e) {
            logger.error("Exception during creating service resource resolver ", e);
        }
    }

    private Map<String, Object> getAuthenticationInfo() {
        return Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, SERVICE_USER);
    }

}

You can create resource on specific location e.g "/etc/sling-pipes/example" and execute it with HTTP request

1
2
3
4
5
6
<?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:description="Create directory pipe"
  jcr:primaryType="sling:OrderedFolder"
  sling:resourceType="slingPipes/mkdir"
  expr="/content/sling-pipes/foo"/>

Sum up

Basicly that's it what I discover so far. In general I like Sling Pipes, specialy how with few lines of code you can do a lot of stuff. Didn't try with some more complex examples but I guess it would be almost the same.

For more details take a look into offical documentation.

Let me know if for mkdir pipe is possible to specify different jcr:primaryType? If it's only "sling:Folder" then 👎

Also for write pipe, is it possible to write in property some value different then String?

I hope that you learn something and that you will give a try Sling Pipes. Let me know what you think and what are your experiences with Sling Pipes :)

You don't want a miss new blog post?

Sign up for the newsletter

© 2020 by devz