package com.eas.client.scripts;
import com.eas.client.AppElementFiles;
import com.eas.client.Application;
import com.eas.client.AsyncProcess;
import com.eas.client.ModuleStructure;
import com.eas.client.ServerModuleInfo;
import com.eas.client.cache.PlatypusFiles;
import com.eas.client.queries.QueriesProxy;
import com.eas.client.queries.Query;
import com.eas.client.settings.SettingsConstants;
import com.eas.client.threetier.http.Cookie;
import com.eas.client.threetier.http.PlatypusHttpConstants;
import com.eas.script.Scripts;
import com.eas.util.BinaryUtils;
import com.eas.util.FileUtils;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.IDN;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import java.util.zip.InflaterInputStream;
import javax.script.ScriptException;
import jdk.nashorn.api.scripting.JSObject;
import jdk.nashorn.api.scripting.NashornException;
/**
*
* @author vv
*/
public class ScriptedResource {
private static final Pattern HTTP_PATTERN = Pattern.compile("https?://.*");
protected static volatile Application<?> app;
/**
* Initializes a static fields.
*
* @param aApp
* @param aAbsoluteApiPath
* @param aGlobalAPI
* @throws Exception If something goes wrong
*/
public static void init(Application<?> aApp, Path aAbsoluteApiPath, boolean aGlobalAPI) throws Exception {
assert app == null : "Platypus application resource may be initialized only once.";
app = aApp;
Scripts.init(aAbsoluteApiPath, aGlobalAPI);
}
public static Application<?> getApp() {
return app;
}
/**
* Do not use. Only for tests.
*
* @param aClient
* @param aPrincipalHost
* @throws Exception
*
* public static void initForTests(Client aClient, PrincipalHost
* aPrincipalHost) throws Exception { client = aClient; principalHost =
* aPrincipalHost; }
*/
/**
* Gets an principal provider.
*
* @return Principal host instance
*
* public static PrincipalHost getPrincipalHost() { return principalHost; }
*/
/**
* Gets an absolute path to the application's directory.
*
* @return Application's directory full path or null if not path is not
* avaliable
* @throws java.lang.Exception
*/
public static String getApplicationPath() throws Exception {
return app.getModules().getLocalPath().toString();
}
/**
* Part of manual dependencies resolving process
*
* @param aRemotesNames
* @param aOnSuccess
* @param aOnFailure
* @throws java.lang.Exception
*/
public static void loadRemotes(String[] aRemotesNames, JSObject aOnSuccess, JSObject aOnFailure) throws Exception {
sRequire(aRemotesNames, Scripts.getSpace(), aOnSuccess != null ? (Void v) -> {
aOnSuccess.call(null, new Object[]{});
} : null, aOnFailure != null ? (Exception aReason) -> {
aOnFailure.call(null, new Object[]{aReason.getMessage()});
} : null);
}
/**
* Part of manual dependencies resolving process
*
* @param aQueriesNames
* @param aOnSuccess
* @param aOnFailure
* @throws java.lang.Exception
*/
public static void loadEntities(String[] aQueriesNames, JSObject aOnSuccess, JSObject aOnFailure) throws Exception {
qRequire(aQueriesNames, Scripts.getSpace(), aOnSuccess != null ? (Void v) -> {
aOnSuccess.call(null, new Object[]{});
} : null, aOnFailure != null ? (Exception aReason) -> {
aOnFailure.call(null, new Object[]{aReason.getMessage()});
} : null);
}
public static Object load(final String aResourceName, String aCalledFromFile) throws Exception {
return load(aResourceName, aCalledFromFile, (JSObject) null, (JSObject) null);
}
public static Object load(final String aResourceName, String aCalledFromFile, JSObject aOnSuccess) throws Exception {
return load(aResourceName, aCalledFromFile, aOnSuccess, (JSObject) null);
}
public static Object load(final String aResourceName, String aCalledFromFile, JSObject aOnSuccess, JSObject aOnFailure) throws Exception {
Scripts.Space space = Scripts.getSpace();
return _load(aResourceName, aCalledFromFile, space, aOnSuccess != null ? (Object aLoaded) -> {
aOnSuccess.call(null, new Object[]{space.toJs(aLoaded)});
} : null, aOnFailure != null ? (Exception ex) -> {
aOnFailure.call(null, new Object[]{space.toJs(ex.getMessage())});
} : null);
}
public static Object _load(final String aResourceName, String aCalledFromFile, Scripts.Space aSpace) throws Exception {
return _load(aResourceName, aCalledFromFile, aSpace, null, null);
}
public static Object _load(final String aResourceName, String aCalledFromFile, Scripts.Space aSpace, Consumer<Object> onSuccess, Consumer<Exception> onFailure) throws Exception {
if (onSuccess != null) {
Matcher htppMatcher = HTTP_PATTERN.matcher(aResourceName);
if (htppMatcher.matches()) {
Scripts.startBIO(() -> {
try {
SEHttpResponse httpResponse = requestHttpResource(aResourceName, null, null, null);
try {
aSpace.process(() -> {
onSuccess.accept(httpResponse.getBody() != null ? httpResponse.getBody() : httpResponse.getBodyBuffer());
});
} catch (Exception ex) {
Logger.getLogger(ScriptedResource.class.getName()).log(Level.SEVERE, null, ex);
}
} catch (Exception ex) {
if (onFailure != null) {
aSpace.process(() -> {
onFailure.accept(ex);
});
} else {
Logger.getLogger(ScriptedResource.class.getName()).log(Level.SEVERE, null, ex);
}
}
});
} else {
Path apiPath = Scripts.getAbsoluteApiPath();
Path appPath = getAbsoluteAppPath();
Path calledFromFile = aCalledFromFile != null ? resolveApiApp(aCalledFromFile, apiPath, appPath) : null;
String resourceName = calledFromFile != null ? relativizeApiApp(aResourceName, calledFromFile, apiPath, calledFromFile.getParent(), appPath) : aResourceName;
app.getModules().getResource(resourceName, aSpace, (File resourceFile) -> {
try {
if (resourceFile.exists() && !resourceFile.isDirectory()) {
byte[] data = FileUtils.readBytes(resourceFile);
String fileExt = FileUtils.getFileExtension(resourceFile);
String encoding;
if (PlatypusFiles.isPlatypusProjectFileExt(fileExt) && !PlatypusFiles.REPORT_LAYOUT_EXTENSION.equalsIgnoreCase(fileExt) && !PlatypusFiles.REPORT_LAYOUT_EXTENSION_X.equalsIgnoreCase(fileExt)) {
encoding = SettingsConstants.COMMON_ENCODING;
} else {
String contentType = Files.probeContentType(resourceFile.toPath());
if (contentType != null && contentType.toLowerCase().startsWith("text/")) {
encoding = SettingsConstants.COMMON_ENCODING;
} else {
encoding = null;// assume binary content
}
}
onSuccess.accept(encoding != null ? new String(data, encoding) : data);
} else {
Exception ex = new IllegalArgumentException(String.format("Resource %s not found", resourceName));
if (onFailure != null) {
onFailure.accept(ex);
} else {
throw ex;
}
}
} catch (Exception ex) {
Logger.getLogger(ScriptedResource.class.getName()).log(Level.SEVERE, null, ex);
}
}, (Exception ex) -> {
if (onFailure != null) {
onFailure.accept(ex);
}
});
}
return null;
} else {
return loadSync(aResourceName, aCalledFromFile, aSpace);
}
}
/**
* Loads a resource as text for UTF-8 encoding.
*
* @param aResourceName An relative path to the resource
* @param aCalledFromFile .js file, the code is invoked from
* @param aSpace
* @return Resource's text
* @throws Exception If some error occurs when reading the resource
*/
protected static Object loadSync(String aResourceName, String aCalledFromFile, Scripts.Space aSpace) throws Exception {
byte[] data = null;
String encoding;
Matcher htppMatcher = HTTP_PATTERN.matcher(aResourceName);
if (htppMatcher.matches()) {
SEHttpResponse httpResponse = requestHttpResource(aResourceName, null, null, null);
return httpResponse.getBody() != null ? httpResponse.getBody() : httpResponse.getBodyBuffer();
} else {
Path apiPath = Scripts.getAbsoluteApiPath();
Path appPath = getAbsoluteAppPath();
Path calledFromFile = aCalledFromFile != null ? resolveApiApp(aCalledFromFile, apiPath, appPath) : null;
String resourceName = calledFromFile != null ? relativizeApiApp(aResourceName, calledFromFile, apiPath, calledFromFile.getParent(), appPath) : aResourceName;
File resourceFile = app.getModules().getResource(resourceName, aSpace, null, null);
if (resourceFile.exists() && !resourceFile.isDirectory()) {
data = FileUtils.readBytes(resourceFile);
String fileExt = FileUtils.getFileExtension(resourceFile);
if (PlatypusFiles.isPlatypusProjectFileExt(fileExt) && !PlatypusFiles.REPORT_LAYOUT_EXTENSION.equalsIgnoreCase(fileExt) && !PlatypusFiles.REPORT_LAYOUT_EXTENSION_X.equalsIgnoreCase(fileExt)) {
encoding = SettingsConstants.COMMON_ENCODING;
} else {
String contentType = Files.probeContentType(resourceFile.toPath());
if (contentType != null && contentType.toLowerCase().startsWith("text/")) {
encoding = SettingsConstants.COMMON_ENCODING;
} else {
encoding = null;// assume binary content
}
}
} else {
throw new IllegalArgumentException(String.format("Resource %s not found", resourceName));
}
return encoding != null ? new String(data, encoding) : data;
}
}
public static JSObject jsRequestHttpResource(String aUrl, String aMethod, String aRequestBody, JSObject aHeaders, JSObject aOnSuccess, JSObject aOnFailure) throws Exception {
Scripts.Space space = Scripts.getSpace();
Map<String, Object> headers = new HashMap<>();
if (aHeaders != null) {
aHeaders.keySet().stream().forEach((String aKey) -> {
Object oValue = space.toJava(aHeaders.getMember(aKey));
if (oValue != null) {
headers.put(aKey.toLowerCase(), oValue);
}
});
}
if (aOnSuccess != null) {
Scripts.startBIO(() -> {
try {
SEHttpResponse httResponse = requestHttpResource(aUrl, aMethod, aRequestBody, headers);
space.process(() -> {
JSObject jsResponse = httResponse.toJs(space);
aOnSuccess.call(null, new Object[]{jsResponse});
});
} catch (Exception ex) {
Logger.getLogger(ScriptedResource.class.getName()).log(Level.SEVERE, null, ex);
space.process(() -> {
aOnFailure.call(null, new Object[]{space.toJs(ex.getMessage())});
});
}
});
return null;
} else {
SEHttpResponse httResponse = requestHttpResource(aUrl, aMethod, aRequestBody, headers);
return httResponse.toJs(space);
}
}
public static String toModuleId(Path apiPath, Path appPath, String aScriptName, String aCalledFromFile) throws URISyntaxException {
if (aScriptName != null && !aScriptName.isEmpty()) {
Path calledFromFile = resolveApiApp(aCalledFromFile, apiPath, appPath);
Path calledFromDir = calledFromFile.getParent();
return relativizeApiApp(aScriptName, calledFromFile, apiPath, calledFromDir, appPath);
} else {
return aScriptName;
}
}
protected static String relativizeApiApp(String relative, Path calledFromFile, Path apiPath, Path calledFromDir, Path appPath) {
String absolute;
if (relative.startsWith("./") || relative.startsWith("../")) {
if (calledFromFile.startsWith(apiPath)) {// api relative
Path apiFile = calledFromDir.resolve(relative).normalize();
absolute = apiPath.relativize(apiFile).toString().replace(File.separator, "/");
} else if (calledFromFile.startsWith(appPath)) {// application relative
Path appFile = calledFromDir.resolve(relative).normalize();
absolute = appPath.relativize(appFile).toString().replace(File.separator, "/");
} else {
absolute = relative;
}
} else {
absolute = relative;
}
return absolute;
}
protected static Path resolveApiApp(String aCalledFromFile, Path apiPath, Path appPath) {
Path calledFromFile = Paths.get(aCalledFromFile.replace("/", File.separator));
Path apiResolvedCallPoint = apiPath.resolve(calledFromFile);
Path appResolvedCallPoint = appPath.resolve(calledFromFile);
if (apiResolvedCallPoint.toFile().exists()) {
calledFromFile = apiResolvedCallPoint.normalize();
} else if (appResolvedCallPoint.toFile().exists()) {
calledFromFile = appResolvedCallPoint.normalize();
}
return calledFromFile;
}
public static Path lookupPlatypusJs() throws URISyntaxException {
URL platypusURL = Thread.currentThread().getContextClassLoader().getResource(Scripts.INTERNALS_JS_FILENAME);
if (platypusURL != null) {
Path apiPath = Paths.get(platypusURL.toURI());
apiPath = apiPath.getParent();
return apiPath;
} else {
throw new IllegalStateException("Couldn't find Platypus.js API script " + Scripts.INTERNALS_JS_FILENAME);
}
}
protected static class SEHttpResponse {
protected int status;
protected String statusText;
protected String characterEncoding;
protected List<Cookie> cookies = new ArrayList<>();
protected Map<String, Object> headers = new HashMap<>();
protected byte[] bodyContent;
protected String body;
public SEHttpResponse() {
super();
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getStatusText() {
return statusText;
}
public void setStatusText(String statusText) {
this.statusText = statusText;
}
public String getCharacterEncoding() {
return characterEncoding;
}
public void setCharacterEncoding(String characterEncoding) {
this.characterEncoding = characterEncoding;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public List<Cookie> getCookies() {
return cookies;
}
public void setCookies(List<Cookie> cookies) {
this.cookies = cookies;
}
public Map<String, Object> getHeaders() {
return headers;
}
public void setHeaders(Map<String, Object> headers) {
this.headers = headers;
}
public byte[] getBodyBuffer() {
return bodyContent;
}
public void setBodyContent(byte[] bodyContent) {
this.bodyContent = bodyContent;
}
public JSObject toJs(Scripts.Space aSpace) {
JSObject jsResp = aSpace.makeObj();
// general
jsResp.setMember("status", getStatus());
jsResp.setMember("statusText", getStatusText());
jsResp.setMember("contentType", getHeaders().get(PlatypusHttpConstants.HEADER_CONTENTTYPE));
jsResp.setMember("body", getBody());
jsResp.setMember("bodyBuffer", getBodyBuffer());
jsResp.setMember("characterEncoding", getCharacterEncoding());
// headers
JSObject jsHeaders = aSpace.makeObj();
getHeaders().entrySet().stream().forEach((Map.Entry<String, Object> aEntry) -> {
jsHeaders.setMember(aEntry.getKey(), aSpace.toJs(aEntry.getValue()));
});
jsResp.setMember("headers", jsHeaders);
// cookies
JSObject jsCookies = aSpace.makeObj();
getCookies().forEach((Cookie aCookie) -> {
JSObject jsCookie = aSpace.makeObj();
jsCookie.setMember("name", aCookie.getName());
jsCookie.setMember("domain", aCookie.getDomain());
jsCookie.setMember("expires", aSpace.toJs(aCookie.getExpires()));
jsCookie.setMember("maxAge", (double) aCookie.getMaxAge());
jsCookie.setMember("path", aCookie.getPath());
jsCookie.setMember("value", aCookie.getValue());
jsCookies.setMember(aCookie.getName(), jsCookie);
});
jsResp.setMember("cookies", jsCookies);
return jsResp;
}
}
public static SEHttpResponse requestHttpResource(String aUrl, String aMethod, String aRequestBody, Map<String, Object> aHeaders) throws Exception {
byte[] data;
String encoding;
Callable<HttpURLConnection> connFactory = () -> {
try {
return (HttpURLConnection) new URL(aUrl).openConnection();
} catch (IOException ex) {
URL encodedUrl = encodeUrl(new URL(aUrl));
return (HttpURLConnection) encodedUrl.openConnection();
}
};
final HttpURLConnection conn = connFactory.call();
if (aMethod != null && !aMethod.isEmpty()) {
conn.setRequestMethod(aMethod);
}
conn.setDoInput(true);
conn.setRequestProperty("accept-encoding", "deflate");
if (aHeaders != null) {
aHeaders.entrySet().stream().forEach((Map.Entry<String, Object> aHeader) -> {
Object oValue = aHeader.getValue();
if (oValue instanceof Number) {
conn.setRequestProperty(aHeader.getKey(), "" + ((Number) oValue).intValue());
} else if (oValue instanceof Date) {
DateFormat df = new SimpleDateFormat(PlatypusHttpConstants.HTTP_DATE_FORMAT);
df.setTimeZone(TimeZone.getTimeZone("UTC"));
conn.setRequestProperty(aHeader.getKey(), df.format((Date) oValue));
} else if (oValue != null) {
conn.setRequestProperty(aHeader.getKey(), "" + oValue);
}
});
}
if (aRequestBody != null && !aRequestBody.isEmpty()) {
conn.setRequestProperty(PlatypusHttpConstants.HEADER_CONTENTTYPE, aHeaders != null && aHeaders.containsKey(PlatypusHttpConstants.HEADER_CONTENTTYPE.toLowerCase()) ? "" + aHeaders.get(PlatypusHttpConstants.HEADER_CONTENTTYPE.toLowerCase()) : "text/plain;charset=" + SettingsConstants.COMMON_ENCODING);
byte[] body = aRequestBody.getBytes(SettingsConstants.COMMON_ENCODING);
conn.setRequestProperty(PlatypusHttpConstants.HEADER_CONTENTLENGTH, "" + body.length);
conn.setDoOutput(true);
try (OutputStream os = conn.getOutputStream()) {
os.write(body);
}
}
SEHttpResponse resp = new SEHttpResponse();
resp.setStatus(conn.getResponseCode());
resp.setStatusText(conn.getResponseMessage());
InputStream is = conn.getInputStream();
String contentEncoding = conn.getContentEncoding();
if (contentEncoding != null) {
if (contentEncoding.contains("gzip") || contentEncoding.contains("zip")) {
is = new GZIPInputStream(is);
} else if (contentEncoding.contains("deflate")) {
is = new InflaterInputStream(is);
}
}
try (InputStream _is = is) {
data = BinaryUtils.readStream(_is, -1);
String contentType = conn.getContentType();
if (contentType != null) {
contentType = contentType.replaceAll("\\s+", "").toLowerCase();
if (contentType.startsWith("text/") || contentType.contains("charset") || contentType.startsWith("application/json")) {
if (contentType.contains(";charset=")) {
String[] typeCharset = contentType.split(";charset=");
if (typeCharset.length == 2 && typeCharset[1] != null) {
encoding = typeCharset[1];
} else {
Logger.getLogger(ScriptedResource.class.getName()).log(Level.WARNING, CHARSET_MISSING_MSG);
encoding = SettingsConstants.COMMON_ENCODING;
}
} else {
Logger.getLogger(ScriptedResource.class.getName()).log(Level.WARNING, CHARSET_MISSING_MSG);
encoding = SettingsConstants.COMMON_ENCODING;
}
} else {
encoding = null;// assume binary response
}
} else {
Logger.getLogger(ScriptedResource.class.getName()).log(Level.WARNING, CHARSET_MISSING_MSG);
encoding = SettingsConstants.COMMON_ENCODING;
}
}
if (encoding != null) {
resp.setCharacterEncoding(encoding);
resp.setBody(new String(data, encoding));
} else {
resp.setBodyContent(data);
}
Map<String, List<String>> headers = conn.getHeaderFields();
headers.entrySet().stream().forEach((Map.Entry<String, List<String>> aEntry) -> {
/* Crazy J2SE Http client!*/
if (aEntry.getKey() != null && !aEntry.getValue().isEmpty()) {
resp.getHeaders().put(aEntry.getKey().toLowerCase(), aEntry.getValue().get(0));
}
});
List<String> cookieHeaders = headers.get(PlatypusHttpConstants.HEADER_SETCOOKIE);
if (cookieHeaders != null) {
cookieHeaders.stream().forEach((setCookieHeaderValue) -> {
try {
Cookie cookie = Cookie.parse(setCookieHeaderValue.toLowerCase());
resp.getCookies().add(cookie);
} catch (ParseException | NumberFormatException ex) {
Logger.getLogger(ScriptedResource.class.getName()).log(Level.SEVERE, ex.getMessage());
}
});
}
long contentLength = conn.getContentLengthLong();
if (contentLength != -1) {
resp.getHeaders().put(PlatypusHttpConstants.HEADER_CONTENTLENGTH, contentLength);
}
long date = conn.getDate();
if (date != 0) {
resp.getHeaders().put(PlatypusHttpConstants.HEADER_DATE, new Date(date));
}
long expires = conn.getExpiration();
if (expires != 0) {
resp.getHeaders().put(PlatypusHttpConstants.HEADER_EXPIRES, new Date(expires));
}
long lastModified = conn.getLastModified();
if (lastModified != 0) {
resp.getHeaders().put(PlatypusHttpConstants.HEADER_LAST_MODIFIED, new Date(lastModified));
}
return resp;
}
public static final String CHARSET_MISSING_MSG = "Charset missing in http response. Falling back to " + SettingsConstants.COMMON_ENCODING;
private static URL encodeUrl(URL url) throws URISyntaxException, MalformedURLException {
String file = "";
if (url.getPath() != null && !url.getPath().isEmpty()) {
file += (new URI(null, null, url.getPath(), null)).toASCIIString();
}
if (url.getQuery() != null && !url.getQuery().isEmpty()) {
file += "?" + url.getQuery();
}
if (url.getRef() != null && !url.getRef().isEmpty()) {
file += "#" + url.getRef();
}
url = new URL(url.getProtocol(), IDN.toASCII(url.getHost()), url.getPort(), file);
return url;
}
protected static class RequireProcess extends AsyncProcess<String, Void> {
public RequireProcess(int aExpected, Consumer<Void> aOnSuccess, Consumer<Exception> aOnFailure) {
super(aExpected, aOnSuccess, aOnFailure);
}
@Override
public void complete(String aValue, Exception aFailureCause) {
if (aFailureCause != null) {
exceptions.add(aFailureCause);
}
if (++completed == expected) {
doComplete(null);
}
}
}
private static final Map<String, ModuleStructure> scriptsOfModulesStructures = new ConcurrentHashMap<>();
private static void loadScriptOfModule(String aModuleName, String aCalledFromFile, Scripts.Space aSpace, Set<String> aCyclic, Path apiPath, Consumer<Path> onSuccess, Consumer<Exception> onFailure) {
// API content is not compressible into bundles and so API module name is transformed into a script file name directly
// Also API files can't have global dependencies and can't have prefetched resources
Path apiLocalPath = apiPath.resolve(aModuleName + PlatypusFiles.JAVASCRIPT_FILE_END);
if (apiLocalPath != null && apiLocalPath.toFile().exists() && !apiLocalPath.toFile().isDirectory()) {
// network activity simulation
aSpace.process(() -> {
onSuccess.accept(apiLocalPath.normalize());
});
} else {// Module is application module, so let's discover what file contains the module.
Consumer<ModuleStructure> withModuleStructure = (ModuleStructure structure) -> {
if (structure != null) {
scriptsOfModulesStructures.put(aModuleName, structure);
try {
AppElementFiles files = structure.getParts();
File sourceFile = files.findFileByExtension(PlatypusFiles.JAVASCRIPT_EXTENSION);
RequireProcess moduleProcess = new RequireProcess(3, (Void v) -> {
Path appLocalPath = Paths.get(sourceFile.toURI());
onSuccess.accept(appLocalPath);
}, (Exception ex) -> {
onFailure.accept(ex);
});
if (files.isModule()) {
try {
// 1
qRequire(structure.getQueryDependencies().toArray(new String[]{}), aSpace, (Void v) -> {
moduleProcess.complete(aModuleName + ".q", null);
}, (Exception ex) -> {
moduleProcess.complete(aModuleName + ".q", ex);
});
// 2
sRequire(structure.getServerDependencies().toArray(new String[]{}), aSpace, (Void v) -> {
moduleProcess.complete(aModuleName + ".s", null);
}, (Exception ex) -> {
moduleProcess.complete(aModuleName + ".s", ex);
});
} catch (Exception ex) {
Logger.getLogger(ScriptedResource.class.getName()).log(Level.INFO, "{0} - Failed {1}", new Object[]{checkedModuleName(aModuleName), ex.toString()});
}
} else {
// 1
moduleProcess.complete(aModuleName + ".q", null);// instead of qRequire
// 2
moduleProcess.complete(aModuleName + ".s", null);// instead of sRequire
}
// 3
_require(structure.getClientDependencies().toArray(new String[]{}), aCalledFromFile, aSpace, aCyclic, (Void v) -> {
moduleProcess.complete(null, null);
}, (Exception ex) -> {
moduleProcess.complete(null, ex);
});
} catch (Exception ex) {
Logger.getLogger(ScriptedResource.class.getName()).log(Level.INFO, "{0} - Failed {1}", new Object[]{checkedModuleName(aModuleName), ex.toString()});
}
} else {
Exception ex = new FileNotFoundException(aModuleName);
onFailure.accept(ex);
}
};
ModuleStructure cached = scriptsOfModulesStructures.get(aModuleName);
if (cached != null) {
// network activity simulation
aSpace.process(() -> {
withModuleStructure.accept(cached);
});
} else {
try {
app.getModules().getModule(aModuleName, aSpace, withModuleStructure, (Exception ex) -> {
onFailure.accept(ex);
});
} catch (Exception ex) {
Logger.getLogger(ScriptedResource.class.getName()).log(Level.INFO, "{0} - Failed {1}", new Object[]{aModuleName, ex.toString()});
}
}
}
}
private static Object checkedModuleName(String aScriptName) {
return aScriptName != null && !aScriptName.isEmpty() ? aScriptName : "[start]";
}
public static void require(String[] aModulesNames, String aCalledFromFile, JSObject onSuccess, JSObject onFailure) throws Exception {
Scripts.Space space = Scripts.getSpace();
_require(aModulesNames, aCalledFromFile, space, new HashSet<>(), (Void v) -> {
if (onSuccess != null) {
onSuccess.call(null, new Object[]{});
}
}, (Exception ex) -> {
if (onFailure != null) {
onFailure.call(null, new Object[]{ex.getMessage()});
}
});
}
public static void _require(String[] aModulesNames, String aCalledFromFile, Scripts.Space aSpace, Set<String> aCyclic, Consumer<Void> onSuccess, Consumer<Exception> onFailure) throws Exception {
String[] modulesNames = aModulesNames != null ? new HashSet<>(Arrays.asList(aModulesNames)).toArray(new String[]{}) : null;
if (modulesNames != null && modulesNames.length > 0) {
Path apiPath = Scripts.getAbsoluteApiPath();
Path appPath = getAbsoluteAppPath();
RequireProcess process = new RequireProcess(modulesNames.length, (Void v) -> {
aSpace.process(() -> {
onSuccess.accept(v);
});
}, (Exception ex) -> {
aSpace.process(() -> {
onFailure.accept(ex);
});
});
for (String moduleName : modulesNames) {
if (aSpace.getDefined().containsKey(moduleName)) {
process.complete(moduleName, null);
} else if (aCyclic.contains(moduleName)) {
Logger.getLogger(ScriptedResource.class.getName()).log(Level.WARNING, "Cyclic dependency detected: {0}", checkedModuleName(moduleName));
process.complete(moduleName, null);
} else {
aCyclic.add(moduleName);
// add callbacks to pendings
aSpace.pendOn(moduleName, new Scripts.Pending((Void v) -> {
process.complete(moduleName, null);
}, (Exception ex) -> {
process.complete(moduleName, ex);
}));
Logger.getLogger(ScriptedResource.class.getName()).log(Level.INFO, "Loading {0} ...", checkedModuleName(moduleName));
loadScriptOfModule(moduleName, aCalledFromFile, aSpace, aCyclic, apiPath, (Path aScriptFile) -> {
try {
// sync require may occur while pending
if (!aSpace.getDefined().containsKey(moduleName)) {
URL scriptURL = aScriptFile.toUri().toURL();
Set<String> amdNames;
if (!aSpace.getExecuted().containsKey(scriptURL)) {
Path relativeLocalPath;
if (aScriptFile.startsWith(apiPath)) {
relativeLocalPath = apiPath.relativize(aScriptFile);
} else if (aScriptFile.startsWith(appPath)) {
relativeLocalPath = appPath.relativize(aScriptFile);
} else {
relativeLocalPath = aScriptFile;
}
aSpace.exec(relativeLocalPath.toString().replace(File.separator, "/"), scriptURL);
amdNames = aSpace.getExecuted().get(scriptURL);
Collection<Scripts.AmdDefine> amdDefines = aSpace.consumeAmdDefines();
// Amd in action...
for (Scripts.AmdDefine amdDefine : amdDefines) {
assert amdDefine.getModuleName() != null : DEFAULT_MODULE_NAME_ASSERT_MSG;
amdNames.add(amdDefine.getModuleName());
final String amdModuleName = amdDefine.getModuleName();
String[] amdDependencies = amdDefine.getAmdDependencies();
JSObject amdModuleDefiner = amdDefine.getModuleDefiner();
_require(amdDependencies, null, aSpace, new HashSet<>(), (Void v) -> {
try {
amdModuleDefiner.call(null, new Object[]{amdModuleName});
// If module is still not defined because of buggy definer in script,
// we have to put it definition as undefined by hand.
if (!aSpace.getDefined().containsKey(amdModuleName)) {
aSpace.getDefined().put(amdModuleName, null);
}
Logger.getLogger(ScriptedResource.class.getName()).log(Level.INFO, "{0} - Loaded", checkedModuleName(amdModuleName));
aSpace.notifyLoaded(amdModuleName);
} catch (NashornException ex) {
Logger.getLogger(ScriptedResource.class.getName()).log(Level.WARNING, "{0} - Failed {1}", new Object[]{checkedModuleName(amdModuleName), ex.toString()});
aSpace.notifyFailed(amdModuleName, ex);
}
}, (Exception ex) -> {
Logger.getLogger(ScriptedResource.class.getName()).log(Level.WARNING, "{0} - Failed {1}", new Object[]{checkedModuleName(amdModuleName), ex.toString()});
aSpace.notifyFailed(amdModuleName, ex);
});
}
} else {
amdNames = aSpace.getExecuted().get(scriptURL);
}
if (!amdNames.contains(moduleName)) {
Logger.getLogger(ScriptedResource.class.getName()).log(Level.INFO, "{0} - Loaded", checkedModuleName(moduleName));
aSpace.notifyLoaded(moduleName);
// If module is global or it is a plain *.js file, we have to put its definition as undefined
// in AMD structure.
if (!aSpace.getDefined().containsKey(moduleName)) {
aSpace.getDefined().put(moduleName, null);
} else {
Logger.getLogger(ScriptedResource.class.getName()).log(Level.WARNING, "Module {0} is defined multiple times. May be it exists both as AMD module and as a global function.", checkedModuleName(moduleName));
}
}
} else {
aSpace.notifyLoaded(moduleName);
}
} catch (NashornException | ScriptException ex) {
Logger.getLogger(ScriptedResource.class.getName()).log(Level.WARNING, "{0} - Failed {1}", new Object[]{checkedModuleName(moduleName), ex.toString()});
aSpace.notifyFailed(moduleName, ex);
} catch (Exception ex) {
Logger.getLogger(ScriptedResource.class.getName()).log(Level.SEVERE, null, ex);
}
}, (Exception ex) -> {
Logger.getLogger(ScriptedResource.class.getName()).log(Level.WARNING, "{0} - Failed {1}", new Object[]{checkedModuleName(moduleName), ex.toString()});
aSpace.notifyFailed(moduleName, ex);
});
}
}
} else {
aSpace.process(() -> {
onSuccess.accept(null);
});
}
}
private static final String DEFAULT_MODULE_NAME_ASSERT_MSG = "Default module name assumption failed";
public static void require(String[] aModulesNames, String aCalledFromFile) throws Exception {
Scripts.Space space = Scripts.getSpace();
_require(aModulesNames, aCalledFromFile, space, new HashSet<>());
}
public static void _require(String[] aModulesNames, String aCalledFromFile, Scripts.Space aSpace, Set<String> aCyclic) throws Exception {
if (aModulesNames != null && aModulesNames.length > 0) {
Path apiPath = Scripts.getAbsoluteApiPath();
Path appPath = getAbsoluteAppPath();
for (String moduleName : aModulesNames) {
if (!aSpace.getDefined().containsKey(moduleName)) {
if (aCyclic.contains(moduleName)) {
Logger.getLogger(ScriptedResource.class.getName()).log(Level.WARNING, "Cyclic dependency detected: {0}", checkedModuleName(moduleName));
} else {
aCyclic.add(moduleName);
Path apiLocalPath = apiPath.resolve(moduleName + PlatypusFiles.JAVASCRIPT_FILE_END);
if (apiLocalPath != null && apiLocalPath.toFile().exists() && !apiLocalPath.toFile().isDirectory()) {
URL scriptURL = apiLocalPath.toUri().toURL();
aSpace.exec(moduleName, scriptURL);
} else {
ModuleStructure structure = app.getModules().getModule(moduleName, null, null, null);
if (structure != null) {
AppElementFiles files = structure.getParts();
File sourceFile = files.findFileByExtension(PlatypusFiles.JAVASCRIPT_EXTENSION);
URL scriptURL = sourceFile.toURI().toURL();
if (!aSpace.getExecuted().containsKey(scriptURL)) {
if (files.isModule()) {
qRequire(structure.getQueryDependencies().toArray(new String[]{}), null, null, null);
sRequire(structure.getServerDependencies().toArray(new String[]{}), null, null, null);
}
String[] autoDiscoveredDependencies = structure.getClientDependencies().toArray(new String[]{});
_require(autoDiscoveredDependencies, null, aSpace, aCyclic);
Path fileToLoad = Paths.get(scriptURL.toURI());
Path appRelative = appPath.relativize(fileToLoad);
aSpace.exec(appRelative.toString().replace(File.separator, "/"), scriptURL);
}
} else {
throw new FileNotFoundException(moduleName);
}
}
Collection<Scripts.AmdDefine> amdDefines = aSpace.consumeAmdDefines();
Collection<String> amdNames = new HashSet<>();
for (Scripts.AmdDefine amdDefine : amdDefines) {
assert amdDefine.getModuleName() != null : DEFAULT_MODULE_NAME_ASSERT_MSG;
amdNames.add(amdDefine.getModuleName());
final String amdModuleName = amdDefine.getModuleName();
final String[] amdDependencies = amdDefine.getAmdDependencies();
final JSObject amdModuleDefiner = amdDefine.getModuleDefiner();
_require(amdDependencies, null, aSpace, aCyclic);
amdModuleDefiner.call(null, new Object[]{amdModuleName});
// If module is still not defined (buggy definer in script, etc.)
// we have to put it definition as undefined by hand.
if (!aSpace.getDefined().containsKey(amdModuleName)) {
aSpace.getDefined().put(amdModuleName, null);
}
}
// If module is global or it is a plain *.js file, we have to
// put it as undefined in AMD structure.
if (!amdNames.contains(moduleName)) {
if (!aSpace.getDefined().containsKey(moduleName)) {
aSpace.getDefined().put(moduleName, null);
} else {
Logger.getLogger(ScriptedResource.class.getName()).log(Level.WARNING, "Module {0} is defined multiple times. May be it exists both as AMD module and as a global function", checkedModuleName(moduleName));
}
}
}
}
}
}
}
public static Path getAbsoluteAppPath() {
return app.getModules().getLocalPath();
}
protected static void qRequire(String[] aQueriesNames, Scripts.Space aSpace, Consumer<Void> onSuccess, Consumer<Exception> onFailure) throws Exception {
if (onSuccess != null) {
if (aQueriesNames != null && aQueriesNames.length > 0) {
RequireProcess process = new RequireProcess(aQueriesNames.length, onSuccess, onFailure);
for (String queryName : aQueriesNames) {
((QueriesProxy<Query>) app.getQueries()).getQuery(queryName, aSpace, (Query query) -> {
process.complete(queryName, null);
}, (Exception ex) -> {
process.complete(queryName, ex);
});
}
} else {
aSpace.process(() -> {
onSuccess.accept(null);
});
}
} else {
for (String queryName : aQueriesNames) {
app.getQueries().getQuery(queryName, null, null, null);
}
}
}
protected static void sRequire(String[] aModulesNames, Scripts.Space aSpace, Consumer<Void> onSuccess, Consumer<Exception> onFailure) throws Exception {
if (onSuccess != null) {
if (aModulesNames != null && aModulesNames.length > 0) {
RequireProcess process = new RequireProcess(aModulesNames.length, onSuccess, onFailure);
for (String moduleName : aModulesNames) {
app.getServerModules().getServerModuleStructure(moduleName, aSpace, (ServerModuleInfo info) -> {
process.complete(moduleName, null);
}, (Exception ex) -> {
process.complete(moduleName, ex);
});
}
} else {
aSpace.process(() -> {
onSuccess.accept(null);
});
}
} else {
for (String moduleName : aModulesNames) {
app.getServerModules().getServerModuleStructure(moduleName, null, null, null);
}
}
}
}