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}