Skip to content
Asynchronous Templates & API Scripting HTTP Server
Scala Other
  1. Scala 97.0%
  2. Other 3.0%
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
docker
project
src/main
.gitignore
LICENSE
README.md
build.sbt
docker-build.bat
docker-build.sh
run.sh
sbt-launch.jar

README.md

HazMat Web Server

Asynchronous HTTP Server with Templates & API Scripting Engine for https://hazm.at/, so called Static Site Generator with on demand.

Quick Start

You can use the Docker image which you can try immediately by just creating the site directory. Place docroot, cache, conf under the directory $SERVER_HOME and NOTE that the cache have to be 777 permission to write temporary data by server.

$ mkdir $SERVER_HOME/cache
$ chmod 777 $SERVER_HOME/cache
$ cat $SITE/docroot/index.html
<html>
...
</html>
$ cat $SERVER_HOME/conf/server.conf
server.bind-address = "0.0.0.0:8088"

$ sudo docker run --rm -it -p 8088:8088 -v $SERVER_HOME:/opt/site torao/hazmat-webserver /opt/site
$ curl http://localhost:8088/index.html
<html>
...
</html>

Introduction

HazMat Web Server はテンプレートエンジンと API スクリプティングの機能を持った Web サーバ。オンデマンドで静的コンテンツを生成する。Apache のような静的な Web サーバと Ruby on Rails のような鈍重なフルスタックフレームワークの中間を埋めることを目的としている。

このサーバは静的コンテンツのレイアウトを統一的に管理することを目的としている。 メテーのサイトを構築するのにタダの HTTP サーバに静的なコンテンツじゃページごとのレイアウトを統一的に管理できなくて超不便じゃん? CMS は便利だがスキームがブログや Wiki なんかに特化してるんで THE 俺様ホームページ向けではないしな。 かといってフルスタック Web フレームワークを持ち込んでも大ナタも甚だしいだろ。

だいたい俺くらいのレベルだとシンプルで素直な XHTML に文章を書いてサーバサイドで XSL 適用するのが一番楽なんだよ。 それに加えてサーヴァサイドの JavaScript で少々 API のようなものが組めればよい。 HazMat Server はそのために実装した特別なサーバだ。

Startup Server

Java 8 の最新版が必要。

$ ./sbt "runMain at.hazm.webserver.Server $SERVER_HOME"

$SERVER_HOME はコンテンツやサーバ設定を記述したディレクトリだ。省略した場合は起動時のカレントディレクトリ . を指定したのと同じ意味。 $SERVER_HOME/docroot がサイト URL のルートとマッピングされるからトップページはここに置いておけばいい。

$SERVER_HOME/
  +- conf/
      +- server.conf
  +- docroot/
      +- index.html
      +- ...
  +-- cache/

$SERVER_HOME/cache にはサーバが一時ファイルを書き込むため多くの場合 777 パーミッションを設定しなければならない。

以下 $SERVER_HOME/docroot$DOCROOT と記す。

テンプレート処理の対象外のファイルは通常の静的ファイルと同様に要求があればそのままクライアントへ送信されるだろう。

Configurations

$SERVER_HOME/conf/server.conf を参照する。構文は Typesafe Config を使用できる。

NAME DESCRIPTION DEFAULT
server.request.timeout リクエストタイムアウト (秒) 30
server.compression-level 圧縮レベル 0
server.max-request-size リクエストの最大サイズ (B) 500
server.bind-address バインドアドレス localhost:80
server.send-buffer-size 送信バッファサイズ (B) 4096
template.update-check-interval ファイル更新確認間隔 (秒) 2
script.timeout スクリプト実行タイムアウト (ミリ秒) 10000
script.extensions サーバサイドスクリプトとして実行するファイルの拡張子 .xjs
script.extensions-java サーバサイド Java として実行するファイルの拡張子 .java
script.libs 外部ライブラリ(複数指定可) ""
redirect: { (regex: url)* } リダイレクト
error: { (regex: path)* } エラーページ

リダイレクト

redirect: {
  "/foo/(.*)": "/bar/$1",  // 302 Temporary Moved
  "/foo/(.*)": "!/bar/$1", // 301 Permanently Moved
}

エラーページ

error: {
  "/foo/.*": "/bar.xsl"
}

外部ライブラリの読み込みはディレクトリ内のサブディレクトリを捜査しすべての *.jar を参照する。

Templates Processing

テンプレート処理の対象かはファイルの拡張子で判断する。 例えば $DOCROOT/foo/bar.xml が存在する状態で /foo/bar.html に要求があったとしよう。 *.xml は XSL 処理の対象で *.html を出力する想定だから、この要求は XSL テンプレートエンジンを起動する。 bar.xml で指定された XSL を適用し、生成された HTML をあたかも bar.html というファイルがあるかのように応答するだろう。

