模块作用域调整,新增__dirname,__filename
This commit is contained in:
@@ -5,6 +5,7 @@ import android.view.View
|
||||
import org.autojs.autojs.core.automator.UiObjectCollection
|
||||
import org.autojs.autojs.core.ui.ViewExtras
|
||||
import org.autojs.autojs.engine.module.AssetAndUrlModuleSourceProvider
|
||||
import org.autojs.autojs.engine.module.ScopeRequire
|
||||
import org.autojs.autojs.execution.ExecutionConfig
|
||||
import org.autojs.autojs.pio.UncheckedIOException
|
||||
import org.autojs.autojs.project.ScriptConfig
|
||||
@@ -121,11 +122,7 @@ open class RhinoJavaScriptEngine(private val mAndroidContext: android.content.Co
|
||||
mAndroidContext, MODULES_PATH,
|
||||
listOf<URI>(File("/").toURI())
|
||||
)
|
||||
RequireBuilder()
|
||||
.setModuleScriptProvider(SoftCachingModuleScriptProvider(provider))
|
||||
.setSandboxed(true)
|
||||
.createRequire(context, scope)
|
||||
.install(scope)
|
||||
ScopeRequire(context, scope,SoftCachingModuleScriptProvider(provider)).install(scope)
|
||||
}
|
||||
|
||||
protected fun createScope(context: Context): TopLevelScope {
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
package org.autojs.autojs.engine.module;
|
||||
|
||||
import android.content.res.AssetManager;
|
||||
|
||||
import org.autojs.autojs.engine.encryption.ScriptEncryption;
|
||||
import org.autojs.autojs.script.EncryptedScriptFileHeader;
|
||||
import org.mozilla.javascript.commonjs.module.provider.ModuleSource;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URLConnection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by Stardust on 2017/5/9.
|
||||
*/
|
||||
public class AssetAndUrlModuleSourceProvider extends UrlModuleSourceProvider {
|
||||
|
||||
private final android.content.Context mContext;
|
||||
private final URI mBaseURI;
|
||||
private final String mAssetDirPath;
|
||||
private final AssetManager mAssetManager;
|
||||
|
||||
public AssetAndUrlModuleSourceProvider(android.content.Context context, String assetDirPath, List<URI> list) {
|
||||
super(list, null);
|
||||
mContext = context;
|
||||
mAssetDirPath = assetDirPath;
|
||||
mBaseURI = URI.create("file:///android_asset/" + assetDirPath);
|
||||
mAssetManager = mContext.getAssets();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ModuleSource loadFromPrivilegedLocations(String moduleId, Object validator) throws IOException, URISyntaxException {
|
||||
String moduleIdWithExtension = moduleId;
|
||||
if (!moduleIdWithExtension.endsWith(".js")) {
|
||||
moduleIdWithExtension += ".js";
|
||||
}
|
||||
try {
|
||||
return new ModuleSource(new InputStreamReader(mAssetManager.open(mAssetDirPath + "/" + moduleIdWithExtension)), null,
|
||||
new URI(mBaseURI.toString() + "/" + moduleIdWithExtension), mBaseURI, validator);
|
||||
} catch (FileNotFoundException e) {
|
||||
return super.loadFromPrivilegedLocations(moduleId, validator);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Reader getReader(URLConnection urlConnection) throws IOException {
|
||||
InputStream stream = urlConnection.getInputStream();
|
||||
byte[] bytes = new byte[stream.available()];
|
||||
stream.read(bytes);
|
||||
stream.close();
|
||||
if (EncryptedScriptFileHeader.isValidFile(bytes)) {
|
||||
byte[] clearText = ScriptEncryption.decrypt(bytes, EncryptedScriptFileHeader.BLOCK_SIZE, bytes.length);
|
||||
return new InputStreamReader(new ByteArrayInputStream(clearText));
|
||||
}
|
||||
return new InputStreamReader(new ByteArrayInputStream(bytes));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package org.autojs.autojs.engine.module
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.google.gson.Gson
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.mozilla.javascript.commonjs.module.provider.ModuleSource
|
||||
import org.mozilla.javascript.commonjs.module.provider.ModuleSourceProviderBase
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.net.URI
|
||||
|
||||
class AssetAndUrlModuleSourceProvider(
|
||||
context: Context,
|
||||
assetDirPath: String,
|
||||
list: List<URI>? = null
|
||||
) : ModuleSourceProviderBase() {
|
||||
val mContext = context
|
||||
private val okHttpClient = OkHttpClient.Builder().followRedirects(true).build()
|
||||
private val contentResolver: ContentResolver = context.contentResolver
|
||||
private val moduleSources: ArrayList<URI> = arrayListOf(mBaseURI, npmModuleSource)
|
||||
|
||||
companion object {
|
||||
val mBaseURI: URI = URI.create("file:/android_asset/modules")
|
||||
val npmModuleSource:URI = URI.create("file:/android_asset/modules/npm")
|
||||
}
|
||||
//初始化脚本以及启动文件只会从此方法加载模块,子模块加载没有以"./"或"../"开头的模块也会从此方法加载
|
||||
override fun loadFromPrivilegedLocations(moduleId: String, validator: Any?): ModuleSource? {
|
||||
//println("加载私有模块:$moduleId")
|
||||
val uri = if (moduleId.startsWith("/")) {
|
||||
File(moduleId).toURI()
|
||||
} else if (moduleId.startsWith("http://") || moduleId.startsWith("https://")) {
|
||||
URI.create(moduleId)
|
||||
} else null
|
||||
if (uri != null) {
|
||||
return loadFromUri(uri, File(uri.path).parentFile?.toURI(), validator)
|
||||
}
|
||||
for (baseUri in moduleSources) {
|
||||
val sourceUri = URI.create("$baseUri/$moduleId")
|
||||
val moduleSource = loadFromUri(sourceUri, baseUri, validator)
|
||||
if (moduleSource != null) {
|
||||
return moduleSource
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
//这里处理node_module目录的模块
|
||||
override fun loadFromFallbackLocations(moduleId: String, validator: Any?): ModuleSource? {
|
||||
return super.loadFromFallbackLocations(moduleId, validator)
|
||||
}
|
||||
|
||||
//子模块以相对路径加载时调用此方法
|
||||
override fun loadFromUri(uri: URI, base: URI?, validator: Any?): ModuleSource? {
|
||||
var uri = uri
|
||||
if (uri.scheme == null) uri = File(uri.path).toURI()
|
||||
//println("加载模块:$uri")
|
||||
if (uri.scheme == "http" || uri.scheme == "https") {
|
||||
return loadFromHttp(uri, base, validator)
|
||||
}
|
||||
val moduleSource = loadAt(uri, base, validator) ?: loadAt(
|
||||
File(uri.path + ".js").toURI(), base, validator
|
||||
)
|
||||
if (moduleSource != null) {
|
||||
return moduleSource
|
||||
}
|
||||
//尝试从目录加载
|
||||
//尝试读取package.json指定的文件
|
||||
val mainFile: URI? = try {
|
||||
val packageFile = File(uri.path, "package.json")
|
||||
val json = Gson().fromJson<Map<String, Any>>(
|
||||
InputStreamReader(packageFile.inputStream()),
|
||||
Map::class.java
|
||||
)
|
||||
val main = json["main"] as String?
|
||||
main?.let {
|
||||
packageFile.toURI().resolve(main)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
val main: URI = mainFile ?: File(uri.path, "index.js").toURI()
|
||||
return loadAt(main, uri, validator)
|
||||
}
|
||||
|
||||
private fun loadAt(uri: URI, base: URI?, validator: Any?): ModuleSource? {
|
||||
if (uri.scheme == "http" || uri.scheme == "https") {
|
||||
return loadFromHttp(uri, base, validator)
|
||||
}
|
||||
return try {
|
||||
val inputStream = if (uri.path.startsWith("/android_asset/")) {
|
||||
mContext.assets.open(uri.path.replace("/android_asset/", ""))
|
||||
} else contentResolver.openInputStream(Uri.parse(uri.toString()))
|
||||
if (inputStream != null) {
|
||||
createModuleSource(inputStream, uri, base, validator)
|
||||
} else null
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadFromHttp(uri: URI, base: URI?, validator: Any?): ModuleSource? {
|
||||
return try {
|
||||
Request.Builder().url(uri.toString()).build().let { request ->
|
||||
val response = okHttpClient.newCall(request).execute()
|
||||
if (!response.isSuccessful) {
|
||||
response.close()
|
||||
return null
|
||||
}
|
||||
response.body?.let {
|
||||
val charset = it.contentType()?.charset()?.toString() ?: "utf-8"
|
||||
return createModuleSource(it.byteStream(), uri, base, validator, charset)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun createModuleSource(
|
||||
inputStream: InputStream,
|
||||
uri: URI,
|
||||
base: URI?,
|
||||
validator: Any?,
|
||||
charset: String? = null
|
||||
): ModuleSource {
|
||||
val id = if (uri.scheme == "file") {
|
||||
URI.create(uri.path)
|
||||
} else uri
|
||||
return ModuleSource(
|
||||
InputStreamReader(inputStream, charset ?: "utf-8"),
|
||||
null,
|
||||
id,
|
||||
base,
|
||||
validator
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package org.autojs.autojs.engine.module
|
||||
|
||||
import org.mozilla.javascript.*
|
||||
import org.mozilla.javascript.commonjs.module.ModuleScope
|
||||
import org.mozilla.javascript.commonjs.module.ModuleScript
|
||||
import org.mozilla.javascript.commonjs.module.ModuleScriptProvider
|
||||
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
|
||||
open class ScopeRequire(
|
||||
cx: Context, private val nativeScope: Scriptable,
|
||||
private val moduleScriptProvider: ModuleScriptProvider, private val preExec: Script?,
|
||||
private val postExec: Script?, private val sandboxed: Boolean = true
|
||||
) : BaseFunction() {
|
||||
private var paths: Scriptable? = null
|
||||
private var mainModuleId: String? = null
|
||||
private var mainExports: Scriptable? = null
|
||||
|
||||
// Modules that completed loading; visible to all threads
|
||||
private val exportedModuleInterfaces: MutableMap<String, Scriptable?> = ConcurrentHashMap()
|
||||
private val loadLock = Any()
|
||||
|
||||
constructor(cx: Context, nativeScope: Scriptable, moduleScriptProvider: ModuleScriptProvider)
|
||||
: this(cx, nativeScope, moduleScriptProvider, null, null, false)
|
||||
|
||||
init {
|
||||
prototype = getFunctionPrototype(nativeScope)
|
||||
if (!sandboxed) {
|
||||
paths = cx.newArray(nativeScope, 0)
|
||||
defineReadOnlyProperty(this, "paths", paths)
|
||||
} else paths = null
|
||||
}
|
||||
|
||||
|
||||
fun requireMain(cx: Context, mainModuleId: String): Scriptable? {
|
||||
if (this.mainModuleId != null) {
|
||||
if (this.mainModuleId != mainModuleId) {
|
||||
throw IllegalStateException("Main module already set to " + this.mainModuleId)
|
||||
}
|
||||
return mainExports
|
||||
}
|
||||
val moduleScript: ModuleScript? = try {
|
||||
moduleScriptProvider.getModuleScript(cx, mainModuleId, null, null, paths)
|
||||
} catch (x: RuntimeException) {
|
||||
throw x
|
||||
} catch (x: Exception) {
|
||||
throw RuntimeException(x)
|
||||
}
|
||||
if (moduleScript != null) {
|
||||
mainExports = getExportedModuleInterface(
|
||||
cx, mainModuleId,
|
||||
null, null, true
|
||||
)
|
||||
} else if (!sandboxed) {
|
||||
var mainUri: URI? = try {
|
||||
URI(mainModuleId)
|
||||
} catch (_: URISyntaxException) {
|
||||
null
|
||||
}
|
||||
if (mainUri == null || !mainUri.isAbsolute) {
|
||||
val file = File(mainModuleId)
|
||||
if (!file.isFile) {
|
||||
throw ScriptRuntime.throwError(
|
||||
cx, nativeScope,
|
||||
"Module \"$mainModuleId\" not found."
|
||||
)
|
||||
}
|
||||
mainUri = file.toURI()
|
||||
}
|
||||
mainExports = getExportedModuleInterface(
|
||||
cx, mainUri.toString(),
|
||||
mainUri, null, true
|
||||
)
|
||||
}
|
||||
this.mainModuleId = mainModuleId
|
||||
return mainExports
|
||||
}
|
||||
|
||||
fun install(scope: Scriptable?) {
|
||||
putProperty(scope, "require", this)
|
||||
}
|
||||
|
||||
override fun call(cx: Context, scope: Scriptable, thisObj: Scriptable, args: Array<Any>?): Any {
|
||||
if (args == null || args.isEmpty()) {
|
||||
throw ScriptRuntime.throwError(
|
||||
cx, scope,
|
||||
"require() needs one argument"
|
||||
)
|
||||
}
|
||||
var id = Context.jsToJava(args[0], String::class.java) as String
|
||||
var uri: URI? = null
|
||||
var base: URI? = null
|
||||
if (id.startsWith("./") || id.startsWith("../")) {
|
||||
if (thisObj !is ModuleScope) {
|
||||
throw ScriptRuntime.throwError(
|
||||
cx, scope,
|
||||
"Can't resolve relative module ID \"" + id +
|
||||
"\" when require() is used outside of a module"
|
||||
)
|
||||
}
|
||||
base = thisObj.base
|
||||
val current = thisObj.uri
|
||||
uri = current.resolve(id)
|
||||
if (base == null) {
|
||||
id = uri.toString()
|
||||
} else {
|
||||
id = base.relativize(current).resolve(id).toString()
|
||||
if (id[0] == '.') {
|
||||
if (sandboxed) {
|
||||
throw ScriptRuntime.throwError(
|
||||
cx, scope,
|
||||
"Module \"$id\" is not contained in sandbox."
|
||||
)
|
||||
}
|
||||
id = uri.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
return (getExportedModuleInterface(cx, id, uri, base, false))!!
|
||||
}
|
||||
|
||||
override fun construct(cx: Context, scope: Scriptable, args: Array<Any>): Scriptable {
|
||||
throw ScriptRuntime.throwError(
|
||||
cx, scope,
|
||||
"require() can not be invoked as a constructor"
|
||||
)
|
||||
}
|
||||
|
||||
private fun getExportedModuleInterface(
|
||||
cx: Context, id: String, uri: URI?, base: URI?, isMain: Boolean
|
||||
): Scriptable? {
|
||||
// Check if the requested module is already completely loaded
|
||||
var exports = exportedModuleInterfaces[id]
|
||||
if (exports != null) {
|
||||
if (isMain) {
|
||||
throw IllegalStateException("Attempt to set main module after it was loaded")
|
||||
} else
|
||||
return exports
|
||||
}
|
||||
var threadLoadingModules: MutableMap<String, Scriptable>? =
|
||||
loadingModuleInterfaces.get() as? MutableMap<String, Scriptable>
|
||||
exports = threadLoadingModules?.get(id)
|
||||
if (exports != null) return exports
|
||||
|
||||
synchronized(loadLock) {
|
||||
exports = exportedModuleInterfaces[id]
|
||||
if (exports != null) return exports
|
||||
|
||||
val moduleScript: ModuleScript = getModule(cx, id, uri, base)
|
||||
if (sandboxed && !moduleScript.isSandboxed) {
|
||||
throw ScriptRuntime.throwError(
|
||||
cx, nativeScope, ("Module \"$id\" is not contained in sandbox.")
|
||||
)
|
||||
}
|
||||
exports = cx.newObject(nativeScope)
|
||||
val outermostLocked: Boolean = threadLoadingModules == null
|
||||
if (outermostLocked) {
|
||||
threadLoadingModules = HashMap()
|
||||
loadingModuleInterfaces.set(threadLoadingModules)
|
||||
}
|
||||
|
||||
threadLoadingModules?.set(id, exports!!)
|
||||
try {
|
||||
val newExports: Scriptable = executeModuleScript(
|
||||
cx, id, exports,
|
||||
moduleScript, isMain
|
||||
)
|
||||
if (exports !== newExports) {
|
||||
threadLoadingModules?.put(id, newExports)
|
||||
exports = newExports
|
||||
}
|
||||
} catch (e: RuntimeException) {
|
||||
threadLoadingModules?.remove(id)
|
||||
throw e
|
||||
} finally {
|
||||
if (outermostLocked) {
|
||||
exportedModuleInterfaces.putAll((threadLoadingModules!!))
|
||||
loadingModuleInterfaces.set(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
return exports
|
||||
}
|
||||
|
||||
private fun executeModuleScript(
|
||||
cx: Context, id: String,
|
||||
exports: Scriptable?, moduleScript: ModuleScript, isMain: Boolean
|
||||
): Scriptable {
|
||||
val moduleObject = cx.newObject(nativeScope) as ScriptableObject
|
||||
val uri = moduleScript.uri
|
||||
val base = moduleScript.base
|
||||
defineReadOnlyProperty(moduleObject, "id", id)
|
||||
if (!sandboxed) {
|
||||
defineReadOnlyProperty(moduleObject, "uri", uri.toString())
|
||||
}
|
||||
val executionScope: Scriptable = ModuleScope(nativeScope, uri, base)
|
||||
executionScope.put("__filename", executionScope, File(uri.path).path)
|
||||
executionScope.put("__dirname", executionScope, File(uri.path).parent)
|
||||
executionScope.put("exports", executionScope, exports)
|
||||
executionScope.put("module", executionScope, moduleObject)
|
||||
moduleObject.put("exports", moduleObject, exports)
|
||||
install(executionScope)
|
||||
if (isMain) {
|
||||
defineReadOnlyProperty(this, "main", moduleObject)
|
||||
}
|
||||
//创建新作用域
|
||||
val funScope = cx.newObject(executionScope)
|
||||
funScope.parentScope = executionScope
|
||||
|
||||
executeOptionalScript(preExec, cx, funScope)
|
||||
moduleScript.script.exec(cx, funScope)
|
||||
executeOptionalScript(postExec, cx, funScope)
|
||||
return ScriptRuntime.toObject(
|
||||
cx, nativeScope,
|
||||
getProperty(moduleObject, "exports")
|
||||
)
|
||||
}
|
||||
|
||||
private fun getModule(cx: Context, id: String, uri: URI?, base: URI?): ModuleScript {
|
||||
try {
|
||||
return moduleScriptProvider.getModuleScript(cx, id, uri, base, paths)
|
||||
?: throw ScriptRuntime.throwError(
|
||||
cx, nativeScope, ("Module \"$id\" not found.")
|
||||
)
|
||||
} catch (e: RuntimeException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
throw Context.throwAsScriptRuntimeEx(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFunctionName() = "require"
|
||||
override fun getArity() = 1
|
||||
override fun getLength() = 1
|
||||
|
||||
companion object {
|
||||
private const val serialVersionUID = 1L
|
||||
|
||||
private val loadingModuleInterfaces = ThreadLocal<Map<String, Scriptable>>()
|
||||
private fun executeOptionalScript(
|
||||
script: Script?, cx: Context,
|
||||
executionScope: Scriptable
|
||||
) {
|
||||
script?.exec(cx, executionScope)
|
||||
}
|
||||
|
||||
private fun defineReadOnlyProperty(
|
||||
obj: ScriptableObject,
|
||||
name: String, value: Any?
|
||||
) {
|
||||
putProperty(obj, name, value)
|
||||
obj.setAttributes(
|
||||
name, READONLY or
|
||||
PERMANENT
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,360 +0,0 @@
|
||||
package org.autojs.autojs.engine.module;
|
||||
|
||||
import org.mozilla.javascript.commonjs.module.provider.DefaultUrlConnectionExpiryCalculator;
|
||||
import org.mozilla.javascript.commonjs.module.provider.ModuleSource;
|
||||
import org.mozilla.javascript.commonjs.module.provider.ModuleSourceProviderBase;
|
||||
import org.mozilla.javascript.commonjs.module.provider.ParsedContentType;
|
||||
import org.mozilla.javascript.commonjs.module.provider.UrlConnectionExpiryCalculator;
|
||||
import org.mozilla.javascript.commonjs.module.provider.UrlConnectionSecurityDomainProvider;
|
||||
|
||||
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<URI> privilegedUris;
|
||||
private final Iterable<URI> 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<URI> privilegedUris,
|
||||
Iterable<URI> 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<URI> privilegedUris,
|
||||
Iterable<URI> 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<URI> 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 final URLValidator uriValidator) {
|
||||
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 | IOException e) {
|
||||
close(urlConnection);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
protected 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";
|
||||
}
|
||||
|
||||
protected 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.contains("no-cache")) {
|
||||
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<String> etags = urlConnection.getHeaderFields().get("ETag");
|
||||
if (etags == null || etags.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
final StringBuilder b = new StringBuilder();
|
||||
final Iterator<String> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user