CLOVER🍀

That was when it all began.

続・XHTMLからJSPへの変換

前回は中途半端に終わってしまった、XSLTを使ってXHTMLからJSPへ変換する試みですが、一応その後ある程度できたので、公開しておきます。

別に、万能ツールとして作成したかったわけではありません。変換後のJSPが利用される想定は…

  • SAStrutsおよびStrutsのカスタムタグが使える環境
  • HTMLの属性からStrutsのHTMLフォームに変換する際には一部読み替えが必要なので、今回はホワイトリスト的に変換する
  • taglibディレクティブは、web.xmljsp-configで一括でインクルードし、個々のJSPには書かない
  • できる限り、XHTML形式で記述

んで、一応ある程度満足いくまで書いたXSLスタイルシートがこちら。

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
		xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
		xmlns:xhtml="http://www.w3.org/1999/xhtml">

	<xsl:output
			method="xml"
			indent="yes"
			encoding="UTF-8"
			omit-xml-declaration="yes"
			doctype-public="-//W3C//DTD XHTML 1.0 Transitional//EN"
			doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" />

	<xsl:template match="/">
		<xsl:text disable-output-escaping="yes"><![CDATA[<%@page pageEncoding="UTF-8"%>
]]></xsl:text>
		<xsl:apply-templates />
	</xsl:template>

	<!-- scriptタグの展開抑止  -->
	<xsl:template match="xhtml:script">
		<xsl:text disable-output-escaping="yes"><![CDATA[<script ]]></xsl:text>

		<xsl:for-each select="@*"><xsl:value-of select="name()" />="<xsl:value-of select="." />" </xsl:for-each>

		<xsl:text disable-output-escaping="yes"><![CDATA[>]]></xsl:text>

		<xsl:if test="text()"><xsl:value-of select="text" /></xsl:if>

		<xsl:text disable-output-escaping="yes"><![CDATA[</script>]]></xsl:text>
	</xsl:template>

	<!-- エラーメッセージテンプレート -->
	<xsl:template match="xhtml:li[@class='error']">
		<xsl:text disable-output-escaping="yes"><![CDATA[<html:errors />]]></xsl:text>
	</xsl:template>


	<!-- htmlタグテンプレート -->
	<!-- プログラム内で変換  -->

	<!-- formタグテンプレート  -->
	<xsl:template match="xhtml:form">
		<xsl:text disable-output-escaping="yes"><![CDATA[<s:form ]]></xsl:text>

		<xsl:if test="@method">method="<xsl:value-of select="@method" />" </xsl:if>
		<xsl:if test="@action">action="<xsl:value-of select="@action" />" </xsl:if>
		<xsl:if test="@class">styleClass="<xsl:value-of select="@class" />" </xsl:if>

		<xsl:text disable-output-escaping="yes"><![CDATA[>]]></xsl:text>

		<xsl:apply-templates />

		<xsl:text disable-output-escaping="yes"><![CDATA[</s:form>]]></xsl:text>
	</xsl:template>

	<!-- aタグテンプレート  -->
	<xsl:template match="xhtml:a">
		<xsl:text disable-output-escaping="yes"><![CDATA[<s:link ]]></xsl:text>

		<xsl:if test="@href">href="<xsl:value-of select="@href" />" </xsl:if>

		<xsl:choose>
			<xsl:when test="node()">
				<xsl:text disable-output-escaping="yes"><![CDATA[>]]></xsl:text>
				<xsl:apply-templates />
				<xsl:text disable-output-escaping="yes"><![CDATA[</s:link>]]></xsl:text>
			</xsl:when>
			<xsl:otherwise>
				<xsl:text disable-output-escaping="yes"><![CDATA[ />]]></xsl:text>
			</xsl:otherwise>
		</xsl:choose>
	</xsl:template>

	<!-- ******************** 各種HTMLフォームテンプレート ******************** -->
	<!-- submit -->
	<xsl:template match="xhtml:input[@type='submit']">
		<xsl:text disable-output-escaping="yes"><![CDATA[<s:submit ]]></xsl:text>

		<xsl:if test="@class">styleClass="<xsl:value-of select="@class" />" </xsl:if>
		<xsl:if test="@disabled">disabled="true" </xsl:if>
		<xsl:if test="@value">value="<xsl:value-of select="@value" />" </xsl:if>
		<xsl:if test="@onclick">onclick="<xsl:value-of select="@onclick" />" </xsl:if>

		<xsl:text disable-output-escaping="yes"><![CDATA[ />]]></xsl:text>
	</xsl:template>

	<!-- text -->
	<xsl:template match="xhtml:input[@type='text']">
		<xsl:text disable-output-escaping="yes"><![CDATA[<html:text ]]></xsl:text>

		<xsl:if test="@class">styleClass="<xsl:value-of select="@class" />" </xsl:if>
		<xsl:if test="@disabled">disabled="true" </xsl:if>
		<xsl:if test="true()">property="<xsl:value-of select="@name" />" </xsl:if>
		<!-- <xsl:if test="@name">property="<xsl:value-of select="@name" />" </xsl:if> -->
		<xsl:if test="@value">value="<xsl:value-of select="@value" />" </xsl:if>
		<xsl:if test="@title">title="<xsl:value-of select="@title" />" </xsl:if>
		<xsl:if test="@size">size="<xsl:value-of select="@size" />" </xsl:if>
		<xsl:if test="@maxlength">maxlength="<xsl:value-of select="@maxlength" />" </xsl:if>

		<xsl:text disable-output-escaping="yes"><![CDATA[ />]]></xsl:text>
	</xsl:template>

	<!-- radio -->
	<xsl:template match="xhtml:input[@type='radio']">
		<xsl:text disable-output-escaping="yes"><![CDATA[<html:radio ]]></xsl:text>

		<xsl:if test="@class">styleClass="<xsl:value-of select="@class" />" </xsl:if>
		<xsl:if test="@disabled">disabled="true" </xsl:if>
		<xsl:if test="true()">property="<xsl:value-of select="@name" />" </xsl:if>
		<!-- <xsl:if test="@name">property="<xsl:value-of select="@name" />" </xsl:if> -->
		<xsl:if test="true()">value="<xsl:value-of select="@value" />" </xsl:if>

		<xsl:text disable-output-escaping="yes"><![CDATA[ />]]></xsl:text>
	</xsl:template>

	<!-- checkbox -->
	<xsl:template match="xhtml:input[@type='checkbox']">
		<xsl:variable name="name" select="@name" />

		<xsl:choose>
			<xsl:when test="preceding-sibling::xhtml:input[@type='checkbox' and @name=$name]">
				<xsl:text disable-output-escaping="yes"><![CDATA[<html:multibox ]]></xsl:text>
			</xsl:when>
			<xsl:when test="following-sibling::xhtml:input[@type='checkbox' and @name=$name]">
				<xsl:text disable-output-escaping="yes"><![CDATA[<html:multibox ]]></xsl:text>
			</xsl:when>
			<xsl:otherwise>
				<xsl:text disable-output-escaping="yes"><![CDATA[<html:checkbox ]]></xsl:text>
			</xsl:otherwise>
		</xsl:choose>

		<xsl:if test="@class">styleClass="<xsl:value-of select="@class" />" </xsl:if>
		<xsl:if test="@disabled">disabled="true" </xsl:if>
		<xsl:if test="true()">property="<xsl:value-of select="@name" />" </xsl:if>
		<!-- <xsl:if test="@name">property="<xsl:value-of select="@name" />" </xsl:if> -->
		<xsl:if test="true()">value="<xsl:value-of select="@value" />" </xsl:if>

		<xsl:text disable-output-escaping="yes"><![CDATA[ />]]></xsl:text>
	</xsl:template>

	<!-- file -->
	<xsl:template match="xhtml:input[@type='file']">
		<xsl:text disable-output-escaping="yes"><![CDATA[<html:file ]]></xsl:text>

		<xsl:if test="@class">styleClass="<xsl:value-of select="@class" />" </xsl:if>
		<xsl:if test="@disabled">disabled="true" </xsl:if>
		<xsl:if test="true()">property="<xsl:value-of select="@name" />" </xsl:if>
		<!-- <xsl:if test="@name">property="<xsl:value-of select="@name" />" </xsl:if> -->
		<xsl:if test="@size">size="<xsl:value-of select="@size" />" </xsl:if>

		<xsl:text disable-output-escaping="yes"><![CDATA[ />]]></xsl:text>
	</xsl:template>

	<!-- reset -->
	<xsl:template match="xhtml:input[@type='reset']">
		<xsl:text disable-output-escaping="yes"><![CDATA[<html:reset ]]></xsl:text>

		<xsl:if test="@class">styleClass="<xsl:value-of select="@class" />" </xsl:if>
		<xsl:if test="@value">value="<xsl:value-of select="@value" />" </xsl:if>
		<xsl:if test="@onclick">onclick="<xsl:value-of select="@onclick" />" </xsl:if>

		<xsl:text disable-output-escaping="yes"><![CDATA[ />]]></xsl:text>
	</xsl:template>

	<!-- button -->
	<xsl:template match="xhtml:input[@type='button']">
		<xsl:text disable-output-escaping="yes"><![CDATA[<html:button ]]></xsl:text>

		<xsl:if test="@class">styleClass="<xsl:value-of select="@class" />" </xsl:if>
		<xsl:if test="@name">property="<xsl:value-of select="@name" />" </xsl:if>
		<xsl:if test="@value">value="<xsl:value-of select="@value" />" </xsl:if>
		<xsl:if test="@onclick">onclick="<xsl:value-of select="@onclick" />" </xsl:if>

		<xsl:text disable-output-escaping="yes"><![CDATA[ />]]></xsl:text>
	</xsl:template>

	<!-- select/option -->
	<xsl:template match="xhtml:select">
		<xsl:text disable-output-escaping="yes"><![CDATA[<html:select ]]></xsl:text>

		<xsl:if test="@id">styleId="<xsl:value-of select="@id" />" </xsl:if>
		<xsl:if test="true()">property="<xsl:value-of select="@name" />" </xsl:if>
		<!-- <xsl:if test="@name">property="<xsl:value-of select="@name" />" </xsl:if> -->
		<xsl:if test="@size">size="<xsl:value-of select="@size" />" </xsl:if>
		<xsl:if test="@title">title="<xsl:value-of select="@title" />" </xsl:if>
		<xsl:if test="@onchange">onchange="<xsl:value-of select="@onchange" />" </xsl:if>

		<xsl:text disable-output-escaping="yes"><![CDATA[ >]]></xsl:text>

		<xsl:for-each select="xhtml:option">
			<xsl:text disable-output-escaping="yes"><![CDATA[<html:option ]]></xsl:text>
				<xsl:if test="@value">value="<xsl:value-of select="@value" />"</xsl:if>
			<xsl:text disable-output-escaping="yes"><![CDATA[ >]]></xsl:text>

			<xsl:value-of select="text()" />

			<xsl:text disable-output-escaping="yes"><![CDATA[</html:option>]]></xsl:text>
		</xsl:for-each>

		<xsl:text disable-output-escaping="yes"><![CDATA[</html:select>]]></xsl:text>
	</xsl:template>

	<!-- textarea  -->
	<xsl:template match="xhtml:textarea">
		<xsl:text disable-output-escaping="yes"><![CDATA[<html:textarea ]]></xsl:text>

		<xsl:if test="true()">property="<xsl:value-of select="@name" />" </xsl:if>
		<!-- <xsl:if test="@name">property="<xsl:value-of select="@name" />" </xsl:if> -->
		<xsl:if test="@cols">cols="<xsl:value-of select="@cols" />" </xsl:if>
		<xsl:if test="@rows">rows="<xsl:value-of select="@rows" />" </xsl:if>
		<xsl:if test="@disabled">disabled="true" </xsl:if>
		<xsl:if test="text()">value="<xsl:value-of select="text()" />"</xsl:if>

		<xsl:text disable-output-escaping="yes"><![CDATA[ />]]></xsl:text>
	</xsl:template>
	<!-- ******************** 各種HTMLフォームテンプレート ******************** -->


	<!-- デフォルトテンプレート(全要素・属性をコピー)  -->
	<xsl:template match="node()|@*">
		<xsl:copy>
			<xsl:apply-templates select="node()|@*" />
		</xsl:copy>
	</xsl:template>
