All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dev.katsute.mal4j.MyAnimeListImpl Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2024 Katsute 
 *
 * 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 2 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, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

package dev.katsute.mal4j;

import dev.katsute.mal4j.APIStruct.Response;
import dev.katsute.mal4j.anime.Anime;
import dev.katsute.mal4j.anime.AnimeListStatus;
import dev.katsute.mal4j.anime.AnimeRanking;
import dev.katsute.mal4j.anime.property.AnimeRankingType;
import dev.katsute.mal4j.anime.property.time.Season;
import dev.katsute.mal4j.character.Character;
import dev.katsute.mal4j.exception.ExperimentalFeatureException;
import dev.katsute.mal4j.exception.HttpException;
import dev.katsute.mal4j.exception.InvalidTokenException;
import dev.katsute.mal4j.forum.ForumCategory;
import dev.katsute.mal4j.forum.ForumTopic;
import dev.katsute.mal4j.forum.ForumTopicDetail;
import dev.katsute.mal4j.forum.Post;
import dev.katsute.mal4j.manga.Manga;
import dev.katsute.mal4j.manga.MangaListStatus;
import dev.katsute.mal4j.manga.MangaRanking;
import dev.katsute.mal4j.manga.property.MangaRankingType;
import dev.katsute.mal4j.people.Person;
import dev.katsute.mal4j.property.ExperimentalFeature;
import dev.katsute.mal4j.query.*;
import dev.katsute.mal4j.user.User;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.regex.Pattern;

import static dev.katsute.mal4j.Json.*;
import static dev.katsute.mal4j.MyAnimeListSchema_Anime.*;
import static dev.katsute.mal4j.MyAnimeListSchema_Character.*;
import static dev.katsute.mal4j.MyAnimeListSchema_Forum.*;
import static dev.katsute.mal4j.MyAnimeListSchema_Manga.*;
import static dev.katsute.mal4j.MyAnimeListSchema_People.*;
import static dev.katsute.mal4j.MyAnimeListSchema_User.*;

final class MyAnimeListImpl extends MyAnimeList {

    private transient String token     = null;
    private transient String client_id = null;
    private final boolean isTokenAuth;

    private MyAnimeListAuthenticator authenticator;

    private final MyAnimeListService service = MyAnimeListService.create();

    MyAnimeListImpl(final String token_or_client, final boolean isToken){
        Objects.requireNonNull(token_or_client, (isToken ? "OAuth token" : "Client ID") + " can not be null");
        this.isTokenAuth = isToken;
        if(isToken){
            if(!token_or_client.startsWith("Bearer "))
                throw new InvalidTokenException("OAuth token should start with 'Bearer'");
            this.token = token_or_client;
        }else
            this.client_id = token_or_client;
        Logging.addMask(token_or_client);
    }

    MyAnimeListImpl(final MyAnimeListAuthenticator authenticator){
        Objects.requireNonNull(authenticator, "Authenticator cannot be null");
        this.authenticator = authenticator;
        this.token         = authenticator.getAccessToken().getToken();
        this.isTokenAuth   = true;
    }

    @Override
    public synchronized final void refreshToken(){
        if(authenticator == null)
            throw new UnsupportedOperationException("OAuth token refresh can only be used with authorization");
        this.token = authenticator.refreshAccessToken().getToken();
    }

// experimental features

    // features that are no longer experimental (make sure to deprecate)
    @SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
    private final List nativeFeatures = Arrays.asList();

    // experimental features that are enabled
    private final List enabledFeatures = new ArrayList<>();

    @SuppressWarnings("SameParameterValue")
    final void checkExperimentalFeatureEnabled(final ExperimentalFeature feature){
        if(nativeFeatures.contains(feature) || enabledFeatures.contains(feature) || enabledFeatures.contains(ExperimentalFeature.ALL))
            return;

        throw new ExperimentalFeatureException("The feature " + feature.name() + " is an experimental feature and must be enabled using the enableExperimentalFeature method");
    }

