1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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.util.data.Prelude.unexhaustiveMatch;
29 import static org.opencastproject.util.data.functions.Misc.chuck;
30
31 import org.opencastproject.mediapackage.MediaPackage;
32 import org.opencastproject.oaipmh.Granularity;
33 import org.opencastproject.oaipmh.OaiPmhConstants;
34 import org.opencastproject.oaipmh.OaiPmhUtil;
35 import org.opencastproject.oaipmh.persistence.OaiPmhDatabase;
36 import org.opencastproject.oaipmh.persistence.OaiPmhDatabaseException;
37 import org.opencastproject.oaipmh.persistence.OaiPmhSetDefinition;
38 import org.opencastproject.oaipmh.persistence.OaiPmhSetDefinitionFilter;
39 import org.opencastproject.oaipmh.persistence.OaiPmhSetDefinitionImpl;
40 import org.opencastproject.oaipmh.persistence.SearchResult;
41 import org.opencastproject.oaipmh.persistence.SearchResultItem;
42 import org.opencastproject.oaipmh.util.XmlGen;
43
44 import org.apache.commons.collections4.EnumerationUtils;
45 import org.apache.commons.lang3.StringUtils;
46 import org.osgi.service.cm.ConfigurationException;
47 import org.osgi.service.cm.ManagedService;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50 import org.w3c.dom.Element;
51 import org.w3c.dom.Node;
52
53 import java.util.ArrayList;
54 import java.util.Calendar;
55 import java.util.Collections;
56 import java.util.Date;
57 import java.util.Dictionary;
58 import java.util.LinkedList;
59 import java.util.List;
60 import java.util.Optional;
61 import java.util.function.Function;
62 import java.util.function.Supplier;
63 import java.util.stream.Collectors;
64 import java.util.stream.Stream;
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87 public abstract class OaiPmhRepository implements ManagedService {
88 private static final Logger logger = LoggerFactory.getLogger(OaiPmhRepository.class);
89 private static final OaiDcMetadataProvider OAI_DC_METADATA_PROVIDER = new OaiDcMetadataProvider();
90 private static final String OAI_NS = OaiPmhConstants.OAI_2_0_XML_NS;
91
92 private static final String CONF_KEY_SET_PREFIX = "set.";
93 private static final String CONF_KEY_SET_SETSPEC_SUFFIX = ".setSpec";
94 private static final String CONF_KEY_SET_NAME_SUFFIX = ".name";
95 private static final String CONF_KEY_SET_DESCRIPTION_SUFFIX = ".description";
96 private static final String CONF_KEY_SET_FILTER_INFIX = ".filter.";
97 private static final String CONF_KEY_SET_FILTER_FLAVOR_SUFFIX = ".flavor";
98 private static final String CONF_KEY_SET_FILTER_CONTAINS_SUFFIX = ".contains";
99 private static final String CONF_KEY_SET_FILTER_CONTAINSNOT_SUFFIX = ".containsnot";
100 private static final String CONF_KEY_SET_FILTER_MATCH_SUFFIX = ".match";
101
102
103 public abstract Granularity getRepositoryTimeGranularity();
104
105
106 public abstract String getRepositoryName();
107
108
109 public abstract String getRepositoryId();
110
111 public abstract OaiPmhDatabase getPersistence();
112
113 public abstract String getAdminEmail();
114
115 private List<OaiPmhSetDefinition> sets = new ArrayList<>();
116
117
118
119
120
121
122
123
124
125 @Override
126 public void updated(Dictionary<String, ?> properties) throws ConfigurationException {
127 if (properties == null) {
128 return;
129 }
130
131
132 sets = new ArrayList<>();
133 List<String> confKeys = EnumerationUtils.toList(properties.keys());
134 for (String confKey : confKeys) {
135 if (confKey.startsWith(CONF_KEY_SET_PREFIX) && confKey.endsWith(CONF_KEY_SET_SETSPEC_SUFFIX)) {
136 String confKeyPrefix = confKey.replace(CONF_KEY_SET_SETSPEC_SUFFIX, "");
137 String setSpec = (String) properties.get(confKey);
138 String setSpecName = (String) properties.get(confKeyPrefix + CONF_KEY_SET_NAME_SUFFIX);
139 String setDescription = null;
140 if (confKey.contains(confKeyPrefix + CONF_KEY_SET_DESCRIPTION_SUFFIX)) {
141 setDescription = (String) properties.get(confKeyPrefix + CONF_KEY_SET_DESCRIPTION_SUFFIX);
142 }
143 try {
144 OaiPmhSetDefinitionImpl setDefinition = OaiPmhSetDefinitionImpl.build(setSpec, setSpecName, setDescription);
145 List<String> confKeyFilterNames = confKeys.stream()
146 .filter(key -> key.startsWith(confKeyPrefix + CONF_KEY_SET_FILTER_INFIX)
147 && key.endsWith(CONF_KEY_SET_FILTER_FLAVOR_SUFFIX))
148 .map(key -> key.replace(confKeyPrefix + CONF_KEY_SET_FILTER_INFIX, "")
149 .replace(CONF_KEY_SET_FILTER_FLAVOR_SUFFIX, ""))
150 .distinct().collect(Collectors.toList());
151 for (String filterName : confKeyFilterNames) {
152 String setSpecFilterFlavor = (String) properties
153 .get(confKeyPrefix + CONF_KEY_SET_FILTER_INFIX + filterName + CONF_KEY_SET_FILTER_FLAVOR_SUFFIX);
154 List<String> confKeyCriteria = confKeys.stream()
155 .filter(key -> key.startsWith(confKeyPrefix + CONF_KEY_SET_FILTER_INFIX + filterName)
156 && (key.endsWith(CONF_KEY_SET_FILTER_CONTAINS_SUFFIX)
157 || key.endsWith(CONF_KEY_SET_FILTER_CONTAINSNOT_SUFFIX)
158 || key.endsWith(CONF_KEY_SET_FILTER_MATCH_SUFFIX)))
159 .distinct().collect(Collectors.toList());
160 for (String confKeyCriterion : confKeyCriteria) {
161 String criterion = null;
162 if (confKeyCriterion.endsWith(CONF_KEY_SET_FILTER_CONTAINS_SUFFIX)) {
163 criterion = OaiPmhSetDefinitionFilter.CRITERION_CONTAINS;
164 } else if (confKeyCriterion.endsWith(CONF_KEY_SET_FILTER_CONTAINSNOT_SUFFIX)) {
165 criterion = OaiPmhSetDefinitionFilter.CRITERION_CONTAINSNOT;
166 } else if (confKeyCriterion.endsWith(CONF_KEY_SET_FILTER_MATCH_SUFFIX)) {
167 criterion = OaiPmhSetDefinitionFilter.CRITERION_MATCH;
168 } else {
169 logger.warn("Configuration key {} not valid.", confKeyCriterion);
170 continue;
171 }
172 setDefinition.addFilter(filterName, setSpecFilterFlavor, criterion,
173 (String) properties.get(confKeyCriterion));
174 }
175 }
176 if (setDefinition.getFilters().isEmpty()) {
177 logger.warn("No filter criteria defined for OAI-PMH set definition {}.", setDefinition.getSetSpec());
178 } else {
179 sets.add(setDefinition);
180 logger.debug("OAI-PMH set difinition {} initialized.", setDefinition.getSetSpec());
181 }
182 } catch (IllegalArgumentException e) {
183 logger.warn("Unable to parse OAI-PMH set definition for setSpec {}.", setSpec, e);
184 }
185 }
186 }
187 }
188
189
190
191
192
193
194 public abstract String saveQuery(ResumableQuery query);
195
196
197 public abstract Optional<ResumableQuery> getSavedQuery(String resumptionToken);
198
199
200 public abstract int getResultLimit();
201
202
203
204
205
206
207
208 public abstract List<MetadataProvider> getRepositoryMetadataProviders();
209
210
211 public Date currentDate() {
212 return new Date();
213 }
214
215
216 public final List<MetadataProvider> getMetadataProviders() {
217 return Stream.concat(
218 Stream.of(OAI_DC_METADATA_PROVIDER),
219 getRepositoryMetadataProviders().stream()
220 ).toList();
221 }
222
223
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
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", Optional.<String>empty(), p.getRepositoryUrl(), "Illegal OAI verb or verb is missing.");
250 }
251 }
252
253
254 public Optional<MetadataProvider> getMetadataProvider(final String metadataPrefix) {
255 return getMetadataProviders().stream()
256 .filter(metadataProvider -> metadataProvider.getMetadataFormat().getPrefix().equals(metadataPrefix))
257 .findFirst();
258 }
259
260
261 private XmlGen handleGetRecord(final Params p) {
262 if (p.getIdentifier().isEmpty() || p.getMetadataPrefix().isEmpty()) {
263 return createBadArgumentResponse(p);
264 } else {
265 var metadataProviders = p.getMetadataPrefix().flatMap(mp -> getMetadataProvider(mp)).stream().toList();
266 for (final MetadataProvider metadataProvider : metadataProviders) {
267 if (p.getSet().isPresent() && !sets.stream().anyMatch(
268 setDef -> StringUtils.equals(setDef.getSetSpec(), p.getSet().get()))) {
269
270 return createNoRecordsMatchResponse(p);
271 }
272 final SearchResult res = getPersistence()
273 .search(queryRepo(getRepositoryId()).mediaPackageId(p.getIdentifier())
274 .setDefinitions(sets)
275 .setSpec(p.getSet().orElse(null)).build());
276 final List<SearchResultItem> items = res.getItems();
277 switch (items.size()) {
278 case 0:
279 return createIdDoesNotExistResponse(p);
280 case 1:
281 final SearchResultItem item = items.get(0);
282 return new OaiVerbXmlGen(OaiPmhRepository.this, p) {
283 @Override
284 public Element create() {
285
286 Element metadata = metadataProvider.createMetadata(OaiPmhRepository.this, item, p.getSet());
287 return oai(request($a("identifier", p.getIdentifier().get()), metadataPrefixAttr(p)),
288 verb(record(item, metadata)));
289 }
290 };
291 default:
292 throw new RuntimeException("ERROR: Search index contains more than one item with id "
293 + p.getIdentifier());
294 }
295 }
296
297 return createCannotDisseminateFormatResponse(p);
298 }
299 }
300
301
302
303
304
305
306
307
308
309
310
311 private XmlGen handleIdentify(final Params p) {
312 return new OaiVerbXmlGen(this, p) {
313 @Override
314 public Element create() {
315 return oai(
316 request(),
317 verb($eTxt("repositoryName", OAI_NS, getRepositoryName()),
318 $eTxt("baseURL", OAI_NS, p.getRepositoryUrl()),
319 $eTxt("protocolVersion", OAI_NS, "2.0"),
320 $eTxt("adminEmail", OAI_NS, getAdminEmail()),
321 $eTxt("earliestDatestamp", OAI_NS, "2010-01-01"),
322 $eTxt("deletedRecord", OAI_NS, "transient"),
323 $eTxt("granularity", OAI_NS, toOaiRepresentation(getRepositoryTimeGranularity()))));
324 }
325 };
326 }
327
328 private XmlGen handleListMetadataFormats(final Params p) {
329 if (p.getIdentifier().isPresent()) {
330 final SearchResult res = getPersistence().search(queryRepo(
331 getRepositoryId()).mediaPackageId(p.getIdentifier().get()).build());
332 if (res.getItems().size() != 1) {
333 return createIdDoesNotExistResponse(p);
334 }
335 }
336 return new OaiVerbXmlGen(this, p) {
337 @Override
338 public Element create() {
339 final List<Node> metadataFormats = getMetadataProviders().stream()
340 .map(metadataProvider -> (Node) metadataFormat(metadataProvider.getMetadataFormat()))
341 .toList();
342 return oai(request($aSome("identifier", p.getIdentifier())), verb(metadataFormats));
343 }
344 };
345 }
346
347 private XmlGen handleListRecords(final Params p) {
348 final ListItemsEnv env = new ListItemsEnv() {
349 @Override
350 protected ListXmlGen respond(ListGenParams listParams) {
351 return new ListXmlGen(listParams) {
352 @Override
353 protected List<Node> createContent(final Optional<String> set) {
354 return params.getResult().getItems().stream()
355 .map(new Function<SearchResultItem, Node>() {
356 @Override
357 public Node apply(SearchResultItem item) {
358 logger.debug("Requested set: {}", set);
359 final Element metadata = params.getMetadataProvider()
360 .createMetadata(OaiPmhRepository.this, item, set);
361 return record(item, metadata);
362 }
363 })
364 .toList();
365 }
366 };
367 }
368 };
369 return env.apply(p);
370 }
371
372 private XmlGen handleListIdentifiers(final Params p) {
373 final ListItemsEnv env = new ListItemsEnv() {
374 @Override
375 protected ListXmlGen respond(ListGenParams listParams) {
376
377 return new ListXmlGen(listParams) {
378 protected List<Node> createContent(Optional<String> set) {
379 return params.getResult().getItems().stream()
380 .map(item -> (Node) header(item))
381 .toList();
382 }
383 };
384 }
385 };
386 return env.apply(p);
387 }
388
389
390 private XmlGen handleListSets(final Params p) {
391 return new OaiVerbXmlGen(this, p) {
392 @Override
393 public Element create() {
394 if (sets.isEmpty()) {
395 return createNoSetHierarchyResponse(p).create();
396 }
397 List<Node> setNodes = new LinkedList<>();
398 sets.forEach(set -> {
399 String setSpec = set.getSetSpec();
400 String name = set.getName();
401 String description = set.getDescription();
402 if (StringUtils.isNotBlank(description)) {
403 setNodes.add($e("set", $eTxt("setSpec", setSpec), $eTxt("setName", name),
404 $e("setDescription", dc($eTxt("dc:description", description)))));
405 } else {
406 setNodes.add($e("set", $eTxt("setSpec", setSpec), $eTxt("setName", name)));
407 }
408 });
409 return oai(request(), verb(setNodes));
410 }
411 };
412 }
413
414
415
416 private XmlGen createCannotDisseminateFormatResponse(Params p) {
417 return createErrorResponse(
418 OaiPmhConstants.ERROR_CANNOT_DISSEMINATE_FORMAT, p.getVerb(), p.getRepositoryUrl(),
419 "The metadata format identified by the value given for the metadataPrefix argument is not supported by the "
420 + "item or by the repository.");
421 }
422
423 private XmlGen createIdDoesNotExistResponse(Params p) {
424 return createErrorResponse(
425 OaiPmhConstants.ERROR_ID_DOES_NOT_EXIST, p.getVerb(), p.getRepositoryUrl(),
426 format("The requested id %s does not exist in the repository.",
427 p.getIdentifier().orElse("?")));
428 }
429
430 private XmlGen createBadArgumentResponse(Params p) {
431 return createErrorResponse(OaiPmhConstants.ERROR_BAD_ARGUMENT, p.getVerb(), p.getRepositoryUrl(),
432 "The request includes illegal arguments or is missing required arguments.");
433 }
434
435 private XmlGen createBadResumptionTokenResponse(Params p) {
436 return createErrorResponse(
437 OaiPmhConstants.ERROR_BAD_RESUMPTION_TOKEN, p.getVerb(), p.getRepositoryUrl(),
438 "The value of the resumptionToken argument is either invalid or expired.");
439 }
440
441 private XmlGen createNoRecordsMatchResponse(Params p) {
442 return createErrorResponse(
443 OaiPmhConstants.ERROR_NO_RECORDS_MATCH, p.getVerb(), p.getRepositoryUrl(),
444 "The combination of the values of the from, until, and set arguments results in an empty list.");
445 }
446
447 private XmlGen createNoSetHierarchyResponse(Params p) {
448 return createErrorResponse(
449 OaiPmhConstants.ERROR_NO_SET_HIERARCHY, p.getVerb(), p.getRepositoryUrl(),
450 "This repository does not support sets");
451 }
452
453 private XmlGen createErrorResponse(
454 final String code, final Optional<String> verb, final String repositoryUrl, final String msg) {
455 return new OaiXmlGen(this) {
456 @Override
457 public Element create() {
458 return oai($e("request",
459 OaiPmhConstants.OAI_2_0_XML_NS,
460 $aSome("verb", verb),
461 $txt(repositoryUrl)),
462 $e("error", OaiPmhConstants.OAI_2_0_XML_NS, $a("code", code), $cdata(msg)));
463 }
464 };
465 }
466
467
468
469
470
471
472
473
474 String toSupportedGranularity(Date d) {
475 return toUtc(d, getRepositoryTimeGranularity());
476 }
477
478 private Date granulate(Date date) {
479 return granulate(getRepositoryTimeGranularity(), date);
480 }
481
482
483
484
485 public static Date granulate(Granularity g, Date d) {
486 switch (g) {
487 case SECOND: {
488 Calendar c = Calendar.getInstance();
489 c.setTimeZone(OaiPmhUtil.newDateFormat().getTimeZone());
490 c.setTime(d);
491 c.set(Calendar.MILLISECOND, 0);
492 return c.getTime();
493 }
494 case DAY: {
495 final Calendar c = Calendar.getInstance();
496 c.setTimeZone(OaiPmhUtil.newDateFormat().getTimeZone());
497 c.setTime(d);
498 c.set(Calendar.HOUR_OF_DAY, 0);
499 c.set(Calendar.MINUTE, 0);
500 c.set(Calendar.SECOND, 0);
501 c.set(Calendar.MILLISECOND, 0);
502 return c.getTime();
503 }
504 default:
505 return unexhaustiveMatch();
506 }
507 }
508
509 static class BadArgumentException extends RuntimeException {
510 }
511
512
513
514
515
516
517 abstract class ListItemsEnv {
518 ListItemsEnv() {
519 }
520
521
522 protected abstract ListXmlGen respond(ListGenParams params);
523
524
525 public XmlGen apply(final Params p) {
526
527 if (p.getSet().isPresent() && sets.isEmpty()) {
528 return createNoSetHierarchyResponse(p);
529 }
530 final boolean resumptionTokenExists = p.getResumptionToken().isPresent();
531 final boolean otherParamExists = p.getMetadataPrefix().isPresent() || p.getFrom().isPresent()
532 || p.getUntil().isPresent() || p.getSet().isPresent();
533
534 if (resumptionTokenExists && otherParamExists || !resumptionTokenExists && !otherParamExists) {
535 return createBadArgumentResponse(p);
536 }
537 final Optional<Date> from = p.getFrom().map(Functions::asDate).map(d -> granulate(d));
538 final Function<Date, Date> untilAdjustment = getRepositoryTimeGranularity() == Granularity.DAY
539 ? addDay(1)
540 : Function.identity();
541 final Optional<Date> untilGranularity = p.getUntil()
542 .map(Functions::asDate)
543 .map(d -> granulate(d))
544 .map(untilAdjustment::apply);
545 if (from.isPresent() && untilGranularity.isPresent()) {
546 Date fromDate = from.get();
547 Date untilDate = untilGranularity.get();
548 if (!fromDate.before(untilDate)) {
549 return createBadArgumentResponse(p);
550 }
551 }
552 if (otherParamExists && p.getMetadataPrefix().isEmpty()) {
553 return createBadArgumentResponse(p);
554 }
555
556
557 final Optional<Date> until = Optional.of(untilGranularity.orElseGet(() -> currentDate()));
558
559 final String metadataPrefix = p.getResumptionToken()
560 .flatMap(t -> getMetadataPrefixFromToken(t))
561 .orElseGet(getMetadataPrefix(p));
562
563 final List<MetadataProvider> metadataProviders;
564 if (p.getResumptionToken().isPresent()) {
565 metadataProviders = getMetadataProviderFromToken.apply(p.getResumptionToken().get())
566 .map(Collections::singletonList)
567 .orElseGet(Collections::emptyList);
568 } else {
569 metadataProviders = Collections.singletonList(getMetadataProvider(metadataPrefix).orElseThrow(() ->
570 new IllegalStateException("No MetadataProvider found for fallback")
571 ));
572 }
573
574 for (MetadataProvider metadataProvider : metadataProviders) {
575 try {
576 final SearchResult result;
577 @SuppressWarnings("unchecked")
578 final Optional<String>[] set = new Optional[]{p.getSet()};
579
580 if (!resumptionTokenExists) {
581
582 if (p.getSet().isPresent() && !sets.stream().anyMatch(
583 setDef -> StringUtils.equals(setDef.getSetSpec(), p.getSet().get()))) {
584
585 return createNoRecordsMatchResponse(p);
586 }
587 result = getPersistence().search(
588 queryRepo(getRepositoryId())
589 .setDefinitions(sets)
590 .setSpec(p.getSet().orElse(null))
591 .modifiedAfter(from)
592 .modifiedBefore(until)
593 .limit(getResultLimit()).build());
594 } else {
595
596 ResumableQuery rq = getSavedQuery(p.getResumptionToken().get())
597 .orElseThrow(BadResumptionTokenException::new);
598 set[0] = rq.getSet();
599 result = getPersistence().search(
600 queryRepo(getRepositoryId())
601 .setDefinitions(sets)
602 .setSpec(rq.getSet().orElse(null))
603 .modifiedAfter(rq.getLastResult())
604 .modifiedBefore(rq.getUntil())
605 .limit(getResultLimit())
606 .subsequentRequest(true).build());
607 }
608
609 if (result.size() > 0) {
610 return respond(new ListGenParams(
611 OaiPmhRepository.this,
612 result,
613 metadataProvider,
614 metadataPrefix,
615 p.getResumptionToken(),
616 from,
617 until.get(),
618 set[0],
619 p));
620 } else {
621 return createNoRecordsMatchResponse(p);
622 }
623
624 } catch (BadResumptionTokenException e) {
625 return createBadResumptionTokenResponse(p);
626 }
627 }
628
629 return createCannotDisseminateFormatResponse(p);
630 }
631
632
633 private Optional<String> getMetadataPrefixFromToken(String token) {
634 return getSavedQuery(token).map(resumableQuery -> resumableQuery.getMetadataPrefix());
635 }
636
637
638 private final Function<String, Optional<MetadataProvider>> getMetadataProviderFromToken =
639 new Function<String, Optional<MetadataProvider>>() {
640 @Override
641 public Optional<MetadataProvider> apply(String token) {
642 return getSavedQuery(token).flatMap(resumableQuery -> getMetadataProvider(resumableQuery.getMetadataPrefix()));
643 }
644 };
645
646
647 private Supplier<String> getMetadataPrefix(final Params p) {
648 return () -> {
649 try {
650 return p.getMetadataPrefix()
651 .orElse(OaiPmhConstants.OAI_DC_METADATA_FORMAT.getPrefix());
652 } catch (Exception e) {
653 return chuck(e);
654 }
655 };
656 }
657
658
659 abstract class ListXmlGen extends OaiVerbXmlGen {
660
661 protected final ListGenParams params;
662
663 ListXmlGen(ListGenParams p) {
664 super(p.getRepository(), p.getParams());
665 this.params = p;
666 }
667
668
669 protected abstract List<Node> createContent(Optional<String> set);
670
671 @Override
672 public Element create() {
673 final List<Node> content = new ArrayList<Node>(createContent(params.getSet()));
674 if (content.size() == 0) {
675 return createNoRecordsMatchResponse(params.getParams()).create();
676 }
677 content.add(resumptionToken(params.getResumptionToken(), params.getMetadataPrefix(), params.getResult(),
678 params.getUntil(), params.getSet()));
679 return oai(
680 request($a("metadataPrefix", params.getMetadataPrefix()),
681 $aSome("from", params.getFrom().map(d -> toSupportedGranularity(d))),
682 $aSome("until", Optional.of(toSupportedGranularity(params.getUntil()))),
683 $aSome("set", params.getSet())), verb(content));
684 }
685 }
686
687 private class BadResumptionTokenException extends RuntimeException {
688 }
689 }
690 }
691
692
693 final class ListGenParams {
694 private final OaiPmhRepository repository;
695 private final SearchResult result;
696 private final MetadataProvider metadataProvider;
697 private final String metadataPrefix;
698 private final Optional<String> resumptionToken;
699 private final Optional<Date> from;
700 private final Date until;
701 private final Optional<String> set;
702 private final Params params;
703
704
705 ListGenParams(OaiPmhRepository repository,
706 SearchResult result, MetadataProvider metadataProvider,
707 String metadataPrefix, Optional<String> resumptionToken,
708 Optional<Date> from, Date until,
709 Optional<String> set,
710 Params params) {
711 this.repository = repository;
712 this.result = result;
713 this.metadataProvider = metadataProvider;
714 this.resumptionToken = resumptionToken;
715 this.metadataPrefix = metadataPrefix;
716 this.from = from;
717 this.until = until;
718 this.set = set;
719 this.params = params;
720 }
721
722
723 public OaiPmhRepository getRepository() {
724 return repository;
725 }
726
727 public SearchResult getResult() {
728 return result;
729 }
730
731 public MetadataProvider getMetadataProvider() {
732 return metadataProvider;
733 }
734
735 public Optional<String> getResumptionToken() {
736 return resumptionToken;
737 }
738
739 public String getMetadataPrefix() {
740 return metadataPrefix;
741 }
742
743 public Optional<Date> getFrom() {
744 return from;
745 }
746
747 public Date getUntil() {
748 return until;
749 }
750
751 public Optional<String> getSet() {
752 return set;
753 }
754
755
756 public Params getParams() {
757 return params;
758 }
759 }