</xsl:stylesheet>

う〜ん、長い…。

そして、これを使う際のScalaプログラムがこちら。

import scala.io.Source

import java.io.{Reader, StringReader, StringWriter}
import javax.xml.transform.TransformerFactory
import javax.xml.transform.stream.{StreamResult, StreamSource}

import ResourceHandler._

object XhtmlToJspConverter {
  val XHTML_TRANSITIONAL_DOCTYPE: String = """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">"""

  def main(args: Array[String]): Unit = {
    val inputXhtmlPath = args(0)
    val stylesheetPath = args(1)

    val writer = new StringWriter
    val transformerFactory = TransformerFactory.newInstance
    val transformer = transformerFactory.newTransformer(new StreamSource(stylesheetPath))
    val transformXml = new StreamResult(writer)

    transformer.transform(new StreamSource(toFilteredReader(inputXhtmlPath)), transformXml)

    val xml = toJspFilteredContents(writer.toString)
    println(xml)
  }
 
  def toFilteredReader(filePath: String, encoding: String = "UTF-8"): Reader = {
    val contents = using(Source.fromFile(filePath, encoding))(_.addString(new StringBuilder).toString)
    new StringReader(contents
		     .replaceAll(XHTML_TRANSITIONAL_DOCTYPE + "\\r?\\n?", "")
                     .replaceAll("&", "&amp;"))
  }

