AemObjectReflectionToStringBuilder.java

/*
 * #%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<String, Object> 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<T> {
    private final Class<T> type;
    private final Function<T, Object> processor;
    TypedValueProcessor(Class<T> type, Function<T, Object> processor) {
      this.type = type;
      this.processor = processor;
    }
  }

}