- merge update from trunk
[cdmlib.git] / cdmlib-remote / src / main / java / eu / etaxonomy / cdm / remote / controller / BaseController.java
index 6b979b91b72776de1886d2949a2d6eb6af81f034..b43e7ee0e2a0a89ad278dd7633f5af04a1f2ac20 100644 (file)
@@ -1,34 +1,52 @@
 // $Id$\r
 /**\r
 * Copyright (C) 2007 EDIT\r
-* European Distributed Institute of Taxonomy \r
+* European Distributed Institute of Taxonomy\r
 * http://www.e-taxonomy.eu\r
-* \r
+*\r
 * The contents of this file are subject to the Mozilla Public License Version 1.1\r
 * See LICENSE.TXT at the top of this package for the full license terms.\r
 */\r
 \r
 package eu.etaxonomy.cdm.remote.controller;\r
 \r
+import java.beans.PropertyDescriptor;\r
 import java.io.IOException;\r
+import java.lang.reflect.InvocationTargetException;\r
+import java.lang.reflect.Method;\r
+import java.lang.reflect.ParameterizedType;\r
+import java.lang.reflect.Type;\r
+import java.util.ArrayList;\r
 import java.util.Arrays;\r
+import java.util.Collection;\r
 import java.util.List;\r
 import java.util.UUID;\r
-import java.util.regex.Matcher;\r
-import java.util.regex.Pattern;\r
 \r
 import javax.servlet.http.HttpServletRequest;\r
 import javax.servlet.http.HttpServletResponse;\r
 \r
-import org.apache.log4j.Logger;\r
-import org.springframework.util.Assert;\r
+import org.apache.commons.beanutils.PropertyUtils;\r
+import org.apache.commons.io.FilenameUtils;\r
+import org.apache.commons.lang.StringUtils;\r
+import org.hibernate.mapping.Map;\r
+import org.springframework.web.bind.WebDataBinder;\r
+import org.springframework.web.bind.annotation.InitBinder;\r
+import org.springframework.web.bind.annotation.PathVariable;\r
 import org.springframework.web.bind.annotation.RequestMapping;\r
 import org.springframework.web.bind.annotation.RequestMethod;\r
+import org.springframework.web.bind.annotation.RequestParam;\r
+import org.springframework.web.servlet.ModelAndView;\r
 \r
 import eu.etaxonomy.cdm.api.service.IService;\r
+import eu.etaxonomy.cdm.api.service.pager.Pager;\r
+import eu.etaxonomy.cdm.api.service.pager.impl.DefaultPagerImpl;\r
+import eu.etaxonomy.cdm.hibernate.HibernateProxyHelper;\r
 import eu.etaxonomy.cdm.model.common.CdmBase;\r
+import eu.etaxonomy.cdm.model.reference.INomenclaturalReference;\r
+import eu.etaxonomy.cdm.remote.controller.util.PagerParameters;\r
+import eu.etaxonomy.cdm.remote.editor.UUIDPropertyEditor;\r
+import eu.etaxonomy.cdm.remote.exception.NoRecordsMatchException;\r
 \r
-//$Id$\r
 /**\r
  * based on org.cateproject.controller.common\r
  * @author b.clark\r
@@ -38,129 +56,363 @@ import eu.etaxonomy.cdm.model.common.CdmBase;
  * @param <SERVICE>\r
  */\r
 \r
-public abstract class BaseController<T extends CdmBase, SERVICE extends IService<T>> {\r
-       \r
-       public static final Logger logger = Logger.getLogger(BaseController.class);\r
-       \r
-       protected static final Integer DEFAULT_PAGE_SIZE = 30;\r
-       \r
-       protected SERVICE service;\r
-       \r
-       protected Pattern uuidParameterPattern = null;\r
-       \r
-       protected List<String> initializationStrategy = null;\r
-\r
-       public abstract void setService(SERVICE service);\r
-       \r
-       protected void setUuidParameterPattern(String pattern){\r
-               uuidParameterPattern = Pattern.compile(pattern);\r
-       }\r
-       \r
-       public void setInitializationStrategy(List<String> initializationStrategy) {\r
-               this.initializationStrategy = initializationStrategy;\r
-       }\r
-\r
-       /**@InitBinder\r
+public abstract class BaseController<T extends CdmBase, SERVICE extends IService<T>> extends AbstractController<T, SERVICE> {\r
+\r
+/*     protected SERVICE service;\r
+\r
+    public abstract void setService(SERVICE service);*/\r
+\r
+    protected Class<T> baseClass;\r
+\r
+    public BaseController (){\r
+\r
+        Type superClass = this.getClass().getGenericSuperclass();\r
+        if(superClass instanceof ParameterizedType){\r
+            ParameterizedType parametrizedSuperClass = (ParameterizedType) superClass;\r
+            Type[] typeArguments = parametrizedSuperClass.getActualTypeArguments();\r
+\r
+            if(typeArguments.length > 1 && typeArguments[0] instanceof Class<?>){\r
+                baseClass = (Class<T>) typeArguments[0];\r
+            } else {\r
+                logger.error("unable to find baseClass");\r
+            }\r
+        }\r
+    }\r
+\r
+    @InitBinder\r
     public void initBinder(WebDataBinder binder) {\r
-               binder.registerCustomEditor(UUID.class, new UUIDPropertyEditor());\r
-               //TODO do we need this one?: binder.registerCustomEditor(Class.class, new ClassPropertyEditor());\r
-       }\r
-       */\r
-       \r
-       @RequestMapping(method = RequestMethod.GET)\r
-       public T doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {\r
-               T obj = (T) getCdmBase(request, response, initializationStrategy, CdmBase.class);\r
-               return obj;\r
-       }\r
-\r
-       \r
-       protected UUID readValueUuid(HttpServletRequest request) {\r
-               String path = request.getServletPath();\r
-               if(path != null) {\r
-                       Matcher uuidMatcher = uuidParameterPattern.matcher(path);\r
-                       if(uuidMatcher.matches() && uuidMatcher.groupCount() > 0){\r
-                               try {\r
-                                       UUID uuid = UUID.fromString(uuidMatcher.group(1));\r
-                                       return uuid;\r
-                               } catch (Exception e) {\r
-                                       throw new IllegalArgumentException(HttpStatusMessage.UUID_INVALID.toString());\r
-                               }\r
-                       } else {\r
-                               throw new IllegalArgumentException(HttpStatusMessage.UUID_MISSING.toString());\r
-                       }\r
-               }\r
-               return null;\r
-       }\r
-       \r
-\r
-       /**\r
-        * @param request\r
-        * @param response\r
-        * @param obj\r
-        * @return\r
-        * @throws IOException\r
-        */\r
-       protected <CDM_BASE> CDM_BASE  getCdmBase(HttpServletRequest request, HttpServletResponse response, \r
-                       List<String> initStrategy, Class<CDM_BASE> clazz) throws IOException {\r
-               T obj = null;\r
-               try {\r
-                       UUID uuid = readValueUuid(request);\r
-                       Assert.notNull(uuid, HttpStatusMessage.UUID_MISSING.toString());\r
-                       \r
-                       if(initStrategy != null){\r
-                               obj = service.load(uuid, initStrategy);\r
-                       } else {                                \r
-                               obj = service.findByUuid(uuid);\r
-                       }\r
-                       Assert.notNull(obj, HttpStatusMessage.UUID_NOT_FOUND.toString());\r
-                       \r
-               } catch (IllegalArgumentException iae) {\r
-                       HttpStatusMessage.fromString(iae.getMessage()).send(response);\r
-               }\r
-               CDM_BASE t;\r
-               try {\r
-                       t = (CDM_BASE)obj;\r
-                       return t;\r
-               } catch (Exception e) {\r
-                       HttpStatusMessage.UUID_REFERENCES_WRONG_TYPE.send(response);\r
-                       return null;\r
-               }\r
-       }\r
-\r
-         /* TODO implement\r
-          * \r
-         @RequestMapping(method = RequestMethod.POST)\r
-         public T doPost(@PathVariable(value = "uuid") UUID uuid, @ModelAttribute("object") T object, BindingResult result) {\r
-               validator.validate(object, result);\r
-               if (result.hasErrors()) {\r
-                       // set http status code depending upon what happened, possibly return\r
-                   // the put object and errors so that they can be rendered into a suitable error response\r
-               } else {\r
-                 // requires merging detached object ?gilead?\r
-                 service.update(object);\r
-               }\r
-         }\r
-\r
-         @RequestMapping(method = RequestMethod.PUT) // the cdm-server may not allow clients to specify the uuid for resources\r
-         public T doPut(@PathVariable(value = "uuid") UUID uuid, @ModelAttribute("object") T object, BindingResult result) {\r
-               validator.validate(object, result);\r
-               if (result.hasErrors()) {\r
-                       // set http status code depending upon what happened, possibly return\r
-                   // the put object and errors so that they can be rendered into a suitable error response\r
-               } else {\r
-                 service.save(object);\r
-               }\r
-         }\r
-\r
-          @RequestMapping(method = RequestMethod.DELETE)\r
-          public void doDelete(@PathVariable(value = "uuid") UUID uuid) {\r
-              T object = service.find(uuid);\r
-              // provided the object exists\r
-              service.delete(uuid);\r
-              // might return 204 or 200\r
-          }\r
-       }\r
+        binder.registerCustomEditor(UUID.class, new UUIDPropertyEditor());\r
+    }\r
+\r
+    //TODO implement bulk version of this method\r
+    @RequestMapping(method = RequestMethod.GET)\r
+    public T doGet(@PathVariable("uuid") UUID uuid,\r
+                HttpServletRequest request,\r
+                HttpServletResponse response) throws IOException {\r
+        if(request != null) {\r
+            logger.info("doGet() " + request.getRequestURI());\r
+        }\r
+        T obj = getCdmBaseInstance(uuid, response, initializationStrategy);\r
+        return obj;\r
+    }\r
+\r
+    /**\r
+     * @param uuid\r
+     * @param request\r
+     * @param response\r
+     * @return\r
+     * @throws IOException\r
+     *\r
+     * TODO implement bulk version of this method\r
+     */\r
+    @RequestMapping(value = "*", method = RequestMethod.GET)\r
+    public ModelAndView doGetMethod(\r
+            @PathVariable("uuid") UUID uuid,\r
+            // doPage request parametes\r
+            @RequestParam(value = "pageNumber", required = false) Integer pageNumber,\r
+            @RequestParam(value = "pageSize", required = false) Integer pageSize,\r
+            // doList request parametes\r
+            @RequestParam(value = "start", required = false) Integer start,\r
+            @RequestParam(value = "limit", required = false) Integer limit,\r
+            HttpServletRequest request,\r
+            HttpServletResponse response) throws IOException {\r
+\r
+        ModelAndView modelAndView = new ModelAndView();\r
+\r
+        String servletPath = request.getServletPath();\r
+        String baseName = FilenameUtils.getBaseName(servletPath);\r
+\r
+        if(request != null) {\r
+            logger.info("doGetMethod()[doGet" + StringUtils.capitalize(baseName) + "] " + request.getRequestURI());\r
+        }\r
+\r
+        // <CUT\r
+//             T instance = getCdmBaseInstance(uuid, response, (List<String>)null);\r
+\r
+        //Class<?> propertyClass = propertyClass(instance, baseName);\r
+\r
+        Object objectFromProperty = getCdmBaseProperty(uuid, baseName, response);//   invokeProperty(instance, baseName, response);\r
+\r
+        // CUT>\r
+\r
+        if(objectFromProperty != null){\r
+\r
+            if( Collection.class.isAssignableFrom(objectFromProperty.getClass())){\r
+                // Map types cannot be returned as list or in a pager!\r
+\r
+                Collection c = (Collection)objectFromProperty;\r
+                if(start != null){\r
+                    // return list\r
+                    limit = (limit == null ? DEFAULT_PAGE_SIZE : limit);\r
+                    Collection sub_c = subCollection(c, start, limit);\r
+                    modelAndView.addObject(sub_c);\r
+\r
+                } else {\r
+                    //FIXME use real paging mechanism of according service class instead of subCollection()\r
+                    //FIXME use BaseListController.normalizeAndValidatePagerParameters(pageNumber, pageSize, response);\r
+                    PagerParameters pagerParameters = new PagerParameters(pageSize, pageNumber);\r
+                    pagerParameters.normalizeAndValidate(response);\r
+\r
+                    start = pagerParameters.getPageIndex() * pagerParameters.getPageSize();\r
+                    List sub_c = subCollection(c, start, pagerParameters.getPageSize());\r
+                    Pager p = new DefaultPagerImpl(pageNumber, c.size(), pagerParameters.getPageSize(), sub_c);\r
+                    modelAndView.addObject(p);\r
+                }\r
+\r
+            } else {\r
+                modelAndView.addObject(objectFromProperty);\r
+            }\r
+\r
+        }\r
+\r
+        if(modelAndView.isEmpty()){\r
+            return null;\r
+        } else {\r
+\r
+            return modelAndView;\r
+        }\r
+    }\r
+\r
+    public Object getCdmBaseProperty(UUID uuid, String property, HttpServletResponse response) throws IOException{\r
+\r
+        T instance = (T) HibernateProxyHelper.deproxy(getCdmBaseInstance(uuid, response, property));\r
+\r
+        Object objectFromProperty = invokeProperty(instance, property, response);\r
+\r
+        return objectFromProperty;\r
+    }\r
+\r
+    private Class<?> propertyClass(T instance, String baseName) {\r
+        PropertyDescriptor propertyDescriptor = null;\r
+        Class<?> c = null;\r
+        try {\r
+            propertyDescriptor = PropertyUtils.getPropertyDescriptor(instance, baseName);\r
+            if(propertyDescriptor != null){\r
+                c =  propertyDescriptor.getClass();\r
+            }\r
+        } catch (IllegalAccessException e) {\r
+            // TODO Auto-generated catch block\r
+            e.printStackTrace();\r
+        } catch (InvocationTargetException e) {\r
+            // TODO Auto-generated catch block\r
+            e.printStackTrace();\r
+        } catch (NoSuchMethodException e) {\r
+            // TODO Auto-generated catch block\r
+            e.printStackTrace();\r
+        }\r
+        return c;\r
+    }\r
+\r
+    /**\r
+     * @param <SUB_T>\r
+     * @param clazz\r
+     * @param uuid\r
+     * @param response\r
+     * @param pathProperties\r
+     * @return\r
+     * @throws IOException\r
+     */\r
+    @SuppressWarnings("unchecked")\r
+    protected final <SUB_T extends T> SUB_T getCdmBaseInstance(Class<SUB_T> clazz, UUID uuid, HttpServletResponse response, List<String> pathProperties)\r
+    throws IOException {\r
+\r
+        CdmBase cdmBaseObject = getCdmBaseInstance(uuid, response, pathProperties);\r
+        if(!clazz.isAssignableFrom(cdmBaseObject.getClass())){\r
+            HttpStatusMessage.UUID_REFERENCES_WRONG_TYPE.send(response);\r
+        }\r
+        return (SUB_T) cdmBaseObject;\r
+    }\r
+\r
+    /**\r
+     * @param <SUB_T>\r
+     * @param clazz\r
+     * @param uuid\r
+     * @param response\r
+     * @param pathProperty\r
+     * @return\r
+     * @throws IOException\r
+     */\r
+    @SuppressWarnings("unchecked")\r
+    protected final <SUB_T extends T> SUB_T getCdmBaseInstance(Class<SUB_T> clazz, UUID uuid, HttpServletResponse response, String pathProperty)\r
+    throws IOException {\r
+\r
+        CdmBase cdmBaseObject = getCdmBaseInstance(uuid, response, pathProperty);\r
+        if(!clazz.isAssignableFrom(cdmBaseObject.getClass())){\r
+            HttpStatusMessage.UUID_REFERENCES_WRONG_TYPE.send(response);\r
+        }\r
+        return (SUB_T) cdmBaseObject;\r
+    }\r
+\r
+    /**\r
+     * @param uuid\r
+     * @param response\r
+     * @param pathProperty\r
+     * @return\r
+     * @throws IOException\r
+     */\r
+    protected final T getCdmBaseInstance(UUID uuid, HttpServletResponse response, String pathProperty)\r
+            throws IOException {\r
+        return getCdmBaseInstance(baseClass, uuid, response, Arrays\r
+                .asList(new String[] { pathProperty }));\r
+    }\r
+\r
+\r
+    /**\r
+     * @param uuid\r
+     * @param response\r
+     * @param pathProperties\r
+     * @return\r
+     * @throws IOException\r
+     */\r
+    protected final T getCdmBaseInstance(UUID uuid, HttpServletResponse response, List<String> pathProperties)\r
+            throws IOException {\r
+        return getCdmBaseInstance(baseClass, service, uuid, response, pathProperties);\r
+    }\r
+\r
+    /**\r
+     * @param <CDM_BASE>\r
+     * @param clazz\r
+     * @param service\r
+     * @param uuid\r
+     * @param response\r
+     * @param pathProperties\r
+     * @return\r
+     * @throws IOException\r
+     */\r
+    protected final <CDM_BASE extends CdmBase> CDM_BASE getCdmBaseInstance(Class<CDM_BASE> clazz, IService<CDM_BASE> service, UUID uuid, HttpServletResponse response, List<String> pathProperties)\r
+    throws IOException {\r
+\r
+        CDM_BASE cdmBaseObject = service.load(uuid, pathProperties);\r
+        if (cdmBaseObject == null) {\r
+            HttpStatusMessage.UUID_NOT_FOUND.send(response);\r
+            throw new NoRecordsMatchException("No instance found for UUID " + uuid.toString());\r
+        }\r
+        return cdmBaseObject;\r
+        }\r
+\r
+    /**\r
+     * @param instance\r
+     * @param baseName\r
+     * @param response\r
+     * @return\r
+     * @throws IOException\r
+     */\r
+    private final Object invokeProperty(T instance,\r
+            String baseName, HttpServletResponse response) throws IOException {\r
+\r
+        Object result = null;\r
+        try {\r
+            PropertyDescriptor propertyDescriptor = PropertyUtils.getPropertyDescriptor(instance, baseName);\r
+            if(propertyDescriptor == null){\r
+                throw new NoSuchMethodException("No such method: " + instance.getClass().getSimpleName() + ".get" + baseName);\r
+            }\r
+            Method method = propertyDescriptor.getReadMethod();\r
+\r
+            Class<?> returnType = method.getReturnType();\r
+\r
+            if(CdmBase.class.isAssignableFrom(returnType)\r
+                    || Collection.class.isAssignableFrom(returnType)\r
+                    || Map.class.isAssignableFrom(returnType)\r
+                    || INomenclaturalReference.class.isAssignableFrom(returnType)){\r
+\r
+                result = method.invoke(instance, (Object[])null);\r
+\r
+                result = HibernateProxyHelper.deproxy(result);\r
+\r
+            }else{\r
+                HttpStatusMessage.UUID_REFERENCES_WRONG_TYPE.send(response);\r
+            }\r
+        } catch (SecurityException e) {\r
+            logger.error("SecurityException: ", e);\r
+            HttpStatusMessage.INTERNAL_ERROR.send(response);\r
+        } catch (NoSuchMethodException e) {\r
+            HttpStatusMessage.PROPERTY_NOT_FOUND.send(response);\r
+        } catch (IllegalArgumentException e) {\r
+            HttpStatusMessage.PROPERTY_NOT_FOUND.send(response);\r
+        } catch (IllegalAccessException e) {\r
+            HttpStatusMessage.PROPERTY_NOT_FOUND.send(response);\r
+        } catch (InvocationTargetException e) {\r
+            HttpStatusMessage.PROPERTY_NOT_FOUND.send(response);\r
+        }\r
+        return result;\r
+    }\r
+\r
+    private <E> List<E> subCollection(Collection<? extends E> c, Integer start, Integer length){\r
+        List<E> sub_c = new ArrayList<E>(length);\r
+        if(c.size() > length){\r
+            E[] a = (E[]) c.toArray();\r
+            for(int i = start; i < start + length; i++){\r
+                sub_c.add(a[i]);\r
+            }\r
+        } else {\r
+            sub_c.addAll(c);\r
+        }\r
+        return sub_c;\r
+\r
+    }\r
+\r
+\r
+      /* TODO implement\r
+\r
+      private Validator validator;\r
+\r
+      private javax.validation.Validator javaxValidator;\r
+\r
+      @RequestMapping(method = RequestMethod.PUT, headers="content-type=multipart/form-data")\r
+      public T doPutForm(@PathVariable(value = "uuid") UUID uuid, @ModelAttribute("object") T object, BindingResult result) {\r
+          object.setUuid(uuid);\r
+          validator.validate(object, result);\r
+          if (result.hasErrors()) {\r
+              throw new Error();\r
+                // set http status code depending upon what happened, possibly return\r
+                // the put object and errors so that they can be rendered into a suitable error response\r
+          } else {\r
+             // requires merging detached object ?gilead?\r
+             service.save(object);\r
+          }\r
+\r
+            return object;\r
+      }\r
+\r
+      @RequestMapping(method = RequestMethod.PUT, headers="content-type=text/json")\r
+      public T doPutJSON(@PathVariable(value = "uuid") UUID uuid, @RequestBody String jsonMessage) {\r
+          JSONObject jsonObject = JSONObject.fromObject(jsonMessage);\r
+          T object = (T)JSONObject.toBean(jsonObject, this.getClass());\r
+\r
+\r
+          Set<ConstraintViolation<T>> constraintViolations = javaxValidator.validate(object);\r
+            if (!constraintViolations.isEmpty()) {\r
+                throw new Error();\r
+                    // set http status code depending upon what happened, possibly return\r
+                // the put object and errors so that they can be rendered into a suitable error response\r
+            } else {\r
+              // requires merging detached object ?gilead?\r
+              service.save(object);\r
+            }\r
+\r
+            return object;\r
+      }\r
+\r
+      @RequestMapping(method = RequestMethod.PUT) // the cdm-server may not allow clients to specify the uuid for resources\r
+      public T doPut(@PathVariable(value = "uuid") UUID uuid, @ModelAttribute("object") T object, BindingResult result) {\r
+            validator.validate(object, result);\r
+            if (result.hasErrors()) {\r
+                    // set http status code depending upon what happened, possibly return\r
+                // the put object and errors so that they can be rendered into a suitable error response\r
+            } else {\r
+              service.save(object);\r
+            }\r
+      }\r
+\r
+       @RequestMapping(method = RequestMethod.DELETE)\r
+       public void doDelete(@PathVariable(value = "uuid") UUID uuid) {\r
+           T object = service.find(uuid);\r
+           // provided the object exists\r
+           service.delete(uuid);\r
+           // might return 204 or 200\r
+       }\r
+    }\r
 */\r
-       \r
+\r
 \r
 }\r