From 44976ffecf942773176c66773f7128f1fcea08e8 Mon Sep 17 00:00:00 2001 From: Stepan Miakchilo Date: Wed, 8 Apr 2026 09:36:41 +0300 Subject: [PATCH 01/13] [Tech] Introduce ResourceFactory utility class --- .../toolkit/core/utils/ResourceFactory.java | 405 ++++++++++++++++++ 1 file changed, 405 insertions(+) create mode 100644 core/src/main/java/com/exadel/aem/toolkit/core/utils/ResourceFactory.java 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..b420b1d03 --- /dev/null +++ b/core/src/main/java/com/exadel/aem/toolkit/core/utils/ResourceFactory.java @@ -0,0 +1,405 @@ +/* + * 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.lang.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.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.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 Resource} instance based on the provided parameters + * @param resolver The {@code ResourceResolver} to be used for resource creation + * @return A {@link Builder} instance for fluent resource creation + */ + public static Builder newResource(ResourceResolver resolver) { + return new Builder<>(resolver, Builder.class); + } + + /** + * Creates a new {@link Resource} instance representing a Granite UI field based on the provided parameters + * @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(SlingHttpServletRequest request) { + return new FieldBuilder(request.getResourceResolver()) + .path(request.getResource().getPath() + "/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 Int value + */ + 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 type; + + 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.type = type; + } + + // Builder methods + + /** + * Assigns child resources to the resource being built + * @param value The collection of child resources + * @return The builder instance + */ + public T children(Collection value) { + if (CollectionUtils.isEmpty(value)) { + return type.cast(this); + } + value.forEach(this::child); + return type.cast(this); + } + + /** + * Assigns a single child resource to the resource being built + * @param value The child resource + * @return The builder instance + */ + public T child(Resource value) { + if (value == null) { + return type.cast(this); + } + if (children == null) { + children = new ArrayList<>(); + } + children.add(value); + return type.cast(this); + } + + /** + * Assigns a path to the resource being built + * @param value The path segments + * @return The builder instance + */ + public T path(String... value) { + if (ArrayUtils.isEmpty(value)) { + return type.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 type.cast(this); + } + + /** + * Assigns multiple properties to the resource being built + * @param value The map of properties + * @return The builder instance + */ + public T properties(Map value) { + if (MapUtils.isEmpty(value)) { + return type.cast(this); + } + value.forEach(this::property); + return type.cast(this); + } + + /** + * Assigns a single property to the resource being built + * @param name The property name + * @param value The property value + * @return The builder instance + */ + public T property(String name, Object value) { + if (StringUtils.isEmpty(name) || value == null) { + return type.cast(this); + } + if (properties == null) { + properties = new HashMap<>(); + } + properties.put(name, value); + return type.cast(this); + } + + /** + * Assigns a resource type to the resource being built + * @param value The resource type + * @return The builder instance + */ + public T resourceType(String value) { + if (StringUtils.isNotEmpty(value)) { + this.resourceType = value; + } + return type.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<>()); + } + }); + 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(JcrResourceConstants.SLING_RESOURCE_TYPE_PROPERTY, ResourceTypes.MULTIFIELD); + wrapperValueMap.put(CoreConstants.PN_FIELD_LABEL, getProperties().get(CoreConstants.PN_FIELD_LABEL)); + if (ResourceTypes.CONTAINER.equals(getResourceType())) { + wrapperValueMap.put("composite", true); + } + Map nestedValueMap = new HashMap<>(getProperties()); + nestedValueMap.remove(CoreConstants.PN_FIELD_LABEL); + Resource nestedField = ResourceFactory.newResource(getResolver()) + .path(getPath(), "field") + .resourceType(getResourceType()) + .properties(nestedValueMap) + .children(getChildren()) + .build(); + return ResourceFactory.newResource(getResolver()) + .path(getPath()) + .resourceType(ResourceTypes.MULTIFIELD) + .properties(wrapperValueMap) + .child(nestedField) + .build(); + } + } +} From c580330613f0edeca62898cb98fc63ca53082fb6 Mon Sep 17 00:00:00 2001 From: Stepan Miakchilo Date: Wed, 8 Apr 2026 10:25:31 +0300 Subject: [PATCH 02/13] [Tech] Add unit tests --- .../toolkit/core/utils/ResourceFactory.java | 4 +- .../java/com/exadel/aem/toolkit/AllTests.java | 5 +- .../core/utils/ResourceFactoryTest.java | 307 ++++++++++++++++++ 3 files changed, 313 insertions(+), 3 deletions(-) create mode 100644 core/src/test/java/com/exadel/aem/toolkit/core/utils/ResourceFactoryTest.java 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 index b420b1d03..606cec92d 100644 --- 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 @@ -101,7 +101,7 @@ public static class Builder> { * 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.type = type; @@ -338,7 +338,7 @@ private static int compareByPathDepth(String first, String second) { /** * Extends the generic {@link Builder} class for creating Granite UI field resources with a fluent API */ - public static class FieldBuilder extends Builder{ + public static class FieldBuilder extends Builder { private boolean isMultiValue; /** 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/utils/ResourceFactoryTest.java b/core/src/test/java/com/exadel/aem/toolkit/core/utils/ResourceFactoryTest.java new file mode 100644 index 000000000..70988f9e0 --- /dev/null +++ b/core/src/test/java/com/exadel/aem/toolkit/core/utils/ResourceFactoryTest.java @@ -0,0 +1,307 @@ +/* + * 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.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +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 shouldBuildSimpleResource() { + 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 shouldBuildResourceWithMultipleChildren() { + 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 shouldBuildResourceWithNestedProperties() { + 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)); + + Iterator children = resource.listChildren(); + assertNotNull(children); + assertTrue(children.hasNext()); + + Resource alphaChild = children.next(); + assertNotNull(alphaChild); + assertEquals("alpha", alphaChild.getName()); + assertEquals("Nested text", alphaChild.getValueMap().get("text", String.class)); + assertEquals("Nested value", alphaChild.getValueMap().get("value", String.class)); + + Resource betaChild = children.next(); + assertNotNull(betaChild); + assertEquals("More nested text", betaChild.getValueMap().get("text", String.class)); + } + + @Test + public void shouldBuildResourceWithDeeplyNestedProperties() { + 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 shouldBuildGraniteFieldWithIncrementingPaths() { + 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 shouldBuildMultifieldWrapper() { + ResourceFactory.FieldBuilder builder = new ResourceFactory.FieldBuilder(context.resourceResolver()); + builder.path("content/form/myField") + .resourceType("acme/components/field") + .property(CoreConstants.PN_FIELD_LABEL, "Multi Label") + .property("name", "./myField") + .multi(true); + + Resource wrapper = builder.build(); + + assertNotNull(wrapper); + assertEquals(ResourceTypes.MULTIFIELD, wrapper.getResourceType()); + assertEquals("Multi Label", wrapper.getValueMap().get(CoreConstants.PN_FIELD_LABEL, String.class)); + + Iterator children = wrapper.listChildren(); + assertNotNull(children); + assertTrue(children.hasNext()); + Resource nestedField = children.next(); + assertNotNull(nestedField); + assertEquals("acme/components/field", nestedField.getResourceType()); + } + + @Test + public void shouldBuildCompositeMultifieldWrapper() { + ResourceFactory.FieldBuilder builder = new ResourceFactory.FieldBuilder(context.resourceResolver()); + builder.path("content/form/compositeField") + .resourceType(ResourceTypes.CONTAINER) + .property(CoreConstants.PN_FIELD_LABEL, "Composite Label") + .multi(true); + + Resource wrapper = builder.build(); + + assertNotNull(wrapper); + assertEquals(ResourceTypes.MULTIFIELD, wrapper.getResourceType()); + boolean composite = wrapper.getValueMap().get("composite", false); + assertTrue(composite); + } + + @Test + public void shouldPassPropertiesMapToBuilder() { + 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 shouldBuildChildrenFromCollection() { + Resource child1 = ResourceFactory.newResource(context.resourceResolver()) + .path("c1").property("v", "one").build(); + Resource child2 = ResourceFactory.newResource(context.resourceResolver()) + .path("c2").property("v", "two").build(); + + Resource parent = ResourceFactory.newResource(context.resourceResolver()) + .path("parent") + .children(Arrays.asList(child1, child2)) + .build(); + + assertNotNull(parent); + Iterator it = parent.listChildren(); + assertNotNull(it); + assertTrue(it.hasNext()); + it.next(); + assertTrue(it.hasNext()); + it.next(); + assertFalse(it.hasNext()); + } +} From 93913a96af6a571a9a008587c734284ca6ab6aab Mon Sep 17 00:00:00 2001 From: Stepan Miakchilo Date: Wed, 8 Apr 2026 13:13:21 +0300 Subject: [PATCH 03/13] [Tech] Switch to using ResourceFactory in OptionProvider resolvers logic --- .../resolvers/ConstantsResolverHelper.java | 24 ++++------- .../impl/resolvers/EnumResolverHelper.java | 26 +++++------ .../resolvers/HttpOptionSourceResolver.java | 43 +++++++------------ .../resolvers/InlineOptionSourceResolver.java | 24 ++++------- 4 files changed, 45 insertions(+), 72 deletions(-) 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..88d1499cf 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,6 @@ import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -29,16 +28,15 @@ 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 @@ -79,19 +77,15 @@ Resource resolve(SlingHttpServletRequest request) { 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.get(OptionProviderConstants.PARAMETER_NAME, String.class)) + .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(); } /** 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..f3ba9c2b8 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; @@ -28,14 +27,13 @@ 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,19 +62,17 @@ 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(); } /** 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..1aec38e77 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(createValueMap(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(createValueMap(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 * @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 createValueMap(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..9a566ea02 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,7 +15,6 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.Iterator; import java.util.List; @@ -23,11 +22,8 @@ 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 +31,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 @@ -75,18 +72,15 @@ public Resource resolve(SlingHttpServletRequest request, PathParameters params) } } 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(valueMap.get(OptionProviderConstants.PARAMETER_NAME, StringUtils.EMPTY)) + .properties(valueMap) + .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(); } } From 4b1074d6cb88ee512abc58191c77af63300e519d Mon Sep 17 00:00:00 2001 From: Stepan Miakchilo Date: Wed, 8 Apr 2026 13:15:50 +0300 Subject: [PATCH 04/13] [Tech] Switch to using ResourceFactory in RenderConditionTest --- .../models/internal/RenderConditionTest.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) 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); } From d214df00ce7f57f3d97d07a9204c85ac5ac8ca3d Mon Sep 17 00:00:00 2001 From: Stepan Miakchilo Date: Wed, 8 Apr 2026 13:18:32 +0300 Subject: [PATCH 05/13] [Tech] Switch to using ResourceFactory in ItemComponentsServlet --- .../lists/servlets/ItemComponentsServlet.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) 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); From ff49eb79b94fba2e1f8f97cc37bb6061cc40323c Mon Sep 17 00:00:00 2001 From: Stepan Miakchilo Date: Wed, 8 Apr 2026 23:14:07 +0300 Subject: [PATCH 06/13] [Tech] Switch to using ResourceFactory in ListHelper --- .../toolkit/core/lists/utils/ListHelper.java | 3 +- .../core/lists/utils/ListResourceUtil.java | 34 ++++--------------- 2 files changed, 8 insertions(+), 29 deletions(-) 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..a09c5ea77 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); 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..897b63e6a 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,12 +21,10 @@ 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; @@ -35,6 +33,7 @@ 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; /** @@ -70,31 +69,6 @@ public static void createListItem( valueMapWithoutSystemProps); } - /** - * 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 @@ -105,7 +79,11 @@ public static List mapToValueMapResources(ResourceResolver resourceRes 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()); } From 0677dbbb0366a0248b1f33db7e4f41043cf0298d Mon Sep 17 00:00:00 2001 From: Stepan Miakchilo Date: Wed, 8 Apr 2026 23:27:19 +0300 Subject: [PATCH 07/13] [Tech] Retire ValueMapBuilder --- .../resolvers/ConstantsResolverHelper.java | 62 +++++++++++-------- .../resolvers/InlineOptionSourceResolver.java | 14 ++--- .../impl/resolvers/ValueMapBuilder.java | 54 ---------------- 3 files changed, 43 insertions(+), 87 deletions(-) delete mode 100644 core/src/main/java/com/exadel/aem/toolkit/core/optionprovider/services/impl/resolvers/ValueMapBuilder.java 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 88d1499cf..fe18ae656 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,6 +17,7 @@ import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -27,7 +28,6 @@ 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.slf4j.Logger; import org.slf4j.LoggerFactory; import com.day.cq.commons.jcr.JcrConstants; @@ -65,21 +65,23 @@ 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 -> ResourceFactory .newResource(request.getResourceResolver()) - .path(valueMap.get(OptionProviderConstants.PARAMETER_NAME, String.class)) + .path(valueMap.getOrDefault(OptionProviderConstants.PARAMETER_NAME, StringUtils.EMPTY).toString()) .properties(valueMap) .build()) .collect(Collectors.toList()); @@ -89,14 +91,17 @@ Resource resolve(SlingHttpServletRequest request) { } /** - * 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 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)) { @@ -104,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/InlineOptionSourceResolver.java b/core/src/main/java/com/exadel/aem/toolkit/core/optionprovider/services/impl/resolvers/InlineOptionSourceResolver.java index 9a566ea02..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,13 +15,14 @@ import java.io.IOException; import java.util.ArrayList; +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.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fasterxml.jackson.databind.JsonNode; @@ -62,19 +63,18 @@ 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 = ResourceFactory.newResource(request.getResourceResolver()) - .path(valueMap.get(OptionProviderConstants.PARAMETER_NAME, StringUtils.EMPTY)) - .properties(valueMap) + .path(properties.getOrDefault(OptionProviderConstants.PARAMETER_NAME, StringUtils.EMPTY).toString()) + .properties(properties) .build(); children.add(child); } 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); - } -} From c9ad8f215134bac3a7fc27b0c42b53173f2aaded Mon Sep 17 00:00:00 2001 From: Stepan Miakchilo Date: Thu, 9 Apr 2026 00:35:35 +0300 Subject: [PATCH 08/13] [Tech] Fix naming, Javadoc prose --- .../toolkit/core/lists/utils/ListHelper.java | 2 +- .../core/lists/utils/ListResourceUtil.java | 13 +++-- .../impl/resolvers/EnumResolverHelper.java | 3 +- .../toolkit/core/utils/ResourceFactory.java | 14 +++--- .../lists/utils/ListResourceUtilTest.java | 2 +- .../core/utils/ResourceFactoryTest.java | 49 ++++++------------- 6 files changed, 32 insertions(+), 51 deletions(-) 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 a09c5ea77..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 @@ -246,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 897b63e6a..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 @@ -27,7 +27,6 @@ import org.apache.sling.api.resource.ResourceUtil; 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; @@ -70,12 +69,12 @@ public static void createListItem( } /** - * 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() @@ -89,7 +88,7 @@ public static List mapToValueMapResources(ResourceResolver resourceRes /** * 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/EnumResolverHelper.java b/core/src/main/java/com/exadel/aem/toolkit/core/optionprovider/services/impl/resolvers/EnumResolverHelper.java index f3ba9c2b8..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 @@ -26,7 +26,6 @@ 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.slf4j.Logger; import org.slf4j.LoggerFactory; import com.day.cq.commons.jcr.JcrConstants; @@ -76,7 +75,7 @@ Resource resolve(SlingHttpServletRequest request) { } /** - * 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/utils/ResourceFactory.java b/core/src/main/java/com/exadel/aem/toolkit/core/utils/ResourceFactory.java index 606cec92d..9c8cfbebd 100644 --- 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 @@ -52,7 +52,7 @@ private ResourceFactory() { } /** - * Creates a new {@link Resource} instance based on the provided parameters + * 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 */ @@ -61,7 +61,7 @@ public static Builder newResource(ResourceResolver resolver) { } /** - * Creates a new {@link Resource} instance representing a Granite UI field based on the provided parameters + * 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 */ @@ -73,7 +73,7 @@ public static FieldBuilder newGraniteField(SlingHttpServletRequest 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 Int value + * @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 @@ -171,10 +171,10 @@ public T properties(Map value) { } /** - * Assigns a single property to the resource being built - * @param name The property name - * @param value The property value - * @return The builder instance + * Assigns a single property to the resource being built + * @param name The property name + * @param value The property value + * @return The builder instance */ public T property(String name, Object value) { if (StringUtils.isEmpty(name) || value == null) { 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 index 70988f9e0..0e0f6fb59 100644 --- 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 @@ -13,7 +13,6 @@ */ package com.exadel.aem.toolkit.core.utils; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; @@ -41,7 +40,7 @@ public class ResourceFactoryTest { public final AemContext context = AemContextFactory.newInstance(); @Test - public void shouldBuildSimpleResource() { + public void shouldSetPathAndProperties() { Resource resource = ResourceFactory.newResource(context.resourceResolver()) .path("content/test") .resourceType("acme/components/test") @@ -67,11 +66,17 @@ public void shouldUseDefaultResourceTypeWhenNoneSet() { } @Test - public void shouldBuildResourceWithMultipleChildren() { - Resource child1 = ResourceFactory.newResource(context.resourceResolver()) - .path("child1").property("key", "val1").build(); - Resource child2 = ResourceFactory.newResource(context.resourceResolver()) - .path("child2").property("key", "val2").build(); + 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") @@ -92,7 +97,7 @@ public void shouldBuildResourceWithMultipleChildren() { } @Test - public void shouldBuildResourceWithNestedProperties() { + public void shouldHandleNestedProperties() { Resource resource = ResourceFactory.newResource(context.resourceResolver()) .path("content/composite") .property("title", "Main") @@ -120,7 +125,7 @@ public void shouldBuildResourceWithNestedProperties() { } @Test - public void shouldBuildResourceWithDeeplyNestedProperties() { + public void shouldHandleDeeplyNestedProperties() { Resource resource = ResourceFactory.newResource(context.resourceResolver()) .path("content/node") .property("title", "Root") @@ -217,7 +222,7 @@ public void shouldIgnoreNullOrEmptyInputsInBuilder() { } @Test - public void shouldBuildGraniteFieldWithIncrementingPaths() { + public void shouldIncrementGraniteFieldPaths() { context.create().resource("/content/myForm"); context.request().setResource(context.resourceResolver().getResource("/content/myForm")); @@ -268,7 +273,7 @@ public void shouldBuildCompositeMultifieldWrapper() { } @Test - public void shouldPassPropertiesMapToBuilder() { + public void shouldAcceptPropertiesMap() { Map props = new HashMap<>(); props.put("alpha", "a"); props.put("beta", "b"); @@ -282,26 +287,4 @@ public void shouldPassPropertiesMapToBuilder() { assertEquals("a", resource.getValueMap().get("alpha", String.class)); assertEquals("b", resource.getValueMap().get("beta", String.class)); } - - @Test - public void shouldBuildChildrenFromCollection() { - Resource child1 = ResourceFactory.newResource(context.resourceResolver()) - .path("c1").property("v", "one").build(); - Resource child2 = ResourceFactory.newResource(context.resourceResolver()) - .path("c2").property("v", "two").build(); - - Resource parent = ResourceFactory.newResource(context.resourceResolver()) - .path("parent") - .children(Arrays.asList(child1, child2)) - .build(); - - assertNotNull(parent); - Iterator it = parent.listChildren(); - assertNotNull(it); - assertTrue(it.hasNext()); - it.next(); - assertTrue(it.hasNext()); - it.next(); - assertFalse(it.hasNext()); - } } From a27bfa23f8a5caba0374a7a6f7000ab3be06b3bc Mon Sep 17 00:00:00 2001 From: Stepan Miakchilo Date: Thu, 9 Apr 2026 00:36:21 +0300 Subject: [PATCH 09/13] [Tech] Avoid a failed resource build in edge cases --- .../exadel/aem/toolkit/core/utils/ResourceFactory.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 index 9c8cfbebd..d9ded71bf 100644 --- 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 @@ -30,7 +30,6 @@ import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; 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; @@ -310,6 +309,8 @@ private static Map> extractDerivedValueMaps(Map 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; } @@ -381,12 +382,13 @@ public Resource build() { return super.build(); } Map wrapperValueMap = new HashMap<>(); - wrapperValueMap.put(JcrResourceConstants.SLING_RESOURCE_TYPE_PROPERTY, ResourceTypes.MULTIFIELD); - wrapperValueMap.put(CoreConstants.PN_FIELD_LABEL, getProperties().get(CoreConstants.PN_FIELD_LABEL)); + wrapperValueMap.put( + CoreConstants.PN_FIELD_LABEL, + MapUtils.emptyIfNull(getProperties()).get(CoreConstants.PN_FIELD_LABEL)); if (ResourceTypes.CONTAINER.equals(getResourceType())) { wrapperValueMap.put("composite", true); } - Map nestedValueMap = new HashMap<>(getProperties()); + Map nestedValueMap = new HashMap<>(MapUtils.emptyIfNull(getProperties())); nestedValueMap.remove(CoreConstants.PN_FIELD_LABEL); Resource nestedField = ResourceFactory.newResource(getResolver()) .path(getPath(), "field") From 3ab053414fbcb2af3966653d584bbdc1d65c5fe7 Mon Sep 17 00:00:00 2001 From: Stepan Miakchilo Date: Thu, 9 Apr 2026 00:52:18 +0300 Subject: [PATCH 10/13] [Tech] Modify test method to handle a variation is childr resources order --- .../core/utils/ResourceFactoryTest.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) 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 index 0e0f6fb59..6d527b418 100644 --- 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 @@ -16,8 +16,10 @@ 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; @@ -109,17 +111,23 @@ public void shouldHandleNestedProperties() { assertNotNull(resource); assertEquals("Main", resource.getValueMap().get("title", String.class)); - Iterator children = resource.listChildren(); + List children = IteratorUtils.toList(resource.listChildren()); assertNotNull(children); - assertTrue(children.hasNext()); - Resource alphaChild = children.next(); + Resource alphaChild = children + .stream() + .filter(child -> "alpha".equals(child.getName())) + .findFirst() + .orElse(null); assertNotNull(alphaChild); - assertEquals("alpha", alphaChild.getName()); assertEquals("Nested text", alphaChild.getValueMap().get("text", String.class)); assertEquals("Nested value", alphaChild.getValueMap().get("value", String.class)); - Resource betaChild = children.next(); + 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)); } From de6f7c54e634fe3e8f4c33aeaadd2a9d6cf4df14 Mon Sep 17 00:00:00 2001 From: Stepan Miakchilo Date: Thu, 9 Apr 2026 01:00:43 +0300 Subject: [PATCH 11/13] [Tech] Fix naming, Javadoc prose --- .../resolvers/ConstantsResolverHelper.java | 2 +- .../resolvers/HttpOptionSourceResolver.java | 8 +-- .../toolkit/core/utils/ResourceFactory.java | 51 ++++++++++--------- 3 files changed, 31 insertions(+), 30 deletions(-) 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 fe18ae656..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 @@ -94,7 +94,7 @@ Resource resolve(SlingHttpServletRequest request) { * 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 */ 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 1aec38e77..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 @@ -210,7 +210,7 @@ private static Resource createResource(SlingHttpServletRequest request, String p } Resource resource = ResourceFactory.newResource(request.getResourceResolver()) .path(path, nextField.getKey()) - .properties(createValueMap(nextField.getValue())) + .properties(createPropertiesMap(nextField.getValue())) .build(); children.add(resource); } @@ -224,7 +224,7 @@ private static Resource createResource(SlingHttpServletRequest request, String p } Resource resource = ResourceFactory.newResource(request.getResourceResolver()) .path(path, CoreConstants.NN_ITEM + elementIndex++) - .properties(createValueMap(nextElement)) + .properties(createPropertiesMap(nextElement)) .build(); children.add(resource); } @@ -237,11 +237,11 @@ private static Resource createResource(SlingHttpServletRequest request, String p /** * 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 {@code Map} object */ - private static Map createValueMap(JsonNode jsonNode) { + 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())); 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 index d9ded71bf..b1297294f 100644 --- 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 @@ -24,12 +24,13 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; -import org.apache.commons.lang.ArrayUtils; +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; @@ -55,7 +56,7 @@ private ResourceFactory() { * @param resolver The {@code ResourceResolver} to be used for resource creation * @return A {@link Builder} instance for fluent resource creation */ - public static Builder newResource(ResourceResolver resolver) { + public static Builder newResource(@NotNull ResourceResolver resolver) { return new Builder<>(resolver, Builder.class); } @@ -64,9 +65,9 @@ public static Builder newResource(ResourceResolver resolver) { * @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(SlingHttpServletRequest request) { + public static FieldBuilder newGraniteField(@NotNull SlingHttpServletRequest request) { return new FieldBuilder(request.getResourceResolver()) - .path(request.getResource().getPath() + "/field" + getAndIncrementFieldCount(request)); + .path(request.getResource().getPath(), "field" + getAndIncrementFieldCount(request)); } /** @@ -89,7 +90,7 @@ private static int getAndIncrementFieldCount(SlingHttpServletRequest request) { public static class Builder> { private final ResourceResolver resolver; - private final Class type; + private final Class builderClass; private List children; private String path; @@ -103,7 +104,7 @@ public static class Builder> { */ Builder(ResourceResolver resolver, Class type) { this.resolver = resolver; - this.type = type; + this.builderClass = type; } // Builder methods @@ -111,40 +112,40 @@ public static class Builder> { /** * Assigns child resources to the resource being built * @param value The collection of child resources - * @return The builder instance + * @return This builder instance */ public T children(Collection value) { if (CollectionUtils.isEmpty(value)) { - return type.cast(this); + return builderClass.cast(this); } value.forEach(this::child); - return type.cast(this); + return builderClass.cast(this); } /** * Assigns a single child resource to the resource being built * @param value The child resource - * @return The builder instance + * @return This builder instance */ public T child(Resource value) { if (value == null) { - return type.cast(this); + return builderClass.cast(this); } if (children == null) { children = new ArrayList<>(); } children.add(value); - return type.cast(this); + return builderClass.cast(this); } /** * Assigns a path to the resource being built * @param value The path segments - * @return The builder instance + * @return This builder instance */ public T path(String... value) { if (ArrayUtils.isEmpty(value)) { - return type.cast(this); + return builderClass.cast(this); } String effectivePath = Stream.of(value) .map(v -> StringUtils.strip(v, CoreConstants.SEPARATOR_SLASH)) @@ -153,49 +154,49 @@ public T path(String... value) { if (StringUtils.isNotEmpty(effectivePath)) { path = effectivePath; } - return type.cast(this); + return builderClass.cast(this); } /** * Assigns multiple properties to the resource being built * @param value The map of properties - * @return The builder instance + * @return This builder instance */ public T properties(Map value) { if (MapUtils.isEmpty(value)) { - return type.cast(this); + return builderClass.cast(this); } value.forEach(this::property); - return type.cast(this); + return builderClass.cast(this); } /** * Assigns a single property to the resource being built * @param name The property name * @param value The property value - * @return The builder instance - */ + * @return This builder instance + */ public T property(String name, Object value) { if (StringUtils.isEmpty(name) || value == null) { - return type.cast(this); + return builderClass.cast(this); } if (properties == null) { properties = new HashMap<>(); } properties.put(name, value); - return type.cast(this); + return builderClass.cast(this); } /** - * Assigns a resource type to the resource being built + * Assigns a Sling resource type to the resource being built * @param value The resource type - * @return The builder instance + * @return This builder instance */ public T resourceType(String value) { if (StringUtils.isNotEmpty(value)) { this.resourceType = value; } - return type.cast(this); + return builderClass.cast(this); } /** From 1bd3a50e7c4f668e867350430c8c6580930cf33d Mon Sep 17 00:00:00 2001 From: Stepan Miakchilo Date: Thu, 9 Apr 2026 18:25:38 +0300 Subject: [PATCH 12/13] [Tech] Optimize unit tests --- .../core/utils/ResourceFactoryTest.java | 132 ++++++++++++------ 1 file changed, 90 insertions(+), 42 deletions(-) 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 index 6d527b418..3696afb28 100644 --- 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 @@ -98,6 +98,22 @@ public void shouldIncludeChildren() { 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()) @@ -229,34 +245,24 @@ public void shouldIgnoreNullOrEmptyInputsInBuilder() { assertEquals(JcrConstants.NT_UNSTRUCTURED, builder.getResourceType()); } - @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 shouldBuildMultifieldWrapper() { - ResourceFactory.FieldBuilder builder = new ResourceFactory.FieldBuilder(context.resourceResolver()); - builder.path("content/form/myField") - .resourceType("acme/components/field") - .property(CoreConstants.PN_FIELD_LABEL, "Multi Label") - .property("name", "./myField") - .multi(true); - - Resource wrapper = builder.build(); + 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(wrapper); - assertEquals(ResourceTypes.MULTIFIELD, wrapper.getResourceType()); - assertEquals("Multi Label", wrapper.getValueMap().get(CoreConstants.PN_FIELD_LABEL, String.class)); + assertNotNull(multifield); + assertEquals(ResourceTypes.MULTIFIELD, multifield.getResourceType()); + assertEquals("Multi Label", multifield.getValueMap().get(CoreConstants.PN_FIELD_LABEL, String.class)); - Iterator children = wrapper.listChildren(); + Iterator children = multifield.listChildren(); assertNotNull(children); assertTrue(children.hasNext()); Resource nestedField = children.next(); @@ -266,33 +272,75 @@ public void shouldBuildMultifieldWrapper() { @Test public void shouldBuildCompositeMultifieldWrapper() { - ResourceFactory.FieldBuilder builder = new ResourceFactory.FieldBuilder(context.resourceResolver()); - builder.path("content/form/compositeField") - .resourceType(ResourceTypes.CONTAINER) - .property(CoreConstants.PN_FIELD_LABEL, "Composite Label") - .multi(true); + context.request().setResource(context.create().resource("/content/myForm")); + + Resource externalChild = ResourceFactory.newResource(context.resourceResolver()) + .path("external") + .property("from", "external") + .build(); - Resource wrapper = builder.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(wrapper); - assertEquals(ResourceTypes.MULTIFIELD, wrapper.getResourceType()); - boolean composite = wrapper.getValueMap().get("composite", false); + assertNotNull(multifield); + assertEquals(ResourceTypes.MULTIFIELD, multifield.getResourceType()); + boolean composite = multifield.getValueMap().get("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 shouldAcceptPropertiesMap() { - Map props = new HashMap<>(); - props.put("alpha", "a"); - props.put("beta", "b"); + public void shouldIncrementGraniteFieldPaths() { + context.create().resource("/content/myForm"); + context.request().setResource(context.resourceResolver().getResource("/content/myForm")); - Resource resource = ResourceFactory.newResource(context.resourceResolver()) - .path("content/test") - .properties(props) + 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); - assertEquals("a", resource.getValueMap().get("alpha", String.class)); - assertEquals("b", resource.getValueMap().get("beta", String.class)); + 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()); } } From 10a4aa23a6635eca355d4d132c0841ce5dadbe75 Mon Sep 17 00:00:00 2001 From: Stepan Miakchilo Date: Thu, 9 Apr 2026 18:44:24 +0300 Subject: [PATCH 13/13] [Tech] Re-use core constants --- .../com/exadel/aem/toolkit/core/utils/ResourceFactory.java | 6 +++--- .../exadel/aem/toolkit/core/utils/ResourceFactoryTest.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) 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 index b1297294f..b9a5256d2 100644 --- 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 @@ -67,7 +67,7 @@ public static Builder newResource(@NotNull ResourceResolver resolver) { */ public static FieldBuilder newGraniteField(@NotNull SlingHttpServletRequest request) { return new FieldBuilder(request.getResourceResolver()) - .path(request.getResource().getPath(), "field" + getAndIncrementFieldCount(request)); + .path(request.getResource().getPath(), CoreConstants.NN_FIELD + getAndIncrementFieldCount(request)); } /** @@ -387,12 +387,12 @@ public Resource build() { CoreConstants.PN_FIELD_LABEL, MapUtils.emptyIfNull(getProperties()).get(CoreConstants.PN_FIELD_LABEL)); if (ResourceTypes.CONTAINER.equals(getResourceType())) { - wrapperValueMap.put("composite", true); + 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(), "field") + .path(getPath(), CoreConstants.NN_FIELD) .resourceType(getResourceType()) .properties(nestedValueMap) .children(getChildren()) 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 index 3696afb28..17a31e115 100644 --- 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 @@ -290,7 +290,7 @@ public void shouldBuildCompositeMultifieldWrapper() { assertNotNull(multifield); assertEquals(ResourceTypes.MULTIFIELD, multifield.getResourceType()); - boolean composite = multifield.getValueMap().get("composite", false); + boolean composite = multifield.getValueMap().get(CoreConstants.PN_COMPOSITE, false); assertTrue(composite); Iterator wrapperChildren = multifield.listChildren();