1 package eu
.etaxonomy
.cdm
.remote
.controller
.oaipmh
;
4 import java
.util
.ArrayList
;
5 import java
.util
.HashSet
;
9 import javax
.servlet
.http
.HttpServletRequest
;
11 import org
.hibernate
.envers
.query
.AuditEntity
;
12 import org
.hibernate
.envers
.query
.criteria
.AuditCriterion
;
13 import org
.joda
.time
.DateTime
;
14 import org
.springframework
.beans
.TypeMismatchException
;
15 import org
.springframework
.beans
.factory
.annotation
.Autowired
;
16 import org
.springframework
.http
.HttpStatus
;
17 import org
.springframework
.web
.bind
.MissingServletRequestParameterException
;
18 import org
.springframework
.web
.bind
.WebDataBinder
;
19 import org
.springframework
.web
.bind
.annotation
.ExceptionHandler
;
20 import org
.springframework
.web
.bind
.annotation
.InitBinder
;
21 import org
.springframework
.web
.bind
.annotation
.RequestMapping
;
22 import org
.springframework
.web
.bind
.annotation
.RequestMethod
;
23 import org
.springframework
.web
.bind
.annotation
.RequestParam
;
24 import org
.springframework
.web
.bind
.annotation
.ResponseStatus
;
25 import org
.springframework
.web
.servlet
.ModelAndView
;
26 import org
.springmodules
.cache
.CachingModel
;
27 import org
.springmodules
.cache
.provider
.CacheProviderFacade
;
29 import eu
.etaxonomy
.cdm
.api
.service
.IAuditEventService
;
30 import eu
.etaxonomy
.cdm
.api
.service
.IIdentifiableEntityService
;
31 import eu
.etaxonomy
.cdm
.api
.service
.pager
.Pager
;
32 import eu
.etaxonomy
.cdm
.model
.common
.IdentifiableEntity
;
33 import eu
.etaxonomy
.cdm
.model
.common
.LSID
;
34 import eu
.etaxonomy
.cdm
.model
.view
.AuditEvent
;
35 import eu
.etaxonomy
.cdm
.model
.view
.AuditEventRecord
;
36 import eu
.etaxonomy
.cdm
.persistence
.dao
.common
.AuditEventSort
;
37 import eu
.etaxonomy
.cdm
.remote
.controller
.BadResumptionTokenException
;
38 import eu
.etaxonomy
.cdm
.remote
.controller
.IdDoesNotExistException
;
39 import eu
.etaxonomy
.cdm
.remote
.dto
.oaipmh
.DeletedRecord
;
40 import eu
.etaxonomy
.cdm
.remote
.dto
.oaipmh
.ErrorCode
;
41 import eu
.etaxonomy
.cdm
.remote
.dto
.oaipmh
.Granularity
;
42 import eu
.etaxonomy
.cdm
.remote
.dto
.oaipmh
.MetadataPrefix
;
43 import eu
.etaxonomy
.cdm
.remote
.dto
.oaipmh
.ResumptionToken
;
44 import eu
.etaxonomy
.cdm
.remote
.dto
.oaipmh
.SetSpec
;
45 import eu
.etaxonomy
.cdm
.remote
.dto
.oaipmh
.Verb
;
46 import eu
.etaxonomy
.cdm
.remote
.editor
.IsoDateTimeEditor
;
47 import eu
.etaxonomy
.cdm
.remote
.editor
.LSIDPropertyEditor
;
48 import eu
.etaxonomy
.cdm
.remote
.editor
.MetadataPrefixEditor
;
49 import eu
.etaxonomy
.cdm
.remote
.editor
.SetSpecEditor
;
50 import eu
.etaxonomy
.cdm
.remote
.editor
.UUIDPropertyEditor
;
51 import eu
.etaxonomy
.cdm
.remote
.exception
.CannotDisseminateFormatException
;
52 import eu
.etaxonomy
.cdm
.remote
.exception
.NoRecordsMatchException
;
54 public abstract class AbstractOaiPmhController
<T
extends IdentifiableEntity
, SERVICE
extends IIdentifiableEntityService
<T
>> {
56 protected SERVICE service
;
58 protected IAuditEventService auditEventService
;
60 private String repositoryName
;
62 private String baseURL
;
64 private String protocolVersion
;
66 private String adminEmail
;
68 private String description
;
70 private Integer pageSize
;
72 public abstract void setService(SERVICE service
);
74 private CacheProviderFacade cacheProviderFacade
;
76 private CachingModel cachingModel
;
78 private boolean onlyItemsWithLsid
= false;
80 public boolean isRestrictToLsid() {
81 return onlyItemsWithLsid
;
84 public void setRestrictToLsid(boolean restrictToLsid
) {
85 this.onlyItemsWithLsid
= restrictToLsid
;
89 * sets cache name to be used
92 public void setCacheProviderFacade(CacheProviderFacade cacheProviderFacade
) {
93 this.cacheProviderFacade
= cacheProviderFacade
;
97 public void setCachingModel(CachingModel cachingModel
) {
98 this.cachingModel
= cachingModel
;
102 * Subclasses should override this method to return a list of property
103 * paths that should be initialized for the getRecord, listRecords methods
106 protected List
<String
> getPropertyPaths() {
107 return new ArrayList
<String
>();
111 * Subclasses should override this method and add a collection of
112 * eu.etaxonomy.cdm.remote.dto.oaipmh.Set objects called "sets" that
113 * will be returned in the response
114 * @param modelAndView
116 protected void addSets(ModelAndView modelAndView
) {
117 modelAndView
.addObject("sets",new HashSet
<SetSpec
>());
121 public void setAuditEventService(IAuditEventService auditEventService
) {
122 this.auditEventService
= auditEventService
;
125 public void setRepositoryName(String repositoryName
) {
126 this.repositoryName
= repositoryName
;
129 public void setBaseURL(String baseURL
) {
130 this.baseURL
= baseURL
;
133 public void setProtocolVersion(String protocolVersion
) {
134 this.protocolVersion
= protocolVersion
;
137 public void setAdminEmail(String adminEmail
) {
138 this.adminEmail
= adminEmail
;
141 public void setDescription(String description
) {
142 this.description
= description
;
145 public void setPageSize(Integer pageSize
) {
146 this.pageSize
= pageSize
;
150 public void initBinder(WebDataBinder binder
) {
151 binder
.registerCustomEditor(DateTime
.class, new IsoDateTimeEditor());
152 binder
.registerCustomEditor(LSID
.class, new LSIDPropertyEditor());
153 binder
.registerCustomEditor(MetadataPrefix
.class, new MetadataPrefixEditor());
154 binder
.registerCustomEditor(SetSpec
.class, new SetSpecEditor());
155 binder
.registerCustomEditor(UUID
.class, new UUIDPropertyEditor());
160 * CannotDisseminateFormatException thrown by MetadataPrefixEditor
162 * @throws IdDoesNotExistException
164 @RequestMapping(method
= RequestMethod
.GET
, params
= "verb=GetRecord")
165 public ModelAndView
getRecord(
166 @RequestParam(value
= "identifier", required
= true) LSID identifier
,
167 @RequestParam(value
= "metadataPrefix", required
= true) MetadataPrefix metadataPrefix
)
168 throws IdDoesNotExistException
{
170 ModelAndView modelAndView
= new ModelAndView();
171 modelAndView
.addObject("metadataPrefix", metadataPrefix
);
173 finishModelAndView(identifier
, metadataPrefix
, modelAndView
);
178 @RequestMapping(method
= RequestMethod
.GET
, params
= "verb=GetRecord")
179 public ModelAndView
getRecord(
180 @RequestParam(value
= "identifier", required
= true) UUID identifier
,
181 @RequestParam(value
= "metadataPrefix", required
= true) MetadataPrefix metadataPrefix
)
182 throws IdDoesNotExistException
{
184 ModelAndView modelAndView
= new ModelAndView();
185 modelAndView
.addObject("metadataPrefix", metadataPrefix
);
187 System
.err
.println("############");
194 * @param metadataPrefix
195 * @param modelAndView
196 * @throws IdDoesNotExistException
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");
217 * @throws IdDoesNotExistException
219 protected AuditEventRecord
<T
> obtainCdmEntity(LSID identifier
)
220 throws IdDoesNotExistException
{
221 T object
= service
.find(identifier
);
223 throw new IdDoesNotExistException(identifier
);
226 Pager
<AuditEventRecord
<T
>> results
= service
.pageAuditEvents(object
, 1,
227 0, AuditEventSort
.BACKWARDS
, getPropertyPaths());
229 if (results
.getCount() == 0) {
230 throw new IdDoesNotExistException(identifier
);
232 return results
.getRecords().get(0);
237 * CannotDisseminateFormatException thrown by MetadataPrefixEditor
238 * @throws IdDoesNotExistException
240 @RequestMapping(method
= RequestMethod
.GET
,params
= "verb=ListMetadataFormats")
241 public ModelAndView
listMetadataFormats(@RequestParam(value
= "identifier", required
= false) LSID identifier
) throws IdDoesNotExistException
{
243 ModelAndView modelAndView
= new ModelAndView("oai/listMetadataFormats");
245 if(identifier
!= null) {
246 T object
= service
.find(identifier
);
248 throw new IdDoesNotExistException(identifier
);
256 * CannotDisseminateFormatException thrown by MetadataPrefixEditor
258 @RequestMapping(method
= RequestMethod
.GET
,params
= "verb=ListSets")
259 public ModelAndView
listSets() {
261 ModelAndView modelAndView
= new ModelAndView("oai/listSets");
263 addSets(modelAndView
);
268 @RequestMapping(method
= RequestMethod
.GET
,params
= "verb=Identify")
269 public ModelAndView
identify() {
270 ModelAndView modelAndView
= new ModelAndView("oai/identify");
271 modelAndView
.addObject("repositoryName", repositoryName
);
272 modelAndView
.addObject("baseURL",baseURL
);
273 modelAndView
.addObject("protocolVersion",protocolVersion
);
274 modelAndView
.addObject("deletedRecord",DeletedRecord
.PERSISTENT
);
275 modelAndView
.addObject("granularity",Granularity
.YYYY_MM_DD_THH_MM_SS_Z
);
277 Pager
<AuditEvent
> auditEvents
= auditEventService
.list(0,1,AuditEventSort
.FORWARDS
);
278 modelAndView
.addObject("earliestDatestamp",auditEvents
.getRecords().get(0).getDate());
279 modelAndView
.addObject("adminEmail",adminEmail
);
280 modelAndView
.addObject("description",description
);
285 @RequestMapping(method
= RequestMethod
.GET
, params
= {"verb=ListIdentifiers", "!resumptionToken"})
286 public ModelAndView
listIdentifiers(
287 @RequestParam(value
= "from", required
= false) DateTime from
,
288 @RequestParam(value
= "until", required
= false) DateTime until
,
289 @RequestParam(value
= "metadataPrefix",required
= true) MetadataPrefix metadataPrefix
,
290 @RequestParam(value
= "set", required
= false) SetSpec set
) {
292 ModelAndView modelAndView
= new ModelAndView("oai/listIdentifiers");
293 modelAndView
.addObject("metadataPrefix",metadataPrefix
);
295 AuditEvent fromAuditEvent
= null;
296 if(from
!= null) { // if from is specified, use the event at that date
297 modelAndView
.addObject("from",from
);
298 fromAuditEvent
= auditEventService
.find(from
);
301 AuditEvent untilAuditEvent
= null;
303 modelAndView
.addObject("until",until
);
304 untilAuditEvent
= auditEventService
.find(until
);
309 modelAndView
.addObject("set",set
);
310 clazz
= (Class
)set
.getSetClass();
313 List
<AuditCriterion
> criteria
= new ArrayList
<AuditCriterion
>();
314 if(onlyItemsWithLsid
){
315 //criteria.add(AuditEntity.property("lsid_lsid").isNotNull());
316 //TODO this isNotNull criterion did not work with mysql, so using a like statement as interim solution
317 criteria
.add(AuditEntity
.property("lsid_lsid").like("urn:lsid:%"));
319 Pager
<AuditEventRecord
<T
>> results
= service
.pageAuditEvents(clazz
, fromAuditEvent
, untilAuditEvent
, criteria
, pageSize
, 0, AuditEventSort
.FORWARDS
, null);
321 if(results
.getCount() == 0) {
322 throw new NoRecordsMatchException("No records match");
325 modelAndView
.addObject("pager",results
);
327 if(results
.getCount() > results
.getRecords().size() && cacheProviderFacade
!= null) {
328 ResumptionToken resumptionToken
= new ResumptionToken(results
, from
, until
, metadataPrefix
, set
);
329 modelAndView
.addObject("resumptionToken",resumptionToken
);
330 cacheProviderFacade
.putInCache(resumptionToken
.getValue(), cachingModel
, resumptionToken
);
336 @RequestMapping(method
= RequestMethod
.GET
, params
= {"verb=ListIdentifiers", "resumptionToken"})
337 public ModelAndView
listIdentifiers(@RequestParam(value
= "resumptionToken",required
= true) String rToken
) {
338 ResumptionToken resumptionToken
;
339 if(cacheProviderFacade
!= null && cacheProviderFacade
.getFromCache(rToken
, cachingModel
) != null) {
340 resumptionToken
= (ResumptionToken
) cacheProviderFacade
.getFromCache(rToken
, cachingModel
);
341 ModelAndView modelAndView
= new ModelAndView("oai/listIdentifiers");
342 modelAndView
.addObject("metadataPrefix",resumptionToken
.getMetadataPrefix());
344 AuditEvent fromAuditEvent
= null;
345 if(resumptionToken
.getFrom() != null) { // if from is specified, use the event at that date
346 modelAndView
.addObject("from",resumptionToken
.getFrom());
347 fromAuditEvent
= auditEventService
.find(resumptionToken
.getFrom());
350 AuditEvent untilAuditEvent
= null;
351 if(resumptionToken
.getUntil() != null) {
352 modelAndView
.addObject("until",resumptionToken
.getUntil());
353 untilAuditEvent
= auditEventService
.find(resumptionToken
.getUntil());
357 if(resumptionToken
.getSet() != null) {
358 modelAndView
.addObject("set",resumptionToken
.getSet());
359 clazz
= (Class
)resumptionToken
.getSet().getSetClass();
362 List
<AuditCriterion
> criteria
= new ArrayList
<AuditCriterion
>();
363 if(onlyItemsWithLsid
){
364 //criteria.add(AuditEntity.property("lsid_lsid").isNotNull());
365 //TODO this isNotNull criterion did not work with mysql, so using a like statement as interim solution
366 criteria
.add(AuditEntity
.property("lsid_lsid").like("urn:lsid:%"));
368 Pager
<AuditEventRecord
<T
>> results
= service
.pageAuditEvents(clazz
,fromAuditEvent
,untilAuditEvent
,criteria
, pageSize
, (resumptionToken
.getCursor().intValue() / pageSize
) + 1, AuditEventSort
.FORWARDS
,null);
370 if(results
.getCount() == 0) {
371 throw new NoRecordsMatchException("No records match");
374 modelAndView
.addObject("pager",results
);
376 if(results
.getCount() > ((results
.getPageSize() * results
.getCurrentIndex()) + results
.getRecords().size())) {
377 resumptionToken
.updateResults(results
);
378 modelAndView
.addObject("resumptionToken",resumptionToken
);
379 cacheProviderFacade
.putInCache(resumptionToken
.getValue(),cachingModel
, resumptionToken
);
381 resumptionToken
= ResumptionToken
.emptyResumptionToken();
382 modelAndView
.addObject("resumptionToken",resumptionToken
);
383 cacheProviderFacade
.removeFromCache(rToken
,cachingModel
);
388 throw new BadResumptionTokenException();
392 @RequestMapping(method
= RequestMethod
.GET
, params
= {"verb=ListRecords", "!resumptionToken"})
393 public ModelAndView
listRecords(@RequestParam(value
= "from", required
= false) DateTime from
,
394 @RequestParam(value
= "until", required
= false) DateTime until
,
395 @RequestParam(value
= "metadataPrefix", required
= true) MetadataPrefix metadataPrefix
,
396 @RequestParam(value
= "set", required
= false) SetSpec set
) {
398 ModelAndView modelAndView
= new ModelAndView();
399 modelAndView
.addObject("metadataPrefix",metadataPrefix
);
401 switch(metadataPrefix
) {
403 modelAndView
.setViewName("oai/listRecords.rdf");
407 modelAndView
.setViewName("oai/listRecords.dc");
410 AuditEvent fromAuditEvent
= null;
411 if(from
!= null) { // if from is specified, use the event at that date
412 modelAndView
.addObject("from",from
);
413 fromAuditEvent
= auditEventService
.find(from
);
416 AuditEvent untilAuditEvent
= null;
418 modelAndView
.addObject("until",until
);
419 untilAuditEvent
= auditEventService
.find(until
);
424 modelAndView
.addObject("set",set
);
425 clazz
= (Class
)set
.getSetClass();
428 List
<AuditCriterion
> criteria
= new ArrayList
<AuditCriterion
>();
429 if(onlyItemsWithLsid
){
430 //criteria.add(AuditEntity.property("lsid_lsid").isNotNull());
431 //TODO this isNotNull criterion did not work with mysql, so using a like statement as interim solution
432 criteria
.add(AuditEntity
.property("lsid_lsid").like("urn:lsid:%"));
434 Pager
<AuditEventRecord
<T
>> results
= service
.pageAuditEvents(clazz
, fromAuditEvent
, untilAuditEvent
, criteria
, pageSize
, 0, AuditEventSort
.FORWARDS
, getPropertyPaths());
436 if(results
.getCount() == 0) {
437 throw new NoRecordsMatchException("No records match");
440 modelAndView
.addObject("pager",results
);
442 if(results
.getCount() > results
.getRecords().size() && cacheProviderFacade
!= null) {
443 ResumptionToken resumptionToken
= new ResumptionToken(results
, from
, until
, metadataPrefix
, set
);
444 modelAndView
.addObject("resumptionToken",resumptionToken
);
445 cacheProviderFacade
.putInCache(resumptionToken
.getValue(), cachingModel
, resumptionToken
);
451 @RequestMapping(method
= RequestMethod
.GET
, params
= {"verb=ListRecords", "resumptionToken"})
452 public ModelAndView
listRecords(@RequestParam("resumptionToken") String rToken
) {
454 ResumptionToken resumptionToken
;
455 if(cacheProviderFacade
!= null && cacheProviderFacade
.getFromCache(rToken
,cachingModel
) != null) {
456 resumptionToken
= (ResumptionToken
) cacheProviderFacade
.getFromCache(rToken
,cachingModel
);
457 ModelAndView modelAndView
= new ModelAndView();
458 modelAndView
.addObject("metadataPrefix",resumptionToken
.getMetadataPrefix());
460 switch (resumptionToken
.getMetadataPrefix()) {
462 modelAndView
.setViewName("oai/listRecords.rdf");
466 modelAndView
.setViewName("oai/listRecords.dc");
469 AuditEvent fromAuditEvent
= null;
470 if(resumptionToken
.getFrom() != null) { // if from is specified, use the event at that date
471 modelAndView
.addObject("from",resumptionToken
.getFrom());
472 fromAuditEvent
= auditEventService
.find(resumptionToken
.getFrom());
475 AuditEvent untilAuditEvent
= null;
476 if(resumptionToken
.getUntil() != null) {
477 modelAndView
.addObject("until",resumptionToken
.getUntil());
478 untilAuditEvent
= auditEventService
.find(resumptionToken
.getUntil());
482 if(resumptionToken
.getSet() != null) {
483 modelAndView
.addObject("set",resumptionToken
.getSet());
484 clazz
= (Class
)resumptionToken
.getSet().getSetClass();
486 List
<AuditCriterion
> criteria
= new ArrayList
<AuditCriterion
>();
487 if(onlyItemsWithLsid
){
488 //criteria.add(AuditEntity.property("lsid_lsid").isNotNull());
489 //TODO this isNotNull criterion did not work with mysql, so using a like statement as interim solution
490 criteria
.add(AuditEntity
.property("lsid_lsid").like("urn:lsid:%"));
492 Pager
<AuditEventRecord
<T
>> results
= service
.pageAuditEvents(clazz
,fromAuditEvent
,untilAuditEvent
,criteria
, pageSize
, (resumptionToken
.getCursor().intValue() / pageSize
) + 1, AuditEventSort
.FORWARDS
,getPropertyPaths());
494 if(results
.getCount() == 0) {
495 throw new NoRecordsMatchException("No records match");
498 modelAndView
.addObject("pager",results
);
500 if(results
.getCount() > ((results
.getPageSize() * results
.getCurrentIndex()) + results
.getRecords().size())) {
501 resumptionToken
.updateResults(results
);
502 modelAndView
.addObject("resumptionToken",resumptionToken
);
503 cacheProviderFacade
.putInCache(resumptionToken
.getValue(),cachingModel
,resumptionToken
);
505 resumptionToken
= ResumptionToken
.emptyResumptionToken();
506 modelAndView
.addObject("resumptionToken",resumptionToken
);
507 cacheProviderFacade
.removeFromCache(rToken
,cachingModel
);
512 throw new BadResumptionTokenException();
516 private ModelAndView
doException(Exception ex
, HttpServletRequest request
, ErrorCode code
) {
517 ModelAndView modelAndView
= new ModelAndView("oai/exception");
518 modelAndView
.addObject("message", ex
.getMessage());
519 if(request
.getParameter("verb") != null) {
521 modelAndView
.addObject("verb", Verb
.fromValue(request
.getParameter("verb")));
522 } catch(Exception e
) {// prevent endless recursion
526 modelAndView
.addObject("code",code
);
530 @ResponseStatus(HttpStatus
.BAD_REQUEST
)
531 @ExceptionHandler({IllegalArgumentException
.class,TypeMismatchException
.class,MissingServletRequestParameterException
.class})
532 public ModelAndView
handleBadArgument(Exception ex
, HttpServletRequest request
) {
533 return doException(ex
,request
,ErrorCode
.BAD_ARGUMENT
);
536 @ResponseStatus(HttpStatus
.BAD_REQUEST
)
537 @ExceptionHandler(CannotDisseminateFormatException
.class)
538 public ModelAndView
handleCannotDisseminateFormat(Exception ex
, HttpServletRequest request
) {
539 return doException(ex
,request
,ErrorCode
.CANNOT_DISSEMINATE_FORMAT
);
542 @ResponseStatus(HttpStatus
.BAD_REQUEST
)
543 @ExceptionHandler(BadResumptionTokenException
.class)
544 public ModelAndView
handleBadResumptionToken(Exception ex
, HttpServletRequest request
) {
545 return doException(ex
,request
,ErrorCode
.BAD_RESUMPTION_TOKEN
);
548 @ExceptionHandler(NoRecordsMatchException
.class)
549 public ModelAndView
handleNoRecordsMatch(Exception ex
, HttpServletRequest request
) {
550 return doException(ex
,request
,ErrorCode
.NO_RECORDS_MATCH
);
553 @ResponseStatus(HttpStatus
.NOT_FOUND
)
554 @ExceptionHandler(IdDoesNotExistException
.class)
555 public ModelAndView
handleIdDoesNotExist(Exception ex
, HttpServletRequest request
) {
556 return doException(ex
,request
,ErrorCode
.ID_DOES_NOT_EXIST
);