From d3bced0f860439a23ed4d6e69ea5d603073a57fd Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Sat, 3 Dec 2011 11:55:57 +0100 Subject: [PATCH] DATAJPA-123 - Added classpath-scanning PersistenceUnitPostProcessor. Added ClasspathScanningPersistenceUnitPostProcessor that will scan the configured base package for classes annotated with @Entity or @MappedSuperclass and add them to the PersistenceUnit handled. Beyond that it will scan for JPA XML mapping files if an optional mapping file name pattern is configured on the PUPP instance. --- ...hScanningPersistenceUnitPostProcessor.java | 144 ++++++++++++++++++ ...PersistenceUnitPostProcessorUnitTests.java | 95 ++++++++++++ .../data/jpa/support/mapping.xml | 1 + 3 files changed, 240 insertions(+) create mode 100644 src/main/java/org/springframework/data/jpa/support/ClasspathScanningPersistenceUnitPostProcessor.java create mode 100644 src/test/java/org/springframework/data/jpa/support/ClasspathScanningPersistenceUnitPostProcessorUnitTests.java create mode 100644 src/test/resources/org/springframework/data/jpa/support/mapping.xml diff --git a/src/main/java/org/springframework/data/jpa/support/ClasspathScanningPersistenceUnitPostProcessor.java b/src/main/java/org/springframework/data/jpa/support/ClasspathScanningPersistenceUnitPostProcessor.java new file mode 100644 index 000000000..b4a2d275a --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/support/ClasspathScanningPersistenceUnitPostProcessor.java @@ -0,0 +1,144 @@ +/* + * Copyright 2011 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.jpa.support; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import javax.persistence.Entity; +import javax.persistence.MappedSuperclass; +import javax.persistence.PersistenceUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.orm.jpa.persistenceunit.MutablePersistenceUnitInfo; +import org.springframework.orm.jpa.persistenceunit.PersistenceUnitPostProcessor; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link PersistenceUnitPostProcessor} that will scan for classes annotated with {@link Entity} or + * {@link MappedSuperclass} and add them to the {@link PersistenceUnit} post prcessed. Beyond that JPA XML mapping files + * can be scanned as well by configuring a file name pattern. + * + * @author Oliver Gierke + */ +public class ClasspathScanningPersistenceUnitPostProcessor implements PersistenceUnitPostProcessor, ResourceLoaderAware { + + private static final Logger LOG = LoggerFactory.getLogger(ClasspathScanningPersistenceUnitPostProcessor.class); + + private final String basePackage; + private ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(getClass().getClassLoader()); + private String mappingFileNamePattern; + + /** + * Creates a new {@link ClasspathScanningPersistenceUnitPostProcessor} using the given base package as scan base. + * + * @param basePackage must not be {@literal null} or empty. + */ + public ClasspathScanningPersistenceUnitPostProcessor(String basePackage) { + Assert.hasText(basePackage); + this.basePackage = basePackage; + } + + /** + * Configures the file name pattern JPA entity mapping files shall scanned from the classpath. Lookup will use the + * configured base package as root. + * + * @param mappingFileNamePattern must not be {@literal null} or empty. + */ + public void setMappingFileNamePattern(String mappingFilePattern) { + Assert.hasText(mappingFilePattern); + this.mappingFileNamePattern = mappingFilePattern; + } + + /* + * (non-Javadoc) + * @see org.springframework.context.ResourceLoaderAware#setResourceLoader(org.springframework.core.io.ResourceLoader) + */ + public void setResourceLoader(ResourceLoader resourceLoader) { + Assert.notNull(resourceLoader); + this.resolver = new PathMatchingResourcePatternResolver(resourceLoader); + } + + /* + * (non-Javadoc) + * @see org.springframework.orm.jpa.persistenceunit.PersistenceUnitPostProcessor#postProcessPersistenceUnitInfo(org.springframework.orm.jpa.persistenceunit.MutablePersistenceUnitInfo) + */ + public void postProcessPersistenceUnitInfo(MutablePersistenceUnitInfo pui) { + + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.addIncludeFilter(new AnnotationTypeFilter(Entity.class)); + provider.addIncludeFilter(new AnnotationTypeFilter(MappedSuperclass.class)); + + for (BeanDefinition definition : provider.findCandidateComponents(basePackage)) { + + LOG.debug("Registering classpath-scanned entity %s in persistence unit info!", definition.getBeanClassName()); + pui.addManagedClassName(definition.getBeanClassName()); + } + + for (String location : scanForMappingFileLocations()) { + LOG.debug("Registering classpath-scanned entity mapping file in persistence unit info!", location); + pui.addMappingFileName(location); + } + + } + + /** + * Scanes the configured base package for files matching the configured mapping file name pattern. Will simply return + * an empty {@link Set} in case no {@link ResourceLoader} or mapping file name pattern was configured. + * + * @return + */ + private Set scanForMappingFileLocations() { + + if (resolver == null || !StringUtils.hasText(mappingFileNamePattern)) { + return Collections.emptySet(); + } + + String path = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + basePackage.replace('.', File.separatorChar) + + File.separator + mappingFileNamePattern; + Set mappingFileUris = new HashSet(); + Resource[] scannedResources = new Resource[0]; + + try { + scannedResources = resolver.getResources(path); + } catch (IOException e) { + throw new IllegalStateException(String.format("Cannot load mapping files from path %s!", path), e); + } + + for (Resource resource : scannedResources) { + try { + mappingFileUris.add(resource.getURI().toString()); + } catch (IOException e) { + throw new IllegalStateException(String.format("Couldn't get URI for %s!", resource.toString(), e)); + } + } + + return mappingFileUris; + } +} diff --git a/src/test/java/org/springframework/data/jpa/support/ClasspathScanningPersistenceUnitPostProcessorUnitTests.java b/src/test/java/org/springframework/data/jpa/support/ClasspathScanningPersistenceUnitPostProcessorUnitTests.java new file mode 100644 index 000000000..963e0a0f2 --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/support/ClasspathScanningPersistenceUnitPostProcessorUnitTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2011 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.jpa.support; + +import static org.mockito.Mockito.*; + +import javax.persistence.Entity; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.orm.jpa.persistenceunit.MutablePersistenceUnitInfo; +import org.springframework.orm.jpa.persistenceunit.PersistenceUnitPostProcessor; + +/** + * Unit tests for {@link ClasspathScanningPersistenceUnitPostProcessor}. + * + * @author Oliver Gierke + */ +@RunWith(MockitoJUnitRunner.class) +public class ClasspathScanningPersistenceUnitPostProcessorUnitTests { + + @Mock + MutablePersistenceUnitInfo pui; + String basePackage = getClass().getPackage().getName(); + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullBasePackage() { + new ClasspathScanningPersistenceUnitPostProcessor(null); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsEmptyBasePackage() { + new ClasspathScanningPersistenceUnitPostProcessor(""); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullMappingFileNamePattern() { + ClasspathScanningPersistenceUnitPostProcessor processor = new ClasspathScanningPersistenceUnitPostProcessor( + basePackage); + processor.setMappingFileNamePattern(null); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsEmptyMappingFileNamePattern() { + ClasspathScanningPersistenceUnitPostProcessor processor = new ClasspathScanningPersistenceUnitPostProcessor( + basePackage); + processor.setMappingFileNamePattern(""); + } + + @Test + public void findsEntityClassesForBasePackage() { + + PersistenceUnitPostProcessor processor = new ClasspathScanningPersistenceUnitPostProcessor(basePackage); + processor.postProcessPersistenceUnitInfo(pui); + + verify(pui).addManagedClassName(SampleEntity.class.getName()); + } + + @Test + public void findsMappingFile() { + + DefaultResourceLoader loader = new DefaultResourceLoader(); + String expected = getClass().getResource("mapping.xml").toString(); + + ClasspathScanningPersistenceUnitPostProcessor processor = new ClasspathScanningPersistenceUnitPostProcessor( + basePackage); + processor.setMappingFileNamePattern("*.xml"); + processor.setResourceLoader(loader); + processor.postProcessPersistenceUnitInfo(pui); + + verify(pui).addManagedClassName(SampleEntity.class.getName()); + verify(pui).addMappingFileName(expected); + } + + @Entity + public static class SampleEntity { + + } +} diff --git a/src/test/resources/org/springframework/data/jpa/support/mapping.xml b/src/test/resources/org/springframework/data/jpa/support/mapping.xml new file mode 100644 index 000000000..12bbf745c --- /dev/null +++ b/src/test/resources/org/springframework/data/jpa/support/mapping.xml @@ -0,0 +1 @@ +