$DOCROOT/foo/bar.html というファイルが存在しているときは bar.xml のテンプレート処理よりも優先されるので注意が必要だ。 また XML も XSL も公開ファイルだということに注意してほしい。例えば /foo/bar.xml で直接 XML を参照することができるだろう。

XSL で変換されたファイルは元の XML や XSL が更新されない限り変化しないためローカルディスク $SERVER_HOME/cache のどこかに保存される。 だからサーバが起動して最初のリクエストは遅いが 2 度目の要求からは静的なファイルを返すのとほぼ同じ速度となるだろう。

Available Template Processing

Source Depends Destination Features
*.xml, *.xhtml *.xml, *.xsl *.html XInclude, XSL
*.ts *.js TypeScript
*.scss, *.sass *.css SCSS
*.svg, *.svgz *.png SVG

XSLT Processing

filename.html にリクエストしたケースを考える。もし docroot 以下に該当する filename.html が存在しない代わりに filename.xml があった場合、サーバは XSL Translation 処理を行った結果を filename.html のレスポンスとして返す。

<?xml version="1.0" encoding="UTF-8"?>
<?xsl-stylesheet type="text/xsl" href="template.xsl"?>
<html>
  <head>
    <title>Welcome My Site</title>
  </head>
  <body>
  <p>This is Sample Site.</p>
  </body>
</html>

以下はこの xhtml をブラウザ表示用に変換する template.xsl だ。

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="/html">
    <html lang="en">
      <xsl:apply-templates/>
    </html>
  </xsl:template>
  <xsl:template match="head">
    <head>
      <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
      <meta charset="utf-8"/>
      <meta name="author" content="your name"/>
      <meta name="viewport" content="width=device-width, initial-scale=1"/>
      <link rel="shortcut icon" href="/favicon.png" type="image/png" />
      <link rel="icon" href="/favicon.png" type="image/png" />
      <link rel="stylesheet" href="/stylesheet/common.css"/>
      <xsl:copy-of select="*" />
    </head>
  </xsl:template>
  <xsl:template match="body">
    <body>
      <nav><img src="/header.png"/></nav>
      <div class="main">
        <xsl:copy-of select="node()"/>
      </div>
      <footer>Copyright &#169; 2017 your name. All Rights Reserved.</footer>
    </body>
  </xsl:template>
</xsl:stylesheet>

このように共通のレイアウトを XSL 側に記述することでコンテンツとなる xhtml の作成に注力することができる。 もちろん元のコンテンツは xhtml である必要はなく独自スキーマをもつ XML でもかまわない。

XSL テンプレートには method, host, uri, path がパラメータとして渡される。ただしこれらのパラメータを使用して HTML を作成した 場合、リクエスト事にその値が変化してもキャッシュが再構築されないことに注意。

DOM Scripting

*.xml または *.xhtml ファイルに対する XSL テンプレートの適用が終わった後の DOM に対して JavaScript を用いて DOM の操作を 行うことができます。これは XSL のみの機能では難しいコンテンツ生成を行うための機能です。例えば下位ディレクトリの全ての ページのインデックスを作成する事ができます。

$SERVER_HOME/scripts に保存されている *.js ファイルが使用されます。複数の JS ファイルが存在する場合はファイル名のソート 順に適用されます。

変数名 意味
doc org.w3c.dom.Document 操作するドキュメント -
docroot String サイトのルートディレクトリ file:///opt/mysite/
location String ドキュメントのURI /moxbox/index.xhtml
context Context ヘルパー関数 -

ヘルパー関数は ScriptProcessor$Context クラスのメソッドが使用できる。

メソッド
findElements(node:(Document or Element), namespace:String, localName:String):Array[Element]
loadXML(url:String):Document
getString(node:Node, xpath:String):String
getBoolean(node:Node, xpath:String):Boolean
getStrings(node:Node, xpath:String):Array[String]

スクリプト実行中に loadXML() したファイルは生成元コンテンツの依存先として登録される。それ以外に依存先として 登録したい URI があればスクリプトの評価結果で配列として返すことで認識される。

Scripts Processing

Getting Started

HazMat サーバで API を実装するには HTML ファイルと同様に JavaScript ファイルを置いておくだけだ。 例えば以下のスクリプトが /api/add.xjs でアクセス可能な場所に置いてあったとする。

(function(){
  var a = parseInt(request.query["a"]);
  var b = parseInt(request.query["b"]);
  return { result: a + b };
})();

このとき、リクエスト GET /api/add.xjs?a=1&b=2{"result":3} を返すだろう。

$ curl "http://$SERVER/api/add.xjs?a=1&b=2"
{"result":3}

このように HazMat サーバのスクリプトは非常に簡単で、スクリプトファイルの評価結果を JSON で返すことに特化している。

サーバがスクリプトファイルをサーバサイドで実行するかはそのファイルの拡張子で判断する。 デフォルトは .xjs だが設定で変更することができるし、もちろんどのようなファイルもサーバサイドで実行しないようにもできる。

