2013年3月10日日曜日

Netty with html template engine.


最近 playframework やら、twitter の Finagle などが利用されていることで知られる nettyをいろいろ個人的に使っているのですが、netty は基本的に network framework なので基本的には html を生成するような昨日はありません。こういったことはアプリケーションの実装に任されています。playframework などは基盤のevent-drivenのnetworkフレームワークにnettyを利用しながら、httpのframeworkとしてよく考えられていることで人気があります。
playframework を使っても良かったのですが、playframework も設定などはplayframeworkの作法があります。netty だけを使ってアプリケーションは自由に実装したいと思いまずは、html をレンダリングすることからとおもいます。html のテンプレートエンジンは前回のブログで書いた mustache を使おうとおもいます。

まずは、pom.xml
<project 
  xmlns="http://maven.apache.org/POM/4.0.0" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>

 <groupId>com.blogspot.3agiroh.netty.template.html</groupId>
 <artifactId>mastache-template-test</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 <packaging>jar</packaging>

 <name>mastache-template-test</name>
 <url>http://maven.apache.org</url>

 <properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 </properties>

 <build>
  <plugins>
   <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
     <source>1.7</source>
     <target>1.7</target>
     <encoding>UTF-8</encoding>
    </configuration>
   </plugin>
   <plugin>
    <groupId>org.apache.felix</groupId>
    <artifactId>maven-bundle-plugin</artifactId>
    <version>2.3.7</version>
    <extensions>true</extensions>
    <configuration>
     <instructions>
      <!--<Export-Package>*</Export-Package> <Import-Package>*</Import-Package> -->
     </instructions>
    </configuration>
   </plugin>
  </plugins>
 </build>

 <dependencies>
  <!-- mustache template engine. 
  @see https://github.com/spullara/mustache.java -->
  <dependency>
   <groupId>com.github.spullara.mustache.java</groupId>
   <artifactId>compiler</artifactId>
   <version>0.8.10</version>
  </dependency>

  <dependency>
   <groupId>io.netty</groupId>
   <artifactId>netty</artifactId>
   <version>3.6.2.Final</version>
  </dependency>
  <dependency>
   <groupId>org.slf4j</groupId>
   <artifactId>slf4j-api</artifactId>
   <version>1.7.2</version>
  </dependency>
  <dependency>
   <groupId>ch.qos.logback</groupId>
   <artifactId>logback-classic</artifactId>
   <version>1.0.9</version>
  </dependency>
  <dependency>
   <groupId>commons-configuration</groupId>
   <artifactId>commons-configuration</artifactId>
   <version>1.9</version>
  </dependency>
  <dependency>
   <groupId>commons-lang</groupId>
   <artifactId>commons-lang</artifactId>
   <version>2.6</version>
  </dependency>

  <dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>3.8.1</version>
   <scope>test</scope>
  </dependency>
 </dependencies>
</project>

起動のクラスはこんな感じで

package com.blogspot.agiroh.netty.template.html;

import java.net.InetSocketAddress;
import java.util.concurrent.Executors;

import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.jboss.netty.handler.codec.http.HttpChunkAggregator;
import org.jboss.netty.handler.codec.http.HttpContentCompressor;
import org.jboss.netty.handler.codec.http.HttpRequestDecoder;
import org.jboss.netty.handler.codec.http.HttpResponseEncoder;

public class SimpleHttpServer {

 final int port;
 
 public SimpleHttpServer( int port) {
  this.port = port;
 }
 
 public void run() {
  
  ServerBootstrap bootstrap = new ServerBootstrap(
      new NioServerSocketChannelFactory(
        Executors.newCachedThreadPool(), 
        Executors.newCachedThreadPool()));
  
  bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
   
   @Override
   public ChannelPipeline getPipeline() throws Exception {
    
    ChannelPipeline pipeline = Channels.pipeline();
    pipeline.addLast("decoder", new HttpRequestDecoder());
    pipeline.addLast("aggregator", new HttpChunkAggregator(1048576));
    pipeline.addLast("encoder", new HttpResponseEncoder());
    pipeline.addLast("deflater", new HttpContentCompressor());
    pipeline.addLast("htmlrender", new HtmlRenderHandler());
    
    return pipeline;
   }
  });
  
  bootstrap.bind(new InetSocketAddress(port));
  
 }
 
 public static void main(String[] args) {
  int port = 9000;
  new SimpleHttpServer(port).run();
 }
}
で、HTMLを生成するところは mustache java を利用してこんな感じ
package com.blogspot.agiroh.netty.template.html;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBufferOutputStream;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpMessage;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.jboss.netty.handler.codec.http.HttpVersion;
import org.jboss.netty.handler.codec.http.QueryStringDecoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheFactory;

