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

com.nimbusds.infinispan.persistence.ldap.LDAPStore Maven / Gradle / Ivy

There is a newer version: 3.1.3
Show newest version
package com.nimbusds.infinispan.persistence.ldap;


import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;

import com.nimbusds.infinispan.persistence.common.InfinispanEntry;
import com.nimbusds.infinispan.persistence.common.InfinispanStore;
import com.nimbusds.infinispan.persistence.common.query.QueryExecutor;
import com.nimbusds.infinispan.persistence.ldap.backend.LDAPConnector;
import com.nimbusds.infinispan.persistence.ldap.query.LDAPQueryExecutor;
import com.nimbusds.infinispan.persistence.ldap.query.LDAPQueryExecutorInitContext;
import com.unboundid.ldap.sdk.DN;
import com.unboundid.ldap.sdk.ReadOnlyEntry;
import net.jcip.annotations.ThreadSafe;
import org.infinispan.commons.configuration.ConfiguredBy;
import org.infinispan.filter.KeyFilter;
import org.infinispan.marshall.core.MarshalledEntry;
import org.infinispan.marshall.core.MarshalledEntryFactory;
import org.infinispan.persistence.TaskContextImpl;
import org.infinispan.persistence.spi.InitializationContext;
import org.infinispan.persistence.spi.PersistenceException;
import org.kohsuke.MetaInfServices;


/**
 * LDAP store for Infinispan 8.2+ caches and maps.
 */
@ThreadSafe
@MetaInfServices
@ConfiguredBy(LDAPStoreConfiguration.class)
public class LDAPStore extends InfinispanStore {


	/**
	 * The LDAP store configuration.
	 */
	private LDAPStoreConfiguration config;


	/**
	 * The LDAP backend connector.
	 */
	private LDAPConnector ldapConnector;


	/**
	 * The LDAP entry transformer (to / from Infinispan entries).
	 */
	private LDAPEntryTransformer ldapEntryTransformer;
	
	
	/**
	 * The optional LDAP search query executor.
	 */
	private LDAPQueryExecutor queryExecutor;


	/**
	 * The marshalled Infinispan entry factory.
	 */
	private MarshalledEntryFactory marshalledEntryFactory;


	/**
	 * Purges expired entries found in the LDAP store, as indicated by
	 * their persisted metadata (optional, may be ignored / not stored).
	 */
	private ExpiredEntryReaper reaper;


	/**
	 * Loads an LDAP entry transformer with the specified class name.
	 *
	 * @param className The class name. Must not be {@code null}.
	 *
	 * @return The LDAP entry transformer.
	 */
	@SuppressWarnings( "unchecked" )
	private LDAPEntryTransformer loadEntryTransformerClass(final String className) {

		try {
			Class> clazz = (Class>)Class.forName(className);
			return clazz.newInstance();
		} catch (Exception e) {
			throw new PersistenceException("Couldn't load LDAP entry transformer class: " + e.getMessage(), e);
		}
	}
	
	
	/**
	 * Loads an LDAP search query executor with the specified class name.
	 *
	 * @param className The class name. Must not be {@code null}.
	 *
	 * @return The LDAP search query executor.
	 */
	@SuppressWarnings( "unchecked" )
	private LDAPQueryExecutor loadQueryExecutorClass(final String className) {
		
		try {
			Class> clazz = (Class>)Class.forName(className);
			return clazz.newInstance();
		} catch (Exception e) {
			throw new PersistenceException("Couldn't load LDAP search query executor class: " + e.getMessage(), e);
		}
	}


	@Override
	@SuppressWarnings("unchecked")
	public void init(final InitializationContext ctx) {

		// This method will be invoked by the PersistenceManager during initialization. The InitializationContext
		// contains:
		// - this CacheLoader's configuration
		// - the cache to which this loader is applied. Your loader might want to use the cache's name to construct
		//   cache-specific identifiers
		// - the StreamingMarshaller that needs to be used to marshall/unmarshall the entries
		// - a TimeService which the loader can use to determine expired entries
		// - a ByteBufferFactory which needs to be used to construct ByteBuffers
		// - a MarshalledEntryFactory which needs to be used to construct entries from the data retrieved by the loader

		super.init(ctx);
		
		this.config = ctx.getConfiguration();

		Loggers.MAIN_LOG.info("[IL0201] LDAP store configuration properties for cache {}:", getCacheName());
		config.log();

		Loggers.MAIN_LOG.debug("[IL0202] Loading LDAP entry transformer class {} for cache {}...",
			config.ldapDirectory.entryTransformer,
			getCacheName());

		ldapEntryTransformer = loadEntryTransformerClass(config.ldapDirectory.entryTransformer);

		marshalledEntryFactory = (MarshalledEntryFactory)ctx.getMarshalledEntryFactory();

		Loggers.MAIN_LOG.info("[IL0203] Initialized LDAP external store for cache {}", getCacheName());
	}
	
	
	@Override
	public QueryExecutor getQueryExecutor() {
		
		return queryExecutor;
	}


