From fe1f344ae889ad5f291f2943426d7f5190d27a32 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 1 Jun 2015 13:23:17 -0700 Subject: [PATCH] Add livereload auto-configuration Add auto-configuration to start and trigger livereload. Closes gh-3085 --- .../DeveloperToolsProperties.java | 39 ++++++++++ .../LocalDeveloperToolsAutoConfiguration.java | 42 ++++++++++ .../OptionalLiveReloadServer.java | 77 +++++++++++++++++++ ...lDeveloperToolsAutoConfigurationTests.java | 64 +++++++++++++++ .../OptionalLiveReloadServerTests.java | 51 ++++++++++++ 5 files changed, 273 insertions(+) create mode 100644 spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/OptionalLiveReloadServer.java create mode 100644 spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/autoconfigure/OptionalLiveReloadServerTests.java 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 index 5356bd0685..6afbc3a140 100644 --- 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 @@ -31,10 +31,16 @@ public class DeveloperToolsProperties { private Restart restart = new Restart(); + private Livereload livereload = new Livereload(); + public Restart getRestart() { return this.restart; } + public Livereload getLivereload() { + return this.livereload; + } + /** * Restart properties */ @@ -68,4 +74,37 @@ public class DeveloperToolsProperties { } + /** + * LiveReload properties + */ + public static class Livereload { + + /** + * Enable a livereload.com compatible server. + */ + private boolean enabled = true; + + /** + * Server port. + */ + private int port = 35729; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public int getPort() { + return this.port; + } + + public void setPort(int port) { + this.port = port; + } + + } + } 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 f83bf08cb3..922389c3c3 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 @@ -27,10 +27,13 @@ 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.livereload.LiveReloadServer; import org.springframework.boot.developertools.restart.ConditionalOnInitializedRestarter; +import org.springframework.boot.developertools.restart.RestartScope; import org.springframework.boot.developertools.restart.Restarter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; /** @@ -52,6 +55,45 @@ public class LocalDeveloperToolsAutoConfiguration { return new LocalDeveloperPropertyDefaultsPostProcessor(); } + /** + * Local LiveReload configuration. + */ + @ConditionalOnProperty(prefix = "spring.developertools.livereload", name = "enabled", matchIfMissing = true) + static class LiveReloadConfiguration { + + @Autowired + private DeveloperToolsProperties properties; + + @Autowired(required = false) + private LiveReloadServer liveReloadServer; + + @Bean + @RestartScope + @ConditionalOnMissingBean + public LiveReloadServer liveReloadServer() { + return new LiveReloadServer(this.properties.getLivereload().getPort(), + Restarter.getInstance().getThreadFactory()); + } + + @EventListener + public void onContextRefreshed(ContextRefreshedEvent event) { + optionalLiveReloadServer().triggerReload(); + } + + @EventListener + public void onClassPathChanged(ClassPathChangedEvent event) { + if (!event.isRestartRequired()) { + optionalLiveReloadServer().triggerReload(); + } + } + + @Bean + public OptionalLiveReloadServer optionalLiveReloadServer() { + return new OptionalLiveReloadServer(this.liveReloadServer); + } + + } + /** * Local Restart Configuration. */ diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/OptionalLiveReloadServer.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/OptionalLiveReloadServer.java new file mode 100644 index 0000000000..a9f4ad2539 --- /dev/null +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/OptionalLiveReloadServer.java @@ -0,0 +1,77 @@ +/* + * 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 javax.annotation.PostConstruct; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.boot.developertools.livereload.LiveReloadServer; + +/** + * Manages an optional {@link LiveReloadServer}. The {@link LiveReloadServer} may + * gracefully fail to start (e.g. because of a port conflict) or may be omitted entirely. + * + * @author Phillip Webb + * @since 1.3.0 + */ +public class OptionalLiveReloadServer { + + private static final Log logger = LogFactory.getLog(OptionalLiveReloadServer.class); + + private LiveReloadServer server; + + /** + * Create a new {@link OptionalLiveReloadServer} instance. + * @param server the server to manage or {@code null} + */ + public OptionalLiveReloadServer(LiveReloadServer server) { + this.server = server; + } + + /** + * {@link PostConstruct} method to start the server if possible. + * @throws Exception + */ + @PostConstruct + public void startServer() throws Exception { + if (this.server != null) { + try { + if (!this.server.isStarted()) { + this.server.start(); + } + logger.info("LiveReload server is running on port " + + this.server.getPort()); + } + catch (Exception ex) { + logger.warn("Unable to start LiveReload server"); + logger.debug("Live reload start error", ex); + this.server = null; + } + } + } + + /** + * Trigger LiveReload if the server is up an running. + */ + public void triggerReload() { + if (this.server != null) { + this.server.triggerReload(); + } + } + +} 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 86edfbab8e..85e01a0279 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 @@ -30,19 +30,24 @@ import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfigurati 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.livereload.LiveReloadServer; import org.springframework.boot.developertools.restart.MockRestartInitializer; import org.springframework.boot.developertools.restart.MockRestarter; import org.springframework.boot.developertools.restart.Restarter; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.context.event.ContextRefreshedEvent; 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.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; /** @@ -77,6 +82,53 @@ public class LocalDeveloperToolsAutoConfigurationTests { assertThat(resolver.isCacheable(), equalTo(false)); } + @Test + public void liveReloadServer() throws Exception { + this.context = initializeAndRun(Config.class); + LiveReloadServer server = this.context.getBean(LiveReloadServer.class); + assertThat(server.isStarted(), equalTo(true)); + } + + @Test + public void liveReloadTriggeredOnContextRefresh() throws Exception { + this.context = initializeAndRun(ConfigWithMockLiveReload.class); + LiveReloadServer server = this.context.getBean(LiveReloadServer.class); + reset(server); + this.context.publishEvent(new ContextRefreshedEvent(this.context)); + verify(server).triggerReload(); + } + + @Test + public void liveReloadTriggerdOnClassPathChangeWithoutRestart() throws Exception { + this.context = initializeAndRun(ConfigWithMockLiveReload.class); + LiveReloadServer server = this.context.getBean(LiveReloadServer.class); + reset(server); + ClassPathChangedEvent event = new ClassPathChangedEvent(this.context, + Collections. emptySet(), false); + this.context.publishEvent(event); + verify(server).triggerReload(); + } + + @Test + public void liveReloadNotTriggerdOnClassPathChangeWithRestart() throws Exception { + this.context = initializeAndRun(ConfigWithMockLiveReload.class); + LiveReloadServer server = this.context.getBean(LiveReloadServer.class); + reset(server); + ClassPathChangedEvent event = new ClassPathChangedEvent(this.context, + Collections. emptySet(), true); + this.context.publishEvent(event); + verify(server, never()).triggerReload(); + } + + @Test + public void liveReloadDisabled() throws Exception { + Map properties = new HashMap(); + properties.put("spring.developertools.livereload.enabled", false); + this.context = initializeAndRun(Config.class, properties); + this.thrown.expect(NoSuchBeanDefinitionException.class); + this.context.getBean(OptionalLiveReloadServer.class); + } + @Test public void restartTriggerdOnClassPathChangeWithRestart() throws Exception { this.context = initializeAndRun(Config.class); @@ -142,4 +194,16 @@ public class LocalDeveloperToolsAutoConfigurationTests { } + @Configuration + @Import({ LocalDeveloperToolsAutoConfiguration.class, + ThymeleafAutoConfiguration.class }) + public static class ConfigWithMockLiveReload { + + @Bean + public LiveReloadServer liveReloadServer() { + return mock(LiveReloadServer.class); + } + + } + } diff --git a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/autoconfigure/OptionalLiveReloadServerTests.java b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/autoconfigure/OptionalLiveReloadServerTests.java new file mode 100644 index 0000000000..7c74c14aa3 --- /dev/null +++ b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/autoconfigure/OptionalLiveReloadServerTests.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.autoconfigure; + +import org.junit.Test; +import org.springframework.boot.developertools.livereload.LiveReloadServer; + +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link OptionalLiveReloadServer}. + * + * @author Phillip Webb + */ +public class OptionalLiveReloadServerTests { + + @Test + public void nullServer() throws Exception { + OptionalLiveReloadServer server = new OptionalLiveReloadServer(null); + server.startServer(); + server.triggerReload(); + } + + @Test + public void serverWontStart() throws Exception { + LiveReloadServer delegate = mock(LiveReloadServer.class); + OptionalLiveReloadServer server = new OptionalLiveReloadServer(delegate); + willThrow(new RuntimeException("Error")).given(delegate).start(); + server.startServer(); + server.triggerReload(); + verify(delegate, never()).triggerReload(); + } + +}