public class HtmlRenderHandler
  extends SimpleChannelUpstreamHandler {

 static Logger log = LoggerFactory.getLogger(HtmlRenderHandler.class);

 public static final int RES_BUFFER_CAPACITY = 65535;

 public static class ExamplePojo {
  String str;
  long num;
  boolean flag;
  Map<String, Object> data;
  List<String> array;
  String escape;
 }

 @Override
 public void messageReceived(
   ChannelHandlerContext ctx, MessageEvent e) throws Exception {
  
  Object msg = e.getMessage();
  
  if (msg instanceof HttpRequest) {

   HttpRequest request = (HttpRequest) msg;

   if (HttpHeaders.is100ContinueExpected(request)) {
    send100Continue(e);
   }

   ExamplePojo pojo = new ExamplePojo();
   pojo.str = "テスト";
   pojo.num = 30;
   pojo.flag = true;

   QueryStringDecoder qsd = new QueryStringDecoder(request.getUri());
   Map<String, List<String>> params = qsd.getParameters();
   pojo.data = new HashMap<String, Object>();
   if (!params.isEmpty()) {
    for (Map.Entry<String, List<String>> entry : params.entrySet())
     pojo.data.put(entry.getKey(), StringUtils.join(entry.getValue(), ","));
   } else {
    pojo.data.put("key1", "value1");
   }

   pojo.array = Arrays.asList("hoge", "fuga");
   pojo.escape = "<p>hogehogehoge</p>";

   render(e, "html/ja/test.mustache", pojo);
   return;
  }
  
  log.debug("[unknown]");
 }

 protected void render(
   MessageEvent e,  // HTTP request event.
   String resource, // mustache template. 
   Object data)   // mustache template bindings.
     throws IOException {
  
  log.debug("[{}] start rendering.", resource);
  
  HttpMessage req = (HttpMessage) e.getMessage();
  HttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);

  MustacheFactory mf = new DefaultMustacheFactory();
  Mustache mustache = mf.compile(resource);

  ChannelBuffer buffer = ChannelBuffers.buffer(RES_BUFFER_CAPACITY);
  ChannelBufferOutputStream out = new ChannelBufferOutputStream(buffer);
  mustache.execute(new PrintWriter(out), data).flush();
  res.setContent(out.buffer());
  try {
   out.close();
  } catch (Exception ee) {}

  res.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/html; charset=utf-8;");

  final boolean keepalive = HttpHeaders.isKeepAlive(req);
  if (keepalive) {
   res.setHeader(HttpHeaders.Names.CONTENT_LENGTH, res.getContent().readableBytes());
   res.setHeader(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
  }

  // Unsupported cookie.

  ChannelFuture future = e.getChannel().write(res);
  if (!keepalive) {
   future.addListener(ChannelFutureListener.CLOSE);
  }

 }

 private static void send100Continue(MessageEvent e) {
  log.info("send 100 continue.");
  HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
  e.getChannel().write(response);
 }
 
 @Override
    public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e)
            throws Exception {
  log.error("Error!! : {}", e.getCause().getMessage());
        e.getCause().printStackTrace();
        e.getChannel().close();
    }
}

そしてテンプレートは以下のようにして実行する。

<!DOCTYPE html>
<html>
<head>
<title>Mustache</title>
<meta charset="UTF-8">
</head>
<body>
 {{!This is comment of Mustache!!}}
 <h1>pojo.str</h1>
 <h2>{{str}}</h2>
 
 <h1>pojo.num</h1>
 <h2>{{num}}</h2>
 
 <h1>pojo.flag</h1>
 {{#flag}}flag = true {{/flag}}
 {{^flag}}flag = false {{/flag}}
 
 <h1>pojo.array</h1>
 {{#array}}
 {{.}}</br>
 {{/array}}
 
 <h1>pojo.data</h1>
 {{#data}}
 <p>upper: {{a}} , {{b}}</p></br>
 {{/data}}
 
 <h1>Escaped Characters</h1>
 {{escape}}
</body>
</html>

http://localhost:9000?a=hogehoge&b=fugafuga こんなアクセスをすると結果こうなりました。きちんと表示されますね。


0 件のコメント:

コメントを投稿