Author: mhermanto
Date: Wed Dec  8 01:34:28 2010
New Revision: 1043273

URL: http://svn.apache.org/viewvc?rev=1043273&view=rev
Log:
CC: enable server-driven gadget metadata caching. 
http://codereview.appspot.com/3260041/

Modified:
    shindig/trunk/features/src/main/javascript/features/container/constant.js
    shindig/trunk/features/src/main/javascript/features/container/container.js
    shindig/trunk/features/src/main/javascript/features/container/service.js
    shindig/trunk/features/src/main/javascript/features/container/util.js
    
shindig/trunk/features/src/test/javascript/features/container/container_test.js
    
shindig/trunk/features/src/test/javascript/features/container/service_test.js

Modified: 
shindig/trunk/features/src/main/javascript/features/container/constant.js
URL: 
http://svn.apache.org/viewvc/shindig/trunk/features/src/main/javascript/features/container/constant.js?rev=1043273&r1=1043272&r2=1043273&view=diff
==============================================================================
--- shindig/trunk/features/src/main/javascript/features/container/constant.js 
(original)
+++ shindig/trunk/features/src/main/javascript/features/container/constant.js 
Wed Dec  8 01:34:28 2010
@@ -30,6 +30,15 @@ shindig.container = {};
 
 
 /**
+ * Constants to key into gadget metadata state.
+ * @enum {string}
+ */
+shindig.container.MetadataParam = {};
+shindig.container.MetadataParam.LOCAL_EXPIRE_TIME = 'localExpireTimeMs';
+shindig.container.MetadataParam.URL = 'url';
+
+
+/**
  * Constants to key into gadget metadata response JSON.
  * @enum {string}
  */
@@ -37,11 +46,13 @@ shindig.container.MetadataResponse = {};
 shindig.container.MetadataResponse.IFRAME_URL = 'iframeUrl';
 shindig.container.MetadataResponse.NEEDS_TOKEN_REFRESH = 'needsTokenRefresh';
 shindig.container.MetadataResponse.VIEWS = 'views';
+shindig.container.MetadataResponse.EXPIRE_TIME_MS = 'expireTimeMs';
 shindig.container.MetadataResponse.FEATURES = 'features';
 shindig.container.MetadataResponse.HEIGHT = 'height';
 shindig.container.MetadataResponse.MODULE_PREFS = 'modulePrefs';
 shindig.container.MetadataResponse.PREFERRED_HEIGHT = 'preferredHeight';
 shindig.container.MetadataResponse.PREFERRED_WIDTH = 'preferredWidth';
+shindig.container.MetadataResponse.RESPONSE_TIME_MS = 'responseTimeMs';
 shindig.container.MetadataResponse.WIDTH = 'width';
 
 

Modified: 
shindig/trunk/features/src/main/javascript/features/container/container.js
URL: 
http://svn.apache.org/viewvc/shindig/trunk/features/src/main/javascript/features/container/container.js?rev=1043273&r1=1043272&r2=1043273&view=diff
==============================================================================
--- shindig/trunk/features/src/main/javascript/features/container/container.js 
(original)
+++ shindig/trunk/features/src/main/javascript/features/container/container.js 
Wed Dec  8 01:34:28 2010
@@ -153,6 +153,8 @@ shindig.container.Container.prototype.na
     renderParams[shindig.container.RenderParam.TEST_MODE] = true;
   }
 
