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

com.afrozaar.wordpress.wpapi.v2.Client Maven / Gradle / Ivy

There is a newer version: 4.8.3
Show newest version
package com.afrozaar.wordpress.wpapi.v2;

import static com.afrozaar.wordpress.wpapi.v2.util.FieldExtractor.extractField;

import static java.lang.String.format;
import static java.net.URLDecoder.decode;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static java.util.Optional.ofNullable;

import com.afrozaar.wordpress.wpapi.v2.api.Contexts;
import com.afrozaar.wordpress.wpapi.v2.exception.ExceptionCodes;
import com.afrozaar.wordpress.wpapi.v2.exception.InvalidParameterException;
import com.afrozaar.wordpress.wpapi.v2.exception.PageNotFoundException;
import com.afrozaar.wordpress.wpapi.v2.exception.ParsedRestException;
import com.afrozaar.wordpress.wpapi.v2.exception.PostCreateException;
import com.afrozaar.wordpress.wpapi.v2.exception.PostNotFoundException;
import com.afrozaar.wordpress.wpapi.v2.exception.TermNotFoundException;
import com.afrozaar.wordpress.wpapi.v2.exception.UserEmailAlreadyExistsException;
import com.afrozaar.wordpress.wpapi.v2.exception.UserNotFoundException;
import com.afrozaar.wordpress.wpapi.v2.exception.UsernameAlreadyExistsException;
import com.afrozaar.wordpress.wpapi.v2.exception.WpApiParsedException;
import com.afrozaar.wordpress.wpapi.v2.model.DeleteResponse;
import com.afrozaar.wordpress.wpapi.v2.model.Link;
import com.afrozaar.wordpress.wpapi.v2.model.Media;
import com.afrozaar.wordpress.wpapi.v2.model.Page;
import com.afrozaar.wordpress.wpapi.v2.model.Post;
import com.afrozaar.wordpress.wpapi.v2.model.PostMeta;
import com.afrozaar.wordpress.wpapi.v2.model.PostStatus;
import com.afrozaar.wordpress.wpapi.v2.model.RenderableField;
import com.afrozaar.wordpress.wpapi.v2.model.Taxonomy;
import com.afrozaar.wordpress.wpapi.v2.model.Term;
import com.afrozaar.wordpress.wpapi.v2.model.User;
import com.afrozaar.wordpress.wpapi.v2.request.Request;
import com.afrozaar.wordpress.wpapi.v2.request.SearchRequest;
import com.afrozaar.wordpress.wpapi.v2.response.CustomRenderableParser;
import com.afrozaar.wordpress.wpapi.v2.response.PagedResponse;
import com.afrozaar.wordpress.wpapi.v2.util.AuthUtil;
import com.afrozaar.wordpress.wpapi.v2.util.MavenProperties;
import com.afrozaar.wordpress.wpapi.v2.util.Tuple2;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;

import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.apache.commons.beanutils.BeanUtils;
import org.assertj.core.util.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import javax.xml.transform.Source;

