package com.adobe.acs.commons.synth.children;
import org.apache.commons.collections.IteratorUtils;
import org.apache.jackrabbit.JcrConstants;
import org.apache.sling.api.resource.ModifiableValueMap;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceWrapper;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.wrappers.ValueMapDecorator;
import org.apache.sling.commons.json.JSONException;
import org.apache.sling.commons.json.JSONObject;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.RepositoryException;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
/**
* Class to wrapper a real resource to facilitate the persistence of children resources in a property (serialized as
* JSON).
*
* Can be used as follows...
*
* To write data:
*
* Resource real = resolve.getResource("/content/real");
* ChildrenAsPropertyResource wrapper = new ChildrenAsPropertyResource(real);
* Resource child = wrapper.create("child-1", "nt:unstructured");
* ModifiableValueMap mvm = child.adaptTo(ModifiableValueMap.class);
* mvm.put("prop-1", "some data");
* mvm.put("prop-2", Calendar.getInstance());
* wrapper.persist();
* resolver.commit();
*
* To read data:
*
* Resource real = resolve.getResource("/content/real");
* ChildrenAsPropertyResource wrapper = new ChildrenAsPropertyResource(real);
* for(Resource child : wrapper.getChildren()) {
* child.getValueMap().get("prop-1", String.class);
* }
*
*/
public class ChildrenAsPropertyResource extends ResourceWrapper {
private static final Logger log = LoggerFactory.getLogger(ChildrenAsPropertyResource.class);
private static final String EMPTY_JSON = "{}";
private static final String DEFAULT_PROPERTY_NAME = "children";
private final Resource resource;
private final String propertyName;
private Map<String, Resource> lookupCache = null;
private Set<Resource> orderedCache = null;
private Comparator<Resource> comparator = null;
public static final Comparator<Resource> RESOURCE_NAME_COMPARATOR = new ResourceNameComparator();
/**
* ResourceWrapper that allows resource children to be modeled in data stored into a property using the default
* property name of "children".
*
* @param resource the resource to store the children as properties on
* @throws InvalidDataFormatException
*/
public ChildrenAsPropertyResource(Resource resource) throws InvalidDataFormatException {
this(resource, DEFAULT_PROPERTY_NAME, null);
}
/**
* ResourceWrapper that allows resource children to be modeled in data stored into a property.
*
* @param resource the resource to store the children as properties on
* @param propertyName the property name to store the children as properties in
*/
public ChildrenAsPropertyResource(Resource resource, String propertyName) throws InvalidDataFormatException {
this(resource, propertyName, null);
}
/**
* ResourceWrapper that allows resource children to be modeled in data stored into a property.
*
* @param resource the resource to store the children as properties on
* @param propertyName the property name to store the children as properties in
* @param comparator the comparator used to order the serialized children
* @throws InvalidDataFormatException
*/
public ChildrenAsPropertyResource(Resource resource, String propertyName, Comparator<Resource> comparator)
throws InvalidDataFormatException {
super(resource);
this.resource = resource;
this.propertyName = propertyName;
this.comparator = comparator;
if (this.comparator == null) {
this.orderedCache = new LinkedHashSet<Resource>();
} else {
this.orderedCache = new TreeSet<Resource>(this.comparator);
}
this.lookupCache = new HashMap<String, Resource>();
for (SyntheticChildAsPropertyResource r : this.deserialize()) {
this.orderedCache.add(r);
this.lookupCache.put(r.getName(), r);
}
}
/**
* {@inheritDoc}
**/
@Override
public final Iterator<Resource> listChildren() {
return IteratorUtils.getIterator(this.orderedCache);
}
/**
* {@inheritDoc}
**/
@Override
public final Iterable<Resource> getChildren() {
return this.orderedCache;
}
/**
* {@inheritDoc}
**/
@Override
public final Resource getChild(String name) {
return this.lookupCache.get(name);
}
/**
* {@inheritDoc}
**/
@Override
public final Resource getParent() {
return this.resource;
}
public final Resource create(String name, String primaryType) throws RepositoryException {
return create(name, primaryType, null);
}
public final Resource create(String name, String primaryType, Map<String, Object> data) throws RepositoryException {
if (data == null) {
data = new HashMap<String, Object>();
}
if (data.containsKey(JcrConstants.JCR_PRIMARYTYPE) && primaryType != null) {
data.put(JcrConstants.JCR_PRIMARYTYPE, primaryType);
}
final SyntheticChildAsPropertyResource child =
new SyntheticChildAsPropertyResource(this.resource, name, data);
if (this.lookupCache.containsKey(child.getName())) {
log.info("Existing synthetic child [ {} ] overwritten", name);
}
this.lookupCache.put(child.getName(), child);
this.orderedCache.add(child);
return child;
}
/**
* Deletes the named child.
*
* Requires subsequent call to persist().
*
* @param name the child node name to delete
* @throws RepositoryException
*/
public final void delete(String name) throws RepositoryException {
if (this.lookupCache.containsKey(name)) {
Resource tmp = this.lookupCache.get(name);
this.orderedCache.remove(tmp);
this.lookupCache.remove(name);
}
}
/**
* Delete all children.
*
* Requires subsequent call to persist().
*
* @throws InvalidDataFormatException
*/
public final void deleteAll() throws InvalidDataFormatException {
// Clear the caches; requires serialize
if (this.comparator == null) {
this.orderedCache = new LinkedHashSet<Resource>();
} else {
this.orderedCache = new TreeSet<Resource>(this.comparator);
}
this.lookupCache = new HashMap<String, Resource>();
}
/**
* Persist changes to the underlying valuemap so they are available for persisting to the JCR.
*
* @throws RepositoryException
*/
public final void persist() throws RepositoryException {
this.serialize();
}
/**
* Serializes all children data as JSON to the resource's propertyName.
*
* @throws InvalidDataFormatException
*/
private void serialize() throws InvalidDataFormatException {
final long start = System.currentTimeMillis();
final ModifiableValueMap modifiableValueMap = this.resource.adaptTo(ModifiableValueMap.class);
JSONObject childrenJSON = new JSONObject();
try {
// Add the new entries to the JSON
for (Resource childResource : this.orderedCache) {
childrenJSON.put(childResource.getName(), this.serializeToJSON(childResource));
}
if (childrenJSON.length() > 0) {
// Persist the JSON back to the Node
modifiableValueMap.put(this.propertyName, childrenJSON.toString());
} else {
// Nothing to persist; delete the property
modifiableValueMap.remove(this.propertyName);
}
log.debug("Persist operation for [ {} ] in [ {} ms ]",
this.resource.getPath() + "/" + this.propertyName,
System.currentTimeMillis() - start);
} catch (JSONException e) {
throw new InvalidDataFormatException(this.resource, this.propertyName, childrenJSON.toString());
} catch (NoSuchMethodException e) {
throw new InvalidDataFormatException(this.resource, this.propertyName, childrenJSON.toString());
} catch (IllegalAccessException e) {
throw new InvalidDataFormatException(this.resource, this.propertyName, childrenJSON.toString());
} catch (InvocationTargetException e) {
throw new InvalidDataFormatException(this.resource, this.propertyName, childrenJSON.toString());
}
}
/**
* Convert the serialized JSON data found in the node property to Resources.
*
* @return the list of children sorting using the comparator.
* @throws InvalidDataFormatException
*/
private List<SyntheticChildAsPropertyResource> deserialize() throws InvalidDataFormatException {
final long start = System.currentTimeMillis();
final String propertyData = this.resource.getValueMap().get(this.propertyName, EMPTY_JSON);
List<SyntheticChildAsPropertyResource> resources;
try {
resources = deserializeToSyntheticChildResources(new JSONObject(propertyData));
} catch (JSONException e) {
throw new InvalidDataFormatException(this.resource, this.propertyName, propertyData);
}
if (this.comparator != null) {
Collections.sort(resources, this.comparator);
}
log.debug("Get operation for [ {} ] in [ {} ms ]",
this.resource.getPath() + "/" + this.propertyName,
System.currentTimeMillis() - start);
return resources;
}
/**
* Converts a list of SyntheticChildAsPropertyResource to their JSON representation, keeping the provided order.
*
* @param resourceToSerialize the resource to serialize to JSON.
* @return the JSONObject representing the resources.
* @throws JSONException
*/
protected final JSONObject serializeToJSON(final Resource resourceToSerialize)
throws JSONException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
final DateTimeFormatter dtf = ISODateTimeFormat.dateTime();
final Map<String, Object> serializedData = new HashMap<String, Object>();
for (Map.Entry<String, Object> entry : resourceToSerialize.getValueMap().entrySet()) {
if (entry.getValue() instanceof Calendar) {
final Calendar cal = (Calendar) entry.getValue();
serializedData.put(entry.getKey(), dtf.print(cal.getTimeInMillis()));
} else if (entry.getValue() instanceof Date) {
final Date date = (Date) entry.getValue();
serializedData.put(entry.getKey(), dtf.print(date.getTime()));
} else {
serializedData.put(entry.getKey(), entry.getValue());
}
}
return new JSONObject(serializedData);
}
/**
* Converts a JSONObject to the list of SyntheticChildAsPropertyResources.
*
* @param jsonObject the JSONObject to deserialize.
* @return the list of SyntheticChildAsPropertyResources the jsonObject represents.
* @throws JSONException
*/
protected final List<SyntheticChildAsPropertyResource> deserializeToSyntheticChildResources(JSONObject jsonObject)
throws JSONException {
final List<SyntheticChildAsPropertyResource> resources = new ArrayList<SyntheticChildAsPropertyResource>();
final Iterator<String> keys = jsonObject.keys();
while (keys.hasNext()) {
final String nodeName = keys.next();
JSONObject entryJSON = jsonObject.optJSONObject(nodeName);
if (entryJSON == null) {
continue;
}
final ValueMap properties = new ValueMapDecorator(new HashMap<String, Object>());
final Iterator<String> propertyNames = entryJSON.keys();
while (propertyNames.hasNext()) {
final String propName = propertyNames.next();
properties.put(propName, entryJSON.optString(propName));
}
resources.add(new SyntheticChildAsPropertyResource(this.getParent(), nodeName, properties));
}
return resources;
}
/**
* Sort by resource name ascending (resource.getName()).
*/
private static final class ResourceNameComparator implements Comparator<Resource>, Serializable {
private static final long serialVersionUID = 0L;
@Override
public int compare(final Resource o1, final Resource o2) {
return o1.getName().compareTo(o2.getName().toString());
}
}
}