Browse Source

Add filesystem watcher support

Add a filesystem watcher that can be used to inform listeners of
changes (add, delete, modify) to files in specific source folders.

The implementation uses a background thread rather than the WatchService
API to remain compatible with Java 6 and because WatchService is slow on
OSX.

See gh-3084
pull/3077/merge
Phillip Webb 11 years ago
parent
commit
0862412eb4
  1. 128
      spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/filewatch/ChangedFile.java
  2. 89
      spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/filewatch/ChangedFiles.java
  3. 36
      spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/filewatch/FileChangeListener.java
  4. 84
      spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/filewatch/FileSnapshot.java
  5. 211
      spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/filewatch/FileSystemWatcher.java
  6. 141
      spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/filewatch/FolderSnapshot.java
  7. 21
      spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/filewatch/package-info.java
  8. 87
      spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/filewatch/ChangedFileTests.java
  9. 112
      spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/filewatch/FileSnapshotTests.java
  10. 255
      spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/filewatch/FileSystemWatcherTests.java
  11. 156
      spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/filewatch/FolderSnapshotTests.java

128
spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/filewatch/ChangedFile.java

@ -0,0 +1,128 @@ @@ -0,0 +1,128 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.developertools.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
}
}

89
spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/filewatch/ChangedFiles.java

@ -0,0 +1,89 @@ @@ -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;
}
}

36
spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/filewatch/FileChangeListener.java

@ -0,0 +1,36 @@ @@ -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);
}

84
spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/filewatch/FileSnapshot.java

@ -0,0 +1,84 @@ @@ -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();
}
}

211
spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/filewatch/FileSystemWatcher.java

@ -0,0 +1,211 @@ @@ -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;
}
}
}

141
spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/filewatch/FolderSnapshot.java

@ -0,0 +1,141 @@ @@ -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;
}
}

21
spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/filewatch/package-info.java

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Class to watch the local filesystem for changes.
*/
package org.springframework.boot.developertools.filewatch;

87
spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/filewatch/ChangedFileTests.java

@ -0,0 +1,87 @@ @@ -0,0 +1,87 @@
/*
* 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.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.springframework.boot.developertools.filewatch.ChangedFile.Type;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link ChangedFile}.
*
* @author Phillip Webb
*/
public class ChangedFileTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Rule
public TemporaryFolder temp = new TemporaryFolder();
@Test
public void sourceFolderMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("SourceFolder must not be null");
new ChangedFile(null, this.temp.newFile(), Type.ADD);
}
@Test
public void fileMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("File must not be null");
new ChangedFile(this.temp.newFolder(), null, Type.ADD);
}
@Test
public void typeMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Type must not be null");
new ChangedFile(this.temp.newFile(), this.temp.newFolder(), null);
}
@Test
public void getFile() throws Exception {
File file = this.temp.newFile();
ChangedFile changedFile = new ChangedFile(this.temp.newFolder(), file, Type.ADD);
assertThat(changedFile.getFile(), equalTo(file));
}
@Test
public void getType() throws Exception {
ChangedFile changedFile = new ChangedFile(this.temp.newFolder(),
this.temp.newFile(), Type.DELETE);
assertThat(changedFile.getType(), equalTo(Type.DELETE));
}
@Test
public void getRelativeName() throws Exception {
File folder = this.temp.newFolder();
File subFolder = new File(folder, "A");
File file = new File(subFolder, "B.txt");
ChangedFile changedFile = new ChangedFile(folder, file, Type.ADD);
assertThat(changedFile.getRelativeName(), equalTo("A/B.txt"));
}
}

112
spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/filewatch/FileSnapshotTests.java

