diff --git a/changes.xml b/changes.xml
index 0bc59ba..2110bc2 100644
--- a/changes.xml
+++ b/changes.xml
@@ -23,9 +23,9 @@
xsi:schemaLocation="http://maven.apache.org/changes/1.0.0 http://maven.apache.org/plugins/maven-changes-plugin/xsd/changes-1.0.0.xsd">
-
-
- Switch to AEM 6.5.17 as minimum version.
+
+
+ Add AemObjectsReflectionToStringBuilder to support reflection-based toString() methods with more compact/human-readable output of AEM-related objects.
diff --git a/pom.xml b/pom.xml
index e6a1f75..439edd8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -64,7 +64,7 @@
io.wcm
io.wcm.sling.commons
- 1.4.0
+ 1.6.2
compile
diff --git a/src/main/java/io/wcm/wcm/commons/util/AemObjectReflectionToStringBuilder.java b/src/main/java/io/wcm/wcm/commons/util/AemObjectReflectionToStringBuilder.java
new file mode 100644
index 0000000..3ae815e
--- /dev/null
+++ b/src/main/java/io/wcm/wcm/commons/util/AemObjectReflectionToStringBuilder.java
@@ -0,0 +1,100 @@
+/*
+ * #%L
+ * wcm.io
+ * %%
+ * Copyright (C) 2024 wcm.io
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+package io.wcm.wcm.commons.util;
+
+import java.lang.reflect.Field;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ValueMap;
+
+import com.day.cq.dam.api.Asset;
+import com.day.cq.wcm.api.Page;
+
+/**
+ * Extends ReflectionToStringBuilder to provide custom handling for AEM-related objects
+ * (Resource, Page, Asset, ValueMap) for a more compact log output.
+ */
+public class AemObjectReflectionToStringBuilder extends ReflectionToStringBuilder {
+
+ private static final TypedValueProcessor[] PROCESSORS = {
+ new TypedValueProcessor<>(Resource.class, Resource::getPath),
+ new TypedValueProcessor<>(Page.class, Page::getPath),
+ new TypedValueProcessor<>(Asset.class, Asset::getPath),
+ new TypedValueProcessor<>(ValueMap.class, AemObjectReflectionToStringBuilder::filteredValueMap)
+ };
+
+ /**
+ * @param object Object to output
+ */
+ public AemObjectReflectionToStringBuilder(Object object) {
+ super(object);
+ }
+
+ /**
+ * @param object Object to output
+ * @param style Style
+ */
+ public AemObjectReflectionToStringBuilder(Object object, ToStringStyle style) {
+ super(object, style);
+ }
+
+ @Override
+ @SuppressWarnings({ "unchecked", "java:S3740" })
+ protected Object getValue(Field field) throws IllegalAccessException {
+ final Class> fieldType = field.getType();
+ // check if a dedicated processor is registered for the given field type
+ for (TypedValueProcessor item : PROCESSORS) {
+ if (item.type.isAssignableFrom(fieldType)) {
+ Object value = field.get(this.getObject());
+ if (value != null) {
+ return item.processor.apply(value);
+ }
+ }
+ }
+ return super.getValue(field);
+ }
+
+ /**
+ * Filter value map to exclude jcr:* properties and null values.
+ * @param props Value map
+ * @return Filtered value map, sorted by key
+ */
+ public static Map filteredValueMap(ValueMap props) {
+ return props.entrySet().stream()
+ .filter(entry -> !entry.getKey().startsWith("jcr:") && entry.getValue() != null)
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (o1, o2) -> o1, TreeMap::new));
+ }
+
+ private static class TypedValueProcessor {
+ private final Class type;
+ private final Function processor;
+ TypedValueProcessor(Class type, Function processor) {
+ this.type = type;
+ this.processor = processor;
+ }
+ }
+
+}
diff --git a/src/main/java/io/wcm/wcm/commons/util/package-info.java b/src/main/java/io/wcm/wcm/commons/util/package-info.java
index 658aa7b..04900ee 100644
--- a/src/main/java/io/wcm/wcm/commons/util/package-info.java
+++ b/src/main/java/io/wcm/wcm/commons/util/package-info.java
@@ -20,5 +20,5 @@
/**
* Miscellaneous WCM helper classes.
*/
-@org.osgi.annotation.versioning.Version("1.4.0")
+@org.osgi.annotation.versioning.Version("1.5.0")
package io.wcm.wcm.commons.util;
diff --git a/src/test/java/io/wcm/wcm/commons/util/AemObjectReflectionToStringBuilderTest.java b/src/test/java/io/wcm/wcm/commons/util/AemObjectReflectionToStringBuilderTest.java
new file mode 100644
index 0000000..4aa993a
--- /dev/null
+++ b/src/test/java/io/wcm/wcm/commons/util/AemObjectReflectionToStringBuilderTest.java
@@ -0,0 +1,106 @@
+/*
+ * #%L
+ * wcm.io
+ * %%
+ * Copyright (C) 2024 wcm.io
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+package io.wcm.wcm.commons.util;
+
+import static com.day.cq.commons.jcr.JcrConstants.JCR_CREATED;
+import static com.day.cq.commons.jcr.JcrConstants.JCR_PRIMARYTYPE;
+import static com.day.cq.commons.jcr.JcrConstants.NT_UNSTRUCTURED;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.lang3.builder.ToStringStyle;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.wrappers.ValueMapDecorator;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import com.day.cq.dam.api.Asset;
+import com.day.cq.wcm.api.Page;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import io.wcm.testing.mock.aem.junit5.AemContext;
+import io.wcm.testing.mock.aem.junit5.AemContextExtension;
+import io.wcm.wcm.commons.contenttype.ContentType;
+import io.wcm.wcm.commons.testcontext.AppAemContext;
+
+@ExtendWith(AemContextExtension.class)
+class AemObjectReflectionToStringBuilderTest {
+
+ private final AemContext context = AppAemContext.newAemContext();
+
+ private static final ValueMap VALUEMAP_SAMPLE;
+ static {
+ final Map props = new HashMap<>();
+ props.put("prop1", "value1");
+ props.put(JCR_CREATED, new Date());
+ props.put(JCR_PRIMARYTYPE, NT_UNSTRUCTURED);
+ props.put("prop2", 5);
+ props.put("prop3", null);
+ VALUEMAP_SAMPLE = new ValueMapDecorator(props);
+ }
+
+ @Test
+ void testBuild() {
+ ClassWithFields obj = new ClassWithFields();
+ obj.prop1 = "value1";
+ obj.resource = context.create().resource("/content/resource1",
+ "prop2", "value2");
+ obj.page = context.create().page("/content/page1");
+ obj.asset = context.create().asset("/content/dam/asset1.jpg", 10, 10, ContentType.JPEG);
+ obj.props = VALUEMAP_SAMPLE;
+
+ assertEquals("[asset=/content/dam/asset1.jpg,"
+ + "page=/content/page1,"
+ + "prop1=value1,"
+ + "props={prop1=value1, prop2=5},"
+ + "resource=/content/resource1]",
+ new AemObjectReflectionToStringBuilder(obj, ToStringStyle.NO_CLASS_NAME_STYLE).build());
+ }
+
+ @Test
+ void testBuild_NullObjects() {
+ ClassWithFields obj = new ClassWithFields();
+
+ assertNotNull(new AemObjectReflectionToStringBuilder(obj).build());
+ }
+
+ @Test
+ void testFilteredValueMap() {
+ Map filtered = AemObjectReflectionToStringBuilder.filteredValueMap(VALUEMAP_SAMPLE);
+
+ assertEquals(Map.of("prop1", "value1", "prop2", 5), filtered);
+ }
+
+ @SuppressWarnings("unused")
+ @SuppressFBWarnings("URF_UNREAD_FIELD")
+ private static final class ClassWithFields {
+ String prop1;
+ Resource resource;
+ Page page;
+ Asset asset;
+ ValueMap props;
+ }
+
+}