
io.wcm.maven.plugins.jsondlgcnv.DialogConverter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of json-dialog-conversion-plugin Show documentation
Show all versions of json-dialog-conversion-plugin Show documentation
Converts AEM Dialog Definitions in JSON Format with Rules from "Adobe AEM Dialog Conversion Tool".
The newest version!
/*
* #%L
* wcm.io
* %%
* Copyright (C) 2017 wcm.io
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package io.wcm.maven.plugins.jsondlgcnv;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.plugin.logging.Log;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.commons.json.JSONArray;
import org.apache.sling.commons.json.JSONException;
import org.apache.sling.commons.json.JSONObject;
import org.apache.sling.fsprovider.internal.mapper.ContentFile;
import org.apache.sling.testing.mock.sling.junit.SlingContext;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
//CHECKSTYLE:OFF
/**
* This converter logic is heavily based on
* NodeBasedRewriteRule
* and uses exactly the same logic - but operations on local JSON files as output.
*/
//CHECKSTYLE:ON
class DialogConverter {
// pattern that matches the regex for mapped properties: ${}
private static final Pattern MAPPED_PATTERN = Pattern.compile("^(\\!{0,1})\\$\\{(\'.*?\'|.*?)(:(.+))?\\}$");
// special properties
private static final String PROPERTY_MAP_CHILDREN = "cq:rewriteMapChildren";
private static final String PROPERTY_IS_FINAL = "cq:rewriteFinal";
private static final String PROPERTY_COMMON_ATTRS = "cq:rewriteCommonAttrs";
private static final String PROPERTY_RENDER_CONDITION = "cq:rewriteRenderCondition";
// special nodes
private static final String NN_CQ_REWRITE_PROPERTIES = "cq:rewriteProperties";
// node names
private static final String NN_RENDER_CONDITION = "rendercondition";
private static final String NN_GRANITE_RENDER_CONDITION = "granite:rendercondition";
private static final String NN_GRANITE_DATA = "granite:data";
// Granite
private static final String[] GRANITE_COMMON_ATTR_PROPERTIES = { "id", "rel", "class", "title", "hidden", "itemscope", "itemtype", "itemprop" };
private static final String RENDER_CONDITION_CORAL2_RESOURCE_TYPE_PREFIX = "granite/ui/components/foundation/renderconditions";
private static final String RENDER_CONDITION_CORAL3_RESOURCE_TYPE_PREFIX = "granite/ui/components/coral/foundation/renderconditions";
private static final String DATA_PREFIX = "data-";
private final Rules rules;
private final Resource sourceRoot;
private final Log log;
DialogConverter(SlingContext context, String rulesPath, Log log) {
this.rules = new Rules(context.resourceResolver().getResource(rulesPath));
this.sourceRoot = context.resourceResolver().getResource("/source");
this.log = log;
}
/**
* Convert and format all JSON files with dialog definitions.
*/
public void convert() {
traverse(sourceRoot, true);
}
/**
* Format (but not convert) all JSON files with dialog definitions.
*/
public void format() {
traverse(sourceRoot, false);
}
private void traverse(Resource resource, boolean convert) {
checkRuleMatch(resource, convert);
Iterator children = resource.listChildren();
while (children.hasNext()) {
traverse(children.next(), convert);
}
}
private void checkRuleMatch(Resource resource, boolean convert) {
Rule rule = rules.getRule(resource);
if (rule != null) {
log.info("Convert " + StringUtils.removeStart(resource.getPath(), "/source/") + " with rule '" + rule.getName() + "'.");
ContentFile contentFile = resource.adaptTo(ContentFile.class);
try {
JSONObject jsonContent = new JSONObject(FileUtils.readFileToString(contentFile.getFile()));
JsonElement wrapper = getJsonElement(jsonContent, contentFile.getSubPath());
if (convert) {
applyRule(wrapper, rule);
}
// format json string with gson pretty print
Gson gson = new GsonBuilder().setPrettyPrinting().create();
JsonObject gsonJson = gson.fromJson(jsonContent.toString(), JsonObject.class);
FileUtils.write(contentFile.getFile(), gson.toJson(gsonJson));
}
catch (JSONException | IOException ex) {
throw new RuntimeException(ex);
}
}
}
private JsonElement getJsonElement(JSONObject json, String path) throws JSONException {
if (StringUtils.isEmpty(path)) {
return new JsonElement(json, null, null);
}
if (StringUtils.contains(path, "/")) {
String name = StringUtils.substringBefore(path, "/");
String remainder = StringUtils.substringAfter(path, "/");
JSONObject child = json.getJSONObject(name);
return getJsonElement(child, remainder);
}
else {
return new JsonElement(json.getJSONObject(path), path, json);
}
}
private void applyRule(JsonElement wrapper, Rule rule) throws JSONException {
// check if the 'replacement' node exists
Resource replacement = rule.getReplacement();
if (replacement == null) {
throw new RuntimeException("The rule " + rule + " does not define a 'replacement' section.");
}
ValueMap replacementProps = replacement.getValueMap();
// if the replacement node has no children, we replace the tree by the empty tree,
// i.e. we remove the original tree
if (!replacement.hasChildren()) {
wrapper.parent.remove(wrapper.key);
return;
}
JSONObject root = cloneJson(wrapper.element);
// true if the replacement tree is final and all its nodes are excluded from
// further processing by the algorithm
boolean treeIsFinal = replacementProps.get(PROPERTY_IS_FINAL, false);
// copy replacement to original tree under original name
Resource replacementNext = replacement.listChildren().next();
JSONObject copy = wrapper.element;
clearJson(copy);
copyToJson(copy, replacementNext);
// common attribute mapping
if (replacementProps.containsKey(PROPERTY_COMMON_ATTRS)) {
addCommonAttrMappings(root, copy);
}
// render condition mapping
if (replacementProps.containsKey(PROPERTY_RENDER_CONDITION)) {
if (root.has(NN_GRANITE_RENDER_CONDITION) || root.has(NN_RENDER_CONDITION)) {
JSONObject renderConditionRoot = root.has(NN_GRANITE_RENDER_CONDITION) ? root.getJSONObject(NN_GRANITE_RENDER_CONDITION)
: root.getJSONObject(NN_RENDER_CONDITION);
JSONObject renderConditionCopy = copy.put(NN_GRANITE_RENDER_CONDITION, renderConditionRoot);
// convert render condition resource types recursively
Iterator renderConditionIterator = collectTree(renderConditionCopy).iterator();
while (renderConditionIterator.hasNext()) {
JSONObject renderConditionNode = renderConditionIterator.next();
String resourceType = renderConditionNode.getString(ResourceResolver.PROPERTY_RESOURCE_TYPE);
if (resourceType.startsWith(RENDER_CONDITION_CORAL2_RESOURCE_TYPE_PREFIX)) {
resourceType = resourceType.replace(RENDER_CONDITION_CORAL2_RESOURCE_TYPE_PREFIX, RENDER_CONDITION_CORAL3_RESOURCE_TYPE_PREFIX);
renderConditionNode.put(ResourceResolver.PROPERTY_RESOURCE_TYPE, resourceType);
}
}
}
}
// collect mappings: (node in original tree) -> (node in replacement tree)
Map mappings = new LinkedHashMap<>();
// traverse nodes of newly copied replacement tree
Iterator nodeIterator = collectTree(copy).iterator();
while (nodeIterator.hasNext()) {
JSONObject node = nodeIterator.next();
// iterate over all properties
Map replacementProperties = getProperties(node);
Iterator> propertyIterator = replacementProperties.entrySet().iterator();
JSONObject rewritePropertiesNode = null;
if (node.has(NN_CQ_REWRITE_PROPERTIES)) {
rewritePropertiesNode = node.getJSONObject(NN_CQ_REWRITE_PROPERTIES);
}
while (propertyIterator.hasNext()) {
Map.Entry property = propertyIterator.next();
// add mapping to collection
if (PROPERTY_MAP_CHILDREN.equals(property.getKey())) {
mappings.put(cleanup((String)property.getValue()), node);
// remove property, as we don't want it to be part of the result
node.remove(cleanup(property.getKey()));
continue;
}
// add single node to final nodes
if (PROPERTY_IS_FINAL.equals(property.getKey())) {
if (!treeIsFinal) {
// TODO: required?
//finalNodes.add(node);
}
node.remove(cleanup(property.getKey()));
continue;
}
// set value from original tree in case this is a mapped property
boolean mappedProperty = mapProperty(root, node, property.getKey(), (String)property.getValue());
if (mappedProperty && rewritePropertiesNode != null) {
if (rewritePropertiesNode.has(cleanup(property.getKey()))) {
rewriteProperty(node, cleanup(property.getKey()), rewritePropertiesNode.getJSONArray(cleanup(property.getKey())));
}
}
}
// remove node post-mapping
if (rewritePropertiesNode != null) {
node.remove(NN_CQ_REWRITE_PROPERTIES);
}
// reorder children and properties
reorderChildrenProperties(node, root);
}
// copy children from original tree to replacement tree according to the mappings found
for (Map.Entry mapping : mappings.entrySet()) {
String key = cleanup(mapping.getKey());
if (!root.has(cleanup(key))) {
// the node specified in the mapping does not exist in the original tree
continue;
}
JSONObject source = root.getJSONObject(cleanup(key));
JSONObject destination = mapping.getValue();
Iterator> iterator = getChildren(source).entrySet().iterator();
// copy over the source's children to the destination
while (iterator.hasNext()) {
Map.Entry child = iterator.next();
destination.put(cleanup(child.getKey()), child.getValue());
}
}
// TODO: not required?
/*
// we add the complete subtree to the final nodes
if (treeIsFinal) {
nodeIterator = collectTree(copy).iterator();
while (nodeIterator.hasNext()) {
finalNodes.add(nodeIterator.next());
}
}
*/
}
/**
* Replaces the value of a mapped property with a value from the original tree.
* @param root the root node of the original tree
* @param node the replacement tree object
* @param key property name of the (potentially) mapped property in the replacement copy tree
* @return true if there was a successful mapping, false otherwise
* @throws JSONException
*/
private boolean mapProperty(JSONObject root, JSONObject node, String key, String... mapping) throws JSONException {
boolean deleteProperty = false;
for (String value : mapping) {
Matcher matcher = MAPPED_PATTERN.matcher(value);
if (matcher.matches()) {
// this is a mapped property, we will delete it if the mapped destination
// property doesn't exist
deleteProperty = true;
String path = matcher.group(2);
// unwrap quoted property paths
path = StringUtils.removeStart(StringUtils.stripEnd(path, "\'"), "\'");
if (root.has(cleanup(path))) {
// replace property by mapped value in the original tree
Object originalValue = root.get(cleanup(path));
node.put(cleanup(key), originalValue);
// negate boolean properties if negation character has been set
String negate = matcher.group(1);
if ("!".equals(negate) && (originalValue instanceof Boolean)) {
node.put(cleanup(key), !((Boolean)originalValue));
}
// the mapping was successful
deleteProperty = false;
break;
}
else {
String defaultValue = matcher.group(4);
if (defaultValue != null) {
node.put(cleanup(key), defaultValue);
deleteProperty = false;
break;
}
}
}
}
if (deleteProperty) {
// mapped destination does not exist, we don't include the property in replacement tree
node.remove(key);
return false;
}
return true;
}
/**
* Applies a string rewrite to a property.
* @param node Node
* @param key the property name to rewrite
* @param rewriteProperty the property that defines the string rewrite
* @throws JSONException
*/
private void rewriteProperty(JSONObject node, String key, JSONArray rewriteProperty) throws JSONException {
if (node.get(cleanup(key)) instanceof String) {
if (rewriteProperty.length() == 2) {
if (rewriteProperty.get(0) instanceof String && rewriteProperty.get(1) instanceof String) {
String pattern = rewriteProperty.getString(0);
String replacement = rewriteProperty.getString(1);
Pattern compiledPattern = Pattern.compile(pattern);
Matcher matcher = compiledPattern.matcher(node.getString(cleanup(key)));
node.put(cleanup(key), matcher.replaceAll(replacement));
}
}
}
}
/**
* Adds property mappings on a replacement node for Granite common attributes.
* @param root the root node
* @param node the replacement node
* @throws JSONException
*/
private void addCommonAttrMappings(JSONObject root, JSONObject node) throws JSONException {
for (String property : GRANITE_COMMON_ATTR_PROPERTIES) {
String[] mapping = { "${./" + property + "}", "${\'./granite:" + property + "\'}" };
mapProperty(root, node, "granite:" + property, mapping);
}
if (root.has(NN_GRANITE_DATA)) {
// the root has granite:data defined, copy it before applying data-* properties
node.put(NN_GRANITE_DATA, root.get(NN_GRANITE_DATA));
}
// map data-* prefixed properties to granite:data child
for (Map.Entry entry : getProperties(root).entrySet()) {
if (!StringUtils.startsWith(entry.getKey(), DATA_PREFIX)) {
continue;
}
// add the granite:data child if necessary
JSONObject dataNode;
if (!node.has(NN_GRANITE_DATA)) {
dataNode = new JSONObject();
node.put(NN_GRANITE_DATA, dataNode);
}
else {
dataNode = node.getJSONObject(NN_GRANITE_DATA);
}
// set up the property mapping
String nameWithoutPrefix = entry.getKey().substring(DATA_PREFIX.length());
mapProperty(root, dataNode, nameWithoutPrefix, "${./" + entry.getKey() + "}");
}
}
private JSONObject cloneJson(JSONObject item) throws JSONException {
JSONObject newItem = new JSONObject();
Set> props = getProperties(item).entrySet();
for (Map.Entry prop : props) {
newItem.put(prop.getKey(), prop.getValue());
}
Set> children = getChildren(item).entrySet();
for (Map.Entry child : children) {
newItem.put(child.getKey(), cloneJson(child.getValue()));
}
return newItem;
}
private void clearJson(JSONObject item) throws JSONException {
Set> props = getProperties(item).entrySet();
Set> children = getChildren(item).entrySet();
for (Map.Entry prop : props) {
item.remove(prop.getKey());
}
for (Map.Entry child : children) {
item.remove(child.getKey());
}
}
private void copyToJson(JSONObject dest, Resource resource) throws JSONException {
for (Map.Entry entry : resource.getValueMap().entrySet()) {
if (StringUtils.equals(cleanup(entry.getKey()), "jcr:primaryType")) {
continue;
}
dest.put(cleanup(entry.getKey()), entry.getValue());
}
Iterator children = resource.listChildren();
while (children.hasNext()) {
Resource child = children.next();
JSONObject childObject = new JSONObject();
copyToJson(childObject, child);
dest.put(child.getName(), childObject);
}
}
private List collectTree(JSONObject item) throws JSONException {
List items = new ArrayList<>();
items.add(item);
for (JSONObject child : getChildren(item).values()) {
items.addAll(collectTree(child));
}
return items;
}
private Map getProperties(JSONObject item) throws JSONException {
Map props = new LinkedHashMap<>();
JSONArray names = item.names();
if (names != null) {
for (int i = 0; i < names.length(); i++) {
String name = names.getString(i);
Object value = item.get(name);
if (!(value instanceof JSONObject)) {
props.put(name, value);
}
}
}
return props;
}
private Map getChildren(JSONObject item) throws JSONException {
Map children = new LinkedHashMap<>();
JSONArray names = item.names();
if (names != null) {
for (int i = 0; i < names.length(); i++) {
String name = names.getString(i);
Object value = item.get(name);
if (value instanceof JSONObject) {
children.put(name, (JSONObject)value);
}
}
}
return children;
}
private String cleanup(String name) {
return StringUtils.removeStart(name, "./");
}
private void reorderChildrenProperties(JSONObject item, JSONObject orginal) throws JSONException {
Map props = new TreeMap(getProperties(item));
Map children = getChildren(item);
for (String key : props.keySet()) {
item.remove(key);
}
for (String key : children.keySet()) {
item.remove(key);
}
// put all properties and items that where already present in original node back in same order
for (String key : getProperties(orginal).keySet()) {
if (props.containsKey(key)) {
item.put(key, props.get(key));
props.remove(key);
}
}
// put all other props in alphabetical order
for (Map.Entry entry : props.entrySet()) {
item.put(entry.getKey(), entry.getValue());
}
// put all children that where already present in original node back in same order
for (String key : getProperties(orginal).keySet()) {
if (children.containsKey(key)) {
item.put(key, children.get(key));
children.remove(key);
}
}
// put all other children in alphabetical order
for (Map.Entry entry : children.entrySet()) {
item.put(entry.getKey(), entry.getValue());
}
}
private static class JsonElement {
private final JSONObject element;
private final String key;
private final JSONObject parent;
JsonElement(JSONObject element, String key, JSONObject parent) {
this.element = element;
this.key = key;
this.parent = parent;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy