waffle
Version:
シンプルなWEBアプリケーションフレームワークです。(ALL YOUR NODE ARE BELONG TO US)
769 lines (688 loc) • 23 kB
JavaScript
/*
* Copyright 2012 Katsunori Koyanagi
*
* 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.
*/
/**
* @overview コア機能を提供します。
*/
"use strict";
require("./utils/patch");
var fs = require("fs");
var lang = require("./utils/Lang");
var Context = require("./core/Context");
var Config = require("./core/Config");
var Deployer = require("./core/Deployer");
var statusCodes = require("http").STATUS_CODES;
var applications = {};
/**
* コアモジュールが含まれます。
*
* @namespace コアモジュールが含まれます。
* @see Waffle.core
*/
// JSDOC用のダミー
var Core = {};
/**
* ユーティリティモジュールが含まれます。
*
* @namespace ユーティリティモジュールが含まれます。
* @see Waffle.utils
*/
// JSDOC用のダミー
var Utils = {};
/**
* 拡張機能を提供するアドオンが含まれます。
*
* @namespace 拡張機能を提供するアドオンが含まれます。
* @see Waffle.extensions
*/
// JSDOC用のダミー
var Extensions = {};
/**
* レンダリング機能を提供するアドオンが含まれます。
* <p>
* レンダラの詳細については、{@link RendererConfig}を参照してください。
* </p>
*
* @see RendererConfig
* @namespace レンダリング機能を提供するアドオンが含まれます。
* @see Waffle.renderers
*/
// JSDOC用のダミー
var Renderers = {};
/**
* フィルタ機能を提供するアドオンが含まれます。
* <p>
* フィルタの詳細については、{@link FilterConfig}を参照してください。
* </p>
*
* @see FilterConfig
* @namespace フィルタ機能を提供するアドオンが含まれます。
* @see Waffle.filters
*/
// JSDOC用のダミー
var Filters = {};
/**
* アプリケーションのクラスです。
*
* @property {String} name アプリケーション名
* @property {Object} descriptor アプリケーションのデスクリプタ
* @property {String} approot アプリケーションのルートディレクトリ
* @class アプリケーションのクラスです。
* @constructor
*/
function Waffle() {
this.__globals__ = {};
this.__globals__.data = {};
}
//
// アプリケーションクラスのスタティックメンバ設定
//
(function() {
/**
* アプリケーションを開始します。
* <p>
* アプリケーションのデスクリプタファイルは、jsonやjsなど、require関数によってオブジェクトとして評価できれば、書式については問われません。
* デスクリプタによって定義されるオブジェクトは、以下の値が使用されます。
* </p>
* <table>
* <tr>
* <th>名前</th>
* <th>必須</th>
* <th>型</th>
* <th>意味</th>
* <th>デフォルト値</th>
* </tr>
* <tr>
* <td>name</td>
* <td>true</td>
* <td>String</td>
* <td>アプリケーション名</td>
* <td></td>
* </tr>
* <tr>
* <td>port</td>
* <td>false</td>
* <td>Number</td>
* <td>リスンするポート番号</td>
* <td>httpなら80、httpsなら443</td>
* </tr>
* <tr>
* <td>secure</td>
* <td>false</td>
* <td>Boolean</td>
* <td>httpsを使用するか</td>
* <td>false</td>
* </tr>
* <tr>
* <td>defaultTimeout</td>
* <td>false</td>
* <td>Number</td>
* <td>標準のタイムアウトを示すミリ秒の時間</td>
* <td>120000(2分)</td>
* </tr>
* <tr>
* <td>approot</td>
* <td>false</td>
* <td>String</td>
* <td>デスクリプタファイルの存在するディレクトリを起点とする相対パス</td>
* <td></td>
* </tr>
* <tr>
* <td>configurator</td>
* <td>false</td>
* <td>String</td>
* <td>アプリケーションの初期化関数のパス(アプリケーションルートを起点とする)</td>
* <td>config</td>
* </tr>
* <tr>
* <td>defaultControllerMethod</td>
* <td>false</td>
* <td>String</td>
* <td>メソッド付きのコントローラ名が指定され、メソッド名からメソッドが解決できない場合のフォールバック先のメソッド名</td>
* <td>index</td>
* </tr>
* <tr>
* <td>data</td>
* <td>false</td>
* <td>Object</td>
* <td>{@link Waffle#data}によって自由に参照可能なオブジェクト</td>
* <td></td>
* </tr>
* </table>
* <p>
* デスクリプタで指定されたコンフィグレータは、引数に{@link Config}を受け取る関数でなければなりません。
* この関数内でレンダラやフィルタ、ルータなどの設定を行なってください。 コンフィグレータが更新されると、自動的に再設定が行われます。
* </p>
*
* @example require("waffle").start("web.json")
* @param {String}
* appconf アプリケーションのデスクリプタのパスになります。相対パスの指定が可能です。
* デスクリプタファイルが見つからない場合や指定されていない場合はエラーとなります。
* @return {Waffle} アプリケーションのインスタンス
*/
Waffle.start = function(appconf) {
return new Waffle().start(appconf);
};
/**
* 実行中のアプリケーションを返します。
*
* @param {String}
* name アプリケーション名
* @return {Waffle} アプリケーションのインスタンス、存在しない場合はundefined
*/
Waffle.getApplication = function(name) {
return applications[name];
};
/**
* このモジュールのpackage.jsonの内容を示すデスクリプタです。
*
* @type Object
*/
Waffle.descriptor = require("../package.json");
/**
* このモジュールのバージョンです。
*
* @type String
*/
Waffle.version = Waffle.descriptor.name + "-" + Waffle.descriptor.version;
/**
* フレームワーク内部で利用されるロガーです。
*
* @type Log
*/
Waffle.log = require("./utils/Log");
/**
* エラーハンドラです。
* <p>
* 設定すると自動でキャッチされない例外をフックすることができます。 関数にはErrorが渡されます。
* nullを設定することで解除することも可能です。
* </p>
*
* @type Function
*/
// JSDOC用のダミーの宣言
Waffle.errorHandler = null;
var handler = null;
Waffle.__defineGetter__("errorHandler", function() {
return handler;
});
Waffle.__defineSetter__("errorHandler", function(errorHandler) {
if (handler) {
if (!errorHandler) {
process.removeListener("uncaughtException", handler);
}
} else {
process.on("uncaughtException", errorHandler);
}
handler = errorHandler;
});
//
// テンポラリの設定
//
var base;
if (process.platform == "win32") {
base = process.env.TEMP || process.env.TMP || "\\tmp";
} else {
base = process.env.TMPDIR || "/tmp";
}
base = fs.join(base, Waffle.descriptor.name + "_tmp");
try {
fs.mkdirSync(base);
Waffle.log("temp directory was created.(%s)", base);
} catch (e) {
}
/**
* 一時作業用のディレクトリの位置を示します。 <br>
* <p>
* 初期状態で存在しない場合には、自動でディレクトリが作成されます。 作成先はプラットフォームの標準のテンポラリディレクトリ以下になります。
* </p>
*
* @type String
*/
Waffle.tmp = base;
/**
* コアモジュールが含まれます。
*
* @type Core
*/
// JSDOC用のダミーの宣言
Waffle.core = null;
/**
* ユーティリティモジュールが含まれます。
*
* @type Utils
*/
// JSDOC用のダミーの宣言
Waffle.utils = null;
/**
* 拡張機能を提供するアドオンが含まれます。
*
* @type Extensions
*/
// JSDOC用のダミーの宣言
Waffle.extensions = null;
/**
* レンダリング機能を提供するアドオンが含まれます。
*
* @type Renderers
*/
// JSDOC用のダミーの宣言
Waffle.renderers = null;
/**
* フィルタ機能を提供するアドオンが含まれます。
*
* @type Filters
*/
// JSDOC用のダミーの宣言
Waffle.filters = null;
//
// モジュールの読み込み
//
function getModuleGetter(filename) {
return function() {
return require(filename);
};
}
function loadModules(category) {
var modules = {};
Waffle.__defineGetter__(category, function(name) {
return modules;
});
var js = /^[a-zA-Z$_][a-zA-Z$_0-9]*\.js$/;
var base = __dirname + "/" + category + "/"
var files = fs.readdirSync(base);
for ( var i = 0, len = files.length; i < len; i++) {
var basename = files[i];
js.lastIndex = 0;
if (!js.test(basename)) {
continue;
}
var name = fs.basename(basename, ".js");
var getter = getModuleGetter(base + files[i]);
modules.__defineGetter__(name, getter);
}
}
loadModules("core");
loadModules("utils");
loadModules("extensions");
loadModules("renderers");
loadModules("filters");
})();
//
// アプリケーションクラスのプロトタイプ設定
//
(function() {
/**
* アプリケーション内で共有されるデータです。名前でアクセスすることができます。
*
* @example app.data.foo = "bar";
* @example console.log(app.data.bar);
* @type Object
*/
// JSDOC用のダミーの宣言
Waffle.prototype.data = null;
Waffle.prototype.__defineGetter__("data", function() {
return this.__globals__.data;
});
/**
* アプリケーションルートを起点とした相対パスでバイナリのリソースを取得して返します。
* <p>
* この関数によって取得されるリソースは内部でキャッシュされます。
* callback関数を省略した場合は同期処理となり、取得されたリソースが返されます。
* callback関数を設定した場合は、通常非同期で第1引数にリソース、第2引数にキャッシュ済みのリソースを取得したのかを示すフラグが渡されます。
* キャッシュ済みの場合は同期処理によるコールバックになります。
* 取得に失敗した場合はnullが返されるか、callback関数にnullが渡されます。
* </p>
*
* @param {String}
* path リソースのパス
* @param {Function}
* callback コールバック関数(省略可能)
* @return バイナリ(コールバック関数が省略された場合)
*/
Waffle.prototype.getFile = function(path, callback) {
var deployer = this.__globals__.deployer;
return deployer.getFile.apply(deployer, arguments);
};
/**
* アプリケーションルートを起点とした相対パスでテキストのリソースを取得して返します。
* <p>
* この関数によって取得されるリソースは内部でキャッシュされます。
* callback関数を省略した場合は同期処理となり、取得されたリソースが返されます。
* callback関数を設定した場合は、通常非同期で第1引数にリソース、第2引数にキャッシュ済みのリソースを取得したのかを示すフラグが渡されます。
* キャッシュ済みの場合は同期処理によるコールバックになります。
* 取得に失敗した場合はnullが返されるか、callback関数にnullが渡されます。
* </p>
*
* @example app.getText("foo/bar.txt", "UTF-8", callback);
* @example app.getText("foo/bar.txt", callback);
* @example var text = app.getText("foo/bar.txt", "UTF-8");
* @example var text = app.getText("foo/bar.txt");
*
* @param {String}
* path リソースのパス
* @param {String}
* encoding エンコーディング(省略可能、省略時はUTF-8とみなされる)
* @param {Function}
* callback コールバック関数(省略可能)
* @return テキスト(コールバック関数が省略された場合)
*/
Waffle.prototype.getText = function(path, encoding, callback) {
var deployer = this.__globals__.deployer;
return deployer.getText.apply(deployer, arguments);
};
/**
* アプリケーションルートを起点とした相対パスで示されるリソースがキャッシュ済みであるかを返します。
*
* @param {String}
* path リソースのパス
* @return {Boolean} キャッシュ済みならtrue
*/
Waffle.prototype.isResourceInCache = function(path) {
var deployer = this.__globals__.deployer;
return deployer.isResourceInCache(path);
};
/**
* アプリケーションルートを起点とした相対パスで多言語メッセージのリソースを取得して返します。
* <p>
* この関数によって取得されるリソースは内部でキャッシュされます。
* </p>
*
* @param {String}
* path リソースのパス
* @return {ResourceBundle} メッセージリソース
*/
Waffle.prototype.getMessages = function(path) {
var deployer = this.__globals__.deployer;
return deployer.getMessages(path);
};
/**
* アプリケーションルートを起点とした相対パスでrequireを行った結果を返します。
* <p>
* 通常はこのフレームワークによって、アプリケーションルートがモジュールの検索パスに加えられます。
* そのため、通常のrequire関数でもアプリケーションルートを起点とした相対パスで、
* 他のモジュールやアプリケーションルート内のモジュールを参照することも可能です。
* </p>
*
* @param {String}
* path モジュールもしくはJSファイル等のパス
* @return {Object} モジュール
* @throws {Error}
* モジュールが見つからない場合
*/
Waffle.prototype.require = function(path) {
return this.__globals__.deployer.require(path);
};
/**
* アプリケーションを開始します。
* <p>
* 既に開始済みの場合はエラーとなり、また、再起動には対応していません。 アプリケーションを再起動したい場合は、一旦停止を行い、
* 新しいアプリケーションのインスタンスを作成して開始を行なってください。
* </p>
*
* @param {String}
* appconf アプリケーションのデスクリプタのパスになります。相対パスの指定が可能です。
* デスクリプタファイルが見つからない場合や指定されていない場合はエラーとなります。
* @param {Object}
* sslOptions SSLを使用する場合のオプション。 詳細は<a
* href="http://nodejs.org/api/tls.html#tls_tls_createserver_options_secureconnectionlistener">
* tls.createServer(options, [secureConnectionListener])</a>を参照してください。
*/
Waffle.prototype.start = function(appconf, sslOptions) {
if (this.__started__) {
throw new Error("already started.");
}
this.__started__ = true;
// アプリケーションのデスクリプタの読み込み
var descriptor;
try {
descriptor = require(appconf);
} catch (e) {
try {
appconf = fs.dirname(process.mainModule.filename) + "/"
+ appconf;
descriptor = require(appconf);
} catch (e2) {
throw e;
}
}
// アプリケーション名の取得
this.name = descriptor.name || appconf;
this.approot = fs.join(fs.dirname(appconf), descriptor.approot);
applications[this.name] = this;
// アプリケーションのデータの設定
var globals = this.__globals__;
globals.descriptor = descriptor;
globals.data.__proto__ = descriptor.data;
// デプロイヤの設定
globals.deployer = new Deployer();
globals.deployer.start(this.approot);
// サーバの起動
var secure = !!descriptor.secure;
var port = secure ? (parseInt(descriptor.port, 10) || 443) : (parseInt(
descriptor.port, 10) || 80);
var http = require(secure ? "https" : "http");
globals.defaultTimeout = descriptor.defaultTimeout;
globals.listener = onRequest.bind(this);
globals.server = secure ? http.createServer(sslOptions,
globals.listener) : http.createServer(globals.listener);
globals.server.listen(port);
Waffle.log("application root is '%s'", this.approot);
Waffle.log("application '%s' started on http%s:%d", this.name,
secure ? "s" : "", port);
return this;
};
/**
* アプリケーションの停止を行います。アプリケーションが開始していない場合は無視されます。
*/
Waffle.prototype.stop = function() {
if (!this.__started__) {
return;
}
var globals = this.__globals__;
globals.deployer.destory();
globals.server.close();
globals.server.removeListener("request", globals.listener);
delete applications[this.name];
};
//
// リクエストを受け付けて、リクエストプロセッサへ移譲
//
function onRequest(req, res) {
var globals = this.__globals__;
var source = globals.descriptor.configurator || "config";
var configrator = this.require(source);
// 初期化されていない場合は初期化を行う
if (!configrator.__configured__) {
configrator.__configured__ = true;
globals.config = new Config(this);
configrator(globals.config);
}
var context = new Context(req, res, this);
var processor = new RequestProcessor(context, globals.config,
globals.defaultTimeout);
processor.process();
}
})();
/**
* リクエストプロセッサのクラスです。このクラスは外部から参照することはできません。
*
* @class リクエストプロセッサのクラスです。
* @constructor
*/
var RequestProcessor = function(context, config, timeout) {
this.context = context;
this.app = config.app;
this.router = config.router;
this.filterMapping = config.filter;
this.renderer = config.renderer;
this.index = 0;
this.pre = true;
this.next0 = this.next.bind(this);
this.controllerCallback0 = this.controllerCallback.bind(this);
this.timeout = timeout;
};
//
// リクエストプロセッサクラスのプロトタイプ設定
//
(function() {
/**
* リクエストの処理を行います。
*/
RequestProcessor.prototype.process = function() {
// コンテキストを開始
this.context.__start__(this.timeout);
// パスを取得
var path = this.context.url.pathname;
// コントローラを検索
var controller = this.router
.getController(this.app, this.context, path);
if (!controller) {
controller = notFound;
}
// フィルタを検索
var filters = controller.filters;
if (filters) {
filters = filters.slice();
}
filters = this.filterMapping.findFilters(path, filters);
// フィルタとコントローラを実行
this.filters = filters;
this.controller = controller;
this.next();
};
/**
* コントローラへ処理の移譲を行います。
*/
RequestProcessor.prototype.dispatchController = function() {
var context = this.context;
this.context.__callback__ = this.controllerCallback0;
this.controller(this.context);
};
/**
* レンダリングを行います。
*/
RequestProcessor.prototype.render = function(view, data) {
var param;
var name;
if (arguments.length === 1) {
data = view;
view = "";
}
var index = view.indexOf(":");
if (index > -1) {
param = view.substring(index + 1);
name = view.substring(0, index);
} else {
index = view.lastIndexOf(".");
if (index > -1) {
name = view.substring(index + 1);
} else {
name = view;
}
param = view;
}
var renderer = this.renderer.getRenderer(name);
if (renderer == null || !renderer.render) {
this.next();
return;
}
this.context.__callback__ = this.controllerCallback0;
renderer.render(this.context, param, data, this.next0);
};
/**
* コントローラからのコールバックです。
*/
RequestProcessor.prototype.controllerCallback = function(result, data,
path, code, error) {
this.controller = null;
switch (result) {
case "dispatch":
this.controller = this.router.getController(this.app, this.context,
path);
break;
case "error":
if (!this.__error__) {
this.context.res.statusCode = code;
this.__error__ = true;
if (error == null) {
error = code + " " + statusCodes[code];
}
this.context.errorInfo = error;
this.controller = this.router.getController(this.app,
this.context, code);
if (this.controller == null && error != null) {
Waffle.log.error(error);
}
}
break;
case "redirect":
this.context.res.writeHead(302, {
Location : path
});
break;
case "render":
this.render(path, data);
return;
}
this.next();
};
/**
* フィルタを実行します。
*/
RequestProcessor.prototype.next = function() {
var filter = this.filters == null ? null : this.filters[this.index];
if (filter != null) {
if (this.pre) {
this.index++;
if (filter.pre) {
filter.pre(this.context, this.next0);
} else {
this.next();
}
} else {
this.index--;
if (filter.post) {
filter.post(this.context, this.next0);
} else {
this.next();
}
}
return;
}
if (this.pre) {
if (this.controller) {
this.dispatchController();
return;
}
this.pre = false;
this.index--;
this.next();
return;
}
this.context.res.end();
this.context.__close__();
};
//
// 404を返すだけのダミーのコントローラ
//
function notFound(context) {
context.error(404, "404 Not Found(path=" + context.req.url + ")");
}
})();
//
// expose
//
module.exports = Waffle;