com.wanikani.wklib.Connection.java Source code

Java tutorial

Introduction

Here is the source code for com.wanikani.wklib.Connection.java

Source

package com.wanikani.wklib;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Authenticator;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.Vector;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

/* 
 *  Copyright (c) 2013 Alberto Cuda
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

public class Connection {

    public interface Meter {

        public void count(int data);

        public void sync();

    }

    class Response {

        UserInformation ui;

        JSONObject infoAsObj;

        JSONArray infoAsArray;

        public Response(JSONObject obj, boolean isArray) throws JSONException, IOException {
            if (!obj.isNull("user_information")) {
                ui = new UserInformation(obj.getJSONObject("user_information"));
                if (!obj.isNull("requested_information")) {
                    if (isArray)
                        infoAsArray = obj.getJSONArray("requested_information");
                    else
                        infoAsObj = obj.getJSONObject("requested_information");
                }
            } else {
                throw ApplicationException.buildFromJSON(obj);
            }
        }
    }

    class NotModifiedException extends IOException {

        private static final long serialVersionUID = 1L;

        public NotModifiedException() {
            super("Document not modified");
        }

    }

    class CacheInfo {

        public String etag;

        public Date modified;

        public CacheInfo(String etag, Date modified) {
            this.etag = etag;
            this.modified = modified;
        }

        public CacheInfo() {
            /* empty */
        }

        public boolean hasData() {
            return etag != null || modified != null;
        }
    }

    public static final int CONNECT_TIMEOUT = 20000;

    public static final int READ_TIMEOUT = 60000;

    private static final int CACHE_DISPERSION_GROUPS = 7;

    private static final long CACHE_STALE_DISPERSION = 12 * 3600 * 1000;

    private static final long CACHE_STALE_TIME = 7 * 24 * 3600 * 1000;

    UserLogin login;

    Config config;

    Authenticator auth;

    UserInformation ui;

    public ItemsCacheInterface cache;

    public Connection(UserLogin login, Config config) {
        this.login = login;
        this.config = config;
        cache = new ItemsCache();
    }

    public void flush() {
        cache.flush();
    }

    public UserInformation getUserInformation(Meter meter) throws IOException {
        if (ui == null)
            ui = call(meter, "user-information", false).ui;

        return ui;
    }

    public StudyQueue getStudyQueue(Meter meter) throws IOException {
        Response res;

        try {
            res = call(meter, "study-queue", false);
            ui = res.ui;

            return new StudyQueue(res.infoAsObj);
        } catch (JSONException e) {
            throw new ParseException();
        }
    }

    public SRSDistribution getSRSDistribution(Meter meter) throws IOException {
        Response res;

        try {
            res = call(meter, "srs-distribution", false);
            ui = res.ui;

            return new SRSDistribution(res.infoAsObj);
        } catch (JSONException e) {
            throw new ParseException();
        }
    }

    public LevelProgression getLevelProgression(Meter meter) throws IOException {
        Response res;

        try {
            res = call(meter, "level-progression", false);
            ui = res.ui;

            return new LevelProgression(res.infoAsObj);
        } catch (JSONException e) {
            throw new ParseException();
        }
    }

    public ExtendedLevelProgression getExtendedLevelProgression(Meter meter) throws IOException {
        ItemLibrary<Radical> rlib;
        ItemLibrary<Kanji> klib;
        UserInformation ui;

        ui = getUserInformation(meter);
        rlib = getRadicals(meter, ui.level);
        klib = getKanji(meter, ui.level);

        return new ExtendedLevelProgression(rlib, klib);
    }

    private int[] getAllLevels(Meter meter) throws IOException {
        UserInformation ui;
        int ans[];
        int i;

        ui = getUserInformation(meter);

        ans = new int[ui.level];
        for (i = 0; i < ans.length; i++)
            ans[i] = i + 1;

        return ans;
    }

    private static String levelList(List<Integer> level) {
        StringBuffer sb;
        int i;

        sb = new StringBuffer();
        sb.append(level.get(0));
        for (i = 1; i < level.size(); i++)
            sb.append(',').append(level.get(i));

        return sb.toString();
    }

    private static <T extends Item> boolean isDataStale(ItemsCacheInterface.LevelData<T> ld, int level) {
        Date now;
        long age;

        now = new Date();
        age = now.getTime() - ld.date.getTime();
        if (age > CACHE_STALE_TIME + CACHE_STALE_DISPERSION * (level % CACHE_DISPERSION_GROUPS))
            return true;

        for (T item : ld.lib.list) {
            /* May become available any time */
            if (item.getAvailableDate() == null)
                return true;
            if (item.stats != null && item.stats.burned)
                continue;
            if (item.getAvailableDate().before(now))
                return true;
        }

        return false;
    }

    public ItemLibrary<Radical> getRadicals(Meter meter, int level) throws IOException {
        return getItems(meter, level, "radicals", Item.Type.RADICAL, Radical.FACTORY);
    }

    public ItemLibrary<Radical> getRadicals(Meter meter) throws IOException {
        return getRadicals(meter, getAllLevels(meter));
    }

    public ItemLibrary<Radical> getRadicals(Meter meter, int levels[]) throws IOException {
        return getItems(meter, levels, "radicals", Item.Type.RADICAL, Radical.FACTORY);
    }

    public ItemLibrary<Kanji> getKanji(Meter meter, int level) throws IOException {
        return getItems(meter, level, "kanji", Item.Type.KANJI, Kanji.FACTORY);
    }

    public ItemLibrary<Kanji> getKanji(Meter meter) throws IOException {
        return getKanji(meter, getAllLevels(meter));
    }

    public ItemLibrary<Kanji> getKanji(Meter meter, int levels[]) throws IOException {
        return getItems(meter, levels, "kanji", Item.Type.KANJI, Kanji.FACTORY);
    }

    public ItemLibrary<Vocabulary> getVocabulary(Meter meter, int level) throws IOException {
        return getItems(meter, level, "vocabulary", Item.Type.VOCABULARY, Vocabulary.FACTORY);
    }

    public ItemLibrary<Vocabulary> getVocabulary(Meter meter) throws IOException {
        return getVocabulary(meter, getAllLevels(meter));
    }

    public ItemLibrary<Vocabulary> getVocabulary(Meter meter, int levels[]) throws IOException {
        return getItems(meter, levels, "vocabulary", Item.Type.VOCABULARY, Vocabulary.FACTORY);
    }

    protected <T extends Item> ItemLibrary<T> getItems(Meter meter, int level, String resource, Item.Type type,
            Item.Factory<T> factory) throws IOException {
        ItemsCacheInterface.LevelData<T> data;
        ItemsCacheInterface.Cache<T> ic;
        ItemLibrary<T> lib;
        CacheInfo cinfo;
        Response res;

        ic = cache.get(type);
        data = ic.get(level);

        switch (data.quality) {
        case GOOD:
            if (!isDataStale(data, level))
                return data.lib;
            cinfo = new CacheInfo(data.etag, data.date);
            break;

        case MISSING:
        default:
            cinfo = new CacheInfo();
        }

        try {
            res = call(meter, resource, true, Integer.toString(level), cinfo);

            lib = new ItemLibrary<T>(factory, res.infoAsArray);

            data = new ItemsCacheInterface.LevelData<T>(cinfo.modified, cinfo.etag, lib);
            ic.put(data);

            return lib;

        } catch (NotModifiedException e) {
            return data.lib;
        } catch (JSONException e) {
            throw new ParseException();
        }
    }

    protected <T extends Item> ItemLibrary<T> getItems(Meter meter, int levels[], String resource, Item.Type type,
            Item.Factory<T> factory) throws IOException {
        Map<Integer, ItemsCacheInterface.LevelData<T>> map;
        ItemsCacheInterface.LevelData<T> ld;
        ItemsCacheInterface.Cache<T> ic;
        List<Integer> badl, missingl;
        ItemLibrary<T> ans, lib;
        CacheInfo cinfo;
        Response res;

        ic = cache.get(type);
        map = ItemsCacheInterface.LevelData.createMap(levels);
        ic.get(map);

        cinfo = new CacheInfo();

        cinfo.modified = new Date();
        ans = new ItemLibrary<T>();
        badl = new Vector<Integer>();
        missingl = new Vector<Integer>();
        for (Map.Entry<Integer, ItemsCacheInterface.LevelData<T>> e : map.entrySet()) {
            ld = e.getValue();
            switch (ld.quality) {
            case GOOD:
                if (isDataStale(ld, e.getKey())) {
                    if (ld.date.before(cinfo.modified))
                        cinfo.modified = ld.date;
                    badl.add(e.getKey());
                } else
                    ans.add(ld.lib);
                break;

            case MISSING:
                missingl.add(e.getKey());
            }
        }

        try {
            if (!badl.isEmpty()) {
                res = call(meter, resource, true, levelList(badl), cinfo);
                lib = new ItemLibrary<T>(factory, res.infoAsArray);
                ic.put(new ItemsCacheInterface.LevelData<T>(cinfo.modified, null, lib));
                ans.add(lib);
            }
        } catch (NotModifiedException e) {
            for (Integer i : badl)
                ans.add(map.get(i).lib);
        } catch (JSONException e) {
            throw new ParseException();
        }

        cinfo = new CacheInfo();
        try {
            if (!missingl.isEmpty()) {
                res = call(meter, resource, true, levelList(missingl), cinfo);
                lib = new ItemLibrary<T>(factory, res.infoAsArray);
                ic.put(new ItemsCacheInterface.LevelData<T>(cinfo.modified, null, lib));
                ans.add(lib);
            }
        } catch (JSONException e) {
            throw new ParseException();
        }

        return ans;
    }

    public ItemLibrary<Item> getRecentUnlocks(Meter meter, int count) throws IOException {
        Response res;

        try {
            res = call(meter, "recent-unlocks", true, Integer.toString(count));

            return new ItemLibrary<Item>(Item.FACTORY, res.infoAsArray);

        } catch (JSONException e) {
            throw new ParseException();
        }
    }

    public ItemLibrary<Item> getCriticalItems(Meter meter) throws IOException {
        Response res;

        try {
            res = call(meter, "critical-items", true);
            return new ItemLibrary<Item>(Item.FACTORY, res.infoAsArray);

        } catch (JSONException e) {
            throw new ParseException();
        }
    }

    public ItemLibrary<Item> getItems(Meter meter, int level) throws IOException {
        ItemLibrary<Radical> radicals;
        ItemLibrary<Kanji> kanji;
        ItemLibrary<Vocabulary> vocab;

        radicals = getRadicals(meter, level);
        kanji = getKanji(meter, level);
        vocab = getVocabulary(meter, level);

        return new ItemLibrary<Item>().add(radicals).add(kanji).add(vocab);
    }

    private static String readStream(Meter meter, InputStream is) throws IOException {
        InputStreamReader ir;
        StringBuffer sb;
        char buf[];
        int rd;

        buf = new char[1024];
        sb = new StringBuffer();
        ir = new InputStreamReader(is, "UTF-8");
        while (true) {
            rd = ir.read(buf, 0, buf.length);
            if (rd < 0)
                break;
            meter.count(rd);
            sb.append(buf, 0, rd);
        }

        meter.sync();
        return sb.toString();
    }

    protected Response call(Meter meter, String resource, boolean isArray) throws IOException {
        return call(meter, resource, isArray, null);
    }

    protected Response call(Meter meter, String resource, boolean isArray, String arg) throws IOException {
        return call(meter, resource, isArray, arg, null);
    }

    protected Response call(Meter meter, String resource, boolean isArray, String arg, CacheInfo cinfo)
            throws IOException {
        HttpURLConnection conn;
        JSONTokener tok;
        InputStream is;
        URL url;

        url = new URL(makeURL(resource, arg));
        conn = null;
        tok = null;
        try {
            conn = (HttpURLConnection) url.openConnection();
            if (cinfo != null) {
                if (cinfo.etag != null)
                    conn.setRequestProperty("If-None-Match", cinfo.etag);
                else if (cinfo.modified != null)
                    conn.setIfModifiedSince(cinfo.modified.getTime());
            }
            setTimeouts(conn);
            conn.connect();
            if (cinfo != null && cinfo.hasData() && conn.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED)
                throw new NotModifiedException();
            measureHeaders(meter, conn, false);
            is = conn.getInputStream();
            tok = new JSONTokener(readStream(meter, is));
        } finally {
            if (conn != null)
                conn.disconnect();
        }

        if (cinfo != null) {
            cinfo.modified = new Date();
            if (conn.getDate() > 0)
                cinfo.modified = new Date(conn.getDate());
            if (conn.getLastModified() > 0)
                cinfo.modified = new Date(conn.getLastModified());

            cinfo.etag = conn.getHeaderField("ETag");
        }

        try {
            return new Response(new JSONObject(tok), isArray);
        } catch (JSONException e) {
            throw new ParseException();
        }
    }

    public void resolve(Meter meter, UserInformation ui, int size, Bitmap defAvatar) {
        HttpURLConnection conn;
        InputStream is;
        URL url;
        int code;

        conn = null;
        try {
            url = new URL(config.gravatarUrl + "/" + ui.gravatar + "?s=" + size + "&d=404");
            conn = (HttpURLConnection) url.openConnection();
            setTimeouts(conn);
            code = conn.getResponseCode();
            if (code == 200) {
                is = conn.getInputStream();
                ui.gravatarBitmap = BitmapFactory.decodeStream(is);
            } else if (code == 404)
                ui.gravatarBitmap = defAvatar;
            measureHeaders(meter, conn, true);
        } catch (IOException e) {
            /* empty */
        } finally {
            if (conn != null)
                conn.disconnect();
        }
    }

    protected void measureHeaders(Meter meter, URLConnection conn, boolean clen) {
        Map<String, List<String>> hdrs;

        hdrs = conn.getHeaderFields();
        if (hdrs == null)
            return;
        for (Map.Entry<String, List<String>> e : hdrs.entrySet()) {
            if (e.getKey() != null)
                meter.count(e.getKey().length() + 1);
            for (String s : e.getValue())
                meter.count(s.length() + 3);
            if (clen && e.getKey() != null && e.getKey().equals("Content-Length") && !e.getValue().isEmpty()) {
                try {
                    meter.count(Integer.parseInt(e.getValue().get(0)));
                } catch (NumberFormatException x) {
                    /* empty */
                }
            }
        }

        meter.sync();
    }

    private String makeURL(String resource, String arg) {
        String ans;

        ans = String.format("%s/user/%s/%s", config.url, login.userkey, resource);
        if (arg != null)
            ans += "/" + arg;

        return ans;
    }

    private void setTimeouts(HttpURLConnection conn) {
        conn.setConnectTimeout(CONNECT_TIMEOUT);
        conn.setReadTimeout(READ_TIMEOUT);
    }
}