HCPとcloudBitでIoT - 4.受け取ったデータをDBに格納

[前回](http://qiita.com/mfunaki/items/6b5b5f70dd8c87f43a88)は``CloudbitSubscriptionServlet``クラスに、

* ``doPost``メソッド - littleBits社のクラウドから呼び出され、受け取ったデータをDBに格納する(ので人が直接結果を見ることはない)
* ``doGet``メソッド - WebブラウザでURL指定時に呼び出され、DB中のデータを表示する(ので人が目視で確認できる)

の枠を用意しておきました。

さて、cloudBitから受け取ったデータは``CloudbitEvent``オブジェクトに格納できるように準備しましたが、DBへのデータの格納、およびDBからのデータの取り出しのために、``CloudbitEventDAO``クラスを定義します。

```Java
public class CloudbitEventDAO {
	protected DataSource dataSource;

	public CloudbitEventDAO(DataSource newDataSource) throws SQLException {
		setDataSource(newDataSource);
	}

	public DataSource getDataSource() {
		return dataSource;
	}
}
```

コンストラクタでアクセス先のDBの``DataSource``オブジェクトを格納しておくようにします。``setDataSource()``メソッドの中を見てみましょう。

```Java
	public void setDataSource(DataSource newDataSource) throws SQLException {
		this.dataSource = newDataSource;
		Connection connection = dataSource.getConnection();
		try {
			PreparedStatement pstmt = connection.prepareCall(
					"set schema NEO_XXXXXXXXXXXXXXXXXXXXXXXXX");
			pstmt.execute();
		} finally {
			if (connection != null) {
				connection.close();
			}
		}
		checkTable();
	}
```

ここでは、インスタンス変数``dataSource``に引数で渡されたオブジェクトを格納しておくだけでなく、前回も触れさせていただいた[SAP HANA Cloud Platform](http://hcp.sap.com/)(HCP)を使う際に、これから使用しようとしているテーブルがどのスキーマに属しているのかをset schema文で設定しています。その後に、``checkTable()``メソッドで``CloudbitEvent``オブジェクトの格納先となるテーブルの有無を確認します。

```Java
	void checkTable() throws SQLException {
		Connection connection = null;
		try {
			connection = dataSource.getConnection();
			if (!existsTable(connection)) {
				createTable(connection);	// wouldn't be called
			}
		} finally {
			if (connection != null) {
				connection.close();
			}
		}
	}

	protected boolean existsTable(Connection conn) throws SQLException {
		String tableName = "EVENTS";
		DatabaseMetaData meta = conn.getMetaData();
		ResultSet rs = meta.getTables(null, null, tableName, null);
		while (rs.next()) {
			String name = rs.getString("TABLE_NAME");
			if (name.equals(tableName)) {
				return true;
			}
		}
		return false;
	}

	void createTable(Connection conn) throws SQLException {
		PreparedStatement pstmt = conn.prepareStatement(
				"CREATE COLUMN TABLE EVENTS (" +
				"EVENT_ID   varchar(255)," +
				"EVENT_TYPE varchar(255)," +
				"EVENT_TIMESTAMP bigint," +
				"EVENT_USERID integer," +
				"EVENT_BITID  varchar(255)," +
				"PAYLOAD_ABSOLUTE integer," +
				"PAYLOAD_PERCENT integer," +
				"PAYLOAD_LEVEL varchar(255)," +
				"PAYLOAD_DELTA varchar(255)," +
				"PRIMARY KEY (EVENT_ID))");
		pstmt.execute();
		pstmt.close();
	}
```

``existsTable()``メソッドで格納先テーブル(EVENTS)の有無を確認し、まだ存在していないようであれば、``createTable()``メソッドでEVENTSテーブルを新規に作成しています。テーブルの存在確認、およびテーブルの新規作成(``createTable()``メソッド内のCREATE TABLE文)に関しては、HCP以外の環境では使用するDBに合わせて修正が必要となるでしょう。CREATE TABLE文にCOLUMNが入っているのは、インメモリ＋カラム型テーブルを使用するためです([CREATE TABLE](http://help.sap.com/saphelp_hanaplatform/helpdata/en/20/d58a5f75191014b2fe92141b7df228/frameset.htm)マニュアル)。

さて、データをDBに格納するために、``addRow()``メソッドを用意します。引数にはデータを格納した``CloudbitEvent``オブジェクトを指定します。

```Java
	public void addRow(CloudbitEvent data) throws SQLException {
		CloudbitEvent eventData = (CloudbitEvent) data;
		Connection connection = dataSource.getConnection();
		try {
			PreparedStatement pstmt = connection.prepareCall(
					"INSERT INTO EVENTS " + 
					"(EVENT_ID,EVENT_TYPE,EVENT_TIMESTAMP,EVENT_USERID,EVENT_BITID," +
					"PAYLOAD_ABSOLUTE,PAYLOAD_PERCENT,PAYLOAD_LEVEL,PAYLOAD_DELTA) " +
					"VALUES(?,?,?,?,?,?,?,?,?)");
			pstmt.setString(1, UUID.randomUUID().toString());
			pstmt.setString(2, eventData.getEventType());
			pstmt.setLong(3, eventData.getEventTimestamp());
			pstmt.setInt(4, eventData.getEventUserId());
			pstmt.setString(5, eventData.getEventBitId());
			pstmt.setInt(6,  eventData.getPayloadAbsolute());
			pstmt.setInt(7, eventData.getPayloadPercent());
			pstmt.setString(8,  eventData.getPayloadLevel());
			pstmt.setString(9, eventData.getPayloadDelta());
			pstmt.executeUpdate();
			pstmt.close();
		} finally {
			if (connection != null) {
				connection.close();
			}
		}
	}
```

一方、データの取り出しには、``selectAllRows()``を使います(Allとは言いつつも、ここでは直近のものから最新100件までにしてあります)。

```Java
	public List<CloudbitEvent> selectAllRows() throws SQLException {
		Connection connection = dataSource.getConnection();
		try {
			PreparedStatement pstmt = connection.prepareStatement(
					"SELECT EVENT_ID,EVENT_TYPE,EVENT_TIMESTAMP,EVENT_USERID,EVENT_BITID," +
					"PAYLOAD_ABSOLUTE,PAYLOAD_PERCENT,PAYLOAD_LEVEL,PAYLOAD_DELTA " +
					"FROM EVENTS ORDER BY EVENT_TIMESTAMP DESC");
			ResultSet rs = pstmt.executeQuery();
			ArrayList<CloudbitEvent> list = new ArrayList<CloudbitEvent>();
			int count = 0;
			while (rs.next() && count++ < 100) {
				CloudbitEvent p = new CloudbitEvent();
				p.setEventId(rs.getString(1));
				p.setEventType(rs.getString(2));
				p.setEventTimestamp(rs.getInt(3));
				p.setEventUserId(rs.getInt(4));
				p.setEventBitId(rs.getString(5));
				p.setPayloadAbsolute(rs.getInt(6));
				p.setPayloadPercent(rs.getInt(7));
				p.setPayloadLevel(rs.getString(8));
				p.setPayloadDelta(rs.getString(9));
				list.add(p);
			}
			pstmt.close();
			return list;
		} finally {
			if (connection != null) {
				connection.close();
			}
		}
	}
```

ここまで、どちらかというとDBアクセスまわりの力技系の実装を見てきました。ここから、前回に枠組みを作っておいた``CloudbitSubscriptionServlet``クラスに移りましょう。まずは、先ほど実装した``CloudbitEventDAO``クラスを利用するための準備をします。

```Java
	private CloudbitEventDAO cloudbitEventDAO;
      
	public void init() throws ServletException {
		super.init();
		try {
			InitialContext ctx = new InitialContext();
			DataSource dataSource = (DataSource) ctx
					.lookup("java:comp/env/jdbc/DefaultDB");
			cloudbitEventDAO = new CloudbitEventDAO(dataSource);
		} catch (SQLException e) {
			throw new ServletException(e);
		} catch (NamingException e) {
			throw new ServletException(e);
		}
	}
```

また、web.xmlファイルの中にlookupしているデータソースに関する情報を追加しておきます。

```XML
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
  <resource-ref>
    <res-ref-name>jdbc/DefaultDB</res-ref-name>
    <res-type>javax.sql.DataSource</res-type>
  </resource-ref>
</web-app>
```

HCP以外の環境で使用する場合には、``DataSource``オブジェクトの取得に関しては、修正が必要になりかもしれません。いずれにせよ、これ以降、メンバ変数``cloudbitEventDAO``を経由してDBへのアクセスが可能になりました。

次に、``doPost()``メソッド、および``doGet()``メソッドの中身を見て行きましょう。

まずは、littleBitsクラウドからの呼び出しを受ける``doPost()``メソッドです。前回までの時点では何もしない実装のままでしたが、ここでは、実際の処理は``doAdd()``メソッドで行うようにしています。

```Java
	protected void doPost(HttpServletRequest request, 
			HttpServletResponse response) throws ServletException, IOException {
		try {
			doAdd(request);
		} catch (Exception e) {
			response.getWriter().println(
					"Database operation(POST) failed with reason: " +
					e.getMessage());
			LOGGER.error("Database operation(POST) failed", e);
		}
	}
```

さて、実際に処理を行う``doAdd()``メソッドです。

```Java
	private static final String JSON_TYPE = "type";
	private static final String JSON_TIMESTAMP = "timestamp";
	private static final String JSON_USERID = "user_id";
	private static final String JSON_BITID = "bit_id";
	private static final String JSON_PAYLOAD = "payload";
	private static final String JSON_ABSOLUTE = "absolute";
	private static final String JSON_PERCENT = "percent";
	private static final String JSON_LEVEL = "level";
	private static final String JSON_DELTA = "delta";
	
	private void doAdd(HttpServletRequest request) throws ServletException, IOException, SQLException {
		BufferedReader reader = new BufferedReader(request.getReader());
		StringBuffer body = new StringBuffer();
		String line;
		
		while ((line = reader.readLine()) != null) {
			body.append(line);
		}
		
		CloudbitEvent eventData = new CloudbitEvent();
		try {
			JSONObject json = new JSONObject(body.toString());
			if (json.has(JSON_TYPE)) {
				eventData.setEventType(json.getString(JSON_TYPE));
			}
			if (json.has(JSON_TIMESTAMP)) {
				eventData.setEventTimestamp(json.getInt(JSON_TIMESTAMP));
			}
			if (json.has(JSON_USERID)) {
				eventData.setEventUserId(json.getInt(JSON_USERID));
			}
			if (json.has(JSON_BITID)) {
				eventData.setEventBitId(json.getString(JSON_BITID));
			}
			if (json.has(JSON_PAYLOAD)) {
				JSONObject json2 = json.getJSONObject(JSON_PAYLOAD);
				if (json2.has(JSON_ABSOLUTE)) {
					eventData.setPayloadAbsolute(json2.getInt(JSON_ABSOLUTE));
				}
				if (json2.has(JSON_PERCENT)) {
					eventData.setPayloadPercent(json2.getInt(JSON_PERCENT));
				}
				if (json2.has(JSON_LEVEL)) {
					eventData.setPayloadLevel(json2.getString(JSON_LEVEL));
				}
				if (json2.has(JSON_DELTA)) {
					eventData.setPayloadDelta(json2.getString(JSON_DELTA));
				}
			}
		} catch (Exception e) {
			new IOException("JSON parse exception: " + e.getMessage());
		}
		cloudbitEventDAO.addRow(eventData);
	}
```

ソースコード自体は長いものの、内部的にはlittleBitsクラウドから通知されたJSON形式のデータを解析しながら``CloudbitEvent``オブジェクトに設定していき、最後に先ほど紹介した``CloudbitEventDAO``クラスの``addRow()``メソッドでDBにINSERTするという単純なものです。

なお、JSON形式のデータの解析には、json.orgで公開されているパーザを使用しています。今回の範囲であれば、``JSONArray``, ``JSONException``, ``JSONObject``, ``JSONString``, ``JSONTokener``の5クラスがあれば用が足ります。

さて、受け取ったデータの表示の方に移りましょう。

``doGet()``メソッドで最後の&lt;/body&gt;&lt;/html&gt;を出力する直前に、以下の部分を追加します。

```Java
		try {
			appendTableData(request, response);
		} catch (Exception e) {
			writer.println(
					"Database operation(GET) failed with reason: " +
							e.getMessage());
		}
```

実際に出力を行うのは、``appendTableData()``メソッドです。

```Java
	void appendTableData(HttpServletRequest request, HttpServletResponse response) 
			throws SQLException, IOException {
		PrintWriter writer = response.getWriter();
		
		List<CloudbitEvent> resultList = cloudbitEventDAO.selectAllRows();
		writer.println(
				"<table><tbody><tr><th colspan=\"6\">" +
				(resultList.isEmpty() ? "No" : resultList.size()) +
				" Record(s) in HCP</th></tr>");
		writer.println("<tr><th>Timestamp</th>" +
				"<th>Label</th><th>Absolute</th><th>Percent</th><th>Level</th><th>Delta</th></tr>");
		IXSSEncoder xssEncoder = XSSEncoder.getInstance();
		for (CloudbitEvent p : resultList) {
			Date date = new Date(p.getEventTimestamp());
			String eventBitId = p.getEventBitId();
			// Timestamp
			writer.println("<tr><td>" +
					xssEncoder.encodeHTML(date.toString()) +
					"</td>");
			// Label
			String label = "(" + eventBitId + ")";
			writer.println("<td>" + xssEncoder.encodeHTML(label) + "</td>");
			// Absolute
			writer.println("<td>" +
					p.getPayloadAbsolute() +
					"</td>");
			// Percent
			writer.println("<td>" +
					p.getPayloadPercent() +
					"</td>");
			// Level
			writer.println("<td>" + 
					xssEncoder.encodeHTML(p.getPayloadLevel()) +
					"</td>");
			// Delta
			writer.println("<td>" + 
					xssEncoder.encodeHTML(p.getPayloadDelta()) + 
					"</td></tr>");
		}
		writer.println("</tbody></table>");
	}
```

DBから取得したデータをテーブル形式で表示するだけの単純な内容ですが、ここでは、Stringデータを出力する際には、HCPのSDKが提供するIXSSEncoderを使って、不正なデータが原因となってXSS(クロスサイトスクリプティング)が発生しないようにフィルタリングしています。

最後に動作確認です。現時点ではcloudBitから送られてきたデータをDBに格納してはいないものの、前回と同様、ブラウザ上で``doGet()``メソッドの出力画面が見られれば(別の言い方をすれば、DBアクセスに問題がなければ)動作検証できた、ということにしておきましょう。

例によって、ソースコードに関しては、github上で[cloudbit-javaプロジェクト](https://github.com/mfunaki/cloudbit-java)を構築しました。さらに今回使った内容に関しては、[3-CloudBit_Postブランチ](https://github.com/mfunaki/cloudbit-java/tree/3-CloudBit_Post)にて、動作・参照可能なソースコードを公開させて頂いております。

それでは続きはまたにしましょう！

0. [はじめに](http://qiita.com/mfunaki/items/cc3f6210bd0f72d34e54)
1. [cloudBitの情報取得](http://qiita.com/mfunaki/items/a11c32456cc3c1066fd9)
2. [cloudBitからの情報受け取り口](http://qiita.com/mfunaki/items/6b5b5f70dd8c87f43a88)
3. [受け取ったデータをDBに格納](http://qiita.com/mfunaki/items/f9ede4f2fdcf25b5ed25)