diff --git a/pom.xml b/pom.xml index 700ba48..4fea2a2 100644 --- a/pom.xml +++ b/pom.xml @@ -86,6 +86,44 @@ provided + + + + org.apache.geronimo.specs + geronimo-jcdi_2.0_spec + 1.0.1 + test + + + + javax.interceptor + javax.interceptor-api + 1.2.1 + test + + + + javax.enterprise + cdi-api + ${cdi} + provided + true + + + + javax.annotation + javax.annotation-api + ${javax-annotation-api} + test + + + + org.apache.openwebbeans + openwebbeans-se + ${webbeans} + test + + diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 0221325..817f8e8 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -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. diff --git a/src/main/asciidoc/reference/ldap-repositories.adoc b/src/main/asciidoc/reference/ldap-repositories.adoc index 17a667b..8e63a6b 100644 --- a/src/main/asciidoc/reference/ldap-repositories.adoc +++ b/src/main/asciidoc/reference/ldap-repositories.adoc @@ -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 people = repository.findAll(); + } +} +---- diff --git a/src/main/java/org/springframework/data/ldap/repository/cdi/LdapRepositoryBean.java b/src/main/java/org/springframework/data/ldap/repository/cdi/LdapRepositoryBean.java new file mode 100644 index 0000000..6c5f29f --- /dev/null +++ b/src/main/java/org/springframework/data/ldap/repository/cdi/LdapRepositoryBean.java @@ -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 extends CdiRepositoryBean { + + private final Bean 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 operations, Set qualifiers, Class repositoryType, + BeanManager beanManager, Optional 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 creationalContext, Class repositoryType, + Optional customImplementation) { + + LdapOperations ldapOperations = getDependencyInstance(operations, LdapOperations.class); + LdapRepositoryFactory factory = new LdapRepositoryFactory(ldapOperations); + + return customImplementation.map(o -> factory.getRepository(repositoryType, o)) + .orElseGet(() -> factory.getRepository(repositoryType)); + } +} diff --git a/src/main/java/org/springframework/data/ldap/repository/cdi/LdapRepositoryExtension.java b/src/main/java/org/springframework/data/ldap/repository/cdi/LdapRepositoryExtension.java new file mode 100644 index 0000000..389f7c1 --- /dev/null +++ b/src/main/java/org/springframework/data/ldap/repository/cdi/LdapRepositoryExtension.java @@ -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, Bean> ldapOperations = new HashMap<>(); + + public LdapRepositoryExtension() { + LOG.info("Activating CDI extension for Spring Data LDAP repositories."); + } + + @SuppressWarnings("unchecked") + void processBean(@Observes ProcessBean processBean) { + + Bean 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) bean); + } + } + } + + void afterBeanDiscovery(@Observes AfterBeanDiscovery afterBeanDiscovery, BeanManager beanManager) { + + for (Entry, Set> entry : getRepositoryTypes()) { + + Class repositoryType = entry.getKey(); + Set 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 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 CdiRepositoryBean createRepositoryBean(Class repositoryType, Set qualifiers, + BeanManager beanManager) { + + // Determine the LdapOperations bean which matches the qualifiers of the repository. + Bean 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())); + } +} diff --git a/src/main/java/org/springframework/data/ldap/repository/cdi/package-info.java b/src/main/java/org/springframework/data/ldap/repository/cdi/package-info.java new file mode 100644 index 0000000..90e60bc --- /dev/null +++ b/src/main/java/org/springframework/data/ldap/repository/cdi/package-info.java @@ -0,0 +1,5 @@ +/** + * CDI support for LDAP specific repository implementation. + */ +@org.springframework.lang.NonNullApi +package org.springframework.data.ldap.repository.cdi; diff --git a/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension b/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension new file mode 100644 index 0000000..e969cc7 --- /dev/null +++ b/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension @@ -0,0 +1 @@ +org.springframework.data.ldap.repository.cdi.LdapRepositoryExtension diff --git a/src/test/java/org/springframework/data/ldap/repository/cdi/CdiExtensionIntegrationTests.java b/src/test/java/org/springframework/data/ldap/repository/cdi/CdiExtensionIntegrationTests.java new file mode 100644 index 0000000..c2efe30 --- /dev/null +++ b/src/test/java/org/springframework/data/ldap/repository/cdi/CdiExtensionIntegrationTests.java @@ -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); + } +} diff --git a/src/test/java/org/springframework/data/ldap/repository/cdi/LdapTemplateProducer.java b/src/test/java/org/springframework/data/ldap/repository/cdi/LdapTemplateProducer.java new file mode 100644 index 0000000..30995c4 --- /dev/null +++ b/src/test/java/org/springframework/data/ldap/repository/cdi/LdapTemplateProducer.java @@ -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; + } +} diff --git a/src/test/java/org/springframework/data/ldap/repository/cdi/RepositoryClient.java b/src/test/java/org/springframework/data/ldap/repository/cdi/RepositoryClient.java new file mode 100644 index 0000000..ff74a11 --- /dev/null +++ b/src/test/java/org/springframework/data/ldap/repository/cdi/RepositoryClient.java @@ -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; +} diff --git a/src/test/java/org/springframework/data/ldap/repository/cdi/SampleRepository.java b/src/test/java/org/springframework/data/ldap/repository/cdi/SampleRepository.java new file mode 100644 index 0000000..7b108b0 --- /dev/null +++ b/src/test/java/org/springframework/data/ldap/repository/cdi/SampleRepository.java @@ -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, SampleRepositoryCustom {} diff --git a/src/test/java/org/springframework/data/ldap/repository/cdi/SampleRepositoryCustom.java b/src/test/java/org/springframework/data/ldap/repository/cdi/SampleRepositoryCustom.java new file mode 100644 index 0000000..ff12c7d --- /dev/null +++ b/src/test/java/org/springframework/data/ldap/repository/cdi/SampleRepositoryCustom.java @@ -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(); +} diff --git a/src/test/java/org/springframework/data/ldap/repository/cdi/SampleRepositoryImpl.java b/src/test/java/org/springframework/data/ldap/repository/cdi/SampleRepositoryImpl.java new file mode 100644 index 0000000..18f70a3 --- /dev/null +++ b/src/test/java/org/springframework/data/ldap/repository/cdi/SampleRepositoryImpl.java @@ -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; + } +}