GAE/Jではてなカウンター集計ツールを作りました

Google App Engine for Javaで「はてなカウンター集計ツール」を作りました。

目次

  • 概要
  • ツールのURL
  • 使い方
  • ソースコード
    • LoginServlet.java
    • SelectServlet.java
    • ResultServlet.java
    • SummarizeServlet.java
    • HatenaCounterAccess.java(※記事中に納まらないので、後日再掲します。)
  • 集計結果のサンプル

概要

はてなカウンター集計ツールは、はてなカウンターから月毎のアクセス数を取得し、集計するツールです。
本ツールで生成したブログ貼り付け用コードをブログへコピペすれば、毎月のアクセス数のまとめを簡単に行えます。

ツールのURL

はてなカウンター集計ツールは以下のURLで利用できます。
https://summarize-hatena-counter.appspot.com/

使い方

1.ログイン画面でログイン情報を入力します。

はてなカウンターの利用時に使用するはてなIDとパスワードを入力します。
入力が完了したら、ログインボタンを押します。

2.集計条件入力画面で条件を入力し、集計ボタンを押します。
  1. 集計対象のカウンターを選択します。
  2. 集計対象の年/月を選択します。
  3. 必要に応じて集計結果のタイトルから除外したい文字列を指定します。たとえば、本ブログではタイトルの末尾に必ず「 - 何かしらの言語による記述を解析する日記」が付くので、これを除外文字列に指定しています。

条件の入力が完了したら、集計ボタンを押します。

3.集計中画面が表示されるので、しばらく待ちます。

集計処理に5〜10分かかります。ひょっとするともっとかかります。
また、集計途中で500エラーが表示されることがあります。その場合は、F5キーを押せば集計中画面が復活して処理が継続すると思います。

4.集計結果

集計結果が表示されます。

以下は集計結果のプレビューです。

以下は集計結果のブログ貼り付け用コードです。上段がはてなダイアリー貼り付け用のコード、下段がその他ブログ等貼り付け用コードです。

ソースコード

はてなカウンター集計ツールのソースコードです。

LoginServlet.java

ログイン画面のサーブレットです。

package shc;

import java.io.IOException;
import java.util.*;
import java.util.logging.*;
import java.util.regex.*;
import javax.servlet.http.*;

/**
 * ログイン画面
 * @author nattou_curry
 */
public class LoginServlet extends HttpServlet {

	// ロガー
	private static Logger logger = Logger.getLogger( LoginServlet.class.getName() );

	/**
	 * GETリクエスト時の処理
	 * @param req HTTPリクエスト
	 * @param resp HTTPレスポンス
	 */
	public void doGet( HttpServletRequest req, HttpServletResponse resp ) throws IOException {

		try {
			// ログイン画面を出力する。
			printHTML( req, resp );

		} catch ( Exception e ) {
			logger.log( Level.SEVERE, "エラーが発生しました。", e );

			resp.setContentType("text/html; charset=UTF-8");
			resp.getWriter().println( "エラーが発生しました。" );
		}
	}

	/**
	 * POSTリクエスト時の処理
	 * @param req HTTPリクエスト
	 * @param resp HTTPレスポンス
	 */
	public void doPost( HttpServletRequest req, HttpServletResponse resp ) throws IOException {

		try {
			// エラーメッセージの一覧
			List<String> errorList = new ArrayList<String>();

			//////////////////////////////////////////////////////////////////
			// リクエストパラメータから入力値を取得する。
			//////////////////////////////////////////////////////////////////
			
			// はてなIDを取得する。
			String name_in = req.getParameter( "name" );
			// パスワードを取得する。
			String password_in = req.getParameter( "password" );
	
			//////////////////////////////////////////////////////////////////
			// 入力値の妥当性を確認する。 
			//////////////////////////////////////////////////////////////////
			
			Pattern p;
			Matcher m;
			
			// はてなIDの妥当性を確認する。 
			p = Pattern.compile( "[a-zA-Z][a-zA-Z0-9_-]{1,30}[a-zA-Z0-9]" );
			m = p.matcher( name_in );
			if ( ! m.matches() ) {
				// はてなIDが妥当でない:
				
				// エラーメッセージと共にログイン画面を出力する。
				errorList.add( "はてなIDが不正です。" );
				printHTML( req, resp, errorList );
				return;
			}

			// パスワードの妥当性を確認する。 
			p = Pattern.compile( "[a-zA-Z0-9]{5,}" );
			m = p.matcher( password_in );
			if ( ! m.matches() ) {
				// パスワードが妥当でない:
				
				// エラーメッセージと共にログイン画面を出力する。
				errorList.add( "パスワードが不正です。" );
				printHTML( req, resp, errorList );
				return;
			}

			//////////////////////////////////////////////////////////////////
			// はてなカウンターにログインする。
			//////////////////////////////////////////////////////////////////
	
			Map<String, String> loginInfo;
			try {

				// ログインする(SSL)。
				loginInfo = HatenaCounterAccess.loginWithSSL( name_in, password_in );

			} catch ( Exception e ) {
				// ログインに失敗:
				logger.log( Level.SEVERE, "ログインに失敗しました。", e );

				// エラーメッセージと共にログイン画面を出力する。
				errorList.add( "ログインに失敗しました。" );
				printHTML( req, resp, errorList );
				return;
			}
			
			//////////////////////////////////////////////////////////////////
			// 入力値をセッションに格納する。
			//////////////////////////////////////////////////////////////////
			
			// セッションを取得する。
			HttpSession session = req.getSession( true );

			// はてなIDをセッションに格納する。
			session.setAttribute( "name", name_in );
			// パスワードをセッションに格納する。
			session.setAttribute( "password", password_in );

			//////////////////////////////////////////////////////////////////
			// 集約条件入力画面にリダイレクトする。
			//////////////////////////////////////////////////////////////////
			
			resp.sendRedirect( req.getContextPath() + "/Select" );

		} catch ( Exception e ) {
			logger.log( Level.SEVERE, "エラーが発生しました。", e );

			resp.setContentType("text/html; charset=UTF-8");
			resp.getWriter().println( "エラーが発生しました。" );
		}
	}

	/**
	 * ログイン画面を出力する。(エラーメッセージなし)
	 * @param req HTTPリクエスト
	 * @param resp HTTPレスポンス
	 */
	private void printHTML( HttpServletRequest req, HttpServletResponse resp ) throws IOException {
		printHTML( req, resp, null );
	}

