This is an automated email from the ASF dual-hosted git repository. reschke pushed a commit to branch SLING-12734 in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-resourceresolver.git
commit c187c3193d3b7d343b2c0cfd95f4290818c08866 Author: Julian Reschke <[email protected]> AuthorDate: Wed Apr 2 07:27:53 2025 +0100 SLING-12734: Move AliasHandler into new class --- .../impl/mapping/AliasHandler.java | 529 +++++++++++++++++++++ .../resourceresolver/impl/mapping/MapEntries.java | 495 ------------------- .../mapping/AbstractMappingMapEntriesTest.java | 2 +- .../impl/mapping/AliasMapEntriesTest.java | 12 +- .../impl/mapping/MapEntriesTest.java | 2 +- 5 files changed, 536 insertions(+), 504 deletions(-) diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/AliasHandler.java b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/AliasHandler.java new file mode 100755 index 00000000..4982fcd4 --- /dev/null +++ b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/AliasHandler.java @@ -0,0 +1,529 @@ +/* + * 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.resourceresolver.impl.mapping; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; + +import org.apache.sling.api.resource.LoginException; +import org.apache.sling.api.resource.QuerySyntaxException; +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.resourceresolver.impl.ResourceResolverImpl; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * All things related to the handling of aliases. + */ +class AliasHandler { + + private static final String JCR_CONTENT = "jcr:content"; + + private static final String JCR_CONTENT_PREFIX = JCR_CONTENT + "/"; + + private static final String JCR_CONTENT_SUFFIX = "/" + JCR_CONTENT; + + private MapConfigurationProvider factory; + + private final ReentrantLock initializing; + + private final Logger log = LoggerFactory.getLogger(AliasHandler.class); + + // keep track of some defunct aliases for diagnostics (thus size-limited) + private static final int MAX_REPORT_DEFUNCT_ALIASES = 50; + + private Runnable doUpdateConfiguration; + private Runnable sendChangeEvent; + + /** + * The key of the map is the parent path, while the value is a map with the the resource name as key and the actual aliases as values) + */ + Map<String, Map<String, Collection<String>>> aliasMapsMap; + + final AtomicLong aliasResourcesOnStartup; + final AtomicLong detectedConflictingAliases; + final AtomicLong detectedInvalidAliases; + + public AliasHandler( + MapConfigurationProvider factory, + ReentrantLock initializing, + Runnable doUpdateConfiguration, + Runnable sendChangeEvent) { + this.factory = factory; + this.initializing = initializing; + this.aliasMapsMap = new ConcurrentHashMap<>(); + this.doUpdateConfiguration = doUpdateConfiguration; + this.sendChangeEvent = sendChangeEvent; + + this.aliasResourcesOnStartup = new AtomicLong(0); + this.detectedConflictingAliases = new AtomicLong(0); + this.detectedInvalidAliases = new AtomicLong(0); + } + + public void dispose() { + this.factory = null; + } + + /** + * Actual initializer. Guards itself against concurrent use by using a + * ReentrantLock. Does nothing if the resource resolver has already been + * null-ed. + * + * @return true if the optimizedAliasResolution is enabled, false otherwise + */ + protected boolean initializeAliases() { + + this.initializing.lock(); + try { + // already disposed? + if (this.factory == null) { + return false; + } + + List<String> conflictingAliases = new ArrayList<>(); + List<String> invalidAliases = new ArrayList<>(); + + boolean isOptimizeAliasResolutionEnabled = this.factory.isOptimizeAliasResolutionEnabled(); + + // optimization made in SLING-2521 + if (isOptimizeAliasResolutionEnabled) { + try { + final Map<String, Map<String, Collection<String>>> loadedMap = + this.loadAliases(conflictingAliases, invalidAliases); + this.aliasMapsMap = loadedMap; + + // warn if there are more than a few defunct aliases + if (conflictingAliases.size() >= MAX_REPORT_DEFUNCT_ALIASES) { + log.warn( + "There are {} conflicting aliases; excerpt: {}", + conflictingAliases.size(), + conflictingAliases); + } else if (!conflictingAliases.isEmpty()) { + log.warn("There are {} conflicting aliases: {}", conflictingAliases.size(), conflictingAliases); + } + if (invalidAliases.size() >= MAX_REPORT_DEFUNCT_ALIASES) { + log.warn("There are {} invalid aliases; excerpt: {}", invalidAliases.size(), invalidAliases); + } else if (!invalidAliases.isEmpty()) { + log.warn("There are {} invalid aliases: {}", invalidAliases.size(), invalidAliases); + } + } catch (final Exception e) { + + logDisableAliasOptimization(e); + + // disable optimize alias resolution + isOptimizeAliasResolutionEnabled = false; + } + } + + doUpdateConfiguration.run(); + sendChangeEvent.run(); + + return isOptimizeAliasResolutionEnabled; + + } finally { + + this.initializing.unlock(); + } + } + + boolean doAddAlias(final Resource resource) { + return loadAlias(resource, this.aliasMapsMap, null, null); + } + + /** + * Remove all aliases for the content path + * + * @param contentPath The content path + * @param path Optional sub path of the vanity path + * @return {@code true} if a change happened + */ + boolean removeAlias( + ResourceResolver resolver, final String contentPath, final String path, final Runnable notifyOfChange) { + // if path is specified we first need to find out if it is + // a direct child of vanity path but not jcr:content, or a jcr:content child of a direct child + // otherwise we can discard the event + boolean handle = true; + final String resourcePath; + if (path != null && path.length() > contentPath.length()) { + final String subPath = path.substring(contentPath.length() + 1); + final int firstSlash = subPath.indexOf('/'); + if (firstSlash == -1) { + if (subPath.equals(JCR_CONTENT)) { + handle = false; + } + resourcePath = path; + } else if (subPath.lastIndexOf('/') == firstSlash) { + if (subPath.startsWith(JCR_CONTENT_PREFIX) || !subPath.endsWith(JCR_CONTENT_SUFFIX)) { + handle = false; + } + resourcePath = ResourceUtil.getParent(path); + } else { + handle = false; + resourcePath = null; + } + } else { + resourcePath = contentPath; + } + if (!handle) { + return false; + } + + this.initializing.lock(); + try { + final Map<String, Collection<String>> aliasMapEntry = aliasMapsMap.get(contentPath); + if (aliasMapEntry != null) { + notifyOfChange.run(); + + String prefix = contentPath.endsWith("/") ? contentPath : contentPath + "/"; + if (aliasMapEntry.entrySet().removeIf(e -> (prefix + e.getKey()).startsWith(resourcePath)) + && (aliasMapEntry.isEmpty())) { + this.aliasMapsMap.remove(contentPath); + } + + Resource containingResource = resolver != null ? resolver.getResource(resourcePath) : null; + + if (containingResource != null) { + if (containingResource.getValueMap().containsKey(ResourceResolverImpl.PROP_ALIAS)) { + doAddAlias(containingResource); + } + final Resource child = containingResource.getChild(JCR_CONTENT); + if (child != null && child.getValueMap().containsKey(ResourceResolverImpl.PROP_ALIAS)) { + doAddAlias(child); + } + } + } + return aliasMapEntry != null; + } finally { + this.initializing.unlock(); + } + } + + /** + * Update alias from a resource + * + * @param resource The resource + * @return {@code true} if any change + */ + boolean doUpdateAlias(final Resource resource) { + + // resource containing the alias + final Resource containingResource = getResourceToBeAliased(resource); + + if (containingResource != null) { + final String containingResourceName = containingResource.getName(); + final String parentPath = ResourceUtil.getParent(containingResource.getPath()); + + final Map<String, Collection<String>> aliasMapEntry = + parentPath == null ? null : aliasMapsMap.get(parentPath); + if (aliasMapEntry != null) { + aliasMapEntry.remove(containingResourceName); + if (aliasMapEntry.isEmpty()) { + this.aliasMapsMap.remove(parentPath); + } + } + + boolean changed = aliasMapEntry != null; + + if (containingResource.getValueMap().containsKey(ResourceResolverImpl.PROP_ALIAS)) { + changed |= doAddAlias(containingResource); + } + final Resource child = containingResource.getChild(JCR_CONTENT); + if (child != null && child.getValueMap().containsKey(ResourceResolverImpl.PROP_ALIAS)) { + changed |= doAddAlias(child); + } + + return changed; + } else { + log.warn("containingResource is null for alias on {}, skipping.", resource.getPath()); + } + + return false; + } + + public @NotNull Map<String, Collection<String>> getAliasMap(final String parentPath) { + Map<String, Collection<String>> aliasMapForParent = aliasMapsMap.get(parentPath); + return aliasMapForParent != null ? aliasMapForParent : Collections.emptyMap(); + } + + /** + * Load aliases - Search for all nodes (except under /jcr:system) below + * configured alias locations having the sling:alias property + */ + private Map<String, Map<String, Collection<String>>> loadAliases( + List<String> conflictingAliases, List<String> invalidAliases) throws LoginException { + + final ResourceResolver resolver = + factory.getServiceResourceResolver(factory.getServiceUserAuthenticationInfo("mapping")); + + final Map<String, Map<String, Collection<String>>> map = new ConcurrentHashMap<>(); + final String baseQueryString = generateAliasQuery(); + + Iterator<Resource> it; + try { + final String queryStringWithSort = + baseQueryString + " AND FIRST([sling:alias]) >= '%s' ORDER BY FIRST([sling:alias])"; + it = new PagedQueryIterator("alias", "sling:alias", resolver, queryStringWithSort, 2000); + } catch (QuerySyntaxException ex) { + log.debug("sort with first() not supported, falling back to base query", ex); + it = queryUnpaged(baseQueryString, resolver); + } catch (UnsupportedOperationException ex) { + log.debug("query failed as unsupported, retrying without paging/sorting", ex); + it = queryUnpaged(baseQueryString, resolver); + } + + log.debug("alias initialization - start"); + long count = 0; + long processStart = System.nanoTime(); + while (it.hasNext()) { + count += 1; + loadAlias(it.next(), map, conflictingAliases, invalidAliases); + } + long processElapsed = System.nanoTime() - processStart; + long resourcePerSecond = (count * TimeUnit.SECONDS.toNanos(1) / (processElapsed == 0 ? 1 : processElapsed)); + + String diagnostics = ""; + if (it instanceof PagedQueryIterator) { + PagedQueryIterator pit = (PagedQueryIterator) it; + + if (!pit.getWarning().isEmpty()) { + log.warn(pit.getWarning()); + } + + diagnostics = pit.getStatistics(); + } + + log.info( + "alias initialization - completed, processed {} resources with sling:alias properties in {}ms (~{} resource/s){}", + count, + TimeUnit.NANOSECONDS.toMillis(processElapsed), + resourcePerSecond, + diagnostics); + + this.aliasResourcesOnStartup.set(count); + + return map; + } + + /* + * generate alias query based on configured alias locations + */ + private String generateAliasQuery() { + final Set<String> allowedLocations = this.factory.getAllowedAliasLocations(); + + StringBuilder baseQuery = new StringBuilder("SELECT [sling:alias] FROM [nt:base] WHERE"); + + if (allowedLocations.isEmpty()) { + baseQuery.append(" ").append(QueryBuildHelper.excludeSystemPath()); + } else { + Iterator<String> pathIterator = allowedLocations.iterator(); + baseQuery.append(" ("); + String sep = ""; + while (pathIterator.hasNext()) { + String prefix = pathIterator.next(); + baseQuery + .append(sep) + .append("isdescendantnode('") + .append(QueryBuildHelper.escapeString(prefix)) + .append("')"); + sep = " OR "; + } + baseQuery.append(")"); + } + + baseQuery.append(" AND [sling:alias] IS NOT NULL"); + return baseQuery.toString(); + } + + /** + * Load alias given a resource + */ + private boolean loadAlias( + final Resource resource, + Map<String, Map<String, Collection<String>>> map, + List<String> conflictingAliases, + List<String> invalidAliases) { + + // resource containing the alias + final Resource containingResource = getResourceToBeAliased(resource); + + if (containingResource == null) { + log.warn("containingResource is null for alias on {}, skipping.", resource.getPath()); + return false; + } else { + final Resource parent = containingResource.getParent(); + + if (parent == null) { + log.warn( + "{} is null for alias on {}, skipping.", + containingResource == resource ? "parent" : "grandparent", + resource.getPath()); + return false; + } else { + final String[] aliasArray = resource.getValueMap().get(ResourceResolverImpl.PROP_ALIAS, String[].class); + if (aliasArray == null) { + return false; + } else { + return loadAliasFromArray( + aliasArray, + map, + conflictingAliases, + invalidAliases, + containingResource.getName(), + parent.getPath()); + } + } + } + } + + /** + * Load alias given a an alias array, return success flag. + */ + private boolean loadAliasFromArray( + final String[] aliasArray, + Map<String, Map<String, Collection<String>>> map, + List<String> conflictingAliases, + List<String> invalidAliases, + final String resourceName, + final String parentPath) { + + boolean hasAlias = false; + + log.debug("Found alias, total size {}", aliasArray.length); + + // the order matters here, the first alias in the array must come first + for (final String alias : aliasArray) { + if (isAliasInvalid(alias)) { + long invalids = detectedInvalidAliases.incrementAndGet(); + log.warn( + "Encountered invalid alias '{}' under parent path '{}' (total so far: {}). Refusing to use it.", + alias, + parentPath, + invalids); + if (invalidAliases != null && invalids < MAX_REPORT_DEFUNCT_ALIASES) { + invalidAliases.add((String.format("'%s'/'%s'", parentPath, alias))); + } + } else { + Map<String, Collection<String>> parentMap = + map.computeIfAbsent(parentPath, key -> new ConcurrentHashMap<>()); + Optional<String> siblingResourceNameWithDuplicateAlias = parentMap.entrySet().stream() + .filter(entry -> !entry.getKey().equals(resourceName)) // ignore entry for the current resource + .filter(entry -> entry.getValue().contains(alias)) + .findFirst() + .map(Map.Entry::getKey); + if (siblingResourceNameWithDuplicateAlias.isPresent()) { + long conflicting = detectedConflictingAliases.incrementAndGet(); + log.warn( + "Encountered duplicate alias '{}' under parent path '{}'. Refusing to replace current target '{}' with '{}' (total duplicated aliases so far: {}).", + alias, + parentPath, + siblingResourceNameWithDuplicateAlias.get(), + resourceName, + conflicting); + if (conflictingAliases != null && conflicting < MAX_REPORT_DEFUNCT_ALIASES) { + conflictingAliases.add((String.format( + "'%s': '%s'/'%s' vs '%s'/'%s'", + parentPath, resourceName, alias, siblingResourceNameWithDuplicateAlias.get(), alias))); + } + } else { + Collection<String> existingAliases = + parentMap.computeIfAbsent(resourceName, name -> new CopyOnWriteArrayList<>()); + existingAliases.add(alias); + hasAlias = true; + } + } + } + + return hasAlias; + } + + /** + * Given a resource, check whether the name is "jcr:content", in which case return the parent resource + * + * @param resource resource to check + * @return parent of jcr:content resource (may be null), otherwise the resource itself + */ + @Nullable + private Resource getResourceToBeAliased(Resource resource) { + if (JCR_CONTENT.equals(resource.getName())) { + return resource.getParent(); + } else { + return resource; + } + } + + /** + * Check alias syntax + */ + private boolean isAliasInvalid(String alias) { + boolean invalid = alias.equals("..") || alias.equals(".") || alias.isEmpty(); + if (!invalid) { + for (final char c : alias.toCharArray()) { + // invalid if / or # or a ? + if (c == '/' || c == '#' || c == '?') { + invalid = true; + break; + } + } + } + return invalid; + } + + private Iterator<Resource> queryUnpaged(String query, ResourceResolver resolver) { + log.debug("start alias query: {}", query); + long queryStart = System.nanoTime(); + final Iterator<Resource> it = resolver.findResources(query, "JCR-SQL2"); + long queryElapsed = System.nanoTime() - queryStart; + log.debug("end alias query; elapsed {}ms", TimeUnit.NANOSECONDS.toMillis(queryElapsed)); + return it; + } + + private final AtomicLong lastTimeLogged = new AtomicLong(-1); + + private final long LOGGING_ERROR_PERIOD = 1000 * 60 * 5; + + void logDisableAliasOptimization(final Exception e) { + if (e != null) { + log.error( + "Unexpected problem during initialization of optimize alias resolution. Therefore disabling optimize alias resolution. Please fix the problem.", + e); + } else { + final long now = System.currentTimeMillis(); + if (now - lastTimeLogged.getAndSet(now) > LOGGING_ERROR_PERIOD) { + log.error( + "A problem occured during initialization of optimize alias resolution. Optimize alias resolution is disabled. Check the logs for the reported problem.", + e); + } + } + } +} diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java index 9397f487..48950b3a 100644 --- a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java +++ b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java @@ -35,20 +35,16 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; -import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.ReentrantLock; import org.apache.sling.api.SlingConstants; import org.apache.sling.api.resource.LoginException; -import org.apache.sling.api.resource.QuerySyntaxException; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.resource.ResourceUtil; @@ -59,7 +55,6 @@ import org.apache.sling.api.resource.observation.ResourceChangeListener; import org.apache.sling.resourceresolver.impl.ResourceResolverImpl; import org.apache.sling.resourceresolver.impl.ResourceResolverMetrics; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; import org.osgi.framework.ServiceRegistration; @@ -763,494 +758,4 @@ public class MapEntries implements MapEntriesHandler, ResourceChangeListener, Ex sendChangeEvent(); } } - - // Alias handling code - - class AliasHandler { - - private static final String JCR_CONTENT = "jcr:content"; - - private static final String JCR_CONTENT_PREFIX = JCR_CONTENT + "/"; - - private static final String JCR_CONTENT_SUFFIX = "/" + JCR_CONTENT; - - private MapConfigurationProvider factory; - - private final ReentrantLock initializing; - - private final Logger log = LoggerFactory.getLogger(AliasHandler.class); - - // keep track of some defunct aliases for diagnostics (thus size-limited) - private static final int MAX_REPORT_DEFUNCT_ALIASES = 50; - - private Runnable doUpdateConfiguration; - private Runnable sendChangeEvent; - - /** - * The key of the map is the parent path, while the value is a map with the the resource name as key and the actual aliases as values) - */ - private Map<String, Map<String, Collection<String>>> aliasMapsMap; - - final AtomicLong aliasResourcesOnStartup; - final AtomicLong detectedConflictingAliases; - final AtomicLong detectedInvalidAliases; - - public AliasHandler( - MapConfigurationProvider factory, - ReentrantLock initializing, - Runnable doUpdateConfiguration, - Runnable sendChangeEvent) { - this.factory = factory; - this.initializing = initializing; - this.aliasMapsMap = new ConcurrentHashMap<>(); - this.doUpdateConfiguration = doUpdateConfiguration; - this.sendChangeEvent = sendChangeEvent; - - this.aliasResourcesOnStartup = new AtomicLong(0); - this.detectedConflictingAliases = new AtomicLong(0); - this.detectedInvalidAliases = new AtomicLong(0); - } - - public void dispose() { - this.factory = null; - } - - /** - * Actual initializer. Guards itself against concurrent use by using a - * ReentrantLock. Does nothing if the resource resolver has already been - * null-ed. - * @return true if the optimizedAliasResolution is enabled, false otherwise - */ - protected boolean initializeAliases() { - - this.initializing.lock(); - try { - // already disposed? - if (this.factory == null) { - return false; - } - - List<String> conflictingAliases = new ArrayList<>(); - List<String> invalidAliases = new ArrayList<>(); - - boolean isOptimizeAliasResolutionEnabled = this.factory.isOptimizeAliasResolutionEnabled(); - - // optimization made in SLING-2521 - if (isOptimizeAliasResolutionEnabled) { - try { - final Map<String, Map<String, Collection<String>>> loadedMap = - this.loadAliases(conflictingAliases, invalidAliases); - this.aliasMapsMap = loadedMap; - - // warn if there are more than a few defunct aliases - if (conflictingAliases.size() >= MAX_REPORT_DEFUNCT_ALIASES) { - log.warn( - "There are {} conflicting aliases; excerpt: {}", - conflictingAliases.size(), - conflictingAliases); - } else if (!conflictingAliases.isEmpty()) { - log.warn( - "There are {} conflicting aliases: {}", - conflictingAliases.size(), - conflictingAliases); - } - if (invalidAliases.size() >= MAX_REPORT_DEFUNCT_ALIASES) { - log.warn( - "There are {} invalid aliases; excerpt: {}", invalidAliases.size(), invalidAliases); - } else if (!invalidAliases.isEmpty()) { - log.warn("There are {} invalid aliases: {}", invalidAliases.size(), invalidAliases); - } - } catch (final Exception e) { - - logDisableAliasOptimization(e); - - // disable optimize alias resolution - isOptimizeAliasResolutionEnabled = false; - } - } - - doUpdateConfiguration.run(); - sendChangeEvent.run(); - - return isOptimizeAliasResolutionEnabled; - - } finally { - - this.initializing.unlock(); - } - } - - private boolean doAddAlias(final Resource resource) { - return loadAlias(resource, this.aliasMapsMap, null, null); - } - - /** - * Remove all aliases for the content path - * @param contentPath The content path - * @param path Optional sub path of the vanity path - * @return {@code true} if a change happened - */ - private boolean removeAlias( - ResourceResolver resolver, final String contentPath, final String path, final Runnable notifyOfChange) { - // if path is specified we first need to find out if it is - // a direct child of vanity path but not jcr:content, or a jcr:content child of a direct child - // otherwise we can discard the event - boolean handle = true; - final String resourcePath; - if (path != null && path.length() > contentPath.length()) { - final String subPath = path.substring(contentPath.length() + 1); - final int firstSlash = subPath.indexOf('/'); - if (firstSlash == -1) { - if (subPath.equals(JCR_CONTENT)) { - handle = false; - } - resourcePath = path; - } else if (subPath.lastIndexOf('/') == firstSlash) { - if (subPath.startsWith(JCR_CONTENT_PREFIX) || !subPath.endsWith(JCR_CONTENT_SUFFIX)) { - handle = false; - } - resourcePath = ResourceUtil.getParent(path); - } else { - handle = false; - resourcePath = null; - } - } else { - resourcePath = contentPath; - } - if (!handle) { - return false; - } - - this.initializing.lock(); - try { - final Map<String, Collection<String>> aliasMapEntry = aliasMapsMap.get(contentPath); - if (aliasMapEntry != null) { - notifyOfChange.run(); - - String prefix = contentPath.endsWith("/") ? contentPath : contentPath + "/"; - if (aliasMapEntry.entrySet().removeIf(e -> (prefix + e.getKey()).startsWith(resourcePath)) - && (aliasMapEntry.isEmpty())) { - this.aliasMapsMap.remove(contentPath); - } - - Resource containingResource = resolver != null ? resolver.getResource(resourcePath) : null; - - if (containingResource != null) { - if (containingResource.getValueMap().containsKey(ResourceResolverImpl.PROP_ALIAS)) { - doAddAlias(containingResource); - } - final Resource child = containingResource.getChild(JCR_CONTENT); - if (child != null && child.getValueMap().containsKey(ResourceResolverImpl.PROP_ALIAS)) { - doAddAlias(child); - } - } - } - return aliasMapEntry != null; - } finally { - this.initializing.unlock(); - } - } - - /** - * Update alias from a resource - * @param resource The resource - * @return {@code true} if any change - */ - private boolean doUpdateAlias(final Resource resource) { - - // resource containing the alias - final Resource containingResource = getResourceToBeAliased(resource); - - if (containingResource != null) { - final String containingResourceName = containingResource.getName(); - final String parentPath = ResourceUtil.getParent(containingResource.getPath()); - - final Map<String, Collection<String>> aliasMapEntry = - parentPath == null ? null : aliasMapsMap.get(parentPath); - if (aliasMapEntry != null) { - aliasMapEntry.remove(containingResourceName); - if (aliasMapEntry.isEmpty()) { - this.aliasMapsMap.remove(parentPath); - } - } - - boolean changed = aliasMapEntry != null; - - if (containingResource.getValueMap().containsKey(ResourceResolverImpl.PROP_ALIAS)) { - changed |= doAddAlias(containingResource); - } - final Resource child = containingResource.getChild(JCR_CONTENT); - if (child != null && child.getValueMap().containsKey(ResourceResolverImpl.PROP_ALIAS)) { - changed |= doAddAlias(child); - } - - return changed; - } else { - log.warn("containingResource is null for alias on {}, skipping.", resource.getPath()); - } - - return false; - } - - public @NotNull Map<String, Collection<String>> getAliasMap(final String parentPath) { - Map<String, Collection<String>> aliasMapForParent = aliasMapsMap.get(parentPath); - return aliasMapForParent != null ? aliasMapForParent : Collections.emptyMap(); - } - - /** - * Load aliases - Search for all nodes (except under /jcr:system) below - * configured alias locations having the sling:alias property - */ - private Map<String, Map<String, Collection<String>>> loadAliases( - List<String> conflictingAliases, List<String> invalidAliases) throws LoginException { - - final ResourceResolver resolver = - factory.getServiceResourceResolver(factory.getServiceUserAuthenticationInfo("mapping")); - - final Map<String, Map<String, Collection<String>>> map = new ConcurrentHashMap<>(); - final String baseQueryString = generateAliasQuery(); - - Iterator<Resource> it; - try { - final String queryStringWithSort = - baseQueryString + " AND FIRST([sling:alias]) >= '%s' ORDER BY FIRST([sling:alias])"; - it = new PagedQueryIterator("alias", "sling:alias", resolver, queryStringWithSort, 2000); - } catch (QuerySyntaxException ex) { - log.debug("sort with first() not supported, falling back to base query", ex); - it = queryUnpaged("alias", baseQueryString); - } catch (UnsupportedOperationException ex) { - log.debug("query failed as unsupported, retrying without paging/sorting", ex); - it = queryUnpaged("alias", baseQueryString); - } - - log.debug("alias initialization - start"); - long count = 0; - long processStart = System.nanoTime(); - while (it.hasNext()) { - count += 1; - loadAlias(it.next(), map, conflictingAliases, invalidAliases); - } - long processElapsed = System.nanoTime() - processStart; - long resourcePerSecond = (count * TimeUnit.SECONDS.toNanos(1) / (processElapsed == 0 ? 1 : processElapsed)); - - String diagnostics = ""; - if (it instanceof PagedQueryIterator) { - PagedQueryIterator pit = (PagedQueryIterator) it; - - if (!pit.getWarning().isEmpty()) { - log.warn(pit.getWarning()); - } - - diagnostics = pit.getStatistics(); - } - - log.info( - "alias initialization - completed, processed {} resources with sling:alias properties in {}ms (~{} resource/s){}", - count, - TimeUnit.NANOSECONDS.toMillis(processElapsed), - resourcePerSecond, - diagnostics); - - this.aliasResourcesOnStartup.set(count); - - return map; - } - - /* - * generate alias query based on configured alias locations - */ - private String generateAliasQuery() { - final Set<String> allowedLocations = this.factory.getAllowedAliasLocations(); - - StringBuilder baseQuery = new StringBuilder("SELECT [sling:alias] FROM [nt:base] WHERE"); - - if (allowedLocations.isEmpty()) { - baseQuery.append(" ").append(QueryBuildHelper.excludeSystemPath()); - } else { - Iterator<String> pathIterator = allowedLocations.iterator(); - baseQuery.append(" ("); - String sep = ""; - while (pathIterator.hasNext()) { - String prefix = pathIterator.next(); - baseQuery - .append(sep) - .append("isdescendantnode('") - .append(QueryBuildHelper.escapeString(prefix)) - .append("')"); - sep = " OR "; - } - baseQuery.append(")"); - } - - baseQuery.append(" AND [sling:alias] IS NOT NULL"); - return baseQuery.toString(); - } - - /** - * Load alias given a resource - */ - private boolean loadAlias( - final Resource resource, - Map<String, Map<String, Collection<String>>> map, - List<String> conflictingAliases, - List<String> invalidAliases) { - - // resource containing the alias - final Resource containingResource = getResourceToBeAliased(resource); - - if (containingResource == null) { - log.warn("containingResource is null for alias on {}, skipping.", resource.getPath()); - return false; - } else { - final Resource parent = containingResource.getParent(); - - if (parent == null) { - log.warn( - "{} is null for alias on {}, skipping.", - containingResource == resource ? "parent" : "grandparent", - resource.getPath()); - return false; - } else { - final String[] aliasArray = - resource.getValueMap().get(ResourceResolverImpl.PROP_ALIAS, String[].class); - if (aliasArray == null) { - return false; - } else { - return loadAliasFromArray( - aliasArray, - map, - conflictingAliases, - invalidAliases, - containingResource.getName(), - parent.getPath()); - } - } - } - } - - /** - * Load alias given a an alias array, return success flag. - */ - private boolean loadAliasFromArray( - final String[] aliasArray, - Map<String, Map<String, Collection<String>>> map, - List<String> conflictingAliases, - List<String> invalidAliases, - final String resourceName, - final String parentPath) { - - boolean hasAlias = false; - - log.debug("Found alias, total size {}", aliasArray.length); - - // the order matters here, the first alias in the array must come first - for (final String alias : aliasArray) { - if (isAliasInvalid(alias)) { - long invalids = detectedInvalidAliases.incrementAndGet(); - log.warn( - "Encountered invalid alias '{}' under parent path '{}' (total so far: {}). Refusing to use it.", - alias, - parentPath, - invalids); - if (invalidAliases != null && invalids < MAX_REPORT_DEFUNCT_ALIASES) { - invalidAliases.add((String.format("'%s'/'%s'", parentPath, alias))); - } - } else { - Map<String, Collection<String>> parentMap = - map.computeIfAbsent(parentPath, key -> new ConcurrentHashMap<>()); - Optional<String> siblingResourceNameWithDuplicateAlias = parentMap.entrySet().stream() - .filter(entry -> - !entry.getKey().equals(resourceName)) // ignore entry for the current resource - .filter(entry -> entry.getValue().contains(alias)) - .findFirst() - .map(Map.Entry::getKey); - if (siblingResourceNameWithDuplicateAlias.isPresent()) { - long conflicting = detectedConflictingAliases.incrementAndGet(); - log.warn( - "Encountered duplicate alias '{}' under parent path '{}'. Refusing to replace current target '{}' with '{}' (total duplicated aliases so far: {}).", - alias, - parentPath, - siblingResourceNameWithDuplicateAlias.get(), - resourceName, - conflicting); - if (conflictingAliases != null && conflicting < MAX_REPORT_DEFUNCT_ALIASES) { - conflictingAliases.add((String.format( - "'%s': '%s'/'%s' vs '%s'/'%s'", - parentPath, - resourceName, - alias, - siblingResourceNameWithDuplicateAlias.get(), - alias))); - } - } else { - Collection<String> existingAliases = - parentMap.computeIfAbsent(resourceName, name -> new CopyOnWriteArrayList<>()); - existingAliases.add(alias); - hasAlias = true; - } - } - } - - return hasAlias; - } - - /** - * Given a resource, check whether the name is "jcr:content", in which case return the parent resource - * @param resource resource to check - * @return parent of jcr:content resource (may be null), otherwise the resource itself - */ - @Nullable - private Resource getResourceToBeAliased(Resource resource) { - if (JCR_CONTENT.equals(resource.getName())) { - return resource.getParent(); - } else { - return resource; - } - } - - /** - * Check alias syntax - */ - private boolean isAliasInvalid(String alias) { - boolean invalid = alias.equals("..") || alias.equals(".") || alias.isEmpty(); - if (!invalid) { - for (final char c : alias.toCharArray()) { - // invalid if / or # or a ? - if (c == '/' || c == '#' || c == '?') { - invalid = true; - break; - } - } - } - return invalid; - } - - private Iterator<Resource> queryUnpaged(String subject, String query) { - log.debug("start {} query: {}", subject, query); - long queryStart = System.nanoTime(); - final Iterator<Resource> it = resolver.findResources(query, "JCR-SQL2"); - long queryElapsed = System.nanoTime() - queryStart; - log.debug("end {} query; elapsed {}ms", subject, TimeUnit.NANOSECONDS.toMillis(queryElapsed)); - return it; - } - - private final AtomicLong lastTimeLogged = new AtomicLong(-1); - - private final long LOGGING_ERROR_PERIOD = 1000 * 60 * 5; - - private void logDisableAliasOptimization(final Exception e) { - if (e != null) { - log.error( - "Unexpected problem during initialization of optimize alias resolution. Therefore disabling optimize alias resolution. Please fix the problem.", - e); - } else { - final long now = System.currentTimeMillis(); - if (now - lastTimeLogged.getAndSet(now) > LOGGING_ERROR_PERIOD) { - log.error( - "A problem occured during initialization of optimize alias resolution. Optimize alias resolution is disabled. Check the logs for the reported problem.", - e); - } - } - } - } } diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/AbstractMappingMapEntriesTest.java b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/AbstractMappingMapEntriesTest.java index 9472e0c3..35d0dd7d 100644 --- a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/AbstractMappingMapEntriesTest.java +++ b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/AbstractMappingMapEntriesTest.java @@ -123,7 +123,7 @@ public abstract class AbstractMappingMapEntriesTest { mapEntries = new MapEntries( resourceResolverFactory, bundleContext, eventAdmin, stringInterpolationProvider, metrics); - final Field aliasMapField = MapEntries.AliasHandler.class.getDeclaredField("aliasMapsMap"); + final Field aliasMapField = AliasHandler.class.getDeclaredField("aliasMapsMap"); aliasMapField.setAccessible(true); this.aliasMap = (Map<String, Map<String, String>>) aliasMapField.get(mapEntries.ah); } diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/AliasMapEntriesTest.java b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/AliasMapEntriesTest.java index bba9c33e..ed410fde 100644 --- a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/AliasMapEntriesTest.java +++ b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/AliasMapEntriesTest.java @@ -116,17 +116,15 @@ public class AliasMapEntriesTest extends AbstractMappingMapEntriesTest { mapEntries = new MapEntries( resourceResolverFactory, bundleContext, eventAdmin, stringInterpolationProvider, metrics); - final Field aliasMapField = MapEntries.AliasHandler.class.getDeclaredField("aliasMapsMap"); + final Field aliasMapField = AliasHandler.class.getDeclaredField("aliasMapsMap"); aliasMapField.setAccessible(true); this.aliasMap = (Map<String, Map<String, String>>) aliasMapField.get(mapEntries.ah); - final Field detectedInvalidAliasesField = - MapEntries.AliasHandler.class.getDeclaredField("detectedInvalidAliases"); + final Field detectedInvalidAliasesField = AliasHandler.class.getDeclaredField("detectedInvalidAliases"); detectedInvalidAliasesField.setAccessible(true); this.detectedInvalidAliases = (AtomicLong) detectedInvalidAliasesField.get(mapEntries.ah); - final Field detectedConflictingAliasesField = - MapEntries.AliasHandler.class.getDeclaredField("detectedConflictingAliases"); + final Field detectedConflictingAliasesField = AliasHandler.class.getDeclaredField("detectedConflictingAliases"); detectedConflictingAliasesField.setAccessible(true); this.detectedConflictingAliases = (AtomicLong) detectedConflictingAliasesField.get(mapEntries.ah); } @@ -159,7 +157,7 @@ public class AliasMapEntriesTest extends AbstractMappingMapEntriesTest { String path, Runnable callback) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException { - Method method = MapEntries.AliasHandler.class.getDeclaredMethod( + Method method = AliasHandler.class.getDeclaredMethod( "removeAlias", ResourceResolver.class, String.class, String.class, Runnable.class); method.setAccessible(true); method.invoke(mapEntries.ah, resourceResolver, contentPath, path, callback); @@ -1176,7 +1174,7 @@ public class AliasMapEntriesTest extends AbstractMappingMapEntriesTest { @Test public void test_initAliasesAfterDispose() { - MapEntries.AliasHandler ah = mapEntries.ah; + AliasHandler ah = mapEntries.ah; mapEntries.dispose(); boolean enabled = ah.initializeAliases(); assertFalse("return value (isOptimizeAliasResolutionEnabled) should be false", enabled); diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/MapEntriesTest.java b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/MapEntriesTest.java index 4f650f45..d5d37ed6 100644 --- a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/MapEntriesTest.java +++ b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/MapEntriesTest.java @@ -110,7 +110,7 @@ public class MapEntriesTest extends AbstractMappingMapEntriesTest { mapEntries = new MapEntries( resourceResolverFactory, bundleContext, eventAdmin, stringInterpolationProvider, metrics); - final Field aliasMapField = MapEntries.AliasHandler.class.getDeclaredField("aliasMapsMap"); + final Field aliasMapField = AliasHandler.class.getDeclaredField("aliasMapsMap"); aliasMapField.setAccessible(true); this.aliasMap = (Map<String, Map<String, String>>) aliasMapField.get(mapEntries.ah); }
