/**
* personium.io
* Copyright 2014 FUJITSU LIMITED
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.fujitsu.dc.engine;
import java.io.Closeable;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import org.apache.http.HttpStatus;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.NativeObject;
import org.mozilla.javascript.Script;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.WrappedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fujitsu.dc.client.utils.DcLoggerFactory;
import com.fujitsu.dc.engine.adapter.DcEngineDao;
import com.fujitsu.dc.engine.adapter.DcRequestBodyStream;
import com.fujitsu.dc.engine.adapter.Require;
import com.fujitsu.dc.engine.extension.support.AbstractExtensionScriptableObject;
import com.fujitsu.dc.engine.extension.support.ExtensionJarLoader;
import com.fujitsu.dc.engine.extension.support.ExtensionLogger;
import com.fujitsu.dc.engine.extension.support.IExtensionLogger;
import com.fujitsu.dc.engine.extension.support.JavaClassRevealFilter;
import com.fujitsu.dc.engine.jsgi.DcResponse;
import com.fujitsu.dc.engine.jsgi.JSGIRequest;
import com.fujitsu.dc.engine.source.ISourceManager;
import com.fujitsu.dc.engine.utils.DcEngineConfig;
import com.fujitsu.dc.engine.utils.DcEngineLoggerFactory;
/**
* DC-Engineのメインクラス.
*/
public class DcEngineContext implements Closeable {
/** ログオブジェクト. */
private static Logger log = LoggerFactory.getLogger(DcEngineContext.class);
private static final String DC_SCOPE = "dc";
private static final String DC_EXTENSION_SCOPE = "extension";
private static Map<String, Script> engineLibCache = new ConcurrentHashMap<String, Script>();
/** Cell名. */
private String currentCellName;
/** Box名. */
private String currentBoxName;
/** データスキーマURI. */
private String currentSchemeUri;
/** RhinoのContext. */
private org.mozilla.javascript.Context cx;
/** Rhino、ContextFactory. */
private DcJsContextFactory factory;
/** Rhino、Scope. */
private Scriptable scope;
/** 基底URL. */
private String baseUrl;
/** ソース情報管理. */
private ISourceManager sourceManager;
static {
ContextFactory.initGlobal(new DcJsContextFactory());
}
/**
* コンストラクタ.
* @throws DcEngineException DcEngine例外
*/
public DcEngineContext() throws DcEngineException {
// Rhinoの実行環境を作成する
this.factory = new DcJsContextFactory();
this.cx = factory.enterContext();
this.scope = cx.initStandardObjects();
}
/**
* Extensionクラスを JavaScriptに公開する.
* この際、ロガークラス実体を Extensionクラス側に設定する。
* @throws DcEngineException 公開失敗時
*/
/**
* @throws DcEngineException
*/
private void prepareExtensionClass() throws DcEngineException {
// Extension用 jarのロード
ExtensionJarLoader extLoader = null;
try {
extLoader = ExtensionJarLoader.getInstance(this.cx.getApplicationClassLoader(),
new JavaClassRevealFilter());
factory.initApplicationClassLoader(extLoader.getClassLoader());
} catch (IOException e) {
throw new DcEngineException("Server Error", DcEngineException.STATUSCODE_SERVER_ERROR, e);
} catch (DcEngineException e) {
throw e;
}
// Javascript内でプロトタイプとして使用可能な Javaクラスを定義する。
// スコープの設定
NativeObject dcScope = (NativeObject) this.scope.get(DC_SCOPE, this.scope);
NativeObject declaringClass = (NativeObject) dcScope.get(DC_EXTENSION_SCOPE, dcScope);
for (Class<? extends Scriptable> clazz : extLoader.getPrototypeClassSet()) {
try {
if (AbstractExtensionScriptableObject.class.isAssignableFrom(clazz)) {
// AbstractExtensionScriptableObjectであれば、ロガー設定を行う。
@SuppressWarnings("unchecked")
Class<? extends AbstractExtensionScriptableObject> extensionClazz
= (Class<? extends AbstractExtensionScriptableObject>) clazz;
// Extensionクラス内で利用可能なロガーを渡す。
// この処理の間に例外が発生しても、何も行わず無視する。(ロガー設定はしないまま正常に動作させる。)
try {
Method setLoggerMethod = extensionClazz.getMethod(
"setLogger",
new Class[] {
Class.class,
IExtensionLogger.class });
setLoggerMethod.setAccessible(true);
setLoggerMethod.invoke(null,
new Object[] {extensionClazz, new ExtensionLogger(extensionClazz)});
} catch (Exception e) {
log.info("setLogger method cannot be called.", e);
}
}
// ############################################################################3
// 以下のメソッドから例外が出力された場合、スクリプト実行の障害となるが、複数の extensionが導入されている場合、
// 問題となる extensionを利用していない UserScriptまで実行できなくなるのを防ぐため、ここからは例外は投げない。
// 問題のプロトタイプにアクセスした場合、Script実行時のエラーとなる。
// ############################################################################3
ScriptableObject.defineClass(declaringClass, clazz);
} catch (RuntimeException e) {
log.warn(String.format("Warn: Extension class(%s) could not be revealed to javascript.: %s",
clazz.getCanonicalName(), e.getMessage()));
} catch (Exception e) {
log.warn(String.format("Warn: Extension class(%s) could not be revealed to javascript.: %s",
clazz.getCanonicalName(), e.getMessage()));
}
}
}
/**
* ソース情報を設定する.
* @param value the ISourceManager
*/
public final void setSourceManager(final ISourceManager value) {
this.sourceManager = value;
}
/**
* グローバルオブジェクトをロード. 過去はグローバルオブジェクトを作成する関数だったが、現状は単なるsetterになっている。
* @param url 基底URL
* @param cell Cell名
* @param scheme データスキーマURI
* @param box Box名
* @param service サービス名
*/
public final void loadGlobalObject(final String url,
final String cell,
final String scheme,
final String box,
final String service) {
this.baseUrl = url;
this.currentCellName = cell;
this.currentBoxName = box;
this.currentSchemeUri = scheme;
}
/**
* JSGIを実行.
* @param source 実行するユーザースクリプト
* @param req Requestオブジェクト
* @param res Responseオブジェクト
* @param is リクエストストリームオブジェクト
* @param serviceSubject サービスサブジェクト
* @return Response
* @throws DcEngineException DcEngine例外
*/
public final Response runJsgi(final String source,
final HttpServletRequest req,
final HttpServletResponse res,
final InputStream is,
final String serviceSubject) throws DcEngineException {
// JSGI実行準備
// DAOオブジェクトを生成
DcEngineDao dc = createDao(req, serviceSubject);
// DAOオブジェクトをJavaScriptプロパティへ設定
javaToJs(dc, "dcjvm");
// RequireオブジェクトをJavaScriptプロパティへ設定
javaToJs(createRequireObject(), "dcrequire");
// dc-dao.js を読み込み
try {
loadJs("dc-dao");
} catch (IOException e1) {
log.info("runJsgi error (DAO load io error) ", e1);
throw new DcEngineException("Server Error", DcEngineException.STATUSCODE_SERVER_ERROR, e1);
}
// dc-lib.js を読み込み
try {
loadJs("dc-lib");
} catch (IOException e1) {
log.info("runJsgi error (dc-lib load io error) ", e1);
throw new DcEngineException("Server Error", DcEngineException.STATUSCODE_SERVER_ERROR, e1);
}
// jsgi-lib.jsを読み込み
try {
loadJs("jsgi-lib");
} catch (IOException e1) {
log.info("runJsgi error (jsgi-lib load io error) ", e1);
throw new DcEngineException("Server Error", DcEngineException.STATUSCODE_SERVER_ERROR, e1);
}
// dc名前空間に、Extensionのクラス群を定義する。
prepareExtensionClass();
// RequestオブジェクトをJavaScriptプロパティへ設定
JSGIRequest dcReq = new JSGIRequest(req, new DcRequestBodyStream(is));
// JSGI実行
// ユーザースクリプトを実行(eval)する
try {
Object ret;
log.info("eval user script : script size = " + source.length());
ret = evalUserScript(source, dcReq);
log.info("[" + DcEngineConfig.getVersion() + "] " + "<<< Request Ended ");
DcResponse dcRes = DcResponse.parseJsgiResponse(ret);
return dcRes.build();
} catch (Error e) {
// ユーザースクリプトのタイムアウトはINFOレベルでログ出力
log.info("UserScript TimeOut", e);
throw new DcEngineException("Script TimeOut", HttpStatus.SC_SERVICE_UNAVAILABLE);
} catch (Exception e) {
if (e instanceof WrappedException) {
e = (Exception) ((WrappedException) e).getWrappedException();
}
// ユーザースクリプト内でのエラーはINFOレベルでログ出力
log.info("User Script Evalucation Error : " + e.getMessage(), e);
throw new DcEngineException("Server Error : " + e.getMessage(), DcEngineException.STATUSCODE_SERVER_ERROR,
e);
}
}
/**
* UserScript実行.
* @param source ユーザースクリプトソース
* @throws IOException IO例外
* @throws DcEngineException DcEngineException
*/
private Object evalUserScript(final String source, JSGIRequest dcReq) throws DcEngineException {
cx.evaluateString(scope, "fn_jsgi = " + source, null, 1, null);
Object fObj = scope.get("fn_jsgi", scope);
Object result = null;
if (!(fObj instanceof Function)) {
log.warn("fn_jsgi not found");
throw new DcEngineException("Server Error", DcEngineException.STATUSCODE_SERVER_ERROR);
}
Object[] functionArgs = {dcReq.getRequestObject() };
Function f = (Function) fObj;
result = f.call(cx, scope, scope, functionArgs);
return result;
}
/**
* DAOオブジェクトを作成.
* @param req Requestオブジェクト
* @param serviceSubject サービスサブジェクト
* @return DAOオブジェクト
*/
private DcEngineDao createDao(final HttpServletRequest req, final String serviceSubject) {
DcEngineLoggerFactory engLogFactory = new DcEngineLoggerFactory();
DcLoggerFactory.setDefaultFactory(engLogFactory);
DcEngineDao dccx = new DcEngineDao(baseUrl, currentCellName, currentSchemeUri, currentBoxName);
dccx.setServiceSubject(serviceSubject);
dccx.setBoxSchema(req.getHeader("X-Dc-Box-Schema"));
String auth = req.getHeader(HttpHeaders.AUTHORIZATION);
String version = req.getHeader(DcEngineDao.DC_VERSION);
if (version != null && !(version.equals(""))) {
dccx.setDcVersion(version);
}
log.debug("auth : --------------------------------------------------------------------------");
log.debug(auth);
if (auth != null && auth.length() > "Bearer".length()) {
dccx.setClientToken(auth.substring("Bearer".length()).trim());
}
return dccx;
}
/**
* Requireオブジェクトを作成.
* @param localPath ローカル実行時のソースパス
* @return 生成したRequireオブジェクト
*/
private Require createRequireObject() {
Require requireComp = new Require(this);
requireComp.setSourceManager(this.sourceManager);
log.debug("RequireObject created");
return requireComp;
}
/**
* JavaScriptファイルを解析し、オブジェクトに登録.
* @param name JavaScriptソース名
* @throws IOException IO例外
*/
private Object loadJs(final String name) throws IOException {
URL path = getClass().getResource("/js-lib/" + name + ".js");
Script jsBuildObject = null;
if (engineLibCache.containsKey(path.toString())) {
jsBuildObject = engineLibCache.get(path.toString());
} else {
FileInputStream fis = new FileInputStream(path.getFile());
InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
jsBuildObject = cx.compileReader(isr, path.getPath(), 1, null);
engineLibCache.put(path.toString(), jsBuildObject);
}
if (jsBuildObject == null) {
return null;
}
Object ret = jsBuildObject.exec(cx, scope);
log.debug("Load JavaScript from Local Resource : " + path);
return ret;
}
/**
* JavaのオブジェクトをJavaScriptオブジェクトに変換.
* @param obj Javaオブジェクト
* @param propertyName JavaScriptオブジェクトの変数名
*/
private void javaToJs(final Object obj, final String propertyName) {
log.debug("JavaObject to JavaScriptProperty " + propertyName);
Object jObj = org.mozilla.javascript.Context.javaToJS(obj, scope);
ScriptableObject.putProperty(scope, propertyName, jObj);
}
/**
* JavaScriptファイルを解析し、オブジェクトに登録.
* @param source JavaScriptソースの中身
* @param path JavaScriptソース名
* @return オブジェクト
*/
public Object requireJs(final String source, final String path) {
Object ret = cx.evaluateString(scope, source, path, 1, null);
log.debug("Load JavaScript from Require Resource : " + path);
return ret;
}
@Override
public void close() throws IOException {
DcJsContext.exit();
}
}