    @Override
    public final void enableExperimentalFeature(final ExperimentalFeature feature){
        if(nativeFeatures.contains(feature))
            Logging.getLogger().warning("The feature " + feature.name() + " is no longer an experimental feature, you do not have to enable it anymore");
        else if(!enabledFeatures.contains(feature))
            enabledFeatures.add(feature);
    }

    final void clearExperimentalFeatures(){
        enabledFeatures.clear();
    }

// provider

    @Override
    public final AnimeSearchQuery getAnime(){
        return new AnimeSearchQuery(){

            @Override
            public final List search(){
                final JsonObject response = handleResponse(
                    () -> service.getAnime(
                        isTokenAuth ? token : null,
                        !isTokenAuth ? client_id : null,
                        query,
                        limit,
                        offset,
                        convertFields(Fields.anime, fields),
                        nsfw
                    )
                );
                if(response == null) return null;

                final List anime = new ArrayList<>();
                final JsonObject[] arr = response.getJsonArray("data");
                if(arr == null) return null;
                for(final JsonObject iterator : arr)
                    anime.add(asAnime(MyAnimeListImpl.this, iterator.getJsonObject("node")));
                return anime;
            }

            @Override
            public final PaginatedIterator searchAll(){
                return new PagedIterator<>(
                    offset,
                    offset -> service.getAnime(
                        isTokenAuth ? token : null,
                        !isTokenAuth ? client_id : null,
                        query,
                        limit,
                        offset,
                        convertFields(Fields.anime, fields),
                        nsfw
                    ),
                    iterator -> asAnime(MyAnimeListImpl.this, iterator.getJsonObject("node"))
                );
            }
        };
    }

    @Override
    public final Anime getAnime(final long id){
        return getAnime(id, (String[]) null);
    }

    @Override
    public final Anime getAnime(final long id, final String... fields){
        return asAnime(this, getAnimeSchema(id, fields));
    }

    final JsonObject getAnimeSchema(final long id, final String... fields){
        return handleResponse(
            () -> service.getAnime(
                isTokenAuth ? token : null,
                !isTokenAuth ? client_id : null,
                id,
                convertFields(Fields.anime, fields)
            )
        );
    }

    @Override
    public final AnimeCharacterQuery getAnimeCharacters(final long id){
        checkExperimentalFeatureEnabled(ExperimentalFeature.CHARACTERS);
        return new AnimeCharacterQuery(){

            @Override
            public final List search(){
                final JsonObject response = handleResponse(
                    () -> service.getAnimeCharacters(
                        isTokenAuth ? token : null,
                        !isTokenAuth ? client_id : null,
                        id,
                        limit,
                        offset,
                        convertFields(Fields.character, fields)
                    )
                );
                if(response == null) return null;

                final List characters = new ArrayList<>();
                final JsonObject[] arr = response.getJsonArray("data");
                if(arr == null) return null;
                for(final JsonObject iterator : arr)
                    characters.add(MyAnimeListSchema_Character.asCharacter(MyAnimeListImpl.this, iterator.getJsonObject("node")));
                return characters;
            }

            @Override
            public final PaginatedIterator searchAll(){
                return new PagedIterator<>(
                    offset,
                    offset -> service.getAnimeCharacters(
                        isTokenAuth ? token : null,
                        !isTokenAuth ? client_id : null,
                        id,
                        limit,
                        offset,
                        convertFields(Fields.character, fields)
                    ),
                    iterator -> MyAnimeListSchema_Character.asCharacter(MyAnimeListImpl.this, iterator.getJsonObject("node"))
                );
            }

        };
    }

    @Override
    public final AnimeRankingQuery getAnimeRanking(final AnimeRankingType rankingType){
        return getAnimeRanking(Objects.requireNonNull(rankingType, "Ranking type cannot be null").field());
    }

