com.cuddlesoft.norilib.clients.DanbooruLegacy.java Source code

Java tutorial

Introduction

Here is the source code for com.cuddlesoft.norilib.clients.DanbooruLegacy.java

Source

/*
 * This file is part of nori.
 * Copyright (c) 2014 vomitcuddle <shinku@dollbooru.org>
 * License: ISC
 */

package com.cuddlesoft.norilib.clients;

import android.net.Uri;
import android.os.AsyncTask;
import android.text.TextUtils;

import com.cuddlesoft.norilib.Image;
import com.cuddlesoft.norilib.SearchResult;
import com.cuddlesoft.norilib.Tag;
import com.squareup.okhttp.Authenticator;
import com.squareup.okhttp.Credentials;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;

import java.io.IOException;
import java.io.StringReader;
import java.net.Proxy;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;

/**
 * Client for the Danbooru 1.x API.
 */
public class DanbooruLegacy implements SearchClient {
    /**
     * Number of images per search results page.
     * Best to use a large value to minimize number of unique HTTP requests.
     */
    private static final int DEFAULT_LIMIT = 100;
    /** OkHTTP Client. */
    private final OkHttpClient okHttpClient = new OkHttpClient();
    /** Human-readable service name. */
    protected final String name;
    /** URL to the HTTP API Endpoint - the server implementing the API. */
    protected final String apiEndpoint;
    /** Username used for authentication. (optional) */
    protected final String username;
    /** Password used for authentication. (optional) */
    protected final String password;

    /**
     * Create a new Danbooru 1.x client without authentication.
     *
     * @param endpoint URL to the HTTP API Endpoint - the server implementing the API.
     */
    public DanbooruLegacy(String name, String endpoint) {
        this.name = name;
        this.apiEndpoint = endpoint;
        this.username = null;
        this.password = null;
    }

    /**
     * Create a new Danbooru 1.x client with authentication.
     *
     * @param endpoint URL to the HTTP API Endpoint - the server implementing the API.
     * @param username Username used for authentication.
     * @param password Password used for authentication.
     */
    public DanbooruLegacy(String name, String endpoint, String username, String password) {
        this.name = name;
        this.apiEndpoint = endpoint;
        this.username = username;
        this.password = password;

        // Enable HTTP basic authentication.
        okHttpClient.setAuthenticator(new Authenticator() {
            @Override
            public Request authenticate(Proxy proxy, Response response) throws IOException {
                final String credential = Credentials.basic(DanbooruLegacy.this.username,
                        DanbooruLegacy.this.password);
                return response.request().newBuilder().header("Authorization", credential).build();
            }

            @Override
            public Request authenticateProxy(Proxy proxy, Response response) throws IOException {
                return null;
            }
        });
    }

    @Override
    public SearchResult search(String tags) throws IOException {
        // Return results for page 0.
        return search(tags, 0);
    }

    @Override
    public SearchResult search(String tags, int pid) throws IOException {
        // Create HTTP request.
        final Request request = new Request.Builder().url(createSearchURL(tags, pid, DEFAULT_LIMIT)).build();
        // Get HTTP response.
        final Response response = okHttpClient.newCall(request).execute();
        final String body = response.body().string();

        // Return parsed SearchResult.
        return parseXMLResponse(body, tags, pid);
    }

    @Override
    public void search(String tags, SearchCallback callback) {
        // Return results for page 0.
        search(tags, 0, callback);
    }

    @Override
    public void search(final String tags, final int pid, final SearchCallback callback) {
        // Fetch results on a background thread.
        new AsyncTask<Void, Void, SearchResult>() {
            /** Error returned when attempting to fetch the SearchResult. */
            private IOException error;

            @Override
            protected SearchResult doInBackground(Void... voids) {
                try {
                    return DanbooruLegacy.this.search(tags, pid);
                } catch (IOException e) {
                    // Hold on to the error for now and handle it on the main UI thread in #postExecute().
                    error = e;
                }
                return null;
            }

            @Override
            protected void onPostExecute(SearchResult searchResult) {
                // Pass the result or error to the SearchCallback.
                if (error != null || searchResult == null) {
                    callback.onFailure(error);
                } else {
                    callback.onSuccess(searchResult);
                }
            }
        }.execute();
    }

