org.dspace.app.rest.BitstreamContentRestControllerIT.java Source code

Java tutorial

Introduction

Here is the source code for org.dspace.app.rest.BitstreamContentRestControllerIT.java

Source

/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.app.rest;

import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.head;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.io.Writer;
import java.util.UUID;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.CharEncoding;
import org.apache.commons.lang3.StringUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.dspace.app.rest.builder.BitstreamBuilder;
import org.dspace.app.rest.builder.CollectionBuilder;
import org.dspace.app.rest.builder.CommunityBuilder;
import org.dspace.app.rest.builder.GroupBuilder;
import org.dspace.app.rest.builder.ItemBuilder;
import org.dspace.app.rest.test.AbstractControllerIntegrationTest;
import org.dspace.content.Bitstream;
import org.dspace.content.Collection;
import org.dspace.content.Item;
import org.dspace.disseminate.CitationDocumentServiceImpl;
import org.dspace.eperson.Group;
import org.dspace.services.ConfigurationService;
import org.dspace.solr.MockSolrServer;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * Integration test to test the /api/core/bitstreams/[id]/content endpoint
 *
 * @author Tom Desair (tom dot desair at atmire dot com)
 * @author Frederic Van Reet (frederic dot vanreet at atmire dot com)
 */
public class BitstreamContentRestControllerIT extends AbstractControllerIntegrationTest {

    private MockSolrServer mockSolrServer;

    @Autowired
    private ConfigurationService configurationService;

    @Autowired
    private CitationDocumentServiceImpl citationDocumentService;

    @Before
    public void setup() throws Exception {
        super.setUp();
        mockSolrServer = new MockSolrServer("statistics");
        mockSolrServer.getSolrServer().deleteByQuery("*:*");
        mockSolrServer.getSolrServer().commit();
        configurationService.setProperty("citation-page.enable_globally", false);
    }

    @After
    public void destroy() throws Exception {
        super.destroy();
        mockSolrServer.destroy();
    }

    @Test
    public void retrieveFullBitstream() throws Exception {
        context.turnOffAuthorisationSystem();

        //** GIVEN **
        //1. A community-collection structure with one parent community and one collections.
        parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build();

        Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1")
                .build();

        //2. A public item with a bitstream
        String bitstreamContent = "0123456789";

        try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {

            Item publicItem1 = ItemBuilder.createItem(context, col1).withTitle("Public item 1")
                    .withIssueDate("2017-10-17").withAuthor("Smith, Donald").withAuthor("Doe, John").build();

            Bitstream bitstream = BitstreamBuilder.createBitstream(context, publicItem1, is)
                    .withName("Test bitstream").withDescription("This is a bitstream to test range requests")
                    .withMimeType("text/plain").build();

            //** WHEN **
            //We download the bitstream
            getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content"))

                    //** THEN **
                    .andExpect(status().isOk())

                    //The Content Length must match the full length
                    .andExpect(header().longValue("Content-Length", bitstreamContent.getBytes().length))
                    //The server should indicate we support Range requests
                    .andExpect(header().string("Accept-Ranges", "bytes"))
                    //The ETag has to be based on the checksum
                    .andExpect(header().string("ETag", bitstream.getChecksum()))
                    //We expect the content type to match the bitstream mime type
                    .andExpect(content().contentType("text/plain"))
                    //THe bytes of the content must match the original content
                    .andExpect(content().bytes(bitstreamContent.getBytes()));

            //A If-None-Match HEAD request on the ETag must tell is the bitstream is not modified
            getClient().perform(head("/api/core/bitstreams/" + bitstream.getID() + "/content")
                    .header("If-None-Match", bitstream.getChecksum())).andExpect(status().isNotModified());

            //The download and head request should also be logged as a statistics record
            checkNumberOfStatsRecords(bitstream, 2);
        }
    }

