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 {