You want to write a custom SlingServlet to render an HTML page or generate JSON to be consumed by a front end such as a mobile device.
You also want the OSGI bundle to have its settings editable in the Felix Console.
The Solution
So you want to make a configurable OSGI bundle to allow various items to be changed in the Felix Console?
Let's take look at a servlet that reads some settings from the internal config, and replaces any text from a webpage and re-renders the text in the client.
Note, if you are getting code complete issues due to some packages not being recognized by CRXDE, then
http://localhost:4502/crx/packageshare/
Log in if required with your adobe id.
Click on Download for:
1. Missing CRXDE-Libs
2. CRX Hotfix Pack
And install these packages.
Restart CQ5 and CRXDE, and hopefully, this will have resolved the issue.
Step 1 - Create a bundle
Bundles are a set of files with a manifest that CQ5 can install.
Create standard nt:folder to represent your application. In this case, we'll call it SlingServlet.
1. Open up, CRXDE Lite.
2. Right-click on the apps folder, click on create a folder, and call it SlingServletExample.
3. VERY IMPORTANT: Right-click on the SlingServletExample folder and create a folder called install. Note, the install folder should be /apps/SlingServletExample/install. Failure to do so will mean your bundle will not automatically be deployed. See the gotchya below for full details
4. Right-click on the SlingServletExample folder, click on new->Create Bundle. Fill in the Symbolic Name and Name to be WebpageTransformer. Type "Transforms webpages" into the description, and give it a package name of org.example.slingservlet.transformer. Click OK.
Congratulations. You've created a bundle.
BEFORE YOU MOVE ON:
Bit of a gotchya here. If you look at the WebpageTransformer.bnd package, you'll see the following two lines:
# Export-Package: *
# Import-Package: *
This is a bit of a nasty one, as to access this bundle from the outside world, you'll need to remove the # from both lines, or else all packages will remain as private only.
For full details, see the gotchya link below.
Step 2 - Create the Transformer Sling Servlet
Right-click on the transformer folder, and click New -> Class
Name it TransformerClass, and set superclass to be org.apache.sling.api.servlets.SlingSafeMethodsServlet
Click on Finish.
This example takes a webpage and transforms a supplied set of text into another set of text.
Here is the full TransformerClass.java content.
TransformerClass.java
package org.example.slingservlet.transformer;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.rmi.ServerException;
import java.util.Dictionary;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.sling.SlingServlet;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.apache.sling.commons.osgi.OsgiUtil;
import org.apache.sling.jcr.api.SlingRepository;
import org.osgi.service.component.ComponentContext;
@SlingServlet(paths = "/bin/mySearchServlet", methods = "GET", metatype = true)
@Properties({
@org.apache.felix.scr.annotations.Property(name = "PAGE-PROVIDER", description = "Page Provider Address", value = "http://www.google.com"),
@org.apache.felix.scr.annotations.Property(name = "SOURCE-TEXT", description = "Source Text to Replace", value = "Search"),
@org.apache.felix.scr.annotations.Property(name = "TARGET-TEXT", description = "Target Replacement Text", value = "Find of Awesomeness")
})
public class TransformerClass extends SlingSafeMethodsServlet {
private static final long serialVersionUID = 2598426539166789515 L;
public static final String PAGE_PROVIDER = "PAGE-PROVIDER";
public static final String SOURCE_TEXT = "SOURCE-TEXT";
public static final String TARGET_TEXT = "TARGET-TEXT";
private String pageProvider;
private String sourceText;
private String targetText;
@SuppressWarnings("unused")
@Reference
private SlingRepository repository;
protected void activate(ComponentContext componentContext) {
configure(componentContext.getProperties());
}
protected void configure(Dictionary < ? , ? > properties) {
this.pageProvider = OsgiUtil.toString(properties.get(PAGE_PROVIDER), null);
this.sourceText = OsgiUtil.toString(properties.get(SOURCE_TEXT), null);
this.targetText = OsgiUtil.toString(properties.get(TARGET_TEXT), null);
}
@Override
protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServerException, IOException {
URL serverAddress = null;
BufferedReader reader = null;
HttpURLConnection internalHttpRequest = null;
PrintWriter responseWriter = response.getWriter();
try {
serverAddress = new URL(pageProvider);
internalHttpRequest = (HttpURLConnection) serverAddress.openConnection();
InputStream in = null;
try {
in = internalHttpRequest.getInputStream();
} catch (Exception e) {
responseWriter.append(e.toString());
}
if ( in != null) {
reader = new BufferedReader(new InputStreamReader( in ));
}
} finally {
if (reader != null) {
String line = null;
while ((line = reader.readLine()) != null) {
responseWriter.append(line.replace(sourceText, targetText) + "\n");
}
}
responseWriter.flush();
if (internalHttpRequest != null) {
internalHttpRequest.disconnect();
}
}
}
}
Servlet Address
@SlingServlet(paths="/bin/mySearchServlet", methods = "GET", metatype=true)
CQ5 utilizes Java Annotations to configure classes.
Here, we are setting the SlingServlet annotation to have 3 values:
paths, methods, and metatype.
The paths variable allows you to set the url that will resolve to the slingservlet.
In our case, if you access http://localhost:4502/bin/mySearchServlet, you will be served the servlet, rather than whatever may reside there in the JCR.
Methods specify a GET method
The metatype option being set to true instructs CQ5 to provide a config page in the Felix console.
Servlet PropertiesThe @Properties annotation contains multiple @Property annotations contained in our Sling Servlet.
It is worth noting that if you are using javax.jcr.Property to get a property from a JCR Node, you will have a common namespace conflict with the org.apache.felix.scr.annotations.Property.
I personally think that it's best to use org.apache.felix.scr.annotations.Property as the inline declaration, rather than javax.jcr.Property, as the Property is something that is generally still readable in a long format, and won't influence the legibility of your code.
@SlingServlet(paths = "/bin/mySearchServlet", methods = "GET", metatype = true)
@Properties({
@org.apache.felix.scr.annotations.Property(name = "PAGE-PROVIDER", description = "Page Provider Address", value = "http://www.google.com"),
@org.apache.felix.scr.annotations.Property(name = "SOURCE-TEXT", description = "Source Text to Replace", value = "Search"),
@org.apache.felix.scr.annotations.Property(name = "TARGET-TEXT", description = "Target Replacement Text", value = "Find of Awesomeness")
})
Here you can see that the individual @Property has a number of values that will be visible via the OSGi console:
Property NamePurpose
name The name of the property used as the key to retrieve it later in the configure function
description The text that is used to describe the property in the OSGi property editor
value The default value that should be used for the property in the OSGi console.
*It is worth noting that if you add a subsequent property to a previously configured or saved OSGi console, the default value will not be used. Also, if the default value is changed in code, the default settings will need ot be manually invoked during a deploy, even if the package has been reinstalled.
Each servlet Class is instantiated as a stateless singleton when CQ5 is started, save for any global configuration values. These values need to be loaded from the config when the servlet is activated in the following function:
protected void activate(ComponentContext componentContext){
configure(componentContext.getProperties());
}
The activate function is called via the SlingServlet interface (If you know the specific interface, please comment below, short on time to find it right now.) when the Bundle is activated.
The activate function calls our own configure function, which gets a property dictionary.
protected void configure(Dictionary<?, ?> properties) {
this.pageProvider=OsgiUtil.toString(properties.get(PAGE_PROVIDER), null);
this.sourceText=OsgiUtil.toString(properties.get(SOURCE_TEXT), null);
this.targetText=OsgiUtil.toString(properties.get(TARGET_TEXT), null);
}Here, we set our private variables with the OsgiUtil.toString function. Note that this class has been deprecated, but as of CQ5.5, I am not aware of a cleaner way of doing this. If you know of one, please comment below.
The final thing you need to know is that you must override the following function:
@Override
protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServerException, IOException {
Note, that you have access to both the request and response, and can do whatever you need it to do.
I shall leave it to the reader to go through my code for the page transformation.
Step 3 - Build the servlet
The first thing to make sure of is that your install folder is in the right place. (read Step 1-3 very carefully. IF YOU DO NOT DO THIS, YOUR COMPONENT WILL NOT AUTOMATICALLY INSTALL DURING BUILD).
Double click on the WebpageTransformer.bnd file, and increment the Bundle Version number. If you do not increment the number, CQ5 may get confused and not automatically deploy your bundle.
When you are happy, right click on the ProviderAssets.bnd file, navigate to the Build sub menu, and click on Build Bundle.
If everything goes well, your bundle should be built and deployed.
Step 4 - Configuring your bundle Navigates to your felix OSGi console. http://localhost:4502/system/console/configMgr
Find your WebpageTransformer Bundle in this list. To the right, you should see a pencil icon. If you don't, check your metadata is correct, and contains metatype=true.
@SlingServlet(paths="/bin/mySearchServlet", methods = "GET", metatype=true)
take note of the settings, and make sure that they are default.
Navigate to http://localhost:4502/bin/mySearchServlet
If nothing comes up, you may need to change Apache Sling Servlet/Script Resolver and Error Handler config on this page.
Search for it, and click the pencil to edit your config, and look at your execution paths. You may need to add /bin/ to your execution paths if any are specified.
Once you can see the page, notice that it displays the google page, except the Search button displays Find of Awesomeness as the button label.
You can play with the settings, update them, and see the changes on the servlet page.
Discussion
Sling Servlets are a very handy tool to convert anything in your JCR to JSON, to aggregate and expose data sources to other components and applications in your ecosystem, or to provide an endpoint for the wild for any information you need to supply.
You can integrate the servlet with your security engine, and generally do whatever needs to be done.
Right-click on the transformer folder, and click New -> Class
Name it TransformerClass, and set superclass to be org.apache.sling.api.servlets.SlingSafeMethodsServlet
Click on Finish.
This example takes a webpage and transforms a supplied set of text into another set of text.
Here is the full TransformerClass.java content.
TransformerClass.java
package org.example.slingservlet.transformer;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.rmi.ServerException;
import java.util.Dictionary;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.sling.SlingServlet;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.apache.sling.commons.osgi.OsgiUtil;
import org.apache.sling.jcr.api.SlingRepository;
import org.osgi.service.component.ComponentContext;
@SlingServlet(paths = "/bin/mySearchServlet", methods = "GET", metatype = true)
@Properties({
@org.apache.felix.scr.annotations.Property(name = "PAGE-PROVIDER", description = "Page Provider Address", value = "http://www.google.com"),
@org.apache.felix.scr.annotations.Property(name = "SOURCE-TEXT", description = "Source Text to Replace", value = "Search"),
@org.apache.felix.scr.annotations.Property(name = "TARGET-TEXT", description = "Target Replacement Text", value = "Find of Awesomeness")
})
public class TransformerClass extends SlingSafeMethodsServlet {
private static final long serialVersionUID = 2598426539166789515 L;
public static final String PAGE_PROVIDER = "PAGE-PROVIDER";
public static final String SOURCE_TEXT = "SOURCE-TEXT";
public static final String TARGET_TEXT = "TARGET-TEXT";
private String pageProvider;
private String sourceText;
private String targetText;
@SuppressWarnings("unused")
@Reference
private SlingRepository repository;
protected void activate(ComponentContext componentContext) {
configure(componentContext.getProperties());
}
protected void configure(Dictionary < ? , ? > properties) {
this.pageProvider = OsgiUtil.toString(properties.get(PAGE_PROVIDER), null);
this.sourceText = OsgiUtil.toString(properties.get(SOURCE_TEXT), null);
this.targetText = OsgiUtil.toString(properties.get(TARGET_TEXT), null);
}
@Override
protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServerException, IOException {
URL serverAddress = null;
BufferedReader reader = null;
HttpURLConnection internalHttpRequest = null;
PrintWriter responseWriter = response.getWriter();
try {
serverAddress = new URL(pageProvider);
internalHttpRequest = (HttpURLConnection) serverAddress.openConnection();
InputStream in = null;
try {
in = internalHttpRequest.getInputStream();
} catch (Exception e) {
responseWriter.append(e.toString());
}
if ( in != null) {
reader = new BufferedReader(new InputStreamReader( in ));
}
} finally {
if (reader != null) {
String line = null;
while ((line = reader.readLine()) != null) {
responseWriter.append(line.replace(sourceText, targetText) + "\n");
}
}
responseWriter.flush();
if (internalHttpRequest != null) {
internalHttpRequest.disconnect();
}
}
}
}
Servlet Address
@SlingServlet(paths="/bin/mySearchServlet", methods = "GET", metatype=true)
CQ5 utilizes Java Annotations to configure classes.
Here, we are setting the SlingServlet annotation to have 3 values:
paths, methods, and metatype.
The paths variable allows you to set the url that will resolve to the slingservlet.
In our case, if you access http://localhost:4502/bin/mySearchServlet, you will be served the servlet, rather than whatever may reside there in the JCR.
Methods specify a GET method
The metatype option being set to true instructs CQ5 to provide a config page in the Felix console.
Servlet PropertiesThe @Properties annotation contains multiple @Property annotations contained in our Sling Servlet.
It is worth noting that if you are using javax.jcr.Property to get a property from a JCR Node, you will have a common namespace conflict with the org.apache.felix.scr.annotations.Property.
I personally think that it's best to use org.apache.felix.scr.annotations.Property as the inline declaration, rather than javax.jcr.Property, as the Property is something that is generally still readable in a long format, and won't influence the legibility of your code.
@SlingServlet(paths = "/bin/mySearchServlet", methods = "GET", metatype = true)
@Properties({
@org.apache.felix.scr.annotations.Property(name = "PAGE-PROVIDER", description = "Page Provider Address", value = "http://www.google.com"),
@org.apache.felix.scr.annotations.Property(name = "SOURCE-TEXT", description = "Source Text to Replace", value = "Search"),
@org.apache.felix.scr.annotations.Property(name = "TARGET-TEXT", description = "Target Replacement Text", value = "Find of Awesomeness")
})
Here you can see that the individual @Property has a number of values that will be visible via the OSGi console:
Property NamePurpose
name The name of the property used as the key to retrieve it later in the configure function
description The text that is used to describe the property in the OSGi property editor
value The default value that should be used for the property in the OSGi console.
*It is worth noting that if you add a subsequent property to a previously configured or saved OSGi console, the default value will not be used. Also, if the default value is changed in code, the default settings will need ot be manually invoked during a deploy, even if the package has been reinstalled.
Each servlet Class is instantiated as a stateless singleton when CQ5 is started, save for any global configuration values. These values need to be loaded from the config when the servlet is activated in the following function:
protected void activate(ComponentContext componentContext){
configure(componentContext.getProperties());
}
The activate function is called via the SlingServlet interface (If you know the specific interface, please comment below, short on time to find it right now.) when the Bundle is activated.
The activate function calls our own configure function, which gets a property dictionary.
protected void configure(Dictionary<?, ?> properties) {
this.pageProvider=OsgiUtil.toString(properties.get(PAGE_PROVIDER), null);
this.sourceText=OsgiUtil.toString(properties.get(SOURCE_TEXT), null);
this.targetText=OsgiUtil.toString(properties.get(TARGET_TEXT), null);
}Here, we set our private variables with the OsgiUtil.toString function. Note that this class has been deprecated, but as of CQ5.5, I am not aware of a cleaner way of doing this. If you know of one, please comment below.
The final thing you need to know is that you must override the following function:
@Override
protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServerException, IOException {
Note, that you have access to both the request and response, and can do whatever you need it to do.
I shall leave it to the reader to go through my code for the page transformation.
Step 3 - Build the servlet
The first thing to make sure of is that your install folder is in the right place. (read Step 1-3 very carefully. IF YOU DO NOT DO THIS, YOUR COMPONENT WILL NOT AUTOMATICALLY INSTALL DURING BUILD).
Double click on the WebpageTransformer.bnd file, and increment the Bundle Version number. If you do not increment the number, CQ5 may get confused and not automatically deploy your bundle.
When you are happy, right click on the ProviderAssets.bnd file, navigate to the Build sub menu, and click on Build Bundle.
If everything goes well, your bundle should be built and deployed.
Step 4 - Configuring your bundle Navigates to your felix OSGi console. http://localhost:4502/system/console/configMgr
Find your WebpageTransformer Bundle in this list. To the right, you should see a pencil icon. If you don't, check your metadata is correct, and contains metatype=true.
@SlingServlet(paths="/bin/mySearchServlet", methods = "GET", metatype=true)
take note of the settings, and make sure that they are default.
Navigate to http://localhost:4502/bin/mySearchServlet
If nothing comes up, you may need to change Apache Sling Servlet/Script Resolver and Error Handler config on this page.
Search for it, and click the pencil to edit your config, and look at your execution paths. You may need to add /bin/ to your execution paths if any are specified.
Once you can see the page, notice that it displays the google page, except the Search button displays Find of Awesomeness as the button label.
You can play with the settings, update them, and see the changes on the servlet page.
Discussion
Sling Servlets are a very handy tool to convert anything in your JCR to JSON, to aggregate and expose data sources to other components and applications in your ecosystem, or to provide an endpoint for the wild for any information you need to supply.
You can integrate the servlet with your security engine, and generally do whatever needs to be done.
No comments:
Post a Comment
If you have any doubts or questions, please let us know.