AEM Articles / Blogs / Perficient https://blogs.perficient.com/tag/aem/ Expert Digital Insights Thu, 19 Sep 2024 15:01:14 +0000 en-US hourly 1 https://blogs.perficient.com/files/favicon-194x194-1-150x150.png AEM Articles / Blogs / Perficient https://blogs.perficient.com/tag/aem/ 32 32 30508587 Create Content Fragment Variations in AEM With GenAI https://blogs.perficient.com/2024/09/24/create-content-fragment-variations-in-aem-with-genai/ https://blogs.perficient.com/2024/09/24/create-content-fragment-variations-in-aem-with-genai/#respond Tue, 24 Sep 2024 10:50:17 +0000 https://blogs.perficient.com/?p=369459

Earlier this year, Adobe introduced new generative AI capabilities in Adobe Experience Manager (AEM). As a Platinum partner of Adobe, Perficient has early adopter features provisioned for our environments. One of the more exciting and relevant features is the ability to use GenAI to generate variations within Content Fragments in AEM.

In this blog, we’ll talk about a sample use-case scenario, the steps involved with this new feature, and show how it can empower marketers and content authors who spend a lot of time in AEM and make their lives easier. 

The Scenario

In this sample use case, we have contributors who write content for a site called WKND Adventures. We’d like to create a contributor biography to enable an engaging experience for the end user. A biography will further enhance the user experience and increase the chance of content leading to a conversion, such as booking a vacation.  

How to Quickly Create a Content Fragment Variation 

1. Open a Content Fragment for Editing

After logging into AEM as a Cloud Service authoring environment, head over to a Content Fragment and open it up for editing.

Note: If you don’t see the new editor, try selecting the “Try New Editor” button to bring up the latest interface.

AEM as a Cloud Service Content Fragment Editing

As you can see, we still have the standard editing features such as associating images, making rich text edits, and publishing capabilities.

2. Generate Variations

Select the “Generate Variations” button on the top toolbar, and then a new window opens with the Generative Variations interface as seen in the image below.

AEM as a Cloud Service Generative Variations

What’s important to note here is that we are tied to the authoring environment in this interface. So, any variations that are generated will be brought back into our content fragment interface. Although a new prompt can be generated, we’ll start with the Cards option.

Note: There will be more prompt templates created after the writing of this blog.

3. Prompt Templates

The Cards option is pre-filled with some default helper text to provide guidance on a potential prompt and help fine-tune what’s being generated. Providing relevant and concise explanations to the user interaction will also improve the generated results. The interaction can also be explained. The generations can be further enhanced by providing Adobe Target, or a CSV file to further improve the generations. Providing a tone of voice also further defines the variations.

AEMaaCS Generative Variations Prompts

One of our favorite features is the ability to provide a URL for domain knowledge. In this case, we’re going to select a site from Rick Steves on winter escapes as seen in the image below.

AEMaaCS Generative Variations Prompt Url Domain Knowledge

After selecting the appropriate user interaction, tone, temperature intent, and number of variations, we select the “Generate” button.

4. Choose a Content Fragment Variation

Once the variations are created, we can review the results and then choose one to bring back into our Content Fragment editor.

AEMaaCS Generative Variations Selection

After selecting a variation and giving it a name, we can then export that variation. This will create a new variation of that content fragment in AEM.

AEM as a Cloud Service New Content Fragment

Although this is a simple example, many other prompt templates can be used to generate variations that can be used in AEM. Such as creating FAQs, a headline, hero banners, tiles, and more. Additional technical details can be found on Adobe’s GenAI page.

The Exciting Future of Content Creation

Having a direct integration to generate variations from an authoring environment will certainly speed up content creation and allow authors to create relevant and engaging content with the help of GenAI. We look forward to more features and improvements from Adobe in this exciting space, and helping customers adopt the technologies to effectively and safely create content to build exciting experiences.

]]>
https://blogs.perficient.com/2024/09/24/create-content-fragment-variations-in-aem-with-genai/feed/ 0 369459
Running AEM Author, Publisher, and Dispatcher Within Docker https://blogs.perficient.com/2024/09/18/running-aem-author-publisher-and-dispatcher-within-docker/ https://blogs.perficient.com/2024/09/18/running-aem-author-publisher-and-dispatcher-within-docker/#respond Wed, 18 Sep 2024 10:03:16 +0000 https://blogs.perficient.com/?p=369172

About eight years ago, I was introduced to Docker during a meetup at a restaurant with a coworker. He was so engrossed in discussing the Docker engine and containers that he barely touched the hors d’oeuvres. I was skeptical. 

I was familiar with Virtual Machines (VMs) and appreciated the convenience of setting up application servers without worrying about hardware. I wanted to know what advantages Docker could offer that VMs couldn’t. He explained that instead of virtualizing the entire computer, Docker only virtualizes the OS, making containers much slimmer than their VM counterparts. Each container shares the host OS kernel and often binaries and libraries. 

Curious, I wondered how AEM would perform inside Docker—a Java application running within the Java Virtual Machine, inside a Docker container, all on top of a desktop PC. I expected the performance to be terrible. Surprisingly, the performance was comparable to running AEM directly on my desktop PC. In hindsight, this should not have been surprising. The Docker container shared my desktop PC’s kernel, RAM, CPUs, storage, and network allowing the container to behave like a native application. 

I’ve been using Docker for my local AEM development ever since. I love how I can quickly spin up a new author, publish, or dispatch environment whenever I need it and just as easily tear it down. Switching to a new laptop or PC is a breeze — I don’t have to worry about installing the correct version of Java or other dependencies to get AEM up and running. 

In this blog, we’ll discuss running AEM author, publisher, and dispatcher within Docker and the setup process.

Setup Requirements

The AEM SDK, which includes the Quickstart JAR and Dispatcher tools, is necessary for this setup.  Additionally, Apache Maven must be installed. For the Graphical User Interface, we will use Rancher Desktop by SUSE, which operates on top of Docker’s command-line tools.  While the Docker engine itself is open source, Docker Desktop, the GUI distributed by Docker, is not. 

Step One: Installing Rancher Desktop

Download and Install Rancher Desktop by SUSE. Installing Racker Desktop will provide the Docker CLI (command line interface). If you wish to install the Docker CLI without Rancher Desktop, run the following command:

Windows

Install WinGet via the Microsoft store.

winget install --id=Docker.DockerCLI -e

Mac

Install Homebrew: 

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh) 

brew cask install docker

Step Two: Creating the “AEM in Docker” folder

Create a folder named “aem-in-docker”.  Unzip the contents of the AEM SDK into this folder.  Copy your AEM “license.properties” file to this directory. 

Step Three: Creating the subfolders to contain the Docker Image instructions

Make three subfolders within your “aem-in-docker” folder named “base”, “author”, and “publish”. 

Your “aem-in-docker” folder should look something like this: 

AEM-In-Docker Folder

Step Four: Creating the Base Docker Image instruction (Dockerfile)

Create a file named “Dockerfile” within the “base” subdirectory.

Ensure the file does not have an extension.  Set the contents of the file to the following:

FROM ubuntu
# Setting the working directory
WORKDIR /opt/aem
# Copy the license file
COPY license.properties .
# Copy Quickstart jar file
COPY aem-sdk-quickstart-2024.8.17465.20240813T175259Z-240800.jar cq-quickstart.jar
# Install Java, Vim, and Wget.  Install Dynamic Media dependencies.
RUN apt-get update && \
    apt-get install -y curl && \
    apt-get install -y software-properties-common && \
    add-apt-repository ppa:openjdk-r/ppa && \
    apt-get update && \
    apt-get install -y openjdk-11-jdk vim ca-certificates gnupg wget imagemagick ffmpeg fontconfig expat freetype2-demos
# Unack the Jar file
RUN java -jar cq-quickstart.jar -unpack
# Set the LD_LIBRARY_PATH environmental variable
ENV LD_LIBRARY_PATH=/usr/local/lib

This file directs Docker to build a new image using the official Ubuntu image as a base. It specifies the working directory, copies the license file and the quickstart file into the image (note that your quickstart file might have a different name), installs additional packages (like Java, Vim, Wget, and some Dynamic Media dependencies), unpacks the quickstart file, and sets some environment variables.

Step Five: Create the Base Docker Image

Run the following command from within the “aem-in-docker” folder.

docker build -f base/Dockerfile -t aem-base .

It should take a few minutes to run. After the command has been completed run:

docker image ls

You should see your newly created “aem-base” image.

AEM Base Image

Step Six: Creating the Author Docker Image instruction (Dockerfile)

Create a file named “Dockerfile” within the “author” subdirectory.

Set the contents of the file to the following:

# Use the previously created aem-base
FROM aem-base

