前回は中途半端に終わってしまった、XSLTを使ってXHTMLからJSPへ変換する試みですが、一応その後ある程度できたので、公開しておきます。
別に、万能ツールとして作成したかったわけではありません。変換後のJSPが利用される想定は…
- SAStrutsおよびStrutsのカスタムタグが使える環境
- HTMLの属性からStrutsのHTMLフォームに変換する際には一部読み替えが必要なので、今回はホワイトリスト的に変換する
- taglibディレクティブは、web.xmlのjsp-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("&", "&")) } def toJspFilteredContents(contents: String): String = contents .replaceAll("&", "&") .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の取得/評価にものすごい時間をかけてしまうので、待ってられないためです(笑)。
あと、&を入力時に「&」→「&」と変換し、出力時に「&」→「&」と変換しているのは、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 :<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 © 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 :<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 © Kazuhira, All Right Reserved. </div> </div> </body> </html:html> [success] Total time: 0 s, completed Jul 30, 2011 11:32:42 PM
一応、動いてますよ〜。今回はXSLTのいい勉強になりました♪