    @Override
    public final AnimeRankingQuery getAnimeRanking(final String rankingType){
        return new AnimeRankingQuery(Objects.requireNonNull(rankingType, "Ranking type cannot be null")){

            @Override
            public final List search(){
                final JsonObject response = handleResponse(
                    () -> service.getAnimeRanking(
                        isTokenAuth ? token : null,
                        !isTokenAuth ? client_id : null,
                        rankingType,
                        limit,
                        offset,
                        convertFields(Fields.anime, fields),
                        nsfw
                    )
                );
                if(response == null) return null;

                final List anime = new ArrayList<>();
                final JsonObject[] arr = response.getJsonArray("data");
                if(arr == null) return null;
                for(final JsonObject iterator : arr)
                    anime.add(asAnimeRanking(MyAnimeListImpl.this, iterator));
                return anime;
            }

            @Override
            public final PaginatedIterator searchAll(){
                return new PagedIterator<>(
                    offset,
                    offset -> service.getAnimeRanking(
                        isTokenAuth ? token : null,
                        !isTokenAuth ? client_id : null,
                        rankingType,
                        limit,
                        offset,
                        convertFields(Fields.anime, fields),
                        nsfw
                    ),
                    iterator -> asAnimeRanking(MyAnimeListImpl.this, iterator)
                );
            }
        };
    }

    @Override
    public final AnimeSeasonQuery getAnimeSeason(final int year, final Season season){
        return new AnimeSeasonQuery(year, Objects.requireNonNull(season, "Season cannot be null")){

            @Override
            public final List search(){
                final JsonObject response = handleResponse(
                    () -> service.getAnimeSeason(
                        isTokenAuth ? token : null,
                        !isTokenAuth ? client_id : null,
                        year,
                        season.field(),
                        sort,
                        limit,
                        offset,
                        convertFields(Fields.anime, fields),
                        nsfw
                    )
                );
                if(response == null) return null;

                final List anime = new ArrayList<>();
                final JsonObject[] arr = response.getJsonArray("data");
                if(arr == null) return null;
                for(final JsonObject iterator : arr)
                    anime.add(asAnime(MyAnimeListImpl.this, iterator.getJsonObject("node")));
                return anime;
            }

            @Override
            public final PaginatedIterator searchAll(){
                return new PagedIterator<>(
                    offset,
                    offset -> service.getAnimeSeason(
                        isTokenAuth ? token : null,
                        !isTokenAuth ? client_id : null,
                        year,
                        season.field(),
                        sort,
                        limit,
                        offset,
                        convertFields(Fields.anime, fields),
                        nsfw
                    ),
                    iterator -> asAnime(MyAnimeListImpl.this, iterator.getJsonObject("node"))
                );
            }

        };
    }

    @Override
    public final AnimeSuggestionQuery getAnimeSuggestions(){
        return new AnimeSuggestionQuery(){

            @Override
            public final List search(){
                final JsonObject response = handleResponse(
                    () -> service.getAnimeSuggestions(
                        Objects.requireNonNull(token, "Client ID not supported for this endpoint, create a MyAnimeList object with either an Authenticator or Token"),
                        limit,
                        offset,
                        convertFields(Fields.anime, fields),
                        nsfw
                    )
                );
                if(response == null) return null;

                final List anime = new ArrayList<>();
                final JsonObject[] arr = response.getJsonArray("data");
                if(arr == null) return null;
                for(final JsonObject iterator : arr)
                    anime.add(asAnime(MyAnimeListImpl.this, iterator.getJsonObject("node")));
                return anime;
            }

            @Override
            public final PaginatedIterator searchAll(){
                return new PagedIterator<>(
                    offset,
                    offset -> service.getAnimeSuggestions(
                        Objects.requireNonNull(token, "Client ID not supported for this endpoint, create MyAnimeList object with either an Authenticator or Token"),
                        limit,
                        offset,
                        convertFields(Fields.anime, fields),
                        nsfw
                    ),
                    iterator -> asAnime(MyAnimeListImpl.this, iterator.getJsonObject("node"))
                );
            }

        };
    }

