You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tapestry.apache.org by hl...@apache.org on 2013/03/15 00:09:18 UTC

[12/14] git commit: Properly handle resource changes - Invalidate more caches - Recompute clientURL of Assets that are not discarded

Properly handle resource changes
- Invalidate more caches
- Recompute clientURL of Assets that are not discarded


Project: http://git-wip-us.apache.org/repos/asf/tapestry-5/repo
Commit: http://git-wip-us.apache.org/repos/asf/tapestry-5/commit/58ad2241
Tree: http://git-wip-us.apache.org/repos/asf/tapestry-5/tree/58ad2241
Diff: http://git-wip-us.apache.org/repos/asf/tapestry-5/diff/58ad2241

Branch: refs/heads/master
Commit: 58ad22413e7a7cacc94dd2a608bad7398010691b
Parents: 6709278
Author: Howard M. Lewis Ship <hl...@apache.org>
Authored: Thu Mar 14 16:01:53 2013 -0700
Committer: Howard M. Lewis Ship <hl...@apache.org>
Committed: Thu Mar 14 16:01:53 2013 -0700

----------------------------------------------------------------------
 .../src/main/java/org/apache/tapestry5/Asset.java  |   10 ++-
 .../internal/services/AbstractAssetFactory.java    |   31 +++++++++
 .../internal/services/AssetSourceImpl.java         |    9 ++-
 .../internal/services/ClasspathAssetFactory.java   |   25 ++++---
 .../internal/services/ContextAssetFactory.java     |   51 +++++++++-----
 .../internal/services/PageSourceImpl.java          |   10 +++-
 .../internal/services/RequestPageCacheImpl.java    |    6 +-
 .../assets/AssetChecksumGeneratorImpl.java         |   19 +++++-
 .../apache/tapestry5/services/AssetFactory.java    |   17 ++++-
 .../services/javascript/StylesheetLink.java        |   33 +++++++--
 10 files changed, 160 insertions(+), 51 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/58ad2241/tapestry-core/src/main/java/org/apache/tapestry5/Asset.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/Asset.java b/tapestry-core/src/main/java/org/apache/tapestry5/Asset.java
index 5ba36fe..b2c76ac 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/Asset.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/Asset.java
@@ -1,4 +1,4 @@
-// Copyright 2006, 2008, 2009 The Apache Software Foundation
+// Copyright 2006, 2008, 2009, 2013 The Apache Software Foundation
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -34,10 +34,14 @@ public interface Asset
      * Returns a URL that can be passed, unchanged, to the client in order for it to access the resource. The same value
      * is returned from <code>toString()</code>.
      * <p/>
-     * Tapestry's built-in asset types (context and classpath) always incorporate a version number as part of the path,
+     * Tapestry's built-in asset types (context and classpath) always incorporate a checksum as part of the path,
      * and alternate implementations are encouraged to do so as well. In addition, Tapestry ensures that context and
      * classpath assets have a far-future expires header (to ensure aggressive caching by the client).
-     * <p/>
+     * <p/>Note that starting in Tapestry 5.4, it is expected that Asset instances recognize
+     * when the underlying Resource's content has changed, and update the clientURL to reflect the new content's
+     * checksum. This wasn't an issue in earlier releases where the clientURL incorporated a version number.
+     *
+     * @see org.apache.tapestry5.services.AssetSource
      */
     String toClientURL();
 

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/58ad2241/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/AbstractAssetFactory.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/AbstractAssetFactory.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/AbstractAssetFactory.java
new file mode 100644
index 0000000..7caff2c
--- /dev/null
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/AbstractAssetFactory.java
@@ -0,0 +1,31 @@
+// Copyright 2013 The Apache Software Foundation
+//
+// 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.apache.tapestry5.internal.services;
+
+import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker;
+import org.apache.tapestry5.internal.util.RecomputableSupport;
+import org.apache.tapestry5.ioc.annotations.PostInjection;
+import org.apache.tapestry5.services.AssetFactory;
+
+public abstract class AbstractAssetFactory implements AssetFactory
+{
+    protected final RecomputableSupport recomputable = new RecomputableSupport();
+
+    @PostInjection
+    public void updateVersionNumberOnResourceChange(ResourceChangeTracker tracker)
+    {
+        recomputable.initialize(tracker);
+    }
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/58ad2241/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/AssetSourceImpl.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/AssetSourceImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/AssetSourceImpl.java
index b22cbbf..0994ff5 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/AssetSourceImpl.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/AssetSourceImpl.java
@@ -1,4 +1,4 @@
-// Copyright 2006, 2007, 2008, 2009, 2010, 2011, 2012 The Apache Software Foundation
+// Copyright 2006-2013 The Apache Software Foundation
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -18,7 +18,9 @@ import org.apache.tapestry5.Asset;
 import org.apache.tapestry5.ComponentResources;
 import org.apache.tapestry5.internal.AssetConstants;
 import org.apache.tapestry5.internal.TapestryInternalUtils;
+import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker;
 import org.apache.tapestry5.ioc.Resource;
+import org.apache.tapestry5.ioc.annotations.PostInjection;
 import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
 import org.apache.tapestry5.ioc.internal.util.InternalUtils;
 import org.apache.tapestry5.ioc.internal.util.LockSupport;
@@ -77,6 +79,11 @@ public class AssetSourceImpl extends LockSupport implements AssetSource
         registry = StrategyRegistry.newInstance(AssetFactory.class, byResourceClass);
     }
 
