io.github.hidroh.materialistic.accounts.UserServicesClient.java Source code

Java tutorial

Introduction

Here is the source code for io.github.hidroh.materialistic.accounts.UserServicesClient.java

Source

/*
 * Copyright (c) 2015 Ha Duy Trung
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.github.hidroh.materialistic.accounts;

import android.content.Context;
import android.net.Uri;
import android.support.v4.util.Pair;
import android.text.TextUtils;
import android.widget.Toast;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.inject.Inject;

import io.github.hidroh.materialistic.AppUtils;
import io.github.hidroh.materialistic.R;
import okhttp3.Call;
import okhttp3.FormBody;
import okhttp3.HttpUrl;
import okhttp3.Request;
import okhttp3.Response;
import rx.Observable;
import rx.Scheduler;
import rx.android.schedulers.AndroidSchedulers;

public class UserServicesClient implements UserServices {
    private static final String BASE_WEB_URL = "https://news.ycombinator.com";
    private static final String LOGIN_PATH = "login";
    private static final String VOTE_PATH = "vote";
    private static final String COMMENT_PATH = "comment";
    private static final String SUBMIT_PATH = "submit";
    private static final String ITEM_PATH = "item";
    private static final String SUBMIT_POST_PATH = "r";
    private static final String LOGIN_PARAM_ACCT = "acct";
    private static final String LOGIN_PARAM_PW = "pw";
    private static final String LOGIN_PARAM_CREATING = "creating";
    private static final String LOGIN_PARAM_GOTO = "goto";
    private static final String ITEM_PARAM_ID = "id";
    private static final String VOTE_PARAM_ID = "id";
    private static final String VOTE_PARAM_HOW = "how";
    private static final String COMMENT_PARAM_PARENT = "parent";
    private static final String COMMENT_PARAM_TEXT = "text";
    private static final String SUBMIT_PARAM_TITLE = "title";
    private static final String SUBMIT_PARAM_URL = "url";
    private static final String SUBMIT_PARAM_TEXT = "text";
    private static final String SUBMIT_PARAM_FNID = "fnid";
    private static final String SUBMIT_PARAM_FNOP = "fnop";
    private static final String VOTE_DIR_UP = "up";
    private static final String DEFAULT_REDIRECT = "news";
    private static final String CREATING_TRUE = "t";
    private static final String DEFAULT_FNOP = "submit-page";
    private static final String DEFAULT_SUBMIT_REDIRECT = "newest";
    private static final String REGEX_INPUT = "<\\s*input[^>]*>";
    private static final String REGEX_VALUE = "value[^\"]*\"([^\"]*)\"";
    private static final String HEADER_LOCATION = "location";
    private static final String HEADER_COOKIE = "cookie";
    private static final String HEADER_SET_COOKIE = "set-cookie";
    private final Call.Factory mCallFactory;
    private final Scheduler mIoScheduler;

    @Inject
    public UserServicesClient(Call.Factory callFactory, Scheduler ioScheduler) {
        mCallFactory = callFactory;
        mIoScheduler = ioScheduler;
    }

    @Override
    public void login(String username, String password, boolean createAccount, Callback callback) {
        execute(postLogin(username, password, createAccount))
                .map(response -> response.code() == HttpURLConnection.HTTP_MOVED_TEMP)
                .observeOn(AndroidSchedulers.mainThread()).subscribe(callback::onDone, callback::onError);
    }

    @Override
    public boolean voteUp(Context context, String itemId, Callback callback) {
        Pair<String, String> credentials = AppUtils.getCredentials(context);
        if (credentials == null) {
            return false;
        }
        Toast.makeText(context, R.string.sending, Toast.LENGTH_SHORT).show();
        execute(postVote(credentials.first, credentials.second, itemId))
                .map(response -> response.code() == HttpURLConnection.HTTP_MOVED_TEMP)
                .observeOn(AndroidSchedulers.mainThread()).subscribe(callback::onDone, callback::onError);
        return true;
    }

    @Override
    public void reply(Context context, String parentId, String text, Callback callback) {
        Pair<String, String> credentials = AppUtils.getCredentials(context);
        if (credentials == null) {
            callback.onDone(false);
            return;
        }
        execute(postReply(parentId, text, credentials.first, credentials.second))
                .map(response -> response.code() == HttpURLConnection.HTTP_MOVED_TEMP)
                .observeOn(AndroidSchedulers.mainThread()).subscribe(callback::onDone, callback::onError);
    }

    @Override
    public void submit(Context context, String title, String content, boolean isUrl, Callback callback) {
        Pair<String, String> credentials = AppUtils.getCredentials(context);
        if (credentials == null) {
            callback.onDone(false);
            return;
        }
        /**
         * The flow:
         * POST /submit with acc, pw
         *  if 302 to /login, considered failed
         * POST /r with fnid, fnop, title, url or text
         *  if 302 to /newest, considered successful
         *  if 302 to /x, considered error, maybe duplicate or invalid input
         *  if 200 or anything else, considered error
         */
        // fetch submit page with given credentials
        execute(postSubmitForm(credentials.first, credentials.second)).flatMap(
                response -> response.code() != HttpURLConnection.HTTP_MOVED_TEMP ? Observable.just(response)
                        : Observable.error(new IOException()))
                .flatMap(response -> {
                    try {
                        return Observable.just(
                                new String[] { response.header(HEADER_SET_COOKIE), response.body().string() });
                    } catch (IOException e) {
                        return Observable.error(e);
                    } finally {
                        response.close();
                    }
                }).map(array -> {
                    array[1] = getInputValue(array[1], SUBMIT_PARAM_FNID);
                    return array;
                })
                .flatMap(array -> !TextUtils.isEmpty(array[1]) ? Observable.just(array)
                        : Observable.error(new IOException()))
                .flatMap(array -> execute(postSubmit(title, content, isUrl, array[0], array[1])))
                .flatMap(response -> response.code() == HttpURLConnection.HTTP_MOVED_TEMP
                        ? Observable.just(Uri.parse(response.header(HEADER_LOCATION)))
                        : Observable.error(new IOException()))
                .flatMap(uri -> TextUtils.equals(uri.getPath(), DEFAULT_SUBMIT_REDIRECT) ? Observable.just(true)
                        : Observable.error(buildException(uri)))
                .observeOn(AndroidSchedulers.mainThread()).subscribe(callback::onDone, callback::onError);
    }

    private Request postLogin(String username, String password, boolean createAccount) {
        FormBody.Builder formBuilder = new FormBody.Builder().add(LOGIN_PARAM_ACCT, username)
                .add(LOGIN_PARAM_PW, password).add(LOGIN_PARAM_GOTO, DEFAULT_REDIRECT);
        if (createAccount) {
            formBuilder.add(LOGIN_PARAM_CREATING, CREATING_TRUE);
        }
        return new Request.Builder()
                .url(HttpUrl.parse(BASE_WEB_URL).newBuilder().addPathSegment(LOGIN_PATH).build())
                .post(formBuilder.build()).build();
    }

    private Request postVote(String username, String password, String itemId) {
        return new Request.Builder().url(HttpUrl.parse(BASE_WEB_URL).newBuilder().addPathSegment(VOTE_PATH).build())
                .post(new FormBody.Builder().add(LOGIN_PARAM_ACCT, username).add(LOGIN_PARAM_PW, password)
                        .add(VOTE_PARAM_ID, itemId).add(VOTE_PARAM_HOW, VOTE_DIR_UP).build())
                .build();
    }

    private Request postReply(String parentId, String text, String username, String password) {
        return new Request.Builder()
                .url(HttpUrl.parse(BASE_WEB_URL).newBuilder().addPathSegment(COMMENT_PATH).build())
                .post(new FormBody.Builder().add(LOGIN_PARAM_ACCT, username).add(LOGIN_PARAM_PW, password)
                        .add(COMMENT_PARAM_PARENT, parentId).add(COMMENT_PARAM_TEXT, text).build())
                .build();
    }

    private Request postSubmitForm(String username, String password) {
        return new Request.Builder()
                .url(HttpUrl.parse(BASE_WEB_URL).newBuilder().addPathSegment(SUBMIT_PATH).build())
                .post(new FormBody.Builder().add(LOGIN_PARAM_ACCT, username).add(LOGIN_PARAM_PW, password).build())
                .build();
    }

    private Request postSubmit(String title, String content, boolean isUrl, String cookie, String fnid) {
        Request.Builder builder = new Request.Builder()
                .url(HttpUrl.parse(BASE_WEB_URL).newBuilder().addPathSegment(SUBMIT_POST_PATH).build())
                .post(new FormBody.Builder().add(SUBMIT_PARAM_FNID, fnid).add(SUBMIT_PARAM_FNOP, DEFAULT_FNOP)
                        .add(SUBMIT_PARAM_TITLE, title).add(isUrl ? SUBMIT_PARAM_URL : SUBMIT_PARAM_TEXT, content)
                        .build());
        if (!TextUtils.isEmpty(cookie)) {
            builder.addHeader(HEADER_COOKIE, cookie);
        }
        return builder.build();
    }

    private Observable<Response> execute(Request request) {
        return Observable.defer(() -> {
            try {
                return Observable.just(mCallFactory.newCall(request).execute());
            } catch (IOException e) {
                return Observable.error(e);
            }
        }).subscribeOn(mIoScheduler);
    }

    private Throwable buildException(Uri uri) {
        switch (uri.getPath()) {
        case ITEM_PATH:
            UserServices.Exception exception = new UserServices.Exception(R.string.item_exist);
            String itemId = uri.getQueryParameter(ITEM_PARAM_ID);
            if (!TextUtils.isEmpty(itemId)) {
                exception.data = AppUtils.createItemUri(itemId);
            }
            return exception;
        default:
            return new IOException();
        }
    }

    private String getInputValue(String html, String name) {
        // extract <input ... >
        Matcher matcherInput = Pattern.compile(REGEX_INPUT).matcher(html);
        while (matcherInput.find()) {
            String input = matcherInput.group();
            if (input.contains(name)) {
                // extract value="..."
                Matcher matcher = Pattern.compile(REGEX_VALUE).matcher(input);
                return matcher.find() ? matcher.group(1) : null; // return first match if any
            }
        }
        return null;
    }
}