Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.composum.sling.core.util.JsonUtil Maven / Gradle / Ivy
package com.composum.sling.core.util;
import com.composum.sling.core.exception.PropertyValueFormatException;
import com.composum.sling.core.filter.StringFilter;
import com.composum.sling.core.mapping.MappingRules;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.jcr.Binary;
import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.jcr.ValueFactory;
import javax.jcr.ValueFormatException;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.nodetype.PropertyDefinition;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.regex.Matcher;
import static com.composum.sling.core.util.CoreConstants.PROPO_FROZEN_UUID;
import static com.composum.sling.core.util.CoreConstants.PROP_BASE_VERSION;
import static com.composum.sling.core.util.CoreConstants.PROP_PREDECESSORS;
import static com.composum.sling.core.util.CoreConstants.PROP_RESOURCE_SUPER_TYPE;
import static com.composum.sling.core.util.CoreConstants.PROP_RESOURCE_TYPE;
import static com.composum.sling.core.util.CoreConstants.PROP_ROOT_VERSION;
import static com.composum.sling.core.util.CoreConstants.PROP_SUCCESSORS;
import static com.composum.sling.core.util.CoreConstants.PROP_UUID;
import static com.composum.sling.core.util.CoreConstants.PROP_VERSION_HISTORY;
/**
* The utility class to transform JCR object and Sling resources into JSON and update such objects using JSON data.
*/
public class JsonUtil {
private static final Logger LOG = LoggerFactory.getLogger(JsonUtil.class);
/**
* the declared builder for JSON POJO mapping
*/
public static final GsonBuilder GSON_BUILDER = new GsonBuilder();
/**
* the structure for parsing property values from JSON using Gson
*/
public static class JsonProperty {
public String name;
public String oldname;
public Object value; // can be a 'String' or a list of 'String' (multi value)
public String type = PropertyType.nameFromValue(PropertyType.STRING);
public boolean multi = false;
}
//
// mapping of objects and maps
//
/**
* Transforms a JSON object (stream) into a Map object.
*/
public static Map jsonMap(JsonReader reader) throws IOException {
Map map = new HashMap<>();
reader.beginObject();
while (reader.hasNext() && reader.peek() == JsonToken.NAME) {
String name = reader.nextName();
Object value = jsonValue(reader);
if (value != null) {
map.put(name, value);
}
}
reader.endObject();
return map;
}
/**
* Transforms a JSON object (stream) into an object.
*/
public static Object jsonValue(JsonReader reader) throws IOException {
switch (reader.peek()) {
case STRING:
return reader.nextString();
case BOOLEAN:
return reader.nextBoolean();
case NUMBER:
try {
return reader.nextLong();
} catch (NumberFormatException nfex) {
return reader.nextDouble();
}
case BEGIN_ARRAY:
ArrayList list = new ArrayList<>();
reader.beginArray();
while (reader.peek() != JsonToken.END_ARRAY) {
list.add(jsonValue(reader));
}
reader.endArray();
return list;
case BEGIN_OBJECT:
return jsonMap(reader);
default:
reader.skipValue();
}
return null;
}
/**
* Transforms a Map object into a JSON object (stream).
*/
public static void jsonMap(@NotNull final JsonWriter writer, @Nullable final Map map)
throws IOException {
if (map != null) {
writer.beginObject();
jsonMapEntries(writer, map);
writer.endObject();
}
}
public static void jsonMapEntries(@NotNull final JsonWriter writer, @Nullable final Map map)
throws IOException {
if (map != null) {
for (Map.Entry entry : map.entrySet()) {
writer.name(entry.getKey());
jsonValue(writer, entry.getValue());
}
}
}
/**
* Transforms an object into a JSON object (stream).
*/
@SuppressWarnings("unchecked")
public static void jsonValue(@NotNull final JsonWriter writer, @Nullable final Object value)
throws IOException {
if (value == null) {
writer.nullValue();
} else {
if (value instanceof Map) {
jsonMap(writer, (Map) value);
} else if (value instanceof Collection) {
writer.beginArray();
for (Object val : ((Collection>) value)) {
jsonValue(writer, val);
}
writer.endArray();
} else if (value instanceof Object[]) {
writer.beginArray();
for (Object val : ((Object[]) value)) {
jsonValue(writer, val);
}
writer.endArray();
} else if (value instanceof Boolean) {
writer.value((Boolean) value);
} else if (value instanceof Long) {
writer.value((Long) value);
} else if (value instanceof Double) {
writer.value((Double) value);
} else if (value instanceof Number) {
writer.value((Number) value);
} else {
writer.value(value.toString());
}
}
}
//
// ==== the Sling resource level ===========================================
//
// - wrapping for structured objects stored in the repository
//
//
// export resource structures as JSON objects
//
/**
* Writes a resources JSON view to a writer using the default application rules for filtering.
*
* @param writer the writer for the JSON transformation
* @param resource the resource to transform
*/
public static void exportJson(@NotNull final JsonWriter writer, @NotNull final Resource resource)
throws RepositoryException, IOException {
exportJson(writer, resource, MappingRules.getDefaultMappingRules());
}
/**
* @param writer the writer for the JSON transformation
* @param resource the resource to transform
* @param mapping the mapping policy rule set
*/
public static void exportJson(@NotNull final JsonWriter writer, @NotNull final Resource resource,
MappingRules mapping)
throws RepositoryException, IOException {
exportJson(writer, resource, mapping, 1);
}
/**
* @param writer the writer for the JSON transformation
* @param resource the resource to transform
* @param mapping the mapping policy rule set
* @param depth the max depth for the rendering
*/
public static void exportJson(JsonWriter writer, Resource resource, MappingRules mapping, int depth)
throws RepositoryException, IOException {
if (resource != null) {
writer.beginObject();
exportProperties(writer, resource, mapping);
// export children after the properties(!) if depth is not reached or not restricted
if (mapping.maxDepth == 0 || depth < mapping.maxDepth) {
depth++;
for (Resource child : resource.getChildren()) {
if (mapping.resourceFilter.accept(child)) {
writer.name(child.getName());
exportJson(writer, child, mapping, depth);
}
}
}
writer.endObject();
}
}
/**
* @param writer the writer for the JSON transformation
* @param resource the resource to transform
* @param mapping the mapping policy rule set
*/
public static void exportProperties(@NotNull final JsonWriter writer,
@NotNull final Resource resource, MappingRules mapping)
throws RepositoryException, IOException {
String path = resource.getPath();
Node node = resource.adaptTo(Node.class);
// property collection for a sorted output
TreeMap propertiesSet = new TreeMap<>();
if (node != null) {
// retrieve properties from the resources repository node
PropertyIterator iterator = node.getProperties();
while (iterator.hasNext()) {
Property property = iterator.nextProperty();
propertiesSet.put(property.getName(), property);
}
} else {
// for synthetic resources use the synthetic values from the resources value map
ValueMap properties = ResourceUtil.getValueMap(resource);
for (Map.Entry entry : properties.entrySet()) {
propertiesSet.put(entry.getKey(), entry.getValue());
}
}
// write properties first to ensure that types are read first on import
if (mapping.propertyFormat.scope != MappingRules.PropertyFormat.Scope.value) {
writer.name(MappingRules.PROPERTIES_NAME);
writer.beginArray();
}
for (Map.Entry entry : propertiesSet.entrySet()) {
String name = entry.getKey();
if (mapping.exportPropertyFilter.accept(name)) {
Object value = entry.getValue();
if (value instanceof Property) {
Property property = (Property) value;
writeJsonProperty(resource, writer, property, mapping);
} else {
// if no node exists (synthetic resource) the properties are simple values
writeJsonProperty(resource, writer, name, value, mapping);
}
}
}
if (mapping.propertyFormat.scope != MappingRules.PropertyFormat.Scope.value) {
writer.endArray();
}
}
/**
* Writes the names of all children (except the 'jcr:content' child) into one array value
* named '_child_order_' as a hint to recalculate the original order of the children.
*/
public static void exportChildOrderProperty(JsonWriter writer, Resource resource)
throws IOException {
List names = new ArrayList<>();
Iterable children = resource.getChildren();
for (Resource child : children) {
String name = child.getName();
if (!ResourceUtil.CONTENT_NODE.equals(name)) {
names.add(name);
}
}
if (names.size() > 0) {
writer.name(MappingRules.CHILD_ORDER_NAME);
writer.beginArray();
for (String name : names) {
if (!ResourceUtil.CONTENT_NODE.equals(name)) {
writer.value(name);
}
}
writer.endArray();
}
}
//
// import JSON mapped (exported) resource structures
//
protected static final Gson GSON = JsonUtil.GSON_BUILDER.create();
/**
*
*/
public static Resource importJson(JsonReader reader, ResourceResolver resolver, String path)
throws RepositoryException, IOException {
return importJson(reader, resolver, path, MappingRules.getDefaultMappingRules());
}
/**
*
*/
public static Resource importJson(JsonReader reader, ResourceResolver resolver,
String path, MappingRules mapping)
throws RepositoryException, IOException {
Session session = Objects.requireNonNull(resolver.adaptTo(Session.class));
ValueFactory factory = session.getValueFactory();
Resource resource = resolver.getResource(path);
ArrayList childrenSet = new ArrayList<>();
HashMap propertiesSet = new HashMap<>();
reader.beginObject();
JsonToken token;
while (reader.hasNext()) {
String name = reader.nextName();
if (MappingRules.PROPERTIES_NAME.equals(name)) {
reader.beginArray();
while ((token = reader.peek()) != JsonToken.END_ARRAY) {
switch (token) {
case BEGIN_OBJECT:
JsonProperty property = GSON.fromJson(reader, JsonProperty.class);
if (resource != null) {
if (importJsonProperty(factory, resource, property, mapping)) {
// remember all properties found in JSON object for finalizing
propertiesSet.put(property.name, property);
}
} else {
// remember all properties found in JSON object for later store
propertiesSet.put(property.name, property);
}
break;
}
}
reader.endArray();
} else {
JsonProperty property = null;
token = reader.peek();
switch (token) {
case BEGIN_OBJECT:
// child resource
try {
if (resource == null) {
// to avoid expensive memory consumption the resource is created before
// the first child is imported; that can be a problem if the types are
// not available (primary type, mixin types) at this time
resource = createResource(resolver, path, propertiesSet, factory, mapping);
}
importJson(reader, resolver, path + "/" + name, mapping);
} catch (ConstraintViolationException cvex) {
LOG.error(cvex.getMessage() + " (" + path + "/" + name + ")", cvex);
}
childrenSet.add(name);
break;
case BEGIN_ARRAY:
// multi value property (short format)
reader.beginArray();
if (reader.peek() != JsonToken.END_ARRAY) {
// ignore the additional child order array
// FIXME: child reorder implementation needed
if (!MappingRules.CHILD_ORDER_NAME.equals(name)) {
property = parseJsonProperty(reader, name);
ArrayList values = new ArrayList<>();
values.add(property.value);
while ((token = reader.peek()) != JsonToken.END_ARRAY) {
switch (token) {
case BOOLEAN:
values.add(reader.nextBoolean());
break;
case NUMBER:
values.add(reader.nextLong());
break;
case STRING:
values.add(parseJsonString(reader, property));
break;
case NULL:
reader.nextNull();
break;
}
}
property.value = values.toArray();
property.multi = true;
} else {
while (reader.peek() != JsonToken.END_ARRAY) {
reader.nextString();
}
}
}
reader.endArray();
break;
default:
// single value property (short format)
property = parseJsonProperty(reader, name);
break;
}
if (property != null) {
if (resource != null) {
if (importJsonProperty(factory, resource, property, mapping)) {
// remember all properties found in JSON object for finalizing
propertiesSet.put(property.name, property);
}
} else {
// remember all properties found in JSON object for later store
propertiesSet.put(property.name, property);
}
}
}
}
reader.endObject();
if (resource == null) {
resource = createResource(resolver, path, propertiesSet, factory, mapping);
}
if (mapping.changeRule == MappingRules.ChangeRule.update) {
Node node = resource.adaptTo(Node.class);
if (node != null && !node.isNew()) {
// remove all properties not included in JSON object if 'update' rule is specified
PropertyIterator iterator = node.getProperties();
while (iterator.hasNext()) {
Property property = iterator.nextProperty();
String propertyName = property.getName();
if (propertiesSet.get(propertyName) == null
&& mapping.importPropertyFilter.accept(propertyName)) {
try {
node.setProperty(propertyName, (Value) null);
} catch (ValueFormatException vfex) {
node.setProperty(propertyName, (Value[]) null);
}
}
}
// remove all children not included in JSON object if the 'update' rule is specified
for (Resource child : resource.getChildren()) {
String childName = child.getName();
if (!childrenSet.contains(childName) && mapping.resourceFilter.accept(child)) {
Node childNode = child.adaptTo(Node.class);
if (childNode != null) {
childNode.remove();
}
}
}
}
}
return resource;
}
/**
* Creates the resource parsed from JSON.
* Uses the 'jcr:primaryType' property from the propertySet to determine
* the right primary type for the new node.
* Uses the 'jcr:mixinTypes' property to set up the mixin types of the node
* before the other properties are set (it's important that all main type settings
* are done before the properties are set to avoid constraint violations).
*/
public static Resource createResource(ResourceResolver resolver, String path,
Map propertiesSet,
ValueFactory factory, MappingRules mapping)
throws RepositoryException {
// determine the new nodes primary type from the properties
JsonProperty primaryType = propertiesSet.get(PropertyUtil.PROP_PRIMARY_TYPE);
Resource resource = ResourceUtil.getOrCreateResource(resolver, path,
primaryType != null ? (String) primaryType.value : null);
if (resource != null) {
JsonProperty mixinTypes = propertiesSet.get(PropertyUtil.PROP_MIXIN_TYPES);
if (mixinTypes != null) {
// import mixin types property first(!)
if (!importJsonProperty(factory, resource, mixinTypes, mapping)) {
propertiesSet.remove(PropertyUtil.PROP_MIXIN_TYPES);
}
}
for (Map.Entry entry : propertiesSet.entrySet()) {
JsonProperty property = entry.getValue();
// import all the other properties - not the primary and mixin types
if (!PropertyUtil.PROP_PRIMARY_TYPE.equals(property.name)
&& !PropertyUtil.PROP_MIXIN_TYPES.equals(property.name)) {
if (!importJsonProperty(factory, resource, property, mapping)) {
entry.setValue(null);
}
}
}
}
return resource;
}
/**
* Parses a single property of the first element of an array and returns the result as a JSON POJO object.
*
* @param reader the reader with the JSON stream
* @param name the already parsed name of the property
* @return a new JsonProperty object with the name, type, and value set
*/
public static JsonProperty parseJsonProperty(JsonReader reader, String name)
throws IOException {
JsonToken token = reader.peek();
JsonProperty property = new JsonProperty();
property.name = name;
switch (token) {
case BOOLEAN:
// map boolean values directly if not a string
property.type = PropertyType.nameFromValue(PropertyType.BOOLEAN);
property.value = reader.nextBoolean();
break;
case NUMBER:
// map numver values to LONG directly if not a string
property.type = PropertyType.nameFromValue(PropertyType.LONG);
property.value = reader.nextLong();
break;
case STRING:
// parse the string with an option type hint within
parseJsonString(reader, property);
break;
case NULL:
reader.nextNull();
break;
}
return property;
}
/**
* The parser for a JSON string value with an optional type hint ('{type}...') as the values prefix.
* Sets the value and type of the property POJO and returns the string value without the type.
* This is used for single property values and also for multiple values (in the array loop).
*/
public static String parseJsonString(JsonReader reader, JsonProperty property) throws IOException {
if (property.type == null) {
// set property type to the default value if not set otherwise
// (e.g. on by the value before in an array loop)
property.type = PropertyType.nameFromValue(PropertyType.STRING);
}
String string = reader.nextString();
// check for a type hint string pattern and extract the parts if matching
Matcher matcher = MappingRules.TYPED_PROPERTY_STRING.matcher(string);
if (matcher.matches()) {
try {
// ensure that the type ist known in the JCR repository
property.type = PropertyType.nameFromValue(
PropertyType.valueFromName(matcher.group(1)));
string = matcher.group(2);
} catch (IllegalArgumentException iaex) {
// if not a known type let the string unchanged
}
}
property.value = string;
return string;
}
/**
* Changes the property specified by the JSON POJO of the given node if the change rule is appropriate to
* the current state of the node (changes are made only if not only 'extend' is specified as rule or if
* the property is new) and if the property filter of the rules accepts the property.
* ConstraintViolationExceptions are catched if thrown and logged with 'info' level.
*
* @param factory the ValueFactory to create the JCR value from the JSON value
* @param resource the resource which stores the property
* @param property the JSON POJO for the property
* @param mapping the change rules for merging with existing values
*/
public static boolean importJsonProperty(ValueFactory factory, Resource resource,
JsonProperty property, MappingRules mapping)
throws RepositoryException {
Node node;
if (resource != null && (node = resource.adaptTo(Node.class)) != null) {
// change property if new or not only 'extend' is specified
if ((mapping.changeRule != MappingRules.ChangeRule.extend
|| node.getProperty(property.name) == null)
&& mapping.importPropertyFilter.accept(property.name)) {
try {
setJsonProperty(node, property, mapping);
return true;
} catch (ConstraintViolationException cvex) {
LOG.info(cvex.toString() + " (" + node.getPath() + "@" + property.name + ")");
}
}
}
return false;
}
//
// ==== the JCR level ======================================================
//
// - wrapping for nodes and properties to JSON
//
/**
* Write all properties of an node accepted by the filter into an JSON array.
*
* @param writer the JSON writer object (with the JSON state)
* @param filter the property name filter
* @param node the JCR node to write
* @throws javax.jcr.RepositoryException error on accessing JCR
* @throws java.io.IOException error on write JSON
*/
public static void writeJsonProperties(@NotNull final Resource resource, @NotNull final JsonWriter writer,
@NotNull final StringFilter filter, @Nullable final Node node,
@NotNull final MappingRules mapping)
throws RepositoryException, IOException {
if (node != null) {
TreeMap sortedProperties = new TreeMap<>();
PropertyIterator iterator = node.getProperties();
while (iterator.hasNext()) {
Property property = iterator.nextProperty();
String name = property.getName();
if (filter.accept(name)) {
sortedProperties.put(name, property);
}
}
if (mapping.propertyFormat.scope == MappingRules.PropertyFormat.Scope.value) {
writer.beginObject();
} else {
writer.beginArray();
}
for (Map.Entry entry : sortedProperties.entrySet()) {
writeJsonProperty(resource, writer, entry.getValue(), mapping);
}
if (mapping.propertyFormat.scope == MappingRules.PropertyFormat.Scope.value) {
writer.endObject();
} else {
writer.endArray();
}
}
}
/**
* Write all properties of an node accepted by the filter into an JSON array.
*
* @param writer the JSON writer object (with the JSON state)
* @param filter the property name filter
* @param values the Sling ValueMap to write
* @throws javax.jcr.RepositoryException error on accessing JCR
* @throws java.io.IOException error on write JSON
*/
public static void writeJsonValueMap(@NotNull final Resource resource, @NotNull final JsonWriter writer,
@NotNull final StringFilter filter, @Nullable final ValueMap values,
@NotNull final MappingRules mapping)
throws RepositoryException, IOException {
if (values != null) {
TreeMap sortedProperties = new TreeMap<>();
for (Map.Entry entry : values.entrySet()) {
String key = entry.getKey();
if (filter.accept(key)) {
sortedProperties.put(key, entry.getValue());
}
}
if (mapping.propertyFormat.scope == MappingRules.PropertyFormat.Scope.value) {
writer.beginObject();
} else {
writer.beginArray();
}
for (Map.Entry entry : sortedProperties.entrySet()) {
writeJsonProperty(resource, writer, entry.getKey(), entry.getValue(), mapping);
}
if (mapping.propertyFormat.scope == MappingRules.PropertyFormat.Scope.value) {
writer.endObject();
} else {
writer.endArray();
}
}
}
/**
* Writes a JCR property as an JSON object: { name: ..., value: ..., type: ..., multi: ...}.
*
* @param writer the JSON writer object (with the JSON state)
* @param property the JCR property to write
* @param mapping the format in the JSON output
* @throws javax.jcr.RepositoryException error on accessing JCR
* @throws java.io.IOException error on write JSON
*/
public static void writeJsonProperty(@NotNull final Resource resource, @NotNull final JsonWriter writer,
@Nullable final Property property, @NotNull final MappingRules mapping)
throws RepositoryException, IOException {
if (property != null &&
(PropertyType.BINARY != property.getType() ||
mapping.propertyFormat.binary != MappingRules.PropertyFormat.Binary.skip)) {
String name = property.getName();
int type = property.getType();
if (mapping.propertyFormat.scope == MappingRules.PropertyFormat.Scope.value) {
writer.name(name);
} else {
writer.beginObject();
writer.name("name").value(name);
writer.name("value");
}
StringBuilder valueString = PropertyType.STRING == type ? new StringBuilder() : null;
if (property.isMultiple()) {
writer.beginArray();
for (Value value : property.getValues()) {
JsonUtil.writeJsonValue(resource, writer, name, value, type, mapping);
if (valueString != null) {
if (valueString.length() > 0) {
valueString.append(',');
}
valueString.append(value.getString());
}
}
writer.endArray();
} else {
Value value = property.getValue();
JsonUtil.writeJsonValue(resource, writer, name, value, type, mapping);
if (valueString != null) {
valueString.append(value.getString());
}
}
if (mapping.propertyFormat.scope != MappingRules.PropertyFormat.Scope.value) {
writer.name("type").value(PropertyType.nameFromValue(type));
writer.name("multi").value(property.isMultiple());
if (mapping.propertyFormat.scope == MappingRules.PropertyFormat.Scope.definition) {
PropertyDefinition definition = property.getDefinition();
writer.name("auto").value(definition.isAutoCreated());
writer.name("protected").value(definition.isProtected());
if (valueString != null) {
writer.name("subtype").value(PropertyUtil.getStringSubtype(valueString.toString()).name());
}
}
if (!property.isMultiple()) {
String target = null;
switch (type) {
case PropertyType.STRING:
case PropertyType.PATH:
case PropertyType.REFERENCE:
case PropertyType.WEAKREFERENCE:
target = getValueTarget(resource, name, property.getString());
break;
}
if (StringUtils.isNotBlank(target)) {
writer.name("target").value(target);
}
}
writer.endObject();
}
}
}
/**
* Writes a resource property (without node - probably synthetic resource)
*/
public static void writeJsonProperty(@NotNull final Resource resource, @NotNull final JsonWriter writer,
@Nullable final String name, @Nullable final Object value,
@NotNull final MappingRules mapping)
throws RepositoryException, IOException {
if (name != null && value != null) {
int type = PropertyType.STRING;
if (value instanceof Boolean) {
type = PropertyType.BOOLEAN;
} else if (value instanceof Long || value instanceof Integer) {
type = PropertyType.LONG;
} else if (value instanceof Double || value instanceof Float) {
type = PropertyType.DOUBLE;
} else if (value instanceof BigDecimal) {
type = PropertyType.DECIMAL;
} else if (value instanceof Calendar) {
type = PropertyType.DATE;
} else if (value instanceof InputStream) {
type = PropertyType.BINARY;
}
if (mapping.propertyFormat.scope != MappingRules.PropertyFormat.Scope.value) {
writer.beginObject();
}
if (mapping.propertyFormat.scope == MappingRules.PropertyFormat.Scope.value) {
writer.name(name);
} else {
writer.name("name").value(name);
writer.name("value");
}
boolean isMultiple = value instanceof Object[];
if (isMultiple) {
writer.beginArray();
for (Object val : (Object[]) value) {
JsonUtil.writeJsonValue(resource, writer, name, val, type, mapping);
}
writer.endArray();
} else {
JsonUtil.writeJsonValue(resource, writer, name, value, type, mapping);
}
if (mapping.propertyFormat.scope != MappingRules.PropertyFormat.Scope.value) {
writer.name("type").value(PropertyType.nameFromValue(type));
writer.name("multi").value(isMultiple);
if (!isMultiple && type == PropertyType.STRING) {
String target = getValueTarget(resource, name, value.toString());
if (StringUtils.isNotBlank(target)) {
writer.name("target").value(target);
}
}
writer.endObject();
}
}
}
public static String getValueTarget(@Nullable final Resource resource,
@Nullable final String name,
@Nullable final String value) {
if (resource != null && StringUtils.isNotBlank(value)) {
ResourceResolver resolver = resource.getResourceResolver();
Resource target;
if (value.startsWith("/")) {
target = resolver.getResource(value);
if (target != null) {
return target.getPath();
}
}
boolean checkAsRelativePath = false;
if (StringUtils.isNotBlank(name)) {
switch (name) {
case PROP_RESOURCE_TYPE:
case PROP_RESOURCE_SUPER_TYPE:
checkAsRelativePath = true;
break;
case PROP_UUID:
break;
case PROPO_FROZEN_UUID:
case PROP_ROOT_VERSION:
case PROP_BASE_VERSION:
case PROP_VERSION_HISTORY:
case PROP_PREDECESSORS:
case PROP_SUCCESSORS:
default:
Session session = resolver.adaptTo(Session.class);
if (session != null) {
try {
Node node = session.getNodeByIdentifier(value);
return node.getPath();
} catch (Exception ignore) {
}
}
// heuristics whether it could be a path: it should contain at least a slash but no whitespace
checkAsRelativePath = value.contains("/") && !value.matches(".*\\s+.*");
break;
}
}
if (checkAsRelativePath) {
for (String root : resolver.getSearchPath()) {
target = resolver.getResource(root + value);
if (target != null) {
return target.getPath();
}
}
}
}
return null;
}
// Java type transformations
/**
* Write a JCR value to the JSON writer.
*
* @param writer the JSON writer object (with the JSON state)
* @param name the name of the value
* @param value the value itself, should be a JCR property otherwise a java scalar object or array of scalars
* @param type the type of the value
* @throws javax.jcr.RepositoryException error on accessing JCR
* @throws java.io.IOException error on write JSON
*/
public static void writeJsonValue(@NotNull final Resource resource, @NotNull final JsonWriter writer,
@NotNull final String name, @NotNull final Object value,
@NotNull final Integer type, @NotNull final MappingRules mapping)
throws RepositoryException, IOException {
Value jcrValue = value instanceof Value ? (Value) value : null;
switch (type) {
case PropertyType.BINARY:
if (mapping.propertyFormat.binary == MappingRules.PropertyFormat.Binary.link) {
String uri = "/bin/cpm/nodes/property.bin"
+ LinkUtil.encodePath(resource.getPath())
+ "?name=" + UrlCodec.QUERYPART.encode(name);
boolean htmlSafe = writer.isHtmlSafe();
writer.setHtmlSafe(false);
writer.value(uri);
writer.setHtmlSafe(htmlSafe);
} else if (
mapping.propertyFormat.binary == MappingRules.PropertyFormat.Binary.base64) {
byte[] buffer = null;
if (jcrValue != null) {
Binary binary = jcrValue.getBinary();
buffer = IOUtils.toByteArray(binary.getStream());
} else if (value instanceof InputStream) {
buffer = IOUtils.toByteArray((InputStream) value);
}
String encoded = Base64.encodeBase64String(buffer != null ? buffer : new byte[0]);
writer.value(getValueString(encoded, type, mapping));
} else {
writer.nullValue();
}
break;
case PropertyType.BOOLEAN:
writer.value(jcrValue != null ? jcrValue.getBoolean()
: (value instanceof Boolean ? (Boolean) value : Boolean.parseBoolean(value.toString())));
break;
case PropertyType.DATE:
Calendar cal = jcrValue != null ? jcrValue.getDate()
: (value instanceof Calendar ? (Calendar) value : null);
if (cal != null) {
SimpleDateFormat dateFormat = new SimpleDateFormat(MappingRules.MAP_DATE_FORMAT);
dateFormat.setTimeZone(cal.getTimeZone());
writer.value(getValueString(dateFormat.format(cal.getTime()), type, mapping));
}
break;
case PropertyType.DECIMAL:
writer.value(getValueString(jcrValue != null
? jcrValue.getDecimal() : (value instanceof BigDecimal ? (BigDecimal) value
: new BigDecimal(value.toString())), type, mapping));
break;
case PropertyType.DOUBLE:
writer.value(getValueString(jcrValue != null
? jcrValue.getDouble() : (value instanceof Double ? (Double) value
: Double.valueOf(value.toString())), type, mapping));
break;
case PropertyType.LONG:
writer.value(jcrValue != null ? jcrValue.getLong()
: (value instanceof Long ? (Long) value : Long.valueOf(value.toString())));
break;
case PropertyType.NAME:
case PropertyType.PATH:
case PropertyType.REFERENCE:
case PropertyType.STRING:
case PropertyType.URI:
case PropertyType.WEAKREFERENCE:
writer.value(getValueString(jcrValue != null
? jcrValue.getString() : value.toString(), type, mapping));
break;
case PropertyType.UNDEFINED:
writer.nullValue();
break;
}
}
// helper methods to decouple value write from value get (can probably throw an exception)
public static void writeValue(JsonWriter writer, String name, String value) throws IOException {
writer.name(name).value(value);
}
public static void writeValue(JsonWriter writer, String name, boolean value) throws IOException {
writer.name(name).value(value);
}
public static void writeValue(JsonWriter writer, String name, double value) throws IOException {
writer.name(name).value(value);
}
public static void writeValue(JsonWriter writer, String name, long value) throws IOException {
writer.name(name).value(value);
}
public static void writeValue(JsonWriter writer, String name, Number value) throws IOException {
writer.name(name).value(value);
}
/**
* Embeds the property type in the string value if the formats scope is 'value'.
*/
public static String getValueString(Object value, int type, MappingRules mapping) {
String string = value.toString();
if (type != PropertyType.STRING &&
mapping.propertyFormat.embedType &&
mapping.propertyFormat.scope == MappingRules.PropertyFormat.Scope.value) {
string = "{" + PropertyType.nameFromValue(type) + "}" + string;
}
return string;
}
// helper methos for general use
/**
*
*/
public static void writeJsonArray(JsonWriter writer, String[] values) throws IOException {
if (values != null) {
writer.beginArray();
for (String value : values) {
writer.value(value);
}
writer.endArray();
}
}
/**
*
*/
public static void writeJsonArray(JsonWriter writer, Iterator values) throws IOException {
if (values != null) {
writer.beginArray();
while (values.hasNext()) {
writer.value(values.next());
}
writer.endArray();
}
}
public interface ElementCallback {
String doWithElement(T element) throws Exception;
}
public static void writeJsonArray(JsonWriter writer, Iterator values, ElementCallback elementCallback) throws IOException {
if (values != null) {
writer.beginArray();
while (values.hasNext()) {
try {
String extractedValue = elementCallback.doWithElement(values.next());
writer.value(extractedValue);
} catch (Exception e) {
throw new IOException(e);
}
}
writer.endArray();
}
}
public static void writeJsonArray(JsonWriter writer, String name, Iterator values, ElementCallback elementCallback) throws IOException {
writer.name(name);
writeJsonArray(writer, values, elementCallback);
}
// receiving JSON ...
/**
* Creates or updates one property at a JCR node.
*
* @param node the node which holds the property
* @param property the property object transformed from JSON
* @return true
if the property is set and available
* @throws RepositoryException if storing was not possible (some reasons)
*/
public static boolean setJsonProperty(Node node,
JsonProperty property, MappingRules mapping)
throws RepositoryException {
if (property != null) {
int type = StringUtils.isNotBlank(property.type)
? PropertyType.valueFromName(property.type) : PropertyType.STRING;
String name = property.name;
String oldname = property.oldname;
if (!StringUtils.isBlank(oldname) && !name.equals(oldname) && node.hasProperty(name)) {
throw new RepositoryException("property '" + name + "' already exists");
}
if (property.multi || property.value instanceof Object[]) {
// make or store a multi value property
Object[] jsonValues = property.value instanceof Object[] ? (Object[]) property.value : null;
if (jsonValues == null) {
if (property.value instanceof List) {
// if the value is already a multi value use this directly
List> list = (List>) property.value;
jsonValues = list.toArray(new Object[0]);
} else {
// make a multi value by splitting the string using a comma as delimiter
jsonValues = property.value != null
? property.value.toString().split("\\s*,\\s*") : new String[0];
}
}
// make a JCR value for each string value
Value[] values = new Value[jsonValues.length];
try {
for (int i = 0; i < jsonValues.length; i++) {
values[i] = makeJcrValue(node, type, jsonValues[i], mapping);
}
} catch (PropertyValueFormatException pfex) {
return false;
}
Property jcrProperty = null;
try {
jcrProperty = PropertyUtil.setProperty(node, name, values, type);
} catch (ValueFormatException vfex) {
// if this exception occurs the property must be transformed to multi value
node.setProperty(name, (Value) null);
PropertyUtil.setProperty(node, name, values, type);
}
if (!StringUtils.isBlank(oldname) && !name.equals(oldname)) {
node.setProperty(oldname, (Value) null);
}
return jcrProperty != null;
} else {
// make or store a single value property
String stringValue;
if (property.value instanceof List) {
// if the value was a multi value before join this to one string
//noinspection unchecked
stringValue = StringUtils.join((List) property.value, ',');
} else {
stringValue = property.value != null ? property.value.toString() : null;
}
Value value;
try {
value = makeJcrValue(node, type, stringValue, mapping);
} catch (PropertyValueFormatException pfex) {
return false;
}
Property jcrProperty = null;
try {
jcrProperty = PropertyUtil.setProperty(node, name, value, type);
} catch (ValueFormatException vfex) {
// if this exception occurs the property must be transformed to single value
node.setProperty(name, (Value[]) null);
PropertyUtil.setProperty(node, name, value, type);
}
if (!StringUtils.isBlank(oldname) && !name.equals(oldname)) {
node.setProperty(oldname, (Value) null);
}
return jcrProperty != null;
}
}
return false;
}
/**
* Create a JCR value from string value for the designated JCR type.
*
* @param node the node of the property
* @param type the JCR type according to the types declared in PropertyType
* @param object the value in the right type or a string representation of the value,
* for binary values a input stream can be used as parameter or a string
* with the base64 encoded data for the binary property
*/
public static Value makeJcrValue(Node node, int type, Object object,
MappingRules mapping)
throws PropertyValueFormatException, RepositoryException {
Session session = node.getSession();
ValueFactory factory = session.getValueFactory();
Value value = null;
if (object != null) {
switch (type) {
case PropertyType.BINARY:
if (mapping.propertyFormat.binary != MappingRules.PropertyFormat.Binary.skip) {
InputStream input = null;
if (object instanceof InputStream) {
input = (InputStream) object;
} else if (object instanceof String) {
if (mapping.propertyFormat.binary == MappingRules.PropertyFormat.Binary.base64) {
byte[] decoded = Base64.decodeBase64((String) object);
input = new ByteArrayInputStream(decoded);
}
}
if (input != null) {
Binary binary = factory.createBinary(input);
value = factory.createValue(binary);
}
}
break;
case PropertyType.BOOLEAN:
value = factory.createValue(object instanceof Boolean
? (Boolean) object : Boolean.parseBoolean(object.toString()));
break;
case PropertyType.DATE:
Date date = object instanceof Date ? (Date) object : null;
if (date == null) {
String string = object.toString();
date = mapping.dateParser.parse(string);
}
if (date != null) {
GregorianCalendar cal = new GregorianCalendar();
cal.setTime(date);
value = factory.createValue(cal);
} else {
throw new PropertyValueFormatException("invalid date/time value: " + object);
}
break;
case PropertyType.DECIMAL:
value = factory.createValue(object instanceof BigDecimal
? (BigDecimal) object : new BigDecimal(object.toString()));
break;
case PropertyType.DOUBLE:
value = factory.createValue(object instanceof Double
? (Double) object : Double.parseDouble(object.toString()));
break;
case PropertyType.LONG:
value = factory.createValue(object instanceof Long
? (Long) object : Long.parseLong(object.toString()));
break;
case PropertyType.REFERENCE:
case PropertyType.WEAKREFERENCE:
final Node refNode = session.getNodeByIdentifier(object.toString());
final String identifier = refNode.getIdentifier();
value = factory.createValue(identifier, type);
break;
case PropertyType.NAME:
case PropertyType.PATH:
case PropertyType.STRING:
case PropertyType.URI:
value = factory.createValue(object.toString(), type);
break;
case PropertyType.UNDEFINED:
break;
}
}
return value;
}
/**
* Create a JCR value from string value for the designated JCR type.
*
* @param type the JCR type according to the types declared in PropertyType
* @param value the value in the right type or a string representation of the value,
* for binary values a input stream can be used as parameter or a string
* with the base64 encoded data for the binary property
*/
public static Object makeValueObject(int type, Object value) {
MappingRules mapping = MappingRules.getDefaultMappingRules();
Object object = null;
if (value != null) {
if (value instanceof Collection) {
@SuppressWarnings("rawtypes")
Collection> collection = (Collection) value;
Object[] multi = new Object[collection.size()];
Iterator> it = collection.iterator();
for (int i = 0; i < collection.size(); i++) {
multi[i] = makeValueObject(type, it.next());
}
object = multi;
} else if (value instanceof Object[]) {
Object[] array = (Object[]) value;
Object[] multi = new Object[array.length];
for (int i = 0; i < array.length; i++) {
multi[i] = makeValueObject(type, array[i]);
}
object = multi;
} else {
switch (type) {
case PropertyType.BINARY:
if (value instanceof InputStream) {
object = value;
} else if (value instanceof String) {
byte[] decoded = Base64.decodeBase64(value.toString());
object = new ByteArrayInputStream(decoded);
}
break;
case PropertyType.BOOLEAN:
object = value instanceof Boolean ? (Boolean) value : Boolean.parseBoolean(value.toString());
break;
case PropertyType.DATE:
Date date = value instanceof Date ? (Date) value : null;
if (date == null) {
date = mapping.dateParser.parse(value.toString());
}
if (date != null) {
GregorianCalendar cal = new GregorianCalendar();
cal.setTime(date);
object = cal;
} else {
throw new IllegalArgumentException("invalid date/time value: " + value);
}
break;
case PropertyType.DECIMAL:
object = value instanceof BigDecimal ? (BigDecimal) value : new BigDecimal(value.toString());
break;
case PropertyType.DOUBLE:
object = value instanceof Double ? (Double) value : Double.parseDouble(value.toString());
break;
case PropertyType.LONG:
object = value instanceof Long ? (Long) value : Long.parseLong(value.toString());
break;
case PropertyType.REFERENCE:
case PropertyType.WEAKREFERENCE:
case PropertyType.NAME:
case PropertyType.PATH:
case PropertyType.STRING:
case PropertyType.URI:
object = value.toString();
break;
case PropertyType.UNDEFINED:
break;
}
}
}
return object;
}
}