+    @PostInjection
+    public void clearCacheWhenResourcesChange(ResourceChangeTracker tracker) {
+        tracker.clearOnInvalidation(cache);
+    }
+
     public Asset getClasspathAsset(String path)
     {
         return getClasspathAsset(path, null);

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/58ad2241/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ClasspathAssetFactory.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ClasspathAssetFactory.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ClasspathAssetFactory.java
index 46c02fc..0173058 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ClasspathAssetFactory.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ClasspathAssetFactory.java
@@ -1,4 +1,4 @@
-// Copyright 2006, 2007, 2008, 2009, 2011, 2013 The Apache Software Foundation
+// Copyright 2006-2013 The Apache Software Foundation
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -15,10 +15,10 @@
 package org.apache.tapestry5.internal.services;
 
 import org.apache.tapestry5.Asset;
+import org.apache.tapestry5.ioc.Invokable;
 import org.apache.tapestry5.ioc.Resource;
 import org.apache.tapestry5.ioc.annotations.Marker;
 import org.apache.tapestry5.ioc.internal.util.ClasspathResource;
-import org.apache.tapestry5.services.AssetFactory;
 import org.apache.tapestry5.services.AssetPathConverter;
 import org.apache.tapestry5.services.ClasspathAssetAliasManager;
 import org.apache.tapestry5.services.ClasspathProvider;
@@ -29,7 +29,7 @@ import org.apache.tapestry5.services.ClasspathProvider;
  * @see AssetDispatcher
  */
 @Marker(ClasspathProvider.class)
-public class ClasspathAssetFactory implements AssetFactory
+public class ClasspathAssetFactory extends AbstractAssetFactory
 {
     private final ClasspathAssetAliasManager aliasManager;
 
@@ -86,13 +86,21 @@ public class ClasspathAssetFactory implements AssetFactory
     }
 
     /**
-     * An invariant asset is normal, and only needs to compute the clientURL for the resource once.
+     * An invariant asset is normal, and only needs to compute the clientURL for the resource once (
+     * or when the underlying Resource's content has changed).
      */
     private Asset createInvariantAsset(final Resource resource)
     {
         return new AbstractAsset(true)
         {
-            private String clientURL;
+            private final Invokable<String> clientURL = recomputable.create(new Invokable<String>()
+            {
+                @Override
+                public String invoke()
+                {
+                    return clientURL(resource);
+                }
+            });
 
             public Resource getResource()
             {
@@ -101,12 +109,7 @@ public class ClasspathAssetFactory implements AssetFactory
 
             public String toClientURL()
             {
-                if (clientURL == null)
-                {
-                    clientURL = clientURL(resource);
-                }
-
-                return clientURL;
+                return clientURL.invoke();
             }
         };
     }

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/58ad2241/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ContextAssetFactory.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ContextAssetFactory.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ContextAssetFactory.java
index bd6c6b3..32d63f7 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ContextAssetFactory.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/ContextAssetFactory.java
@@ -15,6 +15,7 @@
 package org.apache.tapestry5.internal.services;
 
 import org.apache.tapestry5.Asset;
+import org.apache.tapestry5.ioc.Invokable;
 import org.apache.tapestry5.ioc.Resource;
 import org.apache.tapestry5.ioc.internal.util.InternalUtils;
 import org.apache.tapestry5.services.AssetFactory;
@@ -29,7 +30,7 @@ import java.io.IOException;
  *
  * @see org.apache.tapestry5.internal.services.ContextResource
  */
-public class ContextAssetFactory implements AssetFactory
+public class ContextAssetFactory extends AbstractAssetFactory
 {
     private final AssetPathConstructor assetPathConstructor;
 
@@ -52,16 +53,19 @@ public class ContextAssetFactory implements AssetFactory
 
     public Asset createAsset(Resource resource)
     {
-        try
+        if (invariant)
         {
-            String defaultPath = assetPathConstructor.constructAssetPath(RequestConstants.CONTEXT_FOLDER, resource.getPath(), resource);
+            return createInvariantAsset(resource);
+        }
 
-            if (invariant)
-            {
-                return createInvariantAsset(resource, defaultPath);
-            }
+        return createVariantAsset(resource);
+    }
 
-            return createVariantAsset(resource, defaultPath);
+    private String defaultPath(Resource resource)
+    {
+        try
+        {
+            return assetPathConstructor.constructAssetPath(RequestConstants.CONTEXT_FOLDER, resource.getPath(), resource);
         } catch (IOException ex)
         {
             throw new RuntimeException(String.format("Unable to construct asset path for %s: %s",
@@ -69,11 +73,18 @@ public class ContextAssetFactory implements AssetFactory
         }
     }
 
-    private Asset createInvariantAsset(final Resource resource, final String defaultPath)
+    private Asset createInvariantAsset(final Resource resource)
     {
         return new AbstractAsset(true)
         {
-            private String clientURL;
+            private final Invokable<String> clientURL = recomputable.create(new Invokable<String>()
+            {
+                @Override
+                public String invoke()
+                {
+                    return converter.convertAssetPath(defaultPath(resource));
+                }
+            });
 
             public Resource getResource()
             {
@@ -82,20 +93,24 @@ public class ContextAssetFactory implements AssetFactory
 
             public String toClientURL()
             {
-                if (clientURL == null)
-                {
-                    clientURL = converter.convertAssetPath(defaultPath);
-                }
-
-                return clientURL;
+                return clientURL.invoke();
             }
         };
     }
 
-    private Asset createVariantAsset(final Resource resource, final String defaultPath)
+    private Asset createVariantAsset(final Resource resource)
     {
         return new AbstractAsset(false)
         {
+            private final Invokable<String> defaultPath = recomputable.create(new Invokable<String>()
+            {
+                @Override
+                public String invoke()
+                {
+                    return defaultPath(resource);
+                }
+            });
+
             public Resource getResource()
             {
                 return resource;
@@ -103,7 +118,7 @@ public class ContextAssetFactory implements AssetFactory
 
             public String toClientURL()
             {
-                return converter.convertAssetPath(defaultPath);
+                return converter.convertAssetPath(defaultPath.invoke());
             }
         };
     }

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/58ad2241/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PageSourceImpl.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PageSourceImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PageSourceImpl.java
index 066e154..09afc4f 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PageSourceImpl.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/PageSourceImpl.java
@@ -16,6 +16,7 @@ package org.apache.tapestry5.internal.services;
 
 import org.apache.tapestry5.func.F;
 import org.apache.tapestry5.func.Mapper;
+import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker;
 import org.apache.tapestry5.internal.structure.Page;
 import org.apache.tapestry5.ioc.annotations.PostInjection;
 import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
@@ -111,11 +112,18 @@ public class PageSourceImpl implements PageSource
     @PostInjection
     public void setupInvalidation(@ComponentClasses InvalidationEventHub classesHub,
                                   @ComponentTemplates InvalidationEventHub templatesHub,
-                                  @ComponentMessages InvalidationEventHub messagesHub)
+                                  @ComponentMessages InvalidationEventHub messagesHub,
+                                  ResourceChangeTracker resourceChangeTracker)
     {
         classesHub.clearOnInvalidation(pageCache);
         templatesHub.clearOnInvalidation(pageCache);
         messagesHub.clearOnInvalidation(pageCache);
+
+        // Because Assets can be injected into pages, and Assets are invalidated when
+        // an Asset's value is changed (partly due to the change, in 5.4, to include the asset's
+        // checksum as part of the asset URL), then when we notice a change to
+        // any Resource, it is necessary to discard all page instances.
+        resourceChangeTracker.clearOnInvalidation(pageCache);
     }
 
     public void clearCache()

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/58ad2241/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/RequestPageCacheImpl.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/RequestPageCacheImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/RequestPageCacheImpl.java
index aa663da..5fb378c 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/RequestPageCacheImpl.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/RequestPageCacheImpl.java
@@ -34,7 +34,7 @@ import java.util.Map;
  * @since 5.2
  */
 @Scope(ScopeConstants.PERTHREAD)
-public class RequestPageCacheImpl implements RequestPageCache, ThreadCleanupListener
+public class RequestPageCacheImpl implements RequestPageCache, Runnable
 {
     private final Logger logger;
 
@@ -54,10 +54,10 @@ public class RequestPageCacheImpl implements RequestPageCache, ThreadCleanupList
     @PostInjection
     public void listenForThreadCleanup(PerthreadManager perthreadManager)
     {
-        perthreadManager.addThreadCleanupListener(this);
+        perthreadManager.addThreadCleanupCallback(this);
     }
 
-    public void threadDidCleanup()
+    public void run()
     {
         for (Page page : cache.values())
         {

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/58ad2241/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/AssetChecksumGeneratorImpl.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/AssetChecksumGeneratorImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/AssetChecksumGeneratorImpl.java
index eaf7464..5fcbaf0 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/AssetChecksumGeneratorImpl.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/assets/AssetChecksumGeneratorImpl.java
@@ -16,6 +16,7 @@ package org.apache.tapestry5.internal.services.assets;
 
 import org.apache.commons.codec.binary.Hex;
 import org.apache.tapestry5.ioc.Resource;
+import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
 import org.apache.tapestry5.ioc.internal.util.InternalUtils;
 import org.apache.tapestry5.services.assets.AssetChecksumGenerator;
 import org.apache.tapestry5.services.assets.StreamableResource;
@@ -25,6 +26,7 @@ import org.apache.tapestry5.services.assets.StreamableResourceSource;
 import java.io.IOException;
 import java.io.InputStream;
 import java.security.MessageDigest;
+import java.util.Map;
 
 public class AssetChecksumGeneratorImpl implements AssetChecksumGenerator
 {
@@ -34,16 +36,18 @@ public class AssetChecksumGeneratorImpl implements AssetChecksumGenerator
 
     private final ResourceChangeTracker tracker;
 
+    private final Map<StreamableResource, String> cache = CollectionFactory.newConcurrentMap();
+
     public AssetChecksumGeneratorImpl(StreamableResourceSource streamableResourceSource, ResourceChangeTracker tracker)
     {
         this.streamableResourceSource = streamableResourceSource;
         this.tracker = tracker;
+
+        tracker.clearOnInvalidation(cache);
     }
 
     public String generateChecksum(Resource resource) throws IOException
     {
-        // TODO: Caching, and cache invalidation.
-
         StreamableResource streamable = streamableResourceSource.getStreamableResource(resource, StreamableResourceProcessing.COMPRESSION_DISABLED,
                 tracker);
 
@@ -53,7 +57,16 @@ public class AssetChecksumGeneratorImpl implements AssetChecksumGenerator
     @Override
     public String generateChecksum(StreamableResource resource) throws IOException
     {
-        return toChecksum(resource.openStream());
+        String result = cache.get(resource);
+
+        if (result == null)
+        {
+            result = toChecksum(resource.openStream());
+
+            cache.put(resource, result);
+        }
+
+        return result;
     }
 
     private String toChecksum(InputStream is) throws IOException

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/58ad2241/tapestry-core/src/main/java/org/apache/tapestry5/services/AssetFactory.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/AssetFactory.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/AssetFactory.java
index 66fd272..a06dc5a 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/services/AssetFactory.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/AssetFactory.java
@@ -1,4 +1,4 @@
-// Copyright 2006, 2007, 2008 The Apache Software Foundation
+// Copyright 2006, 2007, 2008, 2013 The Apache Software Foundation
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -19,6 +19,16 @@ import org.apache.tapestry5.ioc.Resource;
 
 /**
  * Used by {@link AssetSource} to create new {@link Asset}s as needed.
+ * <p/>
+ * Starting in Tapestry 5.4, the built-in implementations of this interface (for context assets, and for classpath assets)
+ * were changed so that when underlying resources changed, the client URLs for Assets are discarded; this is necessitated by two factors:
+ * <p/>1) the {@linkplain org.apache.tapestry5.Asset#toClientURL() client URL}
+ * for an Asset now includes a checksum based on the content of the underlying resource, so a change to resource content
+ * (during development) results in a change to the URL.
+ * <p/>2) {@link org.apache.tapestry5.services.javascript.JavaScriptStack} (especially the {@link org.apache.tapestry5.services.javascript.ExtensibleJavaScriptStack} implementation)
+ * made no provision for rebuilding the Assets post-construction, and there is no backwards compatible way to
+ * introduce this concept (and JavaScriptStacks are something many applications and third-party libraries make use of).
+ * <p/>So, starting in Tapestry 5.4, the implementations of {@link Asset} should be
  *
  * @see org.apache.tapestry5.services.AssetSource
  */
@@ -33,8 +43,9 @@ public interface AssetFactory
      * Creates an instance of an asset. Starting with 5.1.0.0, it is preferred (but not required) that the factory
      * return an instance of {@link org.apache.tapestry5.Asset2}.
      *
-     * @param resource a resource within this factories domain (derived from the {@linkplain #getRootResource() root
-     *                 resource})
+     * @param resource
+     *         a resource within this factories domain (derived from the {@linkplain #getRootResource() root
+     *         resource})
      * @return an Asset for the resource
      */
     Asset createAsset(Resource resource);

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/58ad2241/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/StylesheetLink.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/StylesheetLink.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/StylesheetLink.java
index 0884e8d..2f6d66a 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/StylesheetLink.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/javascript/StylesheetLink.java
@@ -31,6 +31,8 @@ import org.apache.tapestry5.ioc.internal.util.InternalUtils;
  */
 public final class StylesheetLink
 {
+    private final Asset asset;
+
     private final String url;
 
     private final StylesheetOptions options;
@@ -39,29 +41,39 @@ public final class StylesheetLink
 
     public StylesheetLink(Asset asset)
     {
-        this(asset, null);
+        this(asset, null, null);
     }
 
     public StylesheetLink(Asset asset, StylesheetOptions options)
     {
-        this(asset.toClientURL(), options);
+        this(asset, null, options);
     }
 
     public StylesheetLink(String url)
     {
-        this(url, null);
+        this(null, url, null);
     }
 
     public StylesheetLink(String url, StylesheetOptions options)
     {
-        assert InternalUtils.isNonBlank(url);
+        this(null, url, options);
+    }
+
+    private StylesheetLink(Asset asset, String url, StylesheetOptions options)
+    {
+        assert asset != null || InternalUtils.isNonBlank(url);
+
+        this.asset = asset;
         this.url = url;
         this.options = options != null ? options : BLANK_OPTIONS;
     }
 
     public String getURL()
     {
-        return url;
+        // Only one of asset or url will be non-null.
+        // Starting in 5.4, we keep the asset around and ask it for its clientURL; this is because
+        // clientURLs can change if the content of the underlying Resource changes.
+        return asset != null ? asset.toClientURL() : url;
     }
 
     /**
@@ -76,7 +88,8 @@ public final class StylesheetLink
     /**
      * Invoked to add the stylesheet link to a container element.
      *
-     * @param container to add the new element to
+     * @param container
+     *         to add the new element to
      */
     public void add(Element container)
     {
@@ -90,7 +103,11 @@ public final class StylesheetLink
 
         String rel = options.ajaxInsertionPoint ? "stylesheet t-ajax-insertion-point" : "stylesheet";
 
-        container.element("link", "href", url, "rel", rel, "type", "text/css", "media", options.media);
+        container.element("link",
+                "href", getURL(),
+                "rel", rel,
+                "type", "text/css",
+                "media", options.media);
 
         if (hasCondition)
         {
@@ -115,7 +132,7 @@ public final class StylesheetLink
 
         StylesheetLink ssl = (StylesheetLink) obj;
 
-        return TapestryInternalUtils.isEqual(url, ssl.url) && TapestryInternalUtils.isEqual(options, ssl.options);
+        return TapestryInternalUtils.isEqual(getURL(), ssl.getURL()) && TapestryInternalUtils.isEqual(options, ssl.options);
     }
 
 }