package org.geoserver.platform.resource; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.File; import java.io.IOException; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.geoserver.platform.resource.ResourceNotification.Event; import org.geoserver.platform.resource.ResourceNotification.Kind; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.experimental.theories.DataPoints; import org.junit.rules.TemporaryFolder; public class FileSystemResourceTheoryTest extends ResourceTheoryTest { FileSystemResourceStore store; @Rule public TemporaryFolder folder = new TemporaryFolder(); @DataPoints public static String[] testPaths() { return new String[] { "FileA", "FileB", "DirC", "DirC/FileD", "DirE", "UndefF", "DirC/UndefF", "DirE/UndefF", "DirE/UndefG/UndefH/UndefI" }; } @Override protected Resource getResource(String path) throws Exception { return store.get(path); } @Before public void setUp() throws Exception { folder.newFile("FileA"); folder.newFile("FileB"); File c = folder.newFolder("DirC"); (new File(c, "FileD")).createNewFile(); folder.newFolder("DirE"); store = new FileSystemResourceStore(folder.getRoot()); } @Test public void invalid() { try { Resource resource = store.get(".."); assertNotNull(resource); fail(".. invalid"); } catch (IllegalArgumentException expected) { } } @Test public void fileEvents() throws Exception { File fileD = Paths.toFile(store.baseDirectory, "DirC/FileD"); AwaitResourceListener listener = new AwaitResourceListener(); store.get("DirC/FileD").addListener(listener); store.watcher.schedule(30, TimeUnit.MILLISECONDS); long before = fileD.lastModified(); long after = touch(fileD); assertTrue( "touched", after>before); ResourceNotification n = listener.await(5,TimeUnit.SECONDS); assertNotNull("detected event", n); assertEquals("file modified", Kind.ENTRY_MODIFY, n.getKind()); assertTrue("Resource only", n.events().isEmpty()); listener.reset(); fileD.delete(); n = listener.await(5,TimeUnit.SECONDS); assertEquals("file removed", Kind.ENTRY_DELETE, n.getKind()); listener.reset(); fileD.createNewFile(); n = listener.await(5,TimeUnit.SECONDS); assertEquals("file created", Kind.ENTRY_CREATE, n.getKind()); store.get("DirC/FileD").removeListener(listener); } /** * Must delay long enough to match file system resolution (2 seconds). * <p> * Example: Linux systems expect around 1 second resolution for file modification. * @param file * @return resulting value of lastmodified * @throws InterruptedException */ private long touch(File file ) throws InterruptedException { long origional = file.lastModified(); if( origional == 0l ){ return 0l; // cannot modify a file that does not exsist } Thread.sleep(2000); // wait two seconds long modifided = System.currentTimeMillis(); file.setLastModified( modifided ); for(modifided = file.lastModified(); (modifided-origional) < 2000; modifided = file.lastModified() ){ file.setLastModified( System.currentTimeMillis() ); Thread.sleep(1000); } return modifided; } @Test public void eventNotification() throws InterruptedException { AwaitResourceListener listener = new AwaitResourceListener(); ResourceNotification n = listener.await(5,TimeUnit.SECONDS); // expect timeout as no events will be sent! assertNull( "No events expected", n ); } @Test public void directoryEvents() throws Exception { File fileA = Paths.toFile(store.baseDirectory, "FileA"); File fileB = Paths.toFile(store.baseDirectory, "FileB"); File dirC = Paths.toFile(store.baseDirectory, "DirC"); File fileD = Paths.toFile(store.baseDirectory, "DirC/FileD"); File dirE = Paths.toFile(store.baseDirectory, "DirE"); AwaitResourceListener listener = new AwaitResourceListener(); store.get(Paths.BASE).addListener(listener); store.watcher.schedule(30, TimeUnit.MILLISECONDS); long before = fileB.lastModified(); long after = touch(fileB); assertTrue( "touched", after>before); ResourceNotification n = listener.await(5,TimeUnit.SECONDS); assertEquals(Kind.ENTRY_MODIFY, n.getKind()); assertEquals(Paths.BASE, n.getPath()); assertEquals(1, n.events().size()); Event e = n.events().get(0); assertEquals(Kind.ENTRY_MODIFY, e.getKind()); assertEquals("FileB", e.getPath()); listener.reset(); fileA.delete(); n = listener.await(5,TimeUnit.SECONDS); assertEquals(Kind.ENTRY_MODIFY,n.getKind()); assertEquals(Paths.BASE,n.getPath()); e = n.events().get(0); assertEquals(Kind.ENTRY_DELETE, e.getKind()); assertEquals("FileA", e.getPath()); listener.reset(); fileA.createNewFile(); n = listener.await(5,TimeUnit.SECONDS); assertEquals(Kind.ENTRY_MODIFY,n.getKind()); assertEquals(Paths.BASE,n.getPath()); e = n.events().get(0); assertEquals(Kind.ENTRY_CREATE, e.getKind()); assertEquals("FileA", e.getPath()); store.get(Paths.BASE).removeListener(listener); } /** ResourceListener that traps the next ResourceNotification for testing */ static class AwaitResourceListener extends Await<ResourceNotification> implements ResourceListener { @Override public void changed(ResourceNotification notify) { notify(notify); } } /** * Support class to efficiently wait for event notification. * * @author Jody Garnett (Boundless) * @param <T> Event Type */ static abstract class Await<T> { Lock lock = new ReentrantLock(true); Condition condition = lock.newCondition(); private T event = null; public void notify(T notification) { //System.out.println("Arrived:"+notification); lock.lock(); try { if (this.event == null) { this.event = notification; } condition.signalAll(); // wake up your event is ready } finally { lock.unlock(); } } public T await() throws InterruptedException{ return await(5,TimeUnit.SECONDS); } /** * Wait for event notification. * <p> * If the event has arrived already this method will return immediately, if not * we will wait for signal. If the event still has not arrived after five seconds * null will be returned. * * @return Notification event, or null if it does not arrive within 5 seconds * @throws InterruptedException */ public T await(long howlong, TimeUnit unit) throws InterruptedException { final long DELAY = unit.convert(howlong, TimeUnit.MILLISECONDS); lock.lock(); try { if (this.event == null) { long mark = System.currentTimeMillis(); while (this.event == null) { long check = System.currentTimeMillis(); if( mark + DELAY < check ){ return null; // event did not show up! } boolean signal = condition.await(1, TimeUnit.SECONDS ); //System.out.println("check wait="+signal+" time="+check+" notify="+this.event); } } } finally { lock.unlock(); } return this.event; } public void reset() { lock.lock(); try { this.event = null; } finally { lock.unlock(); } } @Override public String toString() { return "Await [event=" + event + "]"; } } @Override protected Resource getDirectory() { try { folder.newFolder("NonTestDir"); } catch (IOException e) { fail(); } return store.get("NonTestDir"); } @Override protected Resource getResource() { try { folder.newFile("NonTestFile"); } catch (IOException e) { fail(); } return store.get("NonTestFile"); } @Override protected Resource getUndefined() { return store.get("NonTestUndef"); } }