	/**
	 * ログイン画面を出力する。(エラーメッセージあり)
	 * @param req HTTPリクエスト
	 * @param resp HTTPレスポンス
	 * @param errroList エラーメッセージの一覧
	 */
	private void printHTML( HttpServletRequest req, HttpServletResponse resp, List<String> errorList ) throws IOException {

		//////////////////////////////////////////////////////////////////
		// リクエストパラメータから入力値を取得する。
		//////////////////////////////////////////////////////////////////
		
		// リクエストパラメータからはてなIDを取得する。
		String name_in = req.getParameter( "name" );

		if ( name_in == null ) {
			// はてなIDを取得できない:
			name_in = "";
		}

		//////////////////////////////////////////////////////////////////
		// エラーメッセージを作成する。
		//////////////////////////////////////////////////////////////////

		StringBuffer errorBuf = new StringBuffer();

		if ( errorList != null && errorList.size() > 0 ) {
			errorBuf.append( "<font color='red'>" );
			errorBuf.append( "<ul>" );
			for ( int i = 0; i < errorList.size(); ++i ) {
				String error = errorList.get( i );
				errorBuf.append( "<li>" + error + "</li>" );
			}
			errorBuf.append( "</ul>" );
			errorBuf.append( "</font>" );
		}

		String errorMsg = errorBuf.toString();
		
		//////////////////////////////////////////////////////////////////
		// HTMLを作成する。
		//////////////////////////////////////////////////////////////////
		
		// 自サーブレットのURL
		String selfPath = req.getContextPath() + req.getServletPath();

		StringBuffer htmlBuf = new StringBuffer();
		htmlBuf.append( "<html>" );
		htmlBuf.append( "<head>" );
		htmlBuf.append( "<meta http-equiv='content-type' content='text/html; charset=utf-8'/>" );
		htmlBuf.append( "<link rel='stylesheet' href='/style/base.css' type='text/css' />" );
		htmlBuf.append( "<title>ログイン - はてなカウンター集計ツール</title>" );
		htmlBuf.append( "</head>" );
		htmlBuf.append( "<body>" );

		htmlBuf.append( "<a href='/'><img src='/img/title.png' alt='はてなカウンター集計ツール' border='0'></a>" );
		htmlBuf.append( "<hr>" );

		htmlBuf.append( "<h1>ログイン</h1>" );
		htmlBuf.append( errorMsg );
		htmlBuf.append( "はてなカウンターのログイン情報を入力してください。<br>" );
		htmlBuf.append( "<br>" );
		htmlBuf.append( "<form action='" + selfPath + "' method='post'>" );
		htmlBuf.append( "<table>" );
		htmlBuf.append( "<tr>" );
		htmlBuf.append( "<td>はてなID: </td><td><input type='text' name='name' value='" + name_in + "'></td>" );
		htmlBuf.append( "</tr>" );
		htmlBuf.append( "<tr>" );
		htmlBuf.append( "<td>パスワード: </td><td><input type='password' name='password'></td>" );
		htmlBuf.append( "</tr>" );
		htmlBuf.append( "<tr>" );
		htmlBuf.append( "<td>&nbsp;</td><td><input type='submit' value='ログイン'</td>" );
		htmlBuf.append( "</tr>" );
		htmlBuf.append( "<table>" );
		htmlBuf.append( "</form>" );

		htmlBuf.append( "<hr>" );
		htmlBuf.append( "<address>Copyright (c) 2010 <a href='http://d.hatena.ne.jp/nattou_curry_2'>id:nattou_curry_2</a> (<a href='http://twitter.com/nattou_curry'>@nattou_curry</a>)" );

		htmlBuf.append( "</body>" );
		htmlBuf.append( "</html>" );

		String html = htmlBuf.toString();

		//////////////////////////////////////////////////////////////////
		// HTMLを出力する。
		//////////////////////////////////////////////////////////////////
			
		resp.setContentType("text/html; charset=UTF-8");
		resp.getWriter().println( html );
	}
}
SelectServlet.java

集計条件画面のサーブレットです。

package shc;

import com.google.appengine.api.datastore.*;
import com.google.appengine.api.labs.taskqueue.*;
import java.io.IOException;
import java.text.*;
import java.util.*;
import java.util.logging.*;
import java.util.regex.*;
import javax.servlet.http.*;

/**
 * 集計条件画面
 * @author nattou_curry
 */
public class SelectServlet extends HttpServlet {

	// ロガー
	private static Logger logger = Logger.getLogger( SelectServlet.class.getName() );

	/**
	 * GETリクエスト時の処理
	 * @param req HTTPリクエスト
	 * @param resp HTTPレスポンス
	 */
	public void doGet( HttpServletRequest req, HttpServletResponse resp ) throws IOException {

		try {

			// 集計条件が面を出力する。
			printHTML( req, resp );

		} catch ( Exception e ) {
			logger.log( Level.SEVERE, "エラーが発生しました。", e );

			resp.setContentType("text/html; charset=UTF-8");
			resp.getWriter().println( "エラーが発生しました。" );
		}
	}

	/**
	 * POSTリクエスト時の処理
	 * @param req HTTPリクエスト
	 * @param resp HTTPレスポンス
	 */
	public void doPost( HttpServletRequest req, HttpServletResponse resp ) throws IOException {

		try {

			// エラーメッセージの一覧
			List<String> errorList = new ArrayList<String>();

			//////////////////////////////////////////////////////////////////
			// リクエストパラメータから入力値を取得する。
			//////////////////////////////////////////////////////////////////
	
			// カウンターIDを取得する。
			String cid = req.getParameter( "cid" );
			// 年/月を取得する。
			String yearmonth = req.getParameter( "yearmonth" );
			// タイトルから除外する文字列を取得する。
			String exclude_from_title = req.getParameter( "exclude_from_title" );
			
			//////////////////////////////////////////////////////////////////
			// 入力値の妥当性を確認する。 
			//////////////////////////////////////////////////////////////////
			
			// カウンターIDがnullでないこと確認する。 
			if ( cid == null ) {
				// カウンターIDがnull:
				
				// エラーメッセージと共にログイン画面を出力する。
				errorList.add( "カウンターを選択してください。" );
				printHTML( req, resp, errorList );
				return;
			}

			// カウンターIDの妥当性を確認する。 
			Pattern p = Pattern.compile( "[0-9]{1,2}" );
			Matcher m = p.matcher( cid );
			if ( ! m.matches() ) {
				// カウンターIDが妥当でない:
				
				// エラーメッセージと共にログイン画面を出力する。
				errorList.add( "カウンターIDが不正です。" );
				printHTML( req, resp, errorList );
				return;
			}

			// 年/月の妥当性を確認する。 
			try {
				SimpleDateFormat sdf_yyyyMM = new SimpleDateFormat( "yyyy/MM" );
				sdf_yyyyMM.parse( yearmonth );
			} catch ( ParseException e ) {
				// 日付解析できない:
				
				// エラーメッセージと共にログイン画面を出力する。
				errorList.add( "年/月が不正です。" );
				printHTML( req, resp, errorList );
				return;
			}

			//////////////////////////////////////////////////////////////////
			// セッションから値を取得する。
			//////////////////////////////////////////////////////////////////
			
			// セッションを取得する。
			HttpSession session = req.getSession( true );
	
			// セッションからはてなIDを取得する。
			String name = (String) session.getAttribute( "name" );
			// セッションからパスワードを取得する。
			String password = (String) session.getAttribute( "password" );
	
			//////////////////////////////////////////////////////////////////
			// 年/月を年と月に分割する。
			//////////////////////////////////////////////////////////////////
			
			// 年/月の文字列をDate型に変換する。
			SimpleDateFormat sdf_yyyyMM = new SimpleDateFormat( "yyyy/MM" );
			Date dt_yearmonth = sdf_yyyyMM.parse( yearmonth );
				
			// 1ヶ月前のDate型を求める。
			Calendar cal_prev = Calendar.getInstance();
			cal_prev.setTime( dt_yearmonth );
			cal_prev.add( Calendar.MONTH, -1 );
			Date dt_prev = cal_prev.getTime();
			
			// Date型から年を取得する。
			SimpleDateFormat sdf_yyyy = new SimpleDateFormat( "yyyy" );
			String year = sdf_yyyy.format( dt_yearmonth );
			String prevYear = sdf_yyyy.format( dt_prev );

			// Date型から月を取得する。
			SimpleDateFormat sdf_MM = new SimpleDateFormat( "MM" );
			String month = sdf_MM.format( dt_yearmonth );
			String prevMonth = sdf_MM.format( dt_prev );;
		
			//////////////////////////////////////////////////////////////////
			// データストアに集計情報を格納する。
			//////////////////////////////////////////////////////////////////
			
			// データストアサービスを取得する。
			DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

			// 集計情報用エンティティを作成する。
			Entity entity = new Entity( "Summary" );
			entity.setProperty( "name", name );					// はてなID
			entity.setProperty( "cid", cid );					// カウンターID
			entity.setProperty( "year", year );					// 年
			entity.setProperty( "month", month );					// 月
			entity.setProperty( "prevYear", prevYear );				// 前月の年
			entity.setProperty( "prevMonth", prevMonth );				// 前月の月
			entity.setProperty( "exclude_from_title", exclude_from_title );		// タイトルから除外する文字列
			entity.setProperty( "password", password );				// パスワード
			entity.setProperty( "status", 0 );					// 集計状況
			entity.setProperty( "result_H", new Text( "" ) );			// 集計結果(その他ブログ用)
			entity.setProperty( "result_D", new Text( "" ) );			// 集計結果(はてなダイアリー用)
			entity.setProperty( "update_time", new Date() );			// 最終更新日時

			// エンティティをデータストアに格納する。
			Key key = datastore.put( entity );

			// エンティティのキーを文字列に変換する。
			String keyString = KeyFactory.keyToString( key );

			//////////////////////////////////////////////////////////////////
			// タスクキューに集計処理を追加する。
			//////////////////////////////////////////////////////////////////
			
			// タスクキューを取得する。
			com.google.appengine.api.labs.taskqueue.Queue queue = QueueFactory.getDefaultQueue();

			// 集計処理を作成する。
			TaskOptions options = TaskOptions.Builder
						.url( "/Summarize" )
						.param( "keyString", keyString );

			// タスクキューに集計処理を追加する。
			queue.add( options );

			//////////////////////////////////////////////////////////////////
			// 集計結果表示画面にリダイレクトする。
			//////////////////////////////////////////////////////////////////
			
			resp.sendRedirect( req.getContextPath() + "/Result/" + keyString );

		} catch ( Exception e ) {
			logger.log( Level.SEVERE, "エラーが発生しました。", e );

			resp.setContentType("text/html; charset=UTF-8");
			resp.getWriter().println( "エラーが発生しました。" );
		}
	}

	/**
	 * ログイン画面を出力する。(エラーメッセージなし)
	 * @param req HTTPリクエスト
	 * @param resp HTTPレスポンス
	 */
	private void printHTML( HttpServletRequest req, HttpServletResponse resp ) throws Exception {
		printHTML( req, resp, null );
	}

	/**
	 * ログイン画面を出力する。(エラーメッセージあり)
	 * @param req HTTPリクエスト
	 * @param resp HTTPレスポンス
	 * @param errroList エラーメッセージの一覧
	 */
	private void printHTML( HttpServletRequest req, HttpServletResponse resp, List<String> errorList ) throws Exception {

		//////////////////////////////////////////////////////////////////
		// セッションから値を取得する。
		//////////////////////////////////////////////////////////////////
		
		// セッションを取得する。
		HttpSession session = req.getSession( true );

		// セッションからはてなIDを取得する。
		String name_in = (String) session.getAttribute( "name" );
		// セッションからパスワードを取得する。
		String password_in = (String) session.getAttribute( "password" );

		//////////////////////////////////////////////////////////////////
		// リクエストパラメータから入力値を取得する。
		//////////////////////////////////////////////////////////////////

		// カウンターIDを取得する。
		String in_cid = req.getParameter( "cid" );
		if ( in_cid == null ) {
			in_cid = "";
		}

		// 年/月を取得する。
		String in_yearmonth = req.getParameter( "yearmonth" );
		if ( in_yearmonth == null ) {
			in_yearmonth = "";
		}

		// タイトルから除外する文字列を取得する。
		String exclude_from_title = req.getParameter( "exclude_from_title" );
		if ( exclude_from_title == null ) {
			exclude_from_title = "";
		}
			
		//////////////////////////////////////////////////////////////////
		// エラーメッセージを作成する。
		//////////////////////////////////////////////////////////////////

		StringBuffer errorBuf = new StringBuffer();

		if ( errorList != null && errorList.size() > 0 ) {
			errorBuf.append( "<font color='red'>" );
			errorBuf.append( "<ul>" );
			for ( int i = 0; i < errorList.size(); ++i ) {
				String error = errorList.get( i );
				errorBuf.append( "<li>" + error + "</li>" );
			}
			errorBuf.append( "</ul>" );
			errorBuf.append( "</font>" );
		}

		String errorMsg = errorBuf.toString();
		
		//////////////////////////////////////////////////////////////////
		// はてなカウンターにログインする。
		//////////////////////////////////////////////////////////////////

		// ログインする(SSL)。
		Map<String, String> loginInfo;
		try {
			loginInfo = HatenaCounterAccess.loginWithSSL( name_in, password_in );
		} catch ( Exception e ) {
			// ログインに失敗:
			logger.log( Level.SEVERE, "ログインに失敗しました。", e );

			resp.setContentType("text/html; charset=UTF-8");
			resp.getWriter().println( "ログインに失敗しました。" );
			return;
		}

		//////////////////////////////////////////////////////////////////
		// カウンターの一覧を取得する。
		//////////////////////////////////////////////////////////////////

		// カウンターの一覧を取得する。
		List<Map<String, String>> counterList = HatenaCounterAccess.getCounterList( loginInfo );
	
		//////////////////////////////////////////////////////////////////
		// 年月の一覧を作成する。
		//////////////////////////////////////////////////////////////////
		
		SimpleDateFormat sdf_yyyyMM = new SimpleDateFormat( "yyyy/MM" );
		List<String> yearmonthList = new ArrayList<String>();

		Calendar calTo = Calendar.getInstance();
		calTo.set( Calendar.YEAR, 2000 );
		calTo.set( Calendar.MONTH, 1 );
		calTo.set( Calendar.DAY_OF_MONTH, 1 );

		Calendar cal = Calendar.getInstance();
		while ( cal.after( calTo ) ) {
			Date date = cal.getTime();
			String yearmonth = sdf_yyyyMM.format( date );
			yearmonthList.add( yearmonth );
		
			cal.add( Calendar.MONTH, -1 );
		}
		
		//////////////////////////////////////////////////////////////////
		// HTMLを作成する。
		//////////////////////////////////////////////////////////////////
		
		// 自サーブレットのURL
		String selfPath = req.getContextPath() + req.getServletPath();

		StringBuffer htmlBuf = new StringBuffer();
		htmlBuf.append( "<html>" );
		htmlBuf.append( "<head>" );
		htmlBuf.append( "<meta http-equiv='content-type' content='text/html; charset=utf-8'/>" );
		htmlBuf.append( "<link rel='stylesheet' href='/style/base.css' type='text/css' />" );
		htmlBuf.append( "<title>集計条件入力 - はてなカウンター集計ツール</title>" );
		htmlBuf.append( errorMsg );
		htmlBuf.append( "</head>" );
		htmlBuf.append( "<body>" );

		htmlBuf.append( "<a href='/'><img src='/img/title.png' alt='はてなカウンター集計ツール' border='0'></a>" );
		htmlBuf.append( "<hr>" );

		htmlBuf.append( "<h1>集計条件入力</h1>" );
		htmlBuf.append( "集計条件を入力した後、集計ボタンを押してください。<br>" );

		htmlBuf.append( "<form action='" + selfPath + "' method='post'>" );

		htmlBuf.append( "<h2>1. 集計対象のカウンターを選択してください。</h2>" );
		for ( int i = 0; i < counterList.size(); ++i ) {
			Map<String, String> counterInfo = counterList.get( i );
			
			String title = counterInfo.get( "title" );
			String cid = counterInfo.get( "cid" );
			
			if ( cid.equals( in_cid ) ) {
				htmlBuf.append( "<input type='radio' name='cid' value='" + cid + "' selected>" + title + "<br>" );
			} else {
				htmlBuf.append( "<input type='radio' name='cid' value='" + cid + "'>" + title + "<br>" );
			}
		}

		htmlBuf.append( "<h2>2. 集計対象の年/月を選択してください。</h2>" );

		htmlBuf.append( "<select name='yearmonth'>" );
		for ( int i = 0; i < yearmonthList.size(); ++i ) {
			String yearmonth = yearmonthList.get( i );

			if ( yearmonth.equals( in_yearmonth ) ) {
				htmlBuf.append( "<option value='" + yearmonth + "' selected>" + yearmonth + "</option>" );
			} else {
				htmlBuf.append( "<option value='" + yearmonth + "'>" + yearmonth + "</option>" );
			}
		}
		htmlBuf.append( "</select>" );

		htmlBuf.append( "<h2>3. タイトルから除外する文字列を入力してください。</h2>" );
		htmlBuf.append( "<input type='text' name='exclude_from_title' size='100' value='" + exclude_from_title + "'><br>" );
		htmlBuf.append( "<br>" );

		htmlBuf.append( "<h2>4. 集計ボタンを押してください。</h2>" );
		htmlBuf.append( "集計ボタンを押すと集計処理が始まります。なお、集計の完了まで5〜10分程度かかります。<br>" );
		htmlBuf.append( "<br>" );
		htmlBuf.append( "<input type='submit' value='集計'><br>" );

		htmlBuf.append( "</form>" );

		htmlBuf.append( "<hr>" );
		htmlBuf.append( "<address>Copyright (c) 2010 <a href='http://d.hatena.ne.jp/nattou_curry_2'>id:nattou_curry_2</a> (<a href='http://twitter.com/nattou_curry'>@nattou_curry</a>)" );

		htmlBuf.append( "</body>" );
		htmlBuf.append( "</html>" );

		String html = htmlBuf.toString();

		//////////////////////////////////////////////////////////////////
		// HTMLを出力する。
		//////////////////////////////////////////////////////////////////
		
		resp.setContentType("text/html; charset=UTF-8");
		resp.getWriter().println( html );
	}
}
ResultServlet.java