    @Override
    public final AnimeListUpdate updateAnimeListing(final long id){
        return new AnimeListUpdate(id){

            @Override
            public synchronized final AnimeListStatus update(){
                final JsonObject response = handleResponse(
                    () -> service.updateAnimeListing(
                        Objects.requireNonNull(token, "Client ID not supported for this endpoint, create MyAnimeList object with either an Authenticator or Token"),
                        id,
                        status,
                        rewatching,
                        score,
                        MyAnimeListSchema.asYMD(startDate),
                        MyAnimeListSchema.asYMD(finishDate),
                        watchedEpisodes,
                        priority,
                        timesRewatched,
                        rewatchValue,
                        toCommaSeparatedString(tags),
                        comments
                    )
                );
                if(response == null) return null;

                return asAnimeListStatus(MyAnimeListImpl.this, response, id);
            }

        };
    }

    @Override
    public synchronized final void deleteAnimeListing(final long id){
        try{
            handleVoidResponse(
                () -> service.deleteAnimeListing(
                    Objects.requireNonNull(token, "Client ID not supported for this endpoint, create MyAnimeList object with either an Authenticator or Token"),
                    (int) id
                )
            );
        }catch(final HttpException e){
            if(e.code() != 404)
                throw e;
        }
    }

    @Override
    public final UserAnimeListQuery getUserAnimeListing(){
        return getUserAnimeListing("@me");
    }

    @Override
    public final UserAnimeListQuery getUserAnimeListing(final String username){
        return new UserAnimeListQuery(Objects.requireNonNull(username, "Username cannot be null" )){

            @Override
            public final List search(){
                final JsonObject response = handleResponse(
                    () -> service.getUserAnimeListing(
                        isTokenAuth ? token : null,
                        !isTokenAuth ? client_id : null,
                        username.equals("@me") ? "@me" : APICall.encodeUTF8(username),
                        status,
                        sort,
                        limit,
                        offset,
                        convertFields(Fields.anime, fields),
                        nsfw
                    )
                );
                if(response == null) return null;

                final List anime = new ArrayList<>();
                final JsonObject[] arr = response.getJsonArray("data");
                if(arr == null) return null;
                for(final JsonObject iterator : arr)
                    anime.add(asAnimeListStatus(MyAnimeListImpl.this, iterator.getJsonObject("list_status"), asAnime(MyAnimeListImpl.this, iterator.getJsonObject("node"))));
                return anime;
            }

            @Override
            public final PaginatedIterator searchAll(){
                return new PagedIterator<>(
                    offset,
                    offset -> service.getUserAnimeListing(
                        isTokenAuth ? token : null,
                        !isTokenAuth ? client_id : null,
                        username.equals("@me") ? "@me" : APICall.encodeUTF8(username),
                        status,
                        sort,
                        limit,
                        offset,
                        convertFields(Fields.anime, fields),
                        nsfw
                    ),
                    iterator -> asAnimeListStatus(MyAnimeListImpl.this, iterator.getJsonObject("list_status"), asAnime(MyAnimeListImpl.this, iterator.getJsonObject("node")))
                );
            }

        };
    }

    @Override
    public final Character getCharacter(final long id){
        return getCharacter(id, (String[]) null);
    }

    @Override
    public final Character getCharacter(final long id, final String... fields){
        checkExperimentalFeatureEnabled(ExperimentalFeature.CHARACTERS);
        return asCharacter(MyAnimeListImpl.this, getCharacterSchema(id, fields));
    }

    final JsonObject getCharacterSchema(final long id, final String... fields){
        return handleResponse(
            () -> service.getCharacter(
                isTokenAuth ? token : null,
                !isTokenAuth ? client_id : null,
                id,
                convertFields(Fields.character, fields)
            )
        );
    }

    @Override
    public final List getForumBoards(){
        final JsonObject response = handleResponse(
            () -> service.getForumBoards(
                isTokenAuth ? token : null,
                !isTokenAuth ? client_id : null
            )
        );
        if(response == null) return null;

        final List categories = new ArrayList<>();
        final JsonObject[] arr = response.getJsonArray("categories");
        if(arr == null) return null;
        for(final JsonObject iterator : arr)
            categories.add(asForumCategory(MyAnimeListImpl.this, iterator));
        return categories;
    }