Request

スクリプトはグローバル変数 request を使用してリクエスト情報にアクセスすることができる。 どのような情報が渡されているかは以下のコードを試せばよい。

(function(){
  return request;
})();

リクエストパラメータ付きで呼び出された場合、そのクエリーは JS オブジェクトに変換され request.query で参照できる。 例えば a=b&x=y{a:["b"],x:["y"]} のように展開されるため request.query["a"][0]”b” となるだろう。

application/json 形式の POST リクエストはさらに強力だ。 その JSON がそのまま request.query となるため高度に構造化されたデータをスクリプトに渡すことができる。

Response

スクリプトファイルの評価結果がそのまま JSON としてクライアントに送信される。 即時実行関数のスタイルで記述したなら最後の return で返したデータがレスポンスとして送信されると思えばいい。 JSON として有効なデータであれば説明するまでもなくうまく動くが、そうでない場合は若干の訂正を行っている。 例えば単体の文字列や数値 "string" は JSON で定義されていないため要素数1の配列 ["string"] に変換されるし、 レスポンスに紛れ込んだ undefinedNaNnull に置き換えている。 functionDate などのオブジェクトが混じっていた場合は意図した JSON にはならないかもしれないが、いずれも それらは対応しておらず悪いのはオメーだ。

Error Response

文字列リテラルの引用記号がマッチしてないね。

(function(){
  return "hell, world';
})();

スクリプト実行でエラーが発生すると以下の JSON を返している。ファイルと行番号が入っているのでどこの構文エラーかはすぐにわかるだろう。

{
  "error": "script_error",
  "description": "javax.script.ScriptException: /api.xjs:2:23 Missing close quote\r\n  return \"hell, world';\r\n                       ^ in /api.xjs at line number 2 at column number 23"
}

実行時の例外は Java の例外クラス名がそのまま表れるだろう。 エラーレスポンスをカスタマイズしたければ自分で try-catch を使うといい。

Mechanism

HazMat サーバのスクリプトエンジンは Node.js なんかじゃねえ。Java 8 にバンドルされている Nashorn だ。 だから JavaScript の言語機能に加えて Java の機能がフルに使える。 あまり複雑な処理は Reflection の嵐になるのでおすすめしないがどうしても必要なら気合で乗り切れ。

(function(){
  java.lang.println("hello, world");
})();

コンテキストはスクリプトファイルの実行ごとに隔離されているからスクリプト間で直接データの受け渡しすることはできない。 外部スクリプトの読み込み、外部設定、DB やファイルアクセスなどは将来的に (俺の) 需要があれば実装するかもしれない。

External Libraries

JavaScript の実行に外部ライブラリ (JAR) が必要であれば sbt を使うことができる。

まず以下のような build.sbt を作成する。

organization := "at.hazm"

name := "hazmat-contents"

version := "1.0"

scalaVersion := "2.12.3"

resolvers += "CodeLibs Repository" at "http://maven.codelibs.org/"

libraryDependencies += "org.codelibs" % "elasticsearch-analysis-kuromoji-neologd" % "5.+"

retrieveManaged := true

libraryDependencies に必要なライブラリの Maven を指定し、sbt package を実行すれば lib_managed 以下に ライブラリが保存される。

次に設定ファイルの script.libslib/:lib_managed/ のように JAR が保存されたディレクトリを追加する。

Error Processing

$DOCROOT/error/500.html のようなファイルが存在すればそのファイルを表示する。 $DOCROOT/error/error.xsl が存在すればエラー状況を示す XML からレスポンスを動的に生成して返す事ができる。error.xsl が処理するのは以下の XML である。

<error>
  <code>エラーコード</code>
  <phrase>レスポンスフレーズ</phrase>
  <message>エラーメッセージ</message>
</error>

Service Provider Interface

Java の SPI を使用してユーザサイトで変換処理を追加することができる。

Template Engine

テンプレートエンジンはあるファイルの内容を変換するための実装である。例えば SVG として保存されているファイルを PNG でリクエストできるようにすることができる。

at.hazm.webserver.TemplateEngine トレイトを実装し、そのクラス名を META-INF/services/at.hazm.webserver.TemplateEngine に記述する。

Document Processor

ドキュメントプロセッサは XSLT テンプレートエンジンによって生成された DOM を XML レベルで加工することを目的としている。

at.hazm.webserver.templates.xml.DocumentProcessor トレイトを実装し、そのクラス名を META-INF/services/at.hazm.webserver.templates.xml.DocumentProcessor に記述する。

Docker

sbt docker:publishLocal でローカルの docker にイメージを作成する。 Windows の場合は Docker ターミナルから行うこと。また Docker Hub に対して sbt docker:publish を行う場合は先に docker login を行うこと。

You can’t perform that action at this time.