| /* |
| * 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); |
| } |
| } |