DATALDAP-5 - Add CDI extension for LDAP repositories.

We now provide a CDI extension to create LDAP repositories in a CDI container. LDAP repositories require a LdapOperations bean as underlying resource manager for LDAP access.

class LdapTemplateProducer {

  @Produces
  @Singleton
  public LdapTemplate createLdapTemplate() {

    LdapTemplate ldapTemplateMock = …
    return ldapTemplateMock;
  }
}

class RepositoryClient {

  @Inject
  PersonRepository repository;
}
This commit is contained in:
Mark Paluch
2017-11-09 14:18:15 +01:00
parent 1fa3a279d7
commit a5810ffa0f
13 changed files with 509 additions and 0 deletions

38
pom.xml
View File

@@ -86,6 +86,44 @@
<scope>provided</scope>
</dependency>
<!-- CDI -->
<!-- Dependency order required to build against CDI 1.0 and test with CDI 2.0 -->
<dependency>
<groupId>org.apache.geronimo.specs</groupId>
<artifactId>geronimo-jcdi_2.0_spec</artifactId>
<version>1.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
<version>1.2.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.enterprise</groupId>
<artifactId>cdi-api</artifactId>
<version>${cdi}</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>${javax-annotation-api}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.openwebbeans</groupId>
<artifactId>openwebbeans-se</artifactId>
<version>${webbeans}</version>
<scope>test</scope>
</dependency>
</dependencies>
<repositories>

View File

@@ -1,6 +1,10 @@
[[new-features]]
= New & Noteworthy
[[new-features.2.1]]
== What's new in Spring Data LDAP 2.1
* CDI extension to create LDAP repositories within a CDI container.
[[new-features.2.0]]
== What's new in Spring Data LDAP 2.0
* Enhanced tooling support by using Spring Framework's `@NonNullApi` and `@Nullable` annotations.

View File

