/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.sling.resourcemerger.impl;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
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.resource.ValueMap;
import org.apache.sling.resourcemerger.spi.MergedResourcePicker2;
import org.apache.sling.spi.resource.provider.ResolveContext;
import org.apache.sling.spi.resource.provider.ResourceContext;
import org.apache.sling.spi.resource.provider.ResourceProvider;
public class MergingResourceProvider extends ResourceProvider<Void> {
protected final String mergeRootPath;
protected final MergedResourcePicker2 picker;
private final boolean readOnly;
protected final boolean traverseHierarchie;
MergingResourceProvider(final String mergeRootPath,
final MergedResourcePicker2 picker,
final boolean readOnly,
final boolean traverseHierarchie) {
this.mergeRootPath = mergeRootPath;
this.picker = picker;
this.readOnly = readOnly;
this.traverseHierarchie = traverseHierarchie;
}
protected static final class ExcludeEntry {
public final String name;
public final boolean exclude;
public final boolean onlyUnderlying; // if only underlying resources should be affected (and not the local ones)
public ExcludeEntry(final String value, boolean onlyUnderlying) {
this.onlyUnderlying = onlyUnderlying;
if ( value.startsWith("!!") ) {
this.name = value.substring(1);
this.exclude = false;
} else if ( value.startsWith("!") ) {
this.name = value.substring(1);
this.exclude = true;
} else {
this.name = value;
this.exclude = false;
}
}
}
/**
* Class to check whether a child resource must be hidden. It should not be instantiated for the underlying resource
* tree (which is /libs by default) because this check is expensive.
*/
protected static final class ParentHidingHandler {
private List<ExcludeEntry> entries = new ArrayList<ExcludeEntry>();
/**
*
* @param parent the underlying resource
* @param traverseParent if true will also continue with the parent's parent recursively
*/
public ParentHidingHandler(final Resource parent, final boolean traverseParent) {
// evaluate the sling:hideChildren property on the current resource
final ValueMap parentProps = parent.getValueMap();
final String[] childrenToHideArray = parentProps.get(MergedResourceConstants.PN_HIDE_CHILDREN, String[].class);
if (childrenToHideArray != null) {
for (final String value : childrenToHideArray) {
final boolean onlyUnderlying;
if (value.equals("*")) {
onlyUnderlying = true;
} else {
onlyUnderlying = false;
}
final ExcludeEntry entry = new ExcludeEntry(value, onlyUnderlying);
this.entries.add(entry);
}
}
// also check on the parent's parent whether that was hiding the parent
if (parent != null) {
Resource ancestor = parent.getParent();
String previousAncestorName = parent.getName();
while (ancestor != null) {
final ValueMap ancestorProps = ancestor.getValueMap();
final String[] ancestorChildrenToHideArray = ancestorProps.get(MergedResourceConstants.PN_HIDE_CHILDREN, String[].class);
if (ancestorChildrenToHideArray != null) {
for (final String value : ancestorChildrenToHideArray) {
final ExcludeEntry entry = new ExcludeEntry(value, false);
final Boolean hides = hides(entry, previousAncestorName, true);
if (hides != null && hides.booleanValue() == true) {
this.entries.add(new ExcludeEntry("*", false));
break;
}
}
}
if ( !traverseParent ) {
break;
}
previousAncestorName = ancestor.getName();
ancestor = ancestor.getParent();
}
}
}
/**
*
* @param name the name of the resource to check
* @param isLocalResource {@code true} if the check is on a local resource, {@code false} if the check is on an underlying/inherited resource
* @return {@code true} if the local/inherited resource should be hidden, otherwise {@code false}
*/
public boolean isHidden(final String name, boolean isLocalResource) {
boolean hidden = false;
if ( this.entries != null ) {
for(final ExcludeEntry entry : this.entries) {
Boolean result = hides(entry, name, isLocalResource);
if (result != null) {
hidden = result.booleanValue();
break;
}
}
}
return hidden;
}
/**
* Determine if an entry should hide the named resource.
*
* @return a non-null value if the entry matches; a null value if it does not
*/
private Boolean hides(final ExcludeEntry entry, final String name, boolean isLocalResource) {
Boolean result = null;
if (entry.name.equals("*") || entry.name.equals(name)) {
if ((isLocalResource && !entry.onlyUnderlying) || !isLocalResource) {
result = Boolean.valueOf(!entry.exclude);
}
}
return result;
}
}
protected static final class ResourceHolder {
public final String name;
public final List<Resource> resources = new ArrayList<Resource>();
public final List<ValueMap> valueMaps = new ArrayList<ValueMap>();
public ResourceHolder(final String n) {
this.name = n;
}
}
/**
* Create the merged resource based on the provided resources
*/
private Resource createMergedResource(final ResourceResolver resolver, final String relativePath,
final ResourceHolder holder) {
int index = 0;
while (index < holder.resources.size()) {
final Resource baseRes = holder.resources.get(index);
// check if resource is hidden
final ValueMap props = baseRes.getValueMap();
holder.valueMaps.add(props);
if (props.get(MergedResourceConstants.PN_HIDE_RESOURCE, Boolean.FALSE)) {
// clear everything up to now
for (int i = 0; i <= index; i++) {
holder.resources.remove(0);
}
holder.valueMaps.clear();
index = 0; // start at zero
} else {
index++;
}
}
if (!holder.resources.isEmpty()) {
// create a new merged resource based on the list of mapped physical resources
if ( this.readOnly ) {
return new MergedResource(resolver, mergeRootPath, relativePath, holder.resources, holder.valueMaps);
}
return new CRUDMergedResource(resolver, mergeRootPath, relativePath, holder.resources, holder.valueMaps, this.picker);
}
return null;
}
/**
* Gets the relative path out of merge root path
*
* @param path Absolute path
* @return Relative path
*/
protected String getRelativePath(String path) {
if (path.startsWith(mergeRootPath)) {
path = path.substring(mergeRootPath.length());
if (path.length() == 0) {
return path;
} else if (path.charAt(0) == '/') {
return path.substring(1);
}
}
return null;
}
@Override
public Resource getParent(ResolveContext<Void> ctx, Resource child) {
final String parentPath = ResourceUtil.getParent(child.getPath());
if (parentPath == null) {
return null;
}
return this.getResource(ctx, parentPath, ResourceContext.EMPTY_CONTEXT, child);
}
/**
* {@inheritDoc}
*/
@Override
public Resource getResource(final ResolveContext<Void> ctx, final String path, final ResourceContext rCtx, final Resource parent) {
final String relativePath = getRelativePath(path);
if (relativePath != null) {
final ResourceHolder holder = new ResourceHolder(ResourceUtil.getName(path));
final ResourceResolver resolver = ctx.getResourceResolver();
final Iterator<Resource> resources = picker.pickResources(resolver, relativePath, parent).iterator();
if (!resources.hasNext()) {
return null;
}
boolean isUnderlying = true;
while (resources.hasNext()) {
final Resource resource = resources.next();
final boolean hidden;
if (isUnderlying) {
hidden = false;
isUnderlying = false;
} else {
// check parent for hiding
// SLING-3521 : if parent is not readable, nothing is hidden
final Resource resourceParent = resource.getParent();
hidden = resourceParent != null && new ParentHidingHandler(resourceParent, this.traverseHierarchie).isHidden(holder.name, true);
// TODO Usually, the parent does not exist if the resource is a NonExistingResource. Ideally, this
// common case should be optimised
}
if (hidden) {
holder.resources.clear();
} else if (!ResourceUtil.isNonExistingResource(resource)) {
holder.resources.add(resource);
}
}
return createMergedResource(resolver, relativePath, holder);
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public Iterator<Resource> listChildren(final ResolveContext<Void> ctx, final Resource parent) {
final ResourceResolver resolver = parent.getResourceResolver();
final String relativePath = getRelativePath(parent.getPath());
if (relativePath != null) {
final List<ResourceHolder> candidates = new ArrayList<ResourceHolder>();
final Iterator<Resource> resources = picker.pickResources(resolver, relativePath, parent).iterator();
boolean isUnderlying = true;
while (resources.hasNext()) {
Resource parentResource = resources.next();
final ParentHidingHandler handler = !isUnderlying ? new ParentHidingHandler(parentResource, this.traverseHierarchie) : null;
isUnderlying = false;
// remove the hidden child resources from the underlying resource
if (handler != null) {
final Iterator<ResourceHolder> iter = candidates.iterator();
while (iter.hasNext()) {
final ResourceHolder holder = iter.next();
if (handler.isHidden(holder.name, false)) {
iter.remove();
}
}
}
for (final Resource child : parentResource.getChildren()) {
final String rsrcName = child.getName();
ResourceHolder holder = null;
int childPositionInCandidateList = -1;
// check if is this an overlaid resource (i.e. has the resource with the same name already be exposed through the underlying resource)
for (int index=0; index < candidates.size(); index++) {
ResourceHolder current = candidates.get(index);
if (current.name.equals(rsrcName)) {
holder = current;
childPositionInCandidateList = index;
break;
}
}
if (holder == null) {
// remove the hidden child resources from the local resource
if (handler != null && handler.isHidden(rsrcName, true)) {
continue; // skip this child
}
holder = new ResourceHolder(rsrcName);
candidates.add(holder);
}
holder.resources.add(child);
// Check if children need reordering
int orderBeforeIndex = -1;
final ValueMap vm = child.getValueMap();
final String orderBefore = vm.get(MergedResourceConstants.PN_ORDER_BEFORE, String.class);
if (orderBefore != null && !orderBefore.equals(rsrcName)) {
// search entry
int index = 0;
while (index < candidates.size()) {
final ResourceHolder current = candidates.get(index);
if (current.name.equals(orderBefore)) {
orderBeforeIndex = index;
break;
}
index++;
}
}
if (orderBeforeIndex > -1) {
candidates.add(orderBeforeIndex, holder);
candidates.remove(candidates.size() - 1);
} else {
// if there was no explicit order, just assume the order given by the overlying resource
if (childPositionInCandidateList != -1) {
candidates.add(holder);
candidates.remove(childPositionInCandidateList);
}
}
}
}
final List<Resource> children = new ArrayList<Resource>();
for (final ResourceHolder holder : candidates) {
final Resource mergedResource = this.createMergedResource(resolver,
(relativePath.length() == 0 ? holder.name : relativePath + '/' + holder.name), holder);
if (mergedResource != null) {
children.add(mergedResource);
}
}
return children.iterator();
}
return null;
}
}