blob: 6de579a2fa469f2ec0b1390aeaf91d831fece4da [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;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Dictionary;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.TreeBidiMap;
import org.apache.sling.api.resource.ResourceDecorator;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.mapping.PathToUriMappingService;
import org.apache.sling.api.resource.path.Path;
import org.apache.sling.api.resource.runtime.RuntimeService;
import org.apache.sling.resourceresolver.impl.helper.ResourceDecoratorTracker;
import org.apache.sling.resourceresolver.impl.mapping.MapEntries;
import org.apache.sling.resourceresolver.impl.mapping.Mapping;
import org.apache.sling.resourceresolver.impl.mapping.StringInterpolationProvider;
import org.apache.sling.resourceresolver.impl.observation.ResourceChangeListenerWhiteboard;
import org.apache.sling.resourceresolver.impl.providers.ResourceProviderTracker;
import org.apache.sling.resourceresolver.impl.providers.ResourceProviderTracker.ChangeListener;
import org.apache.sling.resourceresolver.impl.providers.RuntimeServiceImpl;
import org.apache.sling.serviceusermapping.ServiceUserMapper;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.osgi.service.event.EventAdmin;
import org.osgi.service.metatype.annotations.Designate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The <code>ResourceResolverFactoryActivator/code> keeps track of required services for the
* resource resolver factory.
* One all required providers and provider factories are available a resource resolver factory
* is registered.
*
*/
@Designate(ocd = ResourceResolverFactoryConfig.class)
@Component(name = "org.apache.sling.jcr.resource.internal.JcrResourceResolverFactoryImpl")
public class ResourceResolverFactoryActivator {
private static final class FactoryRegistration {
/** Registration .*/
public volatile ServiceRegistration<ResourceResolverFactory> factoryRegistration;
/** Runtime registration. */
public volatile ServiceRegistration<RuntimeService> runtimeRegistration;
public volatile CommonResourceResolverFactoryImpl commonFactory;
}
/** Logger. */
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/** Tracker for the resource decorators. */
private final ResourceDecoratorTracker resourceDecoratorTracker = new ResourceDecoratorTracker();
/** all mappings */
private volatile Mapping[] mappings;
/** The fake URLs */
private volatile BidiMap virtualURLMap;
/** the search path for ResourceResolver.getResource(String) */
private volatile List<String> searchPath = Collections.emptyList();
/** the root location of the /etc/map entries */
private volatile String mapRoot;
private volatile String mapRootPrefix;
/** Event admin. */
@Reference
EventAdmin eventAdmin;
/** String Interpolation Provider. */
@Reference
StringInterpolationProvider stringInterpolationProvider;
/** Service User Mapper */
@Reference
ServiceUserMapper serviceUserMapper;
@Reference
ResourceAccessSecurityTracker resourceAccessSecurityTracker;
@Reference
PathToUriMappingService pathToUriMappingService;
volatile ResourceProviderTracker resourceProviderTracker;
volatile ResourceChangeListenerWhiteboard changeListenerWhiteboard;
/** Bundle Context */
private volatile BundleContext bundleContext;
private volatile ResourceResolverFactoryConfig config = DEFAULT_CONFIG;
/** Vanity path whitelist */
private volatile String[] vanityPathWhiteList;
/** Vanity path blacklist */
private volatile String[] vanityPathBlackList;
/** Observation paths */
private volatile Path[] observationPaths;
private final FactoryPreconditions preconds = new FactoryPreconditions();
/** Factory registration. */
private volatile FactoryRegistration factoryRegistration;
/**
* Get the resource decorator tracker.
*/
public ResourceDecoratorTracker getResourceDecoratorTracker() {
return this.resourceDecoratorTracker;
}
public ResourceAccessSecurityTracker getResourceAccessSecurityTracker() {
return this.resourceAccessSecurityTracker;
}
public PathToUriMappingService getPathToUriMappingService() {
return this.pathToUriMappingService;
}
public EventAdmin getEventAdmin() {
return this.eventAdmin;
}
public StringInterpolationProvider getStringInterpolationProvider() {
return stringInterpolationProvider;
}
/**
* This method is called from {@link MapEntries}
*/
public BidiMap getVirtualURLMap() {
return virtualURLMap;
}
/**
* This method is called from {@link MapEntries}
*/
public Mapping[] getMappings() {
return mappings;
}
public List<String> getSearchPath() {
return searchPath;
}
public boolean isMangleNamespacePrefixes() {
return config.resource_resolver_manglenamespaces();
}
public String getMapRoot() {
return mapRoot;
}
public boolean isMapConfiguration(String path) {
return path.equals(this.mapRoot)
|| path.startsWith(this.mapRootPrefix);
}
public int getDefaultVanityPathRedirectStatus() {
return config.resource_resolver_default_vanity_redirect_status();
}
public boolean isVanityPathEnabled() {
return this.config.resource_resolver_enable_vanitypath();
}
public boolean isOptimizeAliasResolutionEnabled() {
return this.config.resource_resolver_optimize_alias_resolution();
}
public boolean isLogUnclosedResourceResolvers() {
return this.config.resource_resolver_log_unclosed();
}
public String[] getVanityPathWhiteList() {
return this.vanityPathWhiteList;
}
public String[] getVanityPathBlackList() {
return this.vanityPathBlackList;
}
public boolean hasVanityPathPrecedence() {
return this.config.resource_resolver_vanity_precedence();
}
public long getMaxCachedVanityPathEntries() {
return this.config.resource_resolver_vanitypath_maxEntries();
}
public boolean isMaxCachedVanityPathEntriesStartup() {
return this.config.resource_resolver_vanitypath_maxEntries_startup();
}
public int getVanityBloomFilterMaxBytes() {
return this.config.resource_resolver_vanitypath_bloomfilter_maxBytes();
}
public boolean shouldLogResourceResolverClosing() {
return this.config.resource_resolver_log_closing();
}
public Path[] getObservationPaths() {
return this.observationPaths;
}
// ---------- SCR Integration ---------------------------------------------
/**
* Activates this component (called by SCR before)
*/
@Activate
protected void activate(final BundleContext bundleContext, final ResourceResolverFactoryConfig config) {
this.bundleContext = bundleContext;
this.config = config;
final BidiMap virtuals = new TreeBidiMap();
for (int i = 0; config.resource_resolver_virtual() != null && i < config.resource_resolver_virtual().length; i++) {
final String[] parts = Mapping.split(config.resource_resolver_virtual()[i]);
virtuals.put(parts[0], parts[2]);
}
virtualURLMap = virtuals;
final List<Mapping> maps = new ArrayList<>();
for (int i = 0; config.resource_resolver_mapping() != null && i < config.resource_resolver_mapping().length; i++) {
maps.add(new Mapping(config.resource_resolver_mapping()[i]));
}
final Mapping[] tmp = maps.toArray(new Mapping[maps.size()]);
// check whether direct mappings are allowed
if (config.resource_resolver_allowDirect()) {
final Mapping[] tmp2 = new Mapping[tmp.length + 1];
tmp2[0] = Mapping.DIRECT;
System.arraycopy(tmp, 0, tmp2, 1, tmp.length);
mappings = tmp2;
} else {
mappings = tmp;
}
// from configuration if available
final List<String> searchPathList = new ArrayList<>();
if (config.resource_resolver_searchpath() != null && config.resource_resolver_searchpath().length > 0) {
for(String path : config.resource_resolver_searchpath()) {
// ensure leading slash
if (!path.startsWith("/")) {
path = "/".concat(path);
}
// ensure trailing slash
if (!path.endsWith("/")) {
path = path.concat("/");
}
searchPathList.add(path);
}
}
if (searchPathList.isEmpty()) {
searchPathList.add("/");
}
this.searchPath = Collections.unmodifiableList(searchPathList);
// the root of the resolver mappings
mapRoot = config.resource_resolver_map_location();
mapRootPrefix = mapRoot + '/';
final String[] paths = config.resource_resolver_map_observation();
this.observationPaths = new Path[paths.length];
for(int i=0;i<paths.length;i++) {
this.observationPaths[i] = new Path(paths[i]);
}
// vanity path white list
this.vanityPathWhiteList = null;
String[] vanityPathPrefixes = config.resource_resolver_vanitypath_whitelist();
if ( vanityPathPrefixes != null ) {
final List<String> prefixList = new ArrayList<>();
for(final String value : vanityPathPrefixes) {
if ( value.trim().length() > 0 ) {
if ( value.trim().endsWith("/") ) {
prefixList.add(value.trim());
} else {
prefixList.add(value.trim() + "/");
}
}
}
if ( prefixList.size() > 0 ) {
this.vanityPathWhiteList = prefixList.toArray(new String[prefixList.size()]);
}
}
// vanity path black list
this.vanityPathBlackList = null;
vanityPathPrefixes = config.resource_resolver_vanitypath_blacklist();
if ( vanityPathPrefixes != null ) {
final List<String> prefixList = new ArrayList<>();
for(final String value : vanityPathPrefixes) {
if ( value.trim().length() > 0 ) {
if ( value.trim().endsWith("/") ) {
prefixList.add(value.trim());
} else {
prefixList.add(value.trim() + "/");
}
}
}
if ( prefixList.size() > 0 ) {
this.vanityPathBlackList = prefixList.toArray(new String[prefixList.size()]);
}
}
// check for required property
Set<String> requiredResourceProvidersLegacy = getStringSet(config.resource_resolver_required_providers());
Set<String> requiredResourceProviderNames = getStringSet(config.resource_resolver_required_providernames());
boolean hasLegacyRequiredProvider = false;
if ( requiredResourceProvidersLegacy != null ) {
hasLegacyRequiredProvider = requiredResourceProvidersLegacy.remove(ResourceResolverFactoryConfig.LEGACY_REQUIRED_PROVIDER_PID);
if ( !requiredResourceProvidersLegacy.isEmpty() ) {
logger.error("ResourceResolverFactory is using deprecated required providers configuration (resource.resolver.required.providers" +
"). Please change to use the property resource.resolver.required.providernames for values: " + requiredResourceProvidersLegacy);
} else {
requiredResourceProvidersLegacy = null;
}
}
if ( hasLegacyRequiredProvider ) {
final boolean hasRequiredProvider;
if ( requiredResourceProviderNames != null ) {
hasRequiredProvider = !requiredResourceProviderNames.add(ResourceResolverFactoryConfig.REQUIRED_PROVIDER_NAME);
} else {
hasRequiredProvider = false;
requiredResourceProviderNames = Collections.singleton(ResourceResolverFactoryConfig.REQUIRED_PROVIDER_NAME);
}
if ( hasRequiredProvider ) {
logger.warn("ResourceResolverFactory is using deprecated required providers configuration (resource.resolver.required.providers" +
") with value '" + ResourceResolverFactoryConfig.LEGACY_REQUIRED_PROVIDER_PID + ". Please remove this configuration property. " +
ResourceResolverFactoryConfig.REQUIRED_PROVIDER_NAME + " is already contained in the property resource.resolver.required.providernames.");
} else {
logger.warn("ResourceResolverFactory is using deprecated required providers configuration (resource.resolver.required.providers" +
") with value '" + ResourceResolverFactoryConfig.LEGACY_REQUIRED_PROVIDER_PID + ". Please remove this configuration property and add " +
ResourceResolverFactoryConfig.REQUIRED_PROVIDER_NAME + " to the property resource.resolver.required.providernames.");
}
}
// for testing: if we run unit test, both trackers are set from the outside
if ( this.resourceProviderTracker == null ) {
this.resourceProviderTracker = new ResourceProviderTracker();
this.changeListenerWhiteboard = new ResourceChangeListenerWhiteboard();
this.preconds.activate(this.bundleContext,
requiredResourceProvidersLegacy,
requiredResourceProviderNames,
resourceProviderTracker);
this.changeListenerWhiteboard.activate(this.bundleContext,
this.resourceProviderTracker, searchPath);
this.resourceProviderTracker.activate(this.bundleContext,
this.eventAdmin,
new ChangeListener() {
@Override
public void providerAdded() {
if ( factoryRegistration == null ) {
checkFactoryPreconditions(null, null);
}
}
@Override
public void providerRemoved(final String name, final String pid, final boolean stateful, final boolean isUsed) {
if ( factoryRegistration != null ) {
if ( isUsed && (stateful || config.resource_resolver_providerhandling_paranoid()) ) {
unregisterFactory();
}
checkFactoryPreconditions(name, pid);
}
}
});
} else {
this.preconds.activate(this.bundleContext,
requiredResourceProvidersLegacy,
requiredResourceProviderNames,
resourceProviderTracker);
this.checkFactoryPreconditions(null, null);
}
}
/**
* Modifies this component (called by SCR to update this component)
*/
@Modified
protected void modified(final BundleContext bundleContext, final ResourceResolverFactoryConfig config) {
this.deactivate();
this.activate(bundleContext, config);
}
/**
* Deactivates this component (called by SCR to take out of service)
*/
@Deactivate
protected void deactivate() {
this.unregisterFactory();
this.bundleContext = null;
this.config = DEFAULT_CONFIG;
this.changeListenerWhiteboard.deactivate();
this.changeListenerWhiteboard = null;
this.resourceProviderTracker.deactivate();
this.resourceProviderTracker = null;
this.preconds.deactivate();
this.resourceDecoratorTracker.close();
// this is just a sanity call to make sure that unregister
// in the case that a registration happened again
// while deactivation
this.unregisterFactory();
}
/**
* Unregister the factory (if registered)
* This method might be called concurrently from deactivate and the
* background thread, therefore we need to synchronize.
*/
private void unregisterFactory() {
FactoryRegistration local = null;
synchronized ( this ) {
if ( this.factoryRegistration != null ) {
local = this.factoryRegistration;
this.factoryRegistration = null;
}
}
this.unregisterFactory(local);
}
/**
* Unregister the provided factory
*/
private void unregisterFactory(final FactoryRegistration local) {
if ( local != null ) {
if ( local.factoryRegistration != null ) {
local.factoryRegistration.unregister();
}
if ( local.runtimeRegistration != null ) {
local.runtimeRegistration.unregister();
}
if ( local.commonFactory != null ) {
local.commonFactory.deactivate();
}
}
}
/**
* Try to register the factory.
*/
private void registerFactory(final BundleContext localContext) {
final FactoryRegistration local = new FactoryRegistration();
if ( localContext != null ) {
// activate and register factory
final Dictionary<String, Object> serviceProps = new Hashtable<>();
serviceProps.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation");
serviceProps.put(Constants.SERVICE_DESCRIPTION, "Apache Sling Resource Resolver Factory");
local.commonFactory = new CommonResourceResolverFactoryImpl(this);
local.commonFactory.activate(localContext);
local.factoryRegistration = localContext.registerService(
ResourceResolverFactory.class, new ServiceFactory<ResourceResolverFactory>() {
@Override
public ResourceResolverFactory getService(final Bundle bundle, final ServiceRegistration<ResourceResolverFactory> registration) {
if ( ResourceResolverFactoryActivator.this.bundleContext == null ) {
return null;
}
final ResourceResolverFactoryImpl r = new ResourceResolverFactoryImpl(
local.commonFactory, bundle,
ResourceResolverFactoryActivator.this.serviceUserMapper);
return r;
}
@Override
public void ungetService(final Bundle bundle, final ServiceRegistration<ResourceResolverFactory> registration, final ResourceResolverFactory service) {
// nothing to do
}
}, serviceProps);
local.runtimeRegistration = localContext.registerService(RuntimeService.class,
this.getRuntimeService(), null);
this.factoryRegistration = local;
}
}
/**
* Get the runtime service
* @return The runtime service
*/
public RuntimeService getRuntimeService() {
return new RuntimeServiceImpl(this.resourceProviderTracker);
}
public ServiceUserMapper getServiceUserMapper() {
return this.serviceUserMapper;
}
public BundleContext getBundleContext() {
return this.bundleContext;
}
/**
* Check the preconditions and if it changed, either register factory or unregister
*/
private void checkFactoryPreconditions(final String unavailableName, final String unavailableServicePid) {
final BundleContext localContext = this.bundleContext;
if ( localContext != null ) {
final boolean result = this.preconds.checkPreconditions(unavailableName, unavailableServicePid);
if ( result && this.factoryRegistration == null ) {
// check system bundle state - if stopping, don't register new factory
final Bundle systemBundle = localContext.getBundle(Constants.SYSTEM_BUNDLE_LOCATION);
if ( systemBundle != null && systemBundle.getState() != Bundle.STOPPING ) {
boolean create = true;
synchronized ( this ) {
if ( this.factoryRegistration == null ) {
this.factoryRegistration = new FactoryRegistration();
} else {
create = false;
}
}
if ( create ) {
this.registerFactory(localContext);
}
}
} else if ( !result && this.factoryRegistration != null ) {
this.unregisterFactory();
}
}
}
/**
* Bind a resource decorator.
*/
@Reference(service = ResourceDecorator.class, cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
protected void bindResourceDecorator(final ResourceDecorator decorator, final Map<String, Object> props) {
this.resourceDecoratorTracker.bindResourceDecorator(decorator, props);
}
/**
* Unbind a resource decorator.
*/
protected void unbindResourceDecorator(final ResourceDecorator decorator, final Map<String, Object> props) {
this.resourceDecoratorTracker.unbindResourceDecorator(decorator, props);
}
/**
* Get the resource provider tracker
* @return The tracker
*/
public ResourceProviderTracker getResourceProviderTracker() {
return resourceProviderTracker;
}
/**
* Utility method to create a set out of a string array
*/
private Set<String> getStringSet(final String[] values) {
if ( values == null || values.length == 0 ) {
return null;
}
final Set<String> set = new HashSet<>();
for(final String val : values) {
if ( val != null && !val.trim().isEmpty() ) {
set.add(val.trim());
}
}
return set.isEmpty() ? null : set;
}
public static ResourceResolverFactoryConfig DEFAULT_CONFIG;
static {
final InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(final Object obj, final Method calledMethod, final Object[] args)
throws Throwable {
if ( calledMethod.getDeclaringClass().isAssignableFrom(ResourceResolverFactoryConfig.class) ) {
return calledMethod.getDefaultValue();
}
if ( calledMethod.getDeclaringClass() == Object.class ) {
if ( calledMethod.getName().equals("toString") && (args == null || args.length == 0) ) {
return "Generated @" + ResourceResolverFactoryConfig.class.getName() + " instance";
}
if ( calledMethod.getName().equals("hashCode") && (args == null || args.length == 0) ) {
return this.hashCode();
}
if ( calledMethod.getName().equals("equals") && args != null && args.length == 1 ) {
return Boolean.FALSE;
}
}
throw new InternalError("unexpected method dispatched: " + calledMethod);
}
};
DEFAULT_CONFIG = (ResourceResolverFactoryConfig) Proxy.newProxyInstance(ResourceResolverFactoryConfig.class.getClassLoader(),
new Class[] { ResourceResolverFactoryConfig.class },
handler);
}
}