# Expose AEM author in port 4502 and debug on port 5005
EXPOSE 4502
EXPOSE 5005
VOLUME ["/opt/aem/crx-quickstart/logs"]
# Make the container always start in Author mode with Port 4502.  Add additional switches to support JAVA 11: https://experienceleague.adobe.com/en/docs/experience-manager-65/content/implementing/deploying/deploying/custom-standalone-install.  Add the Dynamic Media runmode.
ENTRYPOINT ["java", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005", "-XX:+UseParallelGC", "--add-opens=java.desktop/com.sun.imageio.plugins.jpeg=ALL-UNNAMED", "--add-opens=java.base/sun.net.www.protocol.jrt=ALL-UNNAMED", "--add-opens=java.naming/javax.naming.spi=ALL-UNNAMED", "--add-opens=java.xml/com.sun.org.apache.xerces.internal.dom=ALL-UNNAMED", "--add-opens=java.base/java.lang=ALL-UNNAMED", "--add-opens=java.base/jdk.internal.loader=ALL-UNNAMED", "--add-opens=java.base/java.net=ALL-UNNAMED", "-Dnashorn.args=--no-deprecation-warning", "-jar", "cq-quickstart.jar", "-Dsling.run.modes=author,dynamicmedia_scene7", "-p", "4502", "-nointeractive"]

This file instructs Docker to create a new image based on the “aem-base” image. It makes ports 4502 and 5005 available (5005 for debugging purposes), sets up a mount point at “/opt/aem/crx-quickstart/logs”, and specifies the command to run when the image is executed.

Step Seven: Create the Author Docker Image

Run the following command from within the “aem-in-docker” folder.

docker build -f author/Dockerfile -t aem-author .

After the command has been completed run:

docker image ls

You should see your newly created “aem-author” image.

AEM Author Image

Step Eight: Creating the Publisher Docker Image instruction (Dockerfile)

Create a file named “Dockerfile” within the “publish” subdirectory.

Set the contents of the file to the following:

# Use the previously created aem-base
FROM aem-base
# Expose AEM publish in port 4503 and debug on port 5006
EXPOSE 4503
EXPOSE 5006
VOLUME ["/opt/aem/crx-quickstart/logs"]
# Make the container always start in Author mode with Port 4503.  Add additional switches to support JAVA 11: https://experienceleague.adobe.com/en/docs/experience-manager-65/content/implementing/deploying/deploying/custom-standalone-install.  Add the Dynamic Media runmode.
ENTRYPOINT ["java", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5006", "-XX:+UseParallelGC", "--add-opens=java.desktop/com.sun.imageio.plugins.jpeg=ALL-UNNAMED", "--add-opens=java.base/sun.net.www.protocol.jrt=ALL-UNNAMED", "--add-opens=java.naming/javax.naming.spi=ALL-UNNAMED", "--add-opens=java.xml/com.sun.org.apache.xerces.internal.dom=ALL-UNNAMED", "--add-opens=java.base/java.lang=ALL-UNNAMED", "--add-opens=java.base/jdk.internal.loader=ALL-UNNAMED", "--add-opens=java.base/java.net=ALL-UNNAMED", "-Dnashorn.args=--no-deprecation-warning", "-jar", "cq-quickstart.jar", "-Dsling.run.modes=publish,dynamicmedia_scene7", "-p", "4503", "-nointeractive"]

Step Nine: Create the Publisher Docker Image

Run the following command from within the “aem-in-docker” folder.

docker build -f publish/Dockerfile -t aem-publish .

After the command has been completed run:

docker image ls

You should see your newly created “aem-publish” image.

AEM Publish Image

Step Ten: Create the Adobe Network

Let’s set up a network to connect Docker containers and facilitate data sharing between them.

docker network create adobe

Step Eleven: Run the Author Docker Image

It’s time to run our Author Docker Image. First, create a local directory for the logs volume specified in the Dockerfile. Within the author subdirectory, create a directory named “logs.” Run the following command within the new logs folder:

Windows

docker run -d --name author -p 4502:4502 -p 5005:5005 --network adobe -v ${PWD}:/opt/aem/crx-quickstart/logs aem-author

macOS/Linux

docker run -d --name author -p 4502:4502 -p 5005:5005 --network adobe -v `pwd`:/opt/aem/crx-quickstart/logs aem-author

The command will return the ID of the new Docker container. It may take some time for the new AEM instance to start. To check its status, you can monitor the “error.log” file in the logs directory to check its status.

Windows

Get-Content -Path .\error.log -Wait

macOS/Linux

tail -f error.log

After AEM has finished starting up, check that everything is loading correctly by visiting: http://localhost:4502/aem/start.html.

Let’s stop the AEM container for the time being:

docker stop author

Step Twelve: Run the Publisher Docker Image

It’s time to run our Publisher Docker Image.  First, create a local directory for the logs volume specified in the Dockerfile. Within the publish subdirectory, create a directory named “logs.”  Run the following command within the new logs folder:

Windows

docker run -d --name publish -p 4503:4503 -p 5006:5006 --network adobe -v ${PWD}:/opt/aem/crx-quickstart/logs aem-publish

macOS/Linux

docker run -d --name publish -p 4503:4503 -p 5006:5006 --network adobe -v `pwd`:/opt/aem/crx-quickstart/logs aem-publish

The command will return the ID of the new Docker container. It may take some time for the new AEM instance to start. To check its status, you can monitor the “error.log” file in the logs directory to check its status.

Windows

Get-Content -Path .\error.log -Wait

macOS/Linux

tail -f error.log

After AEM has finished starting up, check that everything is loading correctly by visiting: http://localhost:4503/content.html.  You will see a “Not Found” page.  That is fine for now.

Let’s stop the AEM container for the time being:

docker stop publish

Step Thirteen: Start the Containers via Rancher Desktop

Open Rancher Desktop and go to the Containers tab in the left navigation pane. To start individual containers, check the box in the State column for each container you want to start, then click the Start button. To start all containers at once, check the box in the header row of the State column, and then click the Start button. Let’s go ahead and start all containers.

If you prefer using the command line, you can run:

docker start author
docker start publish

Containers Via Rancher Desktop

Step Fourteen: Create an AEM Project and install it on the Author and Publish instance

Since Docker’s mascot is a whale, I thought it would be fun to name our new AEM project after a famous fictional whale: Monstro from Pinocchio.

Run the following command from a command line (Note: you may have to run this command with elevated privileges):

mvn -B archetype:generate -D archetypeGroupId=com.adobe.aem -D archetypeArtifactId=aem-project-archetype -D archetypeVersion=50 -D aemVersion=cloud -D appTitle="Monstro" -D appId="monstro" -D groupId="com.monstro" -D frontendModule=general -D includeExamples=n

Once this project has been created, let us build and deploy it to our Author instance.

Run the following command from within the “Monstro” project:

mvn clean install -PautoInstallSinglePackage

Check that the project is installed by visiting the following URL to view the results: http://localhost:4502/editor.html/content/monstro/us/en.html.  You should see the following:

Project Monstro

Now, let us build and deploy the project to our Publish instance.

Run the following command from within the “Monstro” project:

mvn clean install -PautoInstallSinglePackagePublish

Verify that the project is installed by visiting this URL: http://localhost:4503/content/monstro/us/en.html.  Installation may take up to five minutes. After this period, you should see the following:

Post Installation Project Monstro

Step Fifteen: Set up the Publish Agent on Author

It’s time to configure the publish agent on our author instance. Go to this URL: http://localhost:4502/etc/replication/agents.author/publish.html.

Click the “Edit” button (next to settings).

Publish Agent On Author Setup

  • Click the checkbox next to “Enabled”
  • Enter “admin” in the “Agent User Id” field
  • Navigate to the Transport tab and enter the following in the URI field:  http://publish:4503/bin/receive?sling:authRequestLogin=1
  • Instead of using “localhost,” the hostname for our publish instance is our container’s name, “publish”
  • In the “username” field, enter “admin,” and in the “password” field, enter the admin’s password
  • Click the “OK” button to save the Agent settings
  • Click the “Test Connection” link, and the replication test should be successful

Step Sixteen: Publish content from the Author

Go back to http://localhost:4502/editor.html/content/monstro/us/en.html. Edit the “Hello, World” component by changing the text from “lalala :)” to “Monstro is the enormous, fearsome whale from Disney’s 1940 animated film Pinocchio.” Verify the update and publish the page. Then, check http://localhost:4503/content/monstro/us/en.html to see your changes on the Publisher as well.

Step Seventeen: Create the Dispatcher Container

Make sure the publisher instance is running before proceeding. Extract the AEM SDK Dispatcher tools.

Windows

Expand-Archive .\aem-sdk-dispatcher-tools-2.0.222-windows.zip
Rename-Item -Path .\aem-sdk-dispatcher-tools-2.0.222-windows -NewName dispatcher-sdk-2.0.222

macOS/Linux

chmod +x ./aem-sdk-dispatcher-tools-2.0.222-unix.sh
./aem-sdk-dispatcher-tools-2.0.222-unix.sh

Since we’ve set up a custom network for our AEM containers, the docker run script won’t function correctly because it doesn’t recognize this network. Let’s modify the docker run script.

Windows

Open “dispatcher-sdk-2.0.222\bin\docker_run.cmd” in your favorite editor.

Add the “–network adobe” argument to the docker command inside the “else” statement.

Modify Docker Run Script For Windows

macOS/Linux

Open “dispatcher-sdk-2.0.222/bin/docker_run.sh” in your favorite editor.

Add the “–network adobe” argument to the docker command inside the “else” statement.

Modify Docker Run Script For macOS/Linux

Execute the docker run script with the following parameters. Be sure to replace the dispatcher source path with the path to your “monstro” source.

Windows

.\ dispatcher-sdk-2.0.222\bin\docker_run.cmd C:\Users\shann\Sites\monstro\dispatcher\src publish:4503 8080

macOS/Linux

./dispatcher-sdk-2.0.222/bin/docker_run.sh ~/Sites/monstro/dispatcher/src publish:4503 8080

Once the text stream in your terminal has stopped, go to http://localhost:8080/.  You should see the following:

Dispatch Container For Project Monstro

Open Rancher Desktop and navigate to the Containers tab. Locate the container with an unusual name. If you stop this container, it won’t be possible to start it again. Please go ahead and stop this container. The dispatcher code running in your terminal will also terminate. We want this container to be more permanent, so let’s make some additional changes to the docker run script.

Creating A Permanent Container For Project Monstro

Windows

Open “dispatcher-sdk-2.0.222\bin\docker_run.cmd” in your favorite editor.

macOS/Linux

Open “dispatcher-sdk-2.0.222/bin/docker_run.sh” in your favorite editor.

Add the “–name dispatcher” argument to the “docker” command within the “else” statement. Also, remove the “–rm” switch. According to Docker documentation, the “–rm” switch automatically removes the container and its associated anonymous volumes when it exits, which is not what we want.

Windows

Modify Docker Run Script For Windows 2

macOS/Linux

Modify Docker Run Script For Macos Linux 2

Run the docker run command in your terminal again:

Windows

.\ dispatcher-sdk-2.0.222\bin\docker_run.cmd C:\Users\shann\Sites\monstro\dispatcher\src publish:4503 8080

macOS/Linux

./dispatcher-sdk-2.0.222/bin/docker_run.sh ~/Sites/monstro/dispatcher/src publish:4503 8080

Open Rancher Desktop and go to the Containers tab. You should see a container named “dispatcher.” Stop this container. The dispatcher code running in your terminal will terminate, but the container will remain in Rancher Desktop. You can now stop and restart this container as many times as you’d like. You can also start and stop the dispatcher via the command line:

docker start dispatcher
docker stop dispatcher

Docker Provides Value and Flexibility

We have an author and publisher AEM instance running inside a Docker container. Additionally, we have a dispatcher container created using the source from the Monstro project. Although this dispatcher container isn’t very useful, the advantage of Docker is that you can easily delete and create new containers as needed.

I hope you found this blog helpful. I’ve been using Docker on my local machine for the past eight years and value the flexibility it provides. I can’t imagine going back to managing a local AEM instance or dealing with Apache configurations to get the dispatcher working. Those days are behind me.

]]>
https://blogs.perficient.com/2024/09/18/running-aem-author-publisher-and-dispatcher-within-docker/feed/ 0 369172
AEM OSGi Services: Take Advantage of Strategy Design Pattern https://blogs.perficient.com/2024/09/09/aem-osgi-services-take-advantage-of-strategy-design-pattern/ https://blogs.perficient.com/2024/09/09/aem-osgi-services-take-advantage-of-strategy-design-pattern/#respond Mon, 09 Sep 2024 10:00:41 +0000 https://blogs.perficient.com/?p=368773

Design patterns are pivotal in crafting application solutions that are both maintainable and scalable. The Strategy Design Pattern is ideal for scenarios that require runtime selection among various available algorithms. In this blog, we’ll cover how to implement the Strategy Design Pattern in an AEM OSGi service, boosting your code’s flexibility and manageability. 

What is the Strategy Design Pattern?

The Strategy Design Pattern comes under the category of behavioral design pattern which defines a family of algorithms, encapsulates each one, and makes them interchangeable. This pattern allows the algorithm to vary independently from the clients that use it. It’s particularly useful for scenarios where multiple methods can be applied to achieve a specific goal, and the method to be used can be selected at runtime. 

You can read more about Strategy Pattern in general here. 

UML Diagram of the Strategy Design Pattern

Here’s a UML diagram that illustrates the Strategy Design Pattern: 

UML Diagram of The Strategy Design Pattern

  • Strategy: The interface that defines the common behavior for all strategies.
  •  ConreteImplementation1, ConreteImplementation2, ConreteImplementation3: Concrete implementations of the Strategy interface.
  •  Context: Maintains a reference to a Strategy object and uses it to execute the strategy.

Are There Use Cases in AEM

So, is there any scenario where we can use it in AEM? Yes! There are many AEM solution use cases where we have multiple strategies and the proper strategy is chosen at run time. We generally use conditional statements and switch statements in such scenarios. Here are a few examples where Strategy Pattern can be applied. 

  • Template Specific Behaviors: Different strategies for a component when it has different behavior according to different templates. 
  • Content Transformation: Different strategies for transforming HTML, JSON, or XML content. 
  • Brand-specific behaviors:  Different strategies for individual brand sites in multitenant environments when they share certain common code base and scenarios. 
  • Channel Specific implementations: Different channels for sending notifications such as email, and web. 

Benefits of Using the Strategy Design Pattern

Implementing the Strategy Design Pattern in AEM OSGi services offers numerous benefits, aligning well with SOLID principles and other best practices in software development. 

Six Benefits 

  1. Adaptability and Scalability: The Strategy Design Pattern facilitates the addition of new algorithms without altering the existing code, enhancing the system’s extensibility. When a new algorithm is required, it can be seamlessly integrated by creating a new strategy class, thereby leaving the context and other strategies unchanged. 
  2. Adherence to SOLID Principles: The Strategy Design Pattern aligns with SOLID principles by ensuring each strategy class adheres to the Single Responsibility Principle (SRP), encapsulating a specific algorithm for easier comprehension, testing, and maintenance. It supports the Open/Closed Principle (OCP) by allowing new strategies to be added without modifying existing code. The pattern ensures compliance with the Liskov Substitution Principle (LSP), enabling interchangeable use of strategy implementations without altering functionality. It adheres to the Interface Segregation Principle (ISP) by having clients interact with the strategy interface, thus reducing unnecessary dependencies. Lastly, it upholds the Dependency Inversion Principle (DIP) by making the context depend on abstractions rather than concrete implementations, promoting flexible and decoupled code. 
  3. Ease of Testing: Since each strategy is a separate implementation and does not directly depend on other implementations, it can be unit-tested independently of other strategies and the context. This leads to more manageable and comprehensible tests. 
  4. Code Reusability: If needed Strategies can be reused across different parts of the application or even in different projects. This reduces redundancy and encourages the reuse of well-tested code. 
  5. Maintainability: The clear separation of different algorithms into their own classes means that any changes or additions to the algorithms do not impact the rest of the codebase. This makes the system more maintainable. 
  6. Better Code Readability: By encapsulating the logic of each algorithm in its own implementation class, the Strategy Design Pattern makes the code more readable. Developers can quickly understand the purpose of each strategy by looking at the strategy interface and its implementations. 

Typical Strategy Implementation for AEM 

In AEM, we can utilize regular classes for strategy implementation, but with OSGi, we get a better experience of handling strategies. A typical implementation of the Strategy Design Pattern in AEM will consist of: 

  • Strategy Context: Holds all the strategies for the client. 
  • Strategy Interface: Provides a generic interface for different strategies. 
  • OSGi Implementations for Strategies: Contains the logic for individual algorithms for each strategy. 

Implementation Steps

Step 1: Define the Strategy Interface 

First, create a Java interface that defines the common behavior for all strategies. 

public interface StrategyService { 
 
 
    /** 
     * Name of the strategy. 
     * @return Strategy name. 
     */ 
    String getName(); 
 
 
    /** 
     * Executes the strategy based on criteria. 
     * @param strategyBean Dummy Strategy bean object consists of the fields required for strategy execution. 
     * @return Strategy Result bean. You can replace it with result object according to your need. 
     */ 
    StrategyResult execute(StrategyBean strategyBean); 
 
 
    /** 
     * Method for executing isFit operation if given strategy is fit for scenario. 
     * @param strategyBean Strategy Object consisting of params needed for strategy is fit operation. 
     * @return true if given strategy fits the criteria. 
     */ 
    boolean isFit(StrategyBean strategyBean); 
}

Comments are self-explanatory here but let’s still deep dive into what each method intends to do. 

String getName() 

Name of the strategy. The name should be self-explanatory. So, when someone sneaks into the code, they should come to know for what scenario a strategy is aiming for. 

StrategyResult execute(StrategyBean strategyBean);

This method executes the logic for the operation required at runtime. StrategyResult Bean represented here is just for reference. You can update the same according to your operation. 

StrategyBean consists of params needed for executing the operation and again this is also just for reference you can execute the same with your own object. 

boolean isFit(StrategyBean strategyBean); 

This method is where decisions will be made if the given strategy is fit for the scenario or not. StrategyBean will provide the necessary parameters to execute the operation. 

Step 2: Implement Concrete Strategies

Create multiple implementations of the StrategyService interface. Each class will implement the methods differently. 

Here for reference, let’s create two concrete implementations for StrategyService namely FirstStrategyServiceImpl and SecondStrategyServiceImpl 

@Component(service = StrategyService.class, immediate = true, property = { 
        "strategy.type=FirstStrategy" 
}) 
@Slf4j 
public class FirstStrategyServiceImpl implements StrategyService { 
 
    @Override 
    public String getName() { 
        return "FirstStrategy"; 
    } 
 
    @Override 
    public StrategyResult execute(StrategyBean strategyBean) { 
        log.info("Executing First Strategy Service Implementation...."); 
        //Implement Logic 
        return new StrategyResult(); 
 
    } 
 
    @Override 
    public boolean isFit(StrategyBean strategyBean) { 
        //Implement Logic 
        return new Random().nextBoolean(); 
    } 
 
}

And our Second Service implementation here will look like this:

@Component(service = StrategyService.class, immediate = true, property = { 
        "strategy.type=SecondStrategy" 
}) 
@Slf4j 
public class SecondStrategyServiceImpl implements StrategyService { 
 
    @Override 
    public String getName() { 
        return "SecondStrategy"; 
    } 
 
