blob: 250d8234646f01f63c8dfca6bb3bf4909be82cb4 [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.sitemap;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import javax.jcr.query.Query;
import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.util.ISO9075;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.annotation.versioning.ProviderType;
/**
* A utility class to give access to common functionality used for sitemaps.
*/
@ProviderType
public final class SitemapUtil {
private static final String JCR_SYSTEM_PATH = "/" + JcrConstants.JCR_SYSTEM + "/";
private static final String SITEMAP_SELECTOR = "sitemap";
private static final String SITEMAP_SELECTOR_SUFFIX = "-" + SITEMAP_SELECTOR;
private SitemapUtil() {
super();
}
/**
* Returns the {@link Resource} marked as sitemap root that is closest to the repository root starting with the
* given {@link Resource}.
*
* @param sitemapRoot
* @return
*/
@NotNull
public static Resource getTopLevelSitemapRoot(@NotNull Resource sitemapRoot) {
Resource topLevelSitemapRoot = sitemapRoot;
Resource parent = sitemapRoot.getParent();
while (parent != null) {
if (isSitemapRoot(parent)) {
topLevelSitemapRoot = parent;
}
parent = parent.getParent();
}
return topLevelSitemapRoot;
}
/**
* Returns the parent of the given {@link Resource} when the {@link Resource}'s name is
* {@link JcrConstants#JCR_CONTENT}.
*
* @param resource
* @return
*/
@Nullable
public static Resource normalizeSitemapRoot(@Nullable Resource resource) {
if (!isSitemapRoot(resource)) {
return null;
}
if (resource.getName().equals(JcrConstants.JCR_CONTENT)) {
return resource.getParent();
} else {
return resource;
}
}
/**
* Returns true when the given {@link Resource} is a sitemap root.
*
* @param resource
* @return
*/
public static boolean isSitemapRoot(@Nullable Resource resource) {
if (resource == null) {
return false;
}
Boolean sitemapRoot = resource.getValueMap().get(SitemapService.PROPERTY_SITEMAP_ROOT, Boolean.class);
if (sitemapRoot == null) {
Resource content = resource.getChild(JcrConstants.JCR_CONTENT);
if (content != null) {
sitemapRoot = content.getValueMap().get(SitemapService.PROPERTY_SITEMAP_ROOT, Boolean.FALSE);
} else {
sitemapRoot = Boolean.FALSE;
}
}
return sitemapRoot;
}
/**
* Returns true when the given {@link Resource} is a top level sitemap root.
*
* @param resource
* @return
*/
public static boolean isTopLevelSitemapRoot(@Nullable Resource resource) {
return isSitemapRoot(resource) && getTopLevelSitemapRoot(resource).getPath().equals(resource.getPath());
}
/**
* Returns the selector for the given sitemap root {@link Resource} and the given name.
*
* @param sitemapRoot
* @param topLevelSitemapRoot
* @param name
* @return
*/
@NotNull
public static String getSitemapSelector(@NotNull Resource sitemapRoot, @NotNull Resource topLevelSitemapRoot,
@NotNull String name) {
name = SitemapService.DEFAULT_SITEMAP_NAME.equals(name) ? SITEMAP_SELECTOR : name + SITEMAP_SELECTOR_SUFFIX;
if (!sitemapRoot.getPath().equals(topLevelSitemapRoot.getPath())) {
String sitemapRootSubpath = sitemapRoot.getPath().substring(topLevelSitemapRoot.getPath().length() + 1);
name = sitemapRootSubpath.replace('/', '-') + '-' + name;
}
return name;
}
/**
* Resolves all sitemap root {@link Resource}s for the given selector within the given top level sitemap root
* {@link Resource}. This is the inversion of {@link SitemapUtil#getSitemapSelector(Resource, Resource, String)} with
* sitemap root being a top level sitemap root.
* <p>
* The returned {@link Map} only contains {@link Resource}s, that are sitemap roots according to
* {@link SitemapUtil#isSitemapRoot(Resource)}. Each returned {@link Resource} is mapped to the name which when
* passed to {@link SitemapUtil#getSitemapSelector(Resource, Resource, String)} would return the same selector,
* omitting the optional multi-file index part.
* <p>
* As this resolution may be ambiguous, the returned {@link Map} is sorted with the sitemap root/name combinations
* closest to the top level sitemap root taking precedence.
*
* @param topLevelSitemapRoot
* @param sitemapSelector
* @return a sorted {@link Map}
*/
@NotNull
public static Map<Resource, String> resolveSitemapRoots(@NotNull Resource topLevelSitemapRoot,
@NotNull String sitemapSelector) {
if (!isTopLevelSitemapRoot(topLevelSitemapRoot)) {
// selectors are always relative to a top level sitemap root
return Collections.emptyMap();
}
if (sitemapSelector.equals(SITEMAP_SELECTOR)) {
return Collections.singletonMap(topLevelSitemapRoot, SitemapService.DEFAULT_SITEMAP_NAME);
}
List<String> parts = Arrays.asList(sitemapSelector.split("-"));
List<String> relevantParts;
if (parts.size() == 2 && parts.get(0).equals(SITEMAP_SELECTOR) && isInteger(parts.get(1))) {
// default name with file index
return Collections.singletonMap(topLevelSitemapRoot, SitemapService.DEFAULT_SITEMAP_NAME);
} else if (parts.size() > 1 && parts.get(parts.size() - 1).equals(SITEMAP_SELECTOR)) {
// no file index part
relevantParts = parts.subList(0, parts.size() - 1);
} else if (parts.size() > 2 && parts.get(parts.size() - 2).equals(SITEMAP_SELECTOR)
&& isInteger(parts.get(parts.size() - 1))) {
// with file index part
relevantParts = parts.subList(0, parts.size() - 2);
} else {
return Collections.emptyMap();
}
Map<Resource, String> roots = new LinkedHashMap<>();
resolveSitemapRoots(topLevelSitemapRoot, relevantParts, roots);
return roots;
}
/**
* Returns all sitemap root {@link Resource}s within the given search path.
*
* @param resolver
* @param searchPath
* @return
*/
@NotNull
public static Iterator<Resource> findSitemapRoots(ResourceResolver resolver, String searchPath) {
String correctedSearchPath = searchPath == null ? "/" : searchPath;
StringBuilder query = new StringBuilder(correctedSearchPath.length() + 35);
query.append("/jcr:root").append(ISO9075.encodePath(correctedSearchPath));
if (!correctedSearchPath.endsWith("/")) {
query.append('/');
}
query.append("/*[@").append(SitemapService.PROPERTY_SITEMAP_ROOT).append('=').append(Boolean.TRUE).append(']');
query.append(" option(index tag slingSitemaps)");
return new Iterator<Resource>() {
private final Iterator<Resource> hits = resolver.findResources(query.toString(), Query.XPATH);
private Resource next = seek();
private Resource seek() {
while (hits.hasNext()) {
Resource nextHit = normalizeSitemapRoot(hits.next());
// skip a hit on the given searchPath itself. This may be when a search is done for descendant
// sitemaps given the normalized sitemap root path and the sitemap root's jcr:content is in the
// result set.
if (nextHit == null
|| nextHit.getPath().equals(correctedSearchPath)
|| nextHit.getPath().startsWith(JCR_SYSTEM_PATH)) {
continue;
}
return nextHit;
}
return null;
}
@Override
public boolean hasNext() {
return next != null;
}
@Override
public Resource next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
Resource ret = next;
next = seek();
return ret;
}
};
}
private static boolean isInteger(String text) {
try {
Integer.parseUnsignedInt(text);
return true;
} catch (NumberFormatException ex) {
return false;
}
}
private static void resolveSitemapRoots(@NotNull Resource sitemapRoot, @NotNull List<String> parts,
@NotNull Map<Resource, String> result) {
if (isSitemapRoot(sitemapRoot)) {
result.put(sitemapRoot, String.join("-", parts));
}
for (int i = 0; i < parts.size(); i++) {
// products product page tops
int j = i + 1;
String childName = String.join("-", parts.subList(0, j));
Resource namedChild = sitemapRoot.getChild(childName);
if (namedChild != null) {
if (j == parts.size() && isSitemapRoot(namedChild)) {
result.put(namedChild, SitemapService.DEFAULT_SITEMAP_NAME);
} else if (parts.size() > j) {
resolveSitemapRoots(namedChild, parts.subList(j, parts.size()), result);
}
}
}
}
}