001package org.fcrepo.migration.handlers.ocfl;
002
003import com.fasterxml.jackson.annotation.JsonInclude;
004import com.fasterxml.jackson.databind.ObjectMapper;
005import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
006import edu.wisc.library.ocfl.api.MutableOcflRepository;
007import edu.wisc.library.ocfl.api.model.FileDetails;
008import edu.wisc.library.ocfl.api.model.ObjectVersionId;
009import edu.wisc.library.ocfl.core.OcflRepositoryBuilder;
010import edu.wisc.library.ocfl.core.extension.storage.layout.config.HashedNTupleLayoutConfig;
011import edu.wisc.library.ocfl.core.path.mapper.LogicalPathMappers;
012import edu.wisc.library.ocfl.core.storage.filesystem.FileSystemOcflStorage;
013import org.apache.commons.codec.digest.DigestUtils;
014import org.apache.commons.io.IOUtils;
015import org.apache.commons.lang3.SystemUtils;
016import org.fcrepo.migration.ContentDigest;
017import org.fcrepo.migration.DatastreamInfo;
018import org.fcrepo.migration.DatastreamVersion;
019import org.fcrepo.migration.DefaultObjectInfo;
020import org.fcrepo.migration.MigrationType;
021import org.fcrepo.migration.ObjectProperties;
022import org.fcrepo.migration.ObjectProperty;
023import org.fcrepo.migration.ObjectVersionReference;
024import org.fcrepo.storage.ocfl.CommitType;
025import org.fcrepo.storage.ocfl.DefaultOcflObjectSessionFactory;
026import org.fcrepo.storage.ocfl.InteractionModel;
027import org.fcrepo.storage.ocfl.OcflObjectSession;
028import org.fcrepo.storage.ocfl.OcflObjectSessionFactory;
029import org.fcrepo.storage.ocfl.PersistencePaths;
030import org.fcrepo.storage.ocfl.ResourceHeadersVersion;
031import org.fcrepo.storage.ocfl.cache.NoOpCache;
032import org.junit.Before;
033import org.junit.Rule;
034import org.junit.Test;
035import org.junit.rules.TemporaryFolder;
036import org.mockito.Mockito;
037import org.mockito.stubbing.Answer;
038
039import java.io.ByteArrayInputStream;
040import java.io.IOException;
041import java.io.InputStream;
042import java.io.UncheckedIOException;
043import java.nio.charset.StandardCharsets;
044import java.nio.file.Files;
045import java.nio.file.Path;
046import java.time.Instant;
047import java.util.List;
048import java.util.Objects;
049
050import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS;
051import static org.hamcrest.MatcherAssert.assertThat;
052import static org.hamcrest.Matchers.allOf;
053import static org.hamcrest.Matchers.containsString;
054import static org.junit.Assert.assertEquals;
055import static org.junit.Assert.assertFalse;
056import static org.junit.Assert.assertNotEquals;
057import static org.junit.Assert.assertTrue;
058import static org.mockito.Mockito.doReturn;
059import static org.mockito.Mockito.when;
060
061/**
062 * @author pwinckles
063 */
064public class ArchiveGroupHandlerTest {
065
066    private static final String FCREPO_ROOT = "info:fedora/";
067    private static final String USER = "fedoraAdmin";
068    private static final String INLINE = "X";
069    private static final String PROXY = "E";
070    private static final String REDIRECT = "R";
071    private static final String MANAGED = "M";
072    private static final String DS_ACTIVE = "A";
073    private static final String DS_INACTIVE = "I";
074    private static final String DS_DELETED = "D";
075    private static final String OBJ_ACTIVE = "Active";
076    private static final String OBJ_INACTIVE = "Inactive";
077    private static final String OBJ_DELETED = "Deleted";
078
079    @Rule
080    public TemporaryFolder tempDir = new TemporaryFolder();
081
082    private Path ocflRoot;
083    private Path staging;
084
085    private MutableOcflRepository ocflRepo;
086    private OcflObjectSessionFactory sessionFactory;
087    private OcflObjectSessionFactory plainSessionFactory;
088
089    private ObjectMapper objectMapper;
090    private String date;
091
092    @Before
093    public void setup() throws IOException {
094        ocflRoot = tempDir.newFolder("ocfl").toPath();
095        staging = tempDir.newFolder("staging").toPath();
096
097        final var logicalPathMapper = SystemUtils.IS_OS_WINDOWS ?
098                LogicalPathMappers.percentEncodingWindowsMapper() : LogicalPathMappers.percentEncodingLinuxMapper();
099
100        ocflRepo = new OcflRepositoryBuilder()
101                .defaultLayoutConfig(new HashedNTupleLayoutConfig())
102                .logicalPathMapper(logicalPathMapper)
103                .storage(FileSystemOcflStorage.builder().repositoryRoot(ocflRoot).build())
104                .workDir(staging)
105                .buildMutable();
106
107        objectMapper = new ObjectMapper()
108                .configure(WRITE_DATES_AS_TIMESTAMPS, false)
109                .registerModule(new JavaTimeModule())
110                .setSerializationInclusion(JsonInclude.Include.NON_NULL);
111
112        sessionFactory = new DefaultOcflObjectSessionFactory(ocflRepo, staging, objectMapper,
113                new NoOpCache<>(), new NoOpCache<>(), CommitType.NEW_VERSION,
114                "testing", USER, "info:fedora/fedoraAdmin");
115
116        plainSessionFactory = new PlainOcflObjectSessionFactory(ocflRepo, staging,
117                "testing", USER, "info:fedora/fedoraAdmin", false);
118
119        date = Instant.now().toString().substring(0, 10);
120    }
121
122    @Test
123    public void processObjectSingleVersionF6Format() throws IOException {
124        final var handler = createHandler(MigrationType.FEDORA_OCFL, false, false);
125
126        final var pid = "obj1";
127        final var dsId1 = "ds1";
128        final var dsId2 = "ds2";
129
130        final var ds1 = datastreamVersion(dsId1, true, MANAGED, "text/plain", "hello", null);
131        final var ds2 = datastreamVersion(dsId2, true, INLINE, "application/xml", "<xml>goodbye</xml>", null);
132        when(ds2.getSize()).thenReturn(100L);
133
134        handler.processObjectVersions(List.of(
135                objectVersionReference(pid, true, List.of(ds1, ds2))
136        ), new DefaultObjectInfo(pid, pid, Files.createTempFile(tempDir.getRoot().toPath(), "foxml", "xml")));
137
138        final var ocflObjectId = addPrefix(pid);
139        final var session = sessionFactory.newSession(ocflObjectId);
140
141        verifyObjectRdf(contentToString(session, ocflObjectId));
142        verifyObjectHeaders(session, ocflObjectId);
143
144        verifyBinary(contentToString(session, ocflObjectId, dsId1), ds1);
145        verifyHeaders(session, ocflObjectId, dsId1, ds1);
146        verifyDescRdf(session, ocflObjectId, dsId1, ds1);
147        verifyDescHeaders(session, ocflObjectId, dsId1);
148
149        verifyBinary(contentToString(session, ocflObjectId, dsId2), ds2);
150        verifyHeaders(session, ocflObjectId, dsId2, ds2);
151        verifyDescRdf(session, ocflObjectId, dsId2, ds2);
152        verifyDescHeaders(session, ocflObjectId, dsId2);
153    }
154
155    @Test
156    public void processObjectMultipleVersionsF6Format() throws IOException {
157        final var handler = createHandler(MigrationType.FEDORA_OCFL, false, false);
158
159        final var pid = "obj2";
160        final var dsId1 = "ds3";
161        final var dsId2 = "ds4";
162
163        final var ds1V1 = datastreamVersion(dsId1, true, MANAGED, "application/xml", "<h1>hello</h1>", null);
164        final var ds2V1 = datastreamVersion(dsId2, true, MANAGED, "text/plain", "goodbye", null);
165
166        final var ds2V2 = datastreamVersion(dsId2, false, MANAGED, "text/plain", "fedora", null);
167
168        handler.processObjectVersions(List.of(
169                objectVersionReference(pid, true, List.of(ds1V1, ds2V1)),
170                objectVersionReference(pid, false, List.of(ds2V2))
171        ), new DefaultObjectInfo(pid, pid, Files.createTempFile(tempDir.getRoot().toPath(), "foxml", "xml")));
172
173        final var ocflObjectId = addPrefix(pid);
174        final var session = sessionFactory.newSession(ocflObjectId);
175
176        verifyObjectRdf(contentToString(session, ocflObjectId));
177        verifyObjectHeaders(session, ocflObjectId);
178
179        verifyBinary(contentToString(session, ocflObjectId, dsId1), ds1V1);
180        verifyHeaders(session, ocflObjectId, dsId1, ds1V1);
181        verifyDescRdf(session, ocflObjectId, dsId1, ds1V1);
182        verifyDescHeaders(session, ocflObjectId, dsId1);
183
184        verifyBinary(contentVersionToString(session, ocflObjectId, dsId2, "v1"), ds2V1);
185        verifyHeaders(session, ocflObjectId, dsId2, ds2V1, "v1");
186        verifyDescRdf(session, ocflObjectId, dsId2, ds2V1, "v1");
187        verifyDescHeaders(session, ocflObjectId, dsId2, "v1");
188
189        verifyBinary(contentVersionToString(session, ocflObjectId, dsId2, "v2"), ds2V2);
190        verifyHeaders(session, ocflObjectId, dsId2, ds2V2, "v2");
191        verifyDescRdf(session, ocflObjectId, dsId2, ds2V2, "v2");
192        verifyDescHeaders(session, ocflObjectId, dsId2, "v2");
193    }
194
195    @Test
196    public void processObjectMultipleVersionsWithDeletedDsF6Format() throws IOException {
197        final var handler = createHandler(MigrationType.FEDORA_OCFL, false, false);
198
199        final var pid = "obj2";
200        final var dsId1 = "ds3";
201        final var dsId2 = "ds4";
202
203        final var ds1V1 = datastreamVersion(dsId1, true, MANAGED, "application/xml", "<h1>hello</h1>",
204                DS_INACTIVE, null);
205        final var ds2V1 = datastreamVersion(dsId2, true, MANAGED, "text/plain", "goodbye", DS_DELETED, null);
206
207        final var ds2V2 = datastreamVersion(dsId2, false, MANAGED, "text/plain", "fedora", DS_DELETED, null);
208
209        handler.processObjectVersions(List.of(
210                objectVersionReference(pid, true, List.of(ds1V1, ds2V1)),
211                objectVersionReference(pid, false, List.of(ds2V2))
212        ), new DefaultObjectInfo(pid, pid, Files.createTempFile(tempDir.getRoot().toPath(), "foxml", "xml")));
213
214        final var ocflObjectId = addPrefix(pid);
215        final var session = sessionFactory.newSession(ocflObjectId);
216
217        verifyObjectRdf(contentToString(session, ocflObjectId));
218        verifyObjectHeaders(session, ocflObjectId);
219
220        verifyBinary(contentToString(session, ocflObjectId, dsId1), ds1V1);
221        verifyHeaders(session, ocflObjectId, dsId1, ds1V1);
222        verifyDescRdf(session, ocflObjectId, dsId1, ds1V1);
223        verifyDescHeaders(session, ocflObjectId, dsId1);
224
225        verifyBinary(contentVersionToString(session, ocflObjectId, dsId2, "v1"), ds2V1);
226        verifyHeaders(session, ocflObjectId, dsId2, ds2V1, "v1");
227        verifyDescRdf(session, ocflObjectId, dsId2, ds2V1, "v1");
228        verifyDescHeaders(session, ocflObjectId, dsId2, "v1");
229
230        verifyBinary(contentVersionToString(session, ocflObjectId, dsId2, "v2"), ds2V2);
231        verifyHeaders(session, ocflObjectId, dsId2, ds2V2, "v2");
232        verifyDescRdf(session, ocflObjectId, dsId2, ds2V2, "v2");
233        verifyDescHeaders(session, ocflObjectId, dsId2, "v2");
234
235        verifyResourceDeleted(session, resourceId(ocflObjectId, dsId2));
236        verifyResourceDeleted(session, metadataId(ocflObjectId, dsId2));
237    }
238
239    @Test
240    public void processObjectMultipleVersionsAndDeleteInactiveF6Format() throws IOException {
241        final var handler = createHandler(MigrationType.FEDORA_OCFL, false, true);
242
243        final var pid = "obj2";
244        final var dsId1 = "ds3";
245        final var dsId2 = "ds4";
246
247        final var ds1V1 = datastreamVersion(dsId1, true, MANAGED, "application/xml", "<h1>hello</h1>",
248                DS_INACTIVE, null);
249        final var ds2V1 = datastreamVersion(dsId2, true, MANAGED, "text/plain", "goodbye", DS_DELETED, null);
250
251        final var ds2V2 = datastreamVersion(dsId2, false, MANAGED, "text/plain", "fedora", DS_DELETED, null);
252
253        handler.processObjectVersions(List.of(
254                objectVersionReference(pid, true, List.of(ds1V1, ds2V1)),
255                objectVersionReference(pid, false, List.of(ds2V2))
256        ), new DefaultObjectInfo(pid, pid, Files.createTempFile(tempDir.getRoot().toPath(), "foxml", "xml")));
257
258        final var ocflObjectId = addPrefix(pid);
259        final var session = sessionFactory.newSession(ocflObjectId);
260
261        verifyObjectRdf(contentToString(session, ocflObjectId));
262        verifyObjectHeaders(session, ocflObjectId);
263
264        verifyBinary(contentVersionToString(session, ocflObjectId, dsId1, "v1"), ds1V1);
265        verifyHeaders(session, ocflObjectId, dsId1, ds1V1, "v1");
266        verifyDescRdf(session, ocflObjectId, dsId1, ds1V1, "v1");
267        verifyDescHeaders(session, ocflObjectId, dsId1, "v1");
268
269        verifyBinary(contentVersionToString(session, ocflObjectId, dsId2, "v1"), ds2V1);
270        verifyHeaders(session, ocflObjectId, dsId2, ds2V1, "v1");
271        verifyDescRdf(session, ocflObjectId, dsId2, ds2V1, "v1");
272        verifyDescHeaders(session, ocflObjectId, dsId2, "v1");
273
274        verifyBinary(contentVersionToString(session, ocflObjectId, dsId2, "v2"), ds2V2);
275        verifyHeaders(session, ocflObjectId, dsId2, ds2V2, "v2");
276        verifyDescRdf(session, ocflObjectId, dsId2, ds2V2, "v2");
277        verifyDescHeaders(session, ocflObjectId, dsId2, "v2");
278
279        verifyResourceDeleted(session, resourceId(ocflObjectId, dsId1));
280        verifyResourceDeleted(session, metadataId(ocflObjectId, dsId1));
281        verifyResourceDeleted(session, resourceId(ocflObjectId, dsId2));
282        verifyResourceDeleted(session, metadataId(ocflObjectId, dsId2));
283    }
284
285    @Test
286    public void processObjectMultipleVersionsAndObjectDeletedF6Format() throws IOException {
287        final var handler = createHandler(MigrationType.FEDORA_OCFL, false, false);
288
289        final var pid = "obj2";
290        final var dsId1 = "ds3";
291        final var dsId2 = "ds4";
292
293        final var ds1V1 = datastreamVersion(dsId1, true, MANAGED, "application/xml", "<h1>hello</h1>", null);
294        final var ds2V1 = datastreamVersion(dsId2, true, MANAGED, "text/plain", "goodbye", null);
295
296        final var ds2V2 = datastreamVersion(dsId2, false, MANAGED, "text/plain", "fedora", null);
297
298        handler.processObjectVersions(List.of(
299                objectVersionReference(pid, true, OBJ_DELETED, List.of(ds1V1, ds2V1)),
300                objectVersionReference(pid, false, OBJ_DELETED, List.of(ds2V2))
301        ), new DefaultObjectInfo(pid, pid, Files.createTempFile(tempDir.getRoot().toPath(), "foxml", "xml")));
302
303        final var ocflObjectId = addPrefix(pid);
304        final var session = sessionFactory.newSession(ocflObjectId);
305
306        verifyObjectRdf(contentVersionToString(session, ocflObjectId, "v1"));
307        verifyObjectHeaders(session, ocflObjectId, "v1");
308
309        verifyBinary(contentVersionToString(session, ocflObjectId, dsId1, "v1"), ds1V1);
310        verifyHeaders(session, ocflObjectId, dsId1, ds1V1, "v1");
311        verifyDescRdf(session, ocflObjectId, dsId1, ds1V1, "v1");
312        verifyDescHeaders(session, ocflObjectId, dsId1, "v1");
313
314        verifyBinary(contentVersionToString(session, ocflObjectId, dsId2, "v1"), ds2V1);
315        verifyHeaders(session, ocflObjectId, dsId2, ds2V1, "v1");
316        verifyDescRdf(session, ocflObjectId, dsId2, ds2V1, "v1");
317        verifyDescHeaders(session, ocflObjectId, dsId2, "v1");
318
319        verifyBinary(contentVersionToString(session, ocflObjectId, dsId2, "v2"), ds2V2);
320        verifyHeaders(session, ocflObjectId, dsId2, ds2V2, "v2");
321        verifyDescRdf(session, ocflObjectId, dsId2, ds2V2, "v2");
322        verifyDescHeaders(session, ocflObjectId, dsId2, "v2");
323
324        verifyResourceDeleted(session, ocflObjectId);
325        verifyResourceDeleted(session, resourceId(ocflObjectId, dsId1));
326        verifyResourceDeleted(session, metadataId(ocflObjectId, dsId1));
327        verifyResourceDeleted(session, resourceId(ocflObjectId, dsId2));
328        verifyResourceDeleted(session, metadataId(ocflObjectId, dsId2));
329    }
330
331    @Test
332    public void processObjectMultipleVersionsAndObjectInactiveDeletedF6Format() throws IOException {
333        final var handler = createHandler(MigrationType.FEDORA_OCFL, false, true);
334
335        final var pid = "obj2";
336        final var dsId1 = "ds3";
337        final var dsId2 = "ds4";
338
339        final var ds1V1 = datastreamVersion(dsId1, true, MANAGED, "application/xml", "<h1>hello</h1>", null);
340        final var ds2V1 = datastreamVersion(dsId2, true, MANAGED, "text/plain", "goodbye", null);
341
342        final var ds2V2 = datastreamVersion(dsId2, false, MANAGED, "text/plain", "fedora", null);
343
344        handler.processObjectVersions(List.of(
345                objectVersionReference(pid, true, OBJ_INACTIVE, List.of(ds1V1, ds2V1)),
346                objectVersionReference(pid, false, OBJ_INACTIVE, List.of(ds2V2))
347        ), new DefaultObjectInfo(pid, pid, Files.createTempFile(tempDir.getRoot().toPath(), "foxml", "xml")));
348
349        final var ocflObjectId = addPrefix(pid);
350        final var session = sessionFactory.newSession(ocflObjectId);
351
352        verifyObjectRdf(contentVersionToString(session, ocflObjectId, "v1"));
353        verifyObjectHeaders(session, ocflObjectId, "v1");
354
355        verifyBinary(contentVersionToString(session, ocflObjectId, dsId1, "v1"), ds1V1);
356        verifyHeaders(session, ocflObjectId, dsId1, ds1V1, "v1");
357        verifyDescRdf(session, ocflObjectId, dsId1, ds1V1, "v1");
358        verifyDescHeaders(session, ocflObjectId, dsId1, "v1");
359
360        verifyBinary(contentVersionToString(session, ocflObjectId, dsId2, "v1"), ds2V1);
361        verifyHeaders(session, ocflObjectId, dsId2, ds2V1, "v1");
362        verifyDescRdf(session, ocflObjectId, dsId2, ds2V1, "v1");
363        verifyDescHeaders(session, ocflObjectId, dsId2, "v1");
364
365        verifyBinary(contentVersionToString(session, ocflObjectId, dsId2, "v2"), ds2V2);
366        verifyHeaders(session, ocflObjectId, dsId2, ds2V2, "v2");
367        verifyDescRdf(session, ocflObjectId, dsId2, ds2V2, "v2");
368        verifyDescHeaders(session, ocflObjectId, dsId2, "v2");
369
370        verifyResourceDeleted(session, ocflObjectId);
371        verifyResourceDeleted(session, resourceId(ocflObjectId, dsId1));
372        verifyResourceDeleted(session, metadataId(ocflObjectId, dsId1));
373        verifyResourceDeleted(session, resourceId(ocflObjectId, dsId2));
374        verifyResourceDeleted(session, metadataId(ocflObjectId, dsId2));
375    }
376
377    @Test
378    public void processObjectMultipleVersionsPlainFormat() throws IOException {
379        final var handler = createHandler(MigrationType.PLAIN_OCFL, false, false);
380
381        final var pid = "obj2";
382        final var dsId1 = "ds3";
383        final var dsId2 = "ds4";
384
385        final var ds1V1 = datastreamVersion(dsId1, true, MANAGED, "application/xml", "<h1>hello</h1>", null);
386        final var ds2V1 = datastreamVersion(dsId2, true, MANAGED, "text/plain", "goodbye", null);
387
388        final var ds2V2 = datastreamVersion(dsId2, false, MANAGED, "text/plain", "fedora", null);
389
390        handler.processObjectVersions(List.of(
391                objectVersionReference(pid, true, List.of(ds1V1, ds2V1)),
392                objectVersionReference(pid, false, List.of(ds2V2))
393        ), new DefaultObjectInfo(pid, pid, Files.createTempFile(tempDir.getRoot().toPath(), "foxml", "xml")));
394
395        final var rootResourceId = addPrefix(pid);
396
397        verifyFcrepoNotExists(rootResourceId);
398
399        verifyObjectRdf(rawContentToString(rootResourceId, PersistencePaths.rdfResource(rootResourceId, rootResourceId)
400                .getContentFilePath()));
401
402        verifyBinary(rawContentVersionToString(rootResourceId,
403                PersistencePaths.nonRdfResource(rootResourceId, resourceId(rootResourceId, dsId1)).getContentFilePath(),
404                "v1"), ds1V1);
405        verifyPlainDescRdf(rawContentVersionToString(rootResourceId,
406                PersistencePaths.rdfResource(rootResourceId, metadataId(rootResourceId, dsId1)).getContentFilePath(),
407                "v1"), ds1V1);
408
409        verifyBinary(rawContentVersionToString(rootResourceId,
410                PersistencePaths.nonRdfResource(rootResourceId, resourceId(rootResourceId, dsId2)).getContentFilePath(),
411                "v1"), ds2V1);
412        verifyPlainDescRdf(rawContentVersionToString(rootResourceId,
413                PersistencePaths.rdfResource(rootResourceId, metadataId(rootResourceId, dsId2)).getContentFilePath(),
414                "v1"), ds2V1);
415
416        verifyBinary(rawContentVersionToString(rootResourceId,
417                PersistencePaths.nonRdfResource(rootResourceId, resourceId(rootResourceId, dsId2)).getContentFilePath(),
418                "v2"), ds2V2);
419        verifyPlainDescRdf(rawContentVersionToString(rootResourceId,
420                PersistencePaths.rdfResource(rootResourceId, metadataId(rootResourceId, dsId2)).getContentFilePath(),
421                "v2"), ds2V2);
422    }
423
424    @Test
425    public void processObjectMultipleVersionsWithDeletedDsPlainFormat() throws IOException {
426        final var handler = createHandler(MigrationType.PLAIN_OCFL, false, false);
427
428        final var pid = "obj2";
429        final var dsId1 = "ds3";
430        final var dsId2 = "ds4";
431
432        final var ds1V1 = datastreamVersion(dsId1, true, MANAGED, "application/xml", "<h1>hello</h1>", null);
433        final var ds2V1 = datastreamVersion(dsId2, true, MANAGED, "text/plain", "goodbye", DS_DELETED, null);
434
435        final var ds2V2 = datastreamVersion(dsId2, false, MANAGED, "text/plain", "fedora", DS_DELETED, null);
436
437        handler.processObjectVersions(List.of(
438                objectVersionReference(pid, true, List.of(ds1V1, ds2V1)),
439                objectVersionReference(pid, false, List.of(ds2V2))
440        ), new DefaultObjectInfo(pid, pid, Files.createTempFile(tempDir.getRoot().toPath(), "foxml", "xml")));
441
442        final var rootResourceId = addPrefix(pid);
443
444        verifyFcrepoNotExists(rootResourceId);
445
446        verifyObjectRdf(rawContentToString(rootResourceId, PersistencePaths.rdfResource(rootResourceId, rootResourceId)
447                .getContentFilePath()));
448
449        verifyBinary(rawContentVersionToString(rootResourceId,
450                PersistencePaths.nonRdfResource(rootResourceId, resourceId(rootResourceId, dsId1)).getContentFilePath(),
451                "v1"), ds1V1);
452        verifyPlainDescRdf(rawContentVersionToString(rootResourceId,
453                PersistencePaths.rdfResource(rootResourceId, metadataId(rootResourceId, dsId1)).getContentFilePath(),
454                "v1"), ds1V1);
455
456        verifyBinary(rawContentVersionToString(rootResourceId,
457                PersistencePaths.nonRdfResource(rootResourceId, resourceId(rootResourceId, dsId2)).getContentFilePath(),
458                "v1"), ds2V1);
459        verifyPlainDescRdf(rawContentVersionToString(rootResourceId,
460                PersistencePaths.rdfResource(rootResourceId, metadataId(rootResourceId, dsId2)).getContentFilePath(),
461                "v1"), ds2V1);
462
463        verifyBinary(rawContentVersionToString(rootResourceId,
464                PersistencePaths.nonRdfResource(rootResourceId, resourceId(rootResourceId, dsId2)).getContentFilePath(),
465                "v2"), ds2V2);
466        verifyPlainDescRdf(rawContentVersionToString(rootResourceId,
467                PersistencePaths.rdfResource(rootResourceId, metadataId(rootResourceId, dsId2)).getContentFilePath(),
468                "v2"), ds2V2);
469
470        rawVerifyDoesNotExist(rootResourceId,
471                PersistencePaths.nonRdfResource(rootResourceId, resourceId(rootResourceId, dsId2))
472                        .getContentFilePath());
473        rawVerifyDoesNotExist(rootResourceId,
474                PersistencePaths.rdfResource(rootResourceId, metadataId(rootResourceId, dsId2)).getContentFilePath());
475    }
476
477    @Test
478    public void processObjectMultipleVersionsWithInactiveDeletedObjectPlainFormat() throws IOException {
479        final var handler = createHandler(MigrationType.PLAIN_OCFL, false, true);
480
481        final var pid = "obj2";
482        final var dsId1 = "ds3";
483        final var dsId2 = "ds4";
484
485        final var ds1V1 = datastreamVersion(dsId1, true, MANAGED, "application/xml", "<h1>hello</h1>", null);
486        final var ds2V1 = datastreamVersion(dsId2, true, MANAGED, "text/plain", "goodbye", null);
487
488        final var ds2V2 = datastreamVersion(dsId2, false, MANAGED, "text/plain", "fedora", null);
489
490        handler.processObjectVersions(List.of(
491                objectVersionReference(pid, true, OBJ_INACTIVE, List.of(ds1V1, ds2V1)),
492                objectVersionReference(pid, false, OBJ_INACTIVE, List.of(ds2V2))
493        ), new DefaultObjectInfo(pid, pid, Files.createTempFile(tempDir.getRoot().toPath(), "foxml", "xml")));
494
495        final var rootResourceId = addPrefix(pid);
496
497        verifyFcrepoNotExists(rootResourceId);
498
499        verifyObjectRdf(rawContentVersionToString(rootResourceId,
500                PersistencePaths.rdfResource(rootResourceId, rootResourceId).getContentFilePath(), "v1"));
501
502        verifyBinary(rawContentVersionToString(rootResourceId,
503                PersistencePaths.nonRdfResource(rootResourceId, resourceId(rootResourceId, dsId1)).getContentFilePath(),
504                "v1"), ds1V1);
505        verifyPlainDescRdf(rawContentVersionToString(rootResourceId,
506                PersistencePaths.rdfResource(rootResourceId, metadataId(rootResourceId, dsId1)).getContentFilePath(),
507                "v1"), ds1V1);
508
509        verifyBinary(rawContentVersionToString(rootResourceId,
510                PersistencePaths.nonRdfResource(rootResourceId, resourceId(rootResourceId, dsId2)).getContentFilePath(),
511                "v1"), ds2V1);
512        verifyPlainDescRdf(rawContentVersionToString(rootResourceId,
513                PersistencePaths.rdfResource(rootResourceId, metadataId(rootResourceId, dsId2)).getContentFilePath(),
514                "v1"), ds2V1);
515
516        verifyBinary(rawContentVersionToString(rootResourceId,
517                PersistencePaths.nonRdfResource(rootResourceId, resourceId(rootResourceId, dsId2)).getContentFilePath(),
518                "v2"), ds2V2);
519        verifyPlainDescRdf(rawContentVersionToString(rootResourceId,
520                PersistencePaths.rdfResource(rootResourceId, metadataId(rootResourceId, dsId2)).getContentFilePath(),
521                "v2"), ds2V2);
522
523        rawVerifyDoesNotExist(rootResourceId, PersistencePaths.rdfResource(rootResourceId, rootResourceId)
524                .getContentFilePath());
525
526        rawVerifyDoesNotExist(rootResourceId,
527                PersistencePaths.nonRdfResource(rootResourceId, resourceId(rootResourceId, dsId1))
528                        .getContentFilePath());
529        rawVerifyDoesNotExist(rootResourceId,
530                PersistencePaths.rdfResource(rootResourceId, metadataId(rootResourceId, dsId1)).getContentFilePath());
531
532        rawVerifyDoesNotExist(rootResourceId,
533                PersistencePaths.nonRdfResource(rootResourceId, resourceId(rootResourceId, dsId2))
534                        .getContentFilePath());
535        rawVerifyDoesNotExist(rootResourceId,
536                PersistencePaths.rdfResource(rootResourceId, metadataId(rootResourceId, dsId2)).getContentFilePath());
537    }
538
539    @Test
540    public void processObjectSingleVersionF6FormatWithExtensions() throws IOException {
541        final var handler = createHandler(MigrationType.FEDORA_OCFL, true, false);
542
543        final var pid = "obj1";
544        final var dsId1 = "ds1";
545        final var dsId1Ext = "ds1.txt";
546        final var dsId2 = "ds2";
547        final var dsId2Ext = "ds2.rdf";
548        final var dsId3 = "ds3";
549        final var dsId3Ext = "ds3.jpg";
550
551        final var ds1 = datastreamVersion(dsId1, true, MANAGED, "text/plain", "text", null);
552        final var ds2 = datastreamVersion(dsId2, true, MANAGED, "application/rdf+xml", "xml", null);
553        final var ds3 = datastreamVersion(dsId3, true, MANAGED, "image/jpeg", "image", null);
554
555        handler.processObjectVersions(List.of(
556                objectVersionReference(pid, true, List.of(ds1, ds2, ds3))
557        ), new DefaultObjectInfo(pid, pid, Files.createTempFile(tempDir.getRoot().toPath(), "foxml", "xml")));
558
559        final var ocflObjectId = addPrefix(pid);
560        final var session = sessionFactory.newSession(ocflObjectId);
561
562        verifyObjectRdf(contentToString(session, ocflObjectId));
563        verifyObjectHeaders(session, ocflObjectId);
564
565        verifyBinary(contentToString(session, ocflObjectId, dsId1Ext), ds1);
566        verifyHeaders(session, ocflObjectId, dsId1Ext, ds1);
567        verifyDescRdf(session, ocflObjectId, dsId1Ext, ds1);
568        verifyDescHeaders(session, ocflObjectId, dsId1Ext);
569
570        verifyBinary(contentToString(session, ocflObjectId, dsId2Ext), ds2);
571        verifyHeaders(session, ocflObjectId, dsId2Ext, ds2);
572        verifyDescRdf(session, ocflObjectId, dsId2Ext, ds2);
573        verifyDescHeaders(session, ocflObjectId, dsId2Ext);
574
575        verifyBinary(contentToString(session, ocflObjectId, dsId3Ext), ds3);
576        verifyHeaders(session, ocflObjectId, dsId3Ext, ds3);
577        verifyDescRdf(session, ocflObjectId, dsId3Ext, ds3);
578        verifyDescHeaders(session, ocflObjectId, dsId3Ext);
579    }
580
581    @Test
582    public void processObjectSingleVersionPlainFormatWithExtensions() throws IOException {
583        final var handler = createHandler(MigrationType.PLAIN_OCFL, true, false);
584
585        final var pid = "obj1";
586        final var dsId1 = "ds1";
587        final var dsId1Ext = "ds1.txt";
588        final var dsId2 = "ds2";
589        final var dsId2Ext = "ds2.rdf";
590        final var dsId3 = "ds3";
591        final var dsId3Ext = "ds3.jpg";
592
593        final var ds1 = datastreamVersion(dsId1, true, MANAGED, "text/plain", "text", null);
594        final var ds2 = datastreamVersion(dsId2, true, MANAGED, "application/rdf+xml", "xml", null);
595        final var ds3 = datastreamVersion(dsId3, true, MANAGED, "image/jpeg", "image", null);
596
597        handler.processObjectVersions(List.of(
598                objectVersionReference(pid, true, List.of(ds1, ds2, ds3))
599        ), new DefaultObjectInfo(pid, pid, Files.createTempFile(tempDir.getRoot().toPath(), "foxml", "xml")));
600
601        final var rootResourceId = addPrefix(pid);
602
603        verifyFcrepoNotExists(rootResourceId);
604
605        verifyObjectRdf(rawContentToString(rootResourceId, PersistencePaths.rdfResource(rootResourceId, rootResourceId)
606                .getContentFilePath()));
607
608        verifyBinary(rawContentToString(rootResourceId,
609                PersistencePaths.nonRdfResource(rootResourceId, resourceId(rootResourceId, dsId1Ext))
610                        .getContentFilePath()),
611                ds1);
612        verifyPlainDescRdf(rawContentToString(rootResourceId,
613                PersistencePaths.rdfResource(rootResourceId, metadataId(rootResourceId, dsId1Ext))
614                        .getContentFilePath()),
615                ds1);
616
617        verifyBinary(rawContentToString(rootResourceId,
618                PersistencePaths.nonRdfResource(rootResourceId, resourceId(rootResourceId, dsId2Ext))
619                        .getContentFilePath()),
620                ds2);
621        verifyPlainDescRdf(rawContentToString(rootResourceId,
622                PersistencePaths.rdfResource(rootResourceId, metadataId(rootResourceId, dsId2Ext))
623                        .getContentFilePath()),
624                ds2);
625
626        verifyBinary(rawContentToString(rootResourceId,
627                PersistencePaths.nonRdfResource(rootResourceId, resourceId(rootResourceId, dsId3Ext))
628                        .getContentFilePath()),
629                ds3);
630        verifyPlainDescRdf(rawContentToString(rootResourceId,
631                PersistencePaths.rdfResource(rootResourceId, metadataId(rootResourceId, dsId3Ext))
632                        .getContentFilePath()),
633                ds3);
634    }
635
636    @Test
637    public void processObjectSingleVersionF6FormatWithExternalBinary() throws IOException {
638        final var handler = createHandler(MigrationType.FEDORA_OCFL, false, false);
639
640        final var pid = "obj1";
641        final var dsId1 = "ds1";
642        final var dsId2 = "ds2";
643        final var dsId3 = "ds3";
644
645        final var ds1 = datastreamVersion(dsId1, true, MANAGED, "text/plain", "hello", null);
646        final var ds2 = datastreamVersion(dsId2, true, PROXY, "text/plain", "", "https://external");
647        final var ds3 = datastreamVersion(dsId3, true, REDIRECT, "text/plain", "", "https://redirect");
648
649        handler.processObjectVersions(List.of(
650                objectVersionReference(pid, true, List.of(ds1, ds2, ds3))
651        ), new DefaultObjectInfo(pid, pid, Files.createTempFile(tempDir.getRoot().toPath(), "foxml", "xml")));
652
653        final var ocflObjectId = addPrefix(pid);
654        final var session = sessionFactory.newSession(ocflObjectId);
655
656        verifyObjectRdf(contentToString(session, ocflObjectId));
657        verifyObjectHeaders(session, ocflObjectId);
658
659        verifyBinary(contentToString(session, ocflObjectId, dsId1), ds1);
660        verifyHeaders(session, ocflObjectId, dsId1, ds1);
661        verifyDescRdf(session, ocflObjectId, dsId1, ds1);
662        verifyDescHeaders(session, ocflObjectId, dsId1);
663
664        verifyContentNotExists(session, resourceId(ocflObjectId, dsId2));
665        verifyHeaders(session, ocflObjectId, dsId2, ds2);
666        verifyDescRdf(session, ocflObjectId, dsId2, ds2);
667        verifyDescHeaders(session, ocflObjectId, dsId2);
668
669        verifyContentNotExists(session, resourceId(ocflObjectId, dsId3));
670        verifyHeaders(session, ocflObjectId, dsId3, ds3);
671        verifyDescRdf(session, ocflObjectId, dsId3, ds3);
672        verifyDescHeaders(session, ocflObjectId, dsId3);
673    }
674
675    @Test
676    public void processObjectSingleVersionPlainFormatWithExternalBinary() throws IOException {
677        final var handler = createHandler(MigrationType.PLAIN_OCFL, false, false);
678
679        final var pid = "obj1";
680        final var dsId1 = "ds1";
681        final var dsId2 = "ds2";
682        final var dsId3 = "ds3";
683
684        final var ds1 = datastreamVersion(dsId1, true, MANAGED, "text/plain", "hello", null);
685        final var ds2 = datastreamVersion(dsId2, true, PROXY, "text/plain", "", "https://external");
686        final var ds3 = datastreamVersion(dsId3, true, REDIRECT, "text/plain", "", "https://redirect");
687
688        handler.processObjectVersions(List.of(
689                objectVersionReference(pid, true, List.of(ds1, ds2, ds3))
690        ), new DefaultObjectInfo(pid, pid, Files.createTempFile(tempDir.getRoot().toPath(), "foxml", "xml")));
691
692        final var rootResourceId = addPrefix(pid);
693
694        verifyFcrepoNotExists(rootResourceId);
695
696        verifyObjectRdf(rawContentToString(rootResourceId, PersistencePaths.rdfResource(rootResourceId, rootResourceId)
697                .getContentFilePath()));
698
699        verifyBinary(rawContentToString(rootResourceId,
700                PersistencePaths.nonRdfResource(rootResourceId, resourceId(rootResourceId, dsId1))
701                        .getContentFilePath()),
702                ds1);
703        verifyPlainDescRdf(rawContentToString(rootResourceId,
704                PersistencePaths.rdfResource(rootResourceId, metadataId(rootResourceId, dsId1)).getContentFilePath()),
705                ds1);
706
707        verifyBinary(rawContentToString(rootResourceId,
708                PersistencePaths.nonRdfResource(rootResourceId, resourceId(rootResourceId, dsId2))
709                        .getContentFilePath()),
710                ds2);
711
712        verifyBinary(rawContentToString(rootResourceId,
713                PersistencePaths.nonRdfResource(rootResourceId, resourceId(rootResourceId, dsId3))
714                        .getContentFilePath()),
715                ds3);
716    }
717
718    private void verifyBinary(final String content, final DatastreamVersion datastreamVersion) {
719        try {
720            if ("RE".contains(datastreamVersion.getDatastreamInfo().getControlGroup())) {
721                assertEquals(datastreamVersion.getExternalOrRedirectURL(), content);
722            } else {
723                assertEquals(IOUtils.toString(datastreamVersion.getContent()), content);
724            }
725        } catch (IOException e) {
726            throw new UncheckedIOException(e);
727        }
728    }
729
730    private void verifyHeaders(final OcflObjectSession session,
731                               final String ocflObjectId,
732                               final String dsId,
733                               final DatastreamVersion datastreamVersion) {
734        verifyHeaders(session, ocflObjectId, dsId, datastreamVersion, null);
735    }
736
737    private void verifyHeaders(final OcflObjectSession session,
738                               final String ocflObjectId,
739                               final String dsId,
740                               final DatastreamVersion datastreamVersion,
741                               final String versionNumber) {
742        final var resourceId = resourceId(ocflObjectId, dsId);
743        try (final var content = session.readContent(resourceId, versionNumber)) {
744            final var headers = content.getHeaders();
745            assertEquals(ResourceHeadersVersion.V1_0, headers.getHeadersVersion());
746            assertEquals(resourceId, headers.getId());
747            assertEquals(ocflObjectId, headers.getParent());
748            assertEquals(ocflObjectId, headers.getArchivalGroupId());
749            assertEquals(InteractionModel.NON_RDF.getUri(), headers.getInteractionModel());
750            assertFalse("not AG", headers.isArchivalGroup());
751            assertFalse("not root", headers.isObjectRoot());
752            assertFalse("not deleted", headers.isDeleted());
753            assertEquals(USER, headers.getCreatedBy());
754            assertEquals(USER, headers.getLastModifiedBy());
755            assertThat(headers.getLastModifiedDate().toString(), containsString(date));
756            assertThat(headers.getMementoCreatedDate().toString(), containsString(date));
757            assertThat(headers.getCreatedDate().toString(), containsString(date));
758            if (INLINE.equals(datastreamVersion.getDatastreamInfo().getControlGroup())) {
759                assertNotEquals(datastreamVersion.getSize(), headers.getContentSize());
760            } else {
761                assertEquals(datastreamVersion.getSize(), headers.getContentSize());
762            }
763            assertEquals(datastreamVersion.getMimeType(), headers.getMimeType());
764            assertEquals(dsId, headers.getFilename());
765            assertEquals(DigestUtils.md5Hex(
766                    String.valueOf(Instant.parse(datastreamVersion.getCreated()).toEpochMilli())).toUpperCase(),
767                    headers.getStateToken());
768            if (headers.getExternalHandling() != null) {
769                assertEquals(1, headers.getDigests().size());
770            } else {
771                assertEquals(2, headers.getDigests().size());
772            }
773            assertEquals(datastreamVersion.getExternalOrRedirectURL(), headers.getExternalUrl());
774            if (Objects.equals(REDIRECT, datastreamVersion.getDatastreamInfo().getControlGroup())) {
775                assertEquals("redirect", headers.getExternalHandling());
776            } else if (Objects.equals(PROXY, datastreamVersion.getDatastreamInfo().getControlGroup())) {
777                assertEquals("proxy", headers.getExternalHandling());
778            }
779        } catch (Exception e) {
780            throw new RuntimeException(e);
781        }
782    }
783
784    private void verifyDescHeaders(final OcflObjectSession session,
785                                   final String ocflObjectId,
786                                   final String dsId) {
787        verifyDescHeaders(session, ocflObjectId, dsId, null);
788    }
789
790    private void verifyDescHeaders(final OcflObjectSession session,
791                                   final String ocflObjectId,
792                                   final String dsId,
793                                   final String versionNumber) {
794        try (final var content = session.readContent(metadataId(ocflObjectId, dsId), versionNumber)) {
795            final var headers = content.getHeaders();
796            assertEquals(ResourceHeadersVersion.V1_0, headers.getHeadersVersion());
797            assertEquals(metadataId(ocflObjectId, dsId), headers.getId());
798            assertEquals(resourceId(ocflObjectId, dsId), headers.getParent());
799            assertEquals(ocflObjectId, headers.getArchivalGroupId());
800            assertEquals(InteractionModel.NON_RDF_DESCRIPTION.getUri(), headers.getInteractionModel());
801            assertFalse("not AG", headers.isArchivalGroup());
802            assertFalse("not root", headers.isObjectRoot());
803            assertFalse("not deleted", headers.isDeleted());
804            assertEquals(USER, headers.getCreatedBy());
805            assertEquals(USER, headers.getLastModifiedBy());
806            assertThat(headers.getLastModifiedDate().toString(), containsString(date));
807            assertThat(headers.getMementoCreatedDate().toString(), containsString(date));
808            assertThat(headers.getCreatedDate().toString(), containsString(date));
809        } catch (Exception e) {
810            throw new RuntimeException(e);
811        }
812    }
813
814    private void verifyObjectHeaders(final OcflObjectSession session,
815                                     final String ocflObjectId) {
816        verifyObjectHeaders(session, ocflObjectId, null);
817    }
818
819    private void verifyObjectHeaders(final OcflObjectSession session,
820                                     final String ocflObjectId,
821                                     final String versionNumber) {
822        try (final var content = session.readContent(ocflObjectId, versionNumber)) {
823            final var headers = content.getHeaders();
824            assertEquals(ResourceHeadersVersion.V1_0, headers.getHeadersVersion());
825            assertEquals(ocflObjectId, headers.getId());
826            assertEquals(FCREPO_ROOT, headers.getParent());
827            assertEquals(InteractionModel.BASIC_CONTAINER.getUri(), headers.getInteractionModel());
828            assertTrue("is AG", headers.isArchivalGroup());
829            assertTrue("is root", headers.isObjectRoot());
830            assertFalse("not deleted", headers.isDeleted());
831            assertEquals(USER, headers.getCreatedBy());
832            assertEquals(USER, headers.getLastModifiedBy());
833            assertThat(headers.getLastModifiedDate().toString(), containsString(date));
834            assertThat(headers.getMementoCreatedDate().toString(), containsString(date));
835            assertThat(headers.getCreatedDate().toString(), containsString(date));
836        } catch (Exception e) {
837            throw new RuntimeException(e);
838        }
839    }
840
841    private void verifyDescRdf(final OcflObjectSession session,
842                               final String ocflObjectId,
843                               final String dsId,
844                               final DatastreamVersion datastreamVersion) {
845        verifyDescRdf(session, ocflObjectId, dsId, datastreamVersion, null);
846    }
847
848    private void verifyDescRdf(final OcflObjectSession session,
849                               final String ocflObjectId,
850                               final String dsId,
851                               final DatastreamVersion datastreamVersion,
852                               final String versionNumber) {
853        try (final var content = session.readContent(metadataId(ocflObjectId, dsId), versionNumber)) {
854            final var value = IOUtils.toString(content.getContentStream().get());
855            assertThat(value, allOf(
856                    containsString(datastreamVersion.getLabel()),
857                    containsString(datastreamVersion.getFormatUri()),
858                    containsString("objState")
859            ));
860        } catch (Exception e) {
861            throw new RuntimeException(e);
862        }
863    }
864
865    private void verifyPlainDescRdf(final String content,
866                                    final DatastreamVersion datastreamVersion) {
867        assertThat(content, allOf(
868                containsString(datastreamVersion.getLabel()),
869                containsString(datastreamVersion.getFormatUri()),
870                containsString(datastreamVersion.getMimeType()),
871                containsString(datastreamVersion.getDatastreamInfo().getDatastreamId()),
872                containsString("objState"),
873                containsString("hasMessageDigest"),
874                containsString("lastModified"),
875                containsString(date),
876                containsString("created")));
877    }
878
879    private void verifyObjectRdf(final String content) {
880        assertThat(content, allOf(
881                containsString("lastModifiedDate"),
882                containsString(date),
883                containsString("createdDate")
884        ));
885    }
886
887    private String contentToString(final OcflObjectSession session, final String ocflObjectId) {
888        return contentVersionToString(session, ocflObjectId, null);
889    }
890
891    private String contentVersionToString(final OcflObjectSession session,
892                                          final String ocflObjectId,
893                                          final String versionNumber) {
894        try (final var content = session.readContent(ocflObjectId, versionNumber)) {
895            return IOUtils.toString(content.getContentStream().get());
896        } catch (IOException e) {
897            throw new UncheckedIOException(e);
898        }
899    }
900
901    private String contentToString(final OcflObjectSession session, final String ocflObjectId, final String dsId) {
902        return contentVersionToString(session, ocflObjectId, dsId, null);
903    }
904
905    private String contentVersionToString(final OcflObjectSession session,
906                                          final String ocflObjectId,
907                                          final String dsId,
908                                          final String versionNumber) {
909        try (final var content = session.readContent(resourceId(ocflObjectId, dsId), versionNumber)) {
910            return IOUtils.toString(content.getContentStream().get());
911        } catch (IOException e) {
912            throw new UncheckedIOException(e);
913        }
914    }
915
916    private String rawContentToString(final String ocflObjectId, final String path) {
917        try (final var content = ocflRepo.getObject(ObjectVersionId.head(ocflObjectId))
918                .getFile(path).getStream()) {
919            return IOUtils.toString(content);
920        } catch (IOException e) {
921            throw new UncheckedIOException(e);
922        }
923    }
924
925    private String rawContentVersionToString(final String ocflObjectId, final String path, final String versionNumber) {
926        try (final var content = ocflRepo.getObject(
927                ObjectVersionId.version(ocflObjectId, versionNumber)).getFile(path).getStream()) {
928            return IOUtils.toString(content);
929        } catch (IOException e) {
930            throw new UncheckedIOException(e);
931        }
932    }
933
934    private void rawVerifyDoesNotExist(final String ocflObjectId, final String path) {
935        assertFalse(String.format("object %s not contain path %s", ocflObjectId, path),
936                ocflRepo.describeVersion(ObjectVersionId.head(ocflObjectId)).containsFile(path));
937    }
938
939    private ArchiveGroupHandler createHandler(final MigrationType migrationType,
940                                              final boolean addExtensions,
941                                              final boolean deleteInactive) {
942        if (migrationType == MigrationType.PLAIN_OCFL) {
943            return new ArchiveGroupHandler(plainSessionFactory, migrationType, addExtensions, deleteInactive,
944                    false, USER,"info:fedora/", false);
945        } else {
946            return new ArchiveGroupHandler(sessionFactory, migrationType, addExtensions, deleteInactive, false, USER,
947                    "info:fedora/", false);
948        }
949    }
950
951    private ObjectVersionReference objectVersionReference(final String pid,
952                                                          final boolean isFirst,
953                                                          final List<DatastreamVersion> datastreamVersions)
954            throws IOException {
955        return objectVersionReference(pid, isFirst, OBJ_ACTIVE, datastreamVersions);
956    }
957
958    private ObjectVersionReference objectVersionReference(final String pid,
959                                                          final boolean isFirst,
960                                                          final String state,
961                                                          final List<DatastreamVersion> datastreamVersions)
962            throws IOException {
963        final var mock = Mockito.mock(ObjectVersionReference.class);
964        when(mock.isFirstVersion()).thenReturn(isFirst);
965        if (isFirst) {
966            final var properties = objectProperties(List.of(
967                    objectProperty("info:fedora/fedora-system:def/view#lastModifiedDate", Instant.now().toString()),
968                    objectProperty("info:fedora/fedora-system:def/model#createdDate", Instant.now().toString()),
969                    objectProperty("info:fedora/fedora-system:def/model#state", state)
970            ));
971            when(mock.getObjectProperties()).thenReturn(properties);
972        }
973        when(mock.getObject()).thenReturn(null);
974        when(mock.listChangedDatastreams()).thenReturn(datastreamVersions);
975        when(mock.getVersionDate()).thenReturn(Instant.now().toString());
976        return mock;
977    }
978
979    private ObjectProperties objectProperties(final List<? extends ObjectProperty> properties) {
980        final var mock = Mockito.mock(ObjectProperties.class);
981        doReturn(properties).when(mock).listProperties();
982        return mock;
983    }
984
985    private ObjectProperty objectProperty(final String name, final String value) {
986        final var mock = Mockito.mock(ObjectProperty.class);
987        when(mock.getName()).thenReturn(name);
988        when(mock.getValue()).thenReturn(value);
989        return mock;
990    }
991
992    private DatastreamVersion datastreamVersion(final String datastreamId,
993                                                final boolean isFirst,
994                                                final String controlGroup,
995                                                final String mimeType,
996                                                final String content,
997                                                final String externalUrl) {
998        return datastreamVersion(datastreamId, isFirst, controlGroup, mimeType, content, DS_ACTIVE, externalUrl);
999    }
1000
1001    private DatastreamVersion datastreamVersion(final String datastreamId,
1002                                                final boolean isFirst,
1003                                                final String controlGroup,
1004                                                final String mimeType,
1005                                                final String content,
1006                                                final String state,
1007                                                final String externalUrl) {
1008        final var mock = Mockito.mock(DatastreamVersion.class);
1009        final var info = datastreamInfo(datastreamId, controlGroup, state);
1010        when(mock.getDatastreamInfo()).thenReturn(info);
1011        when(mock.getMimeType()).thenReturn(mimeType);
1012        try {
1013            when(mock.getContent()).thenAnswer((Answer<InputStream>) invocation -> {
1014                return new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
1015            });
1016        } catch (IOException e) {
1017            throw new UncheckedIOException(e);
1018        }
1019        when(mock.isFirstVersionIn(Mockito.isNull())).thenReturn(isFirst);
1020        when(mock.getCreated()).thenReturn(Instant.now().toString());
1021        when(mock.getExternalOrRedirectURL()).thenReturn(externalUrl);
1022        when(mock.getSize()).thenReturn((long) content.length());
1023        final var contentDigest = contentDigest(content);
1024        when(mock.getContentDigest()).thenReturn(contentDigest);
1025        when(mock.getLabel()).thenReturn(datastreamId + "-label");
1026        when(mock.getFormatUri()).thenReturn("http://format-id");
1027        return mock;
1028    }
1029
1030    private DatastreamInfo datastreamInfo(final String datastreamId, final String controlGroup, final String state) {
1031        final var mock = Mockito.mock(DatastreamInfo.class);
1032        when(mock.getDatastreamId()).thenReturn(datastreamId);
1033        when(mock.getControlGroup()).thenReturn(controlGroup);
1034        when(mock.getState()).thenReturn(state);
1035        return mock;
1036    }
1037
1038    private ContentDigest contentDigest(final String content) {
1039        final var mock = Mockito.mock(ContentDigest.class);
1040        when(mock.getType()).thenReturn("md5");
1041        when(mock.getDigest()).thenReturn(DigestUtils.md5Hex(content));
1042        return mock;
1043    }
1044
1045    private String addPrefix(final String pid) {
1046        return FCREPO_ROOT + pid;
1047    }
1048
1049    private String resourceId(final String ocflObjectId, final String dsId) {
1050        return ocflObjectId + "/" + dsId;
1051    }
1052
1053    private String metadataId(final String ocflObjectId, final String dsId) {
1054        return resourceId(ocflObjectId, dsId) + "/fcr:metadata";
1055    }
1056
1057    private void verifyFcrepoNotExists(final String ocflObjectId) {
1058        final var count = ocflRepo.describeVersion(ObjectVersionId.head(ocflObjectId)).getFiles().stream()
1059                .map(FileDetails::getPath)
1060                .filter(file -> file.startsWith(".fcrepo/"))
1061                .count();
1062        assertEquals(0, count);
1063    }
1064
1065    private void verifyContentNotExists(final OcflObjectSession session, final String ocflObjectId) {
1066        try (final var content = session.readContent(ocflObjectId)) {
1067            assertTrue("Content should not exist", content.getContentStream().isEmpty());
1068        } catch (IOException e) {
1069            throw new UncheckedIOException(e);
1070        }
1071    }
1072
1073    private void verifyResourceDeleted(final OcflObjectSession session, final String ocflObjectId) {
1074        final var headers = session.readHeaders(ocflObjectId);
1075        assertTrue("resource " + ocflObjectId + " should be deleted", headers.isDeleted());
1076        verifyContentNotExists(session, ocflObjectId);
1077    }
1078
1079}