	@Override
	public void start() {

		// This method will be invoked by the PersistenceManager to start the CacheLoader. At this stage configuration
		// is complete and the loader can perform operations such as opening a connection to the external storage,
		// initialize internal data structures, etc.

		if (ldapConnector != null) {
			throw new IllegalStateException("LDAP store connector already started");
		}

		ldapConnector = new LDAPConnector(
			config,
			getCacheName(),
			ldapEntryTransformer.getModifiableAttributes(),
			ldapEntryTransformer.includesAttributesWithOptions());

		Loggers.MAIN_LOG.info("[IL0204] Started LDAP external store connector for cache {}", getCacheName());

		reaper = new ExpiredEntryReaper<>(ldapConnector, ldapEntryTransformer);
		
		// Load and initialise the optional LDAP search query executor
		if (config.ldapDirectory.queryExecutor != null) {
			Loggers.MAIN_LOG.debug("[IL0210] Loading optional LDAP search query executor class {} for cache {}...",
				config.ldapDirectory.queryExecutor,
				getCacheName());
			
			queryExecutor = loadQueryExecutorClass(config.ldapDirectory.queryExecutor);
			
			queryExecutor.init(new LDAPQueryExecutorInitContext() {
				
				@Override
				public LDAPConnector getLDAPConnector() {
					return ldapConnector;
				}
				
				
				@Override
				public LDAPEntryTransformer getLDAPEntryTransformer() {
					return ldapEntryTransformer;
				}
			});
		}
	}


	@Override
	public void stop() {

		if (ldapConnector != null) {
			ldapConnector.shutdown();
			Loggers.MAIN_LOG.info("[IL0205] Stopped LDAP external store connector for cache {}", getCacheName());
		}
		
		super.stop();
	}


	@SuppressWarnings("unchecked")
	private K resolveKey(final Object key) {

		if (key instanceof byte[]) {
			throw new PersistenceException("Cannot resolve " + getCacheName() + " cache key from byte[], enable compatibility mode");
		}

		return (K)key;
	}


	@Override
	public boolean contains(final Object key) {

		// This method will be invoked by the PersistenceManager to determine if the loader contains the specified key.
		// The implementation should be as fast as possible, e.g. it should strive to transfer the least amount of data possible
		// from the external storage to perform the check. Also, if possible, make sure the field is indexed on the external storage
		// so that its existence can be determined as quickly as possible.
		//
		// Note that keys will be in the cache's native format, which means that if the cache is being used by a remoting protocol
		// such as HotRod or REST and compatibility mode has not been enabled, then they will be encoded in a byte[].

		Loggers.LDAP_LOG.trace("[IL0250] LDAP store: Checking {} cache key {}", getCacheName(), key);

		DN dn = new DN(ldapEntryTransformer.resolveRDN(resolveKey(key)), config.ldapDirectory.baseDN);

		return ldapConnector.entryExists(dn);
	}


