/* * 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.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.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 String[] searchPath; /** the root location of the /etc/map entries */ private volatile String mapRoot; private volatile String mapRootPrefix; /** Event admin. */ @Reference EventAdmin eventAdmin; /** Service User Mapper */ @Reference ServiceUserMapper serviceUserMapper; @Reference ResourceAccessSecurityTracker resourceAccessSecurityTracker; 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 EventAdmin getEventAdmin() { return this.eventAdmin; } /** * 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 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 searchPath = config.resource_resolver_searchpath(); if (searchPath != null && searchPath.length > 0) { for (int i = 0; i < searchPath.length; i++) { // ensure leading slash if (!searchPath[i].startsWith("/")) { searchPath[i] = "/" + searchPath[i]; } // ensure trailing slash if (!searchPath[i].endsWith("/")) { searchPath[i] += "/"; } } } if (searchPath == null) { searchPath = new String[] { "/" }; } // 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); } }