    /**
     * Parse an XML response returned by the API.
     *
     * @param body   HTTP Response body.
     * @param tags   Tags used to retrieve the response.
     * @param offset Current paging offset.
     * @return A {@link com.cuddlesoft.norilib.SearchResult} parsed from given XML.
     */
    protected SearchResult parseXMLResponse(String body, String tags, int offset) throws IOException {
        // Create variables to hold the values as XML is being parsed.
        final List<Image> imageList = new ArrayList<>(DEFAULT_LIMIT);

        try {
            // Create an XML parser factory and disable namespace awareness for security reasons.
            // See: (http://lists.w3.org/Archives/Public/public-xmlsec/2009Dec/att-0000/sws5-jensen.pdf).
            final XmlPullParserFactory xmlParserFactory = XmlPullParserFactory.newInstance();
            xmlParserFactory.setNamespaceAware(false);

            // Create a new XML parser and feed HTTP response data into it.
            final XmlPullParser xpp = xmlParserFactory.newPullParser();
            xpp.setInput(new StringReader(body));

            // Iterate over each XML element and handle pull parser "events".
            while (xpp.getEventType() != XmlPullParser.END_DOCUMENT) {
                if (xpp.getEventType() == XmlPullParser.START_TAG) {
                    if (xpp.getName().equals("post")) {
                        // <post> tags contain metadata for each image.
                        final Image image = new Image();

                        // Extract image metadata from XML attributes.
                        for (int i = 0; i < xpp.getAttributeCount(); i++) {
                            // Get name and value of current XML attribute.
                            final String name = xpp.getAttributeName(i);
                            final String value = xpp.getAttributeValue(i);

                            // Set the appropriate value for each tag name.
                            if (name.equals("file_url")) {
                                image.fileUrl = normalizeUrl(value);
                            } else if (name.equals("width")) {
                                image.width = Integer.parseInt(value);
                            } else if (name.equals("height")) {
                                image.height = Integer.parseInt(value);
                            } else if (name.equals("preview_url")) {
                                image.previewUrl = normalizeUrl(value);
                            } else if (name.equals("preview_width")) {
                                image.previewWidth = Integer.valueOf(value);
                            } else if (name.equals("preview_height")) {
                                image.previewHeight = Integer.valueOf(value);
                            } else if (name.equals("sample_url")) {
                                image.sampleUrl = normalizeUrl(value);
                            } else if (name.equals("sample_width")) {
                                image.sampleWidth = Integer.valueOf(value);
                            } else if (name.equals("sample_height")) {
                                image.sampleHeight = Integer.valueOf(value);
                            } else if (name.equals("tags")) {
                                image.tags = Tag.arrayFromString(value, Tag.Type.GENERAL);
                            } else if (name.equals("id")) {
                                image.id = value;
                            } else if (name.equals("parent_id")) {
                                image.parentId = value;
                            } else if (name.equals("rating")) {
                                image.obscenityRating = Image.ObscenityRating.fromString(value);
                            } else if (name.equals("score")) {
                                image.score = Integer.parseInt(value);
                            } else if (name.equals("md5")) {
                                image.md5 = value;
                            } else if (name.equals("created_at") || name.equals("date")) {
                                image.createdAt = dateFromString(value);
                            }
                        }

                        // Append values not returned by the API.
                        image.webUrl = webUrlFromId(image.id);
                        image.pixivId = Image.getPixivIdFromUrl(image.source);
                        // Use original file if low-resolution sample does not exist.
                        if (image.sampleUrl == null) {
                            image.sampleUrl = image.fileUrl;
                            image.sampleWidth = image.width;
                            image.sampleHeight = image.height;
                        }
                        // Add Image to search result.
                        imageList.add(image);
                    }
                }
                // Get next XMLPullParser event.
                xpp.next();
            }
        } catch (XmlPullParserException | ParseException e) {
            // Convert into IOException.
            // Needed for consistent method signatures in the SearchClient interface for different APIs.
            // (Throwing an XmlPullParserException would be fine, until dealing with an API using JSON, etc.)
            throw new IOException(e);
        }
        // Create and return a SearchResult.
        return new SearchResult(imageList.toArray(new Image[imageList.size()]), Tag.arrayFromString(tags), offset);
    }

    @Override
    public String getDefaultQuery() {
        // Show all safe-for-work images by default.
        return "";
    }

    @Override
    public Settings getSettings() {
        return new Settings(Settings.APIType.DANBOORU_LEGACY, name, apiEndpoint, username, password);
    }

    /**
     * Convert a relative image URL to an absolute URL.
     *
     * @param url URL to convert.
     * @return Absolute URL.
     */
    protected String normalizeUrl(String url) {
        // Return empty string for empty URLs.
        if (url == null || url.isEmpty()) {
            return "";
        }
        // Prepend API endpoint path if url is relative.
        final Uri uri = Uri.parse(url);
        if (uri.isRelative()) {
            return apiEndpoint + url;
        }
        // URL already absolute.
        return url;
    }

    /**
     * Get a URL viewable in the system web browser for given Image ID.
     *
     * @param id {@link com.cuddlesoft.norilib.Image} ID.
     * @return URL for viewing the image in the browser.
     */
    protected String webUrlFromId(String id) {
        return apiEndpoint + "/post/show/" + id;
    }

    /**
     * Create a {@link java.util.Date} object from String date representation used by this API.
     *
     * @param date Date string.
     * @return Date converted from given String.
     */
    protected Date dateFromString(String date) throws ParseException {
        // Parser for the date format used by upstream Danbooru 1.x.
        final DateFormat DATE_FORMAT_DEFAULT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);

        if (TextUtils.isDigitsOnly(date)) {
            // Moebooru-based boards (Danbooru 1.x fork) use Unix timestamps.
            return new Date(Integer.valueOf(date));
        } else {
            return DATE_FORMAT_DEFAULT.parse(date);
        }
    }

    /**
     * Generate request URL to the search API endpoint.
     *
     * @param tags  Space-separated tags.
     * @param pid   Page number (0-indexed).
     * @param limit Images to fetch per page.
     * @return URL to search results API.
     */
    protected String createSearchURL(String tags, int pid, int limit) {
        // Page numbers are 1-indexed for this API.
        final int page = pid + 1;

        return String.format(Locale.US, apiEndpoint + "/post/index.xml?tags=%s&limit=%d&page=%d", Uri.encode(tags),
                limit, page);
    }

    @Override
    public AuthenticationType requiresAuthentication() {
        return AuthenticationType.OPTIONAL;
    }
}