io.github.tjg1.library.norilib.SearchResult.java Source code

Java tutorial

Introduction

Here is the source code for io.github.tjg1.library.norilib.SearchResult.java

Source

/*
 * This file is part of nori.
 * Copyright (c) 2014-2016 Tomasz Jan Gralczyk <tomg@fastmail.uk>
 * License: ISC
 */

package io.github.tjg1.library.norilib;

import android.os.Parcel;
import android.os.Parcelable;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.Predicate;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

/**
 * Search result received from the API.
 */
public class SearchResult implements Parcelable {

    //region Parcelable
    // Parcelables are the standard Android serialization API used to retain data between sessions.
    /** Class loader used when deserialization from a {@link Parcel}. */
    public static final Parcelable.Creator<SearchResult> CREATOR = new Parcelable.Creator<SearchResult>() {
        @Override
        public SearchResult createFromParcel(Parcel source) {
            return new SearchResult(source);
        }

        @Override
        public SearchResult[] newArray(int size) {
            return new SearchResult[size];
        }
    };

    /**
     * Re-create a SearchResult by deserializing data from a {@link android.os.Parcel}.
     *
     * @param parcel {@link android.os.Parcel} used to deserialize the SearchResult.
     */
    protected SearchResult(Parcel parcel) {
        this.images = parcel.createTypedArrayList(Image.CREATOR);
        this.offset = parcel.readInt();
        this.query = parcel.createTypedArray(Tag.CREATOR);
        this.hasNextPage = (parcel.readByte() == 0x01);
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeTypedList(images);
        dest.writeInt(offset);
        dest.writeTypedArray(query, 0);
        dest.writeByte((byte) (hasNextPage ? 0x01 : 0x00));
    }
    //endregion

    //region Instance fields
    /** List of {@link Image}s included in this SearchResult. */
    private final List<Image> images;

    /** Current offset. Used for paging. */
    private int offset = 0;

    /** List of tags originally used to retrieve this SearchResult. */
    private final Tag[] query;
    /**
     * True if more results may be available on the next page.
     * Set to false when the last page of results has been retrieved and included in {@link #images}.
     */
    private boolean hasNextPage = true;
    //endregion

    //region Constructors
    /**
     * Create a new SearchResult from a list of {@link Image}s.
     *
     * @param images List of images included in this SearchResult.
     * @param query  Tags used to retrieve this query.
     * @param offset Current paging offset.
     */
    public SearchResult(Image[] images, Tag[] query, int offset) {
        // Have to use the ArrayList constructor because the Lists returned by Arrays.asList are not resizable which is a bummer for filtering.
        this.images = new ArrayList<>(Arrays.asList(images));
        this.query = query.clone();
        this.offset = offset;
    }
    //endregion

    //region Adding images
    /**
     * Add more images to this SearchResult.
     * Usually called when new page of results has been fetched from the API.
     * Don't forget to call {@link #filter(Tag[])} and {@link #filter(Image.SafeSearchRating[])}
     * after adding more images.
     *
     * @param images Images to add.
     * @param offset Current paging offset. (ie. page number)
     */
    public void addImages(Image[] images, int offset) {
        // Add images to list.
        this.images.addAll(Arrays.asList(images));
        // Set new offset.
        this.offset = offset;
    }
    //endregion

    //region Filtering results
    /**
     * Remove images with the given set of {@link Tag}s from this SearchResult.
     *
     * @param tags Tags to remove.
     */
    public void filter(final Tag... tags) {
        // Don't waste time filtering against an empty array.
        if (tags == null || tags.length == 0) {
            return;
        }

        // Don't filter tags searched for by the user.
        final Collection<Tag> tagList = CollectionUtils.removeAll(Arrays.asList(tags), Arrays.asList(query));

        // Remove images containing filtered tags.
        CollectionUtils.filter(images, new Predicate<Image>() {
            @Override
            public boolean evaluate(Image image) {
                return !CollectionUtils.containsAny(Arrays.asList(image.tags), tagList);
            }
        });

        reorderImagePageOffsets();
    }

    /**
     * Remove images not in the given set of {@link Image.SafeSearchRating} from this SearchResult.
     *
     * @param safeSearchRatings SafeSearch ratings to remove.
     */
    public void filter(final Image.SafeSearchRating... safeSearchRatings) {
        // Don't waste time filtering against an empty array.
        if (safeSearchRatings == null || safeSearchRatings.length == 0) {
            return;
        }

        // Concert filtered rating array to List
        final List<Image.SafeSearchRating> ratingList = Arrays.asList(safeSearchRatings);
        // Remove images containing filtered ratings.
        CollectionUtils.filter(images, new Predicate<Image>() {
            @Override
            public boolean evaluate(Image image) {
                return ratingList.contains(image.safeSearchRating);
            }
        });

        reorderImagePageOffsets();
    }

    /** Re-calculate image page offsets after filtering. */
    private void reorderImagePageOffsets() {
        int page = 0;
        int offset = 0;

        for (Image image : images) {
            if (image.searchPage != null) {
                if (image.searchPage != page) {
                    page = image.searchPage;
                    offset = 0;
                }
                image.searchPagePosition = offset;
                offset += 1;
            }
        }
    }

    /**
     * Create a smaller version of this SearchResult containing {@link Image}s from the given Search
     * paging offset only. This is to make it suitable for passing between Activities without
     * triggering a {@link android.os.TransactionTooLargeException}.
     *
     * @param page Paging offset used to filter {@link Image}s.
     * @return A {@link SearchResult} containing only {@link Image}s for the given search paging offset.
     */
    public SearchResult getSearchResultForPage(final int page) {
        Collection<Image> selectedImages = CollectionUtils.select(images, new Predicate<Image>() {
            @Override
            public boolean evaluate(Image image) {
                return (image.searchPage != null && image.searchPage == page);
            }
        });

        return new SearchResult(selectedImages.toArray(new Image[selectedImages.size()]), this.query, page);
    }
    //endregion

    //region Getters & Setters
    /**
     * Get {@link Image}s contained in this SearchResult.
     *
     * @return {@link Image}s returned by this SearchResult.
     */
    public Image[] getImages() {
        return images.toArray(new Image[images.size()]);
    }

    /**
     * Get the current paging offset.
     * The way this value works varies greatly between APIs.
     * Some APIs use page numbers, some APIs use mySQL-style index offsets.
     * Don't rely on this value and don't show it to the user, unless you know what you're doing and
     * account for differences in the APIs.
     *
     * @return Current paging offset.
     */
    public int getCurrentOffset() {
        return offset;
    }

    /**
     * Get array of {@link Tag}s used to retrieve this SearchResult.
     *
     * @return {@link Tag}s used to retrieve this SearchResult.
     */
    public Tag[] getQuery() {
        return query;
    }

    /**
     * True if this SearchResult may contain another page that has not been retrieved yet.
     * Useful when implementing endless scrolling.
     *
     * @return True if another page of images could be fetched for this SearchResult.
     * @see #onLastPage()
     */
    public boolean hasNextPage() {
        return hasNextPage;
    }

    /**
     * Marks this SearchResult as having reached its final page.
     * This should be called by the API clients:
     * - Immediately for empty results.
     * - When fetching the next page returns an empty result.
     * Don't rely on length of array returned by {@link #getImages()}, as its value can be affected
     * by {@link #filter(Tag[])} and {@link #filter(Image.SafeSearchRating[])}.
     *
     * @see #hasNextPage()
     */
    public void onLastPage() {
        hasNextPage = false;
    }
    //endregion
}