diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsAutoConfiguration.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsAutoConfiguration.java index 1c157c58fbc..51b35ffdad9 100644 --- a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsAutoConfiguration.java +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsAutoConfiguration.java @@ -20,6 +20,8 @@ import java.util.Collection; import javax.servlet.Filter; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -33,6 +35,10 @@ import org.springframework.boot.developertools.remote.server.Handler; import org.springframework.boot.developertools.remote.server.HandlerMapper; import org.springframework.boot.developertools.remote.server.HttpStatusHandler; import org.springframework.boot.developertools.remote.server.UrlHandlerMapper; +import org.springframework.boot.developertools.restart.server.DefaultSourceFolderUrlFilter; +import org.springframework.boot.developertools.restart.server.HttpRestartServer; +import org.springframework.boot.developertools.restart.server.HttpRestartServerHandler; +import org.springframework.boot.developertools.restart.server.SourceFolderUrlFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.server.ServerHttpRequest; @@ -50,6 +56,9 @@ import org.springframework.http.server.ServerHttpRequest; @EnableConfigurationProperties(DeveloperToolsProperties.class) public class RemoteDeveloperToolsAutoConfiguration { + private static final Log logger = LogFactory + .getLog(RemoteDeveloperToolsAutoConfiguration.class); + @Autowired private DeveloperToolsProperties properties; @@ -67,4 +76,37 @@ public class RemoteDeveloperToolsAutoConfiguration { return new DispatcherFilter(dispatcher); } + /** + * Configuration for remote update and restarts. + */ + @ConditionalOnProperty(prefix = "spring.developertools.remote.restart", name = "enabled", matchIfMissing = true) + static class RemoteRestartConfiguration { + + @Autowired + private DeveloperToolsProperties properties; + + @Bean + @ConditionalOnMissingBean + public SourceFolderUrlFilter remoteRestartSourceFolderUrlFilter() { + return new DefaultSourceFolderUrlFilter(); + } + + @Bean + @ConditionalOnMissingBean + public HttpRestartServer remoteRestartHttpRestartServer( + SourceFolderUrlFilter sourceFolderUrlFilter) { + return new HttpRestartServer(sourceFolderUrlFilter); + } + + @Bean + @ConditionalOnMissingBean(name = "remoteRestartHanderMapper") + public UrlHandlerMapper remoteRestartHanderMapper(HttpRestartServer server) { + String url = this.properties.getRemote().getContextPath() + "/restart"; + logger.warn("Listening for remote restart updates on " + url); + Handler handler = new HttpRestartServerHandler(server); + return new UrlHandlerMapper(url, handler); + } + + } + } diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsProperties.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsProperties.java index 3a2eab4aafa..accff6afd40 100644 --- a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsProperties.java +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsProperties.java @@ -16,7 +16,6 @@ package org.springframework.boot.developertools.autoconfigure; - /** * Configuration properties for remote Spring Boot applications. * @@ -34,6 +33,8 @@ public class RemoteDeveloperToolsProperties { */ private String contextPath = DEFAULT_CONTEXT_PATH; + private Restart restart = new Restart(); + public String getContextPath() { return this.contextPath; } @@ -42,4 +43,25 @@ public class RemoteDeveloperToolsProperties { this.contextPath = contextPath; } + public Restart getRestart() { + return this.restart; + } + + public static class Restart { + + /** + * Enable remote restart + */ + private boolean enabled = true; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + } + } diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/ClassPathChangeUploader.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/ClassPathChangeUploader.java new file mode 100644 index 00000000000..4a7485c0d00 --- /dev/null +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/ClassPathChangeUploader.java @@ -0,0 +1,145 @@ +/* + * 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.remote.client; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.boot.developertools.classpath.ClassPathChangedEvent; +import org.springframework.boot.developertools.filewatch.ChangedFile; +import org.springframework.boot.developertools.filewatch.ChangedFiles; +import org.springframework.boot.developertools.restart.classloader.ClassLoaderFile; +import org.springframework.boot.developertools.restart.classloader.ClassLoaderFile.Kind; +import org.springframework.boot.developertools.restart.classloader.ClassLoaderFiles; +import org.springframework.context.ApplicationListener; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; + +/** + * Listens and pushes any classpath updates to a remote endpoint. + * + * @author Phillip Webb + * @since 1.3.0 + */ +public class ClassPathChangeUploader implements + ApplicationListener { + + private static final Map TYPE_MAPPINGS; + static { + Map map = new HashMap(); + map.put(ChangedFile.Type.ADD, ClassLoaderFile.Kind.ADDED); + map.put(ChangedFile.Type.DELETE, ClassLoaderFile.Kind.DELETED); + map.put(ChangedFile.Type.MODIFY, ClassLoaderFile.Kind.MODIFIED); + TYPE_MAPPINGS = Collections.unmodifiableMap(map); + } + + private static final Log logger = LogFactory.getLog(ClassPathChangeUploader.class); + + private final URI uri; + + private final ClientHttpRequestFactory requestFactory; + + public ClassPathChangeUploader(String url, ClientHttpRequestFactory requestFactory) { + Assert.hasLength(url, "URL must not be empty"); + Assert.notNull(requestFactory, "RequestFactory must not be null"); + try { + this.uri = new URL(url).toURI(); + } + catch (URISyntaxException ex) { + throw new IllegalArgumentException("Malformed URL '" + url + "'"); + } + catch (MalformedURLException ex) { + throw new IllegalArgumentException("Malformed URL '" + url + "'"); + } + this.requestFactory = requestFactory; + } + + @Override + public void onApplicationEvent(ClassPathChangedEvent event) { + try { + ClassLoaderFiles classLoaderFiles = getClassLoaderFiles(event); + ClientHttpRequest request = this.requestFactory.createRequest(this.uri, + HttpMethod.POST); + byte[] bytes = serialize(classLoaderFiles); + HttpHeaders headers = request.getHeaders(); + headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); + headers.setContentLength(bytes.length); + FileCopyUtils.copy(bytes, request.getBody()); + logUpload(classLoaderFiles); + ClientHttpResponse response = request.execute(); + Assert.state(response.getStatusCode() == HttpStatus.OK, "Unexpected " + + response.getStatusCode() + " response uploading class files"); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + private void logUpload(ClassLoaderFiles classLoaderFiles) { + int size = classLoaderFiles.size(); + logger.info("Uploaded " + size + " class " + + (size == 1 ? "resource" : "resources")); + } + + private byte[] serialize(ClassLoaderFiles classLoaderFiles) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream); + objectOutputStream.writeObject(classLoaderFiles); + objectOutputStream.close(); + return outputStream.toByteArray(); + } + + private ClassLoaderFiles getClassLoaderFiles(ClassPathChangedEvent event) + throws IOException { + ClassLoaderFiles files = new ClassLoaderFiles(); + for (ChangedFiles changedFiles : event.getChangeSet()) { + String sourceFolder = changedFiles.getSourceFolder().getAbsolutePath(); + for (ChangedFile changedFile : changedFiles) { + files.addFile(sourceFolder, changedFile.getRelativeName(), + asClassLoaderFile(changedFile)); + } + } + return files; + } + + private ClassLoaderFile asClassLoaderFile(ChangedFile changedFile) throws IOException { + ClassLoaderFile.Kind kind = TYPE_MAPPINGS.get(changedFile.getType()); + byte[] bytes = (kind == Kind.DELETED ? null : FileCopyUtils + .copyToByteArray(changedFile.getFile())); + long lastModified = (kind == Kind.DELETED ? System.currentTimeMillis() + : changedFile.getFile().lastModified()); + return new ClassLoaderFile(kind, lastModified, bytes); + } + +} diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/RemoteClientConfiguration.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/RemoteClientConfiguration.java index ff8c90d71e0..461888cb483 100644 --- a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/RemoteClientConfiguration.java +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/RemoteClientConfiguration.java @@ -16,10 +16,22 @@ package org.springframework.boot.developertools.remote.client; +import java.net.URL; + +import javax.annotation.PostConstruct; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.developertools.autoconfigure.DeveloperToolsProperties; +import org.springframework.boot.developertools.autoconfigure.RemoteDeveloperToolsProperties; +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.DefaultRestartInitializer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; @@ -37,6 +49,8 @@ import org.springframework.http.client.SimpleClientHttpRequestFactory; @EnableConfigurationProperties(DeveloperToolsProperties.class) public class RemoteClientConfiguration { + private static final Log logger = LogFactory.getLog(RemoteClientConfiguration.class); + @Autowired private DeveloperToolsProperties properties; @@ -53,4 +67,50 @@ public class RemoteClientConfiguration { return new SimpleClientHttpRequestFactory(); } + @PostConstruct + private void logWarnings() { + RemoteDeveloperToolsProperties remoteProperties = this.properties.getRemote(); + if (!remoteProperties.getRestart().isEnabled()) { + logger.warn("Remote restart is not enabled."); + } + } + + /** + * Client configuration for remote update and restarts. + */ + @ConditionalOnProperty(prefix = "spring.developertools.remote.restart", name = "enabled", matchIfMissing = true) + static class RemoteRestartClientConfiguration { + + @Autowired + private DeveloperToolsProperties properties; + + @Value("${remoteUrl}") + private String remoteUrl; + + @Bean + public ClassPathFileSystemWatcher classPathFileSystemWatcher() { + DefaultRestartInitializer restartInitializer = new DefaultRestartInitializer(); + URL[] urls = restartInitializer.getInitialUrls(Thread.currentThread()); + if (urls == null) { + urls = new URL[0]; + } + return new ClassPathFileSystemWatcher(classPathRestartStrategy(), urls); + } + + @Bean + public ClassPathRestartStrategy classPathRestartStrategy() { + return new PatternClassPathRestartStrategy(this.properties.getRestart() + .getExclude()); + } + + @Bean + public ClassPathChangeUploader classPathChangeUploader( + ClientHttpRequestFactory requestFactory) { + String url = this.remoteUrl + this.properties.getRemote().getContextPath() + + "/restart"; + return new ClassPathChangeUploader(url, requestFactory); + } + + } + } diff --git a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsAutoConfigurationTests.java b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsAutoConfigurationTests.java index f626f131fd2..4c584593458 100644 --- a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsAutoConfigurationTests.java +++ b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsAutoConfigurationTests.java @@ -16,18 +16,26 @@ package org.springframework.boot.developertools.autoconfigure; +import java.io.IOException; + import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration; import org.springframework.boot.developertools.remote.server.DispatcherFilter; import org.springframework.boot.developertools.restart.MockRestarter; +import org.springframework.boot.developertools.restart.server.HttpRestartServer; +import org.springframework.boot.developertools.restart.server.SourceFolderUrlFilter; import org.springframework.boot.test.EnvironmentTestUtils; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -36,6 +44,7 @@ import org.springframework.web.context.support.AnnotationConfigWebApplicationCon import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; /** * Tests for {@link RemoteDeveloperToolsAutoConfiguration}. @@ -75,6 +84,32 @@ public class RemoteDeveloperToolsAutoConfigurationTests { } } + @Test + public void ignoresUnmappedUrl() throws Exception { + loadContext("spring.developertools.remote.enabled:true"); + DispatcherFilter filter = this.context.getBean(DispatcherFilter.class); + this.request.setRequestURI("/restart"); + filter.doFilter(this.request, this.response, this.chain); + assertRestartInvoked(false); + } + + @Test + public void invokeRestartWithDefaultSetup() throws Exception { + loadContext("spring.developertools.remote.enabled:true"); + DispatcherFilter filter = this.context.getBean(DispatcherFilter.class); + this.request.setRequestURI(DEFAULT_CONTEXT_PATH + "/restart"); + filter.doFilter(this.request, this.response, this.chain); + assertRestartInvoked(true); + } + + @Test + public void disableRestart() throws Exception { + loadContext("spring.developertools.remote.enabled:true", + "spring.developertools.remote.restart.enabled:false"); + this.thrown.expect(NoSuchBeanDefinitionException.class); + this.context.getBean("remoteRestartHanderMapper"); + } + @Test public void developerToolsHealthReturns200() throws Exception { loadContext("spring.developertools.remote.enabled:true"); @@ -85,6 +120,11 @@ public class RemoteDeveloperToolsAutoConfigurationTests { assertThat(this.response.getStatus(), equalTo(200)); } + private void assertRestartInvoked(boolean value) { + assertThat(this.context.getBean(MockHttpRestartServer.class).invoked, + equalTo(value)); + } + private void loadContext(String... properties) { this.context = new AnnotationConfigWebApplicationContext(); this.context.setServletContext(new MockServletContext()); @@ -98,5 +138,31 @@ public class RemoteDeveloperToolsAutoConfigurationTests { @Import(RemoteDeveloperToolsAutoConfiguration.class) static class Config { + @Bean + public HttpRestartServer remoteRestartHttpRestartServer() { + SourceFolderUrlFilter sourceFolderUrlFilter = mock(SourceFolderUrlFilter.class); + return new MockHttpRestartServer(sourceFolderUrlFilter); + } + } + + /** + * Mock {@link HttpRestartServer} implementation. + */ + static class MockHttpRestartServer extends HttpRestartServer { + + private boolean invoked; + + public MockHttpRestartServer(SourceFolderUrlFilter sourceFolderUrlFilter) { + super(sourceFolderUrlFilter); + } + + @Override + public void handle(ServerHttpRequest request, ServerHttpResponse response) + throws IOException { + this.invoked = true; + } + + } + } diff --git a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/client/ClassPathChangeUploaderTests.java b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/client/ClassPathChangeUploaderTests.java new file mode 100644 index 00000000000..9a641a10273 --- /dev/null +++ b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/client/ClassPathChangeUploaderTests.java @@ -0,0 +1,148 @@ +/* + * 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.remote.client; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.util.Collection; +import java.util.Iterator; +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.junit.rules.TemporaryFolder; +import org.springframework.boot.developertools.classpath.ClassPathChangedEvent; +import org.springframework.boot.developertools.filewatch.ChangedFile; +import org.springframework.boot.developertools.filewatch.ChangedFile.Type; +import org.springframework.boot.developertools.filewatch.ChangedFiles; +import org.springframework.boot.developertools.restart.classloader.ClassLoaderFile; +import org.springframework.boot.developertools.restart.classloader.ClassLoaderFile.Kind; +import org.springframework.boot.developertools.restart.classloader.ClassLoaderFiles; +import org.springframework.boot.developertools.restart.classloader.ClassLoaderFiles.SourceFolder; +import org.springframework.boot.developertools.test.MockClientHttpRequestFactory; +import org.springframework.http.HttpStatus; +import org.springframework.mock.http.client.MockClientHttpRequest; +import org.springframework.util.FileCopyUtils; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link ClassPathChangeUploader}. + * + * @author Phillip Webb + */ +public class ClassPathChangeUploaderTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + private MockClientHttpRequestFactory requestFactory; + + private ClassPathChangeUploader uploader; + + @Before + public void setup() { + this.requestFactory = new MockClientHttpRequestFactory(); + this.uploader = new ClassPathChangeUploader("http://localhost/upload", + this.requestFactory); + } + + @Test + public void urlMustNotBeNull() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("URL must not be empty"); + new ClassPathChangeUploader(null, this.requestFactory); + } + + @Test + public void urlMustNotBeEmpty() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("URL must not be empty"); + new ClassPathChangeUploader("", this.requestFactory); + } + + @Test + public void requestFactoryMustNotBeNull() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("RequestFactory must not be null"); + new ClassPathChangeUploader("http://localhost:8080", null); + } + + @Test + public void urlMustNotBeMalformed() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Malformed URL 'htttttp:///ttest'"); + new ClassPathChangeUploader("htttttp:///ttest", this.requestFactory); + } + + @Test + public void sendsClassLoaderFiles() throws Exception { + File sourceFolder = this.temp.newFolder(); + Set files = new LinkedHashSet(); + File file1 = createFile(sourceFolder, "File1"); + File file2 = createFile(sourceFolder, "File2"); + File file3 = createFile(sourceFolder, "File3"); + files.add(new ChangedFile(sourceFolder, file1, Type.ADD)); + files.add(new ChangedFile(sourceFolder, file2, Type.MODIFY)); + files.add(new ChangedFile(sourceFolder, file3, Type.DELETE)); + Set changeSet = new LinkedHashSet(); + changeSet.add(new ChangedFiles(sourceFolder, files)); + ClassPathChangedEvent event = new ClassPathChangedEvent(this, changeSet, false); + this.requestFactory.willRespond(HttpStatus.OK); + this.uploader.onApplicationEvent(event); + MockClientHttpRequest request = this.requestFactory.getExecutedRequests().get(0); + ClassLoaderFiles classLoaderFiles = deserialize(request.getBodyAsBytes()); + Collection sourceFolders = classLoaderFiles.getSourceFolders(); + assertThat(sourceFolders.size(), equalTo(1)); + SourceFolder classSourceFolder = sourceFolders.iterator().next(); + assertThat(classSourceFolder.getName(), equalTo(sourceFolder.getAbsolutePath())); + Iterator classFiles = classSourceFolder.getFiles().iterator(); + assertClassFile(classFiles.next(), "File1", ClassLoaderFile.Kind.ADDED); + assertClassFile(classFiles.next(), "File2", ClassLoaderFile.Kind.MODIFIED); + assertClassFile(classFiles.next(), null, ClassLoaderFile.Kind.DELETED); + assertThat(classFiles.hasNext(), equalTo(false)); + } + + private void assertClassFile(ClassLoaderFile file, String content, Kind kind) { + assertThat(file.getContents(), + equalTo(content == null ? null : content.getBytes())); + assertThat(file.getKind(), equalTo(kind)); + } + + private File createFile(File sourceFolder, String name) throws IOException { + File file = new File(sourceFolder, name); + FileCopyUtils.copy(name.getBytes(), file); + return file; + } + + private ClassLoaderFiles deserialize(byte[] bytes) throws IOException, + ClassNotFoundException { + ObjectInputStream objectInputStream = new ObjectInputStream( + new ByteArrayInputStream(bytes)); + return (ClassLoaderFiles) objectInputStream.readObject(); + } + +} diff --git a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/client/RemoteClientConfigurationTests.java b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/client/RemoteClientConfigurationTests.java index ecd6a4362fa..539c8f8f638 100644 --- a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/client/RemoteClientConfigurationTests.java +++ b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/client/RemoteClientConfigurationTests.java @@ -72,6 +72,13 @@ public class RemoteClientConfigurationTests { } } + @Test + public void warnIfRestartDisabled() throws Exception { + configure("spring.developertools.remote.restart.enabled:false"); + assertThat(this.output.toString(), + containsString("Remote restart is not enabled")); + } + @Test public void doesntWarnIfUsingHttps() throws Exception { configureWithRemoteUrl("https://localhost"); diff --git a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/test/MockClientHttpRequestFactory.java b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/test/MockClientHttpRequestFactory.java new file mode 100644 index 00000000000..d9508e5c56c --- /dev/null +++ b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/test/MockClientHttpRequestFactory.java @@ -0,0 +1,133 @@ +/* + * 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.test; + +import java.io.IOException; +import java.net.URI; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.mock.http.client.MockClientHttpRequest; +import org.springframework.mock.http.client.MockClientHttpResponse; + +/** + * Mock {@link ClientHttpRequestFactory}. + * + * @author Phillip Webb + */ +public class MockClientHttpRequestFactory implements ClientHttpRequestFactory { + + private AtomicLong seq = new AtomicLong(); + + private Deque responses = new ArrayDeque(); + + private List executedRequests = new ArrayList(); + + @Override + public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) + throws IOException { + return new MockRequest(uri, httpMethod); + } + + public void willRespond(HttpStatus... response) { + for (HttpStatus status : response) { + this.responses.add(new Response(0, null, status)); + } + } + + public void willRespond(String... response) { + for (String payload : response) { + this.responses.add(new Response(0, payload.getBytes(), HttpStatus.OK)); + } + } + + public void willRespondAfterDelay(int delay, HttpStatus status) { + this.responses.add(new Response(delay, null, status)); + } + + public List getExecutedRequests() { + return this.executedRequests; + } + + private class MockRequest extends MockClientHttpRequest { + + public MockRequest(URI uri, HttpMethod httpMethod) { + super(httpMethod, uri); + } + + @Override + protected ClientHttpResponse executeInternal() throws IOException { + MockClientHttpRequestFactory.this.executedRequests.add(this); + Response response = MockClientHttpRequestFactory.this.responses.pollFirst(); + if (response == null) { + response = new Response(0, null, HttpStatus.GONE); + } + return response.asHttpResponse(MockClientHttpRequestFactory.this.seq); + } + + } + + static class Response { + + private final int delay; + + private final byte[] payload; + + private final HttpStatus status; + + public Response(int delay, byte[] payload, HttpStatus status) { + this.delay = delay; + this.payload = payload; + this.status = status; + } + + public ClientHttpResponse asHttpResponse(AtomicLong seq) { + MockClientHttpResponse httpResponse = new MockClientHttpResponse( + this.payload, this.status); + waitForDelay(); + if (this.payload != null) { + httpResponse.getHeaders().setContentLength(this.payload.length); + httpResponse.getHeaders().setContentType( + MediaType.APPLICATION_OCTET_STREAM); + httpResponse.getHeaders().add("x-seq", + Long.toString(seq.incrementAndGet())); + } + return httpResponse; + } + + private void waitForDelay() { + if (this.delay > 0) { + try { + Thread.sleep(this.delay); + } + catch (InterruptedException ex) { + } + } + } + + } + +}