 def toJspFilteredContents(contents: String): String = contents
                                                       .replaceAll("&amp;", "&")
                                                       .replaceAll("/>", " />")
                                                       .replaceAll(" +/>", " />")
                                                       .replaceAll(" +>", ">")
                                                       .replaceAll("<html [^>]+>", "<html:html xhtml=\"true\" lang=\"ja\">")
                                                       .replaceAll("</html>", "</html:html>")
}

object ResourceHandler {
  type Closeable = { def close(): Unit }

  def using[A <: Closeable, B](resource: A)(body: A => B): B = {
    try {
      body(resource)
    } finally {
      if (resource != null) resource.close()
    }
  }
}

なんかDOCTYPEを消したりしていますが、これはXMLパーサがDOCTYPEの取得/評価にものすごい時間をかけてしまうので、待ってられないためです(笑)。

あと、&を入力時に「&」→「&amp;」と変換し、出力時に「&amp;」→「&」と変換しているのは、HTMLで使える実体参照が解決できなくてもエラーとならないようにするためです。これはDOCTYPEを削った弊害とも言えますが、かといって削らなかった場合は実体が展開されてしまい、普通の文字になってしまうので、それはそれでよろしくなかろうと。

では、実行。今回のスケープゴート

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta name="description" content="" />
    <meta name="keyword" content="" />
    <link rel="stylesheet" type="text/css" href="css/site.css" media="screen, projection" />
    <title>Little Wings</title>
  </head>
  <body>
    <div id="container">
      <div id="header">
	<h1><a href="/">Little Wings</a></h1>
	<p>〜since 2005.05.03〜</p>
      </div>
      <form action="/action/execute" method="post">
	ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:<input type="text" name="id" size="10" /><br />
	FirstName:<input type="text" name="firstName" size="15" /><br />
	LastName:<input type="text" name="lastName" size="15" /><br />
	<br />
	<input type="submit" value="送信" />
     </form>
      <div id="wrapper">
      </div>
      <div id="footer">
	Copyright &copy; Kazuhira, All Right Reserved.
      </div>
    </div>
  </body>
</html>

実行結果はこちら。

> run sample-contents.html xhtml-to-jsp.xsl
[info] Running XhtmlToJspConverter sample-contents.html xhtml-to-jsp.xsl
<%@page pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html:html xhtml="true" lang="ja">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta name="description" content="" />
    <meta name="keyword" content="" />
    <link rel="stylesheet" type="text/css" href="css/site.css" media="screen, projection" />
    <title>Little Wings</title>
  </head>
  <body>
    <div id="container">
      <div id="header">
	<h1><s:link href="/">Little Wings</s:link></h1>
	<p>〜since 2005.05.03〜</p>
      </div>
      <s:form method="post" action="/action/execute">
	ID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:<html:text property="id" size="10" /><br />
	FirstName:<html:text property="firstName" size="15" /><br />
	LastName:<html:text property="lastName" size="15" /><br />
	<br />
	<s:submit value="送信" />
     </s:form>
      <div id="wrapper">
      </div>
      <div id="footer">
	Copyright &copy; Kazuhira, All Right Reserved.
      </div>
    </div>
  </body>
</html:html>

[success] Total time: 0 s, completed Jul 30, 2011 11:32:42 PM

一応、動いてますよ〜。今回はXSLTのいい勉強になりました♪