Commit 247d2d6a authored by Phillip Webb's avatar Phillip Webb

Merge branch 'gh-3082'

parents 5199eefa 67cc9d00
......@@ -84,6 +84,7 @@
<module>spring-boot</module>
<module>spring-boot-autoconfigure</module>
<module>spring-boot-actuator</module>
<module>spring-boot-developer-tools</module>
<module>spring-boot-docs</module>
<module>spring-boot-starters</module>
<module>spring-boot-cli</module>
......
......@@ -184,6 +184,11 @@
<artifactId>spring-boot-configuration-processor</artifactId>
<version>1.3.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-developer-tools</artifactId>
<version>1.3.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
......
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-parent</artifactId>
<version>1.3.0.BUILD-SNAPSHOT</version>
<relativePath>../spring-boot-parent</relativePath>
</parent>
<artifactId>spring-boot-developer-tools</artifactId>
<name>Spring Boot Developer Tools</name>
<description>Spring Boot Developer Tools</description>
<url>http://projects.spring.io/spring-boot/</url>
<organization>
<name>Pivotal Software, Inc.</name>
<url>http://www.spring.io</url>
</organization>
<properties>
<main.basedir>${basedir}/..</main.basedir>
</properties>
<dependencies>
<!-- Compile -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!-- Optional -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<optional>true</optional>
</dependency>
<!-- Annotation processing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-logging-juli</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-client</artifactId>
<version>${jetty.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>animal-sniffer-maven-plugin</artifactId>
<configuration>
<ignores>
<ignore>org.springframework.boot.developertools.tunnel.server.RemoteDebugPortProvider</ignore>
</ignores>
</configuration>
</plugin>
</plugins>
</build>
</project>
/*
* 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;
import org.springframework.boot.Banner;
import org.springframework.boot.ResourceBanner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.developertools.remote.client.RemoteClientConfiguration;
import org.springframework.boot.developertools.restart.RestartInitializer;
import org.springframework.boot.developertools.restart.Restarter;
import org.springframework.core.io.ClassPathResource;
/**
* Application that can be used to establish a link to remotely running Spring Boot code.
* Allows remote debugging and remote updates (if enabled). This class should be launched
* from within your IDE and should have the same classpath configuration as the locally
* developed application. The remote URL of the application should be provided as a
* non-option argument.
*
* @author Phillip Webb
* @since 1.3.0
* @see RemoteClientConfiguration
*/
public class RemoteSpringApplication {
private void run(String[] args) {
Restarter.initialize(args, RestartInitializer.NONE);
SpringApplication application = new SpringApplication(
RemoteClientConfiguration.class);
application.setWebEnvironment(false);
application.setBanner(getBanner());
application.addListeners(new RemoteUrlPropertyExtractor());
application.run(args);
waitIndefinitely();
}
private Banner getBanner() {
ClassPathResource banner = new ClassPathResource("remote-banner.txt",
RemoteSpringApplication.class);
return new ResourceBanner(banner);
}
private void waitIndefinitely() {
while (true) {
try {
Thread.sleep(1000);
}
catch (InterruptedException ex) {
}
}
}
/**
* Run the {@link RemoteSpringApplication}.
* @param args the program arguments (including the remote URL as a non-option
* argument)
*/
public static void main(String[] args) {
new RemoteSpringApplication().run(args);
}
}
/*
* 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;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.Map;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.CommandLinePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@link ApplicationListener} to extract the remote URL for the
* {@link RemoteSpringApplication} to use.
*
* @author Phillip Webb
*/
class RemoteUrlPropertyExtractor implements
ApplicationListener<ApplicationEnvironmentPreparedEvent> {
private static final String NON_OPTION_ARGS = CommandLinePropertySource.DEFAULT_NON_OPTION_ARGS_PROPERTY_NAME;
@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
ConfigurableEnvironment environment = event.getEnvironment();
String url = environment.getProperty(NON_OPTION_ARGS);
Assert.state(StringUtils.hasLength(url), "No remote URL specified");
Assert.state(url.indexOf(",") == -1, "Multiple URLs specified");
try {
new URI(url);
}
catch (URISyntaxException ex) {
throw new IllegalStateException("Malformed URL '" + url + "'");
}
Map<String, Object> source = Collections.singletonMap("remoteUrl", (Object) url);
PropertySource<?> propertySource = new MapPropertySource("remoteUrl", source);
environment.getPropertySources().addLast(propertySource);
}
}
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.developertools.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Configuration properties for developer tools.
*
* @author Phillip Webb
* @since 1.3.0
*/
@ConfigurationProperties(prefix = "spring.developertools")
public class DeveloperToolsProperties {
private static final String DEFAULT_RESTART_EXCLUDES = "META-INF/resources/**,resource/**,static/**,public/**,templates/**";
private Restart restart = new Restart();
private Livereload livereload = new Livereload();
private RemoteDeveloperToolsProperties remote = new RemoteDeveloperToolsProperties();
public Restart getRestart() {
return this.restart;
}
public Livereload getLivereload() {
return this.livereload;
}
public RemoteDeveloperToolsProperties getRemote() {
return this.remote;
}
/**
* Restart properties
*/
public static class Restart {
/**
* Enable automatic restart.
*/
private boolean enabled = true;
/**
* Patterns that should be excluding for triggering a full restart.
*/
private String exclude = DEFAULT_RESTART_EXCLUDES;
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getExclude() {
return this.exclude;
}
public void setExclude(String exclude) {
this.exclude = exclude;
}
}
/**
* LiveReload properties
*/
public static class Livereload {
/**
* Enable a livereload.com compatible server.
*/
private boolean enabled = true;
/**
* Server port.
*/
private int port = 35729;
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public int getPort() {
return this.port;
}
public void setPort(int port) {
this.port = port;
}
}
}
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.developertools.autoconfigure;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
/**
* {@link BeanFactoryPostProcessor} to add properties that make sense when working
* locally.
*
* @author Phillip Webb
*/
class LocalDeveloperPropertyDefaultsPostProcessor implements BeanFactoryPostProcessor,
EnvironmentAware {
private static final Map<String, Object> PROPERTIES;
static {
Map<String, Object> properties = new HashMap<String, Object>();
properties.put("spring.thymeleaf.cache", "false");
PROPERTIES = Collections.unmodifiableMap(properties);
}
private Environment environment;
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {
if (this.environment instanceof ConfigurableEnvironment) {
postProcessEnvironment((ConfigurableEnvironment) this.environment);
}
}
private void postProcessEnvironment(ConfigurableEnvironment environment) {
PropertySource<?> propertySource = new MapPropertySource("refresh", PROPERTIES);
environment.getPropertySources().addFirst(propertySource);
}
}
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.developertools.autoconfigure;
import java.net.URL;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.developertools.classpath.ClassPathChangedEvent;
import org.springframework.boot.developertools.classpath.ClassPathFileSystemWatcher;
import org.springframework.boot.developertools.classpath.ClassPathRestartStrategy;
import org.springframework.boot.developertools.classpath.PatternClassPathRestartStrategy;
import org.springframework.boot.developertools.livereload.LiveReloadServer;
import org.springframework.boot.developertools.restart.ConditionalOnInitializedRestarter;
import org.springframework.boot.developertools.restart.RestartScope;
import org.springframework.boot.developertools.restart.Restarter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
/**
* {@link EnableAutoConfiguration Auto-configuration} for local development support.
*
* @author Phillip Webb
* @since 1.3.0
*/
@Configuration
@ConditionalOnInitializedRestarter
@EnableConfigurationProperties(DeveloperToolsProperties.class)
public class LocalDeveloperToolsAutoConfiguration {
@Autowired
private DeveloperToolsProperties properties;
@Bean
public static LocalDeveloperPropertyDefaultsPostProcessor localDeveloperPropertyDefaultsPostProcessor() {
return new LocalDeveloperPropertyDefaultsPostProcessor();
}
/**
* Local LiveReload configuration.
*/
@ConditionalOnProperty(prefix = "spring.developertools.livereload", name = "enabled", matchIfMissing = true)
static class LiveReloadConfiguration {
@Autowired
private DeveloperToolsProperties properties;
@Autowired(required = false)
private LiveReloadServer liveReloadServer;
@Bean
@RestartScope
@ConditionalOnMissingBean
public LiveReloadServer liveReloadServer() {
return new LiveReloadServer(this.properties.getLivereload().getPort(),
Restarter.getInstance().getThreadFactory());
}
@EventListener
public void onContextRefreshed(ContextRefreshedEvent event) {
optionalLiveReloadServer().triggerReload();
}
@EventListener
public void onClassPathChanged(ClassPathChangedEvent event) {
if (!event.isRestartRequired()) {
optionalLiveReloadServer().triggerReload();
}
}
@Bean
public OptionalLiveReloadServer optionalLiveReloadServer() {
return new OptionalLiveReloadServer(this.liveReloadServer);
}
}
/**
* Local Restart Configuration.
*/
@ConditionalOnProperty(prefix = "spring.developertools.restart", name = "enabled", matchIfMissing = true)
static class RestartConfiguration {
@Autowired
private DeveloperToolsProperties properties;
@Bean
@ConditionalOnMissingBean
public ClassPathFileSystemWatcher classPathFileSystemWatcher() {
URL[] urls = Restarter.getInstance().getInitialUrls();
return new ClassPathFileSystemWatcher(classPathRestartStrategy(), urls);
}
@Bean
@ConditionalOnMissingBean
public ClassPathRestartStrategy classPathRestartStrategy() {
return new PatternClassPathRestartStrategy(this.properties.getRestart()
.getExclude());
}
@EventListener
public void onClassPathChanged(ClassPathChangedEvent event) {
if (event.isRestartRequired()) {
Restarter.getInstance().restart();
}
}
}
}
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.developertools.autoconfigure;
import javax.annotation.PostConstruct;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.developertools.livereload.LiveReloadServer;
/**
* Manages an optional {@link LiveReloadServer}. The {@link LiveReloadServer} may
* gracefully fail to start (e.g. because of a port conflict) or may be omitted entirely.
*
* @author Phillip Webb
* @since 1.3.0
*/
public class OptionalLiveReloadServer {
private static final Log logger = LogFactory.getLog(OptionalLiveReloadServer.class);
private LiveReloadServer server;
/**
* Create a new {@link OptionalLiveReloadServer} instance.
* @param server the server to manage or {@code null}
*/
public OptionalLiveReloadServer(LiveReloadServer server) {
this.server = server;
}
/**
* {@link PostConstruct} method to start the server if possible.
* @throws Exception
*/
@PostConstruct
public void startServer() throws Exception {
if (this.server != null) {
try {
if (!this.server.isStarted()) {
this.server.start();
}
logger.info("LiveReload server is running on port "
+ this.server.getPort());
}
catch (Exception ex) {
logger.warn("Unable to start LiveReload server");
logger.debug("Live reload start error", ex);
this.server = null;
}
}
}
/**
* Trigger LiveReload if the server is up an running.
*/
public void triggerReload() {
if (this.server != null) {
this.server.triggerReload();
}
}
}
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.developertools.autoconfigure;
import 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.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.developertools.remote.server.AccessManager;
import org.springframework.boot.developertools.remote.server.Dispatcher;
import org.springframework.boot.developertools.remote.server.DispatcherFilter;
import org.springframework.boot.developertools.remote.server.Handler;
import org.springframework.boot.developertools.remote.server.HandlerMapper;
import org.springframework.boot.developertools.remote.server.HttpHeaderAccessManager;
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.boot.developertools.tunnel.server.HttpTunnelServer;
import org.springframework.boot.developertools.tunnel.server.HttpTunnelServerHandler;
import org.springframework.boot.developertools.tunnel.server.RemoteDebugPortProvider;
import org.springframework.boot.developertools.tunnel.server.SocketTargetServerConnection;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.ServerHttpRequest;
/**
* {@link EnableAutoConfiguration Auto-configuration} for remote development support.
*
* @author Phillip Webb
* @author Rob Winch
* @since 1.3.0
*/
@Configuration
@ConditionalOnProperty(prefix = "spring.developertools.remote", name = "secret")
@ConditionalOnClass({ Filter.class, ServerHttpRequest.class })
@EnableConfigurationProperties(DeveloperToolsProperties.class)
public class RemoteDeveloperToolsAutoConfiguration {
private static final Log logger = LogFactory
.getLog(RemoteDeveloperToolsAutoConfiguration.class);
@Autowired
private DeveloperToolsProperties properties;
@Bean
@ConditionalOnMissingBean
public AccessManager remoteDeveloperToolsAccessManager() {
RemoteDeveloperToolsProperties remoteProperties = this.properties.getRemote();
return new HttpHeaderAccessManager(remoteProperties.getSecretHeaderName(),
remoteProperties.getSecret());
}
@Bean
public HandlerMapper remoteDeveloperToolsHealthCheckHandlerMapper() {
Handler handler = new HttpStatusHandler();
return new UrlHandlerMapper(this.properties.getRemote().getContextPath(), handler);
}
@Bean
@ConditionalOnMissingBean
public DispatcherFilter remoteDeveloperToolsDispatcherFilter(
AccessManager accessManager, Collection<HandlerMapper> mappers) {
Dispatcher dispatcher = new Dispatcher(accessManager, mappers);
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);
}
}
/**
* Configuration for remote debug HTTP tunneling.
*/
@ConditionalOnProperty(prefix = "spring.developertools.remote.debug", name = "enabled", matchIfMissing = true)
static class RemoteDebugTunnelConfiguration {
@Autowired
private DeveloperToolsProperties properties;
@Bean
@ConditionalOnMissingBean(name = "remoteDebugHanderMapper")
public UrlHandlerMapper remoteDebugHanderMapper(
@Qualifier("remoteDebugHttpTunnelServer") HttpTunnelServer server) {
String url = this.properties.getRemote().getContextPath() + "/debug";
logger.warn("Listening for remote debug traffic on " + url);
Handler handler = new HttpTunnelServerHandler(server);
return new UrlHandlerMapper(url, handler);
}
@Bean
@ConditionalOnMissingBean(name = "remoteDebugHttpTunnelServer")
public HttpTunnelServer remoteDebugHttpTunnelServer() {
return new HttpTunnelServer(new SocketTargetServerConnection(
new RemoteDebugPortProvider()));
}
}
}
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.developertools.autoconfigure;
/**
* Configuration properties for remote Spring Boot applications.
*
* @author Phillip Webb
* @author Rob Winch
* @since 1.3.0
* @see DeveloperToolsProperties
*/
public class RemoteDeveloperToolsProperties {
public static final String DEFAULT_CONTEXT_PATH = "/.~~spring-boot!~";
public static final String DEFAULT_SECRET_HEADER_NAME = "X-AUTH-TOKEN";
/**
* Context path used to handle the remote connection.
*/
private String contextPath = DEFAULT_CONTEXT_PATH;
/**
* A shared secret required to establish a connection (required to enable remote
* support).
*/
private String secret;
/**
* HTTP header used to transfer the shared secret.
*/
private String secretHeaderName = DEFAULT_SECRET_HEADER_NAME;
private Restart restart = new Restart();
private Debug debug = new Debug();
public String getContextPath() {
return this.contextPath;
}
public void setContextPath(String contextPath) {
this.contextPath = contextPath;
}
public String getSecret() {
return this.secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public String getSecretHeaderName() {
return this.secretHeaderName;
}
public void setSecretHeaderName(String secretHeaderName) {
this.secretHeaderName = secretHeaderName;
}
public Restart getRestart() {
return this.restart;
}
public Debug getDebug() {
return this.debug;
}
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;
}
}
public static class Debug {
public static final Integer DEFAULT_LOCAL_PORT = 8000;
/**
* Enable remote debug support.
*/
private boolean enabled = true;
/**
* Local remote debug server port.
*/
private int localPort = DEFAULT_LOCAL_PORT;
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public int getLocalPort() {
return this.localPort;
}
public void setLocalPort(int localPort) {
this.localPort = localPort;
}
}
}
/*
* 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.
*/
/**
* Auto-configuration for {@code spring-boot-developer-tools}.
*/
package org.springframework.boot.developertools.autoconfigure;
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.developertools.classpath;
import java.util.Set;
import org.springframework.boot.developertools.filewatch.ChangedFiles;
import org.springframework.context.ApplicationEvent;
import org.springframework.util.Assert;
/**
* {@link ApplicationEvent} containing details of a classpath change.
*
* @author Phillip Webb
* @since 1.3.0
* @see ClassPathFileChangeListener
*/
public class ClassPathChangedEvent extends ApplicationEvent {
private final Set<ChangedFiles> changeSet;
private final boolean restartRequired;
/**
* Create a new {@link ClassPathChangedEvent}.
* @param source the source of the event
* @param changeSet the changed files
* @param restartRequired if a restart is required due to the change
*/
public ClassPathChangedEvent(Object source, Set<ChangedFiles> changeSet,
boolean restartRequired) {
super(source);
Assert.notNull(changeSet, "ChangeSet must not be null");
this.changeSet = changeSet;
this.restartRequired = restartRequired;
}
/**
* Return details of the files that changed.
* @return the changed files
*/
public Set<ChangedFiles> getChangeSet() {
return this.changeSet;
}
/**
* Return if an application restart is required due to the change.
* @return if an application restart is required
*/
public boolean isRestartRequired() {
return this.restartRequired;
}
}
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.developertools.classpath;
import java.util.Set;
import org.springframework.boot.developertools.filewatch.ChangedFile;
import org.springframework.boot.developertools.filewatch.ChangedFiles;
import org.springframework.boot.developertools.filewatch.FileChangeListener;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.util.Assert;
/**
* A {@link FileChangeListener} to publish {@link ClassPathChangedEvent
* ClassPathChangedEvents}.
*
* @author Phillip Webb
* @since 1.3.0
* @see ClassPathFileSystemWatcher
*/
public class ClassPathFileChangeListener implements FileChangeListener {
private final ApplicationEventPublisher eventPublisher;
private final ClassPathRestartStrategy restartStrategy;
/**
* Create a new {@link ClassPathFileChangeListener} instance.
* @param eventPublisher the event publisher used send events
* @param restartStrategy the restart strategy to use
*/
public ClassPathFileChangeListener(ApplicationEventPublisher eventPublisher,
ClassPathRestartStrategy restartStrategy) {
Assert.notNull(eventPublisher, "EventPublisher must not be null");
Assert.notNull(restartStrategy, "RestartStrategy must not be null");
this.eventPublisher = eventPublisher;
this.restartStrategy = restartStrategy;
}
@Override
public void onChange(Set<ChangedFiles> changeSet) {
boolean restart = isRestartRequired(changeSet);
ApplicationEvent event = new ClassPathChangedEvent(this, changeSet, restart);
this.eventPublisher.publishEvent(event);
}
private boolean isRestartRequired(Set<ChangedFiles> changeSet) {
for (ChangedFiles changedFiles : changeSet) {
for (ChangedFile changedFile : changedFiles) {
if (this.restartStrategy.isRestartRequired(changedFile)) {
return true;
}
}
}
return false;
}
}
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.developertools.classpath;
import java.net.URL;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.developertools.filewatch.FileSystemWatcher;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.Assert;
import org.springframework.util.ResourceUtils;
/**
* Encapsulates a {@link FileSystemWatcher} to watch the local classpath folders for
* changes.
*
* @author Phillip Webb
* @since 1.3.0
* @see ClassPathFileChangeListener
*/
public class ClassPathFileSystemWatcher implements InitializingBean, DisposableBean,
ApplicationContextAware {
private static final Log logger = LogFactory.getLog(ClassPathFileSystemWatcher.class);
private final FileSystemWatcher fileSystemWatcher;
private ClassPathRestartStrategy restartStrategy;
private ApplicationContext applicationContext;
/**
* Create a new {@link ClassPathFileSystemWatcher} instance.
* @param urls the classpath URLs to watch
*/
public ClassPathFileSystemWatcher(URL[] urls) {
this(new FileSystemWatcher(), null, urls);
}
/**
* Create a new {@link ClassPathFileSystemWatcher} instance.
* @param restartStrategy the classpath restart strategy
* @param urls the URLs to watch
*/
public ClassPathFileSystemWatcher(ClassPathRestartStrategy restartStrategy, URL[] urls) {
this(new FileSystemWatcher(), restartStrategy, urls);
}
/**
* Create a new {@link ClassPathFileSystemWatcher} instance.
* @param fileSystemWatcher the underlying {@link FileSystemWatcher} used to monitor
* the local file system
* @param restartStrategy the classpath restart strategy
* @param urls the URLs to watch
*/
protected ClassPathFileSystemWatcher(FileSystemWatcher fileSystemWatcher,
ClassPathRestartStrategy restartStrategy, URL[] urls) {
Assert.notNull(fileSystemWatcher, "FileSystemWatcher must not be null");
Assert.notNull(urls, "Urls must not be null");
this.fileSystemWatcher = new FileSystemWatcher();
this.restartStrategy = restartStrategy;
addUrls(urls);
}
private void addUrls(URL[] urls) {
for (URL url : urls) {
addUrl(url);
}
}
private void addUrl(URL url) {
if (url.getProtocol().equals("file") && url.getPath().endsWith("/")) {
try {
this.fileSystemWatcher.addSourceFolder(ResourceUtils.getFile(url));
}
catch (Exception ex) {
logger.warn("Unable to watch classpath URL " + url);
logger.trace("Unable to watch classpath URL " + url, ex);
}
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void afterPropertiesSet() throws Exception {
if (this.restartStrategy != null) {
this.fileSystemWatcher.addListener(new ClassPathFileChangeListener(
this.applicationContext, this.restartStrategy));
}
this.fileSystemWatcher.start();
}
@Override
public void destroy() throws Exception {
this.fileSystemWatcher.stop();
}
}
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.developertools.classpath;
import org.springframework.boot.developertools.filewatch.ChangedFile;
/**
* Strategy interface used to determine when a changed classpath file should trigger a
* full application restart. For example, static web resources might not require a full
* restart where as class files would.
*
* @author Phillip Webb
* @since 1.3.0
* @see PatternClassPathRestartStrategy
*/
public interface ClassPathRestartStrategy {
/**
* Return true if a full restart is required.
* @param file the changed file
* @return {@code true} if a full restart is required
*/
boolean isRestartRequired(ChangedFile file);
}
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.developertools.classpath;
import org.springframework.boot.developertools.filewatch.ChangedFile;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
/**
* Ant style pattern based {@link ClassPathRestartStrategy}.
*
* @author Phillip Webb
* @since 1.3.0
* @see ClassPathRestartStrategy
*/
public class PatternClassPathRestartStrategy implements ClassPathRestartStrategy {
private final AntPathMatcher matcher = new AntPathMatcher();
private final String[] excludePatterns;
public PatternClassPathRestartStrategy(String excludePatterns) {
this.excludePatterns = StringUtils
.commaDelimitedListToStringArray(excludePatterns);
}
@Override
public boolean isRestartRequired(ChangedFile file) {
for (String pattern : this.excludePatterns) {
if (this.matcher.match(pattern, file.getRelativeName())) {
return false;
}
}
return true;
}
}
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Support for classpath monitoring
*/
package org.springframework.boot.developertools.classpath;
/*
* 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.filewatch;
import java.io.File;
import org.springframework.util.Assert;
/**
* A single file that has changed.
*
* @author Phillip Webb
* @since 1.3.0
* @see ChangedFiles
*/
public final class ChangedFile {
private final File sourceFolder;
private final File file;
private final Type type;
/**
* Create a new {@link ChangedFile} instance.
* @param sourceFolder the source folder
* @param file the file
* @param type the type of change
*/
public ChangedFile(File sourceFolder, File file, Type type) {
Assert.notNull(sourceFolder, "SourceFolder must not be null");
Assert.notNull(file, "File must not be null");
Assert.notNull(type, "Type must not be null");
this.sourceFolder = sourceFolder;
this.file = file;
this.type = type;
}
/**
* Return the file that was changed.
* @return the file
*/
public File getFile() {
return this.file;
}
/**
* Return the type of change.
* @return the type of change
*/
public Type getType() {
return this.type;
}
/**
* Return the name of the file relative to the source folder.
* @return the relative name
*/
public String getRelativeName() {
String folderName = this.sourceFolder.getAbsoluteFile().getPath();
String fileName = this.file.getAbsoluteFile().getPath();
Assert.state(fileName.startsWith(folderName), "The file " + fileName
+ " is not contained in the source folder " + folderName);
return fileName.substring(folderName.length() + 1);
}
@Override
public int hashCode() {
return this.file.hashCode() * 31 + this.type.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null) {
return false;
}
if (obj instanceof ChangedFile) {
ChangedFile other = (ChangedFile) obj;
return this.file.equals(other.file) && this.type.equals(other.type);
}
return super.equals(obj);
}
@Override
public String toString() {
return this.file + " (" + this.type + ")";
}
/**
* Change types.
*/
public static enum Type {
/**
* A new file has been added.
*/
ADD,
/**
* An existing file has been modified.
*/
MODIFY,
/**
* An existing file has been deleted.
*/
DELETE
}
}
/*
* 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.filewatch;
import java.io.File;
import java.util.Collections;
import java.util.Iterator;
import java.util.Set;
/**
* A collections of files from a specific source folder that have changed.
*
* @author Phillip Webb
* @since 1.3.0
* @see FileChangeListener
* @see ChangedFiles
*/
public final class ChangedFiles implements Iterable<ChangedFile> {
private final File sourceFolder;
private final Set<ChangedFile> files;
public ChangedFiles(File sourceFolder, Set<ChangedFile> files) {
this.sourceFolder = sourceFolder;
this.files = Collections.unmodifiableSet(files);
}
/**
* The source folder being watched.
* @return the source folder
*/
public File getSourceFolder() {
return this.sourceFolder;
}
@Override
public Iterator<ChangedFile> iterator() {
return getFiles().iterator();
}
/**
* The files that have been changed.
* @return the changed files
*/
public Set<ChangedFile> getFiles() {
return this.files;
}
@Override
public int hashCode() {
return this.files.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (obj instanceof ChangedFiles) {
ChangedFiles other = (ChangedFiles) obj;
return this.sourceFolder.equals(other.sourceFolder)
&& this.files.equals(other.files);
}
return super.equals(obj);
}
@Override
public String toString() {
return this.sourceFolder + " " + this.files;
}
}
/*
* 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.filewatch;
import java.util.Set;
/**
* Callback interface when file changes are detected.
*
* @author Andy Clement
* @author Phillip Webb
* @since 1.3.0
*/
public interface FileChangeListener {
/**
* Called when files have been changed.
* @param changeSet a set of the {@link ChangedFiles}
*/
void onChange(Set<ChangedFiles> changeSet);
}
/*
* 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.filewatch;
import java.io.File;
import org.springframework.util.Assert;
/**
* A snapshot of a File at a given point in time.
*
* @author Phillip Webb
*/
class FileSnapshot {
private final File file;
private final boolean exists;
private final long length;
private final long lastModified;
public FileSnapshot(File file) {
Assert.notNull(file, "File must not be null");
Assert.isTrue(file.isFile() || !file.exists(), "File must not be a folder");
this.file = file;
this.exists = file.exists();
this.length = file.length();
this.lastModified = file.lastModified();
}
public File getFile() {
return this.file;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (obj instanceof FileSnapshot) {
FileSnapshot other = (FileSnapshot) obj;
boolean equals = this.file.equals(other.file);
equals &= this.exists == other.exists;
equals &= this.length == other.length;
equals &= this.lastModified == other.lastModified;
return equals;
}
return super.equals(obj);
}
@Override
public int hashCode() {
int hashCode = this.file.hashCode();
hashCode = 31 * hashCode + (this.exists ? 1231 : 1237);
hashCode = 31 * hashCode + (int) (this.length ^ (this.length >>> 32));
hashCode = 31 * hashCode + (int) (this.lastModified ^ (this.lastModified >>> 32));
return hashCode;
}
@Override
public String toString() {
return this.file.toString();
}
}
/*
* 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.filewatch;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.util.Assert;
/**
* Watches specific folders for file changes.
*
* @author Andy Clement
* @author Phillip Webb
* @see FileChangeListener
* @since 1.3.0
*/
public class FileSystemWatcher {
private static final long DEFAULT_IDLE_TIME = 400;
private static final long DEFAULT_QUIET_TIME = 200;
private List<FileChangeListener> listeners = new ArrayList<FileChangeListener>();
private final boolean daemon;
private final long idleTime;
private final long quietTime;
private Thread watchThread;
private AtomicInteger remainingScans = new AtomicInteger(-1);
private Map<File, FolderSnapshot> folders = new LinkedHashMap<File, FolderSnapshot>();
/**
* Create a new {@link FileSystemWatcher} instance.
*/
public FileSystemWatcher() {
this(true, DEFAULT_IDLE_TIME, DEFAULT_QUIET_TIME);
}
/**
* Create a new {@link FileSystemWatcher} instance.
* @param daemon if a daemon thread used to monitor changes
* @param idleTime the amount of time to wait between checking for changes
* @param quietTime the amount of time required after a change has been detected to
* ensure that updates have completed
*/
public FileSystemWatcher(boolean daemon, long idleTime, long quietTime) {
this.daemon = daemon;
this.idleTime = idleTime;
this.quietTime = quietTime;
}
/**
* Add listener for file change events. Cannot be called after the watcher has been
* {@link #start() started}.
* @param fileChangeListener the listener to add
*/
public synchronized void addListener(FileChangeListener fileChangeListener) {
Assert.notNull(fileChangeListener, "FileChangeListener must not be null");
checkNotStarted();
this.listeners.add(fileChangeListener);
}
/**
* Add a source folder to monitor. Cannot be called after the watcher has been
* {@link #start() started}.
* @param folder the folder to monitor
*/
public synchronized void addSourceFolder(File folder) {
Assert.notNull(folder, "Folder must not be null");
Assert.isTrue(folder.isDirectory(), "Folder must not be a file");
checkNotStarted();
this.folders.put(folder, null);
}
private void checkNotStarted() {
Assert.state(this.watchThread == null, "FileSystemWatcher already started");
}
/**
* Start monitoring the source folder for changes.
*/
public synchronized void start() {
saveInitalSnapshots();
if (this.watchThread == null) {
this.watchThread = new Thread() {
@Override
public void run() {
int remainingScans = FileSystemWatcher.this.remainingScans.get();
while (remainingScans > 0 || remainingScans == -1) {
try {
if (remainingScans > 0) {
FileSystemWatcher.this.remainingScans.decrementAndGet();
}
scan();
remainingScans = FileSystemWatcher.this.remainingScans.get();
}
catch (InterruptedException ex) {
}
}
};
};
this.watchThread.setName("File Watcher");
this.watchThread.setDaemon(this.daemon);
this.remainingScans = new AtomicInteger(-1);
this.watchThread.start();
}
}
private void saveInitalSnapshots() {
for (File folder : this.folders.keySet()) {
this.folders.put(folder, new FolderSnapshot(folder));
}
}
private void scan() throws InterruptedException {
Thread.sleep(this.idleTime - this.quietTime);
Set<FolderSnapshot> previous;
Set<FolderSnapshot> current = new HashSet<FolderSnapshot>(this.folders.values());
do {
previous = current;
current = getCurrentSnapshots();
Thread.sleep(this.quietTime);
}
while (!previous.equals(current));
updateSnapshots(current);
}
private Set<FolderSnapshot> getCurrentSnapshots() {
Set<FolderSnapshot> snapshots = new LinkedHashSet<FolderSnapshot>();
for (File folder : this.folders.keySet()) {
snapshots.add(new FolderSnapshot(folder));
}
return snapshots;
}
private void updateSnapshots(Set<FolderSnapshot> snapshots) {
Map<File, FolderSnapshot> updated = new LinkedHashMap<File, FolderSnapshot>();
Set<ChangedFiles> changeSet = new LinkedHashSet<ChangedFiles>();
for (FolderSnapshot snapshot : snapshots) {
FolderSnapshot previous = this.folders.get(snapshot.getFolder());
updated.put(snapshot.getFolder(), snapshot);
ChangedFiles changedFiles = previous.getChangedFiles(snapshot);
if (!changedFiles.getFiles().isEmpty()) {
changeSet.add(changedFiles);
}
}
if (!changeSet.isEmpty()) {
fireListeners(Collections.unmodifiableSet(changeSet));
}
this.folders = updated;
}
private void fireListeners(Set<ChangedFiles> changeSet) {
for (FileChangeListener listener : this.listeners) {
listener.onChange(changeSet);
}
}
/**
* Stop monitoring the source folders.
*/
public synchronized void stop() {
stopAfter(0);
}
/**
* Stop monitoring the source folders.
* @param remainingScans the number of scans remaming
*/
synchronized void stopAfter(int remainingScans) {
Thread thread = this.watchThread;
if (thread != null) {
this.remainingScans.set(remainingScans);
try {
thread.join();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
this.watchThread = null;
}
}
}
/*
* 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.filewatch;
import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.boot.developertools.filewatch.ChangedFile.Type;
import org.springframework.util.Assert;
/**
* A snapshot of a folder at a given point in time.
*
* @author Phillip Webb
*/
class FolderSnapshot {
private static final Set<String> DOT_FOLDERS = Collections
.unmodifiableSet(new HashSet<String>(Arrays.asList(".", "..")));
private final File folder;
private final Date time;
private Set<FileSnapshot> files;
/**
* Create a new {@link FolderSnapshot} for the given folder.
* @param folder the source folder
*/
public FolderSnapshot(File folder) {
Assert.notNull(folder, "Folder must not be null");
Assert.isTrue(folder.isDirectory(), "Folder must not be a file");
this.folder = folder;
this.time = new Date();
Set<FileSnapshot> files = new LinkedHashSet<FileSnapshot>();
collectFiles(folder, files);
this.files = Collections.unmodifiableSet(files);
}
private void collectFiles(File source, Set<FileSnapshot> result) {
File[] children = source.listFiles();
if (children != null) {
for (File child : children) {
if (child.isDirectory() && !DOT_FOLDERS.contains(child.getName())) {
collectFiles(child, result);
}
else if (child.isFile()) {
result.add(new FileSnapshot(child));
}
}
}
}
public ChangedFiles getChangedFiles(FolderSnapshot snapshot) {
Assert.notNull(snapshot, "Snapshot must not be null");
File folder = this.folder;
Assert.isTrue(snapshot.folder.equals(folder), "Snapshot source folder must be '"
+ folder + "'");
Set<ChangedFile> changes = new LinkedHashSet<ChangedFile>();
Map<File, FileSnapshot> previousFiles = getFilesMap();
for (FileSnapshot currentFile : snapshot.files) {
FileSnapshot previousFile = previousFiles.remove(currentFile.getFile());
if (previousFile == null) {
changes.add(new ChangedFile(folder, currentFile.getFile(), Type.ADD));
}
else if (!previousFile.equals(currentFile)) {
changes.add(new ChangedFile(folder, currentFile.getFile(), Type.MODIFY));
}
}
for (FileSnapshot previousFile : previousFiles.values()) {
changes.add(new ChangedFile(folder, previousFile.getFile(), Type.DELETE));
}
return new ChangedFiles(folder, changes);
}
private Map<File, FileSnapshot> getFilesMap() {
Map<File, FileSnapshot> files = new LinkedHashMap<File, FileSnapshot>();
for (FileSnapshot file : this.files) {
files.put(file.getFile(), file);
}
return files;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (obj instanceof FolderSnapshot) {
FolderSnapshot other = (FolderSnapshot) obj;
return this.folder.equals(other.folder) && this.files.equals(other.files);
}
return super.equals(obj);
}
@Override
public int hashCode() {
int hashCode = this.folder.hashCode();
hashCode = 31 * hashCode + this.files.hashCode();
return hashCode;
}
/**
* Return the source folder of this snapshot.
* @return the source folder
*/
public File getFolder() {
return this.folder;
}
@Override
public String toString() {
return this.folder + " snaphost at " + this.time;
}
}
/*
* 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.
*/
/**
* Class to watch the local filesystem for changes.
*/
package org.springframework.boot.developertools.filewatch;
/*
* 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.livereload;
import java.nio.charset.Charset;
/**
* Simple Base64 Encoder.
*
* @author Phillip Webb
*/
class Base64Encoder {
private static final Charset UTF_8 = Charset.forName("UTF-8");
private static final String ALPHABET_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "abcdefghijklmnopqrstuvwxyz0123456789+/";
static final byte[] ALPHABET = ALPHABET_CHARS.getBytes(UTF_8);
private static final byte EQUALS_SIGN = '=';
public static String encode(String string) {
return encode(string.getBytes(UTF_8));
}
public static String encode(byte[] bytes) {
byte[] encoded = new byte[bytes.length / 3 * 4 + (bytes.length % 3 == 0 ? 0 : 4)];
for (int i = 0; i < encoded.length; i += 3) {
encodeBlock(bytes, i, Math.min((bytes.length - i), 3), encoded, i / 3 * 4);
}
return new String(encoded, UTF_8);
}
private static void encodeBlock(byte[] src, int srcPos, int blockLen, byte[] dest,
int destPos) {
if (blockLen > 0) {
int inBuff = (blockLen > 0 ? ((src[srcPos] << 24) >>> 8) : 0)
| (blockLen > 1 ? ((src[srcPos + 1] << 24) >>> 16) : 0)
| (blockLen > 2 ? ((src[srcPos + 2] << 24) >>> 24) : 0);
for (int i = 0; i < 4; i++) {
dest[destPos + i] = (i > blockLen ? EQUALS_SIGN
: ALPHABET[(inBuff >>> (6 * (3 - i))) & 0x3f]);
}
}
}
}
/*
* Copyright 2012-2014 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.livereload;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* A {@link LiveReloadServer} connection.
*/
class Connection {
private static Log logger = LogFactory.getLog(Connection.class);
private static final Pattern WEBSOCKET_KEY_PATTERN = Pattern.compile(
"^Sec-WebSocket-Key:(.*)$", Pattern.MULTILINE);
public final static String WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
private final Socket socket;
private final ConnectionInputStream inputStream;
private final ConnectionOutputStream outputStream;
private final String header;
private volatile boolean webSocket;
private volatile boolean running = true;
/**
* Create a new {@link Connection} instance.
* @param socket the source socket
* @param inputStream the socket input stream
* @param outputStream the socket output stream
* @throws IOException
*/
public Connection(Socket socket, InputStream inputStream, OutputStream outputStream)
throws IOException {
this.socket = socket;
this.inputStream = new ConnectionInputStream(inputStream);
this.outputStream = new ConnectionOutputStream(outputStream);
this.header = this.inputStream.readHeader();
logger.debug("Established livereload connection [" + this.header + "]");
}
/**
* Run the connection.
* @throws Exception
*/
public void run() throws Exception {
if (this.header.contains("Upgrade: websocket")
&& this.header.contains("Sec-WebSocket-Version: 13")) {
runWebSocket(this.header);
}
if (this.header.contains("GET /livereload.js")) {
this.outputStream.writeHttp(getClass().getResourceAsStream("livereload.js"),
"text/javascript");
}
}
private void runWebSocket(String header) throws Exception {
String accept = getWebsocketAcceptResponse();
this.outputStream.writeHeaders("HTTP/1.1 101 Switching Protocols",
"Upgrade: websocket", "Connection: Upgrade", "Sec-WebSocket-Accept: "
+ accept);
new Frame("{\"command\":\"hello\",\"protocols\":"
+ "[\"http://livereload.com/protocols/official-7\"],"
+ "\"serverName\":\"spring-boot\"}").write(this.outputStream);
Thread.sleep(100);
this.webSocket = true;
while (this.running) {
readWebSocketFrame();
}
}
private void readWebSocketFrame() throws IOException {
try {
Frame frame = Frame.read(this.inputStream);
if (frame.getType() == Frame.Type.PING) {
writeWebSocketFrame(new Frame(Frame.Type.PONG));
}
else if (frame.getType() == Frame.Type.CLOSE) {
throw new ConnectionClosedException();
}
else if (frame.getType() == Frame.Type.TEXT) {
logger.debug("Recieved LiveReload text frame " + frame);
}
else {
throw new IOException("Unexpected Frame Type " + frame.getType());
}
}
catch (SocketTimeoutException ex) {
writeWebSocketFrame(new Frame(Frame.Type.PING));
Frame frame = Frame.read(this.inputStream);
if (frame.getType() != Frame.Type.PONG) {
throw new IllegalStateException("No Pong");
}
}
}
/**
* Trigger livereload for the client using this connection.
* @throws IOException
*/
public void triggerReload() throws IOException {
if (this.webSocket) {
logger.debug("Triggering LiveReload");
writeWebSocketFrame(new Frame("{\"command\":\"reload\",\"path\":\"/\"}"));
}
}
private synchronized void writeWebSocketFrame(Frame frame) throws IOException {
frame.write(this.outputStream);
}
private String getWebsocketAcceptResponse() throws NoSuchAlgorithmException {
Matcher matcher = WEBSOCKET_KEY_PATTERN.matcher(this.header);
if (!matcher.find()) {
throw new IllegalStateException("No Sec-WebSocket-Key");
}
String response = matcher.group(1).trim() + WEBSOCKET_GUID;
MessageDigest messageDigest = MessageDigest.getInstance("SHA-1");
messageDigest.update(response.getBytes(), 0, response.length());
return Base64Encoder.encode(messageDigest.digest());
}
/**
* Close the connection.
* @throws IOException
*/
public void close() throws IOException {
this.running = false;
this.socket.close();
}
}
/*
* 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.livereload;
import java.io.IOException;
/**
* Exception throw when the client closes the connection.
*
* @author Phillip Webb
*/
class ConnectionClosedException extends IOException {
public ConnectionClosedException() {
super("Connection closed");
}
}
/*
* Copyright 2012-2014 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.livereload;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* {@link InputStream} for a server connection.
*
* @author Phillip Webb
*/
class ConnectionInputStream extends FilterInputStream {
private static final String HEADER_END = "\r\n\r\n";
private static final int BUFFER_SIZE = 4096;
public ConnectionInputStream(InputStream in) {
super(in);
}
/**
* Read the HTTP header from the {@link InputStream}. Note: This method doesn't expect
* any HTTP content after the header since the initial request is usually just a
* WebSocket upgrade.
* @return the HTTP header
* @throws IOException
*/
public String readHeader() throws IOException {
byte[] buffer = new byte[BUFFER_SIZE];
StringBuffer content = new StringBuffer(BUFFER_SIZE);
while (content.indexOf(HEADER_END) == -1) {
int amountRead = checkedRead(buffer, 0, BUFFER_SIZE);
content.append(new String(buffer, 0, amountRead));
}
return content.substring(0, content.indexOf(HEADER_END)).toString();
}
/**
* Repeatedly read the underlying {@link InputStream} until the requested number of
* bytes have been loaded.
* @param buffer the destination buffer
* @param offset the buffer offset
* @param length the amount of data to read
* @throws IOException
*/
public void readFully(byte[] buffer, int offset, int length) throws IOException {
while (length > 0) {
int amountRead = checkedRead(buffer, offset, length);
offset += amountRead;
length -= amountRead;
}
}
/**
* Read a single byte from the stream (checking that the end of the stream hasn't been
* reached.
* @return the content
* @throws IOException
*/
public int checkedRead() throws IOException {
int b = read();
if (b == -1) {
throw new IOException("End of stream");
}
return (b & 0xff);
}
/**
* Read a a number of bytes from the stream (checking that the end of the stream
* hasn't been reached)
* @param buffer the destination buffer
* @param offset the buffer offset
* @param length the length to read
* @return the amount of data read
* @throws IOException
*/
public int checkedRead(byte[] buffer, int offset, int length) throws IOException {
int amountRead = read(buffer, offset, length);
if (amountRead == -1) {
throw new IOException("End of stream");
}
return amountRead;
}
}
/*
* Copyright 2012-2014 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.livereload;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.springframework.util.FileCopyUtils;
/**
* {@link OutputStream} for a server connection.
*
* @author Phillip Webb
*/
class ConnectionOutputStream extends FilterOutputStream {
public ConnectionOutputStream(OutputStream out) {
super(out);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
this.out.write(b, off, len);
}
public void writeHttp(InputStream content, String contentType) throws IOException {
byte[] bytes = FileCopyUtils.copyToByteArray(content);
writeHeaders("HTTP/1.1 200 OK", "Content-Type: " + contentType,
"Content-Length: " + bytes.length, "Connection: close");
write(bytes);
flush();
}
public void writeHeaders(String... headers) throws IOException {
StringBuilder response = new StringBuilder();
for (String header : headers) {
response.append(header).append("\r\n");
}
response.append("\r\n");
write(response.toString().getBytes());
}
}
/*
* Copyright 2012-2014 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.livereload;
import java.io.IOException;
import java.io.OutputStream;
import org.springframework.util.Assert;
/**
* A limited implementation of a WebSocket Frame used to carry LiveReload data.
*
* @author Phillip Webb
*/
class Frame {
private static final byte[] NO_BYTES = new byte[0];
private final Type type;
private final byte[] payload;
/**
* Create a new {@link Type#TEXT text} {@link Frame} instance with the specified
* payload.
* @param payload the text payload
*/
public Frame(String payload) {
Assert.notNull(payload, "Payload must not be null");
this.type = Type.TEXT;
this.payload = payload.getBytes();
}
public Frame(Type type) {
Assert.notNull(type, "Type must not be null");
this.type = type;
this.payload = NO_BYTES;
}
private Frame(Type type, byte[] payload) {
this.type = type;
this.payload = payload;
}
public Type getType() {
return this.type;
}
public byte[] getPayload() {
return this.payload;
}
@Override
public String toString() {
return new String(this.payload);
}
public void write(OutputStream outputStream) throws IOException {
outputStream.write(0x80 | this.type.code);
if (this.payload.length < 126) {
outputStream.write(0x00 | (this.payload.length & 0x7F));
}
else {
outputStream.write(0x7E);
outputStream.write(this.payload.length >> 8 & 0xFF);
outputStream.write(this.payload.length >> 0 & 0xFF);
}
outputStream.write(this.payload);
outputStream.flush();
}
public static Frame read(ConnectionInputStream inputStream) throws IOException {
int firstByte = inputStream.checkedRead();
Assert.state((firstByte & 0x80) != 0, "Fragmented frames are not supported");
int maskAndLength = inputStream.checkedRead();
boolean hasMask = (maskAndLength & 0x80) != 0;
int length = (maskAndLength & 0x7F);
Assert.state(length != 127, "Large frames are not supported");
if (length == 126) {
length = ((inputStream.checkedRead()) << 8 | inputStream.checkedRead());
}
byte[] mask = new byte[4];
if (hasMask) {
inputStream.readFully(mask, 0, mask.length);
}
byte[] payload = new byte[length];
inputStream.readFully(payload, 0, length);
if (hasMask) {
for (int i = 0; i < payload.length; i++) {
payload[i] ^= mask[i % 4];
}
}
return new Frame(Type.forCode(firstByte & 0x0F), payload);
}
public static enum Type {
/**
* Continuation frame.
*/
CONTINUATION(0x00),
/**
* Text frame.
*/
TEXT(0x01),
/**
* Binary frame.
*/
BINARY(0x02),
/**
* Close frame.
*/
CLOSE(0x08),
/**
* Ping frame.
*/
PING(0x09),
/**
* Pong frame.
*/
PONG(0x0A);
private final int code;
private Type(int code) {
this.code = code;
}
public static Type forCode(int code) {
for (Type type : values()) {
if (type.code == code) {
return type;
}
}
throw new IllegalStateException("Unknown code " + code);
}
}
}
/*
* Copyright 2012-2014 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.livereload;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.Assert;
/**
* A <a href="http://livereload.com">livereload</a> server.
*
* @author Phillip Webb
* @see <a href="http://livereload.com">livereload.com</a>
* @since 1.3.0
*/
public class LiveReloadServer {
/**
* The default live reload server port.
*/
public static final int DEFAULT_PORT = 35729;
private static Log logger = LogFactory.getLog(LiveReloadServer.class);
private static final int READ_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(4);
private final int port;
private final ThreadFactory threadFactory;
private ServerSocket serverSocket;
private Thread listenThread;
private ExecutorService executor = Executors
.newCachedThreadPool(new WorkerThreadFactory());
private List<Connection> connections = new ArrayList<Connection>();
/**
* Create a new {@link LiveReloadServer} listening on the default port.
*/
public LiveReloadServer() {
this(DEFAULT_PORT);
}
/**
* Create a new {@link LiveReloadServer} listening on the default port with a specific
* {@link ThreadFactory}.
* @param threadFactory the thread factory
*/
public LiveReloadServer(ThreadFactory threadFactory) {
this(DEFAULT_PORT, threadFactory);
}
/**
* Create a new {@link LiveReloadServer} listening on the specified port.
* @param port the listen port
*/
public LiveReloadServer(int port) {
this(port, new ThreadFactory() {
@Override
public Thread newThread(Runnable runnable) {
return new Thread(runnable);
}
});
}
/**
* Create a new {@link LiveReloadServer} listening on the specified port with a
* specific {@link ThreadFactory}.
* @param port the listen port
* @param threadFactory the thread factory
*/
public LiveReloadServer(int port, ThreadFactory threadFactory) {
this.port = port;
this.threadFactory = threadFactory;
}
/**
* Start the livereload server and accept incoming connections.
* @throws IOException
*/
public synchronized void start() throws IOException {
Assert.state(!isStarted(), "Server already started");
logger.debug("Starting live reload server on port " + this.port);
this.serverSocket = new ServerSocket(this.port);
this.listenThread = this.threadFactory.newThread(new Runnable() {
@Override
public void run() {
acceptConnections();
}
});
this.listenThread.setDaemon(true);
this.listenThread.setName("Live Reload Server");
this.listenThread.start();
}
/**
* Return if the server has been started.
* @return {@code true} if the server is running
*/
public synchronized boolean isStarted() {
return this.listenThread != null;
}
/**
* Return the port that the server is listening on
* @return the server port
*/
public int getPort() {
return this.port;
}
private void acceptConnections() {
do {
try {
Socket socket = this.serverSocket.accept();
socket.setSoTimeout(READ_TIMEOUT);
this.executor.execute(new ConnectionHandler(socket));
}
catch (SocketTimeoutException ex) {
// Ignore
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("LiveReload server error", ex);
}
}
}
while (!this.serverSocket.isClosed());
}
/**
* Gracefully stop the livereload server.
* @throws IOException
*/
public synchronized void stop() throws IOException {
if (this.listenThread != null) {
closeAllConnections();
try {
this.executor.shutdown();
this.executor.awaitTermination(1, TimeUnit.MINUTES);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
this.serverSocket.close();
try {
this.listenThread.join();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
this.listenThread = null;
this.serverSocket = null;
}
}
private void closeAllConnections() throws IOException {
synchronized (this.connections) {
for (Connection connection : this.connections) {
connection.close();
}
}
}
/**
* Trigger livereload of all connected clients.
*/
public void triggerReload() {
synchronized (this.connections) {
for (Connection connection : this.connections) {
try {
connection.triggerReload();
}
catch (Exception ex) {
logger.debug("Unable to send reload message", ex);
}
}
}
}
private void addConnection(Connection connection) {
synchronized (this.connections) {
this.connections.add(connection);
}
}
private void removeConnection(Connection connection) {
synchronized (this.connections) {
this.connections.remove(connection);
}
}
/**
* Factory method used to create the {@link Connection}.
* @param socket the source socket
* @param inputStream the socket input stream
* @param outputStream the socket output stream
* @return a connection
* @throws IOException
*/
protected Connection createConnection(Socket socket, InputStream inputStream,
OutputStream outputStream) throws IOException {
return new Connection(socket, inputStream, outputStream);
}
/**
* {@link Runnable} to handle a single connection.
* @see Connection
*/
private class ConnectionHandler implements Runnable {
private final Socket socket;
private final InputStream inputStream;
public ConnectionHandler(Socket socket) throws IOException {
this.socket = socket;
this.inputStream = socket.getInputStream();
}
@Override
public void run() {
try {
handle();
}
catch (ConnectionClosedException ex) {
logger.debug("LiveReload connection closed");
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("LiveReload error", ex);
}
}
}
private void handle() throws Exception {
try {
try {
OutputStream outputStream = this.socket.getOutputStream();
try {
Connection connection = createConnection(this.socket,
this.inputStream, outputStream);
runConnection(connection);
}
finally {
outputStream.close();
}
}
finally {
this.inputStream.close();
}
}
finally {
this.socket.close();
}
}
private void runConnection(Connection connection) throws IOException, Exception {
try {
addConnection(connection);
connection.run();
}
finally {
removeConnection(connection);
}
}
}
/**
* {@link ThreadFactory} to create the worker threads,
*/
private static class WorkerThreadFactory implements ThreadFactory {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setDaemon(true);
thread.setName("Live Reload #" + this.threadNumber.getAndIncrement());
return thread;
}
}
}
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Support for the livereload protocol.
*/
package org.springframework.boot.developertools.livereload;
/*
* 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.
*/
/**
* Spring Boot developer tools.
*/
package org.springframework.boot.developertools;
/*
* 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<ClassPathChangedEvent> {
private static final Map<ChangedFile.Type, ClassLoaderFile.Kind> TYPE_MAPPINGS;
static {
Map<ChangedFile.Type, ClassLoaderFile.Kind> map = new HashMap<ChangedFile.Type, ClassLoaderFile.Kind>();
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);
}
}
/*
* 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.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.developertools.autoconfigure.OptionalLiveReloadServer;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.Assert;
/**
* {@link Runnable} that waits to triggers live reload until the remote server has
* restarted.
*
* @author Phillip Webb
*/
class DelayedLiveReloadTrigger implements Runnable {
private static final long SHUTDOWN_TIME = 1000;
private static final long SLEEP_TIME = 500;
private static final long TIMEOUT = 30000;
private static final Log logger = LogFactory.getLog(DelayedLiveReloadTrigger.class);
private final OptionalLiveReloadServer liveReloadServer;
private final ClientHttpRequestFactory requestFactory;
private final URI uri;
private long shutdownTime = SHUTDOWN_TIME;
private long sleepTime = SLEEP_TIME;
private long timeout = TIMEOUT;
public DelayedLiveReloadTrigger(OptionalLiveReloadServer liveReloadServer,
ClientHttpRequestFactory requestFactory, String url) {
Assert.notNull(liveReloadServer, "LiveReloadServer must not be null");
Assert.notNull(requestFactory, "RequestFactory must not be null");
Assert.hasLength(url, "URL must not be empty");
this.liveReloadServer = liveReloadServer;
this.requestFactory = requestFactory;
try {
this.uri = new URI(url);
}
catch (URISyntaxException ex) {
throw new IllegalArgumentException(ex);
}
}
protected void setTimings(long shutdown, long sleep, long timeout) {
this.shutdownTime = shutdown;
this.sleepTime = sleep;
this.timeout = timeout;
}
@Override
public void run() {
try {
Thread.sleep(this.shutdownTime);
long start = System.currentTimeMillis();
while (!isUp()) {
long runTime = System.currentTimeMillis() - start;
if (runTime > this.timeout) {
return;
}
Thread.sleep(this.sleepTime);
}
logger.info("Remote server has changed, triggering LiveReload");
this.liveReloadServer.triggerReload();
}
catch (InterruptedException ex) {
}
}
private boolean isUp() {
try {
ClientHttpRequest request = createRequest();
ClientHttpResponse response = request.execute();
return response.getStatusCode() == HttpStatus.OK;
}
catch (Exception ex) {
return false;
}
}
private ClientHttpRequest createRequest() throws IOException {
return this.requestFactory.createRequest(this.uri, HttpMethod.GET);
}
}
/*
* 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.IOException;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.Assert;
/**
* {@link ClientHttpRequestInterceptor} to populate arbitrary HTTP headers with a value.
* For example, it might be used to provide an X-AUTH-TOKEN and value for security
* purposes.
*
* @author Rob Winch
* @since 1.3.0
*/
public class HttpHeaderInterceptor implements ClientHttpRequestInterceptor {
private final String name;
private final String value;
/**
* Creates a new {@link HttpHeaderInterceptor} instance.
* @param name the header name to populate. Cannot be null or empty.
* @param value the header value to populate. Cannot be null or empty.
*/
public HttpHeaderInterceptor(String name, String value) {
Assert.hasLength(name, "Name must not be empty");
Assert.hasLength(value, "Value" + " must not be empty");
this.name = name;
this.value = value;
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
request.getHeaders().add(this.name, this.value);
return execution.execute(request, body);
}
}
/*
* 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 javax.net.ServerSocketFactory;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.bind.RelaxedPropertyResolver;
import org.springframework.boot.developertools.autoconfigure.RemoteDeveloperToolsProperties;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* Condition used to check that the actual local port is available.
*/
class LocalDebugPortAvailableCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(
context.getEnvironment(), "spring.developertools.remote.debug.");
Integer port = resolver.getProperty("local-port", Integer.class);
if (port == null) {
port = RemoteDeveloperToolsProperties.Debug.DEFAULT_LOCAL_PORT;
}
if (isPortAvailable(port)) {
return ConditionOutcome.match("Local debug port availble");
}
return ConditionOutcome.noMatch("Local debug port unavailble");
}
private boolean isPortAvailable(int port) {
try {
ServerSocketFactory.getDefault().createServerSocket(port).close();
return true;
}
catch (Exception ex) {
return false;
}
}
}
/*
* 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.nio.channels.SocketChannel;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.developertools.tunnel.client.TunnelClientListener;
/**
* {@link TunnelClientListener} to log open/close events.
*
* @author Phillip Webb
*/
class LoggingTunnelClientListener implements TunnelClientListener {
private static final Log logger = LogFactory
.getLog(LoggingTunnelClientListener.class);
@Override
public void onOpen(SocketChannel socket) {
logger.info("Remote debug connection opened");
}
@Override
public void onClose(SocketChannel socket) {
logger.info("Remote debug connection closed");
}
}
/*
* 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.net.URL;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.annotation.PostConstruct;
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.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.developertools.autoconfigure.DeveloperToolsProperties;
import org.springframework.boot.developertools.autoconfigure.OptionalLiveReloadServer;
import org.springframework.boot.developertools.autoconfigure.RemoteDeveloperToolsProperties;
import org.springframework.boot.developertools.classpath.ClassPathChangedEvent;
import org.springframework.boot.developertools.classpath.ClassPathFileSystemWatcher;
import org.springframework.boot.developertools.classpath.ClassPathRestartStrategy;
import org.springframework.boot.developertools.classpath.PatternClassPathRestartStrategy;
import org.springframework.boot.developertools.livereload.LiveReloadServer;
import org.springframework.boot.developertools.restart.DefaultRestartInitializer;
import org.springframework.boot.developertools.restart.RestartScope;
import org.springframework.boot.developertools.restart.Restarter;
import org.springframework.boot.developertools.tunnel.client.HttpTunnelConnection;
import org.springframework.boot.developertools.tunnel.client.TunnelClient;
import org.springframework.boot.developertools.tunnel.client.TunnelConnection;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.InterceptingClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.util.Assert;
/**
* Configuration used to connect to remote Spring Boot applications.
*
* @author Phillip Webb
* @since 1.3.0
* @see org.springframework.boot.developertools.RemoteSpringApplication
*/
@Configuration
@EnableConfigurationProperties(DeveloperToolsProperties.class)
public class RemoteClientConfiguration {
private static final Log logger = LogFactory.getLog(RemoteClientConfiguration.class);
@Autowired
private DeveloperToolsProperties properties;
@Value("${remoteUrl}")
private String remoteUrl;
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
@Bean
public ClientHttpRequestFactory clientHttpRequestFactory() {
List<ClientHttpRequestInterceptor> interceptors = Arrays
.asList(getSecurityInterceptor());
return new InterceptingClientHttpRequestFactory(
new SimpleClientHttpRequestFactory(), interceptors);
}
private ClientHttpRequestInterceptor getSecurityInterceptor() {
RemoteDeveloperToolsProperties remoteProperties = this.properties.getRemote();
String secretHeaderName = remoteProperties.getSecretHeaderName();
String secret = remoteProperties.getSecret();
Assert.state(secret != null,
"The environment value 'spring.developertools.remote.secret' "
+ "is required to secure your connection.");
return new HttpHeaderInterceptor(secretHeaderName, secret);
}
@PostConstruct
private void logWarnings() {
RemoteDeveloperToolsProperties remoteProperties = this.properties.getRemote();
if (!remoteProperties.getDebug().isEnabled()
&& !remoteProperties.getRestart().isEnabled()) {
logger.warn("Remote restart and debug are both disabled.");
}
if (!this.remoteUrl.startsWith("https://")) {
logger.warn("The connection to " + this.remoteUrl
+ " is insecure. You should use a URL starting with 'https://'.");
}
}
/**
* LiveReload configuration.
*/
@ConditionalOnProperty(prefix = "spring.developertools.livereload", name = "enabled", matchIfMissing = true)
static class LiveReloadConfiguration {
@Autowired
private DeveloperToolsProperties properties;
@Autowired(required = false)
private LiveReloadServer liveReloadServer;
@Autowired
private ClientHttpRequestFactory clientHttpRequestFactory;
@Value("${remoteUrl}")
private String remoteUrl;
private ExecutorService executor = Executors.newSingleThreadExecutor();
@Bean
@RestartScope
@ConditionalOnMissingBean
public LiveReloadServer liveReloadServer() {
return new LiveReloadServer(this.properties.getLivereload().getPort(),
Restarter.getInstance().getThreadFactory());
}
@EventListener
public void onClassPathChanged(ClassPathChangedEvent event) {
String url = this.remoteUrl + this.properties.getRemote().getContextPath();
this.executor.execute(new DelayedLiveReloadTrigger(
optionalLiveReloadServer(), this.clientHttpRequestFactory, url));
}
@Bean
public OptionalLiveReloadServer optionalLiveReloadServer() {
return new OptionalLiveReloadServer(this.liveReloadServer);
}
final ExecutorService getExecutor() {
return this.executor;
}
}
/**
* 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);
}
}
/**
* Client configuration for remote debug HTTP tunneling.
*/
@ConditionalOnProperty(prefix = "spring.developertools.remote.debug", name = "enabled", matchIfMissing = true)
@ConditionalOnClass(Filter.class)
@Conditional(LocalDebugPortAvailableCondition.class)
static class RemoteDebugTunnelClientConfiguration {
@Autowired
private DeveloperToolsProperties properties;
@Value("${remoteUrl}")
private String remoteUrl;
@Bean
public TunnelClient remoteDebugTunnelClient(
ClientHttpRequestFactory requestFactory) {
RemoteDeveloperToolsProperties remoteProperties = this.properties.getRemote();
String url = this.remoteUrl + remoteProperties.getContextPath() + "/debug";
TunnelConnection connection = new HttpTunnelConnection(url, requestFactory);
int localPort = remoteProperties.getDebug().getLocalPort();
TunnelClient client = new TunnelClient(localPort, connection);
client.addListener(new LoggingTunnelClientListener());
return client;
}
}
}
/*
* 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.
*/
/**
* Client support for a remotely running Spring Boot application.
*/
package org.springframework.boot.developertools.remote.client;
/*
* 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.server;
import org.springframework.http.server.ServerHttpRequest;
/**
* Provides access control for a {@link Dispatcher}.
*
* @author Phillip Webb
* @since 1.3.0
*/
public interface AccessManager {
/**
* {@link AccessManager} that permits all requests.
*/
public static final AccessManager PERMIT_ALL = new AccessManager() {
@Override
public boolean isAllowed(ServerHttpRequest request) {
return true;
}
};
/**
* Determine if the specific request is allowed to be handled by the
* {@link Dispatcher}.
* @param request the request to check
* @return {@code true} if access is allowed.
*/
boolean isAllowed(ServerHttpRequest request);
}
/*
* 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.server;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.Assert;
/**
* Dispatcher used to route incoming remote server requests to a {@link Handler}. Similar
* to {@code DispatchServlet} in Spring MVC but separate to ensure that remote support can
* be used regardless of any web framework.
*
* @author Phillip Webb
* @since 1.3.0
* @see HandlerMapper
*/
public class Dispatcher {
private final AccessManager accessManager;
private final List<HandlerMapper> mappers;
public Dispatcher(AccessManager accessManager, Collection<HandlerMapper> mappers) {
Assert.notNull(accessManager, "AccessManager must not be null");
Assert.notNull(mappers, "Mappers must not be null");
this.accessManager = accessManager;
this.mappers = new ArrayList<HandlerMapper>(mappers);
AnnotationAwareOrderComparator.sort(this.mappers);
}
/**
* Dispatch the specified request to an appropriate {@link Handler}.
* @param request the request
* @param response the response
* @return {@code true} if the request was dispatched
* @throws IOException
*/
public boolean handle(ServerHttpRequest request, ServerHttpResponse response)
throws IOException {
for (HandlerMapper mapper : this.mappers) {
Handler handler = mapper.getHandler(request);
if (handler != null) {
handle(handler, request, response);
return true;
}
}
return false;
}
private void handle(Handler handler, ServerHttpRequest request,
ServerHttpResponse response) throws IOException {
if (!this.accessManager.isAllowed(request)) {
response.setStatusCode(HttpStatus.FORBIDDEN);
return;
}
handler.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.remote.server;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.util.Assert;
/**
* Servlet filter providing integration with the remote server {@link Dispatcher}.
*
* @author Phillip Webb
* @author Rob Winch
* @since 1.3.0
*/
public class DispatcherFilter implements Filter {
private final Dispatcher dispatcher;
public DispatcherFilter(Dispatcher dispatcher) {
Assert.notNull(dispatcher, "Dispatcher must not be null");
this.dispatcher = dispatcher;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
if (request instanceof HttpServletRequest
&& response instanceof HttpServletResponse) {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
else {
chain.doFilter(request, response);
}
}
private void doFilter(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
ServerHttpRequest serverRequest = new ServletServerHttpRequest(request);
ServerHttpResponse serverResponse = new ServletServerHttpResponse(response);
if (!this.dispatcher.handle(serverRequest, serverResponse)) {
chain.doFilter(request, response);
}
}
@Override
public void destroy() {
}
}
/*
* 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.server;
import java.io.IOException;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
/**
* A single handler that is able to process an incoming remote server request.
*
* @author Phillip Webb
* @since 1.3.0
*/
public interface Handler {
/**
* Handle the request.
* @param request the request
* @param response the response
* @throws IOException
*/
void handle(ServerHttpRequest request, ServerHttpResponse response)
throws IOException;
}
/*
* 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.server;
import org.springframework.http.server.ServerHttpRequest;
/**
* Interface to provide a mapping between a {@link ServerHttpRequest} and a
* {@link Handler}.
*
* @author Phillip Webb
* @since 1.3.0
*/
public interface HandlerMapper {
/**
* Return the handler for the given request or {@code null}.
* @param request the request
* @return a {@link Handler} or {@code null}
*/
Handler getHandler(ServerHttpRequest request);
}
/*
* 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.server;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.util.Assert;
/**
* {@link AccessManager} that checks for the presence of a HTTP header secret.
*
* @author Rob Winch
* @author Phillip Webb
* @since 1.3.0
*/
public class HttpHeaderAccessManager implements AccessManager {
private final String headerName;
private final String expectedSecret;
public HttpHeaderAccessManager(String headerName, String expectedSecret) {
Assert.hasLength(headerName, "HeaderName must not be empty");
Assert.hasLength(expectedSecret, "ExpectedSecret must not be empty");
this.headerName = headerName;
this.expectedSecret = expectedSecret;
}
@Override
public boolean isAllowed(ServerHttpRequest request) {
String providedSecret = request.getHeaders().getFirst(this.headerName);
return this.expectedSecret.equals(providedSecret);
}
}
/*
* 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.server;
import java.io.IOException;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.Assert;
/**
* {@link Handler} that responds with a specific {@link HttpStatus}.
*
* @author Phillip Webb
*/
public class HttpStatusHandler implements Handler {
private final HttpStatus status;
/**
* Create a new {@link HttpStatusHandler} instance that will respond with a HTTP OK 200
* status.
*/
public HttpStatusHandler() {
this(HttpStatus.OK);
}
/**
* Create a new {@link HttpStatusHandler} instance that will respond with the specified
* status.
* @param status the status
*/
public HttpStatusHandler(HttpStatus status) {
Assert.notNull(status, "Status must not be null");
this.status = status;
}
@Override
public void handle(ServerHttpRequest request, ServerHttpResponse response)
throws IOException {
response.setStatusCode(this.status);
}
}
/*
* 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.server;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.util.Assert;
/**
* {@link HandlerMapper} implementation that maps incoming URLs
*
* @author Rob Winch
* @author Phillip Webb
* @since 1.3.0
*/
public class UrlHandlerMapper implements HandlerMapper {
private final String requestUri;
private final Handler hander;
/**
* Create a new {@link UrlHandlerMapper}.
* @param url the URL to map
* @param handler the handler to use
*/
public UrlHandlerMapper(String url, Handler handler) {
Assert.hasLength(url, "URL must not be empty");
Assert.isTrue(url.startsWith("/"), "URL must start with '/'");
this.requestUri = url;
this.hander = handler;
}
@Override
public Handler getHandler(ServerHttpRequest request) {
if (this.requestUri.equals(request.getURI().getPath())) {
return this.hander;
}
return null;
}
}
/*
* 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.
*/
/**
* Server support for a remotely running Spring Boot application.
*/
package org.springframework.boot.developertools.remote.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;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Pattern;
/**
* A filtered collections of URLs which can be change after the application has started.
*
* @author Phillip Webb
*/
class ChangeableUrls implements Iterable<URL> {
private static final String[] SKIPPED_PROJECTS = { "spring-boot",
"spring-boot-developer-tools", "spring-boot-autoconfigure",
"spring-boot-actuator", "spring-boot-starter" };
private static final Pattern STARTER_PATTERN = Pattern
.compile("\\/spring-boot-starter-[\\w-]+\\/");
private final List<URL> urls;
private ChangeableUrls(URL... urls) {
List<URL> reloadableUrls = new ArrayList<URL>(urls.length);
for (URL url : urls) {
if (isReloadable(url)) {
reloadableUrls.add(url);
}
}
this.urls = Collections.unmodifiableList(reloadableUrls);
}
private boolean isReloadable(URL url) {
String urlString = url.toString();
return isFolderUrl(urlString) && !isSkipped(urlString);
}
private boolean isFolderUrl(String urlString) {
return urlString.startsWith("file:") && urlString.endsWith("/");
}
private boolean isSkipped(String urlString) {
// Skip certain spring-boot projects to allow them to be imported in the same IDE
for (String skipped : SKIPPED_PROJECTS) {
if (urlString.contains("/" + skipped + "/target/classes/")) {
return true;
}
}
// Skip all starter projects
if (STARTER_PATTERN.matcher(urlString).find()) {
return true;
}
return false;
}
@Override
public Iterator<URL> iterator() {
return this.urls.iterator();
}
public int size() {
return this.urls.size();
}
public URL[] toArray() {
return this.urls.toArray(new URL[this.urls.size()]);
}
public List<URL> toList() {
return Collections.unmodifiableList(this.urls);
}
@Override
public String toString() {
return this.urls.toString();
}
public static ChangeableUrls fromUrlClassLoader(URLClassLoader classLoader) {
return fromUrls(classLoader.getURLs());
}
public static ChangeableUrls fromUrls(Collection<URL> urls) {
return fromUrls(new ArrayList<URL>(urls).toArray(new URL[urls.size()]));
}
public static ChangeableUrls fromUrls(URL... urls) {
return new ChangeableUrls(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;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Conditional;
/**
* {@link Conditional} that only matches when the {@link RestartInitializer} has been
* applied with non {@code null} URLs.
*
* @author Phillip Webb
* @since 1.3.0
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnInitializedRestarterCondition.class)
public @interface ConditionalOnInitializedRestarter {
}
/*
* 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;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* Default {@link RestartInitializer} that only enable initial restart when running a
* standard "main" method. Skips initialization when running "fat" jars (included
* exploded) or when running from a test.
*
* @author Phillip Webb
* @since 1.3.0
*/
public class DefaultRestartInitializer implements RestartInitializer {
private static final Set<String> SKIPPED_STACK_ELEMENTS;
static {
Set<String> skipped = new LinkedHashSet<String>();
skipped.add("org.junit.runners.");
skipped.add("org.springframework.boot.test.");
SKIPPED_STACK_ELEMENTS = Collections.unmodifiableSet(skipped);
}
@Override
public URL[] getInitialUrls(Thread thread) {
if (!isMain(thread)) {
return null;
}
for (StackTraceElement element : thread.getStackTrace()) {
if (isSkippedStackElement(element)) {
return null;
}
}
return getUrls(thread);
}
/**
* Returns if the thread is for a main invocation. By default checks the name of the
* thread and the context classloader.
* @param thread the thread to check
* @return {@code true} if the thread is a main invocation
*/
protected boolean isMain(Thread thread) {
return thread.getName().equals("main")
&& thread.getContextClassLoader().getClass().getName()
.contains("AppClassLoader");
}
/**
* Checks if a specific {@link StackTraceElement} should cause the initializer to be
* skipped.
* @param element the stack element to check
* @return {@code true} if the stack element means that the initializer should be
* skipped
*/
protected boolean isSkippedStackElement(StackTraceElement element) {
for (String skipped : SKIPPED_STACK_ELEMENTS) {
if (element.getClassName().startsWith(skipped)) {
return true;
}
}
return false;
}
/**
* Return the URLs that should be used with initialization.
* @param thread the source thread
* @return the URLs
*/
protected URL[] getUrls(Thread thread) {
return ChangeableUrls.fromUrlClassLoader(
(URLClassLoader) thread.getContextClassLoader()).toArray();
}
}
/*
* 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;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import org.springframework.util.Assert;
/**
* The "main" method located from a running thread.
*
* @author Phillip Webb
*/
class MainMethod {
private final Method method;
public MainMethod() {
this(Thread.currentThread());
}
public MainMethod(Thread thread) {
Assert.notNull(thread, "Thread must not be null");
this.method = getMainMethod(thread);
}
private Method getMainMethod(Thread thread) {
for (StackTraceElement element : thread.getStackTrace()) {
if ("main".equals(element.getMethodName())) {
Method method = getMainMethod(element);
if (method != null) {
return method;
}
}
}
throw new IllegalStateException("Unable to find main method");
}
private Method getMainMethod(StackTraceElement element) {
try {
Class<?> elementClass = Class.forName(element.getClassName());
Method method = elementClass.getDeclaredMethod("main", String[].class);
if (Modifier.isStatic(method.getModifiers())) {
return method;
}
}
catch (Exception ex) {
// Ignore
}
return null;
}
/**
* Returns the actual main method.
* @return the main method
*/
public Method getMethod() {
return this.method;
}
/**
* Return the name of the declaring class.
* @return the declaring class name
*/
public String getDeclaringClassName() {
return this.method.getDeclaringClass().getName();
}
}
/*
* 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;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* {@link Condition} that checks that a {@link Restarter} is available an initialized.
*
* @author Phillip Webb
* @see ConditionalOnInitializedRestarter
*/
class OnInitializedRestarterCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
Restarter restarter = getRestarter();
if (restarter == null) {
return ConditionOutcome.noMatch("Restarter unavailable");
}
if (restarter.getInitialUrls() == null) {
return ConditionOutcome.noMatch("Restarter initialized without URLs");
}
return ConditionOutcome.match("Restarter available and initialized");
}
private Restarter getRestarter() {
try {
return Restarter.getInstance();
}
catch (Exception ex) {
return null;
}
}
}
/*
* 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;
import org.springframework.boot.context.event.ApplicationFailedEvent;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.Ordered;
/**
* {@link ApplicationListener} to initialize the {@link Restarter}.
*
* @author Phillip Webb
* @since 1.3.0
* @see Restarter
*/
public class RestartApplicationListener implements ApplicationListener<ApplicationEvent>,
Ordered {
private int order = HIGHEST_PRECEDENCE;
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationStartedEvent) {
Restarter.initialize(((ApplicationStartedEvent) event).getArgs());
}
if (event instanceof ApplicationReadyEvent
|| event instanceof ApplicationFailedEvent) {
Restarter.getInstance().finish();
}
}
@Override
public int getOrder() {
return this.order;
}
/**
* Set the order of the listener.
* @param order the order of the listener
*/
public void setOrder(int order) {
this.order = order;
}
}
/*
* 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;
import java.net.URL;
/**
* Strategy interface used to initialize a {@link Restarter}.
*
* @author Phillip Webb
* @since 1.3.0
* @see DefaultRestartInitializer
*/
public interface RestartInitializer {
/**
* {@link RestartInitializer} that doesn't return any URLs.
*/
public static final RestartInitializer NONE = new RestartInitializer() {
@Override
public URL[] getInitialUrls(Thread thread) {
return null;
}
};
/**
* Return the initial set of URLs for the {@link Restarter} or {@code null} if no
* initial restart is required.
* @param thread the source thread
* @return initial URLs or {@code null}
*/
URL[] getInitialUrls(Thread thread);
}
/*
* 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;
import java.lang.reflect.Method;
/**
* Thread used to launch a restarted application.
*
* @author Phillip Webb
*/
class RestartLauncher extends Thread {
private final String mainClassName;
private final String[] args;
public RestartLauncher(ClassLoader classLoader, String mainClassName, String[] args,
UncaughtExceptionHandler exceptionHandler) {
this.mainClassName = mainClassName;
this.args = args;
setName("restartedMain");
setUncaughtExceptionHandler(exceptionHandler);
setDaemon(false);
setContextClassLoader(classLoader);
}
@Override
public void run() {
try {
Class<?> mainClass = getContextClassLoader().loadClass(this.mainClassName);
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke(null, new Object[] { this.args });
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
}
/*
* 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;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Scope;
/**
* Restart {@code @Scope} Annotation used to indicate that a bean shoul remain beteen
* restarts.
*
* @author Phillip Webb
* @since 1.3.0
* @see RestartScopeInitializer
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope("restart")
public @interface RestartScope {
}
/*
* 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;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
/**
* Support for a 'restart' {@link Scope} that allows beans to remain between restarts.
*
* @author Phillip Webb
* @since 1.3.0
*/
public class RestartScopeInitializer implements
ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
applicationContext.getBeanFactory().registerScope("restart", new RestartScope());
}
/**
* {@link Scope} that stores beans as {@link Restarter} attributes.
*/
private static class RestartScope implements Scope {
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
return Restarter.getInstance().getOrAddAttribute(name, objectFactory);
}
@Override
public Object remove(String name) {
return Restarter.getInstance().removeAttribute(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
}
@Override
public Object resolveContextualObject(String key) {
return null;
}
@Override
public String getConversationId() {
return null;
}
}
}
/*
* Copyright 2012-2014 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;
import java.lang.Thread.UncaughtExceptionHandler;
/**
* {@link UncaughtExceptionHandler} decorator that allows a thread to exit silently.
*
* @author Phillip Webb
*/
class SilentExitExceptionHandler implements UncaughtExceptionHandler {
private final UncaughtExceptionHandler delegate;
public SilentExitExceptionHandler(UncaughtExceptionHandler delegate) {
this.delegate = delegate;
}
@Override
public void uncaughtException(Thread thread, Throwable exception) {
if (exception instanceof SilentExitException) {
return;
}
if (this.delegate != null) {
this.delegate.uncaughtException(thread, exception);
}
}
public static void setup(Thread thread) {
UncaughtExceptionHandler handler = thread.getUncaughtExceptionHandler();
if (!(handler instanceof SilentExitExceptionHandler)) {
handler = new SilentExitExceptionHandler(handler);
thread.setUncaughtExceptionHandler(handler);
}
}
public static void exitCurrentThread() {
throw new SilentExitException();
}
private static class SilentExitException extends RuntimeException {
}
}
/*
* 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.classloader;
import java.io.Serializable;
import org.springframework.util.Assert;
/**
* A single file that may be served from a {@link ClassLoader}. Can be used to represent
* files that have been added, modified or deleted since the original JAR was created.
*
* @author Phillip Webb
* @see ClassLoaderFileRepository
* @since 1.3.0
*/
public class ClassLoaderFile implements Serializable {
private static final long serialVersionUID = 1;
private final Kind kind;
private final byte[] contents;
private final long lastModified;
/**
* Create a new {@link ClassLoaderFile} instance.
* @param kind the kind of file
* @param contents the file contents
*/
public ClassLoaderFile(Kind kind, byte[] contents) {
this(kind, System.currentTimeMillis(), contents);
}
/**
* Create a new {@link ClassLoaderFile} instance.
* @param kind the kind of file
* @param lastModified the last modified time
* @param contents the file contents
*/
public ClassLoaderFile(Kind kind, long lastModified, byte[] contents) {
Assert.notNull(kind, "Kind must not be null");
Assert.isTrue(kind == Kind.DELETED ? contents == null : contents != null,
"Contents must " + (kind == Kind.DELETED ? "" : "not ") + "be null");
this.kind = kind;
this.lastModified = lastModified;
this.contents = contents;
}
/**
* Return the file {@link Kind} (added, modified, deleted).
* @return the kind
*/
public Kind getKind() {
return this.kind;
}
/**
* Return the time that the file was last modified.
* @return the last modified time
*/
public long getLastModified() {
return this.lastModified;
}
/**
* Return the contents of the file as a byte array or {@code null} if
* {@link #getKind()} is {@link Kind#DELETED}.
* @return the contents or {@code null}
*/
public byte[] getContents() {
return this.contents;
}
/**
* The kinds of class load files.
*/
public static enum Kind {
/**
* The file has been added since the original JAR was created.
*/
ADDED,
/**
* The file has been modified since the original JAR was created.
*/
MODIFIED,
/**
* The file has been deleted since the original JAR was created.
*/
DELETED
}
}
/*
* 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.classloader;
/**
* A container for files that may be served from a {@link ClassLoader}. Can be used to
* represent files that have been added, modified or deleted since the original JAR was
* created.
*
* @author Phillip Webb
* @since 1.3.0
* @see ClassLoaderFile
*/
public interface ClassLoaderFileRepository {
/**
* Empty {@link ClassLoaderFileRepository} implementation.
*/
public static final ClassLoaderFileRepository NONE = new ClassLoaderFileRepository() {
@Override
public ClassLoaderFile getFile(String name) {
return null;
}
};
/**
* Return a {@link ClassLoaderFile} for the given name or {@code null} if no file is
* contained in this collection.
* @param name the name of the file
* @return a {@link ClassLoaderFile} or {@code null}
*/
ClassLoaderFile getFile(String name);
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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