org.springframework
spring-test
diff --git a/spring-boot/src/main/java/org/springframework/boot/orm/jpa/EntityScan.java b/spring-boot/src/main/java/org/springframework/boot/orm/jpa/EntityScan.java
new file mode 100644
index 00000000000..3cb01309746
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/orm/jpa/EntityScan.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2012-2014 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.boot.orm.jpa;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.context.annotation.Import;
+import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+
+/**
+ * Configures the {@link LocalContainerEntityManagerFactoryBean} to to scan for entity
+ * classes in the classpath. This annotation provides an alternative to manually setting
+ * {@link LocalContainerEntityManagerFactoryBean#setPackagesToScan(String...)} and is
+ * particularly useful if you want to configure entity scanning in a type-safe way, or if
+ * your {@link LocalContainerEntityManagerFactoryBean} is auto-configured.
+ *
+ * A {@link LocalContainerEntityManagerFactoryBean} must be configured within your Spring
+ * ApplicationContext in order to use entity scanning. Furthermore, any existing
+ * {@code packagesToScan} setting will be replaced.
+ *
+ * One of {@link #basePackageClasses()}, {@link #basePackages()} or its alias
+ * {@link #value()} may be specified to define specific packages to scan. If specific
+ * packages are not defined scanning will occur from the package of the class with this
+ * annotation.
+ *
+ * @author Phillip Webb
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Import(EntityScanRegistrar.class)
+public @interface EntityScan {
+
+ /**
+ * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation
+ * declarations e.g.: {@code @EntityScan("org.my.pkg")} instead of
+ * {@code @EntityScan(basePackages="org.my.pkg")}.
+ */
+ String[] value() default {};
+
+ /**
+ * Base packages to scan for annotated entities.
+ *
+ * {@link #value()} is an alias for (and mutually exclusive with) this attribute.
+ *
+ * Use {@link #basePackageClasses()} for a type-safe alternative to String-based
+ * package names.
+ */
+ String[] basePackages() default {};
+
+ /**
+ * Type-safe alternative to {@link #basePackages()} for specifying the packages to
+ * scan for annotated entities. The package of each class specified will be scanned.
+ *
+ * Consider creating a special no-op marker class or interface in each package that
+ * serves no purpose other than being referenced by this attribute.
+ */
+ Class>[] basePackageClasses() default {};
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/orm/jpa/EntityScanRegistrar.java b/spring-boot/src/main/java/org/springframework/boot/orm/jpa/EntityScanRegistrar.java
new file mode 100644
index 00000000000..07477ddd8cf
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/orm/jpa/EntityScanRegistrar.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2012-2014 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.boot.orm.jpa;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.beans.factory.support.GenericBeanDefinition;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.core.annotation.AnnotationAttributes;
+import org.springframework.core.type.AnnotationMetadata;
+import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.ObjectUtils;
+
+/**
+ * {@link ImportBeanDefinitionRegistrar} used by {@link EntityScan}.
+ *
+ * @author Phillip Webb
+ */
+class EntityScanRegistrar implements ImportBeanDefinitionRegistrar {
+
+ private static final String BEAN_NAME = "entityScanBeanPostProcessor";
+
+ @Override
+ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
+ BeanDefinitionRegistry registry) {
+ if (!registry.containsBeanDefinition(BEAN_NAME)) {
+ GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
+ beanDefinition.setBeanClass(EntityScanBeanPostProcessor.class);
+ beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(
+ getPackagesToScan(importingClassMetadata));
+ registry.registerBeanDefinition(BEAN_NAME, beanDefinition);
+ }
+ }
+
+ private String[] getPackagesToScan(AnnotationMetadata metadata) {
+ AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata
+ .getAnnotationAttributes(EntityScan.class.getName()));
+ String[] value = attributes.getStringArray("value");
+ String[] basePackages = attributes.getStringArray("basePackages");
+ Class>[] basePackageClasses = attributes.getClassArray("basePackageClasses");
+
+ if (!ObjectUtils.isEmpty(value)) {
+ Assert.state(ObjectUtils.isEmpty(basePackages),
+ "@EntityScan basePackages and value attributes are mutually exclusive");
+ }
+
+ Set packagesToScan = new LinkedHashSet();
+ packagesToScan.addAll(Arrays.asList(value));
+ packagesToScan.addAll(Arrays.asList(basePackages));
+ for (Class> basePackageClass : basePackageClasses) {
+ packagesToScan.add(ClassUtils.getPackageName(basePackageClass));
+ }
+ if (packagesToScan.isEmpty()) {
+ return new String[] { ClassUtils.getPackageName(metadata.getClassName()) };
+ }
+ return new ArrayList(packagesToScan).toArray(new String[packagesToScan
+ .size()]);
+ }
+
+ /**
+ * {@link BeanPostProcessor} to set
+ * {@link LocalContainerEntityManagerFactoryBean#setPackagesToScan(String...)} based
+ * on an {@link EntityScan} annotation.
+ */
+ static class EntityScanBeanPostProcessor implements BeanPostProcessor,
+ ApplicationListener {
+
+ private final String[] packagesToScan;
+
+ private boolean processed;
+
+ public EntityScanBeanPostProcessor(String[] packagesToScan) {
+ this.packagesToScan = packagesToScan;
+ }
+
+ @Override
+ public Object postProcessBeforeInitialization(Object bean, String beanName)
+ throws BeansException {
+ if (bean instanceof LocalContainerEntityManagerFactoryBean) {
+ LocalContainerEntityManagerFactoryBean factoryBean = (LocalContainerEntityManagerFactoryBean) bean;
+ factoryBean.setPackagesToScan(this.packagesToScan);
+ this.processed = true;
+ }
+ return bean;
+ }
+
+ @Override
+ public Object postProcessAfterInitialization(Object bean, String beanName)
+ throws BeansException {
+ return bean;
+ }
+
+ @Override
+ public void onApplicationEvent(ContextRefreshedEvent event) {
+ Assert.state(this.processed, "Unable to configure "
+ + "LocalContainerEntityManagerFactoryBean from @EntityScan, "
+ + "ensure an appropriate bean is registered.");
+ }
+ }
+}
diff --git a/spring-boot/src/test/java/org/springframework/boot/orm/jpa/EntityScanTests.java b/spring-boot/src/test/java/org/springframework/boot/orm/jpa/EntityScanTests.java
new file mode 100644
index 00000000000..c0fe02df520
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/orm/jpa/EntityScanTests.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2012-2014 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.boot.orm.jpa;
+
+import javax.persistence.EntityManagerFactory;
+import javax.persistence.PersistenceException;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests for {@link EntityScan}.
+ *
+ * @author Phillip Webb
+ */
+public class EntityScanTests {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private AnnotationConfigApplicationContext context;
+
+ @Test
+ public void testValue() throws Exception {
+ this.context = new AnnotationConfigApplicationContext(ValueConfig.class);
+ assertSetPackagesToScan("com.mycorp.entity");
+ }
+
+ @Test
+ public void basePackages() throws Exception {
+ this.context = new AnnotationConfigApplicationContext(BasePackagesConfig.class);
+ assertSetPackagesToScan("com.mycorp.entity2");
+ }
+
+ @Test
+ public void basePackageClasses() throws Exception {
+ this.context = new AnnotationConfigApplicationContext(
+ BasePackageClassesConfig.class);
+ assertSetPackagesToScan(getClass().getPackage().getName());
+ }
+
+ @Test
+ public void fromConfigurationClass() throws Exception {
+ this.context = new AnnotationConfigApplicationContext(FromConfigConfig.class);
+ assertSetPackagesToScan(getClass().getPackage().getName());
+ }
+
+ @Test
+ public void valueAndBasePackagesThrows() throws Exception {
+ this.thrown.expect(IllegalStateException.class);
+ this.thrown.expectMessage("@EntityScan basePackages and value "
+ + "attributes are mutually exclusive");
+ this.context = new AnnotationConfigApplicationContext(ValueAndBasePackages.class);
+ }
+
+ @Test
+ public void valueAndBasePackageClassesMerges() throws Exception {
+ this.context = new AnnotationConfigApplicationContext(
+ ValueAndBasePackageClasses.class);
+ assertSetPackagesToScan("com.mycorp.entity", getClass().getPackage().getName());
+ }
+
+ @Test
+ public void basePackageAndBasePackageClassesMerges() throws Exception {
+ this.context = new AnnotationConfigApplicationContext(
+ BasePackagesAndBasePackageClasses.class);
+ assertSetPackagesToScan("com.mycorp.entity2", getClass().getPackage().getName());
+ }
+
+ @Test
+ public void needsEntityManageFactory() throws Exception {
+ this.thrown.expect(IllegalStateException.class);
+ this.thrown.expectMessage("Unable to configure "
+ + "LocalContainerEntityManagerFactoryBean from @EntityScan, "
+ + "ensure an appropriate bean is registered.");
+ this.context = new AnnotationConfigApplicationContext(MissingEntityManager.class);
+ }
+
+ private void assertSetPackagesToScan(String... expected) {
+ String[] actual = this.context.getBean(
+ TestLocalContainerEntityManagerFactoryBean.class).getPackagesToScan();
+ assertThat(actual, equalTo(expected));
+ }
+
+ @Configuration
+ static class BaseConfig {
+
+ @Bean
+ public TestLocalContainerEntityManagerFactoryBean entityManagerFactoryBean() {
+ return new TestLocalContainerEntityManagerFactoryBean();
+ }
+
+ }
+
+ @EntityScan("com.mycorp.entity")
+ static class ValueConfig extends BaseConfig {
+ }
+
+ @EntityScan(basePackages = "com.mycorp.entity2")
+ static class BasePackagesConfig extends BaseConfig {
+ }
+
+ @EntityScan(basePackageClasses = EntityScanTests.class)
+ static class BasePackageClassesConfig extends BaseConfig {
+ }
+
+ @EntityScan
+ static class FromConfigConfig extends BaseConfig {
+ }
+
+ @EntityScan(value = "com.mycorp.entity", basePackages = "com.mycorp")
+ static class ValueAndBasePackages extends BaseConfig {
+ }
+
+ @EntityScan(value = "com.mycorp.entity", basePackageClasses = EntityScanTests.class)
+ static class ValueAndBasePackageClasses extends BaseConfig {
+ }
+
+ @EntityScan(basePackages = "com.mycorp.entity2", basePackageClasses = EntityScanTests.class)
+ static class BasePackagesAndBasePackageClasses extends BaseConfig {
+ }
+
+ @Configuration
+ @EntityScan("com.mycorp.entity")
+ static class MissingEntityManager {
+ }
+
+ private static class TestLocalContainerEntityManagerFactoryBean extends
+ LocalContainerEntityManagerFactoryBean {
+
+ private String[] packagesToScan;
+
+ @Override
+ protected EntityManagerFactory createNativeEntityManagerFactory()
+ throws PersistenceException {
+ return mock(EntityManagerFactory.class);
+ }
+
+ @Override
+ public void setPackagesToScan(String... packagesToScan) {
+ this.packagesToScan = packagesToScan;
+ }
+
+ public String[] getPackagesToScan() {
+ return this.packagesToScan;
+ }
+
+ }
+
+}