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}