blob: 2731fe1744243fe0a97c1a89257abd80a0f1761c [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.api.resource;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import org.apache.sling.api.wrappers.ValueMapDecorator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* The <code>ResourceUtil</code> class provides helper methods dealing with
* resources.
* <p>
* This class is not intended to be extended or instantiated because it just
* provides static utility methods not intended to be overwritten.
*/
public class ResourceUtil {
/**
* Resolves relative path segments '.' and '..' in the absolute path.
* Returns {@code null} if not possible (.. points above root) or if path is not
* absolute.
*
* @param path The path to normalize
* @return The normalized path or {@code null}.
*/
public static @Nullable String normalize(@NotNull String path) {
// don't care for empty paths
if (path.length() == 0) {
return path;
}
// prepare the path buffer with trailing slash (simplifies impl)
int absOffset = (path.charAt(0) == '/') ? 0 : 1;
char[] buf = new char[path.length() + 1 + absOffset];
if (absOffset == 1) {
buf[0] = '/';
}
path.getChars(0, path.length(), buf, absOffset);
buf[buf.length - 1] = '/';
int lastSlash = 0; // last slash in path
int numDots = 0; // number of consecutive dots after last slash
int bufPos = 0;
for (int bufIdx = lastSlash; bufIdx < buf.length; bufIdx++) {
char c = buf[bufIdx];
if (c == '/') {
if (numDots == 2) {
if (bufPos == 0) {
return null;
}
do {
bufPos--;
} while (bufPos > 0 && buf[bufPos] != '/');
}
lastSlash = bufIdx;
numDots = 0;
} else if (c == '.' && useDot(buf, bufIdx) && numDots < 2) {
numDots++;
} else {
// find the next slash
int nextSlash = bufIdx + 1;
while (nextSlash < buf.length && buf[nextSlash] != '/') {
nextSlash++;
}
// append up to the next slash (or end of path)
if (bufPos < lastSlash) {
int segLen = nextSlash - bufIdx + 1;
System.arraycopy(buf, lastSlash, buf, bufPos, segLen);
bufPos += segLen;
} else {
bufPos = nextSlash;
}
numDots = 0;
lastSlash = nextSlash;
bufIdx = nextSlash;
}
}
String resolved;
if (bufPos == 0 && numDots == 0) {
resolved = (absOffset == 0) ? "/" : "";
} else if ((bufPos - absOffset) == path.length()) {
resolved = path;
} else {
resolved = new String(buf, absOffset, bufPos - absOffset);
}
return resolved;
}
// use this dot only if followed by /
// don't use if followed by neither . nor /
// keep checking till a non-dot is found
private static boolean useDot(char[] buf, int bufIdx) {
while(bufIdx < buf.length -1) {
if(buf[bufIdx] == '/') {
return true;
}
else if(buf[bufIdx] != '.') {
return false;
}
bufIdx++;
}
return true;
}
/**
* Utility method returns the parent path of the given <code>path</code>,
* which is normalized by {@link #normalize(String)} before resolving the
* parent.
*
* @param path The path whose parent is to be returned.
* @return <code>null</code> if <code>path</code> is the root path (
* <code>/</code>) or if <code>path</code> is a single name
* containing no slash (<code>/</code>) characters.
* @throws IllegalArgumentException If the path cannot be normalized by the
* {@link #normalize(String)} method.
* @throws NullPointerException If <code>path</code> is <code>null</code>.
*/
public static @Nullable String getParent(@NotNull String path) {
if ("/".equals(path)) {
return null;
}
// normalize path (remove . and ..)
path = normalize(path);
// if normalized to root, there is no parent
if (path == null || "/".equals(path)) {
return null;
}
String workspaceName = null;
final int wsSepPos = path.indexOf(":/");
if (wsSepPos != -1) {
workspaceName = path.substring(0, wsSepPos);
path = path.substring(wsSepPos + 1);
}
// find the last slash, after which to cut off
int lastSlash = path.lastIndexOf('/');
if (lastSlash < 0) {
// no slash in the path
return null;
} else if (lastSlash == 0) {
// parent is root
if (workspaceName != null) {
return workspaceName + ":/";
}
return "/";
}
String parentPath = path.substring(0, lastSlash);
if (workspaceName != null) {
return workspaceName + ":" + parentPath;
}
return parentPath;
}
/**
* Utility method returns the ancestor's path at the given <code>level</code>
* relative to <code>path</code>, which is normalized by {@link #normalize(String)}
* before resolving the ancestor.
*
* <ul>
* <li><code>level</code> = 0 returns the <code>path</code>.</li>
* <li><code>level</code> = 1 returns the parent of <code>path</code>, if it exists, <code>null</code> otherwise.</li>
* <li><code>level</code> = 2 returns the grandparent of <code>path</code>, if it exists, <code>null</code> otherwise.</li>
* </ul>
*
* @param path The path whose ancestor is to be returned.
* @param level The relative level of the ancestor, relative to <code>path</code>.
* @return <code>null</code> if <code>path</code> doesn't have an ancestor at the
* specified <code>level</code>.
* @throws IllegalArgumentException If the path cannot be normalized by the
* {@link #normalize(String)} method or if <code>level</code> &lt; 0.
* @throws NullPointerException If <code>path</code> is <code>null</code>.
* @since 2.2 (Sling API Bundle 2.2.0)
*/
public static String getParent(final String path, final int level) {
if ( level < 0 ) {
throw new IllegalArgumentException("level must be non-negative");
}
String result = path;
for(int i=0; i<level; i++) {
result = getParent(result);
if ( result == null ) {
break;
}
}
return result;
}
/**
* Utility method returns the parent resource of the resource.
*
* @param rsrc The resource to get the parent of.
* @return The parent resource or null if the rsrc is the root.
* @throws NullPointerException If <code>rsrc</code> is <code>null</code>.
* @throws org.apache.sling.api.SlingException If an error occurs trying to
* get the resource object from the path.
* @throws IllegalStateException if the resource resolver has already been
* closed}.
* @deprecated since 2.1.0, use {@link Resource#getParent()} instead
*/
@Deprecated
public static @Nullable Resource getParent(@NotNull Resource rsrc) {
return rsrc.getParent();
}
/**
* Utility method returns the name of the resource.
*
* @param rsrc The resource to get the name from.
* @return The name of the resource
* @throws NullPointerException If <code>rsrc</code> is <code>null</code>.
* @deprecated since 2.1.0, use {@link Resource#getName()} instead
*/
@Deprecated
public static @NotNull String getName(@NotNull Resource rsrc) {
/*
* Same as AbstractResource.getName() implementation to prevent problems
* if there are implementations of the pre-2.1.0 Resource interface in
* the framework.
*/
return getName(rsrc.getPath());
}
/**
* Utility method returns the name of the given <code>path</code>, which is
* normalized by {@link #normalize(String)} before resolving the name.
*
* @param path The path whose name (the last path element) is to be
* returned.
* @return The empty string if <code>path</code> is the root path (
* <code>/</code>) or if <code>path</code> is a single name
* containing no slash (<code>/</code>) characters.
* @throws IllegalArgumentException If the path cannot be normalized by the
* {@link #normalize(String)} method.
* @throws NullPointerException If <code>path</code> is <code>null</code>.
*/
public static @NotNull String getName(@NotNull String path) {
if ("/".equals(path)) {
return "";
}
// normalize path (remove . and ..)
path = normalize(path);
if ("/".equals(path)) {
return "";
}
// find the last slash
return path.substring(path.lastIndexOf('/') + 1);
}
/**
* Returns <code>true</code> if the resource <code>res</code> is a synthetic
* resource.
* <p>
* This method checks whether the resource is an instance of the
* <code>org.apache.sling.resource.SyntheticResource</code> class.
*
* @param res The <code>Resource</code> to check whether it is a synthetic
* resource.
* @return <code>true</code> if <code>res</code> is a synthetic resource.
* <code>false</code> is returned if <code>res</code> is
* <code>null</code> or not an instance of the
* <code>org.apache.sling.resource.SyntheticResource</code> class.
*/
public static boolean isSyntheticResource(@NotNull Resource res) {
if (res instanceof SyntheticResource) {
return true;
}
if (!(res instanceof ResourceWrapper)) {
return false;
}
do {
res = ((ResourceWrapper) res).getResource();
} while (res instanceof ResourceWrapper);
return res instanceof SyntheticResource;
}
/**
* Returns <code>true</code> if the resource <code>res</code> is a "star
* resource". A <i>star resource</i> is a resource returned from the
* <code>ResourceResolver.resolve(HttpServletRequest)</code> whose path
* terminates in a <code>/*</code>. Generally such resource result from
* requests to something like <code>/some/path/*</code> or
* <code>/some/path/*.html</code> which may be used web applications to
* uniformly handle resources to be created.
* <p>
* This method checks whether the resource path ends with a <code>/*</code>
* indicating such a star resource.
*
* @param res The <code>Resource</code> to check whether it is a star
* resource.
* @return <code>true</code> if <code>res</code> is to be considered a star
* resource.
* @throws NullPointerException if <code>res</code> is <code>null</code>.
*/
public static boolean isStarResource(@NotNull Resource res) {
return res.getPath().endsWith("/*");
}
/**
* Returns <code>true</code> if the resource <code>res</code> is a
* non-existing resource.
* <p>
* This method checks the resource type of the resource to match the
* well-known resource type <code>sling:nonexisting</code> of the
* <code>NonExistingResource</code> class defined in the Sling API.
*
* @param res The <code>Resource</code> to check whether it is a
* non-existing resource.
* @return <code>true</code> if <code>res</code> is to be considered a
* non-existing resource.
* @throws NullPointerException if <code>res</code> is <code>null</code>.
*/
public static boolean isNonExistingResource(@NotNull Resource res) {
return Resource.RESOURCE_TYPE_NON_EXISTING.equals(res.getResourceType());
}
/**
* Returns an <code>Iterator</code> of {@link Resource} objects loaded from
* the children of the given <code>Resource</code>.
* <p>
* This is a convenience method for
* {@link ResourceResolver#listChildren(Resource)}.
*
* @param parent The {@link Resource Resource} whose children are requested.
* @return An <code>Iterator</code> of {@link Resource} objects.
* @throws NullPointerException If <code>parent</code> is <code>null</code>.
* @throws IllegalStateException if the resource resolver has already been
* closed}.
* @throws org.apache.sling.api.SlingException If any error occurs acquiring
* the child resource iterator.
* @see ResourceResolver#listChildren(Resource)
* @deprecated since 2.1.0, use {@link Resource#listChildren()} instead
*/
@Deprecated
public static @NotNull Iterator<Resource> listChildren(@NotNull Resource parent) {
// directly call the resource resolver to ensure the correct result
return parent.getResourceResolver().listChildren(parent);
}
/**
* Returns an <code>ValueMap</code> object for the given
* <code>Resource</code>. This method calls {@link Resource#getValueMap()}.
* If <code>null</code> is provided as the resource an empty map is returned as
* well.
* <p>For backward compatibility reasons the map returned is not immutable,
* but it is not recommend to rely on this behavior.</p>
*
* @param res The <code>Resource</code> to adapt to the value map.
* @return A value map.
*/
public static @NotNull ValueMap getValueMap(final Resource res) {
if ( res == null ) {
// use empty map
return new ValueMapDecorator(new HashMap<String, Object>());
}
return res.getValueMap();
}
/**
* Helper method, which returns the given resource type as returned from the
* {@link org.apache.sling.api.resource.Resource#getResourceType()} as a
* relative path.
*
* @param type The resource type to be converted into a path
* @return The resource type as a path.
* @since 2.0.6 (Sling API Bundle 2.0.6)
*/
public static @NotNull String resourceTypeToPath(@NotNull final String type) {
return type.replace(':', '/');
}
/**
* Returns the super type of the given resource type. This method converts
* the resource type to a resource path by calling
* {@link #resourceTypeToPath(String)} and uses the
* <code>resourceResolver</code> to get the corresponding resource. If the
* resource exists, the {@link Resource#getResourceSuperType()} method is
* called.
*
* @param resourceResolver The <code>ResourceResolver</code> used to access
* the resource whose path (relative or absolute) is given by the
* <code>resourceType</code> parameter.
* @param resourceType The resource type whose super type is to be returned.
* This type is turned into a path by calling the
* {@link #resourceTypeToPath(String)} method before trying to
* get the resource through the <code>resourceResolver</code>.
* @return the super type of the <code>resourceType</code> or
* <code>null</code> if the resource type does not exists or returns
* <code>null</code> for its super type.
* @throws IllegalStateException if the resource resolver has already been
* closed}.
* @since 2.0.6 (Sling API Bundle 2.0.6)
* @deprecated Use {@link ResourceResolver#getParentResourceType(String)}
*/
@Deprecated
public static @Nullable String getResourceSuperType(
final @NotNull ResourceResolver resourceResolver, final String resourceType) {
return resourceResolver.getParentResourceType(resourceType);
}
/**
* Returns the super type of the given resource. This method checks first if
* the resource itself knows its super type by calling
* {@link Resource#getResourceSuperType()}. If that returns
* <code>null</code> {@link #getResourceSuperType(ResourceResolver, String)}
* is invoked with the resource type of the resource.
*
* @param resource The resource to return the resource super type for.
* @return the super type of the <code>resource</code> or <code>null</code>
* if no super type could be computed.
* @throws IllegalStateException if the resource resolver has already been
* closed}.
* @since 2.0.6 (Sling API Bundle 2.0.6)
* @deprecated Use {@link ResourceResolver#getParentResourceType(Resource)}
*/
@Deprecated
public static @Nullable String findResourceSuperType(@NotNull final Resource resource) {
if ( resource == null ) {
return null;
}
return resource.getResourceResolver().getParentResourceType(resource);
}
/**
* 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.
*
* In case the type of the given resource or the given resource type starts with one of the resource resolver's search paths
* it is converted to a relative resource type by stripping off the resource resolver's search path
* before doing the comparison.
*
* @param resource the resource to check
* @param resourceType the resource type to check the resource against
* @return <code>false</code> if <code>resource</code> is <code>null</code>.
* Otherwise returns the result of calling
* {@link Resource#isResourceType(String)} with the given
* <code>resourceType</code>.
* @throws IllegalStateException if the resource resolver has already been
* closed}.
* @since 2.0.6 (Sling API Bundle 2.0.6)
* @deprecated Use {@link ResourceResolver#isResourceType(Resource, String)}
*/
@Deprecated
public static boolean isA(@NotNull final Resource resource, final String resourceType) {
if ( resource == null ) {
return false;
}
return resource.getResourceResolver().isResourceType(resource, resourceType);
}
/**
* Return an iterator for objects of the specified type. A new iterator is
* returned which tries to adapt the provided resources to the given type
* (using {@link Resource#adaptTo(Class)}. If a resource in the original
* iterator is not adaptable to the given class, this object is skipped.
* This implies that the number of objects returned by the new iterator
* might be less than the number of resource objects.
*
* @param iterator A resource iterator.
* @param <T> The adapted type
* @param type The adapted type
* @return An iterator of the adapted objects
* @since 2.0.6 (Sling API Bundle 2.0.6)
*/
public static @NotNull <T> Iterator<T> adaptTo(final @NotNull Iterator<Resource> iterator,
final Class<T> type) {
return new Iterator<T>() {
private T nextObject = seek();
@Override
public boolean hasNext() {
return nextObject != null;
}
@Override
public T next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
final T object = nextObject;
nextObject = seek();
return object;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
private T seek() {
T result = null;
while (result == null && iterator.hasNext()) {
final Resource r = iterator.next();
result = r.adaptTo(type);
}
return result;
}
};
}
/**
* Creates or gets the resource at the given path.
*
* @param resolver The resource resolver to use for creation
* @param path The full path to be created
* @param resourceType The optional resource type of the final resource to create
* @param intermediateResourceType THe optional resource type of all intermediate resources
* @param autoCommit If set to true, a commit is performed after each resource creation.
* @return The resource for the path.
* @throws org.apache.sling.api.SlingException If an error occurs trying to
* get/create the resource object from the path.
* @throws IllegalStateException if the resource resolver has already been
* closed}.
* @throws PersistenceException If a persistence error occurs.
* @since 2.3.0 (Sling API Bundle 2.4.0)
*/
public static @NotNull Resource getOrCreateResource(
final @NotNull ResourceResolver resolver,
final @NotNull String path,
final String resourceType,
final String intermediateResourceType,
final boolean autoCommit)
throws PersistenceException {
final Map<String, Object> props;
if ( resourceType == null ) {
props = null;
} else {
props = Collections.singletonMap(ResourceResolver.PROPERTY_RESOURCE_TYPE, (Object)resourceType);
}
return getOrCreateResource(resolver, path, props, intermediateResourceType, autoCommit);
}
/**
* Creates or gets the resource at the given path.
* If an exception occurs, it retries the operation up to five times if autoCommit is enabled.
* In this case, {@link ResourceResolver#revert()} is called on the resolver before the
* creation is retried.
*
* @param resolver The resource resolver to use for creation
* @param path The full path to be created
* @param resourceProperties The optional resource properties of the final resource to create
* @param intermediateResourceType THe optional resource type of all intermediate resources
* @param autoCommit If set to true, a commit is performed after each resource creation.
* @return The resource for the path.
* @throws org.apache.sling.api.SlingException If an error occurs trying to
* get/create the resource object from the path.
* @throws IllegalStateException if the resource resolver has already been
* closed}.
* @throws PersistenceException If a persistence error occurs.
* @since 2.3.0 (Sling API Bundle 2.4.0)
*/
public static @NotNull Resource getOrCreateResource(
final @NotNull ResourceResolver resolver,
final @NotNull String path,
final Map<String, Object> resourceProperties,
final String intermediateResourceType,
final boolean autoCommit)
throws PersistenceException {
PersistenceException mostRecentPE = null;
for(int i=0;i<5;i++) {
try {
return ResourceUtil.getOrCreateResourceInternal(resolver,
path,
resourceProperties,
intermediateResourceType,
autoCommit);
} catch ( final PersistenceException pe ) {
if ( autoCommit ) {
// in case of exception, revert to last clean state and retry
resolver.revert();
resolver.refresh();
mostRecentPE = pe;
} else {
throw pe;
}
}
}
throw mostRecentPE;
}
/**
* Creates or gets the resource at the given path.
*
* @param resolver The resource resolver to use for creation
* @param path The full path to be created
* @param resourceProperties The optional resource properties of the final resource to create
* @param intermediateResourceType THe optional resource type of all intermediate resources
* @param autoCommit If set to true, a commit is performed after each resource creation.
* @return The resource for the path.
* @throws org.apache.sling.api.SlingException If an error occurs trying to
* get/create the resource object from the path.
* @throws IllegalStateException if the resource resolver has already been
* closed}.
* @throws PersistenceException If a persistence error occurs.
*/
private static Resource getOrCreateResourceInternal(
final ResourceResolver resolver,
final String path,
final Map<String, Object> resourceProperties,
final String intermediateResourceType,
final boolean autoCommit)
throws PersistenceException {
Resource rsrc = resolver.getResource(path);
if ( rsrc == null ) {
final int lastPos = path.lastIndexOf('/');
final String name = path.substring(lastPos + 1);
final Resource parentResource;
if ( lastPos == 0 ) {
parentResource = resolver.getResource("/");
} else {
final String parentPath = path.substring(0, lastPos);
parentResource = getOrCreateResource(resolver,
parentPath,
intermediateResourceType,
intermediateResourceType,
autoCommit);
}
if ( autoCommit ) {
resolver.refresh();
}
try {
int retry = 5;
while ( retry > 0 && rsrc == null ) {
rsrc = resolver.create(parentResource, name, resourceProperties);
// check for SNS
if ( !name.equals(rsrc.getName()) ) {
resolver.refresh();
resolver.delete(rsrc);
rsrc = resolver.getResource(parentResource, name);
}
retry--;
}
if ( rsrc == null ) {
throw new PersistenceException("Unable to create resource.");
}
} catch ( final PersistenceException pe ) {
// this could be thrown because someone else tried to create this
// node concurrently
resolver.refresh();
rsrc = resolver.getResource(parentResource, name);
if ( rsrc == null ) {
throw pe;
}
}
if ( autoCommit ) {
try {
resolver.commit();
resolver.refresh();
rsrc = resolver.getResource(parentResource, name);
} catch ( final PersistenceException pe ) {
// try again - maybe someone else did create the resource in the meantime
// or we ran into Jackrabbit's stale item exception in a clustered environment
resolver.revert();
resolver.refresh();
rsrc = resolver.getResource(parentResource, name);
if ( rsrc == null ) {
rsrc = resolver.create(parentResource, name, resourceProperties);
resolver.commit();
}
}
}
}
return rsrc;
}
/**
* Create a unique name for a child of the <code>parent</code>.
* Creates a unique name and test if child already exists.
* If child resource with the same name exists, iterate until a unique one is found.
*
* @param parent The parent resource
* @param name The name of the child resource
* @return a unique non-existing name for child resource for a given <code>parent</code>
*
* @throws PersistenceException if it can not find unique name for child resource.
* @throws NullPointerException if <code>parent</code> is null
* @throws IllegalStateException if the resource resolver has already been
* closed}.
* @since 2.5 (Sling API Bundle 2.7.0)
*/
public static String createUniqueChildName(final Resource parent, final String name)
throws PersistenceException {
if (parent.getChild(name) != null) {
// leaf node already exists, create new unique name
String childNodeName = null;
int i = 0;
do {
childNodeName = name + String.valueOf(i);
//just so that it does not run into an infinite loop
// this should not happen though :)
if (i == Integer.MAX_VALUE) {
String message = MessageFormat.format("can not find a unique name {0} for {1}", name, parent.getPath());
throw new PersistenceException(message);
}
i++;
} while (parent.getChild(childNodeName) != null);
return childNodeName;
}
return name;
}
/**
* Unwrap the resource and return the wrapped implementation.
* @param rsrc The resource to unwrap
* @return The unwrapped resource
* @since 2.5 (Sling API Bundle 2.7.0)
*/
public static @NotNull Resource unwrap(final @NotNull Resource rsrc) {
Resource result = rsrc;
while ( result instanceof ResourceWrapper ) {
result = ((ResourceWrapper)result).getResource();
}
return result;
}
/**
* A batch resource remover deletes resources in batches. Once the batch
* size (threshold) is reached, an intermediate commit is performed. Resource
* trees are deleted resource by resource starting with the deepest children first.
* Once all resources have been passed to the batch resource remover, a final
* commit needs to be called on the resource resolver.
* @since 2.6 (Sling API Bundle 2.8.0)
*/
public static class BatchResourceRemover {
private final int max;
private int count;
public BatchResourceRemover(final int batchSize) {
this.max = (batchSize < 1 ? 50 : batchSize);
}
public void delete(@NotNull final Resource rsrc)
throws PersistenceException {
final ResourceResolver resolver = rsrc.getResourceResolver();
for(final Resource child : rsrc.getChildren()) {
delete(child);
}
resolver.delete(rsrc);
count++;
if ( count >= max ) {
resolver.commit();
count = 0;
}
}
}
/**
* Create a batch resource remover.
* A batch resource remove can be used to delete resources in batches.
* Once the passed in threshold of deleted resources is reached, an intermediate
* commit is called on the resource resolver. In addition the batch remover
* deletes a resource recursively.
* Once all resources to delete are passed to the remover, a final commit needs
* to be call on the resource resolver.
* @param threshold The threshold for the intermediate saves.
* @return A new batch resource remover.
* Since 2.6
*/
public static @NotNull BatchResourceRemover getBatchResourceRemover(final int threshold) {
return new BatchResourceRemover(threshold);
}
}