GWTでWeb Storageを利用する
GWTではWeb Storageを利用するためのAPIが用意されている。(ただし、Experimental API)
http://kumo2ji.appspot.com/
では、実験的にCellBrowserのWidthをユーザーごとに記憶するために利用している。
CellBrowserの幅を変更した場合、次回以降はその幅で初期化される。
public class Config { private StorageMap storageMap = null; private static String CELL_BROWSER_KEY = "cellBrowser"; public Config() { Storage storage = Storage.getLocalStorageIfSupported(); if (storage != null) { storageMap = new StorageMap(storage); } } public double getCellBrowserSize() { if (storageMap == null || !storageMap.containsKey(CELL_BROWSER_KEY)) { return 300; } String size = storageMap.get(CELL_BROWSER_KEY); return Double.valueOf(size); } public void setCellBrowserSize(double size) { if (storageMap == null) { return; } storageMap.put(CELL_BROWSER_KEY, String.valueOf(size)); } }
初期化は
Storage storage = Storage.getLocalStorageIfSupported(); if (storage != null) { storageMap = new StorageMap(storage); }
で行う。
保存できるのは文字列のみなので、保存したいものが数値の場合は、
storageMap.put(CELL_BROWSER_KEY, String.valueOf(size));
のように文字列にしてからputする。
getする場合も
String size = storageMap.get(CELL_BROWSER_KEY);
return Double.valueOf(size);
として、返す。
今期放送されているアニメ情報の取得
現在作成中のアニメ情報収集サイトでは、今期放送されているアニメを毎期入力している。
最初は手作業で追加していたが、面倒になってきたので、ある程度自動化した。
自動化する際に
アニメ番組表 API | アニメマップ
のAPIを利用させていただいた。
APIの提供ありがとうございます。
http://animemap.net/api/table/tokyo.json
にアクセスすると東京で放映されるアニメ情報がJSON形式で返ってくるので、これをそのままGWTに渡し、表の形にして表示している。
サーバー側のサービスにgetAnimemapJsonというメソッドを追加し、
@Override public String getAnimemapJson() { try { URL url = new URL("http://animemap.net/api/table/tokyo.json"); BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), "UTF-8")); return IOUtils.toString(reader); } catch (IOException e) { log.severe(e.getMessage()); } return null; }
GWTのJSONParserを使って、JSONをパースする。
public class AnimemapJson { private JSONObject json; public AnimemapJson(String json) { this.json = JSONParser.parseStrict(json).isObject(); } public List<String> getTitleList() { JSONObject responseObject = json.get("response").isObject(); JSONArray itemArray = responseObject.get("item").isArray(); List<String> titleList = new ArrayList<String>(); for (int i = 0; i < itemArray.size(); i++) { JSONObject itemObject = itemArray.get(i).isObject(); JSONString titleString = itemObject.get("title").isString(); titleList.add(titleString.stringValue()); } return titleList; } }
更新情報をつぶやくTwitter Botを作成
現在作成しているサイトでは、GAEのcronを使って、定期的にPixivの新着、公式ツイート、RSSの取得、Amazonの取得を行っている。
この取得した件数をつぶやくTwitter Botを作成した。
140文字制限があるので、140文字を超える場合は、ツイートを分けるようにした。
ファンアートを更新しました。
神々の悪戯:2件, テンカイナイト:2件, 蟲師 続章:1件, ラブライブ!(第2期):9件, ブラック・ブレット:1件, 一週間フレンズ。:1件, ご注文はうさぎですか?:2件, ブレイク ブレイド:1件, メカクシティアクターズ:10件, ...
— 雲の中の2次 (@kumo2ji) 2014, 5月 10
デート・ア・ライブII:2件, ピンポン:1件, 棺姫のチャイカ:1件, ソウルイーターノット!:2件, 悪魔のリドル:2件, ノーゲーム・ノーライフ:1件, 僕らはみんな河合荘:1件, selector infected WIXOSS:1件, ジョジョの奇妙な冒険 第 ...
— 雲の中の2次 (@kumo2ji) 2014, 5月 10
三部 スターダストクルセイダース:5件, ドラゴンボール改 魔人ブウ編:3件
— 雲の中の2次 (@kumo2ji) 2014, 5月 10
Pixiv投稿数はやはりラブライブが一番多く、土日は1日で300作品程度は投稿されているようだ。
次点で、メカクシティアクターズが来ている。
これは元々カゲロウプロジェクトとして、Pixivランキングで見かけていたので、Pixivにはそれなりの固定ファンがいるようだ。
Twitter APIを利用するためのJavaコード
JavaでTwitter REST API v1.1のリクエスト - GAE + GWT プログラミングメモ
で書いたコードを整理した。
Twitterクラス
public class Twitter { private static String USERS_URL = "https://api.twitter.com/1.1/users/"; private static String SHOW_URL = USERS_URL + "show.json"; private static String SEARCH_URL = USERS_URL + "search.json"; private static String OAUTH_CONSUMER_KEY = "your consumer key"; private static String OAUTH_ACCESS_TOKEN = "your access token"; private static String OAUTH_CONSUMER_SECRET = "your consumer secret"; private static String OAUTH_ACCESS_TOKEN_SECRET = "your access token secret"; private static OAuth oAuth = new OAuth( OAUTH_CONSUMER_KEY, OAUTH_ACCESS_TOKEN, OAUTH_CONSUMER_SECRET, OAUTH_ACCESS_TOKEN_SECRET); public String getUsersShow(String screen_name) throws InvalidKeyException, NoSuchAlgorithmException, IOException { Map<String, String> urlParamMap = new HashMap<String, String>(); urlParamMap.put("screen_name", screen_name); return oAuth.getJson(SHOW_URL, urlParamMap); } public String getUsersSearch(String query) throws InvalidKeyException, NoSuchAlgorithmException, IOException { String encodedQuery = URLEncoder.encode(query, "UTF-8"); Map<String, String> paramMap = new HashMap<String, String>(); paramMap.put("q", encodedQuery); return oAuth.getJson(SEARCH_URL, paramMap); } }
GETしか実装していないが、必要になったらPOSTもその内実装するかも。
OAuth部分を抜き出して、Twitter用の処理を分離した。
OAuthクラス
OAuth部分の抜き出し。
使い方は、コンストラクタで、Consumer Key、Access token等を設定し、getJsonの引数で、URLとURL Parameterを指定する。
コンストラクタ
public class OAuth { private static String HMAC_SHA1 = "HMAC-SHA1"; private static String OAUTH_CONSUMER_KEY_KEY = "oauth_consumer_key"; private static String OAUTH_NONCE_KEY = "oauth_nonce"; private static String OAUTH_SIGNATURE_METHOD_KEY = "oauth_signature_method"; private static String OAUTH_TIMESTAMP_KEY = "oauth_timestamp"; private static String OAUTH_TOKEN_KEY = "oauth_token"; private static String OAUTH_VERSION_KEY = "oauth_version"; private static String OAUTH_SIGNATURE_KEY = "oauth_signature"; private String oAuthConsumerSecret; private String oAuthAccessTokenSecret; private Map<String, String> oAuthParamMap = new HashMap<String, String>(); public OAuth(String oAuthConsumerKey, String oAuthAccessToken, String oAuthConsumerSecret, String oAuthAccessTokenSecret) { super(); this.oAuthConsumerSecret = oAuthConsumerSecret; this.oAuthAccessTokenSecret = oAuthAccessTokenSecret; createOAuthParamMap(oAuthConsumerKey, oAuthAccessToken); } private void createOAuthParamMap(String oAuthConsumerKey, String oAuthAccessToken) { long millis = System.currentTimeMillis(); String oAuthNonce = String.valueOf(millis); String oAuthSignatureMethod = HMAC_SHA1; String oAuthTimestamp = getTimestamp(millis); String oAuthVersion = "1.0"; oAuthParamMap.put(OAUTH_CONSUMER_KEY_KEY, oAuthConsumerKey); oAuthParamMap.put(OAUTH_NONCE_KEY, oAuthNonce); oAuthParamMap.put(OAUTH_SIGNATURE_METHOD_KEY, oAuthSignatureMethod); oAuthParamMap.put(OAUTH_TIMESTAMP_KEY, oAuthTimestamp); oAuthParamMap.put(OAUTH_TOKEN_KEY, oAuthAccessToken); oAuthParamMap.put(OAUTH_VERSION_KEY, oAuthVersion); } private String getTimestamp(long millis) { long secs = millis / 1000; return String.valueOf(secs); } }
createOAuthParamMapでOAuth認証に必要となるパラメーターをMapに準備している。
getJson実装
public class OAuth { private static String GET_METHOD = "GET"; private static String AUTHORIZATION = "Authorization"; public String getJson(String url, Map<String, String> paramMap) throws IOException, InvalidKeyException, NoSuchAlgorithmException { String urlWithParams = getUrlWithParams(url, paramMap); URLConnection urlConnection = new URL(urlWithParams).openConnection(); String authorization = getAuthorizationValue(GET_METHOD, url, paramMap); urlConnection.setRequestProperty(AUTHORIZATION, authorization); StringWriter writer = new StringWriter(); IOUtils.copy(urlConnection.getInputStream(), writer); return writer.toString(); } }
urlとparamMapからgetUrlWithParamsでリクエストURLを作成する。
また、getAuthorizationValueで認証のためのValue値を作成し、setRequestPropertyする。
OAuthクラスの実装
public class OAuth { private static String AUTHORIZATION = "Authorization"; private static String UTF8 = "UTF-8"; private static String HMAC_SHA1 = "HMAC-SHA1"; private static String OAUTH_CONSUMER_KEY_KEY = "oauth_consumer_key"; private static String OAUTH_NONCE_KEY = "oauth_nonce"; private static String OAUTH_SIGNATURE_METHOD_KEY = "oauth_signature_method"; private static String OAUTH_TIMESTAMP_KEY = "oauth_timestamp"; private static String OAUTH_TOKEN_KEY = "oauth_token"; private static String OAUTH_VERSION_KEY = "oauth_version"; private static String OAUTH_SIGNATURE_KEY = "oauth_signature"; private static String GET_METHOD = "GET"; private String oAuthConsumerSecret; private String oAuthAccessTokenSecret; private Map<String, String> oAuthParamMap = new HashMap<String, String>(); public OAuth(String oAuthConsumerKey, String oAuthAccessToken, String oAuthConsumerSecret, String oAuthAccessTokenSecret) { super(); this.oAuthConsumerSecret = oAuthConsumerSecret; this.oAuthAccessTokenSecret = oAuthAccessTokenSecret; createOAuthParamMap(oAuthConsumerKey, oAuthAccessToken); } private void createOAuthParamMap(String oAuthConsumerKey, String oAuthAccessToken) { long millis = System.currentTimeMillis(); String oAuthNonce = String.valueOf(millis); String oAuthSignatureMethod = HMAC_SHA1; String oAuthTimestamp = getTimestamp(millis); String oAuthVersion = "1.0"; oAuthParamMap.put(OAUTH_CONSUMER_KEY_KEY, oAuthConsumerKey); oAuthParamMap.put(OAUTH_NONCE_KEY, oAuthNonce); oAuthParamMap.put(OAUTH_SIGNATURE_METHOD_KEY, oAuthSignatureMethod); oAuthParamMap.put(OAUTH_TIMESTAMP_KEY, oAuthTimestamp); oAuthParamMap.put(OAUTH_TOKEN_KEY, oAuthAccessToken); oAuthParamMap.put(OAUTH_VERSION_KEY, oAuthVersion); } private String getTimestamp(long millis) { long secs = millis / 1000; return String.valueOf(secs); } public String getJson(String url, Map<String, String> paramMap) throws IOException, InvalidKeyException, NoSuchAlgorithmException { String urlWithParams = getUrlWithParams(url, paramMap); URLConnection urlConnection = new URL(urlWithParams).openConnection(); String authorization = getAuthorizationValue(GET_METHOD, url, paramMap); urlConnection.setRequestProperty(AUTHORIZATION, authorization); StringWriter writer = new StringWriter(); IOUtils.copy(urlConnection.getInputStream(), writer); return writer.toString(); } private String getUrlWithParams(String url, Map<String, String> urlParamMap) { if (urlParamMap.isEmpty()) { return url; } else { return url + "?" + createParamString(urlParamMap); } } private String getAuthorizationValue(String method, String url, Map<String, String> paramMap) throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException { StringBuffer buffer = new StringBuffer("OAuth "); for (Entry<String, String> entry : oAuthParamMap.entrySet()) { buffer.append(entry.getKey()); buffer.append("=\"" + entry.getValue() + "\""); buffer.append(", "); } String oAuthSignature = computeSignature(method, url, paramMap); buffer.append(OAUTH_SIGNATURE_KEY); buffer.append("=\"" + oAuthSignature + "\""); return buffer.toString(); } private String getSignatureBase(String method, String url, Map<String, String> paramMap) throws UnsupportedEncodingException { TreeMap<String, String> sortedParamMap = new TreeMap<String, String>(); sortedParamMap.putAll(paramMap); sortedParamMap.putAll(oAuthParamMap); String paramString = createParamString(sortedParamMap); String signatureBase = URLEncoder.encode(method, UTF8) + "&" + URLEncoder.encode(url, UTF8) + "&" + URLEncoder.encode(paramString, UTF8); return signatureBase; } private String createParamString(Map<String, String> paramMap) { TreeMap<String, String> treeMap = new TreeMap<String, String>(paramMap); StringBuffer paramStringBuffer = new StringBuffer(); for (Entry<String, String> paramEntry : treeMap.entrySet()) { if (!paramEntry.equals(treeMap.firstEntry())) { paramStringBuffer.append("&"); } paramStringBuffer.append(paramEntry.getKey() + "=" + paramEntry.getValue()); } return paramStringBuffer.toString(); } private String computeSignature(String method, String url, Map<String, String> paramMap) throws NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException { SecretKey secretKey = null; String compositeKey = URLEncoder.encode(oAuthConsumerSecret, UTF8) + "&" + URLEncoder.encode(oAuthAccessTokenSecret, UTF8); byte[] keyBytes = compositeKey.getBytes(); secretKey = new SecretKeySpec(keyBytes, "HmacSHA1"); Mac mac = Mac.getInstance("HmacSHA1"); mac.init(secretKey); String signatureBase = getSignatureBase(method, url, paramMap); byte[] text = signatureBase.getBytes(); String signature = new String(Base64.encodeBase64(mac.doFinal(text))).trim(); return URLEncoder.encode(signature, UTF8); } }
Slim3のMemcacheを利用してDatastoreアクセスを少なくする その2
Slim3のMemcacheを利用してDatastoreアクセスを少なくする その1 - GAE + GWT プログラミングメモ
の続き
overwrite実装
上書き保存のための実装。
Slim3では、保存しようとしているKeyが既に存在する場合は、上書きする。
public class DataAccessor<T extends Slim3Model> { private DaoBase<T> dao; private KeyMemcache memcache = new KeyMemcache(); public List<Key> overwrite(Collection<T> modelCollection) { Map<Key, T> map = new HashMap<Key, T>(); for (T model : modelCollection) { map.put(model.getKey(), model); } memcache.putAll(map); return dao.put(new ArrayList<T>(modelCollection)); } }
dao.putするついでに、memcache.putAllして、Memcacheに保存し、次回アクセスを速くする。
write実装
writeはDatastoreに既にKeyが存在する場合、上書きしたくないときのための実装。
public class DataAccessor<T extends Slim3Model> { public List<Key> write(Collection<T> modelCollection) { List<T> storedList = read(toKeyList(modelCollection)); Collection<T> subtract = CollectionUtils.subtract(modelCollection, storedList); return overwrite(subtract); } private List<Key> toKeyList(Collection<T> modelCollection) { List<Key> keyList = new ArrayList<Key>(); for (T model : modelCollection) { keyList.add(model.getKey()); } return keyList; } }
readで保存してあるListを取得し、書き込もうとしている集合との差集合subtractをとる。
この差集合のみoverwriteする。
全実装
public class DataAccessor<T extends Slim3Model> { private DaoBase<T> dao; private KeyMemcache memcache = new KeyMemcache(); public DataAccessor(DaoBase<T> dao) { this.dao = dao; } public T read(Key key) { List<Key> keyList = new ArrayList<Key>(); keyList.add(key); return read(keyList).get(0); } public List<T> read(List<Key> keyList) { Map<Key, T> memcachedModelMap = memcache.getAll(keyList); Map<Key, T> daoModelMap = getDaoModelMap(keyList, memcachedModelMap); memcache.putAll(daoModelMap); List<T> modelList = new ArrayList<T>(); for (Key key : keyList) { if (memcachedModelMap.containsKey(key)) { modelList.add(memcachedModelMap.get(key)); } else if (daoModelMap.containsKey(key)) { modelList.add(daoModelMap.get(key)); } } return modelList; } private Map<Key, T> getDaoModelMap(List<Key> keyList, Map<Key, T> memcachedModelMap) { Collection<Key> subtractedCollection = CollectionUtils.subtract(keyList, memcachedModelMap.keySet()); List<Key> subtractedList = new ArrayList<Key>(subtractedCollection); return dao.getAsMap(subtractedList); } public List<Key> overwrite(Collection<T> modelCollection) { Map<Key, T> map = new HashMap<Key, T>(); for (T model : modelCollection) { map.put(model.getKey(), model); } memcache.putAll(map); return dao.put(new ArrayList<T>(modelCollection)); } public List<Key> write(Collection<T> modelCollection) { List<T> storedList = read(toKeyList(modelCollection)); Collection<T> subtract = CollectionUtils.subtract(modelCollection, storedList); return overwrite(subtract); } private List<Key> toKeyList(Collection<T> modelCollection) { List<Key> keyList = new ArrayList<Key>(); for (T model : modelCollection) { keyList.add(model.getKey()); } return keyList; } private class KeyMemcache { public void putAll(Map<Key, T> values) { Map<Object, Object> objectMap = new HashMap<Object, Object>(); for (Entry<Key, T> entry : values.entrySet()) { Object key = entry.getKey(); Object model = entry.getValue(); objectMap.put(key, model); } Memcache.putAll(objectMap); } public Map<Key, T> getAll(List<Key> keyList) { Map<Key, T> map = new HashMap<Key, T>(); Map<Object, Object> memcachedMap = Memcache.getAll(keyList); for (Entry<Object, Object> entry : memcachedMap.entrySet()) { Key key = (Key) entry.getKey(); @SuppressWarnings("unchecked") T model = (T) entry.getValue(); map.put(key, model); } return map; } } }
Slim3のMemcacheを利用してDatastoreアクセスを少なくする その1
GAEでは、DatastoreへのRead、Writeともに課金されているので、なるべく減らしたい。
Memcacheを使えば、レスポンスを速くしつつ、Datastoreへのアクセスを減らすことができる。
DataAccessor
Slim3では、DataBaseの派生クラスのDAOでDatastoreにアクセスしているが、
さらに1つクラスを用意して、Memcacheを間に挟む。
ここでは、DataAccessorという名前にした。
public class DataAccessor<T extends Slim3Model> { private DaoBase<T> dao; public DataAccessor(DaoBase<T> dao) { this.dao = dao; } }
Slim3Modelインターフェース
ここで、Slim3Modelは
public interface Slim3Model { Key getKey(); Long getVersion(); }
というインターフェースで、Slim3でModelを作成すると自動生成されるインターフェースを明示したもの。
使い方としては、
public class BlogSlim3ModelDao extends DaoBase<BlogSlim3Model>{ private DataAccessor<BlogSlim3Model> dataAccessor = new DataAccessor<BlogSlim3Model>(this); public List<BlogSlim3Model> getModelList() { List<Key> keyList = query().asKeyList(); return dataAccessor.read(keyList); } }
のような形になる。
BlogSlim3ModelDaoはSlim3のgen-model-with-daoで自動生成される。
read実装
readは以下のように実装する。
public class DataAccessor<T extends Slim3Model> { private KeyMemcache memcache = new KeyMemcache(); public List<T> read(List<Key> keyList) { Map<Key, T> memcachedModelMap = memcache.getAll(keyList); Map<Key, T> daoModelMap = getDaoModelMap(keyList, memcachedModelMap); memcache.putAll(daoModelMap); List<T> modelList = new ArrayList<T>(); for (Key key : keyList) { if (memcachedModelMap.containsKey(key)) { modelList.add(memcachedModelMap.get(key)); } else if (daoModelMap.containsKey(key)) { modelList.add(daoModelMap.get(key)); } } return modelList; } private Map<Key, T> getDaoModelMap(List<Key> keyList, Map<Key, T> memcachedModelMap) { Collection<Key> subtractedCollection = CollectionUtils.subtract(keyList, memcachedModelMap.keySet()); List<Key> subtractedList = new ArrayList<Key>(subtractedCollection); return dao.getAsMap(subtractedList); } }
Daoにアクセスする前に、Memcacheに保存されていないか確認する。
MemcacheにはDatastoreのKeyクラスをMemcacheのKey、Slim3により自動生成されるModelクラスをValueとして保存する。
Datastoreからは、CollectionUtils.subtractでMemcacheから取得できなかったKeyのみを取り出して、getする。
DatastoreからgetしたdaoModelMapはmemcache.putAll(daoModelMap)で、Memcacheに保存して、次回以降Memcacheから取得できるようにする。
MemcacheとDaoから取得したModelはこのままでは、順番がバラバラなので、最後に順番を整理して返す。
KeyMemcacheクラス
Memcacheは型情報が無くなるので、KeyMemcacheというクラスを用意して、型情報を付与している。
public class DataAccessor<T extends Slim3Model> { private class KeyMemcache { public void putAll(Map<Key, T> values) { Map<Object, Object> objectMap = new HashMap<Object, Object>(); for (Entry<Key, T> entry : values.entrySet()) { Object key = entry.getKey(); Object model = entry.getValue(); objectMap.put(key, model); } Memcache.putAll(objectMap); } public Map<Key, T> getAll(List<Key> keyList) { Map<Key, T> map = new HashMap<Key, T>(); Map<Object, Object> memcachedMap = Memcache.getAll(keyList); for (Entry<Object, Object> entry : memcachedMap.entrySet()) { Key key = (Key) entry.getKey(); @SuppressWarnings("unchecked") T model = (T) entry.getValue(); map.put(key, model); } return map; } } }