    @Override 
    public StrategyResult execute(StrategyBean strategyBean) { 
        log.info("Executing Second Strategy Service Implementation...."); 
        //Implement Logic 
        return new StrategyResult(); 
    } 
 
    @Override 
    public boolean isFit(StrategyBean strategyBean) { 
        //Implement Logic 
        return new Random().nextBoolean(); 
    } 
}

Here, we do not have any logic. We’re just giving dummy implementations. You can add your operational logic in execute and isFit method. 

Step 3: Implement Context Service Class 

Our context service implementation will hold all the strategies with dynamic binding and will look like this. 

 
@Component(service = StrategyContextService.class, immediate = true) 
@Slf4j 
public class StrategyContextServiceImpl implements StrategyContextService { 
 
    private final Map<String, StrategyService> strategies = new HashMap<>(); 
 
    @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) 
    protected void bindStrategy(final StrategyService strategy) { 
        strategies.put(strategy.getName(), strategy); 
    } 
 
    protected void unbindStrategy(final StrategyService strategy) { 
        strategies.remove(strategy.getName()); 
    } 
 
 
    public Map<String, StrategyService> getStrategies() { 
        return strategies; 
    } 
 
    @Override 
    public StrategyService getApplicableStrategy(final StrategyBean strategyBean) { 
        return strategies 
                .values() 
                .stream() 
                .filter(strategyService -> strategyService.isFit(strategyBean)) 
                .findFirst() 
                .orElse(null); 
 
    } 
 
    @Override 
    public StrategyResult executeApplicableStrategy(final StrategyBean strategyBean) { 
        final var strategyService = getApplicableStrategy(strategyBean); 
        if (strategyService != null) { 
            return strategyService.execute(strategyBean); 
        } else { 
            // Run Default Strategy Or Update Logic Accordingly 
            return strategies.get("default").execute(strategyBean); 
        } 
    } 
 
    @Override 
    public Collection<StrategyService> getAvailableStrategies() { 
        return strategies.values(); 
    } 
 
    @Override 
    public Map<String, StrategyService> getAvailableStrategiesMap() { 
        return strategies; 
    } 
}

Here you can see we are dynamically binding instances of StrategyService implementations. So, we don’t need to worry about change in this context class whenever new Strategy is implemented. The context here will dynamically bind the same and update available strategies. 

We are providing methods here to get the strategy, execute the strategies, and get all the available strategies. 

The advantage we have is we don’t need to worry about changes needed for additional strategies. When a new strategy is needed, we just need its implementation class and don’t need to worry about its impact on others. If isFit logic is not causing conflict with others we don’t need to worry about other services. This isolation gives us confidence for complex solutions as we have the flexibility here to not worry about the impact of newly added functionalities. 

Step 4: Using this implementation 

Since this is OSGi implementation you can use this in any OSGi Services, Servlet, Workflows, Schedulers, Sling Jobs, Sling Models, etc. 

You can either use this directly or you can create a wrapper service around this if any further post-processing of strategy results is needed. 

Our example consumer service and sling model will look like this: 

@Component(service = StrategyConsumer.class, immediate = true) 
public class StrategyConsumer { 
 
    @Reference 
    private StrategyContextService strategyContext; 
 
    public StrategyResult performTask() { 
        final StrategyBean strategyBean = new StrategyBean(); 
        //Set strategy object based on data 
 
        StrategyResult strategyResult = strategyContext.executeApplicableStrategy(strategyBean); 
        //Do Post Processing 
        return  strategyResult; 
    } 
 
 
    public StrategyResult performTask(final StrategyBean strategyBean) { 
        StrategyResult strategyResult = strategyContext.executeApplicableStrategy(strategyBean); 
        //Do Post Processing 
        return  strategyResult; 
    } 
}

The method can be updated to accommodate logical operation for the addition of data into strategy bean if needed. 

Sample sling model which will use consumer service or directly connect with context service will look like this: 

@Model(adaptables = SlingHttpServletRequest.class) 
public class StrategyConsumerModel { 
    @OSGiService 
    private StrategyConsumer strategyConsumer; 
 
    @PostConstruct 
    public void init(){ 
        StrategyResult strategyResult = strategyConsumer.performTask(); 
        //do processing if needed 
 
        // OR 
        StrategyBean strategyBean = new StrategyBean(); 
        strategyResult = strategyConsumer.performTask(strategyBean); 
 
    } 
}

And you are all set now.  

This was just a dummy implementation, but you can twist it according to your business needs. 

In the introduction, use cases have already been mentioned. You can use this in implementing such scenarios. Additional use cases can be any task where you need to select implementation dynamically at run time based on scenarios.  

Final Thoughts

With the above implementation, we can see that The Strategy Design pattern is well suited for managing and extending complex algorithms in a clean and modular way which also adheres to best practices including SOLID principles. It promotes flexibility, scalability, maintainability, and ease of  testing. As you develop AEM solutions consider incorporating this, it will significantly improve code quality and reusability ensuring that evolving business requirements can be accommodated easily. 

 

Learn more about AEM developer tips and tricks by reading our Adobe blog!

]]>
https://blogs.perficient.com/2024/09/09/aem-osgi-services-take-advantage-of-strategy-design-pattern/feed/ 0 368773
Mastering Page Properties With Granite Render Conditions and Context-Aware Configuration https://blogs.perficient.com/2024/07/29/mastering-page-properties-with-granite-render-conditions-and-context-aware-configuration/ https://blogs.perficient.com/2024/07/29/mastering-page-properties-with-granite-render-conditions-and-context-aware-configuration/#respond Mon, 29 Jul 2024 11:10:02 +0000 https://blogs.perficient.com/?p=366394

From Static to Dynamic: The Evolution of Template Management

Do you remember the days of static templates? We had a plethora of templates, each with its own page components and CQ dialogs. It was a maintenance nightmare! 

But then came editable templates, and everything changed. With this new approach, we can define a single-page component and create multiple templates from it. Sounds like a dream come true, right? 

But there’s a catch. What if we need different dialogs for different templates? Do we really need to create separate template types for each one? That would mean maintaining multiple template types and trying to keep track of which template uses which type. Not exactly the most efficient use of our time. 

Managing Page Properties in AEM

In this post, we’ll explore the challenges of template management and how we can overcome them using Granite render conditions and context-aware configurations. 

When managing page properties, we’re often faced with a dilemma. While context-aware configurations are ideal for setting up configurations at the domain or language level, they fall short when it comes to managing individual pages. 

The usual go-to solution is to update the Page Properties dialog, but this approach has its own set of limitations. So, what’s a developer to do? 

Fortunately, there’s a solution that combines the power of Granite render conditions with the flexibility of context-aware configurations. 

What is Granite Render Condition? 

Render condition is just conditional logic to render a specific section of the component UI. If you want a more detailed description, you can read Adobe’s official documentation

A Real-World Use Case Using Both Granite Render Condition and Context-Aware Configuration

Say we want to display and hide the page properties tab based on the template name, which can be configured using context-aware configuration without any hardcoded code.   

First, we’d need to build the CAC which will contain fields for adding the template names and tab path to show.   

We will create a service for context-aware configuration which will read config and provide the mapping. 

public interface PageTabsMappingService { 
    List<PageTabsMappingConfig> getPageTabsMappingConfigList(); 
     
}

Here PageTabsMappingConfig is just a POJO bean class that consists of a page tab path and template path. 

@Data 
public class PageTabsMappingConfig { 
    private String templatePath; 
    private String tabPath; 
}

Now let’s create a context-aware configuration implementation class, which will consist of a template path and tabs path configuration ability. 

We want this to be more author-friendly, so we will be using a custom data source. This data source can be found in this blog post

For this example, we need two data sources, one for template path and one for tab paths.  

 So finally, our configuration will look like this:

@Configuration(label = "Page Tabs Mapping Configuration", description = "Page Tabs Mapping Config", property = 
        {EditorProperties.PROPERTY_CATEGORY + "=TemplateAndTabs"}, collection = true) 
public @interface PageTabsMappingConfiguration { 
 
    @Property(label = "Select Template To Be Mapped", description = "Select Template Name To Be Mapped", property = { 
            "widgetType=dropdown", 
            "dropdownOptionsProvider=templateDataSource" 
    },order = 1) 
    String getTemplatePath(); 
 
 
    @Property(label = "Select Tab to be mapped", description = "Select Tab to be mapped", property = { 
            "widgetType=dropdown", 
            "dropdownOptionsProvider=tabDataSource" 
    },order = 2) 
    String getTabPath(); 
     
 
}

Now let’s implement Service to read this config. 

public interface PageTabsMappingService { 
    List<PageTabsMappingConfig> getPageTabsMappingConfigList(Resource resource); 
 
}
@Component(service = PageTabsMappingService.class, 
        immediate = true) 
@ServiceDescription("Implementation For PageTabsMappingService ") 
@Slf4j 
public class PageTabsMappingServiceImpl implements PageTabsMappingService { 
 
    @Override 
    public List<PageTabsMappingConfig> getPageTabsMappingConfigList(final Resource resource) { 
 
        final ConfigurationBuilder configurationBuilder = Optional.ofNullable(resource) 
                .map(resource1 -> resource1.adaptTo(ConfigurationBuilder.class)) 
                .orElse(null); 
        return new ArrayList<>(Optional 
                .ofNullable(configurationBuilder) 
                .map(builder -> builder 
                        .name(PageTabsMappingConfiguration.class.getName()) 
                        .asCollection(PageTabsMappingConfiguration.class)) 
                .orElse(new ArrayList<>())) 
                .stream().map(pageTabsMappingConfiguration ->new PageTabsMappingConfig(pageTabsMappingConfiguration.getTabPath(),pageTabsMappingConfiguration.getTemplatePath())) 
                .collect(Collectors.toList()); 
    } 
}

In the above code, we are reading context-aware configuration and providing the list for further use. 

Now let us create render condition to show and hide tabs in page properties which will utilize the CAC mapping configuration. 

We will be using the Sling Model for the same. This will be invoked whenever Page properties tabs are opened, in page editor mode, creation wizard, or on sites wizard. 

@Model(adaptables = SlingHttpServletRequest.class) 
public class TabsRenderConditionModel { 
 
    @Self 
    private SlingHttpServletRequest request; 
 
    @OSGiService 
    private PageTabsMappingService pageTabsMappingService; 
 
    /** 
     * This is to set render condition for tabs. 
     */ 
    @PostConstruct 
    public void init() { 
 
        final var resource = request.getResource() 
                .getResourceResolver().getResource("/content"); 
        //We are considering root level site config since this will be global. 
        //For multitenant environment you can add additional OSGI Config and use the path accordingly 
        final List<PageTabsMappingConfig> tabRenderConfig = 
                pageTabsMappingService.getPageTabsMappingConfigList(resource); 
        final var name = Optional.ofNullable(request.getResource().getParent()) 
                .map(Resource::getName).orElse(StringUtils.EMPTY); 
        final var props = (ValueMap) request.getAttribute("granite.ui.form.values"); 
        final var template = Optional.ofNullable(props) 
                .map(props1 -> props1.get("cq:template", String.class)) 
                .orElse(StringUtils.EMPTY); 
 
        final var renderFlag = tabRenderConfig.stream() 
                .anyMatch(tabConfig -> 
                        BooleanUtils.and(new Boolean[]{StringUtils.equals(name, tabConfig.getTabName()), 
                                StringUtils.equals(template, tabConfig.getTemplatePath())})); 
 
        request.setAttribute(RenderCondition.class.getName(), 
                new SimpleRenderCondition(renderFlag)); 
    } 
 
 
}

After reading template we simply check if this given tab name mapping exists or not. Based on that, using the simple render condition we are setting a flag for showing and hiding the tab. 

Now it is time to use this Sling model in the actual render condition script file. In our project directory let’s assume /apps/my-project/render-conditions/tabs-renderconditions 

Create tabs-renderconditions.html  

And add content as: 

<sly data-sly-use.tab="com.mybrand.demo.models.TabsRenderConditionModel" />

Build a customs tabs under the base page template folder as follows: 

/apps/my-project/components/structure/page/base-page/tabs 

-landing-page-tab 

-home-page-tab 

-country-page-tab 

-state-page-tab  

-hero-page-tab

And our cq:dialog will be referring the same as this:

<?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"> 
    <content jcr:primaryType="nt:unstructured"> 
        <items jcr:primaryType="nt:unstructured"> 
            <tabs jcr:primaryType="nt:unstructured"> 
                <items jcr:primaryType="nt:unstructured"> 
 
                    <additionalHeroPage 
                            jcr:primaryType="nt:unstructured" 
                            sling:resourceType="granite/ui/components/foundation/include" 
                            path="/mnt/override/apps/my-project/components/structure/page/tabs/additional-hero-page"/> 
                    <additionalStatePage 
                            jcr:primaryType="nt:unstructured" 
                            sling:resourceType="granite/ui/components/foundation/include" 
                            path="/mnt/override/apps/my-project/components/structure/page/tabs/additionalstatepage"/> 
                </items> 
            </tabs> 
        </items> 
    </content> 
</jcr:root>

And our sample tab with render condition config will looks like this: 

<additionalHeroPage 
        cq:showOnCreate="{Boolean}true" 
        jcr:primaryType="nt:unstructured" 
        jcr:title="Additional Hero Page Setting" 
        sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns"> 
    <items jcr:primaryType="nt:unstructured"> 
        <column 
                jcr:primaryType="nt:unstructured" 
                sling:resourceType="granite/ui/components/coral/foundation/container"> 
            <items jcr:primaryType="nt:unstructured"> 
                <section1 
                        jcr:primaryType="nt:unstructured" 
                        jcr:title="Settings" 
                        sling:resourceType="granite/ui/components/coral/foundation/form/fieldset"> 
                    <items jcr:primaryType="nt:unstructured"> 
                        <testProperty 
                                cq:showOnCreate="{Boolean}true" 
                                jcr:primaryType="nt:unstructured" 
                                sling:resourceType="granite/ui/components/coral/foundation/form/textfield" 
                                fieldDescription="Test Property" 
                                fieldLabel="Test Property" 
                                name="./testProp" 
                                required="{Boolean}true"> 
                                <granite:data 
                                    jcr:primaryType="nt:unstructured" 
                                    cq-msm-lockable="./testProp"/> 
                         </testProperty> 
                    </items> 
                </section1> 
            </items> 
        </column> 
    </items> 
    <granite:rendercondition 
            jcr:primaryType="nt:unstructured" 
            sling:resourceType="my-project/render-conditions/tabs-renderconditions"/> 
