001/*
002 * Licensed to DuraSpace under one or more contributor license agreements.
003 * See the NOTICE file distributed with this work for additional information
004 * regarding copyright ownership.
005 *
006 * DuraSpace licenses this file to you under the Apache License,
007 * Version 2.0 (the "License"); you may not use this file except in
008 * compliance with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.fcrepo.client.integration;
019
020import static javax.ws.rs.core.Response.Status.CREATED;
021import static org.apache.jena.rdf.model.ResourceFactory.createProperty;
022import static org.apache.jena.rdf.model.ResourceFactory.createResource;
023import static org.fcrepo.client.FedoraHeaderConstants.MEMENTO_DATETIME;
024import static org.fcrepo.client.FedoraTypes.MEMENTO_TYPE;
025import static org.fcrepo.client.HeaderHelpers.UTC_RFC_1123_FORMATTER;
026import static org.fcrepo.client.LinkHeaderConstants.DESCRIBEDBY_REL;
027import static org.fcrepo.client.LinkHeaderConstants.MEMENTO_TIME_MAP_REL;
028import static org.fcrepo.client.TestUtils.rdfTtl;
029import static org.junit.Assert.assertEquals;
030import static org.junit.Assert.assertTrue;
031
032import java.io.ByteArrayInputStream;
033import java.io.ByteArrayOutputStream;
034import java.io.InputStream;
035import java.net.URI;
036import java.time.Instant;
037import java.time.LocalDateTime;
038import java.time.ZoneOffset;
039import java.time.ZonedDateTime;
040
041import org.apache.commons.io.IOUtils;
042import org.apache.jena.rdf.model.Model;
043import org.apache.jena.rdf.model.Property;
044import org.apache.jena.rdf.model.Resource;
045import org.fcrepo.client.FcrepoClient;
046import org.fcrepo.client.FcrepoResponse;
047import org.fcrepo.kernel.api.RdfLexicon;
048import org.junit.Test;
049import org.junit.runner.RunWith;
050import org.springframework.test.context.ContextConfiguration;
051import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
052
053/**
054 * @author bbpennel
055 */
056@RunWith(SpringJUnit4ClassRunner.class)
057@ContextConfiguration("/spring-test/test-container.xml")
058public class VersioningIT extends AbstractResourceIT {
059
060    private static final Property DC_TITLE = createProperty("http://purl.org/dc/elements/1.1/title");
061
062    private final ZonedDateTime HISTORIC_DATETIME = LocalDateTime.of(2000, 1, 1, 0, 0).atZone(ZoneOffset.UTC);
063
064    private final String HISTORIC_TIMESTAMP = UTC_RFC_1123_FORMATTER.format(HISTORIC_DATETIME);
065
066    public VersioningIT() {
067        super();
068
069        client = FcrepoClient.client()
070                .credentials("fedoraAdmin", "password")
071                .authScope("localhost")
072                .build();
073    }
074
075    @Test
076    public void testCreateMementoFromOriginal() throws Exception {
077        // Create original resource with custom property
078        final FcrepoResponse createOriginalResp = client.post(new URI(serverAddress))
079                .body(new ByteArrayInputStream(rdfTtl.getBytes()), "text/turtle")
080                .perform();
081
082        final URI timemapURI = createOriginalResp.getLinkHeaders(MEMENTO_TIME_MAP_REL).get(0);
083
084        final FcrepoResponse mementoResp = client.createMemento(timemapURI)
085                .perform();
086        assertEquals(CREATED.getStatusCode(), mementoResp.getStatusCode());
087
088        final URI mementoURI = mementoResp.getLocation();
089
090        final FcrepoResponse getResp = client.get(mementoURI).perform();
091        assertTrue("Retrieved object must be a memento", getResp.hasType(MEMENTO_TYPE));
092        final Model respModel = getResponseModel(getResp);
093        assertTrue(respModel.contains(null, DC_TITLE, "Test Object"));
094    }
095
096    // create memento from historic version, binary
097    @Test
098    public void testCreateHistoricBinaryMemento() throws Exception {
099        // Create original binary
100        final String mimetype = "text/plain";
101        final String bodyContent = "Hello world";
102        final FcrepoResponse createOriginalResp = client.post(new URI(serverAddress))
103                .body(new ByteArrayInputStream(bodyContent.getBytes()), mimetype)
104                .perform();
105
106        final URI originalURI = createOriginalResp.getLocation();
107        final URI timemapURI = createOriginalResp.getLinkHeaders(MEMENTO_TIME_MAP_REL).get(0);
108
109        // Create memento of the binary
110        final String mementoType = "text/old";
111        final String mementoContent = "Hello old world";
112        final FcrepoResponse binMementoResp = client.createMemento(timemapURI, HISTORIC_TIMESTAMP)
113                .body(new ByteArrayInputStream(mementoContent.getBytes()), mementoType)
114                .perform();
115        assertEquals(CREATED.getStatusCode(), binMementoResp.getStatusCode());
116
117        // Determine location of description and its timemap
118        final URI descURI = binMementoResp.getLinkHeaders(DESCRIBEDBY_REL).get(0);
119        final URI descTimeMapURI = client.head(descURI).perform().getLinkHeaders(MEMENTO_TIME_MAP_REL).get(0);
120
121        // Create memento of the binary description
122        final String titleValue = "Ancient Binary";
123        // Modify the original model before creating memento
124        final Model originalModel = getResponseModel(client.get(descURI).perform());
125        final Resource resc = originalModel.getResource(originalURI.toString());
126        resc.addLiteral(DC_TITLE, titleValue);
127        // Set the updated mimetype for the memento
128        resc.removeAll(RdfLexicon.HAS_MIME_TYPE);
129        resc.addLiteral(RdfLexicon.HAS_MIME_TYPE, mementoType);
130
131        final FcrepoResponse descMementoResp = client.createMemento(descTimeMapURI, HISTORIC_TIMESTAMP)
132                .body(modelToInputStream(originalModel), "text/turtle")
133                .perform();
134        assertEquals(CREATED.getStatusCode(), descMementoResp.getStatusCode());
135
136        // Verify the historic binary was created
137        final URI binaryMementoURI = binMementoResp.getLocation();
138        final FcrepoResponse getBinMementoResp = client.get(binaryMementoURI).perform();
139        final String getBinContent = IOUtils.toString(getBinMementoResp.getBody(), "UTF-8");
140
141        assertEquals(mementoContent, getBinContent);
142        assertEquals(mementoType, getBinMementoResp.getContentType());
143        assertTrue("Retrieved object must be a memento", getBinMementoResp.hasType(MEMENTO_TYPE));
144        assertEquals("Memento did not have expected datetime",
145                HISTORIC_TIMESTAMP, getBinMementoResp.getHeaderValue(MEMENTO_DATETIME));
146
147        // Find the memento of the description by its memento datetime
148        final FcrepoResponse mementoDescNegoResp = client.get(descURI)
149                .acceptDatetime(HISTORIC_TIMESTAMP)
150                .perform();
151
152        assertEquals("Memento did not have expected datetime",
153                HISTORIC_TIMESTAMP, mementoDescNegoResp.getHeaderValue(MEMENTO_DATETIME));
154        final Model respModel = getResponseModel(mementoDescNegoResp);
155        assertTrue("Memento must contain provided property",
156                respModel.contains(createResource(originalURI.toString()), DC_TITLE, titleValue));
157    }
158
159    // create memento from historic version, container
160    @Test
161    public void testCreateHistoricContainerMemento() throws Exception {
162        // Create original resource with custom property
163        final FcrepoResponse createOriginalResp = client.post(new URI(serverAddress))
164                .perform();
165
166        final URI originalURI = createOriginalResp.getLocation();
167        final URI timemapURI = createOriginalResp.getLinkHeaders(MEMENTO_TIME_MAP_REL).get(0);
168
169        // Add a property to the resource to use as the historic version
170        final String titleValue = "Very Historical";
171        final FcrepoResponse originalResp = client.get(originalURI).perform();
172        final Model originalModel = getResponseModel(originalResp);
173        originalModel.getResource(originalURI.toString())
174                .addLiteral(DC_TITLE, titleValue);
175
176        // Create historic memento with updated model
177        final FcrepoResponse mementoResp = client.createMemento(timemapURI, HISTORIC_DATETIME.toInstant())
178                .body(modelToInputStream(originalModel), "text/turtle")
179                .perform();
180        assertEquals(CREATED.getStatusCode(), mementoResp.getStatusCode());
181
182        // Verify that the memento matches expectations
183        final URI mementoURI = mementoResp.getLocation();
184        final FcrepoResponse getResp = client.get(mementoURI).perform();
185        assertTrue("Retrieved object must be a memento", getResp.hasType(MEMENTO_TYPE));
186        assertEquals("Memento did not have expected datetime",
187                HISTORIC_TIMESTAMP, getResp.getHeaderValue(MEMENTO_DATETIME));
188        final Model respModel = getResponseModel(getResp);
189        assertTrue("Memento must contain provided property",
190                respModel.contains(null, DC_TITLE, titleValue));
191    }
192
193    @Test
194    public void testDatetimeNegotiation() throws Exception {
195        final FcrepoResponse createOriginalResp = client.post(new URI(serverAddress))
196                .perform();
197
198        final URI location = createOriginalResp.getLocation();
199        final URI timemapURI = createOriginalResp.getLinkHeaders(MEMENTO_TIME_MAP_REL).get(0);
200        // create version
201        client.createMemento(timemapURI).perform();
202
203        // Negotiate for the most recent memento
204        final FcrepoResponse negotiateResp = client.get(location)
205                .acceptDatetime(Instant.now())
206                .perform();
207
208        assertTrue(negotiateResp.hasType(MEMENTO_TYPE));
209    }
210
211    private InputStream modelToInputStream(final Model model) {
212        final ByteArrayOutputStream out = new ByteArrayOutputStream();
213        model.write(out, "text/turtle");
214        return new ByteArrayInputStream(out.toByteArray());
215    }
216}