From 86b0315f57c891c677973308ac6ba3aa2cbc630e Mon Sep 17 00:00:00 2001 From: Yegor Kozlov Date: Thu, 14 Sep 2023 16:29:34 +0200 Subject: [PATCH] Feature: Content Sync (#3151) * new feature: content sync --- CHANGELOG.md | 3 + bundle/pom.xml | 6 + .../acs/commons/contentsync/CatalogItem.java | 74 ++ .../contentsync/ConfigurationUtils.java | 60 + .../commons/contentsync/ContentCatalog.java | 84 ++ .../commons/contentsync/ContentReader.java | 214 +++ .../acs/commons/contentsync/ContentSync.java | 314 +++++ .../commons/contentsync/RemoteInstance.java | 176 +++ .../contentsync/SyncHostConfiguration.java | 54 + .../commons/contentsync/UpdateStrategy.java | 54 + .../impl/AssetChecksumStrategy.java | 123 ++ .../impl/LastModifiedStrategy.java | 264 ++++ .../acs/commons/contentsync/package-info.java | 21 + .../servlet/ContentCatalogServlet.java | 109 ++ .../contentsync/TestContentReader.java | 221 ++++ .../commons/contentsync/TestContentSync.java | 408 ++++++ .../impl/TestLastModifiedStrategy.java | 202 +++ .../servlet/TestContentCatalogServlet.java | 151 +++ .../src/test/resources/contentsync/asset.json | 1158 +++++++++++++++++ .../resources/contentsync/jcr_content.json | 107 ++ .../resources/contentsync/ordered-folder.json | 8 + .../riverside-camping-australia.1.json | 115 ++ .../test/resources/contentsync/wknd-faqs.json | 301 +++++ .../components/utilities/contentsync/POST.jsp | 254 ++++ .../contentsync/clientlibs/.content.xml | 5 + .../utilities/contentsync/clientlibs/css.txt | 3 + .../contentsync/clientlibs/css/app.css | 13 + .../utilities/contentsync/clientlibs/js.txt | 3 + .../contentsync/clientlibs/js/app.js | 72 + .../configurehostdatasource.jsp | 38 + .../configurehostentry/configurehostentry.jsp | 73 ++ .../utilities/contentsync/iframe/.content.xml | 3 + .../utilities/contentsync/iframe/iframe.jsp | 36 + .../selecthostdatasource.jsp | 51 + .../strategydatasource/strategydatasource.jsp | 44 + .../content/contentsync/.content.xml | 130 ++ .../contentsync/configure/.content.xml | 275 ++++ .../nav/tools/acs-commons/.content.xml | 1 + .../acs-commons/contentsync/.content.xml | 27 + 39 files changed, 5255 insertions(+) create mode 100644 bundle/src/main/java/com/adobe/acs/commons/contentsync/CatalogItem.java create mode 100644 bundle/src/main/java/com/adobe/acs/commons/contentsync/ConfigurationUtils.java create mode 100644 bundle/src/main/java/com/adobe/acs/commons/contentsync/ContentCatalog.java create mode 100644 bundle/src/main/java/com/adobe/acs/commons/contentsync/ContentReader.java create mode 100644 bundle/src/main/java/com/adobe/acs/commons/contentsync/ContentSync.java create mode 100644 bundle/src/main/java/com/adobe/acs/commons/contentsync/RemoteInstance.java create mode 100644 bundle/src/main/java/com/adobe/acs/commons/contentsync/SyncHostConfiguration.java create mode 100644 bundle/src/main/java/com/adobe/acs/commons/contentsync/UpdateStrategy.java create mode 100644 bundle/src/main/java/com/adobe/acs/commons/contentsync/impl/AssetChecksumStrategy.java create mode 100644 bundle/src/main/java/com/adobe/acs/commons/contentsync/impl/LastModifiedStrategy.java create mode 100644 bundle/src/main/java/com/adobe/acs/commons/contentsync/package-info.java create mode 100644 bundle/src/main/java/com/adobe/acs/commons/contentsync/servlet/ContentCatalogServlet.java create mode 100644 bundle/src/test/java/com/adobe/acs/commons/contentsync/TestContentReader.java create mode 100644 bundle/src/test/java/com/adobe/acs/commons/contentsync/TestContentSync.java create mode 100644 bundle/src/test/java/com/adobe/acs/commons/contentsync/impl/TestLastModifiedStrategy.java create mode 100644 bundle/src/test/java/com/adobe/acs/commons/contentsync/servlet/TestContentCatalogServlet.java create mode 100644 bundle/src/test/resources/contentsync/asset.json create mode 100644 bundle/src/test/resources/contentsync/jcr_content.json create mode 100644 bundle/src/test/resources/contentsync/ordered-folder.json create mode 100644 bundle/src/test/resources/contentsync/riverside-camping-australia.1.json create mode 100644 bundle/src/test/resources/contentsync/wknd-faqs.json create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/POST.jsp create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/.content.xml create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/css.txt create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/css/app.css create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/js.txt create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/js/app.js create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/configure/configurehostdatasource/configurehostdatasource.jsp create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/configure/configurehostentry/configurehostentry.jsp create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/iframe/.content.xml create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/iframe/iframe.jsp create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/selecthostdatasource/selecthostdatasource.jsp create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/strategydatasource/strategydatasource.jsp create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/content/contentsync/.content.xml create mode 100755 ui.apps/src/main/content/jcr_root/apps/acs-commons/content/contentsync/configure/.content.xml create mode 100644 ui.apps/src/main/content/jcr_root/apps/cq/core/content/nav/tools/acs-commons/contentsync/.content.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index bba7095e22..b0a67eb18e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com) ## Unreleased ([details][unreleased changes details]) +- #3151 - New ContentSync utility +- #3147 - Fixed setting initial content-type when importing CFs from a spreadsheet + ## 6.1.0 - 2023-09-08 ## Added diff --git a/bundle/pom.xml b/bundle/pom.xml index c0d2b8c9ec..da16bdf0b2 100644 --- a/bundle/pom.xml +++ b/bundle/pom.xml @@ -736,6 +736,12 @@ gson test + + org.apache.sling + org.apache.sling.jcr.contentloader + 2.2.6 + test + diff --git a/bundle/src/main/java/com/adobe/acs/commons/contentsync/CatalogItem.java b/bundle/src/main/java/com/adobe/acs/commons/contentsync/CatalogItem.java new file mode 100644 index 0000000000..cace9378bd --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/contentsync/CatalogItem.java @@ -0,0 +1,74 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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 com.adobe.acs.commons.contentsync; + + +import javax.json.JsonObject; + +import static org.apache.jackrabbit.JcrConstants.JCR_CONTENT; + +/** + * A Json object describing a resource to sync. + * + * Required fields: + * - path + * - jcr:primaryType + * - exportUri + */ +public class CatalogItem { + private final JsonObject object; + + public CatalogItem(JsonObject object){ + this.object = object; + } + + public String getPath(){ + return object.getString("path"); + } + + public String getPrimaryType(){ + return object.getString("jcr:primaryType"); + } + + public boolean hasContentResource(){ + return getContentUri().endsWith("/" + JCR_CONTENT + ".infinity.json"); + } + + public String getContentUri(){ + return object.getString("exportUri"); + } + + public String getString(String key){ + return object.containsKey(key) ? object.getString(key) : null; + } + + public long getLong(String key){ + return object.containsKey(key) ? object.getJsonNumber(key).longValue() : 0L; + } + + public String getCustomExporter(){ + return object.containsKey("renderServlet") ? object.getString("renderServlet") : null; + + } + + public JsonObject getJsonObject(){ + return object; + } +} diff --git a/bundle/src/main/java/com/adobe/acs/commons/contentsync/ConfigurationUtils.java b/bundle/src/main/java/com/adobe/acs/commons/contentsync/ConfigurationUtils.java new file mode 100644 index 0000000000..736d7fb9b6 --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/contentsync/ConfigurationUtils.java @@ -0,0 +1,60 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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 com.adobe.acs.commons.contentsync; + +import com.adobe.acs.commons.contentsync.impl.LastModifiedStrategy; +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.ResourceUtil; + +import java.util.HashMap; +import java.util.Map; + +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; +import static org.apache.jackrabbit.JcrConstants.NT_UNSTRUCTURED; +import static org.apache.sling.jcr.resource.api.JcrResourceConstants.NT_SLING_FOLDER; + +public class ConfigurationUtils { + public static final String CONFIG_PATH = "/var/acs-commons/contentsync"; + public static final String SETTINGS_PATH = CONFIG_PATH + "/settings"; + public static final String HOSTS_PATH = CONFIG_PATH + "/hosts"; + + public static final String UPDATE_STRATEGY_KEY = "update-strategy"; + public static final String EVENT_USER_DATA_KEY = "event-user-data"; + + private ConfigurationUtils(){ + + } + + public static Resource getSettingsResource(ResourceResolver resourceResolver) throws PersistenceException { + Map resourceProperties = new HashMap<>(); + resourceProperties.put(JCR_PRIMARYTYPE, NT_UNSTRUCTURED); + resourceProperties.put(UPDATE_STRATEGY_KEY, LastModifiedStrategy.class.getName()); + resourceProperties.put(EVENT_USER_DATA_KEY, "changedByPageManagerCopy"); + return ResourceUtil.getOrCreateResource(resourceResolver, SETTINGS_PATH, resourceProperties, NT_SLING_FOLDER, true); + } + + public static Resource getHostsResource(ResourceResolver resourceResolver) throws PersistenceException { + Map resourceProperties = new HashMap<>(); + resourceProperties.put(JCR_PRIMARYTYPE, NT_UNSTRUCTURED); + return ResourceUtil.getOrCreateResource(resourceResolver, HOSTS_PATH, resourceProperties, NT_SLING_FOLDER, true); + } +} diff --git a/bundle/src/main/java/com/adobe/acs/commons/contentsync/ContentCatalog.java b/bundle/src/main/java/com/adobe/acs/commons/contentsync/ContentCatalog.java new file mode 100644 index 0000000000..da80dbecb1 --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/contentsync/ContentCatalog.java @@ -0,0 +1,84 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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 com.adobe.acs.commons.contentsync; + +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; + +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonObject; +import javax.json.JsonReader; +import javax.json.JsonValue; +import java.io.IOException; +import java.io.StringReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + + +public class ContentCatalog { + + private RemoteInstance remoteInstance; + private final String catalogServlet; + + public ContentCatalog(RemoteInstance remoteInstance, String catalogServlet) { + this.remoteInstance = remoteInstance; + this.catalogServlet = catalogServlet; + } + + public URI getFetchURI(String path, String updateStrategy) throws URISyntaxException { + return remoteInstance.toURI(catalogServlet, "root", path, "strategy", updateStrategy); + } + + public List fetch(String path, String updateStrategy) throws IOException, URISyntaxException { + URI uri = getFetchURI(path, updateStrategy); + + String json = remoteInstance.getString(uri); + + JsonObject response; + try(JsonReader reader = Json.createReader(new StringReader(json))) { + response = reader.readObject(); + } + if (!response.containsKey("resources")) { + throw new IOException("Failed to fetch content catalog from " + uri + ", Response: " + json); + } + JsonArray catalog = response.getJsonArray("resources"); + + return catalog.stream() + .map(JsonValue::asJsonObject) + .map(CatalogItem::new) + .collect(Collectors.toList()); + } + + public List getDelta(List catalog, ResourceResolver resourceResolver, UpdateStrategy updateStrategy) { + List lst = new ArrayList<>(); + for(CatalogItem item : catalog){ + Resource resource = resourceResolver.getResource(item.getPath()); + if(resource == null || updateStrategy.isModified(item, resource)){ + lst.add(item); + } + } + return lst; + } + +} diff --git a/bundle/src/main/java/com/adobe/acs/commons/contentsync/ContentReader.java b/bundle/src/main/java/com/adobe/acs/commons/contentsync/ContentReader.java new file mode 100644 index 0000000000..9db0ee9134 --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/contentsync/ContentReader.java @@ -0,0 +1,214 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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 com.adobe.acs.commons.contentsync; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Workspace; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.PropertyDefinition; +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonObject; +import javax.json.JsonObjectBuilder; +import javax.json.JsonString; +import javax.json.JsonValue; +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES; +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; + +public class ContentReader { + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + static final String BINARY_DATA_PLACEHOLDER = "0"; + + private final NodeTypeManager nodeTypeManager; + private final Collection knownPropertyPrefixes; + + public ContentReader(Session session) throws RepositoryException { + Workspace workspace = session.getWorkspace(); + + knownPropertyPrefixes = new HashSet<>(Arrays.asList(workspace.getNamespaceRegistry().getPrefixes())); + nodeTypeManager = workspace.getNodeTypeManager(); + } + + /** + * Recursive sanitize the give JCR node and remove protected properties + * + * @param node json node representing a JCR node + * @return sanitized json + * @see #getProtectedProperties(JsonObject) + */ + public JsonObject sanitize(JsonObject node) throws RepositoryException { + JsonObjectBuilder out = Json.createObjectBuilder(); + sanitize(node, out); + return out.build(); + } + + private void sanitize(JsonObject node, JsonObjectBuilder out) throws RepositoryException { + Collection sanitizedProperties = getProtectedProperties(node); + + for (Map.Entry field : node.entrySet()) { + String name = field.getKey(); + int colonIdx = name.indexOf(':'); + if (colonIdx > 0) { + // sanitize unknown namespaces. These can come, for example, from asset metadata + String prefix = name.substring(0, colonIdx); + if (!knownPropertyPrefixes.contains(prefix)) { + log.trace("skipping protected property: {}", name); + continue; + } + } + // sanitize protected properties + if (sanitizedProperties.contains(name)) { + log.trace("skipping unknown namespace: {}", name); + continue; + } + + JsonValue value = field.getValue(); + switch (value.getValueType()) { + case OBJECT: + JsonObjectBuilder obj = Json.createObjectBuilder(); + sanitize((JsonObject) value, obj); + out.add(name, obj); + break; + case ARRAY: + JsonArray array = (JsonArray) value; + out.add(name, array); + break; + default: + if (colonIdx == 0) { + // Leading colon in Sling GET Servlet JSON designates binary data, e.g. :jcr:data + // Put the real property instead (without a leading colon) and set a dummy value + out.add(name.substring(1), BINARY_DATA_PLACEHOLDER); + } else { + out.add(name, value); + } + break; + } + } + + } + + /** + * Collect protected properties of a given JCR node (non-recursively). + * The list of protected properties consists of: + * - properties protected by node's primary type + * - properties protected by node's mixins + *

+ * For example, if a cq:Page node does not have any mixins applied this method would return + *

+     *      ["jcr:created", "jcr:createdBy"]
+     *  
+ *

+ * If cq:Page is versionable, i.e. has the "mix:versionable" mixin type, then this method would return + * properties protected by the primary type (cq:Page ) and the mixin (mix:versionable) and the list would be + *

+     *      ["jcr:created", "jcr:createdBy", "jcr:versionHistory", "jcr:baseVersion", "jcr:predecessors",
+     *      "jcr:mergeFailed", "jcr:activity", "jcr:configuration", "jcr:isCheckedOut", "jcr:uuid" ]
+     *  
+ * + * @param node json representing a JCR node + * @return the list of protected properties + */ + public List getProtectedProperties(JsonObject node) throws RepositoryException { + Collection ignored = new HashSet<>(Arrays.asList(JCR_PRIMARYTYPE, JCR_MIXINTYPES)); + + List props = new ArrayList<>(); + props.add("rep:policy"); // ACLs are not importable + + List checkTypes = new ArrayList<>(); + String primaryType = node.getString(JCR_PRIMARYTYPE); + checkTypes.add(primaryType); + JsonArray mixins = node.getJsonArray(JCR_MIXINTYPES); + if (mixins != null) { + for (JsonValue item : mixins) { + checkTypes.add(((JsonString) item).getString()); + } + } + for (String typeName : checkTypes) { + NodeType nodeType = nodeTypeManager.getNodeType(typeName); + for (PropertyDefinition definition : nodeType.getPropertyDefinitions()) { + if (definition.isProtected() && !ignored.contains(definition.getName())) { + props.add(definition.getName()); + } + } + } + + return props; + } + + private void collectBinaryProperties(JsonObject node, String parent, List binaryProperties) { + for (Map.Entry field : node.entrySet()) { + String name = field.getKey(); + JsonValue value = field.getValue(); + switch (value.getValueType()) { + case OBJECT: + collectBinaryProperties((JsonObject) value, parent + "/" + name, binaryProperties); + break; + case NUMBER: + // leading colon in Sling GET Servlet JSON and a numeric value designate binary data + if (name.startsWith(":")) { + String propPath = parent + "/" + name.substring(1); + binaryProperties.add(propPath); + } + break; + default: + break; + } + } + } + + /** + * Recursively collect binary properties from a given json node. + *

+ * For example, if node represents a cq:Page object with inline images, + * the output would look like + * + *

+     *  [
+     *    /jcr:content/image/file/jcr:content/jcr:data,
+     *    /jcr:content/image/file/jcr:content/dam:thumbnails/dam:thumbnail_480.png/jcr:content/jcr:data,
+     *    /jcr:content/image/file/jcr:content/dam:thumbnails/dam:thumbnail_60.png/jcr:content/jcr:data,
+     *    /jcr:content/image/file/jcr:content/dam:thumbnails/dam:thumbnail_300.png/jcr:content/jcr:data,
+     *    /jcr:content/image/file/jcr:content/dam:thumbnails/dam:thumbnail_48.png/jcr:content/jcr:data
+     *  ]
+     * 
+ * + * @param node json representing a JCR node + * @return list of property paths relative to the json root + */ + public List collectBinaryProperties(JsonObject node) { + List binaryProperties = new ArrayList<>(); + collectBinaryProperties(node, "", binaryProperties); + return binaryProperties; + } +} diff --git a/bundle/src/main/java/com/adobe/acs/commons/contentsync/ContentSync.java b/bundle/src/main/java/com/adobe/acs/commons/contentsync/ContentSync.java new file mode 100644 index 0000000000..fdf711e04e --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/contentsync/ContentSync.java @@ -0,0 +1,314 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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 com.adobe.acs.commons.contentsync; + +import com.adobe.granite.workflow.WorkflowException; +import com.adobe.granite.workflow.WorkflowSession; +import com.adobe.granite.workflow.exec.WorkflowData; +import com.adobe.granite.workflow.model.WorkflowModel; +import com.day.cq.dam.api.Asset; +import com.day.cq.dam.api.AssetManager; +import com.day.cq.wcm.api.Page; +import com.day.cq.wcm.api.PageManager; +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.api.resource.ResourceUtil; +import org.apache.sling.jcr.contentloader.ContentImporter; +import org.apache.sling.jcr.contentloader.ImportOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Binary; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.version.VersionManager; +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.JsonWriter; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.lang.invoke.MethodHandles; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.apache.jackrabbit.JcrConstants.JCR_CONTENT; +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; + +public class ContentSync { + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private RemoteInstance remoteInstance; + private ContentImporter importer; + private ResourceResolver resourceResolver; + + public ContentSync(RemoteInstance remoteInstance, ResourceResolver resourceResolver, ContentImporter importer) { + this.remoteInstance = remoteInstance; + this.resourceResolver = resourceResolver; + this.importer = importer; + } + + /** + * Ensure that the order of child nodes matches the order on the remote instance. + *

+ * The method makes an HTTP call to the remote instance to fetch the ordered list of child nodes + * and re-sorts the given node to match it. + * + * @param node the node to sort + * @return children after sort + */ + public List sort(Node node) throws RepositoryException, IOException, URISyntaxException { + List children = remoteInstance.listChildren(node.getPath()); + sort(node, children); + return children; + } + + /** + * Sort child nodes of a JCR node + * + * @param node the node to sort + * @param children the desired order of children + */ + public void sort(Node node, List children) throws RepositoryException { + if (!node.getPrimaryNodeType().hasOrderableChildNodes()) { + // node does not support orderable child nodes + return; + } + + Node prev = null; + for (int i = 0; i < children.size(); i++) { + String childName = children.get(children.size() - 1 - i); + if (!node.hasNode(childName)) { + continue; + } + Node n = node.getNode(childName); + if (prev != null) { + node.orderBefore(n.getName(), prev.getName()); + } + prev = n; + } + } + + /** + * Copy binary data from remote instance and update local resource. + * Performs an HTT call for each property path + * + * @param propertyPaths list of binary properties to update, e.g. + *

+     *                      [
+     *                         "/content/contentsync/jcr:content/image/file/jcr:content/jcr:data",
+     *                         "/content/contentsync/jcr:content/image/file/jcr:content/dam:thumbnails/dam:thumbnail_48.png/jcr:content/jcr:data",
+     *                      ]
+     *                      
+ */ + public void copyBinaries(List propertyPaths) throws IOException, RepositoryException, URISyntaxException { + Session session = resourceResolver.adaptTo(Session.class); + for (String propertyPath : propertyPaths) { + try (InputStream ntData = remoteInstance.getStream(propertyPath)) { + Binary binary = session.getValueFactory().createBinary(ntData); + Property p = session.getProperty(propertyPath); + if (p.getType() == PropertyType.BINARY) { + p.setValue(binary); + } else { + Node propertyNode = p.getParent(); + String propertyName = p.getName(); + p.remove(); + propertyNode.setProperty(propertyName, binary); + } + } + } + } + + public ImportOptions getImportOptions() { + return new ImportOptions() { + + @Override + public boolean isCheckin() { + return false; + } + + @Override + public boolean isAutoCheckout() { + return true; + } + + @Override + public boolean isIgnoredImportProvider(String extension) { + return false; + } + + @Override + public boolean isOverwrite() { + // preserve the node, uuid and version history + return false; + } + + @Override + public boolean isPropertyOverwrite() { + return true; + } + }; + } + + /** + * Clear jcr:content and remove all properties except protected ones + * + * @param node the node to clear + */ + public void clearContent(Node node) throws RepositoryException { + if (!node.hasNode(JCR_CONTENT)) { + return; + } + Node jcrContent = node.getNode(JCR_CONTENT); + if (!jcrContent.isCheckedOut()) { + VersionManager versionManager = node.getSession().getWorkspace().getVersionManager(); + versionManager.checkout(jcrContent.getPath()); + } + // remove children of jcr:content + for (NodeIterator iterator = jcrContent.getNodes(); iterator.hasNext(); ) { + Node n = iterator.nextNode(); + n.remove(); + } + // remove any non-protected properties + for (PropertyIterator iterator = jcrContent.getProperties(); iterator.hasNext(); ) { + Property p = iterator.nextProperty(); + if (!p.getDefinition().isProtected()) { + p.remove(); + } + } + } + + /** + * Ensure parent node exists before importing content. + * + * Parent can be null, for example, if a user is sync-ing /content/my-site/en/one + * and the /content/my-site tree does not exist on the local instance. + * + * In such a case the method would fetch the primary type of the parent node (/content/my-site/en) + * and use it as intermediate node type to ensure parent. + * + * @param path the path to ensure if the parent exists + * @return the parent node + */ + public Node ensureParent(String path) throws RepositoryException, IOException, URISyntaxException { + String parentPath = ResourceUtil.getParent(path); + Session session = resourceResolver.adaptTo(Session.class); + Node parentNode; + if (!session.nodeExists(parentPath)) { + String parentNodeType = remoteInstance.getPrimaryType(parentPath); + parentNode = JcrUtils.getOrCreateByPath(parentPath, parentNodeType, parentNodeType, session, false); + } else { + parentNode = session.getNode(parentPath); + } + return parentNode; + } + + /** + * + * importContent("/content/contentsync/page", "jcr:content.json", .... ) + * where /content/contentsync/page is an existing cq:Page resource + * + * importContent("/content/dam/contentsync/asset", "jcr:content.json", .... ) + * where /content/contentsync/asset is an existing dam:Asset resource + * + * importContent("/content/dam/contentsync", "folderName.json", .... ) + * importContent("/content/misc", "nodeName.json", .... ) + * + * @param catalogItem + * @return + */ + public Node ensureContentNode(CatalogItem catalogItem) throws RepositoryException, IOException, URISyntaxException { + String path = catalogItem.getPath(); + + Node parentNode = ensureParent(path); + + Node contentNode; + if (catalogItem.hasContentResource()) { + String nodeName = ResourceUtil.getName(path); + + String primaryType = catalogItem.getString(JCR_PRIMARYTYPE); + if(parentNode.hasNode(nodeName)){ + contentNode = parentNode.getNode(nodeName); + } else { + contentNode = parentNode.addNode(nodeName, primaryType); + } + } else { + contentNode = parentNode; + } + return contentNode; + } + + public void importData(CatalogItem catalogItem, JsonObject jsonObject) throws RepositoryException, IOException, URISyntaxException { + String path = catalogItem.getPath(); + log.debug("importing {}", path); + + Node contentNode = ensureContentNode(catalogItem); + clearContent(contentNode); + + ImportOptions importOptions = getImportOptions(); + String nodeName; + if (catalogItem.hasContentResource()) { + nodeName = JCR_CONTENT; + } else { + nodeName = ResourceUtil.getName(path); + } + + StringWriter sw = new StringWriter(); + try(JsonWriter writer = Json.createWriter(sw)){ + writer.write(jsonObject); + } + InputStream contentStream = new ByteArrayInputStream(sw.toString().getBytes(StandardCharsets.UTF_8)); + importer.importContent(contentNode, nodeName + ".json", contentStream, importOptions, null); + } + + @SuppressWarnings("squid:S112") + public String createVersion(Resource resource) throws Exception { + String revisionId = null; + if (resource.isResourceType("cq:Page")) { + PageManager pageManager = resourceResolver.adaptTo(PageManager.class); + Page pg = resource.adaptTo(Page.class); + if (pg != null) { + revisionId = pageManager.createRevision(pg, null, "created by contentsync").getId(); + } + } else if (resource.isResourceType("dam:Asset")) { + AssetManager assetManager = resourceResolver.adaptTo(AssetManager.class); + Asset asset = resource.adaptTo(Asset.class); + revisionId = assetManager.createRevision(asset, null, "created by contentsync").getId(); + } + return revisionId; + } + + public void runWorkflows(String workflowModel, List paths) throws WorkflowException { + WorkflowSession workflowSession = resourceResolver.adaptTo(WorkflowSession.class); + WorkflowModel model = workflowSession.getModel(workflowModel); + for (String path : paths) { + WorkflowData data = workflowSession.newWorkflowData("JCR_PATH", path); + workflowSession.startWorkflow(model, data); + } + } +} diff --git a/bundle/src/main/java/com/adobe/acs/commons/contentsync/RemoteInstance.java b/bundle/src/main/java/com/adobe/acs/commons/contentsync/RemoteInstance.java new file mode 100644 index 0000000000..9b364e7e45 --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/contentsync/RemoteInstance.java @@ -0,0 +1,176 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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 com.adobe.acs.commons.contentsync; + +import org.apache.http.HttpStatus; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.config.CookieSpecs; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; + +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.JsonReader; +import javax.json.JsonValue; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static javax.jcr.Property.JCR_PRIMARY_TYPE; + +/** + * HTTP connection to a remote AEM instance + some sugar methods to fetch data + */ +public class RemoteInstance implements Closeable { + private static final int CONNECT_TIMEOUT = 5000; + private static final int SOCKET_TIMEOUT = 60000; + + private final CloseableHttpClient httpClient; + private final SyncHostConfiguration hostConfiguration; + + public RemoteInstance(SyncHostConfiguration hostConfiguration) { + this.hostConfiguration = hostConfiguration; + this.httpClient = createHttpClient(); + } + + private CloseableHttpClient createHttpClient() { + BasicCredentialsProvider provider = new BasicCredentialsProvider(); + provider.setCredentials( + AuthScope.ANY, + new UsernamePasswordCredentials(hostConfiguration.getUsername(), hostConfiguration.getPassword())); + RequestConfig requestConfig = RequestConfig + .custom() + .setConnectTimeout(CONNECT_TIMEOUT) + .setSocketTimeout(SOCKET_TIMEOUT) + .setCookieSpec(CookieSpecs.STANDARD).build(); + return + HttpClients.custom() + .setDefaultRequestConfig(requestConfig) + .setDefaultCredentialsProvider(provider) + .build(); + } + + public InputStream getStream(String path) throws IOException, URISyntaxException { + URI uri = toURI(path); + + return getStream(uri); + } + + public InputStream getStream(URI uri ) throws IOException { + HttpGet request = new HttpGet(uri); + CloseableHttpResponse response = httpClient.execute(request); + String msg; + switch (response.getStatusLine().getStatusCode()){ + case HttpStatus.SC_OK: + return response.getEntity().getContent(); + case HttpStatus.SC_MULTIPLE_CHOICES: + msg = formatError(uri.toString(), response.getStatusLine().getStatusCode(), + "It seems that the \"Json Max Results\" in Sling Get Servlet is too low. Increase it to a higher value, e.g. 1000."); + throw new IOException(msg); + default: + msg = formatError(uri.toString(), response.getStatusLine().getStatusCode(), "Response: " + EntityUtils.toString(response.getEntity())); + throw new IOException(msg); + } + } + + public String getString(URI uri) throws IOException { + HttpGet request = new HttpGet(uri); + try (CloseableHttpResponse response = httpClient.execute(request)) { + String str = EntityUtils.toString(response.getEntity()); + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + return str; + } else { + String msg = formatError(uri.toString(), response.getStatusLine().getStatusCode(), "Response: " + str); + throw new IOException(msg); + } + } + } + + private String formatError(String uri, int statusCode, String message) { + return String.format("Failed to fetch data from %s, HTTP [%d]%n%s", uri, statusCode, message); + } + + public String getPrimaryType(String path) throws IOException, URISyntaxException { + URI uri = toURI(path + "/" + JCR_PRIMARY_TYPE); + String str = getString(uri); + if (str.isEmpty()) { + throw new IllegalStateException("It appears " + hostConfiguration.getUsername() + + " user does not have permissions to read " + uri); + } + return str; + } + + public List listChildren(String path) throws IOException, URISyntaxException { + List children; + try (InputStream is = getStream(path + ".1.json"); JsonReader reader = Json.createReader(is)) { + children = reader + .readObject() + .entrySet() + .stream() + .filter(entry -> entry.getValue().getValueType() == JsonValue.ValueType.OBJECT) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + } + return children; + } + + public JsonObject getJson(String path, String... parameters) throws IOException, URISyntaxException { + URI uri = toURI(path, parameters); + return getJson(uri); + } + + public JsonObject getJson(URI uri) throws IOException { + try (InputStream is = getStream(uri); JsonReader reader = Json.createReader(is)) { + return reader.readObject(); + } + } + + URI toURI(String path, String ... parameters) throws URISyntaxException { + URIBuilder ub = new URIBuilder(hostConfiguration.getHost()) + .setPath(path); + if(parameters != null) { + if (parameters.length % 2 != 0) { + throw new IllegalArgumentException("query string parameters must be an even number of name/values:" + Arrays.asList(parameters)); + } + for (int i = 0; i < parameters.length; i += 2) { + ub.addParameter(parameters[i], parameters[i + 1]); + } + } + return ub.build(); + } + + @Override + public void close() throws IOException { + httpClient.close(); + } +} diff --git a/bundle/src/main/java/com/adobe/acs/commons/contentsync/SyncHostConfiguration.java b/bundle/src/main/java/com/adobe/acs/commons/contentsync/SyncHostConfiguration.java new file mode 100644 index 0000000000..6b01f7b5a6 --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/contentsync/SyncHostConfiguration.java @@ -0,0 +1,54 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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 com.adobe.acs.commons.contentsync; + +import org.apache.sling.api.resource.Resource; +import org.apache.sling.models.annotations.DefaultInjectionStrategy; +import org.apache.sling.models.annotations.Model; +import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy; +import org.apache.sling.models.annotations.injectorspecific.ValueMapValue; + +/** + * Adapts to configuration nodes in /var/acs-commons/contentsync/hosts/* + */ +@Model(adaptables = {Resource.class}, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL) +public class SyncHostConfiguration { + + @ValueMapValue(injectionStrategy = InjectionStrategy.REQUIRED) + private String host; + + @ValueMapValue + private String username; + + @ValueMapValue + private String password; + + public String getHost() { + return host; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } +} diff --git a/bundle/src/main/java/com/adobe/acs/commons/contentsync/UpdateStrategy.java b/bundle/src/main/java/com/adobe/acs/commons/contentsync/UpdateStrategy.java new file mode 100644 index 0000000000..7b9ce36bb5 --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/contentsync/UpdateStrategy.java @@ -0,0 +1,54 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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 com.adobe.acs.commons.contentsync; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.resource.Resource; + +import java.util.List; + +public interface UpdateStrategy { + + /** + * This method is called on the remote instance when Content Sync requests the list of resources to sync + * + * @param request the request from the Content Sync UI + * @return the list of resources to sync + */ + List getItems(SlingHttpServletRequest request); + + + /** + * Compare local and remote resources and decided whether the resource was modified and need to be sync-ed + * + * @param remoteResource json representation of a remote resource + * @param localResource local resource + * @return whether the resource was modified + */ + boolean isModified(CatalogItem remoteResource, Resource localResource); + + /** + * + * @param remoteResource json representation of a remote resource + * @param localResource local resource + * @return message to print in the UI + */ + String getMessage(CatalogItem remoteResource, Resource localResource); +} diff --git a/bundle/src/main/java/com/adobe/acs/commons/contentsync/impl/AssetChecksumStrategy.java b/bundle/src/main/java/com/adobe/acs/commons/contentsync/impl/AssetChecksumStrategy.java new file mode 100644 index 0000000000..1ae1a55319 --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/contentsync/impl/AssetChecksumStrategy.java @@ -0,0 +1,123 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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 com.adobe.acs.commons.contentsync.impl; + +import com.adobe.acs.commons.contentsync.CatalogItem; +import com.adobe.acs.commons.contentsync.UpdateStrategy; +import com.day.cq.dam.api.Asset; +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.resource.Resource; +import org.osgi.service.component.annotations.Component; + +import javax.json.Json; +import javax.json.JsonObjectBuilder; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import static org.apache.jackrabbit.JcrConstants.JCR_CONTENT; +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; + +@Component +public class AssetChecksumStrategy implements UpdateStrategy { + private static final String DAM_SHA1 = "dam:sha1"; + + @Override + public boolean isModified(CatalogItem remoteResource, Resource localResource) { + String remoteChecksum = getChecksum(remoteResource); + String localChecksum = getChecksum(localResource); + + return remoteChecksum != null && !remoteChecksum.equals(localChecksum); + } + + @Override + public List getItems(SlingHttpServletRequest request) { + String rootPath = request.getParameter("root"); + if (rootPath == null) { + throw new IllegalArgumentException("root request parameter is required"); + } + String query = "SELECT * FROM [dam:Asset] AS s WHERE ISDESCENDANTNODE([" + rootPath + "]) ORDER BY s.[jcr:path]"; + Iterator it = request.getResourceResolver().findResources(query, "JCR-SQL2"); + List items = new ArrayList<>(); + while (it.hasNext()) { + Resource res = it.next(); + Asset asset = res.adaptTo(Asset.class); + if(asset == null){ + continue; + } + JsonObjectBuilder json = Json.createObjectBuilder(); + writeMetadata(json, res); + items.add(new CatalogItem(json.build())); + } + return items; + } + + @Override + @SuppressWarnings("squid:S2583") + public String getMessage(CatalogItem remoteResource, Resource localResource) { + String remoteChecksum = getChecksum(remoteResource); + String localChecksum = getChecksum(localResource); + + boolean modified = remoteChecksum != null && !remoteChecksum.equals(localChecksum); + StringBuilder msg = new StringBuilder(); + if (localResource == null) { + msg.append("resource does not exist"); + } else { + msg.append(modified ? "resource modified ... " : "replacing ... "); + if (localChecksum != null) { + msg.append('\n'); + msg.append("\tlocal checksum: " + localChecksum); + } + if (remoteChecksum != null) { + msg.append('\n'); + msg.append("\tremote checksum: " + remoteChecksum); + } + } + return msg.toString(); + } + + private String getChecksum(CatalogItem remoteItem) { + return remoteItem.getString(DAM_SHA1); + } + + @SuppressWarnings("squid:S1144") + private String getChecksum(Resource targetResource) { + if (targetResource == null) { + return null; + } + Asset asset = targetResource.adaptTo(Asset.class); + return asset == null ? null : (String) asset.getMetadata(DAM_SHA1); + } + + public void writeMetadata(JsonObjectBuilder jw, Resource res) { + jw.add("path", res.getPath()); + jw.add(JCR_PRIMARYTYPE, res.getValueMap().get(JCR_PRIMARYTYPE, String.class)); + + Resource jcrContent = res.getChild(JCR_CONTENT); + String exportUri; + if (jcrContent != null) { + exportUri = jcrContent.getPath() + ".infinity.json"; + } else { + exportUri = res.getPath() + ".json"; + } + jw.add("exportUri", exportUri); + jw.add(DAM_SHA1, getChecksum(res)); + } +} diff --git a/bundle/src/main/java/com/adobe/acs/commons/contentsync/impl/LastModifiedStrategy.java b/bundle/src/main/java/com/adobe/acs/commons/contentsync/impl/LastModifiedStrategy.java new file mode 100644 index 0000000000..24123a5566 --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/contentsync/impl/LastModifiedStrategy.java @@ -0,0 +1,264 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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 com.adobe.acs.commons.contentsync.impl; + +import com.adobe.acs.commons.contentsync.CatalogItem; +import com.adobe.acs.commons.contentsync.UpdateStrategy; +import com.adobe.granite.security.user.util.AuthorizableUtil; +import com.day.cq.commons.PathInfo; +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.request.RequestPathInfo; +import org.apache.sling.api.resource.AbstractResourceVisitor; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ValueMap; +import org.apache.sling.api.servlets.ServletResolver; +import org.apache.sling.api.wrappers.SlingHttpServletRequestWrapper; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import javax.json.Json; +import javax.json.JsonObjectBuilder; +import javax.servlet.GenericServlet; +import javax.servlet.Servlet; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.List; + +import static org.apache.jackrabbit.JcrConstants.JCR_CONTENT; +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; + +@Component +public class LastModifiedStrategy implements UpdateStrategy { + public static final String DEFAULT_GET_SERVLET = "org.apache.sling.servlets.get.DefaultGetServlet"; + public static final String REDIRECT_SERVLET = "org.apache.sling.servlets.get.impl.RedirectServlet"; + + @Reference + private ServletResolver servletResolver; + + @Override + public List getItems(SlingHttpServletRequest request) { + String rootPath = request.getParameter("root"); + if (rootPath == null) { + throw new IllegalArgumentException("root request parameter is required"); + } + + Resource root = request.getResourceResolver().getResource(rootPath); + if (root == null) { + return Collections.emptyList(); + } + + List items = new ArrayList<>(); + new AbstractResourceVisitor() { + @Override + public void visit(Resource res) { + if (!accepts(res)) { + return; + } + JsonObjectBuilder json = Json.createObjectBuilder(); + writeMetadata(json, res, request); + items.add(new CatalogItem(json.build())); + } + }.accept(root); + return items; + } + + @Override + public boolean isModified(CatalogItem remoteResource, Resource localResource) { + LastModifiedInfo remoteLastModified = getLastModified(remoteResource); + LastModifiedInfo localLastModified = getLastModified(localResource); + + return remoteLastModified.getLastModified() > localLastModified.getLastModified(); + } + + @Override + @SuppressWarnings("squid:S2583") + public String getMessage(CatalogItem remoteResource, Resource localResource) { + LastModifiedInfo remoteLastModified = getLastModified(remoteResource); + LastModifiedInfo localLastModified = getLastModified(localResource); + + boolean modified = remoteLastModified.getLastModified() > localLastModified.getLastModified(); + SimpleDateFormat dateFormat = new SimpleDateFormat("MMM dd, yyyy, h:mm:ss a"); + StringBuilder msg = new StringBuilder(); + if (localResource == null) { + msg.append("resource does not exist"); + } else { + msg.append(modified ? "resource modified ... " : "replacing ... "); + if (localLastModified.getLastModified() > 0) { + msg.append('\n'); + msg.append("\tlocal lastModified: " + dateFormat.format(localLastModified.getLastModified()) + " by " + localLastModified.getLastModifiedBy()); + } + if (remoteLastModified.getLastModified() > 0) { + msg.append('\n'); + msg.append("\tremote lastModified: " + dateFormat.format(remoteLastModified.getLastModified()) + " by " + remoteLastModified.getLastModifiedBy()); + } + } + return msg.toString(); + } + + /** + * Determines whether to write the resource in the catalog json. + *

+ * For example, implementations can return only dam:Asset nodes, + * or any nt:unstructured nodes, etc. + * + * @param resource the resource to check + * @return whether to write the resource in the catalog + */ + boolean accepts(Resource resource) { + if ( + // don't drill down into jcr:content. The entire content will be grabbed by jcr:content.infinity.json + resource.getPath().contains("/" + JCR_CONTENT) + // ignore rep:policy, rep:cugPolicy, rep:restrictions and such + || resource.getPath().contains("/rep:") + ) { + return false; + } + return true; + } + + /** + * Returns the render servlet for the given urlPath . + * + * @param urlPath the json export url,. e.g. /content/wknd/page/jcr:content.infinity.json + * @return the render servlet, e.g. org.apache.sling.servlets.get.DefaultGetServlet + */ + String getJsonRendererServlet(SlingHttpServletRequest slingRequest, String urlPath) { + Resource resource = slingRequest.getResourceResolver().resolve(urlPath); + Servlet s = servletResolver.resolveServlet(new SlingHttpServletRequestWrapper(slingRequest) { + @Override + public Resource getResource() { + return resource; + } + + @Override + public String getMethod() { + return "GET"; + } + + @Override + public RequestPathInfo getRequestPathInfo() { + return new PathInfo(urlPath); + } + }); + String servletName = null; + if (s instanceof GenericServlet) { + GenericServlet genericServlet = (GenericServlet) s; + servletName = genericServlet.getServletName(); + } + // Sling Redirect Servlet handles json exports by forwarding to DefaultGetServlet. So do we. + if (REDIRECT_SERVLET.equals(servletName)) { + servletName = DEFAULT_GET_SERVLET; + } + return servletName; + } + + void writeMetadata(JsonObjectBuilder jw, Resource res, SlingHttpServletRequest request) { + jw.add("path", res.getPath()); + jw.add(JCR_PRIMARYTYPE, res.getValueMap().get(JCR_PRIMARYTYPE, String.class)); + + Resource jcrContent = res.getChild(JCR_CONTENT); + String exportUri; + Resource contentResource; + if (jcrContent != null) { + contentResource = jcrContent; + exportUri = jcrContent.getPath() + ".infinity.json"; + } else { + contentResource = res; + exportUri = res.getPath() + ".json"; + } + + String renderServlet = getJsonRendererServlet(request, exportUri); + // check if the resource is rendered by DefaultGetServlet, i.e. is exportable + // if it isn't, go one level up and try the parent + if (!DEFAULT_GET_SERVLET.equals(renderServlet)) { + contentResource = contentResource.getParent(); + exportUri = contentResource.getPath() + ".infinity.json"; + } + + // last try: if the rendering servlet is still not DefaultGetServlet then + // put a flag in the output. + renderServlet = getJsonRendererServlet(request, exportUri); + if (!DEFAULT_GET_SERVLET.equals(renderServlet)) { + jw.add("renderServlet", renderServlet); + } + jw.add("exportUri", exportUri); + + LastModifiedInfo lastModified = getLastModified(res); + + if (lastModified.getLastModified() > 0L) { + jw.add("lastModified", lastModified.getLastModified()); + } + if (lastModified.getLastModifiedBy() != null) { + jw.add("lastModifiedBy", lastModified.getLastModifiedBy()); + } + } + + private LastModifiedInfo getLastModified(CatalogItem item) { + long lastModified = item.getLong("lastModified"); + String lastModifiedBy = item.getString("lastModifiedBy"); + return new LastModifiedInfo(lastModified, lastModifiedBy); + } + + @SuppressWarnings("squid:S1144") + private LastModifiedInfo getLastModified(Resource targetResource) { + long lastModified = 0L; + String lastModifiedBy = null; + if (targetResource != null) { + Resource contentResource = targetResource.getChild(JCR_CONTENT); + if (contentResource == null) { + contentResource = targetResource; + } + ValueMap vm = contentResource.getValueMap(); + Calendar c = (Calendar) vm.get("cq:lastModified", (Class) Calendar.class); + if (c == null) { + c = (Calendar) vm.get("jcr:lastModified", (Class) Calendar.class); + } + if (c != null) { + lastModified = c.getTime().getTime(); + } + String modifiedBy = (String) vm.get("cq:lastModifiedBy", (Class) String.class); + if (modifiedBy == null) { + modifiedBy = (String) vm.get("jcr:lastModifiedBy", (Class) String.class); + } + lastModifiedBy = AuthorizableUtil.getFormattedName(targetResource.getResourceResolver(), modifiedBy); + } + return new LastModifiedInfo(lastModified, lastModifiedBy); + } + + private static class LastModifiedInfo { + private final long lastModified; + private final String lastModifiedBy; + + public LastModifiedInfo(long lastModified, String lastModifiedBy) { + this.lastModified = lastModified; + this.lastModifiedBy = lastModifiedBy; + } + + public long getLastModified() { + return lastModified; + } + + public String getLastModifiedBy() { + return lastModifiedBy; + } + } +} diff --git a/bundle/src/main/java/com/adobe/acs/commons/contentsync/package-info.java b/bundle/src/main/java/com/adobe/acs/commons/contentsync/package-info.java new file mode 100644 index 0000000000..7b3da34cc6 --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/contentsync/package-info.java @@ -0,0 +1,21 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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% + */ +@org.osgi.annotation.versioning.Version("1.0.0") +package com.adobe.acs.commons.contentsync; diff --git a/bundle/src/main/java/com/adobe/acs/commons/contentsync/servlet/ContentCatalogServlet.java b/bundle/src/main/java/com/adobe/acs/commons/contentsync/servlet/ContentCatalogServlet.java new file mode 100644 index 0000000000..1ee787c13b --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/contentsync/servlet/ContentCatalogServlet.java @@ -0,0 +1,109 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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 com.adobe.acs.commons.contentsync.servlet; + +import com.adobe.acs.commons.contentsync.CatalogItem; +import com.adobe.acs.commons.contentsync.UpdateStrategy; +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.SlingHttpServletResponse; +import org.apache.sling.api.servlets.SlingSafeMethodsServlet; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; + +import javax.json.Json; +import javax.json.JsonArrayBuilder; +import javax.json.JsonObjectBuilder; +import javax.json.JsonWriter; +import javax.servlet.Servlet; +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; + +@Component(service = Servlet.class, immediate = true, property = { + "sling.servlet.extensions=json", + "sling.servlet.selectors=catalog", + "sling.servlet.resourceTypes=acs-commons/components/utilities/contentsync", +}) +public class ContentCatalogServlet extends SlingSafeMethodsServlet { + + private final transient Map updateStrategies = Collections.synchronizedMap(new LinkedHashMap<>()); + + @Reference(service = UpdateStrategy.class, + cardinality = ReferenceCardinality.MULTIPLE, + policy = ReferencePolicy.DYNAMIC) + protected void bindDeltaStrategy(UpdateStrategy strategy) { + if (strategy != null) { + String key = strategy.getClass().getName(); + updateStrategies.put(key, strategy); + } + } + + protected void unbindDeltaStrategy(UpdateStrategy strategy) { + String key = strategy.getClass().getName(); + updateStrategies.remove(key); + } + + @Override + protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException { + response.setContentType("application/json"); + + JsonObjectBuilder result = Json.createObjectBuilder(); + try { + JsonArrayBuilder resources = Json.createArrayBuilder(); + String pid = request.getParameter("strategy"); + UpdateStrategy updateStrategy = getStrategy(pid); + List items = updateStrategy.getItems(request); + + for (CatalogItem item : items) { + resources.add(item.getJsonObject()); + } + result.add("resources", resources); + } catch (Exception e){ + result.add("error", e.getMessage()); + response.setStatus(SC_INTERNAL_SERVER_ERROR); + } + + try(JsonWriter out = Json.createWriter(response.getWriter())){ + out.writeObject(result.build()); + } + } + + UpdateStrategy getStrategy(String pid) { + UpdateStrategy strategy; + if(pid == null){ + strategy = updateStrategies.values().iterator().next(); + } else { + strategy = updateStrategies.get(pid); + if(strategy == null){ + throw new IllegalArgumentException("Cannot find UpdateStrategy for pid " + pid + "." + + " Available strategies: " + updateStrategies.values() + .stream().map(s -> s.getClass().getName()).collect(Collectors.toList())); + } + } + return strategy; + } +} \ No newline at end of file diff --git a/bundle/src/test/java/com/adobe/acs/commons/contentsync/TestContentReader.java b/bundle/src/test/java/com/adobe/acs/commons/contentsync/TestContentReader.java new file mode 100644 index 0000000000..bb9ae7d2fb --- /dev/null +++ b/bundle/src/test/java/com/adobe/acs/commons/contentsync/TestContentReader.java @@ -0,0 +1,221 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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 com.adobe.acs.commons.contentsync; + +import io.wcm.testing.mock.aem.junit.AemContext; +import org.apache.sling.testing.mock.sling.ResourceResolverType; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.json.Json; +import javax.json.JsonObject; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class TestContentReader { + @Rule + public AemContext context = new AemContext(ResourceResolverType.JCR_OAK); + + ContentReader reader; + JsonObject jcrContent; + + @Before + public void setUp() throws RepositoryException { + reader = new ContentReader(context.resourceResolver().adaptTo(Session.class)); + jcrContent = Json.createReader(getClass().getResourceAsStream("/contentsync/jcr_content.json")).readObject(); + } + + @Test + public void collectBinaryData() { + + List props = reader.collectBinaryProperties(jcrContent); + List expected = Arrays.asList( + "/customBinaryProperty", + "/image/file/jcr:content/jcr:data", + "/image/file/jcr:content/dam:thumbnails/dam:thumbnail_480.png/jcr:content/jcr:data", + "/image/file/jcr:content/dam:thumbnails/dam:thumbnail_60.png/jcr:content/jcr:data", + "/image/file/jcr:content/dam:thumbnails/dam:thumbnail_300.png/jcr:content/jcr:data", + "/image/file/jcr:content/dam:thumbnails/dam:thumbnail_48.png/jcr:content/jcr:data" + ); + assertEquals(expected, props); + } + + @Test + public void collectProtectedProperties() throws Exception { + + JsonObject page1 = Json.createObjectBuilder() + .add("jcr:primaryType", "cq:PageContent") + .build(); + assertEquals(Arrays.asList("rep:policy", "jcr:created", "jcr:createdBy"), reader.getProtectedProperties(page1)); + + JsonObject page2 = Json.createObjectBuilder() + .add("jcr:primaryType", "cq:PageContent") + .add("jcr:mixinTypes", Json.createArrayBuilder().add("mix:versionable").build()) + .build(); + + assertEquals( + Arrays.asList( + "rep:policy", "jcr:created", "jcr:createdBy", "jcr:versionHistory", "jcr:baseVersion", "jcr:predecessors", + "jcr:mergeFailed", "jcr:activity", "jcr:configuration", "jcr:isCheckedOut", "jcr:uuid"), + reader.getProtectedProperties(page2)); + } + + @Test + public void sanitizeProtectedProperties() throws Exception { + JsonObject node = Json.createObjectBuilder() + .add("jcr:primaryType", "cq:PageContent") + .add("jcr:mixinTypes", Json.createArrayBuilder().add("mix:versionable").add("rep:AccessControllable").build()) + .add("jcr:created", "sanitize me") + .add("jcr:createdBy", "sanitize me") + .add("jcr:uuid", "sanitize me") + .add("jcr:versionHistory", "sanitize me") + .add("jcr:predecessors", "sanitize me") + .add("referenceable", Json.createObjectBuilder() + .add("jcr:primaryType", "nt:unstructured") + .add("jcr:mixinTypes", Json.createArrayBuilder().add("mix:referenceable").build()) + .add("jcr:uuid", "sanitize me") + .add("jcr:created", "retain me") + .add("jcr:createdBy", "retain me") + .build()) + .add("content", Json.createObjectBuilder() + .add("jcr:primaryType", "nt:unstructured") + .add("jcr:created", "retain me") + .add("jcr:createdBy", "retain me") + .add("jcr:uuid", "retain me") + .build()) + .add("rep:policy", Json.createObjectBuilder() + .add("jcr:primaryType", "rep:ACL") + .add("allow", Json.createObjectBuilder() + .add("jcr:primaryType", "rep:GrantACE") + .add("rep:principalName", "everyone") + .add("rep:privileges", Json.createArrayBuilder().add("jcr:read").build()) + .build()) + .build()) + .build(); + + JsonObject sanitizedContent = reader.sanitize(node); + + assertFalse(sanitizedContent.containsKey("jcr:created")); // protected by mix:created via cq:PageContent + assertFalse(sanitizedContent.containsKey("jcr:createdBy")); // protected by mix:created via cq:PageContent + assertFalse(sanitizedContent.containsKey("jcr:uuid")); // protected by mix:referenceable via mix:versionable + assertFalse(sanitizedContent.containsKey("jcr:versionHistory")); // protected by mix:referenceable via mix:versionable + assertFalse(sanitizedContent.containsKey("jcr:predecessors")); // protected by mix:referenceable via mix:versionable + + JsonObject referenceable = sanitizedContent.getJsonObject("referenceable"); + assertFalse(referenceable.containsKey("jcr:uuid")); + // jcr:created and jcr:createdBy are retained because the node is not mix:created and these two are not protected + assertTrue(referenceable.containsKey("jcr:created")); + assertTrue(referenceable.containsKey("jcr:createdBy")); + + JsonObject content = sanitizedContent.getJsonObject("content"); + // a plain nt:unstructured node can have any property + assertTrue(content.containsKey("jcr:uuid")); + assertTrue(content.containsKey("jcr:created")); + assertTrue(content.containsKey("jcr:createdBy")); + + // ACls are not importable + assertFalse(sanitizedContent.containsKey("rep:policy")); + } + + @Test + public void sanitizeUnknownNamespaces() throws Exception { + JsonObject content = Json.createObjectBuilder() + .add("jcr:primaryType", "nt:unstructured") + .add("test1:one", "sanitize me") + .add("test1:two", "sanitize me") + .add("metadata", Json.createObjectBuilder() + .add("jcr:primaryType", "nt:unstructured") + .add("test2:one", "sanitize me") + .add("test2:two", "sanitize me") + .build()) + .add("test3:one", Json.createObjectBuilder() + .add("jcr:primaryType", "nt:unstructured") + .build()) + .build(); + + JsonObject sanitizedContent = reader.sanitize(content); + assertFalse(sanitizedContent.containsKey("test1:one")); + assertFalse(sanitizedContent.containsKey("test1:two")); + + assertFalse(sanitizedContent.containsKey("test3:one")); + + JsonObject metadata = sanitizedContent.getJsonObject("metadata"); + assertFalse(metadata.containsKey("test2:one")); + assertFalse(metadata.containsKey("test2:two")); + } + + @Test + public void sanitizeBinaryProperties() throws Exception { + JsonObject node = Json.createObjectBuilder() + .add("jcr:primaryType", "nt:unstructured") + .add(":jcr:data", 100L) + .add(":customProperty", 200L) + .add("file", Json.createObjectBuilder() + .add("jcr:primaryType", "nt:file") + .add("jcr:content", Json.createObjectBuilder() + .add("jcr:primaryType", "nt:resource") + .add(":jcr:data", 300L) + .build()) + .build()) + .build(); + + JsonObject sanitizedContent = reader.sanitize(node); + + assertFalse(sanitizedContent.containsKey(":jcr:data")); + assertEquals(ContentReader.BINARY_DATA_PLACEHOLDER, sanitizedContent.getString("jcr:data")); + + assertFalse(sanitizedContent.containsKey(":customProperty")); + assertEquals(ContentReader.BINARY_DATA_PLACEHOLDER, sanitizedContent.getString("customProperty")); + + JsonObject file = sanitizedContent.getValue("/file/jcr:content").asJsonObject(); + assertFalse(file.containsKey(":jcr:data")); + assertEquals(ContentReader.BINARY_DATA_PLACEHOLDER, file.getString("jcr:data")); + } + + /** + * A node type exists on the source instance, but not on the target. + * + * In this case the import fails with a NoSuchNodeTypeException. + */ + @Test(expected = NoSuchNodeTypeException.class) + public void sanitizeUnknownPropertyTypes() throws Exception { + JsonObject node = Json.createObjectBuilder() + .add("jcr:primaryType", "nt:unstructured") + .add("node1", Json.createObjectBuilder() + .add("jcr:primaryType", "nt:aaa") + .build()) + .add("node2", Json.createObjectBuilder() + .add("jcr:primaryType", "nt:bbb") + .build()) + .build(); + + + JsonObject sanitizedContent = reader.sanitize(node); + + } +} diff --git a/bundle/src/test/java/com/adobe/acs/commons/contentsync/TestContentSync.java b/bundle/src/test/java/com/adobe/acs/commons/contentsync/TestContentSync.java new file mode 100644 index 0000000000..1d4f810896 --- /dev/null +++ b/bundle/src/test/java/com/adobe/acs/commons/contentsync/TestContentSync.java @@ -0,0 +1,408 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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 com.adobe.acs.commons.contentsync; + +import com.day.cq.dam.api.Asset; +import com.day.cq.wcm.api.Page; +import io.wcm.testing.mock.aem.junit.AemContext; +import org.apache.commons.io.IOUtils; +import org.apache.sling.api.resource.ModifiableValueMap; +import org.apache.sling.api.resource.ValueMap; +import org.apache.sling.jcr.contentloader.ContentImporter; +import org.apache.sling.jcr.contentloader.internal.ContentReaderWhiteboard; +import org.apache.sling.jcr.contentloader.internal.DefaultContentImporter; +import org.apache.sling.jcr.contentloader.internal.readers.JsonReader; +import org.apache.sling.testing.mock.sling.ResourceResolverType; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.version.Version; +import javax.jcr.version.VersionManager; +import javax.json.Json; +import javax.json.JsonObject; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +public class TestContentSync { + @Rule + public AemContext context = new AemContext(ResourceResolverType.JCR_OAK); + + ContentSync contentSync; + ContentReader reader; + RemoteInstance remoteInstance; + + @Before + public void setUp() throws RepositoryException { + context.registerInjectActivateService(new JsonReader()); + context.registerInjectActivateService(new ContentReaderWhiteboard()); + context.addModelsForClasses(SyncHostConfiguration.class); + reader = new ContentReader(context.resourceResolver().adaptTo(Session.class)); + + String configPath = "/var/acs-commons/contentsync/config"; + context.build().resource(configPath, "host", "http://localhost:4502", "username", "", "password", ""); + SyncHostConfiguration hostConfiguration = context.resourceResolver().getResource(configPath).adaptTo(SyncHostConfiguration.class); + ContentImporter contentImporter = context.registerInjectActivateService(new DefaultContentImporter()); + remoteInstance = spy(new RemoteInstance(hostConfiguration)); + + contentSync = new ContentSync(remoteInstance, context.resourceResolver(), contentImporter); + } + + @Test + public void sortNodes() throws Exception { + context.build().atParent() + .resource("/content/sorted", "jcr:primaryType", "cq:Page") + .resource("/content/sorted/one") + .resource("/content/sorted/two") + .resource("/content/sorted/three") + ; + Node node = context.resourceResolver().getResource("/content/sorted").adaptTo(Node.class); + // assert initial ordering + assertEquals(Arrays.asList("one", "two", "three"), getChildNodeNames(node)); + + contentSync.sort(node, Arrays.asList("three", "one", "two")); + assertEquals(Arrays.asList("three", "one", "two"), getChildNodeNames(node)); + + // assert initial ordering + contentSync.sort(node, Arrays.asList("one", "unknown", "three", "rep:policy", "two")); // unknown / non-synced nodes are ignored + assertEquals(Arrays.asList("one", "three", "two"), getChildNodeNames(node)); + + context.build() + .resource("/content/nonsortable", "jcr:primaryType", "sling:Folder") + .resource("/content/nonsortable/three") + .resource("/content/nonsortable/two") + .resource("/content/nonsortable/one") + ; + + // The sort operation has no effect if the parent node does not support orderable child nodes + node = context.resourceResolver().getResource("/content/nonsortable").adaptTo(Node.class); + assertEquals(Arrays.asList("three", "two", "one"), getChildNodeNames(node)); + contentSync.sort(node, Arrays.asList("one", "three", "rep:policy", "two")); + assertEquals(Arrays.asList("three", "two", "one"), getChildNodeNames(node)); // ordering didn't change + } + + /** + * @return list of names of child nodes + */ + private List getChildNodeNames(Node node) throws RepositoryException { + List children = new ArrayList<>(); + for (NodeIterator it = node.getNodes(); it.hasNext(); ) { + Node child = it.nextNode(); + children.add(child.getName()); + } + return children; + } + + @Test + public void sortNodesFromRemote() throws Exception { + String contentRoot = "/content/dam/riverside-camping-australia"; + context.load().json(getClass().getResourceAsStream("/contentsync/riverside-camping-australia.1.json"), contentRoot); + + //fake HTTP call to get the list of children from, remote + doReturn(getClass().getResourceAsStream("/contentsync/riverside-camping-australia.1.json")) + .when(remoteInstance).getStream(anyString()); + + Node node = context.resourceResolver().getResource(contentRoot).adaptTo(Node.class); + List children = contentSync.sort(node); + + assertEquals(Arrays.asList( + "adobestock-216674449.jpeg", + "jcr:content", + "adobe_waadobe_wa_mg_2466.jpg", + "riverside-camping-australia", + "adobestock-257541512.jpeg", + "adobestock-238491803.jpeg", + "adobestock-178022573.jpeg", + "adobestock-167833331.jpeg"), children); + } + + @Test + public void testClearContent() throws Exception { + String path = "/content/contentsync/page"; + context.build().resource(path, "jcr:primaryType", "cq:Page"); + context.load().json(getClass().getResourceAsStream("/contentsync/jcr_content.json"), path + "/jcr:content"); + + Node node = context.resourceResolver().getResource(path).adaptTo(Node.class); + Node jcrContent = node.getNode("jcr:content"); + + List userProps = Arrays.asList("jcr:title", "cq:conf", "cq:designPath"); + List systemProps = Arrays.asList( + "jcr:created", "jcr:createdBy", "jcr:uuid", "jcr:versionHistory", + "jcr:predecessors", "jcr:isCheckedOut", "jcr:mixinTypes", "jcr:primaryType", "jcr:baseVersion"); + + + assertTrue(jcrContent.getNodes().hasNext()); // jcr:content has children initially + for (String name : userProps) { + assertTrue(name, jcrContent.hasProperty(name)); // there is user data in jcr:content + } + for (String name : systemProps) { + assertTrue(name, jcrContent.hasProperty(name)); // there are system properties + } + + contentSync.clearContent(node); + + assertFalse(jcrContent.getNodes().hasNext()); // no children under jcr:content + for (String name : userProps) { + assertFalse(name, jcrContent.hasProperty(name)); // user properties in jcr:content are cleared + } + for (String name : systemProps) { + assertTrue(name, jcrContent.hasProperty(name)); // system properties are retained + } + } + + @Test + public void testEnsureParent_NonExisting() throws IOException, RepositoryException, URISyntaxException { + String path = "/content/my-site/folder/page"; + + // parent does not exist. make HTTP call to fetch jcr:primaryType of it from remote + doReturn("cq:Page").when(remoteInstance).getPrimaryType(anyString()); + Node parent = contentSync.ensureParent(path); + + assertEquals("/content/my-site/folder", parent.getPath()); + assertEquals("cq:Page", parent.getPrimaryNodeType().getName()); + assertEquals("cq:Page", parent.getParent().getPrimaryNodeType().getName()); + } + + @Test + public void testEnsureParent_Existing() throws IOException, RepositoryException, URISyntaxException { + String path = "/content/my-site/folder/page"; + context.build().withIntermediatePrimaryType("sling:Folder") + .resource(path); + + Node parent = contentSync.ensureParent(path); + verify(remoteInstance, never()).getPrimaryType(anyString()); // no need to call the remote instance + + assertEquals("/content/my-site/folder", parent.getPath()); + assertEquals("sling:Folder", parent.getPrimaryNodeType().getName()); + assertEquals("sling:Folder", parent.getParent().getPrimaryNodeType().getName()); + } + + @Test + public void testCopyBinaryData_ExistingProperty() throws Exception { + context.build() + .resource("/content/image") + .file("file", new ByteArrayInputStream(new byte[]{1, 2, 3}), "text/plain", 0L); + + // fetch binary data from remote + doReturn(new ByteArrayInputStream(new byte[]{2, 3})).when(remoteInstance).getStream(anyString()); + + String propertyPath = "/content/image/file/jcr:content/jcr:data"; + List paths = Arrays.asList(propertyPath); + contentSync.copyBinaries(paths); + + byte[] data = IOUtils.toByteArray( + context.resourceResolver().adaptTo(Session.class).getProperty(propertyPath).getBinary().getStream() + ); + assertArrayEquals(new byte[]{2, 3}, data); + + } + + @Test + public void testCopyBinaryData_NewProperty() throws Exception { + context.build() + .resource("/content/image/file", "jcr:primaryType", "nt:file") + .resource("/content/image/file/jcr:content", + "jcr:primaryType", "nt:resource", "jcr:data", ContentReader.BINARY_DATA_PLACEHOLDER) + .commit() + ; + + // fetch binary data from remote + doReturn(new ByteArrayInputStream(new byte[]{2, 3})).when(remoteInstance).getStream(anyString()); + + String propertyPath = "/content/image/file/jcr:content/jcr:data"; + List paths = Arrays.asList(propertyPath); + contentSync.copyBinaries(paths); + + byte[] data = IOUtils.toByteArray( + context.resourceResolver().adaptTo(Session.class).getProperty(propertyPath).getBinary().getStream() + ); + assertArrayEquals(new byte[]{2, 3}, data); + } + + + @Test + public void testImportNewAsset() throws Exception { + context.build().resource("/content/dam", "jcr:primaryType", "sling:OrderedFolder"); + JsonObject catalogItem = Json.createObjectBuilder() + .add("path", "/content/dam/asset") + .add("exportUri", "/content/dam/asset/jcr:content.infinity.json") + .add("jcr:primaryType", "dam:Asset") + .build(); + + JsonObject object = Json.createReader(getClass().getResourceAsStream("/contentsync/asset.json")).readObject(); + JsonObject sanitizedJson = reader.sanitize(object); + contentSync.importData(new CatalogItem(catalogItem), sanitizedJson); + + Asset asset = context.resourceResolver().getResource("/content/dam/asset").adaptTo(Asset.class); + + byte[] data = IOUtils.toByteArray( + asset.getOriginal().getStream() + ); + assertArrayEquals(ContentReader.BINARY_DATA_PLACEHOLDER.getBytes(), data); + assertEquals("image/jpeg", asset.getMimeType()); + + assertEquals("no", asset.getMetadata("dam:Progressive")); + assertEquals("Adobe PDF library 15.00", asset.getMetadata("pdf:Producer")); + assertEquals((long) 657, asset.getMetadata("tiff:ImageWidth")); + } + + @Test + public void testUpdateExistingAsset() throws Exception { + context.build().resource("/content/dam", "jcr:primaryType", "sling:OrderedFolder"); + String assetPath = "/content/dam/asset"; + context.assetManager().createAsset(assetPath, new ByteArrayInputStream(new byte[]{1, 2, 3}), "text/plain", false); + context.resourceResolver() + .getResource(assetPath + "/jcr:content/metadata") + .adaptTo(ModifiableValueMap.class) + .put("test", "remove me"); + + JsonObject catalogItem = Json.createObjectBuilder() + .add("path", assetPath) + .add("exportUri", assetPath + "/jcr:content.infinity.json") + .add("jcr:primaryType", "dam:Asset") + .build(); + + JsonObject object = Json.createReader(getClass().getResourceAsStream("/contentsync/asset.json")).readObject(); + JsonObject sanitizedJson = reader.sanitize(object); + contentSync.importData(new CatalogItem(catalogItem), sanitizedJson); + + Asset asset = context.resourceResolver().getResource(assetPath).adaptTo(Asset.class); + + byte[] data = IOUtils.toByteArray( + asset.getOriginal().getStream() + ); + assertArrayEquals(ContentReader.BINARY_DATA_PLACEHOLDER.getBytes(), data); + assertEquals("image/jpeg", asset.getMimeType()); + + assertEquals(null, asset.getMetadata("test")); // any properties from that existed before import are wiped off + assertEquals("no", asset.getMetadata("dam:Progressive")); + assertEquals("Adobe PDF library 15.00", asset.getMetadata("pdf:Producer")); + assertEquals((long) 657, asset.getMetadata("tiff:ImageWidth")); + } + + @Test + public void testUpdatePagePreserveVersionHistory() throws Exception { + context.build().resource("/content/wknd", "jcr:primaryType", "cq:Page"); + Page page = context.pageManager().create("/content/wknd", "test", "test", "Test"); + Node jcrContent = page.getContentResource().adaptTo(Node.class); + + Session session = context.resourceResolver().adaptTo(Session.class); + VersionManager versionManager = session.getWorkspace().getVersionManager(); + + createVersion(jcrContent, "Version 1"); + + String pagePath = "/content/wknd/test"; + assertNotNull(versionManager.getVersionHistory(pagePath + "/jcr:content").getVersionByLabel("Version 1")); + + JsonObject catalogItem = Json.createObjectBuilder() + .add("path", pagePath) + .add("exportUri", pagePath + "/jcr:content.infinity.json") + .add("jcr:primaryType", "cq:Page") + .build(); + + JsonObject object = Json.createReader(getClass().getResourceAsStream("/contentsync/wknd-faqs.json")).readObject(); + JsonObject sanitizedJson = reader.sanitize(object); + contentSync.importData(new CatalogItem(catalogItem), sanitizedJson); + + // assert the version is still there + assertNotNull(versionManager.getVersionHistory(pagePath + "/jcr:content").getVersionByLabel("Version 1")); + } + + /** + * Mimic PageManagerImpl#createVersion + */ + private void createVersion(Node node, String versionLabel) throws RepositoryException { + Session session = context.resourceResolver().adaptTo(Session.class); + VersionManager versionManager = session.getWorkspace().getVersionManager(); + node.addMixin("mix:versionable"); + session.save(); + + try { + Version v = versionManager.checkin(node.getPath()); + v.getContainingHistory().addVersionLabel(v.getName(), versionLabel, false); + + } finally { + versionManager.checkout(node.getPath()); + } + session.save(); + } + + @Test + public void testImportFolder() throws Exception { + context.build().resource("/content/wknd", "jcr:primaryType", "cq:Page"); + + JsonObject catalogItem = Json.createObjectBuilder() + .add("path", "/content/wknd/test") + .add("exportUri", "/content/wknd/test.json") + .add("jcr:primaryType", "sling:OrderedFolder") + .build(); + + JsonObject object = Json.createReader(getClass().getResourceAsStream("/contentsync/ordered-folder.json")).readObject(); + JsonObject sanitizedJson = reader.sanitize(object); + contentSync.importData(new CatalogItem(catalogItem), sanitizedJson); + + ValueMap vm = context.resourceResolver().getResource("/content/wknd/test").getValueMap(); + assertEquals("Wknd Fragments", vm.get("jcr:title")); + assertEquals("sling:OrderedFolder", vm.get("jcr:primaryType")); + assertEquals("html", vm.get("cq:adobeTargetExportFormat")); + } + + @Test + public void testAutoCheckout() throws Exception { + String path = "/content/wknd/page"; + Page pg = context.create().page(path); + Node jcrContent = pg.getContentResource().adaptTo(Node.class); + createVersion(jcrContent, "test 1"); + jcrContent.checkin(); + + JsonObject catalogItem = Json.createObjectBuilder() + .add("path", path) + .add("exportUri", path + "/jcr:content.infinity.json") + .add("jcr:primaryType", "cq:Page") + .build(); + + JsonObject object = Json.createReader(getClass().getResourceAsStream("/contentsync/wknd-faqs.json")).readObject(); + JsonObject sanitizedJson = reader.sanitize(object); + contentSync.importData(new CatalogItem(catalogItem), sanitizedJson); + + ValueMap vm = context.resourceResolver().getResource(path + "/jcr:content").getValueMap(); + assertEquals("FAQs", vm.get("jcr:title")); + assertEquals(true, vm.get("jcr:isCheckedOut")); + } +} \ No newline at end of file diff --git a/bundle/src/test/java/com/adobe/acs/commons/contentsync/impl/TestLastModifiedStrategy.java b/bundle/src/test/java/com/adobe/acs/commons/contentsync/impl/TestLastModifiedStrategy.java new file mode 100644 index 0000000000..25c1aafb34 --- /dev/null +++ b/bundle/src/test/java/com/adobe/acs/commons/contentsync/impl/TestLastModifiedStrategy.java @@ -0,0 +1,202 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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 com.adobe.acs.commons.contentsync.impl; + +import com.adobe.acs.commons.contentsync.CatalogItem; +import com.adobe.acs.commons.contentsync.UpdateStrategy; +import com.day.cq.wcm.api.Page; +import io.wcm.testing.mock.aem.junit.AemContext; +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.servlets.ServletResolver; +import org.apache.sling.testing.mock.sling.ResourceResolverType; +import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import javax.json.Json; +import javax.json.JsonObject; +import javax.servlet.GenericServlet; +import java.io.IOException; +import java.time.ZonedDateTime; +import java.util.Collections; +import java.util.GregorianCalendar; +import java.util.List; + +import static com.adobe.acs.commons.contentsync.impl.LastModifiedStrategy.DEFAULT_GET_SERVLET; +import static com.adobe.acs.commons.contentsync.impl.LastModifiedStrategy.REDIRECT_SERVLET; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +public class TestLastModifiedStrategy { + + @Rule + public AemContext context = new AemContext(ResourceResolverType.RESOURCEPROVIDER_MOCK); + + private UpdateStrategy updateStrategy; + + private ServletResolver servletResolver; + + @Before + public void setUp() { + servletResolver = mock(ServletResolver.class); + context.registerService(ServletResolver.class, servletResolver); + updateStrategy = context.registerInjectActivateService(new LastModifiedStrategy()); + } + + /** + * isModified() returns false if cq:lastModified/jcr:lastModified is not set + */ + @Test + public void testLastModifiedNA() { + String pagePath = "/content/wknd/page"; + JsonObject catalogItem = Json.createObjectBuilder() + .add("path", pagePath) + .add("jcr:primaryType", "cq:Page") + .build(); + + Page page = context.create().page(pagePath); + Resource pageResource = page.adaptTo(Resource.class); + assertFalse(updateStrategy.isModified(new CatalogItem(catalogItem), pageResource)); + } + + @Test + public void testPageModified() { + String pagePath = "/content/wknd/page"; + ZonedDateTime remoteTimestamp = ZonedDateTime.now().minusDays(1); + ZonedDateTime localTimestamp = ZonedDateTime.now().minusDays(2); + + JsonObject catalogItem = Json.createObjectBuilder() + .add("path", pagePath) + .add("jcr:primaryType", "cq:Page") + .add("lastModified", remoteTimestamp.toInstant().toEpochMilli()) + .build(); + + Page page = context.create().page(pagePath, null, Collections.singletonMap("cq:lastModified", GregorianCalendar.from(localTimestamp))); + Resource pageResource = page.adaptTo(Resource.class); + + assertTrue(updateStrategy.isModified(new CatalogItem(catalogItem), pageResource)); + } + + @Test + public void testPageNotModified() { + String pagePath = "/content/wknd/page"; + ZonedDateTime localTimestamp = ZonedDateTime.now().minusDays(1); + ZonedDateTime remoteTimestamp = ZonedDateTime.now().minusDays(2); + + JsonObject catalogItem = Json.createObjectBuilder() + .add("path", pagePath) + .add("jcr:primaryType", "cq:Page") + .add("lastModified", remoteTimestamp.toInstant().toEpochMilli()) + .build(); + + Page page = context.create().page(pagePath, null, Collections.singletonMap("cq:lastModified", GregorianCalendar.from(localTimestamp))); + Resource pageResource = page.adaptTo(Resource.class); + + assertFalse(updateStrategy.isModified(new CatalogItem(catalogItem), pageResource)); + } + + + @Test + public void testForwardRedirectServletToDefaultGetServlet() { + doAnswer(invocation -> { + GenericServlet servlet = mock(GenericServlet.class); + doReturn(REDIRECT_SERVLET).when(servlet).getServletName(); + return servlet; + }).when(servletResolver).resolveServlet(any(SlingHttpServletRequest.class)); + + String path = "/content/cq:tags"; + context.create().resource(path, "jcr:primaryType", "cq:Tag"); + + MockSlingHttpServletRequest request = context.request(); + request.addRequestParameter("root", path); + + List items = updateStrategy.getItems(request); + assertEquals(1, items.size()); + CatalogItem item = items.iterator().next(); + assertEquals("/content/cq:tags.json", item.getContentUri()); + assertNull(item.getCustomExporter()); + } + + /** + * /conf/wknd/settings/wcm/templates/article-page-template/policies (cq:Page) export json rendered by DefaultGetServlet + * + jcr:content - export json rendered by ContentPolicyMappingServlet + */ + @Test + public void testCustomRendererUseParent() throws IOException { + String path = "/conf/wknd/settings/wcm/templates/article-page-template/policies"; + doAnswer(invocation -> { + SlingHttpServletRequest request = invocation.getArgument(0, SlingHttpServletRequest.class); + String resourcePath = request.getResource().getPath(); + GenericServlet servlet = mock(GenericServlet.class); + if (resourcePath.equals(path + "/jcr:content")) { + doReturn("com.day.cq.wcm.core.impl.policies.ContentPolicyMappingServlet").when(servlet).getServletName(); + } else if (resourcePath.equals(path)) { + doReturn(DEFAULT_GET_SERVLET).when(servlet).getServletName(); + } + return servlet; + }).when(servletResolver).resolveServlet(any(SlingHttpServletRequest.class)); + + context.create().page(path); + + MockSlingHttpServletRequest request = context.request(); + request.addRequestParameter("root", path); + request.addRequestParameter("strategy", updateStrategy.getClass().getName()); + + List items = updateStrategy.getItems(request); + assertEquals(1, items.size()); + CatalogItem item = items.iterator().next(); + assertEquals("cq:Page", item.getPrimaryType()); + assertEquals(path + ".infinity.json", item.getContentUri()); + assertEquals(null, item.getCustomExporter()); + + } + + @Test + public void testCustomExporter() { + String path = "/content/wknd/page"; + String customExporter = "com.adobe.CustomJsonExporter"; + doAnswer(invocation -> { + GenericServlet servlet = mock(GenericServlet.class); + doReturn(customExporter).when(servlet).getServletName(); + return servlet; + }).when(servletResolver).resolveServlet(any(SlingHttpServletRequest.class)); + + context.create().page(path); + + MockSlingHttpServletRequest request = context.request(); + request.addRequestParameter("root", path); + request.addRequestParameter("strategy", updateStrategy.getClass().getName()); + + List items = updateStrategy.getItems(request); + assertEquals(1, items.size()); + CatalogItem item = items.iterator().next(); + assertEquals("cq:Page", item.getPrimaryType()); + assertEquals(path + ".infinity.json", item.getContentUri()); + assertEquals(customExporter, item.getCustomExporter()); + } +} diff --git a/bundle/src/test/java/com/adobe/acs/commons/contentsync/servlet/TestContentCatalogServlet.java b/bundle/src/test/java/com/adobe/acs/commons/contentsync/servlet/TestContentCatalogServlet.java new file mode 100644 index 0000000000..f802f77ae0 --- /dev/null +++ b/bundle/src/test/java/com/adobe/acs/commons/contentsync/servlet/TestContentCatalogServlet.java @@ -0,0 +1,151 @@ +/*- + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2013 - 2022 Adobe + * %% + * 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 com.adobe.acs.commons.contentsync.servlet; + +import com.adobe.acs.commons.contentsync.CatalogItem; +import com.adobe.acs.commons.contentsync.UpdateStrategy; +import io.wcm.testing.mock.aem.junit.AemContext; +import org.apache.sling.testing.mock.sling.ResourceResolverType; +import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest; +import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletResponse; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonObject; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; + +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; +import static javax.servlet.http.HttpServletResponse.SC_OK; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; + +public class TestContentCatalogServlet { + @Rule + public AemContext context = new AemContext(ResourceResolverType.RESOURCEPROVIDER_MOCK); + + private ContentCatalogServlet servlet; + private UpdateStrategy updateStrategy; + + @Before + public void setUp() { + updateStrategy = mock(UpdateStrategy.class); + context.registerService(UpdateStrategy.class, updateStrategy); + servlet = context.registerInjectActivateService(new ContentCatalogServlet()); + } + + @Test + public void testMissingRequiredParameters() throws IOException { + doAnswer(invocation -> { + throw new IllegalArgumentException("root request parameter is required"); + }).when(updateStrategy).getItems(eq(context.request())); + + MockSlingHttpServletRequest request = context.request(); + MockSlingHttpServletResponse response = context.response(); + servlet.doGet(request, response); + + assertEquals(SC_INTERNAL_SERVER_ERROR, response.getStatus()); + JsonObject jsonResponse = Json.createReader(new StringReader(response.getOutputAsString())).readObject(); + assertEquals("root request parameter is required", jsonResponse.getString("error")); + } + + /** + * return an empty array if the requested path does not exist + */ + @Test + public void testContentTreeDoesNotExist() throws IOException { + MockSlingHttpServletRequest request = context.request(); + request.addRequestParameter("root", "/content/wknd"); + MockSlingHttpServletResponse response = context.response(); + servlet.doGet(request, response); + + assertEquals("application/json", response.getContentType()); + assertEquals(SC_OK, response.getStatus()); + JsonObject jsonResponse = Json.createReader(new StringReader(response.getOutputAsString())).readObject(); + assertTrue("resources[] is missing in the response json", jsonResponse.containsKey("resources")); + + JsonArray resources = jsonResponse.getJsonArray("resources"); + + assertEquals(0, resources.size()); + } + + @Test + public void testPageTree() throws IOException { + doAnswer(invocation -> { + List items = new ArrayList<>(); + JsonObject o1 = Json.createObjectBuilder() + .add("path", "/content/wknd") + .add("jcr:primaryType", "cq:Page") + .build(); + JsonObject o2 = Json.createObjectBuilder() + .add("path", "/content/wknd/page1") + .add("jcr:primaryType", "cq:Page") + .build(); + items.add(new CatalogItem(o1)); + items.add(new CatalogItem(o2)); + return items; + }).when(updateStrategy).getItems(eq(context.request())); + + MockSlingHttpServletRequest request = context.request(); + request.addRequestParameter("root", "/content/wknd"); + request.addRequestParameter("strategy", updateStrategy.getClass().getName()); + MockSlingHttpServletResponse response = context.response(); + servlet.doGet(request, response); + + assertEquals("application/json", response.getContentType()); + assertEquals(SC_OK, response.getStatus()); + JsonObject jsonResponse = Json.createReader(new StringReader(response.getOutputAsString())).readObject(); + assertTrue("resources[] is missing in the response json", jsonResponse.containsKey("resources")); + + JsonArray resources = jsonResponse.getJsonArray("resources"); + assertEquals(2, resources.size()); + + // first item is the root + JsonObject item1 = resources.getJsonObject(0); + assertEquals("/content/wknd", item1.getString("path")); + assertEquals("cq:Page", item1.getString("jcr:primaryType")); + + JsonObject item2 = resources.getJsonObject(1); + assertEquals("/content/wknd/page1", item2.getString("path")); + assertEquals("cq:Page", item2.getString("jcr:primaryType")); + } + + @Test + public void testInvalidStrategyPid() throws IOException { + MockSlingHttpServletRequest request = context.request(); + request.addRequestParameter("root", "/content/wknd"); + request.addRequestParameter("strategy", "invalid"); + MockSlingHttpServletResponse response = context.response(); + servlet.doGet(request, response); + + assertEquals("application/json", response.getContentType()); + assertEquals(SC_INTERNAL_SERVER_ERROR, response.getStatus()); + JsonObject jsonResponse = Json.createReader(new StringReader(response.getOutputAsString())).readObject(); + assertTrue(jsonResponse.getString("error").startsWith("Cannot find UpdateStrategy for pid")); + } +} diff --git a/bundle/src/test/resources/contentsync/asset.json b/bundle/src/test/resources/contentsync/asset.json new file mode 100644 index 0000000000..ea0875de70 --- /dev/null +++ b/bundle/src/test/resources/contentsync/asset.json @@ -0,0 +1,1158 @@ +{ + "jcr:primaryType": "dam:AssetContent", + "jcr:mixinTypes": ["cq:ReplicationStatus"], + "cq:lastReplicationAction": "Activate", + "cq:lastReplicatedBy": "egor.kozlov", + "jcr:lastModifiedBy": "john.mcfarland@fleetcor.com", + "cq:lastReplicated": "2023-02-19T12:28:37.379Z", + "dam:assetState": "processed", + "jcr:lastModified": "2022-04-26T16:39:56.300Z", + "usages": { + "jcr:primaryType": "nt:unstructured", + "usedBy": ["asset"], + "dam:score": 1, + "asset": { + "jcr:primaryType": "nt:unstructured", + "lastUsed": "2022-05-16T03:49:08.558Z", + "count": 1 + } + }, + "renditions": { + "jcr:primaryType": "nt:folder", + "jcr:createdBy": "john.mcfarland@fleetcor.com", + "jcr:created": "2022-04-26T16:38:58.061Z", + "cq5dam.thumbnail.48.48.png": { + "jcr:primaryType": "nt:file", + "jcr:createdBy": "workflow-process-service", + "jcr:created": "2022-04-26T16:39:56.246Z", + "jcr:content": { + "jcr:primaryType": "oak:Resource", + "jcr:lastModifiedBy": "workflow-process-service", + "jcr:mimeType": "image/png", + "jcr:lastModified": "2022-04-26T16:39:56.247Z", + ":jcr:data": 536 + } + }, + "cq5dam.thumbnail.140.100.png": { + "jcr:primaryType": "nt:file", + "jcr:createdBy": "workflow-process-service", + "jcr:created": "2022-04-26T16:39:56.242Z", + "jcr:content": { + "jcr:primaryType": "oak:Resource", + "jcr:lastModifiedBy": "workflow-process-service", + "jcr:mimeType": "image/png", + "jcr:lastModified": "2022-04-26T16:39:56.243Z", + ":jcr:data": 1684 + } + }, + "cq5dam.web.1280.1280.jpeg": { + "jcr:primaryType": "nt:file", + "jcr:createdBy": "workflow-process-service", + "jcr:created": "2022-04-26T16:39:56.284Z", + "jcr:content": { + "jcr:primaryType": "oak:Resource", + "jcr:lastModifiedBy": "workflow-process-service", + "jcr:mimeType": "image/jpeg", + "jcr:lastModified": "2022-04-26T16:39:56.296Z", + ":jcr:data": 12627 + } + }, + "original": { + "jcr:primaryType": "nt:file", + "jcr:createdBy": "john.mcfarland@fleetcor.com", + "jcr:created": "2022-04-26T16:38:58.062Z", + "jcr:content": { + "jcr:primaryType": "oak:Resource", + "jcr:lastModifiedBy": "john.mcfarland@fleetcor.com", + "jcr:mimeType": "image/jpeg", + "jcr:lastModified": "2022-04-26T16:38:58.062Z", + ":jcr:data": 632024 + } + }, + "cq5dam.thumbnail.319.319.png": { + "jcr:primaryType": "nt:file", + "jcr:createdBy": "workflow-process-service", + "jcr:created": "2022-04-26T16:39:56.256Z", + "jcr:content": { + "jcr:primaryType": "oak:Resource", + "jcr:lastModifiedBy": "workflow-process-service", + "jcr:mimeType": "image/png", + "jcr:lastModified": "2022-04-26T16:39:56.257Z", + ":jcr:data": 4082 + } + } + }, + "related": {"jcr:primaryType": "nt:unstructured"}, + "metadata": { + "jcr:primaryType": "nt:unstructured", + "jcr:mixinTypes": ["cq:Taggable"], + "dam:Physicalheightininches": "-1.0", + "dam:Physicalwidthininches": "-1.0", + "xmpTPg:HasVisibleTransparency": "False", + "dam:Fileformat": "JPEG", + "dam:Progressive": "no", + "tiff:ImageLength": 249, + "xmp:CreatorTool": "Adobe Illustrator 26.0 (Windows)", + "dam:extracted": "2022-04-26T16:39:56.083Z", + "dc:format": "image/jpeg", + "dam:Bitsperpixel": 32, + "xmpMM:DocumentID": "xmp.did:526773d2-a98f-654f-bf01-cbbe9b3a6f9d", + "dam:MIMEtype": "image/jpeg", + "dam:Physicalwidthindpi": -1, + "dam:Physicalheightindpi": -1, + "xmpMM:OriginalDocumentID": "uuid:5D20892493BFDB11914A8590D31508C8", + "ns1_1_:StartupProfile": "Print", + "xmpTPg:NPages": 1, + "dam:Numberofimages": 1, + "xmp:MetadataDate": "2020-05-26T16:53:03.000+05:30", + "xmpMM:RenditionClass": "proof:pdf", + "pdf:Producer": "Adobe PDF library 15.00", + "xmpTPg:HasVisibleOverprint": "False", + "ns1_1_:CreatorSubTool": "Adobe Illustrator", + "dam:Numberoftextualcomments": 0, + "xmpMM:InstanceID": "uuid:c9914b06-205d-4060-b620-fae57f2f5102", + "xmp:ModifyDate": "2021-12-10T14:22:37.000Z", + "xmp:CreateDate": "2021-12-10T08:22:37.000-06:00", + "tiff:ImageWidth": 657, + "dam:sha1": "08b44275cc46301a535ee1c7095c0daa3f3dffe8", + "dam:size": 632024, + "ns1_1_:Type": "Document", + "dc:title": "Print", + "xmp:Thumbnails": { + "jcr:primaryType": "nt:unstructured", + "xmpNodeType": "xmpArray", + "xmpArraySize": 1, + "xmpArrayType": "rdf:Alt", + "1": { + "jcr:primaryType": "nt:unstructured", + "xmpNodeType": "xmpStruct", + "xmpGImg:image": "/9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX//2Q==", + "xmpGImg:width": 256, + "xmpGImg:format": "JPEG", + "xmpGImg:height": 256 + } + }, + "xmpMM:DerivedFrom": { + "jcr:primaryType": "nt:unstructured", + "stRef:instanceID": "uuid:f346e470-2f42-4b41-ab9e-90c244b06151", + "stRef:originalDocumentID": "uuid:5D20892493BFDB11914A8590D31508C8", + "xmpNodeType": "xmpStruct", + "stRef:renditionClass": "proof:pdf", + "stRef:documentID": "xmp.did:68f615b9-b15d-024c-915a-8c6e37af3c70" + }, + "xmpMM:History": { + "jcr:primaryType": "nt:unstructured", + "xmpNodeType": "xmpArray", + "xmpArraySize": 37, + "xmpArrayType": "rdf:Seq", + "1": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "converted", + "stEvt:params": "from application/pdf to ", + "xmpNodeType": "xmpStruct" + }, + "2": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-04-17T14:19:15.000+05:30", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:D27F11740720681191099C3B601C4548" + }, + "3": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "converted", + "stEvt:params": "from application/pdf to ", + "xmpNodeType": "xmpStruct" + }, + "4": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "converted", + "stEvt:params": "from application/pdf to ", + "xmpNodeType": "xmpStruct" + }, + "5": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-15T16:23:06.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:F97F1174072068118D4ED246B3ADB1C6" + }, + "6": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-15T17:10:45.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:FA7F1174072068118D4ED246B3ADB1C6" + }, + "7": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-15T22:53:33.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:EF7F117407206811A46CA4519D24356B" + }, + "8": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-15T23:07:07.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:F07F117407206811A46CA4519D24356B" + }, + "9": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-16T10:35:43.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:F77F117407206811BDDDFD38D0CF24DD" + }, + "10": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "converted", + "stEvt:params": "from application/pdf to ", + "xmpNodeType": "xmpStruct" + }, + "11": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-16T10:40:59.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:F97F117407206811BDDDFD38D0CF24DD" + }, + "12": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "converted", + "stEvt:params": "from application/vnd.adobe.illustrator to ", + "xmpNodeType": "xmpStruct" + }, + "13": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-16T11:26:55.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:FA7F117407206811BDDDFD38D0CF24DD" + }, + "14": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-16T11:29:01.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:FB7F117407206811BDDDFD38D0CF24DD" + }, + "15": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-16T11:29:20.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:FC7F117407206811BDDDFD38D0CF24DD" + }, + "16": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-16T11:30:54.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:FD7F117407206811BDDDFD38D0CF24DD" + }, + "17": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-16T11:31:22.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:FE7F117407206811BDDDFD38D0CF24DD" + }, + "18": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-16T12:23:46.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:B233668C16206811BDDDFD38D0CF24DD" + }, + "19": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-16T13:27:54.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:B333668C16206811BDDDFD38D0CF24DD" + }, + "20": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-16T13:46:13.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:B433668C16206811BDDDFD38D0CF24DD" + }, + "21": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-16T15:47:57.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:F77F11740720681197C1BF14D1759E83" + }, + "22": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-16T15:51:06.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:F87F11740720681197C1BF14D1759E83" + }, + "23": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-16T15:52:22.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:F97F11740720681197C1BF14D1759E83" + }, + "24": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "converted", + "stEvt:params": "from application/vnd.adobe.illustrator to application/vnd.adobe.illustrator", + "xmpNodeType": "xmpStruct" + }, + "25": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-22T13:28:01.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:FA7F117407206811B628E3BF27C8C41B" + }, + "26": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "converted", + "stEvt:params": "from application/vnd.adobe.illustrator to application/vnd.adobe.illustrator", + "xmpNodeType": "xmpStruct" + }, + "27": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-22T16:23:53.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:FF7F117407206811B628E3BF27C8C41B" + }, + "28": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "converted", + "stEvt:params": "from application/vnd.adobe.illustrator to application/vnd.adobe.illustrator", + "xmpNodeType": "xmpStruct" + }, + "29": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-05-28T16:45:26.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:07C3BD25102DDD1181B594070CEB88D9" + }, + "30": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "converted", + "stEvt:params": "from application/vnd.adobe.illustrator to application/vnd.adobe.illustrator", + "xmpNodeType": "xmpStruct" + }, + "31": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-06-02T13:25:25.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:F87F1174072068119098B097FDA39BEF" + }, + "32": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-06-09T14:58:36.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:F77F117407206811BB1DBF8F242B6F84" + }, + "33": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-06-11T14:31:27.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:F97F117407206811ACAFB8DA80854E76" + }, + "34": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-06-11T22:37:35.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:0180117407206811834383CD3A8D2303" + }, + "35": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "stEvt:changed_xmpArrayType": "rdf:Bag", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS4", + "stEvt:when": "2008-06-27T14:40:42.000-07:00", + "stEvt:changed": ["/"], + "stEvt:instanceID": "xmp.iid:F77F117407206811818C85DF6A1A75C3" + }, + "36": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator CS5", + "stEvt:when": "2009-11-26T17:25:36.000+05:30", + "stEvt:changed": "/", + "stEvt:instanceID": "xmp.iid:0580117407206811910989C2A6325BB5" + }, + "37": { + "jcr:primaryType": "nt:unstructured", + "stEvt:action": "saved", + "xmpNodeType": "xmpStruct", + "stEvt:softwareAgent": "Adobe Illustrator 24.2 (Windows)", + "stEvt:when": "2020-05-26T16:53:03.000+05:30", + "stEvt:changed": "/", + "stEvt:instanceID": "xmp.iid:526773d2-a98f-654f-bf01-cbbe9b3a6f9d" + } + }, + "xmpTPg:MaxPageSize": { + "jcr:primaryType": "nt:unstructured", + "stDim:h": "792.000000", + "stDim:w": "612.000000", + "xmpNodeType": "xmpStruct", + "stDim:unit": "Points" + }, + "xmpTPg:SwatchGroups": { + "jcr:primaryType": "nt:unstructured", + "xmpNodeType": "xmpArray", + "xmpArraySize": 3, + "xmpArrayType": "rdf:Seq", + "1": { + "jcr:primaryType": "nt:unstructured", + "xmpNodeType": "xmpStruct", + "xmpG:groupName": "Default Swatch Group", + "xmpG:groupType": 0, + "xmpG:Colorants": { + "jcr:primaryType": "nt:unstructured", + "xmpNodeType": "xmpArray", + "xmpArraySize": 40, + "xmpArrayType": "rdf:Seq", + "1": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "White", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "2": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "100.000000", + "xmpG:swatchName": "Black", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "3": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "100.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "CMYK Red", + "xmpG:yellow": "100.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "4": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "CMYK Yellow", + "xmpG:yellow": "100.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "5": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "CMYK Green", + "xmpG:yellow": "100.000000", + "xmpG:cyan": "100.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "6": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "CMYK Cyan", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "100.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "7": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "100.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "CMYK Blue", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "100.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "8": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "100.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "CMYK Magenta", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "9": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "100.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "10.000000", + "xmpG:swatchName": "C=15 M=100 Y=90 K=10", + "xmpG:yellow": "90.000000", + "xmpG:cyan": "15.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "10": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "90.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=0 M=90 Y=85 K=0", + "xmpG:yellow": "85.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "11": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "80.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=0 M=80 Y=95 K=0", + "xmpG:yellow": "95.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "12": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "50.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=0 M=50 Y=100 K=0", + "xmpG:yellow": "100.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "13": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "35.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=0 M=35 Y=85 K=0", + "xmpG:yellow": "85.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "14": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=5 M=0 Y=90 K=0", + "xmpG:yellow": "90.000000", + "xmpG:cyan": "5.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "15": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=20 M=0 Y=100 K=0", + "xmpG:yellow": "100.000000", + "xmpG:cyan": "20.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "16": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=50 M=0 Y=100 K=0", + "xmpG:yellow": "100.000000", + "xmpG:cyan": "50.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "17": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=75 M=0 Y=100 K=0", + "xmpG:yellow": "100.000000", + "xmpG:cyan": "75.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "18": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "10.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "10.000000", + "xmpG:swatchName": "C=85 M=10 Y=100 K=10", + "xmpG:yellow": "100.000000", + "xmpG:cyan": "85.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "19": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "30.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "30.000000", + "xmpG:swatchName": "C=90 M=30 Y=95 K=30", + "xmpG:yellow": "95.000000", + "xmpG:cyan": "90.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "20": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=75 M=0 Y=75 K=0", + "xmpG:yellow": "75.000000", + "xmpG:cyan": "75.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "21": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "10.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=80 M=10 Y=45 K=0", + "xmpG:yellow": "45.000000", + "xmpG:cyan": "80.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "22": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "15.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=70 M=15 Y=0 K=0", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "70.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "23": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "50.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=85 M=50 Y=0 K=0", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "85.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "24": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "95.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=100 M=95 Y=5 K=0", + "xmpG:yellow": "5.000000", + "xmpG:cyan": "100.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "25": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "100.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "25.000000", + "xmpG:swatchName": "C=100 M=100 Y=25 K=25", + "xmpG:yellow": "25.000000", + "xmpG:cyan": "100.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "26": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "100.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=75 M=100 Y=0 K=0", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "75.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "27": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "100.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=50 M=100 Y=0 K=0", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "50.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "28": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "100.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "10.000000", + "xmpG:swatchName": "C=35 M=100 Y=35 K=10", + "xmpG:yellow": "35.000000", + "xmpG:cyan": "35.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "29": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "100.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=10 M=100 Y=50 K=0", + "xmpG:yellow": "50.000000", + "xmpG:cyan": "10.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "30": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "95.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=0 M=95 Y=20 K=0", + "xmpG:yellow": "20.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "31": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "25.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=25 M=25 Y=40 K=0", + "xmpG:yellow": "40.000000", + "xmpG:cyan": "25.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "32": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "45.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "5.000000", + "xmpG:swatchName": "C=40 M=45 Y=50 K=5", + "xmpG:yellow": "50.000000", + "xmpG:cyan": "40.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "33": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "50.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "25.000000", + "xmpG:swatchName": "C=50 M=50 Y=60 K=25", + "xmpG:yellow": "60.000000", + "xmpG:cyan": "50.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "34": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "60.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "40.000000", + "xmpG:swatchName": "C=55 M=60 Y=65 K=40", + "xmpG:yellow": "65.000000", + "xmpG:cyan": "55.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "35": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "40.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=25 M=40 Y=65 K=0", + "xmpG:yellow": "65.000000", + "xmpG:cyan": "25.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "36": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "50.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "10.000000", + "xmpG:swatchName": "C=30 M=50 Y=75 K=10", + "xmpG:yellow": "75.000000", + "xmpG:cyan": "30.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "37": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "60.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "25.000000", + "xmpG:swatchName": "C=35 M=60 Y=80 K=25", + "xmpG:yellow": "80.000000", + "xmpG:cyan": "35.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "38": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "65.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "35.000000", + "xmpG:swatchName": "C=40 M=65 Y=90 K=35", + "xmpG:yellow": "90.000000", + "xmpG:cyan": "40.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "39": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "70.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "50.000000", + "xmpG:swatchName": "C=40 M=70 Y=100 K=50", + "xmpG:yellow": "100.000000", + "xmpG:cyan": "40.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "40": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "70.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "70.000000", + "xmpG:swatchName": "C=50 M=70 Y=80 K=70", + "xmpG:yellow": "80.000000", + "xmpG:cyan": "50.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + } + } + }, + "2": { + "jcr:primaryType": "nt:unstructured", + "xmpNodeType": "xmpStruct", + "xmpG:groupName": "Grays", + "xmpG:groupType": 1, + "xmpG:Colorants": { + "jcr:primaryType": "nt:unstructured", + "xmpNodeType": "xmpArray", + "xmpArraySize": 11, + "xmpArrayType": "rdf:Seq", + "1": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "100.000000", + "xmpG:swatchName": "C=0 M=0 Y=0 K=100", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "2": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "89.999400", + "xmpG:swatchName": "C=0 M=0 Y=0 K=90", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "3": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "79.998800", + "xmpG:swatchName": "C=0 M=0 Y=0 K=80", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "4": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "69.999700", + "xmpG:swatchName": "C=0 M=0 Y=0 K=70", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "5": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "59.999100", + "xmpG:swatchName": "C=0 M=0 Y=0 K=60", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "6": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "50.000000", + "xmpG:swatchName": "C=0 M=0 Y=0 K=50", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "7": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "39.999400", + "xmpG:swatchName": "C=0 M=0 Y=0 K=40", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "8": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "29.998800", + "xmpG:swatchName": "C=0 M=0 Y=0 K=30", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "9": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "19.999700", + "xmpG:swatchName": "C=0 M=0 Y=0 K=20", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "10": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "9.999100", + "xmpG:swatchName": "C=0 M=0 Y=0 K=10", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "11": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "0.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "4.998800", + "xmpG:swatchName": "C=0 M=0 Y=0 K=5", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + } + } + }, + "3": { + "jcr:primaryType": "nt:unstructured", + "xmpNodeType": "xmpStruct", + "xmpG:groupName": "Brights", + "xmpG:groupType": 1, + "xmpG:Colorants": { + "jcr:primaryType": "nt:unstructured", + "xmpNodeType": "xmpArray", + "xmpArraySize": 6, + "xmpArrayType": "rdf:Seq", + "1": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "100.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=0 M=100 Y=100 K=0", + "xmpG:yellow": "100.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "2": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "75.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=0 M=75 Y=100 K=0", + "xmpG:yellow": "100.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "3": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "10.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=0 M=10 Y=95 K=0", + "xmpG:yellow": "95.000000", + "xmpG:cyan": "0.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "4": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "10.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=85 M=10 Y=100 K=0", + "xmpG:yellow": "100.000000", + "xmpG:cyan": "85.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "5": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "90.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.000000", + "xmpG:swatchName": "C=100 M=90 Y=0 K=0", + "xmpG:yellow": "0.000000", + "xmpG:cyan": "100.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + }, + "6": { + "jcr:primaryType": "nt:unstructured", + "xmpG:magenta": "90.000000", + "xmpNodeType": "xmpStruct", + "xmpG:black": "0.003100", + "xmpG:swatchName": "C=60 M=90 Y=0 K=0", + "xmpG:yellow": "0.003100", + "xmpG:cyan": "60.000000", + "xmpG:mode": "CMYK", + "xmpG:type": "PROCESS" + } + } + } + } + } + } \ No newline at end of file diff --git a/bundle/src/test/resources/contentsync/jcr_content.json b/bundle/src/test/resources/contentsync/jcr_content.json new file mode 100644 index 0000000000..a25daa4f59 --- /dev/null +++ b/bundle/src/test/resources/contentsync/jcr_content.json @@ -0,0 +1,107 @@ +{ + "jcr:primaryType": "cq:PageContent", + "jcr:mixinTypes": [ + "mix:versionable" + ], + "jcr:createdBy": "admin", + "jcr:title": "Test", + "cq:lastReplicationAction": "Activate", + "jcr:versionHistory": "cf2b4a68-8420-4c76-9e1a-c233e77de565", + "cq:lastReplicatedBy": "admin", + "jcr:predecessors": [ + "621ac7f5-949e-4feb-9bdc-7cb5b1914dce" + ], + "jcr:created": "2023-06-29T19:12:49.925+03:00", + "cq:lastReplicated": "2023-07-03T13:16:01.601+03:00", + "cq:lastModified": "2022-12-19T12:54:37.988Z", + "sling:redirectStatus": 302, + "jcr:baseVersion": "621ac7f5-949e-4feb-9bdc-7cb5b1914dce", + "jcr:isCheckedOut": true, + "cq:conf": "/conf/test", + "acs:unknown": "unknown namespace. to be sanitized", + ":customBinaryProperty": 100, + "jcr:uuid": "5ec431ff-d63c-4459-a072-ddaeb7519d4f", + "sling:resourceType": "foundation/components/redirect", + "cq:allowedTemplates": [ + "/apps/test/templates/.*", + "/conf/test/settings/wcm/templates/.*" + ], + "cq:designPath": "/etc/designs/test", + "cq:lastModifiedBy": "vadim.filatov", + "image": { + "jcr:primaryType": "nt:unstructured", + "file": { + "jcr:primaryType": "nt:file", + "jcr:createdBy": "admin", + "jcr:created": "2023-07-03T12:24:06.732+03:00", + "jcr:content": { + "jcr:primaryType": "nt:resource", + "jcr:mixinTypes": [ + "dam:Thumbnails" + ], + "jcr:lastModifiedBy": "vadim.filatov", + "jcr:mimeType": "image/png", + "jcr:lastModified": "2022-12-19T12:54:32.332Z", + ":jcr:data": 1518, + "jcr:uuid": "6866ac8a-3d69-4be7-a5ea-1be1d26bc170", + "dam:thumbnails": { + "jcr:primaryType": "nt:folder", + "jcr:createdBy": "admin", + "jcr:created": "2023-07-03T12:24:06.733+03:00", + "dam:thumbnail_480.png": { + "jcr:primaryType": "nt:file", + "jcr:createdBy": "admin", + "jcr:created": "2023-07-03T12:24:06.733+03:00", + "jcr:content": { + "jcr:primaryType": "nt:resource", + "jcr:lastModifiedBy": "egor.kozlov", + "jcr:mimeType": "image/png", + "jcr:lastModified": "2023-05-24T10:36:11.395Z", + ":jcr:data": 1162, + "jcr:uuid": "ad554ca4-3e9b-4742-80c3-3cb0d6b465e6" + } + }, + "dam:thumbnail_60.png": { + "jcr:primaryType": "nt:file", + "jcr:createdBy": "admin", + "jcr:created": "2023-07-03T12:24:06.733+03:00", + "jcr:content": { + "jcr:primaryType": "nt:resource", + "jcr:lastModifiedBy": "vadim.filatov", + "jcr:mimeType": "image/png", + "jcr:lastModified": "2023-05-26T12:32:39.157Z", + ":jcr:data": 1556, + "jcr:uuid": "4f06e735-8761-48d6-9d62-1a695d61fe68" + } + }, + "dam:thumbnail_300.png": { + "jcr:primaryType": "nt:file", + "jcr:createdBy": "admin", + "jcr:created": "2023-07-03T12:24:06.734+03:00", + "jcr:content": { + "jcr:primaryType": "nt:resource", + "jcr:lastModifiedBy": "egor.kozlov", + "jcr:mimeType": "image/png", + "jcr:lastModified": "2023-05-24T10:36:11.395Z", + ":jcr:data": 1162, + "jcr:uuid": "a342e1bf-2f36-459e-be09-0c1a732f848b" + } + }, + "dam:thumbnail_48.png": { + "jcr:primaryType": "nt:file", + "jcr:createdBy": "admin", + "jcr:created": "2023-07-03T12:24:06.734+03:00", + "jcr:content": { + "jcr:primaryType": "nt:resource", + "jcr:lastModifiedBy": "egor.kozlov", + "jcr:mimeType": "image/png", + "jcr:lastModified": "2023-05-24T10:36:11.396Z", + ":jcr:data": 1230, + "jcr:uuid": "880abe33-61ce-4176-83b0-56267fa1f44f" + } + } + } + } + } + } +} diff --git a/bundle/src/test/resources/contentsync/ordered-folder.json b/bundle/src/test/resources/contentsync/ordered-folder.json new file mode 100644 index 0000000000..207e9aff06 --- /dev/null +++ b/bundle/src/test/resources/contentsync/ordered-folder.json @@ -0,0 +1,8 @@ +{ + "jcr:primaryType": "sling:OrderedFolder", + "jcr:createdBy": "john.doe", + "jcr:title": "Wknd Fragments", + "cq:adobeTargetExportFormat": "html", + "jcr:created": "2021-09-02T15:06:55.647Z", + "cq:allowedTemplates": ["/conf/wknd/settings/wcm/templates/(?!page-).*"] + } \ No newline at end of file diff --git a/bundle/src/test/resources/contentsync/riverside-camping-australia.1.json b/bundle/src/test/resources/contentsync/riverside-camping-australia.1.json new file mode 100644 index 0000000000..ad79931a24 --- /dev/null +++ b/bundle/src/test/resources/contentsync/riverside-camping-australia.1.json @@ -0,0 +1,115 @@ +{ + "jcr:primaryType": "sling:OrderedFolder", + "jcr:createdBy": "admin", + "jcr:created": "2023-06-07T16:09:43.164+03:00", + "adobestock-216674449.jpeg": { + "jcr:primaryType": "dam:Asset", + "jcr:mixinTypes": [ + "mix:versionable" + ], + "jcr:createdBy": "admin", + "jcr:versionHistory": "e07e4951-b3a1-495d-886c-97a1648e0596", + "jcr:predecessors": [ + "72a074a5-3eab-47a1-acff-153e35251e76" + ], + "jcr:created": "2023-06-07T16:10:27.576+03:00", + "jcr:baseVersion": "72a074a5-3eab-47a1-acff-153e35251e76", + "jcr:isCheckedOut": true, + "jcr:uuid": "26227675-38fa-447a-9bf0-cef87b59e227" + }, + "jcr:content": { + "jcr:primaryType": "nt:unstructured", + "jcr:title": "Riverside Camping Australia", + "cq:isTransCreated": true + }, + "adobe_waadobe_wa_mg_2466.jpg": { + "jcr:primaryType": "dam:Asset", + "jcr:mixinTypes": [ + "mix:versionable" + ], + "jcr:createdBy": "admin", + "jcr:versionHistory": "976af8b1-3afc-40ac-a164-ab4b16a8c8ef", + "jcr:predecessors": [ + "d56cedaf-53a3-414f-a28d-11f67338594f" + ], + "jcr:created": "2023-06-07T16:10:28.033+03:00", + "jcr:baseVersion": "d56cedaf-53a3-414f-a28d-11f67338594f", + "jcr:isCheckedOut": true, + "jcr:uuid": "ac3a0532-8823-4950-8209-26d8c0a35b92" + }, + "riverside-camping-australia": { + "jcr:primaryType": "dam:Asset", + "jcr:mixinTypes": [ + "mix:versionable" + ], + "jcr:createdBy": "admin", + "jcr:versionHistory": "37f52909-cd78-480e-912b-ccf313e275ec", + "jcr:predecessors": [ + "68190a39-93f4-4f5a-abe5-9602620736bc" + ], + "jcr:created": "2023-06-07T16:10:27.823+03:00", + "jcr:baseVersion": "68190a39-93f4-4f5a-abe5-9602620736bc", + "jcr:isCheckedOut": true, + "jcr:uuid": "9ead3982-0245-497f-97b0-cdf1b50f4534" + }, + "adobestock-257541512.jpeg": { + "jcr:primaryType": "dam:Asset", + "jcr:mixinTypes": [ + "mix:versionable" + ], + "jcr:createdBy": "admin", + "jcr:versionHistory": "17be11ee-7a05-4571-ac67-f417f85599c3", + "jcr:predecessors": [ + "21cfedac-2838-4351-a4e5-e4ca0842dc5e" + ], + "jcr:created": "2023-06-07T16:10:27.257+03:00", + "jcr:baseVersion": "21cfedac-2838-4351-a4e5-e4ca0842dc5e", + "jcr:isCheckedOut": true, + "jcr:uuid": "700f4628-f618-487a-b4ad-ebf25e8edaec" + }, + "adobestock-238491803.jpeg": { + "jcr:primaryType": "dam:Asset", + "jcr:mixinTypes": [ + "mix:versionable" + ], + "jcr:createdBy": "admin", + "jcr:versionHistory": "e338bb1f-42ce-430c-ad2c-e09500676c8f", + "jcr:predecessors": [ + "7c71b8c4-02a2-4677-bb9d-4e7823895c44" + ], + "jcr:created": "2023-06-07T16:10:26.471+03:00", + "jcr:baseVersion": "7c71b8c4-02a2-4677-bb9d-4e7823895c44", + "jcr:isCheckedOut": true, + "jcr:uuid": "1d32c79d-f849-4a34-8a6d-2bd7cb6635a3" + }, + "adobestock-178022573.jpeg": { + "jcr:primaryType": "dam:Asset", + "jcr:mixinTypes": [ + "mix:versionable" + ], + "jcr:createdBy": "admin", + "jcr:versionHistory": "fa58100d-23ef-4e65-9e94-e8223c061155", + "jcr:predecessors": [ + "e4b3c82a-0b21-4c4d-9f79-9c36ceefde27" + ], + "jcr:created": "2023-06-07T16:10:26.828+03:00", + "jcr:baseVersion": "e4b3c82a-0b21-4c4d-9f79-9c36ceefde27", + "jcr:isCheckedOut": true, + "jcr:uuid": "50ab0979-d7b5-486a-a303-8d876e4a96a4" + }, + "adobestock-167833331.jpeg": { + "jcr:primaryType": "dam:Asset", + "jcr:mixinTypes": [ + "mix:versionable" + ], + "jcr:createdBy": "admin", + "jcr:versionHistory": "d763e50b-14cf-452b-b93f-42614fb1eb30", + "jcr:predecessors": [ + "37bb8525-cfdf-40ac-8078-655aed478dd2" + ], + "jcr:created": "2023-06-07T16:10:25.988+03:00", + "jcr:baseVersion": "37bb8525-cfdf-40ac-8078-655aed478dd2", + "jcr:isCheckedOut": true, + "jcr:uuid": "bc596a32-f7c1-4b0a-8268-cca3e0dbc58e" + } +} \ No newline at end of file diff --git a/bundle/src/test/resources/contentsync/wknd-faqs.json b/bundle/src/test/resources/contentsync/wknd-faqs.json new file mode 100644 index 0000000000..e20d95d1f4 --- /dev/null +++ b/bundle/src/test/resources/contentsync/wknd-faqs.json @@ -0,0 +1,301 @@ +{ + "jcr:primaryType": "cq:PageContent", + "jcr:mixinTypes": [], + "jcr:createdBy": "admin", + "jcr:title": "FAQs", + "cq:template": "/conf/wknd/settings/wcm/templates/content-page-template", + "jcr:created": "2023-07-03T11:58:31.272+03:00", + "cq:lastModified": "2020-07-07T17:58:17.582-07:00", + "jcr:description": "Here we answer some of the questions that we hear most often from the community.", + "cq:tags": [], + "sling:resourceType": "wknd/components/page", + "cq:lastModifiedBy": "admin", + "root": { + "jcr:primaryType": "nt:unstructured", + "sling:resourceType": "wknd/components/container", + "container": { + "jcr:primaryType": "nt:unstructured", + "jcr:lastModifiedBy": "admin", + "layout": "responsiveGrid", + "jcr:lastModified": "2020-07-07T17:56:59.701-07:00", + "sling:resourceType": "wknd/components/container", + "cq:styleIds": ["1554340406437"], + "container": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:lastModifiedBy": "admin", + "layout": "responsiveGrid", + "jcr:lastModified": "2020-07-07T17:57:13.729-07:00", + "sling:resourceType": "wknd/components/container", + "cq:responsive": { + "jcr:primaryType": "nt:unstructured", + "default": { + "jcr:primaryType": "nt:unstructured", + "offset": "0", + "width": "8" + }, + "tablet": { + "jcr:primaryType": "nt:unstructured", + "offset": "0", + "width": "12" + }, + "phone": { + "jcr:primaryType": "nt:unstructured", + "offset": "0", + "width": "12" + } + }, + "title": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:lastModifiedBy": "admin", + "jcr:lastModified": "2019-10-14T09:28:37.553-07:00", + "sling:resourceType": "wknd/components/title", + "cq:styleIds": ["1568996484405"], + "cq:responsive": { + "jcr:primaryType": "nt:unstructured", + "phone": { + "jcr:primaryType": "nt:unstructured", + "offset": "0", + "width": "12" + } + } + }, + "image": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "fileReference": "/content/dam/wknd-shared/en/adventures/climbing-new-zealand/adobestock-277768563.jpeg", + "jcr:lastModifiedBy": "admin", + "jcr:lastModified": "2019-10-25T16:02:31.787-07:00", + "sling:resourceType": "wknd/components/image", + "cq:responsive": { + "jcr:primaryType": "nt:unstructured", + "default": { + "jcr:primaryType": "nt:unstructured", + "offset": "0", + "width": "8" + } + } + }, + "text": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:lastModifiedBy": "admin", + "text": "

WKND is a collective of outdoors, music, crafts, adventure sports, and travel enthusiasts that want to share our experiences, connections, and expertise with the world. Our objective is create a community to help like-minded adventure seekers find fun, engaging, and responsible ways to to enjoy life and create lasting memories.

\n", + "jcr:lastModified": "2019-10-25T11:13:10.055-07:00", + "sling:resourceType": "wknd/components/text", + "textIsRich": "true", + "cq:responsive": { + "jcr:primaryType": "nt:unstructured", + "phone": { + "jcr:primaryType": "nt:unstructured", + "offset": "0", + "width": "12" + }, + "default": { + "jcr:primaryType": "nt:unstructured", + "offset": "0", + "width": "8" + } + } + }, + "accordion": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "headingElement": "h3", + "jcr:lastModifiedBy": "admin", + "jcr:lastModified": "2019-10-25T11:15:13.801-07:00", + "singleExpansion": "false", + "sling:resourceType": "wknd/components/accordion", + "cq:responsive": { + "jcr:primaryType": "nt:unstructured", + "phone": { + "jcr:primaryType": "nt:unstructured", + "offset": "0", + "width": "12" + } + }, + "item_1571070731882": { + "jcr:primaryType": "nt:unstructured", + "cq:panelTitle": "Who is WKND's intended audience?", + "sling:resourceType": "wknd/components/container", + "text": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:lastModifiedBy": "admin", + "text": "

We believe the best adventures and activities are those that are accessible to everyone. WKND is designed to be inclusive of all age ranges, abilities, and budget-levels. We strive to cater to the thrill-seeking adrenaline junkie BASE-jumpers as well as novices that have a spare weekend and interest in trying something new.    

\n", + "jcr:lastModified": "2019-10-25T11:16:36.508-07:00", + "sling:resourceType": "wknd/components/text", + "textIsRich": "true" + } + }, + "item_1": { + "jcr:primaryType": "nt:unstructured", + "jcr:title": "Item 1", + "cq:panelTitle": "How does WKND pay for itself?", + "sling:resourceType": "wknd/components/container", + "text": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:lastModifiedBy": "admin", + "text": "

WKND charges a small fee for local promoters that want to sponsor their adventures and events on the WKND site.  Sponsored Adventures may get sorted to more prominent positions in our Adventures listings pages.

\n

 

\n", + "jcr:lastModified": "2019-10-25T11:17:16.517-07:00", + "sling:resourceType": "wknd/components/text", + "textIsRich": "true" + } + }, + "item_2": { + "jcr:primaryType": "nt:unstructured", + "jcr:title": "Item 2", + "cq:panelTitle": "Can I contribute to WKND?", + "sling:resourceType": "wknd/components/container", + "text": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:lastModifiedBy": "admin", + "text": "

Yes!  If you have the expertise and experiences to share, we’ll provide the platform to spread it.  As a Guest Writer, you will play an integral role in helping people find fun and cool things to do in your community.

\n", + "jcr:lastModified": "2019-10-25T11:17:39.663-07:00", + "sling:resourceType": "wknd/components/text", + "textIsRich": "true" + } + }, + "item_1571070705187": { + "jcr:primaryType": "nt:unstructured", + "cq:panelTitle": "How often is WKDN updated?", + "sling:resourceType": "wknd/components/container", + "text": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:lastModifiedBy": "admin", + "text": "

WKND is updated daily to provide you with the latest in-depth articles on fun activities that we’ve recently exploring and new adventures that are available for you to discover.  Come back often to see the latest or subscribe to our social feeds.

\n", + "jcr:lastModified": "2019-10-25T11:18:11.562-07:00", + "sling:resourceType": "wknd/components/text", + "textIsRich": "true" + } + }, + "item_1571070718887": { + "jcr:primaryType": "nt:unstructured", + "cq:panelTitle": "When was WKND founded?", + "sling:resourceType": "wknd/components/container", + "text": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:lastModifiedBy": "admin", + "text": "

WKND was created in 2015 when our founders, Daniel and Kilian, realized that their friends and family were constantly using them as resources to find fun things to do while they were in Los Angeles. They loved sharing ideas about fun events and activities they knew of, but wanted to be able to do it at larger scale across communities.  They decided to start WKND as a way to share their insights and experiences with as many people as possible.    

\n", + "jcr:lastModified": "2019-10-25T11:18:36.836-07:00", + "sling:resourceType": "wknd/components/text", + "textIsRich": "true" + } + }, + "item_1572027287775": { + "jcr:primaryType": "nt:unstructured", + "cq:panelTitle": "Is a hot dog a sandwich?", + "sling:resourceType": "wknd/components/container", + "text": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:lastModifiedBy": "admin", + "text": "

While it may be described as meat between two pieces of bread, a hot dog is just a sandwich in the same way Michael Jordan was just a basketball player or William Shakespeare was just a playwright.  Technically true, but vastly understated.

\n", + "jcr:lastModified": "2019-10-25T11:19:08.411-07:00", + "sling:resourceType": "wknd/components/text", + "textIsRich": "true" + } + }, + "item_1572027298182": { + "jcr:primaryType": "nt:unstructured", + "cq:panelTitle": "Is WKND a real company?", + "sling:resourceType": "wknd/components/container", + "text": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:lastModifiedBy": "admin", + "text": "

No. The WKND is a fictional online magazine and adventure company that focuses on outdoor activities and trips across the globe. The WKND site is designed to demonstrate functionality for Adobe Experience Manager. There is also a corresponding tutorial that walks a developer through the development.  Special thanks to Lorenzo Buosi and Kilian Amendola who created the beautiful design for the WKND site.

\n", + "jcr:lastModified": "2019-10-25T11:19:41.501-07:00", + "sling:resourceType": "wknd/components/text", + "textIsRich": "true", + "cq:responsive": { + "jcr:primaryType": "nt:unstructured", + "default": { + "jcr:primaryType": "nt:unstructured", + "offset": "0", + "width": "12" + } + } + } + } + } + }, + "container_293505757": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:lastModifiedBy": "admin", + "jcr:lastModified": "2019-10-14T10:20:06.270-07:00", + "sling:resourceType": "wknd/components/container", + "cq:responsive": { + "jcr:primaryType": "nt:unstructured", + "default": { + "jcr:primaryType": "nt:unstructured", + "offset": "1", + "width": "3" + }, + "tablet": { + "jcr:primaryType": "nt:unstructured", + "offset": "0", + "width": "12" + }, + "phone": { + "jcr:primaryType": "nt:unstructured", + "offset": "0", + "width": "12" + } + }, + "separator": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:lastModifiedBy": "admin", + "jcr:lastModified": "2019-10-14T13:49:29.760-07:00", + "sling:resourceType": "wknd/components/separator", + "cq:styleIds": [ + "1571075919520", + "1571075910473" + ] + }, + "title": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:title": "Need more help?", + "jcr:lastModifiedBy": "admin", + "type": "h3", + "jcr:lastModified": "2019-10-14T10:21:13.842-07:00", + "sling:resourceType": "wknd/components/title", + "cq:responsive": { + "jcr:primaryType": "nt:unstructured", + "phone": { + "jcr:primaryType": "nt:unstructured", + "offset": "0", + "width": "12" + } + } + }, + "text": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:lastModifiedBy": "admin", + "text": "

Give us a call at 1-800-800-0000.
\r\nE-mail us at info@wknd.com
\r\nWe love to talk adventures!

\r\n", + "jcr:lastModified": "2019-10-14T10:31:09.107-07:00", + "sling:resourceType": "wknd/components/text", + "textIsRich": "true", + "cq:styleIds": ["1571074070335"], + "cq:responsive": { + "jcr:primaryType": "nt:unstructured", + "phone": { + "jcr:primaryType": "nt:unstructured", + "offset": "0", + "width": "12" + } + } + } + } + } + } + } \ No newline at end of file diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/POST.jsp b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/POST.jsp new file mode 100755 index 0000000000..11c893a26f --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/POST.jsp @@ -0,0 +1,254 @@ +<%-- + * ACS AEM Commons + * + * Copyright (C) 2013 - 2023 Adobe + * + * 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. + --%> +<%@include file="/libs/foundation/global.jsp"%><% +%><%@page session="false" contentType="text/html; charset=utf-8" + pageEncoding="UTF-8" + import=" + java.util.List, + java.util.ArrayList, + java.util.Collection, + java.util.Set, + java.util.LinkedHashSet, + java.util.stream.Collectors, + java.io.InputStream, + java.io.IOException, + java.io.PrintWriter, + java.io.ByteArrayInputStream, + javax.jcr.Session, + javax.json.Json, + javax.json.JsonArray, + javax.json.JsonObject, + org.apache.sling.jcr.contentloader.ContentImporter, + org.apache.sling.api.resource.ResourceUtil, + org.apache.commons.lang3.time.DurationFormatUtils, + com.adobe.acs.commons.contentsync.* + +"%><% +%> + + + + + +
+<%
+
+    String root = request.getParameter("root");
+    String cfgPath = request.getParameter("source");
+
+    boolean dryRun = request.getParameter("dryRun") != null;
+    String workflowModel = request.getParameter("workflowModel");
+	boolean incremental = request.getParameter("incremental") != null;
+	boolean createVersion = request.getParameter("createVersion") != null;
+	boolean delete = request.getParameter("delete") != null;
+
+    ValueMap generalSettings = ConfigurationUtils.getSettingsResource(resourceResolver).getValueMap();
+
+    String observationData = generalSettings.get(ConfigurationUtils.EVENT_USER_DATA_KEY, String.class);
+    String strategyPid = generalSettings.get(ConfigurationUtils.UPDATE_STRATEGY_KEY, String.class);
+
+	Resource cfg = resourceResolver.getResource(cfgPath);
+    SyncHostConfiguration hostConfig = cfg.adaptTo(SyncHostConfiguration.class);
+
+    if(hostConfig == null || hostConfig.getHost().isEmpty()){
+        error(out, "Configure AEM Environments");
+        return;
+    }
+
+    String catalogServlet = slingRequest.getResource().getPath() + ".catalog.json";
+    Session session = resourceResolver.adaptTo(Session.class);
+    ContentReader contentReader = new ContentReader(session);
+
+    UpdateStrategy updateStrategy = sling.getServices(UpdateStrategy.class, "(component.name=" + strategyPid + ")")[0];
+    try(RemoteInstance remoteInstance = new RemoteInstance(hostConfig)){
+        ContentImporter importer = sling.getService(ContentImporter.class);
+        ContentSync contentSync = new ContentSync(remoteInstance, resourceResolver, importer);
+        ContentCatalog contentCatalog = new ContentCatalog(remoteInstance, catalogServlet);
+
+        out.println("building catalog from " + contentCatalog.getFetchURI(root, strategyPid) );
+        out.flush();
+        List catalog;
+        List remoteItems = contentCatalog.fetch(root, strategyPid);
+        long t0 = System.currentTimeMillis();
+        out.println(remoteItems.size() + " resource"+(remoteItems.size() == 1 ? "" : "s")+" fetched in " + (System.currentTimeMillis() - t0) + " ms");
+        if(incremental){
+            catalog = contentCatalog.getDelta(remoteItems, resourceResolver, updateStrategy);
+            out.println(catalog.size() + " resource"+(catalog.size() == 1 ? "" : "s")+" modified");
+        } else {
+            catalog = remoteItems;
+        }
+
+        long count = 0;
+        t0 = System.currentTimeMillis();
+        long t00 = System.currentTimeMillis();
+        List updatedResources = new ArrayList<>();
+
+        // the list of updated resources having child nodes to ensure ordering after update
+        Set sortedNodes = new LinkedHashSet<>();
+
+        for (CatalogItem item : catalog) {
+            String path = item.getPath();
+            String customExporter = item.getCustomExporter();
+            if(customExporter != null){
+                error(out, "\t" + path + " has a custom json exporter (" + customExporter + ") and cannot be imported");
+                continue;
+            }
+
+
+            Resource targetResource = resourceResolver.getResource(path);
+
+            boolean modified = updateStrategy.isModified(item, targetResource);
+
+            if(targetResource == null || modified || !incremental) {
+                out.println(++count + "\t" + path);
+                String msg = updateStrategy.getMessage(item, targetResource);
+                out.println("\t" + msg);
+                if(!dryRun) {
+                    String reqPath = item.getContentUri() ;
+                    JsonObject json = remoteInstance.getJson(reqPath);
+
+                    List binaryProperties = contentReader.collectBinaryProperties(json);
+                    JsonObject sanitizedJson = contentReader.sanitize(json);
+
+                    if(targetResource != null && createVersion) {
+                        String revisionId = contentSync.createVersion(targetResource);
+                        if(revisionId != null) {
+                            out.println("\tcreated revision: " + revisionId);
+                        }
+                    }
+                    if(observationData != null){
+                        session.getWorkspace().getObservationManager().setUserData(observationData);
+                    }
+
+                    out.println("\timporting data");
+                    contentSync.importData(item, sanitizedJson);
+
+                    if(!binaryProperties.isEmpty()){
+                        out.println("\tcopying " + binaryProperties.size() + " binary propert" + (binaryProperties.size() > 1 ? "ies" : "y"));
+                        boolean contentResource = item.hasContentResource();
+                        String basePath = path + (contentResource ? "/jcr:content" : "");
+                        List propertyPaths = binaryProperties.stream().map(p -> basePath + p).collect(Collectors.toList());
+                        contentSync.copyBinaries(propertyPaths);
+                    }
+
+			        String parentPath = ResourceUtil.getParent(path);
+                    if(parentPath.startsWith(root)){
+                        sortedNodes.add(parentPath);
+                    }
+
+                    if(observationData != null){
+                        session.getWorkspace().getObservationManager().setUserData(observationData);
+                    }
+
+                    session.save();
+
+                    // print ETA every 5 seconds
+                    if(System.currentTimeMillis() - t00 > 5000L){
+                        long remainingCycles = catalog.size() - count;
+                        long pace = (System.currentTimeMillis()-t0)/count;
+                        long estimatedTime = remainingCycles * pace ;
+                        String pct = String.format("%.0f", count*100./catalog.size());
+                        String eta = DurationFormatUtils.formatDurationWords(estimatedTime, true, true);
+                        String etaMsg = pct +"%, ETA: " + eta;
+                        t00 = System.currentTimeMillis();
+                        out.println(etaMsg);
+                    }
+
+                    updatedResources.add(path);
+
+                    out.flush();
+                }
+            }
+        }
+
+        if(delete){
+            Collection remotePaths = remoteItems.stream().map(c -> c.getPath()).collect(Collectors.toList());
+            Collection localPaths = updateStrategy.getItems(slingRequest).stream().map(c -> c.getPath()).collect(Collectors.toList());
+            localPaths.removeAll(remotePaths);
+            out.println();
+            for(String path : localPaths){
+				Resource res = resourceResolver.getResource(path);
+                if(res != null){
+                	out.println("deleting " + path);
+                    if(!dryRun) {
+                        if(res != null) {
+                            resourceResolver.delete(res);
+                        }
+                    }
+                }
+            }
+        }
+
+        out.println();
+        for(String parentPath : sortedNodes){
+            Node targetNode = resourceResolver.getResource(parentPath).adaptTo(Node.class);
+            out.println("sorting child nodes of " + targetNode.getPath() );
+            contentSync.sort(targetNode);
+        }
+        session.save();
+
+        out.println();
+        out.println("sync-ed " + count + " resources, in " + (System.currentTimeMillis() - t0) + " ms");
+
+        if(!dryRun && workflowModel != null && !workflowModel.isEmpty()){
+	        out.println();
+            long t1 = System.currentTimeMillis();
+
+            out.println("starting a " + workflowModel + " workflow for each processed item");
+            out.flush();
+            contentSync.runWorkflows(workflowModel, updatedResources);
+	        out.println("started " + updatedResources.size() + " workflows, in " + (System.currentTimeMillis() - t1) + " ms");
+        }
+
+    } catch(Exception e){
+        if(e.getMessage() != null && e.getMessage().startsWith("Not a date string:")){
+            error(out, "It appears Sling GET Servlet on " + hostConfig.getHost() + " is configured to use the legacy ECMA date format.\n" +
+                  "Please edit configuration for PID org.apache.sling.servlets.get.DefaultGetServlet and make sure 'Enable legacy Sling ECMA format for dates' is unchecked.");
+        }
+        error(out, e);
+    }
+
+
+%>
+
+ + +<%! + + void error(JspWriter out, String msg) throws IOException { + out.print(""); + out.print(msg); + out.println(""); + } + + void error(JspWriter out, Throwable e) throws IOException { + out.print(""); + e.printStackTrace(new PrintWriter(out)); + out.println(""); + } +%> \ No newline at end of file diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/.content.xml b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/.content.xml new file mode 100755 index 0000000000..fc394d2f46 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/.content.xml @@ -0,0 +1,5 @@ + + diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/css.txt b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/css.txt new file mode 100755 index 0000000000..2b43fff6c1 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/css.txt @@ -0,0 +1,3 @@ +#base=css + +app.css \ No newline at end of file diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/css/app.css b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/css/app.css new file mode 100755 index 0000000000..332077e510 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/css/app.css @@ -0,0 +1,13 @@ +.diagnosis-panel-with-margin { + margin-top: 1rem; + margin-left: 3rem; + margin-right: 3rem; +} + +.panelWithMarginTop { + margin-top: 1.5rem; +} + +.checkboxes-panel { + column-width: 400px; +} \ No newline at end of file diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/js.txt b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/js.txt new file mode 100755 index 0000000000..149ad78246 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/js.txt @@ -0,0 +1,3 @@ +#base=js + +app.js \ No newline at end of file diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/js/app.js b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/js/app.js new file mode 100755 index 0000000000..1bac84de12 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/clientlibs/js/app.js @@ -0,0 +1,72 @@ +/* + * ACS AEM Commons + * + * Copyright (C) 2013 - 2023 Adobe + * + * 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. + */ +(function(document, $, Granite) { + "use strict"; + + var ui = $(window).adaptTo("foundation-ui"); + + $(window).adaptTo("foundation-registry").register("foundation.collection.action.action", { + name: "acs-commons.dashboard.task.delete", + handler: function(name, el, config, collection, selections) { + if (selections.length === 1) { + var card = $(selections).find("coral-card"); + if(card.length > 0) { + var taskName = card.data("host"); + var message = "You are going to delete the following item:" + + "

" + taskName + "

"; + + ui.prompt("Delete Host", message, "notice", [ + { + text: "Cancel" + }, + { + text: "Delete", + warning: true, + handler: function() { + selections.map(function(item) { + return $.ajax({ + type: "DELETE", + url: $(item).find("coral-card").data("path") + }).then(function() { + window.location.reload(true); + }); + }); + } + } + ]); + } + } + } + }); + + $(document).on("click", ".configureQuickAction", function(e) { + var form = $(this).closest("coral-masonry-item").find("coral-card"); + var host = $(form).data("host"); + var username = $(form).data("username"); + var password = $(form).data("password"); + var action = $(form).data("path"); + var dlg = document.querySelector('#modalConfigureHost'); + $("#configureHost-host").val(host); + $("#configureHost-username").val(username); + $("#configureHost-password").val(password); + $(dlg).find("#createHostForm").attr("action", action); + dlg.show(); + }); + + +})(document, Granite.$); diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/configure/configurehostdatasource/configurehostdatasource.jsp b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/configure/configurehostdatasource/configurehostdatasource.jsp new file mode 100755 index 0000000000..2754504f06 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/configure/configurehostdatasource/configurehostdatasource.jsp @@ -0,0 +1,38 @@ +<%-- + * ACS AEM Commons + * + * Copyright (C) 2013 - 2023 Adobe + * + * 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. + --%> +<%@include file="/libs/foundation/global.jsp"%><% +%><%@page session="false" import=" + com.adobe.granite.ui.components.PagingIterator, + com.adobe.granite.ui.components.ds.DataSource, + com.adobe.granite.ui.components.ds.AbstractDataSource, + java.util.Iterator, + com.adobe.acs.commons.contentsync.ConfigurationUtils + " %><% + + // ensure the settings nodes + ConfigurationUtils.getSettingsResource(resourceResolver); + + Resource hostsResource = ConfigurationUtils.getHostsResource(resourceResolver); + DataSource ds = new AbstractDataSource() { + public Iterator iterator() { + return new PagingIterator(hostsResource.listChildren(), null, null); + } + }; + + request.setAttribute(DataSource.class.getName(), ds); +%> diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/configure/configurehostentry/configurehostentry.jsp b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/configure/configurehostentry/configurehostentry.jsp new file mode 100755 index 0000000000..cf284f193e --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/configure/configurehostentry/configurehostentry.jsp @@ -0,0 +1,73 @@ +<%-- + * ACS AEM Commons + * + * Copyright (C) 2013 - 2023 Adobe + * + * 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. + --%> +<%@include file="/libs/granite/ui/global.jsp"%><% +%><%@page session="false" import=" + org.apache.sling.api.resource.ValueMap, + com.adobe.granite.ui.components.Tag, + com.adobe.granite.ui.components.AttrBuilder" %><% + + String contextPath = request.getContextPath(); + ValueMap valueMap = resource.adaptTo(ValueMap.class); + String username = valueMap.get("username", ""); + String password = valueMap.get("password", ""); + String url = valueMap.get("host", ""); + String icon = "viewList"; + String smallIcon = "search"; + String path = resource.getPath(); + String href = "/apps/acs-commons/content/contentsync.html" + path; + + Tag tag = cmp.consumeTag(); + AttrBuilder attrs = tag.getAttrs(); + + attrs.addClass("foundation-collection-navigator"); + attrs.addClass("whitecard"); +%> + + data-foundation-collection-navigator-href="<%= href %>" + data-host="<%=url%>" + data-username="<%=username%>" + data-password="<%=password%>" + data-path="<%=path%>" + colorhint="#FFFFFF"> + + + + + + + + + + + +
+
+ + +
+
+ <%= url %> +
+
+
+
+ + Select + Configure + diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/iframe/.content.xml b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/iframe/.content.xml new file mode 100755 index 0000000000..491392d539 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/iframe/.content.xml @@ -0,0 +1,3 @@ + + diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/iframe/iframe.jsp b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/iframe/iframe.jsp new file mode 100755 index 0000000000..95e60b52f0 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/iframe/iframe.jsp @@ -0,0 +1,36 @@ +<%-- + * ACS AEM Commons + * + * Copyright (C) 2013 - 2023 Adobe + * + * 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. + --%> +<%@include file="/libs/granite/ui/global.jsp" %><% +%><%@page session="false" + import="java.util.Iterator, + com.adobe.granite.ui.components.AttrBuilder, + com.adobe.granite.ui.components.Config, + com.adobe.granite.ui.components.Tag, + com.adobe.granite.ui.components.Value" %><% +Tag tag = cmp.consumeTag(); +AttrBuilder attrs = tag.getAttrs(); +cmp.populateCommonAttrs(attrs); +Config cfg = cmp.getConfig(); +attrs.add("name", cfg.get("granite:id", String.class)); + +%> +
+ +
diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/selecthostdatasource/selecthostdatasource.jsp b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/selecthostdatasource/selecthostdatasource.jsp new file mode 100755 index 0000000000..4edac63bb2 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/selecthostdatasource/selecthostdatasource.jsp @@ -0,0 +1,51 @@ +<%-- + * ACS AEM Commons + * + * Copyright (C) 2013 - 2023 Adobe + * + * 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. + --%> +<%@include file="/libs/foundation/global.jsp"%><% +%><%@page session="false" import=" + org.apache.sling.api.resource.ResourceMetadata, + org.apache.sling.api.wrappers.ValueMapDecorator, + java.util.List, + java.util.HashMap, + java.util.ArrayList, + com.adobe.granite.ui.components.ds.DataSource, + com.adobe.granite.ui.components.ds.SimpleDataSource, + com.adobe.granite.ui.components.ds.ValueMapResource, + com.adobe.acs.commons.contentsync.ConfigurationUtils + + " %><% + + Resource hostsResource = ConfigurationUtils.getHostsResource(resourceResolver); + Resource suffixResource = slingRequest.getRequestPathInfo().getSuffixResource(); + + List lst = new ArrayList(); + if(hostsResource != null) { + for (Resource item : hostsResource.getChildren()) { + ValueMap vm = new ValueMapDecorator(new HashMap()); + + String host = item.getValueMap().get("host", ""); + vm.put("value", item.getPath()); + vm.put("text", host); + if(suffixResource != null && suffixResource.getPath().equals(item.getPath())) { + vm.put("selected", true); + } + + lst.add(new ValueMapResource(resourceResolver, new ResourceMetadata(), "nt:unstructured", vm)); + } + } + request.setAttribute(DataSource.class.getName(), new SimpleDataSource(lst.iterator())); +%> \ No newline at end of file diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/strategydatasource/strategydatasource.jsp b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/strategydatasource/strategydatasource.jsp new file mode 100755 index 0000000000..602faaf209 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/contentsync/strategydatasource/strategydatasource.jsp @@ -0,0 +1,44 @@ +<%-- + * ACS AEM Commons + * + * Copyright (C) 2013 - 2023 Adobe + * + * 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. + --%> +<%@include file="/libs/foundation/global.jsp"%><% +%><%@page session="false" import=" + java.util.List, + java.util.ArrayList, + java.util.HashMap, + org.apache.sling.api.resource.ResourceMetadata, + org.apache.sling.api.wrappers.ValueMapDecorator, + com.adobe.granite.ui.components.ds.DataSource, + com.adobe.granite.ui.components.ds.SimpleDataSource, + com.adobe.granite.ui.components.ds.ValueMapResource, + com.adobe.acs.commons.contentsync.UpdateStrategy"%><% +%><% + + List lst = new ArrayList(); + UpdateStrategy[] s = sling.getServices(UpdateStrategy.class, null); + if(s != null) { + for (UpdateStrategy stragegy : s) { + ValueMap vm = new ValueMapDecorator(new HashMap()); + + vm.put("value", stragegy.getClass().getName()); + vm.put("text", stragegy.getClass().getName()); + + lst.add(new ValueMapResource(resourceResolver, new ResourceMetadata(), "nt:unstructured", vm)); + } + } + request.setAttribute(DataSource.class.getName(), new SimpleDataSource(lst.iterator())); +%> \ No newline at end of file diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/content/contentsync/.content.xml b/ui.apps/src/main/content/jcr_root/apps/acs-commons/content/contentsync/.content.xml new file mode 100755 index 0000000000..07a5fb73a9 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/content/contentsync/.content.xml @@ -0,0 +1,130 @@ + + + + + + + + + <actions jcr:primaryType="nt:unstructured"> + <secondary jcr:primaryType="nt:unstructured"> + <configure + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/anchorbutton" + href="/apps/acs-commons/content/contentsync/configure.html" + icon="gear" + text="Configure" + variant="primary"/> + </secondary> + </actions> + <content + granite:class="diagnosis-panel-with-margin" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/container"> + <items jcr:primaryType="nt:unstructured"> + <form + granite:class="transform-form" + granite:id="transform-form" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form" + action="/apps/acs-commons/content/contentsync/jcr:content/sync.html" + foundation-form="{Boolean}true" + method="POST" + style="vertical" + target="replication_status_iframe"> + <items jcr:primaryType="nt:unstructured"> + <path + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/pathfield" + fieldLabel="Path to synchronize" + filter="hierarchyNotFile" + name="root" + required="{Boolean}true" + rootPath="/content" + value="/content/my-site"/> + <host + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/select" + fieldLabel="Source environment" + name="source" + required="{Boolean}true"> + <datasource + jcr:primaryType="nt:unstructured" + sling:resourceType="acs-commons/components/utilities/contentsync/selecthostdatasource"/> + </host> + <workflow + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/select" + emptyText="Select one" + fieldLabel="On-complete workflow " + ignoreData="{Boolean}true" + name="workflowModel"> + <datasource + jcr:primaryType="nt:unstructured" + sling:resourceType="cq/gui/components/coral/common/admin/timeline/events/workflow/datasources/models"/> + </workflow> + <checkboxes + granite:class="checkboxes-panel" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/container"> + <items jcr:primaryType="nt:unstructured"> + <incremental + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/checkbox" + checked="{Boolean}true" + name="incremental" + text="Incremental update. Will only copy new and changed resources" + value="true"/> + <dryRun + granite:class="notice-wide" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/checkbox" + checked="{Boolean}true" + name="dryRun" + text="Dry Run" + value="false"/> + <revision + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/checkbox" + name="createVersion" + text="Create revision before update" + value="true"/> + <delete + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/checkbox" + name="delete" + text="Delete files that exist in the destination but not in the source" + value="true"/> + </items> + </checkboxes> + <submit + granite:id="executeQueryButton" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/button" + text="Submit" + type="submit" + variant="primary"/> + <iframe + granite:id="replication_status_iframe" + jcr:primaryType="nt:unstructured" + sling:resourceType="acs-commons/components/utilities/contentsync/iframe"/> + </items> + </form> + </items> + </content> + <sync + jcr:primaryType="nt:unstructured" + sling:resourceType="acs-commons/components/utilities/contentsync"/> + </jcr:content> +</jcr:root> diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/content/contentsync/configure/.content.xml b/ui.apps/src/main/content/jcr_root/apps/acs-commons/content/contentsync/configure/.content.xml new file mode 100755 index 0000000000..e1f3aa870d --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/content/contentsync/configure/.content.xml @@ -0,0 +1,275 @@ +<?xml version="1.0" encoding="UTF-8"?> +<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" + jcr:primaryType="cq:Page"> + <jcr:content + jcr:primaryType="nt:unstructured" + jcr:title="Content Sync - Configure AEM Environments" + sling:resourceType="granite/ui/components/shell/collectionpage" + consoleId="contentsyncHosts" + contentPath="/content" + currentView="${state["shell.collectionpage.layoutId"].string}" + modeGroup="contentsync-hosts-task-collection" + pageURITemplate="/libs/granite/operations/content/diagnosis.html" + targetCollection=".contentsync-hosts-task-collection"> + <head jcr:primaryType="nt:unstructured"> + <clientlibs + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/includeclientlibs" + categories="[coralui3,granite.ui.coral.foundation,acs-commons.contentsync]"/> + </head> + <views jcr:primaryType="nt:unstructured"> + <card + granite:class="contentsync-hosts-task-collection" + jcr:primaryType="nt:unstructured" + jcr:title="Card View" + sling:resourceType="granite/ui/components/coral/foundation/masonry" + icon="viewCard" + layoutId="card" + limit="{Long}20" + selectionCount="single" + selectionMode="{Boolean}false" + size="40" + stateId="shell.collectionpage"> + <datasource + jcr:primaryType="nt:unstructured" + sling:resourceType="acs-commons/components/utilities/contentsync/configure/configurehostdatasource" + itemResourceType="acs-commons/components/utilities/contentsync/configure/configurehostentry" + limit="10" + offset="0" + path="${requestPathInfo.suffix}"/> + <granite:data + jcr:primaryType="nt:unstructured" + foundation-mode-group="contentsync-hosts-task-collection"/> + </card> + </views> + <actions jcr:primaryType="nt:unstructured"> + <primary jcr:primaryType="nt:unstructured"> + <configure + granite:class="foundation-toggleable-control" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/button" + href="#" + icon="gear" + text="General Settings" + variant="actionBar" + x-cq-linkchecker="skip"> + <granite:data + jcr:primaryType="nt:unstructured" + foundation-toggleable-control-action="show" + foundation-toggleable-control-target="#modalGeneralSettings"/> + </configure> + <back + granite:hidden="{Boolean}true" + granite:id="backButton" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/anchorbutton" + href="/libs/granite/operations/content/healthreports/healthreportlist.html" + icon="chevronLeft" + variant="actionBar"/> + <add + granite:class="foundation-toggleable-control" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/button" + href="#" + icon="addCircle" + text="Add Host" + variant="actionBar" + x-cq-linkchecker="skip"> + <granite:data + jcr:primaryType="nt:unstructured" + foundation-toggleable-control-action="show" + foundation-toggleable-control-target="#modalConfigureHost"/> + </add> + </primary> + <secondary jcr:primaryType="nt:unstructured"/> + <selection jcr:primaryType="nt:unstructured"> + <configure + granite:hidden="{Boolean}true" + granite:id="configureHealthCheckButton" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/anchorbutton" + foundation-collection-action="\{"target":".granite-health-reports-collection", "activeSelectionCount":"single"}" + href="/system/console/configMgr" + icon="gear" + target="_blank" + text="Configure" + variant="actionBar" + x-cq-linkchecker="skip"/> + <view + granite:hidden="{Boolean}true" + granite:id="viewHealthCheckDetailsButton" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/anchorbutton" + href="/system/console/configMgr" + icon="browse" + text="View" + variant="actionBar" + x-cq-linkchecker="skip"/> + <delete + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/collection/action" + action="acs-commons.dashboard.task.delete" + activeSelectionCount="single" + href="" + icon="delete" + target=".contentsync-hosts-task-collection" + text="Delete" + variant="actionBar" + x-cq-linkchecker="skip"/> + </selection> + </actions> + <footer + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/container"> + <items jcr:primaryType="nt:unstructured"> + <create + granite:id="modalConfigureHost" + jcr:primaryType="nt:unstructured" + jcr:title="Configure AEM Host To Sync" + sling:resourceType="granite/ui/components/coral/foundation/dialog"> + <items jcr:primaryType="nt:unstructured"> + <form + granite:id="createHostForm" + jcr:primaryType="nt:unstructured" + jcr:title="Configure Host" + sling:resourceType="granite/ui/components/coral/foundation/form" + action="/var/acs-commons/contentsync/hosts/*" + async="{Boolean}true" + enctype="application/x-www-form-urlencoded" + foundationForm="{Boolean}true" + method="POST" + style="vertical"> + <items jcr:primaryType="nt:unstructured"> + <host + granite:id="configureHost-host" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/textfield" + emptyText="http://localhost:4502" + name="./host"/> + <username + granite:id="configureHost-username" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/textfield" + emptyText="username" + name="./username"/> + <password + granite:id="configureHost-password" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/password" + emptyText="password" + name="./password"/> + <resourceType + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/hidden" + ignoreData="{Boolean}true" + name="./sling:resourceType" + value="acs-commons/components/utilities/contentsync/configure/configurehostentry"/> + <primaryType + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/hidden" + name="./jcr:primaryType" + value="nt:unstructured"/> + </items> + <successresponse + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/responses/reload" + target=".cq-workflow-admin-models"/> + </form> + </items> + <footer jcr:primaryType="nt:unstructured"> + <cancel + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/button" + text="Cancel"> + <parentConfig + jcr:primaryType="nt:unstructured" + close="{Boolean}true"/> + </cancel> + <submit + granite:id="createHostFormSubmitBtn" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/button" + formId="createHostForm" + text="Submit" + type="submit" + variant="primary"> + <parentConfig + jcr:primaryType="nt:unstructured" + close="{Boolean}true"/> + </submit> + </footer> + </create> + <general_settings + granite:id="modalGeneralSettings" + jcr:primaryType="nt:unstructured" + jcr:title="General Settings" + sling:resourceType="granite/ui/components/coral/foundation/dialog"> + <items jcr:primaryType="nt:unstructured"> + <form + granite:id="generalSettingsForm" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form" + action="/var/acs-commons/contentsync/settings" + async="{Boolean}true" + dataPath="/var/acs-commons/contentsync/settings" + enctype="application/x-www-form-urlencoded" + foundationForm="{Boolean}true" + method="POST" + style="vertical"> + <items jcr:primaryType="nt:unstructured"> + <event-user-data + granite:id="general-observationData" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/textfield" + fieldDescription="Optional string to pass to the ObservationManager API before changes are committed. This feature can be used to suppress DAM workflows that start when an asset is updated." + fieldLabel="ObservationManager User Data" + name="./event-user-data"/> + <update-strategy + granite:id="general-strategy" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/select" + fieldLabel="Update Strategy" + name="./update-strategy"> + <datasource + jcr:primaryType="nt:unstructured" + sling:resourceType="acs-commons/components/utilities/contentsync/strategydatasource"/> + </update-strategy> + <primaryType + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/hidden" + name="./jcr:primaryType" + value="nt:unstructured"/> + </items> + <successresponse + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/responses/reload" + target=".cq-workflow-admin-models"/> + </form> + </items> + <footer jcr:primaryType="nt:unstructured"> + <cancel + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/button" + text="Cancel"> + <parentConfig + jcr:primaryType="nt:unstructured" + close="{Boolean}true"/> + </cancel> + <submit + granite:id="generalSettingsSubmitBtn" + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/button" + formId="generalSettingsForm" + text="Submit" + type="submit" + variant="primary"> + <parentConfig + jcr:primaryType="nt:unstructured" + close="{Boolean}true"/> + </submit> + </footer> + </general_settings> + </items> + </footer> + </jcr:content> +</jcr:root> diff --git a/ui.apps/src/main/content/jcr_root/apps/cq/core/content/nav/tools/acs-commons/.content.xml b/ui.apps/src/main/content/jcr_root/apps/cq/core/content/nav/tools/acs-commons/.content.xml index 1afddf209b..7e5d593eb7 100644 --- a/ui.apps/src/main/content/jcr_root/apps/cq/core/content/nav/tools/acs-commons/.content.xml +++ b/ui.apps/src/main/content/jcr_root/apps/cq/core/content/nav/tools/acs-commons/.content.xml @@ -24,6 +24,7 @@ <generic-lists/> <redirect-manager/> + <contentsync/> <reports/> <exporters/> diff --git a/ui.apps/src/main/content/jcr_root/apps/cq/core/content/nav/tools/acs-commons/contentsync/.content.xml b/ui.apps/src/main/content/jcr_root/apps/cq/core/content/nav/tools/acs-commons/contentsync/.content.xml new file mode 100644 index 0000000000..2896fddeb4 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/cq/core/content/nav/tools/acs-commons/contentsync/.content.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ ACS AEM Commons + ~ + ~ Copyright (C) 2013 - 2023 Adobe + ~ + ~ 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. + --> + +<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" + jcr:primaryType="nt:unstructured" + jcr:title="Content Sync" + jcr:description="Synchronize content between AEM instances" + icon="sync" + href="/apps/acs-commons/content/contentsync.html" + id="acs-commons__contentsync" + target="_blank"/>