blob: 089c3bf17005feed98782bd803dd3f97ed4c1021 [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 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 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.ResourceResolverFactoryImpl;
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 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 boolean updateBloomFilterFile = false;
@SuppressWarnings({ "unchecked" })
public MapEntries(final MapConfigurationProvider factory, final BundleContext bundleContext, final EventAdmin eventAdmin)
throws LoginException, IOException {
this.resolver = factory.getServiceResourceResolver(factory.getServiceUserAuthenticationInfo("mapping"));
this.factory = factory;
this.eventAdmin = eventAdmin;
this.resolveMapsMap = Collections.singletonMap(GLOBAL_LIST_KEY, (List<MapEntry>)Collections.EMPTY_LIST);
this.mapMaps = Collections.<MapEntry> emptyList();
this.vanityTargets = Collections.<String,List <String>>emptyMap();
this.aliasMap = Collections.<String, Map<String, String>>emptyMap();
doInit();
final Dictionary<String, Object> props = new Hashtable<>();
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();
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(createVanityBloomFilter);
this.vanityTargets = vanityTargets;
}
} 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)) {
changed |= removeAlias(contentPath, path, resolverRefreshed);
} else if ( 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
* @param refreshed Flag if session needs refresh
* @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 = 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() {
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;
}
/*
* 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;
}
/**
* This is for the web console plugin
*/
@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);
}
/**
* Calculate the resolve maps. As the entries have to be sorted by pattern
* length, we have to create a new list containing all relevant 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);
}
/**
* 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(ResourceResolverFactoryImpl, BundleContext, EventAdmin)}
* 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() throws IOException {
if (vanityBloomFilterFile != null && vanityBloomFilter != null) {
FileOutputStream out = new FileOutputStream(vanityBloomFilterFile);
try {
out.write(this.vanityBloomFilter);
} finally {
out.close();
}
}
}
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;
}
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 = "SELECT sling:alias FROM nt:base WHERE sling:alias IS NOT NULL";
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");
while (i.hasNext() && (createVanityBloomFilter || isAllVanityPathEntriesCached() || vanityCounter.longValue() < this.factory.getMaxCachedVanityPathEntries())) {
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 (isAllVanityPathEntriesCached() || vanityCounter.longValue() < this.factory.getMaxCachedVanityPathEntries()) {
// fill up the cache and the bloom filter
loadVanityPath(resource, resolveMapsMap, targetPaths, true,
createVanityBloomFilter);
} else {
// fill up the bloom filter
loadVanityPath(resource, resolveMapsMap, targetPaths, false,
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 = 0;
if (props.containsKey(PROP_VANITY_ORDER)) {
vanityOrder = props.get(PROP_VANITY_ORDER, Long.class);
}
// 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() {
try {
if (updateBloomFilterFile) {
persistBloomFilter();
updateBloomFilterFile = false;
}
} catch (IOException e) {
throw new RuntimeException(
"Error while saving bloom filter to disk", e);
}
}
}
}