    @Override
    public final ForumTopicDetail getForumTopicDetail(final long id){
        return getForumTopicDetail(id, null, null);
    }

    @Override
    public final ForumTopicDetail getForumTopicDetail(final long id, final Integer limit){
        return getForumTopicDetail(id, limit, null);
    }

    @Override
    public final ForumTopicDetail getForumTopicDetail(final long id, final Integer limit, final Integer offset){
        final JsonObject response = handleResponse(
            () -> service.getForumBoard(
                isTokenAuth ? token : null,
                !isTokenAuth ? client_id : null,
                id,
                limit,
                offset
            )
        );
        return response != null ? asForumTopic(MyAnimeListImpl.this, response.getJsonObject("data"), id) : null;
    }

    @Override
    public final ForumTopicDetailPostQuery getForumTopicDetailPostQuery(final long id){
        return new ForumTopicDetailPostQuery(){

            @Override
            public final List search(){
                final JsonObject response = handleResponse(
                    () -> service.getForumBoard(
                        isTokenAuth ? token : null,
                        !isTokenAuth ? client_id : null,
                        id,
                        limit,
                        offset
                    )
                );
                if(response == null) return null;

                final List posts = new ArrayList<>();
                final JsonObject[] arr = response.getJsonObject("data").getJsonArray("posts");
                if(arr == null) return null;
                for(final JsonObject iterator : arr)
                    posts.add(asPost(MyAnimeListImpl.this, iterator, id));
                return posts;
            }

            @Override
            public final PaginatedIterator searchAll(){
                return new PagedIterator<>(
                    offset,
                    offset -> service.getForumBoard(
                        isTokenAuth ? token : null,
                        !isTokenAuth ? client_id : null,
                        id,
                        limit,
                        offset
                    ),
                    iterator -> asPost(MyAnimeListImpl.this, iterator, id)
                );
            }

        };
    }

    @Override
    public final ForumSearchQuery getForumTopics(){
        return new ForumSearchQuery(){

            @Override
            public final List search(){
                final JsonObject response = handleResponse(
                    () -> service.getForumTopics(
                        isTokenAuth ? token : null,
                        !isTokenAuth ? client_id : null,
                        boardId,
                        subboardId,
                        limit,
                        offset,
                        sort.field(),
                        query,
                        topicUsername,
                        username
                    )
                );
                if(response == null) return null;

                final List topics = new ArrayList<>();
                final JsonObject[] arr = response.getJsonArray("data");
                if(arr == null) return null;
                for(final JsonObject iterator : arr)
                    topics.add(asForumTopicDetail(MyAnimeListImpl.this, iterator, boardId, subboardId));
                return topics;
            }

            @Override
            public final PaginatedIterator searchAll(){
                return new PagedIterator<>(
                    offset,
                    offset -> service.getForumTopics(
                        isTokenAuth ? token : null,
                        !isTokenAuth ? client_id : null,
                        boardId,
                        subboardId,
                        limit,
                        offset,
                        sort.field(),
                        query,
                        topicUsername,
                        username
                    ),
                    iterator -> asForumTopicDetail(MyAnimeListImpl.this, iterator, boardId, subboardId)
                );
            }

        };
    }

    @Override
    public final MangaSearchQuery getManga(){
        return new MangaSearchQuery(){

            @Override
            public final List search(){
                final JsonObject response = handleResponse(
                    () -> service.getManga(
                        isTokenAuth ? token : null,
                        !isTokenAuth ? client_id : null,
                        query,
                        limit,
                        offset,
                        convertFields(Fields.manga, fields),
                        nsfw
                    )
                );
                if(response == null) return null;

                final List manga = new ArrayList<>();
                final JsonObject[] arr = response.getJsonArray("data");
                if(arr == null) return null;
                for(final JsonObject iterator : arr)
                    manga.add(asManga(MyAnimeListImpl.this, iterator.getJsonObject("node")));
                return manga;
            }

            @Override
            public final PaginatedIterator searchAll(){
                return new PagedIterator<>(
                    offset,
                    offset -> service.getManga(
                        isTokenAuth ? token : null,
                        !isTokenAuth ? client_id : null,
                        query,
                        limit,
                        offset,
                        convertFields(Fields.manga, fields),
                        nsfw
                    ),
                    iterator -> asManga(MyAnimeListImpl.this, iterator.getJsonObject("node"))
                );
            }

        };
    }

    @Override
    public final Manga getManga(final long id){
        return getManga(id, (String[]) null);
    }

    @Override
    public final Manga getManga(final long id, final String... fields){
        return asManga(this, getMangaSchema(id, fields));
    }

    final JsonObject getMangaSchema(final long id, final String... fields){
        return handleResponse(
            () -> service.getManga(
                isTokenAuth ? token : null,
                !isTokenAuth ? client_id : null,
                id,
                convertFields(Fields.manga, fields)
            )
        );
    }

    @Override
    public final MangaRankingQuery getMangaRanking(final MangaRankingType rankingType){
        return getMangaRanking(Objects.requireNonNull(rankingType, "Ranking type cannot be null").field());
    }

    @Override
    public final MangaRankingQuery getMangaRanking(final String rankingType){
        return new MangaRankingQuery(Objects.requireNonNull(rankingType, "Ranking type cannot be null")){

            @Override
            public final List search(){
                final JsonObject response = handleResponse(
                    () -> service.getMangaRanking(
                        isTokenAuth ? token : null,
                        !isTokenAuth ? client_id : null,
                        rankingType,
                        limit,
                        offset,
                        convertFields(Fields.manga, fields),
                        nsfw
                    )
                );
                if(response == null) return null;

                final List manga = new ArrayList<>();
                final JsonObject[] arr = response.getJsonArray("data");
                if(arr == null) return null;
                for(final JsonObject iterator : arr)
                    manga.add(asMangaRanking(MyAnimeListImpl.this, iterator));
                return manga;
            }

            @Override
            public final PaginatedIterator searchAll(){
                return new PagedIterator<>(
                    offset,
                    offset -> service.getMangaRanking(
                        isTokenAuth ? token : null,
                        !isTokenAuth ? client_id : null,
                        rankingType,
                        limit,
                        offset,
                        convertFields(Fields.manga, fields),
                        nsfw
                    ),
                    iterator -> asMangaRanking(MyAnimeListImpl.this, iterator)
                );
            }

        };
    }

    @Override
    public final MangaListUpdate updateMangaListing(final long id){
        return new MangaListUpdate(id){

            @Override
            public synchronized final MangaListStatus update(){
                final JsonObject response = handleResponse(
                    () -> service.updateMangaListing(
                        Objects.requireNonNull(token, "Client ID not supported for this endpoint, create MyAnimeList object with either an Authenticator or Token"),
                        id,
                        status,
                        rereading,
                        score,
                        MyAnimeListSchema.asYMD(startDate),
                        MyAnimeListSchema.asYMD(finishDate),
                        volumesRead,
                        chaptersRead,
                        priority,
                        timesReread,
                        rereadValue,
                        toCommaSeparatedString(tags),
                        comments
                    )
                );
                if(response == null) return null;

                return asMangaListStatus(MyAnimeListImpl.this, response, id);
            }

        };
    }

    @Override
    public synchronized final void deleteMangaListing(final long id){
        try{
            handleVoidResponse(
                () -> service.deleteMangaListing(
                    Objects.requireNonNull(token, "Client ID not supported for this endpoint, create MyAnimeList object with either an Authenticator or Token"),
                    id
                )
            );
        }catch(final HttpException e){
            if(e.code() != 404)
                throw e;
        }
    }

    @Override
    public final UserMangaListQuery getUserMangaListing(){
        return getUserMangaListing("@me");
    }

