| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.sling.resourceresolver.impl.mapping; |
| |
| import java.io.DataInputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| 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.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.NoSuchElementException; |
| import java.util.SortedMap; |
| import java.util.Timer; |
| import java.util.TimerTask; |
| import java.util.TreeMap; |
| import java.util.TreeSet; |
| import java.util.concurrent.ConcurrentHashMap; |
| 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.function.Supplier; |
| import java.util.stream.Stream; |
| |
| import javax.servlet.http.HttpServletResponse; |
| |
| import org.apache.sling.api.SlingConstants; |
| import org.apache.sling.api.resource.LoginException; |
| 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.mapping.MapConfigurationProvider.VanityPathConfig; |
| 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; |
| |
| 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 String VANITY_BLOOM_FILTER_NAME = "vanityBloomFilter.txt"; |
| |
| 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; |
| |
| private static final String JCR_SYSTEM_PREFIX = "/jcr:system/"; |
| |
| static final String ALIAS_QUERY_DEFAULT = "SELECT sling:alias FROM nt:base WHERE sling:alias IS NOT NULL"; |
| |
| static final String ANY_SCHEME_HOST = "[^/]+/[^/]+"; |
| |
| /** default log */ |
| private final Logger log = LoggerFactory.getLogger(getClass()); |
| |
| private volatile MapConfigurationProvider factory; |
| |
| private volatile ResourceResolver resolver; |
| |
| private volatile EventAdmin eventAdmin; |
| |
| private volatile ServiceRegistration<ResourceChangeListener> registration; |
| |
| private Map<String, List<MapEntry>> resolveMapsMap; |
| |
| private Collection<MapEntry> mapMaps; |
| |
| private Map <String,List <String>> vanityTargets; |
| |
| private Map<String, Map<String, String>> aliasMap; |
| |
| private final ReentrantLock initializing = new ReentrantLock(); |
| |
| private final AtomicLong vanityCounter; |
| |
| private final File vanityBloomFilterFile; |
| |
| private byte[] vanityBloomFilter; |
| |
| private Timer timer; |
| |
| private final AtomicBoolean updateBloomFilterFile = new AtomicBoolean(false); |
| |
| private final StringInterpolationProvider stringInterpolationProvider; |
| |
| public MapEntries(final MapConfigurationProvider factory, final BundleContext bundleContext, final EventAdmin eventAdmin, final StringInterpolationProvider stringInterpolationProvider) |
| 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.aliasMap = Collections.<String, Map<String, String>>emptyMap(); |
| this.stringInterpolationProvider = stringInterpolationProvider; |
| |
| doInit(); |
| |
| 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); |
| log.info("Registering for {}", Arrays.toString(factory.getObservationPaths())); |
| props.put(Constants.SERVICE_DESCRIPTION, "Apache Sling Map Entries Observation"); |
| props.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation"); |
| this.registration = bundleContext.registerService(ResourceChangeListener.class, this, props); |
| |
| this.vanityCounter = new AtomicLong(0); |
| this.vanityBloomFilterFile = bundleContext.getDataFile(VANITY_BLOOM_FILTER_NAME); |
| initializeVanityPaths(); |
| } |
| |
| /** |
| * Actual initializer. Guards itself against concurrent use by using a |
| * ReentrantLock. Does nothing if the resource resolver has already been |
| * null-ed. |
| */ |
| protected void doInit() { |
| |
| this.initializing.lock(); |
| try { |
| final ResourceResolver resolver = this.resolver; |
| final MapConfigurationProvider factory = this.factory; |
| if (resolver == null || factory == null) { |
| return; |
| } |
| |
| final Map<String, List<MapEntry>> newResolveMapsMap = new ConcurrentHashMap<>(); |
| |
| //optimization made in SLING-2521 |
| if (this.factory.isOptimizeAliasResolutionEnabled()) { |
| final Map<String, Map<String, String>> aliasMap = this.loadAliases(resolver); |
| this.aliasMap = aliasMap; |
| } |
| |
| this.resolveMapsMap = newResolveMapsMap; |
| |
| doUpdateConfiguration(); |
| |
| sendChangeEvent(); |
| } catch (final Exception e) { |
| |
| log.warn("doInit: Unexpected problem during initialization", e); |
| |
| } 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 |
| */ |
| protected void initializeVanityPaths() throws IOException { |
| this.initializing.lock(); |
| try { |
| if (this.factory.isVanityPathEnabled()) { |
| |
| if (vanityBloomFilterFile == null) { |
| throw new RuntimeException( |
| "This platform does not have file system support"); |
| } |
| boolean createVanityBloomFilter = false; |
| if (!vanityBloomFilterFile.exists()) { |
| log.debug("creating bloom filter file {}", |
| vanityBloomFilterFile.getAbsolutePath()); |
| vanityBloomFilter = createVanityBloomFilter(); |
| updateBloomFilterFile.set(true); |
| persistBloomFilter(); |
| createVanityBloomFilter = true; |
| } else { |
| // initialize bloom filter from disk |
| vanityBloomFilter = new byte[(int) vanityBloomFilterFile.length()]; |
| try ( DataInputStream dis = new DataInputStream( |
| new FileInputStream(vanityBloomFilterFile)) ) { |
| dis.readFully(vanityBloomFilter); |
| } |
| } |
| |
| // task for persisting the bloom filter every minute (if changes exist) |
| timer = new Timer("VanityPathBloomFilterUpdater", true); |
| timer.schedule(new BloomFilterTask(), 60_000, 60_000); |
| |
| this.vanityTargets = loadVanityPaths(createVanityBloomFilter); |
| } |
| } finally { |
| this.initializing.unlock(); |
| } |
| |
| } |
| |
| 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.factory.isOptimizeAliasResolutionEnabled() && 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.factory.isOptimizeAliasResolutionEnabled() || 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.factory.isOptimizeAliasResolutionEnabled()) { |
| 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.factory.isOptimizeAliasResolutionEnabled()) { |
| for (final String contentPath : this.aliasMap.keySet()) { |
| if (path.startsWith(contentPath + "/") || path.equals(contentPath) |
| || contentPath.startsWith(actualContentPathPrefix)) { |
| 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; |
| String resourcePath = null; |
| 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; |
| } |
| } |
| else { |
| resourcePath = contentPath; |
| } |
| if ( !handle ) { |
| return false; |
| } |
| |
| this.initializing.lock(); |
| try { |
| final Map<String, String> aliasMapEntry = aliasMap.get(contentPath); |
| if (aliasMapEntry != null) { |
| this.refreshResolverIfNecessary(resolverRefreshed); |
| |
| for (Iterator<Map.Entry<String, String>> iterator = aliasMapEntry.entrySet().iterator(); iterator.hasNext(); ) { |
| final Map.Entry<String, String> entry = iterator.next(); |
| String prefix = contentPath.endsWith("/") ? contentPath : contentPath + "/"; |
| if ((prefix + entry.getValue()).startsWith(resourcePath)){ |
| iterator.remove(); |
| } |
| } |
| |
| if (aliasMapEntry.isEmpty()) { |
| this.aliasMap.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 needsUpdate = false; |
| if (isAllVanityPathEntriesCached() || vanityCounter.longValue() < this.factory.getMaxCachedVanityPathEntries()) { |
| // fill up the cache and the bloom filter |
| needsUpdate = loadVanityPath(resource, resolveMapsMap, vanityTargets, true, true); |
| } else { |
| // fill up the bloom filter |
| needsUpdate = loadVanityPath(resource, resolveMapsMap, vanityTargets, false, true); |
| } |
| if ( needsUpdate ) { |
| updateBloomFilterFile.set(true); |
| return true; |
| } |
| return false; |
| } |
| |
| 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.aliasMap); |
| } |
| |
| /** |
| * 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, String> aliasMapEntry = parentPath == null ? null : aliasMap.get(parentPath); |
| if (aliasMapEntry != null) { |
| for (Iterator<Map.Entry<String, String>> iterator = aliasMapEntry.entrySet().iterator(); iterator.hasNext(); ) { |
| final Map.Entry<String, String> entry = iterator.next(); |
| if (containingResourceName.equals(entry.getValue())){ |
| iterator.remove(); |
| } |
| } |
| } |
| |
| if (aliasMapEntry != null && aliasMapEntry.isEmpty()) { |
| this.aliasMap.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() { |
| |
| timer.cancel(); |
| |
| persistBloomFilter(); |
| |
| 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; |
| } |
| |
| @Override |
| public Map<String, String> getAliasMap(final String parentPath) { |
| return aliasMap.get(parentPath); |
| } |
| |
| @Override |
| public Map<String, List<String>> getVanityPathMappings() { |
| return Collections.unmodifiableMap(vanityTargets); |
| } |
| |
| /** |
| * get the MapEnty containing all the nodes having a specific vanityPath |
| */ |
| private List<MapEntry> getMapEntryList(String vanityPath){ |
| List<MapEntry> mapEntries = null; |
| |
| if (BloomFilterUtils.probablyContains(vanityBloomFilter, vanityPath)) { |
| mapEntries = this.resolveMapsMap.get(vanityPath); |
| if (mapEntries == null) { |
| Map<String, List<MapEntry>> mapEntry = getVanityPaths(vanityPath); |
| mapEntries = mapEntry.get(vanityPath); |
| } |
| } |
| |
| return 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)} |
| * constructor makes sure the event listener is registered to only get |
| * appropriate events. |
| */ |
| @Override |
| public void onChange(final List<ResourceChange> changes) { |
| final AtomicBoolean resolverRefreshed = new AtomicBoolean(false); |
| |
| // the config needs to be reloaded only once |
| final AtomicBoolean hasReloadedConfig = new AtomicBoolean(false); |
| for(final ResourceChange rc : changes) { |
| |
| 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; |
| } |
| |
| boolean changed = false; |
| // removal of a resource is handled differently |
| if (rc.getType() == 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 (rc.getType() == 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 (rc.getType() == ResourceChange.ChangeType.CHANGED ) { |
| |
| final Boolean result = handleConfigurationUpdate(path, hasReloadedConfig, resolverRefreshed, false); |
| if ( result != null ) { |
| if ( result ) { |
| changed = true; |
| } else { |
| changed |= updateResource(path, resolverRefreshed); |
| } |
| } |
| |
| } |
| |
| if ( changed ) { |
| this.sendChangeEvent(); |
| } |
| } |
| } |
| |
| // ---------- internal |
| |
| private byte[] createVanityBloomFilter() throws IOException { |
| byte bloomFilter[] = null; |
| if (vanityBloomFilter == null) { |
| bloomFilter = BloomFilterUtils.createFilter(VANITY_BLOOM_FILTER_MAX_ENTRIES, this.factory.getVanityBloomFilterMaxBytes()); |
| } |
| return bloomFilter; |
| } |
| |
| private void persistBloomFilter() { |
| if (vanityBloomFilterFile != null && vanityBloomFilter != null |
| && updateBloomFilterFile.getAndSet(false)) { |
| try (FileOutputStream out = new FileOutputStream(vanityBloomFilterFile)) { |
| out.write(this.vanityBloomFilter); |
| } catch (IOException e) { |
| throw new RuntimeException( |
| "Error while saving bloom filter to disk", e); |
| } |
| } |
| } |
| |
| private boolean isAllVanityPathEntriesCached() { |
| return this.factory.getMaxCachedVanityPathEntries() == -1; |
| } |
| |
| /** |
| * Escapes illegal XPath search characters at the end of a string. |
| * <p> |
| * Example:<br> |
| * A search string like 'test?' will run into a ParseException documented in |
| * http://issues.apache.org/jira/browse/JCR-1248 |
| * |
| * @param s |
| * the string to encode |
| * @return the escaped string |
| */ |
| private static String escapeIllegalXpathSearchChars(String s) { |
| StringBuilder sb = new StringBuilder(); |
| if (s != null && s.length() > 1) { |
| sb.append(s.substring(0, (s.length() - 1))); |
| char c = s.charAt(s.length() - 1); |
| // NOTE: keep this in sync with _ESCAPED_CHAR below! |
| if (c == '!' || c == '(' || c == ':' || c == '^' || c == '[' |
| || c == ']' || c == '{' || c == '}' || c == '?') { |
| sb.append('\\'); |
| } |
| sb.append(c); |
| } |
| return sb.toString(); |
| } |
| |
| /** |
| * 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<>(); |
| |
| // sling:vanityPath (lowercase) is the property name |
| final String queryString = "SELECT sling:vanityPath, sling:redirect, sling:redirectStatus FROM nt:base WHERE sling:vanityPath =" |
| + "'"+escapeIllegalXpathSearchChars(vanityPath).replaceAll("'", "''")+"' OR sling:vanityPath ="+ "'"+escapeIllegalXpathSearchChars(vanityPath.substring(1)).replaceAll("'", "''")+"' ORDER BY sling:vanityOrder DESC"; |
| |
| ResourceResolver queryResolver = null; |
| |
| try { |
| queryResolver = factory.getServiceResourceResolver(factory.getServiceUserAuthenticationInfo("mapping")); |
| final Iterator<Resource> i = queryResolver.findResources(queryString, "sql"); |
| while (i.hasNext()) { |
| 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 ) { |
| if (this.factory.isMaxCachedVanityPathEntriesStartup() || vanityCounter.longValue() < this.factory.getMaxCachedVanityPathEntries()) { |
| loadVanityPath(resource, resolveMapsMap, vanityTargets, true, false); |
| entryMap = resolveMapsMap; |
| } else { |
| final Map <String, List<String>> targetPaths = new HashMap <>(); |
| loadVanityPath(resource, entryMap, targetPaths, true, false); |
| } |
| } |
| } |
| } catch (LoginException e) { |
| log.error("Exception while obtaining queryResolver", e); |
| } finally { |
| if (queryResolver != null) { |
| queryResolver.close(); |
| } |
| } |
| 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 white 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 white 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){ |
| return false; |
| } |
| |
| List<MapEntry> entries = entryMap.get(key); |
| if (entries == null) { |
| entries = new ArrayList<>(); |
| entries.add(entry); |
| // and finally sort list |
| Collections.sort(entries); |
| 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); |
| } |
| return true; |
| } |
| |
| /** |
| * Load aliases Search for all nodes inheriting the sling:alias |
| * property |
| */ |
| private Map<String, Map<String, String>> loadAliases(final ResourceResolver resolver) { |
| final Map<String, Map<String, String>> map = new ConcurrentHashMap<>(); |
| final String queryString = ALIAS_QUERY_DEFAULT; |
| final Iterator<Resource> i = resolver.findResources(queryString, "sql"); |
| while (i.hasNext()) { |
| final Resource resource = i.next(); |
| loadAlias(resource, map); |
| } |
| return map; |
| } |
| |
| /** |
| * Load alias given a resource |
| */ |
| private boolean loadAlias(final Resource resource, Map<String, Map<String, String>> map) { |
| // ignore system tree |
| if (resource.getPath().startsWith(JCR_SYSTEM_PREFIX)) { |
| log.debug("loadAliases: Ignoring {}", resource); |
| return false; |
| } |
| |
| final String resourceName; |
| final String parentPath; |
| if (JCR_CONTENT.equals(resource.getName())) { |
| final Resource containingResource = resource.getParent(); |
| if ( containingResource != null ) { |
| final Resource parent = containingResource.getParent(); |
| if ( parent != null ) { |
| parentPath = parent.getPath(); |
| resourceName = containingResource.getName(); |
| } else { |
| parentPath = null; |
| resourceName = null; |
| } |
| } else { |
| parentPath = null; |
| resourceName = null; |
| } |
| } else { |
| final Resource parent = resource.getParent(); |
| if ( parent != null ) { |
| parentPath = parent.getPath(); |
| resourceName = resource.getName(); |
| } else { |
| parentPath = null; |
| resourceName = null; |
| } |
| } |
| boolean hasAlias = false; |
| if ( parentPath != null ) { |
| // require properties |
| final ValueMap props = resource.getValueMap(); |
| final String[] aliasArray = props.get(ResourceResolverImpl.PROP_ALIAS, String[].class); |
| |
| if ( aliasArray != null ) { |
| Map<String, String> parentMap = map.get(parentPath); |
| for (final String alias : aliasArray) { |
| if (parentMap != null && parentMap.containsKey(alias)) { |
| log.warn("Encountered duplicate alias {} under parent path {}. Refusing to replace current target {} with {}.", new Object[] { |
| alias, |
| parentPath, |
| parentMap.get(alias), |
| resourceName |
| }); |
| } else { |
| // check 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; |
| } |
| } |
| } |
| if ( invalid ) { |
| log.warn("Encountered invalid alias {} under parent path {}. Refusing to use it.", |
| alias, parentPath); |
| } else { |
| if (parentMap == null) { |
| parentMap = new LinkedHashMap<>(); |
| map.put(parentPath, parentMap); |
| } |
| parentMap.put(alias, resourceName); |
| hasAlias = true; |
| } |
| } |
| } |
| } |
| } |
| return hasAlias; |
| } |
| |
| /** |
| * Load vanity paths Search for all nodes inheriting the sling:VanityPath |
| * mixin |
| */ |
| private Map <String, List<String>> loadVanityPaths(boolean createVanityBloomFilter) { |
| // sling:vanityPath (lowercase) is the property name |
| final Map <String, List<String>> targetPaths = new ConcurrentHashMap <>(); |
| final String queryString = "SELECT sling:vanityPath, sling:redirect, sling:redirectStatus FROM nt:base WHERE sling:vanityPath IS NOT NULL"; |
| final Iterator<Resource> i = resolver.findResources(queryString, "sql"); |
| |
| Supplier<Boolean> isCacheComplete = () -> isAllVanityPathEntriesCached() || vanityCounter.longValue() < this.factory.getMaxCachedVanityPathEntries(); |
| while (i.hasNext() && (createVanityBloomFilter || isCacheComplete.get())) { |
| final Resource resource = i.next(); |
| final String resourcePath = resource.getPath(); |
| if (Stream.of(this.factory.getObservationPaths()).anyMatch(path -> path.matches(resourcePath))) { |
| loadVanityPath(resource, resolveMapsMap, targetPaths, isCacheComplete.get(), createVanityBloomFilter); |
| } |
| } |
| |
| return targetPaths; |
| } |
| |
| /** |
| * Load vanity path given a resource |
| */ |
| private boolean loadVanityPath(final Resource resource, final Map<String, List<MapEntry>> entryMap, final Map <String, List<String>> targetPaths, boolean addToCache, boolean newVanity) { |
| |
| if (!isValidVanityPath(resource.getPath())) { |
| return false; |
| } |
| |
| 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]); |
| for (final String pVanityPath : pVanityPaths) { |
| final String[] result = this.getVanityPathDefinition(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); |
| } |
| |
| if (newVanity) { |
| // update bloom filter |
| BloomFilterUtils.add(vanityBloomFilter, checkPath); |
| } |
| } |
| } else { |
| if (newVanity) { |
| // update bloom filter |
| BloomFilterUtils.add(vanityBloomFilter, checkPath); |
| } |
| } |
| } |
| } |
| return hasVanityPath; |
| } |
| |
| 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 pVanityPath) { |
| String[] result = null; |
| if (pVanityPath != null) { |
| final String info = pVanityPath.trim(); |
| if (info.length() > 0) { |
| String prefix = null; |
| String path = null; |
| // check for url |
| 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 {}", pVanityPath); |
| } |
| } else { |
| prefix = "^" + ANY_SCHEME_HOST; |
| if (!info.startsWith("/")) { |
| path = "/" + info; |
| } else { |
| path = info; |
| } |
| } |
| |
| // remove extension |
| if (prefix != null) { |
| final int lastSlash = path.lastIndexOf('/'); |
| final int firstDot = path.indexOf('.', lastSlash + 1); |
| if (firstDot != -1) { |
| path = path.substring(0, firstDot); |
| log.warn("Removing extension from vanity path {}", pVanityPath); |
| } |
| result = new String[] { prefix, path }; |
| } |
| } |
| } |
| return result; |
| } |
| |
| private void loadConfiguration(final MapConfigurationProvider factory, final List<MapEntry> entries) { |
| // virtual uris |
| final Map<?, ?> virtuals = factory.getVirtualURLMap(); |
| if (virtuals != null) { |
| for (final Entry<?, ?> virtualEntry : virtuals.entrySet()) { |
| final String extPath = (String) virtualEntry.getKey(); |
| final String intPath = (String) 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<?, ?> virtuals = factory.getVirtualURLMap(); |
| if (virtuals != null) { |
| for (final Entry<?, ?> virtualEntry : virtuals.entrySet()) { |
| final String extPath = (String) virtualEntry.getKey(); |
| final String intPath = (String) 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 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()) { |
| 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; |
| } |
| |
| final class BloomFilterTask extends TimerTask { |
| @Override |
| public void run() { |
| persistBloomFilter(); |
| } |
| } |
| |
| } |