Author: asanso
Date: Mon Dec 22 12:39:40 2014
New Revision: 1647303
URL: http://svn.apache.org/r1647303
Log:
SLING-4216 - Limit the number of vanityPath MapEntry
Added:
sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/mapping/BloomFilterUtils.java
sling/trunk/bundles/resourceresolver/src/test/java/org/apache/sling/resourceresolver/impl/mapping/BloomFilterUtilsTest.java
Modified:
sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/CommonResourceResolverFactoryImpl.java
sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverFactoryActivator.java
sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapConfigurationProvider.java
sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java
sling/trunk/bundles/resourceresolver/src/test/java/org/apache/sling/resourceresolver/impl/mapping/MapEntriesTest.java
Modified:
sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/CommonResourceResolverFactoryImpl.java
URL:
http://svn.apache.org/viewvc/sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/CommonResourceResolverFactoryImpl.java?rev=1647303&r1=1647302&r2=1647303&view=diff
==============================================================================
---
sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/CommonResourceResolverFactoryImpl.java
(original)
+++
sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/CommonResourceResolverFactoryImpl.java
Mon Dec 22 12:39:40 2014
@@ -241,6 +241,14 @@ public class CommonResourceResolverFacto
public boolean isVanityPathEnabled() {
return this.activator.isVanityPathEnabled();
}
+
+ public long getMaxCachedVanityPathEntries() {
+ return this.activator.getMaxCachedVanityPathEntries();
+ }
+
+ public int getVanityBloomFilterMaxBytes() {
+ return this.activator.getVanityBloomFilterMaxBytes();
+ }
public boolean isOptimizeAliasResolutionEnabled() {
return this.activator.isOptimizeAliasResolutionEnabled();
Modified:
sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverFactoryActivator.java
URL:
http://svn.apache.org/viewvc/sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverFactoryActivator.java?rev=1647303&r1=1647302&r2=1647303&view=diff
==============================================================================
---
sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverFactoryActivator.java
(original)
+++
sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverFactoryActivator.java
Mon Dec 22 12:39:40 2014
@@ -184,7 +184,21 @@ public class ResourceResolverFactoryActi
description = "This flag controls whether all resources with a
sling:vanityPath property " +
"are processed and added to the mappoing table.")
private static final String PROP_ENABLE_VANITY_PATH =
"resource.resolver.enable.vanitypath";
-
+
+ private static final long DEFAULT_MAX_CACHED_VANITY_PATHS = -1;
+ @Property(longValue = DEFAULT_MAX_CACHED_VANITY_PATHS,
+ label = "Maximum number of cached vanity path entries",
+ description = "The maximum number of cached vanity path entries.
" +
+ "Default is -1 (no limit)")
+ private static final String PROP_MAX_CACHED_VANITY_PATHS =
"resource.resolver.max.vanityPath.entries";
+
+ private static final int DEFAULT_VANITY_BLOOM_FILTER_MAX_BYTES = 1024000;
+ @Property(longValue = DEFAULT_VANITY_BLOOM_FILTER_MAX_BYTES,
+ label = "Maximum number of vanity bloom filter bytes",
+ description = "The maximum number of vanity bloom filter bytes.
" +
+ "Changing this value is subject to vanity bloom
filter rebuild")
+ private static final String PROP_VANITY_BLOOM_FILTER_MAX_BYTES =
"resource.resolver.vanity.bloom.filter.max.bytes";
+
private static final boolean DEFAULT_ENABLE_OPTIMIZE_ALIAS_RESOLUTION =
true;
@Property(boolValue = DEFAULT_ENABLE_OPTIMIZE_ALIAS_RESOLUTION ,
label = "Optimize alias resolution",
@@ -260,6 +274,12 @@ public class ResourceResolverFactoryActi
/** alias resource resolution optimization enabled? */
private boolean enableOptimizeAliasResolution =
DEFAULT_ENABLE_OPTIMIZE_ALIAS_RESOLUTION;
+ /** max number of cache vanity path entries */
+ private long maxCachedVanityPathEntries = DEFAULT_MAX_CACHED_VANITY_PATHS;
+
+ /** Maximum number of vanity bloom filter bytes */
+ private int vanityBloomFilterMaxBytes =
DEFAULT_VANITY_BLOOM_FILTER_MAX_BYTES;
+
/** vanity paths will have precedence over existing /etc/map mapping? */
private boolean vanityPathPrecedence = DEFAULT_VANITY_PATH_PRECEDENCE;
@@ -347,6 +367,14 @@ public class ResourceResolverFactoryActi
public boolean hasVanityPathPrecedence() {
return this.vanityPathPrecedence;
}
+
+ public long getMaxCachedVanityPathEntries() {
+ return this.maxCachedVanityPathEntries;
+ }
+
+ public int getVanityBloomFilterMaxBytes() {
+ return this.vanityBloomFilterMaxBytes;
+ }
// ---------- SCR Integration ---------------------------------------------
@@ -449,7 +477,9 @@ public class ResourceResolverFactoryActi
}
this.enableOptimizeAliasResolution =
PropertiesUtil.toBoolean(properties.get(PROP_ENABLE_OPTIMIZE_ALIAS_RESOLUTION),
DEFAULT_ENABLE_OPTIMIZE_ALIAS_RESOLUTION);
-
+ this.maxCachedVanityPathEntries =
PropertiesUtil.toLong(properties.get(PROP_MAX_CACHED_VANITY_PATHS),
DEFAULT_MAX_CACHED_VANITY_PATHS);
+ this.vanityBloomFilterMaxBytes =
PropertiesUtil.toInteger(properties.get(PROP_VANITY_BLOOM_FILTER_MAX_BYTES),
DEFAULT_VANITY_BLOOM_FILTER_MAX_BYTES);
+
this.vanityPathPrecedence =
PropertiesUtil.toBoolean(properties.get(PROP_VANITY_PATH_PRECEDENCE),
DEFAULT_VANITY_PATH_PRECEDENCE);
final BundleContext bc = componentContext.getBundleContext();
Added:
sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/mapping/BloomFilterUtils.java
URL:
http://svn.apache.org/viewvc/sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/mapping/BloomFilterUtils.java?rev=1647303&view=auto
==============================================================================
---
sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/mapping/BloomFilterUtils.java
(added)
+++
sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/mapping/BloomFilterUtils.java
Mon Dec 22 12:39:40 2014
@@ -0,0 +1,115 @@
+/*
+ * 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;
+
+/**
+ * Bloom filter utilities.
+ */
+public class BloomFilterUtils {
+
+ /**
+ * The multiply and shift constants for the supplemental hash function.
+ */
+ private static final int MUL = 2153, SHIFT = 19;
+
+ /**
+ * The number of bits needed per stored element.
+ * Using the formula m = - (n * ln(p)) / (ln(2)^2) as described in
+ * http://en.wikipedia.org/wiki/Bloom_filter
+ * (simplified, as we used a fixed K: 2).
+ */
+ private static final double BIT_FACTOR = -Math.log(0.02) /
Math.pow(Math.log(2), 2);
+
+ /**
+ * Create a bloom filter array for the given number of elements.
+ *
+ * @param elementCount the number of entries
+ * @param maxBytes the maximum number of bytes
+ * @return the empty bloom filter
+ */
+ public static byte[] createFilter(int elementCount, int maxBytes) {
+ int bits = (int) (elementCount * BIT_FACTOR) + 7;
+ return new byte[Math.min(maxBytes, bits / 8)];
+ }
+
+ /**
+ * Add the key.
+ *
+ * @param bloom the bloom filter
+ * @param key the key
+ */
+ public static void add(byte[] bloom, Object key) {
+ int len = bloom.length;
+ if (len > 0) {
+ int h1 = hash(key.hashCode()), h2 = hash(h1);
+ bloom[(h1 >>> 3) % len] |= 1 << (h1 & 7);
+ bloom[(h2 >>> 3) % len] |= 1 << (h2 & 7);
+ }
+ }
+
+ /**
+ * Remove the key.
+ *
+ * @param bloom the bloom filter
+ * @param key the key
+ */
+ public static void remove(byte[] bloom, Object key){
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Check whether the given key is probably in the set. This method never
+ * returns false if the key is in the set, but possibly returns true even
if
+ * it isn't.
+ *
+ * @param bloom the bloom filter
+ * @param key the key
+ * @return true if the given key is probably in the set
+ */
+ public static boolean probablyContains(byte[] bloom, Object key) {
+ int len = bloom.length;
+ if (len == 0) {
+ return true;
+ }
+ int h1 = hash(key.hashCode());
+ long x = bloom[(h1 >>> 3) % len] & (1 << (h1 & 7));
+ if (x != 0) {
+ int h2 = hash(h1);
+ x = bloom[(h2 >>> 3) % len] & (1 << (h2 & 7));
+ }
+ return x != 0;
+ }
+
+ /**
+ * Get the hash value for the given key. The returned hash value is
+ * stretched so that it should work well even for relatively bad hashCode
+ * implementations.
+ *
+ * @param key the key
+ * @return the hash value
+ */
+ private static int hash(int key) {
+ int hash = key;
+ // a supplemental secondary hash function
+ // to protect against hash codes that don't differ much
+ hash = ((hash >>> 16) ^ hash) * 0x45d9f3b;
+ hash = ((hash >>> 16) ^ hash) * 0x45d9f3b;
+ hash = (hash >>> 16) ^ hash;
+ return hash;
+ }
+
+}
Modified:
sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapConfigurationProvider.java
URL:
http://svn.apache.org/viewvc/sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapConfigurationProvider.java?rev=1647303&r1=1647302&r2=1647303&view=diff
==============================================================================
---
sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapConfigurationProvider.java
(original)
+++
sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapConfigurationProvider.java
Mon Dec 22 12:39:40 2014
@@ -39,6 +39,10 @@ public interface MapConfigurationProvide
int getDefaultVanityPathRedirectStatus();
boolean isVanityPathEnabled();
+
+ long getMaxCachedVanityPathEntries();
+
+ int getVanityBloomFilterMaxBytes();
boolean isOptimizeAliasResolutionEnabled();
Modified:
sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java
URL:
http://svn.apache.org/viewvc/sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java?rev=1647303&r1=1647302&r2=1647303&view=diff
==============================================================================
---
sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java
(original)
+++
sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/mapping/MapEntries.java
Mon Dec 22 12:39:40 2014
@@ -18,6 +18,11 @@
*/
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;
@@ -30,6 +35,8 @@ import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Timer;
+import java.util.TimerTask;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.SortedMap;
@@ -37,10 +44,9 @@ import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
-
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;
@@ -75,6 +81,10 @@ public class MapEntries implements Event
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 = "*";
@@ -111,12 +121,26 @@ public class MapEntries implements Event
private final ReentrantLock initializing = new ReentrantLock();
private final boolean enabledVanityPaths;
+
+ private final long maxCachedVanityPathEntries;
+
+ private final int vanityBloomFilterMaxBytes;
private final boolean enableOptimizeAliasResolution;
private final boolean vanityPathPrecedence;
private final List<VanityPathConfig> vanityPathConfig;
+
+ private final AtomicLong vanityCounter;
+
+ private final File vanityBloomFilterFile;
+
+ private byte[] vanityBloomFilter;
+
+ private Timer timer;
+
+ private boolean updateBloomFilterFile = false;
@SuppressWarnings("unchecked")
private MapEntries() {
@@ -131,18 +155,24 @@ public class MapEntries implements Event
this.registration = null;
this.eventAdmin = null;
this.enabledVanityPaths = true;
+ this.maxCachedVanityPathEntries = -1;
+ this.vanityBloomFilterMaxBytes = 0;
this.enableOptimizeAliasResolution = true;
this.vanityPathConfig = null;
this.vanityPathPrecedence = false;
+ this.vanityCounter = new AtomicLong(0);
+ this.vanityBloomFilterFile = null;
}
@SuppressWarnings("unchecked")
public MapEntries(final MapConfigurationProvider factory, final
BundleContext bundleContext, final EventAdmin eventAdmin)
- throws LoginException {
+ throws LoginException, IOException {
this.resolver = factory.getAdministrativeResourceResolver(null);
this.factory = factory;
this.mapRoot = factory.getMapRoot();
this.enabledVanityPaths = factory.isVanityPathEnabled();
+ this.maxCachedVanityPathEntries =
factory.getMaxCachedVanityPathEntries();
+ this.vanityBloomFilterMaxBytes =
factory.getVanityBloomFilterMaxBytes();
this.vanityPathConfig = factory.getVanityPathConfig();
this.enableOptimizeAliasResolution =
factory.isOptimizeAliasResolutionEnabled();
this.vanityPathPrecedence = factory.hasVanityPathPrecedence();
@@ -161,6 +191,10 @@ public class MapEntries implements Event
props.put(Constants.SERVICE_DESCRIPTION, "Map Entries Observation");
props.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation");
this.registration =
bundleContext.registerService(EventHandler.class.getName(), this, props);
+
+ this.vanityCounter = new AtomicLong(0);
+ this.vanityBloomFilterFile =
bundleContext.getDataFile(VANITY_BLOOM_FILTER_NAME);
+ initializeVanityPaths();
}
/**
@@ -179,8 +213,6 @@ public class MapEntries implements Event
}
final Map<String, List<MapEntry>> newResolveMapsMap = new
ConcurrentHashMap<String, List<MapEntry>>();
-
- final Map<String,List<String>> vanityTargets =
(this.enabledVanityPaths ? this.loadVanityPaths(resolver, newResolveMapsMap) :
Collections.<String,List <String>>emptyMap());
//optimization made in SLING-2521
if (enableOptimizeAliasResolution){
@@ -188,7 +220,6 @@ public class MapEntries implements Event
this.aliasMap = aliasMap;
}
- this.vanityTargets = vanityTargets;
this.resolveMapsMap = newResolveMapsMap;
doUpdateConfiguration();
@@ -204,6 +235,58 @@ public class MapEntries implements Event
}
}
+
+ /**
+ * 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.enabledVanityPaths) {
+
+ 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();
+ persistBloomFilter();
+ createVanityBloomFilter = true;
+ } else {
+ // initialize bloom filter from disk
+ vanityBloomFilter = new byte[(int) vanityBloomFilterFile
+ .length()];
+ DataInputStream dis = new DataInputStream(
+ new FileInputStream(vanityBloomFilterFile));
+ try {
+ dis.readFully(vanityBloomFilter);
+ } finally {
+ dis.close();
+ }
+ }
+
+ // task for persisting the bloom filter every minute (if
changes
+ // exist)
+ timer = new Timer();
+ timer.schedule(new BloomFilterTask(), 60 * 1000);
+
+ final Map<String, List<String>> vanityTargets = this
+ .loadVanityPaths(resolver, resolveMapsMap,
+ createVanityBloomFilter);
+ this.vanityTargets = vanityTargets;
+ }
+ } finally {
+ this.initializing.unlock();
+ }
+
+ }
private boolean doNodeAdded(String path, boolean refreshed) {
this.initializing.lock();
@@ -363,7 +446,15 @@ public class MapEntries implements Event
private void doAddVanity(String path) {
Resource resource = resolver.getResource(path);
- loadVanityPath(resource, resolveMapsMap, vanityTargets);
+ if (maxCachedVanityPathEntries < vanityCounter.longValue()) {
+ // fill up the cache and the bloom filter
+ loadVanityPath(resource, resolveMapsMap, vanityTargets, true,
true);
+ vanityCounter.incrementAndGet();
+ } else {
+ // fill up the bloom filter
+ loadVanityPath(resource, resolveMapsMap, vanityTargets, false,
true);
+ }
+ updateBloomFilterFile = true;
}
private void doUpdateVanity(String path) {
@@ -392,6 +483,9 @@ public class MapEntries implements Event
}
}
vanityTargets.remove(actualContentPath);
+ if (vanityCounter.longValue() > 0) {
+ vanityCounter.decrementAndGet();
+ }
}
private void doUpdateVanityOrder(String path, boolean deletion) {
@@ -518,6 +612,12 @@ public class MapEntries implements Event
* Cleans up this class.
*/
public void dispose() {
+ try {
+ persistBloomFilter();
+ } catch (IOException e) {
+ log.error("Error while saving bloom filter to disk", e);
+ }
+
if (this.registration != null) {
this.registration.unregister();
this.registration = null;
@@ -601,6 +701,23 @@ public class MapEntries implements Event
public Map<String, String> getAliasMap(final String parentPath) {
return aliasMap.get(parentPath);
}
+
+ /**
+ * 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;
+ }
// ---------- EventListener interface
@@ -679,6 +796,76 @@ public class MapEntries implements Event
// ---------- internal
+ private byte[] createVanityBloomFilter() throws IOException {
+ byte bloomFilter[] = null;
+ if (vanityBloomFilter == null) {
+ bloomFilter =
BloomFilterUtils.createFilter(VANITY_BLOOM_FILTER_MAX_ENTRIES,
this.vanityBloomFilterMaxBytes);
+ }
+ return bloomFilter;
+ }
+
+ private void persistBloomFilter() throws IOException {
+ if (vanityBloomFilterFile != null && vanityBloomFilter != null) {
+ FileOutputStream out = new FileOutputStream(vanityBloomFilterFile);
+ try {
+ out.write(this.vanityBloomFilter);
+ } finally {
+ out.close();
+ }
+ }
+ }
+
+ private boolean isAllVanityPathEntriesCached() {
+ return maxCachedVanityPathEntries == -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) {
+
+ final Map<String, List<MapEntry>> entryMap = new HashMap<String,
List<MapEntry>>();
+ final Map <String, List<String>> targetPaths = new HashMap <String,
List<String>>();
+
+ // sling:VanityPath (uppercase V) is the mixin name
+ // sling:vanityPath (lowercase) is the property name
+ final String queryString = "SELECT sling:vanityPath, sling:redirect,
sling:redirectStatus FROM sling:VanityPath WHERE sling:vanityPath ="
+ +
"'"+escapeIllegalXpathSearchChars(vanityPath).replaceAll("'", "''")+"' OR
sling:vanityPath ="+
"'"+escapeIllegalXpathSearchChars(vanityPath.substring(1)).replaceAll("'",
"''")+"' ORDER BY sling:vanityOrder DESC";
+ final Iterator<Resource> i = resolver.findResources(queryString,
"sql");
+
+ while (i.hasNext()) {
+ final Resource resource = i.next();
+ loadVanityPath(resource, entryMap, targetPaths, true, false);
+ }
+ return entryMap;
+ }
+
private boolean isValidVanityPath(Resource resource){
// ignore system tree
if (resource.getPath().startsWith(JCR_SYSTEM_PREFIX)) {
@@ -907,25 +1094,36 @@ public class MapEntries implements Event
* Load vanity paths Search for all nodes inheriting the sling:VanityPath
* mixin
*/
- private Map <String, List<String>> loadVanityPaths(final ResourceResolver
resolver, final Map<String, List<MapEntry>> entryMap) {
+ private Map <String, List<String>> loadVanityPaths(final ResourceResolver
resolver, final Map<String, List<MapEntry>> entryMap, boolean
createVanityBloomFilter) {
// sling:VanityPath (uppercase V) is the mixin name
// sling:vanityPath (lowercase) is the property name
final Map <String, List<String>> targetPaths = new ConcurrentHashMap
<String, List<String>>();
final String queryString = "SELECT sling:vanityPath, sling:redirect,
sling:redirectStatus FROM sling:VanityPath WHERE sling:vanityPath IS NOT NULL";
final Iterator<Resource> i = resolver.findResources(queryString,
"sql");
- while (i.hasNext()) {
+ while (i.hasNext() && (createVanityBloomFilter ||
maxCachedVanityPathEntries < vanityCounter.longValue())) {
final Resource resource = i.next();
- loadVanityPath(resource, entryMap, targetPaths);
+ if (maxCachedVanityPathEntries < vanityCounter.longValue()) {
+ // fill up the cache and the bloom filter
+ loadVanityPath(resource, entryMap, targetPaths, true,
+ createVanityBloomFilter);
+ vanityCounter.incrementAndGet();
+ } else {
+ // fill up the bloom filter
+ loadVanityPath(resource, entryMap, targetPaths, false,
+ createVanityBloomFilter);
+ }
+
}
+
return targetPaths;
}
/**
* Load vanity path given a resource
*/
- private void loadVanityPath(final Resource resource, final Map<String,
List<MapEntry>> entryMap, final Map <String, List<String>> targetPaths) {
+ private void loadVanityPath(final Resource resource, final Map<String,
List<MapEntry>> entryMap, final Map <String, List<String>> targetPaths, boolean
addToCache, boolean newVanity) {
if (!isValidVanityPath(resource)) {
return;
@@ -967,25 +1165,36 @@ public class MapEntries implements Event
final String checkPath = result[1];
boolean addedEntry;
- if (redirectName.indexOf('.') > -1) {
- // 1. entry with exact match
- this.addEntry(entryMap, checkPath, getMapEntry(url + "$",
status, false, vanityOrder, redirect));
+ 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);
+ 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 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);
+ // 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);
+ if (newVanity) {
+ // update bloom filter
+ BloomFilterUtils.add(vanityBloomFilter, checkPath);
+ }
+ }
+ } else {
+ if (newVanity) {
+ // update bloom filter
+ BloomFilterUtils.add(vanityBloomFilter, checkPath);
+ }
}
}
}
@@ -1174,7 +1383,7 @@ public class MapEntries implements Event
return filter.toString();
}
- private static final class MapEntryIterator implements Iterator<MapEntry> {
+ private final class MapEntryIterator implements Iterator<MapEntry> {
private final Map<String, List<MapEntry>> resolveMapsMap;
@@ -1239,7 +1448,13 @@ public class MapEntries implements Event
if (lastDotPos != -1) {
key = key.substring(0, lastDotPos);
}
- final List<MapEntry> special =
this.resolveMapsMap.get(key);
+
+ 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();
}
@@ -1306,5 +1521,20 @@ public class MapEntries implements Event
}
return mapEntry;
}
+
+ final class BloomFilterTask extends TimerTask {
+ @Override
+ public void run() {
+ try {
+ if (updateBloomFilterFile) {
+ persistBloomFilter();
+ updateBloomFilterFile = false;
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(
+ "Error while saving bloom filter to disk", e);
+ }
+ }
+ }
}
Added:
sling/trunk/bundles/resourceresolver/src/test/java/org/apache/sling/resourceresolver/impl/mapping/BloomFilterUtilsTest.java
URL:
http://svn.apache.org/viewvc/sling/trunk/bundles/resourceresolver/src/test/java/org/apache/sling/resourceresolver/impl/mapping/BloomFilterUtilsTest.java?rev=1647303&view=auto
==============================================================================
---
sling/trunk/bundles/resourceresolver/src/test/java/org/apache/sling/resourceresolver/impl/mapping/BloomFilterUtilsTest.java
(added)
+++
sling/trunk/bundles/resourceresolver/src/test/java/org/apache/sling/resourceresolver/impl/mapping/BloomFilterUtilsTest.java
Mon Dec 22 12:39:40 2014
@@ -0,0 +1,206 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import java.math.BigInteger;
+import java.util.HashSet;
+import java.util.Random;
+import org.junit.Test;
+
+/**
+ * Test the bloom filter utility class.
+ */
+public class BloomFilterUtilsTest {
+
+ /**
+ * Program to calculate the best shift and multiply constants.
+ */
+ public static void main(String... args) {
+ Random random = new Random(1);
+ HashSet<String> inSet = new HashSet<String>();
+ while (inSet.size() < 100) {
+ inSet.add(randomString(random));
+ }
+ Object[] in = inSet.toArray();
+ HashSet<String> notSet = new HashSet<String>();
+ while (notSet.size() < 10000) {
+ String k = randomString(random);
+ if (!inSet.contains(k)) {
+ notSet.add(k);
+ }
+ }
+ Object[] not = notSet.toArray();
+ int best = Integer.MAX_VALUE;
+ for (int mul = 1; mul < 100000; mul += 2) {
+ if (!BigInteger.valueOf(mul).isProbablePrime(10)) {
+ continue;
+ }
+ for (int shift = 0; shift < 32; shift++) {
+ byte[] bloom = BloomFilterUtils.createFilter(100, 64);
+ for (Object k : in) {
+ int h1 = hash(k.hashCode(), mul, shift), h2 = hash(h1,
mul, shift);
+ add(bloom, h1, h2);
+ }
+ int falsePositives = 0;
+ for (Object k : not) {
+ int h1 = hash(k.hashCode(), mul, shift), h2 = hash(h1,
mul, shift);
+ if (probablyContains(bloom, h1, h2)) {
+ falsePositives++;
+ // short false positives are bad
+ if (k.toString().length() < 4) {
+ falsePositives += 5;
+ }
+ if (falsePositives > best) {
+ break;
+ }
+ }
+ }
+ if (falsePositives < best) {
+ best = falsePositives;
+ System.out.println("mul: " + mul + " shift: "
+ + shift + " falsePositives: " + best);
+ }
+ }
+ }
+ }
+
+ private static String randomString(Random r) {
+ if (r.nextInt(5) == 0) {
+ return randomName(r);
+ }
+ int length = 1 + Math.abs((int) r.nextGaussian() * 5);
+ if (r.nextBoolean()) {
+ length += r.nextInt(10);
+ }
+ char[] chars = new char[length];
+ for (int i = 0; i < length; i++) {
+ chars[i] = randomChar(r);
+ }
+ return new String(chars);
+ }
+
+ private static char randomChar(Random r) {
+ switch (r.nextInt(101) / 100) {
+ case 0:
+ case 1:
+ // 20% ascii
+ return (char) (32 + r.nextInt(127 - 32));
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ // 40% a-z
+ return (char) ('a' + r.nextInt('z' - 'a'));
+ case 6:
+ // 10% A-Z
+ return (char) ('A' + r.nextInt('Z' - 'A'));
+ case 7:
+ case 8:
+ // 20% 0-9
+ return (char) ('0' + r.nextInt('9' - '0'));
+ case 9:
+ // 10% aeiou
+ return "aeiou".charAt(r.nextInt("aeiou".length()));
+ }
+ // 1% unicode
+ return (char) r.nextInt(65535);
+ }
+
+ private static String randomName(Random r) {
+ int i = r.nextInt(1000);
+ // like TPC-C lastName, but lowercase
+ String[] n = {
+ "bar", "ought", "able", "pri", "pres", "ese", "anti",
+ "cally", "ation", "eing" };
+ StringBuilder buff = new StringBuilder();
+ buff.append(n[i / 100]);
+ buff.append(n[(i / 10) % 10]);
+ buff.append(n[i % 10]);
+ return buff.toString();
+ }
+
+
+ private static int hash(int oldHash, int mul, int shift) {
+ return oldHash ^ ((oldHash * mul) >> shift);
+ }
+
+ private static void add(byte[] bloom, int h1, int h2) {
+ int len = bloom.length;
+ if (len > 0) {
+ bloom[(h1 >>> 3) % len] |= 1 << (h1 & 7);
+ bloom[(h2 >>> 3) % len] |= 1 << (h2 & 7);
+ }
+ }
+
+ private static boolean probablyContains(byte[] bloom, int h1, int h2) {
+ int len = bloom.length;
+ if (len == 0) {
+ return true;
+ }
+ int x = bloom[(h1 >>> 3) % len] & (1 << (h1 & 7));
+ if (x != 0) {
+ x = bloom[(h2 >>> 3) % len] & (1 << (h2 & 7));
+ }
+ return x != 0;
+ }
+
+ @Test
+ public void size() {
+ byte[] bloom = BloomFilterUtils.createFilter(100, 64);
+ assertEquals(64, bloom.length);
+ bloom = BloomFilterUtils.createFilter(10, 64);
+ assertEquals(11, bloom.length);
+ bloom = BloomFilterUtils.createFilter(0, 64);
+ assertEquals(0, bloom.length);
+ bloom = BloomFilterUtils.createFilter(1, 64);
+ assertEquals(1, bloom.length);
+ }
+
+ @Test
+ public void probability() {
+ byte[] bloom = BloomFilterUtils.createFilter(20, 64);
+ System.out.println(bloom.length);
+ Random random = new Random(1);
+ random.setSeed(1);
+ for (int i = 0; i < 20; i++) {
+ BloomFilterUtils.add(bloom, random.nextInt());
+ }
+ random.setSeed(1);
+ for (int i = 0; i < 20; i++) {
+ assertTrue(BloomFilterUtils.probablyContains(bloom,
random.nextInt()));
+ }
+ int falsePositives = 0;
+ for (int i = 20; i < 100000; i++) {
+ if (BloomFilterUtils.probablyContains(bloom, random.nextInt())) {
+ falsePositives++;
+ }
+ }
+ assertEquals(4594, falsePositives);
+ }
+ @Test
+ public void negativeHashCode() {
+ BloomFilterUtils.add(new byte[0], new Object() {
+ @Override
+ public int hashCode() {
+ return -1;
+ }
+ });
+ }
+
+}
Modified:
sling/trunk/bundles/resourceresolver/src/test/java/org/apache/sling/resourceresolver/impl/mapping/MapEntriesTest.java
URL:
http://svn.apache.org/viewvc/sling/trunk/bundles/resourceresolver/src/test/java/org/apache/sling/resourceresolver/impl/mapping/MapEntriesTest.java?rev=1647303&r1=1647302&r2=1647303&view=diff
==============================================================================
---
sling/trunk/bundles/resourceresolver/src/test/java/org/apache/sling/resourceresolver/impl/mapping/MapEntriesTest.java
(original)
+++
sling/trunk/bundles/resourceresolver/src/test/java/org/apache/sling/resourceresolver/impl/mapping/MapEntriesTest.java
Mon Dec 22 12:39:40 2014
@@ -26,6 +26,8 @@ import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
+import java.io.File;
+import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -87,6 +89,8 @@ public class MapEntriesTest {
configs.add(new VanityPathConfig("/vanityPathOnJcrContent", false));
Collections.sort(configs);
+
+
when(bundleContext.getDataFile("vanityBloomFilter.txt")).thenReturn(new
File("vanityBloomFilter.txt"));
when(resourceResolverFactory.getAdministrativeResourceResolver(null)).thenReturn(resourceResolver);
when(resourceResolverFactory.isVanityPathEnabled()).thenReturn(true);
when(resourceResolverFactory.getVanityPathConfig()).thenReturn(configs);
@@ -98,6 +102,11 @@ public class MapEntriesTest {
Field field0 = MapEntries.class.getDeclaredField("mapRoot");
field0.setAccessible(true);
field0.set(mapEntries, MapEntries.DEFAULT_MAP_ROOT);
+
+ Field field1 =
MapEntries.class.getDeclaredField("maxCachedVanityPathEntries");
+ field1.setAccessible(true);
+ field1.set(mapEntries, -1);
+
}
@Test
@@ -224,6 +233,7 @@ public class MapEntriesTest {
});
mapEntries.doInit();
+ mapEntries.initializeVanityPaths();
List<MapEntry> entries = mapEntries.getResolveMaps();
assertEquals(8, entries.size());
@@ -267,7 +277,7 @@ public class MapEntriesTest {
}
@Test
- public void test_vanity_path_registration_include_exclude() {
+ public void test_vanity_path_registration_include_exclude() throws
IOException {
final String[] validPaths = {"/libs/somewhere", "/libs/a/b", "/foo/a",
"/baa/a"};
final String[] invalidPaths = {"/libs/denied/a", "/libs/denied/b/c",
"/nowhere"};
@@ -292,6 +302,7 @@ public class MapEntriesTest {
});
mapEntries.doInit();
+ mapEntries.initializeVanityPaths();
List<MapEntry> entries = mapEntries.getResolveMaps();
// each valid resource results in 2 entries