| <html><head> |
| <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> |
| <title>Implement a custom SPDY PushStrategy</title><link rel="stylesheet" type="text/css" href="css/docbook.css"><meta name="generator" content="DocBook XSL-NS Stylesheets V1.76.1"><meta name="keywords" content="jetty, servlet-api, cometd, http, spdy, eclipse, maven, java, software"><link rel="home" href="index.html" title="Jetty : The Definitive Reference"><link rel="up" href="spdy.html" title="Chapter 16. SPDY"><link rel="prev" href="spdy-configuring-push.html" title="Configuring SPDY push"><link rel="next" href="advanced-jetty-start.html" title="Chapter 17. Starting Jetty"><link xmlns:jfetch="java:org.eclipse.jetty.xslt.tools.JavaSourceFetchExtension" xmlns:fetch="java:org.eclipse.jetty.xslt.tools.SourceFetchExtension" xmlns:d="http://docbook.org/ns/docbook" xmlns:l="http://docbook.sourceforge.net/xmlns/l10n/1.0" xmlns:xslthl="http://xslthl.sf.net" xmlns:gcse="http://www.google.com" xmlns:date="http://exslt.org/dates-and-times" rel="shortcut icon" href="images/favicon.ico"><script type="text/javascript" src="js/shCore.js"></script><script type="text/javascript" src="js/shBrushJava.js"></script><script type="text/javascript" src="js/shBrushXml.js"></script><script type="text/javascript" src="js/shBrushBash.js"></script><script type="text/javascript" src="js/shBrushJScript.js"></script><script type="text/javascript" src="js/shBrushSql.js"></script><script type="text/javascript" src="js/shBrushProperties.js"></script><script type="text/javascript" src="js/shBrushPlain.js"></script><link type="text/css" rel="stylesheet" href="css/shCore.css"><link type="text/css" rel="stylesheet" href="css/shThemeEclipse.css"><link type="text/css" rel="stylesheet" href="css/font-awesome.min.css"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><table xmlns:jfetch="java:org.eclipse.jetty.xslt.tools.JavaSourceFetchExtension" xmlns:fetch="java:org.eclipse.jetty.xslt.tools.SourceFetchExtension" xmlns:d="http://docbook.org/ns/docbook" xmlns:l="http://docbook.sourceforge.net/xmlns/l10n/1.0" xmlns:xslthl="http://xslthl.sf.net" xmlns:gcse="http://www.google.com" xmlns:date="http://exslt.org/dates-and-times"><tr><td style="width: 25%"><a href="http://www.eclipse.org/jetty"><img src="images/jetty-header-logo.png" alt="Jetty Logo"></a><br><span style="font-size: small"> |
| Version: 9.0.6.v20130930</span></td><td style="width: 50%"><script type="text/javascript"> (function() { |
| var cx = '016459005284625897022:obd4lsai2ds'; |
| var gcse = document.createElement('script'); |
| gcse.type = 'text/javascript'; |
| gcse.async = true; |
| gcse.src = (document.location.protocol == 'https:' ? 'https:' : 'http:') + |
| '//www.google.com/cse/cse.js?cx=' + cx; |
| var s = document.getElementsByTagName('script')[0]; |
| s.parentNode.insertBefore(gcse, s); |
| })(); |
| </script><gcse:search></gcse:search></td></tr></table><div xmlns:jfetch="java:org.eclipse.jetty.xslt.tools.JavaSourceFetchExtension" xmlns:fetch="java:org.eclipse.jetty.xslt.tools.SourceFetchExtension" xmlns:d="http://docbook.org/ns/docbook" xmlns:l="http://docbook.sourceforge.net/xmlns/l10n/1.0" xmlns:xslthl="http://xslthl.sf.net" xmlns:gcse="http://www.google.com" xmlns:date="http://exslt.org/dates-and-times" class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">Implement a custom SPDY PushStrategy</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="spdy-configuring-push.html"><i class="icon-chevron-left"></i> Previous</a> </td><th width="60%" align="center">Chapter 16. SPDY<br><a accesskey="p" href="index.html"><i class="icon-home"></i> Home</a></th><td width="20%" align="right"> <a accesskey="n" href="advanced-jetty-start.html">Next <i class="icon-chevron-right"></i></a></td></tr></table><hr></div><div xmlns:jfetch="java:org.eclipse.jetty.xslt.tools.JavaSourceFetchExtension" xmlns:fetch="java:org.eclipse.jetty.xslt.tools.SourceFetchExtension" xmlns:d="http://docbook.org/ns/docbook" xmlns:l="http://docbook.sourceforge.net/xmlns/l10n/1.0" xmlns:xslthl="http://xslthl.sf.net" xmlns:gcse="http://www.google.com" xmlns:date="http://exslt.org/dates-and-times" class="jetty-callout"><h5 class="callout"><a href="http://www.webtide.com/support.jsp">Contact the core Jetty developers at |
| <span class="website">www.webtide.com</span></a></h5><p> |
| private support for your internal/customer projects ... custom extensions and distributions ... versioned snapshots for indefinite support ... |
| scalability guidance for your apps and Ajax/Comet projects ... development services from 1 day to full product delivery |
| </p></div><div class="section"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="spdy-implementing-push"></a>Implement a custom SPDY PushStrategy</h2></div></div></div><div class="toc"><dl><dt><span class="section"><a href="spdy-implementing-push.html#d0e10368"> |
| PushStrategy API |
| </a></span></dt><dt><span class="section"><a href="spdy-implementing-push.html#d0e10386">ReferrerPushStrategy</a></span></dt></dl></div><p> |
| The ReferrerPushStrategy that is distributed with Jetty (<a class="xref" href="spdy-configuring-push.html" title="Configuring SPDY push">Configuring SPDY push</a>) does a |
| great job to automatically detect subresources to push for a given main resource. However there might be |
| reasons to implement your own push strategy. |
| </p><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="d0e10368"></a> |
| PushStrategy API |
| </h3></div></div></div><p>The interface that needs to be implemented is<a class="link" href="http://download.eclipse.org/jetty/stable-9/apidocs/org/eclipse/jetty/spdy/server/http/PushStrategy.html" target="_top">PushStrategy</a>. For each |
| request Jetty will call the |
| <a class="link" href="http://download.eclipse.org/jetty/stable-9/apidocs/org/eclipse/jetty/spdy/server/http/PushStrategy.html#apply(org.eclipse.jetty.spdy.api.Stream, org.eclipse.jetty.util.Fields, org.eclipse.jetty.util.Fields)" target="_top"> |
| apply |
| </a> |
| method, which will return a Set with the resources to push. This is the apply method's signature: |
| </p><div class="informalexample"><script type="syntaxhighlighter" class="brush: java;toolbar: false"> |
| <![CDATA[ |
| /** |
| * <p>Applies the SPDY push logic for the primary resource.</p> |
| * |
| * @param stream the primary resource stream |
| * @param requestHeaders the primary resource request headers |
| * @param responseHeaders the primary resource response headers |
| * @return a list of secondary resource URIs to push |
| */ |
| public Set<String> apply(Stream stream, Fields requestHeaders, Fields responseHeaders); |
| ]]> |
| </script></div><p> |
| The parameters are the |
| <a class="link" href="http://download.eclipse.org/jetty/stable-9/apidocs/org/eclipse/jetty/spdy/api/Stream.html" target="_top">Stream |
| </a> |
| for the primary resource request, the request and the response header fields. Based on this information the |
| implementation has to decide which resources to push and return a Set<String> containing the URLs for |
| the resources to push. |
| Jetty will then open a push stream for each URL returned in that list and push the contents of that file. |
| This is the only method that you need to implement. |
| </p></div><div class="section"><div class="titlepage"><div><div><h3 class="title"><a name="d0e10386"></a>ReferrerPushStrategy</h3></div></div></div><p>For reference and as a working example use the |
| <a class="link" href="http://download.eclipse.org/jetty/stable-9/apidocs/org/eclipse/jetty/spdy/server/http/ReferrerPushStrategy.html" target="_top">ReferrerPushStrategy |
| </a> |
| source code: |
| </p><div class="informalexample"><script type="syntaxhighlighter" class="brush: java;toolbar: false"> |
| <![CDATA[// |
| // ======================================================================== |
| // Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd. |
| // ------------------------------------------------------------------------ |
| // All rights reserved. This program and the accompanying materials |
| // are made available under the terms of the Eclipse Public License v1.0 |
| // and Apache License v2.0 which accompanies this distribution. |
| // |
| // The Eclipse Public License is available at |
| // http://www.eclipse.org/legal/epl-v10.html |
| // |
| // The Apache License v2.0 is available at |
| // http://www.opensource.org/licenses/apache2.0.php |
| // |
| // You may elect to redistribute this code under either of these licenses. |
| // ======================================================================== |
| // |
| |
| |
| package org.eclipse.jetty.spdy.server.http; |
| |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Set; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.ConcurrentMap; |
| import java.util.concurrent.CopyOnWriteArraySet; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.atomic.AtomicLong; |
| import java.util.regex.Pattern; |
| |
| import org.eclipse.jetty.spdy.api.Stream; |
| import org.eclipse.jetty.util.Fields; |
| import org.eclipse.jetty.util.log.Log; |
| import org.eclipse.jetty.util.log.Logger; |
| |
| /** |
| * <p>A SPDY push strategy that auto-populates push metadata based on referrer URLs.<p>A typical request for a main |
| * resource such as {@code index.html} is immediately followed by a number of requests for associated resources. |
| * Associated resource requests will have a {@code Referer} HTTP header that points to {@code index.html}, which is |
| * used to link the associated resource to the main resource.<p>However, also following a hyperlink generates a |
| * HTTP request with a {@code Referer} HTTP header that points to {@code index.html}; therefore a proper value for |
| * {@link #setReferrerPushPeriod(int)} has to be set. If the referrerPushPeriod for a main resource has elapsed, |
| * no more associated resources will be added for that main resource.<p>This class distinguishes associated main |
| * resources by their URL path suffix and content type. CSS stylesheets, images and JavaScript files have |
| * recognizable URL path suffixes that are classified as associated resources. The suffix regexs can be configured by |
| * constructor argument</p> |
| * <p>When CSS stylesheets refer to images, the CSS image request will have the CSS stylesheet as referrer. This |
| * implementation will push also the CSS image.<p>The push metadata built by this implementation is limited by the |
| * number of pages of the application itself, and by the {@link #setMaxAssociatedResources(int)} max associated resources} |
| * parameter. This parameter limits the number of associated resources per each main resource, so that if a main |
| * resource has hundreds of associated resources, only up to the number specified by this parameter will be pushed. |
| */ |
| public class ReferrerPushStrategy implements PushStrategy |
| { |
| private static final Logger logger = Log.getLogger(ReferrerPushStrategy.class); |
| private final ConcurrentMap<String, MainResource> mainResources = new ConcurrentHashMap<>(); |
| private final Set<Pattern> pushRegexps = new HashSet<>(); |
| private final Set<String> pushContentTypes = new HashSet<>(); |
| private final Set<Pattern> allowedPushOrigins = new HashSet<>(); |
| private final Set<Pattern> userAgentBlacklist = new HashSet<>(); |
| private volatile int maxAssociatedResources = 32; |
| private volatile int referrerPushPeriod = 5000; |
| |
| public ReferrerPushStrategy() |
| { |
| List<String> defaultPushRegexps = Arrays.asList(".*\\.css", ".*\\.js", ".*\\.png", ".*\\.jpeg", ".*\\.jpg", |
| ".*\\.gif", ".*\\.ico"); |
| addPushRegexps(defaultPushRegexps); |
| |
| List<String> defaultPushContentTypes = Arrays.asList( |
| "text/css", |
| "text/javascript", "application/javascript", "application/x-javascript", |
| "image/png", "image/x-png", |
| "image/jpeg", |
| "image/gif", |
| "image/x-icon", "image/vnd.microsoft.icon"); |
| this.pushContentTypes.addAll(defaultPushContentTypes); |
| } |
| |
| public void setPushRegexps(List<String> pushRegexps) |
| { |
| pushRegexps.clear(); |
| addPushRegexps(pushRegexps); |
| } |
| |
| private void addPushRegexps(List<String> pushRegexps) |
| { |
| for (String pushRegexp : pushRegexps) |
| this.pushRegexps.add(Pattern.compile(pushRegexp)); |
| } |
| |
| public void setPushContentTypes(List<String> pushContentTypes) |
| { |
| pushContentTypes.clear(); |
| pushContentTypes.addAll(pushContentTypes); |
| } |
| |
| public void setAllowedPushOrigins(List<String> allowedPushOrigins) |
| { |
| allowedPushOrigins.clear(); |
| for (String allowedPushOrigin : allowedPushOrigins) |
| this.allowedPushOrigins.add(Pattern.compile(allowedPushOrigin.replace(".", "\\.").replace("*", ".*"))); |
| } |
| |
| public void setUserAgentBlacklist(List<String> userAgentPatterns) |
| { |
| userAgentBlacklist.clear(); |
| for (String userAgentPattern : userAgentPatterns) |
| userAgentBlacklist.add(Pattern.compile(userAgentPattern)); |
| } |
| |
| public void setMaxAssociatedResources(int maxAssociatedResources) |
| { |
| this.maxAssociatedResources = maxAssociatedResources; |
| } |
| |
| public void setReferrerPushPeriod(int referrerPushPeriod) |
| { |
| this.referrerPushPeriod = referrerPushPeriod; |
| } |
| |
| public Set<Pattern> getPushRegexps() |
| { |
| return pushRegexps; |
| } |
| |
| public Set<String> getPushContentTypes() |
| { |
| return pushContentTypes; |
| } |
| |
| public Set<Pattern> getAllowedPushOrigins() |
| { |
| return allowedPushOrigins; |
| } |
| |
| public Set<Pattern> getUserAgentBlacklist() |
| { |
| return userAgentBlacklist; |
| } |
| |
| public int getMaxAssociatedResources() |
| { |
| return maxAssociatedResources; |
| } |
| |
| public int getReferrerPushPeriod() |
| { |
| return referrerPushPeriod; |
| } |
| |
| @Override |
| public Set<String> apply(Stream stream, Fields requestHeaders, Fields responseHeaders) |
| { |
| Set<String> result = Collections.<String>emptySet(); |
| short version = stream.getSession().getVersion(); |
| if (!isIfModifiedSinceHeaderPresent(requestHeaders) && isValidMethod(requestHeaders.get(HTTPSPDYHeader.METHOD |
| .name(version)).value()) && !isUserAgentBlacklisted(requestHeaders)) |
| { |
| String scheme = requestHeaders.get(HTTPSPDYHeader.SCHEME.name(version)).value(); |
| String host = requestHeaders.get(HTTPSPDYHeader.HOST.name(version)).value(); |
| String origin = scheme + "://" + host; |
| String url = requestHeaders.get(HTTPSPDYHeader.URI.name(version)).value(); |
| String absoluteURL = origin + url; |
| logger.debug("Applying push strategy for {}", absoluteURL); |
| if (isMainResource(url, responseHeaders)) |
| { |
| MainResource mainResource = getOrCreateMainResource(absoluteURL); |
| result = mainResource.getResources(); |
| } |
| else if (isPushResource(url, responseHeaders)) |
| { |
| Fields.Field referrerHeader = requestHeaders.get("referer"); |
| if (referrerHeader != null) |
| { |
| String referrer = referrerHeader.value(); |
| MainResource mainResource = mainResources.get(referrer); |
| if (mainResource == null) |
| mainResource = getOrCreateMainResource(referrer); |
| |
| Set<String> pushResources = mainResource.getResources(); |
| if (!pushResources.contains(url)) |
| mainResource.addResource(url, origin, referrer); |
| else |
| result = getPushResources(absoluteURL); |
| } |
| } |
| logger.debug("Pushing {} resources for {}: {}", result.size(), absoluteURL, result); |
| } |
| return result; |
| } |
| |
| private Set<String> getPushResources(String absoluteURL) |
| { |
| Set<String> result = Collections.emptySet(); |
| if (mainResources.get(absoluteURL) != null) |
| result = mainResources.get(absoluteURL).getResources(); |
| return result; |
| } |
| |
| private MainResource getOrCreateMainResource(String absoluteURL) |
| { |
| MainResource mainResource = mainResources.get(absoluteURL); |
| if (mainResource == null) |
| { |
| logger.debug("Creating new main resource for {}", absoluteURL); |
| MainResource value = new MainResource(absoluteURL); |
| mainResource = mainResources.putIfAbsent(absoluteURL, value); |
| if (mainResource == null) |
| mainResource = value; |
| } |
| return mainResource; |
| } |
| |
| private boolean isIfModifiedSinceHeaderPresent(Fields headers) |
| { |
| return headers.get("if-modified-since") != null; |
| } |
| |
| private boolean isValidMethod(String method) |
| { |
| return "GET".equalsIgnoreCase(method); |
| } |
| |
| private boolean isMainResource(String url, Fields responseHeaders) |
| { |
| return !isPushResource(url, responseHeaders); |
| } |
| |
| public boolean isUserAgentBlacklisted(Fields headers) |
| { |
| Fields.Field userAgentHeader = headers.get("user-agent"); |
| if (userAgentHeader != null) |
| for (Pattern userAgentPattern : userAgentBlacklist) |
| if (userAgentPattern.matcher(userAgentHeader.value()).matches()) |
| return true; |
| return false; |
| } |
| |
| private boolean isPushResource(String url, Fields responseHeaders) |
| { |
| for (Pattern pushRegexp : pushRegexps) |
| { |
| if (pushRegexp.matcher(url).matches()) |
| { |
| Fields.Field header = responseHeaders.get("content-type"); |
| if (header == null) |
| return true; |
| |
| String contentType = header.value().toLowerCase(Locale.ENGLISH); |
| for (String pushContentType : pushContentTypes) |
| if (contentType.startsWith(pushContentType)) |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private class MainResource |
| { |
| private final String name; |
| private final CopyOnWriteArraySet<String> resources = new CopyOnWriteArraySet<>(); |
| private final AtomicLong firstResourceAdded = new AtomicLong(-1); |
| |
| private MainResource(String name) |
| { |
| this.name = name; |
| } |
| |
| public boolean addResource(String url, String origin, String referrer) |
| { |
| // We start the push period here and not when initializing the main resource, because a browser with a |
| // prefilled cache won't request the subresources. If the browser with warmed up cache now hits the main |
| // resource after a server restart, the push period shouldn't start until the first subresource is |
| // being requested. |
| firstResourceAdded.compareAndSet(-1, System.nanoTime()); |
| |
| long delay = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - firstResourceAdded.get()); |
| if (!referrer.startsWith(origin) && !isPushOriginAllowed(origin)) |
| { |
| logger.debug("Skipped store of push metadata {} for {}: Origin: {} doesn't match or origin not allowed", |
| url, name, origin); |
| return false; |
| } |
| |
| // This check is not strictly concurrent-safe, but limiting |
| // the number of associated resources is achieved anyway |
| // although in rare cases few more resources will be stored |
| if (resources.size() >= maxAssociatedResources) |
| { |
| logger.debug("Skipped store of push metadata {} for {}: max associated resources ({}) reached", |
| url, name, maxAssociatedResources); |
| return false; |
| } |
| if (delay > referrerPushPeriod) |
| { |
| logger.debug("Delay: {}ms longer than referrerPushPeriod: {}ms. Not adding resource: {} for: {}", delay, referrerPushPeriod, url, name); |
| return false; |
| } |
| |
| logger.debug("Adding resource: {} for: {} with delay: {}ms.", url, name, delay); |
| resources.add(url); |
| return true; |
| } |
| |
| public Set<String> getResources() |
| { |
| return Collections.unmodifiableSet(resources); |
| } |
| |
| public String toString() |
| { |
| return "MainResource: " + name + " associated resources:" + resources.size(); |
| } |
| |
| private boolean isPushOriginAllowed(String origin) |
| { |
| for (Pattern allowedPushOrigin : allowedPushOrigins) |
| if (allowedPushOrigin.matcher(origin).matches()) |
| return true; |
| return false; |
| } |
| } |
| } |
| ]]> |
| </script></div><p> |
| </p></div></div><script type="text/javascript"> |
| SyntaxHighlighter.all() |
| </script><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="spdy-configuring-push.html"><i class="icon-chevron-left"></i> Previous</a> </td><td width="20%" align="center"><a accesskey="u" href="spdy.html"><i class="icon-chevron-up"></i> Top</a></td><td width="40%" align="right"> <a accesskey="n" href="advanced-jetty-start.html">Next <i class="icon-chevron-right"></i></a></td></tr><tr><td width="40%" align="left" valign="top">Configuring SPDY push </td><td width="20%" align="center"><a accesskey="h" href="index.html"><i class="icon-home"></i> Home</a></td><td width="40%" align="right" valign="top"> Chapter 17. Starting Jetty</td></tr></table></div><p xmlns:jfetch="java:org.eclipse.jetty.xslt.tools.JavaSourceFetchExtension" xmlns:fetch="java:org.eclipse.jetty.xslt.tools.SourceFetchExtension" xmlns:d="http://docbook.org/ns/docbook" xmlns:l="http://docbook.sourceforge.net/xmlns/l10n/1.0" xmlns:xslthl="http://xslthl.sf.net" xmlns:gcse="http://www.google.com" xmlns:date="http://exslt.org/dates-and-times"><div class="jetty-callout"> |
| See an error or something missing? |
| <span class="callout"><a href="http://github.com/jetty-project/jetty-documentation">Contribute to this documentation at |
| <span class="website"><i class="icon-github"></i> Github!</span></a></span><span style="float: right"><i>(Generated: 2013-10-30T13:20:28-05:00)</i></span></div></p><script xmlns:jfetch="java:org.eclipse.jetty.xslt.tools.JavaSourceFetchExtension" xmlns:fetch="java:org.eclipse.jetty.xslt.tools.SourceFetchExtension" xmlns:d="http://docbook.org/ns/docbook" xmlns:l="http://docbook.sourceforge.net/xmlns/l10n/1.0" xmlns:xslthl="http://xslthl.sf.net" xmlns:gcse="http://www.google.com" xmlns:date="http://exslt.org/dates-and-times" type="text/javascript"> |
| var _gaq = _gaq || []; |
| _gaq.push(['_setAccount', 'UA-1149868-7']); |
| _gaq.push(['_trackPageview']); |
| |
| (function() { |
| var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; |
| ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; |
| var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); |
| })(); |
| </script></body></html> |