Add a minimal livereload server implementation
Add a minimal server to support livereload.com browser plugins. Includes a partial websocket implementation to save needing a dependency to spring-websocket. See gh-3085
This commit is contained in:
		
							parent
							
								
									3d8db7cddb
								
							
						
					
					
						commit
						f09134180e
					
				| 
						 | 
				
			
			@ -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;
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
				
			
			@ -0,0 +1,53 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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 org.junit.Test;
 | 
			
		||||
 | 
			
		||||
import static org.hamcrest.Matchers.equalTo;
 | 
			
		||||
import static org.junit.Assert.assertThat;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests for {@link Base64Encoder}.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 */
 | 
			
		||||
public class Base64EncoderTests {
 | 
			
		||||
 | 
			
		||||
	private static final String TEXT = "Man is distinguished, not only by his reason, "
 | 
			
		||||
			+ "but by this singular passion from other animals, which is a lust of the "
 | 
			
		||||
			+ "mind, that by a perseverance of delight in the continued and indefatigable "
 | 
			
		||||
			+ "generation of knowledge, exceeds the short vehemence of any carnal pleasure.";
 | 
			
		||||
 | 
			
		||||
	private static final String ENCODED = "TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5I"
 | 
			
		||||
			+ "GhpcyByZWFzb24sIGJ1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbm"
 | 
			
		||||
			+ "ltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmF"
 | 
			
		||||
			+ "uY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGludWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVy"
 | 
			
		||||
			+ "YXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55I"
 | 
			
