Merge branch 'release/5.45.0'
[cdmlib.git] / cdmlib-remote / src / main / java / eu / etaxonomy / cdm / remote / controller / oaipmh / AbstractOaiPmhController.java
1 /**
2 * Copyright (C) 2009 EDIT
3 * European Distributed Institute of Taxonomy
4 * http://www.e-taxonomy.eu
5 *
6 * The contents of this file are subject to the Mozilla Public License Version 1.1
7 * See LICENSE.TXT at the top of this package for the full license terms.
8 */
9 package eu.etaxonomy.cdm.remote.controller.oaipmh;
10
11 import java.util.ArrayList;
12 import java.util.HashSet;
13 import java.util.List;
14 import java.util.UUID;
15
16 import javax.servlet.http.HttpServletRequest;
17
18 import org.hibernate.envers.query.AuditEntity;
19 import org.hibernate.envers.query.criteria.AuditCriterion;
20 import org.joda.time.DateTime;
21 import org.springframework.beans.TypeMismatchException;
22 import org.springframework.beans.factory.annotation.Autowired;
23 import org.springframework.http.HttpStatus;
24 import org.springframework.web.bind.MissingServletRequestParameterException;
25 import org.springframework.web.bind.WebDataBinder;
26 import org.springframework.web.bind.annotation.ExceptionHandler;
27 import org.springframework.web.bind.annotation.InitBinder;
28 import org.springframework.web.bind.annotation.RequestMapping;
29 import org.springframework.web.bind.annotation.RequestMethod;
30 import org.springframework.web.bind.annotation.RequestParam;
31 import org.springframework.web.bind.annotation.ResponseStatus;
32 import org.springframework.web.servlet.ModelAndView;
33 import org.springmodules.cache.CachingModel;
34 import org.springmodules.cache.provider.CacheProviderFacade;
35
36 import eu.etaxonomy.cdm.api.service.IAuditEventService;
37 import eu.etaxonomy.cdm.api.service.IIdentifiableEntityService;
38 import eu.etaxonomy.cdm.api.service.pager.Pager;
39 import eu.etaxonomy.cdm.model.common.IdentifiableEntity;
40 import eu.etaxonomy.cdm.model.common.LSID;
41 import eu.etaxonomy.cdm.model.view.AuditEvent;
42 import eu.etaxonomy.cdm.model.view.AuditEventRecord;
43 import eu.etaxonomy.cdm.persistence.dao.common.AuditEventSort;
44 import eu.etaxonomy.cdm.remote.controller.BadResumptionTokenException;
45 import eu.etaxonomy.cdm.remote.controller.IdDoesNotExistException;
46 import eu.etaxonomy.cdm.remote.dto.oaipmh.DeletedRecord;
47 import eu.etaxonomy.cdm.remote.dto.oaipmh.ErrorCode;
48 import eu.etaxonomy.cdm.remote.dto.oaipmh.Granularity;
49 import eu.etaxonomy.cdm.remote.dto.oaipmh.MetadataPrefix;
50 import eu.etaxonomy.cdm.remote.dto.oaipmh.ResumptionToken;
51 import eu.etaxonomy.cdm.remote.dto.oaipmh.SetSpec;
52 import eu.etaxonomy.cdm.remote.dto.oaipmh.Verb;
53 import eu.etaxonomy.cdm.remote.editor.IsoDateTimeEditor;
54 import eu.etaxonomy.cdm.remote.editor.LSIDPropertyEditor;
55 import eu.etaxonomy.cdm.remote.editor.MetadataPrefixEditor;
56 import eu.etaxonomy.cdm.remote.editor.SetSpecEditor;
57 import eu.etaxonomy.cdm.remote.editor.UUIDPropertyEditor;
58 import eu.etaxonomy.cdm.remote.exception.CannotDisseminateFormatException;
59 import eu.etaxonomy.cdm.remote.exception.NoRecordsMatchException;
60
61 public abstract class AbstractOaiPmhController<T extends IdentifiableEntity, SERVICE
62 extends IIdentifiableEntityService<T>> {
63
64 protected SERVICE service;
65
66 protected IAuditEventService auditEventService;
67
68 private String repositoryName;
69
70 private String baseURL;
71
72 private String protocolVersion;
73
74 private String adminEmail;
75
76 private String description;
77
78 private Integer pageSize;
79
80 private CacheProviderFacade cacheProviderFacade;
81
82 private CachingModel cachingModel;
83
84 private boolean onlyItemsWithLsid = false;
85
86 /**
87 * sets cache name to be used
88 */
89 @Autowired
90 public void setCacheProviderFacade(CacheProviderFacade cacheProviderFacade) {
91 this.cacheProviderFacade = cacheProviderFacade;
92 }
93
94 @Autowired
95 public void setCachingModel(CachingModel cachingModel) {
96 this.cachingModel = cachingModel;
97 }
98
99 public abstract void setService(SERVICE service);
100
101 public boolean isRestrictToLsid() {
102 return onlyItemsWithLsid;
103 }
104 public void setRestrictToLsid(boolean restrictToLsid) {
105 this.onlyItemsWithLsid = restrictToLsid;
106 }
107
108 /**
109 * Subclasses should override this method to return a list of property
110 * paths that should be initialized for the getRecord, listRecords methods
111 * @return
112 */
113 protected List<String> getPropertyPaths() {
114 return new ArrayList<>();
115 }
116
117 /**
118 * Subclasses should override this method and add a collection of
119 * eu.etaxonomy.cdm.remote.dto.oaipmh.Set objects called "sets" that
120 * will be returned in the response
121 * @param modelAndView
122 */
123 protected void addSets(ModelAndView modelAndView) {
124 modelAndView.addObject("sets", new HashSet<SetSpec>());
125 }
126
127 @Autowired
128 public void setAuditEventService(IAuditEventService auditEventService) {
129 this.auditEventService = auditEventService;
130 }
131
132 public void setRepositoryName(String repositoryName) {
133 this.repositoryName = repositoryName;
134 }
135
136 public void setBaseURL(String baseURL) {
137 this.baseURL = baseURL;
138 }
139
140 public void setProtocolVersion(String protocolVersion) {
141 this.protocolVersion = protocolVersion;
142 }
143
144 public void setAdminEmail(String adminEmail) {
145 this.adminEmail = adminEmail;
146 }
147
148 public void setDescription(String description) {
149 this.description = description;
150 }
151
152 public void setPageSize(Integer pageSize) {
153 this.pageSize = pageSize;
154 }
155
156 @InitBinder
157 public void initBinder(WebDataBinder binder) {
158 binder.registerCustomEditor(DateTime.class, new IsoDateTimeEditor());
159 binder.registerCustomEditor(LSID.class, new LSIDPropertyEditor());
160 binder.registerCustomEditor(MetadataPrefix.class, new MetadataPrefixEditor());
161 binder.registerCustomEditor(SetSpec.class, new SetSpecEditor());
162 binder.registerCustomEditor(UUID.class, new UUIDPropertyEditor());
163 }
164
165
166 /**
167 * CannotDisseminateFormatException thrown by MetadataPrefixEditor
168 *
169 * @throws IdDoesNotExistException
170 */
171 // FIXME has same mapping as the other getRecord method: do we really need to support LSIDs or shall we skip this
172 // @RequestMapping(method = RequestMethod.GET, params = "verb=GetRecord")
173 // public ModelAndView getRecord(
174 // @RequestParam(value = "identifier", required = true) LSID identifier,
175 // @RequestParam(value = "metadataPrefix", required = true) MetadataPrefix metadataPrefix)
176 // throws IdDoesNotExistException {
177 //
178 // ModelAndView modelAndView = new ModelAndView();
179 // modelAndView.addObject("metadataPrefix", metadataPrefix);
180 //
181 // finishModelAndView(identifier, metadataPrefix, modelAndView);
182 //
183 // return modelAndView;
184 // }
185
186 @RequestMapping(method = RequestMethod.GET, params = "verb=GetRecord")
187 public ModelAndView getRecord(
188 @RequestParam(value = "identifier", required = true) UUID identifier,
189 @RequestParam(value = "metadataPrefix", required = true) MetadataPrefix metadataPrefix)
190 throws IdDoesNotExistException {
191
192 ModelAndView modelAndView = new ModelAndView();
193 modelAndView.addObject("metadataPrefix", metadataPrefix);
194
195 return modelAndView;
196 }
197
198 protected void finishModelAndView(LSID identifier,
199 MetadataPrefix metadataPrefix, ModelAndView modelAndView)
200 throws IdDoesNotExistException {
201
202 switch (metadataPrefix) {
203 case RDF:
204 modelAndView.addObject("object", obtainCdmEntity(identifier));
205 modelAndView.setViewName("oai/getRecord.rdf");
206 break;
207 case OAI_DC:
208 default:
209 modelAndView.addObject("object", obtainCdmEntity(identifier));
210 modelAndView.setViewName("oai/getRecord.dc");
211 }
212 }
213
214 protected AuditEventRecord<T> obtainCdmEntity(LSID identifier)
215 throws IdDoesNotExistException {
216 T object = service.find(identifier);
217 if(object == null){
218 throw new IdDoesNotExistException(identifier);
219 }
220
221 Pager<AuditEventRecord<T>> results = service.pageAuditEvents(object, 1,
222 0, AuditEventSort.BACKWARDS, getPropertyPaths());
223
224 if (results.getCount() == 0) {
225 throw new IdDoesNotExistException(identifier);
226 }
227 return results.getRecords().get(0);
228 }
229
230
231 /**
232 * CannotDisseminateFormatException thrown by MetadataPrefixEditor
233 * @throws IdDoesNotExistException
234 */
235 @RequestMapping(method = RequestMethod.GET,params = "verb=ListMetadataFormats")
236 public ModelAndView listMetadataFormats(@RequestParam(value = "identifier", required = false) LSID identifier) throws IdDoesNotExistException {
237
238 ModelAndView modelAndView = new ModelAndView("oai/listMetadataFormats");
239
240 if(identifier != null) {
241 T object = service.find(identifier);
242 if(object == null) {
243 throw new IdDoesNotExistException(identifier);
244 }
245 }
246
247 return modelAndView;
248 }
249
250 /**
251 * CannotDisseminateFormatException thrown by MetadataPrefixEditor
252 */
253 @RequestMapping(method = RequestMethod.GET,params = "verb=ListSets")
254 public ModelAndView listSets() {
255
256 ModelAndView modelAndView = new ModelAndView("oai/listSets");
257
258 addSets(modelAndView);
259
260 return modelAndView;
261 }
262
263 @RequestMapping(method = RequestMethod.GET,params = "verb=Identify")
264 public ModelAndView identify() {
265 ModelAndView modelAndView = new ModelAndView("oai/identify");
266 modelAndView.addObject("repositoryName", repositoryName);
267 modelAndView.addObject("baseURL",baseURL);
268 modelAndView.addObject("protocolVersion",protocolVersion);
269 modelAndView.addObject("deletedRecord",DeletedRecord.PERSISTENT);
270 modelAndView.addObject("granularity",Granularity.YYYY_MM_DD_THH_MM_SS_Z);
271
272 Pager<AuditEvent> auditEvents = auditEventService.list(0,1,AuditEventSort.FORWARDS);
273 modelAndView.addObject("earliestDatestamp",auditEvents.getRecords().get(0).getDate());
274 modelAndView.addObject("adminEmail",adminEmail);
275 modelAndView.addObject("description",description);
276
277 return modelAndView;
278 }
279
280 @RequestMapping(method = RequestMethod.GET, params = {"verb=ListIdentifiers", "!resumptionToken"})
281 public ModelAndView listIdentifiers(
282 @RequestParam(value = "from", required = false) DateTime from,
283 @RequestParam(value = "until", required = false) DateTime until,
284 @RequestParam(value = "metadataPrefix",required = true) MetadataPrefix metadataPrefix,
285 @RequestParam(value = "set", required = false) SetSpec set) {
286
287 ModelAndView modelAndView = new ModelAndView("oai/listIdentifiers");
288 modelAndView.addObject("metadataPrefix",metadataPrefix);
289
290 AuditEvent fromAuditEvent = null;
291 if(from != null) { // if from is specified, use the event at that date
292 modelAndView.addObject("from",from);
293 fromAuditEvent = auditEventService.find(from);
294 }
295
296 AuditEvent untilAuditEvent = null;
297 if(until != null) {
298 modelAndView.addObject("until",until);
299 untilAuditEvent = auditEventService.find(until);
300 }
301
302 Class clazz = null;
303 if(set != null) {
304 modelAndView.addObject("set",set);
305 clazz = set.getSetClass();
306 }
307
308 List<AuditCriterion> criteria = new ArrayList<>();
309 if(onlyItemsWithLsid){
310 //criteria.add(AuditEntity.property("lsid_lsid").isNotNull());
311 //TODO this isNotNull criterion did not work with mysql, so using a like statement as interim solution
312 criteria.add(AuditEntity.property("lsid_lsid").like("urn:lsid:%"));
313 }
314 Pager<AuditEventRecord<T>> results = service.pageAuditEvents(clazz, fromAuditEvent, untilAuditEvent, criteria, pageSize, 0, AuditEventSort.FORWARDS, null);
315
316 if(results.getCount() == 0) {
317 throw new NoRecordsMatchException("No records match");
318 }
319
320 modelAndView.addObject("pager",results);
321
322 if(results.getCount() > results.getRecords().size() && cacheProviderFacade != null) {
323 ResumptionToken resumptionToken = new ResumptionToken(results, from, until, metadataPrefix, set);
324 modelAndView.addObject("resumptionToken",resumptionToken);
325 cacheProviderFacade.putInCache(resumptionToken.getValue(), cachingModel, resumptionToken);
326 }
327
328 return modelAndView;
329 }
330
331 @RequestMapping(method = RequestMethod.GET, params = {"verb=ListIdentifiers", "resumptionToken"})
332 public ModelAndView listIdentifiers(@RequestParam(value = "resumptionToken",required = true) String rToken) {
333 ResumptionToken resumptionToken;
334 if(cacheProviderFacade != null && cacheProviderFacade.getFromCache(rToken, cachingModel) != null) {
335 resumptionToken = (ResumptionToken) cacheProviderFacade.getFromCache(rToken, cachingModel);
336 ModelAndView modelAndView = new ModelAndView("oai/listIdentifiers");
337 modelAndView.addObject("metadataPrefix",resumptionToken.getMetadataPrefix());
338
339 AuditEvent fromAuditEvent = null;
340 if(resumptionToken.getFrom() != null) { // if from is specified, use the event at that date
341 modelAndView.addObject("from",resumptionToken.getFrom());
342 fromAuditEvent = auditEventService.find(resumptionToken.getFrom());
343 }
344
345 AuditEvent untilAuditEvent = null;
346 if(resumptionToken.getUntil() != null) {
347 modelAndView.addObject("until",resumptionToken.getUntil());
348 untilAuditEvent = auditEventService.find(resumptionToken.getUntil());
349 }
350
351 Class clazz = null;
352 if(resumptionToken.getSet() != null) {
353 modelAndView.addObject("set",resumptionToken.getSet());
354 clazz = resumptionToken.getSet().getSetClass();
355 }
356
357 List<AuditCriterion> criteria = new ArrayList<>();
358 if(onlyItemsWithLsid){
359 //criteria.add(AuditEntity.property("lsid_lsid").isNotNull());
360 //TODO this isNotNull criterion did not work with mysql, so using a like statement as interim solution
361 criteria.add(AuditEntity.property("lsid_lsid").like("urn:lsid:%"));
362 }
363 Pager<AuditEventRecord<T>> results = service.pageAuditEvents(clazz,fromAuditEvent,untilAuditEvent,criteria, pageSize, (resumptionToken.getCursor().intValue() / pageSize) + 1, AuditEventSort.FORWARDS,null);
364
365 if(results.getCount() == 0) {
366 throw new NoRecordsMatchException("No records match");
367 }
368
369 modelAndView.addObject("pager",results);
370
371 if(results.getCount() > ((results.getPageSize() * results.getCurrentIndex()) + results.getRecords().size())) {
372 resumptionToken.updateResults(results);
373 modelAndView.addObject("resumptionToken", resumptionToken);
374 cacheProviderFacade.putInCache(resumptionToken.getValue(),cachingModel, resumptionToken);
375 } else {
376 resumptionToken = ResumptionToken.emptyResumptionToken();
377 modelAndView.addObject("resumptionToken",resumptionToken);
378 cacheProviderFacade.removeFromCache(rToken,cachingModel);
379 }
380
381 return modelAndView;
382 } else {
383 throw new BadResumptionTokenException();
384 }
385 }
386
387 @RequestMapping(method = RequestMethod.GET, params = {"verb=ListRecords", "!resumptionToken"})
388 public ModelAndView listRecords(@RequestParam(value = "from", required = false) DateTime from,
389 @RequestParam(value = "until", required = false) DateTime until,
390 @RequestParam(value = "metadataPrefix", required = true) MetadataPrefix metadataPrefix,
391 @RequestParam(value = "set", required = false) SetSpec set) {
392
393 ModelAndView modelAndView = new ModelAndView();
394 modelAndView.addObject("metadataPrefix",metadataPrefix);
395
396 switch(metadataPrefix) {
397 case RDF:
398 modelAndView.setViewName("oai/listRecords.rdf");
399 break;
400 case OAI_DC:
401 default:
402 modelAndView.setViewName("oai/listRecords.dc");
403 }
404
405 AuditEvent fromAuditEvent = null;
406 if(from != null) { // if from is specified, use the event at that date
407 modelAndView.addObject("from",from);
408 fromAuditEvent = auditEventService.find(from);
409 }
410
411 AuditEvent untilAuditEvent = null;
412 if(until != null) {
413 modelAndView.addObject("until",until);
414 untilAuditEvent = auditEventService.find(until);
415 }
416
417 Class clazz = null;
418 if(set != null) {
419 modelAndView.addObject("set",set);
420 clazz = set.getSetClass();
421 }
422
423 List<AuditCriterion> criteria = new ArrayList<AuditCriterion>();
424 if(onlyItemsWithLsid){
425 //criteria.add(AuditEntity.property("lsid_lsid").isNotNull());
426 //TODO this isNotNull criterion did not work with mysql, so using a like statement as interim solution
427 criteria.add(AuditEntity.property("lsid_lsid").like("urn:lsid:%"));
428 }
429 Pager<AuditEventRecord<T>> results = service.pageAuditEvents(clazz, fromAuditEvent, untilAuditEvent, criteria, pageSize, 0, AuditEventSort.FORWARDS, getPropertyPaths());
430
431 if(results.getCount() == 0) {
432 throw new NoRecordsMatchException("No records match");
433 }
434
435 modelAndView.addObject("pager",results);
436
437 if(results.getCount() > results.getRecords().size() && cacheProviderFacade != null) {
438 ResumptionToken resumptionToken = new ResumptionToken(results, from, until, metadataPrefix, set);
439 modelAndView.addObject("resumptionToken",resumptionToken);
440 cacheProviderFacade.putInCache(resumptionToken.getValue(), cachingModel, resumptionToken);
441 }
442
443 return modelAndView;
444 }
445
446 @RequestMapping(method = RequestMethod.GET, params = {"verb=ListRecords", "resumptionToken"})
447 public ModelAndView listRecords(@RequestParam("resumptionToken") String rToken) {
448
449 ResumptionToken resumptionToken;
450 if(cacheProviderFacade != null && cacheProviderFacade.getFromCache(rToken,cachingModel) != null) {
451 resumptionToken = (ResumptionToken) cacheProviderFacade.getFromCache(rToken,cachingModel);
452 ModelAndView modelAndView = new ModelAndView();
453 modelAndView.addObject("metadataPrefix",resumptionToken.getMetadataPrefix());
454
455 switch (resumptionToken.getMetadataPrefix()) {
456 case RDF:
457 modelAndView.setViewName("oai/listRecords.rdf");
458 break;
459 case OAI_DC:
460 default:
461 modelAndView.setViewName("oai/listRecords.dc");
462 }
463
464 AuditEvent fromAuditEvent = null;
465 if(resumptionToken.getFrom() != null) { // if from is specified, use the event at that date
466 modelAndView.addObject("from",resumptionToken.getFrom());
467 fromAuditEvent = auditEventService.find(resumptionToken.getFrom());
468 }
469
470 AuditEvent untilAuditEvent = null;
471 if(resumptionToken.getUntil() != null) {
472 modelAndView.addObject("until",resumptionToken.getUntil());
473 untilAuditEvent = auditEventService.find(resumptionToken.getUntil());
474 }
475
476 Class clazz = null;
477 if(resumptionToken.getSet() != null) {
478 modelAndView.addObject("set",resumptionToken.getSet());
479 clazz = resumptionToken.getSet().getSetClass();
480 }
481 List<AuditCriterion> criteria = new ArrayList<>();
482 if(onlyItemsWithLsid){
483 //criteria.add(AuditEntity.property("lsid_lsid").isNotNull());
484 //TODO this isNotNull criterion did not work with mysql, so using a like statement as interim solution
485 criteria.add(AuditEntity.property("lsid_lsid").like("urn:lsid:%"));
486 }
487 Pager<AuditEventRecord<T>> results = service.pageAuditEvents(clazz,fromAuditEvent,untilAuditEvent,criteria, pageSize, (resumptionToken.getCursor().intValue() / pageSize) + 1, AuditEventSort.FORWARDS,getPropertyPaths());
488
489 if(results.getCount() == 0) {
490 throw new NoRecordsMatchException("No records match");
491 }
492
493 modelAndView.addObject("pager",results);
494
495 if(results.getCount() > ((results.getPageSize() * results.getCurrentIndex()) + results.getRecords().size())) {
496 resumptionToken.updateResults(results);
497 modelAndView.addObject("resumptionToken",resumptionToken);
498 cacheProviderFacade.putInCache(resumptionToken.getValue(), cachingModel, resumptionToken);
499 } else {
500 resumptionToken = ResumptionToken.emptyResumptionToken();
501 modelAndView.addObject("resumptionToken",resumptionToken);
502 cacheProviderFacade.removeFromCache(rToken, cachingModel);
503 }
504
505 return modelAndView;
506 } else {
507 throw new BadResumptionTokenException();
508 }
509 }
510
511 private ModelAndView doException(Exception ex, HttpServletRequest request, ErrorCode code) {
512 ModelAndView modelAndView = new ModelAndView("oai/exception");
513 modelAndView.addObject("message", ex.getMessage());
514 if(request.getParameter("verb") != null) {
515 try {
516 modelAndView.addObject("verb", Verb.fromValue(request.getParameter("verb")));
517 } catch(Exception e) {// prevent endless recursion
518
519 }
520 }
521 modelAndView.addObject("code",code);
522 return modelAndView;
523 }
524
525 @ResponseStatus(HttpStatus.BAD_REQUEST)
526 @ExceptionHandler({IllegalArgumentException.class,TypeMismatchException.class,MissingServletRequestParameterException.class})
527 public ModelAndView handleBadArgument(Exception ex, HttpServletRequest request) {
528 return doException(ex,request,ErrorCode.BAD_ARGUMENT);
529 }
530
531 @ResponseStatus(HttpStatus.BAD_REQUEST)
532 @ExceptionHandler(CannotDisseminateFormatException.class)
533 public ModelAndView handleCannotDisseminateFormat(Exception ex, HttpServletRequest request) {
534 return doException(ex,request,ErrorCode.CANNOT_DISSEMINATE_FORMAT);
535 }
536
537 @ResponseStatus(HttpStatus.BAD_REQUEST)
538 @ExceptionHandler(BadResumptionTokenException.class)
539 public ModelAndView handleBadResumptionToken(Exception ex, HttpServletRequest request) {
540 return doException(ex,request,ErrorCode.BAD_RESUMPTION_TOKEN);
541 }
542
543 @ExceptionHandler(NoRecordsMatchException.class)
544 public ModelAndView handleNoRecordsMatch(Exception ex, HttpServletRequest request) {
545 return doException(ex,request,ErrorCode.NO_RECORDS_MATCH);
546 }
547
548 @ResponseStatus(HttpStatus.NOT_FOUND)
549 @ExceptionHandler(IdDoesNotExistException.class)
550 public ModelAndView handleIdDoesNotExist(Exception ex, HttpServletRequest request) {
551 return doException(ex,request,ErrorCode.ID_DOES_NOT_EXIST);
552 }
553
554 }