@ -0,0 +1,112 @@ @@ -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.filewatch;
import java.io.File;
import java.io.IOException;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.springframework.util.FileCopyUtils;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link FileSnapshot}.
*
* @author Phillip Webb
*/
public class FileSnapshotTests {
private static final long TWO_MINS = TimeUnit.MINUTES.toMillis(2);
private static final long MODIFIED = new Date().getTime()
- TimeUnit.DAYS.toMillis(10);
@Rule
public ExpectedException thrown = ExpectedException.none();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void fileMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("File must not be null");
new FileSnapshot(null);
}
@Test
public void fileMustNotBeAFolder() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("File must not be a folder");
new FileSnapshot(this.temporaryFolder.newFolder());
}
@Test
public void equalsIfTheSame() throws Exception {
File file = createNewFile("abc", MODIFIED);
File fileCopy = new File(file, "x").getParentFile();
FileSnapshot snapshot1 = new FileSnapshot(file);
FileSnapshot snapshot2 = new FileSnapshot(fileCopy);
assertThat(snapshot1, equalTo(snapshot2));
assertThat(snapshot1.hashCode(), equalTo(snapshot2.hashCode()));
}
@Test
public void notEqualsIfDeleted() throws Exception {
File file = createNewFile("abc", MODIFIED);
FileSnapshot snapshot1 = new FileSnapshot(file);
file.delete();
assertThat(snapshot1, not(equalTo(new FileSnapshot(file))));
}
@Test
public void notEqualsIfLengthChanges() throws Exception {
File file = createNewFile("abc", MODIFIED);
FileSnapshot snapshot1 = new FileSnapshot(file);
setupFile(file, "abcd", MODIFIED);
assertThat(snapshot1, not(equalTo(new FileSnapshot(file))));
}
@Test
public void notEqualsIfLastModifiedChanges() throws Exception {
File file = createNewFile("abc", MODIFIED);
FileSnapshot snapshot1 = new FileSnapshot(file);
setupFile(file, "abc", MODIFIED + TWO_MINS);
assertThat(snapshot1, not(equalTo(new FileSnapshot(file))));
}
private File createNewFile(String content, long lastModified) throws IOException {
File file = this.temporaryFolder.newFile();
setupFile(file, content, lastModified);
return file;
}
private void setupFile(File file, String content, long lastModified)
throws IOException {
FileCopyUtils.copy(content.getBytes(), file);
file.setLastModified(lastModified);
}
}

255
spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/filewatch/FileSystemWatcherTests.java