		||||
			+ "GNhcm5hbCBwbGVhc3VyZS4=";
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void encodeText() {
 | 
			
		||||
		assertThat(Base64Encoder.encode(TEXT), equalTo(ENCODED));
 | 
			
		||||
		assertThat(Base64Encoder.encode("pleasure."), equalTo("cGxlYXN1cmUu"));
 | 
			
		||||
		assertThat(Base64Encoder.encode("leasure."), equalTo("bGVhc3VyZS4="));
 | 
			
		||||
		assertThat(Base64Encoder.encode("easure."), equalTo("ZWFzdXJlLg=="));
 | 
			
		||||
		assertThat(Base64Encoder.encode("asure."), equalTo("YXN1cmUu"));
 | 
			
		||||
		assertThat(Base64Encoder.encode("sure."), equalTo("c3VyZS4="));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,103 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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.ByteArrayInputStream;
 | 
			
		||||
import java.io.FilterInputStream;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.io.InputStream;
 | 
			
		||||
 | 
			
		||||
import org.junit.Rule;
 | 
			
		||||
import org.junit.Test;
 | 
			
		||||
import org.junit.rules.ExpectedException;
 | 
			
		||||
 | 
			
		||||
import static org.hamcrest.Matchers.equalTo;
 | 
			
		||||
import static org.junit.Assert.assertThat;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests for {@link ConnectionInputStream}.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 */
 | 
			
		||||
@SuppressWarnings("resource")
 | 
			
		||||
public class ConnectionInputStreamTests {
 | 
			
		||||
 | 
			
		||||
	private static final byte[] NO_BYTES = {};
 | 
			
		||||
 | 
			
		||||
	@Rule
 | 
			
		||||
	public ExpectedException thrown = ExpectedException.none();
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void readHeader() throws Exception {
 | 
			
		||||
		String header = "";
 | 
			
		||||
		for (int i = 0; i < 100; i++) {
 | 
			
		||||
			header += "x-something-" + i
 | 
			
		||||
					+ ": xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
 | 
			
		||||
		}
 | 
			
		||||
		String data = header + "\r\n\r\n" + "content\r\n";
 | 
			
		||||
		ConnectionInputStream inputStream = new ConnectionInputStream(
 | 
			
		||||
				new ByteArrayInputStream(data.getBytes()));
 | 
			
		||||
		assertThat(inputStream.readHeader(), equalTo(header));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void readFully() throws Exception {
 | 
			
		||||
		byte[] bytes = "the data that we want to read fully".getBytes();
 | 
			
		||||
		LimitedInputStream source = new LimitedInputStream(
 | 
			
		||||
				new ByteArrayInputStream(bytes), 2);
 | 
			
		||||
		ConnectionInputStream inputStream = new ConnectionInputStream(source);
 | 
			
		||||
		byte[] buffer = new byte[bytes.length];
 | 
			
		||||
		inputStream.readFully(buffer, 0, buffer.length);
 | 
			
		||||
		assertThat(buffer, equalTo(bytes));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void checkedRead() throws Exception {
 | 
			
		||||
		ConnectionInputStream inputStream = new ConnectionInputStream(
 | 
			
		||||
				new ByteArrayInputStream(NO_BYTES));
 | 
			
		||||
		this.thrown.expect(IOException.class);
 | 
			
		||||
		this.thrown.expectMessage("End of stream");
 | 
			
		||||
		inputStream.checkedRead();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void checkedReadArray() throws Exception {
 | 
			
		||||
		ConnectionInputStream inputStream = new ConnectionInputStream(
 | 
			
		||||
				new ByteArrayInputStream(NO_BYTES));
 | 
			
		||||
		this.thrown.expect(IOException.class);
 | 
			
		||||
		this.thrown.expectMessage("End of stream");
 | 
			
		||||
		byte[] buffer = new byte[100];
 | 
			
		||||
		inputStream.checkedRead(buffer, 0, buffer.length);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private static class LimitedInputStream extends FilterInputStream {
 | 
			
		||||
 | 
			
		||||
		private final int max;
 | 
			
		||||
 | 
			
		||||
		protected LimitedInputStream(InputStream in, int max) {
 | 
			
		||||
			super(in);
 | 
			
		||||
			this.max = max;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public int read(byte[] b, int off, int len) throws IOException {
 | 
			
		||||
			return super.read(b, off, Math.min(len, this.max));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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.livereload;
 | 
			
		||||
 | 
			
		||||
import java.io.ByteArrayInputStream;
 | 
			
		||||
import java.io.ByteArrayOutputStream;
 | 
			
		||||
import java.io.OutputStream;
 | 
			
		||||
 | 
			
		||||
import org.junit.Test;
 | 
			
		||||
 | 
			
		||||
import static org.hamcrest.Matchers.equalTo;
 | 
			
		||||
import static org.junit.Assert.assertThat;
 | 
			
		||||
import static org.mockito.Mockito.mock;
 | 
			
		||||
import static org.mockito.Mockito.verify;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests for {@link ConnectionOutputStream}.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 */
 | 
			
		||||
@SuppressWarnings("resource")
 | 
			
		||||
public class ConnectionOutputStreamTests {
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void write() throws Exception {
 | 
			
		||||
		OutputStream out = mock(OutputStream.class);
 | 
			
		||||
		ConnectionOutputStream outputStream = new ConnectionOutputStream(out);
 | 
			
		||||
		byte[] b = new byte[100];
 | 
			
		||||
		outputStream.write(b, 1, 2);
 | 
			
		||||
		verify(out).write(b, 1, 2);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void writeHttp() throws Exception {
 | 
			
		||||
		ByteArrayOutputStream out = new ByteArrayOutputStream();
 | 
			
		||||
		ConnectionOutputStream outputStream = new ConnectionOutputStream(out);
 | 
			
		||||
		outputStream.writeHttp(new ByteArrayInputStream("hi".getBytes()), "x-type");
 | 
			
		||||
		String expected = "";
 | 
			
		||||
		expected += "HTTP/1.1 200 OK\r\n";
 | 
			
		||||
		expected += "Content-Type: x-type\r\n";
 | 
			
		||||
		expected += "Content-Length: 2\r\n";
 | 
			
		||||
		expected += "Connection: close\r\n\r\n";
 | 
			
		||||
		expected += "hi";
 | 
			
		||||
		assertThat(out.toString(), equalTo(expected));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void writeHeaders() throws Exception {
 | 
			
		||||
		ByteArrayOutputStream out = new ByteArrayOutputStream();
 | 
			
		||||
		ConnectionOutputStream outputStream = new ConnectionOutputStream(out);
 | 
			
		||||
		outputStream.writeHeaders("A: a", "B: b");
 | 
			
		||||
		outputStream.flush();
 | 
			
		||||
		String expected = "";
 | 
			
		||||
		expected += "A: a\r\n";
 | 
			
		||||
		expected += "B: b\r\n\r\n";
 | 
			
		||||
		assertThat(out.toString(), equalTo(expected));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,188 @@
 | 
			
		|||
/*
 | 
			
		||||
 * 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.ByteArrayInputStream;
 | 
			
		||||
import java.io.ByteArrayOutputStream;
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
 | 
			
		||||
import org.junit.Rule;
 | 
			
		||||
import org.junit.Test;
 | 
			
		||||
import org.junit.rules.ExpectedException;
 | 
			
		||||
 | 
			
		||||
import static org.hamcrest.Matchers.equalTo;
 | 
			
		||||
import static org.junit.Assert.assertThat;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests for {@link Frame}.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 */
 | 
			
		||||
public class FrameTests {
 | 
			
		||||
 | 
			
		||||
	@Rule
 | 
			
		||||
	public ExpectedException thrown = ExpectedException.none();
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void payloadMustNotBeNull() throws Exception {
 | 
			
		||||
		this.thrown.expect(IllegalArgumentException.class);
 | 
			
		||||
		this.thrown.expectMessage("Payload must not be null");
 | 
			
		||||
		new Frame((String) null);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void typeMustNotBeNull() throws Exception {
 | 
			
		||||
		this.thrown.expect(IllegalArgumentException.class);
 | 
			
		||||
		this.thrown.expectMessage("Type must not be null");
 | 
			
		||||
		new Frame((Frame.Type) null);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void textPayload() throws Exception {
 | 
			
		||||
		Frame frame = new Frame("abc");
 | 
			
		||||
		assertThat(frame.getType(), equalTo(Frame.Type.TEXT));
 | 
			
		||||
		assertThat(frame.getPayload(), equalTo("abc".getBytes()));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void typedPayload() throws Exception {
 | 
			
		||||
		Frame frame = new Frame(Frame.Type.CLOSE);
 | 
			
		||||
		assertThat(frame.getType(), equalTo(Frame.Type.CLOSE));
 | 
			
		||||
		assertThat(frame.getPayload(), equalTo(new byte[] {}));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void writeSmallPayload() throws Exception {
 | 
			
		||||
		String payload = createString(1);
 | 
			
		||||
		Frame frame = new Frame(payload);
 | 
			
		||||
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
 | 
			
		||||
		frame.write(bos);
 | 
			
		||||
		assertThat(bos.toByteArray(), equalTo(new byte[] { (byte) 0x81, 0x01, 0x41 }));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void writeLargePayload() throws Exception {
 | 
			
		||||
		String payload = createString(126);
 | 
			
		||||
		Frame frame = new Frame(payload);
 | 
			
		||||
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
 | 
			
		||||
		frame.write(bos);
 | 
			
		||||
		byte[] bytes = bos.toByteArray();
 | 
			
		||||
		assertThat(bytes.length, equalTo(130));
 | 
			
		||||
		assertThat(bytes[0], equalTo((byte) 0x81));
 | 
			
		||||
		assertThat(bytes[1], equalTo((byte) 0x7E));
 | 
			
		||||
		assertThat(bytes[2], equalTo((byte) 0x00));
 | 
			
		||||
		assertThat(bytes[3], equalTo((byte) 126));
 | 
			
		||||
		assertThat(bytes[4], equalTo((byte) 0x41));
 | 
			
		||||
		assertThat(bytes[5], equalTo((byte) 0x41));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void readFragmentedNotSupported() throws Exception {
 | 
			
		||||
		byte[] bytes = new byte[] { 0x0F };
 | 
			
		||||
		this.thrown.expect(IllegalStateException.class);
 | 
			
		||||
		this.thrown.expectMessage("Fragmented frames are not supported");
 | 
			
		||||
		Frame.read(newConnectionInputStream(bytes));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void readLargeFramesNotSupported() throws Exception {
 | 
			
		||||
		byte[] bytes = new byte[] { (byte) 0x80, (byte) 0xFF };
 | 
			
		||||
		this.thrown.expect(IllegalStateException.class);
 | 
			
		||||
		this.thrown.expectMessage("Large frames are not supported");
 | 
			
		||||
		Frame.read(newConnectionInputStream(bytes));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void readSmallTextFrame() throws Exception {
 | 
			
		||||
		byte[] bytes = new byte[] { (byte) 0x81, (byte) 0x02, 0x41, 0x41 };
 | 
			
		||||
		Frame frame = Frame.read(newConnectionInputStream(bytes));
 | 
			
		||||
		assertThat(frame.getType(), equalTo(Frame.Type.TEXT));
 | 
			
		||||
		assertThat(frame.getPayload(), equalTo(new byte[] { 0x41, 0x41 }));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void readMaskedTextFrame() throws Exception {
 | 
			
		||||
		byte[] bytes = new byte[] { (byte) 0x81, (byte) 0x82, 0x0F, 0x0F, 0x0F, 0x0F,
 | 
			
		||||
				0x4E, 0x4E };
 | 
			
		||||
		Frame frame = Frame.read(newConnectionInputStream(bytes));
 | 
			
		||||
		assertThat(frame.getType(), equalTo(Frame.Type.TEXT));
 | 
			
		||||
		assertThat(frame.getPayload(), equalTo(new byte[] { 0x41, 0x41 }));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void readLargeTextFrame() throws Exception {
 | 
			
		||||
		byte[] bytes = new byte[134];
 | 
			
		||||
		Arrays.fill(bytes, (byte) 0x4E);
 | 
			
		||||
		bytes[0] = (byte) 0x81;
 | 
			
		||||
		bytes[1] = (byte) 0xFE;
 | 
			
		||||
		bytes[2] = 0x00;
 | 
			
		||||
		bytes[3] = 126;
 | 
			
		||||
		bytes[4] = 0x0F;
 | 
			
		||||
		bytes[5] = 0x0F;
 | 
			
		||||
		bytes[6] = 0x0F;
 | 
			
		||||
		bytes[7] = 0x0F;
 | 
			
		||||
		Frame frame = Frame.read(newConnectionInputStream(bytes));
 | 
			
		||||
		assertThat(frame.getType(), equalTo(Frame.Type.TEXT));
 | 
			
		||||
		assertThat(frame.getPayload(), equalTo(createString(126).getBytes()));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void readContinuation() throws Exception {
 | 
			
		||||
		byte[] bytes = new byte[] { (byte) 0x80, (byte) 0x00 };
 | 
			
		||||
		Frame frame = Frame.read(newConnectionInputStream(bytes));
 | 
			
		||||
		assertThat(frame.getType(), equalTo(Frame.Type.CONTINUATION));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void readBinary() throws Exception {
 | 
			
		||||
		byte[] bytes = new byte[] { (byte) 0x82, (byte) 0x00 };
 | 
			
		||||
		Frame frame = Frame.read(newConnectionInputStream(bytes));
 | 
			
		||||
		assertThat(frame.getType(), equalTo(Frame.Type.BINARY));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void readClose() throws Exception {
 | 
			
		||||
		byte[] bytes = new byte[] { (byte) 0x88, (byte) 0x00 };
 | 
			
		||||
		Frame frame = Frame.read(newConnectionInputStream(bytes));
 | 
			
		||||
		assertThat(frame.getType(), equalTo(Frame.Type.CLOSE));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void readPing() throws Exception {
 | 
			
		||||
		byte[] bytes = new byte[] { (byte) 0x89, (byte) 0x00 };
 | 
			
		||||
		Frame frame = Frame.read(newConnectionInputStream(bytes));
 | 
			
		||||
		assertThat(frame.getType(), equalTo(Frame.Type.PING));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void readPong() throws Exception {
 | 
			
		||||
		byte[] bytes = new byte[] { (byte) 0x8A, (byte) 0x00 };
 | 
			
		||||
		Frame frame = Frame.read(newConnectionInputStream(bytes));
 | 
			
		||||
		assertThat(frame.getType(), equalTo(Frame.Type.PONG));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private ConnectionInputStream newConnectionInputStream(byte[] bytes) {
 | 
			
		||||
		return new ConnectionInputStream(new ByteArrayInputStream(bytes));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private String createString(int length) {
 | 
			
		||||
		char[] chars = new char[length];
 | 
			
		||||
		Arrays.fill(chars, 'A');
 | 
			
		||||
		return new String(chars);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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.livereload;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.io.InputStream;
 | 
			
		||||
import java.io.OutputStream;
 | 
			
		||||
import java.net.URI;
 | 
			
		||||
import java.net.URISyntaxException;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.concurrent.ExecutionException;
 | 
			
		||||
 | 
			
		||||
import org.eclipse.jetty.websocket.api.Session;
 | 
			
		||||
import org.eclipse.jetty.websocket.api.WebSocketAdapter;
 | 
			
		||||
import org.eclipse.jetty.websocket.api.WebSocketListener;
 | 
			
		||||
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
 | 
			
		||||
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
 | 
			
		||||
import org.eclipse.jetty.websocket.client.WebSocketClient;
 | 
			
		||||
import org.eclipse.jetty.websocket.common.events.JettyListenerEventDriver;
 | 
			
		||||
import org.junit.After;
 | 
			
		||||
import org.junit.Before;
 | 
			
		||||
import org.junit.Test;
 | 
			
		||||
import org.springframework.util.SocketUtils;
 | 
			
		||||
import org.springframework.web.client.RestTemplate;
 | 
			
		||||
 | 
			
		||||
import static org.hamcrest.Matchers.containsString;
 | 
			
		||||
import static org.hamcrest.Matchers.equalTo;
 | 
			
		||||
import static org.hamcrest.Matchers.greaterThan;
 | 
			
		||||
import static org.junit.Assert.assertThat;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests for {@link LiveReloadServer}.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Phillip Webb
 | 
			
		||||
 */
 | 
			
		||||
public class LiveReloadServerTests {
 | 
			
		||||
 | 
			
		||||
	private static final String HANDSHAKE = "{command: 'hello', "
 | 
			
		||||
			+ "protocols: ['http://livereload.com/protocols/official-7']}";
 | 
			
		||||
 | 
			
		||||
	private static final ByteBuffer NO_DATA = ByteBuffer.allocate(0);
 | 
			
		||||
 | 
			
		||||
	private int port = SocketUtils.findAvailableTcpPort();
 | 
			
		||||
 | 
			
		||||
	private MonitoredLiveReloadServer server;
 | 
			
		||||
 | 
			
		||||
	@Before
 | 
			
		||||
	public void setup() throws Exception {
 | 
			
		||||
		this.server = new MonitoredLiveReloadServer(this.port);
 | 
			
		||||
		this.server.start();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@After
 | 
			
		||||
	public void teardown() throws Exception {
 | 
			
		||||
		this.server.stop();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void servesLivereloadJs() throws Exception {
 | 
			
		||||
		RestTemplate template = new RestTemplate();
 | 
			
		||||
		URI uri = new URI("http://localhost:" + this.port + "/livereload.js");
 | 
			
		||||
		String script = template.getForObject(uri, String.class);
 | 
			
		||||
		assertThat(script, containsString("livereload.com/protocols/official-7"));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void triggerReload() throws Exception {
 | 
			
		||||
		WebSocketClient client = new WebSocketClient();
 | 
			
		||||
		try {
 | 
			
		||||
			Socket socket = openSocket(client, new Socket());
 | 
			
		||||
			this.server.triggerReload();
 | 
			
		||||
			Thread.sleep(500);
 | 
			
		||||
			this.server.stop();
 | 
			
		||||
			assertThat(socket.getMessages(0),
 | 
			
		||||
					containsString("http://livereload.com/protocols/official-7"));
 | 
			
		||||
			assertThat(socket.getMessages(1), containsString("command\":\"reload\""));
 | 
			
		||||
		}
 | 
			
		||||
		finally {
 | 
			
		||||
			client.stop();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void pingPong() throws Exception {
 | 
			
		||||
		WebSocketClient client = new WebSocketClient();
 | 
			
		||||
		try {
 | 
			
		||||
			Socket socket = new Socket();
 | 
			
		||||
			Driver driver = openSocket(client, new Driver(socket));
 | 
			
		||||
			socket.getRemote().sendPing(NO_DATA);
 | 
			
		||||
			Thread.sleep(200);
 | 
			
		||||
			this.server.stop();
 | 
			
		||||
			assertThat(driver.getPongCount(), equalTo(1));
 | 
			
		||||
		}
 | 
			
		||||
		finally {
 | 
			
		||||
			client.stop();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void clientClose() throws Exception {
 | 
			
		||||
		WebSocketClient client = new WebSocketClient();
 | 
			
		||||
		try {
 | 
			
		||||
			Socket socket = openSocket(client, new Socket());
 | 
			
		||||
			socket.getSession().close();
 | 
			
		||||
		}
 | 
			
		||||
		finally {
 | 
			
		||||
			client.stop();
 | 
			
		||||
		}
 | 
			
		||||
		assertThat(this.server.getClosedExceptions().size(), greaterThan(0));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	public void serverClose() throws Exception {
 | 
			
		||||
		WebSocketClient client = new WebSocketClient();
 | 
			
		||||
		try {
 | 
			
		||||
			Socket socket = openSocket(client, new Socket());
 | 
			
		||||
			Thread.sleep(200);
 | 
			
		||||
			this.server.stop();
 | 
			
		||||
			Thread.sleep(200);
 | 
			
		||||
			assertThat(socket.getCloseStatus(), equalTo(1006));
 | 
			
		||||
		}
 | 
			
		||||
		finally {
 | 
			
		||||
			client.stop();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private <T> T openSocket(WebSocketClient client, T socket) throws Exception,
 | 
			
		||||
			URISyntaxException, InterruptedException, ExecutionException, IOException {
 | 
			
		||||
		client.start();
 | 
			
		||||
		ClientUpgradeRequest request = new ClientUpgradeRequest();
 | 
			
		||||
		URI uri = new URI("ws://localhost:" + this.port + "/livereload");
 | 
			
		||||
		Session session = client.connect(socket, uri, request).get();
 | 
			
		||||
		session.getRemote().sendString(HANDSHAKE);
 | 
			
		||||
		Thread.sleep(200);
 | 
			
		||||
		return socket;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private static class Driver extends JettyListenerEventDriver {
 | 
			
		||||
 | 
			
		||||
		private int pongCount;
 | 
			
		||||
 | 
			
		||||
		public Driver(WebSocketListener listener) {
 | 
			
		||||
			super(WebSocketPolicy.newClientPolicy(), listener);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public void onPong(ByteBuffer buffer) {
 | 
			
		||||
			super.onPong(buffer);
 | 
			
		||||
			this.pongCount++;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public int getPongCount() {
 | 
			
		||||
			return this.pongCount;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private static class Socket extends WebSocketAdapter {
 | 
			
		||||
 | 
			
		||||
		private List<String> messages = new ArrayList<String>();
 | 
			
		||||
 | 
			
		||||
		private Integer closeStatus;
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public void onWebSocketText(String message) {
 | 
			
		||||
			this.messages.add(message);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public String getMessages(int index) {
 | 
			
		||||
			return this.messages.get(index);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public void onWebSocketClose(int statusCode, String reason) {
 | 
			
		||||
			this.closeStatus = statusCode;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public Integer getCloseStatus() {
 | 
			
		||||
			return this.closeStatus;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Useful main method for manual testing against a real browser.
 | 
			
		||||
	 * @param args main args
 | 
			
		||||
	 * @throws IOException
 | 
			
		||||
	 */
 | 
			
		||||
	public static void main(String[] args) throws IOException {
 | 
			
		||||
		LiveReloadServer server = new LiveReloadServer();
 | 
			
		||||
		server.start();
 | 
			
		||||
		while (true) {
 | 
			
		||||
			try {
 | 
			
		||||
				Thread.sleep(1000);
 | 
			
		||||
			}
 | 
			
		||||
			catch (InterruptedException ex) {
 | 
			
		||||
				Thread.currentThread().interrupt();
 | 
			
		||||
			}
 | 
			
		||||
			server.triggerReload();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * {@link LiveReloadServer} with additional monitoring.
 | 
			
		||||
	 */
 | 
			
		||||
	private static class MonitoredLiveReloadServer extends LiveReloadServer {
 | 
			
		||||
 | 
			
		||||
		private List<ConnectionClosedException> closedExceptions = new ArrayList<ConnectionClosedException>();
 | 
			
		||||
 | 
			
		||||
		public MonitoredLiveReloadServer(int port) {
 | 
			
		||||
			super(port);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		protected Connection createConnection(java.net.Socket socket,
 | 
			
		||||
				InputStream inputStream, OutputStream outputStream) throws IOException {
 | 
			
		||||
			return new MonitoredConnection(socket, inputStream, outputStream);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public List<ConnectionClosedException> getClosedExceptions() {
 | 
			
		||||
			return this.closedExceptions;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		private class MonitoredConnection extends Connection {
 | 
			
		||||
 | 
			
		||||
			public MonitoredConnection(java.net.Socket socket, InputStream inputStream,
 | 
			
		||||
					OutputStream outputStream) throws IOException {
 | 
			
		||||
				super(socket, inputStream, outputStream);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			@Override
 | 
			
		||||
			public void run() throws Exception {
 | 
			
		||||
				try {
 | 
			
		||||
					super.run();
 | 
			
		||||
				}
 | 
			
		||||
				catch (ConnectionClosedException ex) {
 | 
			
		||||
					MonitoredLiveReloadServer.this.closedExceptions.add(ex);
 | 
			
		||||
					throw ex;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue