前回はCloudbitSubscriptionServlet
クラスに、
-
doPost
メソッド - littleBits社のクラウドから呼び出され、受け取ったデータをDBに格納する(ので人が直接結果を見ることはない) -
doGet
メソッド - WebブラウザでURL指定時に呼び出され、DB中のデータを表示する(ので人が目視で確認できる)
の枠を用意しておきました。
さて、cloudBitから受け取ったデータはCloudbitEvent
オブジェクトに格納できるように準備しましたが、DBへのデータの格納、およびDBからのデータの取り出しのために、CloudbitEventDAO
クラスを定義します。
public class CloudbitEventDAO {
protected DataSource dataSource;
public CloudbitEventDAO(DataSource newDataSource) throws SQLException {
setDataSource(newDataSource);
}
public DataSource getDataSource() {
return dataSource;
}
}
コンストラクタでアクセス先のDBのDataSource
オブジェクトを格納しておくようにします。setDataSource()
メソッドの中を見てみましょう。
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(HCP)を使う際に、これから使用しようとしているテーブルがどのスキーマに属しているのかをset schema文で設定しています。その後に、checkTable()
メソッドでCloudbitEvent
オブジェクトの格納先となるテーブルの有無を確認します。
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マニュアル)。
さて、データをDBに格納するために、addRow()
メソッドを用意します。引数にはデータを格納したCloudbitEvent
オブジェクトを指定します。
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件までにしてあります)。
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
クラスを利用するための準備をします。
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しているデータソースに関する情報を追加しておきます。
<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()
メソッドで行うようにしています。
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()
メソッドです。
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()
メソッドで最後の</body></html>を出力する直前に、以下の部分を追加します。
try {
appendTableData(request, response);
} catch (Exception e) {
writer.println(
"Database operation(GET) failed with reason: " +
e.getMessage());
}
実際に出力を行うのは、appendTableData()
メソッドです。
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プロジェクトを構築しました。さらに今回使った内容に関しては、3-CloudBit_Postブランチにて、動作・参照可能なソースコードを公開させて頂いております。
それでは続きはまたにしましょう!