From 13796403507a8eac20056db04a95b0e64b3d51b8 Mon Sep 17 00:00:00 2001
From: Tor-Einar Skog <tor-einar.skog@nibio.no>
Date: Tue, 30 Aug 2022 09:09:03 +0200
Subject: [PATCH] Improving documentation for the endpoints in LogicService

---
 README.md                                     |   5 +-
 docs/index.md                                 |   1 -
 enunciate.xml                                 |  18 +-
 .../vips/logic/VIPSLogicApplication.java      |  34 +++
 .../logic/controller/session/UserBean.java    |   7 +
 .../vips/logic/service/LogicService.java      | 264 +++++++++++++-----
 6 files changed, 249 insertions(+), 80 deletions(-)

diff --git a/README.md b/README.md
index 4e24bac4..428f54cd 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@ VIPSLogic is the source of data for most clients, including VIPSWeb, the standar
 ![How VIPSLogic fits into the whole VIPS architecture](./docs/illustrations/VIPS_whole_architecture.png "All VIPS system parts")
 
 ## Technical description
-### Environment that VIPSLogic is confirmed to work within
+### Requirements
 * Operating system: Ubuntu Linux >= 18.0.4 
 * Database: PostgreSQL >= 10  + PostGIS >= 2.4
 * Java: OpenJDK >= 11
@@ -32,5 +32,8 @@ VIPSLogic is the source of data for most clients, including VIPSWeb, the standar
 ## License
 VIPSLogic is licensed under the [NIBIO Open Source License](https://nibio.no/licenses), which is basically the [GNU Affero GPL v3 license](https://www.gnu.org/licenses/agpl-3.0.en.html).
 
+## Web services
+The web services documentation is auto generated using Enunciate, you can read it from this path relative to your VIPSLogic deployment: `/public/RESTdocs/apidocs/`, for instance [here on NIBIO's deployment](https://logic.vips.nibio.no/public/RESTdocs/apidocs/)
+
 ## Develop and deploy VIPSLogic
 Please read [the developer documentation](./docs/index.md)
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
index 3937b391..8ac50f39 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -53,7 +53,6 @@ ALTER ROLE vipslogic SUPERUSER;
 
 
 #### Building the VIPSLogic image
-You need the
 Make sure you're located in the parent folder of the VIPSLogic project. You need these resource files/folders in your current folder:
 * standalone.xml (see below)
 * VIPSCommon/ (can be cloned [from here](https://gitlab.nibio.no/VIPS/VIPSCommon)) - built with `mvn install`
diff --git a/enunciate.xml b/enunciate.xml
index 53627989..982546e6 100644
--- a/enunciate.xml
+++ b/enunciate.xml
@@ -1,9 +1,7 @@
 <enunciate xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://enunciate.webcohesion.com/schemas/enunciate-2.0.0-M.4.xsd">
 
   <title>VIPSLogic API</title>
-  <description>
-      The VIPSLogic service API gives access to stored pest predictions and organisms
-  </description>
+  <description format="markdown" file="README.md"/>>
   <copyright>NIBIO</copyright>
   <contact><a href="https://www.nibio.no/ansatte/tor-einar-skog" target="new">Tor-Einar Skog</a></contact>
 
@@ -22,8 +20,20 @@
   <api-classes>
       <exclude pattern="no.nibio.vips.logic.messaging.UniversalMessagingServiceClient"/>
       <exclude pattern="no.nibio.vips.util.weather.dnmipointweb.**"/>
+      <exclude pattern="com.**"/>
+      <exclude pattern="org.**"/>
+      <exclude pattern="net.**"/>
+      <exclude pattern="freemarker.**"/>
+      <exclude pattern="de.**"/>
+      <exclude pattern="resources.**"/>
+      <exclude pattern="thredds.**"/>
+      <exclude pattern="ucar.**"/>
+      <exclude pattern="uk.**"/>
+      <exclude pattern="it.**"/>
+      <exclude pattern="antlr.**"/>
+      <exclude pattern="javax.**"/>
   </api-classes>
   <facets>
         <exclude name="restricted"/>
   </facets>
-</enunciate>
\ No newline at end of file
+</enunciate>
diff --git a/src/main/java/no/nibio/vips/logic/VIPSLogicApplication.java b/src/main/java/no/nibio/vips/logic/VIPSLogicApplication.java
index 97a8feb4..59df3b39 100755
--- a/src/main/java/no/nibio/vips/logic/VIPSLogicApplication.java
+++ b/src/main/java/no/nibio/vips/logic/VIPSLogicApplication.java
@@ -71,6 +71,9 @@ public class VIPSLogicApplication extends Application
      * given list with all resources defined in the project.
      */
     private void addRestResourceClasses(Set<Class<?>> resources) {
+        resources.add(com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider.class);
+        resources.add(com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider.class);
+        resources.add(com.webcohesion.enunciate.rt.EnunciateJaxbContextResolver.class);
         resources.add(no.nibio.vips.logic.messaging.sms.SMSHandlingService.class);
         resources.add(no.nibio.vips.logic.modules.applefruitmoth.AppleFruitMothService.class);
         resources.add(no.nibio.vips.logic.modules.barkbeetle.BarkbeetleService.class);
@@ -87,5 +90,36 @@ public class VIPSLogicApplication extends Application
         resources.add(no.nibio.vips.logic.service.POIService.class);
         resources.add(no.nibio.vips.logic.service.VIPSMobileService.class);
         resources.add(no.nibio.vips.observationdata.ObservationDataService.class);
+        resources.add(org.jboss.resteasy.core.AcceptHeaderByFileSuffixFilter.class);
+        resources.add(org.jboss.resteasy.core.AsynchronousDispatcher.class);
+        resources.add(org.jboss.resteasy.plugins.interceptors.AcceptEncodingGZIPFilter.class);
+        resources.add(org.jboss.resteasy.plugins.interceptors.GZIPDecodingInterceptor.class);
+        resources.add(org.jboss.resteasy.plugins.interceptors.GZIPEncodingInterceptor.class);
+        resources.add(org.jboss.resteasy.plugins.interceptors.MessageSanitizerContainerResponseFilter.class);
+        resources.add(org.jboss.resteasy.plugins.providers.AsyncStreamingOutputProvider.class);
+        resources.add(org.jboss.resteasy.plugins.providers.ByteArrayProvider.class);
+        resources.add(org.jboss.resteasy.plugins.providers.DataSourceProvider.class);
+        resources.add(org.jboss.resteasy.plugins.providers.DefaultBooleanWriter.class);
+        resources.add(org.jboss.resteasy.plugins.providers.DefaultNumberWriter.class);
+        resources.add(org.jboss.resteasy.plugins.providers.DefaultTextPlain.class);
+        resources.add(org.jboss.resteasy.plugins.providers.DocumentProvider.class);
+        resources.add(org.jboss.resteasy.plugins.providers.FileProvider.class);
+        resources.add(org.jboss.resteasy.plugins.providers.FileRangeWriter.class);
+        resources.add(org.jboss.resteasy.plugins.providers.FormUrlEncodedProvider.class);
+        resources.add(org.jboss.resteasy.plugins.providers.IIOImageProvider.class);
+        resources.add(org.jboss.resteasy.plugins.providers.InputStreamProvider.class);
+        resources.add(org.jboss.resteasy.plugins.providers.JaxrsFormProvider.class);
+        resources.add(org.jboss.resteasy.plugins.providers.JaxrsServerFormUrlEncodedProvider.class);
+        resources.add(org.jboss.resteasy.plugins.providers.MultiValuedParamConverterProvider.class);
+        resources.add(org.jboss.resteasy.plugins.providers.ReaderProvider.class);
+        resources.add(org.jboss.resteasy.plugins.providers.SourceProvider.class);
+        resources.add(org.jboss.resteasy.plugins.providers.StreamingOutputProvider.class);
+        resources.add(org.jboss.resteasy.plugins.providers.StringTextStar.class);
+        resources.add(org.jboss.resteasy.plugins.providers.jackson.Jackson2JsonpInterceptor.class);
+        resources.add(org.jboss.resteasy.plugins.providers.jackson.PatchMethodFilter.class);
+        resources.add(org.jboss.resteasy.plugins.providers.jackson.ResteasyJackson2Provider.class);
+        resources.add(org.jboss.resteasy.plugins.providers.jackson.UnrecognizedPropertyExceptionHandler.class);
+        resources.add(org.jboss.resteasy.plugins.providers.sse.SseEventProvider.class);
+        resources.add(org.jboss.resteasy.plugins.providers.sse.SseEventSinkInterceptor.class);
     }
 }
\ No newline at end of file
diff --git a/src/main/java/no/nibio/vips/logic/controller/session/UserBean.java b/src/main/java/no/nibio/vips/logic/controller/session/UserBean.java
index fe252053..557e418a 100755
--- a/src/main/java/no/nibio/vips/logic/controller/session/UserBean.java
+++ b/src/main/java/no/nibio/vips/logic/controller/session/UserBean.java
@@ -353,6 +353,13 @@ public class UserBean {
         return em.createNamedQuery("Organization.findAll").getResultList();
     }
     
+    /**
+     * Check if a password meets all criteria configured by Passay
+     * @param password
+     * @param errorMessageLocale
+     * @return
+     * @throws PasswordValidationException 
+     */
     public boolean isPasswordValid(String password, ULocale errorMessageLocale) throws PasswordValidationException
     {
         // Check if we need localization of error messages
diff --git a/src/main/java/no/nibio/vips/logic/service/LogicService.java b/src/main/java/no/nibio/vips/logic/service/LogicService.java
index b8c8d6cd..8e424b90 100755
--- a/src/main/java/no/nibio/vips/logic/service/LogicService.java
+++ b/src/main/java/no/nibio/vips/logic/service/LogicService.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016 NIBIO <http://www.nibio.no/>. 
+ * Copyright (c) 2022 NIBIO <http://www.nibio.no/>. 
  * 
  * This file is part of VIPSLogic.
  * VIPSLogic is free software: you can redistribute it and/or modify
@@ -21,6 +21,7 @@ package no.nibio.vips.logic.service;
 
 import com.ibm.icu.util.ULocale;
 import com.webcohesion.enunciate.metadata.Facet;
+import com.webcohesion.enunciate.metadata.rs.TypeHint;
 import java.util.TimeZone;
 import de.micromata.opengis.kml.v_2_2_0.Kml;
 import java.text.DateFormat;
@@ -48,6 +49,7 @@ import javax.ws.rs.client.WebTarget;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
 import no.nibio.vips.coremanager.service.ManagerResource;
 import no.nibio.vips.entity.WeatherObservation;
 import no.nibio.vips.logic.authenticate.PasswordValidationException;
@@ -56,6 +58,7 @@ import no.nibio.vips.logic.controller.session.MessageBean;
 import no.nibio.vips.logic.controller.session.OrganismBean;
 import no.nibio.vips.logic.controller.session.PointOfInterestBean;
 import no.nibio.vips.logic.controller.session.UserBean;
+import no.nibio.vips.logic.entity.CropCategory;
 import no.nibio.vips.logic.entity.CropPest;
 import no.nibio.vips.logic.entity.ForecastResult;
 import no.nibio.vips.logic.i18n.SessionLocaleUtil;
@@ -80,7 +83,7 @@ import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget;
 import org.jboss.resteasy.spi.HttpRequest;
 
 /**
- * @copyright 2013-2016 <a href="http://www.nibio.no/">NIBIO</a>
+ * @copyright 2013-2022 <a href="http://www.nibio.no/">NIBIO</a>
  * @author Tor-Einar Skog <tor-einar.skog@nibio.no>
  */
 @Path("rest")
@@ -109,33 +112,34 @@ public class LogicService {
      * Get all results for one pest prediction
      * @param forecastConfigurationId Database id of the configured forecast
      * @param userUUID if the forecast is private, the correct userUUID must be supplied. 
-     * @return JSON with result data. A list of ForecastResult objects. Example:
-     * <pre>
+     * @return JSON with result data. A list of ForecastResult objects. 
+     * @responseExample application/json
      * {
         "forecastResultId": 5710137,
         "validTimeStart": "2019-01-22T23:00:00.000+0000",
         "validTimeEnd": null,
         "warningStatus": 0,
         "forecastConfigurationId": -1000,
-        "validGeometry": { // NORMALLY SET ONLY IF the result set contains results for multiple locations
+        "validGeometry": { 
             "type": "Point",
             "coordinates": [
                 10.333252,
                 57.179002
             ]
         },
-        "keys": [ // A list of the existing parameter names in this result object
+        "keys": [ 
             "GRIDZYMOSE.WHS"
         ],
-        "allValues": { // Parameters with values. Everything is a String. Client must convert to numbers if necessary
+        "allValues": { 
             "GRIDZYMOSE.WHS": "0"
         }
-    }</pre>
+    }
      */
     @GET
     @Path("forecastresults/{forecastConfigurationId}")
     @GZIP
     @Produces("application/json;charset=UTF-8")
+    @TypeHint(ForecastResult[].class)
     public Response getForecastResults(
             @PathParam("forecastConfigurationId") Long forecastConfigurationId,
             @QueryParam("userUUID") String userUUID
@@ -161,6 +165,18 @@ public class LogicService {
      * @param forecastConfigurationId
      * @param userUUID if the forecast is private, the correct userUUID must be supplied. 
      * @return 
+     * @responseExample text/csv
+     * Valid time start,Valid time end,Warning status,WEATHER.BT,NAERSTADMO.SPH,FORECAST.THRESHOLD_LOW,NAERSTADMO.VAS,NAERSTADMO.TSHH,NAERSTADMO.VRS,FORECAST.THRESHOLD_HIGH,NAERSTADMO.WD,WEATHER.RR,NAERSTADMO.IR,WEATHER.Q0,NAERSTADMO.RISK,WEATHER.UM,NAERSTADMO.WHS,NAERSTADMO.WH,WEATHER.TM
+     * 2022-05-21 00:00:00.0,null,2,0,0,1.0,0,0,0,2.5,0,0,0,0,0,77.86,0,0,12.61
+     * 2022-05-21 01:00:00.0,null,2,0,0,1.0,0,0,0,2.5,0,0,0,0,0,81.1,0,0,12.29
+     * 2022-05-21 02:00:00.0,null,2,0,0,1.0,0,0,0,2.5,0,0,0,0,0,84.1,0,0,11.85
+     * 2022-05-21 03:00:00.0,null,2,0,0,1.0,0,11.49,0,2.5,0,0,0,0,0,86.6,0,0,11.49
+     * 2022-05-21 04:00:00.0,null,2,0,0,1.0,0,22.42,0,2.5,0,0,0,1.17,0,90.5,0,0,10.93
+     * 2022-05-21 05:00:00.0,null,2,28,0,1.0,0,33.29,0,2.5,1,0,1,11.08,0,92.1,1,1,10.87
+     * 2022-05-21 06:00:00.0,null,2,60,0,1.0,0,44.32,0,2.5,2,0.2,1,19.02,0,92.3,1,1,11.03
+     * 2022-05-21 07:00:00.0,null,2,60,0,1.0,0,55.39,0,2.5,3,1,1,28.13,0,95,1,1,11.07
+     * 2022-05-21 08:00:00.0,null,2,60,0,1.0,0,66.54,0,2.5,4,1.4,1,49.35,0,97.6,1,1,11.15
+     * 2022-05-21 09:00:00.0,null,2,60,0,1.0,0,78.15,0,2.5,5,1.2,1,89.6,0,95.3,1,1,11.61
      */
     @GET
     @Path("forecastresults/{forecastConfigurationId}/csv")
@@ -215,11 +231,33 @@ public class LogicService {
      * @param latestDays
      * @param userUUID if the forecast is private, the correct userUUID must be supplied. 
      * @return 
+     * @responseExample application/json
+     * {
+        "forecastResultId": 5710137,
+        "validTimeStart": "2019-01-22T23:00:00.000+0000",
+        "validTimeEnd": null,
+        "warningStatus": 0,
+        "forecastConfigurationId": -1000,
+        "validGeometry": { 
+            "type": "Point",
+            "coordinates": [
+                10.333252,
+                57.179002
+            ]
+        },
+        "keys": [ 
+            "GRIDZYMOSE.WHS"
+        ],
+        "allValues": { 
+            "GRIDZYMOSE.WHS": "0"
+        }
+    }
      */
     @GET
     @Path("forecastresults/{forecastConfigurationId}/{latestDays}")
     @GZIP
     @Produces("application/json;charset=UTF-8")
+    @TypeHint(ForecastResult[].class)
     public Response getForecastResults(
             @PathParam("forecastConfigurationId") Long forecastConfigurationId,
             @PathParam("latestDays") Integer latestDays,
@@ -241,10 +279,39 @@ public class LogicService {
         }
     }
     
+    /**
+     * Get the forecast results for a particular forecast configuration in a given period
+     * @param forecastConfigurationId
+     * @param dateStartStr format "yyyy-MM-dd"
+     * @param dateEndStr format "yyyy-MM-dd"
+     * @return The forecast results for a particular forecast configuration in a given period
+     * @responseExample application/json
+     * {
+        "forecastResultId": 5710137,
+        "validTimeStart": "2019-01-22T23:00:00.000+0000",
+        "validTimeEnd": null,
+        "warningStatus": 0,
+        "forecastConfigurationId": -1000,
+        "validGeometry": { 
+            "type": "Point",
+            "coordinates": [
+                10.333252,
+                57.179002
+            ]
+        },
+        "keys": [ 
+            "GRIDZYMOSE.WHS"
+        ],
+        "allValues": { 
+            "GRIDZYMOSE.WHS": "0"
+        }
+    }
+    */
     @GET
     @Path("forecastresults/{forecastConfigurationId}/{dateStart}/{dateEnd}")
     @GZIP
     @Produces("application/json;charset=UTF-8")
+    @TypeHint(ForecastResult[].class)
     public Response getForecastResults(
             @PathParam("forecastConfigurationId") Long forecastConfigurationId,
             @PathParam("dateStart") String dateStartStr,
@@ -265,15 +332,18 @@ public class LogicService {
     }
     
     /**
-     * @param organizationId
-     * @param cropOrganismIds
-     * @return 
+     * @param organizationId Id of the organization
+     * @param cropOrganismIds Integer list of crop ids
+     * @param includeOrganizationIds Optional additional organization ids - include summaries from these organizations as well
+     * @param userUUID unique login token (optional, used to authenticate user logged in via VIPSWeb)
+     * @return A list of forecast configurations (for (a) given organization(s)) with forecast summaries attached
      */
     @GET
     @Path("forecastconfigurationsummaries/{organizationId}")
     @GZIP
     @Produces("application/json;charset=UTF-8")
     @Facet("restricted")
+    @TypeHint(ForecastConfiguration[].class)
     public Response getForecastSummaries(
             @PathParam("organizationId") Integer organizationId,
             @QueryParam("cropOrganismId") List<Integer> cropOrganismIds,
@@ -314,8 +384,9 @@ public class LogicService {
     
     /**
      * 
-     * @param userUUID
-     * @return 
+     * @param userUUID unique login token (optional, used to authenticate user logged in via VIPSWeb)
+     * @return A list of forecast configurations for the user's organization with forecast summaries attached
+     * @ignore
      */
     @GET
     @Path("forecastconfigurationsummaries/private/{userUUID}")
@@ -342,14 +413,18 @@ public class LogicService {
         
     /**
      * Get the configuration of the specified forecast
-     * @param forecastConfigurationId
+     * @param forecastConfigurationId The ID of the requested configuration (crop, pest, model, location, period, owner etc.)
      * @param userUUID if the forecast is private, the correct userUUID must be supplied. 
-     * @return 
+     * @return the configuration (crop, pest, model, location, period, owner etc.) of the specified forecast
      */
     @GET
     @Path("forecastconfigurations/{forecastConfigurationId}")
     @Produces("application/json;charset=UTF-8")
-    public Response getForecastConfiguration(@PathParam("forecastConfigurationId") Long forecastConfigurationId,@QueryParam("userUUID") String userUUID)
+    @TypeHint(ForecastConfiguration.class)
+    public Response getForecastConfiguration(
+            @PathParam("forecastConfigurationId") Long forecastConfigurationId,
+            @QueryParam("userUUID") String userUUID
+    )
     {
         if(forecastBean.isUserAuthorizedForForecastConfiguration(forecastConfigurationId, userUUID))
         {
@@ -364,13 +439,14 @@ public class LogicService {
     
     /**
      * Returns public forecast configurations for the given model and season
-     * @param modelId
-     * @param year
+     * @param modelId The ID of the model. 10 character string. E.g. PSILARTEMP
+     * @param year The year for which to find the configured forecasts
      * @return 
      */
     @GET
     @Path("forecastconfigurations/model/{modelId}/{year}")
     @Produces("application/json;charset=UTF-8")
+    @TypeHint(ForecastConfiguration[].class)
     public Response getForecastConfigurationsForModel(@PathParam("modelId") String modelId, @PathParam("year") Integer year)
     {
         return Response.ok().entity(forecastBean.getForecastConfigurationsForModel(modelId, year)).build();
@@ -378,14 +454,15 @@ public class LogicService {
     
     
     /**
-     * 
-     * @param userUUID
+     * Returns private forecast configurations for the given user
+     * @param userUUID unique login token (optional, used to authenticate user logged in via VIPSWeb)
      * @return 
      */
     @GET
     @Path("forecastconfigurations/private/{userUUID}")
     @Produces("application/json;charset=UTF-8")
     @Facet("restricted")
+    @TypeHint(ForecastConfiguration[].class)
     public Response getPrivateForecastConfigurations(@PathParam("userUUID") String userUUID)
     {
         try
@@ -409,10 +486,19 @@ public class LogicService {
         
     }
     
+    /**
+     * 
+     * @param organizationId The primary organization to get forecast configurations from
+     * @param includeOrganizationIds Additional organizations to get forecast configurations from
+     * @param fromStr Dateformat = "yyyy-MM-dd"
+     * @param toStr Dateformat = "yyyy-MM-dd"
+     * @return A list of forecast configurations (for (a) given organization(s))
+     */
     @GET
     @Path("forecastconfigurationsincludeorgs/{organizationId}")
     @GZIP
     @Produces("application/json;charset=UTF-8")
+    @TypeHint(ForecastConfiguration[].class)
     public Response getActiveForecastConfigurationsWithIncludeOrganizations(
             @PathParam("organizationId") Integer organizationId,
             @QueryParam("includeOrganizationIds") String includeOrganizationIds,
@@ -464,14 +550,15 @@ public class LogicService {
      * Returns a list of forecasts for given organization
      * @param organizationId
      * @param cropOrganismIds
-     * @param from format="yyyy-MM-dd"
-     * @param to format="yyyy-MM-dd"
+     * @param fromStr format="yyyy-MM-dd"
+     * @param toStr format="yyyy-MM-dd"
      * @return 
      */
     @GET
     @Path("organizationforecastconfigurations/{organizationId}")
     @GZIP
     @Produces("application/json;charset=UTF-8")
+    @TypeHint(ForecastConfiguration[].class)
     public Response getForecastConfigurationsForOrganization(
             @PathParam("organizationId") Integer organizationId, 
             @QueryParam("cropOrganismId") List<Integer> cropOrganismIds,
@@ -520,7 +607,7 @@ public class LogicService {
     
     
     /**
-     * 
+     * Check if a proposed password meets the requirements configured by Passay
      * @param password
      * @return 
      */
@@ -533,20 +620,27 @@ public class LogicService {
         ULocale currentLocale = SessionLocaleUtil.getCurrentLocale(httpServletRequest);
         try
         {
-            boolean isPasswordValid = userBean.isPasswordValid(password, currentLocale);
-            return Response.ok().entity("true").build();
+            // Invalid passwords always cause a PasswordValidationException to be thrown
+            Boolean isPasswordValid = userBean.isPasswordValid(password, currentLocale);
+            return Response.ok().entity(isPasswordValid).build();
             
         }
         catch(PasswordValidationException ex)
         {
-            return Response.ok().entity(ex.getMessage()).build();
+            return Response.status(Status.BAD_REQUEST).entity(ex.getMessage()).build();
         }
     }
     
+    /**
+     * The model configuration (model specific parameters and their values) for the given forecast configuration
+     * @param forecastConfigurationId
+     * @return 
+     */
     @GET
     @Path("forecastmodelconfiguration/{forecastConfigurationId}")
     @Produces("application/json;charset=UTF-8")
     @Facet("restricted")
+    @TypeHint(ForecastModelConfiguration.class)
     public Response getForecastModelConfiguration(@PathParam("forecastConfigurationId") Long forecastConfigurationId)
     {
         List<ForecastModelConfiguration> forecastModelConfigurations = forecastBean.getForecastModelConfigurations(forecastConfigurationId);
@@ -554,9 +648,11 @@ public class LogicService {
     }
     
     /**
-     * @param organizationId
-     * @param cropCategoryIds
-     * @return 
+     * 
+     * @param organizationId Get POIs for this organization
+     * @param cropCategoryIds Optionally filter by crop category ids (comma separated)
+     * @param userUUID unique login token (optional, used to authenticate user logged in via VIPSWeb)
+     * @return a KML file with the "worst" warning status for each POI
      */
     @GET
     @Path("forecastresults/aggregate/{organizationId}")
@@ -596,9 +692,10 @@ public class LogicService {
     }
     /**
      * 
-     * @param organizationId
+     * @param organizationIds
      * @param cropCategoryIds
-     * @return 
+     * @param userUUID unique login token (optional, used to authenticate user logged in via VIPSWeb)
+     * @return a KML file with the "worst" warning status for each POI
      */
     @GET
     @Path("forecastresults/aggregate/orgspan")
@@ -652,15 +749,19 @@ public class LogicService {
     
     /**
      * Get a list of weather stations for a given organization
-     * @param excludeWeatherStationId
-     * @param highlightWeatherStationId
+     * @param excludeWeatherStationId Exclude this weather station from the KML
+     * @param highlightWeatherStationId Show highlight icon for this weather station
      * @param organizationId
-     * @return 
+     * @return a KML with weather stations for an organization
      */
     @GET
     @Path("weatherstations/kml/{organizationId}")
     @Produces("application/vnd.google-earth.kml+xml;charset=utf-8")
-    public Response getWeatherStations(@QueryParam("excludeWeatherStationId") Integer excludeWeatherStationId, @QueryParam("highlightWeatherStationId") Integer highlightWeatherStationId, @PathParam("organizationId") Integer organizationId)
+    public Response getWeatherStations(
+            @QueryParam("excludeWeatherStationId") Integer excludeWeatherStationId, 
+            @QueryParam("highlightWeatherStationId") Integer highlightWeatherStationId, 
+            @PathParam("organizationId") Integer organizationId
+    )
     {
         Kml retVal = pointOfInterestBean.getPoisForOrganization(organizationId, excludeWeatherStationId, highlightWeatherStationId, ServletUtil.getServerName(httpServletRequest), SessionLocaleUtil.getI18nBundle(httpServletRequest), PointOfInterestType.POINT_OF_INTEREST_TYPE_WEATHER_STATION);
         return Response.ok().entity(retVal).build();
@@ -668,15 +769,20 @@ public class LogicService {
     
     /**
      * Get a KML list of locations (pois) for a given organization
-     * @param excludePoiId
-     * @param highlightPoiId
+     * @param excludePoiId 
+     * @param highlightPoiId use this if you want to highlight a specific POI. Should be 
+     * used in conjunction with excludePoiId
      * @param organizationId
      * @return KML
      */
     @GET
     @Path("pois/kml/{organizationId}")
     @Produces("application/vnd.google-earth.kml+xml;charset=utf-8")
-    public Response getPois(@QueryParam("excludePoiId") Integer excludePoiId, @QueryParam("highlightPoiId") Integer highlightPoiId, @PathParam("organizationId") Integer organizationId)
+    public Response getPois(
+            @QueryParam("excludePoiId") Integer excludePoiId, 
+            @QueryParam("highlightPoiId") Integer highlightPoiId, 
+            @PathParam("organizationId") Integer organizationId
+    )
     {
         Kml retVal = pointOfInterestBean.getPoisForOrganization(organizationId, excludePoiId, highlightPoiId, ServletUtil.getServerName(httpServletRequest), SessionLocaleUtil.getI18nBundle(httpServletRequest), null);
         return Response.ok().entity(retVal).build();
@@ -692,6 +798,7 @@ public class LogicService {
     @GET
     @Path("poi/organization/{organizationId}")
     @Produces("application/json;charset=UTF-8")
+    @TypeHint(PointOfInterestWeatherStation[].class)
     public Response getPoisForOrganization(@PathParam("organizationId") Integer organizationId)
     {
         Organization organization = userBean.getOrganization(organizationId);
@@ -707,6 +814,7 @@ public class LogicService {
     @GET
     @Path("poi/{pointOfInterestId}")
     @Produces("application/json;charset=UTF-8")
+    @TypeHint(PointOfInterest.class)
     public Response getPoi(@PathParam("pointOfInterestId") Integer pointOfInterestId)
     {
         PointOfInterest retVal = pointOfInterestBean.getPointOfInterest(pointOfInterestId);
@@ -721,6 +829,7 @@ public class LogicService {
     @GET
     @Path("poi/name/{poiName}")
     @Produces("application/json;charset=UTF-8")
+    @TypeHint(PointOfInterest.class)
     public Response getPoiByName(@PathParam("poiName") String poiName)
     {
         PointOfInterest retVal = pointOfInterestBean.getPointOfInterest(poiName);
@@ -729,12 +838,13 @@ public class LogicService {
     
     /**
      * If used outside of VIPSLogic: Requires a valid UUID to be provided in the Authorization header
-     * @return 
+     * @return a list of POIs for the user logged in in this session
      */
     @GET
     @Path("poi/user")
     @Produces("application/json;charset=UTF-8")
     @Facet("restricted")
+    @TypeHint(PointOfInterest[].class)
     public Response getPoisForCurrentUser()
     {
         VipsLogicUser user = (VipsLogicUser) httpServletRequest.getSession().getAttribute("user");
@@ -751,12 +861,13 @@ public class LogicService {
     
     /**
      * 
-     * @return 
+     * @return A list of all organisms (pests and crops)
      */
     @GET
     @Path("organism/list")
     @Produces("application/json;charset=UTF-8")
     @Facet("restricted")
+    @TypeHint(Organism[].class)
     public Response getOrganismList()
     {
         List<Organism> organismList = organismBean.getOrganismSubTree(null);
@@ -764,13 +875,14 @@ public class LogicService {
     }
     
     /**
-     * Look up an organism by its latin name
-     * @param keywords
-     * @return 
+     * Look up (an) organism(s) by its/their latin name(s)
+     * @param keywords comma separated list of latin names
+     * @return List of matching organisms (pests and crops)
      */
     @GET
     @Path("organism/search/latinnames")
     @Produces("application/json;charset=UTF-8")
+    @TypeHint(Organism[].class)
     public Response findOrganismsByLatinNames(@QueryParam("keywords") String keywords)
     {
         List<String> latinNames = Arrays.asList(keywords.split(","));
@@ -780,12 +892,14 @@ public class LogicService {
     
     /**
      * Look up organisms by local names
-     * @param keywords
-     * @return 
+     * @param locale two-letter language code
+     * @param keywords Comma separated list of local name
+     * @return List of matching organisms (pests and crops)
      */
     @GET
     @Path("organism/search/localnames/{locale}")
     @Produces("application/json;charset=UTF-8")
+    @TypeHint(Organism[].class)
     public Response findOrganismsByLocalNames(
             @PathParam("locale") String locale,
             @QueryParam("keywords") String keywords
@@ -798,12 +912,13 @@ public class LogicService {
     
     /**
      * Get a list of all crops
-     * @return 
+     * @return A list of all crops
      */
     @GET
     @Path("organism/crop/list")
     @Produces("application/json;charset=UTF-8")
     @Facet("restricted")
+    @TypeHint(Organism[].class)
     public Response getCropOrganismList()
     {
         List<Organism> organismList = organismBean.getAllCrops();
@@ -813,13 +928,15 @@ public class LogicService {
     /**
      * Get a list of all pests, OR if cropOrganismId is specified,
      * get a list of all pests that are connected with this crop
-     * @param organization Id optional if set, observation data schemas are added
+     * @param cropOrganismId optional if set, only pests for this crop are returned
+     * @param organizationId optional if set, observation data schemas are added
      * @return 
      */
     @GET
     @Path("organism/pest/list")
     @Produces("application/json;charset=UTF-8")
     @Facet("restricted")
+    @TypeHint(Organism[].class)
     public Response getPestOrganismList(
     		@QueryParam("cropOrganismId") Integer cropOrganismId,
     		@QueryParam("organizationId") Integer organizationId
@@ -864,13 +981,14 @@ public class LogicService {
     
     /**
      * 
-     * @param messageId
-     * @return 
+     * @param messageId the ID of the news message
+     * @return a news message
      */
     @GET
     @Path("message/{messageId}")
     @Produces("application/json;charset=UTF-8")
     @Facet("restricted")
+    @TypeHint(Message.class)
     public Response getMessage(@PathParam("messageId") Integer messageId)
     {
         Message message = messageBean.getMessage(messageId);
@@ -880,17 +998,18 @@ public class LogicService {
     
     /**
      * 
-     * @param publishedFrom
-     * @param publishedTo
-     * @param locale
-     * @param organizationId
-     * @return 
+     * @param publishedFrom Format "yyyy-MM-dd"
+     * @param publishedTo Format "yyyy-MM-dd"
+     * @param locale two letter language code for preferred language version (if it exists)
+     * @param organizationId The organization for which to get messages
+     * @return a list of news messages, filtered by the parameters given
      */
     @GET
     @Path("message/list/{organizationId}")
     @GZIP
     @Produces("application/json;charset=UTF-8")
     @Facet("restricted")
+    @TypeHint(Message[].class)
     public Response getMessageList(
             @QueryParam("publishedFrom") String publishedFrom , @QueryParam("publishedTo") String publishedTo,
             @QueryParam("locale") String locale,
@@ -923,16 +1042,21 @@ public class LogicService {
     
     /**
      * 
-     * @param tagIds
-     * @param organizationId
-     * @return 
+     * @param tagIds comma separated list of tagIds to filter the news messages
+     * Use the messagetag/list endpoint to see what tags are available
+     * @param organizationId The organization for which to get messages
+     * @return a list of news messages, filtered by the parameters given
      */
     @GET
     @Path("message/list/{organizationId}/tagfilter")
     @GZIP
     @Produces("application/json;charset=UTF-8")
     @Facet("restricted")
-    public Response getMessageListWithTags(@QueryParam("tagId") List<Integer> tagIds, @PathParam("organizationId") Integer organizationId)
+    @TypeHint(Message[].class)
+    public Response getMessageListWithTags(
+            @QueryParam("tagId") List<Integer> tagIds, 
+            @PathParam("organizationId") Integer organizationId
+    )
     {
         List<Message> messageListWithTags = messageBean.getCurrentFilteredMessagesForOrganization(tagIds, organizationId);
         return Response.ok().entity(messageListWithTags).build();
@@ -940,33 +1064,19 @@ public class LogicService {
     
     /**
      * 
-     * @return 
+     * @return a list of available message tags (for filtering messages)
      */
     @GET
     @Path("messagetag/list")
     @Produces("application/json;charset=UTF-8")
     @Facet("restricted")
+    @TypeHint(MessageTag[].class)
     public Response getMessageTagList()
     {
         List<MessageTag> messageTags = messageBean.getMessageTagList();
         return Response.ok().entity(messageTags).build();
     }
     
-    /**
-     * Get a list of observations for a given organization
-     * @param organizationId
-     * @return 
-     */
-    /*@GET
-    @Path("observation")
-    @GZIP
-    @Produces("application/json;charset=UTF-8")
-    public Response getObservationList(@QueryParam("organizationId") Integer organizationId)
-    {
-        List<Observation> observations = SessionControllerGetter.getObservationBean().getObservations(organizationId);
-        return Response.ok().entity(observations).build();
-    }*/
-    
     /**
      * Not ready for production use!
      * @param latitude
@@ -1112,6 +1222,7 @@ public class LogicService {
      * TODO: Should only be available for trusted clients (like VIPSWeb)
      * @param userUUID
      * @return 
+     * @ignore
      */
     @GET
     @Path("user/uuid/{userUUID}")
@@ -1142,6 +1253,7 @@ public class LogicService {
      * TODO: Must be authenticated or not??
      * @param userUUID
      * @return 
+     * @ignore
      */
     @DELETE
     @Path("user/uuid/{userUUID}")
@@ -1170,6 +1282,7 @@ public class LogicService {
     @Path("organism/croppest/{cropOrganismId}")
     @Produces("application/json;charset=UTF-8")
     @Facet("restricted")
+    @TypeHint(CropPest.class)
     public Response getCropPest(@PathParam("cropOrganismId") Integer cropOrganismId)
     {
         CropPest retVal = organismBean.getCropPestRecursive(cropOrganismId,true);
@@ -1192,6 +1305,7 @@ public class LogicService {
     @Path("organism/cropcategory/{organizationId}")
     @Produces("application/json;charset=UTF-8")
     @Facet("restricted")
+    @TypeHint(CropCategory[].class)
     public Response getCropCategories(@PathParam("organizationId") Integer organizationId)
     {
         if(organizationId != null)
@@ -1207,6 +1321,7 @@ public class LogicService {
     @GET
     @Path("organization")
     @Produces("application/json;charset=UTF-8")
+    @TypeHint(Organization[].class)
     public Response getOrganizations()
     {
         return Response.ok().entity(userBean.getOrganizations()).build();
@@ -1215,6 +1330,7 @@ public class LogicService {
     @GET
     @Path("model/{modelId}")
     @Produces("application/json;charset=UTF-8")
+    @TypeHint(ModelInformation.class)
     public Response getModelInformation(@PathParam("modelId") String modelId)
     {
         ModelInformation retVal = forecastBean.getModelInformation(modelId);
-- 
GitLab