Browse Source
Add server side component to allow remote updates and restarts to a running application. See gh-3086pull/3077/merge
10 changed files with 895 additions and 0 deletions
@ -0,0 +1,94 @@
@@ -0,0 +1,94 @@
|
||||
/* |
||||
* 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; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,85 @@
@@ -0,0 +1,85 @@
|
||||
/* |
||||
* 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); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,51 @@
@@ -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.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); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,182 @@
@@ -0,0 +1,182 @@
|
||||
/* |
||||
* 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(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,39 @@
@@ -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.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); |
||||
|
||||
} |
||||
@ -0,0 +1,21 @@
@@ -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. |
||||
*/ |
||||
|
||||
/** |
||||
* Remote restart server |
||||
*/ |
||||
package org.springframework.boot.developertools.restart.server; |
||||
|
||||
@ -0,0 +1,102 @@
@@ -0,0 +1,102 @@
|
||||
/* |
||||
* 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; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
|
||||
/* |
||||
* 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); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,128 @@
@@ -0,0 +1,128 @@
|
||||
/* |
||||
* 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(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,138 @@
@@ -0,0 +1,138 @@
|
||||
/* |
||||
* 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; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue