diff --git a/core/src/main/java/com/exadel/aem/toolkit/core/lists/servlets/ItemComponentsServlet.java b/core/src/main/java/com/exadel/aem/toolkit/core/lists/servlets/ItemComponentsServlet.java index c67a31740..ba5c8266a 100644 --- a/core/src/main/java/com/exadel/aem/toolkit/core/lists/servlets/ItemComponentsServlet.java +++ b/core/src/main/java/com/exadel/aem/toolkit/core/lists/servlets/ItemComponentsServlet.java @@ -14,7 +14,6 @@ package com.exadel.aem.toolkit.core.lists.servlets; import java.util.ArrayList; -import java.util.HashMap; import java.util.Iterator; import java.util.List; import javax.annotation.Nonnull; @@ -26,21 +25,18 @@ import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.SlingHttpServletResponse; import org.apache.sling.api.resource.Resource; -import org.apache.sling.api.resource.ResourceMetadata; import org.apache.sling.api.resource.ResourceResolver; -import org.apache.sling.api.resource.ValueMap; import org.apache.sling.api.servlets.HttpConstants; import org.apache.sling.api.servlets.SlingSafeMethodsServlet; -import org.apache.sling.api.wrappers.ValueMapDecorator; import org.osgi.service.component.annotations.Component; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.day.cq.commons.jcr.JcrConstants; import com.adobe.granite.ui.components.ds.DataSource; import com.adobe.granite.ui.components.ds.SimpleDataSource; -import com.adobe.granite.ui.components.ds.ValueMapResource; import com.exadel.aem.toolkit.core.CoreConstants; +import com.exadel.aem.toolkit.core.utils.ResourceFactory; /** * Provides the collection of AEM resources that will represent Exadel Toolbox Lists items. This collection will be displayed @@ -76,10 +72,11 @@ protected void doGet(@Nonnull SlingHttpServletRequest request, @Nonnull SlingHtt List actualList = new ArrayList<>(); while (resources.hasNext()) { Resource item = resources.next(); - ValueMap valueMap = new ValueMapDecorator(new HashMap<>()); - valueMap.put(CoreConstants.PN_VALUE, item.getPath()); - valueMap.put(CoreConstants.PN_TEXT, item.getValueMap().get(JcrConstants.JCR_TITLE, StringUtils.EMPTY)); - actualList.add(new ValueMapResource(resolver, new ResourceMetadata(), JcrConstants.NT_UNSTRUCTURED, valueMap)); + actualList.add(ResourceFactory + .newResource(resolver) + .property(CoreConstants.PN_VALUE, item.getPath()) + .property(CoreConstants.PN_TEXT, item.getValueMap().get(JcrConstants.JCR_TITLE, StringUtils.EMPTY)) + .build()); } DataSource dataSource = new SimpleDataSource(actualList.iterator()); request.setAttribute(DataSource.class.getName(), dataSource); diff --git a/core/src/main/java/com/exadel/aem/toolkit/core/lists/utils/ListHelper.java b/core/src/main/java/com/exadel/aem/toolkit/core/lists/utils/ListHelper.java index e05f5fed9..0deed855b 100644 --- a/core/src/main/java/com/exadel/aem/toolkit/core/lists/utils/ListHelper.java +++ b/core/src/main/java/com/exadel/aem/toolkit/core/lists/utils/ListHelper.java @@ -45,6 +45,7 @@ import com.exadel.aem.toolkit.core.CoreConstants; import com.exadel.aem.toolkit.core.lists.models.SimpleListItem; +import com.exadel.aem.toolkit.core.utils.ResourceFactory; /** * Contains methods for manipulating EToolbox Lists @@ -216,7 +217,7 @@ public static Page createList( List resources = values.stream() .map(mapping) .filter(Objects::nonNull) - .map(properties -> ListResourceUtil.createValueMapResource(resourceResolver, properties)) + .map(properties -> ResourceFactory.newResource(resourceResolver).properties(properties).build()) .collect(Collectors.toList()); return createResourceList(resourceResolver, path, resources); @@ -245,7 +246,7 @@ public static Page createList( reportNoItems(path); } - List resources = ListResourceUtil.mapToValueMapResources(resourceResolver, values); + List resources = ListResourceUtil.mapToResources(resourceResolver, values); return createResourceList(resourceResolver, path, resources); } diff --git a/core/src/main/java/com/exadel/aem/toolkit/core/lists/utils/ListResourceUtil.java b/core/src/main/java/com/exadel/aem/toolkit/core/lists/utils/ListResourceUtil.java index 5b5abcc02..9a5c75ec4 100644 --- a/core/src/main/java/com/exadel/aem/toolkit/core/lists/utils/ListResourceUtil.java +++ b/core/src/main/java/com/exadel/aem/toolkit/core/lists/utils/ListResourceUtil.java @@ -21,20 +21,18 @@ import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.ClassUtils; -import org.apache.commons.lang3.StringUtils; 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 org.apache.sling.api.wrappers.ValueMapDecorator; import org.apache.sling.jcr.resource.api.JcrResourceConstants; import com.day.cq.commons.jcr.JcrConstants; -import com.adobe.granite.ui.components.ds.ValueMapResource; import com.exadel.aem.toolkit.core.CoreConstants; import com.exadel.aem.toolkit.core.lists.ListConstants; import com.exadel.aem.toolkit.core.lists.models.SimpleListItem; import com.exadel.aem.toolkit.core.utils.ObjectConversionUtil; +import com.exadel.aem.toolkit.core.utils.ResourceFactory; import com.exadel.aem.toolkit.core.utils.ValueMapUtil; /** @@ -71,47 +69,26 @@ public static void createListItem( } /** - * Creates a {@link ValueMapResource} representation of a list entry using the provided {@code title} and {@code - * value} - * @param resourceResolver Sling {@link ResourceResolver} instance used to create the list - * @param title String value representing the title of the list entry - * @param value String value representing the value of the list entry - * @return {@link ValueMapResource} object - */ - public static Resource createValueMapResource(ResourceResolver resourceResolver, String title, Object value) { - Map properties = new HashMap<>(); - properties.put(JcrConstants.JCR_TITLE, title); - properties.put(CoreConstants.PN_VALUE, value); - return createValueMapResource(resourceResolver, properties); - } - - /** - * Creates a {@link ValueMapResource} representation of a list entry using the provided properties - * @param resourceResolver Sling {@link ResourceResolver} instance used to create the list - * @param properties Resource properties - * @return {@link ValueMapResource} - */ - public static Resource createValueMapResource(ResourceResolver resourceResolver, Map properties) { - return new ValueMapResource(resourceResolver, StringUtils.EMPTY, JcrConstants.NT_UNSTRUCTURED, new ValueMapDecorator(properties)); - } - - /** - * Converts a key-value map to the list of {@link ValueMapResource} objects - * @param resourceResolver Sling {@link ResourceResolver} instance used to create the list - * @param values {@code Map} instance that will be converted to the {@link ValueMapResource} - * @return List of {@link ValueMapResource} objects + * Converts a key-value map to the list of resources representing list items + * @param resourceResolver {@link ResourceResolver} instance used to create the list + * @param values {@code Map} instance that will be converted to the list of resources + * @return List of {@link Resource} objects */ - public static List mapToValueMapResources(ResourceResolver resourceResolver, Map values) { + public static List mapToResources(ResourceResolver resourceResolver, Map values) { return MapUtils.emptyIfNull(values) .entrySet() .stream() - .map(entry -> createValueMapResource(resourceResolver, entry.getKey(), entry.getValue())) + .map(entry -> ResourceFactory + .newResource(resourceResolver) + .property(JcrConstants.JCR_TITLE, entry.getKey()) + .property(CoreConstants.PN_VALUE, entry.getValue()) + .build()) .collect(Collectors.toList()); } /** * Returns a {@code BiFunction} representing the conversion of a Sling model instance into a {@code Map} that can - * further be used for creating a {@link ValueMapResource} + * further be used for creating a list item resource * @param modelType Type of the Sling model * @return {@code BiFunction}. */ diff --git a/core/src/main/java/com/exadel/aem/toolkit/core/optionprovider/services/impl/resolvers/ConstantsResolverHelper.java b/core/src/main/java/com/exadel/aem/toolkit/core/optionprovider/services/impl/resolvers/ConstantsResolverHelper.java index f53dce7e9..55d14a6cb 100644 --- a/core/src/main/java/com/exadel/aem/toolkit/core/optionprovider/services/impl/resolvers/ConstantsResolverHelper.java +++ b/core/src/main/java/com/exadel/aem/toolkit/core/optionprovider/services/impl/resolvers/ConstantsResolverHelper.java @@ -17,7 +17,7 @@ import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -28,17 +28,15 @@ import org.apache.commons.lang3.tuple.Pair; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.resource.Resource; -import org.apache.sling.api.resource.ValueMap; -import org.apache.sling.api.wrappers.ValueMapDecorator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.day.cq.commons.jcr.JcrConstants; -import com.adobe.granite.ui.components.ds.ValueMapResource; import com.exadel.aem.toolkit.core.CoreConstants; import com.exadel.aem.toolkit.core.optionprovider.OptionProviderConstants; import com.exadel.aem.toolkit.core.optionprovider.services.impl.PathParameters; import com.exadel.aem.toolkit.core.optionprovider.utils.PatternUtil; +import com.exadel.aem.toolkit.core.utils.ResourceFactory; /** * Invoked by {@link ClassOptionSourceResolver} to convert a Java class containing constants into an options data @@ -67,42 +65,43 @@ class ConstantsResolverHelper { * @return A non-null {@code Resource} object */ Resource resolve(SlingHttpServletRequest request) { - List individualFieldValueMaps = Arrays.stream(source.getFields()) + List> individualFieldValueMaps = Arrays.stream(source.getFields()) .filter(field -> Modifier.isPublic(field.getModifiers()) && Modifier.isStatic(field.getModifiers())) - .map(field -> new ValueMapBuilder() - .put(OptionProviderConstants.PARAMETER_NAME, field.getName()) - .put(JcrConstants.JCR_TITLE, field.getName()) - .put(CoreConstants.PN_VALUE, getFieldInvocationResult(field)) - .build()) + .map(field -> { + Map map = new HashMap<>(); + map.put(OptionProviderConstants.PARAMETER_NAME, field.getName()); + map.put(JcrConstants.JCR_TITLE, field.getName()); + map.put(CoreConstants.PN_VALUE, getFieldInvocationResult(field)); + return map; + }) .collect(Collectors.toList()); - List pairedValueMaps = reduce(individualFieldValueMaps, pathParameters); + List> pairedValueMaps = reduce(individualFieldValueMaps, pathParameters); List dataSourceOptions = pairedValueMaps .stream() - .map(valueMap -> new ValueMapResource( - request.getResourceResolver(), - valueMap.get(OptionProviderConstants.PARAMETER_NAME, String.class), - JcrConstants.NT_UNSTRUCTURED, - valueMap)) + .map(valueMap -> ResourceFactory + .newResource(request.getResourceResolver()) + .path(valueMap.getOrDefault(OptionProviderConstants.PARAMETER_NAME, StringUtils.EMPTY).toString()) + .properties(valueMap) + .build()) .collect(Collectors.toList()); - - return new ValueMapResource( - request.getResourceResolver(), - StringUtils.EMPTY, - JcrConstants.NT_UNSTRUCTURED, - new ValueMapDecorator(Collections.emptyMap()), - dataSourceOptions); + return ResourceFactory.newResource(request.getResourceResolver()) + .children(dataSourceOptions) + .build(); } /** - * Compacts the provided list of {@link Resource}s that represent separate Java class constants. We search among the + * Compacts the provided list of {@code Map}s that represent separate Java class constants. We search among the * value maps for every pair that refer to the same logical item (like {@code COLOR_LABEL} and {@code COLOR_VALUE}) * and merge it into a single value map that contains both title and value - * @param individualMaps Collection of {@code Resource} instances representing constants + * @param individualMaps Collection of {@code Map} instances representing constants * @param pathParameters {@link PathParameters} object that is used to modify the list of options * @return A reduced list of value maps */ - private static List reduce(List individualMaps, PathParameters pathParameters) { + private static List> reduce( + List> individualMaps, + PathParameters pathParameters) { + if (!PatternUtil.isPattern(pathParameters.getTextMember()) || !PatternUtil.isPattern(pathParameters.getValueMember()) || IterableUtils.isEmpty(individualMaps)) { @@ -110,39 +109,44 @@ private static List reduce(List individualMaps, PathParamete } Map> nameTextEntries = individualMaps .stream() - .filter(valueMap -> PatternUtil.isMatch(valueMap.get(JcrConstants.JCR_TITLE, String.class), pathParameters.getTextMember())) + .filter(valueMap -> PatternUtil.isMatch( + valueMap.getOrDefault(JcrConstants.JCR_TITLE, StringUtils.EMPTY).toString(), + pathParameters.getTextMember())) .collect(Collectors.toMap( valueMap -> PatternUtil.strip( - valueMap.get(JcrConstants.JCR_TITLE, String.class), + valueMap.getOrDefault(JcrConstants.JCR_TITLE, StringUtils.EMPTY).toString(), pathParameters.getTextMember()), valueMap -> Pair.of( - valueMap.get(OptionProviderConstants.PARAMETER_NAME, String.class), - valueMap.get(CoreConstants.PN_VALUE, String.class)), + valueMap.getOrDefault(OptionProviderConstants.PARAMETER_NAME, StringUtils.EMPTY).toString(), + valueMap.getOrDefault(CoreConstants.PN_VALUE, StringUtils.EMPTY).toString()), (first, second) -> first, LinkedHashMap::new)); Map valueEntries = individualMaps .stream() - .filter(valueMap -> PatternUtil.isMatch(valueMap.get(JcrConstants.JCR_TITLE, String.class), pathParameters.getValueMember())) + .filter(valueMap -> PatternUtil.isMatch( + valueMap.getOrDefault(JcrConstants.JCR_TITLE, StringUtils.EMPTY).toString(), + pathParameters.getValueMember())) .collect(Collectors.toMap( - valueMap -> PatternUtil.strip(valueMap.get(JcrConstants.JCR_TITLE, String.class), pathParameters.getValueMember()), - valueMap -> valueMap.get(CoreConstants.PN_VALUE, StringUtils.EMPTY))); + valueMap -> PatternUtil.strip( + valueMap.getOrDefault(JcrConstants.JCR_TITLE, StringUtils.EMPTY).toString(), + pathParameters.getValueMember()), + valueMap -> valueMap.getOrDefault(CoreConstants.PN_VALUE, StringUtils.EMPTY).toString())); - List result = new ArrayList<>(); + List> result = new ArrayList<>(); for (Map.Entry> textEntry : nameTextEntries.entrySet()) { Object value = valueEntries.get(textEntry.getKey()); if (value == null) { continue; } Pair nameAndText = textEntry.getValue(); - ValueMap valueMap = new ValueMapBuilder() - .put( + Map map = new HashMap<>(); + map.put( OptionProviderConstants.PARAMETER_NAME, - PatternUtil.strip(nameAndText.getLeft(), pathParameters.getTextMember())) - .put(pathParameters.getTextMember(), nameAndText.getRight()) - .put(pathParameters.getValueMember(), value) - .build(); - result.add(valueMap); + PatternUtil.strip(nameAndText.getLeft(), pathParameters.getTextMember())); + map.put(pathParameters.getTextMember(), nameAndText.getRight()); + map.put(pathParameters.getValueMember(), value); + result.add(map); } return result; } diff --git a/core/src/main/java/com/exadel/aem/toolkit/core/optionprovider/services/impl/resolvers/EnumResolverHelper.java b/core/src/main/java/com/exadel/aem/toolkit/core/optionprovider/services/impl/resolvers/EnumResolverHelper.java index 8f977c89e..fbd017f60 100644 --- a/core/src/main/java/com/exadel/aem/toolkit/core/optionprovider/services/impl/resolvers/EnumResolverHelper.java +++ b/core/src/main/java/com/exadel/aem/toolkit/core/optionprovider/services/impl/resolvers/EnumResolverHelper.java @@ -19,7 +19,6 @@ import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -27,15 +26,13 @@ import org.apache.commons.lang3.StringUtils; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.resource.Resource; -import org.apache.sling.api.resource.ValueMap; -import org.apache.sling.api.wrappers.ValueMapDecorator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.day.cq.commons.jcr.JcrConstants; -import com.adobe.granite.ui.components.ds.ValueMapResource; import com.exadel.aem.toolkit.core.CoreConstants; import com.exadel.aem.toolkit.core.optionprovider.OptionProviderConstants; +import com.exadel.aem.toolkit.core.utils.ResourceFactory; /** * Invoked by {@link ClassOptionSourceResolver} to convert a Java enum into an options data source @@ -64,23 +61,21 @@ class EnumResolverHelper { Resource resolve(SlingHttpServletRequest request) { List children = new ArrayList<>(); for (Object enumConstant : source.getEnumConstants()) { - ValueMap valueMap = new ValueMapDecorator(buildPropertyMap(enumConstant)); - children.add(new ValueMapResource( - request.getResourceResolver(), - valueMap.get(OptionProviderConstants.PARAMETER_NAME, String.class), - JcrConstants.NT_UNSTRUCTURED, - new ValueMapDecorator(buildPropertyMap(enumConstant)))); + Map valueMap = buildPropertyMap(enumConstant); + children.add(ResourceFactory + .newResource(request.getResourceResolver()) + .path(valueMap.getOrDefault(OptionProviderConstants.PARAMETER_NAME, StringUtils.EMPTY).toString()) + .properties(valueMap) + .build()); } - return new ValueMapResource( - request.getResourceResolver(), - StringUtils.EMPTY, - JcrConstants.NT_UNSTRUCTURED, - new ValueMapDecorator(Collections.emptyMap()), - children); + return ResourceFactory + .newResource(request.getResourceResolver()) + .children(children) + .build(); } /** - * Creates a {@link ValueMap} instance representing a single data source option for the given enum constant + * Creates a {@code Map} instance representing a single data source option for the given enum constant * @param enumConstant An enum object * @return {@link Map} object */ diff --git a/core/src/main/java/com/exadel/aem/toolkit/core/optionprovider/services/impl/resolvers/HttpOptionSourceResolver.java b/core/src/main/java/com/exadel/aem/toolkit/core/optionprovider/services/impl/resolvers/HttpOptionSourceResolver.java index c88f16c37..f8fac650a 100644 --- a/core/src/main/java/com/exadel/aem/toolkit/core/optionprovider/services/impl/resolvers/HttpOptionSourceResolver.java +++ b/core/src/main/java/com/exadel/aem/toolkit/core/optionprovider/services/impl/resolvers/HttpOptionSourceResolver.java @@ -20,7 +20,6 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Base64; -import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -43,17 +42,14 @@ import org.apache.http.util.EntityUtils; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.resource.Resource; -import org.apache.sling.api.resource.ValueMap; -import org.apache.sling.api.wrappers.ValueMapDecorator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.day.cq.commons.jcr.JcrConstants; -import com.adobe.granite.ui.components.ds.ValueMapResource; import com.fasterxml.jackson.databind.JsonNode; import com.exadel.aem.toolkit.core.CoreConstants; import com.exadel.aem.toolkit.core.optionprovider.services.impl.PathParameters; import com.exadel.aem.toolkit.core.utils.ObjectConversionUtil; +import com.exadel.aem.toolkit.core.utils.ResourceFactory; /** * Implements {@link OptionSourceResolver} to facilitate extracting option data sources from HTTP endpoints @@ -212,12 +208,10 @@ private static Resource createResource(SlingHttpServletRequest request, String p if (!nextField.getValue().isObject()) { continue; } - ValueMap valueMap = createValueMap(nextField.getValue()); - Resource resource = new ValueMapResource( - request.getResourceResolver(), - path + CoreConstants.SEPARATOR_SLASH + nextField.getKey(), - JcrConstants.NT_UNSTRUCTURED, - valueMap); + Resource resource = ResourceFactory.newResource(request.getResourceResolver()) + .path(path, nextField.getKey()) + .properties(createPropertiesMap(nextField.getValue())) + .build(); children.add(resource); } } else if (node.isArray()) { @@ -228,34 +222,29 @@ private static Resource createResource(SlingHttpServletRequest request, String p if (!nextElement.isObject()) { continue; } - ValueMap valueMap = createValueMap(nextElement); - Resource resource = new ValueMapResource( - request.getResourceResolver(), - path + CoreConstants.SEPARATOR_SLASH + CoreConstants.NN_ITEM + elementIndex++, - JcrConstants.NT_UNSTRUCTURED, - valueMap); + Resource resource = ResourceFactory.newResource(request.getResourceResolver()) + .path(path, CoreConstants.NN_ITEM + elementIndex++) + .properties(createPropertiesMap(nextElement)) + .build(); children.add(resource); } } - return new ValueMapResource( - request.getResourceResolver(), - path, - JcrConstants.NT_UNSTRUCTURED, - new ValueMapDecorator(Collections.emptyMap()), - children); + return ResourceFactory.newResource(request.getResourceResolver()) + .path(path) + .children(children) + .build(); } /** * Called by {@link HttpOptionSourceResolver#createResource(SlingHttpServletRequest, String, JsonNode)} to convert a - * particular {@link JsonNode} into a {@code ValueMap} containing all the keys and values contained in the node + * particular {@link JsonNode} into a {@code Map} containing all the keys and values contained in the node * @param jsonNode {@link JsonNode} object containing values for the value map - * @return {@link ValueMap} object + * @return {@code Map} object */ - private static ValueMap createValueMap(JsonNode jsonNode) { - Map sourceMap = StreamSupport + private static Map createPropertiesMap(JsonNode jsonNode) { + return StreamSupport .stream(Spliterators.spliteratorUnknownSize(jsonNode.fields(), Spliterator.ORDERED), false) .collect(Collectors.toMap(Map.Entry::getKey, field -> field.getValue().asText())); - return new ValueMapDecorator(sourceMap); } /* ---------------- diff --git a/core/src/main/java/com/exadel/aem/toolkit/core/optionprovider/services/impl/resolvers/InlineOptionSourceResolver.java b/core/src/main/java/com/exadel/aem/toolkit/core/optionprovider/services/impl/resolvers/InlineOptionSourceResolver.java index 7d5716344..88efc0b78 100644 --- a/core/src/main/java/com/exadel/aem/toolkit/core/optionprovider/services/impl/resolvers/InlineOptionSourceResolver.java +++ b/core/src/main/java/com/exadel/aem/toolkit/core/optionprovider/services/impl/resolvers/InlineOptionSourceResolver.java @@ -15,19 +15,16 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.resource.Resource; -import org.apache.sling.api.resource.ValueMap; -import org.apache.sling.api.wrappers.ValueMapDecorator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.day.cq.commons.jcr.JcrConstants; -import com.adobe.granite.ui.components.ds.ValueMapResource; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -35,6 +32,7 @@ import com.exadel.aem.toolkit.core.optionprovider.OptionProviderConstants; import com.exadel.aem.toolkit.core.optionprovider.services.impl.PathParameters; import com.exadel.aem.toolkit.core.utils.ObjectConversionUtil; +import com.exadel.aem.toolkit.core.utils.ResourceFactory; /** * Implements {@link OptionSourceResolver} to transfer the directly provided name-value pairs into the options data @@ -65,28 +63,24 @@ public Resource resolve(SlingHttpServletRequest request, PathParameters params) if (!(node instanceof ObjectNode)) { continue; } - ValueMapBuilder valueMapBuilder = new ValueMapBuilder(); + Map properties = new HashMap<>(); for (Iterator propertyNames = node.fieldNames(); propertyNames.hasNext();) { String propertyName = propertyNames.next(); String propertyValue = node.get(propertyName).asText(); - valueMapBuilder.put(propertyName, propertyValue); + properties.put(propertyName, propertyValue); if (propertyName.equals(params.getTextMember()) && StringUtils.isNotEmpty(propertyValue)) { - valueMapBuilder.put(OptionProviderConstants.PARAMETER_NAME, propertyValue); + properties.put(OptionProviderConstants.PARAMETER_NAME, propertyValue); } } - ValueMap valueMap = valueMapBuilder.build(); - Resource child = new ValueMapResource( - request.getResourceResolver(), - valueMap.get(OptionProviderConstants.PARAMETER_NAME, StringUtils.EMPTY), - JcrConstants.NT_UNSTRUCTURED, - valueMap); + Resource child = ResourceFactory.newResource(request.getResourceResolver()) + .path(properties.getOrDefault(OptionProviderConstants.PARAMETER_NAME, StringUtils.EMPTY).toString()) + .properties(properties) + .build(); children.add(child); } - return new ValueMapResource( - request.getResourceResolver(), - StringUtils.EMPTY, - JcrConstants.NT_UNSTRUCTURED, - new ValueMapDecorator(Collections.emptyMap()), - children); + return ResourceFactory + .newResource(request.getResourceResolver()) + .children(children) + .build(); } } diff --git a/core/src/main/java/com/exadel/aem/toolkit/core/optionprovider/services/impl/resolvers/ValueMapBuilder.java b/core/src/main/java/com/exadel/aem/toolkit/core/optionprovider/services/impl/resolvers/ValueMapBuilder.java deleted file mode 100644 index ed040253c..000000000 --- a/core/src/main/java/com/exadel/aem/toolkit/core/optionprovider/services/impl/resolvers/ValueMapBuilder.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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. - */ -package com.exadel.aem.toolkit.core.optionprovider.services.impl.resolvers; - -import java.util.HashMap; -import java.util.Map; - -import org.apache.sling.api.resource.ValueMap; -import org.apache.sling.api.wrappers.ValueMapDecorator; - -/** - * Creates an instance of {@link ValueMap} via separate value assignments - */ -class ValueMapBuilder { - - private final Map propertyMap; - - /** - * Default constructor - */ - ValueMapBuilder() { - propertyMap = new HashMap<>(); - } - - /** - * Assigns a new key-value pair to the current builder - * @param key Arbitrary string value; a non-empty string is expected - * @param value Arbitrary object - * @return This builder - */ - public ValueMapBuilder put(String key, Object value) { - propertyMap.put(key, value); - return this; - } - - /** - * Completes the builder - * @return {@link ValueMap} instance containing the provided data - */ - public ValueMap build() { - return new ValueMapDecorator(propertyMap); - } -} diff --git a/core/src/main/java/com/exadel/aem/toolkit/core/utils/ResourceFactory.java b/core/src/main/java/com/exadel/aem/toolkit/core/utils/ResourceFactory.java new file mode 100644 index 000000000..b9a5256d2 --- /dev/null +++ b/core/src/main/java/com/exadel/aem/toolkit/core/utils/ResourceFactory.java @@ -0,0 +1,408 @@ +/* + * 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. + */ +package com.exadel.aem.toolkit.core.utils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.api.wrappers.ValueMapDecorator; +import org.jetbrains.annotations.NotNull; +import com.day.cq.commons.jcr.JcrConstants; +import com.adobe.granite.ui.components.ds.ValueMapResource; + +import com.exadel.aem.toolkit.api.annotations.meta.ResourceTypes; +import com.exadel.aem.toolkit.core.CoreConstants; + +/** + * Utility class to create {@code Resource} objects with a fluent API + *

Note: This class is not a part of the public API and is subject to change. Do not use it in your own code

+ */ +public class ResourceFactory { + + private static final String KEY_FIELD_COUNT = ResourceFactory.class.getName() + ".count"; + + /** + * Default (instantiation-restricting) constructor + */ + private ResourceFactory() { + } + + /** + * Creates a new {@link Builder} instance for constructing a Sling resource with given features + * @param resolver The {@code ResourceResolver} to be used for resource creation + * @return A {@link Builder} instance for fluent resource creation + */ + public static Builder newResource(@NotNull ResourceResolver resolver) { + return new Builder<>(resolver, Builder.class); + } + + /** + * Creates a new {@link FieldBuilder} instance for constructing a Granite field resource with given features + * @param request The {@code SlingHttpServletRequest} that serves as the context for field creation + * @return A {@link FieldBuilder} instance for fluent field creation + */ + public static FieldBuilder newGraniteField(@NotNull SlingHttpServletRequest request) { + return new FieldBuilder(request.getResourceResolver()) + .path(request.getResource().getPath(), CoreConstants.NN_FIELD + getAndIncrementFieldCount(request)); + } + + /** + * Retrieves and increments the field count stored in the request attribute + * @param request The {@code SlingHttpServletRequest} that serves as the context for field creation + * @return The zero-based field count so far for the current request, which can be used to generate unique field paths + */ + private static int getAndIncrementFieldCount(SlingHttpServletRequest request) { + int fieldCount = request.getAttribute(KEY_FIELD_COUNT) != null + ? (int) request.getAttribute(KEY_FIELD_COUNT) + : 0; + request.setAttribute(KEY_FIELD_COUNT, fieldCount + 1); + return fieldCount; + } + + /** + * Builder class for creating {@link Resource} instances with a fluent API + * @param The type of the builder + */ + public static class Builder> { + + private final ResourceResolver resolver; + private final Class builderClass; + + private List children; + private String path; + private Map properties; + private String resourceType = JcrConstants.NT_UNSTRUCTURED; + + /** + * Constructs a new Builder instance + * @param resolver The {@link ResourceResolver} to be used for resource creation + * @param type The class type of the builder + */ + Builder(ResourceResolver resolver, Class type) { + this.resolver = resolver; + this.builderClass = type; + } + + // Builder methods + + /** + * Assigns child resources to the resource being built + * @param value The collection of child resources + * @return This builder instance + */ + public T children(Collection value) { + if (CollectionUtils.isEmpty(value)) { + return builderClass.cast(this); + } + value.forEach(this::child); + return builderClass.cast(this); + } + + /** + * Assigns a single child resource to the resource being built + * @param value The child resource + * @return This builder instance + */ + public T child(Resource value) { + if (value == null) { + return builderClass.cast(this); + } + if (children == null) { + children = new ArrayList<>(); + } + children.add(value); + return builderClass.cast(this); + } + + /** + * Assigns a path to the resource being built + * @param value The path segments + * @return This builder instance + */ + public T path(String... value) { + if (ArrayUtils.isEmpty(value)) { + return builderClass.cast(this); + } + String effectivePath = Stream.of(value) + .map(v -> StringUtils.strip(v, CoreConstants.SEPARATOR_SLASH)) + .filter(StringUtils::isNotEmpty) + .collect(Collectors.joining(CoreConstants.SEPARATOR_SLASH)); + if (StringUtils.isNotEmpty(effectivePath)) { + path = effectivePath; + } + return builderClass.cast(this); + } + + /** + * Assigns multiple properties to the resource being built + * @param value The map of properties + * @return This builder instance + */ + public T properties(Map value) { + if (MapUtils.isEmpty(value)) { + return builderClass.cast(this); + } + value.forEach(this::property); + return builderClass.cast(this); + } + + /** + * Assigns a single property to the resource being built + * @param name The property name + * @param value The property value + * @return This builder instance + */ + public T property(String name, Object value) { + if (StringUtils.isEmpty(name) || value == null) { + return builderClass.cast(this); + } + if (properties == null) { + properties = new HashMap<>(); + } + properties.put(name, value); + return builderClass.cast(this); + } + + /** + * Assigns a Sling resource type to the resource being built + * @param value The resource type + * @return This builder instance + */ + public T resourceType(String value) { + if (StringUtils.isNotEmpty(value)) { + this.resourceType = value; + } + return builderClass.cast(this); + } + + /** + * Builds the {@link Resource} instance based on the provided parameters + * @return The built resource instance + */ + public Resource build() { + String effectiveMainPath = StringUtils.defaultString(path); + if (properties == null + || properties.keySet().stream().noneMatch(k -> StringUtils.contains(k, CoreConstants.SEPARATOR_SLASH))) { + return new ValueMapResource( + resolver, + effectiveMainPath, + resourceType, + new ValueMapDecorator(MapUtils.emptyIfNull(properties)), + children); + } + Map> derivedValueMaps = extractDerivedValueMaps(properties); + Map derivedResources = new HashMap<>(); + for (Map.Entry> entry : derivedValueMaps.entrySet()) { + String relativePath = entry.getKey(); + boolean isMainResource = relativePath.isEmpty(); + Map derivedValueMap = entry.getValue(); + List contextualChildren = derivedResources.entrySet().stream() + .filter(e -> !StringUtils.equals(e.getKey(), relativePath)) + .filter(e -> { + if (relativePath.isEmpty()) { + return !StringUtils.contains(e.getKey(), CoreConstants.SEPARATOR_SLASH); + } + return StringUtils.startsWith(e.getKey(), relativePath + CoreConstants.SEPARATOR_SLASH) + && !e.getKey().substring(relativePath.length() + 1).contains(CoreConstants.SEPARATOR_SLASH); + }) + .map(Map.Entry::getValue) + .collect(Collectors.toCollection(ArrayList::new)); + if (isMainResource && CollectionUtils.isNotEmpty(children)) { + contextualChildren.addAll(children); + } + Resource resource = new ValueMapResource( + resolver, + effectiveMainPath + + (isMainResource ? StringUtils.EMPTY : CoreConstants.SEPARATOR_SLASH) + + relativePath, + isMainResource ? resourceType : JcrConstants.NT_UNSTRUCTURED, + new ValueMapDecorator(derivedValueMap), + contextualChildren); + derivedResources.put(relativePath, resource); + } + return derivedResources.get(StringUtils.EMPTY); + } + + // Accessors + + /** + * Retrieves the path assigned to the resource being built + * @return String value + */ + public String getPath() { + return path; + } + + /** + * Retrieves the child resources assigned to the resource being built + * @return List of resources + */ + List getChildren() { + return children; + } + + /** + * Retrieves the properties assigned to the resource being built + * @return Map of properties + */ + Map getProperties() { + return properties; + } + + /** + * Retrieves the resolver assigned to the resource being built + * @return ResourceResolver instance + */ + ResourceResolver getResolver() { + return resolver; + } + + /** + * Retrieves the resource type assigned to the resource being built + * @return String value + */ + String getResourceType() { + return resourceType; + } + + /** + * Extracts value maps representing either the current resource or its children from the provided properties map + * based on the presence of path separators in the keys + * @param properties Map of properties + * @return Map of derived value maps + */ + private static Map> extractDerivedValueMaps(Map properties) { + Map> result = new TreeMap<>(Builder::compareByPathDepth); + properties.forEach((key, value) -> { + String parentPath = StringUtils.contains(key, CoreConstants.SEPARATOR_SLASH) + ? StringUtils.substringBeforeLast(key, CoreConstants.SEPARATOR_SLASH) + : StringUtils.EMPTY; + String propertyName = parentPath.isEmpty() + ? key + : StringUtils.substringAfterLast(key, CoreConstants.SEPARATOR_SLASH); + result.computeIfAbsent(parentPath, k -> new HashMap<>()).put(propertyName, value); + while (parentPath.contains(CoreConstants.SEPARATOR_SLASH)) { + parentPath = StringUtils.substringBeforeLast(parentPath, CoreConstants.SEPARATOR_SLASH); + result.computeIfAbsent(parentPath, k -> new HashMap<>()); + } + }); + // Ensure the main resource is represented in the result map even if it has no properties + result.computeIfAbsent(StringUtils.EMPTY, k -> new HashMap<>()); + return result; + } + + /** + * Comparator method to sort resource paths by their depth in descending order. Used to build a virtual resource + * hierarchy starting from the deepest nodes + * @param first First resource path + * @param second Second resource path + * @return Comparison result + */ + private static int compareByPathDepth(String first, String second) { + int firstDepth = StringUtils.countMatches(first, CoreConstants.SEPARATOR_SLASH); + int secondDepth = StringUtils.countMatches(second, CoreConstants.SEPARATOR_SLASH); + if (firstDepth != secondDepth) { + return Integer.compare(secondDepth, firstDepth); + } + if (StringUtils.isEmpty(first) && !StringUtils.isEmpty(second)) { + return 1; + } else if (StringUtils.isEmpty(second) && !StringUtils.isEmpty(first)) { + return -1; + } + return StringUtils.compare(first, second); + } + } + + /** + * Extends the generic {@link Builder} class for creating Granite UI field resources with a fluent API + */ + public static class FieldBuilder extends Builder { + private boolean isMultiValue; + + /** + * Constructs a new FieldBuilder instance + * @param resolver The {@link ResourceResolver} to be used for field resource creation + */ + FieldBuilder(ResourceResolver resolver) { + super(resolver, FieldBuilder.class); + } + + /** + * Assigns a Granite UI data attribute to the field being built + * @param key The key of the data attribute + * @param value The value of the data attribute + * @return The current FieldBuilder instance + */ + public FieldBuilder graniteData(String key, Object value) { + if (StringUtils.isEmpty(key) || value == null) { + return this; + } + return property(CoreConstants.NN_GRANITE_DATA + CoreConstants.SEPARATOR_SLASH + key, value); + } + + /** + * Designates the field being built as a multifield + * @param value True or false + * @return The current FieldBuilder instance + */ + public FieldBuilder multi(boolean value) { + isMultiValue = value; + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public Resource build() { + if (!isMultiValue) { + return super.build(); + } + Map wrapperValueMap = new HashMap<>(); + wrapperValueMap.put( + CoreConstants.PN_FIELD_LABEL, + MapUtils.emptyIfNull(getProperties()).get(CoreConstants.PN_FIELD_LABEL)); + if (ResourceTypes.CONTAINER.equals(getResourceType())) { + wrapperValueMap.put(CoreConstants.PN_COMPOSITE, true); + } + Map nestedValueMap = new HashMap<>(MapUtils.emptyIfNull(getProperties())); + nestedValueMap.remove(CoreConstants.PN_FIELD_LABEL); + Resource nestedField = ResourceFactory.newResource(getResolver()) + .path(getPath(), CoreConstants.NN_FIELD) + .resourceType(getResourceType()) + .properties(nestedValueMap) + .children(getChildren()) + .build(); + return ResourceFactory.newResource(getResolver()) + .path(getPath()) + .resourceType(ResourceTypes.MULTIFIELD) + .properties(wrapperValueMap) + .child(nestedField) + .build(); + } + } +} diff --git a/core/src/test/java/com/exadel/aem/toolkit/AllTests.java b/core/src/test/java/com/exadel/aem/toolkit/AllTests.java index 865b75bde..b6135d35c 100644 --- a/core/src/test/java/com/exadel/aem/toolkit/AllTests.java +++ b/core/src/test/java/com/exadel/aem/toolkit/AllTests.java @@ -49,6 +49,7 @@ import com.exadel.aem.toolkit.core.optionprovider.services.impl.resolvers.OptionProviderInlineOptionsTest; import com.exadel.aem.toolkit.core.optionprovider.services.impl.resolvers.OptionProviderTest; import com.exadel.aem.toolkit.core.policymanagement.filters.TopLevelPolicyFilterTest; +import com.exadel.aem.toolkit.core.utils.ResourceFactoryTest; /** * Shortcut class for running all available test cases in a batch @@ -92,7 +93,9 @@ PermissionUtilTest.class, ValueUtilTest.class, - TopLevelPolicyFilterTest.class + TopLevelPolicyFilterTest.class, + + ResourceFactoryTest.class }) public class AllTests { } diff --git a/core/src/test/java/com/exadel/aem/toolkit/core/configurator/models/internal/RenderConditionTest.java b/core/src/test/java/com/exadel/aem/toolkit/core/configurator/models/internal/RenderConditionTest.java index ea9f8d5f3..72a2fe481 100644 --- a/core/src/test/java/com/exadel/aem/toolkit/core/configurator/models/internal/RenderConditionTest.java +++ b/core/src/test/java/com/exadel/aem/toolkit/core/configurator/models/internal/RenderConditionTest.java @@ -20,7 +20,6 @@ import javax.jcr.security.AccessControlManager; import javax.jcr.security.Privilege; -import org.apache.commons.lang3.StringUtils; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.resource.ValueMap; @@ -44,7 +43,6 @@ import org.osgi.service.metatype.MetaTypeService; import org.osgi.service.metatype.ObjectClassDefinition; import com.adobe.granite.ui.components.ExpressionResolver; -import com.adobe.granite.ui.components.ds.ValueMapResource; import com.adobe.granite.ui.components.rendercondition.SimpleRenderCondition; import io.wcm.testing.mock.aem.junit.AemContext; import static org.junit.Assert.assertEquals; @@ -55,6 +53,7 @@ import com.exadel.aem.toolkit.core.CoreConstants; import com.exadel.aem.toolkit.core.configurator.ConfiguratorConstants; import com.exadel.aem.toolkit.core.configurator.services.ConfigChangeListener; +import com.exadel.aem.toolkit.core.utils.ResourceFactory; @RunWith(MockitoJUnitRunner.class) public class RenderConditionTest { @@ -318,11 +317,10 @@ private void setUpPermissions(boolean global, boolean local, boolean replication private void createAndInvokeRenderCondition(String feature) { ValueMap valueMap = new ValueMapDecorator(Collections.singletonMap("feature", feature)); - Resource resource = new ValueMapResource( - context.resourceResolver(), - StringUtils.EMPTY, - "etoolbox-authoring-kit/configurator/components/rendercondition", - valueMap); + Resource resource = ResourceFactory.newResource(context.resourceResolver()) + .resourceType("etoolbox-authoring-kit/configurator/components/rendercondition") + .properties(valueMap) + .build(); context.request().setResource(resource); RenderCondition model = context.request().adaptTo(RenderCondition.class); } diff --git a/core/src/test/java/com/exadel/aem/toolkit/core/lists/utils/ListResourceUtilTest.java b/core/src/test/java/com/exadel/aem/toolkit/core/lists/utils/ListResourceUtilTest.java index 6b02483c0..063d889e3 100644 --- a/core/src/test/java/com/exadel/aem/toolkit/core/lists/utils/ListResourceUtilTest.java +++ b/core/src/test/java/com/exadel/aem/toolkit/core/lists/utils/ListResourceUtilTest.java @@ -38,7 +38,7 @@ public class ListResourceUtilTest { public void shouldTransformKeyValuePairMapToResource() { Map properties = Collections.singletonMap("first", "firstValue"); - List resources = ListResourceUtil.mapToValueMapResources(context.resourceResolver(), properties); + List resources = ListResourceUtil.mapToResources(context.resourceResolver(), properties); Resource resource = resources.get(0); assertNotNull(resource); diff --git a/core/src/test/java/com/exadel/aem/toolkit/core/utils/ResourceFactoryTest.java b/core/src/test/java/com/exadel/aem/toolkit/core/utils/ResourceFactoryTest.java new file mode 100644 index 000000000..17a31e115 --- /dev/null +++ b/core/src/test/java/com/exadel/aem/toolkit/core/utils/ResourceFactoryTest.java @@ -0,0 +1,346 @@ +/* + * 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. + */ +package com.exadel.aem.toolkit.core.utils; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.apache.commons.collections4.IteratorUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.sling.api.resource.Resource; +import org.junit.Rule; +import org.junit.Test; +import com.day.cq.commons.jcr.JcrConstants; +import io.wcm.testing.mock.aem.junit.AemContext; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import com.exadel.aem.toolkit.api.annotations.meta.ResourceTypes; +import com.exadel.aem.toolkit.core.AemContextFactory; +import com.exadel.aem.toolkit.core.CoreConstants; + +public class ResourceFactoryTest { + + @Rule + public final AemContext context = AemContextFactory.newInstance(); + + @Test + public void shouldSetPathAndProperties() { + Resource resource = ResourceFactory.newResource(context.resourceResolver()) + .path("content/test") + .resourceType("acme/components/test") + .property("title", "Hello") + .property("count", 42) + .build(); + + assertNotNull(resource); + assertEquals("content/test", resource.getPath()); + assertEquals("acme/components/test", resource.getResourceType()); + assertEquals("Hello", resource.getValueMap().get("title", String.class)); + assertEquals(42, (int) resource.getValueMap().get("count", 0)); + } + + @Test + public void shouldUseDefaultResourceTypeWhenNoneSet() { + Resource resource = ResourceFactory.newResource(context.resourceResolver()) + .path("content/test") + .build(); + + assertNotNull(resource); + assertEquals(JcrConstants.NT_UNSTRUCTURED, resource.getResourceType()); + } + + @Test + public void shouldIncludeChildren() { + Resource child1 = ResourceFactory + .newResource(context.resourceResolver()) + .path("child1") + .property("key", "val1") + .build(); + Resource child2 = ResourceFactory + .newResource(context.resourceResolver()) + .path("child2") + .property("key", "val2") + .build(); + + Resource parent = ResourceFactory.newResource(context.resourceResolver()) + .path("parent") + .child(child1) + .children(Collections.singletonList(child2)) + .build(); + + assertNotNull(parent); + Iterator children = parent.listChildren(); + assertNotNull(children); + assertTrue(children.hasNext()); + Resource firstChild = children.next(); + assertNotNull(firstChild); + assertEquals("val1", firstChild.getValueMap().get("key", String.class)); + assertTrue(children.hasNext()); + assertEquals("val2", children.next().getValueMap().get("key", String.class)); + assertFalse(children.hasNext()); + } + + @Test + public void shouldAcceptPropertiesMap() { + Map props = new HashMap<>(); + props.put("alpha", "a"); + props.put("beta", "b"); + + Resource resource = ResourceFactory.newResource(context.resourceResolver()) + .path("content/test") + .properties(props) + .build(); + + assertNotNull(resource); + assertEquals("a", resource.getValueMap().get("alpha", String.class)); + assertEquals("b", resource.getValueMap().get("beta", String.class)); + } + + @Test + public void shouldHandleNestedProperties() { + Resource resource = ResourceFactory.newResource(context.resourceResolver()) + .path("content/composite") + .property("title", "Main") + .property("alpha/text", "Nested text") + .property("alpha/value", "Nested value") + .property("beta/text", "More nested text") + .build(); + + assertNotNull(resource); + assertEquals("Main", resource.getValueMap().get("title", String.class)); + + List children = IteratorUtils.toList(resource.listChildren()); + assertNotNull(children); + + Resource alphaChild = children + .stream() + .filter(child -> "alpha".equals(child.getName())) + .findFirst() + .orElse(null); + assertNotNull(alphaChild); + assertEquals("Nested text", alphaChild.getValueMap().get("text", String.class)); + assertEquals("Nested value", alphaChild.getValueMap().get("value", String.class)); + + Resource betaChild = children + .stream() + .filter(child -> "beta".equals(child.getName())) + .findFirst() + .orElse(null); + assertNotNull(betaChild); + assertEquals("More nested text", betaChild.getValueMap().get("text", String.class)); + } + + @Test + public void shouldHandleDeeplyNestedProperties() { + Resource resource = ResourceFactory.newResource(context.resourceResolver()) + .path("content/node") + .property("title", "Root") + .property("l1/l2/l3/prop", "deep") + .build(); + + assertNotNull(resource); + assertEquals("Root", resource.getValueMap().get("title", String.class)); + + Iterator children = resource.listChildren(); + assertNotNull(children); + assertTrue(children.hasNext()); + Resource level1Child = children.next(); + assertNotNull(level1Child); + assertEquals(JcrConstants.NT_UNSTRUCTURED, level1Child.getResourceType()); + + Iterator l1Children = level1Child.listChildren(); + assertNotNull(l1Children); + assertTrue(l1Children.hasNext()); + Resource level2Child = l1Children.next(); + assertNotNull(level2Child); + assertEquals(JcrConstants.NT_UNSTRUCTURED, level2Child.getResourceType()); + + Iterator l2Children = level2Child.listChildren(); + assertNotNull(l2Children); + assertTrue(l2Children.hasNext()); + Resource level3Child = l2Children.next(); + assertNotNull(level3Child); + assertEquals("deep", level3Child.getValueMap().get("prop", String.class)); + } + + @Test + public void shouldNormalizePathSegments() { + Resource resource = ResourceFactory.newResource(context.resourceResolver()) + .path("/content/", "/my-page/", "/node") + .build(); + + assertNotNull(resource); + assertEquals("content/my-page/node", resource.getPath()); + + ResourceFactory.Builder builder = ResourceFactory.newResource(context.resourceResolver()); + builder.path("content", "", "page"); + assertEquals("content/page", builder.getPath()); + + ResourceFactory.Builder allBlankBuilder = ResourceFactory.newResource(context.resourceResolver()); + allBlankBuilder.path("/", "/"); + assertNull(allBlankBuilder.getPath()); + } + + @Test + public void shouldIncludeExternalChildren() { + Resource externalChild = ResourceFactory.newResource(context.resourceResolver()) + .path("extra").property("from", "external").build(); + + Resource resource = ResourceFactory.newResource(context.resourceResolver()) + .path("content/composite") + .property("title", "Main") + .property("items/text", "NestedText") + .child(externalChild) + .build(); + + assertNotNull(resource); + Iterator children = resource.listChildren(); + assertNotNull(children); + assertTrue(children.hasNext()); + children.next(); + assertTrue(children.hasNext()); + children.next(); + assertFalse(children.hasNext()); + } + + @Test + public void shouldIgnoreNullOrEmptyInputsInBuilder() { + ResourceFactory.Builder builder = ResourceFactory.newResource(context.resourceResolver()); + + builder.child(null); + builder.children(null); + builder.children(Collections.emptyList()); + assertNull(builder.getChildren()); + + builder.path(); + assertNull(builder.getPath()); + + builder.property(null, "val"); + builder.property(StringUtils.EMPTY, "val"); + builder.property("key", null); + builder.properties(null); + builder.properties(Collections.emptyMap()); + assertNull(builder.getProperties()); + + builder.resourceType(null); + builder.resourceType(""); + assertEquals(JcrConstants.NT_UNSTRUCTURED, builder.getResourceType()); + } + + @Test + public void shouldBuildMultifieldWrapper() { + context.request().setResource(context.create().resource("/content/myForm")); + + Resource multifield = ResourceFactory + .newGraniteField(context.request()) + .path("content/form/myField") + .resourceType("acme/components/field") + .property(CoreConstants.PN_FIELD_LABEL, "Multi Label") + .property("name", "./myField") + .multi(true) + .build(); + + assertNotNull(multifield); + assertEquals(ResourceTypes.MULTIFIELD, multifield.getResourceType()); + assertEquals("Multi Label", multifield.getValueMap().get(CoreConstants.PN_FIELD_LABEL, String.class)); + + Iterator children = multifield.listChildren(); + assertNotNull(children); + assertTrue(children.hasNext()); + Resource nestedField = children.next(); + assertNotNull(nestedField); + assertEquals("acme/components/field", nestedField.getResourceType()); + } + + @Test + public void shouldBuildCompositeMultifieldWrapper() { + context.request().setResource(context.create().resource("/content/myForm")); + + Resource externalChild = ResourceFactory.newResource(context.resourceResolver()) + .path("external") + .property("from", "external") + .build(); + + Resource multifield = ResourceFactory + .newGraniteField(context.request()) + .path("content/form/compositeField") + .resourceType(ResourceTypes.CONTAINER) + .property(CoreConstants.PN_FIELD_LABEL, "Composite Label") + .multi(true) + .child(externalChild) + .build(); + + assertNotNull(multifield); + assertEquals(ResourceTypes.MULTIFIELD, multifield.getResourceType()); + boolean composite = multifield.getValueMap().get(CoreConstants.PN_COMPOSITE, false); + assertTrue(composite); + + Iterator wrapperChildren = multifield.listChildren(); + assertNotNull(wrapperChildren); + assertTrue(wrapperChildren.hasNext()); + Resource nestedField = wrapperChildren.next(); + assertNotNull(nestedField); + assertNull(nestedField.getValueMap().get(CoreConstants.PN_FIELD_LABEL, String.class)); + + Iterator nestedChildren = nestedField.listChildren(); + assertNotNull(nestedChildren); + assertTrue(nestedChildren.hasNext()); + Resource passedChild = nestedChildren.next(); + assertNotNull(passedChild); + assertEquals("external", passedChild.getValueMap().get("from", String.class)); + } + + @Test + public void shouldIncrementGraniteFieldPaths() { + context.create().resource("/content/myForm"); + context.request().setResource(context.resourceResolver().getResource("/content/myForm")); + + ResourceFactory.FieldBuilder builder1 = ResourceFactory.newGraniteField(context.request()); + ResourceFactory.FieldBuilder builder2 = ResourceFactory.newGraniteField(context.request()); + + assertEquals("content/myForm/field0", builder1.getPath()); + assertEquals("content/myForm/field1", builder2.getPath()); + } + + @Test + public void shouldApplyGraniteData() { + ResourceFactory.FieldBuilder builder = new ResourceFactory.FieldBuilder(context.resourceResolver()); + Resource resource = builder + .path("content/field") + .graniteData("myKey", "myValue") + .build(); + + assertNotNull(resource); + List children = IteratorUtils.toList(resource.listChildren()); + assertNotNull(children); + assertEquals(1, children.size()); + Resource graniteDataNode = children.get(0); + assertNotNull(graniteDataNode); + assertEquals(CoreConstants.NN_GRANITE_DATA, graniteDataNode.getName()); + assertEquals("myValue", graniteDataNode.getValueMap().get("myKey", String.class)); + + ResourceFactory.FieldBuilder ignored = new ResourceFactory.FieldBuilder(context.resourceResolver()); + ignored.graniteData("", "val"); + ignored.graniteData(null, "val"); + ignored.graniteData("key", null); + assertNull(ignored.getProperties()); + } +}