GraniteUiSyntheticResource.java

/*
 * #%L
 * wcm.io
 * %%
 * Copyright (C) 2014 - 2015 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.ui.granite.resource;

import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

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.SyntheticResource;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.wrappers.ValueMapDecorator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.annotation.versioning.ProviderType;

import com.day.cq.commons.jcr.JcrConstants;

/**
 * Extended version of {@link SyntheticResource} that allows to pass an own value map and optional child resources.
 * Please note: Accessing child resources does only work when accessing {@link Resource#listChildren()}, and
 * not when calling the same method on resourceResolver. This breaks the contract of the resource API, but should
 * work at least for the Granite UI implementation which seems to always use this method.
 */
@ProviderType
public final class GraniteUiSyntheticResource extends SyntheticResource {

  private final ValueMap props;
  private final Map<String, Resource> children;

  private GraniteUiSyntheticResource(ResourceResolver resourceResolver,
      ResourceMetadata resourceMetadata, String resourceType,
      ValueMap props, Iterable<Resource> children) {
    super(resourceResolver, resourceMetadata, resourceType);
    this.props = props;
    this.children = childrenMap(children);
  }

  private GraniteUiSyntheticResource(ResourceResolver resourceResolver,
      String path,
      String resourceType,
      ValueMap props,
      Iterable<Resource> children) {
    super(resourceResolver, path, resourceType);
    this.props = props;
    this.children = childrenMap(children);
  }

  private static Map<String, Resource> childrenMap(Iterable<Resource> children) {
    Map<String, Resource> result = new LinkedHashMap<>();
    children.forEach(resource -> result.put(resource.getName(), resource));
    return result;
  }

  @SuppressWarnings({ "unchecked", "null" })
  @Override
  public <Type> Type adaptTo(Class<Type> type) {
    if (ValueMap.class.equals(type)) {
      return (Type)props;
    }
    else {
      return super.adaptTo(type);
    }
  }

  @Override
  public Iterator<Resource> listChildren() {
    return children.values().iterator();
  }

  @Override
  public Iterable<Resource> getChildren() {
    return children.values();
  }

  @Override
  public boolean hasChildren() {
    return !children.isEmpty();
  }

  @Override
  public Resource getChild(String relPath) {
    // naive implementation that only covers the simplest-possible case to detect the correct child
    Resource child = children.get(relPath);
    if (child != null) {
      return child;
    }
    return super.getChild(relPath);
  }

  private void addChild(Resource child) {
    children.put(child.getName(), child);
  }

  /**
   * Create synthetic resource.
   * @param resourceResolver Resource resolver
   * @param valueMap Properties
   * @return Resource
   */
  public static Resource create(@NotNull ResourceResolver resourceResolver, @NotNull ValueMap valueMap) {
    return create(resourceResolver, null, JcrConstants.NT_UNSTRUCTURED, valueMap);
  }

  /**
   * Create synthetic resource.
   * @param resourceResolver Resource resolver
   * @param path Resource path
   * @param resourceType Resource type
   * @return Resource
   */
  public static Resource create(@NotNull ResourceResolver resourceResolver, @Nullable String path, @NotNull String resourceType) {
    return create(resourceResolver, path, resourceType, ValueMap.EMPTY);
  }

  /**
   * Create synthetic resource.
   * @param resourceResolver Resource resolver
   * @param path Resource path
   * @param resourceType Resource type
   * @param valueMap Properties
   * @return Resource
   */
  public static Resource create(@NotNull ResourceResolver resourceResolver, @Nullable String path, @NotNull String resourceType,
      @NotNull ValueMap valueMap) {
    return new GraniteUiSyntheticResource(resourceResolver,
        path,
        resourceType,
        valueMap,
        Collections.emptyList());
  }

  /**
   * Wrap a real resource and create a synthetic resource out of it.
   * @param resource Real resource
   * @return Resource
   */
  public static Resource wrap(@NotNull Resource resource) {
    return wrap(resource, resource.getValueMap(), resource.getChildren());
  }

  /**
   * Wrap a real resource and create a synthetic resource out of it.
   * @param resource Real resource
   * @param valueMap Properties to use instead of the real properties
   * @return Resource
   */
  public static Resource wrap(@NotNull Resource resource, @NotNull ValueMap valueMap) {
    return wrap(resource, valueMap, resource.getChildren());
  }

  /**
   * Wrap a real resource and create a synthetic resource out of it.
   * Merges the given properties with the existing properties of the resource.
   * @param resource Real resource
   * @param valueMap Properties to be merged with the real properties
   * @return Resource
   */
  public static Resource wrapMerge(@NotNull Resource resource, @NotNull ValueMap valueMap) {
    Map<String, Object> mergedProperties = new HashMap<>();
    mergedProperties.putAll(resource.getValueMap());
    mergedProperties.putAll(valueMap);
    return wrap(resource, new ValueMapDecorator(mergedProperties), resource.getChildren());
  }

  private static Resource wrap(Resource resource, ValueMap valueMap, Iterable<Resource> children) {
    return new GraniteUiSyntheticResource(resource.getResourceResolver(),
        resource.getResourceMetadata(),
        resource.getResourceType(),
        valueMap,
        children);
  }

  /**
   * Create synthetic resource child resource of the given parent resource.
   * @param parentResource Parent resource (has to be a {@link GraniteUiSyntheticResource} instance)
   * @param name Child resource name
   * @param resourceType Resource type
   * @return Resource
   */
  public static Resource child(@NotNull Resource parentResource, @NotNull String name, @NotNull String resourceType) {
    return child(parentResource, name, resourceType, ValueMap.EMPTY);
  }

  /**
   * Create synthetic resource child resource of the given parent resource.
   * @param parentResource Parent resource (has to be a {@link GraniteUiSyntheticResource} instance)
   * @param name Child resource name
   * @param resourceType Resource type
   * @param valueMap Properties
   * @return Resource
   */
  public static Resource child(@NotNull Resource parentResource, @NotNull String name, @NotNull String resourceType,
      @NotNull ValueMap valueMap) {
    Resource child = new GraniteUiSyntheticResource(parentResource.getResourceResolver(),
        parentResource.getPath() + "/" + name,
        resourceType,
        valueMap,
        Collections.emptyList());
    if (parentResource instanceof GraniteUiSyntheticResource) {
      ((GraniteUiSyntheticResource)parentResource).addChild(child);
    }
    else {
      throw new IllegalArgumentException("Resource is not a GraniteUiSyntheticResource.");
    }
    return child;
  }

  /**
   * Copy the given source resource as synthetic child under the target parent resource, including all children.
   * @param targetParent Target parent resource
   * @param source Source resource
   */
  public static void copySubtree(@NotNull Resource targetParent, @NotNull Resource source) {
    Resource targetChild = child(targetParent, source.getName(), source.getResourceType(), source.getValueMap());
    for (Resource sourceChild : source.getChildren()) {
      copySubtree(targetChild, sourceChild);
    }
  }

}