GAE + GWT プログラミングメモ

Google App Engine とGoogle Web Toolkit のメモ

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);

として、返す。

今期放送されているアニメ情報の取得

http://kumo2ji.appspot.com/

現在作成中のアニメ情報収集サイトでは、今期放送されているアニメを毎期入力している。

f:id:kumo2ji:20140510111801p:plain

最初は手作業で追加していたが、面倒になってきたので、ある程度自動化した。
自動化する際に
アニメ番組表 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を作成

http://kumo2ji.appspot.com/

現在作成しているサイトでは、GAEのcronを使って、定期的にPixivの新着、公式ツイート、RSSの取得、Amazonの取得を行っている。
この取得した件数をつぶやくTwitter Botを作成した。
140文字制限があるので、140文字を超える場合は、ツイートを分けるようにした。



Pixiv投稿数はやはりラブライブが一番多く、土日は1日で300作品程度は投稿されているようだ。
次点で、メカクシティアクターズが来ている。
これは元々カゲロウプロジェクトとして、Pixivランキングで見かけていたので、Pixivにはそれなりの固定ファンがいるようだ。

Twitter BotはTwitter4Jを使って作成した。
ライブラリ提供ありがとうございます。

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;
        }
    }
}