View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  package org.opencastproject.oaipmh.server;
22  
23  import static java.lang.String.format;
24  import static org.opencastproject.oaipmh.OaiPmhUtil.toOaiRepresentation;
25  import static org.opencastproject.oaipmh.OaiPmhUtil.toUtc;
26  import static org.opencastproject.oaipmh.persistence.QueryBuilder.queryRepo;
27  import static org.opencastproject.oaipmh.server.Functions.addDay;
28  import static org.opencastproject.oaipmh.server.Functions.asDate;
29  import static org.opencastproject.util.data.Monadics.mlist;
30  import static org.opencastproject.util.data.Option.some;
31  import static org.opencastproject.util.data.Prelude.unexhaustiveMatch;
32  import static org.opencastproject.util.data.functions.Misc.chuck;
33  
34  import org.opencastproject.mediapackage.MediaPackage;
35  import org.opencastproject.oaipmh.Granularity;
36  import org.opencastproject.oaipmh.OaiPmhConstants;
37  import org.opencastproject.oaipmh.OaiPmhUtil;
38  import org.opencastproject.oaipmh.persistence.OaiPmhDatabase;
39  import org.opencastproject.oaipmh.persistence.OaiPmhDatabaseException;
40  import org.opencastproject.oaipmh.persistence.OaiPmhSetDefinition;
41  import org.opencastproject.oaipmh.persistence.OaiPmhSetDefinitionFilter;
42  import org.opencastproject.oaipmh.persistence.OaiPmhSetDefinitionImpl;
43  import org.opencastproject.oaipmh.persistence.SearchResult;
44  import org.opencastproject.oaipmh.persistence.SearchResultItem;
45  import org.opencastproject.oaipmh.util.XmlGen;
46  import org.opencastproject.util.data.Function;
47  import org.opencastproject.util.data.Function0;
48  import org.opencastproject.util.data.Option;
49  import org.opencastproject.util.data.Predicate;
50  import org.opencastproject.util.data.Tuple;
51  
52  import org.apache.commons.collections4.EnumerationUtils;
53  import org.apache.commons.lang3.StringUtils;
54  import org.osgi.service.cm.ConfigurationException;
55  import org.osgi.service.cm.ManagedService;
56  import org.slf4j.Logger;
57  import org.slf4j.LoggerFactory;
58  import org.w3c.dom.Element;
59  import org.w3c.dom.Node;
60  
61  import java.util.ArrayList;
62  import java.util.Calendar;
63  import java.util.Date;
64  import java.util.Dictionary;
65  import java.util.LinkedList;
66  import java.util.List;
67  import java.util.stream.Collectors;
68  
69  /**
70   * An OAI-PMH protocol compliant repository.
71   * <p>
72   * Currently supported:
73   * <ul>
74   * <li></li>
75   * </ul>
76   * <p>
77   * Currently <em>not</em> supported:
78   * <ul>
79   * <li><a href="http://www.openarchives.org/OAI/openarchivesprotocol.html#deletion">deletions</a></li>
80   * <li><a href="http://www.openarchives.org/OAI/openarchivesprotocol.html#Set">sets</a></li>
81   * <li>&lt;about&gt; containers in records, see section <a
82   * href="http://www.openarchives.org/OAI/openarchivesprotocol.html#Record">2.5. Record</a></li>
83   * <li>
84   * resumption tokens do not report about their expiration date; see section <a
85   * href="http://www.openarchives.org/OAI/openarchivesprotocol.html#FlowControl">3.5. Flow Control</a></li>
86   * </ul>
87   */
88  // todo - malformed date parameter must produce a BadArgument error - if a date parameter has a finer granularity than
89  //        supported by the repository this must produce a BadArgument error
90  public abstract class OaiPmhRepository implements ManagedService {
91    private static final Logger logger = LoggerFactory.getLogger(OaiPmhRepository.class);
92    private static final OaiDcMetadataProvider OAI_DC_METADATA_PROVIDER = new OaiDcMetadataProvider();
93    private static final String OAI_NS = OaiPmhConstants.OAI_2_0_XML_NS;
94  
95    private static final String CONF_KEY_SET_PREFIX = "set.";
96    private static final String CONF_KEY_SET_SETSPEC_SUFFIX = ".setSpec";
97    private static final String CONF_KEY_SET_NAME_SUFFIX = ".name";
98    private static final String CONF_KEY_SET_DESCRIPTION_SUFFIX = ".description";
99    private static final String CONF_KEY_SET_FILTER_INFIX = ".filter.";
100   private static final String CONF_KEY_SET_FILTER_FLAVOR_SUFFIX = ".flavor";
101   private static final String CONF_KEY_SET_FILTER_CONTAINS_SUFFIX = ".contains";
102   private static final String CONF_KEY_SET_FILTER_CONTAINSNOT_SUFFIX = ".containsnot";
103   private static final String CONF_KEY_SET_FILTER_MATCH_SUFFIX = ".match";
104 
105 
106   public abstract Granularity getRepositoryTimeGranularity();
107 
108   /** Display name of the OAI-PMH repository. */
109   public abstract String getRepositoryName();
110 
111   /** Repository ID. */
112   public abstract String getRepositoryId();
113 
114   public abstract OaiPmhDatabase getPersistence();
115 
116   public abstract String getAdminEmail();
117 
118   private List<OaiPmhSetDefinition> sets = new ArrayList<>();
119 
120   /**
121    * Parse service configuration file.
122    *
123    * @param properties
124    *        Service configuration as dictionary
125    * @throws ConfigurationException
126    *        If there is a problem within get configuration
127    */
128   @Override
129   public void updated(Dictionary<String, ?> properties) throws ConfigurationException {
130     if (properties == null) {
131       return;
132     }
133 
134     // Wipe set configuration in case some got removed
135     sets = new ArrayList<>();
136     List<String> confKeys = EnumerationUtils.toList(properties.keys());
137     for (String confKey : confKeys) {
138       if (confKey.startsWith(CONF_KEY_SET_PREFIX) && confKey.endsWith(CONF_KEY_SET_SETSPEC_SUFFIX)) {
139         String confKeyPrefix = confKey.replace(CONF_KEY_SET_SETSPEC_SUFFIX, "");
140         String setSpec = (String) properties.get(confKey);
141         String setSpecName = (String) properties.get(confKeyPrefix + CONF_KEY_SET_NAME_SUFFIX);
142         String setDescription = null;
143         if (confKey.contains(confKeyPrefix + CONF_KEY_SET_DESCRIPTION_SUFFIX)) {
144           setDescription = (String) properties.get(confKeyPrefix + CONF_KEY_SET_DESCRIPTION_SUFFIX);
145         }
146         try {
147           OaiPmhSetDefinitionImpl setDefinition = OaiPmhSetDefinitionImpl.build(setSpec, setSpecName, setDescription);
148           List<String> confKeyFilterNames = confKeys.stream()
149               .filter(key -> key.startsWith(confKeyPrefix + CONF_KEY_SET_FILTER_INFIX)
150                   && key.endsWith(CONF_KEY_SET_FILTER_FLAVOR_SUFFIX))
151               .map(key -> key.replace(confKeyPrefix + CONF_KEY_SET_FILTER_INFIX, "")
152                   .replace(CONF_KEY_SET_FILTER_FLAVOR_SUFFIX, ""))
153               .distinct().collect(Collectors.toList());
154           for (String filterName : confKeyFilterNames) {
155             String setSpecFilterFlavor = (String) properties
156                 .get(confKeyPrefix + CONF_KEY_SET_FILTER_INFIX + filterName + CONF_KEY_SET_FILTER_FLAVOR_SUFFIX);
157             List<String> confKeyCriteria = confKeys.stream()
158                 .filter(key -> key.startsWith(confKeyPrefix + CONF_KEY_SET_FILTER_INFIX + filterName)
159                     && (key.endsWith(CONF_KEY_SET_FILTER_CONTAINS_SUFFIX)
160                     || key.endsWith(CONF_KEY_SET_FILTER_CONTAINSNOT_SUFFIX)
161                     || key.endsWith(CONF_KEY_SET_FILTER_MATCH_SUFFIX)))
162                 .distinct().collect(Collectors.toList());
163             for (String confKeyCriterion : confKeyCriteria) {
164               String criterion = null;
165               if (confKeyCriterion.endsWith(CONF_KEY_SET_FILTER_CONTAINS_SUFFIX)) {
166                 criterion = OaiPmhSetDefinitionFilter.CRITERION_CONTAINS;
167               } else if (confKeyCriterion.endsWith(CONF_KEY_SET_FILTER_CONTAINSNOT_SUFFIX)) {
168                 criterion = OaiPmhSetDefinitionFilter.CRITERION_CONTAINSNOT;
169               } else if (confKeyCriterion.endsWith(CONF_KEY_SET_FILTER_MATCH_SUFFIX)) {
170                 criterion = OaiPmhSetDefinitionFilter.CRITERION_MATCH;
171               } else {
172                 logger.warn("Configuration key {} not valid.", confKeyCriterion);
173                 continue;
174               }
175               setDefinition.addFilter(filterName, setSpecFilterFlavor, criterion,
176                   (String) properties.get(confKeyCriterion));
177             }
178           }
179           if (setDefinition.getFilters().isEmpty()) {
180             logger.warn("No filter criteria defined for OAI-PMH set definition {}.", setDefinition.getSetSpec());
181           } else {
182             sets.add(setDefinition);
183             logger.debug("OAI-PMH set difinition {} initialized.", setDefinition.getSetSpec());
184           }
185         } catch (IllegalArgumentException e) {
186           logger.warn("Unable to parse OAI-PMH set definition for setSpec {}.", setSpec, e);
187         }
188       }
189     }
190   }
191 
192   /**
193    * Save a query.
194    *
195    * @return a resumption token
196    */
197   public abstract String saveQuery(ResumableQuery query);
198 
199   /** Get a saved query. */
200   public abstract Option<ResumableQuery> getSavedQuery(String resumptionToken);
201 
202   /** Maximum number of items returned by the list queries ListIdentifiers, ListRecords and ListSets. */
203   public abstract int getResultLimit();
204 
205   /**
206    * Return a list of available metadata providers. Please do not expose the default provider for the
207    * mandatory oai_dc format since this is automatically added when calling {@link #getMetadataProviders()}.
208    *
209    * @see #getMetadataProviders()
210    */
211   public abstract List<MetadataProvider> getRepositoryMetadataProviders();
212 
213   /** Return the current date. Used in implementation instead of new Date(); to facilitate unit testing. */
214   public Date currentDate() {
215     return new Date();
216   }
217 
218   /** Return a list of all available metadata providers. The <code>oai_dc</code> format is always included. */
219   public final List<MetadataProvider> getMetadataProviders() {
220     return mlist(getRepositoryMetadataProviders()).cons(OAI_DC_METADATA_PROVIDER).value();
221   }
222 
223   /** Add an item to the repository. */
224   public void addItem(MediaPackage mp) {
225     getPersistence().search(queryRepo(getRepositoryId()).build());
226     try {
227       getPersistence().store(mp, getRepositoryId());
228     } catch (OaiPmhDatabaseException e) {
229       chuck(e);
230     }
231   }
232 
233   /** Create an OAI-PMH response based on the given request params. */
234   public XmlGen selectVerb(Params p) {
235     if (p.isVerbListIdentifiers()) {
236       return handleListIdentifiers(p);
237     } else if (p.isVerbListRecords()) {
238       return handleListRecords(p);
239     } else if (p.isVerbGetRecord()) {
240       return handleGetRecord(p);
241     } else if (p.isVerbIdentify()) {
242       return handleIdentify(p);
243     } else if (p.isVerbListMetadataFormats()) {
244       return handleListMetadataFormats(p);
245     } else if (p.isVerbListSets()) {
246       return handleListSets(p);
247     } else {
248       return createErrorResponse(
249               "badVerb", Option.<String>none(), p.getRepositoryUrl(), "Illegal OAI verb or verb is missing.");
250     }
251   }
252 
253   /** Return the metadata provider for a given metadata prefix. */
254   public Option<MetadataProvider> getMetadataProvider(final String metadataPrefix) {
255     return mlist(getMetadataProviders()).find(new Predicate<MetadataProvider>() {
256       @Override
257       public Boolean apply(MetadataProvider metadataProvider) {
258         return metadataProvider.getMetadataFormat().getPrefix().equals(metadataPrefix);
259       }
260     });
261   }
262 
263   /** {@link #getMetadataProvider(String)} as a function. */
264   private final Function<String, Option<MetadataProvider>> getMetadataProvider = new Function<String, Option<MetadataProvider>>() {
265     @Override public Option<MetadataProvider> apply(String metadataPrefix) {
266       return getMetadataProvider(metadataPrefix);
267     }
268   };
269 
270   /** Create the "GetRecord" response. */
271   private XmlGen handleGetRecord(final Params p) {
272     if (p.getIdentifier().isNone() || p.getMetadataPrefix().isNone()) {
273       return createBadArgumentResponse(p);
274     } else {
275       for (final MetadataProvider metadataProvider : p.getMetadataPrefix().bind(getMetadataProvider)) {
276         if (p.getSet().isSome() && !sets.stream().anyMatch(
277             setDef -> StringUtils.equals(setDef.getSetSpec(), p.getSet().get()))) {
278           // If there is no set specification, immediately return a no result response
279           return createNoRecordsMatchResponse(p);
280         }
281         final SearchResult res = getPersistence()
282                 .search(queryRepo(getRepositoryId()).mediaPackageId(p.getIdentifier())
283                                                     .setDefinitions(sets)
284                                                     .setSpec(p.getSet().getOrElseNull()).build());
285         final List<SearchResultItem> items = res.getItems();
286         switch (items.size()) {
287           case 0:
288             return createIdDoesNotExistResponse(p);
289           case 1:
290             final SearchResultItem item = items.get(0);
291             return new OaiVerbXmlGen(OaiPmhRepository.this, p) {
292               @Override
293               public Element create() {
294                 // create the metadata for this item
295                 Element metadata = metadataProvider.createMetadata(OaiPmhRepository.this, item, p.getSet());
296                 return oai(request($a("identifier", p.getIdentifier().get()), metadataPrefixAttr(p)),
297                            verb(record(item, metadata)));
298               }
299             };
300           default:
301             throw new RuntimeException("ERROR: Search index contains more than one item with id "
302                                                + p.getIdentifier());
303         }
304       }
305       // no metadata provider found
306       return createCannotDisseminateFormatResponse(p);
307     }
308   }
309 
310   /** Handle the "Identify" request. */
311   /*<Identify >
312     <repositoryName>Test OAI Repository</repositoryName>
313     <baseURL>http://localhost/oaipmh</baseURL>;
314     <protocolVersion>2.0</protocolVersion>
315     <adminEmail>admin@localhost.org</adminEmail>
316     <earliestDatestamp>2010-01-01</earliestDatestamp>
317     <deletedRecord>transient</deletedRecord>
318     <granularity>YYYY-MM-DD</granularity>
319   </Identify>*/
320   private XmlGen handleIdentify(final Params p) {
321     return new OaiVerbXmlGen(this, p) {
322       @Override
323       public Element create() {
324         return oai(
325                 request(),
326                 verb($eTxt("repositoryName", OAI_NS, getRepositoryName()),
327                      $eTxt("baseURL", OAI_NS, p.getRepositoryUrl()),
328                      $eTxt("protocolVersion", OAI_NS, "2.0"),
329                      $eTxt("adminEmail", OAI_NS, getAdminEmail()),
330                      $eTxt("earliestDatestamp", OAI_NS, "2010-01-01"),
331                      $eTxt("deletedRecord", OAI_NS, "transient"),
332                      $eTxt("granularity", OAI_NS, toOaiRepresentation(getRepositoryTimeGranularity()))));
333       }
334     };
335   }
336 
337   private XmlGen handleListMetadataFormats(final Params p) {
338     for (String id : p.getIdentifier()) {
339       final SearchResult res = getPersistence().search(queryRepo(getRepositoryId()).mediaPackageId(id).build());
340       if (res.getItems().size() != 1)
341         return createIdDoesNotExistResponse(p);
342     }
343     return new OaiVerbXmlGen(this, p) {
344       @Override
345       public Element create() {
346         final List<Node> metadataFormats = mlist(getMetadataProviders()).map(new Function<MetadataProvider, Node>() {
347           @Override
348           public Node apply(MetadataProvider metadataProvider) {
349             return metadataFormat(metadataProvider.getMetadataFormat());
350           }
351         }).value();
352         return oai(request($aSome("identifier", p.getIdentifier())), verb(metadataFormats));
353       }
354     };
355   }
356 
357   private XmlGen handleListRecords(final Params p) {
358     final ListItemsEnv env = new ListItemsEnv() {
359       @Override
360       protected ListXmlGen respond(ListGenParams listParams) {
361         return new ListXmlGen(listParams) {
362           @Override
363           protected List<Node> createContent(final Option<String> set) {
364             return mlist(params.getResult().getItems()).map(new Function<SearchResultItem, Node>() {
365               @Override
366               public Node apply(SearchResultItem item) {
367                 logger.debug("Requested set: {}", set);
368                 final Element metadata = params.getMetadataProvider().createMetadata(OaiPmhRepository.this, item, set);
369                 return record(item, metadata);
370               }
371             }).value();
372           }
373         };
374       }
375     };
376     return env.apply(p);
377   }
378 
379   private XmlGen handleListIdentifiers(final Params p) {
380     final ListItemsEnv env = new ListItemsEnv() {
381       @Override
382       protected ListXmlGen respond(ListGenParams listParams) {
383         // create XML response
384         return new ListXmlGen(listParams) {
385           @Override
386           protected List<Node> createContent(Option<String> set) {
387             return mlist(params.getResult().getItems()).map(new Function<SearchResultItem, Node>() {
388               @Override
389               public Node apply(SearchResultItem item) {
390                 return header(item);
391               }
392             }).value();
393           }
394         };
395       }
396     };
397     return env.apply(p);
398   }
399 
400   /** Create the "ListSets" response. Since the list is short the complete list is returned at once. */
401   private XmlGen handleListSets(final Params p) {
402     return new OaiVerbXmlGen(this, p) {
403       @Override
404       public Element create() {
405         if (sets.isEmpty()) {
406           return createNoSetHierarchyResponse(p).create();
407         }
408         List<Node> setNodes = new LinkedList<>();
409         sets.forEach(set -> {
410           String setSpec = set.getSetSpec();
411           String name = set.getName();
412           String description = set.getDescription();
413           if (StringUtils.isNotBlank(description)) {
414             setNodes.add($e("set", $eTxt("setSpec", setSpec), $eTxt("setName", name),
415                 $e("setDescription", dc($eTxt("dc:description", description)))));
416           } else {
417             setNodes.add($e("set", $eTxt("setSpec", setSpec), $eTxt("setName", name)));
418           }
419         });
420         return oai(request(), verb(setNodes));
421       }
422     };
423   }
424 
425   // --
426 
427   private XmlGen createCannotDisseminateFormatResponse(Params p) {
428     return createErrorResponse(
429             OaiPmhConstants.ERROR_CANNOT_DISSEMINATE_FORMAT, p.getVerb(), p.getRepositoryUrl(),
430             "The metadata format identified by the value given for the metadataPrefix argument is not supported by the item or by the repository.");
431   }
432 
433   private XmlGen createIdDoesNotExistResponse(Params p) {
434     return createErrorResponse(
435             OaiPmhConstants.ERROR_ID_DOES_NOT_EXIST, p.getVerb(), p.getRepositoryUrl(),
436             format("The requested id %s does not exist in the repository.",
437                    p.getIdentifier().getOrElse("?")));
438   }
439 
440   private XmlGen createBadArgumentResponse(Params p) {
441     return createErrorResponse(OaiPmhConstants.ERROR_BAD_ARGUMENT, p.getVerb(), p.getRepositoryUrl(),
442                                "The request includes illegal arguments or is missing required arguments.");
443   }
444 
445   private XmlGen createBadResumptionTokenResponse(Params p) {
446     return createErrorResponse(
447             OaiPmhConstants.ERROR_BAD_RESUMPTION_TOKEN, p.getVerb(), p.getRepositoryUrl(),
448             "The value of the resumptionToken argument is either invalid or expired.");
449   }
450 
451   private XmlGen createNoRecordsMatchResponse(Params p) {
452     return createErrorResponse(
453             OaiPmhConstants.ERROR_NO_RECORDS_MATCH, p.getVerb(), p.getRepositoryUrl(),
454             "The combination of the values of the from, until, and set arguments results in an empty list.");
455   }
456 
457   private XmlGen createNoSetHierarchyResponse(Params p) {
458     return createErrorResponse(
459             OaiPmhConstants.ERROR_NO_SET_HIERARCHY, p.getVerb(), p.getRepositoryUrl(),
460             "This repository does not support sets");
461   }
462 
463   private XmlGen createErrorResponse(
464           final String code, final Option<String> verb, final String repositoryUrl, final String msg) {
465     return new OaiXmlGen(this) {
466       @Override
467       public Element create() {
468         return oai($e("request",
469                       OaiPmhConstants.OAI_2_0_XML_NS,
470                       $aSome("verb", verb),
471                       $txt(repositoryUrl)),
472                    $e("error", OaiPmhConstants.OAI_2_0_XML_NS, $a("code", code), $cdata(msg)));
473       }
474     };
475   }
476 
477   // --
478 
479   /**
480    * Convert a date to the repository supported time granularity.
481    *
482    * @return the converted date or null if d is null
483    */
484   String toSupportedGranularity(Date d) {
485     return toUtc(d, getRepositoryTimeGranularity());
486   }
487 
488   // CHECKSTYLE:OFF
489   final Function<Date, String> toSupportedGranularity = new Function<Date, String>() {
490     @Override
491     public String apply(Date date) {
492       return toSupportedGranularity(date);
493     }
494   };
495   // CHECKSTYLE:ON
496 
497   private final Function<Date, Date> granulate = new Function<Date, Date>() {
498     @Override
499     public Date apply(Date date) {
500       return granulate(getRepositoryTimeGranularity(), date);
501     }
502   };
503 
504   /** "Cut" a date to the repositories supported granularity. Cutting behaves similar to the mathematical floor function. */
505   public static Date granulate(Granularity g, Date d) {
506     switch (g) {
507       case SECOND: {
508         Calendar c = Calendar.getInstance();
509         c.setTimeZone(OaiPmhUtil.newDateFormat().getTimeZone());
510         c.setTime(d);
511         c.set(Calendar.MILLISECOND, 0);
512         return c.getTime();
513       }
514       case DAY: {
515         final Calendar c = Calendar.getInstance();
516         c.setTimeZone(OaiPmhUtil.newDateFormat().getTimeZone());
517         c.setTime(d);
518         c.set(Calendar.HOUR_OF_DAY, 0);
519         c.set(Calendar.MINUTE, 0);
520         c.set(Calendar.SECOND, 0);
521         c.set(Calendar.MILLISECOND, 0);
522         return c.getTime();
523       }
524       default:
525         return unexhaustiveMatch();
526     }
527   }
528 
529   static class BadArgumentException extends RuntimeException {
530   }
531 
532   /**
533    * Environment for the list verbs ListIdentifiers and ListRecords. Handles the boilerplate of getting, validating and
534    * providing the parameters, creating error responses etc. Call {@link #apply(Params)} to create the XML. Also use
535    * {@link org.opencastproject.oaipmh.server.OaiPmhRepository.ListItemsEnv.ListXmlGen} for the XML generation.
536    */
537   abstract class ListItemsEnv {
538     ListItemsEnv() {
539     }
540 
541     /** Create the regular response from the given parameters. */
542     protected abstract ListXmlGen respond(ListGenParams params);
543 
544     /** Call this method to create the XML. */
545     public XmlGen apply(final Params p) {
546       // check parameters
547       if (p.getSet().isSome() && sets.isEmpty()) {
548         return createNoSetHierarchyResponse(p);
549       }
550       final boolean resumptionTokenExists = p.getResumptionToken().isSome();
551       final boolean otherParamExists = p.getMetadataPrefix().isSome() || p.getFrom().isSome() || p.getUntil().isSome()
552               || p.getSet().isSome();
553 
554       if (resumptionTokenExists && otherParamExists || !resumptionTokenExists && !otherParamExists)
555         return createBadArgumentResponse(p);
556       final Option<Date> from = p.getFrom().map(asDate).map(granulate);
557 
558       final Function<Date, Date> untilAdjustment = getRepositoryTimeGranularity() == Granularity.DAY ? addDay(1)
559               : org.opencastproject.util.data.functions.Functions.<Date>identity();
560       final Option<Date> untilGranularity = p.getUntil().map(asDate).map(granulate).map(untilAdjustment);
561       for (Tuple<Date, Date> fromUntil : from.and(untilGranularity)) {
562         if (!fromUntil.getA().before(fromUntil.getB())) {
563           return createBadArgumentResponse(p);
564         }
565       }
566       if (otherParamExists && p.getMetadataPrefix().isNone())
567         return createBadArgumentResponse(p);
568       // <- params are ok
569 
570       final Option<Date> until = untilGranularity.orElse(some(currentDate()));
571 
572       final String metadataPrefix = p.getResumptionToken().flatMap(getMetadataPrefixFromToken)
573               .getOrElse(getMetadataPrefix(p));
574 
575       for (MetadataProvider metadataProvider : p.getResumptionToken()
576               .flatMap(getMetadataProviderFromToken)
577               .orElse(getMetadataProvider.curry(metadataPrefix))) {
578         try {
579           final SearchResult result;
580           @SuppressWarnings("unchecked")
581           final Option<String>[] set = new Option[]{p.getSet()};
582           if (!resumptionTokenExists) {
583             // start a new query
584             if (p.getSet().isSome() && !sets.stream().anyMatch(
585                 setDef -> StringUtils.equals(setDef.getSetSpec(), p.getSet().get()))) {
586               // If there is no set specification, immediately return a no result response
587               return createNoRecordsMatchResponse(p);
588             }
589             result = getPersistence().search(
590                     queryRepo(getRepositoryId())
591                             .setDefinitions(sets)
592                             .setSpec(p.getSet().getOrElseNull())
593                             .modifiedAfter(from)
594                             .modifiedBefore(until)
595                             .limit(getResultLimit()).build());
596           } else {
597             // resume query
598             result = getSavedQuery(p.getResumptionToken().get()).fold(new Option.Match<ResumableQuery, SearchResult>() {
599               @Override
600               public SearchResult some(ResumableQuery rq) {
601                 set[0] = rq.getSet();
602                 return getPersistence().search(
603                         queryRepo(getRepositoryId())
604                                 .setDefinitions(sets)
605                                 .setSpec(rq.getSet().getOrElseNull())
606                                 .modifiedAfter(rq.getLastResult())
607                                 .modifiedBefore(rq.getUntil())
608                                 .limit(getResultLimit())
609                                 .subsequentRequest(true).build());
610               }
611 
612               @Override
613               public SearchResult none() {
614                 // no resumable query found
615                 throw new BadResumptionTokenException();
616               }
617             });
618           }
619           if (result.size() > 0) {
620             return respond(new ListGenParams(OaiPmhRepository.this,
621                                              result,
622                                              metadataProvider,
623                                              metadataPrefix,
624                                              p.getResumptionToken(),
625                                              from, until.get(),
626                                              set[0],
627                                              p));
628           } else {
629             return createNoRecordsMatchResponse(p);
630           }
631         } catch (BadResumptionTokenException e) {
632           return createBadResumptionTokenResponse(p);
633         }
634       }
635       // no metadata provider found
636       return createCannotDisseminateFormatResponse(p);
637     }
638 
639     /** Get a metadata prefix from a resumption token. */
640     private final Function<String, Option<String>> getMetadataPrefixFromToken = new Function<String, Option<String>>() {
641       @Override
642       public Option<String> apply(String token) {
643         return getSavedQuery(token).map(new Function<ResumableQuery, String>() {
644           @Override
645           public String apply(ResumableQuery resumableQuery) {
646             return resumableQuery.getMetadataPrefix();
647           }
648         });
649       }
650     };
651 
652     /** Get a metadata provider from a resumption token. */
653     private final Function<String, Option<MetadataProvider>> getMetadataProviderFromToken = new Function<String, Option<MetadataProvider>>() {
654       @Override
655       public Option<MetadataProvider> apply(String token) {
656         return getSavedQuery(token).flatMap(new Function<ResumableQuery, Option<MetadataProvider>>() {
657           @Override
658           public Option<MetadataProvider> apply(ResumableQuery resumableQuery) {
659             return getMetadataProvider(resumableQuery.getMetadataPrefix());
660           }
661         });
662       }
663     };
664 
665     /** Get the metadata prefix lazily. */
666     private Function0<String> getMetadataPrefix(final Params p) {
667       return new Function0<String>() {
668         @Override
669         public String apply() {
670           return p.getMetadataPrefix().getOrElse(OaiPmhConstants.OAI_DC_METADATA_FORMAT.getPrefix());
671         }
672       };
673     }
674 
675     /** OAI XML response generation environment for list responses. */
676     abstract class ListXmlGen extends OaiVerbXmlGen {
677 
678       protected final ListGenParams params;
679 
680       ListXmlGen(ListGenParams p) {
681         super(p.getRepository(), p.getParams());
682         this.params = p;
683       }
684 
685       /** Implement to create your content. Gets placed as children of the verb node. */
686       protected abstract List<Node> createContent(Option<String> set);
687 
688       @Override
689       public Element create() {
690         final List<Node> content = new ArrayList<Node>(createContent(params.getSet()));
691         if (content.size() == 0)
692           return createNoRecordsMatchResponse(params.getParams()).create();
693         content.add(resumptionToken(params.getResumptionToken(), params.getMetadataPrefix(), params.getResult(),
694                                     params.getUntil(), params.getSet()));
695         return oai(
696                 request($a("metadataPrefix", params.getMetadataPrefix()),
697                         $aSome("from", params.getFrom().map(toSupportedGranularity)),
698                         $aSome("until", some(toSupportedGranularity(params.getUntil()))),
699                         $aSome("set", params.getSet())), verb(content));
700       }
701     }
702 
703     private class BadResumptionTokenException extends RuntimeException {
704     }
705   }
706 }
707 
708 /** Parameter holder for the list generator. */
709 final class ListGenParams {
710   private final OaiPmhRepository repository;
711   private final SearchResult result;
712   private final MetadataProvider metadataProvider;
713   private final String metadataPrefix;
714   private final Option<String> resumptionToken;
715   private final Option<Date> from;
716   private final Date until;
717   private final Option<String> set;
718   private final Params params;
719 
720   // CHECKSTYLE:OFF
721   ListGenParams(OaiPmhRepository repository,
722                 SearchResult result, MetadataProvider metadataProvider,
723                 String metadataPrefix, Option<String> resumptionToken,
724                 Option<Date> from, Date until,
725                 Option<String> set,
726                 Params params) {
727     this.repository = repository;
728     this.result = result;
729     this.metadataProvider = metadataProvider;
730     this.resumptionToken = resumptionToken;
731     this.metadataPrefix = metadataPrefix;
732     this.from = from;
733     this.until = until;
734     this.set = set;
735     this.params = params;
736   }
737   // CHECKSTYLE:ON
738 
739   public OaiPmhRepository getRepository() {
740     return repository;
741   }
742 
743   public SearchResult getResult() {
744     return result;
745   }
746 
747   public MetadataProvider getMetadataProvider() {
748     return metadataProvider;
749   }
750 
751   public Option<String> getResumptionToken() {
752     return resumptionToken;
753   }
754 
755   public String getMetadataPrefix() {
756     return metadataPrefix;
757   }
758 
759   public Option<Date> getFrom() {
760     return from;
761   }
762 
763   public Date getUntil() {
764     return until;
765   }
766 
767   public Option<String> getSet() {
768     return set;
769   }
770 
771   /** The request parameters. */
772   public Params getParams() {
773     return params;
774   }
775 }