集計中画面〜集計結果画面のサーブレットです。集計状況により画面を出し分けます。

package shc;

import com.google.appengine.api.datastore.*;
import java.io.IOException;
import java.text.*;
import java.util.*;
import java.util.logging.*;
import javax.servlet.http.*;

/**
 * 集計結果画面
 * @author nattou_curry
 */
public class ResultServlet extends HttpServlet {

	// ロガー
	private static Logger logger = Logger.getLogger( ResultServlet.class.getName() );

	/**
	 * GETリクエスト時の処理
	 * @param req HTTPリクエスト
	 * @param resp HTTPレスポンス
	 */
	public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {

		try {
			//////////////////////////////////////////////////////////////////
			// リクエストのパス情報からキーを取得する。
			//////////////////////////////////////////////////////////////////

			// リクエストのパス情報からキー文字列を取得する。
			String keyString = req.getPathInfo().substring( 1 );
	
			// キー文字列をエンティティのキーに変換する。
			Key key = KeyFactory.stringToKey( keyString );

			//////////////////////////////////////////////////////////////////
			// データストアから集計状況と集計結果を取得する。 
			//////////////////////////////////////////////////////////////////

			// データストアを取得する。
			DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

			// データストアからエンティティを取得する。
			Entity entity = datastore.get( key );
		
			// 集計状況を取得する。
			long status = (Long) entity.getProperty( "status" );
			// 集計結果(その他ブログ用)を取得する。
			Text result_H = (Text) entity.getProperty( "result_H" );
			// 集計結果(はてなダイアリー用)を取得する。
			Text result_D = (Text) entity.getProperty( "result_D" );

			//////////////////////////////////////////////////////////////////
			// HTMLを作成する。
			//////////////////////////////////////////////////////////////////

			StringBuffer htmlBuf = new StringBuffer();
				
			// 集計状況による場合分け:
			if ( status < 6 ) {
				// 集計中:
				
				//////////////////////////////////////////////////////////////////
				// 集計中メッセージを作成する。
				//////////////////////////////////////////////////////////////////
			
				htmlBuf.append( "<html>" );
				htmlBuf.append( "<head>" );
				htmlBuf.append( "<meta http-equiv='content-type' content='text/html; charset=utf-8'/>" );
				htmlBuf.append( "<meta http-equiv='refresh' content='120'/>" );
				htmlBuf.append( "<link rel='stylesheet' href='/style/base.css' type='text/css' />" );
				htmlBuf.append( "<title>集計中 - はてなカウンター集計ツール</title>" );
				htmlBuf.append( "</head>" );
				htmlBuf.append( "<body>" );

				htmlBuf.append( "<a href='/'><img src='/img/title.png' alt='はてなカウンター集計ツール' border='0'></a>" );
				htmlBuf.append( "<hr>" );

				htmlBuf.append( "<h1>集計中</h1>" );
				htmlBuf.append( "現在、集計中です。しばらくお待ちください。<br>" );
				htmlBuf.append( "<br>" );
				htmlBuf.append( "※集計中に500エラーが発生した場合、F5キーを押すと処理を継続できます<br>" );
				htmlBuf.append( "</body>" );
				htmlBuf.append( "</html>" );

			} else if ( status == 6 ) {
				// 集計終了:
				
				//////////////////////////////////////////////////////////////////
				// 集計結果メッセージを作成する。
				//////////////////////////////////////////////////////////////////
			
				htmlBuf.append( "<html>" );
				htmlBuf.append( "<head>" );
				htmlBuf.append( "<meta http-equiv='content-type' content='text/html; charset=utf-8'/>" );
				htmlBuf.append( "<link rel='stylesheet' href='/style/base.css' type='text/css' />" );
				htmlBuf.append( "<title>集計結果 - はてなカウンター集計ツール</title>" );
				htmlBuf.append( "</head>" );
				htmlBuf.append( "<body>" );

				htmlBuf.append( "<a href='/'><img src='/img/title.png' alt='はてなカウンター集計ツール' border='0'></a>" );
				htmlBuf.append( "<hr>" );

				htmlBuf.append( "<h1>集計結果</h1>" );
				htmlBuf.append( "集計が完了しました。<br>" );
				htmlBuf.append( "<br>" );

				htmlBuf.append( "集計結果を以下の通り表示しています。" );
				htmlBuf.append( "<ul>" );
				htmlBuf.append( "<li><a href='#preview'>プレビュー</a></li>" );
				htmlBuf.append( "<li><a href='#result_D'>はてなダイアリー貼り付け用コード</a></li>" );
				htmlBuf.append( "<li><a href='#result_H'>その他ブログ等貼り付け用コード</a></li>" );
				htmlBuf.append( "</ul>" );

				htmlBuf.append( "<br>" );
				htmlBuf.append( "<h2><a name='preview'>プレビュー</a></h2>" );
				htmlBuf.append( "<pre>" );
				htmlBuf.append( result_H.getValue() );
				htmlBuf.append( "</pre>" );
				htmlBuf.append( "<h2><a name='result_D'>はてなダイアリー貼り付け用コード</a></h2>" );
				htmlBuf.append( "<textarea cols=65 rows=10>" + result_D.getValue() +"</textarea>" );
				htmlBuf.append( "<h2><a name='result_H'>その他ブログ等貼り付け用コード</a></h2>" );
				htmlBuf.append( "<textarea cols=65 rows=10>" + result_H.getValue() +"</textarea>" );
				htmlBuf.append( "</body>" );
				htmlBuf.append( "</html>" );

			} else {
				// 集計エラー:
				
				//////////////////////////////////////////////////////////////////
				// エラーメッセージを作成する。
				//////////////////////////////////////////////////////////////////
			
				htmlBuf.append( "<html>" );
				htmlBuf.append( "<head>" );
				htmlBuf.append( "<meta http-equiv='content-type' content='text/html; charset=utf-8'/>" );
				htmlBuf.append( "<link rel='stylesheet' href='/style/base.css' type='text/css' />" );
				htmlBuf.append( "<title>集計エラー - はてなカウンター集計ツール</title>" );
				htmlBuf.append( "</head>" );
				htmlBuf.append( "<body>" );

				htmlBuf.append( "<a href='/'><img src='/img/title.png' alt='はてなカウンター集計ツール' border='0'></a>" );
				htmlBuf.append( "<hr>" );

				htmlBuf.append( "<h1>集計エラー</h1>" );
				htmlBuf.append( "集計中にエラーが発生しました。<br>" );
				htmlBuf.append( "<br>" );
				htmlBuf.append( "</body>" );
				htmlBuf.append( "</html>" );
			}

			htmlBuf.append( "<hr>" );
			htmlBuf.append( "<address>Copyright (c) 2010 <a href='http://d.hatena.ne.jp/nattou_curry_2'>id:nattou_curry_2</a> (<a href='http://twitter.com/nattou_curry'>@nattou_curry</a>)" );

			String html = htmlBuf.toString();

			//////////////////////////////////////////////////////////////////
			// HTMLを出力する。
			//////////////////////////////////////////////////////////////////
			
			resp.setContentType("text/html; charset=UTF-8");
			resp.getWriter().println( html );

		} catch ( Exception e ) {
			logger.log( Level.SEVERE, "エラーが発生しました。", e );

			resp.setContentType("text/html; charset=UTF-8");
			resp.getWriter().println( "エラーが発生しました。" );
		}
	}
}
SummarizeServlet.java

集計処理です。処理に時間がかかるので、タスクキューに格納して実行します。

package shc;

import com.google.appengine.api.datastore.*;
import java.io.IOException;
import java.net.*;
import java.text.*;
import java.util.*;
import java.util.logging.*;
import javax.servlet.http.*;

/**
 * 集計処理
 * @author nattou_curry
 */
public class SummarizeServlet extends HttpServlet {

	// ロガー
	private static Logger logger = Logger.getLogger( SummarizeServlet.class.getName() );

	/**
	 * POSTリクエスト時の処理
	 * @param req HTTPリクエスト
	 * @param resp HTTPレスポンス
	 */
	public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {

		// データストアを取得する。
		DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
	
		try {
			//////////////////////////////////////////////////////////////////
			// リクエストパラメータからキーを取得する。
			//////////////////////////////////////////////////////////////////
			
			// リクエストパラメータからキー文字列を取得する。
			String keyString = req.getParameter( "keyString" );
			
			// キー文字列をエンティティのキーに変換する。
			Key key = KeyFactory.stringToKey( keyString );

			//////////////////////////////////////////////////////////////////
			// 集計処理が完了するまで繰り返す。
			//////////////////////////////////////////////////////////////////
			
			while ( true ) {

				// トランザクションを開始する。
				Transaction txn = datastore.beginTransaction();
	
				//////////////////////////////////////////////////////////////////
				// データストアから集計情報を取得する。
				//////////////////////////////////////////////////////////////////
				
				// データストアから集計情報エンティティを取得する。
				Entity entity = datastore.get( txn, key );
	
				// エンティティから集計情報を取得する。
				long status = (Long) entity.getProperty( "status" );					// 集計状況
				String name_in = (String) entity.getProperty( "name" );					// はてなID
				String password_in = (String) entity.getProperty( "password" );				// パスワード
				String cid = (String) entity.getProperty( "cid" );					// カウンターID
				String year = (String) entity.getProperty( "year" );					// 年
				String month = (String) entity.getProperty( "month" );					// 月
				String prevYear = (String) entity.getProperty( "prevYear" );				// 前月の年
				String prevMonth = (String) entity.getProperty( "prevMonth" );				// 前月の月
				String exclude_from_title = (String) entity.getProperty( "exclude_from_title" );	// タイトルから除外する文字列

				//////////////////////////////////////////////////////////////////
				// 集計処理が完了しているか確認する。 
				//////////////////////////////////////////////////////////////////

				if ( status >= 6 ) {
					// 完了:
					
					// トランザクションをロールバックする。
					txn.rollback();
					// 繰り返しを抜ける。
					break;
				}

				//////////////////////////////////////////////////////////////////
				// 集計状況により場合分け。
				//////////////////////////////////////////////////////////////////
				
				if ( status == 0 ) {

					//////////////////////////////////////////////////////////////////
					// URL取得(status=0)
					//////////////////////////////////////////////////////////////////

					logger.info( "GetURL(status=0) start" );
	
					//////////////////////////////////////////////////////////////////
					// はてなカウンターにログインする。
					//////////////////////////////////////////////////////////////////
			
					// ログインする(SSL)。
					Map<String, String> loginInfo;
					try {
						loginInfo = HatenaCounterAccess.loginWithSSL( name_in, password_in );
					} catch ( Exception e ) {
						// ログインに失敗:
						logger.log( Level.SEVERE, "ログインに失敗しました。", e );
						throw e;
					}
		
					//////////////////////////////////////////////////////////////////
					// はてなカウンターから当月のアクセス数上位URLを取得する。
					//////////////////////////////////////////////////////////////////
			
					// URLの一覧を取得する。
					List<Map<String, String>> urlList = HatenaCounterAccess.getMonthlyURLList( loginInfo, cid, year, month );
				
					//////////////////////////////////////////////////////////////////
					// 当月のアクセス数上位URLをデータストアに格納する。
					//////////////////////////////////////////////////////////////////
					
					for ( int i = 0; i < urlList.size() && i < 10; ++i ) {
						Map<String, String> urlInfo = urlList.get( i );
						String url = urlInfo.get( "url" );
						String count = urlInfo.get( "count" );
						String ratio = urlInfo.get( "ratio" );
						String date = urlInfo.get( "date" );
	
						Entity urlEntity = new Entity( "URL", key );
						urlEntity.setProperty( "number", i + 1 );	
						urlEntity.setProperty( "url", url );	
						urlEntity.setProperty( "count", count );	
						urlEntity.setProperty( "ratio", ratio );	
						urlEntity.setProperty( "date", date );	
						urlEntity.setProperty( "status", 0 );	
	
						datastore.put( txn, urlEntity );
					}		
	
					status = 1;

					logger.info( "GetURL(status=0) end" );

				} else if ( status == 1 ) {

					//////////////////////////////////////////////////////////////////
					// タイトル取得(status=1)
					//////////////////////////////////////////////////////////////////
					
					logger.info( "GetTitle(status=1) start" );
	
					//////////////////////////////////////////////////////////////////
					// データストアからタイトルを取得していないURLを取得する。
					//////////////////////////////////////////////////////////////////
					
					Query query = new Query( "URL", entity.getKey() );
					query.addFilter( "status", Query.FilterOperator.EQUAL, 0 );
					FetchOptions limit = FetchOptions.Builder.withLimit( 1 );
					PreparedQuery pq = datastore.prepare( query );
		
					List<Entity> urlEntityList = pq.asList( limit );

					if  ( urlEntityList.size() == 1 ) {
						Entity urlEntity = urlEntityList.get( 0 );
						String url = (String) urlEntity.getProperty( "url" );

						//////////////////////////////////////////////////////////////////
						// URLにアクセスし、タイトルを取得する。
						//////////////////////////////////////////////////////////////////

						String title = HatenaCounterAccess.getTitle( url );
						
						// タイトルから指定された文字列を除去する。
						title = title.replaceFirst( exclude_from_title, "" );

						//////////////////////////////////////////////////////////////////
						// データストアにタイトルを格納する。
						//////////////////////////////////////////////////////////////////
						
						urlEntity.setProperty( "title", title );
						urlEntity.setProperty( "status", 1 );

						datastore.put( txn, urlEntity );
					} else {
						status = 2;
					}

					logger.info( "GetTitle(status=1) end" );

				} else if ( status == 2 ) {

					//////////////////////////////////////////////////////////////////
					// 前月のURL取得(status=2)
					//////////////////////////////////////////////////////////////////
					
					logger.info( "GetPrevURL(status=2) start" );
	
					//////////////////////////////////////////////////////////////////
					// はてなカウンターにログインする。
					//////////////////////////////////////////////////////////////////
			
					// ログインする(SSL)。
					Map<String, String> loginInfo;
					try {
						loginInfo = HatenaCounterAccess.loginWithSSL( name_in, password_in );
					} catch ( Exception e ) {
						// ログインに失敗:
						logger.log( Level.SEVERE, "ログインに失敗しました。", e );
						throw e;
					}
		
					//////////////////////////////////////////////////////////////////
					// はてなカウンターから前月のアクセス数上位URLを取得する。
					//////////////////////////////////////////////////////////////////
			
					// URLの一覧を取得する。
					List<Map<String, String>> prevUrlList = HatenaCounterAccess.getMonthlyURLList( loginInfo, cid, prevYear, prevMonth );
				
					//////////////////////////////////////////////////////////////////
					// 前月のアクセス数上位URLをデータストアに格納する。
					//////////////////////////////////////////////////////////////////
					
					for ( int i = 0; i < prevUrlList.size() && i < 10; ++i ) {
						Map<String, String> prevUrlInfo = prevUrlList.get( i );
						String url = prevUrlInfo.get( "url" );
	
						Entity prevUrlEntity = new Entity( "PrevURL", key );
						prevUrlEntity.setProperty( "number", i + 1 );	
						prevUrlEntity.setProperty( "url", url );	
	
						datastore.put( txn, prevUrlEntity );
					}		
	
					status = 3;

					logger.info( "GetPrevURL(status=2) end" );

				} else if ( status == 3 ) {

					//////////////////////////////////////////////////////////////////
					// 検索語(単語)取得(status=3)
					//////////////////////////////////////////////////////////////////
					
					logger.info( "GetSearchWordSingle(status=3) start" );
	
					//////////////////////////////////////////////////////////////////
					// はてなカウンターにログインする。
					//////////////////////////////////////////////////////////////////
			
					// ログインする(SSL)。
					Map<String, String> loginInfo;
					try {
						loginInfo = HatenaCounterAccess.loginWithSSL( name_in, password_in );
					} catch ( Exception e ) {
						// ログインに失敗:
						logger.log( Level.SEVERE, "ログインに失敗しました。", e );
						throw e;
					}
		
					//////////////////////////////////////////////////////////////////
					// はてなカウンターから前月のアクセス数上位検索語(単語)を取得する。
					//////////////////////////////////////////////////////////////////
			
					// 検索語(単語)の一覧を取得する。
					List<Map<String, String>> searchWordSingleList = HatenaCounterAccess.getMonthlySearchWordSingleList( loginInfo, cid, year, month );
				
					//////////////////////////////////////////////////////////////////
					// 前月のアクセス数上位検索語(単語)をデータストアに格納する。
					//////////////////////////////////////////////////////////////////
					
					
					for ( int i = 0; i < searchWordSingleList.size() && i < 10; ++i ) {
						Map<String, String> searchWordSingleInfo = searchWordSingleList.get( i );
						String search_word = searchWordSingleInfo.get( "search_word" );
						String count = searchWordSingleInfo.get( "count" );
						String ratio = searchWordSingleInfo.get( "ratio" );
						
						Entity searchWordSingleEntity = new Entity( "SearchWordSingle", key );
						searchWordSingleEntity.setProperty( "number", i );	
						searchWordSingleEntity.setProperty( "search_word", search_word );	
						searchWordSingleEntity.setProperty( "count", count );	
						searchWordSingleEntity.setProperty( "ratio", ratio );	

						datastore.put( txn, searchWordSingleEntity );
					}
	
					status = 4;

					logger.info( "GetSearchWordSingle(status=3) end" );

				} else if ( status == 4 ) {

					//////////////////////////////////////////////////////////////////
					// 検索語取得(status=4)
					//////////////////////////////////////////////////////////////////
					
					logger.info( "GetSearchWord(status=4) start" );
	
					//////////////////////////////////////////////////////////////////
					// はてなカウンターにログインする。
					//////////////////////////////////////////////////////////////////
			
					// ログインする(SSL)。
					Map<String, String> loginInfo;
					try {
						loginInfo = HatenaCounterAccess.loginWithSSL( name_in, password_in );
					} catch ( Exception e ) {
						// ログインに失敗:
						logger.log( Level.SEVERE, "ログインに失敗しました。", e );
						throw e;
					}
		
					//////////////////////////////////////////////////////////////////
					// はてなカウンターから前月のアクセス数上位検索語を取得する。
					//////////////////////////////////////////////////////////////////
			
					// 検索語の一覧を取得する。
					List<Map<String, String>> searchWordList = HatenaCounterAccess.getMonthlySearchWordList( loginInfo, cid, year, month );
				
					//////////////////////////////////////////////////////////////////
					// 前月のアクセス数上位検索語をデータストアに格納する。
					//////////////////////////////////////////////////////////////////
					

					for ( int i = 0; i < searchWordList.size() && i < 10; ++i ) {
						Map<String, String> searchWordInfo = searchWordList.get( i );
						String search_word = searchWordInfo.get( "search_word" );
						String google_link = searchWordInfo.get( "google_link" );
						String count = searchWordInfo.get( "count" );
						String ratio = searchWordInfo.get( "ratio" );
						
						Entity searchWordEntity = new Entity( "SearchWord", key );
						searchWordEntity.setProperty( "number", i );	
						searchWordEntity.setProperty( "search_word", search_word );	
						searchWordEntity.setProperty( "google_link", google_link );	
						searchWordEntity.setProperty( "count", count );	
						searchWordEntity.setProperty( "ratio", ratio );	

						datastore.put( txn, searchWordEntity );
					}
	
					status = 5;

					logger.info( "GetSearchWord(status=4) end" );

				} else if ( status == 5 ) {

					//////////////////////////////////////////////////////////////////
					// 集計(status=5)
					//////////////////////////////////////////////////////////////////
					
					logger.info( "Summarize(status=5) start" );

					//////////////////////////////////////////////////////////////////
					// データストアから当月の上位10位URLを取得する。
					//////////////////////////////////////////////////////////////////
					
					Query query = new Query( "URL", key );
					query.addSort( "number" );
					PreparedQuery pq = datastore.prepare( query );
					FetchOptions limit = FetchOptions.Builder.withLimit( 10 );
					List<Entity> urlList = pq.asList( limit );

					//////////////////////////////////////////////////////////////////
					// データストアから前月の上位10位URLを取得する。
					//////////////////////////////////////////////////////////////////
					
					query = new Query( "PrevURL", key );
					query.addSort( "number" );
					pq = datastore.prepare( query );
					limit = FetchOptions.Builder.withLimit( 10 );
					List<Entity> prevUrlList = pq.asList( limit );

					//////////////////////////////////////////////////////////////////
					// データストアから当月の上位10位検索語(単語)を取得する。
					//////////////////////////////////////////////////////////////////
					
					query = new Query( "SearchWordSingle", key );
					query.addSort( "number" );
					pq = datastore.prepare( query );
					limit = FetchOptions.Builder.withLimit( 10 );
					List<Entity> searchWordSingleList = pq.asList( limit );

					//////////////////////////////////////////////////////////////////
					// データストアから当月の上位10位検索語を取得する。
					//////////////////////////////////////////////////////////////////
					
					query = new Query( "SearchWord", key );
					query.addSort( "number" );
					pq = datastore.prepare( query );
					limit = FetchOptions.Builder.withLimit( 10 );
					List<Entity> searchWordList = pq.asList( limit );

					//////////////////////////////////////////////////////////////////
					// 上位10位URLの前月からの遷移を決定する。
					//////////////////////////////////////////////////////////////////
					
					for ( int i = 0; i < urlList.size(); ++i ) {
						Entity urlInfo = urlList.get( i );
						
						String date = (String) urlInfo.getProperty( "date" );
						String url = (String) urlInfo.getProperty( "url" );
						
						String diff = "";
						if ( date.substring( 0, 7 ).equals( year + "/" + month ) ) {
							diff = "新";
						} else {
							
							diff = "↑";
							for ( int j = 0; j < prevUrlList.size() && j < 10; ++j ) {
								Entity prevUrlInfo = prevUrlList.get( j );
								String prevUrl = (String) prevUrlInfo.getProperty( "url" );
								
								if ( url.equals( prevUrl ) ) {
									if ( i < j ) {
										diff = "↑";
									} else if ( i == j ) {
										diff = "→";
									} else {
										diff = "↓";
									}
									break;
								}
							}
						}
						
						urlInfo.setProperty( "diff", diff );
					}
			
					//////////////////////////////////////////////////////////////////
					// 集計結果(その他ブログ用)を作成する。
					//////////////////////////////////////////////////////////////////
					
					// ログイン情報から名前を取得する。
					String name = (String) entity.getProperty( "name" );
					
					StringBuffer buf_H = new StringBuffer();
					buf_H.append( "<h3>" + year + "年" + month + "月のアクセス数ランキング</h3>\r\n" );
					
					// URLの一覧から、アクセス数上位10記事を作成する。
					buf_H.append( "<h4>" + year + "年" + month + "月のアクセス数上位10記事</h4>\r\n" );
					buf_H.append( "<table border='1'>\r\n" );
					buf_H.append( "<tbody>\r\n" );
					buf_H.append( "<tr>" );
					buf_H.append( "<th>順位</th>" );
					buf_H.append( "<th>作成日</th>" );
					buf_H.append( "<th>記事</th>" );
					buf_H.append( "<th>回数</th>" );
					buf_H.append( "<th>比率</th>" );
					buf_H.append( "</tr>\r\n" );
					
					for ( int i = 0; i < urlList.size(); ++i ) {
						Entity urlInfo = urlList.get( i );
						
						int no = i + 1;
						String diff = (String) urlInfo.getProperty( "diff" );
						String date = (String) urlInfo.getProperty( "date" );
						String url = (String) urlInfo.getProperty( "url" );
						String title = (String) urlInfo.getProperty( "title" );
						String count = (String) urlInfo.getProperty( "count" );
						String ratio = (String) urlInfo.getProperty( "ratio" );
						String b_entry_url = "http://b.hatena.ne.jp/entry/" + url;
						String b_image_url = "http://b.hatena.ne.jp/entry/image/" + url;
						
						// アクセスランクの遷移を色付けする。
						if ( diff.equals( "新" ) ) {
							diff = "<span style='color:#00BB00;font-weight:bold;'>新</span>";
						} else if ( diff.equals( "→" ) ) {
							diff = "→";
						} else if ( diff.equals( "↑" ) ) {
							diff = "<span style='color:#FF0000;'>↑</span>";
						} else if ( diff.equals( "↓" ) ) {
							diff = "<span style='color:#0000FF;'>↓</span>";
						}
						
						buf_H.append( "<tr>" );
						buf_H.append( "<td>" + diff + " " + no + "</td>" );
						buf_H.append( "<td>" + date +  "</td>" );
						buf_H.append( "<td>" + "<a href='" + url + "'>" + title + "</a><a href='" + b_entry_url + "'><img src='" + b_image_url + "'></td>" );
						buf_H.append( "<td>" + count + "</td>" );
						buf_H.append( "<td>" + ratio + "</td>" );
						buf_H.append( "</tr>\r\n" );
					}
					
					buf_H.append( "</tbody>\r\n" );
					buf_H.append( "</table>\r\n" );

					// 検索語(単語)の一覧から、上位10検索語(単語)を作成する。
					buf_H.append( "<h4>" + year + "年" + month + "月の上位10検索語(単語)</h4>\r\n" );
					buf_H.append( "<table border='1'>\r\n" );
					buf_H.append( "<tbody>\r\n" );
					buf_H.append( "<tr>" );
					buf_H.append( "<th>順位</th>" );
					buf_H.append( "<th>内容</th>" );
					buf_H.append( "<th>回数</th>" );
					buf_H.append( "<th>比率</th>" );
					buf_H.append( "</tr>\r\n" );
			
					for ( int i = 0; i < searchWordSingleList.size(); ++i ) {
						Entity searchWordSingleInfo = searchWordSingleList.get( i );
						
						int no = i + 1;
						String search_word = (String) searchWordSingleInfo.getProperty( "search_word" );
						String count = (String) searchWordSingleInfo.getProperty( "count" );
						String ratio = (String) searchWordSingleInfo.getProperty( "ratio" );
						String searchdiary_link = "http://d.hatena.ne.jp/" + name + "/searchdiary?word=" + URLEncoder.encode( search_word, "EUC-JP" );
						
						buf_H.append( "<tr>" );
						buf_H.append( "<td>" + no + "</td>" );
						buf_H.append( "<td><a href='" + searchdiary_link + "'>" + search_word + "</a></td>" );
						buf_H.append( "<td>" + count + "</td>" );
						buf_H.append( "<td>" + ratio + "</td>\n" );
						buf_H.append( "</tr>\r\n" );
					}
			
					buf_H.append( "</tbody>\r\n" );
					buf_H.append( "</table>\r\n" );

					// 検索語の一覧から、上位10検索語を作成する。
					buf_H.append( "<h4>" + year + "年" + month + "月の上位10検索語</h4>\r\n" );
					buf_H.append( "<table border='1'>\r\n" );
					buf_H.append( "<tbody>\r\n" );
					buf_H.append( "<tr>" );
					buf_H.append( "<th>順位</th>" );
					buf_H.append( "<th>内容</th>" );
					buf_H.append( "<th>回数</th>" );
					buf_H.append( "<th>比率</th>" );
					buf_H.append( "</tr>\r\n" );
			
					for ( int i = 0; i < searchWordList.size(); ++i ) {
						Entity searchWordInfo = searchWordList.get( i );
						
						int no = i + 1;
						String search_word = (String) searchWordInfo.getProperty( "search_word" );
						String count = (String) searchWordInfo.getProperty( "count" );
						String ratio = (String) searchWordInfo.getProperty( "ratio" );
						String searchdiary_link = "http://d.hatena.ne.jp/" + name + "/searchdiary?word=" + URLEncoder.encode( search_word, "EUC-JP" );
						
						buf_H.append( "<tr>" );
						buf_H.append( "<td>" + no + "</td>" );
						buf_H.append( "<td><a href='" + searchdiary_link + "'>" + search_word + "</a></td>" );
						buf_H.append( "<td>" + count + "</td>" );
						buf_H.append( "<td>" + ratio + "</td>" );
						buf_H.append( "</tr>\r\n" );
					}
					
					buf_H.append( "</tbody>\r\n" );
					buf_H.append( "</table>\r\n" );

					buf_H.append( "<h4>おまけ</h4>\r\n" );
			
					// 検索語(単語)の一覧から、上位10検索語(単語)に関連するリンクを作成する。
					buf_H.append( "上位10検索語(単語)に関連するリンクです。\r\n" );
					buf_H.append( "<table border='1'>\r\n" );
					buf_H.append( "<tbody>\r\n" );
					buf_H.append( "<tr>" );
					buf_H.append( "<th>順位</th>" );
					buf_H.append( "<th>はてなキーワード</th>" );
					buf_H.append( "<th>人気ブログ</th>" );
					buf_H.append( "</tr>\r\n" );

					for ( int i = 0; i < searchWordSingleList.size(); ++i ) {
						Entity searchWordSingleInfo = searchWordSingleList.get( i );
						
						int no = i + 1;
						String search_word = (String) searchWordSingleInfo.getProperty( "search_word" );
						String keyword_link = "http://d.hatena.ne.jp/keyword/" + search_word;
						String hotblog_link = "http://k.hatena.ne.jp/hotblog/" + search_word;
						
						buf_H.append( "<tr>" );
						buf_H.append( "<td>" + no + "</td>" );
						buf_H.append( "<td><a href='" + keyword_link + "'>" + search_word + "</a></td>" );
						buf_H.append( "<td><a href='" + hotblog_link + "'>" + search_word + "</a></td>\n" );
						buf_H.append( "</tr>\r\n" );
					}
					
					buf_H.append( "</tbody>\r\n" );
					buf_H.append( "</table>\r\n" );

					// 検索語の一覧から、上位10検索語に関連するリンクを作成する。
					buf_H.append( "上位10検索語に関連するリンクです。\r\n" );
					buf_H.append( "<table border='1'>\r\n" );
					buf_H.append( "<tbody>\r\n" );
					buf_H.append( "<tr>" );
					buf_H.append( "<th>順位</th>" );
					buf_H.append( "<th>google検索</th>" );
					buf_H.append( "</tr>\r\n" );
						
					for ( int i = 0; i < searchWordList.size() && i < 10; ++i ) {
						Entity searchWordInfo = searchWordList.get( i );
						
						int no = i + 1;
						String search_word = (String) searchWordInfo.getProperty( "search_word" );
						String google_link = (String) searchWordInfo.getProperty( "google_link" );
						
						buf_H.append( "<tr>" );
						buf_H.append( "<td>" + no + "</td>" );
						buf_H.append( "<td><a href='" + google_link + "'>" + search_word + "</a></td>\n" );
						buf_H.append( "</tr>\r\n" );
					}

					buf_H.append( "</tbody>\r\n" );
					buf_H.append( "</table>\r\n" );

					//////////////////////////////////////////////////////////////////
					// 集計結果(はてなダイアリー用)を作成する。
					//////////////////////////////////////////////////////////////////
					
					StringBuffer buf_D = new StringBuffer();
					buf_D.append( "*[アクセス数]" + year + "年" + month + "月のアクセス数ランキング\r\n" );
					
					// URLの一覧から、アクセス数上位10記事を作成する。
					buf_D.append( "**" + year + "年" + month + "月のアクセス数上位10記事\r\n" );
					buf_D.append( "|*順位|*作成日|*記事|*回数|*比率|\r\n" );
					
					for ( int i = 0; i < urlList.size(); ++i ) {
						Entity urlInfo = urlList.get( i );
						
						int no = i + 1;
						String diff = (String) urlInfo.getProperty( "diff" );
						String date = (String) urlInfo.getProperty( "date" );
						String url = (String) urlInfo.getProperty( "url" );
						String title = (String) urlInfo.getProperty( "title" );
						String count = (String) urlInfo.getProperty( "count" );
						String ratio = (String) urlInfo.getProperty( "ratio" );
						String b_entry_url = "http://b.hatena.ne.jp/entry/" + url;
						String b_image_url = "http://b.hatena.ne.jp/entry/image/" + url;
						
						// アクセスランクの遷移を色付けする。
						if ( diff.equals( "新" ) ) {
							diff = "<span style='color:#00BB00;font-weight:bold;'>新</span>";
						} else if ( diff.equals( "→" ) ) {
							diff = "→";
						} else if ( diff.equals( "↑" ) ) {
							diff = "<span style='color:#FF0000;'>↑</span>";
						} else if ( diff.equals( "↓" ) ) {
							diff = "<span style='color:#0000FF;'>↓</span>";
						}
						
						buf_D.append( "|" + diff + " " + no + "|" + date +  "|" + "<a href='" + url + "'>" + title + "</a><a href='" + b_entry_url + "'><img src='" + b_image_url + "'>|" + count + "|" + ratio + "|\r\n" );
					}
					
					// 検索語(単語)の一覧から、上位10検索語(単語)を作成する。
					buf_D.append( "**" + year + "年" + month + "月の上位10検索語(単語)\r\n" );
					buf_D.append( "|*順位|*内容|*回数|*比率|\r\n" );
			
					for ( int i = 0; i < searchWordSingleList.size(); ++i ) {
						Entity searchWordSingleInfo = searchWordSingleList.get( i );
						
						int no = i + 1;
						String search_word = (String) searchWordSingleInfo.getProperty( "search_word" );
						String count = (String) searchWordSingleInfo.getProperty( "count" );
						String ratio = (String) searchWordSingleInfo.getProperty( "ratio" );
						String searchdiary_link = "http://d.hatena.ne.jp/" + name + "/searchdiary?word=" + URLEncoder.encode( search_word, "EUC-JP" );
						
						buf_D.append( "|" + no + "|<a href='" + searchdiary_link + "'>" + search_word + "</a>|" + count + "|" + ratio + "|\r\n" );
					}
			
					// 検索語の一覧をから、上位10検索語を作成する。
					buf_D.append( "**" + year + "年" + month + "月の上位10検索語\r\n" );
					buf_D.append( "|*順位|*内容|*回数|*比率|\r\n" );
			
					for ( int i = 0; i < searchWordList.size(); ++i ) {
						Entity searchWordInfo = searchWordList.get( i );
						
						int no = i + 1;
						String search_word = (String) searchWordInfo.getProperty( "search_word" );
						String count = (String) searchWordInfo.getProperty( "count" );
						String ratio = (String) searchWordInfo.getProperty( "ratio" );
						String searchdiary_link = "http://d.hatena.ne.jp/" + name + "/searchdiary?word=" + URLEncoder.encode( search_word, "EUC-JP" );
						
						buf_D.append( "|" + no + "|<a href='" + searchdiary_link + "'>" + search_word + "</a>|" + count + "|" + ratio + "|\r\n" );
					}
					
					buf_D.append( "**おまけ\r\n" );
			
					// 検索語(単語)の一覧から、上位10検索語(単語)に関連するリンクを作成する。
					buf_D.append( "上位10検索語(単語)に関連するリンクです。\r\n" );
					buf_D.append( "|*順位|*はてなキーワード|*人気ブログ|\r\n" );
					for ( int i = 0; i < searchWordSingleList.size(); ++i ) {
						Entity searchWordSingleInfo = searchWordSingleList.get( i );
						
						int no = i + 1;
						String search_word = (String) searchWordSingleInfo.getProperty( "search_word" );
						String keyword_link = "http://d.hatena.ne.jp/keyword/" + URLEncoder.encode( search_word, "EUC-JP" );
						String hotblog_link = "http://k.hatena.ne.jp/hotblog/" + search_word;
						
						buf_D.append( "|" + no + "|<a href='" + keyword_link + "'>" + search_word + "</a>|<a href='" + hotblog_link + "'>" + search_word + "</a>|\r\n" );
					}
					
					// 検索語の一覧から、上位10検索語に関連するリンクを作成する。
					buf_D.append( "上位10検索語に関連するリンクです。\r\n" );
					buf_D.append( "|*順位|*google検索|\r\n" );
						
					for ( int i = 0; i < searchWordList.size() && i < 10; ++i ) {
						Entity searchWordInfo = searchWordList.get( i );
						
						int no = i + 1;
						String search_word = (String) searchWordInfo.getProperty( "search_word" );
						String google_link = (String) searchWordInfo.getProperty( "google_link" );
						
						buf_D.append( "|" + no + "|<a href='" + google_link + "'>" + search_word + "</a>|\r\n" );
					}

					//////////////////////////////////////////////////////////////////
					// 集計結果を更新する。
					//////////////////////////////////////////////////////////////////
					
					Text result_H = new Text( buf_H.toString() );				
					Text result_D = new Text( buf_D.toString() );				

					entity.setProperty( "result_H", result_H );
					entity.setProperty( "result_D", result_D );
					entity.setProperty( "password", null );

					status = 6;

					logger.info( "Summarize(status=5) end" );
				}

				//////////////////////////////////////////////////////////////////
				// 集計状況を更新する。
				//////////////////////////////////////////////////////////////////
				
				entity.setProperty( "status", status );			// 集計状況
				entity.setProperty( "update_time", new Date() );	// 最終更新日時
	
				datastore.put( txn, entity );
	
				// トランザクションをコミットする。
				txn.commit();
			}

		} catch ( IOException e ) {
			logger.log( Level.SEVERE, "エラーが発生しました。", e );
			throw  e;
		} catch ( Exception e ) {
			logger.log( Level.SEVERE, "エラーが発生しました。", e );
			throw new RuntimeException( e );
		} finally {

			Transaction txn = datastore.getCurrentTransaction( null );
			if ( txn != null ) {
				// トランザクションが有効のまま:
				
				// トランザクションをロールバックする。
				txn.rollback();
				logger.info( "トランザクションをロールバックしました。" );
			}
		}
	}

}
HatenaCounterAccess.java

※記事中に納まらないので、後日再掲します。

集計結果のサンプル

本ブログのアクセス数集計結果です。
https://summarize-hatena-counter.appspot.com/Result/ahhzdW1tYXJpemUtaGF0ZW5hLWNvdW50ZXJyDgsSB1N1bW1hcnkY-lUM