@ -0,0 +1,255 @@ @@ -0,0 +1,255 @@
/*
* 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.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.springframework.boot.developertools.filewatch.ChangedFile.Type;
import org.springframework.util.FileCopyUtils;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link FileSystemWatcher}.
*
* @author Phillip Webb
*/
public class FileSystemWatcherTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private FileSystemWatcher watcher;
private List<Set<ChangedFiles>> changes = new ArrayList<Set<ChangedFiles>>();
@Rule
public TemporaryFolder temp = new TemporaryFolder();
@Before
public void setup() throws Exception {
setupWatcher(20, 10);
}
@Test
public void listenerMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("FileChangeListener must not be null");
this.watcher.addListener(null);
}
@Test
public void cannotAddListenerToStartedListener() throws Exception {
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("FileSystemWatcher already started");
this.watcher.start();
this.watcher.addListener(mock(FileChangeListener.class));
}
@Test
public void sourceFolderMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Folder must not be null");
this.watcher.addSourceFolder(null);
}
@Test
public void cannotAddSourceFolderToStartedListener() throws Exception {
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("FileSystemWatcher already started");
this.watcher.start();
this.watcher.addSourceFolder(this.temp.newFolder());
}
@Test
public void addFile() throws Exception {
File folder = startWithNewFolder();
File file = touch(new File(folder, "test.txt"));
this.watcher.stopAfter(1);
ChangedFiles changedFiles = getSingleChangedFiles();
ChangedFile expected = new ChangedFile(folder, file, Type.ADD);
assertThat(changedFiles.getFiles(), contains(expected));
}
@Test
public void addNestedFile() throws Exception {
File folder = startWithNewFolder();
File file = touch(new File(new File(folder, "sub"), "text.txt"));
this.watcher.stopAfter(1);
ChangedFiles changedFiles = getSingleChangedFiles();
ChangedFile expected = new ChangedFile(folder, file, Type.ADD);
assertThat(changedFiles.getFiles(), contains(expected));
}
@Test
public void waitsForIdleTime() throws Exception {
this.changes.clear();
setupWatcher(100, 0);
File folder = startWithNewFolder();
touch(new File(folder, "test1.txt"));
Thread.sleep(200);
touch(new File(folder, "test2.txt"));
this.watcher.stopAfter(1);
assertThat(this.changes.size(), equalTo(2));
}
@Test
public void waitsForQuietTime() throws Exception {
setupWatcher(300, 200);
File folder = startWithNewFolder();
for (int i = 0; i < 10; i++) {
touch(new File(folder, i + "test.txt"));
Thread.sleep(100);
}
this.watcher.stopAfter(1);
ChangedFiles changedFiles = getSingleChangedFiles();
assertThat(changedFiles.getFiles().size(), equalTo(10));
}
@Test
public void withExistingFiles() throws Exception {
File folder = this.temp.newFolder();
touch(new File(folder, "test.txt"));
this.watcher.addSourceFolder(folder);
this.watcher.start();
File file = touch(new File(folder, "test2.txt"));
this.watcher.stopAfter(1);
ChangedFiles changedFiles = getSingleChangedFiles();
ChangedFile expected = new ChangedFile(folder, file, Type.ADD);
assertThat(changedFiles.getFiles(), contains(expected));
}
@Test
public void multipleSources() throws Exception {
File folder1 = this.temp.newFolder();
File folder2 = this.temp.newFolder();
this.watcher.addSourceFolder(folder1);
this.watcher.addSourceFolder(folder2);
this.watcher.start();
File file1 = touch(new File(folder1, "test.txt"));
File file2 = touch(new File(folder2, "test.txt"));
this.watcher.stopAfter(1);
Set<ChangedFiles> change = getSingleOnChange();
assertThat(change.size(), equalTo(2));
for (ChangedFiles changedFiles : change) {
if (changedFiles.getSourceFolder().equals(folder1)) {
ChangedFile file = new ChangedFile(folder1, file1, Type.ADD);
assertEquals(new HashSet<ChangedFile>(Arrays.asList(file)),
changedFiles.getFiles());
}
else {
ChangedFile file = new ChangedFile(folder2, file2, Type.ADD);
assertEquals(new HashSet<ChangedFile>(Arrays.asList(file)),
changedFiles.getFiles());
}
}
}
@Test
public void multipleListeners() throws Exception {
File folder = this.temp.newFolder();
final Set<ChangedFiles> listener2Changes = new LinkedHashSet<ChangedFiles>();
this.watcher.addSourceFolder(folder);
this.watcher.addListener(new FileChangeListener() {
@Override
public void onChange(Set<ChangedFiles> changeSet) {
listener2Changes.addAll(changeSet);
}
});
this.watcher.start();
File file = touch(new File(folder, "test.txt"));
this.watcher.stopAfter(1);
ChangedFiles changedFiles = getSingleChangedFiles();
ChangedFile expected = new ChangedFile(folder, file, Type.ADD);
assertThat(changedFiles.getFiles(), contains(expected));
assertEquals(this.changes.get(0), listener2Changes);
}
@Test
public void modifyDeleteAndAdd() throws Exception {
File folder = this.temp.newFolder();
File modify = touch(new File(folder, "modify.txt"));
File delete = touch(new File(folder, "delete.txt"));
this.watcher.addSourceFolder(folder);
this.watcher.start();
FileCopyUtils.copy("abc".getBytes(), modify);
delete.delete();
File add = touch(new File(folder, "add.txt"));
this.watcher.stopAfter(1);
ChangedFiles changedFiles = getSingleChangedFiles();
Set<ChangedFile> actual = changedFiles.getFiles();
Set<ChangedFile> expected = new HashSet<ChangedFile>();
expected.add(new ChangedFile(folder, modify, Type.MODIFY));
expected.add(new ChangedFile(folder, delete, Type.DELETE));
expected.add(new ChangedFile(folder, add, Type.ADD));
assertEquals(expected, actual);
}
private void setupWatcher(long idleTime, long quietTime) {
this.watcher = new FileSystemWatcher(false, idleTime, quietTime);
this.watcher.addListener(new FileChangeListener() {
@Override
public void onChange(Set<ChangedFiles> changeSet) {
FileSystemWatcherTests.this.changes.add(changeSet);
}
});
}
private File startWithNewFolder() throws IOException {
File folder = this.temp.newFolder();
this.watcher.addSourceFolder(folder);
this.watcher.start();
return folder;
}
private ChangedFiles getSingleChangedFiles() {
Set<ChangedFiles> singleChange = getSingleOnChange();
assertThat(singleChange.size(), equalTo(1));
return singleChange.iterator().next();
}
private Set<ChangedFiles> getSingleOnChange() {
assertThat(this.changes.size(), equalTo(1));
return this.changes.get(0);
}
private File touch(File file) throws FileNotFoundException, IOException {
file.getParentFile().mkdirs();
FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.close();
return file;
}
}