</additionalHomePage>

In the below Template and Tabs CAC configuration, the “Additional Home Page Setting” tab will be displayed in page properties when an author is opening a page created using the hero-page template. 

AEM Handling Page Properties Tabs Render Config Window

Finally, when you open any page made with a configured template, like the Hero page in the image below, you can see the tabs configured for it. 

AEM Handling Page Properties Additional Hero Page Settings Window

More Helpful AEM Tips and Tricks

I hope you have a better understanding of how to overcome some of the challenges of managing templates in AEM.

For more AEM tips and tricks, keep up with us on our Adobe blog! 

]]>
https://blogs.perficient.com/2024/07/29/mastering-page-properties-with-granite-render-conditions-and-context-aware-configuration/feed/ 0 366394
How to Track Git Build Info in Your AEM Multimodule Maven Project https://blogs.perficient.com/2024/07/25/how-to-track-git-build-info-in-your-aem-multimodule-maven-project/ https://blogs.perficient.com/2024/07/25/how-to-track-git-build-info-in-your-aem-multimodule-maven-project/#comments Thu, 25 Jul 2024 11:00:29 +0000 https://blogs.perficient.com/?p=366389

When troubleshooting issues in Adobe Experience Manager (AEM), the first step is often to identify which code version is deployed for the affected projects. However, OSGi bundle versions only provide a partial picture, lacking crucial details like the exact branch used. This becomes especially problematic when managing multiple tenants in the same environment or comparing code across different environments, which requires tedious navigation through CI/CD tool histories like Jenkins. 

The Solution: Unlocking Git Build Transparency

To streamline development workflows and issue resolution, it’s essential to implement a solution that clearly displays vital Git information, including branch names and commit IDs. By accurately tracking and recording this data, teams like Dev/QA can verify code integrity before starting their tasks, ensuring a smoother and more efficient experience. 

Power of git-commit-id plugin

The purpose of the git-commit-id plugin is to generate a property file containing Git build information every time a Maven build is executed. This file can then be utilized in our backend systems and exposed for tracking purposes.  

Now, let’s delve into the steps for implementing and exposing this build information. 

Integration of git-commit-id Plugin in POM.xml

We begin by adding and configuring the git-commit-id plugin in our project’s POM.xml file. This plugin will be responsible for generating the necessary property file containing Git build details during the Maven build process. 

Development of Custom OSGi Service

Next, create a custom OSGi service within our Adobe Experience Manager (AEM) project. This service is designed to read the generated property file and extract the relevant build information. By doing so, we ensure that this data is readily available within our AEM environment. 

Implementation of Sling Servlet

To expose the build information retrieved by the OSGi service, we developed a Sling Servlet. This servlet leverages the OSGi service to access the Git build details and then makes this information accessible through a designated endpoint. Through this endpoint, other components of our AEM project or external systems can easily access and utilize the build information as needed. 

Let’s start with adding POM changes for the plugin. 

Under core bundle POM in the plugin sections of build add an entry for git-commit-id plugin. 

<plugin> 
    <groupId>pl.project13.maven</groupId> 
    <artifactId>git-commit-id-plugin</artifactId> 
    <version>2.2.4</version> 
    <executions> 
        <execution> 
            <id>get-the-git-infos</id> 
            <goals> 
                <goal>revision</goal> 
            </goals> 
        </execution> 
 
    </executions> 
    <configuration> 
        <dotGitDirectory>${project.basedir}/.git</dotGitDirectory> 
        <prefix>git</prefix> 
        <verbose>false</verbose> 
        <generateGitPropertiesFile>true</generateGitPropertiesFile> 
        <generateGitPropertiesFilename>src/main/resources/my-project-git-response 

</generateGitPropertiesFilename> 
        <format>json</format> 
        <dateFormat>yyyy-MM-dd-HH-mm</dateFormat> 
        <gitDescribe> 
            <skip>false</skip> 
            <always>false</always> 
            <dirty>-dirty</dirty> 
        </gitDescribe> 
        <includeOnlyProperties> 
            <includeOnlyProperty>git.branch</includeOnlyProperty> 
            <includeOnlyProperty>git.build.time</includeOnlyProperty> 
            <includeOnlyProperty>git.build.version</includeOnlyProperty> 
            <includeOnlyProperty>git.commit.id</includeOnlyProperty> 
            <includeOnlyProperty>git.commit.time</includeOnlyProperty> 
        </includeOnlyProperties> 
    </configuration> 
</plugin>

It gives us the ability to configure lots of options based on our needs and we can include the properties. Currently, we’ve only considered the git branch which will help us identify the branch name, the time it takes to complete the build process, the git build version, the commit ID, and the timestamp of the exact commit.  

Since the file will be created under src/main/resources we want to make sure the resources entry is added in the build section. If not, we need to make sure the following entry exists there. 

<resources> 
    <resource> 
        <directory>src/main/resources</directory> 
        <filtering>true</filtering> 
    </resource> 
</resources>

Now lets create a bean class representing our Build Info. 

@Data 
public class BuildInformation { 
 
    @SerializedName("git.branch") 
    private String branch; 
 
    @SerializedName("git.build.time") 
    private String buildTime; 
 
    @SerializedName("git.build.version") 
    private String buildVersion; 
 
    @SerializedName("git.commit.id") 
    private String commitId; 
 
    @SerializedName("git.commit.id.abbrev") 
    private String commitIdAbbrev; 
 
    @SerializedName("git.commit.id.describe") 
    private String commitIdDescribe; 
 
    @SerializedName("git.commit.id.describe-short") 
    private String commitIdDescribeShort; 
 
    @SerializedName("git.commit.time") 
    private String commitTime; 
 
    private String aemVersion; 
 
    private List<String> runModes; 
 
}

Our Service interface will look like this: 

public interface BuildInformationService { 
    BuildInformation getBuildInformation(); 
}

Finally, our Implementation class will look like this: 

@Component(service = BuildInformationService.class) 
@ServiceDescription("BuildInformationService Implementation.") 
@Slf4j 
public class BuildInformationServiceImpl implements BuildInformationService { 
 
    @Reference 
    private ProductInfoProvider productInfoProvider; 
 
    @Reference 
    private SlingSettingsService slingSettingsService; 
 
    public static final String GIT_INFO_FILE_PATH = "my-project-git-response.json"; 
 
    private BuildInformation buildInformation; 
 
    @Activate 
    public void activate() { 
 
        buildInformation = new BuildInformation(); 
        try { 
            final var inputStream = this.getClass().getClassLoader().getResourceAsStream(GIT_INFO_FILE_PATH); 
            final var jsonString = IOUtils.toString(inputStream, StandardCharsets.UTF_8); 
            final var gson = new Gson(); 
            buildInformation = gson.fromJson(jsonString, BuildInformation.class); 
 
            var productInfo = productInfoProvider.getProductInfo(); 
            final var version = productInfo.getVersion().toString(); 
            // Set additional fields not part of JSON response 
            buildInformation.setAemVersion(version); 
            buildInformation.setRunModes(slingSettingsService.getRunModes()); 
        } catch (Exception e) { 
            log.error("Error while generating build information"); 
        } 
    } 
 
    @Override 
    public BuildInformation getBuildInformation() { 
        return buildInformation; 
    } 
}

We are just reading the properties file created by the plugin and setting up the object. 

ProductInfo provider API will help to get the AEM Version while the Sling setting service provides run modes information. 

Now let’s create a sample servlet that will utilize this service and will print the information. 

import com.google.gson.Gson; 
import org.apache.sling.api.SlingHttpServletRequest; 
import org.apache.sling.api.SlingHttpServletResponse; 
import org.apache.sling.api.servlets.SlingSafeMethodsServlet; 
import org.apache.sling.servlets.annotations.SlingServletPaths; 
import org.osgi.service.component.annotations.Component; 
import org.osgi.service.component.annotations.Reference; 
import org.osgi.service.component.propertytypes.ServiceDescription; 
 
import javax.servlet.Servlet; 
import javax.servlet.ServletException; 
import java.io.IOException; 
 
@Component(service = Servlet.class) 
@ServiceDescription("This servlet will provide Build Information") 
@SlingServletPaths(BuildInformationServlet.BUILD_INFO_PATH) 
public class BuildInformationServlet extends SlingSafeMethodsServlet { 
 
    public static final String BUILD_INFO_PATH = "/bin/my-project/buildinfo"; 
 
    @Reference 
    private BuildInformationService buildInformationService; 
 
    @Override 
    protected void doGet(final SlingHttpServletRequest request,final SlingHttpServletResponse response) 
            throws ServletException, IOException { 
        response.setContentType("application/json"); 
        response.setCharacterEncoding("UTF-8"); 
        final var writer = response.getWriter(); 
        final var gson = new Gson(); 
        final var jsonString = gson.toJson(buildInformationService.getBuildInformation()); 
        writer.print(jsonString); 
        writer.flush(); 
    } 
} 

After deploying the code and opening the service end point it will give response like this: 

{ 

  "git.branch": "feature/PROJECT-1234", 

  "git.build.time": "2024-04-03-09-37", 

  "git.build.version": "2.22.0-65-SNAPSHOT", 

  "git.commit.id": "1fa29afb73e5b0cbfc90a2bb33db28741d98eec0", 

  "git.commit.id.abbrev": "1fa29af", 

  "git.commit.id.describe": "1fa29af-dirty", 

  "git.commit.id.describe-short": "1fa29af-dirty", 

  "git.commit.time": "2024-04-01-18-04", 

  "aemVersion": "6.5.17.0", 

  "runModes": [ 

    "s7connect", 

    "crx3", 

    "author", 

    "samplecontent", 

    "crx3tar" 

  ] 

}

Now that our Service is up and running, we can leverage it anywhere in our application. Let’s create a reusable component for displaying build information, powered by our trusty Sling model. This component can tap into the Service to fetch the necessary build details. 

How to Include Additional Build Details

Want to include additional build details, such as Jenkins job names and build numbers? Easy! We can pass these attributes from Jenkins via command-line arguments. Then, we store these values in properties files, making them easily consumable by our Sling model, just like our Git information file. 

The benefit of this solution is that it’s flexible, scalable, and reusable across multiple projects in a multitenant development environment.  

 

Make sure to follow our Adobe blog for more Adobe solution tips and tricks!  

]]>
https://blogs.perficient.com/2024/07/25/how-to-track-git-build-info-in-your-aem-multimodule-maven-project/feed/ 2 366389
AEM Local Development With OpenJDK 11 and Maven 3.9.x https://blogs.perficient.com/2024/07/23/aem-local-development-with-openjdk-11-and-maven-3-9-x/ https://blogs.perficient.com/2024/07/23/aem-local-development-with-openjdk-11-and-maven-3-9-x/#respond Tue, 23 Jul 2024 11:00:00 +0000 https://blogs.perficient.com/?p=366017

The official Adobe tutorial for setting up a local AEM development environment requests the reader to install Java JDK 11 for AEM 6.5 and above.  It does not provide a download link for the Java JDK 11.  If you were to do a quick Google search for “JDK 11 download,” you would be presented with a search results page containing links to Oracle. 

Oracle Corporation acquired Sun Microsystems (the creators of the Java Programming Language) in 2010.  In 2019, Oracle significantly changed its Java licensing model, impacting how businesses and developers could use Java.  Oracle now requires payment for commercial use of Oracle JDK for updates and support. 

Slightly lower on the Google search results page, you will see links to OpenLogic.  OpenLogic offers free builds of JDK 11. OpenJDK is available free of charge and on an “as is” basis. 

Installing OpenJDK

The simplest method I’ve found to install OpenJDK 11 is from this site: https://www.openlogic.com/openjdk-downloads

From here, you are presented with a form where you select your Java version (11), operating system, architecture, and Java package (JDK).  Select your preferred option, and the page will display a list of available Java versions. You can then choose to download either the installer for a quick and easy setup or a zip archive for manual installation.  I recommend downloading and running the installer. 

Another option is package managers.  Package managers simplify OpenJDK installation across platforms. They’re especially efficient on Linux. macOS users can utilize Homebrew for easy installation and updates. Windows users now have Winget from Microsoft for managing applications like OpenJDK. 

Links for installing OpenJDK via package managers: 

Installing Maven 

Installing Maven 3.9 requires a few additional steps. 

Installing on MacOS

The Homebrew package manager is the best option for macOS users.  Using the –ignore-dependencies flag is crucial to prevent it from installing a potentially conflicting version of OpenJDK. 

brew install --ignore-dependencies maven

Once Maven has been installed, edit the Z Shell configuration file (.zshrc) to include the following directives (create the file if it doesn’t exist): 

export JDK_HOME=$(/usr/libexec/java_home) 

export JAVA_HOME=$(/usr/libexec/java_home) 

export PATH=$PATH:${JAVA_HOME}/bin:/usr/local/bin

Open a new terminal window and verify Java and Maven are installed correctly: 

java --version 

mvn --version

If the output shows the location (path) and version information for both Java and Maven, congratulations! You’ve successfully installed them on your macOS system. 

Installing on Linux

Download the Maven Binary Archive here: https://maven.apache.org/download.cgi

Unpack the archive and move it to the /opt directory: 

tar -zxvf apache-maven-3.9.8-bin.tar.gz 

sudo mv apache-maven-3.9.8 /opt/apache-maven

Edit your shell configuration file and add the following directives:

export PATH=$PATH:/opt/apache-maven/bin

Open a new terminal window and verify Maven is installed correctly: 

mvn --version

If the output shows the location (path) and version information for Maven, congratulations! You’ve successfully installed Maven on your Linux system. 

Installing on Windows

Download the Maven Binary Archive here: https://maven.apache.org/download.cgi. 

Run PowerShell as an administrator. 

Unzip the Maven Binary Archive: 

Expand-Archive .\apache-maven-3.9.8-bin.zip

