Merge branch 'gh-3082'
This commit is contained in:
		
						commit
						247d2d6a2d
					
				
							
								
								
									
										1
									
								
								pom.xml
								
								
								
								
							
							
						
						
									
										1
									
								
								pom.xml
								
								
								
								
							| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,90 @@
 | 
			
		|||
<?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>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,76 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,61 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,116 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,68 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,129 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,77 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package org.springframework.boot.developertools.autoconfigure;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.PostConstruct;
 | 
			
		||||
 | 
			
		||||
import org.apache.commons.logging.Log;
 | 
			
		||||
import org.apache.commons.logging.LogFactory;
 | 
			
		||||
import org.springframework.boot.developertools.livereload.LiveReloadServer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Manages an optional {@link LiveReloadServer}. The {@link LiveReloadServer} may
 | 
			
		||||
 * gracefully fail to start (e.g. because of a port conflict) or may be omitted entirely.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 * @since 1.3.0
 | 
			
		||||
 */
 | 
			
		||||
public class OptionalLiveReloadServer {
 | 
			
		||||
 | 
			
		||||
	private static final Log logger = LogFactory.getLog(OptionalLiveReloadServer.class);
 | 
			
		||||
 | 
			
		||||
	private LiveReloadServer server;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Create a new {@link OptionalLiveReloadServer} instance.
 | 
			
		||||
	 * @param server the server to manage or {@code null}
 | 
			
		||||
	 */
 | 
			
		||||
	public OptionalLiveReloadServer(LiveReloadServer server) {
 | 
			
		||||
		this.server = server;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * {@link PostConstruct} method to start the server if possible.
 | 
			
		||||
	 * @throws Exception
 | 
			
		||||
	 */
 | 
			
		||||
	@PostConstruct
 | 
			
		||||
	public void startServer() throws Exception {
 | 
			
		||||
		if (this.server != null) {
 | 
			
		||||
			try {
 | 
			
		||||
				if (!this.server.isStarted()) {
 | 
			
		||||
					this.server.start();
 | 
			
		||||
				}
 | 
			
		||||
				logger.info("LiveReload server is running on port "
 | 
			
		||||
						+ this.server.getPort());
 | 
			
		||||
			}
 | 
			
		||||
			catch (Exception ex) {
 | 
			
		||||
				logger.warn("Unable to start LiveReload server");
 | 
			
		||||
				logger.debug("Live reload start error", ex);
 | 
			
		||||
				this.server = null;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Trigger LiveReload if the server is up an running.
 | 
			
		||||
	 */
 | 
			
		||||
	public void triggerReload() {
 | 
			
		||||
		if (this.server != null) {
 | 
			
		||||
			this.server.triggerReload();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,154 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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()));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,134 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Auto-configuration for {@code spring-boot-developer-tools}.
 | 
			
		||||
 */
 | 
			
		||||
package org.springframework.boot.developertools.autoconfigure;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,68 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,73 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,122 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,39 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package org.springframework.boot.developertools.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);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,51 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package org.springframework.boot.developertools.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;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Support for classpath monitoring
 | 
			
		||||
 */
 | 
			
		||||
package org.springframework.boot.developertools.classpath;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,128 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package org.springframework.boot.developertools.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
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,89 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,36 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,84 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,211 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,141 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class to watch the local filesystem for changes.
 | 
			
		||||
 */
 | 
			
		||||
package org.springframework.boot.developertools.filewatch;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,62 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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]);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,162 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,32 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,102 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,59 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,159 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,322 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Support for the livereload protocol.
 | 
			
		||||
 */
 | 
			
		||||
package org.springframework.boot.developertools.livereload;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Spring Boot developer tools.
 | 
			
		||||
 */
 | 
			
		||||
package org.springframework.boot.developertools;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,145 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package org.springframework.boot.developertools.remote.client;
 | 
			
		||||
 | 
			
		||||
import java.io.ByteArrayOutputStream;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.io.ObjectOutputStream;
 | 
			
		||||
import java.net.MalformedURLException;
 | 
			
		||||
import java.net.URI;
 | 
			
		||||
import java.net.URISyntaxException;
 | 
			
		||||
import java.net.URL;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
import org.apache.commons.logging.Log;
 | 
			
		||||
import org.apache.commons.logging.LogFactory;
 | 
			
		||||
import org.springframework.boot.developertools.classpath.ClassPathChangedEvent;
 | 
			
		||||
import org.springframework.boot.developertools.filewatch.ChangedFile;
 | 
			
		||||
import org.springframework.boot.developertools.filewatch.ChangedFiles;
 | 
			
		||||
import org.springframework.boot.developertools.restart.classloader.ClassLoaderFile;
 | 
			
		||||
import org.springframework.boot.developertools.restart.classloader.ClassLoaderFile.Kind;
 | 
			
		||||
import org.springframework.boot.developertools.restart.classloader.ClassLoaderFiles;
 | 
			
		||||
import org.springframework.context.ApplicationListener;
 | 
			
		||||
import org.springframework.http.HttpHeaders;
 | 
			
		||||
import org.springframework.http.HttpMethod;
 | 
			
		||||
import org.springframework.http.HttpStatus;
 | 
			
		||||
import org.springframework.http.MediaType;
 | 
			
		||||
import org.springframework.http.client.ClientHttpRequest;
 | 
			
		||||
import org.springframework.http.client.ClientHttpRequestFactory;
 | 
			
		||||
import org.springframework.http.client.ClientHttpResponse;
 | 
			
		||||
import org.springframework.util.Assert;
 | 
			
		||||
import org.springframework.util.FileCopyUtils;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Listens and pushes any classpath updates to a remote endpoint.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 * @since 1.3.0
 | 
			
		||||
 */
 | 
			
		||||
public class ClassPathChangeUploader implements
 | 
			
		||||
		ApplicationListener<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);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,116 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,60 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,58 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,45 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,228 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Client support for a remotely running Spring Boot application.
 | 
			
		||||
 */
 | 
			
