OSGI & Java
CRX
System Console
https://localhost:4502/system/console
Demo: OSGI Config
Using CRXDE, navigate to /apps/PROJECT/osgiconfig/config
Create a node named com.day.cq.commons.servlets.RootMappingServlet.cfg.json
Enter the following:
{
"rootmapping.target": "/crx/de"
}
Click Save All. Now try to access http://localhost:4502.
End of demo.
Activity: Create OSGI Configs
In your project, go to ui.config/src/main/content/jcr_root/apps/ercot/osgiconfig/config and create a file named:
org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~stockImport.cfg.json
Then copy the following json and paste it into that file:
{
"service.ranking:Integer":0,
"user.mapping":"ercot-training.core:training=[stock-import-service]"
}
In the same ui.config/src/main/content/jcr_root/apps/ercot/osgiconfig/config folder, now create a file named:
org.apache.sling.jcr.repoinit.RepositoryInitializer~stockImport.cfg.json
Then copy the following json into that file:
{
"scripts":[
"create service user stock-import-service\n set ACL on /content\n allow jcr:all for stock-import-service\n end\n"
]
}
End of activity.
Activity: Create Scheduled Job
In the code project, go to core/src/main/java/com/ercot/training/core/schedulers.
Create a file named StockImportScheduler.java
In that file, place the following:
package com.ercot.training.core.schedulers;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
//org.apache.sling.event.* api available in Sling 9 documentation:
import org.apache.sling.event.jobs.JobBuilder;
import org.apache.sling.event.jobs.JobBuilder.ScheduleBuilder;
import org.apache.sling.event.jobs.JobManager;
import org.apache.sling.event.jobs.ScheduledJobInfo;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class adds a Sling Job to the job queue so that a job consumer can process
* work. Sling Jobs are guaranteed to be proceed and the scheduler can be configured
* based on an OSGi config node.
*/
@Component(immediate = true,
configurationPid = "com.ercot.training.core.schedulers.StockImportScheduler",
configurationPolicy = ConfigurationPolicy.REQUIRE)
@Designate(ocd = StockImportScheduler.StockImportConfiguration.class, factory=true)
public class StockImportScheduler {
public static final String JOB_TOPIC_STOCKIMPORT = "com/adobe/training/core/jobs/stockimportjob";
public static final String JOB_PROP_SYMBOL = "symbol";
public static final String JOB_PROP_URL = "url";
public static final String DEFAULT_IMPORT_URL = "https://raw.githubusercontent.com/Adobe-Marketing-Cloud/ADLS-Samples/master/stock-data/";
private final Logger logger = LoggerFactory.getLogger(getClass());
// Convenience string to find the log messages for this training example class
// Logs can be found in crx-quickstart/logs/error.log
private String searchableLogStr = "*****";
@ObjectClassDefinition(name = "Training Stock Importer")
public @interface StockImportConfiguration {
@AttributeDefinition(
name = "Stock Symbol",
description = "Characters representing the stock to be imported",
type = AttributeType.STRING
)
public String symbol() default "";
@AttributeDefinition(
name = "Expression",
description = "Run every so often as defined in the cron-job expression.",
type = AttributeType.STRING
)
public String cronExpression() default "0 0/2 * * * ?";
@AttributeDefinition(
name = "Stock URL",
description = "URL to request the stock data to be imported",
type = AttributeType.STRING
)
public String stock_url() default DEFAULT_IMPORT_URL;
}
@Reference
private JobManager jobManager;
private int schedulerID;
private JobBuilder jobBuilder;
private ScheduleBuilder scheduleBuilder;
private ScheduledJobInfo theScheduledJob;
@Activate
protected void activate(StockImportConfiguration config) {
logger.info(searchableLogStr + "StockImport ScheduledJob '{}' with ID: '{}' Activated", config.symbol(), schedulerID);
schedulerID = config.symbol().hashCode();
startJobScheduler(config);
}
@Modified
protected void modified(StockImportConfiguration config) {
removeJobScheduler(config);
schedulerID = config.symbol().hashCode() + 1; //updates schedulerID
startJobScheduler(config);
}
@Deactivate
protected void deactivate(StockImportConfiguration config) {
removeJobScheduler(config);
}
private void startJobScheduler(StockImportConfiguration config){
jobBuilder = jobManager.createJob(StockImportScheduler.JOB_TOPIC_STOCKIMPORT);
// Create a properties map that contains the configurations we want to pass to the job
HashMap<String, Object> jobProps = new HashMap<>();
jobProps.put(JOB_PROP_SYMBOL, config.symbol());
jobProps.put(JOB_PROP_URL, config.stock_url());
jobBuilder.properties(jobProps);
scheduleBuilder = jobBuilder.schedule();
scheduleBuilder.cron(config.cronExpression());
theScheduledJob = scheduleBuilder.add();
if(theScheduledJob == null){
List<String> errors = new ArrayList<>();
scheduleBuilder.add(errors);
} else {
logger.info(searchableLogStr + "ScheduledJob added to the Queue. Topic: " + theScheduledJob.getJobTopic() + " "
+ "Properties: " + theScheduledJob.getJobProperties().toString() + " "
+ "Next Execution: " + theScheduledJob.getNextScheduledExecution().toString());
}
}
private void removeJobScheduler(StockImportConfiguration config) {
if(theScheduledJob != null) {
logger.info(searchableLogStr + "Removing '{}' ScheduledJob, with ID: '{}'", config.symbol(), schedulerID);
theScheduledJob.unschedule();
}
}
}
Build with
mvn clean install -pl core -PautoInstallBundle
This will only build the java code in the core module, instead of the entire project.
Now, create configurations to activate the scheduled jobs.
In the ui.config/src/main/content/jcr_root/apps/ercot/osgiconfig/config folder, create:
com.ercot.training.core.schedulers.StockImportScheduler~adbe.cfg.json
Fill it with:
{
"symbol":"ADBE",
"stock.url":"https://raw.githubusercontent.com/Adobe-Marketing-Cloud/ADLS-Samples/master/stock-data/",
"cronExpression":"0 0/2 * * * ?"
}
Now create com.ercot.training.core.schedulers.StockImportScheduler~msft.cfg.json and fill it with:
{
"symbol":"MSFT",
"stock.url":"https://raw.githubusercontent.com/Adobe-Marketing-Cloud/ADLS-Samples/master/stock-data/",
"cronExpression":"0 0/2 * * * ?"
}
Run the following to deploy the configuration package
mvn clean install -pl ui.config -PautoInstallPackage
If your editor has an AEM sync plugin, that can be used instead.
Then create the job consumer.
Go to core/src/main/java/com/ercot/training/core and create StockDataWriterJob.java
Fill it with the following:
package com.ercot.training.core;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import javax.jcr.RepositoryException;
import javax.net.ssl.HttpsURLConnection;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ModifiableValueMap;
import org.apache.sling.api.resource.PersistenceException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.event.jobs.Job;
import org.apache.sling.event.jobs.consumer.JobConsumer;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.day.cq.commons.jcr.JcrConstants;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ercot.training.core.schedulers.StockImportScheduler;
/**
* This job consumer takes in a data source url and stock symbol
* and saves the stock quote response to the JCR
*
* /content/stocks/
* + <STOCK_SYMBOL> [sling:OrderedFolder]
*/
@Component(
immediate = true,
service = JobConsumer.class,
property ={
JobConsumer.PROPERTY_TOPICS +"="+ StockImportScheduler.JOB_TOPIC_STOCKIMPORT
}
)
public class StockDataWriterJob implements JobConsumer {
private final Logger logger = LoggerFactory.getLogger(getClass());
// Convenience string to find the log messages for this training example class
// Logs can be found in crx-quickstart/logs/error.log
private String searchableLogStr = "&&&&&";
//Public values for stock data
public static final String STOCK_IMPORT_FOLDER = "/content/stocks";
public static final String COMPANY = "companyName";
public static final String SECTOR = "sector";
public static final String LASTTRADE = "lastTrade";
public static final String UPDATETIME = "timeOfUpdate";
public static final String DAYOFUPDATE = "dayOfLastUpdate";
public static final String OPENPRICE = "openPrice";
public static final String RANGEHIGH = "rangeHigh";
public static final String RANGELOW = "rangeLow";
public static final String VOLUME = "volume";
public static final String UPDOWN = "upDown";
public static final String WEEK52LOW = "week52Low";
public static final String WEEK52HIGH = "week52High";
public static final String YTDCHANGE = "ytdPercentageChange";
@Reference
private ResourceResolverFactory resourceResolverFactory;
/**
* Method that runs on the desired schedule.
* Request the data with the stock symbol and get the returned JSON
* Write the JSON to the JCR
* @return JobConsumer.JobResult.[ OK | FAILED ]
*/
@Override
public JobResult process(Job job) {
//extract properties added to the Job in Scheduler:
String symbol = job.getProperty(StockImportScheduler.JOB_PROP_SYMBOL).toString().toUpperCase();
String stock_url = job.getProperty(StockImportScheduler.JOB_PROP_URL).toString();
//https://raw.githubusercontent.com/Adobe-Marketing-Cloud/ADLS-Samples/master/stock-data/
String stockUrl = stock_url + symbol + ".json";
HttpsURLConnection request = null;
try {
URL sourceUrl = new URL(stockUrl);
request = (HttpsURLConnection) sourceUrl.openConnection();
request.setConnectTimeout(5000);
request.setReadTimeout(10000);
request.connect();
// Convert data return to a JSON object
ObjectMapper objMapper = new ObjectMapper();
JsonFactory factory = new JsonFactory();
JobResult jobResult = null;
if(request != null) {
//Create a JsonParser based on the stream from the request content
try(JsonParser parser = factory.createParser(new InputStreamReader((InputStream) request.getContent()))){
//Create a Map from the JsonParser
Map<String, String> allQuoteData = objMapper.readValue(parser,
new TypeReference<Map<String,String>>(){});
logger.info("Last trade for stock symbol {} was {}", symbol, allQuoteData.get("latestPrice"));
//Use the map to write nodes and properties to the JCR
jobResult = writeToRepository(symbol, allQuoteData);
}
catch (RepositoryException e) {
logger.error(searchableLogStr + "Cannot write stock info for " + symbol + " to the JCR: ", e);
return JobConsumer.JobResult.FAILED;
} catch (JsonParseException e) {
logger.error(searchableLogStr + "Cannot parse stock info for " + symbol, e);
return JobConsumer.JobResult.FAILED;
} catch (IOException e) {
logger.error(searchableLogStr + "IOException: ", e);
return JobConsumer.JobResult.FAILED;
}
}
return jobResult;
} catch (SocketTimeoutException e) {
logger.error(searchableLogStr + "Five Second Timeout occured.");
return JobConsumer.JobResult.FAILED;
}
catch (IOException e) {
logger.error(searchableLogStr + "The stock symbol: " + symbol + " does not exist...");
return JobConsumer.JobResult.FAILED;
}
}
/**
* Creates the stock data structure
*
* + <STOCK_SYMBOL> [sling:OrderedFolder]
* + trade [nt:unstructured]
* - companyName = <value>
* - sector = <value>
* - lastTrade = <value>
* - timeOfUpdate = <value>
* - dayOfLastUpdate = <value>
* - openPrice = <value>
* - rangeHigh = <value>
* - rangeLow = <value>
* - volume = <value>
* - upDownPrice = <value>
* - week52High = <value>
* - week52Low = <value>
* - ytdChange = <value>
* @return JobConsumer.JobResult.[ OK | FAILED ]
*/
private JobResult writeToRepository(String stockSymbol, Map<String, String> quoteData) throws RepositoryException {
logger.info(searchableLogStr + "Stock Symbol: " + stockSymbol);
logger.info(searchableLogStr + "JsonObject to Write: " + quoteData.toString());
//Get the service user (stock-import-service) that belongs to the training.core:training subservice
Map<String, Object> serviceParams = new HashMap<>();
serviceParams.put(ResourceResolverFactory.SUBSERVICE, "training");
try (ResourceResolver resourceResolver = resourceResolverFactory
.getServiceResourceResolver(serviceParams)) {
// Transform the time stamp into a readable format
ZoneId timeZone = ZoneId.of("America/New_York");
long latestUpdateTime = Long.parseLong(quoteData.get("latestUpdate"));
LocalDateTime timePerLatestUpdate = LocalDateTime.ofInstant(Instant.ofEpochMilli(latestUpdateTime),
timeZone);
ZonedDateTime timeWithZone = ZonedDateTime.of(timePerLatestUpdate, timeZone);
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("hh:mm a zz");
//will store timeOfUpdate as: Hour:Minute AM/PM, TimeZone e.g. 11:34 AM, EDT
String UpdateTimeOfDay = timeWithZone.format(timeFormatter);
DateTimeFormatter dayFormatter = DateTimeFormatter.ofPattern("E MMMM d, yyyy");
String dayOfUpdate = timeWithZone.format(dayFormatter);
//Create variables in specific data type and put them into a map
Double lastPrice = Double.parseDouble(quoteData.get("latestPrice"));
Double open = Double.parseDouble(quoteData.get("open"));
Double high = Double.parseDouble(quoteData.get("high"));
Double low = Double.parseDouble(quoteData.get("low"));
Long latestVolume = Long.parseLong(quoteData.get("latestVolume"));
Double change = Double.parseDouble(quoteData.get("change"));
Double week52High = Double.parseDouble(quoteData.get("week52High"));
Double week52Low = Double.parseDouble(quoteData.get("week52Low"));
Double ytdChange = Double.parseDouble(quoteData.get("ytdChange"));
String stockPath = STOCK_IMPORT_FOLDER + "/" + stockSymbol;
//Test if stock import folder exists, otherwise create it
Resource stockFolder = ResourceUtil.getOrCreateResource(resourceResolver, stockPath, "", "", false);
//Check to see if the trade node exists. If not, create it
String tradePath = stockPath + "/trade";
Resource trade = resourceResolver.getResource(tradePath);
if (trade == null) {
// set jcr:primaryType to nt:unstructured when resource is created
Map<String,Object> stockData = new HashMap<String,Object>() {
private static final long serialVersionUID = 1L;
{
put(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED);
}};
trade = resourceResolver.create(stockFolder, "trade", stockData);
}
ModifiableValueMap stockData = trade.adaptTo(ModifiableValueMap.class);
stockData.put(COMPANY, quoteData.get("companyName"));
stockData.put(SECTOR, quoteData.get("sector"));
stockData.put(UPDATETIME, UpdateTimeOfDay);
stockData.put(DAYOFUPDATE, dayOfUpdate);
stockData.put(LASTTRADE, lastPrice);
stockData.put(OPENPRICE, open);
stockData.put(RANGEHIGH, high);
stockData.put(RANGELOW, low);
stockData.put(VOLUME, latestVolume);
stockData.put(UPDOWN,change );
stockData.put(WEEK52HIGH,week52High);
stockData.put(WEEK52LOW,week52Low);
stockData.put(YTDCHANGE,ytdChange);
resourceResolver.commit(); //Write data into the JCR
logger.info(searchableLogStr + "Updated trade data for " + stockSymbol);
} catch (LoginException | PersistenceException e) {
logger.error(searchableLogStr + "Exception with writing resource: ", e);
return JobConsumer.JobResult.FAILED;
}
return JobConsumer.JobResult.OK;
}
}
End of activity.
Activity: Create Sling Resource Listener
Go to core/src/main/java/com/ercot/training/core/listeners, and create StockListener.java
Fill it with:
package com.ercot.training.core.listeners;
import java.io.IOException;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import com.ercot.training.core.StockDataWriterJob;
import com.ercot.training.core.schedulers.StockImportScheduler;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.PersistenceException;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.api.resource.observation.ResourceChange;
import org.apache.sling.api.resource.observation.ResourceChangeListener;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This Sling listener listens to the StockDataWriterJob.STOCK_IMPORT_FOLDER location and creates a
* new scheduler config for each new stock folder added.
*
* To add a symbol from the UI, go to AEM Navigation > Sites > stocks and click the blue Create > Folder
* Add the Stock symbol as the Title. Dummy stock data is available for ADBE,MSFT,GOOG,AMZN,APPL,WDAY
*
* Learn more about creating OSGi configurations programmatically:
* http://www.nateyolles.com/blog/2015/10/updating-osgi-configurations-in-aem-and-sling
*/
@Component( immediate = true,
property = {"resource.paths=" + StockDataWriterJob.STOCK_IMPORT_FOLDER,
"resource.change.types=ADDED",
"resource.change.types=REMOVED"
})
public class StockListener implements ResourceChangeListener{
private final String stockImportSchedulerPID = "com.ercot.training.core.schedulers.StockImportScheduler";
private final Logger logger = LoggerFactory.getLogger(getClass());
// Convenience string to find the log messages for this training example class
// Logs can be found in crx-quickstart/logs/error.log
private String searchableLogStr = "$$$$$";
// Service to get OSGi configurations
@Reference
private ConfigurationAdmin configAdmin;
// Service to add/remove resources if needed
@Reference
private ResourceResolverFactory resourceResolverFactory;
@Override
public void onChange(List<ResourceChange> changes) {
for (final ResourceChange change : changes) {
logger.info(searchableLogStr + "Resource Change Detected: {}", change);
//Get the folder name from the path. Ex: /content/stocks/adbe > adbe
String folderName = change.getPath().substring(change.getPath().lastIndexOf("/")+1);
//In this example a stock symbol must be 4 characters and not be the 'trade' node from the StockDataWriterJob
if((folderName.length() == 4) && (folderName.matches("^[a-zA-Z]*$")) && !folderName.equals("trade")) {
//Check if the added folder is uppercase. If it's not, autofix
if(!folderName.equals(folderName.toUpperCase())&&change.getType().equals(ResourceChange.ChangeType.ADDED)){
//Get the service user (stock-import-service) that belongs to the training.core:training subservice
Map<String, Object> serviceParams = new HashMap<>();
serviceParams.put(ResourceResolverFactory.SUBSERVICE, "training");
try (ResourceResolver resourceResolver = resourceResolverFactory.getServiceResourceResolver(serviceParams)) {
logger.info(searchableLogStr + "Folder added is not uppercase. Recreating resource: "+ change.getPath());
//Remove resource
resourceResolver.delete(resourceResolver.getResource(change.getPath()));
//Recreate resource as uppercase
ResourceUtil.getOrCreateResource(resourceResolver, change.getPath().replace(folderName, folderName.toUpperCase()), "", "", false);
resourceResolver.commit();
} catch (LoginException | PersistenceException e) {
logger.error(searchableLogStr + "Exception with updating resource to uppercase ", e);
}
}
//Create a StockImportScheduler config for the symbol folder added
else if(change.getType().equals(ResourceChange.ChangeType.ADDED)) {
try{
//Get the StockImportScheduler factory from the config admin
Configuration config = configAdmin.createFactoryConfiguration(stockImportSchedulerPID);
//Add the folder name to the configuration
Dictionary<String, Object> properties = new Hashtable<String, Object>();
properties.put(StockImportScheduler.JOB_PROP_SYMBOL, folderName);
config.update(properties);
logger.info(searchableLogStr + "Added " + folderName + " config with PID: "+ config.getPid());
} catch (IOException e) {
logger.error(searchableLogStr + "Could not add OSGi config for: " + folderName);
}
}
//Remove the StockImportScheduler config for the symbol folder removed
else if (change.getType().equals(ResourceChange.ChangeType.REMOVED)) {
try {
String filter = '(' + ConfigurationAdmin.SERVICE_FACTORYPID + '=' + stockImportSchedulerPID + ')';
//Find all the StockImportScheduler configs
Configuration[] configArray = configAdmin.listConfigurations(filter);
//Find the config that matches the removed folder name and delete the config
for( Configuration config :configArray) {
Object configSymbolPropVal = config.getProperties().get(StockImportScheduler.JOB_PROP_SYMBOL);
if(configSymbolPropVal.equals(folderName)) {
logger.info(searchableLogStr + "Removed " + folderName + " config with PID: "+ config.getPid());
config.delete();
}
}
} catch (IOException e) {
logger.error(searchableLogStr + "Could not delete OSGi config for: " + folderName);
} catch (InvalidSyntaxException e) {
logger.error(searchableLogStr + "Could not delete OSGi config for: " + folderName);
}
}
}
}
}
}
mvn clean install -pl core -PautoInstallBundle
In the Sites console, navigate to the Stocks folder.
Inside of it, create a folder with one of these sample stock names:
- AMZN
- APPL
- GOOG
- MSFT
- WDAY
On the next 2-minute interval, check in CRXDE to see if the chosen folder has a node named trade with properties.
End of activity.
Reference Links
Configurations and the Configuration Browser