    @Override
    public final UserMangaListQuery getUserMangaListing(final String username){
        return new UserMangaListQuery(Objects.requireNonNull(username, "Username cannot be null")){

            @Override
            public final List search(){
                final JsonObject response = handleResponse(
                    () -> service.getUserMangaListing(
                        isTokenAuth ? token : null,
                        !isTokenAuth ? client_id : null,
                        username.equals("@me") ? "@me" : APICall.encodeUTF8(username),
                        status,
                        sort,
                        limit,
                        offset,
                        convertFields(Fields.manga, fields),
                        nsfw
                    )
                );
                if(response == null) return null;

                final List manga = new ArrayList<>();
                final JsonObject[] arr = response.getJsonArray("data");
                if(arr == null) return null;
                for(final JsonObject iterator : arr)
                    manga.add(asMangaListStatus(MyAnimeListImpl.this, iterator.getJsonObject("list_status"), asManga(MyAnimeListImpl.this, iterator.getJsonObject("node"))));
                return manga;
            }

            @Override
            public final PaginatedIterator searchAll(){
                return new PagedIterator<>(
                    offset,
                    offset -> service.getUserMangaListing(
                        isTokenAuth ? token : null,
                        !isTokenAuth ? client_id : null,
                        username.equals("@me") ? "@me" : APICall.encodeUTF8(username),
                        status,
                        sort,
                        limit,
                        offset,
                        convertFields(Fields.manga, fields),
                        nsfw
                    ),
                    iterator -> asMangaListStatus(MyAnimeListImpl.this, iterator.getJsonObject("list_status"), asManga(MyAnimeListImpl.this, iterator.getJsonObject("node")))
                );
            }

        };
    }

    @Override
    public final Person getPerson(final long id){
        return getPerson(id, (String[]) null);
    }

    @Override
    public final Person getPerson(final long id, final String... fields){
        checkExperimentalFeatureEnabled(ExperimentalFeature.PEOPLE);
        return asPerson(this, handleResponse(
            () -> service.getPerson(
                isTokenAuth ? token : null,
                !isTokenAuth ? client_id : null,
                id,
                convertFields(Fields.people, fields)
            )
        ));
    }

    @Override
    public final User getAuthenticatedUser(){
        return getUser("@me", (String[]) null);
    }

    @Override
    public final User getAuthenticatedUser(final String... fields){
        return getUser("@me", fields);
    }

    @Override
    public final User getUser(final String username){
        return getUser(username, (String[]) null);
    }

    @Override
    public final User getUser(final String username, final String... fields){
        Objects.requireNonNull(username, "Username cannot be null");
        return asUser(this, handleResponse(
            () -> service.getUser(
                Objects.requireNonNull(token, "Client ID not supported for this endpoint, create MyAnimeList object with either an Authenticator or Token"),
                username.equals("@me") ? "@me" : APICall.encodeUTF8(username),
                convertFields(Fields.user, fields)
            )
        ));
    }

// handle response

    private static void handleVoidResponse(final ExceptionSupplier,IOException> supplier){
        handleResponseCodes(supplier);
    }

    private static JsonObject handleResponse(final ExceptionSupplier,IOException> supplier){
        final Response response = handleResponseCodes(supplier);
        return response.code() == HttpURLConnection.HTTP_OK ? (JsonObject) response.body() : null;
    }

    private static Response handleResponseCodes(final ExceptionSupplier,IOException> supplier){
        try{
            final Response response = supplier.get();
            switch(response.code()){
                case HttpURLConnection.HTTP_OK:
                    return response;
                case HttpURLConnection.HTTP_UNAUTHORIZED:
                    throw new InvalidTokenException("The OAuth token provided is either invalid or expired");
                default:
                    try{
                        throw new HttpException(response.URL(), response.code(), (((JsonObject) response.body()).getString("message") + ' ' + ((JsonObject) response.body()).getString("error")).trim());
                    }catch(final Throwable ignored){
                        throw new HttpException(response.URL(), response.code(), response.raw());
                    }
            }
        }catch(final IOException e){ // client side failure
            throw new UncheckedIOException(e);
        }
    }

    private interface ExceptionSupplier{

        T get() throws E;

    }