Create an “Apache Maven” folder within Program Files: 

New-Item 'C:\Program Files\Apache Maven' -ItemType Directory -ea 0

Move the extracted directory to the “Apache Maven” folder: 

Move-Item -Path .\apache-maven-3.9.8-bin\apache-maven-3.9.8 -Destination 'C:\Program Files\Apache Maven\'

Add the Maven directory to the Path Environment Variables: 

Maven Directory To The Path Environment Variables Example

Maven Directory To The Path Environment Variables Example 2

Maven Directory Edit Environment Variable

Click the “OK” button and open a new PowerShell Prompt to verify Maven is installed correctly: 

mvn --version

If the output shows the location (path) and version information for Maven, congratulations! You’ve successfully installed Maven on Windows. 

Additional Notes

Maven 3.9 will be the last version compatible with Adobe AEM 6.5. Future versions of Maven require JDK 17, which Adobe AEM does not yet support. 

When using Java 11, Adobe recommends adding additional switches to your command line when starting AEM.  See: https://experienceleague.adobe.com/en/docs/experience-manager-65/content/implementing/deploying/deploying/custom-standalone-install#java-considerations

Make sure to follow our Adobe blog for more Adobe solution tips and tricks!  

]]>
https://blogs.perficient.com/2024/07/23/aem-local-development-with-openjdk-11-and-maven-3-9-x/feed/ 0 366017
Adobe Sites: Migrating from Webpack to Vite https://blogs.perficient.com/2024/07/16/adobe-sites-migrating-from-webpack-to-vite/ https://blogs.perficient.com/2024/07/16/adobe-sites-migrating-from-webpack-to-vite/#comments Tue, 16 Jul 2024 20:50:31 +0000 https://blogs.perficient.com/?p=365994

Webpack is an amazing bundler for JavaScript and, with the correct loader, it can also transform CSS, HTML, and other assets.  When a new AEM project is created via the AEM Project Archetype and the front-end module is set to general, Adobe provides a Webpack configuration to generate the project’s client libraries.

Introducing Vite

Vite is a new build tool that has recently come onto the scene.  You can check the NPM trends here.

Compared to Webpack,

  • Vite provides significantly faster build times and hot reloading during development.
  • Vite utilizes Rollup.  Rollup generates small bundles by utilizing optimizations like tree shaking, ES6 modules, scope hoisting, minification, code splitting, and a plugin ecosystem.

Avoid Configuration Challenges With Vite

If you have any experience with Webpack, you know the challenges of configuring different loaders to preprocess your files.  Many of these configurations are unnecessary with Vite.  Vite supports TypeScript out of the box.  Vite provides built-in support for .scss, .sass, .less, .styl, and .stylus files.  There is no need to install Vite-specific plugins for them.  If the project contains a valid PostCSS configuration, it will automatically apply to all imported CSS.  It is truly a game-changer. 

Project “Jete”

“Vite” comes from the French word for “fast”.  In music, the term “Vite” refers to playing at a quickened pace.  For the following tutorial, I have chosen the music term “Jete” for the name of our project.  “Jete” refers to a bowing technique in which the player is instructed to let the bow bounce or jump off the strings.  Let us take a cue from this musical term and “bounce” into our tutorial. 

Migrating From Webpack to Vite Tutorial

Create an AEM Project via the AEM Project Archetype: 

mvn -B archetype:generate -D archetypeGroupId=com.adobe.aem -D archetypeArtifactId=aem-project-archetype -D archetypeVersion=49 -D aemVersion=cloud -D appTitle="Jete" -D appId="jete" -D groupId="com.jete" -D frontendModule=general -D includeExamples=n

Once your project has been created, install your project within your AEM instance:

mvn clean install -PautoInstallSinglePackage

After verifying the Jete site in AEM, we can start migrating our frontend project to Vite. 

Backup the existing ui.frontend directory: 

cd jete/ 

mv ui.frontend ../JeteFrontend 

From within “jete” run: 

npm create vite@latest

Use “aem-maven-archetype” for the project name, select Vanilla for the framework, and “TypeScript” for the variant. 

Rename the directory “aem-maven-archetype” to “ui.frontend”.  We chose that project name to match the name generated by the AEM Archetype. 

mv aem-maven-archetype ui.frontend

Let’s put the pom.xml file back into the frontend directory: 

mv ../JeteFrontend/pom.xml ui.frontend

Since we are updating the POM files, let’s update the Node and NPM versions in the parent.

pom.xml file. 

<configuration>  

  <nodeVersion>v20.14.0</nodeVersion>  

  <npmVersion>10.7.0</npmVersion>  

</configuration>

We will be using various Node utilities within our TypeScript filesLet us install the Node Types package. 

npm install @types/node --save-dev 

Add the following compiler options to our tsconfig.json file: 

"outDir": "dist", 

"baseUrl": ".", 

"paths": { 

  "@/*": [ 

    "src/*" 

  ] 

}, 

"types": [ 

  "node" 

]

These options set the output directory to “dist”, the base url to the current directory: “ui.frontend”, create an alias of “@” to the src directory, and add the Node types to the global scope. 

Let’s move our “public” directory and the index.html file into the “src” directory. 

Create a file named “vite.config.ts” within “ui.frontend” project. 

Add the following vite configurations: 

import path from 'path'; 

import { defineConfig } from 'vite'; 

export default defineConfig({ 

  build: { 

    emptyOutDir: true, 

    outDir: 'dist', 

  }, 

  root: path.join(__dirname, 'src'), 

  plugins: [], 

  server: { 

    port: 3000, 

  }, 

});

Update the index.html file within the “src” directoryChange the reference of the main.ts file from “/src/main.ts” to “./main.ts. 

<script type="module" src="./main.ts"></script>

Run the Vite dev server with the following command: 

npm run dev

You should see the following page: 

AEM Vite + Typescript

We are making progress! 

Let us make some AEM-specific changes to our Vite configuration. 

Change outDir to: 

path.join(__dirname, 'dist/clientlib-site')

Add the following within the build section: 

lib: { 

  entry: path.resolve(__dirname, 'src/main.ts'), 

  formats: ['iife'], 

  name: 'site.bundle', 

}, 

rollupOptions: { 

  output: { 

    assetFileNames: (file) => { 

      if (file.name?.endsWith('.css')) { 

        return 'site.bundle.[ext]'; 

      } 

      return `resources/[name].[ext]`; 

    }, 

    entryFileNames: `site.bundle.js`, 

  }, 

},

These configurations set the entry file, wrap the output within an immediately invoked function expression (to protect against polluting the global namespace), set the JavaScript and CSS bundle names to site.bundle.js and site.bundle.css, and set the output path for assets to a directory named “resources”.  Using the “iife” format requires setting the “process.env.NODE_ENV” variable. 

Add a “define” section at the same level as “build” with the following option: 

define: { 

  'process.env.NODE_ENV': '"production"', 

}, 

Add a “resolve” section at the same level as “define” and “build” to use our “@” alias: 

resolve: { 

  alias: { 

    '@': path.resolve(__dirname, './src'), 

  }, 

}, 

Add the following “proxy” section within the “server” section: 

proxy: { 

  '^/etc.clientlibs/.*': { 

      changeOrigin: true, 

      target: 'http://localhost:4502', 

  }, 

},

These options inform the dev server to proxy all requests starting with /etc.clientlibs to localhost:4502. 

It is time to remove the generated code.  Remove “index.html”, “conter.ts”, “style.css”, “typescript.svg”, “public/vite.svg” from within the “src” directory.  Remove everything from “main.ts”. 

Move the backup of index.html file to the src directory: 

cp ../JeteFrontend/src/main/webpack/static/index.html ui.frontend/src/

Edit the index.html file.  Replace the script including the “clientlib-site.js” with the following: 

<script type="module" src="./main.ts"></script>

Save the following image to “src/public/resources/images/”: 

https://raw.githubusercontent.com/PRFTAdobe/jete/main/ui.frontend/src/public/resources/images/favicon.ico 

Add the following element within the head section of the index.html file: 

<link rel="icon" href="./resources/images/favicon.ico" type="image/x-icon" />

While we are updating favicons, edit the

ui.apps/src/main/content/jcr_root/apps/jete/components/page/customheaderlibs.html file.

Add the following to the end of the file: 

<link rel="icon" href="/etc.clientlibs/jete/clientlibs/clientlib-site/resources/images/favicon.ico" type="image/x-icon" />

Run the Vite dev server once more … 

npm run dev

You should see the following: 

Project Jete With AEM Vite

It is not very attractiveLet us add some stylingRun the following command to install “sass”. 

npm i -D sass

Create a “main.scss” file under the “src” directory. 

touch main.scss

Edit the main.ts file and add the following line to the top of the file: 

import '@/main.scss'

Copy the variables stylesheet from the frontend backup to the “src” directory: 

cp ../JeteFrontend/src/main/webpack/site/_variables.scss ./ui.frontend/src/

Edit the _variables.scss file and add the following: 

$color-foreground-rgb: rgb(32 32 32);

Copy the base stylesheet from the frontend backup to the “src” directory: 

cp ../JeteFrontend/src/main/webpack/site/_base.scss ./ui.frontend/src/

Include references to these files within main.scss: 

@import 'variables'; 

@import 'base';

Run the Vite dev server once more … 

npm run dev

You should see the following: 

Project Jete With AEM Vite Version 2

Things are getting better, but there is still more work to do! 

Copy the component and site stylesheets from the frontend backup to the “src” directory: 

cp -R ../JeteFrontend/src/main/webpack/components ./ui.frontend/src/ 

 

cp -R ../JeteFrontend/src/main/webpack/site/styles ./ui.frontend/src/

Add the following to the main.scss file: 

@import './components/**/*.scss'; 

@import './styles/**/*.scss';

Run the Vite dev server … 

npm run dev

No luck this timeYou will probably see this error: 

Project Jete With AEM Vite Error

Vite doesn’t understand “splat imports”, “wildcard imports”, or “glob imports”.  We can fix this by installing a package and updating the Vite configuration file. 

Install the following package: 

npm i -D vite-plugin-sass-glob-import

Update the vite.config.ts fileAdd the following to the import statements: 

import sassGlobImports from 'vite-plugin-sass-glob-import';

Add “sassGlobImports” to the plugins section: 

plugins: [sassGlobImports()],

Now, let’s run the Vite dev server again. 

npm run dev

You should see the following: 

Project Jete With Aem Vite Version 3

Much better.  The front end is looking great!  Time to work on the JavaScript imports! 

TypeScript has been working well for us so far, so there’s no need to switch back to JavaScript. 

Remove the “helloworld” JavaScript file: 

rm -rf src/components/_helloworld.js

Grab the TypeScript from this URL and save it as src/components/_helloworld.ts: https://raw.githubusercontent.com/PRFTAdobe/jete/main/ui.frontend/src/components/_helloworld.ts 

To see the results of this script within our browser, we have to include this file within main.ts.  Importing splats won’t work on a TypeScript file.  So we can’t write: “import ‘@/components/**/*.ts’”.  Instead, we will write:

import.meta.glob('@/components/**/*.ts', { eager: true });

Now, let’s run the Vite dev server. 

npm run dev

You should see the following in Chrome DevTools: 

Aem Vite Javascript Example

Very good!  The JavaScript is working as well! 

The following section is optional, but it is good practice to add some linting rules. 

Install the following: 

npm i -D @typescript-eslint/eslint-plugin @typescript-eslint/parser autoprefixer eslint eslint-config-airbnb-base eslint-config-airbnb-typescript eslint-config-prettier eslint-import-resolver-typescript eslint-plugin-import eslint-plugin-prettier eslint-plugin-sort-keys eslint-plugin-typescript-sort-keys postcss postcss-dir-pseudo-class postcss-html postcss-logical prettier stylelint stylelint-config-recommended stylelint-config-standard stylelint-config-standard-scss stylelint-order stylelint-use-logical tsx

Save the following URLs to ui.frontend:

https://raw.githubusercontent.com/PRFTAdobe/jete/main/ui.frontend/.eslintrc.json

https://raw.githubusercontent.com/PRFTAdobe/jete/main/ui.frontend/.postcssrc.json 

https://raw.githubusercontent.com/PRFTAdobe/jete/main/ui.frontend/.prettierrc.json 

https://raw.githubusercontent.com/PRFTAdobe/jete/main/ui.frontend/.stylelintrc.json 

Add the following to the “script” section of package.json: 

"lint": "stylelint src/**/*.scss --fix && eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"

Let’s try out our new script by running: 

npm run lint

You should see a fair amount of sass linting errors.  You can fix the errors manually or overwrite your local versions with the ones from the git repo: https://github.com/PRFTAdobe/jete/tree/main/ui.frontend/src 

We are ready to move on from linting.  Let’s work on the AEM build. 

Install the following: 

npm i -D aem-clientlib-generator aemsync

Save the following URLs to ui.frontend: 

https://github.com/PRFTAdobe/jete/blob/main/ui.frontend/aem-sync-push.ts 

https://github.com/PRFTAdobe/jete/blob/main/ui.frontend/clientlib.config.ts 

https://github.com/PRFTAdobe/jete/blob/main/ui.frontend/aem-clientlib-generator.d.ts 

https://github.com/PRFTAdobe/jete/blob/main/ui.frontend/aemsync.d.ts 

The files with the “d.ts” extensions are used to provide typescript type information about the referenced packages. 

The “clientlib.config.ts” script, creates a client library based on the JS and CSS artifacts created during the build process.  It also copies the artifacts to the “clientlib” directory within “ui.apps”. 

The “aem-sync-push.ts” script takes the clientlib created above and pushes it to a running AEM instance. 

It is time to update the “script” section of package.json. 

Remove the existing “build” and “preview” commands.  Add the following commands: 

"build": "tsc && npm run lint && vite build && tsx ./clientlib.config.ts && tsx ./aem-sync-push.ts", 

"prod": "tsc && npm run lint && vite build && tsx ./clientlib.config.ts",

Let’s try out the build command first: 

npm run build

If the command has been completed successfully, you will see messages indicating that the “generator has finished” and the “aem sync has finished”.  You will also notice the creation of a “dist” directory under “ui.frontend”. 

Our last step is to copy over the “assembly.xml” file from the backup we made earlier. 

cp ../JeteFrontend/assembly.xml ui.frontend/

With that file in place, we are ready to rerun the AEM build: 

mvn clean install -PautoInstallSinglePackage

Congratulations!

The build should be complete without errors.  You have successfully migrated from Webpack to Vite! 

Make sure to follow our Adobe blog for more Adobe solution tips and tricks!  

]]>
https://blogs.perficient.com/2024/07/16/adobe-sites-migrating-from-webpack-to-vite/feed/ 1 365994
Enabling Dynamic Media Feature For AEM Custom Components https://blogs.perficient.com/2024/05/28/enabling-dynamic-media-feature-aem-custom-components/ https://blogs.perficient.com/2024/05/28/enabling-dynamic-media-feature-aem-custom-components/#comments Tue, 28 May 2024 11:50:17 +0000 https://blogs.perficient.com/?p=363473

Dynamic Media is one of the versatile tools provided by Adobe. Previously, it was known as Scene7. It has strong Asset editing and authoring tools capabilities. It is based on a Cache delivery network, which loads the contents to improve page load time and renders the right image renditions correctly resized and optimized. Dynamic Media allows requesting devices to request exactly what type of rendition, version, and exact image size is needed by that device at the moment of request.  

Have you wondered about the benefits of integrating the Dynamic Media feature in custom components?  In this blog, I will demonstrate how to integrate Dynamic Media with custom components and the benefits it provides by doing so.

When we integrate dynamic media with AEM, it enables various Dynamic Media functionalities like image/video profiles, DM metadata, DM workflows, and some set of out-of-the-box Dynamic Media Components. Also, some core components like Image provide Dynamic Media functionality. 

Integrate Dynamic Media With AEM

Integrate Dynamic Media With AEM New Component

If you have not integrated Dynamic Media with AEM, please go through the documentation provided by Adobe on how to Configure the Dynamic Media for AEM.

Loading Dynamic Media Library

First, we need to load dynamic media clientlibs on every page to render Dynamic Media Assets through AEM Components.

Paste the following code into the head section page component i.e. customheaderlibs.html 

<!--/* Dynamic Media Integration */--> 

<sly data-sly-use.clientLib="${'/libs/granite/sightly/templates/clientlib.html'}" 

data-sly-call="${clientLib.all @ categories='cq.dam.components.scene7.dynamicmedia,cq.dam.components.scene7.common.i18n'}"></sly>

Custom Sling Model Implementation 

Then create one custom sling model common utility for reading Dynamic Media assets metadata properties which is required to load any AEM assets through dynamic media functionality. 

Sling model Implementation 

public interface DynamicMedia extends BaseModel { 

String getCurrentPagePath(); 

String getPageLocale(); 

String getAssetName(); 

String getAssetPath(); 

String getDynamicMediaViewerPath(); 

String getDynamicMediaImageServerPath(); 

boolean isWcmModeDisabled(); 

@Slf4j 

@Model( 

adaptables = SlingHttpServletRequest.class, 

adapters = DynamicMedia.class, 

defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL 

) 

public class DynamicMediaImpl implements DynamicMedia { 

private static final String DM_IMAGE_SERVER_PATH = "is/image/";  

private static final String DM_VIEWERS_SERVER_PATH = "s7viewers/"; 

@SlingObject 

private SlingHttpServletRequest request; 

@ScriptVariable 

private Page currentPage; 

@RequestAttribute 

private String imageReference; 

@Getter 

private String assetName; 

@Getter 

private String assetPath; 

@Getter 

private String dynamicMediaViewerPath; 

@Getter 

private String dynamicMediaImageServerPath; 

@PostConstruct 

protected void init() { 

if (StringUtils.isNotEmpty(imageReference)) { 

final Resource assetResource = request.getResourceResolver() 

.getResource(imageReference); 

if (assetResource != null) { 

final Asset asset = assetResource.adaptTo(Asset.class); 

if (asset != null) { 

this.id = getId() + "-" + asset.getID(); 

assetName = asset.getName(); 

assetPath = asset.getMetadataValue(Scene7Constants.PN_S7_FILE); 

final String dynamicMediaServer = asset.getMetadataValue( 

Scene7Constants.PN_S7_DOMAIN); 

dynamicMediaImageServerPath = dynamicMediaServer + DM_IMAGE_SERVER_PATH; 

dynamicMediaViewerPath = dynamicMediaServer + DM_VIEWERS_SERVER_PATH; 

} 

} 

} 

} 

@Override 

public String getCurrentPagePath() { 

return currentPage.getPath(); 

} 

@Override 

public String getPageLocale() { 

return LocaleUtils.generateLocaleFromPath(getCurrentPagePath()); 

} 

@Override 

public boolean isWcmModeDisabled() { 

return (WCMMode.fromRequest(request) == WCMMode.DISABLED); 

} 

}

Once we are done with the Sling model implementation part then create one HTL template inside directory /apps/<Project Driectory>/components/atoms/dynamicmedia/image.html  to provide reusability within the component whenever we want to load any image using dynamic media for a specific component. Paste the following code in the image.html file. 

<sly data-sly-template.dynamicMediaImageTemplate="${ @ imageReference, aspectRatio, altText, suffixId}"> 

<div data-sly-use.dynamicMediaImage="${'com.perficientblogs.core.models.commons.DynamicMediaModel' @ imageReference = imageReference}" 

id="${suffixId || dynamicMediaImage.id}" 

data-current-page="${dynamicMediaImage.currentPagePath}" 

data-page-locale="${dynamicMediaImage.pageLocale}" 

data-asset-path="${dynamicMediaImage.assetPath}" 

data-asset-name="${dynamicMediaImage.assetName}" 

data-asset-type="image" 

data-viewer-path="${dynamicMediaImage.dynamicMediaViewerPath}" 

data-imageserver="${dynamicMediaImage.dynamicMediaImageServerPath}" 

data-wcmdisabled="${dynamicMediaImage.wcmModeDisabled}" 

data-dms7="" 

data-mode="smartcrop" //Note: We can set the image preset mode here 

data-enablehd="always" 

data-aspectratio="${aspectRatio @context='scriptString'}" 

data-title="${altText}" 

data-alt="${altText}" 

class="s7dm-dynamic-media"> 

</div> 

</sly>

Now refer to this HTL template file anywhere in the component HTML where we want to load any authored Image through dynamic media CDN. 

<sly data-sly-use.image=" /apps/<Project Driectory>/components/atoms/dynamicmedia/image.html" 

data-sly-call="${image.dynamicMediaImageTemplate @ imageReference= modeloject.fileReference, aspectRatio= false, altText= 'Hello world Component'}"></sly>

Make sure to pass a value for imageReferenceproperty which is mandatory. We can also pass the values with other objects named aspectRatio, altTextas per the need or extend the functionality as per the requirement. 

Image Url Rendered By Custom Component

See the image above for the final output.  Look at the URL of the image rendered by the custom component.  It is delivered from Dynamic Media provided CDN.  That is the benefit you achieve with this. This way the images will be served much faster to the user based on the region they are in.   

If you have any questions or comments, drop them in the chat below! 

 

]]>
https://blogs.perficient.com/2024/05/28/enabling-dynamic-media-feature-aem-custom-components/feed/ 1 363473
Create an RSS Feed using HTL https://blogs.perficient.com/2024/05/03/create-an-rss-feed-using-htl/ https://blogs.perficient.com/2024/05/03/create-an-rss-feed-using-htl/#comments Fri, 03 May 2024 17:15:39 +0000 https://blogs.perficient.com/?p=362326

Did you know you can create an RSS feed in AEM (Adobe Experience Manager) for external applications like Eloqua? While AEM provides out-of-the-box functionality for RSS feeds, customizing them may require additional steps. Below you’ll find several options for creating RSS feeds in AEM along with steps for creating one using HTL.  

3 Options to Create an RSS Feed in AEM  

  1. Override Default JSP Functionality (JSP Approach) 
    • Customize the JSP code to tailor the RSS feed according to your requirements 
    • This approach requires writing backend logic in Java and JSP
  2. Create a Servlet for RSS Feed
    • Implement the logic within the servlet to fetch and format the necessary data into RSS feed XML
    • Configure the servlet to respond to specific requests for the RSS feed endpoint
    • This approach allows more control and flexibility over the RSS feed generation process
  3. Use HTL with Sling Model (HTL Approach)
    • Write HTL templates combined with a Sling Model to generate the RSS feed
    • Leverage Sling Models to retrieve data from AEM and format it within the HTL template
    • This approach utilizes AEM’s modern templating language and component models
    • HTL is preferred for templating tasks due to its simplicity and readability

Expected RSS Feed 

Below is the feed response for an external source to integrate and send emails accordingly. Here the feed results can be filtered by category tag names (category) using query parameters in the feed URL. 

  • https://www.demoproject.com/products/aem.rss 
  • https://www.demoproject.com/products/aem.rss?category=web
<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
        <atom:link rel="self" href="https://www.demoproject.com/products/aem" />
        <link>https://www.demoproject.com/products/aem</link>
        <title>AEM</title>
        <description />
        <pubDate>Fri, 29 Sep 2023 02:08:26 +0000</pubDate>
        <item>
            <guid>https://www.demoproject.com/products/aem/one.rss.xml</guid>
            <atom:link rel="self" href="https://www.demoproject.com/products/aem/sites" />
            <link>https://www.demoproject.com/products/aem/sites</link>
            <title>Aem Sites</title>
            <description><![CDATA[AEM Sites is the content management system within Adobe Experience Manager that gives you one place to create, manage and deliver remarkable digital experiences across websites, mobile sites and on-site screens.]]></description>
            <pubDate>Tue, 31 Oct 2023 02:23:04 +0000</pubDate>
        </item>
        <item>
            <guid>https://www.demoproject.com/products/aem/two.rss.xml</guid>
            <atom:link rel="self" href="https://www.demoproject.com/products/aem/assets" />
            <link>https://www.demoproject.com/products/aem/assets</link>
            <title>Aem Assets</title>
            <description><![CDATA[Adobe Experience Manager (AEM) Assets is a digital asset management system (DAM) that is built into AEM. It stores and delivers a variety of assets (including images, videos, and documents) with their connected metadata in one secure location.]]></description>
            <pubDate>Thu, 26 Oct 2023 02:21:19 +0000</pubDate>
            <category>pdf,doc,image,web</category>
        </item>
    </channel>
</rss>

Steps for Creating RSS Feed Using HTL 

  • Create a HTML file under the page component 
  • Create a PageFeed Sling Model that returns data for the RSS feed 
  • Add a rewrite rule in the dispatcher rewrite rules file 
  • Update the ignoreUrlParams for the required params 

Page Component – RSS html  

Create an HTML file with the name “rss.xml.html” under page component. Both ‘rss.html’ or ‘rss.xml.html’ work fine for this. Here, ‘rss.xml.html’ naming convention indicates that it is generating XML data. PageFeedModel provides the page JSON data for the expected feed.  

  • Category tag is rendered only when page properties are authored with tag values
  • CDATA (character data) is a section of element content to render as only character data instead of markup
<?xml version="1.0" encoding="UTF-8"?>
<sly data-sly-use.model="com.demoproject.aem.core.models.PageFeedModel" />
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <atom:link rel="self" href="${model.link}"/>
        ${model.linkTag @ context='unsafe'}
        <title>${model.title}</title>
        <description>${model.subTitle}</description>
        <pubDate>${model.publishedDate}</pubDate>
        <sly data-sly-list.childPage="${model.entries}">
            <item>
                <guid>${childPage.feedUrl}</guid>
                <atom:link rel="self" href="${childPage.link}"/>
                ${childPage.linkTag @ context='unsafe'}
                <title>${childPage.title}</title>
                <description><![CDATA[${childPage.description}]]></description>
                <pubDate>${childPage.publishedDate}</pubDate>
                <sly data-sly-test="${childPage.tags}">
                    <category>${childPage.tags}</category>
                </sly>
            </item>
        </sly>
    </channel>
</rss>  

Page Feed Model

This is a component model that takes the currentPage as the root and retrieves a list of its child pages. Subsequently, it dynamically constructs properties such as publish date and categories based on the page’s tag field. These properties enable filtering of results based on query parameters. Once implemented, you can seamlessly integrate this model into your component to render the RSS feed.

  • Using currentPage get the current page properties as a value map 
  • Retrieve title, description, pubDate, link for current page 
  • Retrieve title, description, pubDate, link, tags (categories) for child pages 
  • Filter the child pages list based on the query param value (category)
//PageFeedModel sample code 
package com.demoproject.aem.core.models;

import com.adobe.cq.export.json.ExporterConstants;
import com.day.cq.commons.Externalizer;
import com.day.cq.commons.jcr.JcrConstants;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.PageFilter;
import com.demoproject.aem.core.utility.RssFeedUtils;
import lombok.Getter;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingException;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
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.SlingObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

