org.mozilla.javascript.commonjs.module.provider.UrlModuleSourceProvider Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of rhino-runtime Show documentation
Show all versions of rhino-runtime Show documentation
Rhino JavaScript runtime jar, excludes tools & JSR-223 Script Engine wrapper.
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 Reader getReader(URLConnection urlConnection) throws IOException {
return new InputStreamReader(
urlConnection.getInputStream(), getCharacterEncoding(urlConnection));
}
protected 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";
}
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 static 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 static 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 static 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;
}
}
}