+  this.refreshService_();
+  
   var self = this;
   // TODO: Lifecycle, add ability for current gadget to cancel nav.
   site.navigateTo(gadgetUrl, viewParams, renderParams, function(gadgetInfo) {
@@ -204,6 +206,8 @@ shindig.container.Container.prototype.pr
   var callback = opt_callback || function() {};
   var request = shindig.container.util.newMetadataRequest(gadgetUrls);
   var self = this;
+  
+  this.refreshService_();
   this.service_.getGadgetMetadata(request, function(response) {
     for (var id in response) {
       if (response[id].error) {
@@ -221,6 +225,27 @@ shindig.container.Container.prototype.pr
 
 
 /**
+ * Unload preloaded gadget. Makes future preload request possibly uncached.
+ * @param {string} gadgetUrl gadget URI to unload.
+ */
+shindig.container.Container.prototype.unloadGadget = function(gadgetUrl) {
+  this.unloadGadgets([gadgetUrl]);
+};
+
+
+/**
+ * Unload preloaded gadgets. Makes future preload request possibly uncached.
+ * @param {Array} gadgetUrls gadgets URIs to unload.
+ */
+shindig.container.Container.prototype.unloadGadgets = function(gadgetUrls) {
+  for (var i = 0; i < gadgetUrls.length; i++) {
+    var url = gadgetUrls[i];
+    delete this.preloadedGadgetUrls_[url];
+  }
+};
+
+
+/**
  * Fetch the gadget metadata commonly used by container for user preferences.
  * @param {string} gadgetUrl gadgets URI to fetch metadata for. to preload.
  * @param {function(Object)} callback Function called with gadget metadata.
@@ -228,6 +253,8 @@ shindig.container.Container.prototype.pr
 shindig.container.Container.prototype.getGadgetMetadata = function(
     gadgetUrl, callback) {
   var request = shindig.container.util.newMetadataRequest([gadgetUrl]);
+  
+  this.refreshService_();
   this.service_.getGadgetMetadata(request, callback);
 };
 
@@ -364,6 +391,18 @@ shindig.container.ContainerRender.WIDTH 
 
 
 /**
+ * Deletes stale cached data in service. The container knows what data are safe
+ * to be marked for deletion.
+ * @private
+ */
+shindig.container.Container.prototype.refreshService_ = function() {
+  var urls = this.getActiveGadgetUrls_();
+  this.service_.uncacheStaleGadgetMetadataExcept(urls);
+  // TODO: also uncache stale gadget tokens.
+};
+
+
+/**
  * @param {string} id Iframe ID of gadget holder contained in the gadget site 
to get.
  * @return {shindig.container.GadgetSite} The gadget site.
  * @private
@@ -447,8 +486,7 @@ shindig.container.Container.prototype.re
  * @param {string} gadgetUrl URL of preloaded gadget.
  * @private
  */
-shindig.container.Container.prototype.addPreloadedGadgetUrl_ = function(
-    gadgetUrl) {
+shindig.container.Container.prototype.addPreloadedGadgetUrl_ = 
function(gadgetUrl) {
   this.preloadedGadgetUrls_[gadgetUrl] = null;
 };
 
@@ -459,36 +497,50 @@ shindig.container.Container.prototype.ad
  * @return {Array} An array of URLs of gadgets.
  * @private
  */
-shindig.container.Container.prototype.getTokenRefreshableGadgetUrls_ =
-    function() {
+shindig.container.Container.prototype.getTokenRefreshableGadgetUrls_ = 
function() {
   var result = {};
-
-  // Collect preloaded gadget urls.
-  for (var url in this.preloadedGadgetUrls_) {
+  for (var url in this.getActiveGadgetUrls_()) {
     var metadata = this.service_.getCachedGadgetMetadata(url);
     if (metadata[shindig.container.MetadataResponse.NEEDS_TOKEN_REFRESH]) {
       result[url] = null;
     }
   }
+  return shindig.container.util.toArrayOfJsonKeys(result);
+};
+
 
-  // Collect active gadget urls.
+/**
+ * Get gadget urls that are either navigated or preloaded.
+ * @return {Object} JSON of gadget URLs.
+ * @private
+ */
+shindig.container.Container.prototype.getActiveGadgetUrls_ = function() { 
+  return shindig.container.util.mergeJsons(
+      this.getNavigatedGadgetUrls_(),
+      this.preloadedGadgetUrls_);
+};
+
+
+/**
+ * Get gadget urls that are navigated on page.
+ * @return {Object} JSON of gadget URLs.
+ * @private
+ */
+shindig.container.Container.prototype.getNavigatedGadgetUrls_ = function() {
+  var result = {};
   for (var siteId in this.sites_) {
     var holder = this.sites_[siteId].getActiveGadgetHolder();
-    var url = holder.getUrl();
-    var metadata = this.service_.getCachedGadgetMetadata(url);
-    if (metadata[shindig.container.MetadataResponse.NEEDS_TOKEN_REFRESH]) {
-      result[url] = null;
+    if (holder) {
+      result[holder.getUrl()] = null;
     }
   }
-
-  return shindig.container.util.toArrayOfJsonKeys(result);
+  return result;
 };
 
 
 /**
  * Refresh security tokens immediately. This will fetch gadget metadata, along
  * with its token and have the token cache updated.
- * @private
  */
 shindig.container.Container.prototype.refreshTokens_ = function() {
   var ids = this.getTokenRefreshableGadgetUrls_();

Modified: 
shindig/trunk/features/src/main/javascript/features/container/service.js
URL: 
http://svn.apache.org/viewvc/shindig/trunk/features/src/main/javascript/features/container/service.js?rev=1043273&r1=1043272&r2=1043273&view=diff
==============================================================================
--- shindig/trunk/features/src/main/javascript/features/container/service.js 
(original)
+++ shindig/trunk/features/src/main/javascript/features/container/service.js 
Wed Dec  8 01:34:28 2010
@@ -87,8 +87,10 @@ shindig.container.Service.prototype.getG
   // arbitrarily-navigated gadgets. The former should be indefinite, unless
   // unloaded. The later can done without user knowing.
   var callback = opt_callback || function() {};
-  var uncachedUrls = this.getUncachedUrls_(request, this.cachedMetadatas_);
-  var finalResponse = this.getCachedData_(request, this.cachedMetadatas_);
+
+  var uncachedUrls = shindig.container.util.toArrayOfJsonKeys(
+      this.getUncachedDataByRequest_(this.cachedMetadatas_, request));
+  var finalResponse = this.getCachedDataByRequest_(this.cachedMetadatas_, 
request);
 
   // If fully cached, return from cache.
   if (uncachedUrls.length == 0) {
@@ -108,10 +110,18 @@ shindig.container.Service.prototype.getG
 
       // Otherwise, cache response. Augment final response with server 
response.
       } else {
+        var currentTimeMs = shindig.container.util.getCurrentTimeMs();
         for (var id in response) {
-          response[id]['url'] = id; // make sure url is set
-          self.cachedMetadatas_[id] = response[id];
-          finalResponse[id] = response[id];
+          var resp = response[id]; 
+          resp[shindig.container.MetadataParam.URL] = id;
+          
+          // This ignores time to fetch metadata. Okay, expect to be < 2s.
+          resp[shindig.container.MetadataParam.LOCAL_EXPIRE_TIME]
+              = resp[shindig.container.MetadataResponse.EXPIRE_TIME_MS]
+              - resp[shindig.container.MetadataResponse.RESPONSE_TIME_MS]
+              + currentTimeMs;
+          self.cachedMetadatas_[id] = resp;
+          finalResponse[id] = resp;
         }
       }
 
@@ -173,6 +183,22 @@ shindig.container.Service.prototype.getC
 
 
 /**
+ * @param {Object} urls JSON containing gadget URLs to avoid removing.
+ */
+shindig.container.Service.prototype.uncacheStaleGadgetMetadataExcept = 
function(urls) {
+  for (var url in this.cachedMetadatas_) {
+    if (typeof urls[url] === 'undefined') {
+      var gadgetInfo = this.cachedMetadatas_[url];
+      if (gadgetInfo[shindig.container.MetadataParam.LOCAL_EXPIRE_TIME]
+          < shindig.container.util.getCurrentTimeMs()) {
+        delete this.cachedMetadatas_[url];
+      }
+    }
+  }
+};
+
+
+/**
  * Initialize OSAPI endpoint methods/interfaces.
  */
 shindig.container.Service.prototype.registerOsapiServices = function() {
@@ -193,37 +219,49 @@ shindig.container.Service.prototype.regi
 
 
 /**
- * Filter cache with requested ids.
- * @param {Object} request containing ids.
+ * Get cached data by ids listed in request.
  * @param {Object} cache JSON containing cached data.
+ * @param {Object} request containing ids.
  * @return {Object} JSON containing requested and cached entries.
  * @private
  */
-shindig.container.Service.prototype.getCachedData_ = function(request, cache) {
-  var result = {};
-  for (var i = 0; i < request.ids.length; i++) {
-    var id = request.ids[i];
-    if (cache[id]) {
-      result[id] = cache[id];
-    }
-  }
-  return result;
+shindig.container.Service.prototype.getCachedDataByRequest_ = function(
+    cache, request) {
+  return this.filterCachedDataByRequest_(cache, request,
+      function(data) { return (typeof data !== 'undefined') });
 };
 
 
 /**
- * Extract ids in request not in cache.
+ * Get uncached data by ids listed in request.
+ * @param {Object} cache JSON containing cached data.
  * @param {Object} request containing ids.
+ * @return {Object} JSON containing requested and uncached entries.
+ * @private
+ */
+shindig.container.Service.prototype.getUncachedDataByRequest_ = function(
+    cache, request) {
+  return this.filterCachedDataByRequest_(cache, request,
+      function(data) { return (typeof data === 'undefined') });
+};
+
+
+/**
+ * Helper to filter out cached data 
  * @param {Object} cache JSON containing cached data.
- * @return {Array.<string>} keys in the json.
+ * @param {Object} request containing ids.
+ * @param {Function} filterFunc function to filter result.
+ * @return {Object} JSON containing requested and filtered entries.
  * @private
  */
-shindig.container.Service.prototype.getUncachedUrls_ = function(request, 
cache) {
-  var result = [];
+shindig.container.Service.prototype.filterCachedDataByRequest_ = function(
+    data, request, filterFunc) {
+  var result = {};
   for (var i = 0; i < request.ids.length; i++) {
     var id = request.ids[i];
-    if (!cache[id]) {
-      result.push(id);
+    var cachedData = data[id];
+    if (filterFunc(cachedData)) {
+      result[id] = cachedData;
     }
   }
   return result;

Modified: shindig/trunk/features/src/main/javascript/features/container/util.js
URL: 
http://svn.apache.org/viewvc/shindig/trunk/features/src/main/javascript/features/container/util.js?rev=1043273&r1=1043272&r2=1043273&view=diff
==============================================================================
--- shindig/trunk/features/src/main/javascript/features/container/util.js 
(original)
+++ shindig/trunk/features/src/main/javascript/features/container/util.js Wed 
Dec  8 01:34:28 2010
@@ -63,7 +63,7 @@ shindig.container.util.mergeJsons = func
  * Construct a JSON request to get gadget metadata. For now, this will request
  * a super-set of data needed for all CC APIs requiring gadget metadata, since
  * the caching of response is not additive.
- * @param {Array} A list of gadget URLs.
+ * @param {Array} gadgetUrls A list of gadget URLs.
  * @return {Object} the resulting JSON.
  */
 shindig.container.util.newMetadataRequest = function(gadgetUrls) {
@@ -76,7 +76,9 @@ shindig.container.util.newMetadataReques
           'needsTokenRefresh',
           'userPrefs.*',
           'views.preferredHeight',
-          'views.preferredWidth'
+          'views.preferredWidth',
+          'expireTimeMs',
+          'responseTimeMs'
       ]
   };
 };

Modified: 
shindig/trunk/features/src/test/javascript/features/container/container_test.js
URL: 
http://svn.apache.org/viewvc/shindig/trunk/features/src/test/javascript/features/container/container_test.js?rev=1043273&r1=1043272&r2=1043273&view=diff
==============================================================================
--- 
shindig/trunk/features/src/test/javascript/features/container/container_test.js 
(original)
+++ 
shindig/trunk/features/src/test/javascript/features/container/container_test.js 
Wed Dec  8 01:34:28 2010
@@ -34,13 +34,95 @@ ContainerTest.prototype.setUp = function
   window.__API_URI = shindig.uri('http://shindig.com');
   this.containerUri = window.__CONTAINER_URI;
   window.__CONTAINER_URI = shindig.uri('http://container.com');
+  this.shindigContainerGadgetSite = shindig.container.GadgetSite;
+  this.gadgetsRpc = gadgets.rpc;
 };
 
 ContainerTest.prototype.tearDown = function() {
   window.__API_URI = this.apiUri;
   window.__CONTAINER_URI = this.containerUri;
+  shindig.container.GadgetSite = this.shindigContainerGadgetSite;
+  gadgets.rpc = this.gadgetsRpc;
 };
 
-ContainerTest.prototype.testNew = function() {
-  // TODO: here for a placeholder. 
+ContainerTest.prototype.testUnloadGadget = function() {
+  this.setupGadgetsRpcRegister();
+  var container = new shindig.container.Container();
+  container.preloadedGadgetUrls_ = {
+    'preloaded1.xml' : {},
+    'preloaded2.xml' : {}
+  };
+  container.unloadGadget('preloaded1.xml');
+  this.assertTrue('1', container.preloadedGadgetUrls_['preloaded1.xml'] == 
null);
+  this.assertTrue('2', container.preloadedGadgetUrls_['preloaded2.xml'] != 
null);
+};
+
+ContainerTest.prototype.testUnloadGadgets = function() {
+  this.setupGadgetsRpcRegister();
+  var container = new shindig.container.Container();
+  container.preloadedGadgetUrls_ = {
+    'preloaded1.xml' : {},
+    'preloaded2.xml' : {},
+    'preloaded3.xml' : {}
+  };
+  container.unloadGadgets(['preloaded1.xml', 'preloaded2.xml']);
+  this.assertTrue('1', container.preloadedGadgetUrls_['preloaded1.xml'] == 
null);
+  this.assertTrue('2', container.preloadedGadgetUrls_['preloaded2.xml'] == 
null);
+  this.assertTrue('3', container.preloadedGadgetUrls_['preloaded3.xml'] != 
null);
+};
+
+ContainerTest.prototype.testNavigateGadget = function() {
+  this.setupGadgetsRpcRegister();
+  var container = new shindig.container.Container({
+    'allowDefaultView' : true,
+    'renderDebug' : true,
+    'renderTest' : true
+  });
+
+  this.setupGadgetSite(1, {}, null);
+  var site = container.newGadgetSite(null);
+  container.navigateGadget(site, 'gadget.xml', {}, {});
+  this.assertEquals('gadget.xml', this.site_navigateTo_gadgetUrl);
+  this.assertTrue(this.site_navigateTo_renderParams['allowDefaultView']);
+  this.assertTrue(this.site_navigateTo_renderParams['debug']);
+  this.assertTrue(this.site_navigateTo_renderParams['nocache']);
+  this.assertTrue(this.site_navigateTo_renderParams['testmode']);
+};
+
+ContainerTest.prototype.testNewGadgetSite = function() {
+  this.setupGadgetsRpcRegister();
+  var container = new shindig.container.Container();
+  this.setupGadgetSite(1, {}, null);
+  var site1 = container.newGadgetSite(null);
+  this.setupGadgetSite(2, {}, null);
+  var site2 = container.newGadgetSite(null);
+  this.assertTrue(container.sites_[1] != null);
+  this.assertTrue(container.sites_[2] != null);
+};
+
+ContainerTest.prototype.setupGadgetSite = function(id, gadgetInfo, 
gadgetHolder) {
+  var self = this;
+  shindig.container.GadgetSite = function() {
+    return {
+      'getId' : function() {
+        return id;
+      },
+      'navigateTo' : function(gadgetUrl, viewParams, renderParams, func) {
+        self.site_navigateTo_gadgetUrl = gadgetUrl;
+        self.site_navigateTo_viewParams = viewParams;
+        self.site_navigateTo_renderParams = renderParams;
+        func(gadgetInfo);
+      },
+      'getActiveGadgetHolder' : function() {
+        return gadgetHolder;
+      }
+    };
+  };
+};
+
+ContainerTest.prototype.setupGadgetsRpcRegister = function() {
+  gadgets.rpc = {
+    register: function() {
+    }
+  };
 };

Modified: 
shindig/trunk/features/src/test/javascript/features/container/service_test.js
URL: 
http://svn.apache.org/viewvc/shindig/trunk/features/src/test/javascript/features/container/service_test.js?rev=1043273&r1=1043272&r2=1043273&view=diff
==============================================================================
--- 
shindig/trunk/features/src/test/javascript/features/container/service_test.js 
(original)
+++ 
shindig/trunk/features/src/test/javascript/features/container/service_test.js 
Wed Dec  8 01:34:28 2010
@@ -32,13 +32,120 @@ ServiceTest.inherits(TestCase);
 ServiceTest.prototype.setUp = function() {
   this.apiUri = window.__API_URI;
   window.__API_URI = shindig.uri('http://shindig.com');
+  this.container = window.__CONTAINER;
+  window.__CONTAINER = "best_container";
+  this.osapiGadgets = osapi.gadgets;
+  
+  this.self = {};
+  var response = {};
+  response.error = {};
 };
 
 ServiceTest.prototype.tearDown = function() {
   window.__API_URI = this.apiUri;
+  window.__CONTAINER = this.container;
+  osapi.gadgets = this.osapiGadgets;
 };
 
-ServiceTest.prototype.testNew = function() {
+ServiceTest.prototype.setupOsapiGadgetsMetadata = function(response) {
+  osapi.gadgets = {};
+  osapi.gadgets.metadata = function(request) {
+    return {
+      execute: function(func) {
+        func(response);
+      }
+    };
+  };
+};
+
+ServiceTest.prototype.setupUtilCurrentTimeMs = function(time) {
+  shindig.container.util.getCurrentTimeMs = function() {
+    return time;
+  };
+};
+
+ServiceTest.prototype.testGetGadgetMetadata = function() {
+  var service = new shindig.container.Service();
+  service.cachedMetadatas_ = {
+    'cached1.xml' : {
+      'url' : 'cached1.xml',
+      'responseTimeMs' : 80,
+      'expireTimeMs' : 85,
+      'localExpireTimeMs' : 100
+    }
+  };
+
+  var request = shindig.container.util.newMetadataRequest([
+      'cached1.xml', 'resp1.xml', 'resp2.xml', 'resp3.xml'
+  ]);
+
+  var response = {
+    'resp1.xml' : {
+      'responseTimeMs' : 90,
+      'expireTimeMs' : 91
+    },
+    'resp2.xml' : {
+      'responseTimeMs' : 110,
+      'expireTimeMs' : 112
+    },
+    'resp3.xml' : {
+      'responseTimeMs' : 97,
+      'expireTimeMs' : 103
+    }
+  };
+
+  var self = this;
+  var callback = function(response) {
+    self.response = response;
+  };
+  
+  this.setupUtilCurrentTimeMs(100);
+  this.setupOsapiGadgetsMetadata(response);
+  var metadata = service.getGadgetMetadata(request, callback);
+  var response = self.response;
+  
+  this.assertEquals('cached1.xml', response['cached1.xml'].url);
+  this.assertEquals(80, response['cached1.xml'].responseTimeMs);
+  this.assertEquals(85, response['cached1.xml'].expireTimeMs);
+  this.assertEquals(100, response['cached1.xml'].localExpireTimeMs);
+
+  this.assertEquals('resp1.xml', response['resp1.xml'].url);
+  this.assertEquals(90, response['resp1.xml'].responseTimeMs);
+  this.assertEquals(91, response['resp1.xml'].expireTimeMs);
+  this.assertEquals(101, response['resp1.xml'].localExpireTimeMs);
+
+  this.assertEquals('resp2.xml', response['resp2.xml'].url);
+  this.assertEquals(110, response['resp2.xml'].responseTimeMs);
+  this.assertEquals(112, response['resp2.xml'].expireTimeMs);
+  this.assertEquals(102, response['resp2.xml'].localExpireTimeMs);
+
+  this.assertEquals('resp3.xml', response['resp3.xml'].url);
+  this.assertEquals(97, response['resp3.xml'].responseTimeMs);
+  this.assertEquals(103, response['resp3.xml'].expireTimeMs);
+  this.assertEquals(106, response['resp3.xml'].localExpireTimeMs);
+  
+  this.assertTrue(service.cachedMetadatas_['cached1.xml'] != null);
+  this.assertTrue(service.cachedMetadatas_['resp1.xml'] != null);
+  this.assertTrue(service.cachedMetadatas_['resp2.xml'] != null);
+  this.assertTrue(service.cachedMetadatas_['resp3.xml'] != null);
+};
+
+ServiceTest.prototype.testUncacheStaleGadgetMetadataExcept = function() {
   var service = new shindig.container.Service();
-  this.assertTrue(service != null);
+  service.cachedMetadatas_ = {
+      'cached1.xml' : { 'localExpireTimeMs' : 100 },
+      'cached2.xml' : { 'localExpireTimeMs' : 200 },
+      'except1.xml' : { 'localExpireTimeMs' : 100 },
+      'except2.xml' : { 'localExpireTimeMs' : 200 }
+  };
+  this.setupUtilCurrentTimeMs(150);
+  service.uncacheStaleGadgetMetadataExcept({
+      'except1.xml' : null,
+      'except2.xml' : null
+  });
+  this.assertTrue(service.cachedMetadatas_['cached1.xml'] == null);
+  this.assertTrue(service.cachedMetadatas_['cached2.xml'] != null);
+  this.assertTrue(service.cachedMetadatas_['except1.xml'] != null);
+  this.assertTrue(service.cachedMetadatas_['except2.xml'] != null);
 };
+


Reply via email to