All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.sling.jcr.base.AbstractSlingRepositoryManager Maven / Gradle / Ivy

There is a newer version: 2024.11.18751.20241128T090041Z-241100
Show newest version
/*
 * 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.jcr.base;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Dictionary;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import javax.jcr.Repository;

import org.apache.jackrabbit.api.JackrabbitRepository;
import org.apache.sling.jcr.api.SlingRepository;
import org.apache.sling.jcr.api.SlingRepositoryInitializer;
import org.apache.sling.jcr.base.internal.loader.Loader;
import org.apache.sling.jcr.base.internal.LoginAdminWhitelist;
import org.apache.sling.jcr.base.internal.mount.ProxyJackrabbitRepository;
import org.apache.sling.jcr.base.internal.mount.ProxyRepository;
import org.apache.sling.jcr.base.spi.RepositoryMount;
import org.apache.sling.serviceusermapping.ServiceUserMapper;
import org.osgi.annotation.versioning.ProviderType;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceFactory;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
import org.osgi.util.tracker.ServiceTracker;
import org.osgi.util.tracker.ServiceTrackerCustomizer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The AbstractSlingRepositoryManager is the basis for controlling
 * the JCR repository instances used by Sling. As a manager it starts and stops
 * the actual repository instance, manages service registration and hands out
 * {@code SlingRepository} instances to be used by the consumers.
 * 

* This base class controls the livecycle of repository instance whereas * implementations of this class provide actual integration into the runtime * context. The livecycle of the repository instance is defined as follows: *

* To start the repository instance, the implementation calls the * {@link #start(BundleContext, String, boolean)}method which goes through the * steps of instantiating the repository, setting things up, and registering the * repository as an OSGi service: *

    *
  1. {@link #acquireRepository()}
  2. *
  3. {@link #create(Bundle)}
  4. *
  5. {@link #registerService()}
  6. *
* Earlier versions of this class had an additional setup method, * whatever code was there can be moved to the create method. *

* If starting the repository fails, the method {@link #stoppingOnError(String, Throwable)} * will be called. By default the exception is logged as an error, but this can * be customized by overwriting the method. *

* To stop the repository instance, the implementation calls the {@link #stop()} * method which goes through the steps of unregistering the OSGi service, * tearing all special settings down and finally shutting down the repository: *

    *
  1. {@link #unregisterService(ServiceRegistration)}
  2. *
  3. {@link #destroy(AbstractSlingRepository2)}
  4. *
  5. {@link #disposeRepository(Repository)}
  6. *
*

* Instances of this class manage a single repository instance backing the OSGi * service instances. Each consuming bundle, though, gets its own service * instance backed by the single actual repository instance managed by this * class. * * @see AbstractSlingRepository2 * @since API version 2.3 (bundle version 2.2.2) */ @ProviderType public abstract class AbstractSlingRepositoryManager { private static final AtomicInteger startupCounter = new AtomicInteger(); private static final String INTERRUPTED_EXCEPTION_NOTE = "Avoid using Thread.interrupt() with Oak! See https://jackrabbit.apache.org/oak/docs/dos_and_donts.html ."; /** default log */ private final Logger log = LoggerFactory.getLogger(getClass()); private volatile BundleContext bundleContext; private volatile Repository repository; // the SlingRepository instance used to setup basic stuff // see setup and tearDown private volatile AbstractSlingRepository2 masterSlingRepository; private volatile ServiceRegistration repositoryService; private volatile String defaultWorkspace; private volatile boolean disableLoginAdministrative; private volatile ServiceTracker repoInitializerTracker; private volatile Loader loader; private volatile ServiceTracker whitelistTracker; private final Object repoInitLock = new Object(); private volatile Thread startupThread; volatile ServiceTracker mountTracker; private volatile int startupThreadMaxWaitCount; private volatile long startupThreadWaitMillis; /** * Returns the default workspace, which may be null meaning to * use the repository provided default workspace. * * @return the default workspace or {@code null} indicating the repository's * default workspace is actually used. */ public final String getDefaultWorkspace() { return defaultWorkspace; } /** * Returns whether to disable the * {@code SlingRepository.loginAdministrative} method or not. * * @return {@code true} if {@code SlingRepository.loginAdministrative} is * disabled. */ public final boolean isDisableLoginAdministrative() { return disableLoginAdministrative; } /** * Returns the {@code ServiceUserMapper} service to map the service name to * a service user name. *

* The {@code ServiceUserMapper} is used to implement the * {@link AbstractSlingRepository2#loginService(String, String)} method used * to replace the * {@link AbstractSlingRepository2#loginAdministrative(String)} method. If * this method returns {@code null} and hence the * {@code ServiceUserMapperService} is not available, the * {@code loginService} method is not able to login. * * @return The {@code ServiceUserMapper} service or {@code null} if not * available. * @see AbstractSlingRepository2#loginService(String, String) */ protected abstract ServiceUserMapper getServiceUserMapper(); /** * Returns whether or not the provided bundle is allowed to use * {@link SlingRepository#loginAdministrative(String)}. * * @param bundle The bundle requiring access to {@code loginAdministrative} * @return A boolean value indicating whether or not the bundle is allowed * to use {@code loginAdministrative}. */ protected boolean allowLoginAdministrativeForBundle(final Bundle bundle) { return whitelistTracker.getService().allowLoginAdministrative(bundle); } /** * Creates the backing JCR repository instances. It is expected for this * method to just start the repository. *

* This method does not throw any Throwable but instead just * returns null if not repository is available. Any problems * trying to acquire the repository must be caught and logged as * appropriate. * * @return The acquired JCR Repository or null if * not repository can be acquired. * @see #start(BundleContext, String, boolean) */ protected abstract Repository acquireRepository(); /** * Registers this component as an OSGi service with the types provided by * the {@link #getServiceRegistrationInterfaces()} method and properties * provided by the {@link #getServiceRegistrationProperties()} method. *

* The repository is actually registered as an OSGi {@code ServiceFactory} * where the {@link #create(Bundle)} method is called to create an actual * {@link AbstractSlingRepository2} repository instance for a calling * (using) bundle. When the bundle is done using the repository instance, * the {@link #destroy(AbstractSlingRepository2)} method is called to clean * up. * * @return The OSGi ServiceRegistration object representing the * registered service. * @see #start(BundleContext, String, boolean) * @see #getServiceRegistrationInterfaces() * @see #getServiceRegistrationProperties() * @see #create(Bundle) * @see #destroy(AbstractSlingRepository2) */ protected final ServiceRegistration registerService() { final Dictionary props = getServiceRegistrationProperties(); final String[] interfaces = getServiceRegistrationInterfaces(); return bundleContext.registerService(interfaces, new ServiceFactory() { @Override public AbstractSlingRepository2 getService(Bundle bundle, ServiceRegistration registration) { return AbstractSlingRepositoryManager.this.create(bundle); } @Override public void ungetService(Bundle bundle, ServiceRegistration registration, AbstractSlingRepository2 service) { AbstractSlingRepositoryManager.this.destroy(service); } }, props); } /** * Return the service registration properties to be used to register the * repository service in {@link #registerService()}. * * @return The service registration properties to be used to register the * repository service in {@link #registerService()} * @see #registerService() */ protected abstract Dictionary getServiceRegistrationProperties(); /** * Returns the service types to be used to register the repository service * in {@link #registerService()}. All interfaces returned must be accessible * to the class loader of the class of this instance. *

* This method may be overwritten to return additional types but the types * returned from this base implementation, {@code SlingRepository} and * {@code Repository}, must always be included. * * @return The service types to be used to register the repository service * in {@link #registerService()} * @see #registerService() */ protected String[] getServiceRegistrationInterfaces() { return new String[] { SlingRepository.class.getName(), Repository.class.getName() }; } /** * Creates an instance of the {@link AbstractSlingRepository2} * implementation for use by the given {@code usingBundle}. *

* This method is called when the repository service is requested from * within the using bundle for the first time. *

* This method is expected to return a new instance on every call. * * @param usingBundle The bundle providing from which the repository is * requested. * @return The {@link AbstractSlingRepository2} implementation instance to * be used by the {@code usingBundle}. * @see #registerService() */ protected abstract AbstractSlingRepository2 create(Bundle usingBundle); /** * Cleans up the given {@link AbstractSlingRepository2} instance previously * created by the {@link #create(Bundle)} method. * * @param repositoryServiceInstance The {@link AbstractSlingRepository2} * istance to cleanup. * @see #registerService() */ protected abstract void destroy(AbstractSlingRepository2 repositoryServiceInstance); /** * Returns the repository underlying this instance or null if * no repository is currently being available. * * @return The repository */ protected final Repository getRepository() { ServiceReference ref = mountTracker != null ? mountTracker.getServiceReference() : null; Repository mountRepo = (ref != null ? mountTracker.getService(ref) : null); Object mounts = ref != null ? ref.getProperty(RepositoryMount.MOUNT_POINTS_KEY) : null; Set mountPoints = new HashSet<>(); if (mounts != null) { if (mounts instanceof String[]) { for (String mount : ((String[]) mounts)) { mountPoints.add(mount); } } else { mountPoints.add(mounts.toString()); } } else { mountPoints.add("/content/jcrmount"); } return mountRepo != null ? repository instanceof JackrabbitRepository ? new ProxyJackrabbitRepository((JackrabbitRepository) repository, (JackrabbitRepository) mountRepo, mountPoints) : new ProxyRepository<>(repository, mountRepo, mountPoints) : repository; } /** * Unregisters the service represented by the * serviceRegistration. * * @param serviceRegistration The service to unregister */ protected final void unregisterService(ServiceRegistration serviceRegistration) { serviceRegistration.unregister(); } /** * Disposes off the given repository. * * @param repository The repository to be disposed off which is the same as * the one returned from {@link #acquireRepository()}. */ protected abstract void disposeRepository(Repository repository); /** * Called when the repository service cannot be initialized or registered * because an exception occurred. *

* This default implementation logs the exception as an error. * * @param message failure details. * @param t the exception. */ protected void stoppingOnError(String message, Throwable t) { log.error(message, t); } // --------- SCR integration ----------------------------------------------- /** * This method was deprecated with the introduction of asynchronous repository registration. With * asynchronous registration a boolean return value can no longer be guaranteed, as registration * may happen after the method returns. *

* Instead a {@link org.osgi.framework.ServiceListener} for {@link SlingRepository} may be * registered to get informed about its successful registration. * * @param bundleContext The {@code BundleContext} to register the repository * service (and optionally more services required to operate the * repository) * @param defaultWorkspace The name of the default workspace to use to * login. This may be {@code null} to have the actual repository * instance define its own default * @param disableLoginAdministrative Whether to disable the * {@code SlingRepository.loginAdministrative} method or not. * @return {@code true} if the repository has been started and the service * is registered; {@code false} if the service has not been registered, * which may indicate that startup was unsuccessful OR that it is happening * asynchronously. A more reliable way to determin availability of the * {@link SlingRepository} as a service is using a * {@link org.osgi.framework.ServiceListener}. * @deprecated use {@link #start(BundleContext, AbstractSlingRepositoryManager.Config)} instead. */ @Deprecated protected final boolean start(final BundleContext bundleContext, final String defaultWorkspace, final boolean disableLoginAdministrative) { start(bundleContext, new Config(defaultWorkspace, disableLoginAdministrative)); long end = System.currentTimeMillis() + 5000; // wait up to 5 seconds for repository registration while (!isRepositoryServiceRegistered() && end > System.currentTimeMillis()) { try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } return isRepositoryServiceRegistered(); } /** * Configuration pojo to be passed to the {@link #start(BundleContext, Config)} method. */ protected static final class Config { protected final String defaultWorkspace; protected final boolean disableLoginAdministrative; protected final int startupThreadMaxWaitCount; protected final long startupThreadWaitMillis; /** * @param defaultWorkspace The name of the default workspace to use to * login. This may be {@code null} to have the actual repository * instance define its own default * * @param disableLoginAdministrative Whether to disable the * {@code SlingRepository.loginAdministrative} method or not. */ public Config(String defaultWorkspace, boolean disableLoginAdministrative) { this(defaultWorkspace, disableLoginAdministrative, 5, TimeUnit.MINUTES.toMillis(1)); } /** * @param defaultWorkspace The name of the default workspace to use to * login. This may be {@code null} to have the actual repository * instance define its own default * * @param disableLoginAdministrative Whether to disable the * {@code SlingRepository.loginAdministrative} method or not. * * @param startupThreadMaxWaitCount The number of attempts to be performed * when waiting for the repository startup to complete * * @param startupThreadWaitMillis The duration of each of the waits performed * when waiting for the repository startup to complete */ public Config(String defaultWorkspace, boolean disableLoginAdministrative, int startupThreadMaxWaitCount, long startupThreadWaitMillis) { this.defaultWorkspace = defaultWorkspace; this.disableLoginAdministrative = disableLoginAdministrative; this.startupThreadMaxWaitCount = startupThreadMaxWaitCount; this.startupThreadWaitMillis = startupThreadWaitMillis; } } /** * This method actually starts the backing repository instannce and * registeres the repository service. *

* Multiple subsequent calls to this method without calling {@link #stop()} * first have no effect. * * @param bundleContext The {@code BundleContext} to register the repository * service (and optionally more services required to operate the * repository) * @param config The configuration to apply to this instance. */ protected final void start(final BundleContext bundleContext, final Config config) { // already setup ? if (this.bundleContext != null) { log.debug("start: Repository already started and registered"); return; } this.bundleContext = bundleContext; this.defaultWorkspace = config.defaultWorkspace; this.disableLoginAdministrative = config.disableLoginAdministrative; this.startupThreadMaxWaitCount = config.startupThreadMaxWaitCount; this.startupThreadWaitMillis = config.startupThreadWaitMillis; this.mountTracker = new ServiceTracker<>(this.bundleContext, RepositoryMount.class, null); this.mountTracker.open(); this.repoInitializerTracker = new ServiceTracker(bundleContext, SlingRepositoryInitializer.class, new ServiceTrackerCustomizer() { @Override public SlingRepositoryInitializerInfo addingService(final ServiceReference reference) { final SlingRepositoryInitializer service = bundleContext.getService(reference); if ( service != null ) { final SlingRepositoryInitializerInfo info = new SlingRepositoryInitializerInfo(service, reference); synchronized ( repoInitLock ) { if ( masterSlingRepository != null ) { log.debug("Executing {}", info.initializer); try { info.initializer.processRepository(masterSlingRepository); } catch (final Exception e) { log.error("Exception in a SlingRepositoryInitializer: " + info.initializer, e); } } } return info; } return null; } @Override public void modifiedService(final ServiceReference reference, final SlingRepositoryInitializerInfo service) { // nothing to do } @Override public void removedService(final ServiceReference reference, final SlingRepositoryInitializerInfo service) { bundleContext.ungetService(reference); } }); this.repoInitializerTracker.open(); // If allowLoginAdministrativeForBundle is overridden we assume we don't need // a LoginAdminWhitelist service - that's the case if the derived class // implements its own strategy and the LoginAdminWhitelist interface is // not exported by this bundle anyway, so cannot be implemented differently. boolean enableWhitelist = !isAllowLoginAdministrativeForBundleOverridden(); final CountDownLatch waitForWhitelist = new CountDownLatch(enableWhitelist ? 1 : 0); if (enableWhitelist) { whitelistTracker = new ServiceTracker(bundleContext, LoginAdminWhitelist.class, null) { @Override public LoginAdminWhitelist addingService(final ServiceReference reference) { try { return super.addingService(reference); } finally { waitForWhitelist.countDown(); } } }; whitelistTracker.open(); } // start repository asynchronously to allow LoginAdminWhitelist to become available // NOTE: making this conditional allows tests to register a mock whitelist before // activating the RepositoryManager, so they don't need to deal with async startup startupThread = new Thread("Apache Sling Repository Startup Thread #" + startupCounter.incrementAndGet()) { @Override public void run() { try { waitForWhitelist.await(); initializeAndRegisterRepositoryService(); } catch (InterruptedException e) { log.warn("Interrupted while waiting for the {} service, cancelling repository initialisation. {}", LoginAdminWhitelist.class.getSimpleName(), INTERRUPTED_EXCEPTION_NOTE, e); Thread.currentThread().interrupt(); } } }; startupThread.start(); } private boolean isRepositoryServiceRegistered() { return repositoryService != null; } private void initializeAndRegisterRepositoryService() { try { log.debug("start: calling acquireRepository()"); Repository newRepo = this.acquireRepository(); if (newRepo != null) { // ensure we really have the repository log.debug("start: got a Repository"); this.repository = newRepo; synchronized ( this.repoInitLock ) { this.masterSlingRepository = this.create(this.bundleContext.getBundle()); log.debug("start: setting up Loader"); this.loader = new Loader(this.masterSlingRepository, this.bundleContext); log.debug("start: calling SlingRepositoryInitializer"); try { executeRepositoryInitializers(this.masterSlingRepository); } catch(Throwable e) { stoppingOnError("Exception in a SlingRepositoryInitializer, SlingRepository service registration aborted", e); stop(); return; } log.debug("start: calling registerService()"); this.repositoryService = registerService(); log.debug("start: registerService() successful, registration={}", repositoryService); } } } catch (Throwable e) { // consider an uncaught problem an error stoppingOnError("start: Uncaught Throwable trying to access Repository, calling stop()", e); stop(); } } // find out whether allowLoginAdministrativeForBundle is overridden // by iterating through the super classes of the implementation // class and search for the class which defines the method // "allowLoginAdministrativeForBundle". If we don't find // the method before hitting AbstractSlingRepositoryManager // we know that our implementation is inherited. // Note: clazz.get(Declared)Method(name, parameterTypes).getDeclaringClass() // does not yield the same results and is therefore no fitting substitute. private boolean isAllowLoginAdministrativeForBundleOverridden() { Class clazz = getClass(); while (clazz != AbstractSlingRepositoryManager.class) { final Method[] declaredMethods = clazz.getDeclaredMethods(); for (final Method method : declaredMethods) { if (method.getName().equals("allowLoginAdministrativeForBundle") && Arrays.equals(method.getParameterTypes(), new Class[]{Bundle.class})) { return true; } } clazz = clazz.getSuperclass(); } return false; } private void executeRepositoryInitializers(final SlingRepository repo) throws Exception { final SlingRepositoryInitializerInfo [] infos = repoInitializerTracker.getServices(new SlingRepositoryInitializerInfo[0]); if (infos == null || infos.length == 0) { log.debug("No SlingRepositoryInitializer services found"); return; } Arrays.sort(infos); for(final SlingRepositoryInitializerInfo info : infos) { log.debug("Executing {}", info.initializer); info.initializer.processRepository(repo); } } protected final void stop() { log.info("Stop requested"); if ( startupThread != null && startupThread != Thread.currentThread() ) { waitForStartupThreadToComplete(); startupThread = null; } if (this.mountTracker != null) { this.mountTracker.close(); this.mountTracker = null; } // ensure the repository is really disposed off if (repository != null || isRepositoryServiceRegistered()) { log.info("stop: Repository still running, forcing shutdown"); // make sure we are not concurrently unregistering the repository synchronized (repoInitLock) { try { if (isRepositoryServiceRegistered()) { try { log.debug("stop: Unregistering SlingRepository service, registration={}", repositoryService); unregisterService(repositoryService); } catch (Throwable t) { log.info("stop: Uncaught problem unregistering the repository service", t); } repositoryService = null; } if (repository != null) { Repository oldRepo = repository; repository = null; // stop loader if (this.loader != null) { this.loader.dispose(); this.loader = null; } // destroy repository this.destroy(this.masterSlingRepository); try { disposeRepository(oldRepo instanceof ProxyRepository ? ((ProxyRepository) oldRepo).jcr : oldRepo); } catch (Throwable t) { log.info("stop: Uncaught problem disposing the repository", t); } } } catch (Throwable t) { log.warn("stop: Unexpected problem stopping repository", t); } } } if(repoInitializerTracker != null) { repoInitializerTracker.close(); repoInitializerTracker = null; } if (whitelistTracker != null) { whitelistTracker.close(); whitelistTracker = null; } this.repositoryService = null; this.repository = null; this.defaultWorkspace = null; this.bundleContext = null; } private void waitForStartupThreadToComplete() { try { // Oak does play well with interrupted exceptions, so avoid that at all costs // https://jackrabbit.apache.org/oak/docs/dos_and_donts.html for ( int i = 0; i < startupThreadMaxWaitCount; i++ ) { log.info("Waiting {} millis for {} to complete, attempt {}/{}.", startupThreadWaitMillis, startupThread.getName(), (i + 1), startupThreadMaxWaitCount); startupThread.join(startupThreadWaitMillis); if ( !startupThread.isAlive() ) { log.info("{} not alive, proceeding", startupThread.getName()); break; } } } catch (InterruptedException e) { log.warn("Interrupted while waiting for the {} to complete. {}", startupThread.getName(), INTERRUPTED_EXCEPTION_NOTE, e); Thread.currentThread().interrupt(); } if ( startupThread.isAlive() ) { log.warn("Proceeding even though {} is still running, behaviour is undefined.", startupThread.getName()); if ( log.isInfoEnabled() ) { StringBuilder stackTrace = new StringBuilder(); stackTrace.append("Stack trace for ").append(startupThread.getName()).append(" :\n"); for (StackTraceElement traceElement : startupThread.getStackTrace()) stackTrace.append("\tat ").append(traceElement).append('\n'); log.info(stackTrace.toString()); } } } private static final class SlingRepositoryInitializerInfo implements Comparable { final SlingRepositoryInitializer initializer; final ServiceReference ref; SlingRepositoryInitializerInfo(final SlingRepositoryInitializer init, ServiceReference ref) { this.initializer = init; this.ref = ref; } @Override public int compareTo(SlingRepositoryInitializerInfo o) { return ref.compareTo(o.ref); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy