2 * Copyright (C) 2009 EDIT
3 * European Distributed Institute of Taxonomy
4 * http://www.e-taxonomy.eu
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.
9 package eu
.etaxonomy
.cdm
.remote
.controller
.oaipmh
;
11 import java
.util
.ArrayList
;
12 import java
.util
.HashSet
;
13 import java
.util
.List
;
14 import java
.util
.UUID
;
16 import javax
.servlet
.http
.HttpServletRequest
;
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
;
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
;
61 public abstract class AbstractOaiPmhController
<T
extends IdentifiableEntity
, SERVICE
62 extends IIdentifiableEntityService
<T
>> {
64 protected SERVICE service
;
66 protected IAuditEventService auditEventService
;
68 private String repositoryName
;
70 private String baseURL
;
72 private String protocolVersion
;
74 private String adminEmail
;
76 private String description
;
78 private Integer pageSize
;
80 private CacheProviderFacade cacheProviderFacade
;
82 private CachingModel cachingModel
;
84 private boolean onlyItemsWithLsid
= false;
87 * sets cache name to be used
90 public void setCacheProviderFacade(CacheProviderFacade cacheProviderFacade
) {
91 this.cacheProviderFacade
= cacheProviderFacade
;
95 public void setCachingModel(CachingModel cachingModel
) {
96 this.cachingModel
= cachingModel
;
99 public abstract void setService(SERVICE service
);
101 public boolean isRestrictToLsid() {
102 return onlyItemsWithLsid
;
104 public void setRestrictToLsid(boolean restrictToLsid
) {
105 this.onlyItemsWithLsid
= restrictToLsid
;
109 * Subclasses should override this method to return a list of property
110 * paths that should be initialized for the getRecord, listRecords methods
113 protected List
<String
> getPropertyPaths() {
114 return new ArrayList
<>();
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
123 protected void addSets(ModelAndView modelAndView
) {
124 modelAndView
.addObject("sets", new HashSet
<SetSpec
>());
128 public void setAuditEventService(IAuditEventService auditEventService
) {
129 this.auditEventService
= auditEventService
;
132 public void setRepositoryName(String repositoryName
) {
133 this.repositoryName
= repositoryName
;
136 public void setBaseURL(String baseURL
) {
137 this.baseURL
= baseURL
;
140 public void setProtocolVersion(String protocolVersion
) {
141 this.protocolVersion
= protocolVersion
;
144 public void setAdminEmail(String adminEmail
) {
145 this.adminEmail
= adminEmail
;
148 public void setDescription(String description
) {
149 this.description
= description
;
152 public void setPageSize(Integer pageSize
) {
153 this.pageSize
= pageSize
;
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());
167 * CannotDisseminateFormatException thrown by MetadataPrefixEditor
169 * @throws IdDoesNotExistException
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 {
178 // ModelAndView modelAndView = new ModelAndView();
179 // modelAndView.addObject("metadataPrefix", metadataPrefix);
181 // finishModelAndView(identifier, metadataPrefix, modelAndView);
183 // return modelAndView;
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
{
192 ModelAndView modelAndView
= new ModelAndView();
193 modelAndView
.addObject("metadataPrefix", metadataPrefix
);
198 protected void finishModelAndView(LSID identifier
,
199 MetadataPrefix metadataPrefix
, ModelAndView modelAndView
)
200 throws IdDoesNotExistException
{
202 switch (metadataPrefix
) {
204 modelAndView
.addObject("object", obtainCdmEntity(identifier
));
205 modelAndView
.setViewName("oai/getRecord.rdf");
209 modelAndView
.addObject("object", obtainCdmEntity(identifier
));
210 modelAndView
.setViewName("oai/getRecord.dc");
214 protected AuditEventRecord
<T
> obtainCdmEntity(LSID identifier
)
215 throws IdDoesNotExistException
{
216 T object
= service
.find(identifier
);
218 throw new IdDoesNotExistException(identifier
);
221 Pager
<AuditEventRecord
<T
>> results
= service
.pageAuditEvents(object
, 1,
222 0, AuditEventSort
.BACKWARDS
, getPropertyPaths());
224 if (results
.getCount() == 0) {
225 throw new IdDoesNotExistException(identifier
);
227 return results
.getRecords().get(0);
232 * CannotDisseminateFormatException thrown by MetadataPrefixEditor
233 * @throws IdDoesNotExistException
235 @RequestMapping(method
= RequestMethod
.GET
,params
= "verb=ListMetadataFormats")
236 public ModelAndView
listMetadataFormats(@RequestParam(value
= "identifier", required
= false) LSID identifier
) throws IdDoesNotExistException
{
238 ModelAndView modelAndView
= new ModelAndView("oai/listMetadataFormats");
240 if(identifier
!= null) {
241 T object
= service
.find(identifier
);
243 throw new IdDoesNotExistException(identifier
);
251 * CannotDisseminateFormatException thrown by MetadataPrefixEditor
253 @RequestMapping(method
= RequestMethod
.GET
,params
= "verb=ListSets")
254 public ModelAndView
listSets() {
256 ModelAndView modelAndView
= new ModelAndView("oai/listSets");
258 addSets(modelAndView
);
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
);
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
);
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
) {
287 ModelAndView modelAndView
= new ModelAndView("oai/listIdentifiers");
288 modelAndView
.addObject("metadataPrefix",metadataPrefix
);
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
);
296 AuditEvent untilAuditEvent
= null;
298 modelAndView
.addObject("until",until
);
299 untilAuditEvent
= auditEventService
.find(until
);
304 modelAndView
.addObject("set",set
);
305 clazz
= set
.getSetClass();
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:%"));
314 Pager
<AuditEventRecord
<T
>> results
= service
.pageAuditEvents(clazz
, fromAuditEvent
, untilAuditEvent
, criteria
, pageSize
, 0, AuditEventSort
.FORWARDS
, null);
316 if(results
.getCount() == 0) {
317 throw new NoRecordsMatchException("No records match");
320 modelAndView
.addObject("pager",results
);
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
);
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());
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());
345 AuditEvent untilAuditEvent
= null;
346 if(resumptionToken
.getUntil() != null) {
347 modelAndView
.addObject("until",resumptionToken
.getUntil());
348 untilAuditEvent
= auditEventService
.find(resumptionToken
.getUntil());
352 if(resumptionToken
.getSet() != null) {
353 modelAndView
.addObject("set",resumptionToken
.getSet());
354 clazz
= resumptionToken
.getSet().getSetClass();
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:%"));
363 Pager
<AuditEventRecord
<T
>> results
= service
.pageAuditEvents(clazz
,fromAuditEvent
,untilAuditEvent
,criteria
, pageSize
, (resumptionToken
.getCursor().intValue() / pageSize
) + 1, AuditEventSort
.FORWARDS
,null);
365 if(results
.getCount() == 0) {
366 throw new NoRecordsMatchException("No records match");
369 modelAndView
.addObject("pager",results
);
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
);
376 resumptionToken
= ResumptionToken
.emptyResumptionToken();
377 modelAndView
.addObject("resumptionToken",resumptionToken
);
378 cacheProviderFacade
.removeFromCache(rToken
,cachingModel
);
383 throw new BadResumptionTokenException();
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
) {
393 ModelAndView modelAndView
= new ModelAndView();
394 modelAndView
.addObject("metadataPrefix",metadataPrefix
);
396 switch(metadataPrefix
) {
398 modelAndView
.setViewName("oai/listRecords.rdf");
402 modelAndView
.setViewName("oai/listRecords.dc");
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
);
411 AuditEvent untilAuditEvent
= null;
413 modelAndView
.addObject("until",until
);
414 untilAuditEvent
= auditEventService
.find(until
);
419 modelAndView
.addObject("set",set
);
420 clazz
= set
.getSetClass();
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:%"));
429 Pager
<AuditEventRecord
<T
>> results
= service
.pageAuditEvents(clazz
, fromAuditEvent
, untilAuditEvent
, criteria
, pageSize
, 0, AuditEventSort
.FORWARDS
, getPropertyPaths());
431 if(results
.getCount() == 0) {
432 throw new NoRecordsMatchException("No records match");
435 modelAndView
.addObject("pager",results
);
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
);
446 @RequestMapping(method
= RequestMethod
.GET
, params
= {"verb=ListRecords", "resumptionToken"})
447 public ModelAndView
listRecords(@RequestParam("resumptionToken") String rToken
) {
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());
455 switch (resumptionToken
.getMetadataPrefix()) {
457 modelAndView
.setViewName("oai/listRecords.rdf");
461 modelAndView
.setViewName("oai/listRecords.dc");
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());
470 AuditEvent untilAuditEvent
= null;
471 if(resumptionToken
.getUntil() != null) {
472 modelAndView
.addObject("until",resumptionToken
.getUntil());
473 untilAuditEvent
= auditEventService
.find(resumptionToken
.getUntil());
477 if(resumptionToken
.getSet() != null) {
478 modelAndView
.addObject("set",resumptionToken
.getSet());
479 clazz
= resumptionToken
.getSet().getSetClass();
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:%"));
487 Pager
<AuditEventRecord
<T
>> results
= service
.pageAuditEvents(clazz
,fromAuditEvent
,untilAuditEvent
,criteria
, pageSize
, (resumptionToken
.getCursor().intValue() / pageSize
) + 1, AuditEventSort
.FORWARDS
,getPropertyPaths());
489 if(results
.getCount() == 0) {
490 throw new NoRecordsMatchException("No records match");
493 modelAndView
.addObject("pager",results
);
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
);
500 resumptionToken
= ResumptionToken
.emptyResumptionToken();
501 modelAndView
.addObject("resumptionToken",resumptionToken
);
502 cacheProviderFacade
.removeFromCache(rToken
, cachingModel
);
507 throw new BadResumptionTokenException();
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) {
516 modelAndView
.addObject("verb", Verb
.fromValue(request
.getParameter("verb")));
517 } catch(Exception e
) {// prevent endless recursion
521 modelAndView
.addObject("code",code
);
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
);
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
);
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
);
543 @ExceptionHandler(NoRecordsMatchException
.class)
544 public ModelAndView
handleNoRecordsMatch(Exception ex
, HttpServletRequest request
) {
545 return doException(ex
,request
,ErrorCode
.NO_RECORDS_MATCH
);
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
);