I'm adding a feature to some longstanding, complex integration code in AEM 6.5 and discovering existing problems with invalid resource resolvers. Adding the feature is making them worse. This is some of the oldest code in our system, and has gone through several rounds of refactoring and other updates.
I'm unclear about the best way to manage the resource resolver and associated objects, especially as control goes from a service to Sling models that use a resolver or session to traverse or search the JCR. I'm hoping someone can review my code and give me some advice (@joerghoh , possible to get your thoughts?) Here's the general overview, followed by VERY stripped-down versions of the classes involved:
- We store a hierarchy of inventory in the AEM JCR, and update it with messages from an upstream system. The updates can affect a number of objects in the JCR. We combine those changes into models for another external DB and send outbound updates.
- Messages are received by a servlet (not shown) and stored in nodes in the JCR. A separate job is scheduled to run periodically and process the messages:
- MyScheduledTask.run() reads the messages, updates the JCR, and calls a service to update the external DB.
- dbUploadService.updateIndexFor() does a QueryBuilder search the JCR nodes affected by the update. For each hit, the service passes the hit.resource to modelFactory.createModel() to hydrate the index model which in turn hydrates entity models.
There are two types of models: a mostly POJO "index" model and familiar Sling Model "entities" which read data from the JCR. The index models instantiate various entity models, pull various model properties into a map, and that map gets serialized to JSON and sent to the external DB. The index models don't declare a resolver -- I'm not clear on what they're using when they do path traversal. The entity models declare an injected @SlingObject ResourceResolver.
I'm getting "Resource resolver is already closed" from the scheduled task when calling getPath() or listChildren() on a resource.
I'm getting "Resource resolver is already closed" and "This session has been closed" exceptions from the entity models when they try to do path traversal via getParent(), or when the upload service is trying to execute a query.
Any help appreciated!
/************** SCHEDULED TASK *********************/
public class MyScheduledTask implements Runnable {
@Reference private ResourceResolverFactory resourceResolverFactory;
@Reference private DbUploadService dbUploadService;
private ResourceResolver resourceResolver;
private ResourceResolver getResourceResolver() {
if (resourceResolver == null || !resourceResolver.isLive()) {
try {
// I'm getting the resolver from a factory, not the Sling request,
// because this job runs on a schedule rather than on a servlet request
Map<String, Object> serviceUserParam = new HashMap<String, Object>();
serviceUserParam.put(ResourceResolverFactory.SUBSERVICE, "dbServiceUser");
resourceResolver = resourceResolverFactory.getServiceResourceResolver(serviceUserParam);
} catch (Exception e) {
logger.error("Unable to obtain resource resolver", e);
return null; // return here so we don't return a dead resolver
}
}
return resourceResolver;
}
private Session session;
private Session getSession() {
if (this.session == null || !this.session.isLive()) {
session = this.getResourceResolver().adaptTo(Session.class);
}
return session;
}
private PageManager pageManager;
private PageManager getPageManager() {
if (pageManager == null) { // TODO: also check the resolver status?
pageManager = this.getResourceResolver().adaptTo(PageManager.class);
}
return pageManager;
}
private ArrayList<String> activationList;
@Override
public void run() {
try {
Resource msgFolder = this.getResourceResolver().getResource("/path/to/message/nodes");
this.activationList = new ArrayList<>();
Iterator<Resource> tasks = msgFolder.listChildren();
while (tasks.hasNext()) {
Resource task = tasks.next();
if (task.getValueMap().containsKey("message")) {
processItem(task.getValueMap().get("message", String.class));
this.getResourceResolver().delete(task);
this.getSession().save();
}
}
Iterator<String> activationIter = activationList.iterator();
while (activationIter.hasNext()) {
Resource resourceToActivate = this.getResourceResolver().getResource(activationIter.next());
// check activation status, call replicator if last status was Activate
}
} catch (Exception e) {
logger.error(e);
} finally { // I could replace this with try-with-resources
if (this.resourceResolver != null && this.resourceResolver.isLive()) {
this.resourceResolver.close();
}
}
}
private void processItem(String message) {
String idToFind = getIdFrom(message);
Page thePage = findPage(this.getPageManager().getPage("/content/base/page/path"), idToFind);
// lots of processing to update the item with values from the message, save the session
thePage.getContentResource().adaptTo(Node.class).setProperty("foo", "value from message.foo");
this.getSession().save();
activationList.add(thePage.getPath()); // so caller can try to replicate
// call a service to hydrate Sling models, send them to an external database
updateExternalDb(idToFind, thePage.getContentResource());
}
private Page findPage(Page parentPage, String entityId) {
Iterator<Page> children = parentPage.listChildren(); // <--- crash here, resolver is already closed
while (children.hasNext()) {
Page currentPage = children.next();
ValueMap pageProps = currentPage.getProperties();
if (pageProps.containsKey("entityId") && pageProps.get("entityId", "").equals(entityId)) {
return currentPage;
}
}
return null;
}
private void updateExternalDb(String id, Resource contentResource) {
String changeRoot = contentResource.getParent().getPath();
dbUploadService.updateIndexFor(id, "buildingId", changeRoot, Constants.MIXIN_BUILDING));
dbUploadService.updateIndexFor(id, "buildingId", changeRoot, Constants.MIXIN_SUITE);
}
}
/************************** DB UPLOAD SERVICE ********************************/
@Service
@Component
public class DbUploadServiceImpl implements DbUploadService {
@Reference private ResourceResolverFactory resourceResolverFactory;
@Reference private ModelFactory modelFactory;
private ResourceResolver resourceResolver;
private ResourceResolver getResourceResolver() {
if (resourceResolver == null || !resourceResolver.isLive()) {
try { Map<String, Object> serviceUserParam = new HashMap<String, Object>();
serviceUserParam.put(ResourceResolverFactory.SUBSERVICE, "dbServiceUser");
this.resourceResolver = resourceResolverFactory.getServiceResourceResolver(serviceUserParam);
} catch (Exception e) {
LOG.error("Unable to obtain resource resolver");
return null; // return here so we don't return a dead resolver
}
}
return this.resourceResolver;
}
private Session session;
private Session getSession() {
if (this.session == null || !this.session.isLive()) {
this.session = this.getResourceResolver().adaptTo(Session.class);
}
return this.session;
}
@Activate
protected void activate(ComponentContext context) throws RepositoryException {
this.resolver = this.getResourceResolver();
}
/**
* Given the ID and type of a changed entity, search for all children of a given type
* under the changed entity and update the corresponding DB index.
*/
public void updateIndexFor(
String changedEntityId,
String changedEntityIdProp,
String changedEntitySearchPathRoot,
String childTypeToFind
) {
Map<String, String> map;
Query query;
map = new HashMap<String, String>();
map.put("path", changedEntitySearchPathRoot);
map.put("type", childTypeToFind);
map.put("property", changedEntityIdProp);
map.put("property.value", changedEntityId);
query = getQueryBuilder().createQuery(PredicateGroup.create(map), getSession());
query.setHitsPerPage(0);
SearchResult results = query.getResult();
this.upload(results);
}
@Override
public void upload(SearchResult results) {
Map<String, List<DbIndexModel>> indexModels = new HashMap<String, List<DbIndexModel>>();
DBIndexModel indexModel = null;
String indexName = null;
Iterator<Resource> it = results.getResources();
while (it.hasNext()) {
Resource entity = it.next();
try {
switch (InventoryEntityTypeEnum.getEnum(entityType)) {
case BUILDING:
indexModel = modelFactory.createModel(entity, BuildingIndexModel.class);
indexName = "BuildingIndex";
break;
case SUITE:
indexModel = modelFactory.createModel(entity, SuiteIndexModel.class);
indexName = "SuiteIndex";
break;
default:
return;
}
indexModels.add(indexModel);
} catch (Exception e) {
LOG.error("Exception creating model of entity type '{}' with ID '{}'...", entityType, nodeId);
return;
}
}
upload(indexModels, indexName); // mechanics of serializing models to JSON, sending data to DB
}
}
/******** DB INDEX models -- aggregate fields from entity models (below) for updating external DB ***/
@Model(adaptables = Resource.class)
public class BuildingIndexModel extends BaseDbIndexModel {
private MarketEntity market;
private SubmarketEntity submarket;
private PropertyEntity property;
private BuildingEntity building;
private OfficeLocationEntity leasingOffice;
@PostConstruct
public void init() {
try {
// home/locations/{market}/{submarket}/{property}/jcr:content/buildings/{building}
// ^ parent ^ parent ^ parent ^ self
this.building = self.adaptTo(BuildingEntity.class);
// /{property}/jcr:content /buildings /{building} <- self
Resource propertyPage = self.getParent().getParent().getParent();
this.property = propertyPage.getChild(JcrConstants.JCR_CONTENT).adaptTo(PropertyEntity.class);
// /{submkt} /{property} <- propertyPage
Resource submarketPage = propertyPage.getParent();
this.submarket = submarketPage.getChild(JcrConstants.JCR_CONTENT).adaptTo(SubmarketEntity.class);
Resource marketPage = submarketPage.getParent();
this.market = marketPage.getChild(JcrConstants.JCR_CONTENT).adaptTo(MarketEntity.class);
// find closest non-empty leasingOfficePath in hierarchy from bottom to top
String officePath = building.getLeasingOfficePath();
if (StringUtils.isBlank(officePath)) { officePath = property .getLeasingOfficePath(); }
if (StringUtils.isBlank(officePath)) { officePath = submarket .getLeasingOfficePath(); }
if (StringUtils.isBlank(officePath)) { officePath = market .getLeasingOfficePath(); }
this.leasingOffice = LeasingOfficeService.getOfficeFromPath(officePath, self.getResourceResolver());
} catch (Exception e) {
LOG.error("{} exception in BuildingIndexModel @PostConstruct method:", e.getCause());
}
}
@Override public String getObjectID() { return building.getNodeId(); }
@Override public String getEntityType() { return building.getEntityType(); }
@Override public String getName() { return building.getName(); }
@Override
public Map<String, Object> getModel() {
Map<String, Object> model = new HashMap<String, Object>();
model.put("objectID", getObjectID());
model.put("entityType", getEntityType());
model.put("leasingOfficePath", leasingOffice.getPath());
model.put("leasingOfficeAddress", leasingOffice.getAddress());
model.put("leasingOfficePhone", leasingOffice.getPhone());
model.put("marketId", market.getMarketId());
model.put("marketName", market.getMarketName());
model.put("marketPagePath", market.getPath());
model.put("marketThumbnailImages", market.getThumbnailImageUrls());
model.put("marketSearchDescription", market.getSearchDescription());
model.put("propertyId", property.getPropertyId());
model.put("propertyName", property.getName());
model.put("propertyPagePath", property.getPath());
model.put("propertyThumbnailImages", property.getThumbnailImageUrls());
model.put("hasCommonsArea", property.getHasCommonsArea());
model.put("hasConferenceCenter", property.getHasConferenceCenter());
model.put("buildingId", building.getBuildingId());
model.put("buildingName", building.getName());
model.put("buildingStreetAddress", building.getAddress());
model.put("buildingPath", building.getPath());
model.put("buildingThumbnailImages", building.getThumbnailImageUrls());
model.put("isEnergyStarCertified", building.getIsEnergyStarCertified());
return model;
}
}
abstract class BaseDbIndexModel implements DbIndexModel {
@Self protected Resource self;
// dependencies
@Reference protected ModelFactory modelFactory;
public BaseDbIndexModel() {} // required parameterless constructor
public String getModelInfo() {
return String.format( "%s '%s' (id '%s')", getEntityType(), getName(), getObjectID() );
}
@Override
public String toJsonString() {
Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
return gson.toJson( this.getModel() );
}
}
/*********************************** Entity models -- extract data from JCR ***/
@Model(adaptables = Resource.class)
public class MarketEntity extends BaseLocationEntity {
// /content/icop/ico/home/locations/{market} <- self
// model fields from JCR:
@Inject private String marketId;
@Inject private String marketName;
// expose the model:
public String getMarketId() { return marketId; }
public String getMarketName() { return marketName; }
public String getPath() { return self.getParent().getPath(); } // <-- crashes, resolver is closed
public Map<String, String> getThumbnailImageUrls() { return getThumbnailImageUrls(self); }
// fulfill interface contract
@Override public String getName() { return marketName; }
@Override public String getNodeId() { return marketId; }
}
@Model(adaptables = Resource.class)
public class BaseLocationEntity implements LocationEntity {
@Self protected Resource self;
@SlingObject protected ResourceResolver resolver;
@Inject @Optional @Named("leasingOfficeLocationPath") private String leasingOfficePath;
@Inject @Optional private String entityType;
@Inject @Optional protected String name;
@Inject @Optional private String nodeId;
public String getLeasingOfficePath() { return leasingOfficePath; }
public String getPath() { return self.getPath(); }
public String getModelInfo() { return String.format( "%s '%s' (id '%s')", getEntityType(), getName(), getNodeId()); }
}