@@ -229,3 +229,39 @@ Basic QueryDSL support is included in Spring LDAP. This support includes the fol
* A Query implementation, `QueryDslLdapQuery`, for building and executing QueryDSL queries in code.
* Spring Data repository support for QueryDSL predicates. `QueryDslPredicateExecutor` includes a number of additional methods with appropriate parameters; extend this interface along with `LdapRepository` to include this support in your repository.
[[ldap.repositories.misc]]
== Miscellaneous
[[ldap.repositories.misc.cdi-integration]]
=== CDI Integration
Instances of the repository interfaces are usually created by a container, which Spring is the most natural choice when working with Spring Data. As of version 2.1 Spring Data LDAP ships with a custom CDI extension that allows using the repository abstraction in CDI environments. The extension is part of the JAR so all you need to do to activate it is dropping the Spring Data LDAP JAR into your classpath. You can now set up the infrastructure by implementing a CDI Producer for the `LdapTemplate`:
[source,java]
----
class LdapTemplateProducer {
@Produces
@ApplicationScoped
public LdapOperations createLdapTemplate() {
ContextSource contextSource = …
return new LdapTemplate(contextSource);
}
}
----
The Spring Data LDAP CDI extension will pick up the `LdapTemplate` available as CDI bean and create a proxy for a Spring Data repository whenever a bean of a repository type is requested by the container. Thus obtaining an instance of a Spring Data repository is a matter of declaring an `@Inject`-ed property:
[source,java]
----
class RepositoryClient {
@Inject
PersonRepository repository;
public void businessMethod() {
List<Person> people = repository.findAll();
}
}
----

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed 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.springframework.data.ldap.repository.cdi;
import java.lang.annotation.Annotation;
import java.util.Optional;
import java.util.Set;
import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import org.springframework.data.ldap.repository.support.LdapRepositoryFactory;
import org.springframework.data.repository.cdi.CdiRepositoryBean;
import org.springframework.data.repository.config.CustomRepositoryImplementationDetector;
import org.springframework.ldap.core.LdapOperations;
import org.springframework.util.Assert;
/**
* {@link CdiRepositoryBean} to create LDAP repository instances.
*
* @author Mark Paluch
* @since 2.1
*/
public class LdapRepositoryBean<T> extends CdiRepositoryBean<T> {
private final Bean<LdapOperations> operations;
/**
* Creates a new {@link LdapRepositoryBean}.
*
* @param operations must not be {@literal null}.
* @param qualifiers must not be {@literal null}.
* @param repositoryType must not be {@literal null}.
* @param beanManager must not be {@literal null}.
* @param detector detector for the custom {@link org.springframework.data.repository.Repository} implementations
* {@link CustomRepositoryImplementationDetector}, can be {@link Optional#empty()}.
*/
LdapRepositoryBean(Bean<LdapOperations> operations, Set<Annotation> qualifiers, Class<T> repositoryType,
BeanManager beanManager, Optional<CustomRepositoryImplementationDetector> detector) {
super(qualifiers, repositoryType, beanManager, detector);
Assert.notNull(operations, "LdapOperations bean must not be null!");
this.operations = operations;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.cdi.CdiRepositoryBean#create(javax.enterprise.context.spi.CreationalContext, java.lang.Class)
*/
@Override
protected T create(CreationalContext<T> creationalContext, Class<T> repositoryType,
Optional<Object> customImplementation) {
LdapOperations ldapOperations = getDependencyInstance(operations, LdapOperations.class);
LdapRepositoryFactory factory = new LdapRepositoryFactory(ldapOperations);
return customImplementation.map(o -> factory.getRepository(repositoryType, o))
.orElseGet(() -> factory.getRepository(repositoryType));
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed 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.springframework.data.ldap.repository.cdi;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.UnsatisfiedResolutionException;
import javax.enterprise.inject.spi.AfterBeanDiscovery;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.ProcessBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.repository.cdi.CdiRepositoryBean;
import org.springframework.data.repository.cdi.CdiRepositoryExtensionSupport;
import org.springframework.ldap.core.LdapOperations;
/**
* CDI extension to export LDAP repositories.
*
* @author Mark Paluch
* @since 2.1
*/
public class LdapRepositoryExtension extends CdiRepositoryExtensionSupport {
private static final Logger LOG = LoggerFactory.getLogger(LdapRepositoryExtension.class);
private final Map<Set<Annotation>, Bean<LdapOperations>> ldapOperations = new HashMap<>();
public LdapRepositoryExtension() {
LOG.info("Activating CDI extension for Spring Data LDAP repositories.");
}
@SuppressWarnings("unchecked")
<X> void processBean(@Observes ProcessBean<X> processBean) {
Bean<X> bean = processBean.getBean();
for (Type type : bean.getTypes()) {
if (type instanceof Class<?> && LdapOperations.class.isAssignableFrom((Class<?>) type)) {
if (LOG.isDebugEnabled()) {
LOG.debug(
String.format("Discovered %s with qualifiers %s.", LdapOperations.class.getName(), bean.getQualifiers()));
}
// Store the EntityManager bean using its qualifiers.
ldapOperations.put(new HashSet<>(bean.getQualifiers()), (Bean<LdapOperations>) bean);
}
}
}
void afterBeanDiscovery(@Observes AfterBeanDiscovery afterBeanDiscovery, BeanManager beanManager) {
for (Entry<Class<?>, Set<Annotation>> entry : getRepositoryTypes()) {
Class<?> repositoryType = entry.getKey();
Set<Annotation> qualifiers = entry.getValue();
// Create the bean representing the repository.
CdiRepositoryBean<?> repositoryBean = createRepositoryBean(repositoryType, qualifiers, beanManager);
if (LOG.isInfoEnabled()) {
LOG.info(String.format("Registering bean for %s with qualifiers %s.", repositoryType.getName(), qualifiers));
}
// Register the bean to the container.
registerBean(repositoryBean);
afterBeanDiscovery.addBean(repositoryBean);
}
}
/**
* Creates a {@link CdiRepositoryBean} for the repository of the given type.
*
* @param <T> the type of the repository.
* @param repositoryType the class representing the repository.
* @param qualifiers the qualifiers to be applied to the bean.
* @param beanManager the BeanManager instance.
* @return the repository bean.
*/
private <T> CdiRepositoryBean<T> createRepositoryBean(Class<T> repositoryType, Set<Annotation> qualifiers,
BeanManager beanManager) {
// Determine the LdapOperations bean which matches the qualifiers of the repository.
Bean<LdapOperations> LdapOperations = this.ldapOperations.get(qualifiers);
if (LdapOperations == null) {
throw new UnsatisfiedResolutionException(String.format("Unable to resolve a bean for '%s' with qualifiers %s.",
LdapOperations.class.getName(), qualifiers));
}
// Construct and return the repository bean.
return new LdapRepositoryBean<>(LdapOperations, qualifiers, repositoryType, beanManager,
Optional.of(getCustomImplementationDetector()));
}
}

View File

@@ -0,0 +1,5 @@
/**
* CDI support for LDAP specific repository implementation.
*/
@org.springframework.lang.NonNullApi
package org.springframework.data.ldap.repository.cdi;

View File

@@ -0,0 +1 @@
org.springframework.data.ldap.repository.cdi.LdapRepositoryExtension

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed 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.springframework.data.ldap.repository.cdi;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.Collections;
import javax.enterprise.inject.se.SeContainer;
import javax.enterprise.inject.se.SeContainerInitializer;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.data.ldap.config.DummyEntity;
import org.springframework.ldap.core.LdapTemplate;
/**
* Integration tests for {@link LdapRepositoryExtension}.
*
* @author Mark Paluch
*/
public class CdiExtensionIntegrationTests {
static SeContainer container;
@BeforeClass
public static void setUp() {
container = SeContainerInitializer.newInstance() //
.disableDiscovery() //
.addPackages(CdiExtensionIntegrationTests.class) //
.initialize();
}
@AfterClass
public static void tearDown() {
container.close();
}
@Test // DATALDAP-5
public void bootstrapsRepositoryCorrectly() {
RepositoryClient client = container.select(RepositoryClient.class).get();
LdapTemplate ldapTemplateMock = client.getLdapTemplate();
DummyEntity entity = new DummyEntity();
when(ldapTemplateMock.findAll(DummyEntity.class)).thenReturn(Collections.singletonList(entity));
SampleRepository repository = client.getSampleRepository();
assertThat(repository).isNotNull();
repository.deleteAll();
verify(client.getLdapTemplate()).delete(entity);
}
@Test // DATALDAP-5
public void returnOneFromCustomImpl() {
RepositoryClient repositoryConsumer = container.select(RepositoryClient.class).get();
assertThat(repositoryConsumer.getSampleRepository().returnOne()).isEqualTo(1);
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed 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.springframework.data.ldap.repository.cdi;
import static org.mockito.Mockito.*;
import javax.enterprise.inject.Produces;
import javax.inject.Singleton;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.odm.core.ObjectDirectoryMapper;
/**
* Simple component exposing a {@link LdapTemplate} instance as CDI bean.
*
* @author Mark Paluch
*/
class LdapTemplateProducer {
@Produces
@Singleton
public LdapTemplate createLdapTemplate() {
LdapTemplate ldapTemplateMock = mock(LdapTemplate.class);
ObjectDirectoryMapper odmMock = mock(ObjectDirectoryMapper.class);
when(ldapTemplateMock.getObjectDirectoryMapper()).thenReturn(odmMock);
return ldapTemplateMock;
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed 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.springframework.data.ldap.repository.cdi;
import lombok.Getter;
import javax.inject.Inject;
import org.springframework.ldap.core.LdapTemplate;
/**
* @author Mark Paluch
*/
@Getter
class RepositoryClient {
@Inject SampleRepository sampleRepository;
@Inject LdapTemplate ldapTemplate;
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed 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.springframework.data.ldap.repository.cdi;
import javax.naming.Name;
import org.springframework.data.ldap.config.DummyEntity;
import org.springframework.data.repository.CrudRepository;
/**
* @author Mark Paluch
*/
public interface SampleRepository extends CrudRepository<DummyEntity, Name>, SampleRepositoryCustom {}

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed 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.springframework.data.ldap.repository.cdi;
/**
* @author Mark Paluch
*/
interface SampleRepositoryCustom {
int returnOne();
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed 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.springframework.data.ldap.repository.cdi;
/**
* @author Mark Paluch
*/
class SampleRepositoryImpl implements SampleRepositoryCustom {
@Override
public int returnOne() {
return 1;
}
}