	@Override
	@SuppressWarnings("unchecked")
	public MarshalledEntry load(final Object key) {

		// Fetches an entry from the storage using the specified key. The CacheLoader should retrieve from the external storage all
		// of the data that is needed to reconstruct the entry in memory, i.e. the value and optionally the metadata. This method
		// needs to return a MarshalledEntry which can be constructed as follows:
		//
		// ctx.getMarshalledEntryFactory().new MarshalledEntry(key, value, metadata);
		//
		// If the entry does not exist or has expired, this method should return null.
		// If an error occurs while retrieving data from the external storage, this method should throw a PersistenceException
		//
		// Note that keys and values will be in the cache's native format, which means that if the cache is being used by a remoting protocol
		// such as HotRod or REST and compatibility mode has not been enabled, then they will be encoded in a byte[].
		// If the loader needs to have knowledge of the key/value data beyond their binary representation, then it needs access to the key's and value's
		// classes and the marshaller used to encode them.

		Loggers.LDAP_LOG.trace("[IL0251] LDAP store: Loading {} cache entry with key {}", getCacheName(), key);

		DN dn = new DN(ldapEntryTransformer.resolveRDN(resolveKey(key)), config.ldapDirectory.baseDN);

		Loggers.LDAP_LOG.trace("[IL0257] LDAP store: Resolved DN {}", dn);

		ReadOnlyEntry ldapEntry = ldapConnector.retrieveEntry(dn);

		if (ldapEntry == null) {
			// Not found
			Loggers.LDAP_LOG.trace("[IL0258] LDAP store: Entry not found");
			return null;
		}


		if (Loggers.LDAP_LOG.isTraceEnabled()) {
			Loggers.LDAP_LOG.trace("[IL0259] LDAP store: Retrieved entry: {}", ldapEntry.toLDIFString());
		}

		// Transform LDAP entry to Infinispan entry
		InfinispanEntry infinispanEntry = ldapEntryTransformer.toInfinispanEntry(new LDAPEntry(ldapEntry));

		return marshalledEntryFactory.newMarshalledEntry(infinispanEntry.getKey(), infinispanEntry.getValue(), infinispanEntry.getMetadata());
	}


	@Override
	public boolean delete(final Object key) {

		// The CacheWriter should remove from the external storage the entry identified by the specified key.
		// Note that keys will be in the cache's native format, which means that if the cache is being used by a remoting protocol
		// such as HotRod or REST and compatibility mode has not been enabled, then they will be encoded in a byte[].

		Loggers.LDAP_LOG.trace("[IL0252] LDAP store: Deleting {} entry with key {}", getCacheName(), key);

		DN dn = new DN(ldapEntryTransformer.resolveRDN(resolveKey(key)), config.ldapDirectory.baseDN);

		return ldapConnector.deleteEntry(dn);
	}


	@Override
	public void write(final MarshalledEntry marshalledEntry) {

		// The CacheWriter should write the specified entry to the external storage.
		//
		// The PersistenceManager uses MarshalledEntry as the default format so that CacheWriters can efficiently store data coming
		// from a remote node, thus avoiding any additional transformation steps.
		//
		// Note that keys and values will be in the cache's native format, which means that if the cache is being used by a remoting protocol
		// such as HotRod or REST and compatibility mode has not been enabled, then they will be encoded in a byte[].

		Loggers.LDAP_LOG.trace("[IL0253] LDAP store: Writing {} entry {}", getCacheName(), marshalledEntry);

		LDAPEntry ldapEntry = ldapEntryTransformer.toLDAPEntry(
			config.ldapDirectory.baseDN,
			new InfinispanEntry<>(
				marshalledEntry.getKey(),
				marshalledEntry.getValue(),
				marshalledEntry.getMetadata()));

		// Resolve the LDAP write strategy
		LDAPWriteStrategy writeStrategy = ldapEntry.getWriteStrategy();

		if (writeStrategy != null) {
			Loggers.LDAP_LOG.trace("[IL0263] LDAP store: Entry transformer suggested {} write strategy", writeStrategy);
		} else {
			writeStrategy = LDAPWriteStrategy.getDefault();
			Loggers.LDAP_LOG.trace("[IL0264] LDAP store: Defaulted to {} write strategy", writeStrategy);
		}

		// Entry metadata created timestamp unreliable, cannot be used
		// as hint whether LDAP ADD or LDAP MODIFY should be attempted
		// first
		// InternalMetadataImpl{actual=EmbeddedExpirableMetadata{lifespan=-1, maxIdle=-1, version=null}, created=-1, lastUsed=-1}


		switch (writeStrategy) {

			case TRY_LDAP_ADD_FIRST:

				if (ldapConnector.addEntry(ldapEntry.getEntry())) {
					Loggers.LDAP_LOG.trace("[IL0256] LDAP store: Added new {} entry with DN {}", getCacheName(), ldapEntry.getEntry().getDN());
					return; // success
				}

				// Entry already exists, attempt LDAP modify
				if (ldapConnector.replaceEntry(ldapEntry.getEntry())) {
					Loggers.LDAP_LOG.trace("[IL0257] LDAP store: Replaced {} entry with DN {}", getCacheName(), ldapEntry.getEntry().getDN());
					return; // success
				}

				// Try to recover from concurrent LDAP delete
				// (entry deleted between first failed add and second modify attempt)
				if (! ldapConnector.addEntry(ldapEntry.getEntry())) {
					// This should be highly unlikely
					throw new PersistenceException("Failed recovery from concurrent LDAP delete (" + getCacheName() + " cache): " + ldapEntry.getEntry().getDN());
				}

				break;

			case TRY_LDAP_MODIFY_FIRST:

				if (ldapConnector.replaceEntry(ldapEntry.getEntry())) {
					Loggers.LDAP_LOG.trace("[IL0265] LDAP store: Replaced {} entry with DN {}", getCacheName(), ldapEntry.getEntry().getDN());
					return; // success
				}

				// Entry doesn't exist, try LDAP add
				if (ldapConnector.addEntry(ldapEntry.getEntry())) {
					Loggers.LDAP_LOG.trace("[IL0266] LDAP store: Added new {} entry with DN {}", getCacheName(), ldapEntry.getEntry().getDN());
					return; // success
				}

				// Try to recover from concurrent LDAP add
				// (entry added between first failed replace and second add attempt)
				if (! ldapConnector.replaceEntry(ldapEntry.getEntry())) {
					// This should be highly unlikely
					throw new PersistenceException("Failed recovery from concurrent LDAP add (" + getCacheName() + " cache): " + ldapEntry.getEntry().getDN());
				}

				break;

			default:
				throw new PersistenceException("Unexpected LDAP write strategy: " + writeStrategy);
		}
	}


	@Override
	public void process(final KeyFilter keyFilter,
			    final CacheLoaderTask cacheLoaderTask,
			    final Executor executor,
			    final boolean fetchValue,
			    final boolean fetchMetadata) {

		Loggers.LDAP_LOG.trace("[IL0262] LDAP store: Processing key filter for {} cache: fetchValue={} fetchMetadata=", getCacheName(), fetchValue, fetchMetadata);

		final TaskContext taskContext = new TaskContextImpl();

		// TODO consider multi-threaded LDAP retrieval?
		executor.execute(() -> ldapConnector.retrieveEntries(ldapEntry -> {

			// Retrieves entire entry, fetchValue / fetchMetadata params are ignored TODO consider

			if (taskContext.isStopped()) {
				// TODO Consider pushing task context to LDAP connector routine
				return;
			}

			InfinispanEntry infinispanEntry = ldapEntryTransformer.toInfinispanEntry(new LDAPEntry(ldapEntry));

			if (keyFilter.accept(infinispanEntry.getKey())) {

				MarshalledEntry marshalledEntry = marshalledEntryFactory.newMarshalledEntry(
					infinispanEntry.getKey(),
					infinispanEntry.getValue(),
					infinispanEntry.getMetadata());

				try {
					cacheLoaderTask.processEntry(marshalledEntry, taskContext);

				} catch (InterruptedException e) {
					throw new PersistenceException(e.getMessage(), e);
				}
			}
		}));
	}


	@Override
	public int size() {

		// Infinispan code analysis on 8.2 shows that this method is never called in practise, and
		// is not wired to the data / cache container API

		Loggers.LDAP_LOG.trace("[IL0258] LDAP store: Counting {} entries", getCacheName());

		final int count = ldapConnector.countEntries();

		Loggers.LDAP_LOG.trace("[IL0259] LDAP store: Counted {} {} entries", count, getCacheName());

		return count;
	}


	@Override
	public void clear() {

		Loggers.LDAP_LOG.trace("[IL0260] LDAP store: Clearing {} entries", getCacheName());

		int numDeleted = ldapConnector.deleteEntries();

		Loggers.LDAP_LOG.debug("[IL0254] LDAP store: Cleared {} {} entries", numDeleted, getCacheName());
	}


	@Override
	public void purge(final Executor executor, final PurgeListener purgeListener) {

		Loggers.LDAP_LOG.trace("[IL0261] LDAP store: Purging {} entries", getCacheName());

		final AtomicInteger numPurged = new AtomicInteger();

		executor.execute(() -> numPurged.set(reaper.purge(purgeListener)));

		Loggers.LDAP_LOG.debug("[IL0255] LDAP store: Purged {} expired {} entries", numPurged.get(), getCacheName());
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy