org.ambraproject.wombat.controller.BrowseController.java Source code

Java tutorial

Introduction

Here is the source code for org.ambraproject.wombat.controller.BrowseController.java

Source

/*
 * Copyright (c) 2017 Public Library of Science
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

package org.ambraproject.wombat.controller;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimaps;
import org.ambraproject.wombat.config.site.RequestMappingContextDictionary;
import org.ambraproject.wombat.config.site.Site;
import org.ambraproject.wombat.config.site.SiteParam;
import org.ambraproject.wombat.config.site.SiteSet;
import org.ambraproject.wombat.config.site.url.Link;
import org.ambraproject.wombat.identity.ArticlePointer;
import org.ambraproject.wombat.identity.RequestedDoiVersion;
import org.ambraproject.wombat.model.ArticleType;
import org.ambraproject.wombat.model.RelatedArticle;
import org.ambraproject.wombat.service.remote.ApiAddress;
import org.ambraproject.wombat.service.ArticleTransformService;
import org.ambraproject.wombat.service.EntityNotFoundException;
import org.ambraproject.wombat.service.SolrArticleAdapter;
import org.ambraproject.wombat.service.XmlUtil;
import org.ambraproject.wombat.service.remote.ArticleApi;
import org.ambraproject.wombat.service.remote.SolrSearchApi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.io.IOException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Controller for the browse page.
 */
@Controller
public class BrowseController extends WombatController {

    private static final Logger log = LoggerFactory.getLogger(BrowseController.class);

    @Autowired
    private ArticleApi articleApi;
    @Autowired
    private ArticleTransformService articleTransformService;
    @Autowired
    private ArticleMetadata.Factory articleMetadataFactory;
    @Autowired
    private SolrSearchApi solrSearchApi;
    @Autowired
    private SiteSet siteSet;
    @Autowired
    private RequestMappingContextDictionary requestMappingContextDictionary;

    /**
     * Validate that an issue belongs to the current site. If not, throw an exception
     * indicating that the user should be redirected to the appropriate site
     */
    private void validateIssueSite(Site site, Map<String, ?> issueMetadata) throws IOException {
        String issueId = (String) issueMetadata.get("doi");
        Map<String, String> parentVolumeMetadata = (Map<String, String>) issueMetadata.get("parentVolume");
        String publishedJournalKey = parentVolumeMetadata.get("journalKey");
        String siteJournalKey = site.getJournalKey();
        if (!publishedJournalKey.equals(siteJournalKey)) {
            Link link = buildCrossSiteRedirectToIssue(publishedJournalKey, site, issueId);
            throw new InternalRedirectException(link);
        }
    }

    private Link buildCrossSiteRedirectToIssue(String targetJournal, Site site, String id) {
        Site targetSite = site.getTheme().resolveForeignJournalKey(siteSet, targetJournal);
        return Link.toForeignSite(site, targetSite).toPattern(requestMappingContextDictionary, "browseIssues")
                .addQueryParameters(ImmutableMap.of("id", id)).build();
    }

    @RequestMapping(name = "browseVolumes", value = "/volume")
    public String browseVolume(Model model, @SiteParam Site site) throws IOException {
        Map<String, ?> currentIssue = getCurrentIssue(site).orElse(null);
        if (currentIssue != null) {
            Map<String, ?> imageArticle = (Map<String, ?>) currentIssue.get("imageArticle");
            if (imageArticle != null) {
                String imageArticleDoi = (String) imageArticle.get("doi");
                transformIssueImageMetadata(model, site, imageArticleDoi);
            }
            model.addAttribute("currentIssue", currentIssue);
        }

        String journalKey = site.getJournalKey();
        List<Map<String, ?>> volumes = articleApi.requestObject(
                ApiAddress.builder("journals").addToken(journalKey).addToken("volumes").build(), List.class);
        List<Map<String, ?>> populatedVolumes = volumes.parallelStream()
                .map(volume -> populateVolumeWithIssues(journalKey, volume)).collect(Collectors.toList());
        model.addAttribute("volumes", populatedVolumes);

        return site.getKey() + "/ftl/browse/volumes";
    }

    private Map<String, ?> populateVolumeWithIssues(String journalKey, Map<String, ?> volume) {
        Map<String, Object> populatedVolume = new HashMap<>(volume);

        List<Map<String, ?>> issues;
        try {
            issues = articleApi.requestObject(ApiAddress.builder("journals").addToken(journalKey)
                    .addToken("volumes").embedDoi((String) volume.get("doi")).addToken("issues").build(),
                    List.class);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        populatedVolume.put("issues", issues);

        return populatedVolume;
    }

    @RequestMapping(name = "browseIssues", value = "/issue")
    public String browseIssue(Model model, @SiteParam Site site,
            @RequestParam(value = "id", required = false) String issueId) throws IOException {
        model.addAttribute("journal", fetchJournalMetadata(site));

        Map<String, ?> issueMetadata = (issueId == null)
                ? getCurrentIssue(site).orElseThrow(
                        () -> new RuntimeException("Current issue is not set for " + site.getJournalKey()))
                : getIssue(issueId);
        issueId = (String) issueMetadata.get("doi");

        validateIssueSite(site, issueMetadata);

        model.addAttribute("issue", issueMetadata);

        Map<String, ?> imageArticle = (Map<String, ?>) issueMetadata.get("imageArticle");
        if (imageArticle != null) {
            String imageArticleDoi = (String) (imageArticle).get("doi");
            transformIssueImageMetadata(model, site, imageArticleDoi);
        }

        List<Map<String, ?>> articlesInIssue = articleApi.requestObject(
                ApiAddress.builder("issues").embedDoi(issueId).addToken("contents").build(), List.class);
        model.addAttribute("articleGroups", buildArticleGroups(site, issueId, articlesInIssue));

        return site.getKey() + "/ftl/browse/issues";
    }

    private Map<String, Object> fetchJournalMetadata(@SiteParam Site site) throws IOException {
        return articleApi.requestObject(ApiAddress.builder("journals").addToken(site.getJournalKey()).build(),
                Map.class);
    }

    private Optional<Map<String, ?>> getCurrentIssue(Site site) throws IOException {
        try {
            Map<String, ?> currentIssue = articleApi.requestObject(
                    ApiAddress.builder("journals").addToken(site.getJournalKey()).addToken("currentIssue").build(),
                    Map.class);
            return Optional.of(currentIssue);
        } catch (EntityNotFoundException e) {
            return Optional.empty();
        }
    }

    private Map<String, ?> getIssue(String issueId) throws IOException {
        ApiAddress issueAddress = ApiAddress.builder("issues").embedDoi(issueId).build();
        try {
            return articleApi.requestObject(issueAddress, Map.class);
        } catch (EntityNotFoundException e) {
            throw new NotFoundException(e);
        }
    }

    private void transformIssueImageMetadata(Model model, Site site, String imageArticleDoi) throws IOException {
        RequestedDoiVersion requestedDoiVersion = RequestedDoiVersion.of(imageArticleDoi);
        ArticleMetadata imageArticleMetadata;
        try {
            imageArticleMetadata = articleMetadataFactory.get(site, requestedDoiVersion);
        } catch (NotFoundException e) {
            throw new RuntimeException("Issue's image article not found: " + requestedDoiVersion, e);
        }
        ArticlePointer issueImageArticleId = imageArticleMetadata.getArticlePointer();
        String issueDesc = (String) imageArticleMetadata.getIngestionMetadata().get("description");

        model.addAttribute("issueTitle", articleTransformService.transformImageDescription(site,
                issueImageArticleId, XmlUtil.extractElement(issueDesc, "title")));
        model.addAttribute("issueDescription", articleTransformService.transformImageDescription(site,
                issueImageArticleId, XmlUtil.removeElement(issueDesc, "title")));
    }

    public static class TypedArticleGroup {
        private final ArticleType type;
        private final ImmutableList<Map<String, ?>> articles;

        private TypedArticleGroup(ArticleType type, List<Map<String, Object>> articles) {
            this.type = Objects.requireNonNull(type);
            this.articles = ImmutableList.copyOf(articles);
        }

        public ArticleType getType() {
            return type;
        }

        public ImmutableList<Map<String, ?>> getArticles() {
            return articles;
        }
    }

    private List<TypedArticleGroup> buildArticleGroups(Site site, String issueId, List<Map<String, ?>> articles)
            throws IOException {
        // Articles grouped by their type. Order within the value lists is significant.
        ArticleType.Dictionary typeDictionary = ArticleType.getDictionary(site.getTheme());
        ListMultimap<ArticleType, Map<String, Object>> groupedArticles = LinkedListMultimap.create();
        for (Map<String, ?> article : articles) {
            if (!article.containsKey("revisionNumber"))
                continue; // Omit unpublished articles

            Map<String, Object> populatedArticle = new HashMap<>(article);

            Map<String, ?> ingestion = (Map<String, ?>) article.get("ingestion");
            ArticleType articleType = typeDictionary.lookUp((String) ingestion.get("articleType"));

            populateRelatedArticles(populatedArticle);

            populateAuthors(populatedArticle, site);

            groupedArticles.put(articleType, populatedArticle);
        }

        // The article types supported by this site, in the order in which they are supposed to appear.
        ImmutableList<ArticleType> articleTypes = typeDictionary.getSequence();

        // Produce a correctly ordered list of TypedArticleGroup, populated with the article groups.
        List<TypedArticleGroup> articleGroups = new ArrayList<>(articleTypes.size());
        for (ArticleType articleType : articleTypes) {
            List<Map<String, Object>> articlesOfType = groupedArticles.removeAll(articleType);
            if (!articlesOfType.isEmpty()) {
                articleGroups.add(new TypedArticleGroup(articleType, articlesOfType));
            }
        }

        // If any article groups were not matched, append them to the end.
        for (Map.Entry<ArticleType, List<Map<String, Object>>> entry : Multimaps.asMap(groupedArticles)
                .entrySet()) {
            ArticleType type = entry.getKey();
            TypedArticleGroup group = new TypedArticleGroup(type, entry.getValue());
            articleGroups.add(group);

            log.warn(String.format("Issue %s has articles of type \"%s\", which is not configured for %s: %s",
                    issueId, type.getName(), site.getKey(),
                    Lists.transform(group.articles, article -> article.get("doi"))));
        }

        return articleGroups;
    }

    private void populateRelatedArticles(Map<String, Object> article) throws IOException {
        Map<String, Object> relationshipMetadata = articleApi.requestObject(ApiAddress.builder("articles")
                .embedDoi((String) article.get("doi")).addToken("relationships").build(), Map.class);

        List<Map<String, String>> inbound = (List<Map<String, String>>) relationshipMetadata.get("inbound");
        List<Map<String, String>> outbound = (List<Map<String, String>>) relationshipMetadata.get("outbound");
        List<RelatedArticle> relatedArticles = Stream.concat(inbound.stream(), outbound.stream())
                .map(amendment -> new RelatedArticle(amendment.get("doi"), amendment.get("title"),
                        LocalDate.parse(amendment.get("publicationDate"))))
                .distinct().sorted(Comparator.comparing(RelatedArticle::getPublicationDate).reversed())
                .collect(Collectors.toList());

        article.put("relatedArticles", relatedArticles);
    }

    private void populateAuthors(Map<String, Object> article, Site site) throws IOException {
        Map<String, ?> solrResult = (Map<String, ?>) solrSearchApi.lookupArticleByDoi((String) article.get("doi"),
                site);
        List<SolrArticleAdapter> solrArticles = SolrArticleAdapter.unpackSolrQuery(solrResult);
        article.put("authors", solrArticles.size() > 0 ? solrArticles.get(0).getAuthors() : ImmutableList.of());
    }
}