blob: ee62ea1fa35a6bb27237402d55ab5dc825442401 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.sling.resourceresolver.impl.mapping;
import org.apache.commons.collections4.map.LRUMap;
import org.apache.commons.lang3.StringUtils;
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;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.resource.observation.ExternalResourceChangeListener;
import org.apache.sling.api.resource.observation.ResourceChange;
import org.apache.sling.api.resource.observation.ResourceChangeListener;
import org.apache.sling.api.resource.path.Path;
import org.apache.sling.resourceresolver.impl.ResourceResolverImpl;
import org.apache.sling.resourceresolver.impl.ResourceResolverMetrics;
import org.apache.sling.resourceresolver.impl.mapping.MapConfigurationProvider.VanityPathConfig;
import org.jetbrains.annotations.NotNull;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
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 java.util.stream.Stream;
public class MapEntries implements
MapEntriesHandler,
ResourceChangeListener,
ExternalResourceChangeListener {
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 static final String PROP_REG_EXP = "sling:match";
public static final String PROP_REDIRECT_EXTERNAL = "sling:redirect";
public static final String PROP_REDIRECT_EXTERNAL_STATUS = "sling:status";
public static final String PROP_REDIRECT_EXTERNAL_REDIRECT_STATUS = "sling:redirectStatus";
public static final String PROP_VANITY_PATH = "sling:vanityPath";
public static final String PROP_VANITY_ORDER = "sling:vanityOrder";
private static final int VANITY_BLOOM_FILTER_MAX_ENTRIES = 10000000;
/** Key for the global list. */
private static final String GLOBAL_LIST_KEY = "*";
public static final String DEFAULT_MAP_ROOT = "/etc/map";
public static final int DEFAULT_DEFAULT_VANITY_PATH_REDIRECT_STATUS = HttpServletResponse.SC_FOUND;
@SuppressWarnings("java:S1075") // Repository path
private static final String JCR_SYSTEM_PATH = "/jcr:system";
private static final String JCR_SYSTEM_PREFIX = JCR_SYSTEM_PATH + '/';
static final String ANY_SCHEME_HOST = "[^/]+/[^/]+";
/** default log */
private final Logger log = LoggerFactory.getLogger(MapEntries.class);
private volatile MapConfigurationProvider factory;
private volatile ResourceResolver resolver;
private volatile EventAdmin eventAdmin;
private Optional<ResourceResolverMetrics> metrics;
private volatile ServiceRegistration<ResourceChangeListener> registration;
private Map<String, List<MapEntry>> resolveMapsMap;
// Temporary cache for use while doing async vanity path query
private Map<String, List<MapEntry>> temporaryResolveMapsMap;
private List<Map.Entry<String, ResourceChange.ChangeType>> resourceChangeQueue;
private AtomicLong temporaryResolveMapsMapHits = new AtomicLong();
private AtomicLong temporaryResolveMapsMapMisses = new AtomicLong();
private Collection<MapEntry> mapMaps;
private Map <String,List <String>> vanityTargets;
/**
* 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;
private final AtomicLong aliasResourcesOnStartup;
private final ReentrantLock initializing = new ReentrantLock();
private final AtomicLong vanityCounter;
private final AtomicLong vanityResourcesOnStartup;
private final AtomicLong vanityPathLookups;
private final AtomicLong vanityPathBloomNegative;
private final AtomicLong vanityPathBloomFalsePositive;
private byte[] vanityBloomFilter;
private AtomicBoolean vanityPathsProcessed = new AtomicBoolean(false);
private final StringInterpolationProvider stringInterpolationProvider;
private final boolean useOptimizeAliasResolution;
public MapEntries(final MapConfigurationProvider factory,
final BundleContext bundleContext,
final EventAdmin eventAdmin,
final StringInterpolationProvider stringInterpolationProvider,
final Optional<ResourceResolverMetrics> metrics)
throws LoginException, IOException {
this.resolver = factory.getServiceResourceResolver(factory.getServiceUserAuthenticationInfo("mapping"));
this.factory = factory;
this.eventAdmin = eventAdmin;
this.resolveMapsMap = Collections.singletonMap(GLOBAL_LIST_KEY, Collections.emptyList());
this.mapMaps = Collections.<MapEntry> emptyList();
this.vanityTargets = Collections.<String,List <String>>emptyMap();
this.aliasMapsMap = new ConcurrentHashMap<>();
this.stringInterpolationProvider = stringInterpolationProvider;
this.aliasResourcesOnStartup = new AtomicLong(0);
this.useOptimizeAliasResolution = doInit();
this.registration = registerResourceChangeListener(bundleContext);
this.vanityCounter = new AtomicLong(0);
this.vanityResourcesOnStartup = new AtomicLong(0);
this.vanityPathLookups = new AtomicLong(0);
this.vanityPathBloomNegative = new AtomicLong(0);
this.vanityPathBloomFalsePositive = new AtomicLong(0);
initializeVanityPaths();
this.metrics = metrics;
if (metrics.isPresent()) {
this.metrics.get().setNumberOfVanityPathsSupplier(vanityCounter::get);
this.metrics.get().setNumberOfResourcesWithVanityPathsOnStartupSupplier(vanityResourcesOnStartup::get);
this.metrics.get().setNumberOfVanityPathLookupsSupplier(vanityPathLookups::get);
this.metrics.get().setNumberOfVanityPathBloomNegativeSupplier(vanityPathBloomNegative::get);
this.metrics.get().setNumberOfVanityPathBloomFalsePositiveSupplier(vanityPathBloomFalsePositive::get);
this.metrics.get().setNumberOfResourcesWithAliasedChildrenSupplier(() -> (long) aliasMapsMap.size());
this.metrics.get().setNumberOfResourcesWithAliasesOnStartupSupplier(aliasResourcesOnStartup::get);
}
}
private ServiceRegistration<ResourceChangeListener> registerResourceChangeListener(final BundleContext bundleContext) {
final Dictionary<String, Object> props = new Hashtable<>(); // NOSONAR - required by OSGi APIs
final String[] paths = new String[factory.getObservationPaths().length];
for (int i = 0; i < paths.length; i++) {
paths[i] = factory.getObservationPaths()[i].getPath();
}
props.put(ResourceChangeListener.PATHS, paths);
props.put(Constants.SERVICE_DESCRIPTION, "Apache Sling Map Entries Observation");
props.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation");
log.info("Registering for {}", Arrays.toString(factory.getObservationPaths()));
this.resourceChangeQueue = Collections.synchronizedList(new LinkedList<>());
return bundleContext.registerService(ResourceChangeListener.class, this, props);
}
/**
* 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 doInit() {
this.initializing.lock();
try {
final ResourceResolver resolver = this.resolver;
final MapConfigurationProvider factory = this.factory;
if (resolver == null || factory == null) {
return this.factory.isOptimizeAliasResolutionEnabled();
}
boolean isOptimizeAliasResolutionEnabled = this.factory.isOptimizeAliasResolutionEnabled();
//optimization made in SLING-2521
if (isOptimizeAliasResolutionEnabled) {
try {
final Map<String, Map<String, Collection<String>>> loadedMap = this.loadAliases(resolver);
this.aliasMapsMap = loadedMap;
} catch (final Exception e) {
logDisableAliasOptimization(e);
// disable optimize alias resolution
isOptimizeAliasResolutionEnabled = false;
}
}
this.resolveMapsMap = new ConcurrentHashMap<>();
doUpdateConfiguration();
sendChangeEvent();
return isOptimizeAliasResolutionEnabled;
} finally {
this.initializing.unlock();
}
}
/**
* Actual vanity paths initializer. Guards itself against concurrent use by
* using a ReentrantLock. Does nothing if the resource resolver has already
* been null-ed.
*
* @throws IOException in case of problems
*/
protected void initializeVanityPaths() throws IOException {
this.initializing.lock();
try {
if (this.factory.isVanityPathEnabled()) {
this.vanityBloomFilter = createVanityBloomFilter();
VanityPathInitializer vpi = new VanityPathInitializer(this.factory);
if (this.factory.isVanityPathCacheInitInBackground()) {
this.log.debug("bg init starting");
Thread vpinit = new Thread(vpi, "VanityPathInitializer");
vpinit.start();
} else {
vpi.run();
}
}
} finally {
this.initializing.unlock();
}
}
private class VanityPathInitializer implements Runnable {
private int SIZELIMIT = 10000;
private MapConfigurationProvider factory;
public VanityPathInitializer(MapConfigurationProvider factory) {
this.factory = factory;
}
@Override
public void run() {
temporaryResolveMapsMap = Collections.synchronizedMap(new LRUMap<>(SIZELIMIT));
execute();
}
private void drainQueue(List<Map.Entry<String, ResourceChange.ChangeType>> queue) {
final AtomicBoolean resolverRefreshed = new AtomicBoolean(false);
// send the change event only once
boolean sendEvent = false;
// the config needs to be reloaded only once
final AtomicBoolean hasReloadedConfig = new AtomicBoolean(false);
while (!queue.isEmpty()) {
Map.Entry<String, ResourceChange.ChangeType> entry = queue.remove(0);
final ResourceChange.ChangeType type = entry.getValue();
final String path = entry.getKey();
log.trace("drain type={}, path={}", type, path);
boolean changed = handleResourceChange(type, path, resolverRefreshed, hasReloadedConfig);
if (changed) {
sendEvent = true;
}
}
if (sendEvent) {
sendChangeEvent();
}
}
private void execute() {
try (ResourceResolver resolver = factory
.getServiceResourceResolver(factory.getServiceUserAuthenticationInfo("mapping"))) {
long initStart = System.nanoTime();
log.debug("vanity path initialization - start");
vanityTargets = loadVanityPaths(resolver);
// process pending event
drainQueue(resourceChangeQueue);
vanityPathsProcessed.set(true);
// drain once more in case more events have arrived
drainQueue(resourceChangeQueue);
long initElapsed = System.nanoTime() - initStart;
long resourcesPerSecond = (vanityResourcesOnStartup.get() * TimeUnit.SECONDS.toNanos(1) / (initElapsed == 0 ? 1 : initElapsed));
log.info(
"vanity path initialization - completed, processed {} resources with sling:vanityPath properties in {}ms (~{} resource/s)",
vanityResourcesOnStartup.get(), TimeUnit.NANOSECONDS.toMillis(initElapsed), resourcesPerSecond);
} catch (LoginException ex) {
log.error("Vanity path init failed", ex);
} finally {
log.debug("dropping temporary resolver map - {}/{} entries, {} hits, {} misses", temporaryResolveMapsMap.size(),
SIZELIMIT, temporaryResolveMapsMapHits.get(), temporaryResolveMapsMapMisses.get());
temporaryResolveMapsMap = null;
}
}
}
private boolean addResource(final String path, final AtomicBoolean resolverRefreshed) {
this.initializing.lock();
try {
this.refreshResolverIfNecessary(resolverRefreshed);
final Resource resource = this.resolver != null ? resolver.getResource(path) : null;
if (resource != null) {
boolean changed = doAddVanity(resource);
if (this.useOptimizeAliasResolution && resource.getValueMap().containsKey(ResourceResolverImpl.PROP_ALIAS)) {
changed |= doAddAlias(resource);
}
return changed;
}
return false;
} finally {
this.initializing.unlock();
}
}
private boolean updateResource(final String path, final AtomicBoolean resolverRefreshed) {
final boolean isValidVanityPath = this.isValidVanityPath(path);
if ( this.useOptimizeAliasResolution || isValidVanityPath) {
this.initializing.lock();
try {
this.refreshResolverIfNecessary(resolverRefreshed);
final Resource resource = this.resolver != null ? resolver.getResource(path) : null;
if (resource != null) {
boolean changed = false;
if ( isValidVanityPath ) {
// we remove the old vanity path first
changed |= doRemoveVanity(path);
// add back vanity path
Resource contentRsrc = null;
if ( !resource.getName().equals(JCR_CONTENT)) {
// there might be a JCR_CONTENT child resource
contentRsrc = resource.getChild(JCR_CONTENT);
}
changed |= doAddVanity(contentRsrc != null ? contentRsrc : resource);
}
if (this.useOptimizeAliasResolution) {
changed |= doUpdateAlias(resource);
}
return changed;
}
} finally {
this.initializing.unlock();
}
}
return false;
}
private boolean removeResource(final String path, final AtomicBoolean resolverRefreshed) {
boolean changed = false;
final String actualContentPath = getActualContentPath(path);
final String actualContentPathPrefix = actualContentPath + "/";
for (final String target : this.vanityTargets.keySet()) {
if (target.startsWith(actualContentPathPrefix) || target.equals(actualContentPath)) {
changed |= removeVanityPath(target);
}
}
if (this.useOptimizeAliasResolution) {
final String pathPrefix = path + "/";
for (final String contentPath : this.aliasMapsMap.keySet()) {
if (path.startsWith(contentPath + "/") || path.equals(contentPath)
|| contentPath.startsWith(pathPrefix)) {
changed |= removeAlias(contentPath, path, resolverRefreshed);
}
}
}
return changed;
}
/**
* 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(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 boolean removeVanityPath(final String path) {
this.initializing.lock();
try {
return doRemoveVanity(path);
} finally {
this.initializing.unlock();
}
}
/**
* Update the configuration.
* Does no locking and does not send an event at the end
*/
private void doUpdateConfiguration() {
final List<MapEntry> globalResolveMap = new ArrayList<>();
final SortedMap<String, MapEntry> newMapMaps = new TreeMap<>();
// load the /etc/map entries into the maps
loadResolverMap(resolver, globalResolveMap, newMapMaps);
// load the configuration into the resolver map
loadConfiguration(factory, globalResolveMap);
// load the configuration into the mapper map
loadMapConfiguration(factory, newMapMaps);
// sort global list and add to map
Collections.sort(globalResolveMap);
resolveMapsMap.put(GLOBAL_LIST_KEY, globalResolveMap);
this.mapMaps = Collections.unmodifiableSet(new TreeSet<>(newMapMaps.values()));
}
private boolean doAddVanity(final Resource resource) {
log.debug("doAddVanity getting {}", resource.getPath());
boolean updateTheCache = isAllVanityPathEntriesCached() || vanityCounter.longValue() < this.factory.getMaxCachedVanityPathEntries();
return null != loadVanityPath(resource, resolveMapsMap, vanityTargets, updateTheCache);
}
private boolean doRemoveVanity(final String path) {
final String actualContentPath = getActualContentPath(path);
final List <String> l = vanityTargets.remove(actualContentPath);
if (l != null){
for (final String s : l){
final List<MapEntry> entries = this.resolveMapsMap.get(s);
if (entries!= null) {
for (final Iterator<MapEntry> iterator =entries.iterator(); iterator.hasNext(); ) {
final MapEntry entry = iterator.next();
final String redirect = getMapEntryRedirect(entry);
if (redirect != null && redirect.equals(actualContentPath)) {
iterator.remove();
}
}
}
if (entries!= null && entries.isEmpty()) {
this.resolveMapsMap.remove(s);
}
}
if (vanityCounter.longValue() > 0) {
vanityCounter.addAndGet(-2);
}
return true;
}
return false;
}
private boolean doAddAlias(final Resource resource) {
return loadAlias(resource, this.aliasMapsMap);
}
/**
* Update alias from a resource
* @param resource The resource
* @return {@code true} if any change
*/
private boolean doUpdateAlias(final Resource resource) {
final Resource containingResource;
if (JCR_CONTENT.equals(resource.getName())) {
containingResource = resource.getParent();
} else {
containingResource = 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;
}
return false;
}
/**
* Cleans up this class.
*/
public void dispose() {
if (this.registration != null) {
this.registration.unregister();
this.registration = null;
}
/*
* Cooperation with doInit: The same lock as used by doInit is acquired
* thus preventing doInit from running and waiting for a concurrent
* doInit to terminate. Once the lock has been acquired, the resource
* resolver is null-ed (thus causing the init to terminate when
* triggered the right after and prevent the doInit method from doing
* any thing).
*/
// wait at most 10 seconds for a notifcation during initialization
boolean initLocked;
try {
initLocked = this.initializing.tryLock(10, TimeUnit.SECONDS);
} catch (final InterruptedException ie) {
initLocked = false;
}
try {
if (!initLocked) {
log.warn("dispose: Could not acquire initialization lock within 10 seconds; ongoing intialization may fail");
}
// immediately set the resolver field to null to indicate
// that we have been disposed (this also signals to the
// event handler to stop working
final ResourceResolver oldResolver = this.resolver;
this.resolver = null;
if (oldResolver != null) {
oldResolver.close();
} else {
log.warn("dispose: ResourceResolver has already been cleared before; duplicate call to dispose ?");
}
} finally {
if (initLocked) {
this.initializing.unlock();
}
}
// clear the rest of the fields
this.factory = null;
this.eventAdmin = null;
}
@Override
public List<MapEntry> getResolveMaps() {
final List<MapEntry> entries = new ArrayList<>();
for (final List<MapEntry> list : this.resolveMapsMap.values()) {
entries.addAll(list);
}
Collections.sort(entries);
return Collections.unmodifiableList(entries);
}
@Override
public Iterator<MapEntry> getResolveMapsIterator(final String requestPath) {
String key = null;
final int firstIndex = requestPath.indexOf('/');
final int secondIndex = requestPath.indexOf('/', firstIndex + 1);
if (secondIndex != -1) {
key = requestPath.substring(secondIndex);
}
return new MapEntryIterator(key, resolveMapsMap, this.factory.hasVanityPathPrecedence());
}
@Override
public Collection<MapEntry> getMapMaps() {
return mapMaps;
}
public boolean isOptimizeAliasResolutionEnabled() {
return this.useOptimizeAliasResolution;
}
@Override
public @NotNull Map<String, Collection<String>> getAliasMap(final String parentPath) {
Map<String, Collection<String>> aliasMapForParent = aliasMapsMap.get(parentPath);
return aliasMapForParent != null ? aliasMapForParent : Collections.emptyMap();
}
@Override
public Map<String, List<String>> getVanityPathMappings() {
return Collections.unmodifiableMap(vanityTargets);
}
// special singleton entry for negative cache entries
private static final List<MapEntry> NO_MAP_ENTRIES = Collections.emptyList();
/**
* get the MapEntry list containing all the nodes having a specific vanityPath
*/
private List<MapEntry> getMapEntryList(String vanityPath) {
List<MapEntry> mapEntries = null;
boolean initFinished = vanityPathsProcessed.get();
boolean probablyPresent = false;
if (initFinished) {
// total number of lookups after init (and when cache not complete)
long current = this.vanityPathLookups.incrementAndGet();
if (current >= Long.MAX_VALUE - 100000) {
// reset counters when we get close the limit
this.vanityPathLookups.set(1);
this.vanityPathBloomNegative.set(0);
this.vanityPathBloomFalsePositive.set(0);
log.info("Vanity Path metrics reset to 0");
}
// init is done - check the bloom filter
probablyPresent = BloomFilterUtils.probablyContains(vanityBloomFilter, vanityPath);
log.trace("bloom filter lookup for {} -> {}", vanityPath, probablyPresent);
if (!probablyPresent) {
// filtered by Bloom filter
this.vanityPathBloomNegative.incrementAndGet();
}
}
if (!initFinished || probablyPresent) {
mapEntries = this.resolveMapsMap.get(vanityPath);
if (mapEntries == null) {
if (!initFinished && temporaryResolveMapsMap != null) {
mapEntries = temporaryResolveMapsMap.get(vanityPath);
if (mapEntries != null) {
temporaryResolveMapsMapHits.incrementAndGet();
log.trace("getMapEntryList: using temp map entries for {} -> {}", vanityPath, mapEntries);
} else {
temporaryResolveMapsMapMisses.incrementAndGet();
}
}
if (mapEntries == null) {
Map<String, List<MapEntry>> mapEntry = getVanityPaths(vanityPath);
mapEntries = mapEntry.get(vanityPath);
if (!initFinished && temporaryResolveMapsMap != null) {
log.trace("getMapEntryList: caching map entries for {} -> {}", vanityPath, mapEntries);
temporaryResolveMapsMap.put(vanityPath, mapEntries == null ? NO_MAP_ENTRIES : mapEntries);
}
}
}
if (mapEntries == null && probablyPresent) {
// Bloom filter had a false positive
this.vanityPathBloomFalsePositive.incrementAndGet();
}
}
return mapEntries == NO_MAP_ENTRIES ? null : mapEntries;
}
/**
* Refresh the resource resolver if not already done
* @param resolverRefreshed Boolean flag containing the state if the resolver
* has been refreshed. True in any case when this
* method returns
*/
private void refreshResolverIfNecessary(final AtomicBoolean resolverRefreshed) {
if ( resolverRefreshed.compareAndSet(false, true) ) {
this.resolver.refresh();
}
}
/**
* Checks if the path affects the map configuration. If it does
* the configuration is updated.
* @param path The changed path (could be add/remove/update)
* @param hasReloadedConfig If this is already true, the config will not be reloaded
* @param resolverRefreshed Boolean flag handling resolver refresh
* @param isDelete If this is a delete event
* @return {@code true} if the configuration has been updated, {@code false} if
* the path does not affect a config change, {@code null} if the config has already
* been reloaded.
*/
private Boolean handleConfigurationUpdate(final String path,
final AtomicBoolean hasReloadedConfig,
final AtomicBoolean resolverRefreshed,
final boolean isDelete) {
if ( this.factory.isMapConfiguration(path)
|| (isDelete && this.factory.getMapRoot().startsWith(path + "/")) ) {
if ( hasReloadedConfig.compareAndSet(false, true) ) {
this.initializing.lock();
try {
if (this.resolver != null) {
refreshResolverIfNecessary(resolverRefreshed);
doUpdateConfiguration();
}
} finally {
this.initializing.unlock();
}
return true;
}
return null;
}
return false;
}
// ---------- ResourceChangeListener interface
/**
* Handles the change to any of the node properties relevant for vanity URL
* mappings. The {@link #MapEntries(MapConfigurationProvider, BundleContext, EventAdmin, StringInterpolationProvider, Optional)}
* constructor makes sure the event listener is registered to only get
* appropriate events.
*/
@Override
public void onChange(final List<ResourceChange> changes) {
final boolean inStartup = !vanityPathsProcessed.get();
final AtomicBoolean resolverRefreshed = new AtomicBoolean(false);
// send the change event only once
boolean sendEvent = false;
// the config needs to be reloaded only once
final AtomicBoolean hasReloadedConfig = new AtomicBoolean(false);
for (final ResourceChange rc : changes) {
final ResourceChange.ChangeType type = rc.getType();
final String path = rc.getPath();
log.debug("onChange, type={}, path={}", rc.getType(), path);
// don't care for system area
if (path.startsWith(JCR_SYSTEM_PREFIX)) {
continue;
}
// during startup: just enqueue the events
if (inStartup) {
if (type == ResourceChange.ChangeType.REMOVED || type == ResourceChange.ChangeType.ADDED
|| type == ResourceChange.ChangeType.CHANGED) {
Map.Entry<String, ResourceChange.ChangeType> entry = new SimpleEntry<>(path, type);
log.trace("enqueue: {}", entry);
resourceChangeQueue.add(entry);
}
} else {
boolean changed = handleResourceChange(type, path, resolverRefreshed, hasReloadedConfig);
if (changed) {
sendEvent = true;
}
}
}
if (sendEvent) {
this.sendChangeEvent();
}
}
private boolean handleResourceChange(ResourceChange.ChangeType type, String path, AtomicBoolean resolverRefreshed,
AtomicBoolean hasReloadedConfig) {
boolean changed = false;
// removal of a resource is handled differently
if (type == ResourceChange.ChangeType.REMOVED) {
final Boolean result = handleConfigurationUpdate(path, hasReloadedConfig, resolverRefreshed, true);
if (result != null) {
if (result) {
changed = true;
} else {
changed |= removeResource(path, resolverRefreshed);
}
}
// session.move() is handled differently see also SLING-3713 and
} else if (type == ResourceChange.ChangeType.ADDED) {
final Boolean result = handleConfigurationUpdate(path, hasReloadedConfig, resolverRefreshed, false);
if (result != null) {
if (result) {
changed = true;
} else {
changed |= addResource(path, resolverRefreshed);
}
}
} else if (type == ResourceChange.ChangeType.CHANGED) {
final Boolean result = handleConfigurationUpdate(path, hasReloadedConfig, resolverRefreshed, false);
if (result != null) {
if (result) {
changed = true;
} else {
changed |= updateResource(path, resolverRefreshed);
}
}
}
return changed;
}
// ---------- internal
private byte[] createVanityBloomFilter() throws IOException {
return BloomFilterUtils.createFilter(VANITY_BLOOM_FILTER_MAX_ENTRIES, this.factory.getVanityBloomFilterMaxBytes());
}
private boolean isAllVanityPathEntriesCached() {
return this.factory.getMaxCachedVanityPathEntries() == -1;
}
// escapes string for use as literal in JCR SQL within single quotes
private static String queryLiteral(String input) {
return input.replace("'", "''");
}
/**
* get the vanity paths Search for all nodes having a specific vanityPath
*/
private Map<String, List<MapEntry>> getVanityPaths(String vanityPath) {
Map<String, List<MapEntry>> entryMap = new HashMap<>();
final String queryString = String.format(
"SELECT [sling:vanityPath], [sling:redirect], [sling:redirectStatus] FROM [nt:base] "
+ "WHERE NOT isdescendantnode('%s') AND ([sling:vanityPath]='%s' OR [sling:vanityPath]='%s') "
+ "ORDER BY [sling:vanityOrder] DESC",
JCR_SYSTEM_PATH, queryLiteral(vanityPath), queryLiteral(vanityPath.substring(1)));
try (ResourceResolver queryResolver = factory.getServiceResourceResolver(factory.getServiceUserAuthenticationInfo("mapping"));) {
long totalCount = 0;
long totalValid = 0;
log.debug("start vanityPath query: {}", queryString);
final Iterator<Resource> i = queryResolver.findResources(queryString, "JCR-SQL2");
log.debug("end vanityPath query");
while (i.hasNext()) {
totalCount += 1;
final Resource resource = i.next();
boolean isValid = false;
for(final Path sPath : this.factory.getObservationPaths()) {
if ( sPath.matches(resource.getPath())) {
isValid = true;
break;
}
}
if ( isValid ) {
totalValid += 1;
if (this.vanityPathsProcessed.get() && (this.factory.isMaxCachedVanityPathEntriesStartup() || vanityCounter.longValue() < this.factory.getMaxCachedVanityPathEntries())) {
loadVanityPath(resource, resolveMapsMap, vanityTargets, true);
entryMap = resolveMapsMap;
} else {
final Map <String, List<String>> targetPaths = new HashMap<>();
loadVanityPath(resource, entryMap, targetPaths, true);
}
}
}
log.debug("read {} ({} valid) vanityPaths", totalCount, totalValid);
} catch (LoginException e) {
log.error("Exception while obtaining queryResolver", e);
}
return entryMap;
}
/**
* Check if the path is a valid vanity path
* @param path The resource path to check
* @return {@code true} if this is valid, {@code false} otherwise
*/
private boolean isValidVanityPath(final String path){
if (path == null) {
throw new IllegalArgumentException("Unexpected null path");
}
// ignore system tree
if (path.startsWith(JCR_SYSTEM_PREFIX)) {
log.debug("isValidVanityPath: not valid {}", path);
return false;
}
// check allow/deny list
if ( this.factory.getVanityPathConfig() != null ) {
boolean allowed = false;
for(final VanityPathConfig config : this.factory.getVanityPathConfig()) {
if ( path.startsWith(config.prefix) ) {
allowed = !config.isExclude;
break;
}
}
if ( !allowed ) {
log.debug("isValidVanityPath: not valid as not in allow list {}", path);
return false;
}
}
return true;
}
private String getActualContentPath(final String path){
final String checkPath;
if ( path.endsWith(JCR_CONTENT_SUFFIX) ) {
checkPath = ResourceUtil.getParent(path);
} else {
checkPath = path;
}
return checkPath;
}
private String getMapEntryRedirect(final MapEntry mapEntry) {
String[] redirect = mapEntry.getRedirect();
if (redirect.length > 1) {
log.warn("something went wrong, please restart the bundle");
return null;
}
String path = redirect[0];
if (path.endsWith("$1")) {
path = path.substring(0, path.length() - "$1".length());
} else if (path.endsWith(".html")) {
path = path.substring(0, path.length() - ".html".length());
}
return path;
}
/**
* Send an OSGi event
*/
private void sendChangeEvent() {
final EventAdmin local = this.eventAdmin;
if (local != null) {
final Event event = new Event(SlingConstants.TOPIC_RESOURCE_RESOLVER_MAPPING_CHANGED,
(Dictionary<String, ?>) null);
local.postEvent(event);
}
}
private void loadResolverMap(final ResourceResolver resolver, final List<MapEntry> entries, final Map<String, MapEntry> mapEntries) {
// the standard map configuration
final Resource res = resolver.getResource(this.factory.getMapRoot());
if (res != null) {
gather(resolver, entries, mapEntries, res, "");
}
}
private void gather(final ResourceResolver resolver, final List<MapEntry> entries, final Map<String, MapEntry> mapEntries,
final Resource parent, final String parentPath) {
// scheme list
final Iterator<Resource> children = parent.listChildren();
while (children.hasNext()) {
final Resource child = children.next();
final ValueMap vm = ResourceUtil.getValueMap(child);
String name = vm.get(PROP_REG_EXP, String.class);
boolean trailingSlash = false;
if (name == null) {
name = child.getName().concat("/");
trailingSlash = true;
}
// Check for placeholders and replace if needed
name = stringInterpolationProvider.substitute(name);
final String childPath = parentPath.concat(name);
// gather the children of this entry (only if child is not end
// hooked)
if (!childPath.endsWith("$")) {
// add trailing slash to child path to append the child
String childParent = childPath;
if (!trailingSlash) {
childParent = childParent.concat("/");
}
gather(resolver, entries, mapEntries, child, childParent);
}
// add resolution entries for this node
MapEntry childResolveEntry = null;
try{
childResolveEntry=MapEntry.createResolveEntry(childPath, child, trailingSlash);
}catch (IllegalArgumentException iae){
//ignore this entry
log.debug("ignored entry due exception ",iae);
}
if (childResolveEntry != null) {
entries.add(childResolveEntry);
}
// add map entries for this node
final List<MapEntry> childMapEntries = MapEntry.createMapEntry(childPath, child, trailingSlash);
if (childMapEntries != null) {
for (final MapEntry mapEntry : childMapEntries) {
addMapEntry(mapEntries, mapEntry.getPattern(), mapEntry.getRedirect()[0], mapEntry.getStatus());
}
}
}
}
/**
* Add an entry to the resolve map.
*/
private boolean addEntry(final Map<String, List<MapEntry>> entryMap, final String key, final MapEntry entry) {
if (entry == null) {
log.trace("trying to add null entry for {}", key);
return false;
} else {
List<MapEntry> entries = entryMap.get(key);
if (entries == null) {
entries = new ArrayList<>();
entries.add(entry);
entryMap.put(key, entries);
} else {
List<MapEntry> entriesCopy = new ArrayList<>(entries);
entriesCopy.add(entry);
// and finally sort list
Collections.sort(entriesCopy);
entryMap.put(key, entriesCopy);
int size = entriesCopy.size();
if (size == 10) {
log.debug(">= 10 MapEntries for {} - check your configuration", key);
} else if (size == 100) {
log.info(">= 100 MapEntries for {} - check your configuration", key);
}
}
return true;
}
}
/**
* 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(final ResourceResolver resolver) {
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);
}
long processElapsed = System.nanoTime() - processStart;
long resourcePerSecond = (count * TimeUnit.SECONDS.toNanos(1) / (processElapsed == 0 ? 1 : processElapsed));
log.info("alias initialization - completed, processed {} resources with sling:alias properties in {}ms (~{} resource/s)",
count, TimeUnit.NANOSECONDS.toMillis(processElapsed), resourcePerSecond);
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()) {
String jcrSystemPath = StringUtils.removeEnd(JCR_SYSTEM_PREFIX, "/");
baseQuery.append(" NOT isdescendantnode('").append(queryLiteral(jcrSystemPath)).append("')");
} else {
Iterator<String> pathIterator = allowedLocations.iterator();
baseQuery.append(" (");
String sep = "";
while (pathIterator.hasNext()) {
String prefix = pathIterator.next();
baseQuery.append(sep).append("isdescendantnode('").append(queryLiteral(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) {
// resource containing the alias
final Resource containingResource;
if (JCR_CONTENT.equals(resource.getName())) {
containingResource = resource.getParent();
} else {
containingResource = resource;
}
final Resource parent = containingResource.getParent();
if (parent == null) {
log.debug("parent is null for alias on {}.", resource.getName());
return false;
}
else {
// resource the alias is for
String resourceName = containingResource.getName();
// parent path of that resource
String parentPath = parent.getPath();
boolean hasAlias = false;
// require properties
final ValueMap props = resource.getValueMap();
final String[] aliasArray = props.get(ResourceResolverImpl.PROP_ALIAS, String[].class);
if (aliasArray != null) {
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 (isAliasValid(alias)) {
log.warn("Encountered invalid alias {} under parent path {}. Refusing to use it.", alias, parentPath);
} 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()) {
log.warn(
"Encountered duplicate alias {} under parent path {}. Refusing to replace current target {} with {}.",
alias, parentPath, siblingResourceNameWithDuplicateAlias.get(), resourceName);
} else {
Collection<String> existingAliases = parentMap.computeIfAbsent(resourceName, name -> new CopyOnWriteArrayList<>());
existingAliases.add(alias);
hasAlias = true;
}
}
}
}
return hasAlias;
}
}
/**
* Check alias syntax
*/
private static boolean isAliasValid(String alias) {
boolean invalid = alias.equals("..") || alias.equals(".");
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;
}
/**
* Utility class for running paged queries.
*/
private class PagedQueryIterator implements Iterator<Resource> {
private ResourceResolver resolver;
private String subject;
private String propertyName;
private String query;
private String lastValue = "";
private Iterator<Resource> it;
private int count = 0;
private int page = 0;
private int pageSize;
private Resource next = null;
private String[] defaultValue = new String[0];
/**
* @param subject name of the query, will be used only for logging
* @param propertyName name of multivalued string property to query on
* @param resolver resource resolver
* @param query query string in SQL2 syntax
* @param pageSize page size (start a new query after page size is exceeded)
*/
public PagedQueryIterator(String subject, String propertyName, ResourceResolver resolver, String query, int pageSize) {
this.subject = subject;
this.propertyName = propertyName;
this.resolver = resolver;
this.query = query;
this.pageSize = pageSize;
nextPage();
}
private void nextPage() {
count = 0;
String tquery = String.format(query, queryLiteral(lastValue));
log.debug("start {} query (page {}): {}", subject, page, tquery);
long queryStart = System.nanoTime();
this.it = resolver.findResources(tquery, "JCR-SQL2");
long queryElapsed = System.nanoTime() - queryStart;
log.debug("end {} query (page {}); elapsed {}ms", subject, page, TimeUnit.NANOSECONDS.toMillis(queryElapsed));
page += 1;
}
private Resource getNext() throws NoSuchElementException {
Resource resource = it.next();
count += 1;
final String[] values = resource.getValueMap().get(propertyName, defaultValue);
if (values.length > 0) {
String value = values[0];
if (value.compareTo(lastValue) < 0) {
String message = String.format("unexpected query result in page %d, %s of '%s' despite querying for > '%s'",
(page - 1), propertyName, value, lastValue);
log.error(message);
throw new RuntimeException(message);
}
// start next page?
if (count > pageSize && !value.equals(lastValue)) {
log.debug("read {} query (page {}); {} entries", subject, page, count);
lastValue = value;
nextPage();
}
}
return resource;
}
@Override
public boolean hasNext() {
if (next == null) {
try {
next = getNext();
} catch (NoSuchElementException ex) {
// there are no more
next = null;
}
}
return next != null;
}
@Override
public Resource next() throws NoSuchElementException {
Resource result = next != null ? next : getNext();
next = null;
return result;
}
}
/**
* Load vanity paths - search for all nodes (except under /jcr:system)
* having a sling:vanityPath property
*/
private Map<String, List<String>> loadVanityPaths(ResourceResolver resolver) {
final Map<String, List<String>> targetPaths = new ConcurrentHashMap<>();
final String baseQueryString = "SELECT [sling:vanityPath], [sling:redirect], [sling:redirectStatus]" + " FROM [nt:base]"
+ " WHERE NOT isdescendantnode('" + queryLiteral(JCR_SYSTEM_PATH) + "')"
+ " AND [sling:vanityPath] IS NOT NULL";
boolean supportsSort = true;
Iterator<Resource> it;
try {
final String queryStringWithSort = baseQueryString + " AND FIRST([sling:vanityPath]) > '%s' ORDER BY FIRST([sling:vanityPath])";
it = new PagedQueryIterator("vanity path", PROP_VANITY_PATH, resolver, queryStringWithSort, 2000);
} catch (QuerySyntaxException ex) {
log.debug("sort with first() not supported, falling back to base query", ex);
supportsSort = false;
it = queryUnpaged("vanity path", baseQueryString);
} catch (UnsupportedOperationException ex) {
log.debug("query failed as unsupported, retrying without paging/sorting", ex);
supportsSort = false;
it = queryUnpaged("vanity path", baseQueryString);
}
long count = 0;
long countInScope = 0;
long processStart = System.nanoTime();
String previousVanityPath = null;
while (it.hasNext()) {
count += 1;
final Resource resource = it.next();
final String resourcePath = resource.getPath();
if (Stream.of(this.factory.getObservationPaths()).anyMatch(path -> path.matches(resourcePath))) {
countInScope += 1;
final boolean addToCache = isAllVanityPathEntriesCached()
|| vanityCounter.longValue() < this.factory.getMaxCachedVanityPathEntries();
String firstVanityPath = loadVanityPath(resource, resolveMapsMap, targetPaths, addToCache);
if (supportsSort && firstVanityPath != null) {
if (previousVanityPath != null && firstVanityPath.compareTo(previousVanityPath) < 0) {
log.error("Sorting by first(vanityPath) does not appear to work; got " + firstVanityPath + " after " + previousVanityPath);
}
previousVanityPath = firstVanityPath;
}
}
}
long processElapsed = System.nanoTime() - processStart;
log.debug("processed {} resources with sling:vanityPath properties (of which {} in scope) in {}ms", count, countInScope, TimeUnit.NANOSECONDS.toMillis(processElapsed));
if (!isAllVanityPathEntriesCached()) {
if (countInScope > this.factory.getMaxCachedVanityPathEntries()) {
log.warn("Number of resources with sling:vanityPath property ({}) exceeds configured cache size ({}); handling of uncached vanity paths will be much slower. Consider increasing the cache size or decreasing the number of vanity paths.", countInScope, this.factory.getMaxCachedVanityPathEntries());
} else if (countInScope > (this.factory.getMaxCachedVanityPathEntries() / 10) * 9) {
log.info("Number of resources with sling:vanityPath property in scope ({}) within 10% of configured cache size ({})", countInScope, this.factory.getMaxCachedVanityPathEntries());
}
}
this.vanityResourcesOnStartup.set(count);
return targetPaths;
}
/**
* Load vanity path given a resource
*
* @return first vanity path or {@code null}
*/
private String loadVanityPath(final Resource resource, final Map<String, List<MapEntry>> entryMap, final Map <String, List<String>> targetPaths, boolean addToCache) {
if (!isValidVanityPath(resource.getPath())) {
return null;
}
final ValueMap props = resource.getValueMap();
long vanityOrder = props.get(PROP_VANITY_ORDER, 0L);
// url is ignoring scheme and host.port and the path is
// what is stored in the sling:vanityPath property
boolean hasVanityPath = false;
final String[] pVanityPaths = props.get(PROP_VANITY_PATH, new String[0]);
if (log.isTraceEnabled()) {
log.trace("vanity paths on {}: {}", resource.getPath(), Arrays.asList(pVanityPaths));
}
for (final String pVanityPath : pVanityPaths) {
final String[] result = this.getVanityPathDefinition(resource.getPath(), pVanityPath);
if (result != null) {
hasVanityPath = true;
final String url = result[0] + result[1];
// redirect target is the node providing the
// sling:vanityPath
// property (or its parent if the node is called
// jcr:content)
final Resource redirectTarget;
if (JCR_CONTENT.equals(resource.getName())) {
redirectTarget = resource.getParent();
} else {
redirectTarget = resource;
}
final String redirect = redirectTarget.getPath();
final String redirectName = redirectTarget.getName();
// whether the target is attained by a external redirect or
// by an internal redirect is defined by the sling:redirect
// property
final int status = props.get(PROP_REDIRECT_EXTERNAL, false) ? props.get(
PROP_REDIRECT_EXTERNAL_REDIRECT_STATUS, factory.getDefaultVanityPathRedirectStatus())
: -1;
final String checkPath = result[1];
boolean addedEntry;
if (addToCache) {
if (redirectName.indexOf('.') > -1) {
// 1. entry with exact match
this.addEntry(entryMap, checkPath, getMapEntry(url + "$", status, false, vanityOrder, redirect));
final int idx = redirectName.lastIndexOf('.');
final String extension = redirectName.substring(idx + 1);
// 2. entry with extension
addedEntry = this.addEntry(entryMap, checkPath, getMapEntry(url + "\\." + extension, status, false, vanityOrder, redirect));
} else {
// 1. entry with exact match
this.addEntry(entryMap, checkPath, getMapEntry(url + "$", status, false, vanityOrder, redirect + ".html"));
// 2. entry with match supporting selectors and extension
addedEntry = this.addEntry(entryMap, checkPath, getMapEntry(url + "(\\..*)", status, false, vanityOrder, redirect + "$1"));
}
if (addedEntry) {
// 3. keep the path to return
this.updateTargetPaths(targetPaths, redirect, checkPath);
//increment only if the instance variable
if (entryMap == resolveMapsMap) {
vanityCounter.addAndGet(2);
}
// update bloom filter
BloomFilterUtils.add(vanityBloomFilter, checkPath);
}
} else {
// update bloom filter
BloomFilterUtils.add(vanityBloomFilter, checkPath);
}
}
}
return hasVanityPath ? pVanityPaths[0] : null;
}
private void updateTargetPaths(final Map<String, List<String>> targetPaths, final String key, final String entry) {
if (entry == null) {
return;
}
List<String> entries = targetPaths.get(key);
if (entries == null) {
entries = new ArrayList<>();
targetPaths.put(key, entries);
}
entries.add(entry);
}
/**
* Create the vanity path definition. String array containing:
* {protocol}/{host}[.port] {absolute path}
*/
private String[] getVanityPathDefinition(final String sourcePath, final String vanityPath) {
if (vanityPath == null) {
log.trace("getVanityPathDefinition: null vanity path on {}", sourcePath);
return null;
}
String info = vanityPath.trim();
if (info.isEmpty()) {
log.trace("getVanityPathDefinition: empty vanity path on {}", sourcePath);
return null;
}
String prefix = null;
String path = null;
// check for URL-shaped path
if (info.indexOf(":/") > -1) {
try {
final URL u = new URL(info);
prefix = u.getProtocol() + '/' + u.getHost() + '.' + u.getPort();
path = u.getPath();
} catch (final MalformedURLException e) {
log.warn("Ignoring malformed vanity path '{}' on {}", info, sourcePath);
return null;
}
} else {
prefix = "^" + ANY_SCHEME_HOST;
if (!info.startsWith("/")) {
path = "/" + info;
} else {
path = info;
}
}
// remove extension
int lastSlash = path.lastIndexOf('/');
int firstDot = path.indexOf('.', lastSlash + 1);
if (firstDot != -1) {
path = path.substring(0, firstDot);
log.warn("Removing extension from vanity path '{}' on {}", info, sourcePath);
}
return new String[] { prefix, path };
}
private void loadConfiguration(final MapConfigurationProvider factory, final List<MapEntry> entries) {
// virtual uris
final Map<String, String> virtuals = factory.getVirtualURLMap();
if (virtuals != null) {
for (final Entry<String, String> virtualEntry : virtuals.entrySet()) {
final String extPath = virtualEntry.getKey();
final String intPath = virtualEntry.getValue();
if (!extPath.equals(intPath)) {
// this regular expression must match the whole URL !!
final String url = "^" + ANY_SCHEME_HOST + extPath + "$";
final String redirect = intPath;
MapEntry mapEntry = getMapEntry(url, -1, false, redirect);
if (mapEntry!=null){
entries.add(mapEntry);
}
}
}
}
// URL Mappings
final Mapping[] mappings = factory.getMappings();
if (mappings != null) {
final Map<String, List<String>> map = new HashMap<>();
for (final Mapping mapping : mappings) {
if (mapping.mapsInbound()) {
final String url = mapping.getTo();
final String alias = mapping.getFrom();
if (url.length() > 0) {
List<String> aliasList = map.get(url);
if (aliasList == null) {
aliasList = new ArrayList<>();
map.put(url, aliasList);
}
aliasList.add(alias);
}
}
}
for (final Entry<String, List<String>> entry : map.entrySet()) {
MapEntry mapEntry = getMapEntry(ANY_SCHEME_HOST + entry.getKey(), -1, false, entry.getValue().toArray(new String[0]));
if (mapEntry!=null){
entries.add(mapEntry);
}
}
}
}
private void loadMapConfiguration(final MapConfigurationProvider factory, final Map<String, MapEntry> entries) {
// URL Mappings
final Mapping[] mappings = factory.getMappings();
if (mappings != null) {
for (int i = mappings.length - 1; i >= 0; i--) {
final Mapping mapping = mappings[i];
if (mapping.mapsOutbound()) {
final String url = mapping.getTo();
final String alias = mapping.getFrom();
if (!url.equals(alias)) {
addMapEntry(entries, alias, url, -1);
}
}
}
}
// virtual uris
final Map<String, String> virtuals = factory.getVirtualURLMap();
if (virtuals != null) {
for (final Entry<String, String> virtualEntry : virtuals.entrySet()) {
final String extPath = virtualEntry.getKey();
final String intPath = virtualEntry.getValue();
if (!extPath.equals(intPath)) {
// this regular expression must match the whole URL !!
final String path = "^" + intPath + "$";
final String url = extPath;
addMapEntry(entries, path, url, -1);
}
}
}
}
private void addMapEntry(final Map<String, MapEntry> entries, final String path, final String url, final int status) {
MapEntry entry = entries.get(path);
if (entry == null) {
entry = getMapEntry(path, status, false, url);
} else {
final String[] redir = entry.getRedirect();
final String[] newRedir = new String[redir.length + 1];
System.arraycopy(redir, 0, newRedir, 0, redir.length);
newRedir[redir.length] = url;
entry = getMapEntry(entry.getPattern(), entry.getStatus(), false, newRedir);
}
if (entry!=null){
entries.put(path, entry);
}
}
private final AtomicLong lastTimeLogged = new AtomicLong(-1);
private final long LOGGING_ERROR_PERIOD = 1000 * 60 * 5;
@Override
public void logDisableAliasOptimization() {
this.logDisableAliasOptimization(null);
}
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);
}
}
}
private final class MapEntryIterator implements Iterator<MapEntry> {
private final Map<String, List<MapEntry>> resolveMapsMap;
private String key;
private MapEntry next;
private final Iterator<MapEntry> globalListIterator;
private MapEntry nextGlobal;
private Iterator<MapEntry> specialIterator;
private MapEntry nextSpecial;
private boolean vanityPathPrecedence;
public MapEntryIterator(final String startKey, final Map<String, List<MapEntry>> resolveMapsMap, final boolean vanityPathPrecedence) {
this.key = startKey;
this.resolveMapsMap = resolveMapsMap;
this.globalListIterator = this.resolveMapsMap.get(GLOBAL_LIST_KEY).iterator();
this.vanityPathPrecedence = vanityPathPrecedence;
this.seek();
}
/**
* @see java.util.Iterator#hasNext()
*/
@Override
public boolean hasNext() {
return this.next != null;
}
/**
* @see java.util.Iterator#next()
*/
@Override
public MapEntry next() {
if (this.next == null) {
throw new NoSuchElementException();
}
final MapEntry result = this.next;
this.seek();
return result;
}
/**
* @see java.util.Iterator#remove()
*/
@Override
public void remove() {
throw new UnsupportedOperationException();
}
private void seek() {
if (this.nextGlobal == null && this.globalListIterator.hasNext()) {
this.nextGlobal = this.globalListIterator.next();
}
if (this.nextSpecial == null) {
if (specialIterator != null && !specialIterator.hasNext()) {
specialIterator = null;
}
while (specialIterator == null && key != null) {
// remove selectors and extension
final int lastSlashPos = key.lastIndexOf('/');
final int lastDotPos = key.indexOf('.', lastSlashPos);
if (lastDotPos != -1) {
key = key.substring(0, lastDotPos);
}
final List<MapEntry> special;
if (MapEntries.this.isAllVanityPathEntriesCached() && MapEntries.this.vanityPathsProcessed.get()) {
special = this.resolveMapsMap.get(key);
} else {
special = MapEntries.this.getMapEntryList(key);
}
if (special != null) {
specialIterator = special.iterator();
}
// recurse to the parent
if (key.length() > 1) {
final int lastSlash = key.lastIndexOf("/");
if (lastSlash == 0) {
key = null;
} else {
key = key.substring(0, lastSlash);
}
} else {
key = null;
}
}
if (this.specialIterator != null && this.specialIterator.hasNext()) {
this.nextSpecial = this.specialIterator.next();
}
}
if (this.nextSpecial == null) {
this.next = this.nextGlobal;
this.nextGlobal = null;
} else if (!this.vanityPathPrecedence){
if (this.nextGlobal == null) {
this.next = this.nextSpecial;
this.nextSpecial = null;
} else if (this.nextGlobal.getPattern().length() >= this.nextSpecial.getPattern().length()) {
this.next = this.nextGlobal;
this.nextGlobal = null;
}else {
this.next = this.nextSpecial;
this.nextSpecial = null;
}
} else {
this.next = this.nextSpecial;
this.nextSpecial = null;
}
}
};
private MapEntry getMapEntry(String url, final int status, final boolean trailingSlash,
final String... redirect){
MapEntry mapEntry = null;
try{
mapEntry = new MapEntry(url, status, trailingSlash, 0, redirect);
}catch (IllegalArgumentException iae){
//ignore this entry
log.debug("ignored entry due exception ",iae);
}
return mapEntry;
}
private MapEntry getMapEntry(String url, final int status, final boolean trailingSlash, long order,
final String... redirect){
MapEntry mapEntry = null;
try{
mapEntry = new MapEntry(url, status, trailingSlash, order, redirect);
}catch (IllegalArgumentException iae){
//ignore this entry
log.debug("ignored entry due exception ",iae);
}
return mapEntry;
}
}