		||||
package org.springframework.boot.developertools.remote.client;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,49 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,81 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,81 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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() {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,41 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,37 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,48 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,59 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,55 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package org.springframework.boot.developertools.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;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Server support for a remotely running Spring Boot application.
 | 
			
		||||
 */
 | 
			
		||||
package org.springframework.boot.developertools.remote.server;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,111 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,40 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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 {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,94 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package org.springframework.boot.developertools.restart;
 | 
			
		||||
 | 
			
		||||
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();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,84 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,55 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package org.springframework.boot.developertools.restart;
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,62 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,50 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,54 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,41 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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 {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,68 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,568 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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.beans.Introspector;
 | 
			
		||||
import java.lang.Thread.UncaughtExceptionHandler;
 | 
			
		||||
import java.lang.reflect.Field;
 | 
			
		||||
import java.lang.reflect.Method;
 | 
			
		||||
import java.net.URL;
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.IdentityHashMap;
 | 
			
		||||
import java.util.Iterator;
 | 
			
		||||
import java.util.LinkedHashSet;
 | 
			
		||||
import java.util.LinkedList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
import java.util.concurrent.BlockingDeque;
 | 
			
		||||
import java.util.concurrent.Callable;
 | 
			
		||||
import java.util.concurrent.LinkedBlockingDeque;
 | 
			
		||||
import java.util.concurrent.ThreadFactory;
 | 
			
		||||
import java.util.concurrent.locks.Lock;
 | 
			
		||||
import java.util.concurrent.locks.ReentrantLock;
 | 
			
		||||
 | 
			
		||||
import org.apache.commons.logging.Log;
 | 
			
		||||
import org.apache.commons.logging.LogFactory;
 | 
			
		||||
import org.springframework.beans.CachedIntrospectionResults;
 | 
			
		||||
import org.springframework.beans.factory.ObjectFactory;
 | 
			
		||||
import org.springframework.boot.SpringApplication;
 | 
			
		||||
import org.springframework.boot.developertools.restart.classloader.ClassLoaderFiles;
 | 
			
		||||
import org.springframework.boot.developertools.restart.classloader.RestartClassLoader;
 | 
			
		||||
import org.springframework.boot.logging.DeferredLog;
 | 
			
		||||
import org.springframework.cglib.core.ClassNameReader;
 | 
			
		||||
import org.springframework.core.ResolvableType;
 | 
			
		||||
import org.springframework.core.annotation.AnnotationUtils;
 | 
			
		||||
import org.springframework.util.Assert;
 | 
			
		||||
import org.springframework.util.ReflectionUtils;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Allows a running application to be restarted with an updated classpath. The restarter
 | 
			
		||||
 * works by creating a new application ClassLoader that is split into two parts. The top
 | 
			
		||||
 * part contains static URLs that don't change (for example 3rd party libraries and Spring
 | 
			
		||||
 * Boot itself) and the bottom part contains URLs where classes and resources might be
 | 
			
		||||
 * updated.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * The Restarter should be {@link #initialize(String[]) initialized} early to ensure that
 | 
			
		||||
 * classes are loaded multiple times. Mostly the {@link RestartApplicationListener} can be
 | 
			
		||||
 * relied upon to perform initialization, however, you may need to call
 | 
			
		||||
 * {@link #initialize(String[])} directly if your SpringApplication arguments are not
 | 
			
		||||
 * identical to your main method arguments.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * By default, applications running in an IDE (i.e. those not packaged as "fat jars") will
 | 
			
		||||
 * automatically detect URLs that can change. It's also possible to manually configure
 | 
			
		||||
 * URLs or class file updates for remote restart scenarios.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 * @since 1.3.0
 | 
			
		||||
 * @see RestartApplicationListener
 | 
			
		||||
 * @see #initialize(String[])
 | 
			
		||||
 * @see #getInstance()
 | 
			
		||||
 * @see #restart()
 | 
			
		||||
 */
 | 
			
		||||
public class Restarter {
 | 
			
		||||
 | 
			
		||||
	private static Restarter instance;
 | 
			
		||||
 | 
			
		||||
	private Log logger = new DeferredLog();
 | 
			
		||||
 | 
			
		||||
	private final boolean forceReferenceCleanup;
 | 
			
		||||
 | 
			
		||||
	private URL[] initialUrls;
 | 
			
		||||
 | 
			
		||||
	private final String mainClassName;
 | 
			
		||||
 | 
			
		||||
	private final ClassLoader applicationClassLoader;
 | 
			
		||||
 | 
			
		||||
	private final String[] args;
 | 
			
		||||
 | 
			
		||||
	private final UncaughtExceptionHandler exceptionHandler;
 | 
			
		||||
 | 
			
		||||
	private final Set<URL> urls = new LinkedHashSet<URL>();
 | 
			
		||||
 | 
			
		||||
	private final ClassLoaderFiles classLoaderFiles = new ClassLoaderFiles();
 | 
			
		||||
 | 
			
		||||
	private final Map<String, Object> attributes = new HashMap<String, Object>();
 | 
			
		||||
 | 
			
		||||
	private final BlockingDeque<LeakSafeThread> leakSafeThreads = new LinkedBlockingDeque<LeakSafeThread>();
 | 
			
		||||
 | 
			
		||||
	private boolean finished = false;
 | 
			
		||||
 | 
			
		||||
	private Lock stopLock = new ReentrantLock();
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Internal constructor to create a new {@link Restarter} instance.
 | 
			
		||||
	 * @param thread the source thread
 | 
			
		||||
	 * @param args the application arguments
 | 
			
		||||
	 * @param forceReferenceCleanup if soft/weak reference cleanup should be forced
 | 
			
		||||
	 * @param initializer
 | 
			
		||||
	 * @see #initialize(String[])
 | 
			
		||||
	 */
 | 
			
		||||
	protected Restarter(Thread thread, String[] args, boolean forceReferenceCleanup,
 | 
			
		||||
			RestartInitializer initializer) {
 | 
			
		||||
		Assert.notNull(thread, "Thread must not be null");
 | 
			
		||||
		Assert.notNull(args, "Args must not be null");
 | 
			
		||||
		Assert.notNull(initializer, "Initializer must not be null");
 | 
			
		||||
		this.logger.debug("Creating new Restarter for thread " + thread);
 | 
			
		||||
		SilentExitExceptionHandler.setup(thread);
 | 
			
		||||
		this.forceReferenceCleanup = forceReferenceCleanup;
 | 
			
		||||
		this.initialUrls = initializer.getInitialUrls(thread);
 | 
			
		||||
		this.mainClassName = getMainClassName(thread);
 | 
			
		||||
		this.applicationClassLoader = thread.getContextClassLoader();
 | 
			
		||||
		this.args = args;
 | 
			
		||||
		this.exceptionHandler = thread.getUncaughtExceptionHandler();
 | 
			
		||||
		this.leakSafeThreads.add(new LeakSafeThread());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private String getMainClassName(Thread thread) {
 | 
			
		||||
		try {
 | 
			
		||||
			return new MainMethod(thread).getDeclaringClassName();
 | 
			
		||||
		}
 | 
			
		||||
		catch (Exception ex) {
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	protected void initialize(boolean restartOnInitialize) {
 | 
			
		||||
		preInitializeLeakyClasses();
 | 
			
		||||
		if (this.initialUrls != null) {
 | 
			
		||||
			this.urls.addAll(Arrays.asList(this.initialUrls));
 | 
			
		||||
			if (restartOnInitialize) {
 | 
			
		||||
				this.logger.debug("Immediately restarting application");
 | 
			
		||||
				immediateRestart();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void immediateRestart() {
 | 
			
		||||
		try {
 | 
			
		||||
			getLeakSafeThread().callAndWait(new Callable<Void>() {
 | 
			
		||||
 | 
			
		||||
				@Override
 | 
			
		||||
				public Void call() throws Exception {
 | 
			
		||||
					start();
 | 
			
		||||
					return null;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
		catch (Exception ex) {
 | 
			
		||||
			this.logger.warn("Unable to initialize restarter", ex);
 | 
			
		||||
		}
 | 
			
		||||
		SilentExitExceptionHandler.exitCurrentThread();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * CGLIB has a private exception field which needs to initialized early to ensure that
 | 
			
		||||
	 * the stacktrace doesn't retain a reference to the RestartClassLoader.
 | 
			
		||||
	 */
 | 
			
		||||
	private void preInitializeLeakyClasses() {
 | 
			
		||||
		try {
 | 
			
		||||
			Class<?> readerClass = ClassNameReader.class;
 | 
			
		||||
			Field field = readerClass.getDeclaredField("EARLY_EXIT");
 | 
			
		||||
			field.setAccessible(true);
 | 
			
		||||
			((Throwable) field.get(null)).fillInStackTrace();
 | 
			
		||||
		}
 | 
			
		||||
		catch (Exception ex) {
 | 
			
		||||
			this.logger.warn("Unable to pre-initialize classes", ex);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Add additional URLs to be includes in the next restart.
 | 
			
		||||
	 * @param urls the urls to add
 | 
			
		||||
	 */
 | 
			
		||||
	public void addUrls(Collection<URL> urls) {
 | 
			
		||||
		Assert.notNull(urls, "Urls must not be null");
 | 
			
		||||
		this.urls.addAll(ChangeableUrls.fromUrls(urls).toList());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Add additional {@link ClassLoaderFiles} to be included in the next restart.
 | 
			
		||||
	 * @param classLoaderFiles the files to add
 | 
			
		||||
	 */
 | 
			
		||||
	public void addClassLoaderFiles(ClassLoaderFiles classLoaderFiles) {
 | 
			
		||||
		Assert.notNull(classLoaderFiles, "ClassLoaderFiles must not be null");
 | 
			
		||||
		this.classLoaderFiles.addAll(classLoaderFiles);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Return a {@link ThreadFactory} that can be used to create leak safe threads.
 | 
			
		||||
	 * @return a leak safe thread factory
 | 
			
		||||
	 */
 | 
			
		||||
	public ThreadFactory getThreadFactory() {
 | 
			
		||||
		return new LeakSafeThreadFactory();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Restart the running application.
 | 
			
		||||
	 */
 | 
			
		||||
	public void restart() {
 | 
			
		||||
		this.logger.debug("Restarting application");
 | 
			
		||||
		getLeakSafeThread().call(new Callable<Void>() {
 | 
			
		||||
 | 
			
		||||
			@Override
 | 
			
		||||
			public Void call() throws Exception {
 | 
			
		||||
				Restarter.this.stop();
 | 
			
		||||
				Restarter.this.start();
 | 
			
		||||
				return null;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Start the application.
 | 
			
		||||
	 * @throws Exception
 | 
			
		||||
	 */
 | 
			
		||||
	protected void start() throws Exception {
 | 
			
		||||
		Assert.notNull(this.mainClassName, "Unable to find the main class to restart");
 | 
			
		||||
		ClassLoader parent = this.applicationClassLoader;
 | 
			
		||||
		URL[] urls = this.urls.toArray(new URL[this.urls.size()]);
 | 
			
		||||
		ClassLoaderFiles updatedFiles = new ClassLoaderFiles(this.classLoaderFiles);
 | 
			
		||||
		ClassLoader classLoader = new RestartClassLoader(parent, urls, updatedFiles,
 | 
			
		||||
				this.logger);
 | 
			
		||||
		if (this.logger.isDebugEnabled()) {
 | 
			
		||||
			this.logger.debug("Starting application " + this.mainClassName
 | 
			
		||||
					+ " with URLs " + Arrays.asList(urls));
 | 
			
		||||
		}
 | 
			
		||||
		relaunch(classLoader);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Relaunch the application using the specified classloader.
 | 
			
		||||
	 * @param classLoader the classloader to use
 | 
			
		||||
	 * @throws Exception
 | 
			
		||||
	 */
 | 
			
		||||
	protected void relaunch(ClassLoader classLoader) throws Exception {
 | 
			
		||||
		RestartLauncher launcher = new RestartLauncher(classLoader, this.mainClassName,
 | 
			
		||||
				this.args, this.exceptionHandler);
 | 
			
		||||
		launcher.start();
 | 
			
		||||
		launcher.join();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Stop the application.
 | 
			
		||||
	 * @throws Exception
 | 
			
		||||
	 */
 | 
			
		||||
	protected void stop() throws Exception {
 | 
			
		||||
		this.logger.debug("Stopping application");
 | 
			
		||||
		this.stopLock.lock();
 | 
			
		||||
		try {
 | 
			
		||||
			triggerShutdownHooks();
 | 
			
		||||
			cleanupCaches();
 | 
			
		||||
			if (this.forceReferenceCleanup) {
 | 
			
		||||
				forceReferenceCleanup();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		finally {
 | 
			
		||||
			this.stopLock.unlock();
 | 
			
		||||
		}
 | 
			
		||||
		System.gc();
 | 
			
		||||
		System.runFinalization();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@SuppressWarnings("rawtypes")
 | 
			
		||||
	private void triggerShutdownHooks() throws Exception {
 | 
			
		||||
		Class<?> hooksClass = Class.forName("java.lang.ApplicationShutdownHooks");
 | 
			
		||||
		Method runHooks = hooksClass.getDeclaredMethod("runHooks");
 | 
			
		||||
		runHooks.setAccessible(true);
 | 
			
		||||
		runHooks.invoke(null);
 | 
			
		||||
		Field field = hooksClass.getDeclaredField("hooks");
 | 
			
		||||
		field.setAccessible(true);
 | 
			
		||||
		field.set(null, new IdentityHashMap());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void cleanupCaches() throws Exception {
 | 
			
		||||
		Introspector.flushCaches();
 | 
			
		||||
		cleanupKnownCaches();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void cleanupKnownCaches() throws Exception {
 | 
			
		||||
		// Whilst not strictly necessary it helps to cleanup soft reference caches
 | 
			
		||||
		// early rather than waiting for memory limits to be reached
 | 
			
		||||
		clear(ResolvableType.class, "cache");
 | 
			
		||||
		clear("org.springframework.core.SerializableTypeWrapper", "cache");
 | 
			
		||||
		clear(CachedIntrospectionResults.class, "acceptedClassLoaders");
 | 
			
		||||
		clear(CachedIntrospectionResults.class, "strongClassCache");
 | 
			
		||||
		clear(CachedIntrospectionResults.class, "softClassCache");
 | 
			
		||||
		clear(ReflectionUtils.class, "declaredFieldsCache");
 | 
			
		||||
		clear(ReflectionUtils.class, "declaredMethodsCache");
 | 
			
		||||
		clear(AnnotationUtils.class, "findAnnotationCache");
 | 
			
		||||
		clear(AnnotationUtils.class, "annotatedInterfaceCache");
 | 
			
		||||
		clear("com.sun.naming.internal.ResourceManager", "propertiesCache");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void clear(String className, String fieldName) {
 | 
			
		||||
		try {
 | 
			
		||||
			clear(Class.forName(className), fieldName);
 | 
			
		||||
		}
 | 
			
		||||
		catch (Exception ex) {
 | 
			
		||||
			this.logger.debug("Unable to clear field " + className + " " + fieldName, ex);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void clear(Class<?> type, String fieldName) throws Exception {
 | 
			
		||||
		Field field = type.getDeclaredField(fieldName);
 | 
			
		||||
		field.setAccessible(true);
 | 
			
		||||
		Object instance = field.get(null);
 | 
			
		||||
		if (instance instanceof Set) {
 | 
			
		||||
			((Set<?>) instance).clear();
 | 
			
		||||
		}
 | 
			
		||||
		if (instance instanceof Map) {
 | 
			
		||||
			Map<?, ?> map = ((Map<?, ?>) instance);
 | 
			
		||||
			for (Iterator<?> iterator = map.keySet().iterator(); iterator.hasNext();) {
 | 
			
		||||
				Object value = iterator.next();
 | 
			
		||||
				if (value instanceof Class
 | 
			
		||||
						&& ((Class<?>) value).getClassLoader() instanceof RestartClassLoader) {
 | 
			
		||||
					iterator.remove();
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Cleanup any soft/weak references by forcing an {@link OutOfMemoryError} error.
 | 
			
		||||
	 */
 | 
			
		||||
	private void forceReferenceCleanup() {
 | 
			
		||||
		try {
 | 
			
		||||
			final List<long[]> memory = new LinkedList<long[]>();
 | 
			
		||||
			while (true) {
 | 
			
		||||
				memory.add(new long[102400]);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		catch (final OutOfMemoryError ex) {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Called to finish {@link Restarter} initialization when application logging is
 | 
			
		||||
	 * available.
 | 
			
		||||
	 */
 | 
			
		||||
	synchronized void finish() {
 | 
			
		||||
		if (!isFinished()) {
 | 
			
		||||
			this.logger = DeferredLog.replay(this.logger, LogFactory.getLog(getClass()));
 | 
			
		||||
			this.finished = true;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	boolean isFinished() {
 | 
			
		||||
		return this.finished;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private LeakSafeThread getLeakSafeThread() {
 | 
			
		||||
		try {
 | 
			
		||||
			return this.leakSafeThreads.takeFirst();
 | 
			
		||||
		}
 | 
			
		||||
		catch (InterruptedException ex) {
 | 
			
		||||
			Thread.currentThread().interrupt();
 | 
			
		||||
			throw new IllegalStateException(ex);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public Object getOrAddAttribute(final String name,
 | 
			
		||||
			final ObjectFactory<?> objectFactory) {
 | 
			
		||||
		synchronized (this.attributes) {
 | 
			
		||||
			if (!this.attributes.containsKey(name)) {
 | 
			
		||||
				this.attributes.put(name, objectFactory.getObject());
 | 
			
		||||
			}
 | 
			
		||||
			return this.attributes.get(name);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public Object removeAttribute(String name) {
 | 
			
		||||
		synchronized (this.attributes) {
 | 
			
		||||
			return this.attributes.remove(name);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Return the initial set of URLs as configured by the {@link RestartInitializer}.
 | 
			
		||||
	 * @return the initial URLs or {@code null}
 | 
			
		||||
	 */
 | 
			
		||||
	public URL[] getInitialUrls() {
 | 
			
		||||
		return this.initialUrls;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Initialize restart support. See
 | 
			
		||||
	 * {@link #initialize(String[], boolean, RestartInitializer)} for details.
 | 
			
		||||
	 * @param args main application arguments
 | 
			
		||||
	 * @see #initialize(String[], boolean, RestartInitializer)
 | 
			
		||||
	 */
 | 
			
		||||
	public static void initialize(String[] args) {
 | 
			
		||||
		initialize(args, false, new DefaultRestartInitializer());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Initialize restart support. See
 | 
			
		||||
	 * {@link #initialize(String[], boolean, RestartInitializer)} for details.
 | 
			
		||||
	 * @param args main application arguments
 | 
			
		||||
	 * @param initializer the restart initializer
 | 
			
		||||
	 * @see #initialize(String[], boolean, RestartInitializer)
 | 
			
		||||
	 */
 | 
			
		||||
	public static void initialize(String[] args, RestartInitializer initializer) {
 | 
			
		||||
		initialize(args, false, initializer, true);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Initialize restart support. See
 | 
			
		||||
	 * {@link #initialize(String[], boolean, RestartInitializer)} for details.
 | 
			
		||||
	 * @param args main application arguments
 | 
			
		||||
	 * @param forceReferenceCleanup if forcing of soft/weak reference should happen on
 | 
			
		||||
	 * @see #initialize(String[], boolean, RestartInitializer)
 | 
			
		||||
	 */
 | 
			
		||||
	public static void initialize(String[] args, boolean forceReferenceCleanup) {
 | 
			
		||||
		initialize(args, forceReferenceCleanup, new DefaultRestartInitializer());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Initialize restart support. See
 | 
			
		||||
	 * {@link #initialize(String[], boolean, RestartInitializer)} for details.
 | 
			
		||||
	 * @param args main application arguments
 | 
			
		||||
	 * @param forceReferenceCleanup if forcing of soft/weak reference should happen on
 | 
			
		||||
	 * @param initializer the restart initializer
 | 
			
		||||
	 * @see #initialize(String[], boolean, RestartInitializer)
 | 
			
		||||
	 */
 | 
			
		||||
	public static void initialize(String[] args, boolean forceReferenceCleanup,
 | 
			
		||||
			RestartInitializer initializer) {
 | 
			
		||||
		initialize(args, forceReferenceCleanup, initializer, true);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Initialize restart support for the current application. Called automatically by
 | 
			
		||||
	 * {@link RestartApplicationListener} but can also be called directly if main
 | 
			
		||||
	 * application arguments are not the same as those passed to the
 | 
			
		||||
	 * {@link SpringApplication}.
 | 
			
		||||
	 * @param args main application arguments
 | 
			
		||||
	 * @param forceReferenceCleanup if forcing of soft/weak reference should happen on
 | 
			
		||||
	 * each restart. This will slow down restarts and is intended primarily for testing
 | 
			
		||||
	 * @param initializer the restart initializer
 | 
			
		||||
	 * @param restartOnInitialize if the restarter should be restarted immediately when
 | 
			
		||||
	 * the {@link RestartInitializer} returns non {@code null} results
 | 
			
		||||
	 */
 | 
			
		||||
	public static void initialize(String[] args, boolean forceReferenceCleanup,
 | 
			
		||||
			RestartInitializer initializer, boolean restartOnInitialize) {
 | 
			
		||||
		if (instance == null) {
 | 
			
		||||
			synchronized (Restarter.class) {
 | 
			
		||||
				instance = new Restarter(Thread.currentThread(), args,
 | 
			
		||||
						forceReferenceCleanup, initializer);
 | 
			
		||||
			}
 | 
			
		||||
			instance.initialize(restartOnInitialize);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Return the active {@link Restarter} instance. Cannot be called before
 | 
			
		||||
	 * {@link #initialize(String[]) initialization}.
 | 
			
		||||
	 * @return the restarter
 | 
			
		||||
	 */
 | 
			
		||||
	public synchronized static Restarter getInstance() {
 | 
			
		||||
		Assert.state(instance != null, "Restarter has not been initialized");
 | 
			
		||||
		return instance;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Set the restarter instance (useful for testing).
 | 
			
		||||
	 * @param instance the instance to set
 | 
			
		||||
	 */
 | 
			
		||||
	final static void setInstance(Restarter instance) {
 | 
			
		||||
		Restarter.instance = instance;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Clear the instance. Primarily provided for tests and not usually used in
 | 
			
		||||
	 * application code.
 | 
			
		||||
	 */
 | 
			
		||||
	public static void clearInstance() {
 | 
			
		||||
		instance = null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Thread that is created early so not to retain the {@link RestartClassLoader}.
 | 
			
		||||
	 */
 | 
			
		||||
	private class LeakSafeThread extends Thread {
 | 
			
		||||
 | 
			
		||||
		private Callable<?> callable;
 | 
			
		||||
 | 
			
		||||
		private Object result;
 | 
			
		||||
 | 
			
		||||
		public LeakSafeThread() {
 | 
			
		||||
			setDaemon(false);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public void call(Callable<?> callable) {
 | 
			
		||||
			this.callable = callable;
 | 
			
		||||
			start();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@SuppressWarnings("unchecked")
 | 
			
		||||
		public <V> V callAndWait(Callable<V> callable) {
 | 
			
		||||
			this.callable = callable;
 | 
			
		||||
			start();
 | 
			
		||||
			try {
 | 
			
		||||
				join();
 | 
			
		||||
				return (V) this.result;
 | 
			
		||||
			}
 | 
			
		||||
			catch (InterruptedException ex) {
 | 
			
		||||
				Thread.currentThread().interrupt();
 | 
			
		||||
				throw new IllegalStateException(ex);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public void run() {
 | 
			
		||||
			// We are safe to refresh the ActionThread (and indirectly call
 | 
			
		||||
			// AccessController.getContext()) since our stack doesn't include the
 | 
			
		||||
			// RestartClassLoader
 | 
			
		||||
			try {
 | 
			
		||||
				Restarter.this.leakSafeThreads.put(new LeakSafeThread());
 | 
			
		||||
				this.result = this.callable.call();
 | 
			
		||||
			}
 | 
			
		||||
			catch (Exception ex) {
 | 
			
		||||
				ex.printStackTrace();
 | 
			
		||||
				System.exit(1);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * {@link ThreadFactory} that creates a leak safe thead.
 | 
			
		||||
	 */
 | 
			
		||||
	private class LeakSafeThreadFactory implements ThreadFactory {
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public Thread newThread(final Runnable runnable) {
 | 
			
		||||
			return getLeakSafeThread().callAndWait(new Callable<Thread>() {
 | 
			
		||||
 | 
			
		||||
				@Override
 | 
			
		||||
				public Thread call() throws Exception {
 | 
			
		||||
					Thread thread = new Thread(runnable);
 | 
			
		||||
					thread.setContextClassLoader(Restarter.this.applicationClassLoader);
 | 
			
		||||
					return thread;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,60 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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 {
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,112 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,50 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,67 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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.ByteArrayInputStream;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.io.InputStream;
 | 
			
		||||
import java.net.URL;
 | 
			
		||||
import java.net.URLConnection;
 | 
			
		||||
import java.net.URLStreamHandler;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * {@link URLStreamHandler} for the contents of a {@link ClassLoaderFile}.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 */
 | 
			
		||||
class ClassLoaderFileURLStreamHandler extends URLStreamHandler {
 | 
			
		||||
 | 
			
		||||
	private ClassLoaderFile file;
 | 
			
		||||
 | 
			
		||||
	public ClassLoaderFileURLStreamHandler(ClassLoaderFile file) {
 | 
			
		||||
		this.file = file;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	protected URLConnection openConnection(URL url) throws IOException {
 | 
			
		||||
		return new Connection(url);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private class Connection extends URLConnection {
 | 
			
		||||
 | 
			
		||||
		public Connection(URL url) {
 | 
			
		||||
			super(url);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public void connect() throws IOException {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public InputStream getInputStream() throws IOException {
 | 
			
		||||
			return new ByteArrayInputStream(
 | 
			
		||||
					ClassLoaderFileURLStreamHandler.this.file.getContents());
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public long getLastModified() {
 | 
			
		||||
			return ClassLoaderFileURLStreamHandler.this.file.getLastModified();
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,202 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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 java.util.Collection;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.LinkedHashMap;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Map.Entry;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
 | 
			
		||||
import javax.management.loading.ClassLoaderRepository;
 | 
			
		||||
 | 
			
		||||
import org.springframework.util.Assert;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * {@link ClassLoaderFileRepository} that maintains a collection of
 | 
			
		||||
 * {@link ClassLoaderFile} items grouped by source folders.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 * @since 1.3.0
 | 
			
		||||
 * @see ClassLoaderFile
 | 
			
		||||
 * @see ClassLoaderRepository
 | 
			
		||||
 */
 | 
			
		||||
public class ClassLoaderFiles implements ClassLoaderFileRepository, Serializable {
 | 
			
		||||
 | 
			
		||||
	private static final long serialVersionUID = 1;
 | 
			
		||||
 | 
			
		||||
	private final Map<String, SourceFolder> sourceFolders;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Create a new {@link ClassLoaderFiles} instance.
 | 
			
		||||
	 */
 | 
			
		||||
	public ClassLoaderFiles() {
 | 
			
		||||
		this.sourceFolders = new LinkedHashMap<String, SourceFolder>();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Create a new {@link ClassLoaderFiles} instance.
 | 
			
		||||
	 * @param classLoaderFiles the source classloader files.
 | 
			
		||||
	 */
 | 
			
		||||
	public ClassLoaderFiles(ClassLoaderFiles classLoaderFiles) {
 | 
			
		||||
		Assert.notNull(classLoaderFiles, "ClassLoaderFiles must not be null");
 | 
			
		||||
		this.sourceFolders = new LinkedHashMap<String, SourceFolder>(
 | 
			
		||||
				classLoaderFiles.sourceFolders);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Add all elements items from the specified {@link ClassLoaderFiles} to this
 | 
			
		||||
	 * instance.
 | 
			
		||||
	 * @param files the files to add
 | 
			
		||||
	 */
 | 
			
		||||
	public void addAll(ClassLoaderFiles files) {
 | 
			
		||||
		Assert.notNull(files, "Files must not be null");
 | 
			
		||||
		for (SourceFolder folder : files.getSourceFolders()) {
 | 
			
		||||
			for (Map.Entry<String, ClassLoaderFile> entry : folder.getFilesEntrySet()) {
 | 
			
		||||
				addFile(folder.getName(), entry.getKey(), entry.getValue());
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Add a single {@link ClassLoaderFile} to the collection.
 | 
			
		||||
	 * @param name the name of the file
 | 
			
		||||
	 * @param file the file to add
 | 
			
		||||
	 */
 | 
			
		||||
	public void addFile(String name, ClassLoaderFile file) {
 | 
			
		||||
		addFile("", name, file);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Add a single {@link ClassLoaderFile} to the collection.
 | 
			
		||||
	 * @param sourceFolder the source folder of the file
 | 
			
		||||
	 * @param name the name of the file
 | 
			
		||||
	 * @param file the file to add
 | 
			
		||||
	 */
 | 
			
		||||
	public void addFile(String sourceFolder, String name, ClassLoaderFile file) {
 | 
			
		||||
		Assert.notNull(sourceFolder, "SourceFolder must not be null");
 | 
			
		||||
		Assert.notNull(name, "Name must not be null");
 | 
			
		||||
		Assert.notNull(file, "File must not be null");
 | 
			
		||||
		removeAll(name);
 | 
			
		||||
		getOrCreateSourceFolder(sourceFolder).add(name, file);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void removeAll(String name) {
 | 
			
		||||
		for (SourceFolder sourceFolder : this.sourceFolders.values()) {
 | 
			
		||||
			sourceFolder.remove(name);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Get or create a {@link SourceFolder} with the given name.
 | 
			
		||||
	 * @param name the name of the folder
 | 
			
		||||
	 * @return an existing or newly added {@link SourceFolder}
 | 
			
		||||
	 */
 | 
			
		||||
	protected final SourceFolder getOrCreateSourceFolder(String name) {
 | 
			
		||||
		SourceFolder sourceFolder = this.sourceFolders.get(name);
 | 
			
		||||
		if (sourceFolder == null) {
 | 
			
		||||
			sourceFolder = new SourceFolder(name);
 | 
			
		||||
			this.sourceFolders.put(name, sourceFolder);
 | 
			
		||||
		}
 | 
			
		||||
		return sourceFolder;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Return all {@link SourceFolder SourceFolders} that have been added to the
 | 
			
		||||
	 * collection.
 | 
			
		||||
	 * @return a collection of {@link SourceFolder} items
 | 
			
		||||
	 */
 | 
			
		||||
	public Collection<SourceFolder> getSourceFolders() {
 | 
			
		||||
		return Collections.unmodifiableCollection(this.sourceFolders.values());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Return the size of the collection.
 | 
			
		||||
	 * @return the size of the collection
 | 
			
		||||
	 */
 | 
			
		||||
	public int size() {
 | 
			
		||||
		int size = 0;
 | 
			
		||||
		for (SourceFolder sourceFolder : this.sourceFolders.values()) {
 | 
			
		||||
			size += sourceFolder.getFiles().size();
 | 
			
		||||
		}
 | 
			
		||||
		return size;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public ClassLoaderFile getFile(String name) {
 | 
			
		||||
		for (SourceFolder sourceFolder : this.sourceFolders.values()) {
 | 
			
		||||
			ClassLoaderFile file = sourceFolder.get(name);
 | 
			
		||||
			if (file != null) {
 | 
			
		||||
				return file;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * An individual source folder that is being managed by the collection.
 | 
			
		||||
	 */
 | 
			
		||||
	public static class SourceFolder implements Serializable {
 | 
			
		||||
 | 
			
		||||
		private static final long serialVersionUID = 1;
 | 
			
		||||
 | 
			
		||||
		private final String name;
 | 
			
		||||
 | 
			
		||||
		private final Map<String, ClassLoaderFile> files = new LinkedHashMap<String, ClassLoaderFile>();
 | 
			
		||||
 | 
			
		||||
		SourceFolder(String name) {
 | 
			
		||||
			this.name = name;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public Set<Entry<String, ClassLoaderFile>> getFilesEntrySet() {
 | 
			
		||||
			return this.files.entrySet();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		protected final void add(String name, ClassLoaderFile file) {
 | 
			
		||||
			this.files.put(name, file);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		protected final void remove(String name) {
 | 
			
		||||
			this.files.remove(name);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		protected final ClassLoaderFile get(String name) {
 | 
			
		||||
			return this.files.get(name);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/**
 | 
			
		||||
		 * Return the name of the source folder.
 | 
			
		||||
		 * @return the name of the source folder
 | 
			
		||||
		 */
 | 
			
		||||
		public String getName() {
 | 
			
		||||
			return this.name;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/**
 | 
			
		||||
		 * Return all {@link ClassLoaderFile ClassLoaderFiles} in the collection that are
 | 
			
		||||
		 * contained in this source folder.
 | 
			
		||||
		 * @return the files contained in the source folder
 | 
			
		||||
		 */
 | 
			
		||||
		public Collection<ClassLoaderFile> getFiles() {
 | 
			
		||||
			return Collections.unmodifiableCollection(this.files.values());
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,231 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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.IOException;
 | 
			
		||||
import java.net.MalformedURLException;
 | 
			
		||||
import java.net.URL;
 | 
			
		||||
import java.net.URLClassLoader;
 | 
			
		||||
import java.security.AccessController;
 | 
			
		||||
import java.security.PrivilegedAction;
 | 
			
		||||
import java.util.Enumeration;
 | 
			
		||||
 | 
			
		||||
import org.apache.commons.logging.Log;
 | 
			
		||||
import org.apache.commons.logging.LogFactory;
 | 
			
		||||
import org.springframework.boot.developertools.restart.classloader.ClassLoaderFile.Kind;
 | 
			
		||||
import org.springframework.core.SmartClassLoader;
 | 
			
		||||
import org.springframework.util.Assert;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Disposable {@link ClassLoader} used to support application restarting. Provides parent
 | 
			
		||||
 * last loading for the specified URLs.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Andy Clement
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 * @since 1.3.0
 | 
			
		||||
 */
 | 
			
		||||
public class RestartClassLoader extends URLClassLoader implements SmartClassLoader {
 | 
			
		||||
 | 
			
		||||
	private final Log logger;
 | 
			
		||||
 | 
			
		||||
	private final ClassLoaderFileRepository updatedFiles;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Create a new {@link RestartClassLoader} instance.
 | 
			
		||||
	 * @param parent the parent classloader
 | 
			
		||||
	 * @param urls the urls managed by the classloader
 | 
			
		||||
	 */
 | 
			
		||||
	public RestartClassLoader(ClassLoader parent, URL[] urls) {
 | 
			
		||||
		this(parent, urls, ClassLoaderFileRepository.NONE);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Create a new {@link RestartClassLoader} instance.
 | 
			
		||||
	 * @param parent the parent classloader
 | 
			
		||||
	 * @param updatedFiles any files that have been updated since the JARs referenced in
 | 
			
		||||
	 * URLs were created.
 | 
			
		||||
	 * @param urls the urls managed by the classloader
 | 
			
		||||
	 */
 | 
			
		||||
	public RestartClassLoader(ClassLoader parent, URL[] urls,
 | 
			
		||||
			ClassLoaderFileRepository updatedFiles) {
 | 
			
		||||
		this(parent, urls, updatedFiles, LogFactory.getLog(RestartClassLoader.class));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Create a new {@link RestartClassLoader} instance.
 | 
			
		||||
	 * @param parent the parent classloader
 | 
			
		||||
	 * @param updatedFiles any files that have been updated since the JARs referenced in
 | 
			
		||||
	 * URLs were created.
 | 
			
		||||
	 * @param urls the urls managed by the classloader
 | 
			
		||||
	 * @param logger the logger used for messages
 | 
			
		||||
	 */
 | 
			
		||||
	public RestartClassLoader(ClassLoader parent, URL[] urls,
 | 
			
		||||
			ClassLoaderFileRepository updatedFiles, Log logger) {
 | 
			
		||||
		super(urls, parent);
 | 
			
		||||
		Assert.notNull(parent, "Parent must not be null");
 | 
			
		||||
		Assert.notNull(updatedFiles, "UpdatedFiles must not be null");
 | 
			
		||||
		Assert.notNull(logger, "Logger must not be null");
 | 
			
		||||
		this.updatedFiles = updatedFiles;
 | 
			
		||||
		this.logger = logger;
 | 
			
		||||
		if (logger.isDebugEnabled()) {
 | 
			
		||||
			logger.debug("Created RestartClassLoader " + toString());
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public Enumeration<URL> getResources(String name) throws IOException {
 | 
			
		||||
		// Use the parent since we're shadowing resource and we don't want duplicates
 | 
			
		||||
		Enumeration<URL> resources = getParent().getResources(name);
 | 
			
		||||
		ClassLoaderFile file = this.updatedFiles.getFile(name);
 | 
			
		||||
		if (file != null) {
 | 
			
		||||
			// Assume that we're replacing just the first item
 | 
			
		||||
			if (resources.hasMoreElements()) {
 | 
			
		||||
				resources.nextElement();
 | 
			
		||||
			}
 | 
			
		||||
			if (file.getKind() != Kind.DELETED) {
 | 
			
		||||
				return new CompoundEnumeration<URL>(createFileUrl(name, file), resources);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return resources;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public URL getResource(String name) {
 | 
			
		||||
		ClassLoaderFile file = this.updatedFiles.getFile(name);
 | 
			
		||||
		if (file != null && file.getKind() == Kind.DELETED) {
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
		URL resource = findResource(name);
 | 
			
		||||
		if (resource != null) {
 | 
			
		||||
			return resource;
 | 
			
		||||
		}
 | 
			
		||||
		return getParent().getResource(name);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public URL findResource(final String name) {
 | 
			
		||||
		final ClassLoaderFile file = this.updatedFiles.getFile(name);
 | 
			
		||||
		if (file == null) {
 | 
			
		||||
			return super.findResource(name);
 | 
			
		||||
		}
 | 
			
		||||
		if (file.getKind() == Kind.DELETED) {
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
		return AccessController.doPrivileged(new PrivilegedAction<URL>() {
 | 
			
		||||
			@Override
 | 
			
		||||
			public URL run() {
 | 
			
		||||
				return createFileUrl(name, file);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
 | 
			
		||||
		String path = name.replace('.', '/').concat(".class");
 | 
			
		||||
		ClassLoaderFile file = this.updatedFiles.getFile(path);
 | 
			
		||||
		if (file != null && file.getKind() == Kind.DELETED) {
 | 
			
		||||
			throw new ClassNotFoundException(name);
 | 
			
		||||
		}
 | 
			
		||||
		Class<?> loadedClass = findLoadedClass(name);
 | 
			
		||||
		if (loadedClass == null) {
 | 
			
		||||
			try {
 | 
			
		||||
				loadedClass = findClass(name);
 | 
			
		||||
			}
 | 
			
		||||
			catch (ClassNotFoundException ex) {
 | 
			
		||||
				loadedClass = getParent().loadClass(name);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if (resolve) {
 | 
			
		||||
			resolveClass(loadedClass);
 | 
			
		||||
		}
 | 
			
		||||
		return loadedClass;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	protected Class<?> findClass(final String name) throws ClassNotFoundException {
 | 
			
		||||
		String path = name.replace('.', '/').concat(".class");
 | 
			
		||||
		final ClassLoaderFile file = this.updatedFiles.getFile(path);
 | 
			
		||||
		if (file == null) {
 | 
			
		||||
			return super.findClass(name);
 | 
			
		||||
		}
 | 
			
		||||
		if (file.getKind() == Kind.DELETED) {
 | 
			
		||||
			throw new ClassNotFoundException(name);
 | 
			
		||||
		}
 | 
			
		||||
		return AccessController.doPrivileged(new PrivilegedAction<Class<?>>() {
 | 
			
		||||
			@Override
 | 
			
		||||
			public Class<?> run() {
 | 
			
		||||
				byte[] bytes = file.getContents();
 | 
			
		||||
				return defineClass(name, bytes, 0, bytes.length);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private URL createFileUrl(String name, ClassLoaderFile file) {
 | 
			
		||||
		try {
 | 
			
		||||
			return new URL("reloaded", null, -1, "/" + name,
 | 
			
		||||
					new ClassLoaderFileURLStreamHandler(file));
 | 
			
		||||
		}
 | 
			
		||||
		catch (MalformedURLException ex) {
 | 
			
		||||
			throw new IllegalStateException(ex);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	protected void finalize() throws Throwable {
 | 
			
		||||
		if (this.logger.isDebugEnabled()) {
 | 
			
		||||
			this.logger.debug("Finalized classloader " + toString());
 | 
			
		||||
		}
 | 
			
		||||
		super.finalize();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public boolean isClassReloadable(Class<?> classType) {
 | 
			
		||||
		return (classType.getClassLoader() instanceof RestartClassLoader);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Compound {@link Enumeration} that adds an additional item to the front.
 | 
			
		||||
	 */
 | 
			
		||||
	private static class CompoundEnumeration<E> implements Enumeration<E> {
 | 
			
		||||
 | 
			
		||||
		private E firstElement;
 | 
			
		||||
 | 
			
		||||
		private final Enumeration<E> enumeration;
 | 
			
		||||
 | 
			
		||||
		public CompoundEnumeration(E firstElement, Enumeration<E> enumeration) {
 | 
			
		||||
			this.firstElement = firstElement;
 | 
			
		||||
			this.enumeration = enumeration;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public boolean hasMoreElements() {
 | 
			
		||||
			return (this.firstElement != null || this.enumeration.hasMoreElements());
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public E nextElement() {
 | 
			
		||||
			if (this.firstElement == null) {
 | 
			
		||||
				return this.enumeration.nextElement();
 | 
			
		||||
			}
 | 
			
		||||
			E element = this.firstElement;
 | 
			
		||||
			this.firstElement = null;
 | 
			
		||||
			return element;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Classloaders used for reload support
 | 
			
		||||
 */
 | 
			
		||||
package org.springframework.boot.developertools.restart.classloader;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Application restart support
 | 
			
		||||
 */
 | 
			
		||||
package org.springframework.boot.developertools.restart;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,94 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package org.springframework.boot.developertools.restart.server;
 | 
			
		||||
 | 
			
		||||
import java.net.URL;
 | 
			
		||||
import java.util.regex.Matcher;
 | 
			
		||||
import java.util.regex.Pattern;
 | 
			
		||||
 | 
			
		||||
import org.springframework.util.StringUtils;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Default implementation of {@link SourceFolderUrlFilter} that attempts to match URLs
 | 
			
		||||
 * using common naming conventions.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 * @since 1.3.0
 | 
			
		||||
 */
 | 
			
		||||
public class DefaultSourceFolderUrlFilter implements SourceFolderUrlFilter {
 | 
			
		||||
 | 
			
		||||
	private static final String[] COMMON_ENDINGS = { "/target/classes", "/bin" };
 | 
			
		||||
 | 
			
		||||
	private static final Pattern URL_MODULE_PATTERN = Pattern.compile(".*\\/(.+)\\.jar");
 | 
			
		||||
 | 
			
		||||
	private static final Pattern VERSION_PATTERN = Pattern
 | 
			
		||||
			.compile("^-\\d+(?:\\.\\d+)*(?:[.-].+)?$");
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public boolean isMatch(String sourceFolder, URL url) {
 | 
			
		||||
		String jarName = getJarName(url);
 | 
			
		||||
		if (!StringUtils.hasLength(jarName)) {
 | 
			
		||||
			return false;
 | 
			
		||||
		}
 | 
			
		||||
		return isMatch(sourceFolder, jarName);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private String getJarName(URL url) {
 | 
			
		||||
		Matcher matcher = URL_MODULE_PATTERN.matcher(url.toString());
 | 
			
		||||
		if (matcher.find()) {
 | 
			
		||||
			return matcher.group(1);
 | 
			
		||||
		}
 | 
			
		||||
		return null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private boolean isMatch(String sourceFolder, String jarName) {
 | 
			
		||||
		sourceFolder = stripTrailingSlash(sourceFolder);
 | 
			
		||||
		sourceFolder = stripCommonEnds(sourceFolder);
 | 
			
		||||
		String[] folders = StringUtils.delimitedListToStringArray(sourceFolder, "/");
 | 
			
		||||
		for (int i = folders.length - 1; i >= 0; i--) {
 | 
			
		||||
			if (isFolderMatch(folders[i], jarName)) {
 | 
			
		||||
				return true;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private boolean isFolderMatch(String folder, String jarName) {
 | 
			
		||||
		if (!jarName.startsWith(folder)) {
 | 
			
		||||
			return false;
 | 
			
		||||
		}
 | 
			
		||||
		String version = jarName.substring(folder.length());
 | 
			
		||||
		return version.isEmpty() || VERSION_PATTERN.matcher(version).matches();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private String stripTrailingSlash(String string) {
 | 
			
		||||
		if (string.endsWith("/")) {
 | 
			
		||||
			return string.substring(0, string.length() - 1);
 | 
			
		||||
		}
 | 
			
		||||
		return string;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private String stripCommonEnds(String string) {
 | 
			
		||||
		for (String ending : COMMON_ENDINGS) {
 | 
			
		||||
			if (string.endsWith(ending)) {
 | 
			
		||||
				return string.substring(0, string.length() - ending.length());
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return string;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,85 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package org.springframework.boot.developertools.restart.server;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.io.ObjectInputStream;
 | 
			
		||||
 | 
			
		||||
import org.apache.commons.logging.Log;
 | 
			
		||||
import org.apache.commons.logging.LogFactory;
 | 
			
		||||
import org.springframework.boot.developertools.restart.classloader.ClassLoaderFiles;
 | 
			
		||||
import org.springframework.http.HttpStatus;
 | 
			
		||||
import org.springframework.http.server.ServerHttpRequest;
 | 
			
		||||
import org.springframework.http.server.ServerHttpResponse;
 | 
			
		||||
import org.springframework.util.Assert;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A HTTP server that can be used to upload updated {@link ClassLoaderFiles} and trigger
 | 
			
		||||
 * restarts.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 * @since 1.3.0
 | 
			
		||||
 * @see RestartServer
 | 
			
		||||
 */
 | 
			
		||||
public class HttpRestartServer {
 | 
			
		||||
 | 
			
		||||
	private static final Log logger = LogFactory.getLog(HttpRestartServer.class);
 | 
			
		||||
 | 
			
		||||
	private final RestartServer server;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Create a new {@link HttpRestartServer} instance.
 | 
			
		||||
	 * @param sourceFolderUrlFilter the source filter used to link remote folder to the
 | 
			
		||||
	 * local classpath
 | 
			
		||||
	 */
 | 
			
		||||
	public HttpRestartServer(SourceFolderUrlFilter sourceFolderUrlFilter) {
 | 
			
		||||
		Assert.notNull(sourceFolderUrlFilter, "SourceFolderUrlFilter must not be null");
 | 
			
		||||
		this.server = new RestartServer(sourceFolderUrlFilter);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Create a new {@link HttpRestartServer} instance.
 | 
			
		||||
	 * @param restartServer the underlying restart server
 | 
			
		||||
	 */
 | 
			
		||||
	public HttpRestartServer(RestartServer restartServer) {
 | 
			
		||||
		Assert.notNull(restartServer, "RestartServer must not be null");
 | 
			
		||||
		this.server = restartServer;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Handle a server request.
 | 
			
		||||
	 * @param request the request
 | 
			
		||||
	 * @param response the response
 | 
			
		||||
	 * @throws IOException
 | 
			
		||||
	 */
 | 
			
		||||
	public void handle(ServerHttpRequest request, ServerHttpResponse response)
 | 
			
		||||
			throws IOException {
 | 
			
		||||
		try {
 | 
			
		||||
			Assert.state(request.getHeaders().getContentLength() > 0, "No content");
 | 
			
		||||
			ObjectInputStream objectInputStream = new ObjectInputStream(request.getBody());
 | 
			
		||||
			ClassLoaderFiles files = (ClassLoaderFiles) objectInputStream.readObject();
 | 
			
		||||
			objectInputStream.close();
 | 
			
		||||
			this.server.updateAndRestart(files);
 | 
			
		||||
			response.setStatusCode(HttpStatus.OK);
 | 
			
		||||
		}
 | 
			
		||||
		catch (Exception ex) {
 | 
			
		||||
			logger.warn("Unable to handler restart server HTTP request", ex);
 | 
			
		||||
			response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,51 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package org.springframework.boot.developertools.restart.server;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
 | 
			
		||||
import org.springframework.boot.developertools.remote.server.Handler;
 | 
			
		||||
import org.springframework.http.server.ServerHttpRequest;
 | 
			
		||||
import org.springframework.http.server.ServerHttpResponse;
 | 
			
		||||
import org.springframework.util.Assert;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Adapts {@link HttpRestartServer} to a {@link Handler}.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 * @since 1.3.0
 | 
			
		||||
 */
 | 
			
		||||
public class HttpRestartServerHandler implements Handler {
 | 
			
		||||
 | 
			
		||||
	private final HttpRestartServer server;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Create a new {@link HttpRestartServerHandler} instance.
 | 
			
		||||
	 * @param server the server to adapt
 | 
			
		||||
	 */
 | 
			
		||||
	public HttpRestartServerHandler(HttpRestartServer server) {
 | 
			
		||||
		Assert.notNull(server, "Server must not be null");
 | 
			
		||||
		this.server = server;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void handle(ServerHttpRequest request, ServerHttpResponse response)
 | 
			
		||||
			throws IOException {
 | 
			
		||||
		this.server.handle(request, response);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,182 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package org.springframework.boot.developertools.restart.server;
 | 
			
		||||
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.net.URL;
 | 
			
		||||
import java.net.URLClassLoader;
 | 
			
		||||
import java.util.LinkedHashSet;
 | 
			
		||||
import java.util.Map.Entry;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
 | 
			
		||||
import org.apache.commons.logging.Log;
 | 
			
		||||
import org.apache.commons.logging.LogFactory;
 | 
			
		||||
import org.springframework.boot.developertools.restart.Restarter;
 | 
			
		||||
import org.springframework.boot.developertools.restart.classloader.ClassLoaderFile;
 | 
			
		||||
import org.springframework.boot.developertools.restart.classloader.ClassLoaderFile.Kind;
 | 
			
		||||
import org.springframework.boot.developertools.restart.classloader.ClassLoaderFiles;
 | 
			
		||||
import org.springframework.boot.developertools.restart.classloader.ClassLoaderFiles.SourceFolder;
 | 
			
		||||
import org.springframework.util.Assert;
 | 
			
		||||
import org.springframework.util.FileCopyUtils;
 | 
			
		||||
import org.springframework.util.ResourceUtils;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Server used to {@link Restarter restart} the current application with updated
 | 
			
		||||
 * {@link ClassLoaderFiles}.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 * @since 1.3.0
 | 
			
		||||
 */
 | 
			
		||||
public class RestartServer {
 | 
			
		||||
 | 
			
		||||
	private static final Log logger = LogFactory.getLog(RestartServer.class);
 | 
			
		||||
 | 
			
		||||
	private final SourceFolderUrlFilter sourceFolderUrlFilter;
 | 
			
		||||
 | 
			
		||||
	private final ClassLoader classLoader;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Create a new {@link RestartServer} instance.
 | 
			
		||||
	 * @param sourceFolderUrlFilter the source filter used to link remote folder to the
 | 
			
		||||
	 * local classpath
 | 
			
		||||
	 */
 | 
			
		||||
	public RestartServer(SourceFolderUrlFilter sourceFolderUrlFilter) {
 | 
			
		||||
		this(sourceFolderUrlFilter, Thread.currentThread().getContextClassLoader());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Create a new {@link RestartServer} instance.
 | 
			
		||||
	 * @param sourceFolderUrlFilter the source filter used to link remote folder to the
 | 
			
		||||
	 * local classpath
 | 
			
		||||
	 * @param classLoader the application classloader
 | 
			
		||||
	 */
 | 
			
		||||
	public RestartServer(SourceFolderUrlFilter sourceFolderUrlFilter,
 | 
			
		||||
			ClassLoader classLoader) {
 | 
			
		||||
		Assert.notNull(sourceFolderUrlFilter, "SourceFolderUrlFilter must not be null");
 | 
			
		||||
		Assert.notNull(classLoader, "ClassLoader must not be null");
 | 
			
		||||
		this.sourceFolderUrlFilter = sourceFolderUrlFilter;
 | 
			
		||||
		this.classLoader = classLoader;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Update the current running application with the specified {@link ClassLoaderFiles}
 | 
			
		||||
	 * and trigger a reload.
 | 
			
		||||
	 * @param files updated class loader files
 | 
			
		||||
	 */
 | 
			
		||||
	public void updateAndRestart(ClassLoaderFiles files) {
 | 
			
		||||
		Set<URL> urls = new LinkedHashSet<URL>();
 | 
			
		||||
		Set<URL> classLoaderUrls = getClassLoaderUrls();
 | 
			
		||||
		for (SourceFolder folder : files.getSourceFolders()) {
 | 
			
		||||
			for (Entry<String, ClassLoaderFile> entry : folder.getFilesEntrySet()) {
 | 
			
		||||
				for (URL url : classLoaderUrls) {
 | 
			
		||||
					if (updateFileSystem(url, entry.getKey(), entry.getValue())) {
 | 
			
		||||
						urls.add(url);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			urls.addAll(getMatchingUrls(classLoaderUrls, folder.getName()));
 | 
			
		||||
		}
 | 
			
		||||
		updateTimeStamp(urls);
 | 
			
		||||
		restart(urls, files);
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private boolean updateFileSystem(URL url, String name, ClassLoaderFile classLoaderFile) {
 | 
			
		||||
		if (!isFolderUrl(url.toString())) {
 | 
			
		||||
			return false;
 | 
			
		||||
		}
 | 
			
		||||
		try {
 | 
			
		||||
			File folder = ResourceUtils.getFile(url);
 | 
			
		||||
			File file = new File(folder, name);
 | 
			
		||||
			if (file.exists() && file.canWrite()) {
 | 
			
		||||
				if (classLoaderFile.getKind() == Kind.DELETED) {
 | 
			
		||||
					return file.delete();
 | 
			
		||||
				}
 | 
			
		||||
				FileCopyUtils.copy(classLoaderFile.getContents(), file);
 | 
			
		||||
				return true;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		catch (IOException ex) {
 | 
			
		||||
			// Ignore
 | 
			
		||||
		}
 | 
			
		||||
		return false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private boolean isFolderUrl(String urlString) {
 | 
			
		||||
		return urlString.startsWith("file:") && urlString.endsWith("/");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private Set<URL> getMatchingUrls(Set<URL> urls, String sourceFolder) {
 | 
			
		||||
		Set<URL> matchingUrls = new LinkedHashSet<URL>();
 | 
			
		||||
		for (URL url : urls) {
 | 
			
		||||
			if (this.sourceFolderUrlFilter.isMatch(sourceFolder, url)) {
 | 
			
		||||
				if (logger.isDebugEnabled()) {
 | 
			
		||||
					logger.debug("URL " + url + " matched against source folder "
 | 
			
		||||
							+ sourceFolder);
 | 
			
		||||
				}
 | 
			
		||||
				matchingUrls.add(url);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return matchingUrls;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private Set<URL> getClassLoaderUrls() {
 | 
			
		||||
		Set<URL> urls = new LinkedHashSet<URL>();
 | 
			
		||||
		ClassLoader classLoader = this.classLoader;
 | 
			
		||||
		while (classLoader != null) {
 | 
			
		||||
			if (classLoader instanceof URLClassLoader) {
 | 
			
		||||
				for (URL url : ((URLClassLoader) classLoader).getURLs()) {
 | 
			
		||||
					urls.add(url);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			classLoader = classLoader.getParent();
 | 
			
		||||
		}
 | 
			
		||||
		return urls;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void updateTimeStamp(Iterable<URL> urls) {
 | 
			
		||||
		for (URL url : urls) {
 | 
			
		||||
			updateTimeStamp(url);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void updateTimeStamp(URL url) {
 | 
			
		||||
		try {
 | 
			
		||||
			URL actualUrl = ResourceUtils.extractJarFileURL(url);
 | 
			
		||||
			File file = ResourceUtils.getFile(actualUrl, "Jar URL");
 | 
			
		||||
			file.setLastModified(System.currentTimeMillis());
 | 
			
		||||
		}
 | 
			
		||||
		catch (Exception ex) {
 | 
			
		||||
			// Ignore
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Called to restart the application.
 | 
			
		||||
	 * @param urls the updated URLs
 | 
			
		||||
	 * @param files the updated files
 | 
			
		||||
	 */
 | 
			
		||||
	protected void restart(Set<URL> urls, ClassLoaderFiles files) {
 | 
			
		||||
		Restarter restarter = Restarter.getInstance();
 | 
			
		||||
		restarter.addUrls(urls);
 | 
			
		||||
		restarter.addClassLoaderFiles(files);
 | 
			
		||||
		restarter.restart();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,39 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package org.springframework.boot.developertools.restart.server;
 | 
			
		||||
 | 
			
		||||
import java.net.URL;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Filter URLs based on a source folder name. Used to match URLs from the running
 | 
			
		||||
 * classpath against source folders on a remote system.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 * @since 1.3.0
 | 
			
		||||
 * @see DefaultSourceFolderUrlFilter
 | 
			
		||||
 */
 | 
			
		||||
public interface SourceFolderUrlFilter {
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Determine if the specified URL matches a source folder.
 | 
			
		||||
	 * @param sourceFolder the source folder
 | 
			
		||||
	 * @param url the URL to check
 | 
			
		||||
	 * @return {@code true} if the URL matches
 | 
			
		||||
	 */
 | 
			
		||||
	boolean isMatch(String sourceFolder, URL url);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Remote restart server
 | 
			
		||||
 */
 | 
			
		||||
package org.springframework.boot.developertools.restart.server;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,216 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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.tunnel.client;
 | 
			
		||||
 | 
			
		||||
import java.io.Closeable;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.net.MalformedURLException;
 | 
			
		||||
import java.net.URI;
 | 
			
		||||
import java.net.URISyntaxException;
 | 
			
		||||
import java.net.URL;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.nio.channels.WritableByteChannel;
 | 
			
		||||
import java.util.concurrent.Executor;
 | 
			
		||||
import java.util.concurrent.Executors;
 | 
			
		||||
import java.util.concurrent.ThreadFactory;
 | 
			
		||||
import java.util.concurrent.atomic.AtomicLong;
 | 
			
		||||
 | 
			
		||||
import org.apache.commons.logging.Log;
 | 
			
		||||
import org.apache.commons.logging.LogFactory;
 | 
			
		||||
import org.springframework.boot.developertools.tunnel.payload.HttpTunnelPayload;
 | 
			
		||||
import org.springframework.boot.developertools.tunnel.payload.HttpTunnelPayloadForwarder;
 | 
			
		||||
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 TunnelConnection} implementation that uses HTTP to transfer data.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 * @author Rob Winch
 | 
			
		||||
 * @since 1.3.0
 | 
			
		||||
 * @see TunnelClient
 | 
			
		||||
 * @see org.springframework.boot.developertools.tunnel.server.HttpTunnelServer
 | 
			
		||||
 */
 | 
			
		||||
public class HttpTunnelConnection implements TunnelConnection {
 | 
			
		||||
 | 
			
		||||
	private static Log logger = LogFactory.getLog(HttpTunnelConnection.class);
 | 
			
		||||
 | 
			
		||||
	private final URI uri;
 | 
			
		||||
 | 
			
		||||
	private final ClientHttpRequestFactory requestFactory;
 | 
			
		||||
 | 
			
		||||
	private final Executor executor;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Create a new {@link HttpTunnelConnection} instance.
 | 
			
		||||
	 * @param url the URL to connect to
 | 
			
		||||
	 * @param requestFactory the HTTP request factory
 | 
			
		||||
	 */
 | 
			
		||||
	public HttpTunnelConnection(String url, ClientHttpRequestFactory requestFactory) {
 | 
			
		||||
		this(url, requestFactory, null);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Create a new {@link HttpTunnelConnection} instance.
 | 
			
		||||
	 * @param url the URL to connect to
 | 
			
		||||
	 * @param requestFactory the HTTP request factory
 | 
			
		||||
	 * @param executor the executor used to handle connections
 | 
			
		||||
	 */
 | 
			
		||||
	protected HttpTunnelConnection(String url, ClientHttpRequestFactory requestFactory,
 | 
			
		||||
			Executor executor) {
 | 
			
		||||
		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;
 | 
			
		||||
		this.executor = (executor == null ? Executors
 | 
			
		||||
				.newCachedThreadPool(new TunnelThreadFactory()) : executor);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public TunnelChannel open(WritableByteChannel incomingChannel, Closeable closeable)
 | 
			
		||||
			throws Exception {
 | 
			
		||||
		logger.trace("Opening HTTP tunnel to " + this.uri);
 | 
			
		||||
		return new TunnelChannel(incomingChannel, closeable);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	protected final ClientHttpRequest createRequest(boolean hasPayload)
 | 
			
		||||
			throws IOException {
 | 
			
		||||
		HttpMethod method = (hasPayload ? HttpMethod.POST : HttpMethod.GET);
 | 
			
		||||
		return this.requestFactory.createRequest(this.uri, method);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * A {@link WritableByteChannel} used to transfer traffic.
 | 
			
		||||
	 */
 | 
			
		||||
	protected class TunnelChannel implements WritableByteChannel {
 | 
			
		||||
 | 
			
		||||
		private final HttpTunnelPayloadForwarder forwarder;
 | 
			
		||||
 | 
			
		||||
		private final Closeable closeable;
 | 
			
		||||
 | 
			
		||||
		private boolean open = true;
 | 
			
		||||
 | 
			
		||||
		private AtomicLong requestSeq = new AtomicLong();
 | 
			
		||||
 | 
			
		||||
		public TunnelChannel(WritableByteChannel incomingChannel, Closeable closeable) {
 | 
			
		||||
			this.forwarder = new HttpTunnelPayloadForwarder(incomingChannel);
 | 
			
		||||
			this.closeable = closeable;
 | 
			
		||||
			openNewConnection(null);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public boolean isOpen() {
 | 
			
		||||
			return this.open;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public void close() throws IOException {
 | 
			
		||||
			if (this.open) {
 | 
			
		||||
				this.open = false;
 | 
			
		||||
				this.closeable.close();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public int write(ByteBuffer src) throws IOException {
 | 
			
		||||
			int size = src.remaining();
 | 
			
		||||
			if (size > 0) {
 | 
			
		||||
				openNewConnection(new HttpTunnelPayload(
 | 
			
		||||
						this.requestSeq.incrementAndGet(), src));
 | 
			
		||||
			}
 | 
			
		||||
			return size;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		private synchronized void openNewConnection(final HttpTunnelPayload payload) {
 | 
			
		||||
			HttpTunnelConnection.this.executor.execute(new Runnable() {
 | 
			
		||||
 | 
			
		||||
				@Override
 | 
			
		||||
				public void run() {
 | 
			
		||||
					try {
 | 
			
		||||
						sendAndReceive(payload);
 | 
			
		||||
					}
 | 
			
		||||
					catch (IOException ex) {
 | 
			
		||||
						logger.trace("Unexpected connection error", ex);
 | 
			
		||||
						closeQuitely();
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				private void closeQuitely() {
 | 
			
		||||
					try {
 | 
			
		||||
						close();
 | 
			
		||||
					}
 | 
			
		||||
					catch (IOException ex) {
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		private void sendAndReceive(HttpTunnelPayload payload) throws IOException {
 | 
			
		||||
			ClientHttpRequest request = createRequest(payload != null);
 | 
			
		||||
			if (payload != null) {
 | 
			
		||||
				payload.logIncoming();
 | 
			
		||||
				payload.assignTo(request);
 | 
			
		||||
			}
 | 
			
		||||
			handleResponse(request.execute());
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		private void handleResponse(ClientHttpResponse response) throws IOException {
 | 
			
		||||
			if (response.getStatusCode() == HttpStatus.GONE) {
 | 
			
		||||
				close();
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
			if (response.getStatusCode() == HttpStatus.OK) {
 | 
			
		||||
				HttpTunnelPayload payload = HttpTunnelPayload.get(response);
 | 
			
		||||
				if (payload != null) {
 | 
			
		||||
					this.forwarder.forward(payload);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			if (response.getStatusCode() != HttpStatus.TOO_MANY_REQUESTS) {
 | 
			
		||||
				openNewConnection(null);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * {@link ThreadFactory} used to create the tunnel thread.
 | 
			
		||||
	 */
 | 
			
		||||
	private static class TunnelThreadFactory implements ThreadFactory {
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public Thread newThread(Runnable runnable) {
 | 
			
		||||
			Thread thread = new Thread(runnable, "HTTP Tunnel Connection");
 | 
			
		||||
			thread.setDaemon(true);
 | 
			
		||||
			return thread;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,207 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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.tunnel.client;
 | 
			
		||||
 | 
			
		||||
import java.io.Closeable;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.net.InetSocketAddress;
 | 
			
		||||
import java.net.ServerSocket;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.nio.channels.ServerSocketChannel;
 | 
			
		||||
import java.nio.channels.SocketChannel;
 | 
			
		||||
import java.nio.channels.WritableByteChannel;
 | 
			
		||||
 | 
			
		||||
import org.apache.commons.logging.Log;
 | 
			
		||||
import org.apache.commons.logging.LogFactory;
 | 
			
		||||
import org.springframework.beans.factory.SmartInitializingSingleton;
 | 
			
		||||
import org.springframework.util.Assert;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The client side component of a socket tunnel. Starts a {@link ServerSocket} of the
 | 
			
		||||
 * specified port for local clients to connect to.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 * @since 1.3.0
 | 
			
		||||
 */
 | 
			
		||||
public class TunnelClient implements SmartInitializingSingleton {
 | 
			
		||||
 | 
			
		||||
	private static final int BUFFER_SIZE = 1024 * 100;
 | 
			
		||||
 | 
			
		||||
	private static final Log logger = LogFactory.getLog(TunnelClient.class);
 | 
			
		||||
 | 
			
		||||
	private final int listenPort;
 | 
			
		||||
 | 
			
		||||
	private final TunnelConnection tunnelConnection;
 | 
			
		||||
 | 
			
		||||
	private TunnelClientListeners listeners = new TunnelClientListeners();
 | 
			
		||||
 | 
			
		||||
	private ServerThread serverThread;
 | 
			
		||||
 | 
			
		||||
	public TunnelClient(int listenPort, TunnelConnection tunnelConnection) {
 | 
			
		||||
		Assert.isTrue(listenPort > 0, "ListenPort must be positive");
 | 
			
		||||
		Assert.notNull(tunnelConnection, "TunnelConnection must not be null");
 | 
			
		||||
		this.listenPort = listenPort;
 | 
			
		||||
		this.tunnelConnection = tunnelConnection;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void afterSingletonsInstantiated() {
 | 
			
		||||
		if (this.serverThread == null) {
 | 
			
		||||
			try {
 | 
			
		||||
				start();
 | 
			
		||||
			}
 | 
			
		||||
			catch (IOException ex) {
 | 
			
		||||
				throw new IllegalStateException(ex);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Start the client and accept incoming connections on the port.
 | 
			
		||||
	 * @throws IOException
 | 
			
		||||
	 */
 | 
			
		||||
	public synchronized void start() throws IOException {
 | 
			
		||||
		Assert.state(this.serverThread == null, "Server already started");
 | 
			
		||||
		ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
 | 
			
		||||
		serverSocketChannel.socket().bind(new InetSocketAddress(this.listenPort));
 | 
			
		||||
		logger.trace("Listening for TCP traffic to tunnel on port " + this.listenPort);
 | 
			
		||||
		this.serverThread = new ServerThread(serverSocketChannel);
 | 
			
		||||
		this.serverThread.start();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Stop the client, disconnecting any servers.
 | 
			
		||||
	 * @throws IOException
 | 
			
		||||
	 */
 | 
			
		||||
	public synchronized void stop() throws IOException {
 | 
			
		||||
		if (this.serverThread != null) {
 | 
			
		||||
			logger.trace("Closing tunnel client on port " + this.listenPort);
 | 
			
		||||
			this.serverThread.close();
 | 
			
		||||
			try {
 | 
			
		||||
				this.serverThread.join(2000);
 | 
			
		||||
			}
 | 
			
		||||
			catch (InterruptedException ex) {
 | 
			
		||||
			}
 | 
			
		||||
			this.serverThread = null;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	protected final ServerThread getServerThread() {
 | 
			
		||||
		return this.serverThread;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public void addListener(TunnelClientListener listener) {
 | 
			
		||||
		this.listeners.addListener(listener);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public void removeListener(TunnelClientListener listener) {
 | 
			
		||||
		this.listeners.removeListener(listener);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * The main server thread.
 | 
			
		||||
	 */
 | 
			
		||||
	protected class ServerThread extends Thread {
 | 
			
		||||
 | 
			
		||||
		private final ServerSocketChannel serverSocketChannel;
 | 
			
		||||
 | 
			
		||||
		private boolean acceptConnections = true;
 | 
			
		||||
 | 
			
		||||
		public ServerThread(ServerSocketChannel serverSocketChannel) {
 | 
			
		||||
			this.serverSocketChannel = serverSocketChannel;
 | 
			
		||||
			setName("Tunnel Server");
 | 
			
		||||
			setDaemon(true);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public void close() throws IOException {
 | 
			
		||||
			this.serverSocketChannel.close();
 | 
			
		||||
			this.acceptConnections = false;
 | 
			
		||||
			interrupt();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public void run() {
 | 
			
		||||
			try {
 | 
			
		||||
				while (this.acceptConnections) {
 | 
			
		||||
					SocketChannel socket = this.serverSocketChannel.accept();
 | 
			
		||||
					try {
 | 
			
		||||
						handleConnection(socket);
 | 
			
		||||
					}
 | 
			
		||||
					finally {
 | 
			
		||||
						socket.close();
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			catch (Exception ex) {
 | 
			
		||||
				logger.trace("Unexpected exception from tunnel client", ex);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		private void handleConnection(SocketChannel socketChannel) throws Exception {
 | 
			
		||||
			Closeable closeable = new SocketCloseable(socketChannel);
 | 
			
		||||
			WritableByteChannel outputChannel = TunnelClient.this.tunnelConnection.open(
 | 
			
		||||
					socketChannel, closeable);
 | 
			
		||||
			TunnelClient.this.listeners.fireOpenEvent(socketChannel);
 | 
			
		||||
			try {
 | 
			
		||||
				logger.trace("Accepted connection to tunnel client from "
 | 
			
		||||
						+ socketChannel.socket().getRemoteSocketAddress());
 | 
			
		||||
				while (true) {
 | 
			
		||||
					ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
 | 
			
		||||
					int amountRead = socketChannel.read(buffer);
 | 
			
		||||
					if (amountRead == -1) {
 | 
			
		||||
						outputChannel.close();
 | 
			
		||||
						return;
 | 
			
		||||
					}
 | 
			
		||||
					if (amountRead > 0) {
 | 
			
		||||
						buffer.flip();
 | 
			
		||||
						outputChannel.write(buffer);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			finally {
 | 
			
		||||
				outputChannel.close();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		protected void stopAcceptingConnections() {
 | 
			
		||||
			this.acceptConnections = false;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * {@link Closeable} used to close a {@link SocketChannel} and fire an event.
 | 
			
		||||
	 */
 | 
			
		||||
	private class SocketCloseable implements Closeable {
 | 
			
		||||
 | 
			
		||||
		private final SocketChannel socketChannel;
 | 
			
		||||
 | 
			
		||||
		private boolean closed = false;
 | 
			
		||||
 | 
			
		||||
		public SocketCloseable(SocketChannel socketChannel) {
 | 
			
		||||
			this.socketChannel = socketChannel;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public void close() throws IOException {
 | 
			
		||||
			if (!this.closed) {
 | 
			
		||||
				this.socketChannel.close();
 | 
			
		||||
				TunnelClient.this.listeners.fireCloseEvent(this.socketChannel);
 | 
			
		||||
				this.closed = true;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,41 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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.tunnel.client;
 | 
			
		||||
 | 
			
		||||
import java.nio.channels.SocketChannel;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Listener that can be used to receive {@link TunnelClient} events.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 * @since 1.3.0
 | 
			
		||||
 */
 | 
			
		||||
public interface TunnelClientListener {
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Called when a socket channel is opened.
 | 
			
		||||
	 * @param socket the socket channel
 | 
			
		||||
	 */
 | 
			
		||||
	void onOpen(SocketChannel socket);
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Called when a socket channel is closed.
 | 
			
		||||
	 * @param socket the socket channel
 | 
			
		||||
	 */
 | 
			
		||||
	void onClose(SocketChannel socket);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,56 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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.tunnel.client;
 | 
			
		||||
 | 
			
		||||
import java.nio.channels.SocketChannel;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
import org.springframework.util.Assert;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A collection of {@link TunnelClientListener}.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 */
 | 
			
		||||
class TunnelClientListeners {
 | 
			
		||||
 | 
			
		||||
	private final List<TunnelClientListener> listeners = new ArrayList<TunnelClientListener>();
 | 
			
		||||
 | 
			
		||||
	public void addListener(TunnelClientListener listener) {
 | 
			
		||||
		Assert.notNull(listener, "Listener must not be null");
 | 
			
		||||
		this.listeners.add(listener);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public void removeListener(TunnelClientListener listener) {
 | 
			
		||||
		Assert.notNull(listener, "Listener must not be null");
 | 
			
		||||
		this.listeners.remove(listener);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public void fireOpenEvent(SocketChannel socket) {
 | 
			
		||||
		for (TunnelClientListener listener : this.listeners) {
 | 
			
		||||
			listener.onOpen(socket);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public void fireCloseEvent(SocketChannel socket) {
 | 
			
		||||
		for (TunnelClientListener listener : this.listeners) {
 | 
			
		||||
			listener.onClose(socket);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,42 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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.tunnel.client;
 | 
			
		||||
 | 
			
		||||
import java.io.Closeable;
 | 
			
		||||
import java.nio.channels.WritableByteChannel;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Interface used to manage socket tunnel connections.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 * @since 1.3.0
 | 
			
		||||
 */
 | 
			
		||||
public interface TunnelConnection {
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Open the tunnel connection.
 | 
			
		||||
	 * @param incomingChannel A {@link WritableByteChannel} that should be used to write
 | 
			
		||||
	 * any incoming data received from the remote server.
 | 
			
		||||
	 * @param closeable
 | 
			
		||||
	 * @return A {@link WritableByteChannel} that should be used to send any outgoing data
 | 
			
		||||
	 * destined for the remote server
 | 
			
		||||
	 * @throws Exception
 | 
			
		||||
	 */
 | 
			
		||||
	WritableByteChannel open(WritableByteChannel incomingChannel, Closeable closeable)
 | 
			
		||||
			throws Exception;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Client side TCP tunnel support.
 | 
			
		||||
 */
 | 
			
		||||
package org.springframework.boot.developertools.tunnel.client;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,23 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provides support for tunneling TCP traffic over HTTP. Tunneling is primarily designed
 | 
			
		||||
 * for the Java Debug Wire Protocol (JDWP) and as such only expects a single connection
 | 
			
		||||
 * and isn't particularly worried about resource usage.
 | 
			
		||||
 */
 | 
			
		||||
package org.springframework.boot.developertools.tunnel;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,185 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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.tunnel.payload;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.io.InterruptedIOException;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.nio.channels.Channels;
 | 
			
		||||
import java.nio.channels.ReadableByteChannel;
 | 
			
		||||
import java.nio.channels.WritableByteChannel;
 | 
			
		||||
 | 
			
		||||
import org.apache.commons.logging.Log;
 | 
			
		||||
import org.apache.commons.logging.LogFactory;
 | 
			
		||||
import org.springframework.http.HttpHeaders;
 | 
			
		||||
import org.springframework.http.HttpInputMessage;
 | 
			
		||||
import org.springframework.http.HttpOutputMessage;
 | 
			
		||||
import org.springframework.http.MediaType;
 | 
			
		||||
import org.springframework.util.Assert;
 | 
			
		||||
import org.springframework.util.StringUtils;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Encapsulates a payload data sent via a HTTP tunnel.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 * @since 1.3.0
 | 
			
		||||
 */
 | 
			
		||||
public class HttpTunnelPayload {
 | 
			
		||||
 | 
			
		||||
	private static final String SEQ_HEADER = "x-seq";
 | 
			
		||||
 | 
			
		||||
	private static final int BUFFER_SIZE = 1024 * 100;
 | 
			
		||||
 | 
			
		||||
	final protected static char[] HEX_CHARS = "0123456789ABCDEF".toCharArray();
 | 
			
		||||
 | 
			
		||||
	private static final Log logger = LogFactory.getLog(HttpTunnelPayload.class);
 | 
			
		||||
 | 
			
		||||
	private final long sequence;
 | 
			
		||||
 | 
			
		||||
	private final ByteBuffer data;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Create a new {@link HttpTunnelPayload} instance.
 | 
			
		||||
	 * @param sequence the sequence number of the payload
 | 
			
		||||
	 * @param data the payload data
 | 
			
		||||
	 */
 | 
			
		||||
	public HttpTunnelPayload(long sequence, ByteBuffer data) {
 | 
			
		||||
		Assert.isTrue(sequence > 0, "Sequence must be positive");
 | 
			
		||||
		Assert.notNull(data, "Data must not be null");
 | 
			
		||||
		this.sequence = sequence;
 | 
			
		||||
		this.data = data;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Return the sequence number of the payload.
 | 
			
		||||
	 * @return the sequence
 | 
			
		||||
	 */
 | 
			
		||||
	public long getSequence() {
 | 
			
		||||
		return this.sequence;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Assign this payload to the given {@link HttpOutputMessage}.
 | 
			
		||||
	 * @param message the message to assign this payload to
 | 
			
		||||
	 * @throws IOException
 | 
			
		||||
	 */
 | 
			
		||||
	public void assignTo(HttpOutputMessage message) throws IOException {
 | 
			
		||||
		Assert.notNull(message, "Message must not be null");
 | 
			
		||||
		HttpHeaders headers = message.getHeaders();
 | 
			
		||||
		headers.setContentLength(this.data.remaining());
 | 
			
		||||
		headers.add(SEQ_HEADER, Long.toString(getSequence()));
 | 
			
		||||
		headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
 | 
			
		||||
		WritableByteChannel body = Channels.newChannel(message.getBody());
 | 
			
		||||
		while (this.data.hasRemaining()) {
 | 
			
		||||
			body.write(this.data);
 | 
			
		||||
		}
 | 
			
		||||
		body.close();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Write the content of this payload to the given target channel.
 | 
			
		||||
	 * @param channel the channel to write to
 | 
			
		||||
	 * @throws IOException
 | 
			
		||||
	 */
 | 
			
		||||
	public void writeTo(WritableByteChannel channel) throws IOException {
 | 
			
		||||
		Assert.notNull(channel, "Channel must not be null");
 | 
			
		||||
		while (this.data.hasRemaining()) {
 | 
			
		||||
			channel.write(this.data);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Return the {@link HttpTunnelPayload} for the given message or {@code null} if there
 | 
			
		||||
	 * is no payload.
 | 
			
		||||
	 * @param message the HTTP message
 | 
			
		||||
	 * @return the payload or {@code null}
 | 
			
		||||
	 * @throws IOException
 | 
			
		||||
	 */
 | 
			
		||||
	public static HttpTunnelPayload get(HttpInputMessage message) throws IOException {
 | 
			
		||||
		long length = message.getHeaders().getContentLength();
 | 
			
		||||
		if (length <= 0) {
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
		String seqHeader = message.getHeaders().getFirst(SEQ_HEADER);
 | 
			
		||||
		Assert.state(StringUtils.hasLength(seqHeader), "Missing sequence header");
 | 
			
		||||
		ReadableByteChannel body = Channels.newChannel(message.getBody());
 | 
			
		||||
		ByteBuffer payload = ByteBuffer.allocate((int) length);
 | 
			
		||||
		while (payload.hasRemaining()) {
 | 
			
		||||
			body.read(payload);
 | 
			
		||||
		}
 | 
			
		||||
		body.close();
 | 
			
		||||
		payload.flip();
 | 
			
		||||
		return new HttpTunnelPayload(Long.valueOf(seqHeader), payload);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Return the payload data for the given source {@link ReadableByteChannel} or null if
 | 
			
		||||
	 * the channel timed out whilst reading.
 | 
			
		||||
	 * @param channel the source channel
 | 
			
		||||
	 * @return payload data or {@code null}
 | 
			
		||||
	 * @throws IOException
 | 
			
		||||
	 */
 | 
			
		||||
	public static ByteBuffer getPayloadData(ReadableByteChannel channel)
 | 
			
		||||
			throws IOException {
 | 
			
		||||
		ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
 | 
			
		||||
		try {
 | 
			
		||||
			int amountRead = channel.read(buffer);
 | 
			
		||||
			Assert.state(amountRead != -1, "Target server connection closed");
 | 
			
		||||
			buffer.flip();
 | 
			
		||||
			return buffer;
 | 
			
		||||
		}
 | 
			
		||||
		catch (InterruptedIOException ex) {
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Log incoming payload information at trace level to aid diagnostics.
 | 
			
		||||
	 */
 | 
			
		||||
	public void logIncoming() {
 | 
			
		||||
		log("< ");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Log incoming payload information at trace level to aid diagnostics.
 | 
			
		||||
	 */
 | 
			
		||||
	public void logOutgoing() {
 | 
			
		||||
		log("> ");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void log(String prefix) {
 | 
			
		||||
		if (logger.isTraceEnabled()) {
 | 
			
		||||
			logger.trace(prefix + toHexString());
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Return the payload as a hexadecimal string.
 | 
			
		||||
	 * @return the payload as a hex string
 | 
			
		||||
	 */
 | 
			
		||||
	public String toHexString() {
 | 
			
		||||
		byte[] bytes = this.data.array();
 | 
			
		||||
		char[] hex = new char[this.data.remaining() * 2];
 | 
			
		||||
		for (int i = this.data.position(); i < this.data.remaining(); i++) {
 | 
			
		||||
			int b = bytes[i] & 0xFF;
 | 
			
		||||
			hex[i * 2] = HEX_CHARS[b >>> 4];
 | 
			
		||||
			hex[i * 2 + 1] = HEX_CHARS[b & 0x0F];
 | 
			
		||||
		}
 | 
			
		||||
		return new String(hex);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,69 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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.tunnel.payload;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.nio.channels.WritableByteChannel;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
import org.springframework.util.Assert;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Utility class that forwards {@link HttpTunnelPayload} instances to a destination
 | 
			
		||||
 * channel, respecting sequence order.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 * @since 1.3.0
 | 
			
		||||
 */
 | 
			
		||||
public class HttpTunnelPayloadForwarder {
 | 
			
		||||
 | 
			
		||||
	private static final int MAXIMUM_QUEUE_SIZE = 100;
 | 
			
		||||
 | 
			
		||||
	private final WritableByteChannel targetChannel;
 | 
			
		||||
 | 
			
		||||
	private long lastRequestSeq = 0;
 | 
			
		||||
 | 
			
		||||
	private final Map<Long, HttpTunnelPayload> queue = new HashMap<Long, HttpTunnelPayload>();
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Create a new {@link HttpTunnelPayloadForwarder} instance.
 | 
			
		||||
	 * @param targetChannel the target channel
 | 
			
		||||
	 */
 | 
			
		||||
	public HttpTunnelPayloadForwarder(WritableByteChannel targetChannel) {
 | 
			
		||||
		Assert.notNull(targetChannel, "TargetChannel must not be null");
 | 
			
		||||
		this.targetChannel = targetChannel;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public synchronized void forward(HttpTunnelPayload payload) throws IOException {
 | 
			
		||||
		long seq = payload.getSequence();
 | 
			
		||||
		if (this.lastRequestSeq != seq - 1) {
 | 
			
		||||
			Assert.state(this.queue.size() < MAXIMUM_QUEUE_SIZE,
 | 
			
		||||
					"Too many messages queued");
 | 
			
		||||
			this.queue.put(seq, payload);
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		payload.logOutgoing();
 | 
			
		||||
		payload.writeTo(this.targetChannel);
 | 
			
		||||
		this.lastRequestSeq = seq;
 | 
			
		||||
		HttpTunnelPayload queuedItem = this.queue.get(seq + 1);
 | 
			
		||||
		if (queuedItem != null) {
 | 
			
		||||
			forward(queuedItem);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Classes to deal with payloads sent over a HTTP tunnel.
 | 
			
		||||
 */
 | 
			
		||||
package org.springframework.boot.developertools.tunnel.payload;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,486 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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.tunnel.server;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.net.ConnectException;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.nio.channels.ByteChannel;
 | 
			
		||||
import java.util.ArrayDeque;
 | 
			
		||||
import java.util.Deque;
 | 
			
		||||
import java.util.Iterator;
 | 
			
		||||
import java.util.concurrent.atomic.AtomicLong;
 | 
			
		||||
 | 
			
		||||
import org.apache.commons.logging.Log;
 | 
			
		||||
import org.apache.commons.logging.LogFactory;
 | 
			
		||||
import org.springframework.boot.developertools.tunnel.payload.HttpTunnelPayload;
 | 
			
		||||
import org.springframework.boot.developertools.tunnel.payload.HttpTunnelPayloadForwarder;
 | 
			
		||||
import org.springframework.http.HttpStatus;
 | 
			
		||||
import org.springframework.http.MediaType;
 | 
			
		||||
import org.springframework.http.server.ServerHttpAsyncRequestControl;
 | 
			
		||||
import org.springframework.http.server.ServerHttpRequest;
 | 
			
		||||
import org.springframework.http.server.ServerHttpResponse;
 | 
			
		||||
import org.springframework.util.Assert;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A server that can be used to tunnel TCP traffic over HTTP. Similar in design to the <a
 | 
			
		||||
 * href="http://xmpp.org/extensions/xep-0124.html">Bidirectional-streams Over Synchronous
 | 
			
		||||
 * HTTP (BOSH)</a> XMPP extension protocol, the server uses long polling with HTTP
 | 
			
		||||
 * requests held open until a response is available. A typical traffic pattern would be as
 | 
			
		||||
 * follows:
 | 
			
		||||
 *
 | 
			
		||||
 * <pre>
 | 
			
		||||
 * [ CLIENT ]                      [ SERVER ]
 | 
			
		||||
 *     | (a) Initial empty request     |
 | 
			
		||||
 *     |------------------------------}|
 | 
			
		||||
 *     | (b) Data I                    |
 | 
			
		||||
 *  --}|------------------------------}|---}
 | 
			
		||||
 *     | Response I (a)                |
 | 
			
		||||
 *  {--|<------------------------------|{---
 | 
			
		||||
 *     |                               |
 | 
			
		||||
 *     | (c) Data II                   |
 | 
			
		||||
 *  --}|------------------------------}|---}
 | 
			
		||||
 *     | Response II (b)               |
 | 
			
		||||
 *  {--|{------------------------------|{---
 | 
			
		||||
 *     .                               .
 | 
			
		||||
 *     .                               .
 | 
			
		||||
 * </pre>
 | 
			
		||||
 *
 | 
			
		||||
 * Each incoming request is held open to be used to carry the next available response. The
 | 
			
		||||
 * server will hold at most two connections open at any given time.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Requests should be made using HTTP GET or POST (depending if there is a payload), with
 | 
			
		||||
 * any payload contained in the body. The following response codes can be returned from
 | 
			
		||||
 * the server:
 | 
			
		||||
 * <p>
 | 
			
		||||
 * <table>
 | 
			
		||||
 * <tr>
 | 
			
		||||
 * <th>Status</th>
 | 
			
		||||
 * <th>Meaning</th>
 | 
			
		||||
 * </tr>
 | 
			
		||||
 * <tr>
 | 
			
		||||
 * <td>200 (OK)</td>
 | 
			
		||||
 * <td>Data payload response.</td>
 | 
			
		||||
 * </tr>
 | 
			
		||||
 * <tr>
 | 
			
		||||
 * <td>204 (No Content)</td>
 | 
			
		||||
 * <td>The long poll has timed out and the client should start a new request.</td>
 | 
			
		||||
 * </tr>
 | 
			
		||||
 * <tr>
 | 
			
		||||
 * <td>429 (Too many requests)</td>
 | 
			
		||||
 * <td>There are already enough connections open, this one can be dropped.</td>
 | 
			
		||||
 * </tr>
 | 
			
		||||
 * <tr>
 | 
			
		||||
 * <td>410 (Gone)</td>
 | 
			
		||||
 * <td>The target server has disconnected.</td>
 | 
			
		||||
 * </tr>
 | 
			
		||||
 * </table>
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Requests and responses that contain payloads include a {@code x-seq} header that
 | 
			
		||||
 * contains a running sequence number (used to ensure data is applied in the correct
 | 
			
		||||
 * order). The first request containing a payload should have a {@code x-seq} value of
 | 
			
		||||
 * {@code 1}.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 * @since 1.3.0
 | 
			
		||||
 * @see org.springframework.boot.developertools.tunnel.client.HttpTunnelConnection
 | 
			
		||||
 */
 | 
			
		||||
public class HttpTunnelServer {
 | 
			
		||||
 | 
			
		||||
	private static final int SECONDS = 1000;
 | 
			
		||||
 | 
			
		||||
	private static final int DEFAULT_LONG_POLL_TIMEOUT = 10 * SECONDS;
 | 
			
		||||
 | 
			
		||||
	private static final long DEFAULT_DISCONNECT_TIMEOUT = 30 * SECONDS;
 | 
			
		||||
 | 
			
		||||
	private static final MediaType DISCONNECT_MEDIA_TYPE = new MediaType("application",
 | 
			
		||||
			"x-disconnect");
 | 
			
		||||
 | 
			
		||||
	private static final Log logger = LogFactory.getLog(HttpTunnelServer.class);
 | 
			
		||||
 | 
			
		||||
	private final TargetServerConnection serverConnection;
 | 
			
		||||
 | 
			
		||||
	private int longPollTimeout = DEFAULT_LONG_POLL_TIMEOUT;
 | 
			
		||||
 | 
			
		||||
	private long disconnectTimeout = DEFAULT_DISCONNECT_TIMEOUT;
 | 
			
		||||
 | 
			
		||||
	private volatile ServerThread serverThread;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Creates a new {@link HttpTunnelServer} instance.
 | 
			
		||||
	 * @param serverConnection the connection to the target server
 | 
			
		||||
	 */
 | 
			
		||||
	public HttpTunnelServer(TargetServerConnection serverConnection) {
 | 
			
		||||
		Assert.notNull(serverConnection, "ServerConnection must not be null");
 | 
			
		||||
		this.serverConnection = serverConnection;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Handle an incoming HTTP connection.
 | 
			
		||||
	 * @param request the HTTP request
 | 
			
		||||
	 * @param response the HTTP response
 | 
			
		||||
	 * @throws IOException
 | 
			
		||||
	 */
 | 
			
		||||
	public void handle(ServerHttpRequest request, ServerHttpResponse response)
 | 
			
		||||
			throws IOException {
 | 
			
		||||
		handle(new HttpConnection(request, response));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Handle an incoming HTTP connection.
 | 
			
		||||
	 * @param httpConnection the HTTP connection
 | 
			
		||||
	 * @throws IOException
 | 
			
		||||
	 */
 | 
			
		||||
	protected void handle(HttpConnection httpConnection) throws IOException {
 | 
			
		||||
		try {
 | 
			
		||||
			getServerThread().handleIncomingHttp(httpConnection);
 | 
			
		||||
			httpConnection.waitForResponse();
 | 
			
		||||
		}
 | 
			
		||||
		catch (ConnectException ex) {
 | 
			
		||||
			httpConnection.respond(HttpStatus.GONE);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Returns the active server thread, creating and starting it if necessary.
 | 
			
		||||
	 * @return the {@code ServerThread} (never {@code null})
 | 
			
		||||
	 * @throws IOException
 | 
			
		||||
	 */
 | 
			
		||||
	protected ServerThread getServerThread() throws IOException {
 | 
			
		||||
		synchronized (this) {
 | 
			
		||||
			if (this.serverThread == null) {
 | 
			
		||||
				ByteChannel channel = this.serverConnection.open(this.longPollTimeout);
 | 
			
		||||
				this.serverThread = new ServerThread(channel);
 | 
			
		||||
				this.serverThread.start();
 | 
			
		||||
			}
 | 
			
		||||
			return this.serverThread;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Called when the server thread exits.
 | 
			
		||||
	 */
 | 
			
		||||
	void clearServerThread() {
 | 
			
		||||
		synchronized (this) {
 | 
			
		||||
			this.serverThread = null;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Set the long poll timeout for the server.
 | 
			
		||||
	 * @param longPollTimeout the long poll timeout in milliseconds
 | 
			
		||||
	 */
 | 
			
		||||
	public void setLongPollTimeout(int longPollTimeout) {
 | 
			
		||||
		Assert.isTrue(longPollTimeout > 0, "LongPollTimeout must be a positive value");
 | 
			
		||||
		this.longPollTimeout = longPollTimeout;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Set the maximum amount of time to wait for a client before closing the connection.
 | 
			
		||||
	 * @param disconnectTimeout the disconnect timeout in milliseconds
 | 
			
		||||
	 */
 | 
			
		||||
	public void setDisconnectTimeout(long disconnectTimeout) {
 | 
			
		||||
		Assert.isTrue(disconnectTimeout > 0, "DisconnectTimeout must be a positive value");
 | 
			
		||||
		this.disconnectTimeout = disconnectTimeout;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * The main server thread used to transfer tunnel traffic.
 | 
			
		||||
	 */
 | 
			
		||||
	protected class ServerThread extends Thread {
 | 
			
		||||
 | 
			
		||||
		private final ByteChannel targetServer;
 | 
			
		||||
 | 
			
		||||
		private final Deque<HttpConnection> httpConnections;
 | 
			
		||||
 | 
			
		||||
		private final HttpTunnelPayloadForwarder payloadForwarder;
 | 
			
		||||
 | 
			
		||||
		private boolean closed;
 | 
			
		||||
 | 
			
		||||
		private AtomicLong responseSeq = new AtomicLong();
 | 
			
		||||
 | 
			
		||||
		private long lastHttpRequestTime;
 | 
			
		||||
 | 
			
		||||
		public ServerThread(ByteChannel targetServer) {
 | 
			
		||||
			Assert.notNull(targetServer, "TargetServer must not be null");
 | 
			
		||||
			this.targetServer = targetServer;
 | 
			
		||||
			this.httpConnections = new ArrayDeque<HttpConnection>(2);
 | 
			
		||||
			this.payloadForwarder = new HttpTunnelPayloadForwarder(targetServer);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public void run() {
 | 
			
		||||
			try {
 | 
			
		||||
				try {
 | 
			
		||||
					readAndForwardTargetServerData();
 | 
			
		||||
				}
 | 
			
		||||
				catch (Exception ex) {
 | 
			
		||||
					logger.trace("Unexpected exception from tunnel server", ex);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			finally {
 | 
			
		||||
				this.closed = true;
 | 
			
		||||
				closeHttpConnections();
 | 
			
		||||
				closeTargetServer();
 | 
			
		||||
				HttpTunnelServer.this.clearServerThread();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		private void readAndForwardTargetServerData() throws IOException {
 | 
			
		||||
			while (this.targetServer.isOpen()) {
 | 
			
		||||
				closeStaleHttpConnections();
 | 
			
		||||
				ByteBuffer data = HttpTunnelPayload.getPayloadData(this.targetServer);
 | 
			
		||||
				synchronized (this.httpConnections) {
 | 
			
		||||
					if (data != null) {
 | 
			
		||||
						HttpTunnelPayload payload = new HttpTunnelPayload(
 | 
			
		||||
								this.responseSeq.incrementAndGet(), data);
 | 
			
		||||
						payload.logIncoming();
 | 
			
		||||
						HttpConnection connection = getOrWaitForHttpConnection();
 | 
			
		||||
						connection.respond(payload);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		private HttpConnection getOrWaitForHttpConnection() {
 | 
			
		||||
			synchronized (this.httpConnections) {
 | 
			
		||||
				HttpConnection httpConnection = this.httpConnections.pollFirst();
 | 
			
		||||
				while (httpConnection == null) {
 | 
			
		||||
					try {
 | 
			
		||||
						this.httpConnections.wait(HttpTunnelServer.this.longPollTimeout);
 | 
			
		||||
					}
 | 
			
		||||
					catch (InterruptedException ex) {
 | 
			
		||||
						closeHttpConnections();
 | 
			
		||||
					}
 | 
			
		||||
					httpConnection = this.httpConnections.pollFirst();
 | 
			
		||||
				}
 | 
			
		||||
				return httpConnection;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		private void closeStaleHttpConnections() throws IOException {
 | 
			
		||||
			checkNotDisconnected();
 | 
			
		||||
			synchronized (this.httpConnections) {
 | 
			
		||||
				Iterator<HttpConnection> iterator = this.httpConnections.iterator();
 | 
			
		||||
				while (iterator.hasNext()) {
 | 
			
		||||
					HttpConnection httpConnection = iterator.next();
 | 
			
		||||
					if (httpConnection.isOlderThan(HttpTunnelServer.this.longPollTimeout)) {
 | 
			
		||||
						httpConnection.respond(HttpStatus.NO_CONTENT);
 | 
			
		||||
						iterator.remove();
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		private void checkNotDisconnected() {
 | 
			
		||||
			long timeout = HttpTunnelServer.this.disconnectTimeout;
 | 
			
		||||
			long duration = System.currentTimeMillis() - this.lastHttpRequestTime;
 | 
			
		||||
			Assert.state(duration < timeout, "Disconnect timeout");
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		private void closeHttpConnections() {
 | 
			
		||||
			synchronized (this.httpConnections) {
 | 
			
		||||
				while (!this.httpConnections.isEmpty()) {
 | 
			
		||||
					try {
 | 
			
		||||
						this.httpConnections.removeFirst().respond(HttpStatus.GONE);
 | 
			
		||||
					}
 | 
			
		||||
					catch (Exception ex) {
 | 
			
		||||
						logger.trace("Unable to close remote HTTP connection");
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		private void closeTargetServer() {
 | 
			
		||||
			try {
 | 
			
		||||
				this.targetServer.close();
 | 
			
		||||
			}
 | 
			
		||||
			catch (IOException ex) {
 | 
			
		||||
				logger.trace("Unable to target server connection");
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/**
 | 
			
		||||
		 * Handle an incoming {@link HttpConnection}.
 | 
			
		||||
		 * @param httpConnection the connection to handle.
 | 
			
		||||
		 * @throws IOException
 | 
			
		||||
		 */
 | 
			
		||||
		public void handleIncomingHttp(HttpConnection httpConnection) throws IOException {
 | 
			
		||||
			if (this.closed) {
 | 
			
		||||
				httpConnection.respond(HttpStatus.GONE);
 | 
			
		||||
			}
 | 
			
		||||
			synchronized (this.httpConnections) {
 | 
			
		||||
				while (this.httpConnections.size() > 1) {
 | 
			
		||||
					this.httpConnections.removeFirst().respond(
 | 
			
		||||
							HttpStatus.TOO_MANY_REQUESTS);
 | 
			
		||||
				}
 | 
			
		||||
				this.lastHttpRequestTime = System.currentTimeMillis();
 | 
			
		||||
				this.httpConnections.addLast(httpConnection);
 | 
			
		||||
				this.httpConnections.notify();
 | 
			
		||||
			}
 | 
			
		||||
			forwardToTargetServer(httpConnection);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		private void forwardToTargetServer(HttpConnection httpConnection)
 | 
			
		||||
				throws IOException {
 | 
			
		||||
			if (httpConnection.isDisconnectRequest()) {
 | 
			
		||||
				this.targetServer.close();
 | 
			
		||||
				interrupt();
 | 
			
		||||
			}
 | 
			
		||||
			ServerHttpRequest request = httpConnection.getRequest();
 | 
			
		||||
			HttpTunnelPayload payload = HttpTunnelPayload.get(request);
 | 
			
		||||
			if (payload != null) {
 | 
			
		||||
				this.payloadForwarder.forward(payload);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Encapsulates a HTTP request/response pair.
 | 
			
		||||
	 */
 | 
			
		||||
	protected static class HttpConnection {
 | 
			
		||||
 | 
			
		||||
		private final long createTime;
 | 
			
		||||
 | 
			
		||||
		private final ServerHttpRequest request;
 | 
			
		||||
 | 
			
		||||
		private final ServerHttpResponse response;
 | 
			
		||||
 | 
			
		||||
		private ServerHttpAsyncRequestControl async;
 | 
			
		||||
 | 
			
		||||
		private volatile boolean complete = false;
 | 
			
		||||
 | 
			
		||||
		public HttpConnection(ServerHttpRequest request, ServerHttpResponse response) {
 | 
			
		||||
			this.createTime = System.currentTimeMillis();
 | 
			
		||||
			this.request = request;
 | 
			
		||||
			this.response = response;
 | 
			
		||||
			this.async = startAsync();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/**
 | 
			
		||||
		 * Start asynchronous support or if unavailble return {@code null} to cause
 | 
			
		||||
		 * {@link #waitForResponse()} to block.
 | 
			
		||||
		 * @return the async request control
 | 
			
		||||
		 */
 | 
			
		||||
		protected ServerHttpAsyncRequestControl startAsync() {
 | 
			
		||||
			try {
 | 
			
		||||
				// Try to use async to save blocking
 | 
			
		||||
				ServerHttpAsyncRequestControl async = this.request
 | 
			
		||||
						.getAsyncRequestControl(this.response);
 | 
			
		||||
				async.start();
 | 
			
		||||
				return async;
 | 
			
		||||
			}
 | 
			
		||||
			catch (Exception ex) {
 | 
			
		||||
				return null;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/**
 | 
			
		||||
		 * Return the underlying request.
 | 
			
		||||
		 * @return the request
 | 
			
		||||
		 */
 | 
			
		||||
		public final ServerHttpRequest getRequest() {
 | 
			
		||||
			return this.request;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/**
 | 
			
		||||
		 * Return the underlying response.
 | 
			
		||||
		 * @return the response
 | 
			
		||||
		 */
 | 
			
		||||
		protected final ServerHttpResponse getResponse() {
 | 
			
		||||
			return this.response;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/**
 | 
			
		||||
		 * Determine if a connection is older than the specified time.
 | 
			
		||||
		 * @param time the time to check
 | 
			
		||||
		 * @return {@code true} if the request is older than the time
 | 
			
		||||
		 */
 | 
			
		||||
		public boolean isOlderThan(int time) {
 | 
			
		||||
			long runningTime = System.currentTimeMillis() - this.createTime;
 | 
			
		||||
			return (runningTime > time);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/**
 | 
			
		||||
		 * Cause the request to block or use asynchronous methods to wait until a response
 | 
			
		||||
		 * is available.
 | 
			
		||||
		 */
 | 
			
		||||
		public void waitForResponse() {
 | 
			
		||||
			if (this.async == null) {
 | 
			
		||||
				while (!this.complete) {
 | 
			
		||||
					try {
 | 
			
		||||
						synchronized (this) {
 | 
			
		||||
							wait(1000);
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
					catch (InterruptedException ex) {
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/**
 | 
			
		||||
		 * Detect if the request is actually a signal to disconnect.
 | 
			
		||||
		 * @return if the request is a signal to disconnect
 | 
			
		||||
		 */
 | 
			
		||||
		public boolean isDisconnectRequest() {
 | 
			
		||||
			return DISCONNECT_MEDIA_TYPE.equals(this.request.getHeaders()
 | 
			
		||||
					.getContentType());
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/**
 | 
			
		||||
		 * Send a HTTP status response.
 | 
			
		||||
		 * @param status the status to send
 | 
			
		||||
		 * @throws IOException
 | 
			
		||||
		 */
 | 
			
		||||
		public void respond(HttpStatus status) throws IOException {
 | 
			
		||||
			Assert.notNull(status, "Status must not be null");
 | 
			
		||||
			this.response.setStatusCode(status);
 | 
			
		||||
			complete();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/**
 | 
			
		||||
		 * Send a payload response.
 | 
			
		||||
		 * @param payload the payload to send
 | 
			
		||||
		 * @throws IOException
 | 
			
		||||
		 */
 | 
			
		||||
		public void respond(HttpTunnelPayload payload) throws IOException {
 | 
			
		||||
			Assert.notNull(payload, "Payload must not be null");
 | 
			
		||||
			this.response.setStatusCode(HttpStatus.OK);
 | 
			
		||||
			payload.assignTo(this.response);
 | 
			
		||||
			complete();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		/**
 | 
			
		||||
		 * Called when a request is complete.
 | 
			
		||||
		 */
 | 
			
		||||
		protected void complete() {
 | 
			
		||||
			if (this.async != null) {
 | 
			
		||||
				this.async.complete();
 | 
			
		||||
			}
 | 
			
		||||
			else {
 | 
			
		||||
				synchronized (this) {
 | 
			
		||||
					this.complete = true;
 | 
			
		||||
					notifyAll();
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,51 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package org.springframework.boot.developertools.tunnel.server;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
 | 
			
		||||
import org.springframework.boot.developertools.remote.server.Handler;
 | 
			
		||||
import org.springframework.http.server.ServerHttpRequest;
 | 
			
		||||
import org.springframework.http.server.ServerHttpResponse;
 | 
			
		||||
import org.springframework.util.Assert;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Adapts a {@link HttpTunnelServer} to a {@link Handler}.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 * @since 1.3.0
 | 
			
		||||
 */
 | 
			
		||||
public class HttpTunnelServerHandler implements Handler {
 | 
			
		||||
 | 
			
		||||
	private HttpTunnelServer server;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Create a new {@link HttpTunnelServerHandler} instance.
 | 
			
		||||
	 * @param server the server to adapt
 | 
			
		||||
	 */
 | 
			
		||||
	public HttpTunnelServerHandler(HttpTunnelServer server) {
 | 
			
		||||
		Assert.notNull(server, "Server must not be null");
 | 
			
		||||
		this.server = server;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void handle(ServerHttpRequest request, ServerHttpResponse response)
 | 
			
		||||
			throws IOException {
 | 
			
		||||
		this.server.handle(request, response);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,34 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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.tunnel.server;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Strategy interface to provide access to a port (which may change if an existing
 | 
			
		||||
 * connection is closed).
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 * @since 1.3.0
 | 
			
		||||
 */
 | 
			
		||||
public interface PortProvider {
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Return the port number
 | 
			
		||||
	 * @return the port number
 | 
			
		||||
	 */
 | 
			
		||||
	int getPort();
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,61 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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.tunnel.server;
 | 
			
		||||
 | 
			
		||||
import org.apache.commons.logging.Log;
 | 
			
		||||
import org.apache.commons.logging.LogFactory;
 | 
			
		||||
import org.springframework.boot.lang.UsesUnsafeJava;
 | 
			
		||||
import org.springframework.util.Assert;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * {@link PortProvider} that provides the port being used by the Java remote debugging.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 */
 | 
			
		||||
public class RemoteDebugPortProvider implements PortProvider {
 | 
			
		||||
 | 
			
		||||
	private static final String JDWP_ADDRESS_PROPERTY = "sun.jdwp.listenerAddress";
 | 
			
		||||
 | 
			
		||||
	private static final Log logger = LogFactory.getLog(RemoteDebugPortProvider.class);
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public int getPort() {
 | 
			
		||||
		Assert.state(isRemoteDebugRunning(), "Remote debug is not running");
 | 
			
		||||
		return getRemoteDebugPort();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public static boolean isRemoteDebugRunning() {
 | 
			
		||||
		return getRemoteDebugPort() != -1;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@UsesUnsafeJava
 | 
			
		||||
	@SuppressWarnings("restriction")
 | 
			
		||||
	private static int getRemoteDebugPort() {
 | 
			
		||||
		String property = sun.misc.VMSupport.getAgentProperties().getProperty(
 | 
			
		||||
				JDWP_ADDRESS_PROPERTY);
 | 
			
		||||
		try {
 | 
			
		||||
			if (property != null && property.contains(":")) {
 | 
			
		||||
				return Integer.valueOf(property.split(":")[1]);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		catch (Exception ex) {
 | 
			
		||||
			logger.trace("Unable to get JDWP port from property value '" + property + "'");
 | 
			
		||||
		}
 | 
			
		||||
		return -1;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,101 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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.tunnel.server;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.net.InetSocketAddress;
 | 
			
		||||
import java.net.SocketAddress;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.nio.channels.ByteChannel;
 | 
			
		||||
import java.nio.channels.Channels;
 | 
			
		||||
import java.nio.channels.ReadableByteChannel;
 | 
			
		||||
import java.nio.channels.SocketChannel;
 | 
			
		||||
 | 
			
		||||
import org.apache.commons.logging.Log;
 | 
			
		||||
import org.apache.commons.logging.LogFactory;
 | 
			
		||||
import org.springframework.util.Assert;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Socket based {@link TargetServerConnection}.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 * @since 1.3.0
 | 
			
		||||
 */
 | 
			
		||||
public class SocketTargetServerConnection implements TargetServerConnection {
 | 
			
		||||
 | 
			
		||||
	private static final Log logger = LogFactory
 | 
			
		||||
			.getLog(SocketTargetServerConnection.class);
 | 
			
		||||
 | 
			
		||||
	private final PortProvider portProvider;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Create a new {@link SocketTargetServerConnection}.
 | 
			
		||||
	 * @param portProvider the port provider
 | 
			
		||||
	 */
 | 
			
		||||
	public SocketTargetServerConnection(PortProvider portProvider) {
 | 
			
		||||
		Assert.notNull(portProvider, "PortProvider must not be null");
 | 
			
		||||
		this.portProvider = portProvider;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public ByteChannel open(int socketTimeout) throws IOException {
 | 
			
		||||
		SocketAddress address = new InetSocketAddress(this.portProvider.getPort());
 | 
			
		||||
		logger.trace("Opening tunnel connection to target server on " + address);
 | 
			
		||||
		SocketChannel channel = SocketChannel.open(address);
 | 
			
		||||
		channel.socket().setSoTimeout(socketTimeout);
 | 
			
		||||
		return new TimeoutAwareChannel(channel);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Wrapper to expose the {@link SocketChannel} in such a way that
 | 
			
		||||
	 * {@code SocketTimeoutExceptions} are still thrown from read methods.
 | 
			
		||||
	 */
 | 
			
		||||
	private static class TimeoutAwareChannel implements ByteChannel {
 | 
			
		||||
 | 
			
		||||
		private final SocketChannel socketChannel;
 | 
			
		||||
 | 
			
		||||
		private final ReadableByteChannel readChannel;
 | 
			
		||||
 | 
			
		||||
		public TimeoutAwareChannel(SocketChannel socketChannel) throws IOException {
 | 
			
		||||
			this.socketChannel = socketChannel;
 | 
			
		||||
			this.readChannel = Channels.newChannel(socketChannel.socket()
 | 
			
		||||
					.getInputStream());
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public int read(ByteBuffer dst) throws IOException {
 | 
			
		||||
			return this.readChannel.read(dst);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public int write(ByteBuffer src) throws IOException {
 | 
			
		||||
			return this.socketChannel.write(src);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public boolean isOpen() {
 | 
			
		||||
			return this.socketChannel.isOpen();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public void close() throws IOException {
 | 
			
		||||
			this.socketChannel.close();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,41 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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.tunnel.server;
 | 
			
		||||
 | 
			
		||||
import org.springframework.util.Assert;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * {@link PortProvider} for a static port that won't change.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 * @since 1.3.0
 | 
			
		||||
 */
 | 
			
		||||
public class StaticPortProvider implements PortProvider {
 | 
			
		||||
 | 
			
		||||
	private final int port;
 | 
			
		||||
 | 
			
		||||
	public StaticPortProvider(int port) {
 | 
			
		||||
		Assert.isTrue(port > 0, "Port must be positive");
 | 
			
		||||
		this.port = port;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public int getPort() {
 | 
			
		||||
		return this.port;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,38 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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.tunnel.server;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.nio.channels.ByteChannel;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Manages the connection to the ultimate tunnel target server.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 * @since 1.3.0
 | 
			
		||||
 */
 | 
			
		||||
public interface TargetServerConnection {
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Open a connection to the target server with the specified timeout.
 | 
			
		||||
	 * @param timeout the read timeout
 | 
			
		||||
	 * @return a {@link ByteChannel} providing read/write access to the server
 | 
			
		||||
	 * @throws IOException
 | 
			
		||||
	 */
 | 
			
		||||
	ByteChannel open(int timeout) throws IOException;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Server side TCP tunnel support.
 | 
			
		||||
 */
 | 
			
		||||
package org.springframework.boot.developertools.tunnel.server;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,12 @@
 | 
			
		|||
# Application Initializers
 | 
			
		||||
org.springframework.context.ApplicationContextInitializer=\
 | 
			
		||||
org.springframework.boot.developertools.restart.RestartScopeInitializer
 | 
			
		||||
 | 
			
		||||
# Application Listeners
 | 
			
		||||
org.springframework.context.ApplicationListener=\
 | 
			
		||||
org.springframework.boot.developertools.restart.RestartApplicationListener
 | 
			
		||||
 | 
			
		||||
# Auto Configure
 | 
			
		||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
 | 
			
		||||
org.springframework.boot.developertools.autoconfigure.LocalDeveloperToolsAutoConfiguration,\
 | 
			
		||||
org.springframework.boot.developertools.autoconfigure.RemoteDeveloperToolsAutoConfiguration
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
  .   ____          _                                              __ _ _
 | 
			
		||||
 /\\ / ___'_ __ _ _(_)_ __  __ _          ___               _      \ \ \ \
 | 
			
		||||
( ( )\___ | '_ | '_| | '_ \/ _` |        | _ \___ _ __  ___| |_ ___ \ \ \ \
 | 
			
		||||
 \\/  ___)| |_)| | | | | || (_| []::::::[]   / -_) '  \/ _ \  _/ -_) ) ) ) )
 | 
			
		||||
  '  |____| .__|_| |_|_| |_\__, |        |_|_\___|_|_|_\___/\__\___|/ / / /
 | 
			
		||||
 =========|_|==============|___/===================================/_/_/_/
 | 
			
		||||
 :: Spring Boot Remote :: ${spring-boot.formatted-version}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,81 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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.junit.Rule;
 | 
			
		||||
import org.junit.Test;
 | 
			
		||||
import org.junit.rules.ExpectedException;
 | 
			
		||||
import org.springframework.boot.SpringApplication;
 | 
			
		||||
import org.springframework.boot.developertools.RemoteUrlPropertyExtractor;
 | 
			
		||||
import org.springframework.context.ApplicationContext;
 | 
			
		||||
import org.springframework.context.annotation.Configuration;
 | 
			
		||||
 | 
			
		||||
import static org.hamcrest.Matchers.equalTo;
 | 
			
		||||
import static org.junit.Assert.assertThat;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests for {@link RemoteUrlPropertyExtractor}.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 */
 | 
			
		||||
public class RemoteUrlPropertyExtractorTests {
 | 
			
		||||
 | 
			
		||||
	@Rule
 | 
			
		||||
	public ExpectedException thrown = ExpectedException.none();
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void missingUrl() throws Exception {
 | 
			
		||||
		this.thrown.expect(IllegalStateException.class);
 | 
			
		||||
		this.thrown.expectMessage("No remote URL specified");
 | 
			
		||||
		doTest();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void malformedUrl() throws Exception {
 | 
			
		||||
		this.thrown.expect(IllegalStateException.class);
 | 
			
		||||
		this.thrown.expectMessage("Malformed URL '::://wibble'");
 | 
			
		||||
		doTest("::://wibble");
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void multipleUrls() throws Exception {
 | 
			
		||||
		this.thrown.expect(IllegalStateException.class);
 | 
			
		||||
		this.thrown.expectMessage("Multiple URLs specified");
 | 
			
		||||
		doTest("http://localhost:8080", "http://localhost:9090");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void validUrl() throws Exception {
 | 
			
		||||
		ApplicationContext context = doTest("http://localhost:8080");
 | 
			
		||||
		assertThat(context.getEnvironment().getProperty("remoteUrl"),
 | 
			
		||||
				equalTo("http://localhost:8080"));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private ApplicationContext doTest(String... args) {
 | 
			
		||||
		SpringApplication application = new SpringApplication(Config.class);
 | 
			
		||||
		application.setWebEnvironment(false);
 | 
			
		||||
		application.addListeners(new RemoteUrlPropertyExtractor());
 | 
			
		||||
		return application.run(args);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Configuration
 | 
			
		||||
	static class Config {
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,209 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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.junit.After;
 | 
			
		||||
import org.junit.Rule;
 | 
			
		||||
import org.junit.Test;
 | 
			
		||||
import org.junit.rules.ExpectedException;
 | 
			
		||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
 | 
			
		||||
import org.springframework.boot.SpringApplication;
 | 
			
		||||
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration;
 | 
			
		||||
import org.springframework.boot.developertools.classpath.ClassPathChangedEvent;
 | 
			
		||||
import org.springframework.boot.developertools.classpath.ClassPathFileSystemWatcher;
 | 
			
		||||
import org.springframework.boot.developertools.filewatch.ChangedFiles;
 | 
			
		||||
import org.springframework.boot.developertools.livereload.LiveReloadServer;
 | 
			
		||||
import org.springframework.boot.developertools.restart.MockRestartInitializer;
 | 
			
		||||
import org.springframework.boot.developertools.restart.MockRestarter;
 | 
			
		||||
import org.springframework.boot.developertools.restart.Restarter;
 | 
			
		||||
import org.springframework.context.ConfigurableApplicationContext;
 | 
			
		||||
import org.springframework.context.annotation.Bean;
 | 
			
		||||
import org.springframework.context.annotation.Configuration;
 | 
			
		||||
import org.springframework.context.annotation.Import;
 | 
			
		||||
import org.springframework.context.event.ContextRefreshedEvent;
 | 
			
		||||
import org.springframework.util.SocketUtils;
 | 
			
		||||
import org.thymeleaf.templateresolver.TemplateResolver;
 | 
			
		||||
 | 
			
		||||
import static org.hamcrest.Matchers.equalTo;
 | 
			
		||||
import static org.hamcrest.Matchers.notNullValue;
 | 
			
		||||
import static org.junit.Assert.assertThat;
 | 
			
		||||
import static org.mockito.Mockito.mock;
 | 
			
		||||
import static org.mockito.Mockito.never;
 | 
			
		||||
import static org.mockito.Mockito.reset;
 | 
			
		||||
import static org.mockito.Mockito.verify;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests for {@link LocalDeveloperToolsAutoConfiguration}.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 */
 | 
			
		||||
public class LocalDeveloperToolsAutoConfigurationTests {
 | 
			
		||||
 | 
			
		||||
	@Rule
 | 
			
		||||
	public ExpectedException thrown = ExpectedException.none();
 | 
			
		||||
 | 
			
		||||
	@Rule
 | 
			
		||||
	public MockRestarter mockRestarter = new MockRestarter();
 | 
			
		||||
 | 
			
		||||
	private int liveReloadPort = SocketUtils.findAvailableTcpPort();
 | 
			
		||||
 | 
			
		||||
	private ConfigurableApplicationContext context;
 | 
			
		||||
 | 
			
		||||
	@After
 | 
			
		||||
	public void cleanup() {
 | 
			
		||||
		if (this.context != null) {
 | 
			
		||||
			this.context.close();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void thymeleafCacheIsFalse() throws Exception {
 | 
			
		||||
		this.context = initializeAndRun(Config.class);
 | 
			
		||||
		TemplateResolver resolver = this.context.getBean(TemplateResolver.class);
 | 
			
		||||
		resolver.initialize();
 | 
			
		||||
		assertThat(resolver.isCacheable(), equalTo(false));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void liveReloadServer() throws Exception {
 | 
			
		||||
		this.context = initializeAndRun(Config.class);
 | 
			
		||||
		LiveReloadServer server = this.context.getBean(LiveReloadServer.class);
 | 
			
		||||
		assertThat(server.isStarted(), equalTo(true));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void liveReloadTriggeredOnContextRefresh() throws Exception {
 | 
			
		||||
		this.context = initializeAndRun(ConfigWithMockLiveReload.class);
 | 
			
		||||
		LiveReloadServer server = this.context.getBean(LiveReloadServer.class);
 | 
			
		||||
		reset(server);
 | 
			
		||||
		this.context.publishEvent(new ContextRefreshedEvent(this.context));
 | 
			
		||||
		verify(server).triggerReload();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void liveReloadTriggerdOnClassPathChangeWithoutRestart() throws Exception {
 | 
			
		||||
		this.context = initializeAndRun(ConfigWithMockLiveReload.class);
 | 
			
		||||
		LiveReloadServer server = this.context.getBean(LiveReloadServer.class);
 | 
			
		||||
		reset(server);
 | 
			
		||||
		ClassPathChangedEvent event = new ClassPathChangedEvent(this.context,
 | 
			
		||||
				Collections.<ChangedFiles> emptySet(), false);
 | 
			
		||||
		this.context.publishEvent(event);
 | 
			
		||||
		verify(server).triggerReload();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void liveReloadNotTriggerdOnClassPathChangeWithRestart() throws Exception {
 | 
			
		||||
		this.context = initializeAndRun(ConfigWithMockLiveReload.class);
 | 
			
		||||
		LiveReloadServer server = this.context.getBean(LiveReloadServer.class);
 | 
			
		||||
		reset(server);
 | 
			
		||||
		ClassPathChangedEvent event = new ClassPathChangedEvent(this.context,
 | 
			
		||||
				Collections.<ChangedFiles> emptySet(), true);
 | 
			
		||||
		this.context.publishEvent(event);
 | 
			
		||||
		verify(server, never()).triggerReload();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void liveReloadDisabled() throws Exception {
 | 
			
		||||
		Map<String, Object> properties = new HashMap<String, Object>();
 | 
			
		||||
		properties.put("spring.developertools.livereload.enabled", false);
 | 
			
		||||
		this.context = initializeAndRun(Config.class, properties);
 | 
			
		||||
		this.thrown.expect(NoSuchBeanDefinitionException.class);
 | 
			
		||||
		this.context.getBean(OptionalLiveReloadServer.class);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void restartTriggerdOnClassPathChangeWithRestart() throws Exception {
 | 
			
		||||
		this.context = initializeAndRun(Config.class);
 | 
			
		||||
		ClassPathChangedEvent event = new ClassPathChangedEvent(this.context,
 | 
			
		||||
				Collections.<ChangedFiles> emptySet(), true);
 | 
			
		||||
		this.context.publishEvent(event);
 | 
			
		||||
		verify(this.mockRestarter.getMock()).restart();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void restartNotTriggerdOnClassPathChangeWithRestart() throws Exception {
 | 
			
		||||
		this.context = initializeAndRun(Config.class);
 | 
			
		||||
		ClassPathChangedEvent event = new ClassPathChangedEvent(this.context,
 | 
			
		||||
				Collections.<ChangedFiles> emptySet(), false);
 | 
			
		||||
		this.context.publishEvent(event);
 | 
			
		||||
		verify(this.mockRestarter.getMock(), never()).restart();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void restartWatchingClassPath() throws Exception {
 | 
			
		||||
		this.context = initializeAndRun(Config.class);
 | 
			
		||||
		ClassPathFileSystemWatcher watcher = this.context
 | 
			
		||||
				.getBean(ClassPathFileSystemWatcher.class);
 | 
			
		||||
		assertThat(watcher, notNullValue());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void restartDisabled() throws Exception {
 | 
			
		||||
		Map<String, Object> properties = new HashMap<String, Object>();
 | 
			
		||||
		properties.put("spring.developertools.restart.enabled", false);
 | 
			
		||||
		this.context = initializeAndRun(Config.class, properties);
 | 
			
		||||
		this.thrown.expect(NoSuchBeanDefinitionException.class);
 | 
			
		||||
		this.context.getBean(ClassPathFileSystemWatcher.class);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private ConfigurableApplicationContext initializeAndRun(Class<?> config) {
 | 
			
		||||
		return initializeAndRun(config, Collections.<String, Object> emptyMap());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private ConfigurableApplicationContext initializeAndRun(Class<?> config,
 | 
			
		||||
			Map<String, Object> properties) {
 | 
			
		||||
		Restarter.initialize(new String[0], false, new MockRestartInitializer(), false);
 | 
			
		||||
		SpringApplication application = new SpringApplication(config);
 | 
			
		||||
		application.setDefaultProperties(getDefaultProperties(properties));
 | 
			
		||||
		application.setWebEnvironment(false);
 | 
			
		||||
		ConfigurableApplicationContext context = application.run();
 | 
			
		||||
		return context;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private Map<String, Object> getDefaultProperties(
 | 
			
		||||
			Map<String, Object> specifiedProperties) {
 | 
			
		||||
		Map<String, Object> properties = new HashMap<String, Object>();
 | 
			
		||||
		properties.put("spring.thymeleaf.check-template-location", false);
 | 
			
		||||
		properties.put("spring.developertools.livereload.port", this.liveReloadPort);
 | 
			
		||||
		properties.putAll(specifiedProperties);
 | 
			
		||||
		return properties;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Configuration
 | 
			
		||||
	@Import({ LocalDeveloperToolsAutoConfiguration.class,
 | 
			
		||||
			ThymeleafAutoConfiguration.class })
 | 
			
		||||
	public static class Config {
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Configuration
 | 
			
		||||
	@Import({ LocalDeveloperToolsAutoConfiguration.class,
 | 
			
		||||
			ThymeleafAutoConfiguration.class })
 | 
			
		||||
	public static class ConfigWithMockLiveReload {
 | 
			
		||||
 | 
			
		||||
		@Bean
 | 
			
		||||
		public LiveReloadServer liveReloadServer() {
 | 
			
		||||
			return mock(LiveReloadServer.class);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,51 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2012-2015 the original author or authors.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package org.springframework.boot.developertools.autoconfigure;
 | 
			
		||||
 | 
			
		||||
import org.junit.Test;
 | 
			
		||||
import org.springframework.boot.developertools.livereload.LiveReloadServer;
 | 
			
		||||
 | 
			
		||||
import static org.mockito.BDDMockito.willThrow;
 | 
			
		||||
import static org.mockito.Mockito.mock;
 | 
			
		||||
import static org.mockito.Mockito.never;
 | 
			
		||||
import static org.mockito.Mockito.verify;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests for {@link OptionalLiveReloadServer}.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 */
 | 
			
		||||
public class OptionalLiveReloadServerTests {
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void nullServer() throws Exception {
 | 
			
		||||
		OptionalLiveReloadServer server = new OptionalLiveReloadServer(null);
 | 
			
		||||
		server.startServer();
 | 
			
		||||
		server.triggerReload();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void serverWontStart() throws Exception {
 | 
			
		||||
		LiveReloadServer delegate = mock(LiveReloadServer.class);
 | 
			
		||||
		OptionalLiveReloadServer server = new OptionalLiveReloadServer(delegate);
 | 
			
		||||
		willThrow(new RuntimeException("Error")).given(delegate).start();
 | 
			
		||||
		server.startServer();
 | 
			
		||||
		server.triggerReload();
 | 
			
		||||
		verify(delegate, never()).triggerReload();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,262 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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.io.IOException;
 | 
			
		||||
 | 
			
		||||
import org.junit.After;
 | 
			
		||||
import org.junit.Before;
 | 
			
		||||
import org.junit.Rule;
 | 
			
		||||
import org.junit.Test;
 | 
			
		||||
import org.junit.rules.ExpectedException;
 | 
			
		||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
 | 
			
		||||
import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration;
 | 
			
		||||
import org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration;
 | 
			
		||||
import org.springframework.boot.developertools.remote.server.DispatcherFilter;
 | 
			
		||||
import org.springframework.boot.developertools.restart.MockRestarter;
 | 
			
		||||
import org.springframework.boot.developertools.restart.server.HttpRestartServer;
 | 
			
		||||
import org.springframework.boot.developertools.restart.server.SourceFolderUrlFilter;
 | 
			
		||||
import org.springframework.boot.developertools.tunnel.server.HttpTunnelServer;
 | 
			
		||||
import org.springframework.boot.developertools.tunnel.server.RemoteDebugPortProvider;
 | 
			
		||||
import org.springframework.boot.developertools.tunnel.server.SocketTargetServerConnection;
 | 
			
		||||
import org.springframework.boot.developertools.tunnel.server.TargetServerConnection;
 | 
			
		||||
import org.springframework.boot.test.EnvironmentTestUtils;
 | 
			
		||||
import org.springframework.context.annotation.Bean;
 | 
			
		||||
import org.springframework.context.annotation.Configuration;
 | 
			
		||||
import org.springframework.context.annotation.Import;
 | 
			
		||||
import org.springframework.http.server.ServerHttpRequest;
 | 
			
		||||
import org.springframework.http.server.ServerHttpResponse;
 | 
			
		||||
import org.springframework.mock.web.MockFilterChain;
 | 
			
		||||
import org.springframework.mock.web.MockHttpServletRequest;
 | 
			
		||||
import org.springframework.mock.web.MockHttpServletResponse;
 | 
			
		||||
import org.springframework.mock.web.MockServletContext;
 | 
			
		||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
 | 
			
		||||
 | 
			
		||||
import static org.hamcrest.Matchers.equalTo;
 | 
			
		||||
import static org.junit.Assert.assertThat;
 | 
			
		||||
import static org.mockito.Mockito.mock;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests for {@link RemoteDeveloperToolsAutoConfiguration}.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Rob Winch
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 */
 | 
			
		||||
public class RemoteDeveloperToolsAutoConfigurationTests {
 | 
			
		||||
 | 
			
		||||
	private static final String DEFAULT_CONTEXT_PATH = RemoteDeveloperToolsProperties.DEFAULT_CONTEXT_PATH;
 | 
			
		||||
 | 
			
		||||
	private static final String DEFAULT_SECRET_HEADER_NAME = RemoteDeveloperToolsProperties.DEFAULT_SECRET_HEADER_NAME;
 | 
			
		||||
 | 
			
		||||
	@Rule
 | 
			
		||||
	public MockRestarter mockRestarter = new MockRestarter();
 | 
			
		||||
 | 
			
		||||
	@Rule
 | 
			
		||||
	public ExpectedException thrown = ExpectedException.none();
 | 
			
		||||
 | 
			
		||||
	private AnnotationConfigWebApplicationContext context;
 | 
			
		||||
 | 
			
		||||
	private MockHttpServletRequest request;
 | 
			
		||||
 | 
			
		||||
	private MockHttpServletResponse response;
 | 
			
		||||
 | 
			
		||||
	private MockFilterChain chain;
 | 
			
		||||
 | 
			
		||||
	@Before
 | 
			
		||||
	public void setup() {
 | 
			
		||||
		this.request = new MockHttpServletRequest();
 | 
			
		||||
		this.response = new MockHttpServletResponse();
 | 
			
		||||
		this.chain = new MockFilterChain();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@After
 | 
			
		||||
	public void close() {
 | 
			
		||||
		if (this.context != null) {
 | 
			
		||||
			this.context.close();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void disabledIfRemoteSecretIsMissing() throws Exception {
 | 
			
		||||
		loadContext("a:b");
 | 
			
		||||
		this.thrown.expect(NoSuchBeanDefinitionException.class);
 | 
			
		||||
		this.context.getBean(DispatcherFilter.class);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void ignoresUnmappedUrl() throws Exception {
 | 
			
		||||
		loadContext("spring.developertools.remote.secret:supersecret");
 | 
			
		||||
		DispatcherFilter filter = this.context.getBean(DispatcherFilter.class);
 | 
			
		||||
		this.request.setRequestURI("/restart");
 | 
			
		||||
		this.request.addHeader(DEFAULT_SECRET_HEADER_NAME, "supersecret");
 | 
			
		||||
		filter.doFilter(this.request, this.response, this.chain);
 | 
			
		||||
		assertRestartInvoked(false);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void ignoresIfMissingSecretFromRequest() throws Exception {
 | 
			
		||||
		loadContext("spring.developertools.remote.secret:supersecret");
 | 
			
		||||
		DispatcherFilter filter = this.context.getBean(DispatcherFilter.class);
 | 
			
		||||
		this.request.setRequestURI(DEFAULT_CONTEXT_PATH + "/restart");
 | 
			
		||||
		filter.doFilter(this.request, this.response, this.chain);
 | 
			
		||||
		assertRestartInvoked(false);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void ignoresInvalidSecretInRequest() throws Exception {
 | 
			
		||||
		loadContext("spring.developertools.remote.secret:supersecret");
 | 
			
		||||
		DispatcherFilter filter = this.context.getBean(DispatcherFilter.class);
 | 
			
		||||
		this.request.setRequestURI(DEFAULT_CONTEXT_PATH + "/restart");
 | 
			
		||||
		this.request.addHeader(DEFAULT_SECRET_HEADER_NAME, "invalid");
 | 
			
		||||
		filter.doFilter(this.request, this.response, this.chain);
 | 
			
		||||
		assertRestartInvoked(false);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void invokeRestartWithDefaultSetup() throws Exception {
 | 
			
		||||
		loadContext("spring.developertools.remote.secret:supersecret");
 | 
			
		||||
		DispatcherFilter filter = this.context.getBean(DispatcherFilter.class);
 | 
			
		||||
		this.request.setRequestURI(DEFAULT_CONTEXT_PATH + "/restart");
 | 
			
		||||
		this.request.addHeader(DEFAULT_SECRET_HEADER_NAME, "supersecret");
 | 
			
		||||
		filter.doFilter(this.request, this.response, this.chain);
 | 
			
		||||
		assertRestartInvoked(true);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void disableRestart() throws Exception {
 | 
			
		||||
		loadContext("spring.developertools.remote.secret:supersecret",
 | 
			
		||||
				"spring.developertools.remote.restart.enabled:false");
 | 
			
		||||
		this.thrown.expect(NoSuchBeanDefinitionException.class);
 | 
			
		||||
		this.context.getBean("remoteRestartHanderMapper");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void invokeTunnelWithDefaultSetup() throws Exception {
 | 
			
		||||
		loadContext("spring.developertools.remote.secret:supersecret");
 | 
			
		||||
		DispatcherFilter filter = this.context.getBean(DispatcherFilter.class);
 | 
			
		||||
		this.request.setRequestURI(DEFAULT_CONTEXT_PATH + "/debug");
 | 
			
		||||
		this.request.addHeader(DEFAULT_SECRET_HEADER_NAME, "supersecret");
 | 
			
		||||
		filter.doFilter(this.request, this.response, this.chain);
 | 
			
		||||
		assertTunnelInvoked(true);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void invokeTunnelWithCustomHeaderName() throws Exception {
 | 
			
		||||
		loadContext("spring.developertools.remote.secret:supersecret",
 | 
			
		||||
				"spring.developertools.remote.secretHeaderName:customheader");
 | 
			
		||||
		DispatcherFilter filter = this.context.getBean(DispatcherFilter.class);
 | 
			
		||||
		this.request.setRequestURI(DEFAULT_CONTEXT_PATH + "/debug");
 | 
			
		||||
		this.request.addHeader("customheader", "supersecret");
 | 
			
		||||
		filter.doFilter(this.request, this.response, this.chain);
 | 
			
		||||
		assertTunnelInvoked(true);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void disableRemoteDebug() throws Exception {
 | 
			
		||||
		loadContext("spring.developertools.remote.secret:supersecret",
 | 
			
		||||
				"spring.developertools.remote.debug.enabled:false");
 | 
			
		||||
		this.thrown.expect(NoSuchBeanDefinitionException.class);
 | 
			
		||||
		this.context.getBean("remoteDebugHanderMapper");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void developerToolsHealthReturns200() throws Exception {
 | 
			
		||||
		loadContext("spring.developertools.remote.secret:supersecret");
 | 
			
		||||
		DispatcherFilter filter = this.context.getBean(DispatcherFilter.class);
 | 
			
		||||
		this.request.setRequestURI(DEFAULT_CONTEXT_PATH);
 | 
			
		||||
		this.request.addHeader(DEFAULT_SECRET_HEADER_NAME, "supersecret");
 | 
			
		||||
		this.response.setStatus(500);
 | 
			
		||||
		filter.doFilter(this.request, this.response, this.chain);
 | 
			
		||||
		assertThat(this.response.getStatus(), equalTo(200));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void assertTunnelInvoked(boolean value) {
 | 
			
		||||
		assertThat(this.context.getBean(MockHttpTunnelServer.class).invoked,
 | 
			
		||||
				equalTo(value));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void assertRestartInvoked(boolean value) {
 | 
			
		||||
		assertThat(this.context.getBean(MockHttpRestartServer.class).invoked,
 | 
			
		||||
				equalTo(value));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void loadContext(String... properties) {
 | 
			
		||||
		this.context = new AnnotationConfigWebApplicationContext();
 | 
			
		||||
		this.context.setServletContext(new MockServletContext());
 | 
			
		||||
		this.context.register(Config.class, ServerPropertiesAutoConfiguration.class,
 | 
			
		||||
				PropertyPlaceholderAutoConfiguration.class);
 | 
			
		||||
		EnvironmentTestUtils.addEnvironment(this.context, properties);
 | 
			
		||||
		this.context.refresh();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Configuration
 | 
			
		||||
	@Import(RemoteDeveloperToolsAutoConfiguration.class)
 | 
			
		||||
	static class Config {
 | 
			
		||||
 | 
			
		||||
		@Bean
 | 
			
		||||
		public HttpTunnelServer remoteDebugHttpTunnelServer() {
 | 
			
		||||
			return new MockHttpTunnelServer(new SocketTargetServerConnection(
 | 
			
		||||
					new RemoteDebugPortProvider()));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Bean
 | 
			
		||||
		public HttpRestartServer remoteRestartHttpRestartServer() {
 | 
			
		||||
			SourceFolderUrlFilter sourceFolderUrlFilter = mock(SourceFolderUrlFilter.class);
 | 
			
		||||
			return new MockHttpRestartServer(sourceFolderUrlFilter);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Mock {@link HttpTunnelServer} implementation.
 | 
			
		||||
	 */
 | 
			
		||||
	static class MockHttpTunnelServer extends HttpTunnelServer {
 | 
			
		||||
 | 
			
		||||
		private boolean invoked;
 | 
			
		||||
 | 
			
		||||
		public MockHttpTunnelServer(TargetServerConnection serverConnection) {
 | 
			
		||||
			super(serverConnection);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public void handle(ServerHttpRequest request, ServerHttpResponse response)
 | 
			
		||||
				throws IOException {
 | 
			
		||||
			this.invoked = true;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Mock {@link HttpRestartServer} implementation.
 | 
			
		||||
	 */
 | 
			
		||||
	static class MockHttpRestartServer extends HttpRestartServer {
 | 
			
		||||
 | 
			
		||||
		private boolean invoked;
 | 
			
		||||
 | 
			
		||||
		public MockHttpRestartServer(SourceFolderUrlFilter sourceFolderUrlFilter) {
 | 
			
		||||
			super(sourceFolderUrlFilter);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public void handle(ServerHttpRequest request, ServerHttpResponse response)
 | 
			
		||||
				throws IOException {
 | 
			
		||||
			this.invoked = true;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue