Author: johnh
Date: Mon Jun 21 23:10:33 2010
New Revision: 956728

URL: http://svn.apache.org/viewvc?rev=956728&view=rev
Log:
Updates FeatureResourceLoader to optionally support reloading JS
referenced by a feature on disk when the resource has changed.

The polling rate for changes is configurable, in millis. A polling
approach is used for simplicity (no need for event mechanism) and to
avoid slamming disk.


Modified:
    
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureResourceLoader.java
    
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/features/FeatureResourceLoaderTest.java

Modified: 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureResourceLoader.java
URL: 
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureResourceLoader.java?rev=956728&r1=956727&r2=956728&view=diff
==============================================================================
--- 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureResourceLoader.java
 (original)
+++ 
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureResourceLoader.java
 Mon Jun 21 23:10:33 2010
@@ -18,9 +18,11 @@
 package org.apache.shindig.gadgets.features;
 
 import com.google.inject.Inject;
+import com.google.inject.name.Named;
 
 import org.apache.shindig.common.uri.Uri;
 import org.apache.shindig.common.util.ResourceLoader;
+import org.apache.shindig.common.util.TimeSource;
 import org.apache.shindig.gadgets.GadgetException;
 import org.apache.shindig.gadgets.http.HttpFetcher;
 import org.apache.shindig.gadgets.http.HttpRequest;
@@ -39,12 +41,25 @@ public class FeatureResourceLoader {
       = Logger.getLogger("org.apache.shindig.gadgets");
   
   private HttpFetcher fetcher;
+  private TimeSource timeSource = new TimeSource();
+  private int updateCheckFrequency = 0;  // <= 0 -> only load data once, don't 
check for updates.
 
   @Inject
   public void setHttpFetcher(HttpFetcher fetcher) {
     this.fetcher = fetcher;
   }
   
+  @Inject
+  public void setTimeSource(TimeSource timeSource) {
+    this.timeSource = timeSource;
+  }
+  
+  @Inject(optional = true)
+  public void setSupportFileUpdates(
+      @Named("shindig.features.loader.file-update-check-frequency-ms") int 
updateCheckFrequency) {
+    this.updateCheckFrequency = updateCheckFrequency;
+  }
+  
   /**
    * Primary, and only public, method of FeatureResourceLoader. Loads the 
resource
    * keyed at the given {...@code uri}, which was decorated with the provided 
list of attributes.
@@ -74,8 +89,7 @@ public class FeatureResourceLoader {
   }
   
   protected FeatureResource loadFile(String path, Map<String, String> attribs) 
throws IOException {
-    return new DualModeStaticResource(path, getFileContent(new 
File(getOptPath(path))),
-        getFileContent(new File(path)));
+    return new DualModeFileResource(getOptPath(path), path);
   }
   
   protected String getFileContent(File file) {
@@ -114,6 +128,73 @@ public class FeatureResourceLoader {
     return orig;
   }
   
+  // Overridable for easier testing.
+  protected boolean fileHasChanged(File file, long lastModified) {
+    return file.lastModified() > lastModified;
+  }
+  
+  private class DualModeFileResource extends FeatureResource.Default {
+    private final FileContent optContent;
+    private final FileContent dbgContent;
+    
+    protected DualModeFileResource(String optFilePath, String dbgFilePath) {
+      this.optContent = new FileContent(optFilePath);
+      this.dbgContent = new FileContent(dbgFilePath);
+      if (optContent.get() == null && dbgContent.get() == null) {
+        throw new IllegalArgumentException("Problems reading resource: " + 
dbgFilePath);
+      }
+    }
+
+    public String getContent() {
+      String opt = optContent.get();
+      return opt != null ? opt : dbgContent.get();
+    }
+
+    public String getDebugContent() {
+      String dbg = dbgContent.get();
+      return dbg != null ? dbg : optContent.get();
+    }
+    
+    private class FileContent {
+      private final String filePath;
+      private long lastModified;
+      private long lastUpdateCheckTime;
+      private String content;
+      
+      private FileContent(String filePath) {
+        this.filePath = filePath;
+        this.lastModified = 0;
+        this.lastUpdateCheckTime = 0;
+      }
+      
+      private String get() {
+        long nowTime = timeSource.currentTimeMillis();
+        if (content == null ||
+            (updateCheckFrequency > 0 &&
+             (lastUpdateCheckTime + updateCheckFrequency) < nowTime)) {
+          // Only check for file updates at preconfigured intervals. This 
prevents
+          // overwhelming the file system while maintaining a reasonable 
update rate w/o
+          // implementing a full event-driven mechanism.
+          lastUpdateCheckTime = nowTime;
+          File file = new File(filePath);
+          if (fileHasChanged(file, lastModified)) {
+            // Only reload file content if it's changed (or if it's the first
+            // load, when this check will succeed).
+            String newContent = getFileContent(file);
+            if (newContent != null) {
+              content = newContent;
+              lastModified = file.lastModified();
+            } else if (content != null) {
+              // Content existed before, file removed - log error.
+              LOG.warning("File existed before but is now missing! Name: " + 
filePath);
+            }
+          }
+        }
+        return content;
+      }
+    }
+  }
+  
   private static class DualModeStaticResource extends FeatureResource.Default {
     private final String content;
     private final String debugContent;

Modified: 
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/features/FeatureResourceLoaderTest.java
URL: 
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/features/FeatureResourceLoaderTest.java?rev=956728&r1=956727&r2=956728&view=diff
==============================================================================
--- 
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/features/FeatureResourceLoaderTest.java
 (original)
+++ 
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/features/FeatureResourceLoaderTest.java
 Mon Jun 21 23:10:33 2010
@@ -29,8 +29,10 @@ import static org.junit.Assert.assertNul
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import org.apache.shindig.common.Pair;
 import org.apache.shindig.common.uri.Uri;
 import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.common.util.FakeTimeSource;
 import org.apache.shindig.gadgets.GadgetException;
 import org.apache.shindig.gadgets.http.HttpFetcher;
 import org.apache.shindig.gadgets.http.HttpRequest;
@@ -47,23 +49,43 @@ import java.util.Map;
 
 public class FeatureResourceLoaderTest {
   private final static String FILE_JS = "gadgets.test.pattern = function(){};";
+  private final static String UPDATED_FILE_JS = "different.impl.completely = 
function(){};";
   private final static String UNCOMPRESSED_FILE_JS
       = "/** Some comments* /\n" +
         "gadgets.test.pattern = function() {" +
         "};";
+  private final static String UPDATED_UNCOMPRESSED_FILE_JS
+  = "/** Different comments* /\n" +
+    "different.impl.completely = function() {" +
+    "};";
   private final static String URL_JS = "while(true){alert('hello');}";
   
-  private FeatureResourceLoader loader;
+  private TestFeatureResourceLoader loader;
+  private FakeTimeSource timeSource;
+  
+  private static class TestFeatureResourceLoader extends FeatureResourceLoader 
{
+    private Map<String, Boolean> forceFileChanged = Maps.newHashMap();
+    
+    @Override
+    protected boolean fileHasChanged(File file, long lastModified) {
+      Boolean changeOverride = forceFileChanged.get(file.getAbsolutePath());
+      return file.lastModified() > lastModified ? true :
+          changeOverride != null && changeOverride;
+    }
+  }
   
   @Before
   public void setUp() {
-    loader = new FeatureResourceLoader();
+    loader = new TestFeatureResourceLoader();
+    timeSource = new FakeTimeSource();
+    timeSource.setCurrentTimeMillis(0);
+    loader.setTimeSource(timeSource);
   }
   
   @Test
   public void loadFileOptOnlyAvailable() throws Exception {
-    Uri optUri = makeFile(".opt.js", FILE_JS);
-    FeatureResource resource = loader.load(optUri, null);
+    Pair<Uri, File> optUri = makeFile(".opt.js", FILE_JS);
+    FeatureResource resource = loader.load(optUri.one, null);
     assertEquals(FILE_JS, resource.getContent());
     assertEquals(FILE_JS, resource.getDebugContent());
     assertFalse(resource.isExternal());
@@ -72,8 +94,8 @@ public class FeatureResourceLoaderTest {
   
   @Test
   public void loadFileDebugOnlyAvailable() throws Exception {
-    Uri dbgUri = makeFile(".js", UNCOMPRESSED_FILE_JS);
-    FeatureResource resource = loader.load(dbgUri, null);
+    Pair<Uri, File> dbgUri = makeFile(".js", UNCOMPRESSED_FILE_JS);
+    FeatureResource resource = loader.load(dbgUri.one, null);
     assertEquals(UNCOMPRESSED_FILE_JS, resource.getContent());
     assertEquals(UNCOMPRESSED_FILE_JS, resource.getDebugContent());
     assertFalse(resource.isExternal());
@@ -82,11 +104,11 @@ public class FeatureResourceLoaderTest {
   
   @Test
   public void loadFileBothModesAvailable() throws Exception {
-    Uri optUri = makeFile(".opt.js", FILE_JS);
-    File dbgFile = new File(optUri.getPath().replace(".opt.js", ".js"));
+    Pair<Uri, File> optUri = makeFile(".opt.js", FILE_JS);
+    File dbgFile = new File(optUri.one.getPath().replace(".opt.js", ".js"));
     dbgFile.createNewFile();
-    Uri dbgUri = makeFile(dbgFile, UNCOMPRESSED_FILE_JS);
-    FeatureResource resource = loader.load(dbgUri, null);
+    Pair<Uri, File> dbgUri = makeFile(dbgFile, UNCOMPRESSED_FILE_JS);
+    FeatureResource resource = loader.load(dbgUri.one, null);
     assertEquals(FILE_JS, resource.getContent());
     assertEquals(UNCOMPRESSED_FILE_JS, resource.getDebugContent());
     assertFalse(resource.isExternal());
@@ -103,8 +125,8 @@ public class FeatureResourceLoaderTest {
   @Test
   public void loadFileNoOptPathCalculable() throws Exception {
     // File doesn't end in .js, so it's loaded for both opt and debug.
-    Uri dbgUri = makeFile(".notjssuffix", UNCOMPRESSED_FILE_JS);
-    FeatureResource resource = loader.load(dbgUri, null);
+    Pair<Uri, File> dbgUri = makeFile(".notjssuffix", UNCOMPRESSED_FILE_JS);
+    FeatureResource resource = loader.load(dbgUri.one, null);
     assertEquals(UNCOMPRESSED_FILE_JS, resource.getContent());
     assertEquals(UNCOMPRESSED_FILE_JS, resource.getDebugContent());
     assertFalse(resource.isExternal());
@@ -112,6 +134,64 @@ public class FeatureResourceLoaderTest {
   }
   
   @Test
+  public void loadFileUpdateIgnoredIfUpdatesDisabled() throws Exception {
+    Pair<Uri, File> optUri = makeFile(".opt.js", FILE_JS);
+    FeatureResource resource = loader.load(optUri.one, null);
+    assertEquals(FILE_JS, resource.getContent());
+    assertEquals(FILE_JS, resource.getDebugContent());
+    assertFalse(resource.isExternal());
+    assertTrue(resource.isProxyCacheable());
+    setFileContent(optUri.two, UPDATED_FILE_JS);
+    
+    // Advance the time. Update checks disabled by default.
+    timeSource.incrementSeconds(10);
+    
+    // Same asserts.
+    assertEquals(FILE_JS, resource.getContent());
+    assertEquals(FILE_JS, resource.getDebugContent());
+    assertFalse(resource.isExternal());
+    assertTrue(resource.isProxyCacheable());
+  }
+  
+  @Test
+  public void loadFileUpdateBehavior() throws Exception {
+    loader.setSupportFileUpdates(5000);  // set in millis
+    Pair<Uri, File> optUri = makeFile(".opt.js", FILE_JS);
+    File dbgFile = new File(optUri.one.getPath().replace(".opt.js", ".js"));
+    dbgFile.createNewFile();
+    Pair<Uri, File> dbgUri = makeFile(dbgFile, UNCOMPRESSED_FILE_JS);
+    FeatureResource resource = loader.load(dbgUri.one, null);
+    assertEquals(FILE_JS, resource.getContent());
+    assertEquals(UNCOMPRESSED_FILE_JS, resource.getDebugContent());
+    assertFalse(resource.isExternal());
+    assertTrue(resource.isProxyCacheable());
+    
+    // Update file contents.
+    setFileContent(optUri.two, UPDATED_FILE_JS);
+    loader.forceFileChanged.put(optUri.two.getAbsolutePath(), true);
+    setFileContent(dbgUri.two, UPDATED_UNCOMPRESSED_FILE_JS);
+    loader.forceFileChanged.put(dbgUri.two.getAbsolutePath(), true);
+    
+    // Advance the time, but not by 5 seconds.
+    timeSource.incrementSeconds(4);
+    
+    // Same asserts.
+    assertEquals(FILE_JS, resource.getContent());
+    assertEquals(UNCOMPRESSED_FILE_JS, resource.getDebugContent());
+    assertFalse(resource.isExternal());
+    assertTrue(resource.isProxyCacheable());
+    
+    // Advance the time, now beyond 5 seconds.
+    timeSource.incrementSeconds(4);
+    
+    // New content should be reflected.
+    assertEquals(UPDATED_FILE_JS, resource.getContent());
+    assertEquals(UPDATED_UNCOMPRESSED_FILE_JS, resource.getDebugContent());
+    assertFalse(resource.isExternal());
+    assertTrue(resource.isProxyCacheable());
+  }
+  
+  @Test
   public void loadUriInline() throws Exception {
     Uri uri = Uri.parse("http://apache.org/resource.js";);
     Map<String, String> attribs = Maps.newHashMap();
@@ -168,16 +248,21 @@ public class FeatureResourceLoaderTest {
     assertTrue(resource.isExternal());
   }
   
-  private Uri makeFile(String suffix, String content) throws Exception {
-    return makeFile(File.createTempFile("restmp", suffix), content);
+  private Pair<Uri, File> makeFile(String suffix, String content) throws 
Exception {
+    File tmpFile = File.createTempFile("restmp", suffix);
+    return makeFile(tmpFile, content);
   }
   
-  private Uri makeFile(File file, String content) throws Exception {
+  private Pair<Uri, File> makeFile(File file, String content) throws Exception 
{
     file.deleteOnExit();
+    setFileContent(file, content);
+    return Pair.of(new 
UriBuilder().setScheme("file").setPath(file.getPath()).toUri(), file);
+  }
+  
+  private void setFileContent(File file, String content) throws Exception {
     BufferedWriter out = new BufferedWriter(new FileWriter(file));
     out.write(content);
     out.close();
-    return new UriBuilder().setScheme("file").setPath(file.getPath()).toUri();
   }
   
   private HttpFetcher mockFetcher(Uri toFetch, String content) throws 
Exception {


Reply via email to