156
spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/filewatch/FolderSnapshotTests.java

@ -0,0 +1,156 @@ @@ -0,0 +1,156 @@
/*
* 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.io.IOException;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.springframework.boot.developertools.filewatch.ChangedFile.Type;
import org.springframework.util.FileCopyUtils;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link FolderSnapshot}.
*
* @author Phillip Webb
*/
public class FolderSnapshotTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private File folder;
private FolderSnapshot initialSnapshot;
@Before
public void setup() throws Exception {
this.folder = createTestFolderStructure();
this.initialSnapshot = new FolderSnapshot(this.folder);
}
@Test
public void folderMustNotBeNull() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Folder must not be null");
new FolderSnapshot(null);
}
@Test
public void folderMustNotBeFile() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Folder must not be a file");
new FolderSnapshot(this.temporaryFolder.newFile());
}
@Test
public void equalsWhenNothingHasChanged() throws Exception {
FolderSnapshot updatedSnapshot = new FolderSnapshot(this.folder);
assertThat(this.initialSnapshot, equalTo(updatedSnapshot));
assertThat(this.initialSnapshot.hashCode(), equalTo(updatedSnapshot.hashCode()));
}
@Test
public void notEqualsWhenAFileIsAdded() throws Exception {
new File(new File(this.folder, "folder1"), "newfile").createNewFile();
FolderSnapshot updatedSnapshot = new FolderSnapshot(this.folder);
assertThat(this.initialSnapshot, not(equalTo(updatedSnapshot)));
}
@Test
public void notEqualsWhenAFileIsDeleted() throws Exception {
new File(new File(this.folder, "folder1"), "file1").delete();
FolderSnapshot updatedSnapshot = new FolderSnapshot(this.folder);
assertThat(this.initialSnapshot, not(equalTo(updatedSnapshot)));
}
@Test
public void notEqualsWhenAFileIsModified() throws Exception {
File file1 = new File(new File(this.folder, "folder1"), "file1");
FileCopyUtils.copy("updatedcontent".getBytes(), file1);
FolderSnapshot updatedSnapshot = new FolderSnapshot(this.folder);
assertThat(this.initialSnapshot, not(equalTo(updatedSnapshot)));
}
@Test
public void getChangedFilesSnapshotMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Snapshot must not be null");
this.initialSnapshot.getChangedFiles(null);
}
@Test
public void getChangedFilesSnapshotMustBeTheSameSourceFolder() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Snapshot source folder must be '" + this.folder + "'");
this.initialSnapshot.getChangedFiles(new FolderSnapshot(
createTestFolderStructure()));
}
@Test
public void getChangedFilesWhenNothingHasChanged() throws Exception {
FolderSnapshot updatedSnapshot = new FolderSnapshot(this.folder);
this.initialSnapshot.getChangedFiles(updatedSnapshot);
}
@Test
public void getChangedFilesWhenAFileIsAddedAndDeletedAndChanged() throws Exception {
File folder1 = new File(this.folder, "folder1");
File file1 = new File(folder1, "file1");
File file2 = new File(folder1, "file2");
File newFile = new File(folder1, "newfile");
FileCopyUtils.copy("updatedcontent".getBytes(), file1);
file2.delete();
newFile.createNewFile();
FolderSnapshot updatedSnapshot = new FolderSnapshot(this.folder);
ChangedFiles changedFiles = this.initialSnapshot.getChangedFiles(updatedSnapshot);
assertThat(changedFiles.getSourceFolder(), equalTo(this.folder));
assertThat(getChangedFile(changedFiles, file1).getType(), equalTo(Type.MODIFY));
assertThat(getChangedFile(changedFiles, file2).getType(), equalTo(Type.DELETE));
assertThat(getChangedFile(changedFiles, newFile).getType(), equalTo(Type.ADD));
}
private ChangedFile getChangedFile(ChangedFiles changedFiles, File file) {
for (ChangedFile changedFile : changedFiles) {
if (changedFile.getFile().equals(file)) {
return changedFile;
}
}
return null;
}
private File createTestFolderStructure() throws IOException {
File root = this.temporaryFolder.newFolder();
File folder1 = new File(root, "folder1");
folder1.mkdirs();
FileCopyUtils.copy("abc".getBytes(), new File(folder1, "file1"));
FileCopyUtils.copy("abc".getBytes(), new File(folder1, "file2"));
return root;
}
}
Loading…
Cancel
Save