@Model(adaptables = {
    Resource.class,
    SlingHttpServletRequest.class
}, resourceType = PageFeedModel.RESOURCE_TYPE, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class PageFeedModel {

    protected static final String RESOURCE_TYPE = "demoproject/components/page";
    private static final Logger logger = LoggerFactory.getLogger(PageFeedModel.class);
    @SlingObject
    ResourceResolver resourceResolver;
    @SlingObject
    SlingHttpServletRequest request;
    @Inject
    private Page currentPage;
    @Getter
    private String title;
    @Getter
    private String link;
    @Getter
    private String linkTag;
    @Getter
    private String description;
    @Getter
    private List < ChildPageModel > entries;
    @Inject
    private Externalizer externalizer;
    @Getter
    private String feedUrl;
    @Getter
    private String publishedDate;


    @PostConstruct
    protected void init() {
        try {
            ValueMap properties = currentPage.getContentResource().adaptTo(ValueMap.class);
            title = StringEscapeUtils.escapeXml(null != currentPage.getTitle() ? currentPage.getTitle() : properties.get(JcrConstants.JCR_TITLE, String.class));
            description = StringEscapeUtils.escapeXml(properties.get(JcrConstants.JCR_DESCRIPTION, String.class));

            link = RssFeedUtils.getExternaliseUrl(currentPage.getPath(), externalizer, resourceResolver);
            feedUrl = link + ".rss.xml";
            linkTag = RssFeedUtils.setLinkElements(link);

            String category = request.getParameter("category") != null ? request.getParameter("category").toLowerCase().replaceAll("\\s", "") : StringUtils.EMPTY;
            entries = new ArrayList < > ();
            Iterator < Page > childPages = currentPage.listChildren(new PageFilter(false, false));
            while (childPages.hasNext()) {
                Page childPage = childPages.next();
                ChildPageModel childPageModel = resourceResolver.getResource(childPage.getPath()).adaptTo(ChildPageModel.class);
                if (null != childPageModel) {
                    if (StringUtils.isBlank(category)) entries.add(childPageModel);
                    else {
                        String tags = childPageModel.getTags();
                        if (StringUtils.isNotBlank(tags)) {
                            tags = tags.toLowerCase().replaceAll("\\s", "");
                            List tagsList = Arrays.asList(tags.split(","));
                            String[] categoryList = category.split(",");
                            boolean flag = true;
                            for (String categoryStr: categoryList) {
                                if (tagsList.contains(StringEscapeUtils.escapeXml(categoryStr)) && flag) {
                                    entries.add(childPageModel);
                                    flag = false;
                                }
                            }
                        }
                    }
                }
            }
            publishedDate = RssFeedUtils.getPublishedDate(properties);

        } catch (SlingException e) {
            logger.error("Repository Exception {}", e);
        }
    }
}
//ChildPageModel 
package com.demoproject.aem.core.models;

import com.adobe.cq.export.json.ExporterConstants;
import com.day.cq.commons.Externalizer;
import com.day.cq.commons.jcr.JcrConstants;
import com.demoproject.aem.core.utility.RssFeedUtils;
import lombok.Getter;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.sling.api.SlingException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
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.SlingObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PostConstruct;
import javax.inject.Inject;

@Model(adaptables = {
    Resource.class
}, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class ChildPageModel {
    private static final Logger logger = LoggerFactory.getLogger(ChildPageModel.class);

    @SlingObject
    Resource resource;

    @Getter
    private String title;

    @Getter
    private String link;

    @Getter
    private String linkTag;

    @Getter
    private String feedUrl;

    @Getter
    private String description;

    @Getter
    private String publishedDate;

    @Getter
    private String tags;

    @Inject
    private Externalizer externalizer;

    @PostConstruct
    protected void init() {
        try {
            if (null != resource) {
                String url = resource.getPath();

                ResourceResolver resourceResolver = resource.getResourceResolver();
                link = RssFeedUtils.getExternaliseUrl(url, externalizer, resourceResolver);
                feedUrl = link + ".rss.xml";
                linkTag = RssFeedUtils.setLinkElements(link);

                ValueMap properties = resource.getChild(JcrConstants.JCR_CONTENT).adaptTo(ValueMap.class);
                title = StringEscapeUtils.escapeXml(properties.get(JcrConstants.JCR_TITLE, String.class));
                description = StringEscapeUtils.escapeXml(properties.get(JcrConstants.JCR_DESCRIPTION, String.class));
                publishedDate = RssFeedUtils.getPublishedDate(properties);
                tags = StringEscapeUtils.escapeXml(RssFeedUtils.getPageTags(properties, resourceResolver));

            }
        } catch (SlingException e) {
            logger.error("Error: " + e.getMessage());
        }
    }
}
//RSS Feed Utils 

package com.demoproject.aem.core.utility;

import com.day.cq.commons.Externalizer;
import com.day.cq.commons.jcr.JcrConstants;
import com.day.cq.tagging.Tag;
import com.day.cq.tagging.TagManager;
import com.day.cq.wcm.api.NameConstants;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/** 
 * @desc RSS Feed Utils 
 */
@Slf4j
public class RssFeedUtils {

    public static final String FORMAT_DATE = "E, dd MMM yyyy hh:mm:ss Z";
    public static final String CONTENT_PATH = "/content/demoproject/us/en";

    public static String getPublishedDate(ValueMap pageProperties) {
        String publishedDate = StringUtils.EMPTY;
        SimpleDateFormat dateFormat = new SimpleDateFormat(FORMAT_DATE);
        Date updatedDateVal = pageProperties.get(JcrConstants.JCR_LASTMODIFIED, pageProperties.get(JcrConstants.JCR_CREATED, Date.class));
        if (null != updatedDateVal) {
            Date replicatedDate = pageProperties.get(NameConstants.PN_PAGE_LAST_REPLICATED, updatedDateVal);
            publishedDate = dateFormat.format(replicatedDate);
        }
        return publishedDate;
    }

    public static String getExternaliseUrl(String pagePath, Externalizer externalizer, ResourceResolver resourceResolver) {
        String url = StringUtils.EMPTY;
        if (StringUtils.isNotBlank(pagePath) && null != externalizer && null != resourceResolver)
            url = externalizer.publishLink(resourceResolver, resourceResolver.map(pagePath)).replace(CONTENT_PATH, "");

        return url;
    }

    public static String setLinkElements(String link) {
        String url = StringUtils.EMPTY;
        if (StringUtils.isNotBlank(link)) {
            url = "<link>" + link + "</link>";
        }
        return url;
    }

    public static String getPageTags(ValueMap properties, ResourceResolver resourceResolver) {
        String tags = StringUtils.EMPTY;
        String[] pageTags = properties.get(NameConstants.PN_TAGS, String[].class);
        if (pageTags != null) {
            List < String > tagList = new ArrayList < > ();
            TagManager tagManager = resourceResolver.adaptTo(TagManager.class);
            for (String tagStr: pageTags) {
                Tag tag = tagManager.resolve(tagStr);
                if (tag != null) {
                    tagList.add(tag.getName());
                }
            }
            if (!tagList.isEmpty()) tags = String.join(",", tagList);
        }
        return tags;
    }
}

Dispatcher Changes  

demoproject_rewrites.rules 

In the client project rewrites.rules (/src/conf.d/rewrites) file add a rewrite rule for .rss extension. This rewrite rule takes a URL ending with .rss and rewrites it to point to a corresponding rss.xml file in the page component, effectively changing the file extension from .rss to .rss.xml

#feed rewrite rule
RewriteRule ^/(.*).rss$ /content/demoproject/us/en/$1.rss.xml [PT,L]

100_demoproject_dispatcher_farm.any  

Set the URL parameters that should not be cached for the rss feed. It is recommended that you configure the ignoreUrlParams setting in an allowlist manner. As such, all query parameters are ignored and only known or expected query parameters are exempt (denied) from being ignored.

When a parameter is ignored for a page, the page is cached upon its initial request. As a result, the system subsequently serves requests for the page using the cached version, irrespective of the parameter’s value in the request. Here, we add URL parameters below to serve the content live as required by an external application.

/ignoreUrlParams {
    /0001 { /glob "*" /type "allow" }
    /0002 { /glob "category" /type "deny" }
    /0003 { /glob "pubdate_gt" /type "deny" }
    /0004 { /glob "pubdate_lt" /type "deny" }
}

 

Why is HTL Better?  

We can utilize this approach to produce any XML feed, extending beyond RSS feeds. We have the flexibility to add custom properties to tailor the feed to our specific needs. Plus, we can easily apply filters using query parameters.

 

Big thanks to my director, Grace Siwicki, for her invaluable assistance in brainstorming the implementation and completing this blog work.

]]>
https://blogs.perficient.com/2024/05/03/create-an-rss-feed-using-htl/feed/ 1 362326
Overview and Basic Concepts of Adobe Experience Manager (AEM) Components https://blogs.perficient.com/2024/04/05/overview-and-basic-concepts-of-adobe-experience-manager-aem-components/ https://blogs.perficient.com/2024/04/05/overview-and-basic-concepts-of-adobe-experience-manager-aem-components/#comments Fri, 05 Apr 2024 08:08:52 +0000 https://blogs.perficient.com/?p=361159

Adobe Experience Manager (AEM) is a sophisticated and versatile content management tool. Components are the elements that help structure the page, for example, the header, body, and footer, through authoring. AEM’s core components, have always allowed authors to create pages that are both efficient and simple to use, whereas developers can create components that are customizable and expandable. In this blog, we will look at the mysterious world of AEM components and how they impact the platform’s overall functionality and user experience. 

Fig. Core Components Library

Classification of AEM Components: 

Definition and Types: AEM components are customizable elements that offer unique features or content. Components are classified into two categories:

  1. Core components accessible through AEM out of the box, and custom components designed to fit specific business needs.
  2. Foundational components comprise text, pictures, and navigation components. 
Aem Components Overview

Fig. Types of Components

However, users may create, manage, and publish digital content with Adobe Experience Manager (AEM), a potent web content management system. AEM components are crucial to the process of creating webpages. They have a variety of functions and can be tailored to meet certain requirements. Developing effective and efficient websites requires a thorough understanding of the various AEM component types and their applications. 

Applications of the AEM components:

  1. Content Authoring: Components offer efficient content creation and management. Authors can leverage the user-friendly AEM interface to drag and drop components in pages, resulting in dynamic and visually appealing layouts. This visual creation method minimizes the need for developers to do routine content changes.

    Fig. Author and Publisher

  2. Responsive Design: AEM components are especially important for responsive design. Furthermore, mobile devices are becoming more common, components are designed to work easily across a wide range of platforms, ensuring a positive user experience. This flexible design feature is critical to reaching a wider demographic.

    Fig. Responsive Viewer

  3. Customization and Extensibility: AEM’s custom components enable business entities to build digital experiences to meet specific business demands. Developers can develop customized components that interact with other applications, furnish specialized functionality, or comply with certain design specifications, giving them outstanding flexibility. 

Merits of AEM Components:

  1. AEM Core Components offer uniformity, which shortens development processes and saves time. 
  2. Core Components are intended to be robust and adaptable. They provide a stable basis for website development, helping you to easily add unique AEM functionality, connect external apps, and adapt to shifting business requirements. 
  3. Adaptability ensures that websites designed with Core Components tend to evolve and develop alongside the business you represent. 
  4. AEM Core Components adhere to best practices for effective rendering, caching, and structured data since they are performance and SEO (Search Engine Optimization) emphasized. 

Behind the Scenes: A Glimpse into Component Development 

  1. Component Structure: Components in AEM are developed following a structured approach. This includes creating the component’s Java logic, defining its dialog for content authoring, and designing the component’s frontend using HTML (HTML Template Language) or JSP (Java Server Pages). This separation of concerns ensures maintainability and ease of development. 
  2. Component Lifecycle: Understanding the lifecycle of an AEM component is crucial for developers. Components go through initialization, rendering, and destruction phases. Developers need to be mindful of these stages to specifically optimize component behavior and performance. 

Conclusion: 

The components of Adobe Experience Manager are integral in creating customized and engaging digital experiences, as they join together to form a smooth and fascinating user experience. Their importance cannot be overstated in content writing, responsive design, and extensibility, as they enable businesses to create customized and engaging digital experiences. The importance of AEM components in forming the online world is constant, even as we negotiate the ever-changing terrain of digital encounters. 

 

]]>
https://blogs.perficient.com/2024/04/05/overview-and-basic-concepts-of-adobe-experience-manager-aem-components/feed/ 1 361159
Sling Mappings++ Large Lists and Mapping Arrays https://blogs.perficient.com/2024/03/22/unleash-the-power-of-sling-mappings-fallback-arrays/ https://blogs.perficient.com/2024/03/22/unleash-the-power-of-sling-mappings-fallback-arrays/#comments Fri, 22 Mar 2024 16:16:08 +0000 https://blogs.perficient.com/?p=360041

As all AEM Developers know, AEM, and more-so the underlying JCR, has limitations on how many children a single parent can have before we start to see performance issues.  First in the UI, and then more critically in the ability to look-up and manage content nodes themselves.  When designing for a net-new AEM website, this can be planned around, however, many times when dealing with a migration, there may be requirements to both keep existing links in-tact, which may put us in a situation where a large flat list, at least as exposed to the end-user, is unavoidable.

In this post, I will not go down the rabbit hole of whether 500, 1000, or 10000 children would start to have performance issues (although I do certainly have opinions here), and instead will focus on ways to allow for a single parent, to appear to have many more than the maximum number of children.

Enter Sling Mappings

Now, Sling mappings are not new for most, and commonly used for things like stripping the initial prefix (/content/<project>) away from outgoing URLs, but these mappings have additional untapped power which documentation, in my opinion, does not clearly emphasize.  Before we jump in, lets talk about a few key features of sling mappings as a whole.

Sling mappings are configured based on regular expressions, and often inherited by the node names themselves, within /etc/map.  Within each node, there are optional properties we can add to define how mappings should behave.  For example, if we are wanting to introduce a higher complexity regular expression, we cannot rely on the node name alone (characters required for regex mappings are restricted from being used as a node name).  This means that when we need to define a mapping which does NOT align with the name of the node, we can insert the following property (single value only):

sling:match <Regular Expression or Reverse Regular Expression>
sling:match content/wknd/us/en/magazine/(.*)

Now, the above is a relatively straight-forward example, but we’re here to learn about mappings, not regular expressions.  Do note, I am using a capture group here – multiple capture groups are certainly also supported, but for ease of this post, am keeping it quite simple.

Mappingonrootnode

The other item to note on the above is that I have added this node as a root node in the /etc/map/localhost tree.  As such, the mapping begins from the root element.  If we were to structure the /etc/map node differently, the start-point of that regular expression would change as well, for example:

Mappingonnonrootlevel

In the above scenario, we’d structure the sling:match accordingly, as the content and wknd nodes would have already been matched against:

sling:match us/en/magazine/(.*)

Well, that is half of the issue, is being able to capture the incoming request.  Next is to actually map it to a different internal resource.  This is where the “magic” comes in – on the same node, add an additional property, sling:internalRedirect, as follows:

sling:internalRedirect <path(s) to look for the matched content>
sling:internalRedirect /content/wknd/us/en/magazine/$1

In the above, we’re really just finding content in the same location it was requested.  The $1 in the internalRedirect is referencing the (.*) of the sling:match property, and saying that whatever was matched there, should first look at the location requested to find the resource, technically speaking, we have not really mapped anything at this point.   Now, what if the magazine section above is a long-standing blog, which has all urls published under the same /magazine root.  If organized 1:1 with the JCR structure, this would quickly cause reason for concern!  This is where the often overlooked ARRAY support of the internalRedirect comes to save the day.  From here, we can supply alternative locations to look for the same page.  For example, if we were to archive posts on a yearly basis, as in 2023 posts would live in the JCR structure /content/wknd/us/en/magazine/2023/<post>, while being publicly available still under /content/wknd/us/en/magazine/<post>, we could configure that using the “internalRedirect” array, as follows:

 

sling:internalRedirect /content/wknd/us/en/magazine/$1, /content/wknd/us/en/magazine/2023/$1

 

Now, when a request matches, it will first search under the magazine root folder, and if it does not find a resource there, will look to the 2023 folder, and so-on until all items of the Array have been searched against.  It does search from the top, down, so this means that order is important, especially if same-name nodes are expected.  If the entire list is exhausted and there is no resource found, then the normal 404 behavior is observed.

Now, lets assume we’ve got the following content (note the 2023 and 2024 folders added, with children):

Contentexample

With the forward mapping applied, we should be able to access the “san-diego-surf-2023” and “san-diego-surf-2024” under the /magazine folder directly.  Lets test it out!

Correctlymappedurl

Awesome.  We’re accessing the content without using the “2023” folder in the URL!

 

Great!  Well this solves the problem of accessing the content, however, if we look at a page which links to this content, we still are seeing an issue….

Incorrectlymappedlink

Looks like we still have one other piece of the puzzle to solve for.  One other important thing to know about regular expressions (with capture groups) is that when including in a sling mapping, they will only work in one direction.  Since we have configured it to capture on the request side (match) it will only handle the logic of handling the request.  In order to create a rewrite of outgoing URLs, we will need to also handle the reverse mapping.  For this, I use the suffix “-reverse” with the same node name as initial mapping for consistency.

 

So, next step is to copy the node we just created, and paste it as a sibling (under same parent).  We’ll then reverse the regular expression logic to have capture groups in the “sling:internalRedirect” property, and the references in the sling mapping property, as follows:

 

sling:match /content/wknd/us/en/magazine/$1
sling:internalRedirect /content/wknd/us/en/magazine/(.*),/content/wknd/us/en/magazine/2023/(.*)

 

Reversemappingonrootnode

 

This behaves in much the same way as the other mapping node, but because we have swapped the matching logic for the regular expression, it will now be applied in the url rewriting or output sling mapping as well.

 

Now, lets take a look back at our links to this content, and….

Correctlymappedlink

We’re golden!  Content is hosted under multiple JCR Nodes while being exposed under a single parent to the end-user.

 

I would like to add a disclaimer here: while this method is certainly a suitable work-around, a much more desirable implementation would have the JCR structure more closely matching the expected sitemap.

 

Hope this quick demonstration has been helpful!

 

 

Reference:

]]>
https://blogs.perficient.com/2024/03/22/unleash-the-power-of-sling-mappings-fallback-arrays/feed/ 3 360041
Updating the SPA Editor to Support Vue 3 https://blogs.perficient.com/2024/02/29/updating-the-spa-editor-to-support-vue-3/ https://blogs.perficient.com/2024/02/29/updating-the-spa-editor-to-support-vue-3/#respond Thu, 29 Feb 2024 17:53:38 +0000 https://blogs.perficient.com/?p=357926

I enjoy building websites using the Adobe Experience Manager (AEM) content management system. I also enjoy using JavaScript frameworks like Vue, React, and Angular to create rich web applications. Occasionally I get to do both. AEM provides a SPA Editor to facilitate building components for single-page applications. The SPA Editor enables front-end developers to produce components in React or Angular. These components are used by the author to create rich, native-like experiences.

Some time ago, I was engaged with a customer who needed a single-page application written in Vue and not React or Angular. Sadly, the AEM SPA Editor does not support the Vue JavaScript framework. The customer employed Vue developers and wasn’t willing to migrate their tech stack to another framework. I was tasked to create a version of the SPA Editor that would support Vue.

The Challenge: Editor Functionality and Performance

Because this work would be custom and not have the confidence of Adobe support, I wanted to be sure I could deliver a SPA Editor with all the functionality and performance of the editors written for React and Angular. I visited my favorite search engine with the hopes of finding code repositories from developers who have accomplished this very thing. No such luck. I stumbled upon a few results that looked promising, but upon closer inspection didn’t quite match my use case. It was time to dig into the existing SPA code and see if I could create something similar.

Stanley: Our Vue SPA Editor Demo

At the end of this blog, you will find links to download an AEM project created to demonstrate a working Vue SPA Editor. When deployed, this project presents a cute site named “Stanley” containing a sampling of Core components.

Vue Spa Editor Stanley

Reverse Engineering the Existing SPA Editor

The front-end portion of the SPA Editor is composed of five NPM packages.

  1. AEM Component Mapping (@adobe/aem-SPA-component-mapping)
    A storage library for front-end components. Provides a way for the Single Page Application to map front-end components to Adobe Experience Manager resource types (AEM Components).
  2. Page Model Manager (@adobe/aem-SPA-page-model-manager)
    An interpreter between Adobe Experience Manager Editor and the Adobe Experience Manager Single Page Application (SPA) Editor.
  3. AEM SPA Editable Components (@adobe/aem-react-editable-components or @adobe/aem-angular-editable-components)
    This project provides the React/Angular components and integration layer to get you started with the Adobe Experience Manager SPA Editor.
  4. AEM WCM Components – SPA (@adobe/aem-core-components-react-SPA or @adobe/aem-core-components-angular-SPA)
    This module provides a React/Angular implementation for the containers in the AEM core components.
  5. AEM WCM Components – Base (@adobe/aem-core-components-react-base or @adobe/aem-core-components-angular-base)
    This module provides a React/Angular implementation for the AEM core components.

Of these five packages, three were specific to React or Angular. These are the three packages I would need to reverse engineer and replicate in Vue. I had a fair amount of React experience and decided to use the React packages as my starting point. The React code is based on the older Class Components and not the newer Hooks paradigm. This made digging through the code difficult. I was far more comfortable using the Options API in Vue, but I decided to challenge myself and create the components using the Composition API.

The AEM WCM Components Base package and the AEM WCM Components SPA package contained React components corresponding to AEM core components. I would need to migrate these packages in time, but the core functionality was to be found in the AEM SPA Editable Components package.

Note: As of late 2023, the 47th release of the AEM Project Archetype includes version 1.1.6 of @adobe/aem-react-editable-components, version 1.1.8 of @adobe/aem-core-components-react-base, and version 1.2.0 of @adobe/aem-core-components-react-SPA. I have based my Vue code upon these versions.

AEM SPA Editable Components Package Migration

The methods responsible for mapping AEM component paths to React components can be found in the ComponentMapping.tsx file. The file contains little React, and its migration was easy. Here are the five utility methods used.

MapTo

The primary method in this file, MapTo, takes in as its parameters: a path (or paths) to an AEM component, a corresponding React component, and an Edit configuration containing the readable name of the component and a method that determines if a component qualifies as “empty”.  The MapTo method returns the provided component wrapped with three other React components. These wrapping components would need to be migrated to Vue. I started with the “ContextProvider” component.

ContextProvider

ContextProvider provides a global context for all containing components. If the ContextProvider component is a child of another ContextProvider component, the context comes from the parent ContextProvider. The global context contains a Boolean value indicating whether the application is being accessed in “edit” mode. It also contains an instance of the JavaScript class ComponentMapping. This class manages the mapping between AEM component resource types and the corresponding JavaScript component class.

ModelProvider

ModelProvider adds an AEM listener to the provided component path. When the component is updated via authoring, the new values are passed to the child components. This component was tricky to migrate. Updates to properties can come from two paths. Authoring of the component directly or authoring of a parent component. I have added a reactive property named updatedModelProperties which is populated with the values from authoring or populated with fall-through attributes from the parent.

EditableProvider

EditableProvider wraps the component with a div containing HTML attributes that AEM requires to draw the editable overlays. The wrapping div is only added when in edit mode.

With these three components migrated to Vue, I was able to complete the ComponentMapping code and migrate the test cases to ensure consistent behavior with the code from the React package. With this done, I turned my attention to the foundation components: Container, Page, ResponsiveGrid, and AllowedComponentsContainer.

Foundation Components

The AEM Editable Components package is written in an older flavor of React making use of Class Components. This made it easy for the original authors to extend the existing Container component and override the functionality. The Container component is the base for all the other foundation components.

React now discourages using Class Components and recommends using Functional Components. Vue has moved away from that pattern as well and provides other ways of reusing functionality across components. I chose to make the Container component more extensible.

I created composable functions that encapsulate reusable logic and added them to a utility file. When migrating the AllowedComponentsContainer, I determined that much of the Container component would be reused. It wasn’t worth encapsulating the remaining Container logic. I decided to wrap the Container component and pass any new functionality as props to the component.

I was able to replicate the behavior of the React components and keep my codebase clean and readable. After ensuring the test cases passed for the Foundation components, I was able to conclude my work on the AEM SPA Editable Components package.

AEM WCM Components – SPA Package Migration

The next package to be migrated would be the AEM WCM Components – SPA package. The AEM WCM Components – SPA package contains container components like Container, Accordion, Carousel, Tabs, and Experience Fragments.

These components allow you to nest other components inside of them. Since most of these components extended the foundation Container component, I was able to use the same tricks (and utility methods) I used in the AEM Editable Components package to migrate these components.

The Accordion, Carousel, and Tabs components do not display all nested components at once. The SPAUtils file contains methods to subscribe to the AEM Message Channel and listen for “navigation” events from authoring and display the correct nested components. I upgraded the React rendering code to include the accessibility features from the default AEM components.

The AEM WCM Components – SPA package also contains the Content Fragment component and the ContentFragmentRegistry.ts file. The primary method in this file, MapToContentFragmentModel, takes in as its parameters a path to a content fragment model and a corresponding Vue component. The MapToContentFragmentModel returns the provided component rendered using the properties from the Content Fragment model. If a Content Fragment model is not mapped using the MapToContentFragmentModel method, the component “DefaultContentFragment” will render the component. The method MapToContentFragmentModel is potent. It allows you to create one-off components using a Content Fragment model and a Vue component to display the data.

AEM WCM Components – Base Package Migration

The last package to be migrated is the AEM WCM Components – Base package. It contains the following components: Breadcrumb, Button, Download, Image, Language Navigation, List, Navigation, Separator, Teaser, Text, and Title. It also contains the component CoreLink. This component is used to generate anchor tags for external links or routed links (using Vue Router) for links internal to the application. These are generally simple components. The migration was straightforward.

With the three packages specific to a JavaScript framework migrated to Vue, I was ready to generate a project using my new framework. I ran the following command to generate a new project:

New Project Command Vue Migration

UI Frontend Changes

The next thing I did was to copy the pom.xml file out of the ui.frontend maven project and delete the ui.frontend directory. I then created a new ui.frontend directory and re added the pom.xml file. I edited the pom.xml file and removed the following from the plugins section:

Ui Frontend Changes Plugin

 I also updated my Node and NPM version. The react ui.frontend maven project would use react-scripts to build the project. I decided to use Webpack instead. I initialized a package.json file and added my dev dependencies. I would use:

  • Webpack and Babel to bundle my CSS and JavaScript
  • TypeScript to improve my developer experience and increase my productivity
  • Jest to test my code
  • ESLint and Prettier to lint my TypeScript
  • StyleLint to lint my CSS
  • PostCSS to transform my CSS
  • The AEM Client Library Generator to create a Client Library from my bundled CSS and JavaScript

My dependencies consisted of Vue, Vue Router, my three SPA Editor packages I migrated from React, and the two Adobe packages not dependent upon a frontend framework. I rewrote the client lib config in TypeScript and added a src directory.

Mapping Components

Within the src directory, I added the all-important “import-components” file. The file contains a mapping for every component within ui.apps that has a matching Vue component. For example, mapping for a button was added because the package AEM WCM Components – Base contains a button component. There is no mapping for the progress bar component because there are no corresponding Vue components in any of the packages I migrated from React.

The mapping of the Page AEM Component is not to the Page Vue component. It is to the Composite Route Vue Component. This component adds a Vue Route for each page encountered. The very last command to execute in the App Vue component is a call to the Vue Router to load the route corresponding to the current page path. If a route is found for the current page path, Vue Router will load the Page component for that path and all its child components.

Vue-Specific Changes

You can see the other Vue-specific changes I made here: https://github.com/PRFTAdobe/stanley/tree/main/ui.frontend

The Page and App policies also had to be updated to point to the new Vue client library instead of React. In the project I created above, the file in question can be found here: stanley/ui.content/src/main/content/jcr_root/conf/stanley/settings/wcm/policies/.content.xml. Any reference to “stanley.react” needed to be changed to “stanley.vue”.

The generated “customheaderlibs.html” and “customfooterlibs.html” needed to be updated and references to “clientlib-react” were changed to “clientlib-vue”.

I’ve also added some application-specific components and a custom form with validation. Feel free to check out the repo and try it out: https://github.com/PRFTAdobe/stanley.

I have created a Components page to demo the Core Vue components.

Core Vue Components

You can find the source code for my migrated packages here:

https://github.com/PRFTAdobe/aem-vue-3-editable-components

https://github.com/PRFTAdobe/aem-vue-3-core-wcm-components-SPA

https://github.com/PRFTAdobe/aem-vue-3-core-wcm-components-base

 Fixing Layout Mode

Currently, Layout Mode is not working. I have not had a chance to investigate the issue. This repo is open source. If someone has a fix, feel free to create a PR and I will look.

Get More Out of Adobe Experience Manager

Check out more of our AEM-related blogs for more help making informed decisions for your own implementations.

]]>
https://blogs.perficient.com/2024/02/29/updating-the-spa-editor-to-support-vue-3/feed/ 0 357926