Commit 6ac08aba authored by Phillip Webb's avatar Phillip Webb

Add Restarter server support

Add server side component to allow remote updates and restarts to a
running application.

See gh-3086
parent ada3e1ec
/*
* 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.restart.server;
import java.net.URL;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.util.StringUtils;
/**
* Default implementation of {@link SourceFolderUrlFilter} that attempts to match URLs
* using common naming conventions.
*
* @author Phillip Webb
* @since 1.3.0
*/
public class DefaultSourceFolderUrlFilter implements SourceFolderUrlFilter {
private static final String[] COMMON_ENDINGS = { "/target/classes", "/bin" };
private static final Pattern URL_MODULE_PATTERN = Pattern.compile(".*\\/(.+)\\.jar");
private static final Pattern VERSION_PATTERN = Pattern
.compile("^-\\d+(?:\\.\\d+)*(?:[.-].+)?$");
@Override
public boolean isMatch(String sourceFolder, URL url) {
String jarName = getJarName(url);
if (!StringUtils.hasLength(jarName)) {
return false;
}
return isMatch(sourceFolder, jarName);
}
private String getJarName(URL url) {
Matcher matcher = URL_MODULE_PATTERN.matcher(url.toString());
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
private boolean isMatch(String sourceFolder, String jarName) {
sourceFolder = stripTrailingSlash(sourceFolder);
sourceFolder = stripCommonEnds(sourceFolder);
String[] folders = StringUtils.delimitedListToStringArray(sourceFolder, "/");
for (int i = folders.length - 1; i >= 0; i--) {
if (isFolderMatch(folders[i], jarName)) {
return true;
}
}
return false;
}
private boolean isFolderMatch(String folder, String jarName) {
if (!jarName.startsWith(folder)) {
return false;
}
String version = jarName.substring(folder.length());
return version.isEmpty() || VERSION_PATTERN.matcher(version).matches();
}
private String stripTrailingSlash(String string) {
if (string.endsWith("/")) {
return string.substring(0, string.length() - 1);
}
return string;
}
private String stripCommonEnds(String string) {
for (String ending : COMMON_ENDINGS) {
if (string.endsWith(ending)) {
return string.substring(0, string.length() - ending.length());
}
}
return string;
}
}
/*
* 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.restart.server;
import java.io.IOException;
import java.io.ObjectInputStream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.developertools.restart.classloader.ClassLoaderFiles;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.Assert;
/**
* A HTTP server that can be used to upload updated {@link ClassLoaderFiles} and trigger
* restarts.
*
* @author Phillip Webb
* @since 1.3.0
* @see RestartServer
*/
public class HttpRestartServer {
private static final Log logger = LogFactory.getLog(HttpRestartServer.class);
private final RestartServer server;
/**
* Create a new {@link HttpRestartServer} instance.
* @param sourceFolderUrlFilter the source filter used to link remote folder to the
* local classpath
*/
public HttpRestartServer(SourceFolderUrlFilter sourceFolderUrlFilter) {
Assert.notNull(sourceFolderUrlFilter, "SourceFolderUrlFilter must not be null");
this.server = new RestartServer(sourceFolderUrlFilter);
}
/**
* Create a new {@link HttpRestartServer} instance.
* @param restartServer the underlying restart server
*/
public HttpRestartServer(RestartServer restartServer) {
Assert.notNull(restartServer, "RestartServer must not be null");
this.server = restartServer;
}
/**
* Handle a server request.
* @param request the request
* @param response the response
* @throws IOException
*/
public void handle(ServerHttpRequest request, ServerHttpResponse response)
throws IOException {
try {
Assert.state(request.getHeaders().getContentLength() > 0, "No content");
ObjectInputStream objectInputStream = new ObjectInputStream(request.getBody());
ClassLoaderFiles files = (ClassLoaderFiles) objectInputStream.readObject();
objectInputStream.close();
this.server.updateAndRestart(files);
response.setStatusCode(HttpStatus.OK);
}
catch (Exception ex) {
logger.warn("Unable to handler restart server HTTP request", ex);
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
/*
* 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.restart.server;
import java.io.IOException;
import org.springframework.boot.developertools.remote.server.Handler;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.Assert;
/**
* Adapts {@link HttpRestartServer} to a {@link Handler}.
*
* @author Phillip Webb
* @since 1.3.0
*/
public class HttpRestartServerHandler implements Handler {
private final HttpRestartServer server;
/**
* Create a new {@link HttpRestartServerHandler} instance.
* @param server the server to adapt
*/
public HttpRestartServerHandler(HttpRestartServer server) {
Assert.notNull(server, "Server must not be null");
this.server = server;
}
@Override
public void handle(ServerHttpRequest request, ServerHttpResponse response)
throws IOException {
this.server.handle(request, response);
}
}
/*
* 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.restart.server;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.LinkedHashSet;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.developertools.restart.Restarter;
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.util.Assert;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.ResourceUtils;
/**
* Server used to {@link Restarter restart} the current application with updated
* {@link ClassLoaderFiles}.
*
* @author Phillip Webb
* @since 1.3.0
*/
public class RestartServer {
private static final Log logger = LogFactory.getLog(RestartServer.class);
private final SourceFolderUrlFilter sourceFolderUrlFilter;
private final ClassLoader classLoader;
/**
* Create a new {@link RestartServer} instance.
* @param sourceFolderUrlFilter the source filter used to link remote folder to the
* local classpath
*/
public RestartServer(SourceFolderUrlFilter sourceFolderUrlFilter) {
this(sourceFolderUrlFilter, Thread.currentThread().getContextClassLoader());
}
/**
* Create a new {@link RestartServer} instance.
* @param sourceFolderUrlFilter the source filter used to link remote folder to the
* local classpath
* @param classLoader the application classloader
*/
public RestartServer(SourceFolderUrlFilter sourceFolderUrlFilter,
ClassLoader classLoader) {
Assert.notNull(sourceFolderUrlFilter, "SourceFolderUrlFilter must not be null");
Assert.notNull(classLoader, "ClassLoader must not be null");
this.sourceFolderUrlFilter = sourceFolderUrlFilter;
this.classLoader = classLoader;
}
/**
* Update the current running application with the specified {@link ClassLoaderFiles}
* and trigger a reload.
* @param files updated class loader files
*/
public void updateAndRestart(ClassLoaderFiles files) {
Set<URL> urls = new LinkedHashSet<URL>();
Set<URL> classLoaderUrls = getClassLoaderUrls();
for (SourceFolder folder : files.getSourceFolders()) {
for (Entry<String, ClassLoaderFile> entry : folder.getFilesEntrySet()) {
for (URL url : classLoaderUrls) {
if (updateFileSystem(url, entry.getKey(), entry.getValue())) {
urls.add(url);
}
}
}
urls.addAll(getMatchingUrls(classLoaderUrls, folder.getName()));
}
updateTimeStamp(urls);
restart(urls, files);
}
private boolean updateFileSystem(URL url, String name, ClassLoaderFile classLoaderFile) {
if (!isFolderUrl(url.toString())) {
return false;
}
try {
File folder = ResourceUtils.getFile(url);
File file = new File(folder, name);
if (file.exists() && file.canWrite()) {
if (classLoaderFile.getKind() == Kind.DELETED) {
return file.delete();
}
FileCopyUtils.copy(classLoaderFile.getContents(), file);
return true;
}
}
catch (IOException ex) {
// Ignore
}
return false;
}
private boolean isFolderUrl(String urlString) {
return urlString.startsWith("file:") && urlString.endsWith("/");
}
private Set<URL> getMatchingUrls(Set<URL> urls, String sourceFolder) {
Set<URL> matchingUrls = new LinkedHashSet<URL>();
for (URL url : urls) {
if (this.sourceFolderUrlFilter.isMatch(sourceFolder, url)) {
if (logger.isDebugEnabled()) {
logger.debug("URL " + url + " matched against source folder "
+ sourceFolder);
}
matchingUrls.add(url);
}
}
return matchingUrls;
}
private Set<URL> getClassLoaderUrls() {
Set<URL> urls = new LinkedHashSet<URL>();
ClassLoader classLoader = this.classLoader;
while (classLoader != null) {
if (classLoader instanceof URLClassLoader) {
for (URL url : ((URLClassLoader) classLoader).getURLs()) {
urls.add(url);
}
}
classLoader = classLoader.getParent();
}
return urls;
}
private void updateTimeStamp(Iterable<URL> urls) {
for (URL url : urls) {
updateTimeStamp(url);
}
}
private void updateTimeStamp(URL url) {
try {
URL actualUrl = ResourceUtils.extractJarFileURL(url);
File file = ResourceUtils.getFile(actualUrl, "Jar URL");
file.setLastModified(System.currentTimeMillis());
}
catch (Exception ex) {
// Ignore
}
}
/**
* Called to restart the application.
* @param urls the updated URLs
* @param files the updated files
*/
protected void restart(Set<URL> urls, ClassLoaderFiles files) {
Restarter restarter = Restarter.getInstance();
restarter.addUrls(urls);
restarter.addClassLoaderFiles(files);
restarter.restart();
}
}
/*
* 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.restart.server;
import java.net.URL;
/**
* Filter URLs based on a source folder name. Used to match URLs from the running
* classpath against source folders on a remote system.
*
* @author Phillip Webb
* @since 1.3.0
* @see DefaultSourceFolderUrlFilter
*/
public interface SourceFolderUrlFilter {
/**
* Determine if the specified URL matches a source folder.
* @param sourceFolder the source folder
* @param url the URL to check
* @return {@code true} if the URL matches
*/
boolean isMatch(String sourceFolder, URL url);
}
/*
* 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.
*/
/**
* Remote restart server
*/
package org.springframework.boot.developertools.restart.server;
/*
* 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.restart.server;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.junit.Test;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link DefaultSourceFolderUrlFilter}.
*
* @author Phillip Webb
*/
public class DefaultSourceFolderUrlFilterTests {
private static final String SOURCE_ROOT = "/Users/me/code/some-root/";
private static final List<String> COMMON_POSTFIXES;
static {
List<String> postfixes = new ArrayList<String>();
postfixes.add(".jar");
postfixes.add("-1.3.0.jar");
postfixes.add("-1.3.0-SNAPSHOT.jar");
postfixes.add("-1.3.0.BUILD-SNAPSHOT.jar");
postfixes.add("-1.3.0.M1.jar");
postfixes.add("-1.3.0.RC1.jar");
postfixes.add("-1.3.0.RELEASE.jar");
postfixes.add("-1.3.0.Final.jar");
postfixes.add("-1.3.0.GA.jar");
postfixes.add("-1.3.0.0.0.0.jar");
COMMON_POSTFIXES = Collections.unmodifiableList(postfixes);
}
private DefaultSourceFolderUrlFilter filter = new DefaultSourceFolderUrlFilter();
@Test
public void mavenSourceFolder() throws Exception {
doTest("my-module/target/classes/");
}
@Test
public void gradleEclipseSourceFolder() throws Exception {
doTest("my-module/bin/");
}
@Test
public void unusualSourceFolder() throws Exception {
doTest("my-module/something/quite/quite/mad/");
}
private void doTest(String sourcePostfix) throws MalformedURLException {
doTest(sourcePostfix, "my-module", true);
doTest(sourcePostfix, "my-module-other", false);
doTest(sourcePostfix, "my-module-other-again", false);
doTest(sourcePostfix, "my-module.other", false);
}
private void doTest(String sourcePostfix, String moduleRoot, boolean expected)
throws MalformedURLException {
String sourceFolder = SOURCE_ROOT + sourcePostfix;
for (String postfix : COMMON_POSTFIXES) {
for (URL url : getUrls(moduleRoot + postfix)) {
boolean match = this.filter.isMatch(sourceFolder, url);
assertThat(url + " against " + sourceFolder, match, equalTo(expected));
}
}
}
private List<URL> getUrls(String name) throws MalformedURLException {
List<URL> urls = new ArrayList<URL>();
urls.add(new URL("file:/some/path/" + name));
urls.add(new URL("file:/some/path/" + name + "!/"));
for (String postfix : COMMON_POSTFIXES) {
urls.add(new URL("jar:file:/some/path/lib-module" + postfix + "!/lib/" + name));
urls.add(new URL("jar:file:/some/path/lib-module" + postfix + "!/lib/" + name
+ "!/"));
}
return urls;
}
}
/*
* 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.restart.server;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link HttpRestartServerHandler}.
*
* @author Phillip Webb
*/
public class HttpRestartServerHandlerTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void serverMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Server must not be null");
new HttpRestartServerHandler(null);
}
@Test
public void handleDelegatesToServer() throws Exception {
HttpRestartServer server = mock(HttpRestartServer.class);
HttpRestartServerHandler handler = new HttpRestartServerHandler(server);
ServerHttpRequest request = mock(ServerHttpRequest.class);
ServerHttpResponse response = mock(ServerHttpResponse.class);
handler.handle(request, response);
verify(server).handle(request, response);
}
}
/*
* 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.restart.server;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
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.Mock;
import org.mockito.MockitoAnnotations;
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.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
/**
* Tests for {@link HttpRestartServer}.
*
* @author Phillip Webb
*/
public class HttpRestartServerTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Mock
private RestartServer delegate;
private HttpRestartServer server;
@Captor
private ArgumentCaptor<ClassLoaderFiles> filesCaptor;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
this.server = new HttpRestartServer(this.delegate);
}
@Test
public void sourceFolderUrlFilterMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("SourceFolderUrlFilter must not be null");
new HttpRestartServer((SourceFolderUrlFilter) null);
}
@Test
public void restartServerMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("RestartServer must not be null");
new HttpRestartServer((RestartServer) null);
}
@Test
public void sendClassLoaderFiles() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
ClassLoaderFiles files = new ClassLoaderFiles();
files.addFile("name", new ClassLoaderFile(Kind.ADDED, new byte[0]));
byte[] bytes = serialize(files);
request.setContent(bytes);
this.server.handle(new ServletServerHttpRequest(request),
new ServletServerHttpResponse(response));
verify(this.delegate).updateAndRestart(this.filesCaptor.capture());
assertThat(this.filesCaptor.getValue().getFile("name"), notNullValue());
assertThat(response.getStatus(), equalTo(200));
}
@Test
public void sendNoContent() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
this.server.handle(new ServletServerHttpRequest(request),
new ServletServerHttpResponse(response));
verifyZeroInteractions(this.delegate);
assertThat(response.getStatus(), equalTo(500));
}
@Test
public void sendBadData() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
request.setContent(new byte[] { 0, 0, 0 });
this.server.handle(new ServletServerHttpRequest(request),
new ServletServerHttpResponse(response));
verifyZeroInteractions(this.delegate);
assertThat(response.getStatus(), equalTo(500));
}
private byte[] serialize(Object object) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(object);
oos.close();
return bos.toByteArray();
}
}
/*
* 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.restart.server;
import java.io.File;
import java.io.FileOutputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
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.util.FileCopyUtils;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link RestartServer}.
*
* @author Phillip Webb
*/
public class RestartServerTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Rule
public TemporaryFolder temp = new TemporaryFolder();
@Test
public void sourceFolderUrlFilterMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("SourceFolderUrlFilter must not be null");
new RestartServer((SourceFolderUrlFilter) null);
}
@Test
public void updateAndRestart() throws Exception {
URL url1 = new URL("file:/proj/module-a.jar!/");
URL url2 = new URL("file:/proj/module-b.jar!/");
URL url3 = new URL("file:/proj/module-c.jar!/");
URL url4 = new URL("file:/proj/module-d.jar!/");
URLClassLoader classLoaderA = new URLClassLoader(new URL[] { url1, url2 });
URLClassLoader classLoaderB = new URLClassLoader(new URL[] { url3, url4 },
classLoaderA);
SourceFolderUrlFilter filter = new DefaultSourceFolderUrlFilter();
MockRestartServer server = new MockRestartServer(filter, classLoaderB);
ClassLoaderFiles files = new ClassLoaderFiles();
ClassLoaderFile fileA = new ClassLoaderFile(Kind.ADDED, new byte[0]);
ClassLoaderFile fileB = new ClassLoaderFile(Kind.ADDED, new byte[0]);
files.addFile("my/module-a", "ClassA.class", fileA);
files.addFile("my/module-c", "ClassB.class", fileB);
server.updateAndRestart(files);
Set<URL> expectedUrls = new LinkedHashSet<URL>(Arrays.asList(url1, url3));
assertThat(server.restartUrls, equalTo(expectedUrls));
assertThat(server.restartFiles, equalTo(files));
}
@Test
public void updateSetsJarLastModified() throws Exception {
long startTime = System.currentTimeMillis();
File folder = this.temp.newFolder();
File jarFile = new File(folder, "module-a.jar");
new FileOutputStream(jarFile).close();
jarFile.setLastModified(0);
URL url = jarFile.toURI().toURL();
URLClassLoader classLoader = new URLClassLoader(new URL[] { url });
SourceFolderUrlFilter filter = new DefaultSourceFolderUrlFilter();
MockRestartServer server = new MockRestartServer(filter, classLoader);
ClassLoaderFiles files = new ClassLoaderFiles();
ClassLoaderFile fileA = new ClassLoaderFile(Kind.ADDED, new byte[0]);
files.addFile("my/module-a", "ClassA.class", fileA);
server.updateAndRestart(files);
assertThat(jarFile.lastModified(), greaterThan(startTime - 1000));
}
@Test
public void updateReplacesLocalFilesWhenPossible() throws Exception {
// This is critical for Cloud Foundry support where the application is
// run exploded and resources can be found from the servlet root (outside of the
// classloader)
File folder = this.temp.newFolder();
File classFile = new File(folder, "ClassA.class");
FileCopyUtils.copy("abc".getBytes(), classFile);
URL url = folder.toURI().toURL();
URLClassLoader classLoader = new URLClassLoader(new URL[] { url });
SourceFolderUrlFilter filter = new DefaultSourceFolderUrlFilter();
MockRestartServer server = new MockRestartServer(filter, classLoader);
ClassLoaderFiles files = new ClassLoaderFiles();
ClassLoaderFile fileA = new ClassLoaderFile(Kind.ADDED, "def".getBytes());
files.addFile("my/module-a", "ClassA.class", fileA);
server.updateAndRestart(files);
assertThat(FileCopyUtils.copyToByteArray(classFile), equalTo("def".getBytes()));
}
private static class MockRestartServer extends RestartServer {
public MockRestartServer(SourceFolderUrlFilter sourceFolderUrlFilter,
ClassLoader classLoader) {
super(sourceFolderUrlFilter, classLoader);
}
private Set<URL> restartUrls;
private ClassLoaderFiles restartFiles;
@Override
protected void restart(Set<URL> urls, ClassLoaderFiles files) {
this.restartUrls = urls;
this.restartFiles = files;
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment