You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by re...@apache.org on 2025/02/26 14:09:58 UTC
(sling-org-apache-sling-resourceresolver) 02/02: experimental alias refactoring
This is an automated email from the ASF dual-hosted git repository.
reschke pushed a commit to branch reschke/aliases
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-resourceresolver.git
commit b19e2f41d1346e47a17a6b30ea8b93490d39aaa8
Author: Julian Reschke <re...@apache.org>
AuthorDate: Wed Feb 26 15:09:37 2025 +0100
experimental alias refactoring
---
.../impl/mapping/AliasHandler.java | 920 ++++++++++-----------
1 file changed, 460 insertions(+), 460 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 9b50071..e6d0fdc
--- 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,460 +1,460 @@
-/*
- * 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 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.Nullable;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.ArrayList;
-import java.util.Collection;
-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.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.concurrent.locks.ReentrantLock;
-
-public 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 final 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);
- }
-
- /**
- * 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 {
- if (this.factory == null) {
- return this.factory.isOptimizeAliasResolutionEnabled();
- }
-
- 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);
- }
-
- /**
- * 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;
- }
-
- /**
- * 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(resolver, "alias", baseQueryString);
- } catch (UnsupportedOperationException ex) {
- log.debug("query failed as unsupported, retrying without paging/sorting", ex);
- it = queryUnpaged(resolver, "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;
- }
-
- /**
- * 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;
- }
- }
-
- /*
- * 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();
- }
-
- /**
- * Check alias syntax
- */
- private static 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;
- }
-
- /**
- * 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(final String contentPath, final String path, final AtomicBoolean resolverRefreshed) {
- // 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) {
- this.refreshResolverIfNecessary(resolverRefreshed);
-
- String prefix = contentPath.endsWith("/") ? contentPath : contentPath + "/";
- if (aliasMapEntry.entrySet().removeIf(e -> (prefix + e.getKey()).startsWith(resourcePath)) && (aliasMapEntry.isEmpty())) {
- this.aliasMapsMap.remove(contentPath);
- }
-
- Resource containingResource = this.resolver != null ? this.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();
- }
- }
-
- private Iterator<Resource> queryUnpaged(ResourceResolver resolver, 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;
-
- 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 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.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collection;
+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.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.ReentrantLock;
+
+public 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 final 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);
+ }
+
+ /**
+ * 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 {
+ if (this.factory == null) {
+ return this.factory.isOptimizeAliasResolutionEnabled();
+ }
+
+ 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);
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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(resolver, "alias", baseQueryString);
+ } catch (UnsupportedOperationException ex) {
+ log.debug("query failed as unsupported, retrying without paging/sorting", ex);
+ it = queryUnpaged(resolver, "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;
+ }
+
+ /**
+ * 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;
+ }
+ }
+
+ /*
+ * 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();
+ }
+
+ /**
+ * Check alias syntax
+ */
+ private static 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;
+ }
+
+ /**
+ * 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(final String contentPath, final String path, final AtomicBoolean resolverRefreshed) {
+ // 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) {
+ this.refreshResolverIfNecessary(resolverRefreshed);
+
+ String prefix = contentPath.endsWith("/") ? contentPath : contentPath + "/";
+ if (aliasMapEntry.entrySet().removeIf(e -> (prefix + e.getKey()).startsWith(resourcePath)) && (aliasMapEntry.isEmpty())) {
+ this.aliasMapsMap.remove(contentPath);
+ }
+
+ Resource containingResource = this.resolver != null ? this.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();
+ }
+ }
+
+ private Iterator<Resource> queryUnpaged(ResourceResolver resolver, 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;
+
+ 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);
+ }
+ }
+ }
+}