diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/DeveloperToolsProperties.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/DeveloperToolsProperties.java new file mode 100644 index 00000000000..5356bd0685c --- /dev/null +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/DeveloperToolsProperties.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2015 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.developertools.autoconfigure; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for developer tools. + * + * @author Phillip Webb + * @since 1.3.0 + */ +@ConfigurationProperties(prefix = "spring.developertools") +public class DeveloperToolsProperties { + + private static final String DEFAULT_RESTART_EXCLUDES = "META-INF/resources/**,resource/**,static/**,public/**,templates/**"; + + private Restart restart = new Restart(); + + public Restart getRestart() { + return this.restart; + } + + /** + * Restart properties + */ + public static class Restart { + + /** + * Enable automatic restart. + */ + private boolean enabled = true; + + /** + * Patterns that should be excluding for triggering a full restart. + */ + private String exclude = DEFAULT_RESTART_EXCLUDES; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getExclude() { + return this.exclude; + } + + public void setExclude(String exclude) { + this.exclude = exclude; + } + + } + +} diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/LocalDeveloperToolsAutoConfiguration.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/LocalDeveloperToolsAutoConfiguration.java index 3c2943e3346..f83bf08cb3b 100644 --- a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/LocalDeveloperToolsAutoConfiguration.java +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/LocalDeveloperToolsAutoConfiguration.java @@ -16,10 +16,22 @@ package org.springframework.boot.developertools.autoconfigure; +import java.net.URL; + +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.developertools.classpath.ClassPathChangedEvent; +import org.springframework.boot.developertools.classpath.ClassPathFileSystemWatcher; +import org.springframework.boot.developertools.classpath.ClassPathRestartStrategy; +import org.springframework.boot.developertools.classpath.PatternClassPathRestartStrategy; import org.springframework.boot.developertools.restart.ConditionalOnInitializedRestarter; +import org.springframework.boot.developertools.restart.Restarter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; /** * {@link EnableAutoConfiguration Auto-configuration} for local development support. @@ -29,11 +41,47 @@ import org.springframework.context.annotation.Configuration; */ @Configuration @ConditionalOnInitializedRestarter +@EnableConfigurationProperties(DeveloperToolsProperties.class) public class LocalDeveloperToolsAutoConfiguration { + @Autowired + private DeveloperToolsProperties properties; + @Bean public static LocalDeveloperPropertyDefaultsPostProcessor localDeveloperPropertyDefaultsPostProcessor() { return new LocalDeveloperPropertyDefaultsPostProcessor(); } + /** + * Local Restart Configuration. + */ + @ConditionalOnProperty(prefix = "spring.developertools.restart", name = "enabled", matchIfMissing = true) + static class RestartConfiguration { + + @Autowired + private DeveloperToolsProperties properties; + + @Bean + @ConditionalOnMissingBean + public ClassPathFileSystemWatcher classPathFileSystemWatcher() { + URL[] urls = Restarter.getInstance().getInitialUrls(); + return new ClassPathFileSystemWatcher(classPathRestartStrategy(), urls); + } + + @Bean + @ConditionalOnMissingBean + public ClassPathRestartStrategy classPathRestartStrategy() { + return new PatternClassPathRestartStrategy(this.properties.getRestart() + .getExclude()); + } + + @EventListener + public void onClassPathChanged(ClassPathChangedEvent event) { + if (event.isRestartRequired()) { + Restarter.getInstance().restart(); + } + } + + } + } diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/classpath/ClassPathChangedEvent.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/classpath/ClassPathChangedEvent.java new file mode 100644 index 00000000000..ddd70040920 --- /dev/null +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/classpath/ClassPathChangedEvent.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-2015 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.developertools.classpath; + +import java.util.Set; + +import org.springframework.boot.developertools.filewatch.ChangedFiles; +import org.springframework.context.ApplicationEvent; +import org.springframework.util.Assert; + +/** + * {@link ApplicationEvent} containing details of a classpath change. + * + * @author Phillip Webb + * @since 1.3.0 + * @see ClassPathFileChangeListener + */ +public class ClassPathChangedEvent extends ApplicationEvent { + + private final Set changeSet; + + private final boolean restartRequired; + + /** + * Create a new {@link ClassPathChangedEvent}. + * @param source the source of the event + * @param changeSet the changed files + * @param restartRequired if a restart is required due to the change + */ + public ClassPathChangedEvent(Object source, Set changeSet, + boolean restartRequired) { + super(source); + Assert.notNull(changeSet, "ChangeSet must not be null"); + this.changeSet = changeSet; + this.restartRequired = restartRequired; + } + + /** + * Return details of the files that changed. + * @return the changed files + */ + public Set getChangeSet() { + return this.changeSet; + } + + /** + * Return if an application restart is required due to the change. + * @return if an application restart is required + */ + public boolean isRestartRequired() { + return this.restartRequired; + } + +} diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/classpath/ClassPathFileChangeListener.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/classpath/ClassPathFileChangeListener.java new file mode 100644 index 00000000000..13a231c47c9 --- /dev/null +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/classpath/ClassPathFileChangeListener.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-2015 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.developertools.classpath; + +import java.util.Set; + +import org.springframework.boot.developertools.filewatch.ChangedFile; +import org.springframework.boot.developertools.filewatch.ChangedFiles; +import org.springframework.boot.developertools.filewatch.FileChangeListener; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.util.Assert; + +/** + * A {@link FileChangeListener} to publish {@link ClassPathChangedEvent + * ClassPathChangedEvents}. + * + * @author Phillip Webb + * @since 1.3.0 + * @see ClassPathFileSystemWatcher + */ +public class ClassPathFileChangeListener implements FileChangeListener { + + private final ApplicationEventPublisher eventPublisher; + + private final ClassPathRestartStrategy restartStrategy; + + /** + * Create a new {@link ClassPathFileChangeListener} instance. + * @param eventPublisher the event publisher used send events + * @param restartStrategy the restart strategy to use + */ + public ClassPathFileChangeListener(ApplicationEventPublisher eventPublisher, + ClassPathRestartStrategy restartStrategy) { + Assert.notNull(eventPublisher, "EventPublisher must not be null"); + Assert.notNull(restartStrategy, "RestartStrategy must not be null"); + this.eventPublisher = eventPublisher; + this.restartStrategy = restartStrategy; + } + + @Override + public void onChange(Set changeSet) { + boolean restart = isRestartRequired(changeSet); + ApplicationEvent event = new ClassPathChangedEvent(this, changeSet, restart); + this.eventPublisher.publishEvent(event); + } + + private boolean isRestartRequired(Set changeSet) { + for (ChangedFiles changedFiles : changeSet) { + for (ChangedFile changedFile : changedFiles) { + if (this.restartStrategy.isRestartRequired(changedFile)) { + return true; + } + } + } + return false; + } + +} diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/classpath/ClassPathFileSystemWatcher.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/classpath/ClassPathFileSystemWatcher.java new file mode 100644 index 00000000000..826373fa700 --- /dev/null +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/classpath/ClassPathFileSystemWatcher.java @@ -0,0 +1,122 @@ +/* + * Copyright 2012-2015 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.developertools.classpath; + +import java.net.URL; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.developertools.filewatch.FileSystemWatcher; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.util.Assert; +import org.springframework.util.ResourceUtils; + +/** + * Encapsulates a {@link FileSystemWatcher} to watch the local classpath folders for + * changes. + * + * @author Phillip Webb + * @since 1.3.0 + * @see ClassPathFileChangeListener + */ +public class ClassPathFileSystemWatcher implements InitializingBean, DisposableBean, + ApplicationContextAware { + + private static final Log logger = LogFactory.getLog(ClassPathFileSystemWatcher.class); + + private final FileSystemWatcher fileSystemWatcher; + + private ClassPathRestartStrategy restartStrategy; + + private ApplicationContext applicationContext; + + /** + * Create a new {@link ClassPathFileSystemWatcher} instance. + * @param urls the classpath URLs to watch + */ + public ClassPathFileSystemWatcher(URL[] urls) { + this(new FileSystemWatcher(), null, urls); + } + + /** + * Create a new {@link ClassPathFileSystemWatcher} instance. + * @param restartStrategy the classpath restart strategy + * @param urls the URLs to watch + */ + public ClassPathFileSystemWatcher(ClassPathRestartStrategy restartStrategy, URL[] urls) { + this(new FileSystemWatcher(), restartStrategy, urls); + } + + /** + * Create a new {@link ClassPathFileSystemWatcher} instance. + * @param fileSystemWatcher the underlying {@link FileSystemWatcher} used to monitor + * the local file system + * @param restartStrategy the classpath restart strategy + * @param urls the URLs to watch + */ + protected ClassPathFileSystemWatcher(FileSystemWatcher fileSystemWatcher, + ClassPathRestartStrategy restartStrategy, URL[] urls) { + Assert.notNull(fileSystemWatcher, "FileSystemWatcher must not be null"); + Assert.notNull(urls, "Urls must not be null"); + this.fileSystemWatcher = new FileSystemWatcher(); + this.restartStrategy = restartStrategy; + addUrls(urls); + } + + private void addUrls(URL[] urls) { + for (URL url : urls) { + addUrl(url); + } + } + + private void addUrl(URL url) { + if (url.getProtocol().equals("file") && url.getPath().endsWith("/")) { + try { + this.fileSystemWatcher.addSourceFolder(ResourceUtils.getFile(url)); + } + catch (Exception ex) { + logger.warn("Unable to watch classpath URL " + url); + logger.trace("Unable to watch classpath URL " + url, ex); + } + } + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { + this.applicationContext = applicationContext; + } + + @Override + public void afterPropertiesSet() throws Exception { + if (this.restartStrategy != null) { + this.fileSystemWatcher.addListener(new ClassPathFileChangeListener( + this.applicationContext, this.restartStrategy)); + } + this.fileSystemWatcher.start(); + } + + @Override + public void destroy() throws Exception { + this.fileSystemWatcher.stop(); + } + +} diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/classpath/ClassPathRestartStrategy.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/classpath/ClassPathRestartStrategy.java new file mode 100644 index 00000000000..0e5644f3e81 --- /dev/null +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/classpath/ClassPathRestartStrategy.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2015 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.developertools.classpath; + +import org.springframework.boot.developertools.filewatch.ChangedFile; + +/** + * Strategy interface used to determine when a changed classpath file should trigger a + * full application restart. For example, static web resources might not require a full + * restart where as class files would. + * + * @author Phillip Webb + * @since 1.3.0 + * @see PatternClassPathRestartStrategy + */ +public interface ClassPathRestartStrategy { + + /** + * Return true if a full restart is required. + * @param file the changed file + * @return {@code true} if a full restart is required + */ + boolean isRestartRequired(ChangedFile file); + +} diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/classpath/PatternClassPathRestartStrategy.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/classpath/PatternClassPathRestartStrategy.java new file mode 100644 index 00000000000..65af3b0f3f1 --- /dev/null +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/classpath/PatternClassPathRestartStrategy.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2015 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.developertools.classpath; + +import org.springframework.boot.developertools.filewatch.ChangedFile; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.StringUtils; + +/** + * Ant style pattern based {@link ClassPathRestartStrategy}. + * + * @author Phillip Webb + * @since 1.3.0 + * @see ClassPathRestartStrategy + */ +public class PatternClassPathRestartStrategy implements ClassPathRestartStrategy { + + private final AntPathMatcher matcher = new AntPathMatcher(); + + private final String[] excludePatterns; + + public PatternClassPathRestartStrategy(String excludePatterns) { + this.excludePatterns = StringUtils + .commaDelimitedListToStringArray(excludePatterns); + } + + @Override + public boolean isRestartRequired(ChangedFile file) { + for (String pattern : this.excludePatterns) { + if (this.matcher.match(pattern, file.getRelativeName())) { + return false; + } + } + return true; + } + +} diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/classpath/package-info.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/classpath/package-info.java new file mode 100644 index 00000000000..4ce5e42b61f --- /dev/null +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/classpath/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2015 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. + */ + +/** + * Support for classpath monitoring + */ +package org.springframework.boot.developertools.classpath; + diff --git a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/autoconfigure/LocalDeveloperToolsAutoConfigurationTests.java b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/autoconfigure/LocalDeveloperToolsAutoConfigurationTests.java index a1dd6a68984..86edfbab8eb 100644 --- a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/autoconfigure/LocalDeveloperToolsAutoConfigurationTests.java +++ b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/autoconfigure/LocalDeveloperToolsAutoConfigurationTests.java @@ -24,8 +24,12 @@ import org.junit.After; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration; +import org.springframework.boot.developertools.classpath.ClassPathChangedEvent; +import org.springframework.boot.developertools.classpath.ClassPathFileSystemWatcher; +import org.springframework.boot.developertools.filewatch.ChangedFiles; import org.springframework.boot.developertools.restart.MockRestartInitializer; import org.springframework.boot.developertools.restart.MockRestarter; import org.springframework.boot.developertools.restart.Restarter; @@ -36,7 +40,10 @@ import org.springframework.util.SocketUtils; import org.thymeleaf.templateresolver.TemplateResolver; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; /** * Tests for {@link LocalDeveloperToolsAutoConfiguration}. @@ -70,6 +77,41 @@ public class LocalDeveloperToolsAutoConfigurationTests { assertThat(resolver.isCacheable(), equalTo(false)); } + @Test + public void restartTriggerdOnClassPathChangeWithRestart() throws Exception { + this.context = initializeAndRun(Config.class); + ClassPathChangedEvent event = new ClassPathChangedEvent(this.context, + Collections. emptySet(), true); + this.context.publishEvent(event); + verify(this.mockRestarter.getMock()).restart(); + } + + @Test + public void restartNotTriggerdOnClassPathChangeWithRestart() throws Exception { + this.context = initializeAndRun(Config.class); + ClassPathChangedEvent event = new ClassPathChangedEvent(this.context, + Collections. emptySet(), false); + this.context.publishEvent(event); + verify(this.mockRestarter.getMock(), never()).restart(); + } + + @Test + public void restartWatchingClassPath() throws Exception { + this.context = initializeAndRun(Config.class); + ClassPathFileSystemWatcher watcher = this.context + .getBean(ClassPathFileSystemWatcher.class); + assertThat(watcher, notNullValue()); + } + + @Test + public void restartDisabled() throws Exception { + Map properties = new HashMap(); + properties.put("spring.developertools.restart.enabled", false); + this.context = initializeAndRun(Config.class, properties); + this.thrown.expect(NoSuchBeanDefinitionException.class); + this.context.getBean(ClassPathFileSystemWatcher.class); + } + private ConfigurableApplicationContext initializeAndRun(Class config) { return initializeAndRun(config, Collections. emptyMap()); } diff --git a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/classpath/ClassPathChangedEventTests.java b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/classpath/ClassPathChangedEventTests.java new file mode 100644 index 00000000000..6d50383dd50 --- /dev/null +++ b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/classpath/ClassPathChangedEventTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-2015 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.developertools.classpath; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.boot.developertools.filewatch.ChangedFiles; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link ClassPathChangedEvent}. + * + * @author Phillip Webb + */ +public class ClassPathChangedEventTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private Object source = new Object(); + + @Test + public void changeSetMustNotBeNull() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("ChangeSet must not be null"); + new ClassPathChangedEvent(this.source, null, false); + } + + @Test + public void getChangeSet() throws Exception { + Set changeSet = new LinkedHashSet(); + ClassPathChangedEvent event = new ClassPathChangedEvent(this.source, changeSet, + false); + assertThat(event.getChangeSet(), sameInstance(changeSet)); + } + + @Test + public void getRestartRequired() throws Exception { + Set changeSet = new LinkedHashSet(); + ClassPathChangedEvent event; + event = new ClassPathChangedEvent(this.source, changeSet, false); + assertThat(event.isRestartRequired(), equalTo(false)); + event = new ClassPathChangedEvent(this.source, changeSet, true); + assertThat(event.isRestartRequired(), equalTo(true)); + } + +} diff --git a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/classpath/ClassPathFileChangeListenerTests.java b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/classpath/ClassPathFileChangeListenerTests.java new file mode 100644 index 00000000000..445a7900f66 --- /dev/null +++ b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/classpath/ClassPathFileChangeListenerTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2012-2015 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.developertools.classpath; + +import java.io.File; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.MockitoAnnotations; +import org.springframework.boot.developertools.filewatch.ChangedFile; +import org.springframework.boot.developertools.filewatch.ChangedFiles; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link ClassPathFileChangeListener}. + * + * @author Phillip Webb + */ +public class ClassPathFileChangeListenerTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Captor + private ArgumentCaptor eventCaptor; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void eventPublisherMustNotBeNull() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("EventPublisher must not be null"); + new ClassPathFileChangeListener(null, mock(ClassPathRestartStrategy.class)); + } + + @Test + public void restartStrategyMustNotBeNull() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("RestartStrategy must not be null"); + new ClassPathFileChangeListener(mock(ApplicationEventPublisher.class), null); + } + + @Test + public void sendsEventWithoutRestart() throws Exception { + testSendsEvent(false); + } + + @Test + public void sendsEventWithRestart() throws Exception { + testSendsEvent(true); + } + + private void testSendsEvent(boolean restart) { + ApplicationEventPublisher eventPublisher = mock(ApplicationEventPublisher.class); + ClassPathRestartStrategy restartStrategy = mock(ClassPathRestartStrategy.class); + ClassPathFileChangeListener listener = new ClassPathFileChangeListener( + eventPublisher, restartStrategy); + File folder = new File("s1"); + File file = new File("f1"); + ChangedFile file1 = new ChangedFile(folder, file, ChangedFile.Type.ADD); + ChangedFile file2 = new ChangedFile(folder, file, ChangedFile.Type.ADD); + Set files = new LinkedHashSet(); + files.add(file1); + files.add(file2); + ChangedFiles changedFiles = new ChangedFiles(new File("source"), files); + Set changeSet = Collections.singleton(changedFiles); + if (restart) { + given(restartStrategy.isRestartRequired(file2)).willReturn(true); + } + listener.onChange(changeSet); + verify(eventPublisher).publishEvent(this.eventCaptor.capture()); + ClassPathChangedEvent actualEvent = (ClassPathChangedEvent) this.eventCaptor + .getValue(); + assertThat(actualEvent.getChangeSet(), equalTo(changeSet)); + assertThat(actualEvent.isRestartRequired(), equalTo(restart)); + } + +} diff --git a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/classpath/ClassPathFileSystemWatcherTests.java b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/classpath/ClassPathFileSystemWatcherTests.java new file mode 100644 index 00000000000..1c2066563e0 --- /dev/null +++ b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/classpath/ClassPathFileSystemWatcherTests.java @@ -0,0 +1,136 @@ +/* + * Copyright 2012-2015 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.developertools.classpath; + +import java.io.File; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.developertools.filewatch.ChangedFile; +import org.springframework.boot.developertools.filewatch.FileSystemWatcher; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.util.FileCopyUtils; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link ClassPathFileSystemWatcher}. + * + * @author Phillip Webb + */ +public class ClassPathFileSystemWatcherTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Test + public void urlsMustNotBeNull() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Urls must not be null"); + URL[] urls = null; + new ClassPathFileSystemWatcher(urls); + } + + @Test + public void configuredWithRestartStrategy() throws Exception { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + Map properties = new HashMap(); + File folder = this.temp.newFolder(); + List urls = new ArrayList(); + urls.add(new URL("http://spring.io")); + urls.add(folder.toURI().toURL()); + properties.put("urls", urls); + MapPropertySource propertySource = new MapPropertySource("test", properties); + context.getEnvironment().getPropertySources().addLast(propertySource); + context.register(Config.class); + context.refresh(); + Thread.sleep(100); + File classFile = new File(folder, "Example.class"); + FileCopyUtils.copy("file".getBytes(), classFile); + Thread.sleep(1100); + List events = context.getBean(Listener.class).getEvents(); + assertThat(events.size(), equalTo(1)); + assertThat(events.get(0).getChangeSet().iterator().next().getFiles().iterator() + .next().getFile(), equalTo(classFile)); + context.close(); + } + + @Configuration + public static class Config { + + @Autowired + public Environment environemnt; + + @Bean + public ClassPathFileSystemWatcher watcher() { + FileSystemWatcher watcher = new FileSystemWatcher(false, 100, 10); + URL[] urls = this.environemnt.getProperty("urls", URL[].class); + return new ClassPathFileSystemWatcher(watcher, restartStrategy(), urls); + } + + @Bean + public ClassPathRestartStrategy restartStrategy() { + return new ClassPathRestartStrategy() { + + @Override + public boolean isRestartRequired(ChangedFile file) { + return false; + } + + }; + } + + @Bean + public Listener listener() { + return new Listener(); + } + + } + + public static class Listener implements ApplicationListener { + + private List events = new ArrayList(); + + @Override + public void onApplicationEvent(ClassPathChangedEvent event) { + this.events.add(event); + } + + public List getEvents() { + return this.events; + } + + } + +} diff --git a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/classpath/PatternClassPathRestartStrategyTests.java b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/classpath/PatternClassPathRestartStrategyTests.java new file mode 100644 index 00000000000..53dd7a55400 --- /dev/null +++ b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/classpath/PatternClassPathRestartStrategyTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2015 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.developertools.classpath; + +import java.io.File; + +import org.junit.Test; +import org.springframework.boot.developertools.filewatch.ChangedFile; +import org.springframework.boot.developertools.filewatch.ChangedFile.Type; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link PatternClassPathRestartStrategy}. + * + * @author Phillip Webb + */ +public class PatternClassPathRestartStrategyTests { + + @Test + public void nullPattern() throws Exception { + ClassPathRestartStrategy strategy = createStrategy(null); + assertRestartRequired(strategy, "a/b.txt", true); + } + + @Test + public void emptyPattern() throws Exception { + ClassPathRestartStrategy strategy = createStrategy(""); + assertRestartRequired(strategy, "a/b.txt", true); + } + + @Test + public void singlePattern() throws Exception { + ClassPathRestartStrategy strategy = createStrategy("static/**"); + assertRestartRequired(strategy, "static/file.txt", false); + assertRestartRequired(strategy, "static/folder/file.txt", false); + assertRestartRequired(strategy, "public/file.txt", true); + assertRestartRequired(strategy, "public/folder/file.txt", true); + } + + @Test + public void multiplePatterns() throws Exception { + ClassPathRestartStrategy strategy = createStrategy("static/**,public/**"); + assertRestartRequired(strategy, "static/file.txt", false); + assertRestartRequired(strategy, "static/folder/file.txt", false); + assertRestartRequired(strategy, "public/file.txt", false); + assertRestartRequired(strategy, "public/folder/file.txt", false); + assertRestartRequired(strategy, "src/file.txt", true); + assertRestartRequired(strategy, "src/folder/file.txt", true); + } + + private ClassPathRestartStrategy createStrategy(String pattern) { + return new PatternClassPathRestartStrategy(pattern); + } + + private void assertRestartRequired(ClassPathRestartStrategy strategy, + String relativeName, boolean expected) { + assertThat(strategy.isRestartRequired(mockFile(relativeName)), equalTo(expected)); + } + + private ChangedFile mockFile(String relativeName) { + return new ChangedFile(new File("."), new File("./" + relativeName), Type.ADD); + } + +}