001/** 002 * Copyright 2015 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 */ 016package org.fcrepo.oai.service; 017 018import static java.util.Collections.emptyMap; 019import static com.hp.hpl.jena.rdf.model.ResourceFactory.createProperty; 020import static org.fcrepo.kernel.modeshape.rdf.converters.PropertyConverter.getPropertyNameFromPredicate; 021 022import java.io.IOException; 023import java.io.InputStream; 024import java.io.UnsupportedEncodingException; 025import java.net.URLDecoder; 026import java.net.URLEncoder; 027import java.util.ArrayList; 028import java.util.GregorianCalendar; 029import java.util.List; 030import java.util.Map; 031 032import javax.annotation.PostConstruct; 033import javax.jcr.RepositoryException; 034import javax.jcr.Session; 035import javax.jcr.query.Query; 036import javax.jcr.query.QueryManager; 037import javax.jcr.query.QueryResult; 038import javax.jcr.query.Row; 039import javax.jcr.query.RowIterator; 040import javax.ws.rs.core.UriInfo; 041import javax.xml.bind.JAXBContext; 042import javax.xml.bind.JAXBElement; 043import javax.xml.bind.JAXBException; 044import javax.xml.bind.Unmarshaller; 045import javax.xml.datatype.DatatypeConfigurationException; 046import javax.xml.datatype.DatatypeFactory; 047import javax.xml.namespace.QName; 048import javax.xml.transform.stream.StreamSource; 049 050import org.apache.commons.codec.binary.Base64; 051import org.apache.commons.io.IOUtils; 052import org.apache.commons.lang.StringUtils; 053import org.fcrepo.http.api.FedoraLdp; 054import org.fcrepo.http.api.FedoraNodes; 055import org.fcrepo.http.commons.api.rdf.HttpResourceConverter; 056import org.fcrepo.http.commons.session.SessionFactory; 057import org.fcrepo.kernel.api.FedoraJcrTypes; 058import org.fcrepo.kernel.api.RdfLexicon; 059import org.fcrepo.kernel.modeshape.rdf.converters.ValueConverter; 060import org.fcrepo.kernel.modeshape.rdf.impl.PropertiesRdfContext; 061import org.fcrepo.kernel.api.models.Container; 062import org.fcrepo.kernel.api.models.FedoraBinary; 063import org.fcrepo.kernel.api.models.FedoraResource; 064import org.fcrepo.kernel.api.services.BinaryService; 065import org.fcrepo.kernel.api.services.ContainerService; 066import org.fcrepo.kernel.api.services.NodeService; 067import org.fcrepo.kernel.api.services.RepositoryService; 068import org.fcrepo.kernel.api.utils.iterators.RdfStream; 069import org.fcrepo.oai.dublincore.JcrPropertiesGenerator; 070import org.fcrepo.oai.rdf.PropertyPredicate; 071import org.fcrepo.oai.http.ResumptionToken; 072import org.fcrepo.oai.jersey.XmlDeclarationStrippingInputStream; 073import org.joda.time.DateTimeZone; 074import org.joda.time.format.DateTimeFormatter; 075import org.joda.time.format.ISODateTimeFormat; 076import org.modeshape.jcr.api.NamespaceRegistry; 077import org.openarchives.oai._2.DescriptionType; 078import org.openarchives.oai._2.GetRecordType; 079import org.openarchives.oai._2.HeaderType; 080import org.openarchives.oai._2.IdentifyType; 081import org.openarchives.oai._2.ListIdentifiersType; 082import org.openarchives.oai._2.ListMetadataFormatsType; 083import org.openarchives.oai._2.ListRecordsType; 084import org.openarchives.oai._2.ListSetsType; 085import org.openarchives.oai._2.MetadataFormatType; 086import org.openarchives.oai._2.MetadataType; 087import org.openarchives.oai._2.OAIPMHerrorType; 088import org.openarchives.oai._2.OAIPMHerrorcodeType; 089import org.openarchives.oai._2.OAIPMHtype; 090import org.openarchives.oai._2.ObjectFactory; 091import org.openarchives.oai._2.RecordType; 092import org.openarchives.oai._2.RequestType; 093import org.openarchives.oai._2.SetType; 094import org.openarchives.oai._2.VerbType; 095import org.openarchives.oai._2_0.oai_dc.OaiDcType; 096import org.slf4j.Logger; 097import org.slf4j.LoggerFactory; 098import org.springframework.beans.factory.annotation.Autowired; 099 100import com.hp.hpl.jena.rdf.model.Property; 101import com.hp.hpl.jena.rdf.model.Resource; 102 103/** 104 * The type OAI provider service. 105 * 106 * @author lsitu 107 * @author Frank Asseg 108 */ 109public class OAIProviderService { 110 111 private static final Logger log = LoggerFactory.getLogger(OAIProviderService.class); 112 113 private static final ObjectFactory oaiFactory = new ObjectFactory(); 114 115 private final DatatypeFactory dataFactory; 116 117 private final Unmarshaller unmarshaller; 118 119 private String setsRootPath; 120 121 private String propertyHasSets; 122 123 private String propertySetName; 124 125 private String propertyHasSetSpec; 126 127 private String propertyIsPartOfSet; 128 129 private String propertyOaiRepositoryName; 130 131 private String propertyOaiDescription; 132 133 private String propertyOaiAdminEmail; 134 135 private String oaiNamespace; 136 137 private boolean setsEnabled; 138 139 private boolean autoGenerateOaiDc; 140 141 private Map<String, String> descriptiveContent; 142 143 private Map<String, MetadataFormat> metadataFormats; 144 145 private DateTimeFormatter dateFormat = ISODateTimeFormat.dateTimeNoMillis().withZone(DateTimeZone.UTC); 146 147 private int maxListSize; 148 149 @Autowired 150 private BinaryService binaryService; 151 152 @Autowired 153 private NodeService nodeService; 154 155 @Autowired 156 private SessionFactory sessionFactory; 157 158 @Autowired 159 private ContainerService containerService; 160 161 @Autowired 162 private RepositoryService repositoryService; 163 164 @Autowired 165 private JcrPropertiesGenerator jcrPropertiesGenerator; 166 167 /** 168 * Sets property has set spec. 169 * 170 * @param propertyHasSetSpec the property has set spec 171 */ 172 public void setPropertyHasSetSpec(final String propertyHasSetSpec) { 173 this.propertyHasSetSpec = propertyHasSetSpec; 174 } 175 176 /** 177 * Sets property set name. 178 * 179 * @param propertySetName the property set name 180 */ 181 public void setPropertySetName(final String propertySetName) { 182 this.propertySetName = propertySetName; 183 } 184 185 /** 186 * Sets property has sets. 187 * 188 * @param propertyHasSets the property has sets 189 */ 190 public void setPropertyHasSets(final String propertyHasSets) { 191 this.propertyHasSets = propertyHasSets; 192 } 193 194 /** 195 * Sets max list size. 196 * 197 * @param maxListSize the max list size 198 */ 199 public void setMaxListSize(final int maxListSize) { 200 this.maxListSize = maxListSize; 201 } 202 203 /** 204 * Sets property is part of set. 205 * 206 * @param propertyIsPartOfSet the property is part of set 207 */ 208 public void setPropertyIsPartOfSet(final String propertyIsPartOfSet) { 209 this.propertyIsPartOfSet = propertyIsPartOfSet; 210 } 211 212 /** 213 * Set propertyOaiRepositoryName 214 * @param propertyOaiRepositoryName the oai repository name 215 */ 216 public void setPropertyOaiRepositoryName(final String propertyOaiRepositoryName) { 217 this.propertyOaiRepositoryName = propertyOaiRepositoryName; 218 } 219 220 /** 221 * Set propertyOaiDescription 222 * @param propertyOaiDescription the oai description 223 */ 224 public void setPropertyOaiDescription(final String propertyOaiDescription) { 225 this.propertyOaiDescription = propertyOaiDescription; 226 } 227 228 /** 229 * Set propertyOaiAdminEmail 230 * @param propertyOaiAdminEmail the oai admin email 231 */ 232 public void setPropertyOaiAdminEmail(final String propertyOaiAdminEmail) { 233 this.propertyOaiAdminEmail = propertyOaiAdminEmail; 234 } 235 236 /** 237 * Set oaiNamespace 238 * @param oaiNamespace the oai namespace 239 */ 240 public void setOaiNamespace(final String oaiNamespace) { 241 this.oaiNamespace = oaiNamespace; 242 } 243 244 /** 245 * Sets sets root path. 246 * 247 * @param setsRootPath the sets root path 248 */ 249 public void setSetsRootPath(final String setsRootPath) { 250 this.setsRootPath = setsRootPath; 251 } 252 253 /** 254 * Sets sets enabled. 255 * 256 * @param setsEnabled the sets enabled 257 */ 258 public void setSetsEnabled(final boolean setsEnabled) { 259 this.setsEnabled = setsEnabled; 260 } 261 262 /** 263 * Sets auto generate oai dc. 264 * 265 * @param autoGenerateOaiDc the auto generate oai dc 266 */ 267 public void setAutoGenerateOaiDc(final boolean autoGenerateOaiDc) { 268 this.autoGenerateOaiDc = autoGenerateOaiDc; 269 } 270 271 /** 272 * Sets metadata formats. 273 * 274 * @param metadataFormats the metadata formats 275 */ 276 public void setMetadataFormats(final Map<String, MetadataFormat> metadataFormats) { 277 this.metadataFormats = metadataFormats; 278 } 279 280 /** 281 * Sets descriptive content. 282 * 283 * @param descriptiveContent the descriptive content 284 */ 285 public void setDescriptiveContent(final Map<String, String> descriptiveContent) { 286 this.descriptiveContent = descriptiveContent; 287 } 288 289 /** 290 * Service intitialization 291 * 292 * @throws RepositoryException the repository exception 293 */ 294 @PostConstruct 295 public void init() throws RepositoryException { 296 /* check if set root node exists */ 297 final Session session = sessionFactory.getInternalSession(); 298 299 final NamespaceRegistry namespaceRegistry = 300 (org.modeshape.jcr.api.NamespaceRegistry) session.getWorkspace().getNamespaceRegistry(); 301 // Register the oai namespace if it's not found 302 if (!namespaceRegistry.isRegisteredPrefix("oai")) { 303 namespaceRegistry.registerNamespace("oai", oaiNamespace); 304 } 305 306 if (!this.nodeService.exists(session, setsRootPath)) { 307 308 log.info("Initializing OAI root {} ...", setsRootPath); 309 310 final Container root = this.containerService.findOrCreate(session, setsRootPath); 311 session.save(); 312 313 final String repositoryName = descriptiveContent.get("repositoryName"); 314 final String description = descriptiveContent.get("description"); 315 final String adminEmail = descriptiveContent.get("adminEmail"); 316 root.getNode().setProperty(getPropertyName(session, 317 createProperty(propertyOaiRepositoryName)), repositoryName); 318 root.getNode().setProperty(getPropertyName(session, createProperty(propertyOaiDescription)), description); 319 root.getNode().setProperty(getPropertyName(session, createProperty(propertyOaiAdminEmail)), adminEmail); 320 session.save(); 321 } 322 } 323 324 /** 325 * Instantiates a new OAI provider service. 326 * 327 * @throws DatatypeConfigurationException the datatype configuration exception 328 * @throws JAXBException the jAXB exception 329 */ 330 public OAIProviderService() throws DatatypeConfigurationException, JAXBException { 331 this.dataFactory = DatatypeFactory.newInstance(); 332 final JAXBContext ctx = JAXBContext.newInstance(OAIPMHtype.class, IdentifyType.class, SetType.class); 333 this.unmarshaller = ctx.createUnmarshaller(); 334 } 335 336 /** 337 * Identify jAXB element. 338 * 339 * @param session the session 340 * @param uriInfo the uri info 341 * @return the jAXB element 342 * @throws RepositoryException the repository exception 343 * @throws JAXBException the jAXB exception 344 */ 345 public JAXBElement<OAIPMHtype> identify(final Session session, final UriInfo uriInfo) throws RepositoryException, 346 JAXBException { 347 final HttpResourceConverter converter = new HttpResourceConverter(session, uriInfo.getBaseUriBuilder() 348 .clone().path(FedoraNodes.class)); 349 350 final FedoraResource root = this.nodeService.find(session, setsRootPath); 351 352 final IdentifyType id = oaiFactory.createIdentifyType(); 353 // TODO: Need real values here from the root node? 354 id.setBaseURL(uriInfo.getBaseUri().toASCIIString()); 355 356 id.setEarliestDatestamp(dateFormat.print(root.getCreatedDate().getTime())); 357 358 id.setProtocolVersion("2.0"); 359 360 // repository name, project version 361 RdfStream triples = root.getTriples(converter, PropertiesRdfContext.class).filter( 362 new PropertyPredicate(propertyOaiRepositoryName)); 363 id.setRepositoryName(triples.next().getObject().getLiteralValue().toString()); 364 365 // admin email 366 triples = root.getTriples(converter, PropertiesRdfContext.class).filter( 367 new PropertyPredicate(propertyOaiAdminEmail)); 368 id.getAdminEmail().add(0, triples.next().getObject().getLiteralValue().toString()); 369 370 // description 371 triples = root.getTriples(converter, PropertiesRdfContext.class).filter( 372 new PropertyPredicate(propertyOaiDescription)); 373 final String description = triples.next().getObject().getLiteralValue().toString(); 374 final DescriptionType desc = oaiFactory.createDescriptionType(); 375 desc.setAny(new JAXBElement<String>(new QName("general"), String.class, description)); 376 377 id.getDescription().add(0, desc); 378 379 final RequestType req = oaiFactory.createRequestType(); 380 req.setVerb(VerbType.IDENTIFY); 381 req.setValue(uriInfo.getRequestUri().toASCIIString()); 382 383 final OAIPMHtype oai = oaiFactory.createOAIPMHtype(); 384 oai.setIdentify(id); 385 oai.setResponseDate(dataFactory.newXMLGregorianCalendar(new GregorianCalendar())); 386 oai.setRequest(req); 387 return oaiFactory.createOAIPMH(oai); 388 } 389 390 /** 391 * List metadata formats. 392 * 393 * @param session the session 394 * @param uriInfo the uri info 395 * @param identifier the identifier 396 * @return the jAXB element 397 * @throws RepositoryException the repository exception 398 */ 399 public JAXBElement<OAIPMHtype> listMetadataFormats(final Session session, final UriInfo uriInfo, 400 final String identifier) throws RepositoryException { 401 402 final ListMetadataFormatsType listMetadataFormats = oaiFactory.createListMetadataFormatsType(); 403 final HttpResourceConverter converter = new HttpResourceConverter(session, uriInfo.getBaseUriBuilder() 404 .clone().path(FedoraNodes.class)); 405 406 /* check which formats are available on top of oai_dc for this object */ 407 if (identifier != null && !identifier.isEmpty()) { 408 final String path = "/" + identifier; 409 if (path != null && !path.isEmpty()) { 410 /* generate metadata format response for a single pid */ 411 if (!this.nodeService.exists(session, path)) { 412 return error(VerbType.LIST_METADATA_FORMATS, identifier, null, 413 OAIPMHerrorcodeType.ID_DOES_NOT_EXIST, 414 "The object does not exist"); 415 } 416 final Container obj = this.containerService.findOrCreate(session, "/" + identifier); 417 for (final MetadataFormat mdf : metadataFormats.values()) { 418 if (mdf.getPrefix().equals("oai_dc")) { 419 listMetadataFormats.getMetadataFormat().add(mdf.asMetadataFormatType()); 420 } else { 421 final RdfStream triples = obj.getTriples(converter, PropertiesRdfContext.class).filter( 422 new PropertyPredicate(mdf.getPropertyName())); 423 if (triples.hasNext()) { 424 listMetadataFormats.getMetadataFormat().add(mdf.asMetadataFormatType()); 425 } 426 } 427 } 428 } 429 } else { 430 /* generate a general metadata format response */ 431 listMetadataFormats.getMetadataFormat().addAll(listAvailableMetadataFormats()); 432 } 433 434 final RequestType req = oaiFactory.createRequestType(); 435 req.setVerb(VerbType.LIST_METADATA_FORMATS); 436 req.setValue(uriInfo.getRequestUri().toASCIIString()); 437 438 final OAIPMHtype oai = oaiFactory.createOAIPMHtype(); 439 oai.setListMetadataFormats(listMetadataFormats); 440 oai.setRequest(req); 441 return oaiFactory.createOAIPMH(oai); 442 } 443 444 private List<MetadataFormatType> listAvailableMetadataFormats() { 445 final List<MetadataFormatType> types = new ArrayList<>(metadataFormats.size()); 446 for (final MetadataFormat mdf : metadataFormats.values()) { 447 final MetadataFormatType mdft = oaiFactory.createMetadataFormatType(); 448 mdft.setMetadataPrefix(mdf.getPrefix()); 449 mdft.setMetadataNamespace(mdf.getNamespace()); 450 mdft.setSchema(mdf.getSchemaUrl()); 451 types.add(mdft); 452 } 453 return types; 454 } 455 456 /** 457 * Gets record. 458 * 459 * @param session the session 460 * @param uriInfo the uri info 461 * @param identifier the identifier 462 * @param metadataPrefix the metadata prefix 463 * @return the record 464 * @throws RepositoryException the repository exception 465 */ 466 public JAXBElement<OAIPMHtype> getRecord(final Session session, final UriInfo uriInfo, final String identifier, 467 final String metadataPrefix) throws RepositoryException { 468 final MetadataFormat format = metadataFormats.get(metadataPrefix); 469 if (format == null) { 470 return error(VerbType.GET_RECORD, identifier, metadataPrefix, 471 OAIPMHerrorcodeType.CANNOT_DISSEMINATE_FORMAT, "The metadata format is not available"); 472 } 473 474 final String path = "/" + identifier; 475 if (!this.nodeService.exists(session, path)) { 476 return error(VerbType.GET_RECORD, identifier, metadataPrefix, OAIPMHerrorcodeType.ID_DOES_NOT_EXIST, 477 "The requested identifier does not exist"); 478 } 479 480 /* Prepare the OAI response objects */ 481 final Container obj = this.containerService.findOrCreate(session, "/" + identifier); 482 483 final OAIPMHtype oai = oaiFactory.createOAIPMHtype(); 484 final RequestType req = oaiFactory.createRequestType(); 485 req.setVerb(VerbType.GET_RECORD); 486 req.setValue(uriInfo.getRequestUri().toASCIIString()); 487 req.setMetadataPrefix("oai_dc"); 488 oai.setRequest(req); 489 490 final GetRecordType getRecord = oaiFactory.createGetRecordType(); 491 final RecordType record; 492 try { 493 record = this.createRecord(session, format, obj.getPath(), uriInfo); 494 getRecord.setRecord(record); 495 oai.setGetRecord(getRecord); 496 return this.oaiFactory.createOAIPMH(oai); 497 } catch (final IOException e) { 498 log.error("Unable to create OAI record for object " + obj.getPath()); 499 return error(VerbType.GET_RECORD, identifier, metadataPrefix, OAIPMHerrorcodeType.ID_DOES_NOT_EXIST, 500 "The requested OAI record does not exist for object " + obj.getPath()); 501 } 502 } 503 504 private JAXBElement<OaiDcType> generateOaiDc(final Session session, final Container obj, 505 final UriInfo uriInfo) throws RepositoryException { 506 507 return jcrPropertiesGenerator.generateDC(session, obj, uriInfo); 508 } 509 510 private JAXBElement<String> fetchOaiResponse(final Container obj, final Session session, 511 final MetadataFormat format, final UriInfo uriInfo) throws RepositoryException, IOException { 512 513 final HttpResourceConverter converter = new HttpResourceConverter(session, uriInfo.getBaseUriBuilder().clone() 514 .path(FedoraNodes.class)); 515 final RdfStream triples = obj.getTriples(converter, PropertiesRdfContext.class).filter( 516 new PropertyPredicate(format.getPropertyName())); 517 518 if (!triples.hasNext()) { 519 log.error("There is no OAI record of type " + format.getPrefix() + " associated with the object " 520 + obj.getPath()); 521 return null; 522 } 523 524 final String recordPath = triples.next().getObject().getLiteralValue().toString(); 525 final FedoraBinary bin = binaryService.findOrCreate(session, "/" + recordPath); 526 527 try (final InputStream src = new XmlDeclarationStrippingInputStream(bin.getContent())) { 528 return new JAXBElement<String>(new QName(format.getPrefix()), String.class, IOUtils.toString(src)); 529 } 530 } 531 532 /** 533 * Creates a OAI error response for JAX-B 534 * 535 * @param verb the verb 536 * @param identifier the identifier 537 * @param metadataPrefix the metadata prefix 538 * @param errorCode the error code 539 * @param msg the msg 540 * @return the jAXB element 541 */ 542 public static JAXBElement<OAIPMHtype> error(final VerbType verb, final String identifier, 543 final String metadataPrefix, final OAIPMHerrorcodeType errorCode, final String msg) { 544 final OAIPMHtype oai = oaiFactory.createOAIPMHtype(); 545 final RequestType req = oaiFactory.createRequestType(); 546 req.setVerb(verb); 547 req.setIdentifier(identifier); 548 req.setMetadataPrefix(metadataPrefix); 549 oai.setRequest(req); 550 551 final OAIPMHerrorType error = oaiFactory.createOAIPMHerrorType(); 552 error.setCode(errorCode); 553 error.setValue(msg); 554 oai.getError().add(error); 555 return oaiFactory.createOAIPMH(oai); 556 } 557 558 /** 559 * List identifiers. 560 * 561 * @param session the session 562 * @param uriInfo the uri info 563 * @param metadataPrefix the metadata prefix 564 * @param from the from 565 * @param until the until 566 * @param set the set 567 * @param offset the offset 568 * @return the jAXB element 569 * @throws RepositoryException the repository exception 570 */ 571 public JAXBElement<OAIPMHtype> listIdentifiers(final Session session, final UriInfo uriInfo, 572 final String metadataPrefix, final String from, final String until, 573 final String set, final int offset) 574 throws RepositoryException { 575 576 if (metadataPrefix == null) { 577 return error(VerbType.LIST_IDENTIFIERS, null, null, OAIPMHerrorcodeType.BAD_ARGUMENT, 578 "metadataprefix is invalid"); 579 } 580 581 final MetadataFormat mdf = metadataFormats.get(metadataPrefix); 582 if (mdf == null) { 583 return error(VerbType.LIST_IDENTIFIERS, null, metadataPrefix, 584 OAIPMHerrorcodeType.CANNOT_DISSEMINATE_FORMAT, "Unavailable metadata format"); 585 } 586 587 if (StringUtils.isNotBlank(set) && !setsEnabled) { 588 return error(VerbType.LIST_RECORDS, null, metadataPrefix, OAIPMHerrorcodeType.NO_SET_HIERARCHY, 589 "Sets are not enabled"); 590 } 591 592 // dateTime format validation 593 try { 594 validateDateTimeFormat(from); 595 validateDateTimeFormat(until); 596 } catch (final IllegalArgumentException e) { 597 return error(VerbType.LIST_IDENTIFIERS, null, metadataPrefix, OAIPMHerrorcodeType.BAD_ARGUMENT, 598 e.getMessage()); 599 } 600 601 final HttpResourceConverter converter = new HttpResourceConverter(session, 602 uriInfo.getBaseUriBuilder().clone().path(FedoraNodes.class)); 603 final ValueConverter valueConverter = new ValueConverter(session, converter); 604 605 final String jql = listResourceQuery(session, FedoraJcrTypes.FEDORA_CONTAINER, 606 from, until, set, maxListSize, offset); 607 try { 608 final QueryManager queryManager = session.getWorkspace().getQueryManager(); 609 final RowIterator result = executeQuery(queryManager, jql); 610 611 if (!result.hasNext()) { 612 return error(VerbType.LIST_IDENTIFIERS, null, metadataPrefix, OAIPMHerrorcodeType.NO_RECORDS_MATCH, 613 "No record found"); 614 } 615 616 final OAIPMHtype oai = oaiFactory.createOAIPMHtype(); 617 final ListIdentifiersType ids = oaiFactory.createListIdentifiersType(); 618 619 while (result.hasNext()) { 620 final HeaderType h = oaiFactory.createHeaderType(); 621 final Row sol = result.nextRow(); 622 final Resource sub = valueConverter.convert(sol.getValue("sub")).asResource(); 623 final String path = converter.convert(sub).getPath(); 624 625 h.setIdentifier(sub.getURI()); 626 final Container obj = 627 this.containerService.findOrCreate(session, path); 628 h.setDatestamp(dateFormat.print(obj.getLastModifiedDate().getTime())); 629 630 final RdfStream triples = obj.getTriples(converter, PropertiesRdfContext.class).filter( 631 new PropertyPredicate(propertyIsPartOfSet)); 632 final List<String> setNames = new ArrayList<>(); 633 while (triples.hasNext()) { 634 setNames.add(triples.next().getObject().getLiteralValue().toString()); 635 } 636 for (final String name : setNames) { 637 final Container setObject = this.containerService.findOrCreate(session, setsRootPath + "/" 638 + name); 639 final RdfStream setTriples = setObject.getTriples(converter, PropertiesRdfContext.class).filter( 640 new PropertyPredicate(propertyHasSetSpec)); 641 h.getSetSpec().add(setTriples.next().getObject().getLiteralValue().toString()); 642 } 643 ids.getHeader().add(h); 644 } 645 646 final RequestType req = oaiFactory.createRequestType(); 647 if (ids.getHeader().size() == maxListSize) { 648 req.setResumptionToken(encodeResumptionToken(VerbType.LIST_IDENTIFIERS.value(), metadataPrefix, from, 649 until, set, 650 offset + maxListSize)); 651 } 652 req.setVerb(VerbType.LIST_IDENTIFIERS); 653 req.setMetadataPrefix(metadataPrefix); 654 oai.setRequest(req); 655 oai.setListIdentifiers(ids); 656 return oaiFactory.createOAIPMH(oai); 657 } catch (final Exception e) { 658 e.printStackTrace(); 659 throw new RepositoryException(e); 660 } 661 } 662 663 /** 664 * Encode resumption token. 665 * 666 * @param verb the verb 667 * @param metadataPrefix the metadata prefix 668 * @param from the from 669 * @param until the until 670 * @param set the set 671 * @param offset the offset 672 * @return the string 673 * @throws UnsupportedEncodingException the unsupported encoding exception 674 */ 675 public static String encodeResumptionToken(final String verb, final String metadataPrefix, final String from, 676 final String until, final String set, final int offset) throws UnsupportedEncodingException { 677 678 final String[] data = new String[] { 679 urlEncode(verb), 680 urlEncode(metadataPrefix), 681 urlEncode(from != null ? from : ""), 682 urlEncode(until != null ? until : ""), 683 urlEncode(set != null ? set : ""), 684 urlEncode(String.valueOf(offset)) 685 }; 686 return Base64.encodeBase64URLSafeString(StringUtils.join(data, ':').getBytes("UTF-8")); 687 } 688 689 /** 690 * Url encode. 691 * 692 * @param value the value 693 * @return the string 694 * @throws UnsupportedEncodingException the unsupported encoding exception 695 */ 696 public static String urlEncode(final String value) throws UnsupportedEncodingException { 697 return URLEncoder.encode(value, "UTF-8"); 698 } 699 700 /** 701 * Url decode. 702 * 703 * @param value the value 704 * @return the string 705 * @throws UnsupportedEncodingException the unsupported encoding exception 706 */ 707 public static String urlDecode(final String value) throws UnsupportedEncodingException { 708 return URLDecoder.decode(value, "UTF-8"); 709 } 710 711 /** 712 * Decode resumption token. 713 * 714 * @param token the token 715 * @return the resumption token 716 * @throws UnsupportedEncodingException the unsupported encoding exception 717 */ 718 public static ResumptionToken decodeResumptionToken(final String token) throws UnsupportedEncodingException { 719 final String[] data = StringUtils.splitPreserveAllTokens(new String(Base64.decodeBase64(token)), ':'); 720 final String verb = urlDecode(data[0]); 721 final String metadataPrefix = urlDecode(data[1]); 722 final String from = urlDecode(data[2]); 723 final String until = urlDecode(data[3]); 724 final String set = urlDecode(data[4]); 725 final int offset = Integer.parseInt(urlDecode(data[5])); 726 return new ResumptionToken(verb, metadataPrefix, from, until, offset, set); 727 } 728 729 /** 730 * List sets. 731 * 732 * @param session the session 733 * @param uriInfo the uri info 734 * @param offset the offset 735 * @return the jAXB element 736 * @throws RepositoryException the repository exception 737 */ 738 public JAXBElement<OAIPMHtype> listSets(final Session session, final UriInfo uriInfo, final int offset) 739 throws RepositoryException { 740 final HttpResourceConverter converter = 741 new HttpResourceConverter(session, uriInfo.getBaseUriBuilder().clone().path(FedoraLdp.class)); 742 final ValueConverter valueConverter = new ValueConverter(session, converter); 743 744 try { 745 if (!setsEnabled) { 746 return error(VerbType.LIST_SETS, null, null, OAIPMHerrorcodeType.NO_SET_HIERARCHY, 747 "Set are not enabled"); 748 } 749 750 final String propJcrPath = getPropertyName(session, 751 createProperty(RdfLexicon.JCR_NAMESPACE + "path")); 752 final String propOAISet_ref = getPropertyName(session, createProperty(propertyHasSets + "_ref")); 753 754 final String jql = "SELECT [" + propOAISet_ref + "] AS obj FROM [" + FedoraJcrTypes.FEDORA_RESOURCE + "]" 755 + " WHERE [" + propJcrPath + "] = '" + setsRootPath + "'"; 756 final QueryManager queryManager = session.getWorkspace().getQueryManager(); 757 final RowIterator result = executeQuery(queryManager, jql); 758 if (!result.hasNext()) { 759 return error(VerbType.LIST_IDENTIFIERS, null, null, OAIPMHerrorcodeType.NO_RECORDS_MATCH, 760 "No record found"); 761 } 762 763 final OAIPMHtype oai = oaiFactory.createOAIPMHtype(); 764 final ListSetsType sets = oaiFactory.createListSetsType(); 765 final String propHasOAISetName = getPropertyName(session, createProperty(propertySetName)); 766 final String propHasOAISetSpec = getPropertyName(session, createProperty(propertyHasSetSpec)); 767 768 while (result.hasNext()) { 769 final Row row = result.nextRow(); 770 final Resource setRes = valueConverter.convert(row.getValue("obj")).asResource(); 771 772 final String setJql = "SELECT [" + propHasOAISetName + "] AS name," 773 + " [" + propHasOAISetSpec + "] AS spec FROM [" + FedoraJcrTypes.FEDORA_RESOURCE + "]" 774 + " WHERE [" + propJcrPath + "] = '" + converter.convert(setRes).getPath() + "'"; 775 776 final RowIterator setResult = executeQuery(queryManager, setJql); 777 while (setResult.hasNext()) { 778 final SetType set = oaiFactory.createSetType(); 779 final Row sol = setResult.nextRow(); 780 set.setSetName(valueConverter.convert(sol.getValue("name")).asLiteral().getString()); 781 set.setSetSpec(valueConverter.convert(sol.getValue("spec")).asLiteral().getString()); 782 sets.getSet().add(set); 783 } 784 } 785 oai.setListSets(sets); 786 return oaiFactory.createOAIPMH(oai); 787 } catch (final Exception e) { 788 e.printStackTrace(); 789 throw new RepositoryException(e); 790 } 791 } 792 793 /** 794 * Create set. 795 * 796 * @param session the session 797 * @param uriInfo the uri info 798 * @param src the src 799 * @return the string 800 * @throws RepositoryException the repository exception 801 */ 802 public String createSet(final Session session, final UriInfo uriInfo, final InputStream src) 803 throws RepositoryException { 804 final HttpResourceConverter converter = 805 new HttpResourceConverter(session, uriInfo.getBaseUriBuilder().clone().path(FedoraLdp.class)); 806 try { 807 final SetType set = this.unmarshaller.unmarshal(new StreamSource(src), SetType.class).getValue(); 808 final String setId = getSetId(set); 809 if (!this.nodeService.exists(session, setsRootPath)) { 810 throw new RepositoryException("The root set object does not exist"); 811 } 812 final Container setRoot = this.containerService.findOrCreate(session, setsRootPath); 813 if (set.getSetSpec() != null) { 814 /* validate that the hierarchy of sets exists */ 815 } 816 817 if (this.nodeService.exists(session, setsRootPath + "/" + setId)) { 818 throw new RepositoryException("The OAI Set with the id already exists"); 819 } 820 final Container setObject = this.containerService.findOrCreate(session, setsRootPath + "/" + setId); 821 822 final StringBuilder sparql = 823 new StringBuilder("INSERT DATA {<" + converter.toDomain(setRoot.getPath()) + "> <" + 824 propertyHasSets + "> <" + converter.toDomain(setObject.getPath()) + ">}"); 825 setRoot.updateProperties(converter, sparql.toString(), new RdfStream()); 826 827 sparql.setLength(0); 828 sparql.append("INSERT DATA {") 829 .append("<" + converter.toDomain(setObject.getPath()) + "> <" + propertySetName + 830 "> '" + set.getSetName() + "' .") 831 .append("<" + converter.toDomain(setObject.getPath()) + "> <" + propertyHasSetSpec + 832 "> '" + set.getSetSpec() + "' ."); 833 for (final DescriptionType desc : set.getSetDescription()) { 834 // TODO: save description 835 } 836 sparql.append("}"); 837 setObject.updateProperties(converter, sparql.toString(), new RdfStream()); 838 session.save(); 839 return setObject.getPath(); 840 } catch (final JAXBException e) { 841 e.printStackTrace(); 842 throw new RepositoryException(e); 843 } 844 } 845 846 private String getSetId(final SetType set) throws RepositoryException { 847 if (set.getSetSpec() == null) { 848 throw new RepositoryException("SetSpec can not be empty"); 849 } 850 String id = set.getSetSpec(); 851 final int colonPos = id.indexOf(':'); 852 while (colonPos > 0) { 853 id = id.substring(colonPos + 1); 854 } 855 return id; 856 } 857 858 /** 859 * List records. 860 * 861 * @param session the session 862 * @param uriInfo the uri info 863 * @param metadataPrefix the metadata prefix 864 * @param from the from 865 * @param until the until 866 * @param set the set 867 * @param offset the offset 868 * @return the jAXB element 869 * @throws RepositoryException the repository exception 870 */ 871 public JAXBElement<OAIPMHtype> listRecords(final Session session, final UriInfo uriInfo, 872 final String metadataPrefix, final String from, final String until, 873 final String set, final int offset) throws RepositoryException { 874 875 final HttpResourceConverter converter = 876 new HttpResourceConverter(session, uriInfo.getBaseUriBuilder().clone().path(FedoraNodes.class)); 877 final ValueConverter valueConverter = new ValueConverter(session, converter); 878 879 if (metadataPrefix == null) { 880 return error(VerbType.LIST_RECORDS, null, null, OAIPMHerrorcodeType.BAD_ARGUMENT, 881 "metadataprefix is invalid"); 882 } 883 final MetadataFormat mdf = metadataFormats.get(metadataPrefix); 884 if (mdf == null) { 885 return error(VerbType.LIST_RECORDS, null, metadataPrefix, 886 OAIPMHerrorcodeType.CANNOT_DISSEMINATE_FORMAT, "Unavailable metadata format"); 887 } 888 889 // dateTime format validation 890 try { 891 validateDateTimeFormat(from); 892 validateDateTimeFormat(until); 893 } catch (final IllegalArgumentException e) { 894 return error(VerbType.LIST_IDENTIFIERS, null, metadataPrefix, OAIPMHerrorcodeType.BAD_ARGUMENT, 895 e.getMessage()); 896 } 897 898 if (StringUtils.isNotBlank(set) && !setsEnabled) { 899 return error(VerbType.LIST_RECORDS, null, metadataPrefix, OAIPMHerrorcodeType.NO_SET_HIERARCHY, 900 "Sets are not enabled"); 901 } 902 903 final String jql = listResourceQuery(session, FedoraJcrTypes.FEDORA_CONTAINER, 904 from, until, set, maxListSize, offset); 905 try { 906 907 final QueryManager queryManager = session.getWorkspace().getQueryManager(); 908 final RowIterator result = executeQuery(queryManager, jql); 909 910 if (!result.hasNext()) { 911 return error(VerbType.LIST_RECORDS, null, metadataPrefix, OAIPMHerrorcodeType.NO_RECORDS_MATCH, 912 "No record found"); 913 } 914 915 final OAIPMHtype oai = oaiFactory.createOAIPMHtype(); 916 final ListRecordsType records = oaiFactory.createListRecordsType(); 917 while (result.hasNext()) { 918 // check if the records exists 919 final Row solution = result.nextRow(); 920 final Resource subjectUri = valueConverter.convert(solution.getValue("sub")).asResource(); 921 final RecordType record = this.createRecord(session, mdf, converter.asString(subjectUri), uriInfo); 922 records.getRecord().add(record); 923 } 924 925 final RequestType req = oaiFactory.createRequestType(); 926 if (records.getRecord().size() == maxListSize) { 927 req.setResumptionToken(encodeResumptionToken(VerbType.LIST_RECORDS.value(), metadataPrefix, from, 928 until, set, 929 offset + maxListSize)); 930 } 931 req.setVerb(VerbType.LIST_RECORDS); 932 req.setMetadataPrefix(metadataPrefix); 933 oai.setRequest(req); 934 oai.setListRecords(records); 935 return oaiFactory.createOAIPMH(oai); 936 } catch (final Exception e) { 937 e.printStackTrace(); 938 throw new RepositoryException(e); 939 } 940 } 941 942 private RecordType createRecord(final Session session, final MetadataFormat mdf, final String s, 943 final UriInfo uriInfo) throws IOException, RepositoryException { 944 945 final HttpResourceConverter converter = new HttpResourceConverter(session, uriInfo.getBaseUriBuilder().clone() 946 .path(FedoraNodes.class)); 947 final HeaderType h = oaiFactory.createHeaderType(); 948 final String subjectUri = converter.toDomain(s).getURI(); 949 h.setIdentifier(subjectUri); 950 951 final Container obj = 952 this.containerService.findOrCreate(session, s); 953 h.setDatestamp(dateFormat.print(obj.getLastModifiedDate().getTime())); 954 // get set names this object is part of 955 956 final RdfStream triples = obj.getTriples(converter, PropertiesRdfContext.class).filter( 957 new PropertyPredicate(propertyIsPartOfSet)); 958 final List<String> setNames = new ArrayList<>(); 959 while (triples.hasNext()) { 960 setNames.add(triples.next().getObject().getLiteralValue().toString()); 961 } 962 for (final String name : setNames) { 963 final Container setObject = this.containerService.findOrCreate(session, 964 setsRootPath + "/" + name); 965 final RdfStream setTriples = setObject.getTriples(converter, PropertiesRdfContext.class).filter( 966 new PropertyPredicate(propertyHasSetSpec)); 967 h.getSetSpec().add(setTriples.next().getObject().getLiteralValue().toString()); 968 } 969 970 // get the metadata record from fcrepo 971 final MetadataType md = this.oaiFactory.createMetadataType(); 972 if (mdf.getPrefix().equals("oai_dc")) { 973 /* generate a OAI DC reponse using the DC Generator from fcrepo4 */ 974 md.setAny(generateOaiDc(session, obj, uriInfo)); 975 } else { 976 /* generate a OAI response from the linked Binary */ 977 md.setAny(fetchOaiResponse(obj, session, mdf, uriInfo)); 978 } 979 980 final RecordType record = this.oaiFactory.createRecordType(); 981 record.setMetadata(md); 982 record.setHeader(h); 983 return record; 984 } 985 986 private String listResourceQuery(final Session session, final String mixinTypes, final String from, 987 final String until, final String set, final int limit, final int offset) throws RepositoryException { 988 989 final String propJcrPath = getPropertyName(session, 990 createProperty(RdfLexicon.JCR_NAMESPACE + "path")); 991 final String propHasMixinType = getPropertyName(session, RdfLexicon.HAS_MIXIN_TYPE); 992 final String propJcrLastModifiedDate = getPropertyName(session, RdfLexicon.LAST_MODIFIED_DATE); 993 final StringBuilder jql = new StringBuilder(); 994 jql.append("SELECT res.[" + propJcrPath + "] AS sub FROM [" + FedoraJcrTypes.FEDORA_RESOURCE + "] AS [res]"); 995 jql.append(" WHERE "); 996 997 // mixin type constraint 998 jql.append("res.[" + propHasMixinType + "] = '" + mixinTypes + "'"); 999 1000 // start datetime constraint 1001 if (StringUtils.isNotBlank(from)) { 1002 jql.append(" AND "); 1003 jql.append("res.[" + propJcrLastModifiedDate + "] >= CAST( '" + from + "' AS DATE)"); 1004 } 1005 // end datetime constraint 1006 if (StringUtils.isNotBlank(until)) { 1007 jql.append(" AND "); 1008 jql.append("res.[" + propJcrLastModifiedDate + "] <= CAST( '" + until + "' AS DATE)"); 1009 } 1010 1011 // set constraint 1012 if (StringUtils.isNotBlank(set)) { 1013 final String predicateIsPartOfOAISet = getPropertyName(session, 1014 createProperty(propertyIsPartOfSet)); 1015 jql.append(" AND "); 1016 jql.append("res.[" + predicateIsPartOfOAISet + "] = '" + set + "'"); 1017 } 1018 1019 if (limit > 0) { 1020 jql.append(" LIMIT ").append(maxListSize) 1021 .append(" OFFSET ").append(offset); 1022 } 1023 return jql.toString(); 1024 } 1025 1026 private RowIterator executeQuery(final QueryManager queryManager, final String jql) 1027 throws RepositoryException { 1028 final Query query = queryManager.createQuery(jql, Query.JCR_SQL2); 1029 final QueryResult results = query.execute(); 1030 return results.getRows(); 1031 } 1032 1033 private void validateDateTimeFormat(final String dateTime) { 1034 if (StringUtils.isNotBlank(dateTime)) { 1035 dateFormat.parseDateTime(dateTime); 1036 } 1037 } 1038 1039 /** 1040 * Get a property name for an RDF predicate 1041 * @param session 1042 * @param predicate 1043 * @return property name from the given predicate 1044 * @throws RepositoryException 1045 */ 1046 private String getPropertyName(final Session session, final Property predicate) 1047 throws RepositoryException { 1048 1049 final NamespaceRegistry namespaceRegistry = 1050 (org.modeshape.jcr.api.NamespaceRegistry) session.getWorkspace().getNamespaceRegistry(); 1051 final Map<String, String> namespaceMapping = emptyMap(); 1052 return getPropertyNameFromPredicate(namespaceRegistry, predicate, namespaceMapping); 1053 } 1054}