    @Test
    public void retrieveRangeBitstream() throws Exception {
        context.turnOffAuthorisationSystem();

        //** GIVEN **
        //1. A community-collection structure with one parent community and one collections.
        parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build();

        Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1")
                .build();

        //2. A public item with a bitstream
        String bitstreamContent = "0123456789";

        try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {

            Item publicItem1 = ItemBuilder.createItem(context, col1).withTitle("Public item 1")
                    .withIssueDate("2017-10-17").withAuthor("Smith, Donald").withAuthor("Doe, John").build();

            Bitstream bitstream = BitstreamBuilder.createBitstream(context, publicItem1, is)
                    .withName("Test bitstream").withDescription("This is a bitstream to test range requests")
                    .withMimeType("text/plain").build();

            //** WHEN **
            //We download only a specific byte range of the bitstream
            getClient()
                    .perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content").header("Range",
                            "bytes=1-3"))

                    //** THEN **
                    .andExpect(status().is(206))

                    //The Content Length must match the requested range
                    .andExpect(header().longValue("Content-Length", 3))
                    //The server should indicate we support Range requests
                    .andExpect(header().string("Accept-Ranges", "bytes"))
                    //The ETag has to be based on the checksum
                    .andExpect(header().string("ETag", bitstream.getChecksum()))
                    //The response should give us details about the range
                    .andExpect(header().string("Content-Range", "bytes 1-3/10"))
                    //We expect the content type to match the bitstream mime type
                    .andExpect(content().contentType("text/plain"))
                    //We only expect the bytes 1, 2 and 3
                    .andExpect(content().bytes("123".getBytes()));

            //** WHEN **
            //We download the rest of the range
            getClient()
                    .perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content").header("Range",
                            "bytes=4-"))

                    //** THEN **
                    .andExpect(status().is(206))

                    //The Content Length must match the requested range
                    .andExpect(header().longValue("Content-Length", 6))
                    //The server should indicate we support Range requests
                    .andExpect(header().string("Accept-Ranges", "bytes"))
                    //The ETag has to be based on the checksum
                    .andExpect(header().string("ETag", bitstream.getChecksum()))
                    //The response should give us details about the range
                    .andExpect(header().string("Content-Range", "bytes 4-9/10"))
                    //We expect the content type to match the bitstream mime type
                    .andExpect(content().contentType("text/plain"))
                    //We all remaining bytes, starting at byte 4
                    .andExpect(content().bytes("456789".getBytes()));

            //Check that NO statistics record was logged for the Range requests
            checkNumberOfStatsRecords(bitstream, 0);
        }
    }

    @Test
    public void testBitstreamNotFound() throws Exception {
        getClient().perform(get("/api/core/bitstreams/" + UUID.randomUUID() + "/content"))
                .andExpect(status().isNotFound());
    }

    @Test
    public void testEmbargoedBitstream() throws Exception {
        context.turnOffAuthorisationSystem();

        //** GIVEN **
        //1. A community-collection structure with one parent community and one collections.
        parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build();

        Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1")
                .build();

        //2. A public item with an embargoed bitstream
        String bitstreamContent = "Embargoed!";

        try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {

            Item publicItem1 = ItemBuilder.createItem(context, col1).withTitle("Public item 1")
                    .withIssueDate("2017-10-17").withAuthor("Smith, Donald").withAuthor("Doe, John").build();

            Bitstream bitstream = BitstreamBuilder.createBitstream(context, publicItem1, is)
                    .withName("Test Embargoed Bitstream").withDescription("This bitstream is embargoed")
                    .withMimeType("text/plain").withEmbargoPeriod("6 months").build();

            //** WHEN **
            //We download the bitstream
            getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content"))

                    //** THEN **
                    .andExpect(status().isUnauthorized());

            //An unauthorized request should not log statistics
            checkNumberOfStatsRecords(bitstream, 0);
        }
    }

    @Test
    public void testPrivateBitstream() throws Exception {
        context.turnOffAuthorisationSystem();

        //** GIVEN **
        //1. A community-collection structure with one parent community and one collections.
        parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build();

        Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1")
                .build();

        //2. A public item with a private bitstream
        Item publicItem1 = ItemBuilder.createItem(context, col1).withTitle("Public item 1")
                .withIssueDate("2017-10-17").withAuthor("Smith, Donald").withAuthor("Doe, John").build();

        Group internalGroup = GroupBuilder.createGroup(context).withName("Internal Group").build();

        String bitstreamContent = "Private!";
        try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {

            Bitstream bitstream = BitstreamBuilder.createBitstream(context, publicItem1, is)
                    .withName("Test Embargoed Bitstream").withDescription("This bitstream is embargoed")
                    .withMimeType("text/plain").withReaderGroup(internalGroup).build();

            //** WHEN **
            //We download the bitstream
            getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content"))

                    //** THEN **
                    .andExpect(status().isUnauthorized());

            //An unauthorized request should not log statistics
            checkNumberOfStatsRecords(bitstream, 0);

        }
    }

    private void checkNumberOfStatsRecords(Bitstream bitstream, int expectedNumberOfStatsRecords)
            throws SolrServerException, IOException {
        mockSolrServer.getSolrServer().commit();

        SolrQuery query = new SolrQuery("id:\"" + bitstream.getID() + "\"").setRows(0).setStart(0);
        QueryResponse queryResponse = mockSolrServer.getSolrServer().query(query);
        assertEquals(expectedNumberOfStatsRecords, queryResponse.getResults().getNumFound());
    }

    @Test
    public void retrieveCitationCoverpageOfBitstream() throws Exception {
        configurationService.setProperty("citation-page.enable_globally", true);
        citationDocumentService.afterPropertiesSet();
        context.turnOffAuthorisationSystem();

        //** GIVEN **
        //1. A community-collection structure with one parent community and one collections.
        parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build();

        Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1")
                .build();

        //2. A public item with a bitstream
        File originalPdf = new File(testProps.getProperty("test.bitstream"));

        try (InputStream is = new FileInputStream(originalPdf)) {

            Item publicItem1 = ItemBuilder.createItem(context, col1)
                    .withTitle("Public item citation cover page test 1").withIssueDate("2017-10-17")
                    .withAuthor("Smith, Donald").withAuthor("Doe, John").build();

            Bitstream bitstream = BitstreamBuilder.createBitstream(context, publicItem1, is)
                    .withName("Test bitstream")
                    .withDescription("This is a bitstream to test the citation cover page.")
                    .withMimeType("application/pdf").build();

            //** WHEN **
            //We download the bitstream
            byte[] content = getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content"))

                    //** THEN **
                    .andExpect(status().isOk())

                    //The Content Length must match the full length
                    .andExpect(header().string("Content-Length", not(nullValue())))
                    //The server should indicate we support Range requests
                    .andExpect(header().string("Accept-Ranges", "bytes"))
                    //The ETag has to be based on the checksum
                    .andExpect(header().string("ETag", bitstream.getChecksum()))
                    //We expect the content type to match the bitstream mime type
                    .andExpect(content().contentType("application/pdf"))
                    //THe bytes of the content must match the original content
                    .andReturn().getResponse().getContentAsByteArray();

            // The citation cover page contains the item title.
            // We will now verify that the pdf text contains this title.
            String pdfText = extractPDFText(content);
            System.out.println(pdfText);
            assertTrue(StringUtils.contains(pdfText, "Public item citation cover page test 1"));

            // The dspace-api/src/test/data/dspaceFolder/assetstore/ConstitutionofIreland.pdf file contains 64 pages,
            // manually counted + 1 citation cover page
            assertEquals(65, getNumberOfPdfPages(content));

            //A If-None-Match HEAD request on the ETag must tell is the bitstream is not modified
            getClient().perform(head("/api/core/bitstreams/" + bitstream.getID() + "/content")
                    .header("If-None-Match", bitstream.getChecksum())).andExpect(status().isNotModified());

            //The download and head request should also be logged as a statistics record
            checkNumberOfStatsRecords(bitstream, 2);
        }
    }

    private String extractPDFText(byte[] content) throws IOException {
        PDFTextStripper pts = new PDFTextStripper();
        pts.setSortByPosition(true);

        try (ByteArrayInputStream source = new ByteArrayInputStream(content);
                Writer writer = new StringWriter();
                PDDocument pdfDoc = PDDocument.load(source)) {

            pts.writeText(pdfDoc, writer);
            return writer.toString();
        }
    }

    private int getNumberOfPdfPages(byte[] content) throws IOException {
        try (ByteArrayInputStream source = new ByteArrayInputStream(content);
                PDDocument pdfDoc = PDDocument.load(source)) {
            return pdfDoc.getNumberOfPages();
        }
    }

}