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

Java tutorial

Introduction

Here is the source code for org.ambraproject.wombat.controller.ArticleController.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.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.io.ByteStreams;
import com.google.gson.Gson;
import org.ambraproject.wombat.config.RuntimeConfiguration;
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.Reference;
import org.ambraproject.wombat.service.ArticleTransformService;
import org.ambraproject.wombat.service.DoiToJournalResolutionService;
import org.ambraproject.wombat.service.ParseXmlService;
import org.ambraproject.wombat.service.remote.CorpusContentApi;
import org.ambraproject.wombat.service.remote.orcid.OrcidApi;
import org.ambraproject.wombat.service.remote.orcid.OrcidAuthenticationTokenExpiredException;
import org.ambraproject.wombat.service.remote.orcid.OrcidAuthenticationTokenReusedException;
import org.apache.commons.io.output.WriterOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
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 org.w3c.dom.Document;

import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Controller for rendering an article.
 */
@Controller
public class ArticleController extends WombatController {

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

    /**
     * Initial size (in bytes) of buffer that holds transformed article HTML before passing it to the model.
     */
    private static final int XFORM_BUFFER_SIZE = 0x8000;

    @Autowired
    private Charset charset;
    @Autowired
    private CorpusContentApi corpusContentApi;
    @Autowired
    private ArticleTransformService articleTransformService;
    @Autowired
    private ArticleMetadata.Factory articleMetadataFactory;
    @Autowired
    private ParseXmlService parseXmlService;
    @Autowired
    private Gson gson;
    @Autowired
    private RuntimeConfiguration runtimeConfiguration;
    @Autowired
    private SiteSet siteSet;
    @Autowired
    private RequestMappingContextDictionary requestMappingContextDictionary;
    @Autowired
    private DoiToJournalResolutionService doiToJournalResolutionService;
    @Autowired
    private OrcidApi orcidApi;

    // TODO: this method currently makes 5 backend RPCs, all sequentially. Explore reducing this
    // number, or doing them in parallel, if this is a performance bottleneck.
    @RequestMapping(name = "article", value = "/article")
    public String renderArticle(HttpServletRequest request, Model model, @SiteParam Site site,
            RequestedDoiVersion articleId) throws IOException {
        ArticlePointer articlePointer = articleMetadataFactory.get(site, articleId).validateVisibility("article")
                .populate(request, model).fillAmendments(model).getArticlePointer();

        XmlContent xmlContent = getXmlContent(site, articlePointer, request);
        model.addAttribute("articleText", xmlContent.html);
        model.addAttribute("references", xmlContent.references);

        return site + "/ftl/article/article";
    }

    /**
     * Serves a request for the "about the authors" page for an article.
     *
     * @param model     data to pass to the view
     * @param site      current site
     * @param articleId specifies the article
     * @return path to the template
     * @throws IOException
     */
    @RequestMapping(name = "articleAuthors", value = "/article/authors")
    public String renderArticleAuthors(HttpServletRequest request, Model model, @SiteParam Site site,
            RequestedDoiVersion articleId) throws IOException {
        articleMetadataFactory.get(site, articleId).validateVisibility("articleAuthors").populate(request, model);
        return site + "/ftl/article/authors";
    }

    /**
     * Serves the article metrics tab content for an article.
     *
     * @param model     data to pass to the view
     * @param site      current site
     * @param articleId specifies the article
     * @return path to the template
     * @throws IOException
     */
    @RequestMapping(name = "articleMetrics", value = "/article/metrics")
    public String renderArticleMetrics(HttpServletRequest request, Model model, @SiteParam Site site,
            RequestedDoiVersion articleId) throws IOException {
        articleMetadataFactory.get(site, articleId).validateVisibility("articleMetrics").populate(request, model);
        return site + "/ftl/article/metrics";
    }

    /**
     * Serves the related content tab content for an article.
     *
     * @param model     data to pass to the view
     * @param site      current site
     * @param articleId specifies the article
     * @return path to the template
     * @throws IOException
     */
    @RequestMapping(name = "articleRelatedContent", value = "/article/related")
    public String renderArticleRelatedContent(HttpServletRequest request, Model model, @SiteParam Site site,
            RequestedDoiVersion articleId) throws IOException {
        articleMetadataFactory.get(site, articleId).validateVisibility("articleRelatedContent").populate(request,
                model);
        return site + "/ftl/article/relatedContent";
    }

    /**
     * Serves the peer review tab content for an article.
     *
     * @param model     data to pass to the view
     * @param site      current site
     * @param articleId specifies the article
     * @return path to the template
     * @throws IOException
     */
    @RequestMapping(name = "articlePeerReview", value = "/article/peerReview")
    public String renderArticlePeerReview(HttpServletRequest request, Model model, @SiteParam Site site,
            RequestedDoiVersion articleId) throws IOException {
        articleMetadataFactory.get(site, articleId).validateVisibility("articlePeerReview").populate(request,
                model);

        throwIfPeerReviewNotFound(model.asMap());

        return site + "/ftl/article/peerReview";
    }

    /**
     * Returns a list of figures and tables of a given article; main usage is the figshare tile on the Metrics
     * tab
     *
     * @param site current site
     * @param articleId DOI identifying the article
     * @return a list of figures and tables of a given article
     * @throws IOException
     */
    @RequestMapping(name = "articleFigsAndTables", value = "/article/assets/figsAndTables")
    public ResponseEntity<List> listArticleFiguresAndTables(@SiteParam Site site, RequestedDoiVersion articleId)
            throws IOException {
        List<Map<String, ?>> figureView = articleMetadataFactory.get(site, articleId)
                .validateVisibility("articleFigsAndTables").getFigureView();

        HttpHeaders headers = new HttpHeaders();
        headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        return new ResponseEntity<>(figureView, headers, HttpStatus.OK);
    }

    @RequestMapping(name = "uploadPreprintRevision", value = "/article/uploadPreprintRevision")
    public String uploadPreprintRevision(HttpServletRequest request, Model model, @SiteParam Site site,
            @RequestParam("state") String state, @RequestParam("code") String code)
            throws IOException, URISyntaxException {
        final byte[] decodedState = Base64.getDecoder().decode(state);
        final String decodedJson = URLDecoder.decode(new String(decodedState), "UTF-8");
        Map<String, Object> stateJson = gson.fromJson(decodedJson, HashMap.class);

        String correspondingAuthorOrcidId = (String) stateJson.get("orcid_id");
        String authenticatedOrcidId = "";

        try {
            authenticatedOrcidId = orcidApi.getOrcidIdFromAuthorizationCode(site, code);
        } catch (OrcidAuthenticationTokenExpiredException | OrcidAuthenticationTokenReusedException e) {
            model.addAttribute("orcidAuthenticationError", e.getMessage());
        }

        boolean isError = true;
        if (correspondingAuthorOrcidId.equals(authenticatedOrcidId)) {
            model.addAttribute("orcidId", correspondingAuthorOrcidId);
            isError = false;
        } else if (!Strings.isNullOrEmpty(authenticatedOrcidId)) {
            model.addAttribute("orcidAuthenticationError",
                    "ORCID IDs do not match. " + "Corresponding author ORCID ID must be used.");
        }

        if (isError) {
            final RequestedDoiVersion articleId = RequestedDoiVersion.of((String) stateJson.get("doi"));
            return renderArticle(request, model, site, articleId);
        } else {
            return site + "/ftl/article/uploadPreprintRevision";
        }
    }

    @SuppressWarnings("serial")
    static class XmlContent implements Serializable {
        private final String html;
        private final ImmutableList<Reference> references;

        public XmlContent(String html, List<Reference> references) {
            this.html = Objects.requireNonNull(html);
            this.references = ImmutableList.copyOf(references);
        }
    }

    void throwIfPeerReviewNotFound(Map<String, Object> map) {
        if (null == map.get("peerReview")) {
            throw new NotFoundException();
        }
    }

    /**
     * Gets article xml from cache if it exists; otherwise, gets it from rhino and caches it. Then it parses the
     * references and does html transform
     *
     * @param articlePointer
     * @param request
     * @return an XmlContent containing the list of references and article html
     * @throws IOException
     */
    private XmlContent getXmlContent(Site site, ArticlePointer articlePointer, HttpServletRequest request)
            throws IOException {
        return corpusContentApi.readManuscript(articlePointer, site, "html", (InputStream stream) -> {
            byte[] xml = ByteStreams.toByteArray(stream);
            final Document document = parseXmlService.getDocument(new ByteArrayInputStream(xml));

            // do not supply Solr related link service now
            List<Reference> references = parseXmlService.parseArticleReferences(document, null);

            // invoke the Solr API once to resolve all journal keys
            List<String> dois = references.stream().map(ref -> ref.getDoi()).filter(doi -> inPlosJournal(doi))
                    .collect(Collectors.toList());
            List<String> keys = doiToJournalResolutionService.getJournalKeysFromDois(dois, site);

            // store the link text from journal key to references.
            // since Reference is immutable, need to create a new list of new reference objects.
            Iterator<Reference> itRef = references.iterator();
            Iterator<String> itKey = keys.iterator();
            List<Reference> referencesWithLinks = new ArrayList<Reference>();
            while (itRef.hasNext()) {
                Reference ref = itRef.next();
                if (!inPlosJournal(ref.getDoi())) {
                    referencesWithLinks.add(ref);
                    continue;
                }

                String key = itKey.next();
                if (Strings.isNullOrEmpty(key)) {
                    referencesWithLinks.add(ref);
                    continue;
                }

                Reference.Builder builder = new Reference.Builder(ref);
                Reference refWithLink = builder.setFullArticleLink(getLinkText(site, request, ref.getDoi(), key))
                        .build();
                referencesWithLinks.add(refWithLink);
            }

            references = referencesWithLinks;

            StringWriter articleHtml = new StringWriter(XFORM_BUFFER_SIZE);
            try (OutputStream outputStream = new WriterOutputStream(articleHtml, charset)) {
                articleTransformService.transformArticle(site, articlePointer, references,
                        new ByteArrayInputStream(xml), outputStream);
            }

            return new XmlContent(articleHtml.toString(), references);
        });
    }

    private Boolean inPlosJournal(String doi) {
        return doi != null && doi.startsWith("10.1371/");
    }

    private String getLinkText(Site site, HttpServletRequest request, String doi, String citationJournalKey)
            throws IOException {
        String linkText = null;
        if (citationJournalKey != null) {
            linkText = Link.toForeignSite(site, citationJournalKey, siteSet)
                    .toPattern(requestMappingContextDictionary, "article").addQueryParameter("id", doi).build()
                    .get(request);
        }
        return linkText;
    }
}