    @Override
    public final String toString(){
        return "MyAnimeList{" +
               "authenticator=" + authenticator +
               ", service=" + service +
               '}';
    }

// iterator

    private static class PagedIterator extends PaginatedIterator {

        private final Function> fullPageSupplier;
        private final Function listAdapter;

        private final AtomicReference nextOffset = new AtomicReference<>();

        private JsonObject nextPage = null; // pre-fetch next page #404

        PagedIterator(
            final Integer offset,
            final Function> fullPageSupplier,
            final Function listAdapter
        ){
            this.fullPageSupplier   = fullPageSupplier;
            this.listAdapter        = listAdapter;

            // handle first page
            nextOffset.set(offset);
            list = getNextPage();
            size = list == null ? 0 : list.size();
        }

        @Override
        final boolean hasNextPage(){
            return nextOffset.get() != -1;
        }

        @SuppressWarnings("DataFlowIssue")
        @Override
        synchronized final List getNextPage(){
            // use pre-fetched next page or fetch the first page
            final JsonObject response = nextPage != null ? nextPage : handleResponse(() -> fullPageSupplier.apply(nextOffset.get()));

            if(response == null){
                nextOffset.set(-1);
                return null;
            }

            // convert response to list
            final List list = new ArrayList<>();
            for(final JsonObject data :
                response.get("data") instanceof JsonObject && response.getJsonObject("data").containsKey("posts") // post iterator support
                ? response.getJsonObject("data").getJsonArray("posts")
                : response.getJsonArray("data")
            )
                list.add(listAdapter.apply(data));

            // handle next offset
            if(response.getJsonObject("paging").containsKey("next")){
                final Integer b4 = nextOffset.get();
                nextOffset.set((b4 == null ? 0 : b4) + list.size());

                // peek into next page #404
                nextPage = handleResponse(() -> fullPageSupplier.apply(nextOffset.get()));

                if( // check if next page actually has any content
                    (nextPage.get("data") instanceof JsonObject && nextPage.getJsonObject("data").containsKey("posts") // post iterator support
                    ? nextPage.getJsonObject("data").getJsonArray("posts")
                    : nextPage.getJsonArray("data"))
                    .length > 0
                )
                    return list;
            }

            // last page
            nextPage = null;
            nextOffset.set(-1);

            return list;
        }

    }

// formatter

    private static String toCommaSeparatedString(final List fields){
        return toCommaSeparatedString(fields == null ? null : fields.toArray(new String[0]));
    }

    private static String toCommaSeparatedString(final String... fields){
        if(fields != null){
            if(fields.length == 0) return ""; // return blank for empty array

            final StringBuilder SB = new StringBuilder();
            for(final String field : fields)
                if(field.trim().length() > 0)
                    SB.append(field).append(',');
            return SB.toString().endsWith(",") ? SB.deleteCharAt(SB.length()-1).toString() : "";
        }
        return null;
    }

    private static final String inverted = "^%s$|^%s(?=,)|(?<=\\w)\\{%s}|(?:^|,)%s\\{.*?}|,%s|(?<=\\{)%s,";

    private static String convertFields(final String defaultFields, final List fields){
        return convertFields(defaultFields, fields == null ? null : fields.toArray(new String[0]));
    }

    /**
     * Converts field array to comma separated string
     *
     * @param defaultFields default fields
     * @param fields fields
     * @return comma separated fields
     */
    private static String convertFields(final String defaultFields, final String... fields){
        if(fields == null || (fields.length == 1 && fields[0].equals(Fields.INVERTED)))
            return defaultFields;
        else if(fields.length == 0)
            return "";

        boolean inverted = false;
        for(final String field : fields){
            if(field.equals(Fields.INVERTED)){
                inverted = true;
                break;
            }
        }

        if(!inverted){
            return toCommaSeparatedString(fields);
        }else{
            String buffer = defaultFields;
            for(final String field : fields) // remove fields that are specified
                buffer = buffer.replaceAll(MyAnimeListImpl.inverted.replace("%s", Pattern.quote(field)), "");
            return buffer;
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy