/*
* 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.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.Set;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import org.apache.sling.api.SlingConstants;
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.osgi.service.event.Event;
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;
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
public Resource resolve(final HttpServletRequest request, final 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
public Resource resolve(final String absPath) {
return resolve(null, absPath);
}
@Override
public String map(final String resourcePath) {
return map(null, resourcePath);
}
@Override
public String map(final HttpServletRequest request, final 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 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 ) {
final Resource rsrc = new MockResource(normalizedPath, tempProps, this);
return rsrc;
}
synchronized ( this.resources ) {
final Map<String, Object> props = this.resources.get(normalizedPath);
if ( props != null ) {
final Resource rsrc = new MockResource(normalizedPath, props, this);
return rsrc;
}
}
} 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, 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 String[] getSearchPath() {
return this.options.getSearchPaths();
}
@Override
public Iterator<Resource> listChildren(final 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(new MockResource(e.getKey(), e.getValue(), this));
}
return children.iterator();
}
// part of Resource API 2.5.0
public Iterable<Resource> getChildren(final Resource parent) {
return new Iterable<Resource>() {
@Override
public Iterator<Resource> iterator() {
return listChildren(parent);
}
};
}
@Override
public boolean isLive() {
return true;
}
@Override
public void close() {
this.factory.closed(this);
}
@Override
public String getUserID() {
return null;
}
@Override
public Iterator<String> getAttributeNames() {
return attributes.keySet().iterator();
}
@Override
public Object getAttribute(final String name) {
return attributes.get(name);
}
@Override
public void delete(final 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 Resource create(Resource parent, 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 = new MockResource(path, properties, this);
this.temporaryResources.put(path, ResourceUtil.getValueMap(mockResource));
return mockResource;
}
@Override
public void revert() {
this.deletedResources.clear();
this.temporaryResources.clear();
}
@Override
public void commit() throws PersistenceException {
synchronized ( this.resources ) {
for(final String path : this.deletedResources ) {
if ( this.resources.remove(path) != null && this.options.getEventAdmin() != 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);
this.options.getEventAdmin().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 ( this.options.getEventAdmin() != 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);
this.options.getEventAdmin().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) {
return resource.getResourceType().equals(resourceType);
}
@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;
}
// part of Resource API 2.6.0
public boolean hasChildren(Resource resource) {
return this.listChildren(resource).hasNext();
}
// part of Resource API 2.11.0
public Resource getParent(Resource child) {
final String parentPath = ResourceUtil.getParent(child.getPath());
if (parentPath == null) {
return null;
}
return this.getResource(parentPath);
}
// --- unsupported operations ---
@Override
@Deprecated
public Resource resolve(final HttpServletRequest request) {
throw new UnsupportedOperationException();
}
@Override
public Iterator<Resource> findResources(final String query, final String language) {
throw new UnsupportedOperationException();
}
@Override
public Iterator<Map<String, Object>> queryResources(String query, String language) {
throw new UnsupportedOperationException();
}
@Override
public ResourceResolver clone(Map<String, Object> authenticationInfo) throws LoginException {
throw new UnsupportedOperationException();
}
// part of Resource API 2.11.0
public Resource copy(String srcAbsPath, String destAbsPath) throws PersistenceException {
throw new UnsupportedOperationException();
}
// part of Resource API 2.11.0
public Resource move(String srcAbsPath, String destAbsPath) throws PersistenceException {
throw new UnsupportedOperationException();
}
}