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 c2ad9f6bdeeaa2302fe3d8c6641e23b1be45c6e3 Author: Julian Reschke <[email protected]> AuthorDate: Wed Apr 2 07:28:27 2025 +0100 SLING-12734: Move AliasHandler into new class --- .../impl/mapping/AliasHandler.java | 1058 ++++++++++---------- 1 file changed, 529 insertions(+), 529 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 old mode 100755 new mode 100644 index 4982fcd4..8d3c10d9 --- a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/AliasHandler.java +++ b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/AliasHandler.java @@ -1,529 +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); - } - } - } -} +/* + * 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); + } + } + } +}
