blob: 8cb5d1d44d70e1692276bc0f2059ac6ac2025441 [file] [log] [blame]
/*
* 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.testing.resourceresolver;
import java.io.Closeable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import org.apache.sling.api.SlingConstants;
import org.apache.sling.api.SlingException;
import org.apache.sling.api.adapter.SlingAdaptable;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.NonExistingResource;
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.resource.ValueMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
public class MockResourceResolver extends SlingAdaptable implements ResourceResolver {
private final Map<String, Map<String, Object>> resources;
private final Map<String, Map<String, Object>> temporaryResources = new LinkedHashMap<String, Map<String,Object>>();
private final Set<String> deletedResources = new HashSet<String>();
private final MockResourceResolverFactoryOptions options;
private final MockResourceResolverFactory factory;
private final Map<String,Object> attributes;
private Map<String,Object> propertyMap;
private final List<MockFindResourcesHandler> findResourcesHandlers = new ArrayList<>();
private final List<MockQueryResourceHandler> queryResourcesHandlers = new ArrayList<>();
public MockResourceResolver(final MockResourceResolverFactoryOptions options,
final MockResourceResolverFactory factory,
final Map<String, Map<String, Object>> resources) {
this(options, factory, resources, Collections.<String,Object>emptyMap());
}
public MockResourceResolver(final MockResourceResolverFactoryOptions options,
final MockResourceResolverFactory factory,
final Map<String, Map<String, Object>> resources,
final Map<String,Object> attributes) {
this.factory = factory;
this.options = options;
this.resources = resources;
this.attributes = attributes;
}
@Override
@SuppressWarnings("unused")
public @NotNull Resource resolve(final @NotNull HttpServletRequest request, final @NotNull String absPath) {
String path = absPath;
if (path == null) {
path = "/";
}
// split off query string or fragment that may be appendend to the URL
String urlRemainder = null;
int urlRemainderPos = Math.min(path.indexOf('?'), path.indexOf('#'));
if (urlRemainderPos >= 0) {
urlRemainder = path.substring(urlRemainderPos);
path = path.substring(0, urlRemainderPos);
}
// unmangle namespaces
if (options.isMangleNamespacePrefixes()) {
path = NamespaceMangler.unmangleNamespaces(path);
}
// build full path again
path = path + (urlRemainder != null ? urlRemainder : "");
Resource resource = this.getResource(path);
if (resource == null) {
resource = new NonExistingResource(this, absPath);
}
return resource;
}
@Override
@SuppressWarnings("null")
public @NotNull Resource resolve(final @NotNull String absPath) {
return resolve(null, absPath);
}
@Override
@SuppressWarnings("null")
public @NotNull String map(final @NotNull String resourcePath) {
return map(null, resourcePath);
}
@Override
public String map(final @NotNull HttpServletRequest request, final @NotNull String resourcePath) {
String path = resourcePath;
// split off query string or fragment that may be appendend to the URL
String urlRemainder = null;
int urlRemainderPos = Math.min(path.indexOf('?'), path.indexOf('#'));
if (urlRemainderPos >= 0) {
urlRemainder = path.substring(urlRemainderPos);
path = path.substring(0, urlRemainderPos);
}
// mangle namespaces
if (options.isMangleNamespacePrefixes()) {
path = NamespaceMangler.mangleNamespaces(path);
}
// build full path again
return path + (urlRemainder != null ? urlRemainder : "");
}
@Override
public Resource getResource(final @NotNull String path) {
Resource resource = getResourceInternal(path);
// if not resource found check if this is a reference to a property
if (resource == null && path != null) {
String parentPath = ResourceUtil.getParent(path);
if (parentPath != null) {
String name = ResourceUtil.getName(path);
Resource parentResource = getResourceInternal(parentPath);
if (parentResource!=null) {
ValueMap props = ResourceUtil.getValueMap(parentResource);
if (props.containsKey(name)) {
return new MockPropertyResource(path, props, this);
}
}
}
}
return resource;
}
private Resource getResourceInternal(final String path) {
if (path == null) {
return null;
}
String normalizedPath = ResourceUtil.normalize(path);
if (normalizedPath == null) {
return null;
} else if ( normalizedPath.startsWith("/") ) {
if ( this.deletedResources.contains(normalizedPath) ) {
return null;
}
final Map<String, Object> tempProps = this.temporaryResources.get(normalizedPath);
if ( tempProps != null ) {
return newMockResource(normalizedPath, tempProps, this);
}
synchronized ( this.resources ) {
final Map<String, Object> props = this.resources.get(normalizedPath);
if ( props != null ) {
return newMockResource(normalizedPath, props, this);
}
}
} else {
for(final String s : this.getSearchPath() ) {
final Resource rsrc = this.getResource(s + '/' + normalizedPath);
if ( rsrc != null ) {
return rsrc;
}
}
}
return null;
}
@Override
public Resource getResource(Resource base, @NotNull String path) {
if ( path == null || path.length() == 0 ) {
path = "/";
}
if ( path.startsWith("/") ) {
return getResource(path);
}
if ( base.getPath().equals("/") ) {
return getResource(base.getPath() + path);
}
return getResource(base.getPath() + '/' + path);
}
@Override
public @NotNull String[] getSearchPath() {
return this.options.getSearchPaths();
}
@Override
public @NotNull Iterator<Resource> listChildren(final @NotNull Resource parent) {
final String pathPrefix = "/".equals(parent.getPath()) ? "" : parent.getPath();
final Pattern childPathMatcher = Pattern.compile("^" + Pattern.quote(pathPrefix) + "/[^/]+$");
final Map<String, Map<String, Object>> candidates = new LinkedHashMap<String, Map<String,Object>>();
synchronized ( this.resources ) {
for(final Map.Entry<String, Map<String, Object>> e : this.resources.entrySet()) {
if (childPathMatcher.matcher(e.getKey()).matches()) {
if ( !this.deletedResources.contains(e.getKey()) ) {
candidates.put(e.getKey(), e.getValue());
}
}
}
for(final Map.Entry<String, Map<String, Object>> e : this.temporaryResources.entrySet()) {
if (childPathMatcher.matcher(e.getKey()).matches()) {
if ( !this.deletedResources.contains(e.getKey()) ) {
candidates.put(e.getKey(), e.getValue());
}
}
}
}
final List<Resource> children = new ArrayList<Resource>();
for(final Map.Entry<String, Map<String, Object>> e : candidates.entrySet()) {
children.add(newMockResource(e.getKey(), e.getValue(), this));
}
return children.iterator();
}
private Resource newMockResource(final String path,
final Map<String, Object> properties,
final ResourceResolver resolver) {
return this.options.getMockResourceFactory()
.newMockResource(path, properties, resolver);
}
@Override
public @NotNull Iterable<Resource> getChildren(final @NotNull Resource parent) {
return new Iterable<Resource>() {
@Override
public Iterator<Resource> iterator() {
return listChildren(parent);
}
};
}
@Override
public boolean isLive() {
return true;
}
@Override
public void close() {
clearPropertyMap();
this.factory.closed(this);
}
private void clearPropertyMap(){
if (propertyMap != null) {
for (Entry<String, Object> entry : propertyMap.entrySet()) {
if (entry.getValue() instanceof Closeable) {
try {
((Closeable) entry.getValue()).close();
} catch (Exception e) {
// ignore
}
}
}
propertyMap.clear();
}
}
@Override
public String getUserID() {
return null;
}
@Override
public @NotNull Iterator<String> getAttributeNames() {
return attributes.keySet().iterator();
}
@Override
public Object getAttribute(final @NotNull String name) {
return attributes.get(name);
}
@Override
public void delete(final @NotNull Resource resource) throws PersistenceException {
this.deletedResources.add(resource.getPath());
this.temporaryResources.remove(resource.getPath());
final String prefixPath = resource.getPath() + '/';
synchronized ( this.resources ) {
for(final Map.Entry<String, Map<String, Object>> e : this.resources.entrySet()) {
if (e.getKey().startsWith(prefixPath)) {
this.deletedResources.add(e.getKey());
}
}
final Iterator<Map.Entry<String, Map<String, Object>>> i = this.temporaryResources.entrySet().iterator();
while ( i.hasNext() ) {
final Map.Entry<String, Map<String, Object>> e = i.next();
if (e.getKey().startsWith(prefixPath) ) {
i.remove();
}
}
}
}
@Override
public @NotNull Resource create(@NotNull Resource parent, @NotNull String name,
Map<String, Object> properties) throws PersistenceException {
final String path = (parent.getPath().equals("/") ? parent.getPath() + name : parent.getPath() + '/' + name);
if ( this.temporaryResources.containsKey(path) ) {
throw new PersistenceException("Path already exists: " + path);
}
synchronized ( this.resources ) {
if ( this.resources.containsKey(path) && !this.deletedResources.contains(path) ) {
throw new PersistenceException("Path already exists: " + path);
}
}
this.deletedResources.remove(path);
if ( properties == null ) {
properties = new HashMap<String, Object>();
}
Resource mockResource = newMockResource(path, properties, this);
this.temporaryResources.put(path, ResourceUtil.getValueMap(mockResource));
return mockResource;
}
@Override
public void revert() {
this.deletedResources.clear();
this.temporaryResources.clear();
}
@Override
@SuppressWarnings("deprecation")
public void commit() throws PersistenceException {
EventAdmin eventAdmin = this.options.getEventAdmin();
synchronized ( this.resources ) {
for(final String path : this.deletedResources ) {
if ( this.resources.remove(path) != null && eventAdmin != null ) {
final Dictionary<String, Object> props = new Hashtable<String, Object>();
props.put(SlingConstants.PROPERTY_PATH, path);
final Event e = new Event(SlingConstants.TOPIC_RESOURCE_REMOVED, props);
eventAdmin.sendEvent(e);
}
this.temporaryResources.remove(path);
}
for(final String path : this.temporaryResources.keySet() ) {
final boolean changed = this.resources.containsKey(path);
this.resources.put(path, this.temporaryResources.get(path));
if ( eventAdmin != null ) {
final Dictionary<String, Object> props = new Hashtable<String, Object>();
props.put(SlingConstants.PROPERTY_PATH, path);
if ( this.resources.get(path).get(ResourceResolver.PROPERTY_RESOURCE_TYPE) != null ) {
props.put(SlingConstants.PROPERTY_RESOURCE_TYPE, this.resources.get(path).get(ResourceResolver.PROPERTY_RESOURCE_TYPE));
}
final Event e = new Event(changed ? SlingConstants.TOPIC_RESOURCE_CHANGED : SlingConstants.TOPIC_RESOURCE_ADDED, props);
eventAdmin.sendEvent(e);
}
}
}
this.revert();
}
@Override
public boolean hasChanges() {
return this.temporaryResources.size() > 0 || this.deletedResources.size() > 0;
}
@Override
public boolean isResourceType(Resource resource, String resourceType) {
boolean result = false;
if ( resource != null && resourceType != null ) {
// Check if the resource is of the given type. This method first checks the
// resource type of the resource, then its super resource type and continues
// to go up the resource super type hierarchy.
if (ResourceTypeUtil.areResourceTypesEqual(resourceType, resource.getResourceType(), getSearchPath())) {
result = true;
} else {
Set<String> superTypesChecked = new HashSet<>();
String superType = this.getParentResourceType(resource);
while (!result && superType != null) {
if (ResourceTypeUtil.areResourceTypesEqual(resourceType, superType, getSearchPath())) {
result = true;
} else {
superTypesChecked.add(superType);
superType = this.getParentResourceType(superType);
if (superType != null && superTypesChecked.contains(superType)) {
throw new SlingException("Cyclic dependency for resourceSuperType hierarchy detected on resource " + resource.getPath()) {
// anonymous class to avoid problem with null cause
private static final long serialVersionUID = 1L;
};
}
}
}
}
}
return result;
}
@Override
public void refresh() {
// nothing to do
}
public void addChanged(final String path, final Map<String, Object> props) {
this.temporaryResources.put(path, props);
}
@Override
public String getParentResourceType(Resource resource) {
String resourceSuperType = null;
if ( resource != null ) {
resourceSuperType = resource.getResourceSuperType();
if (resourceSuperType == null) {
resourceSuperType = this.getParentResourceType(resource.getResourceType());
}
}
return resourceSuperType;
}
@Override
public String getParentResourceType(String resourceType) {
// normalize resource type to a path string
final String rtPath = (resourceType == null ? null : ResourceUtil.resourceTypeToPath(resourceType));
// get the resource type resource and check its super type
String resourceSuperType = null;
if ( rtPath != null ) {
final Resource rtResource = getResource(rtPath);
if (rtResource != null) {
resourceSuperType = rtResource.getResourceSuperType();
}
}
return resourceSuperType;
}
@Override
public boolean hasChildren(@NotNull Resource resource) {
return this.listChildren(resource).hasNext();
}
@Override
public Resource getParent(@NotNull Resource child) {
final String parentPath = ResourceUtil.getParent(child.getPath());
if (parentPath == null) {
return null;
}
return this.getResource(parentPath);
}
@Override
@SuppressWarnings("null")
public @NotNull Iterator<Resource> findResources(final @NotNull String query, final String language) {
return findResourcesHandlers.stream()
.map(handler -> handler.findResources(query, language))
.filter(Objects::nonNull)
.findFirst()
.orElse(Collections.emptyIterator());
}
/**
* Adds a handler that can provide a mocked find resources result. You can add multiple handlers which are called
* in the order they were added when calling {@link #findResources(String, String)}.
* The result of the first handler that returns a non-null result is used.
* If no handler delivers a result, an empty result is returned.
* @param handler Handler
*/
public void addFindResourceHandler(@NotNull MockFindResourcesHandler handler) {
findResourcesHandlers.add(handler);
}
@Override
@SuppressWarnings("null")
public @NotNull Iterator<Map<String, Object>> queryResources(@NotNull String query, String language) {
return queryResourcesHandlers.stream()
.map(handler -> handler.queryResources(query, language))
.filter(Objects::nonNull)
.findFirst()
.orElse(Collections.emptyIterator());
}
/**
* Adds a handler that can provide a mocked query resources result. You can add multiple handlers which are called
* in the order they were added when calling {@link #queryResources(String, String)}.
* The result of the first handler that returns a non-null result is used.
* If no handler delivers a result, an empty result is returned.
* @param handler Handler
*/
public void addQueryResourceHandler(@NotNull MockQueryResourceHandler handler) {
queryResourcesHandlers.add(handler);
}
// Sling API 2.24.0
public @NotNull Map<String, Object> getPropertyMap() {
if (propertyMap == null) {
propertyMap = new HashMap<>();
}
return propertyMap;
}
// --- unsupported operations ---
@Override
@Deprecated
public @NotNull Resource resolve(final @NotNull HttpServletRequest request) {
throw new UnsupportedOperationException();
}
@Override
public @NotNull ResourceResolver clone(Map<String, Object> authenticationInfo) throws LoginException {
throw new UnsupportedOperationException();
}
@Override
public Resource copy(String srcAbsPath, String destAbsPath) throws PersistenceException {
throw new UnsupportedOperationException();
}
@Override
public Resource move(String srcAbsPath, String destAbsPath) throws PersistenceException {
throw new UnsupportedOperationException();
}
// Sling API 2.24.0
public boolean orderBefore(@NotNull Resource parent, @NotNull String name,
@Nullable String followingSiblingName) throws UnsupportedOperationException, PersistenceException, IllegalArgumentException {
throw new UnsupportedOperationException();
}
}