Merge branch 'release/5.44.0'
[cdmlib.git] / cdmlib-remote / src / main / java / eu / etaxonomy / cdm / remote / controller / BaseController.java
1 /**
2 * Copyright (C) 2007 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;
10
11 import java.beans.PropertyDescriptor;
12 import java.io.IOException;
13 import java.lang.reflect.InvocationTargetException;
14 import java.lang.reflect.Method;
15 import java.lang.reflect.ParameterizedType;
16 import java.lang.reflect.Type;
17 import java.util.ArrayList;
18 import java.util.Arrays;
19 import java.util.Collection;
20 import java.util.List;
21 import java.util.Set;
22 import java.util.UUID;
23
24 import javax.servlet.http.HttpServletRequest;
25 import javax.servlet.http.HttpServletResponse;
26
27 import org.apache.commons.beanutils.PropertyUtils;
28 import org.apache.commons.io.FilenameUtils;
29 import org.apache.commons.lang3.StringUtils;
30 import org.apache.logging.log4j.LogManager;
31 import org.apache.logging.log4j.Logger;
32 import org.hibernate.mapping.Map;
33 import org.springframework.web.bind.WebDataBinder;
34 import org.springframework.web.bind.annotation.InitBinder;
35 import org.springframework.web.bind.annotation.PathVariable;
36 import org.springframework.web.bind.annotation.RequestMapping;
37 import org.springframework.web.bind.annotation.RequestMethod;
38 import org.springframework.web.bind.annotation.RequestParam;
39
40 import eu.etaxonomy.cdm.api.service.IClassificationService;
41 import eu.etaxonomy.cdm.api.service.IService;
42 import eu.etaxonomy.cdm.api.service.ITaxonNodeService;
43 import eu.etaxonomy.cdm.api.service.pager.Pager;
44 import eu.etaxonomy.cdm.hibernate.HibernateProxyHelper;
45 import eu.etaxonomy.cdm.model.common.CdmBase;
46 import eu.etaxonomy.cdm.model.common.IPublishable;
47 import eu.etaxonomy.cdm.model.reference.INomenclaturalReference;
48 import eu.etaxonomy.cdm.model.taxon.Classification;
49 import eu.etaxonomy.cdm.model.taxon.TaxonNode;
50 import eu.etaxonomy.cdm.remote.editor.UUIDPropertyEditor;
51
52 /**
53 * based on org.cateproject.controller.common
54 * @author b.clark
55 * @author a.kohlbecker
56 */
57 public abstract class BaseController<T extends CdmBase, SERVICE extends IService<T>>
58 extends AbstractController<T, SERVICE> {
59
60 private static final Logger logger = LogManager.getLogger();
61
62 protected Class<T> baseClass;
63
64 @SuppressWarnings("unchecked")
65 public BaseController (){
66
67 //define base class //TODO can't we do this more straight forward e.g.
68 //by an abstract method returning the class?
69 Type superClass = this.getClass().getGenericSuperclass();
70 while(true){
71 if(superClass instanceof ParameterizedType){
72 ParameterizedType parametrizedSuperClass = (ParameterizedType) superClass;
73 Type[] typeArguments = parametrizedSuperClass.getActualTypeArguments();
74
75 if(typeArguments.length > 1 && typeArguments[0] instanceof Class<?>){
76 baseClass = (Class<T>) typeArguments[0];
77 } else {
78 logger.error("unable to find baseClass");
79 }
80 break;
81 } else if(superClass instanceof Class<?>){
82 superClass = ((Class<?>) superClass).getGenericSuperclass();
83 } else {
84 // no point digging deeper if neither Class or ParameterizedType
85 logger.error("unable to find baseClass");
86 break;
87 }
88 }
89 }
90
91 @InitBinder
92 public void initBinder(WebDataBinder binder) {
93 binder.registerCustomEditor(UUID.class, new UUIDPropertyEditor());
94 }
95
96 //TODO implement bulk version of this method
97 @RequestMapping(method = RequestMethod.GET)
98 public T doGet(@PathVariable("uuid") UUID uuid,
99 HttpServletRequest request,
100 HttpServletResponse response) throws IOException {
101 if(request != null) {
102 logger.info("doGet() " + request.getRequestURI());
103 }
104 T obj = getCdmBaseInstance(uuid, response, initializationStrategy);
105 if (obj instanceof IPublishable){
106 obj = (T)checkExistsAndAccess((IPublishable)obj, NO_UNPUBLISHED, response);
107 }
108 return obj;
109 }
110
111 /**
112 * @param uuid
113 * @param request
114 * @param response
115 * @return
116 * @throws IOException
117 *
118 * TODO implement bulk version of this method
119 */
120 @RequestMapping(value = "*", method = RequestMethod.GET)
121 public Object doGetMethod(
122 @PathVariable("uuid") UUID uuid,
123 // doPage request parameters
124 @RequestParam(value = "pageIndex", required = false) Integer pageIndex,
125 @RequestParam(value = "pageSize", required = false) Integer pageSize,
126 // doList request parameters
127 @RequestParam(value = "start", required = false) Integer start,
128 @RequestParam(value = "limit", required = false) Integer limit,
129 HttpServletRequest request,
130 HttpServletResponse response) throws IOException {
131
132 String servletPath = request.getServletPath();
133 String propertyName = FilenameUtils.getBaseName(servletPath);
134
135 logger.info("doGetMethod()[doGet" + StringUtils.capitalize(propertyName) + "] " + requestPathAndQuery(request));
136
137 // <CUT
138 // T instance = getCdmBaseInstance(uuid, response, (List<String>)null);
139 //Class<?> propertyClass = propertyClass(instance, baseName);
140 Object objectFromProperty = getCdmBaseProperty(uuid, propertyName, response);// invokeProperty(instance, baseName, response);
141 // CUT>
142 if(objectFromProperty != null){
143 if( Collection.class.isAssignableFrom(objectFromProperty.getClass())){
144 // Map types cannot be returned as list or in a pager!
145 return pageFromCollection((Collection<CdmBase>)objectFromProperty, pageIndex, pageSize, start, limit, response);
146 } else {
147 return objectFromProperty;
148 }
149 }
150 return null;
151 }
152
153 /**
154 * Returns a sub-collection of <code>c</code>. A pager object will be returned if the <code>pageNumber</code> and
155 * <code>pageSize</code> are given. Otherwise a <code>List</code> in case of <code>start</code> and <code>limit</code>.
156 *
157 * @param pageNumber
158 * @param pageSize
159 * @param start
160 * @param limit
161 * @param response
162 * @param objectFromProperty
163 * @return either a List or Pager depending on the parameter combination.
164 *
165 * @throws IOException
166 */
167 protected Object pageFromCollection(Collection<? extends CdmBase> c, Integer pageNumber, Integer pageSize, Integer start,
168 Integer limit, HttpServletResponse response) throws IOException {
169
170 if(c instanceof Set){
171 // sets need to be sorted to have a defined order
172 List<CdmBase> list = new ArrayList<>(c);
173 java.util.Collections.sort(list, (o1, o2)-> {
174 if (o1 == null && o2 == null){
175 return 0;
176 }else if (o1 == null){
177 return -1;
178 }else if (o2 == null){
179 return 1;
180 }
181 return Integer.compare(o1.getId(), o2.getId());
182 }
183 );
184 c = list;
185 }
186
187 if(start != null){
188 // return list
189 limit = (limit == null ? DEFAULT_PAGE_SIZE : limit);
190 Collection<CdmBase> sub_c = subCollection(c, start, limit);
191 return sub_c;
192 } else {
193 //FIXME use real paging mechanism of according service class instead of subCollection()
194 //FIXME use BaseListController.normalizeAndValidatePagerParameters(pageNumber, pageSize, response);
195 Pager<? extends CdmBase> p = pagerForSubCollectionOf(c, pageNumber, pageSize, response);
196 return p;
197 }
198 }
199
200 public Object getCdmBaseProperty(UUID uuid, String property, HttpServletResponse response) throws IOException{
201
202 T instance = HibernateProxyHelper.deproxy(getCdmBaseInstance(uuid, response, property));
203
204 Object objectFromProperty = invokeProperty(instance, property, response);
205
206 return objectFromProperty;
207 }
208
209 @SuppressWarnings("unchecked")
210 protected final <SUB_T extends T> SUB_T getCdmBaseInstance(Class<SUB_T> clazz,
211 UUID uuid, HttpServletResponse response, List<String> pathProperties)
212 throws IOException {
213
214 CdmBase cdmBaseObject = getCdmBaseInstance(uuid, response, pathProperties);
215 if(!clazz.isAssignableFrom(cdmBaseObject.getClass())){
216 HttpStatusMessage.UUID_REFERENCES_WRONG_TYPE.send(response);
217 }
218 return (SUB_T) cdmBaseObject;
219 }
220
221 @SuppressWarnings("unchecked")
222 protected final <SUB_T extends T> SUB_T getCdmBaseInstance(Class<SUB_T> clazz, UUID uuid, HttpServletResponse response, String pathProperty)
223 throws IOException {
224
225 CdmBase cdmBaseObject = getCdmBaseInstance(uuid, response, pathProperty);
226 if(!clazz.isAssignableFrom(cdmBaseObject.getClass())){
227 HttpStatusMessage.UUID_REFERENCES_WRONG_TYPE.send(response);
228 }
229 return (SUB_T) cdmBaseObject;
230 }
231
232 protected final T getCdmBaseInstance(UUID uuid, HttpServletResponse response, String pathProperty)
233 throws IOException {
234 return getCdmBaseInstance(baseClass, uuid, response, Arrays
235 .asList(new String[] { pathProperty }));
236 }
237
238 protected final T getCdmBaseInstance(UUID uuid, HttpServletResponse response, List<String> pathProperties)
239 throws IOException {
240 return getCdmBaseInstance(baseClass, service, uuid, response, pathProperties);
241 }
242
243 protected final <CDM_BASE extends CdmBase> CDM_BASE getCdmBaseInstance(
244 Class<CDM_BASE> clazz, IService<CDM_BASE> service, UUID uuid,
245 HttpServletResponse response, List<String> pathProperties)
246 throws IOException {
247
248 @SuppressWarnings("unused")
249 boolean includeUnpublished = NO_UNPUBLISHED;
250 CDM_BASE cdmBaseObject;
251 // if (service instanceof IPublishableService){
252 // cdmBaseObject = ((IPublishableService<CDM_BASE>)service).load(uuid, includeUnpublished, pathProperties);
253 // }else{
254 pathProperties = complementInitStrategy(clazz, pathProperties);
255 cdmBaseObject = service.load(uuid, pathProperties);
256 // }
257 if (cdmBaseObject == null) {
258 HttpStatusMessage.UUID_NOT_FOUND.send(response);
259 }
260 return cdmBaseObject;
261 }
262
263 /**
264 * Implementations of the BaseController can override this method to
265 * extend the <code>pathProperties</code> to for example avoid
266 * <code>LazyInitializationExceptions</code> which can happen when
267 * {@link #doGetMethod(UUID, Integer, Integer, Integer, Integer, HttpServletRequest, HttpServletResponse)} is being used.
268 *
269 * @param clazz
270 * @param pathProperties
271 */
272 protected <CDM_BASE extends CdmBase> List<String> complementInitStrategy(@SuppressWarnings("unused") Class<CDM_BASE> clazz, List<String> pathProperties) {
273 return pathProperties;
274 }
275
276 private final Object invokeProperty(T instance,
277 String baseName, HttpServletResponse response) throws IOException {
278
279 Object result = null;
280 try {
281 PropertyDescriptor propertyDescriptor = PropertyUtils.getPropertyDescriptor(instance, baseName);
282 if(propertyDescriptor == null){
283 throw new NoSuchMethodException("No such method: " + instance.getClass().getSimpleName() + ".get" + baseName);
284 }
285 Method method = propertyDescriptor.getReadMethod();
286
287 Class<?> returnType = method.getReturnType();
288
289 if(CdmBase.class.isAssignableFrom(returnType)
290 || Collection.class.isAssignableFrom(returnType)
291 || Map.class.isAssignableFrom(returnType)
292 || INomenclaturalReference.class.isAssignableFrom(returnType)){
293
294 result = method.invoke(instance, (Object[])null);
295
296 result = HibernateProxyHelper.deproxy(result);
297
298 }else{
299 HttpStatusMessage.UUID_REFERENCES_WRONG_TYPE.send(response);
300 }
301 } catch (SecurityException e) {
302 logger.error("SecurityException: ", e);
303 HttpStatusMessage.INTERNAL_ERROR.send(response);
304 } catch (NoSuchMethodException e) {
305 HttpStatusMessage.PROPERTY_NOT_FOUND.send(response);
306 } catch (IllegalArgumentException e) {
307 HttpStatusMessage.PROPERTY_NOT_FOUND.send(response);
308 } catch (IllegalAccessException e) {
309 HttpStatusMessage.PROPERTY_NOT_FOUND.send(response);
310 } catch (InvocationTargetException e) {
311 HttpStatusMessage.PROPERTY_NOT_FOUND.send(response);
312 }
313 return result;
314 }
315
316 /**
317 * Checks if an {@link IPublishable} was found and if it is publish.
318 * If not the according {@link HttpStatusMessage http messages} are added to response.
319 * @param publishable
320 * @param includeUnpublished
321 * @param response
322 * @return
323 * @throws IOException
324 */
325 protected <S extends IPublishable> S checkExistsAndAccess(S publishable, boolean includeUnpublished,
326 HttpServletResponse response) throws IOException {
327 if (publishable == null){
328 HttpStatusMessage.UUID_NOT_FOUND.send(response);
329 }else if (!includeUnpublished && !publishable.isPublish()){
330 HttpStatusMessage.ACCESS_DENIED.send(response);
331 publishable = null;
332 }
333 return publishable;
334 }
335
336 protected <S extends IPublishable> S checkExistsAccessType(IPublishable publishable, boolean includeUnpublished,
337 Class<S> clazz, HttpServletResponse response) throws IOException {
338 IPublishable result = this.checkExistsAndAccess(publishable, includeUnpublished, response);
339 if (clazz != null && !clazz.isAssignableFrom(result.getClass())){
340 HttpStatusMessage.UUID_REFERENCES_WRONG_TYPE.send(response);
341 result = null;
342 }
343 return (S)result;
344 }
345
346 protected TaxonNode getSubtreeOrError(UUID subtreeUuid, ITaxonNodeService taxonNodeService, HttpServletResponse response) throws IOException {
347 TaxonNode subtree = null;
348 if (subtreeUuid != null){
349 subtree = taxonNodeService.find(subtreeUuid);
350 if(subtree == null) {
351 response.sendError(404 , "TaxonNode not found using " + subtreeUuid );
352 //will not happen
353 return null;
354 }
355 }
356 return subtree;
357 }
358
359 protected Classification getClassificationOrError(UUID classificationUuid,
360 IClassificationService classificationService, HttpServletResponse response) throws IOException {
361 Classification classification = null;
362 if (classificationUuid != null){
363 classification = classificationService.find(classificationUuid);
364 if(classification == null) {
365 response.sendError(404 , "Classification not found: " + classificationUuid );
366 //will not happen
367 return null;
368 }
369 }
370 return classification;
371 }
372
373 /* TODO implement
374
375 private Validator validator;
376
377 private javax.validation.Validator javaxValidator;
378
379 @RequestMapping(method = RequestMethod.PUT, headers="content-type=multipart/form-data")
380 public T doPutForm(@PathVariable(value = "uuid") UUID uuid, @ModelAttribute("object") T object, BindingResult result) {
381 object.setUuid(uuid);
382 validator.validate(object, result);
383 if (result.hasErrors()) {
384 throw new Error();
385 // set http status code depending upon what happened, possibly return
386 // the put object and errors so that they can be rendered into a suitable error response
387 } else {
388 // requires merging detached object ?gilead?
389 service.save(object);
390 }
391
392 return object;
393 }
394
395 @RequestMapping(method = RequestMethod.PUT, headers="content-type=text/json")
396 public T doPutJSON(@PathVariable(value = "uuid") UUID uuid, @RequestBody String jsonMessage) {
397 JSONObject jsonObject = JSONObject.fromObject(jsonMessage);
398 T object = (T)JSONObject.toBean(jsonObject, this.getClass());
399
400
401 Set<ConstraintViolation<T>> constraintViolations = javaxValidator.validate(object);
402 if (!constraintViolations.isEmpty()) {
403 throw new Error();
404 // set http status code depending upon what happened, possibly return
405 // the put object and errors so that they can be rendered into a suitable error response
406 } else {
407 // requires merging detached object ?gilead?
408 service.save(object);
409 }
410
411 return object;
412 }
413
414 @RequestMapping(method = RequestMethod.PUT) // the cdm-server may not allow clients to specify the uuid for resources
415 public T doPut(@PathVariable(value = "uuid") UUID uuid, @ModelAttribute("object") T object, BindingResult result) {
416 validator.validate(object, result);
417 if (result.hasErrors()) {
418 // set http status code depending upon what happened, possibly return
419 // the put object and errors so that they can be rendered into a suitable error response
420 } else {
421 service.save(object);
422 }
423 }
424
425 @RequestMapping(method = RequestMethod.DELETE)
426 public void doDelete(@PathVariable(value = "uuid") UUID uuid) {
427 T object = service.find(uuid);
428 // provided the object exists
429 service.delete(uuid);
430 // might return 204 or 200
431 }
432 }
433 */
434 }