org.mozilla.javascript.commonjs.module.provider.UrlModuleSourceProvider Maven / Gradle / Ivy
The newest version!
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.javascript.commonjs.module.provider;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Serializable;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Iterator;
import java.util.List;
/**
* A URL-based script provider that can load modules against a set of base
* privileged and fallback URIs. It is deliberately not named "URI provider"
* but a "URL provider" since it actually only works against those URIs that
* are URLs (and the JRE has a protocol handler for them). It creates cache
* validators that are suitable for use with both file: and http: URL
* protocols. Specifically, it is able to use both last-modified timestamps and
* ETags for cache revalidation, and follows the HTTP cache expiry calculation
* model, and allows for fallback heuristic expiry calculation when no server
* specified expiry is provided.
* @author Attila Szegedi
* @version $Id: UrlModuleSourceProvider.java,v 1.4 2011/04/07 20:26:12 hannes%helma.at Exp $
*/
public class UrlModuleSourceProvider extends ModuleSourceProviderBase
{
private static final long serialVersionUID = 1L;
private final Iterable privilegedUris;
private final Iterable fallbackUris;
private final UrlConnectionSecurityDomainProvider
urlConnectionSecurityDomainProvider;
private final UrlConnectionExpiryCalculator urlConnectionExpiryCalculator;
/**
* Creates a new module script provider that loads modules against a set of
* privileged and fallback URIs. It will use a fixed default cache expiry
* of 60 seconds, and provide no security domain objects for the resource.
* @param privilegedUris an iterable providing the privileged URIs. Can be
* null if no privileged URIs are used.
* @param fallbackUris an iterable providing the fallback URIs. Can be
* null if no fallback URIs are used.
*/
public UrlModuleSourceProvider(Iterable privilegedUris,
Iterable fallbackUris)
{
this(privilegedUris, fallbackUris,
new DefaultUrlConnectionExpiryCalculator(), null);
}
/**
* Creates a new module script provider that loads modules against a set of
* privileged and fallback URIs. It will use the specified heuristic cache
* expiry calculator and security domain provider.
* @param privilegedUris an iterable providing the privileged URIs. Can be
* null if no privileged URIs are used.
* @param fallbackUris an iterable providing the fallback URIs. Can be
* null if no fallback URIs are used.
* @param urlConnectionExpiryCalculator the calculator object for heuristic
* calculation of the resource expiry, used when no expiry is provided by
* the server of the resource. Can be null, in which case the maximum age
* of cached entries without validation will be zero.
* @param urlConnectionSecurityDomainProvider object that provides security
* domain objects for the loaded sources. Can be null, in which case the
* loaded sources will have no security domain associated with them.
*/
public UrlModuleSourceProvider(Iterable privilegedUris,
Iterable fallbackUris,
UrlConnectionExpiryCalculator urlConnectionExpiryCalculator,
UrlConnectionSecurityDomainProvider urlConnectionSecurityDomainProvider)
{
this.privilegedUris = privilegedUris;
this.fallbackUris = fallbackUris;
this.urlConnectionExpiryCalculator = urlConnectionExpiryCalculator;
this.urlConnectionSecurityDomainProvider =
urlConnectionSecurityDomainProvider;
}
@Override
protected ModuleSource loadFromPrivilegedLocations(
String moduleId, Object validator)
throws IOException, URISyntaxException
{
return loadFromPathList(moduleId, validator, privilegedUris);
}
@Override
protected ModuleSource loadFromFallbackLocations(
String moduleId, Object validator)
throws IOException, URISyntaxException
{
return loadFromPathList(moduleId, validator, fallbackUris);
}
private ModuleSource loadFromPathList(String moduleId,
Object validator, Iterable paths)
throws IOException, URISyntaxException
{
if(paths == null) {
return null;
}
for (URI path : paths) {
final ModuleSource moduleSource = loadFromUri(
path.resolve(moduleId), path, validator);
if (moduleSource != null) {
return moduleSource;
}
}
return null;
}
@Override
protected ModuleSource loadFromUri(URI uri, URI base, Object validator)
throws IOException, URISyntaxException
{
// We expect modules to have a ".js" file name extension ...
URI fullUri = new URI(uri + ".js");
ModuleSource source = loadFromActualUri(fullUri, base, validator);
// ... but for compatibility we support modules without extension,
// or ids with explicit extension.
return source != null ?
source : loadFromActualUri(uri, base, validator);
}
protected ModuleSource loadFromActualUri(URI uri, URI base, Object validator)
throws IOException
{
final URL url = new URL(base == null ? null : base.toURL(), uri.toString());
final long request_time = System.currentTimeMillis();
final URLConnection urlConnection = openUrlConnection(url);
final URLValidator applicableValidator;
if(validator instanceof URLValidator) {
final URLValidator uriValidator = ((URLValidator)validator);
applicableValidator = uriValidator.appliesTo(uri) ? uriValidator :
null;
}
else {
applicableValidator = null;
}
if(applicableValidator != null) {
applicableValidator.applyConditionals(urlConnection);
}
try {
urlConnection.connect();
if(applicableValidator != null &&
applicableValidator.updateValidator(urlConnection,
request_time, urlConnectionExpiryCalculator))
{
close(urlConnection);
return NOT_MODIFIED;
}
return new ModuleSource(getReader(urlConnection),
getSecurityDomain(urlConnection), uri, base,
new URLValidator(uri, urlConnection, request_time,
urlConnectionExpiryCalculator));
}
catch(FileNotFoundException e) {
return null;
}
catch(RuntimeException e) {
close(urlConnection);
throw e;
}
catch(IOException e) {
close(urlConnection);
throw e;
}
}
private static Reader getReader(URLConnection urlConnection)
throws IOException
{
return new InputStreamReader(urlConnection.getInputStream(),
getCharacterEncoding(urlConnection));
}
private static String getCharacterEncoding(URLConnection urlConnection) {
final ParsedContentType pct = new ParsedContentType(
urlConnection.getContentType());
final String encoding = pct.getEncoding();
if(encoding != null) {
return encoding;
}
final String contentType = pct.getContentType();
if(contentType != null && contentType.startsWith("text/")) {
return "8859_1";
}
else {
return "utf-8";
}
}
private Object getSecurityDomain(URLConnection urlConnection) {
return urlConnectionSecurityDomainProvider == null ? null :
urlConnectionSecurityDomainProvider.getSecurityDomain(
urlConnection);
}
private void close(URLConnection urlConnection) {
try {
urlConnection.getInputStream().close();
}
catch(IOException e) {
onFailedClosingUrlConnection(urlConnection, e);
}
}
/**
* Override if you want to get notified if the URL connection fails to
* close. Does nothing by default.
* @param urlConnection the connection
* @param cause the cause it failed to close.
*/
protected void onFailedClosingUrlConnection(URLConnection urlConnection,
IOException cause) {
}
/**
* Can be overridden in subclasses to customize the URL connection opening
* process. By default, just calls {@link URL#openConnection()}.
* @param url the URL
* @return a connection to the URL.
* @throws IOException if an I/O error occurs.
*/
protected URLConnection openUrlConnection(URL url) throws IOException {
return url.openConnection();
}
@Override
protected boolean entityNeedsRevalidation(Object validator) {
return !(validator instanceof URLValidator)
|| ((URLValidator)validator).entityNeedsRevalidation();
}
private static class URLValidator implements Serializable {
private static final long serialVersionUID = 1L;
private final URI uri;
private final long lastModified;
private final String entityTags;
private long expiry;
public URLValidator(URI uri, URLConnection urlConnection,
long request_time, UrlConnectionExpiryCalculator
urlConnectionExpiryCalculator) {
this.uri = uri;
this.lastModified = urlConnection.getLastModified();
this.entityTags = getEntityTags(urlConnection);
expiry = calculateExpiry(urlConnection, request_time,
urlConnectionExpiryCalculator);
}
boolean updateValidator(URLConnection urlConnection, long request_time,
UrlConnectionExpiryCalculator urlConnectionExpiryCalculator)
throws IOException
{
boolean isResourceChanged = isResourceChanged(urlConnection);
if(!isResourceChanged) {
expiry = calculateExpiry(urlConnection, request_time,
urlConnectionExpiryCalculator);
}
return isResourceChanged;
}
private boolean isResourceChanged(URLConnection urlConnection)
throws IOException {
if(urlConnection instanceof HttpURLConnection) {
return ((HttpURLConnection)urlConnection).getResponseCode() ==
HttpURLConnection.HTTP_NOT_MODIFIED;
}
return lastModified == urlConnection.getLastModified();
}
private long calculateExpiry(URLConnection urlConnection,
long request_time, UrlConnectionExpiryCalculator
urlConnectionExpiryCalculator)
{
if("no-cache".equals(urlConnection.getHeaderField("Pragma"))) {
return 0L;
}
final String cacheControl = urlConnection.getHeaderField(
"Cache-Control");
if(cacheControl != null ) {
if(cacheControl.indexOf("no-cache") != -1) {
return 0L;
}
final int max_age = getMaxAge(cacheControl);
if(-1 != max_age) {
final long response_time = System.currentTimeMillis();
final long apparent_age = Math.max(0, response_time -
urlConnection.getDate());
final long corrected_received_age = Math.max(apparent_age,
urlConnection.getHeaderFieldInt("Age", 0) * 1000L);
final long response_delay = response_time - request_time;
final long corrected_initial_age = corrected_received_age +
response_delay;
final long creation_time = response_time -
corrected_initial_age;
return max_age * 1000L + creation_time;
}
}
final long explicitExpiry = urlConnection.getHeaderFieldDate(
"Expires", -1L);
if(explicitExpiry != -1L) {
return explicitExpiry;
}
return urlConnectionExpiryCalculator == null ? 0L :
urlConnectionExpiryCalculator.calculateExpiry(urlConnection);
}
private int getMaxAge(String cacheControl) {
final int maxAgeIndex = cacheControl.indexOf("max-age");
if(maxAgeIndex == -1) {
return -1;
}
final int eq = cacheControl.indexOf('=', maxAgeIndex + 7);
if(eq == -1) {
return -1;
}
final int comma = cacheControl.indexOf(',', eq + 1);
final String strAge;
if(comma == -1) {
strAge = cacheControl.substring(eq + 1);
}
else {
strAge = cacheControl.substring(eq + 1, comma);
}
try {
return Integer.parseInt(strAge);
}
catch(NumberFormatException e) {
return -1;
}
}
private String getEntityTags(URLConnection urlConnection) {
final List etags = urlConnection.getHeaderFields().get("ETag");
if(etags == null || etags.isEmpty()) {
return null;
}
final StringBuilder b = new StringBuilder();
final Iterator it = etags.iterator();
b.append(it.next());
while(it.hasNext()) {
b.append(", ").append(it.next());
}
return b.toString();
}
boolean appliesTo(URI uri) {
return this.uri.equals(uri);
}
void applyConditionals(URLConnection urlConnection) {
if(lastModified != 0L) {
urlConnection.setIfModifiedSince(lastModified);
}
if(entityTags != null && entityTags.length() > 0) {
urlConnection.addRequestProperty("If-None-Match", entityTags);
}
}
boolean entityNeedsRevalidation() {
return System.currentTimeMillis() > expiry;
}
}
}