import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Client implements Wordpress {
    private static final String DEFAULT_CONTEXT = "/wp-json/wp/v2";
    private static final Logger LOG = LoggerFactory.getLogger(Client.class);
    private static final String META_KEY = "key";
    private static final String META_VALUE = "value";
    private static final String FORCE = "force";
    private static final String CONTEXT_ = "context";
    private static final String REASSIGN = "reassign";
    private static final String VIEW = "view";
    private static final String DATA = "data";
    private static final String VERSION = "version";
    private static final String ARTIFACT_ID = "artifactId";

    protected final RestTemplate restTemplate;
    private final Predicate next = link -> Strings.NEXT.equals(link.getRel());
    private final Predicate previous = link -> Strings.PREV.equals(link.getRel());
    private final Tuple2 userAgentTuple;

    public final String context;
    public final String baseUrl;
    public final String username;
    public final String password;
    public final boolean debug;
    public final boolean permalinkEndpoint;
    private Boolean canDeleteMetaViaPost = null;

    {
        Properties properties = MavenProperties.getProperties();
        userAgentTuple = Tuple2.of("User-Agent", format("%s/%s", properties.getProperty(ARTIFACT_ID), properties.getProperty(VERSION)));
    }

    public Client(String baseUrl, String username, String password, boolean usePermalinkEndpoint, boolean debug) {
       this(null, baseUrl, username, password, usePermalinkEndpoint, debug, null);
    }

    public Client(String baseUrl, String username, String password, boolean usePermalinkEndpoint, boolean debug, ClientHttpRequestFactory requestFactory) {
        this(null, baseUrl, username, password, usePermalinkEndpoint, debug, requestFactory);
    }

    public Client(String context, String baseUrl, String username, String password, boolean usePermalinkEndpoint, boolean debug) {
       this(context, baseUrl, username, password, usePermalinkEndpoint, debug, null);
    }

    public Client(String context, String baseUrl, String username, String password, boolean usePermalinkEndpoint, boolean debug,
                  ClientHttpRequestFactory requestFactory) {
        this.context = context;
        this.baseUrl = baseUrl;
        this.username = username;
        this.password = password;
        this.debug = debug;
        this.permalinkEndpoint = usePermalinkEndpoint;

        final ObjectMapper emptyArrayAsNullObjectMapper = Jackson2ObjectMapperBuilder.json().featuresToEnable(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT).build();

        List> messageConverters = new ArrayList<>();
        messageConverters.add(new ByteArrayHttpMessageConverter());
        messageConverters.add(new StringHttpMessageConverter());
        messageConverters.add(new ResourceHttpMessageConverter());
        messageConverters.add(new SourceHttpMessageConverter());
        messageConverters.add(new AllEncompassingFormHttpMessageConverter());
        messageConverters.add(new MappingJackson2HttpMessageConverter(emptyArrayAsNullObjectMapper));
        //messageConverters.add(new MappingJackson2HttpMessageConverter());
        restTemplate = new RestTemplate(messageConverters);

        if (requestFactory != null) {
            restTemplate.setRequestFactory(requestFactory);
        } 
        
    }

    @Override
    public String getContext() {
        return ofNullable(this.context).orElse(DEFAULT_CONTEXT);
    }

    @Override
    public Post createPost(final Map postFields, PostStatus status) throws PostCreateException {
        final ImmutableMap post = new ImmutableMap.Builder().putAll(postFields).put("status", status.value).build();
        try {
            return doExchange1(Request.POSTS, HttpMethod.POST, Post.class, forExpand(), null, post, MediaType.APPLICATION_JSON).getBody();
        } catch (HttpClientErrorException e) {
            throw new PostCreateException(e);
        }
    }

    @Override
    public Post createPost(Post post, PostStatus status) throws PostCreateException {
        return createPost(fieldsFrom(post), status);
    }

    @Override
    public Post getPost(Long id) throws PostNotFoundException {
        return getPost(id, Contexts.VIEW);
    }

    @Override
    public Post getPost(Long id, String context) throws PostNotFoundException {
        try {
            return doExchange1(Request.POST, HttpMethod.GET, Post.class, forExpand(id), ImmutableMap.of(CONTEXT_, context), null).getBody();
        } catch (HttpClientErrorException e) {
            if (e.getStatusCode().is4xxClientError() && e.getStatusCode().value() == 404) {
                throw new PostNotFoundException(e);
            } else {
                throw e;
            }
        }
    }

    @Override
    public Post updatePost(Post post) {
        final ResponseEntity exchange = doExchange1(Request.POST, HttpMethod.PUT, Post.class, forExpand(post.getId()), ImmutableMap.of(), fieldsFrom(post));
        return exchange.getBody();
    }

    @Override
    public Post updatePostField(Long postId, String field, Object value) {
        return doExchange1(Request.POST, HttpMethod.PUT, Post.class, forExpand(postId), null, ImmutableMap.of(field, value)).getBody();
    }

    @Override
    public Post deletePost(Post post) {
        final ResponseEntity exchange = doExchange1(Request.POST, HttpMethod.DELETE, Post.class, forExpand(post.getId()), null, null);// Deletion of a post returns the post's data before removing it.
        Preconditions.checkArgument(exchange.getStatusCode().is2xxSuccessful());
        return exchange.getBody();
    }

    @Override
    public  PagedResponse search(SearchRequest search) {
        final URI uri = search.usingClient(this).build().toUri();
        return getPagedResponse(uri, search.getClazz());
    }

    @SuppressWarnings("unchecked")
    @Override
    public Media createMedia(Media media, Resource resource) throws WpApiParsedException {
        Objects.requireNonNull(resource.getFilename(), "The resource used to create a media item does not provide a filename. Please supply a Resource that overrides getFilename().");

        try {
            final MultiValueMap uploadMap = new LinkedMultiValueMap<>();
            BiConsumer p = (index, value) -> ofNullable(value).ifPresent(v -> uploadMap.add(index, v));

            p.accept("title", extractField(Media::getTitle, media).orElse(null));
            p.accept("post", media.getPost());
            p.accept("alt_text", media.getAltText());
            p.accept("caption", media.getCaption());
            p.accept("description", media.getDescription());

            uploadMap.add("file", resource);

            return CustomRenderableParser.parseMedia(doExchange1(Request.MEDIAS, HttpMethod.POST, String.class, forExpand(), null, uploadMap).getBody());
        } catch (HttpClientErrorException | HttpServerErrorException e) {
            throw WpApiParsedException.of(e);
        }
    }

    @Override
    public Post setPostFeaturedMedia(Long postId, Media media) {
        Preconditions.checkArgument("image".equals(media.getMediaType()), "Can not set non-image media type as a featured image on a post.");
        return updatePostField(postId, "featured_media", media.getId());
    }

    @Override
    public List getPostMedias(Long postId) {
        Media[] medias = CustomRenderableParser.parse(
                doExchange1(
                        Request.MEDIAS, HttpMethod.GET, String.class, forExpand(),
                        ImmutableMap.of("parent", postId, CONTEXT_, Contexts.EDIT), null
                ).getBody(),
                Media[].class);
        return Arrays.asList(medias);
    }

    @Override
    public List getMedia() {
        List collected = new ArrayList<>();
        PagedResponse pagedResponse = this.getPagedResponse(Request.MEDIAS, Media.class);
        collected.addAll(pagedResponse.getList());
        while (pagedResponse.hasNext()) {
            pagedResponse = this.traverse(pagedResponse, PagedResponse.NEXT);
            collected.addAll(pagedResponse.getList());
        }
        return collected;
    }

    @Override
    public Media getMedia(Long id) {
        return CustomRenderableParser.parse(doExchange1(Request.MEDIA, HttpMethod.GET, String.class, forExpand(id), ImmutableMap.of(CONTEXT_, Contexts.EDIT), null), Media.class);
    }

    @Override
    public Media updateMedia(Media media) {
        ImmutableMap.Builder builder = new ImmutableMap.Builder<>();
        BiConsumer p = (key, value) -> ofNullable(value).ifPresent(v -> builder.put(key, v));

        p.accept("title", extractField(Media::getTitle, media).orElse(null));
        p.accept("post", media.getPost());
        p.accept("alt_text", media.getAltText());
        p.accept("caption", media.getCaption());
        p.accept("description", media.getDescription());

        return CustomRenderableParser.parse(doExchange1(Request.MEDIA, HttpMethod.POST, String.class, forExpand(media.getId()), null, builder.build()), Media.class);
    }

    @Override
    public boolean deleteMedia(Media media, boolean force) {
        final ResponseEntity exchange = doExchange1(Request.MEDIA, HttpMethod.DELETE, String.class, forExpand(media.getId()), ImmutableMap.of(FORCE, force), null);
        return exchange.getStatusCode().is2xxSuccessful();
    }

    @Override
    public boolean deleteMedia(Media media) {
        // We don't care to deserialize the received response back into a media object.
        final ResponseEntity exchange = doExchange1(Request.MEDIA, HttpMethod.DELETE, String.class, forExpand(media.getId()), null, null);
        return exchange.getStatusCode().is2xxSuccessful();
    }

    @Override
    public PostMeta createMeta(Long postId, String key, String value) {
        final Map body = ImmutableMap.of(META_KEY, key, META_VALUE, value);
        final ResponseEntity exchange = doExchange1(Request.METAS, HttpMethod.POST, PostMeta.class, forExpand(postId), null, body,
                MediaType.APPLICATION_JSON);
        return exchange.getBody();
    }

    @Override
    public List getPostMetas(Long postId) {
        final ResponseEntity exchange = doExchange1(Request.METAS, HttpMethod.GET, PostMeta[].class, forExpand(postId), null, null);
        return Arrays.asList(exchange.getBody());
    }

    @Override
    public PostMeta getPostMeta(Long postId, Long metaId) {
        final ResponseEntity exchange = doExchange1(Request.META, HttpMethod.GET, PostMeta.class, forExpand(postId, metaId), null, null);
        return exchange.getBody();
    }

    @Override
    public PostMeta updatePostMetaValue(Long postId, Long metaId, String value) {
        return updatePostMeta(postId, metaId, null, value);
    }

    @Override
    public PostMeta updatePostMeta(Long postId, Long metaId, String key, String value) {
        ImmutableMap.Builder builder = new ImmutableMap.Builder<>();
        BiConsumer biConsumer = (key1, value1) -> ofNullable(value1).ifPresent(v -> builder.put(key1, v));

        biConsumer.accept(META_KEY, key);
        biConsumer.accept(META_VALUE, value);
        final ResponseEntity exchange = doExchange1(Request.META, HttpMethod.POST, PostMeta.class, forExpand(postId, metaId), null, builder.build());

        return exchange.getBody();
    }

    private BiFunction supportsMetaDeleteViaPostMethod = (pid, mid) -> {
        if (nonNull(canDeleteMetaViaPost)) {
            return canDeleteMetaViaPost;
        }

        try {
            Function expected = map -> nonNull(map) && Stream.of("endpoints", "methods", "namespace").allMatch(map::containsKey) && Objects.equals(((ArrayList) map.get("methods")).get(0), "POST");

            final ResponseEntity responseEntity = doExchange1(Request.META_POST_DELETE, HttpMethod.OPTIONS, Map.class, forExpand(pid, mid), null, null);
            final Map body = responseEntity.getBody();
            canDeleteMetaViaPost = responseEntity.getStatusCode().is2xxSuccessful() && expected.apply(body);
            LOG.info("Wordpress instance at {} supports deleting meta via POST /posts/:pid/meta/:mid/delete : {}", Client.this.baseUrl, canDeleteMetaViaPost);
            return canDeleteMetaViaPost;
        } catch (Exception jme) {
            canDeleteMetaViaPost = false;

            //com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of java.util.LinkedHashMap out of START_ARRAY token
            if (!(jme instanceof JsonMappingException)) {
                LOG.error("Unexpected exception pinging for POST /posts/:pid/meta/:mid/delete", jme);
            }

            return canDeleteMetaViaPost;
        }
    };

    @Override
    public boolean deletePostMeta(Long postId, Long metaId) {
        return deletePostMeta(postId, metaId, null);
    }

    @Override
    public boolean deletePostMeta(Long postId, Long metaId, Boolean force) {
        if (supportsMetaDeleteViaPostMethod.apply(postId, metaId)) {
            // deleting meta via meta POST is available, so use that to delete.
            final ResponseEntity result = doExchange1(Request.META_POST_DELETE, HttpMethod.POST, Map.class, forExpand(postId, metaId), isNull(force) ? null : ImmutableMap.of(FORCE, force), null);
            return result.getStatusCode().is2xxSuccessful() && "Deleted meta".equals(result.getBody().get("message"));
        } else {
            // attempt normal delete
            final ResponseEntity exchange = doExchange1(Request.META, HttpMethod.DELETE, Map.class, forExpand(postId, metaId), isNull(force) ? null : ImmutableMap.of(FORCE, force), null);
            Preconditions.checkArgument(exchange.getStatusCode().is2xxSuccessful(), format("Expected success on post meta delete request: /posts/%s/meta/%s", postId, metaId));

            return exchange.getStatusCode().is2xxSuccessful();
        }
    }

    @SuppressWarnings("unchecked")
    @Override
    public List getTaxonomies() {
        final ResponseEntity exchange = doExchange1(Request.TAXONOMIES, HttpMethod.GET, Map.class, forExpand(), null, null);

        final Map body = exchange.getBody();
        List toReturn = new ArrayList<>();
        body.forEach((key, obj) -> {
            try {
                Taxonomy target = new Taxonomy();
                Map source = (Map) obj;
                BeanUtils.populate(target, source);
                toReturn.add(target);
            } catch (IllegalAccessException | InvocationTargetException e) {
                LOG.error("Error ", e);
            }
        });
        return toReturn;
    }

    @Override
    public Taxonomy getTaxonomy(String slug) {
        return doExchange1(Request.TAXONOMY, HttpMethod.GET, Taxonomy.class, forExpand(slug), null, null).getBody();
    }

    @Override
    public List getTerms(String taxonomy) {
        List collected = new ArrayList<>();
        PagedResponse pagedResponse = this.getPagedResponse(Request.TERMS, Term.class, taxonomy);
        collected.addAll(pagedResponse.getList());
        while (pagedResponse.hasNext()) {
            pagedResponse = this.traverse(pagedResponse, PagedResponse.NEXT);
            collected.addAll(pagedResponse.getList());
        }
        return collected;
    }

    @Override
    public Term getTerm(String taxonomy, Long id) throws TermNotFoundException {
        try {
            return doExchange1(Request.TERM, HttpMethod.GET, Term.class, forExpand(taxonomy, id), null, null).getBody();
        } catch (HttpClientErrorException e) {
            if (e.getStatusCode().is4xxClientError() && e.getStatusCode().value() == 404) {
                throw new TermNotFoundException(e);
            } else {
                throw e;
            }
        }
    }

    @Override
    public Term updateTerm(String taxonomy, Term term) {
        return doExchange1(Request.TERM, HttpMethod.POST, Term.class, forExpand(taxonomy, term.getId()), null, term.asMap()).getBody();
    }

    @Override
    public Term deleteTerm(String taxonomy, Term term) throws TermNotFoundException {
        try {
            return doExchange1(Request.TERM, HttpMethod.DELETE, Term.class, forExpand(taxonomy, term.getId()), null, null).getBody();
        } catch (HttpClientErrorException e) {
            if (e.getStatusCode().is4xxClientError() && e.getStatusCode().value() == 404) {
                throw new TermNotFoundException(e);
            } else {
                throw e;
            }
        }
    }

    @Override
    public List deleteTerms(String taxonomy, Term... terms) {
        List deletedTerms = new ArrayList<>(terms.length);

        for (Term term : terms) {
            try {
                deletedTerms.add(deleteTerm(taxonomy, term));
            } catch (TermNotFoundException e) {
                LOG.error("Error ", e);
            }
        }

        return deletedTerms;
    }

    @Override
    public Term createTag(Term tagTerm) throws WpApiParsedException {
        try {
            return doExchange1(Request.TAGS, HttpMethod.POST, Term.class, forExpand(), tagTerm.asMap(), null).getBody();
        } catch (HttpClientErrorException | HttpServerErrorException e) {
            final WpApiParsedException exception = WpApiParsedException.of(e);
            LOG.error("Could not create tag '{}'. {} ", tagTerm.getName(), exception.getMessage(), exception);
            throw exception;
        }
    }

    @Override
    public List getTags() {
        return getAllTermsForEndpoint(Request.TAGS);
    }

    @Override
    public Term getTag(Long id) throws TermNotFoundException {
        try {
            return doExchange1(Request.TAG, HttpMethod.GET, Term.class, forExpand(id), null, null).getBody();
        } catch (HttpClientErrorException e) {
            if (e.getStatusCode().is4xxClientError() && e.getStatusCode().value() == 404) {
                throw new TermNotFoundException(e);
            } else {
                throw e;
            }
        }
    }

    @Override
    public Term deleteTag(Term tagTerm) throws TermNotFoundException {
        return deleteTag(tagTerm, false);
    }

    @Override
    public Term deleteTag(Term tagTerm, boolean force) throws TermNotFoundException {
        try {
            Map queryParams = force ? ImmutableMap.of("force", true) : null;

            final ResponseEntity tResponseEntity = doExchange1(Request.TAG, HttpMethod.DELETE, String.class, forExpand(tagTerm.getId()), queryParams, null);
            final DeleteResponse termDeleteResponse = CustomRenderableParser.parseDeleteResponse(tResponseEntity, Term.class);
            final Term previous = termDeleteResponse.getPrevious();
            LOG.debug("Deleted term @{}/'{}' of taxonomy '{}': {}", previous.getId(), previous.getName(), previous.getTaxonomySlug(), termDeleteResponse.getDeleted());
            return previous;
        } catch (HttpClientErrorException e) {
            if (e.getStatusCode().is4xxClientError() && e.getStatusCode().value() == 404) {
                throw new TermNotFoundException(e);
            } else {
                throw e;
            }
        }
    }

    @Override
    public Term createPostTag(Post post, Term tag) throws WpApiParsedException {
        final Term termToUse = nonNull(tag.getId()) ? tag : createTag(tag);
        final List postTags = new ArrayList<>(getPostTags(post));
        postTags.add(termToUse);
        final List tagIds = postTags.stream().map(Term::getId).collect(Collectors.toList());
        //Map body = ImmutableMap.of("tags", tagIds);
        updatePostField(post.getId(), "tags", tagIds);
        return termToUse;
        //return doExchange1(Request.POST, HttpMethod.POST, Term.class, forExpand(post.getId(), null, termToUse.getId()), null, body).getBody();
    }

    @Override
    public List getPostTags(Post post) {
        return getAllTermsForEndpoint(Request.POST_TAGS, post.getId().toString());
    }

    @Override
    public Term deletePostTag(Post post, Term tagTerm, boolean force) throws TermNotFoundException {
        try {
            final List postTags = new ArrayList<>(getPostTags(post));
            final Optional found = postTags.stream().filter(term -> Objects.equals(term.getId(), tagTerm.getId())).findFirst();

            if (found.isPresent()) {
                postTags.remove(found.get());
                updatePostField(post.getId(), "tags", termIds.apply(postTags));
                return tagTerm;
            } else {
                throw new RuntimeException("Expected to find term in post's term list.");
            }

            //return doExchange1(Request.POST_TERM, HttpMethod.DELETE, Term.class, forExpand(post.getId(), Taxonomies.TAGS, tagTerm.getId()), ImmutableMap.of(FORCE, force), null).getBody();
        } catch (HttpClientErrorException e) {
            if (e.getStatusCode().is4xxClientError() && e.getStatusCode().value() == 404) {
                throw new TermNotFoundException(e);
            } else {
                throw e;
            }
        }
    }

    @Override
    public Term getPostTag(Post post, Term tagTerm) throws TermNotFoundException {
        try {
            return doExchange1(Request.POST_TERM, HttpMethod.GET, Term.class, forExpand(post.getId(), TAGS, tagTerm.getId()), null, null).getBody();
        } catch (HttpClientErrorException e) {
            if (e.getStatusCode().is4xxClientError() && e.getStatusCode().value() == 404) {
                throw new TermNotFoundException(e);
            } else {
                throw e;
            }
        }
    }

    @Override
    public Term updateTag(Term tag) {
        return doExchange1(Request.TAG, HttpMethod.POST, Term.class, forExpand(tag.getId()), null, tag.asMap()).getBody();
    }

    @Override
    public Term getCategory(Long id) {
        return doExchange1(Request.CATEGORY, HttpMethod.GET, Term.class, forExpand(id), null, null).getBody();
    }

    @Override
    public List getCategories() {
        return getAllTermsForEndpoint(Request.CATEGORIES);
    }

    private List getAllTermsForEndpoint(final String endpoint, String... expandParams) {
        List collected = new ArrayList<>();
        PagedResponse pagedResponse = this.getPagedResponse(endpoint, Term.class, expandParams);
        collected.addAll(pagedResponse.getList());
        while (pagedResponse.hasNext()) {
            pagedResponse = this.traverse(pagedResponse, PagedResponse.NEXT);
            collected.addAll(pagedResponse.getList());
        }
        return collected;
    }

    @Override
    public Term createCategory(Term categoryTerm) {
        return doExchange1(Request.CATEGORIES, HttpMethod.POST, Term.class, forExpand(), null, categoryTerm.asMap(), MediaType.APPLICATION_JSON).getBody();
    }

    @Override
    public Term deleteCategory(Term categoryTerm) throws TermNotFoundException {
        return deleteCategory(categoryTerm, false);
    }

    @Override
    public Term deleteCategory(Term categoryTerm, boolean force) throws TermNotFoundException {
        try {
            Map queryParams = force ? ImmutableMap.of("force", true) : null;
            return doExchange1(Request.CATEGORY, HttpMethod.DELETE, Term.class, forExpand(categoryTerm.getId()), queryParams, null).getBody();
        } catch (HttpClientErrorException e) {
            if (e.getStatusCode().is4xxClientError() && e.getStatusCode().value() == 404) {
                throw new TermNotFoundException(e);
            } else {
                throw e;
            }
        }
    }

    @Override
    public List deleteCategories(Term... terms) {
        return deleteCategories(false, terms);
    }

    @Override
    public List deleteCategories(boolean force, Term... terms) {
        List deletedTerms = new ArrayList<>(terms.length);

        for (Term term : terms) {
            try {
                deletedTerms.add(deleteCategory(term, force));
            } catch (TermNotFoundException e) {
                LOG.error("Error ", e);
            }
        }

        return deletedTerms;
    }

    @Override
    public Page createPage(Page page, PostStatus status) {
        final Map map = page.asMap();
        final ImmutableMap pageFields = new ImmutableMap.Builder().putAll(map).put("status", status.value).build();

        return doExchange1(Request.PAGES, HttpMethod.POST, Page.class, forExpand(), null, pageFields).getBody();
    }

    @Override
    public Page getPage(Long pageId) throws PageNotFoundException {
        try {
            return getPage(pageId, VIEW);
        } catch (HttpClientErrorException e) {
            throw new PageNotFoundException(e);
        }
    }

    @Override
    public Page getPage(Long pageId, String context) {
        return doExchange1(Request.PAGE, HttpMethod.GET, Page.class, forExpand(pageId), ImmutableMap.of(CONTEXT_, context), null).getBody();
    }

    @Override
    public Page updatePage(Page page) {
        return doExchange1(Request.PAGE, HttpMethod.POST, Page.class, forExpand(page.getId()), null, page.asMap()).getBody();
    }

    @Override
    public Page deletePage(Page page) {
        return doExchange1(Request.PAGE, HttpMethod.DELETE, Page.class, forExpand(page.getId()), null, null).getBody();
    }

    @Override
    public Page deletePage(Page page, boolean force) {
        return doExchange1(Request.PAGE, HttpMethod.DELETE, Page.class, forExpand(page.getId()), ImmutableMap.of(FORCE, force), null).getBody();
    }

    @Override
    public List getUsers() {
        return getUsers(Contexts.VIEW);
    }

    @Override
    public List getUsers(final String contextType) {
        List collected = new ArrayList<>();
        PagedResponse usersResponse = this.getPagedResponse(Request.USERS_WITH_CONTEXT, User.class, contextType);
        collected.addAll(usersResponse.getList());
        while (usersResponse.hasNext()) {
            usersResponse = traverse(usersResponse, PagedResponse.NEXT);
            collected.addAll(usersResponse.getList());
        }
        return collected;
    }

    @SuppressWarnings("unchecked")
    @Override
    public User createUser(User user, String username, String password) throws WpApiParsedException {

        final MultiValueMap userAsMap = userMap.apply(user);
        userAsMap.add("username", username); // Required: true
        userAsMap.add("password", password); // Required: true
        try {
            return doExchange1(Request.USERS, HttpMethod.POST, User.class, forExpand(), null, userAsMap).getBody();
        } catch (HttpServerErrorException | HttpClientErrorException e) {
            try {
                ParsedRestException restException = ParsedRestException.of(e);
                switch (restException.getCode()) {
                case ExceptionCodes.INVALID_PARAM:
                    throw new InvalidParameterException(restException);
                case ExceptionCodes.EXISTING_USER_LOGIN:
                    throw new UsernameAlreadyExistsException(restException);
                case ExceptionCodes.EXISTING_USER_EMAIL:
                    throw new UserEmailAlreadyExistsException(restException);
                }
            } catch (RuntimeException rte) {
                LOG.info("error parsing {}", e.getResponseBodyAsString(), e);
            }
            throw e;
        }

    }

    @Override
    public User getUser(long userId) throws UserNotFoundException {
        return getUser(userId, null);
    }

    @Override
    public User getUser(long userId, String context) throws UserNotFoundException {
        final Map params = context == null ? null : ImmutableMap.of(CONTEXT_, context);
        try {
            return doExchange1(Request.USER, HttpMethod.GET, User.class, forExpand(userId), params, null).getBody();
        } catch (HttpClientErrorException e) {
            if (e.getStatusCode().is4xxClientError() && e.getStatusCode().value() == 404) {
                throw new UserNotFoundException(e);
            } else {
                throw e;
            }
        }
    }

    @Override
    public User deleteUser(User user) {
        return deleteUser(user, null);
    }

    @Override
    public User deleteUser(User user, Long reassign) {
        try {
            return doExchange1(Request.USER, HttpMethod.DELETE, User.class, forExpand(user.getId()), ImmutableMap.of(FORCE, true, REASSIGN, (nonNull(reassign) ? reassign : 1L)), null).getBody();
        } catch (HttpClientErrorException | HttpServerErrorException e) {
            final WpApiParsedException of = WpApiParsedException.of(e);
            LOG.error("Error Deleting user {}", user.getId(), of);
            throw new RuntimeException(of);
        }
    }

    @Override
    public User updateUser(User user) {
        return doExchange1(Request.USER, HttpMethod.POST, User.class, forExpand(user.getId()), null, userMap.apply(user)).getBody();
    }

    @SuppressWarnings("unchecked")
    @Override
    public  PagedResponse getPagedResponse(String context, Class typeRef, String... expandParams) {
        final URI uri = Request.of(context).usingClient(this).buildAndExpand((Object[]) expandParams).toUri();
        return getPagedResponse(uri, typeRef);
    }

    @SuppressWarnings("unchecked")
    @Override
    public  PagedResponse getPagedResponse(final URI uri, Class typeRef) {
        try {

            final ResponseEntity exchange = doExchange0(HttpMethod.GET, uri, String.class, null, null);
            final String body1 = exchange.getBody();
            //LOG.debug("about to parse response for paged response {}: {}", typeRef, body1);
            final T[] parse = CustomRenderableParser.parse(body1, (Class) Class.forName("[L" + typeRef.getName() + ";"));

            final HttpHeaders headers = exchange.getHeaders();
            final List links = parseLinks(headers);

            final List body = Arrays.asList(parse); // Ugly... but the only way to get the generic stuff working

            return PagedResponse.Builder.aPagedResponse(typeRef)
                    .withPages(headers)
                    .withPosts(body)
                    .withSelf(uri.toASCIIString())
                    .withNext(link(links, next))
                    .withPrevious(link(links, previous))
                    .build();
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    @SuppressWarnings("unchecked")
    @Override
    public  PagedResponse traverse(PagedResponse response, Function, String> direction) {
        final URI uri = response.getUri(direction);
        return getPagedResponse(uri, response.getClazz());
    }

    public List parseLinks(HttpHeaders headers) {
        //Link -> [; rel="next"]

        Optional> linkHeader = ofNullable(headers.get(Strings.HEADER_LINK));

        return linkHeader
                .map(List::stream)
                .map(Stream::findFirst)
                .map(rawResponse -> {
                    final String[] links = rawResponse.get().split(", ");

                    return Arrays.stream(links).map(link -> { // ; rel="next"
                        String[] linkData = link.split("; ");
                        final String href = linkData[0].replace("<", "").replace(">", "");
                        final String rel = linkData[1].substring(4).replace("\"", "");
                        return Link.of(fixQuery(href), rel);
                    }).collect(Collectors.toList());
                })
                .orElse(Collections.emptyList());
    }

    private String fixQuery(String href) {

        final UriComponents build = UriComponentsBuilder.fromHttpUrl(href).build();

        final MultiValueMap queryParams = build.getQueryParams();
        final MultiValueMap queryParamsFixed = new LinkedMultiValueMap<>();

        queryParams.forEach((key, values) -> queryParamsFixed.put(decode(key), values.stream().map(URLDecoder::decode).collect(Collectors.toList())));

        return UriComponentsBuilder.fromPath(build.getPath())
                .scheme(build.getScheme())
                .queryParams(queryParamsFixed)
                .fragment(build.getFragment())
                .port(build.getPort())
                .host(build.getHost()).build().toUriString();

    }

    @VisibleForTesting
    @SuppressWarnings("unchecked")
    protected Map fieldsFrom(Post post) {
        ImmutableMap.Builder builder = new ImmutableMap.Builder<>();

        BiConsumer biConsumer = (key, value) -> ofNullable(value).ifPresent(v -> builder.put(key, v));

        List processableFields = Arrays.asList(
                "author",
                "categories",
                "comment_status",
                "content",
                "date",
                "featured_media",
                "format",
                "excerpt",
                "modified_gmt",
                "ping_status",
                //"slug",
                //"status",
                "sticky",
                "tags",
                "title"
                //"type"
        );

        // types ignored for now: slug, status, type

        Arrays.stream(post.getClass().getDeclaredFields())
                .filter(field -> field.getAnnotationsByType(JsonProperty.class).length > 0)
                .map(field -> Tuple2.of(field, field.getAnnotationsByType(JsonProperty.class)[0]))
                .filter(fieldTuple -> processableFields.contains(fieldTuple.b.value()))
                .forEach(field -> {
                    try {
                        ReflectionUtils.makeAccessible(field.a);
                        Object theField = field.a.get(post);
                        if (nonNull(theField)) {
                            final Object value;
                            if (theField instanceof RenderableField) {
                                value = ((RenderableField) theField).getRendered();
                            } else {
                                value = theField;
                            }
                            biConsumer.accept(field.b.value(), value);
                        }
                    } catch (IllegalAccessException e) {
                        LOG.error("Error populating post fields builder.", e);
                    }
                });

        return builder.build();
    }

    private  ResponseEntity doExchange0(HttpMethod method, URI uri, Class typeRef, B body, @Nullable MediaType mediaType) {
        final Tuple2 authTuple = AuthUtil.authTuple(username, password);
        final RequestEntity.BodyBuilder builder = RequestEntity.method(method, uri).header(authTuple.a, authTuple.b).header(userAgentTuple.a, userAgentTuple.b);

        ofNullable(mediaType).ifPresent(builder::contentType);

        final RequestEntity entity = builder.body(body);
        debugRequest(entity);
        final ResponseEntity exchange = restTemplate.exchange(entity, typeRef);
        debugHeaders(exchange.getHeaders());
        return exchange;
    }

    private  ResponseEntity doExchange0(HttpMethod method, UriComponents uriComponents, Class typeRef, B body, @Nullable MediaType mediaType) {
        return doExchange0(method, uriComponents.toUri(), typeRef, body, mediaType);
    }

    @Override
    public  ResponseEntity doCustomExchange(String context, HttpMethod method, Class typeRef, Object[] buildAndExpand,
                                                     Map queryParams, B body, @Nullable MediaType mediaType) {
        return doExchange1(context, method, typeRef, buildAndExpand, queryParams, body, mediaType);
    }

    private  ResponseEntity doExchange1(String context, HttpMethod method, Class typeRef, Object[] buildAndExpand, Map queryParams, B body) {
        return doExchange1(context, method, typeRef, buildAndExpand, queryParams, body, null);
    }

    private  ResponseEntity doExchange1(String context, HttpMethod method, Class typeRef, Object[] buildAndExpand, Map queryParams, B body,
              @Nullable MediaType mediaType) {
        final UriComponentsBuilder builder = Request.of(context).usingClient(this);

        ofNullable(queryParams).ifPresent(params -> params.forEach(builder::queryParam));

        return doExchange0(method, builder.buildAndExpand(buildAndExpand), typeRef, body, mediaType);
    }

    private Optional link(List links, Predicate linkPredicate) {
        return links.stream().filter(linkPredicate).map(Link::getHref).findFirst();
    }

    private void debugRequest(RequestEntity entity) {
        if (debug) {
            LOG.debug("Request Entity: {}", entity);
        }
    }

    private void debugHeaders(HttpHeaders headers) {
        if (debug) {
            LOG.debug("Response Headers:");
            headers.forEach((key, value) -> LOG.debug("{} -> {}", key, value));
        }
    }

    static Object[] forExpand(Object... values) {
        return values;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy