a little bit documentation
[cdmlib.git] / cdmlib-remote / src / main / java / eu / etaxonomy / cdm / remote / controller / oaipmh / AbstractOaiPmhController.java
1 package eu.etaxonomy.cdm.remote.controller.oaipmh;
2
3 import java.net.URI;
4 import java.util.ArrayList;
5 import java.util.HashSet;
6 import java.util.List;
7 import java.util.UUID;
8
9 import javax.servlet.http.HttpServletRequest;
10
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;
28
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;
53
54 public abstract class AbstractOaiPmhController<T extends IdentifiableEntity, SERVICE extends IIdentifiableEntityService<T>> {
55
56 protected SERVICE service;
57
58 protected IAuditEventService auditEventService;
59
60 private String repositoryName;
61
62 private String baseURL;
63
64 private String protocolVersion;
65
66 private String adminEmail;
67
68 private String description;
69
70 private Integer pageSize;
71
72 public abstract void setService(SERVICE service);
73
74 private CacheProviderFacade cacheProviderFacade;
75
76 private CachingModel cachingModel;
77
78 private boolean onlyItemsWithLsid = false;
79
80 public boolean isRestrictToLsid() {
81 return onlyItemsWithLsid;
82 }
83
84 public void setRestrictToLsid(boolean restrictToLsid) {
85 this.onlyItemsWithLsid = restrictToLsid;
86 }
87
88 /**
89 * sets cache name to be used
90 */
91 @Autowired
92 public void setCacheProviderFacade(CacheProviderFacade cacheProviderFacade) {
93 this.cacheProviderFacade = cacheProviderFacade;
94 }
95
96 @Autowired
97 public void setCachingModel(CachingModel cachingModel) {
98 this.cachingModel = cachingModel;
99 }
100
101 /**
102 * Subclasses should override this method to return a list of property
103 * paths that should be initialized for the getRecord, listRecords methods
104 * @return
105 */
106 protected List<String> getPropertyPaths() {
107 return new ArrayList<String>();
108 }
109
110 /**
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
115 */
116 protected void addSets(ModelAndView modelAndView) {
117 modelAndView.addObject("sets",new HashSet<SetSpec>());
118 }
119
120 @Autowired
121 public void setAuditEventService(IAuditEventService auditEventService) {
122 this.auditEventService = auditEventService;
123 }
124
125 public void setRepositoryName(String repositoryName) {
126 this.repositoryName = repositoryName;
127 }
128
129 public void setBaseURL(String baseURL) {
130 this.baseURL = baseURL;
131 }
132
133 public void setProtocolVersion(String protocolVersion) {
134 this.protocolVersion = protocolVersion;
135 }
136
137 public void setAdminEmail(String adminEmail) {
138 this.adminEmail = adminEmail;
139 }
140
141 public void setDescription(String description) {
142 this.description = description;
143 }
144
145 public void setPageSize(Integer pageSize) {
146 this.pageSize = pageSize;
147 }
148
149 @InitBinder
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());
156 }
157
158
159 /**
160 * CannotDisseminateFormatException thrown by MetadataPrefixEditor
161 *
162 * @throws IdDoesNotExistException
163 */
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 {
169
170 ModelAndView modelAndView = new ModelAndView();
171 modelAndView.addObject("metadataPrefix", metadataPrefix);
172
173 finishModelAndView(identifier, metadataPrefix, modelAndView);
174
175 return modelAndView;
176 }
177
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 {
183
184 ModelAndView modelAndView = new ModelAndView();
185 modelAndView.addObject("metadataPrefix", metadataPrefix);
186
187 System.err.println("############");
188
189 return modelAndView;
190 }
191
192 /**
193 * @param identifier
194 * @param metadataPrefix
195 * @param modelAndView
196 * @throws IdDoesNotExistException
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 /**
215 * @param identifier
216 * @return
217 * @throws IdDoesNotExistException
218 */
219 protected AuditEventRecord<T> obtainCdmEntity(LSID identifier)
220 throws IdDoesNotExistException {
221 T object = service.find(identifier);
222 if(object == null){
223 throw new IdDoesNotExistException(identifier);
224 }
225
226 Pager<AuditEventRecord<T>> results = service.pageAuditEvents(object, 1,
227 0, AuditEventSort.BACKWARDS, getPropertyPaths());
228
229 if (results.getCount() == 0) {
230 throw new IdDoesNotExistException(identifier);
231 }
232 return results.getRecords().get(0);
233 }
234
235
236 /**
237 * CannotDisseminateFormatException thrown by MetadataPrefixEditor
238 * @throws IdDoesNotExistException
239 */
240 @RequestMapping(method = RequestMethod.GET,params = "verb=ListMetadataFormats")
241 public ModelAndView listMetadataFormats(@RequestParam(value = "identifier", required = false) LSID identifier) throws IdDoesNotExistException {
242
243 ModelAndView modelAndView = new ModelAndView("oai/listMetadataFormats");
244
245 if(identifier != null) {
246 T object = service.find(identifier);
247 if(object == null) {
248 throw new IdDoesNotExistException(identifier);
249 }
250 }
251
252 return modelAndView;
253 }
254
255 /**
256 * CannotDisseminateFormatException thrown by MetadataPrefixEditor
257 */
258 @RequestMapping(method = RequestMethod.GET,params = "verb=ListSets")
259 public ModelAndView listSets() {
260
261 ModelAndView modelAndView = new ModelAndView("oai/listSets");
262
263 addSets(modelAndView);
264
265 return modelAndView;
266 }
267
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);
276
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);
281
282 return modelAndView;
283 }
284
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) {
291
292 ModelAndView modelAndView = new ModelAndView("oai/listIdentifiers");
293 modelAndView.addObject("metadataPrefix",metadataPrefix);
294
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);
299 }
300
301 AuditEvent untilAuditEvent = null;
302 if(until != null) {
303 modelAndView.addObject("until",until);
304 untilAuditEvent = auditEventService.find(until);
305 }
306
307 Class clazz = null;
308 if(set != null) {
309 modelAndView.addObject("set",set);
310 clazz = (Class)set.getSetClass();
311 }
312
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:%"));
318 }
319 Pager<AuditEventRecord<T>> results = service.pageAuditEvents(clazz, fromAuditEvent, untilAuditEvent, criteria, pageSize, 0, AuditEventSort.FORWARDS, null);
320
321 if(results.getCount() == 0) {
322 throw new NoRecordsMatchException("No records match");
323 }
324
325 modelAndView.addObject("pager",results);
326
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);
331 }
332
333 return modelAndView;
334 }
335
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());
343
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());
348 }
349
350 AuditEvent untilAuditEvent = null;
351 if(resumptionToken.getUntil() != null) {
352 modelAndView.addObject("until",resumptionToken.getUntil());
353 untilAuditEvent = auditEventService.find(resumptionToken.getUntil());
354 }
355
356 Class clazz = null;
357 if(resumptionToken.getSet() != null) {
358 modelAndView.addObject("set",resumptionToken.getSet());
359 clazz = (Class)resumptionToken.getSet().getSetClass();
360 }
361
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:%"));
367 }
368 Pager<AuditEventRecord<T>> results = service.pageAuditEvents(clazz,fromAuditEvent,untilAuditEvent,criteria, pageSize, (resumptionToken.getCursor().intValue() / pageSize) + 1, AuditEventSort.FORWARDS,null);
369
370 if(results.getCount() == 0) {
371 throw new NoRecordsMatchException("No records match");
372 }
373
374 modelAndView.addObject("pager",results);
375
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);
380 } else {
381 resumptionToken = ResumptionToken.emptyResumptionToken();
382 modelAndView.addObject("resumptionToken",resumptionToken);
383 cacheProviderFacade.removeFromCache(rToken,cachingModel);
384 }
385
386 return modelAndView;
387 } else {
388 throw new BadResumptionTokenException();
389 }
390 }
391
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) {
397
398 ModelAndView modelAndView = new ModelAndView();
399 modelAndView.addObject("metadataPrefix",metadataPrefix);
400
401 switch(metadataPrefix) {
402 case RDF:
403 modelAndView.setViewName("oai/listRecords.rdf");
404 break;
405 case OAI_DC:
406 default:
407 modelAndView.setViewName("oai/listRecords.dc");
408 }
409
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);
414 }
415
416 AuditEvent untilAuditEvent = null;
417 if(until != null) {
418 modelAndView.addObject("until",until);
419 untilAuditEvent = auditEventService.find(until);
420 }
421
422 Class clazz = null;
423 if(set != null) {
424 modelAndView.addObject("set",set);
425 clazz = (Class)set.getSetClass();
426 }
427
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:%"));
433 }
434 Pager<AuditEventRecord<T>> results = service.pageAuditEvents(clazz, fromAuditEvent, untilAuditEvent, criteria, pageSize, 0, AuditEventSort.FORWARDS, getPropertyPaths());
435
436 if(results.getCount() == 0) {
437 throw new NoRecordsMatchException("No records match");
438 }
439
440 modelAndView.addObject("pager",results);
441
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);
446 }
447
448 return modelAndView;
449 }
450
451 @RequestMapping(method = RequestMethod.GET, params = {"verb=ListRecords", "resumptionToken"})
452 public ModelAndView listRecords(@RequestParam("resumptionToken") String rToken) {
453
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());
459
460 switch (resumptionToken.getMetadataPrefix()) {
461 case RDF:
462 modelAndView.setViewName("oai/listRecords.rdf");
463 break;
464 case OAI_DC:
465 default:
466 modelAndView.setViewName("oai/listRecords.dc");
467 }
468
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());
473 }
474
475 AuditEvent untilAuditEvent = null;
476 if(resumptionToken.getUntil() != null) {
477 modelAndView.addObject("until",resumptionToken.getUntil());
478 untilAuditEvent = auditEventService.find(resumptionToken.getUntil());
479 }
480
481 Class clazz = null;
482 if(resumptionToken.getSet() != null) {
483 modelAndView.addObject("set",resumptionToken.getSet());
484 clazz = (Class)resumptionToken.getSet().getSetClass();
485 }
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:%"));
491 }
492 Pager<AuditEventRecord<T>> results = service.pageAuditEvents(clazz,fromAuditEvent,untilAuditEvent,criteria, pageSize, (resumptionToken.getCursor().intValue() / pageSize) + 1, AuditEventSort.FORWARDS,getPropertyPaths());
493
494 if(results.getCount() == 0) {
495 throw new NoRecordsMatchException("No records match");
496 }
497
498 modelAndView.addObject("pager",results);
499
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);
504 } else {
505 resumptionToken = ResumptionToken.emptyResumptionToken();
506 modelAndView.addObject("resumptionToken",resumptionToken);
507 cacheProviderFacade.removeFromCache(rToken,cachingModel);
508 }
509
510 return modelAndView;
511 } else {
512 throw new BadResumptionTokenException();
513 }
514 }
515
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) {
520 try {
521 modelAndView.addObject("verb", Verb.fromValue(request.getParameter("verb")));
522 } catch(Exception e) {// prevent endless recursion
523
524 }
525 }
526 modelAndView.addObject("code",code);
527 return modelAndView;
528 }
529
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);
534 }
535
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);
540 }
541
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);
546 }
547
548 @ExceptionHandler(NoRecordsMatchException.class)
549 public ModelAndView handleNoRecordsMatch(Exception ex, HttpServletRequest request) {
550 return doException(ex,request,ErrorCode.NO_RECORDS_MATCH);
551 }
552
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);
557 }
558
559
560 }