001/*
002 * Copyright 2019 DuraSpace, Inc.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.fcrepo.migration.handlers.ocfl;
018
019import at.favre.lib.bytes.Bytes;
020import com.google.common.base.Preconditions;
021import com.google.common.base.Strings;
022import org.apache.commons.codec.digest.DigestUtils;
023import org.apache.commons.io.IOUtils;
024import org.apache.commons.lang3.StringUtils;
025import org.apache.jena.datatypes.xsd.XSDDatatype;
026import org.apache.jena.rdf.model.Model;
027import org.apache.jena.rdf.model.ModelFactory;
028import org.apache.tika.config.TikaConfig;
029import org.apache.tika.detect.Detector;
030import org.apache.tika.io.TikaInputStream;
031import org.apache.tika.metadata.Metadata;
032import org.apache.tika.mime.MimeType;
033import org.apache.tika.mime.MimeTypeException;
034import org.apache.tika.mime.MimeTypes;
035import org.fcrepo.migration.DatastreamVersion;
036import org.fcrepo.migration.FedoraObjectVersionHandler;
037import org.fcrepo.migration.MigrationType;
038import org.fcrepo.migration.ObjectVersionReference;
039import org.fcrepo.migration.ObjectInfo;
040import org.fcrepo.migration.ContentDigest;
041import org.fcrepo.storage.ocfl.InteractionModel;
042import org.fcrepo.storage.ocfl.OcflObjectSession;
043import org.fcrepo.storage.ocfl.OcflObjectSessionFactory;
044import org.fcrepo.storage.ocfl.ResourceHeaders;
045import org.fcrepo.storage.ocfl.ResourceHeadersVersion;
046import org.slf4j.Logger;
047
048import java.io.ByteArrayInputStream;
049import java.io.ByteArrayOutputStream;
050import java.io.IOException;
051import java.io.InputStream;
052import java.io.UncheckedIOException;
053import java.net.URI;
054import java.nio.file.Files;
055import java.security.DigestInputStream;
056import java.security.MessageDigest;
057import java.security.NoSuchAlgorithmException;
058import java.time.Instant;
059import java.time.OffsetDateTime;
060import java.time.ZoneOffset;
061import java.util.ArrayList;
062import java.util.HashMap;
063import java.util.Map;
064import java.util.concurrent.atomic.AtomicBoolean;
065
066import static org.slf4j.LoggerFactory.getLogger;
067
068/**
069 * Writes a Fedora object as a single ArchiveGroup.
070 * <p>
071 * All datastreams and object metadata from a fcrepo3 object are persisted to a
072 * single OCFL object (ArchiveGroup in fcrepo6 parlance).
073 * </p>
074 * <p>
075 * The contents of each datastream are written verbatim. No attempt is made to
076 * re-write the RELS-EXT to replace subjects and objects with their LDP
077 * counterparts.
078 * </p>
079 * <p>
080 * Note: fedora-specific OCFL serialization features (such as redirects,
081 * container metadata, etc) is not fully defined yet, so are not included here
082 *
083 * @author apb@jhu.edu
084 */
085public class ArchiveGroupHandler implements FedoraObjectVersionHandler {
086
087    private static final Logger LOGGER = getLogger(ArchiveGroupHandler.class);
088
089    private static final String FCREPO_ROOT = "info:fedora/";
090
091    private static final Map<String, String> externalHandlingMap = Map.of(
092            "E", "proxy",
093            "R", "redirect"
094    );
095
096    private static final String INLINE_XML = "X";
097
098    private static final String DS_INACTIVE = "I";
099    private static final String DS_DELETED = "D";
100
101    private static final String OBJ_STATE_PROP = "info:fedora/fedora-system:def/model#state";
102    private static final String OBJ_INACTIVE = "Inactive";
103    private static final String OBJ_DELETED = "Deleted";
104
105    private final OcflObjectSessionFactory sessionFactory;
106    private final boolean addDatastreamExtensions;
107    private final boolean deleteInactive;
108    private final boolean foxmlFile;
109    private final MigrationType migrationType;
110    private final String user;
111    private final String idPrefix;
112    private final Detector mimeDetector;
113    private final boolean disableChecksumValidation;
114
115    /**
116     * Create an ArchiveGroupHandler,
117     *
118     * @param sessionFactory
119     *        OCFL session factory
120     * @param migrationType
121     *        the type of migration to do
122     * @param addDatastreamExtensions
123     *        true if datastreams should be written with file extensions
124     * @param deleteInactive
125     *        true if inactive objects and datastreams should be migrated as deleted
126     * @param foxmlFile
127     *        true if foxml file should be migrated as a whole file, instead of creating property files
128     * @param user
129     *        the username to associated with the migrated resources
130     * @param idPrefix
131     *        the prefix to add to the Fedora 3 pid (default "info:fedora/", like Fedora 3)
132     * @param disableChecksumValidation
133     *        if true, migrator should not try to verify that the datastream content matches Fedora 3 checksums
134     */
135    public ArchiveGroupHandler(final OcflObjectSessionFactory sessionFactory,
136                               final MigrationType migrationType,
137                               final boolean addDatastreamExtensions,
138                               final boolean deleteInactive,
139                               final boolean foxmlFile,
140                               final String user,
141                               final String idPrefix,
142                               final boolean disableChecksumValidation) {
143        this.sessionFactory = Preconditions.checkNotNull(sessionFactory, "sessionFactory cannot be null");
144        this.migrationType = Preconditions.checkNotNull(migrationType, "migrationType cannot be null");
145        this.addDatastreamExtensions = addDatastreamExtensions;
146        this.deleteInactive = deleteInactive;
147        this.foxmlFile = foxmlFile;
148        this.user = Preconditions.checkNotNull(Strings.emptyToNull(user), "user cannot be blank");
149        this.idPrefix = idPrefix;
150        this.disableChecksumValidation = disableChecksumValidation;
151        try {
152            this.mimeDetector = new TikaConfig().getDetector();
153        } catch (Exception e) {
154            throw new RuntimeException(e);
155        }
156    }
157
158    @Override
159    public void processObjectVersions(final Iterable<ObjectVersionReference> versions, final ObjectInfo objectInfo) {
160        // We use the PID to identify the OCFL object
161        final String objectId = objectInfo.getPid();
162        final String f6ObjectId = idPrefix + objectId;
163
164        // We need to manually keep track of the datastream creation dates
165        final Map<String, String> dsCreateDates = new HashMap<>();
166
167        String objectState = null;
168        final Map<String, String> datastreamStates = new HashMap<>();
169
170        for (var ov : versions) {
171            final OcflObjectSession session = new OcflObjectSessionWrapper(sessionFactory.newSession(f6ObjectId));
172
173            if (ov.isFirstVersion()) {
174                if (session.containsResource(f6ObjectId)) {
175                    throw new RuntimeException(f6ObjectId + " already exists!");
176                }
177                objectState = getObjectState(ov, objectId);
178                // Object properties are written only once (as fcrepo3 object properties were unversioned).
179                if (foxmlFile) {
180                    try (InputStream is = Files.newInputStream(objectInfo.getFoxmlPath())) {
181                        final var foxmlDsId = f6ObjectId + "/FOXML";
182                        final var headers = createHeaders(foxmlDsId, f6ObjectId,
183                                InteractionModel.NON_RDF).build();
184                        session.writeResource(headers, is);
185                        //mark FOXML as a deleted datastream so it gets deleted in handleDeletedResources()
186                        datastreamStates.put(foxmlDsId, DS_DELETED);
187                    } catch (IOException io) {
188                        LOGGER.error("error writing " + objectId + " FOXML file to " + f6ObjectId + ": " + io);
189                        throw new UncheckedIOException(io);
190                    }
191                } else {
192                    writeObjectFiles(objectId, f6ObjectId, ov, session);
193                }
194            }
195
196            // Write datastreams and their metadata
197            for (var dv : ov.listChangedDatastreams()) {
198                final var mimeType = resolveMimeType(dv);
199                final String dsId = dv.getDatastreamInfo().getDatastreamId();
200                final String f6DsId = resolveF6DatastreamId(dsId, f6ObjectId, mimeType);
201                final var datastreamFilename = lastPartFromId(f6DsId);
202
203                if (dv.isFirstVersionIn(ov.getObject())) {
204                    dsCreateDates.put(dsId, dv.getCreated());
205                    datastreamStates.put(f6DsId, dv.getDatastreamInfo().getState());
206                }
207                final var createDate = dsCreateDates.get(dsId);
208
209                final var datastreamHeaders = createDatastreamHeaders(dv, f6DsId, f6ObjectId,
210                        datastreamFilename, mimeType, createDate);
211
212                if (externalHandlingMap.containsKey(dv.getDatastreamInfo().getControlGroup())) {
213                    InputStream content = null;
214                    // for plain OCFL migrations, write a file containing the external/redirect URL
215                    if (migrationType == MigrationType.PLAIN_OCFL) {
216                        content = IOUtils.toInputStream(dv.getExternalOrRedirectURL());
217                    }
218                    session.writeResource(datastreamHeaders, content);
219                } else {
220                    try (var contentStream = dv.getContent()) {
221                        writeDatastreamContent(dv, datastreamHeaders, contentStream, session);
222                    } catch (final IOException e) {
223                        throw new UncheckedIOException(e);
224                    }
225                }
226
227                if (!foxmlFile) {
228                    writeDescriptionFiles(f6DsId, datastreamFilename, createDate, datastreamHeaders, dv, session);
229                }
230            }
231
232            LOGGER.debug("Committing object <{}>", f6ObjectId);
233
234            session.versionCreationTimestamp(OffsetDateTime.parse(ov.getVersionDate()));
235            session.commit();
236        }
237
238        handleDeletedResources(f6ObjectId, objectState, datastreamStates);
239    }
240
241    private boolean fedora3DigestValid(final ContentDigest f3Digest) {
242        return f3Digest != null && StringUtils.isNotBlank(f3Digest.getType()) &&
243                StringUtils.isNotBlank(f3Digest.getDigest());
244    }
245
246    private void writeDatastreamContent(final DatastreamVersion dv,
247                                        final ResourceHeaders datastreamHeaders,
248                                        final InputStream contentStream,
249                                        final OcflObjectSession session) throws IOException {
250        if (disableChecksumValidation) {
251            session.writeResource(datastreamHeaders, contentStream);
252            return;
253        }
254        final var f3Digest = dv.getContentDigest();
255        final var ocflObjectId = session.ocflObjectId();
256        final var datastreamId = dv.getDatastreamInfo().getDatastreamId();
257        final var datastreamControlGroup = dv.getDatastreamInfo().getControlGroup();
258        if (fedora3DigestValid(f3Digest)) {
259            try {
260                final var messageDigest = MessageDigest.getInstance(f3Digest.getType());
261                if (migrationType == MigrationType.PLAIN_OCFL) {
262                    session.writeResource(datastreamHeaders, contentStream);
263                } else {
264                    try (var digestStream = new DigestInputStream(contentStream, messageDigest)) {
265                        session.writeResource(datastreamHeaders, digestStream);
266                        final var expectedDigest = f3Digest.getDigest();
267                        final var actualDigest = Bytes.wrap(digestStream.getMessageDigest().digest()).encodeHex();
268                        if (!actualDigest.equalsIgnoreCase(expectedDigest)) {
269                            final var msg = String.format("%s/%s: digest %s doesn't match expected digest %s",
270                                    ocflObjectId, datastreamId, actualDigest, expectedDigest);
271                            throw new RuntimeException(msg);
272                        }
273                    }
274                }
275            } catch (final NoSuchAlgorithmException e) {
276                final var msg = String.format("%s/%s: no digest algorithm %s. Writing resource & continuing.",
277                        ocflObjectId, datastreamId, f3Digest.getType());
278                LOGGER.warn(msg);
279                session.writeResource(datastreamHeaders, contentStream);
280            }
281        } else {
282            if (datastreamControlGroup.equalsIgnoreCase("M")) {
283                final var msg = String.format("%s/%s: missing/invalid digest. Writing resource & continuing.",
284                        ocflObjectId, datastreamId);
285                LOGGER.warn(msg);
286            }
287            session.writeResource(datastreamHeaders, contentStream);
288        }
289    }
290
291    private void handleDeletedResources(final String f6ObjectId,
292                                        final String objectState,
293                                        final Map<String, String> datastreamStates) {
294        final OcflObjectSession session = new OcflObjectSessionWrapper(sessionFactory.newSession(f6ObjectId));
295
296        try {
297            final var now = OffsetDateTime.now().withOffsetSameInstant(ZoneOffset.UTC);
298            final var hasDeletes = new AtomicBoolean(false);
299
300            if (OBJ_DELETED.equals(objectState) || (deleteInactive && OBJ_INACTIVE.equals(objectState))) {
301                hasDeletes.set(true);
302
303                datastreamStates.keySet().forEach(f6DsId -> {
304                    deleteDatastream(f6DsId, now.toInstant(), session);
305                });
306
307                if (migrationType == MigrationType.PLAIN_OCFL) {
308                    deleteOcflMigratedResource(f6ObjectId, InteractionModel.BASIC_CONTAINER, session);
309                } else {
310                    deleteF6MigratedResource(f6ObjectId, now.toInstant(), session);
311                }
312            } else {
313                datastreamStates.forEach((f6DsId, state) -> {
314                    if (DS_DELETED.equals(state) || (deleteInactive && DS_INACTIVE.equals(state))) {
315                        hasDeletes.set(true);
316                        deleteDatastream(f6DsId, now.toInstant(), session);
317                    }
318                });
319            }
320
321            if (hasDeletes.get()) {
322                session.versionCreationTimestamp(now);
323                session.commit();
324            } else {
325                session.abort();
326            }
327        } catch (RuntimeException e) {
328            session.abort();
329            throw e;
330        }
331    }
332
333    private void writeObjectFiles(final String pid,
334                                  final String f6ObjectId,
335                                  final ObjectVersionReference ov,
336                                  final OcflObjectSession session) {
337        final var objectHeaders = createObjectHeaders(f6ObjectId, ov);
338        final var content = getObjTriples(ov, pid);
339        session.writeResource(objectHeaders, content);
340    }
341
342    private void writeDescriptionFiles(final String f6Dsid,
343                                       final String datastreamFilename,
344                                       final String createDate,
345                                       final ResourceHeaders datastreamHeaders,
346                                       final DatastreamVersion dv,
347                                       final OcflObjectSession session) {
348        final var descriptionHeaders = createDescriptionHeaders(f6Dsid,
349                datastreamFilename,
350                datastreamHeaders);
351        session.writeResource(descriptionHeaders, getDsTriples(dv, f6Dsid, createDate));
352    }
353
354    private String f6DescriptionId(final String f6ResourceId) {
355        return f6ResourceId + "/fcr:metadata";
356    }
357
358    private String lastPartFromId(final String id) {
359        return id.substring(id.lastIndexOf('/') + 1);
360    }
361
362    private String resolveF6DatastreamId(final String datastreamId, final String f6ObjectId, final String mimeType) {
363        var id = f6ObjectId + "/" + datastreamId;
364
365        if (addDatastreamExtensions && !Strings.isNullOrEmpty(mimeType)) {
366            id += getExtension(mimeType);
367        }
368
369        return id;
370    }
371
372    private ResourceHeaders.Builder createHeaders(final String id,
373                                                  final String parentId,
374                                                  final InteractionModel model) {
375        final var headers = ResourceHeaders.builder();
376        headers.withHeadersVersion(ResourceHeadersVersion.V1_0);
377        headers.withId(id);
378        headers.withParent(parentId);
379        headers.withInteractionModel(model.getUri());
380        return headers;
381    }
382
383    private ResourceHeaders createObjectHeaders(final String f6ObjectId, final ObjectVersionReference ov) {
384        final var headers = createHeaders(f6ObjectId, FCREPO_ROOT, InteractionModel.BASIC_CONTAINER);
385        headers.withArchivalGroup(true);
386        headers.withObjectRoot(true);
387        headers.withLastModifiedBy(user);
388        headers.withCreatedBy(user);
389
390        ov.getObjectProperties().listProperties().forEach(p -> {
391            if (p.getName().contains("lastModifiedDate")) {
392                final var lastModified = Instant.parse(p.getValue());
393                headers.withLastModifiedDate(lastModified);
394                headers.withMementoCreatedDate(lastModified);
395                headers.withStateToken(DigestUtils.md5Hex(
396                        String.valueOf(lastModified.toEpochMilli())).toUpperCase());
397            } else if (p.getName().contains("createdDate")) {
398                headers.withCreatedDate(Instant.parse(p.getValue()));
399            }
400        });
401
402        return headers.build();
403    }
404
405    private ResourceHeaders createDatastreamHeaders(final DatastreamVersion dv,
406                                                    final String f6DsId,
407                                                    final String f6ObjectId,
408                                                    final String filename,
409                                                    final String mime,
410                                                    final String createDate) {
411        final var lastModified = Instant.parse(dv.getCreated());
412        final var headers = createHeaders(f6DsId, f6ObjectId, InteractionModel.NON_RDF);
413        headers.withArchivalGroupId(f6ObjectId);
414        headers.withFilename(filename);
415        headers.withCreatedDate(Instant.parse(createDate));
416        headers.withLastModifiedDate(lastModified);
417        headers.withLastModifiedBy(user);
418        headers.withCreatedBy(user);
419        headers.withMementoCreatedDate(lastModified);
420
421        if (externalHandlingMap.containsKey(dv.getDatastreamInfo().getControlGroup())) {
422            headers.withExternalHandling(
423                    externalHandlingMap.get(dv.getDatastreamInfo().getControlGroup()));
424            headers.withExternalUrl(dv.getExternalOrRedirectURL());
425        }
426
427        headers.withArchivalGroup(false);
428        headers.withObjectRoot(false);
429        if (dv.getSize() > -1 && !INLINE_XML.equals(dv.getDatastreamInfo().getControlGroup())) {
430            headers.withContentSize(dv.getSize());
431        }
432
433        if (dv.getContentDigest() != null && !Strings.isNullOrEmpty(dv.getContentDigest().getDigest())) {
434            final var digest = dv.getContentDigest();
435            final var digests = new ArrayList<URI>();
436            digests.add(URI.create("urn:" + digest.getType().toLowerCase() + ":" + digest.getDigest().toLowerCase()));
437            headers.withDigests(digests);
438        }
439
440        headers.withMimeType(mime);
441        headers.withStateToken(DigestUtils.md5Hex(
442                String.valueOf(lastModified.toEpochMilli())).toUpperCase());
443
444        return headers.build();
445    }
446
447    private ResourceHeaders createDescriptionHeaders(final String f6DsId,
448                                                     final String filename,
449                                                     final ResourceHeaders datastreamHeaders) {
450        final var id = f6DescriptionId(f6DsId);
451        final var headers = createHeaders(id, f6DsId, InteractionModel.NON_RDF_DESCRIPTION);
452
453        headers.withArchivalGroupId(datastreamHeaders.getArchivalGroupId());
454        headers.withFilename(filename);
455        headers.withCreatedDate(datastreamHeaders.getCreatedDate());
456        headers.withLastModifiedDate(datastreamHeaders.getLastModifiedDate());
457        headers.withCreatedBy(datastreamHeaders.getCreatedBy());
458        headers.withLastModifiedBy(datastreamHeaders.getLastModifiedBy());
459        headers.withMementoCreatedDate(datastreamHeaders.getMementoCreatedDate());
460
461        headers.withArchivalGroup(false);
462        headers.withObjectRoot(false);
463        headers.withStateToken(datastreamHeaders.getStateToken());
464
465        return headers.build();
466    }
467
468    private String resolveMimeType(final DatastreamVersion dv) {
469        String mime = dv.getMimeType();
470
471        if (Strings.isNullOrEmpty(mime)) {
472            final var meta = new Metadata();
473            meta.set(Metadata.RESOURCE_NAME_KEY, dv.getDatastreamInfo().getDatastreamId());
474            try (var content = TikaInputStream.get(dv.getContent())) {
475                mime = mimeDetector.detect(content, meta).toString();
476            } catch (IOException e) {
477                throw new UncheckedIOException(e);
478            }
479        }
480
481        return mime;
482    }
483
484    private void deleteDatastream(final String id,
485                                  final Instant lastModified,
486                                  final OcflObjectSession session) {
487        if (migrationType == MigrationType.PLAIN_OCFL) {
488            deleteOcflMigratedResource(id, InteractionModel.NON_RDF, session);
489            deleteOcflMigratedResource(f6DescriptionId(id), InteractionModel.NON_RDF_DESCRIPTION, session);
490        } else {
491            deleteF6MigratedResource(id, lastModified, session);
492            deleteF6MigratedResource(f6DescriptionId(id), lastModified, session);
493        }
494    }
495
496    private void deleteF6MigratedResource(final String id,
497                                          final Instant lastModified,
498                                          final OcflObjectSession session) {
499        LOGGER.debug("Deleting resource {}", id);
500        final var headers = session.readHeaders(id);
501        session.deleteContentFile(ResourceHeaders.builder(headers)
502                .withDeleted(true)
503                .withLastModifiedDate(lastModified)
504                .withMementoCreatedDate(lastModified)
505                .build());
506    }
507
508    private void deleteOcflMigratedResource(final String id,
509                                            final InteractionModel interactionModel,
510                                            final OcflObjectSession session) {
511        LOGGER.debug("Deleting resource {}", id);
512        session.deleteContentFile(ResourceHeaders.builder()
513                .withId(id)
514                .withInteractionModel(interactionModel.getUri())
515                .build());
516    }
517
518    private String getObjectState(final ObjectVersionReference ov, final String pid) {
519        return ov.getObjectProperties().listProperties().stream()
520                .filter(prop -> OBJ_STATE_PROP.equals(prop.getName()))
521                .findFirst()
522                .orElseThrow(() -> new IllegalStateException(String.format("Object %s is missing state information",
523                        pid)))
524                .getValue();
525    }
526
527    // Get object-level triples
528    private static InputStream getObjTriples(final ObjectVersionReference o, final String pid) {
529        final ByteArrayOutputStream out = new ByteArrayOutputStream();
530        final Model triples = ModelFactory.createDefaultModel();
531        try {
532            final String uri = "info:fedora/" + pid;
533
534            o.getObjectProperties().listProperties().forEach(p -> {
535                if (p.getName().contains("Date")) {
536                    addDateLiteral(triples, uri, p.getName(), p.getValue());
537                } else {
538                    addStringLiteral(triples, uri, p.getName(), p.getValue());
539                }
540            });
541
542            triples.write(out, "N-TRIPLES");
543            return new ByteArrayInputStream(out.toByteArray());
544        } finally {
545            triples.close();
546        }
547    }
548
549    // Get datastream-level triples
550    private InputStream getDsTriples(final DatastreamVersion dv,
551                                            final String f6DsId,
552                                            final String createDate) {
553        final ByteArrayOutputStream out = new ByteArrayOutputStream();
554        final Model triples = ModelFactory.createDefaultModel();
555
556        try {
557            if (migrationType == MigrationType.PLAIN_OCFL) {
558                // These triples are server managed in F6
559                addDateLiteral(triples,
560                        f6DsId,
561                        "http://fedora.info/definitions/v4/repository#created",
562                        createDate);
563                addDateLiteral(triples,
564                        f6DsId,
565                        "http://fedora.info/definitions/v4/repository#lastModified",
566                        dv.getCreated());
567                addStringLiteral(triples,
568                        f6DsId,
569                        "http://purl.org/dc/terms/identifier",
570                        dv.getDatastreamInfo().getDatastreamId());
571                addStringLiteral(triples,
572                        f6DsId,
573                        "http://www.ebu.ch/metadata/ontologies/ebucore/ebucore#hasMimeType",
574                        dv.getMimeType());
575                addLongLiteral(triples,
576                        f6DsId,
577                        "http://www.loc.gov/premis/rdf/v1#size",
578                        dv.getSize());
579
580                if (dv.getContentDigest() != null) {
581                    addStringLiteral(triples,
582                            f6DsId,
583                            "http://www.loc.gov/premis/rdf/v1#hasMessageDigest",
584                            "urn:" + dv.getContentDigest().getType().toLowerCase() + ":" +
585                                    dv.getContentDigest().getDigest().toLowerCase());
586                }
587            }
588
589            addStringLiteral(triples,
590                    f6DsId,
591                    "http://purl.org/dc/terms/title",
592                    dv.getLabel());
593            addStringLiteral(triples,
594                    f6DsId,
595                    "http://fedora.info/definitions/1/0/access/objState",
596                    dv.getDatastreamInfo().getState());
597            addStringLiteral(triples,
598                    f6DsId,
599                    "http://www.loc.gov/premis/rdf/v1#formatDesignation",
600                    dv.getFormatUri());
601
602            triples.write(out, "N-TRIPLES");
603            return new ByteArrayInputStream(out.toByteArray());
604        } finally {
605            triples.close();
606        }
607    }
608
609    private static void addStringLiteral(final Model m,
610                                         final String s,
611                                         final String p,
612                                         final String o) {
613        if (o != null) {
614            m.add(m.createResource(s), m.createProperty(p), o);
615        }
616    }
617
618    private static void addDateLiteral(final Model m,
619                                       final String s,
620                                       final String p,
621                                       final String date) {
622        if (date != null) {
623            m.addLiteral(m.createResource(s),
624                         m.createProperty(p),
625                         m.createTypedLiteral(date, XSDDatatype.XSDdateTime));
626        }
627    }
628
629    private static void addLongLiteral(final Model m,
630                                       final String s,
631                                       final String p,
632                                       final long number) {
633        if (number != -1) {
634            m.addLiteral(m.createResource(s),
635                    m.createProperty(p),
636                    m.createTypedLiteral(number, XSDDatatype.XSDlong));
637        }
638    }
639
640    /**
641     * @param mime any mimetype as String
642     * @return extension associated with arg mime, return includes '.' in extension (.txt).
643     *                  ..Empty String if unrecognized mime
644     */
645    private static String getExtension(final String mime) {
646        final MimeTypes allTypes = MimeTypes.getDefaultMimeTypes();
647        MimeType type;
648        try {
649            type = allTypes.forName(mime);
650        } catch (final MimeTypeException e) {
651            type = null;
652        }
653
654        if (type != null) {
655            return type.getExtension();
656        }
657
658        LOGGER.warn("No mimetype found for '{}'", mime);
659        return "";
660    }
661
662}