diff --git a/pom.xml b/pom.xml index 43ddcb1c1ac15097a54958a389ea93245a0ee2ea..1b18ec74e849d188a3f1add271dda6cb60e555e7 100755 --- a/pom.xml +++ b/pom.xml @@ -209,7 +209,7 @@ <dependency> <groupId>no.nibio.vips</groupId> <artifactId>VIPSCommon</artifactId> - <version>2.0.3-SNAPSHOT</version> + <version>2.1.0</version> </dependency> <dependency> <groupId>javax</groupId> diff --git a/src/main/java/no/nibio/vips/logic/VIPSLogicApplication.java b/src/main/java/no/nibio/vips/logic/VIPSLogicApplication.java index a2d59c2e342bea1b638b1cd3ef8fcb1c610369f3..68256d9a181b43c79ccd34f668c9cf7560e7c4ea 100755 --- a/src/main/java/no/nibio/vips/logic/VIPSLogicApplication.java +++ b/src/main/java/no/nibio/vips/logic/VIPSLogicApplication.java @@ -55,8 +55,8 @@ public class VIPSLogicApplication extends Application 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.service.ObservationService.class); + resources.add(no.nibio.vips.logic.service.ObservationTimeSeriesService.class); resources.add(no.nibio.vips.logic.service.ModelFormService.class); - resources.add(no.nibio.vips.logic.service.JacksonConfig.class); //resources.add(no.nibio.vips.logic.service.JSONBConfig.class); //resources.add(no.nibio.vips.coremanager.service.ManagerResourceImpl.class); diff --git a/src/main/java/no/nibio/vips/logic/controller/session/ObservationBean.java b/src/main/java/no/nibio/vips/logic/controller/session/ObservationBean.java index 212388ab9961fb819023385ac5fe2c9c947a4953..64bbec3af66d153652142c9ff242c7b18e556c32 100755 --- a/src/main/java/no/nibio/vips/logic/controller/session/ObservationBean.java +++ b/src/main/java/no/nibio/vips/logic/controller/session/ObservationBean.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018 NIBIO <http://www.nibio.no/>. + * Copyright (c) 2018 NIBIO <http://www.nibio.no/>. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -26,7 +26,7 @@ import com.ibm.icu.util.ULocale; import java.io.File; import java.io.IOException; import java.nio.file.Files; -import java.nio.file.Paths; +import java.nio.file.Paths; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.ArrayList; @@ -49,18 +49,8 @@ import javax.persistence.NoResultException; import javax.persistence.PersistenceContext; import javax.persistence.Query; import javax.servlet.http.HttpServletRequest; -import no.nibio.vips.logic.entity.CropCategory; -import no.nibio.vips.logic.entity.Gis; -import no.nibio.vips.logic.entity.Observation; -import no.nibio.vips.logic.entity.ObservationFormShortcut; -import no.nibio.vips.logic.entity.ObservationIllustration; -import no.nibio.vips.logic.entity.ObservationIllustrationPK; -import no.nibio.vips.logic.entity.ObservationStatusType; -import no.nibio.vips.logic.entity.Organism; -import no.nibio.vips.logic.entity.Organization; -import no.nibio.vips.logic.entity.PointOfInterest; -import no.nibio.vips.logic.entity.PolygonService; -import no.nibio.vips.logic.entity.VipsLogicUser; + +import no.nibio.vips.logic.entity.*; import no.nibio.vips.logic.i18n.SessionLocaleUtil; import no.nibio.vips.logic.util.SystemTime; import org.apache.commons.codec.binary.Base64; @@ -75,178 +65,161 @@ import org.wololo.geojson.FeatureCollection; import org.wololo.geojson.GeoJSONFactory; /** - * @copyright 2014-2020 <a href="http://www.nibio.no/">NIBIO</a> * @author Tor-Einar Skog <tor-einar.skog@nibio.no> + * @copyright 2014-2020 <a href="http://www.nibio.no/">NIBIO</a> */ @Stateless public class ObservationBean { - @PersistenceContext(unitName="VIPSLogic-PU") + private static Logger LOGGER = LoggerFactory.getLogger(ObservationBean.class); + @PersistenceContext(unitName = "VIPSLogic-PU") EntityManager em; - @EJB PointOfInterestBean pointOfInterestBean; @EJB UserBean userBean; - private static Logger LOGGER = LoggerFactory.getLogger(ObservationBean.class); - - public List<Observation> getObservations(Integer organizationId) - { - Organization organization = em.find(Organization.class,organizationId); + public List<Observation> getObservations(Integer organizationId) { + Organization organization = em.find(Organization.class, organizationId); List<Observation> observations = em.createNamedQuery("Observation.findByOrganizationId") .setParameter("organizationId", organization) .getResultList(); - + observations = this.getObservationsWithGeoInfo(observations); observations = this.getObservationsWithLocations(observations); observations = this.getObservationsWithObservers(observations); - - return observations; + + return observations; } - - public List<Observation> getObservations(Integer organizationId, Date periodStart, Date periodEnd) - { - Organization organization = em.find(Organization.class,organizationId); + + public List<Observation> getObservations(Integer organizationId, Date periodStart, Date periodEnd) { + Organization organization = em.find(Organization.class, organizationId); List<Observation> observations = em.createNamedQuery("Observation.findByOrganizationIdAndPeriod") .setParameter("organizationId", organization) .setParameter("start", periodStart) .setParameter("end", periodEnd) .getResultList(); - + observations = this.getObservationsWithGeoInfo(observations); observations = this.getObservationsWithLocations(observations); observations = this.getObservationsWithObservers(observations); - + return observations; } - - public List<Observation> getObservations(Integer organizationId, Integer statusTypeId) - { - Organization organization= em.find(Organization.class, organizationId); + + public List<Observation> getObservations(Integer organizationId, Integer statusTypeId) { + Organization organization = em.find(Organization.class, organizationId); /*List<VipsLogicUser> users = em.createNamedQuery("VipsLogicUser.findByOrganizationId", VipsLogicUser.class) .setParameter("organizationId", organization) .getResultList();*/ - + List<Observation> retVal = this.getObservationsWithGeoInfo(em.createNamedQuery("Observation.findByOrganizationIdAndStatusTypeId") .setParameter("organizationId", organization) .setParameter("statusTypeId", statusTypeId) .getResultList()); - + return retVal; } - - public List<Observation> getObservationsForUser(VipsLogicUser user) - { + + public List<Observation> getObservationsForUser(VipsLogicUser user) { List<Observation> retVal = this.getObservationsWithGeoInfo(em.createNamedQuery("Observation.findByUserId") .setParameter("userId", user.getUserId()) .getResultList()); - + retVal = this.getObservationsWithLocations(retVal); retVal = this.getObservationsWithObservers(retVal); - + return retVal; } - + public List<Observation> getObservationsLastEditedByUser(VipsLogicUser user) { List<Observation> retVal = this.getObservationsWithGeoInfo(em.createNamedQuery("Observation.findByLastEditedBy") .setParameter("lastEditedBy", user.getUserId()) .getResultList()); - + retVal = this.getObservationsWithLocations(retVal); retVal = this.getObservationsWithObservers(retVal); - + return retVal; } - + public List<Observation> getObservationsStatusChangedByUser(VipsLogicUser user) { List<Observation> retVal = this.getObservationsWithGeoInfo(em.createNamedQuery("Observation.findByStatusChangedByUserId") .setParameter("statusChangedByUserId", user.getUserId()) .getResultList()); - + retVal = this.getObservationsWithLocations(retVal); retVal = this.getObservationsWithObservers(retVal); - + return retVal; } - - public List<Observation> getObservationsForUser(VipsLogicUser user, Date periodStart, Date periodEnd) - { + + public List<Observation> getObservationsForUser(VipsLogicUser user, Date periodStart, Date periodEnd) { List<Observation> retVal = this.getObservationsWithGeoInfo(em.createNamedQuery("Observation.findByUserIdAndPeriod") .setParameter("userId", user.getUserId()) .setParameter("start", periodStart) .setParameter("end", periodEnd) .getResultList()); - + retVal = this.getObservationsWithLocations(retVal); retVal = this.getObservationsWithObservers(retVal); - + return retVal; } - - public List<Observation> getObservationsForUser(VipsLogicUser user, Integer statusTypeId) - { + + public List<Observation> getObservationsForUser(VipsLogicUser user, Integer statusTypeId) { List<Observation> retVal = this.getObservationsWithGeoInfo(em.createNamedQuery("Observation.findByUserIdAndStatusTypeId") .setParameter("userId", user.getUserId()) .setParameter("statusTypeId", statusTypeId) .getResultList()); - + return retVal; } - - public Observation getObservation(Integer observationId) - { + + public Observation getObservation(Integer observationId) { Observation retVal = em.find(Observation.class, observationId); - if(retVal != null) - { + if (retVal != null) { retVal.setGeoinfos(this.getGeoinfoForObservation(retVal)); retVal.setUser(em.find(VipsLogicUser.class, retVal.getUserId())); - if(retVal.getLastEditedBy() != null) - { + if (retVal.getLastEditedBy() != null) { retVal.setLastEditedByUser(em.find(VipsLogicUser.class, retVal.getLastEditedBy())); } } return retVal; } - - public List<Gis> getGeoinfoForObservation(Observation obs) - { + + public List<Gis> getGeoinfoForObservation(Observation obs) { List<Integer> gisIds = em.createNativeQuery("SELECT gis_id FROM public.gis_observation WHERE observation_id = :observationId") .setParameter("observationId", obs.getObservationId()) .getResultList(); - - if(gisIds != null && ! gisIds.isEmpty()) - { - return em.createNamedQuery("Gis.findByGisIds",Gis.class) + + if (gisIds != null && !gisIds.isEmpty()) { + return em.createNamedQuery("Gis.findByGisIds", Gis.class) .setParameter("gisIds", gisIds) .getResultList(); - } - else - { + } else { return null; } } - - public List<Observation> getObservationsWithGeoInfo(List<Observation> observations) - { - if(observations.isEmpty()) - { + + public List<Observation> getObservationsWithGeoInfo(List<Observation> observations) { + if (observations.isEmpty()) { return observations; } - + // Using this method as opposed to db query for each observation, we // slim the time down from 24 seconds to 270 milliseconds! DB connections are expensive.... - + // Indexing observations Map<Integer, Observation> obsBucket = new HashMap<>(); observations.stream().forEach((obs) -> { obsBucket.put(obs.getObservationId(), obs); }); - + // Getting many-to-many relations Query q = em.createNativeQuery("SELECT gis_id, observation_id FROM public.gis_observation WHERE observation_id IN :observationIds"); q.setParameter("observationIds", obsBucket.keySet()); List<Object[]> gisObservationIds = q.getResultList(); - + // Collecting and indexing geoInfo Query q2 = em.createNativeQuery("SELECT * FROM public.gis WHERE gis_id IN (SELECT gis_id FROM public.gis_observation WHERE observation_id IN :observationIds)", Gis.class); List<Gis> geoInfos = q2.setParameter("observationIds", obsBucket.keySet()).getResultList(); @@ -254,7 +227,7 @@ public class ObservationBean { geoInfos.stream().forEach((geoinfo) -> { gisBucket.put(geoinfo.getGisId(), geoinfo); }); - + // Iterating the many-to-many relations, // adding geoinfo to the correct observations gisObservationIds.stream().forEach((gisObsIds) -> { @@ -262,12 +235,11 @@ public class ObservationBean { Integer observationId = (Integer) gisObsIds[1]; obsBucket.get(observationId).addGeoInfo(gisBucket.get(gisId)); }); - + return observations; } /** - * * @param observation * @return The merged object */ @@ -283,14 +255,13 @@ public class ObservationBean { .setParameter("observationId", retVal.getObservationId()) .executeUpdate(); // Then persist the new ones - if(observation.getGeoinfos() != null && ! observation.getGeoinfos().isEmpty()) - { + if (observation.getGeoinfos() != null && !observation.getGeoinfos().isEmpty()) { observation.getGeoinfos().stream().forEach((gis) -> { em.persist(gis); }); - + Query q = em.createNativeQuery("INSERT INTO public.gis_observation(gis_id,observation_id) VALUES(:gisId,:observationId)") - .setParameter("observationId", retVal.getObservationId()); + .setParameter("observationId", retVal.getObservationId()); observation.getGeoinfos().stream().forEach((gis) -> { q.setParameter("gisId", gis.getGisId()) .executeUpdate(); @@ -304,54 +275,49 @@ public class ObservationBean { em.persist(gis); gises.add(gis); }*/ - + // The GisObservations are not included in the merged object, so we should add them retVal.setGeoinfos(this.getGeoinfoForObservation(retVal)); - + return retVal; } public void deleteObservation(Integer observationId) { Observation observation = em.find(Observation.class, observationId); - if(observation != null) - { - // Delete all current group memberships + if (observation != null) { + // Delete all current group memberships em.createNativeQuery("DELETE FROM public.organization_group_observation WHERE observation_id=:observationId") .setParameter("observationId", observation.getObservationId()) .executeUpdate(); - + // Delete all illustrations (including removing files on disk) String[] filesToDelete = observation.getObservationIllustrationSet().stream() - .map(ill->ill.getObservationIllustrationPK().getFileName()) - .collect(Collectors.toList()) - .toArray(new String[0]); + .map(ill -> ill.getObservationIllustrationPK().getFileName()) + .collect(Collectors.toList()) + .toArray(new String[0]); this.deleteObservationIllustration(observation, filesToDelete); em.remove(observation); } } /** - * * @param organizationId * @param season - * @return + * @return */ public List<Observation> getBroadcastObservations(Integer organizationId, Integer season) { - Organization organization= em.find(Organization.class, organizationId); + Organization organization = em.find(Organization.class, organizationId); /*List<VipsLogicUser> users = em.createNamedQuery("VipsLogicUser.findByOrganizationId", VipsLogicUser.class) .setParameter("organizationId", organization) .getResultList();*/ List<Observation> retVal = null; - if(season == null) - { - retVal = this.getObservationsWithGeoInfo(em.createNamedQuery("Observation.findByOrganizationIdAndStatusTypeIdAndBroadcastMessage") + if (season == null) { + retVal = this.getObservationsWithGeoInfo(em.createNamedQuery("Observation.findByOrganizationIdAndStatusTypeIdAndBroadcastMessage") .setParameter("organizationId", organization) .setParameter("statusTypeId", Observation.STATUS_TYPE_ID_APPROVED) .getResultList()); - } - else - { + } else { Calendar cal = Calendar.getInstance(); cal.set(season, Calendar.JANUARY, 1, 0, 0, 0); Date start = cal.getTime(); @@ -361,31 +327,29 @@ public class ObservationBean { } return retVal; } - + /** - * * @param organizationId - * @param start When period starts. Default: Jan 1st 2000 - * @param end When period ends. Default: 100 years from now - * @return + * @param start When period starts. Default: Jan 1st 2000 + * @param end When period ends. Default: 100 years from now + * @return */ public List<Observation> getBroadcastObservations(Integer organizationId, Date start, Date end) { - if(start == null || end == null) - { + if (start == null || end == null) { Calendar cal = Calendar.getInstance(); - if(start == null) // Default Jan 1st 2000 + if (start == null) // Default Jan 1st 2000 { - cal.set(2000, Calendar.JANUARY,1,0,0,0); + cal.set(2000, Calendar.JANUARY, 1, 0, 0, 0); start = cal.getTime(); } - if(end == null) // Default: Today + 100 years + if (end == null) // Default: Today + 100 years { cal.setTime(SystemTime.getSystemTime()); cal.add(Calendar.YEAR, 100); end = cal.getTime(); } } - Organization organization= em.find(Organization.class, organizationId); + Organization organization = em.find(Organization.class, organizationId); return this.getObservationsWithGeoInfo(em.createNamedQuery("Observation.findByOrganizationIdAndStatusTypeIdAndBroadcastMessageAndPeriod") .setParameter("organizationId", organization) .setParameter("statusTypeId", Observation.STATUS_TYPE_ID_APPROVED) @@ -393,45 +357,42 @@ public class ObservationBean { .setParameter("end", end) .getResultList()); } - - + + /** - * * @param observation * @return [OBSERVATION_ILLUSTRATION_PATH]/[ORGANISM_ID]/ */ - private String getFilePath(Observation observation) - { - return System.getProperty("no.nibio.vips.logic.OBSERVATION_ILLUSTRATION_PATH") + "/" - + observation.getOrganismId(); - } - - public Observation storeObservationIllustration(Observation observation, String fileName, String base64Data) - { - String[] metaAndData = base64Data.split(","); - byte[] imageData = Base64.decodeBase64(metaAndData[1]); - Path path = Paths.get(this.getFilePath(observation) + "/" + fileName); - try { - // Make sure the directory exists - File testDirectoryfile = new File(this.getFilePath(observation)); - if(!testDirectoryfile.exists()) - { - testDirectoryfile.mkdirs(); - } - Files.write(path,imageData, StandardOpenOption.CREATE); - ObservationIllustration newIllustration = new ObservationIllustration(new ObservationIllustrationPK(observation.getObservationId(), fileName)); - newIllustration = em.merge(newIllustration); - - // Add the new illustration - if(observation.getObservationIllustrationSet() == null) - { - observation.setObservationIllustrationSet(new HashSet<ObservationIllustration>()); - } - observation.getObservationIllustrationSet().add(newIllustration); - - return observation; - } - catch(IOException ex) {ex.printStackTrace(); return observation;} + private String getFilePath(Observation observation) { + return System.getProperty("no.nibio.vips.logic.OBSERVATION_ILLUSTRATION_PATH") + "/" + + observation.getOrganismId(); + } + + public Observation storeObservationIllustration(Observation observation, String fileName, String base64Data) { + String[] metaAndData = base64Data.split(","); + byte[] imageData = Base64.decodeBase64(metaAndData[1]); + Path path = Paths.get(this.getFilePath(observation) + "/" + fileName); + try { + // Make sure the directory exists + File testDirectoryfile = new File(this.getFilePath(observation)); + if (!testDirectoryfile.exists()) { + testDirectoryfile.mkdirs(); + } + Files.write(path, imageData, StandardOpenOption.CREATE); + ObservationIllustration newIllustration = new ObservationIllustration(new ObservationIllustrationPK(observation.getObservationId(), fileName)); + newIllustration = em.merge(newIllustration); + + // Add the new illustration + if (observation.getObservationIllustrationSet() == null) { + observation.setObservationIllustrationSet(new HashSet<ObservationIllustration>()); + } + observation.getObservationIllustrationSet().add(newIllustration); + + return observation; + } catch (IOException ex) { + ex.printStackTrace(); + return observation; + } } public Observation storeObservationIllustration(Observation observation, FileItem item) throws Exception { @@ -442,32 +403,29 @@ public class ObservationBean { // Check availability, and adapt filename until available Integer fileNameSuffix = 1; File illustration = new File(filePath + "/" + fileName); - while(illustration.exists()) - { + while (illustration.exists()) { fileName = observation.getObservationId() + "_illustration_" + fileNameSuffix + "." + FilenameUtils.getExtension(item.getName()); illustration = new File(filePath + "/" + fileName); fileNameSuffix++; } File testDirectoryfile = new File(filePath); // If directory does not exist, create it - if(!testDirectoryfile.exists()) - { + if (!testDirectoryfile.exists()) { testDirectoryfile.mkdirs(); } // Store file item.write(illustration); - + // Update MessageIllustrations observation = em.merge(observation); // Remove the old illustration(s) - + ObservationIllustration newIllustration = new ObservationIllustration(new ObservationIllustrationPK(observation.getObservationId(), fileName)); em.persist(newIllustration); - + // Add the new illustration - if(observation.getObservationIllustrationSet() == null) - { + if (observation.getObservationIllustrationSet() == null) { observation.setObservationIllustrationSet(new HashSet<ObservationIllustration>()); } observation.getObservationIllustrationSet().add(newIllustration); @@ -476,83 +434,78 @@ public class ObservationBean { public Observation deleteObservationIllustration(Observation observation, String[] deleteIllustrations) { observation = em.merge(observation); - - Set <ObservationIllustration> formerIllustrations = observation.getObservationIllustrationSet(); - if(formerIllustrations == null) - { - return observation; + + Set<ObservationIllustration> formerIllustrations = observation.getObservationIllustrationSet(); + if (formerIllustrations == null) { + return observation; } - - Set <ObservationIllustration> deleteThese = new HashSet<>(); - - for(String deleteIllustration:deleteIllustrations) - { - for(ObservationIllustration formerIllustration:formerIllustrations) - { - if(formerIllustration.getObservationIllustrationPK().getFileName() - .equals(deleteIllustration)) - { - deleteThese.add(formerIllustration); - } - } + + Set<ObservationIllustration> deleteThese = new HashSet<>(); + + for (String deleteIllustration : deleteIllustrations) { + for (ObservationIllustration formerIllustration : formerIllustrations) { + if (formerIllustration.getObservationIllustrationPK().getFileName() + .equals(deleteIllustration)) { + deleteThese.add(formerIllustration); + } + } + } + + for (ObservationIllustration ill : deleteThese) { + observation.getObservationIllustrationSet().remove(ill); + em.remove(ill); + // Physically remove it too + File fileToDelete = new File(this.getFilePath(observation) + "/" + ill.getObservationIllustrationPK().getFileName()); + fileToDelete.delete(); } - - for(ObservationIllustration ill: deleteThese) - { - observation.getObservationIllustrationSet().remove(ill); - em.remove(ill); - // Physically remove it too - File fileToDelete = new File(this.getFilePath(observation) + "/" + ill.getObservationIllustrationPK().getFileName()); - fileToDelete.delete(); - } return observation; } /** * Fetch observations of a particular organism at a particular place and period + * * @param organismId * @param pointOfInterestId * @param startDate * @param endDate - * @return + * @return */ public List<no.nibio.vips.observation.Observation> getObservations(Integer organismId, Integer pointOfInterestId, Date startDate, Date endDate) { /*System.out.println("organismId = " + organismId); System.out.println("pointOfInterestId = " + pointOfInterestId); System.out.println("period= " + startDate + "-" + endDate);*/ return em.createNativeQuery( - "SELECT * FROM public.observation " - + "WHERE organism_id = :organismId " - + "AND location_point_of_interest_id = :locationPointOfInterestId " - + "AND time_of_observation BETWEEN :startDate AND :endDate" - ,Observation.class - ) + "SELECT * FROM public.observation " + + "WHERE organism_id = :organismId " + + "AND location_point_of_interest_id = :locationPointOfInterestId " + + "AND time_of_observation BETWEEN :startDate AND :endDate" + , Observation.class + ) .setParameter("organismId", organismId) - .setParameter("locationPointOfInterestId",pointOfInterestId) + .setParameter("locationPointOfInterestId", pointOfInterestId) .setParameter("startDate", startDate) .setParameter("endDate", endDate) .getResultList(); } public List<Observation> getObservationsWithLocations(List<Observation> observations) { - Set<Integer> locationPointOfInterestIds = new HashSet<>(); - observations.stream().filter((o) -> (o.getLocationPointOfInterestId() != null)).forEach((o) -> { - locationPointOfInterestIds.add(o.getLocationPointOfInterestId()); + Set<Integer> locationPointOfInterestIds = new HashSet<>(); + observations.stream().filter((o) -> (o.getLocationPointOfInterestId() != null)).forEach((o) -> { + locationPointOfInterestIds.add(o.getLocationPointOfInterestId()); }); - // Nothing to do? - if(locationPointOfInterestIds.isEmpty()) - { + // Nothing to do? + if (locationPointOfInterestIds.isEmpty()) { return observations; } - List<PointOfInterest> pois = pointOfInterestBean.getPois(locationPointOfInterestIds); - Map<Integer, PointOfInterest> mappedPois = new HashMap<>(); - pois.stream().forEach((poi) -> { - mappedPois.put(poi.getPointOfInterestId(), poi); + List<PointOfInterest> pois = pointOfInterestBean.getPois(locationPointOfInterestIds); + Map<Integer, PointOfInterest> mappedPois = new HashMap<>(); + pois.stream().forEach((poi) -> { + mappedPois.put(poi.getPointOfInterestId(), poi); }); - observations.stream().filter((o) -> (o.getLocationPointOfInterestId() != null)).forEach((o) -> { - o.setLocation(mappedPois.get(o.getLocationPointOfInterestId())); + observations.stream().filter((o) -> (o.getLocationPointOfInterestId() != null)).forEach((o) -> { + o.setLocation(mappedPois.get(o.getLocationPointOfInterestId())); }); - return observations; + return observations; } private List<Observation> getObservationsWithObservers(List<Observation> observations) { @@ -561,42 +514,39 @@ public class ObservationBean { userIds.add(o.getUserId()); }); // Nothing to do? - if(userIds.isEmpty()) - { + if (userIds.isEmpty()) { return observations; } - List<VipsLogicUser> users = userBean.getUsers(userIds); - Map<Integer, VipsLogicUser> mappedUsers = new HashMap<>(); - users.stream().forEach((user) -> { - mappedUsers.put(user.getUserId(), user); + List<VipsLogicUser> users = userBean.getUsers(userIds); + Map<Integer, VipsLogicUser> mappedUsers = new HashMap<>(); + users.stream().forEach((user) -> { + mappedUsers.put(user.getUserId(), user); }); - observations.stream().filter((o) -> (o.getUserId() != null)).forEach((o) -> { - o.setUser(mappedUsers.get(o.getUserId())); + observations.stream().filter((o) -> (o.getUserId() != null)).forEach((o) -> { + o.setUser(mappedUsers.get(o.getUserId())); }); return observations; } - - public List<Observation> getObservationsOfPest(Integer pestOrganismId) - { - List <Observation> observations = - em.createNamedQuery("Observation.findByOrganism") - .setParameter("organism", em.find(Organism.class, pestOrganismId)) - .getResultList(); - + + public List<Observation> getObservationsOfPest(Integer pestOrganismId) { + List<Observation> observations = + em.createNamedQuery("Observation.findByOrganism") + .setParameter("organism", em.find(Organism.class, pestOrganismId)) + .getResultList(); + observations = this.getObservationsWithGeoInfo(observations); observations = this.getObservationsWithLocations(observations); observations = getObservationsWithObservers(observations); return observations; } - - public List<Observation> getObservationsOfPestForUser(VipsLogicUser user, Integer pestOrganismId) - { - List <Observation> observations = - em.createNamedQuery("Observation.findByUserIdAndOrganism") - .setParameter("userId", user.getUserId()) - .setParameter("organism", em.find(Organism.class, pestOrganismId)) - .getResultList(); - + + public List<Observation> getObservationsOfPestForUser(VipsLogicUser user, Integer pestOrganismId) { + List<Observation> observations = + em.createNamedQuery("Observation.findByUserIdAndOrganism") + .setParameter("userId", user.getUserId()) + .setParameter("organism", em.find(Organism.class, pestOrganismId)) + .getResultList(); + observations = this.getObservationsWithGeoInfo(observations); observations = this.getObservationsWithLocations(observations); observations = this.getObservationsWithObservers(observations); @@ -604,60 +554,52 @@ public class ObservationBean { } public List<Observation> getFilteredObservations( - Integer organizationId, - Integer pestId, - Integer cropId, + Integer organizationId, + Integer pestId, + Integer cropId, List<Integer> cropCategoryId, - Date from, + Date from, Date to, Boolean isPositive - ) - { + ) { // The minimum SQL String sql = "SELECT * FROM public.observation \n" + - "WHERE status_type_id = :statusTypeId \n " + - "AND user_id IN (SELECT user_id FROM public.vips_logic_user WHERE organization_id = :organizationId) \n"; - + "WHERE status_type_id = :statusTypeId \n " + + "AND user_id IN (SELECT user_id FROM public.vips_logic_user WHERE organization_id = :organizationId) \n"; + Map<String, Object> parameters = new HashMap<>(); parameters.put("statusTypeId", ObservationStatusType.STATUS_APPROVED); parameters.put("organizationId", organizationId); - + // Filter for pest - if(pestId != null && pestId > 0) - { + if (pestId != null && pestId > 0) { sql += "AND organism_id = :organismId \n"; parameters.put("organismId", pestId); } // Filter either for crop or cropCategoryId - if(cropId != null && cropId > 0) - { + if (cropId != null && cropId > 0) { sql += "AND crop_organism_id = :cropOrganismId \n"; parameters.put("cropOrganismId", cropId); - } - else if(cropCategoryId != null && ! cropCategoryId.isEmpty()) - { + } else if (cropCategoryId != null && !cropCategoryId.isEmpty()) { List<CropCategory> cropCategories = em.createNamedQuery("CropCategory.findByCropCategoryIds", CropCategory.class) .setParameter("cropCategoryIds", cropCategoryId) .getResultList(); - List<Integer> cropIds = new ArrayList(cropCategories.stream().flatMap(cC->Arrays.asList(cC.getCropOrganismIds()).stream()).collect(Collectors.toSet())); - + List<Integer> cropIds = new ArrayList(cropCategories.stream().flatMap(cC -> Arrays.asList(cC.getCropOrganismIds()).stream()).collect(Collectors.toSet())); + sql += "AND crop_organism_id IN (:cropOrganismIds) \n"; parameters.put("cropOrganismIds", cropIds); } // Filter for dates - if(from != null) - { + if (from != null) { sql += "AND time_of_observation >= :from \n"; parameters.put("from", from); } - if(to != null) - { + if (to != null) { sql += "AND time_of_observation <= :to \n"; parameters.put("to", to); } // Filter for positive/negative registrations - if(isPositive != null) - { + if (isPositive != null) { sql += "AND is_positive = :isPositive \n"; parameters.put("isPositive", isPositive); } @@ -667,24 +609,26 @@ public class ObservationBean { Query q = em.createNativeQuery(sql, Observation.class); // Setting the parameters one by one parameters.keySet().stream().forEach( - (key)->{LOGGER.debug(key + ": " + parameters.get(key)); q.setParameter(key, parameters.get(key));} + (key) -> { + LOGGER.debug(key + ": " + parameters.get(key)); + q.setParameter(key, parameters.get(key)); + } ); - + //Date start = new Date(); - + List<Observation> observations = q.getResultList(); //System.out.println("Finding obs took " + (new Date().getTime() - start.getTime()) + " milliseconds"); - + //start = new Date(); observations.stream().forEach( - (observation)->observation.setUser(em.find(VipsLogicUser.class, observation.getUserId())) + (observation) -> observation.setUser(em.find(VipsLogicUser.class, observation.getUserId())) ); - + //System.out.println("Finding users took " + (new Date().getTime() - start.getTime()) + " milliseconds"); - + List<Observation> retVal = new ArrayList<>(); - if(! observations.isEmpty()) - { + if (!observations.isEmpty()) { //Date start = new Date(); retVal = this.getObservationsWithGeoInfo(observations); //System.out.println("Finding geoinfo took " + (new Date().getTime() - start.getTime()) + " milliseconds"); @@ -693,9 +637,9 @@ public class ObservationBean { //System.out.println("Finding locations took " + (new Date().getTime() - start.getTime()) + " milliseconds"); } - + return retVal; - + } public List<Organism> getObservedPests(Integer organizationId) { @@ -706,9 +650,9 @@ public class ObservationBean { return em.createNamedQuery("Organism.findByOrganismIds") .setParameter("organismIds", pestIds) .getResultList(); - + } - + public List<Organism> getObservedCrops(Integer organizationId) { Query q = em.createNativeQuery("SELECT DISTINCT crop_organism_id FROM public.observation WHERE user_id IN (" + " SELECT user_id FROM vips_logic_user WHERE organization_id = :organizationId" @@ -717,7 +661,7 @@ public class ObservationBean { return em.createNamedQuery("Organism.findByOrganismIds") .setParameter("organismIds", cropIds) .getResultList(); - + } public Observation getObservationFromGeoJSON(String geoJSON) throws IOException { @@ -727,23 +671,22 @@ public class ObservationBean { Map<String, Object> properties = firstAndBest.getProperties(); Observation observation = new Observation(); Integer observationId = (Integer) properties.get("observationId"); - if(observationId > 0) - { + if (observationId > 0) { observation = em.find(Observation.class, observationId); } observation.setObservationData((String) properties.get("observationData")); ObjectMapper mapper = new ObjectMapper(); observation.setCropOrganism(mapper.convertValue(properties.get("cropOrganism"), Organism.class)); observation.setOrganism(mapper.convertValue(properties.get("organism"), Organism.class)); - observation.setObservationHeading((String)properties.get("observationHeading")); - observation.setObservationText((String)properties.get("observationText")); + observation.setObservationHeading((String) properties.get("observationHeading")); + observation.setObservationText((String) properties.get("observationText")); observation.setTimeOfObservation(new Date((Long) properties.get("timeOfObservation"))); observation.setGeoinfo(geoJSON); observation.setStatusTypeId((Integer) properties.get("statusTypeId")); observation.setStatusRemarks((String) properties.get("statusRemarks")); observation.setIsQuantified((Boolean) properties.get("isQuantified")); observation.setBroadcastMessage((Boolean) properties.get("broadcastMessage")); - return observation; + return observation; } public void deleteGisObservationByGis(Integer gisId) { @@ -759,41 +702,34 @@ public class ObservationBean { .getResultList(); } - public List<Integer> getOrganizationGroupIds(Observation observation) { - if(observation.getObservationId() != null) - { + public List<Integer> getOrganizationGroupIds(Observation observation) { + if (observation.getObservationId() != null) { return em.createNativeQuery("SELECT organization_group_id FROM public.organization_group_observation " - + "WHERE observation_id = :observationId") + + "WHERE observation_id = :observationId") .setParameter("observationId", observation.getObservationId()) .getResultList(); - } - else - { + } else { return new ArrayList<>(); } } - + public void storeOrganizationGroupObservationIds(Observation obs, String[] organizationGroupIds) { // First delete all current group memberships em.createNativeQuery("DELETE FROM public.organization_group_observation WHERE observation_id=:observationId") .setParameter("observationId", obs.getObservationId()) .executeUpdate(); - - if(organizationGroupIds != null) - { + + if (organizationGroupIds != null) { Query q = em.createNativeQuery("INSERT INTO public.organization_group_observation (organization_group_id, observation_id) " - + "VALUES(:organizationGroupId, :observationId)") + + "VALUES(:organizationGroupId, :observationId)") .setParameter("observationId", obs.getObservationId()); // Then add - for(String groupIdStr:organizationGroupIds) - { + for (String groupIdStr : organizationGroupIds) { try { Integer groupId = Integer.valueOf(groupIdStr); q.setParameter("organizationGroupId", groupId); q.executeUpdate(); - } - catch(NumberFormatException ex) - { + } catch (NumberFormatException ex) { // Continue } } @@ -802,30 +738,27 @@ public class ObservationBean { /** * Returns the first time an observation of the given pest registered in the system was made + * * @param organismId - * @return + * @return */ public Date getFirstObservationTime(Integer organismId) { - - try - { - List<Observation> obs = em.createNamedQuery("Observation.findFirstByOrganism") + + try { + List<Observation> obs = em.createNamedQuery("Observation.findFirstByOrganism") .setParameter("organism", em.find(Organism.class, organismId)) .getResultList(); return obs.get(0).getTimeOfObservation(); - } - catch(NoResultException | IndexOutOfBoundsException ex) - { + } catch (NoResultException | IndexOutOfBoundsException ex) { return null; } - + } - public PolygonService getPolygonService(Integer polygonServiceId) - { - return em.find(PolygonService.class, polygonServiceId); + public PolygonService getPolygonService(Integer polygonServiceId) { + return em.find(PolygonService.class, polygonServiceId); } - + public List<PolygonService> getPolygonServicesForOrganization(Integer organizationId) { return em.createNativeQuery("SELECT * FROM polygon_service p WHERE p.polygon_service_id IN (SELECT polygon_service_id FROM public.organization_polygon_service WHERE organization_id=:organizationId)", PolygonService.class) .setParameter("organizationId", organizationId) @@ -840,7 +773,8 @@ public class ObservationBean { /** * Part of the cleaning up dependencies procedure for when deleting a POI - * @param poi + * + * @param poi */ public void deleteObservationsForLocation(PointOfInterest poi) { em.createNamedQuery("Observation.findByLocationPointOfInterestId", Observation.class) @@ -848,99 +782,96 @@ public class ObservationBean { .getResultList().stream() .forEach(obs -> em.remove(obs)); } - + + public void deleteObservationsForObservationTimeSeries(ObservationTimeSeries observationTimeSeries) { + em.createNamedQuery("Observation.findByObservationTimeSeries", Observation.class) + .setParameter("observationTimeSeries", observationTimeSeries) + .getResultList() + .forEach(obs -> em.remove(obs)); + } + /** - * Returns the appropriate observation data schema + * Returns the appropriate observation data schema * If no existing schema, returns the standard (Requring just a number) + * * @param organizationId * @param organismId - * @param httpServletRequest - * @return + * @return */ - public ObservationDataSchema getObservationDataSchema(Integer organizationId, Integer organismId) - { - try - { + public ObservationDataSchema getObservationDataSchema(Integer organizationId, Integer organismId) { + try { return em.createNamedQuery("ObservationDataSchema.findByPK", ObservationDataSchema.class) - .setParameter("organizationId", organizationId) - .setParameter("organismId", organismId) - .getSingleResult(); - - - } - catch(NoResultException ex) - { + .setParameter("organizationId", organizationId) + .setParameter("organismId", organismId) + .getSingleResult(); + + + } catch (NoResultException ex) { //System.out.println("Could not find schema for orgId " + organizationId + " and organismId " + organismId); return this.getStandardSchema(organizationId); } } - + /** * If there exist title translations for this schema, it + * * @param schema * @param httpServletRequest - * @return + * @return */ - public ObservationDataSchema getLocalizedObservationDataSchema(ObservationDataSchema ods, HttpServletRequest httpServletRequest, ULocale locale) throws IOException - { - if(locale != null) - { - SessionLocaleUtil.setCurrentLocale(httpServletRequest, locale); + public ObservationDataSchema getLocalizedObservationDataSchema(ObservationDataSchema ods, HttpServletRequest httpServletRequest, ULocale locale) throws IOException { + if (locale != null) { + SessionLocaleUtil.setCurrentLocale(httpServletRequest, locale); } ResourceBundle bundle = SessionLocaleUtil.getI18nBundle(httpServletRequest); - - // We iterate the schema, replacing default field labels with - // translated ones - // First: Convert to Jackson JsonNode tree - ObjectMapper m = new ObjectMapper(); - JsonNode rootNode = m.readTree(ods.getDataSchema()); - // Is this the full schema or just the "properties" property? - JsonNode propertiesNode = rootNode.get("properties") == null ? rootNode : rootNode.get("properties"); - - Iterator<Entry<String, JsonNode>> nodeIterator = propertiesNode.fields(); - - String fieldKeyPrefix = "observationDataField_"; - // Loop through each field - while (nodeIterator.hasNext()) { - Map.Entry<String, JsonNode> schemaPropertyField = (Map.Entry<String, JsonNode>) nodeIterator.next(); - // Get the property field key (e.g. "counting2") - String fieldKey = schemaPropertyField.getKey(); - // Find a translation. - if(bundle.containsKey(fieldKeyPrefix + fieldKey)) - { - // If found, replace with translation - // Get the property field (e.g. {"title":"Counting 2"} ) - JsonNode schemaProperty = schemaPropertyField.getValue(); - ((ObjectNode)schemaProperty).put("title", bundle.getString(fieldKeyPrefix + fieldKey)); - ((ObjectNode)propertiesNode).replace(fieldKey, schemaProperty); - } - } - - // I repeat: Is this the full schema or just the "properties" property? - if(rootNode.get("properties") != null) - { - ((ObjectNode)rootNode).replace("properties", propertiesNode); - } - else - { - rootNode = propertiesNode; + + // We iterate the schema, replacing default field labels with + // translated ones + // First: Convert to Jackson JsonNode tree + ObjectMapper m = new ObjectMapper(); + JsonNode rootNode = m.readTree(ods.getDataSchema()); + // Is this the full schema or just the "properties" property? + JsonNode propertiesNode = rootNode.get("properties") == null ? rootNode : rootNode.get("properties"); + + Iterator<Entry<String, JsonNode>> nodeIterator = propertiesNode.fields(); + + String fieldKeyPrefix = "observationDataField_"; + // Loop through each field + while (nodeIterator.hasNext()) { + Map.Entry<String, JsonNode> schemaPropertyField = (Map.Entry<String, JsonNode>) nodeIterator.next(); + // Get the property field key (e.g. "counting2") + String fieldKey = schemaPropertyField.getKey(); + // Find a translation. + if (bundle.containsKey(fieldKeyPrefix + fieldKey)) { + // If found, replace with translation + // Get the property field (e.g. {"title":"Counting 2"} ) + JsonNode schemaProperty = schemaPropertyField.getValue(); + ((ObjectNode) schemaProperty).put("title", bundle.getString(fieldKeyPrefix + fieldKey)); + ((ObjectNode) propertiesNode).replace(fieldKey, schemaProperty); } - ods.setDataSchema(m.writeValueAsString(rootNode)); - return ods; + } + + // I repeat: Is this the full schema or just the "properties" property? + if (rootNode.get("properties") != null) { + ((ObjectNode) rootNode).replace("properties", propertiesNode); + } else { + rootNode = propertiesNode; + } + ods.setDataSchema(m.writeValueAsString(rootNode)); + return ods; } - + /** - * * @param organizationId - * @return + * @return */ - private ObservationDataSchema getStandardSchema(Integer organizationId){ + private ObservationDataSchema getStandardSchema(Integer organizationId) { ObservationDataSchema retVal = new ObservationDataSchema(); retVal.setDataSchema("{\n" - + " \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n" - + " \"type\": \"object\",\n" - + " \"title\": \"Default schema\",\n" - + " \"properties\": {" + + " \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n" + + " \"type\": \"object\",\n" + + " \"title\": \"Default schema\",\n" + + " \"properties\": {" + "\"number\":{\"title\":\"Number\"}," + "\"unit\":{\"title\":\"Unit\"}" + "}" @@ -955,7 +886,6 @@ public class ObservationBean { retVal.setObservationDataSchemaPK(pk); return retVal; } - - + } diff --git a/src/main/java/no/nibio/vips/logic/controller/session/ObservationTimeSeriesBean.java b/src/main/java/no/nibio/vips/logic/controller/session/ObservationTimeSeriesBean.java new file mode 100644 index 0000000000000000000000000000000000000000..be5bd46f578d71bc1a00fd800f840a2ff86281f8 --- /dev/null +++ b/src/main/java/no/nibio/vips/logic/controller/session/ObservationTimeSeriesBean.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2018 NIBIO <http://www.nibio.no/>. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + */ + +package no.nibio.vips.logic.controller.session; + +import no.nibio.vips.logic.entity.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ejb.EJB; +import javax.ejb.Stateless; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; +import java.util.*; + +@Stateless +public class ObservationTimeSeriesBean { + + @PersistenceContext(unitName = "VIPSLogic-PU") + EntityManager em; + @EJB + PointOfInterestBean pointOfInterestBean; + @EJB + UserBean userBean; + @EJB + ObservationBean observationBean; + + public List<ObservationTimeSeries> getObservationTimeSeriesListForUser(VipsLogicUser user) { + List<ObservationTimeSeries> resultList = em.createNamedQuery("ObservationTimeSeries.findByUserId", ObservationTimeSeries.class) + .setParameter("userId", user.getUserId()) + .getResultList(); + this.enrichObservationTimeSeriesListWithPointOfInterest(resultList); + this.enrichObservationTimeSeriesListWithObservers(resultList); + return resultList; + } + + public ObservationTimeSeries getObservationTimeSeries(Integer id) { + ObservationTimeSeries ots = em.find(ObservationTimeSeries.class, id); + if (ots != null) { + ots.setUser(em.find(VipsLogicUser.class, ots.getUserId())); + if (ots.getLastModifiedBy() != null) { + ots.setLastModifiedByUser(em.find(VipsLogicUser.class, ots.getLastModifiedBy())); + } + } + return ots; + } + + /** + * @param ots the observation time series + * @return The merged object + */ + public ObservationTimeSeries storeObservationTimeSeries(ObservationTimeSeries ots) { + return em.merge(ots); + } + + public void deleteObservationTimeSeries(Integer id) { + ObservationTimeSeries observationTimeSeries = em.find(ObservationTimeSeries.class, id); + if (observationTimeSeries != null) { + // The app prevents deletion of time series with observations + observationBean.deleteObservationsForObservationTimeSeries(observationTimeSeries); + em.remove(observationTimeSeries); + } + } + + /** + * Enrich given list of observation time series with point of interest information + * + * @param otsList The list of observation time series to enrich + */ + public void enrichObservationTimeSeriesListWithPointOfInterest(List<ObservationTimeSeries> otsList) { + Set<Integer> locationPoiIds = new HashSet<>(); + otsList.stream().filter((o) -> (o.getLocationPointOfInterestId() != null)).forEach((o) -> { + locationPoiIds.add(o.getLocationPointOfInterestId()); + }); + if (locationPoiIds.isEmpty()) { + return; + } + List<PointOfInterest> pois = pointOfInterestBean.getPois(locationPoiIds); + Map<Integer, PointOfInterest> mappedPois = new HashMap<>(); + pois.stream().forEach((poi) -> { + mappedPois.put(poi.getPointOfInterestId(), poi); + }); + otsList.stream().filter((o) -> (o.getLocationPointOfInterestId() != null)).forEach((o) -> { + o.setLocationPointOfInterest(mappedPois.get(o.getLocationPointOfInterestId())); + }); + } + + public void enrichObservationTimeSeriesWithPointOfInterest(ObservationTimeSeries ots) { + if (ots == null || ots.getLocationPointOfInterestId() == null) { + return; + } + ots.setLocationPointOfInterest(pointOfInterestBean.getPointOfInterest(ots.getLocationPointOfInterestId())); + } + + /** + * Enrich given list of observation time series with user information + * + * @param otsList The list of observation time series to enrich + */ + private void enrichObservationTimeSeriesListWithObservers(List<ObservationTimeSeries> otsList) { + Set<Integer> userIds = new HashSet<>(); + otsList.stream().filter((o) -> (o.getUserId() != null)).forEach((o) -> { + userIds.add(o.getUserId()); + }); + if (userIds.isEmpty()) { + return; + } + List<VipsLogicUser> users = userBean.getUsers(userIds); + Map<Integer, VipsLogicUser> mappedUsers = new HashMap<>(); + users.stream().forEach((user) -> { + mappedUsers.put(user.getUserId(), user); + }); + otsList.stream().filter((o) -> (o.getUserId() != null)).forEach((o) -> { + o.setUser(mappedUsers.get(o.getUserId())); + }); + } +} diff --git a/src/main/java/no/nibio/vips/logic/entity/Observation.java b/src/main/java/no/nibio/vips/logic/entity/Observation.java index cd35d8f8f44239a720899d9377f517f12282a9d1..b0b092d053042e9187d30be2022dd9108e84cc7f 100755 --- a/src/main/java/no/nibio/vips/logic/entity/Observation.java +++ b/src/main/java/no/nibio/vips/logic/entity/Observation.java @@ -19,20 +19,7 @@ package no.nibio.vips.logic.entity; import java.io.Serializable; import java.util.Date; -import javax.persistence.Basic; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.NamedQueries; -import javax.persistence.NamedQuery; -import javax.persistence.Table; -import javax.persistence.Temporal; -import javax.persistence.TemporalType; -import javax.persistence.Transient; +import javax.persistence.*; import javax.validation.constraints.NotNull; import javax.xml.bind.annotation.XmlRootElement; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -41,9 +28,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; -import javax.persistence.CascadeType; -import javax.persistence.FetchType; -import javax.persistence.OneToMany; import javax.validation.constraints.Size; import no.nibio.vips.logic.util.GISEntityUtil; import no.nibio.vips.gis.GISUtil; @@ -62,12 +46,14 @@ import org.hibernate.annotations.TypeDefs; @Table(name = "observation") @XmlRootElement @TypeDefs( {@TypeDef( name= "StringJsonObject", typeClass = StringJsonUserType.class)}) + @NamedQueries({ @NamedQuery(name = "Observation.findAll", query = "SELECT o FROM Observation o"), @NamedQuery(name = "Observation.findByObservationId", query = "SELECT o FROM Observation o WHERE o.observationId = :observationId"), @NamedQuery(name = "Observation.findByUserId", query = "SELECT o FROM Observation o WHERE o.userId IN(:userId)"), @NamedQuery(name = "Observation.findByLastEditedBy", query = "SELECT o FROM Observation o WHERE o.lastEditedBy IN(:lastEditedBy)"), @NamedQuery(name = "Observation.findByLocationPointOfInterestId", query = "SELECT o FROM Observation o WHERE o.locationPointOfInterestId = :locationPointOfInterestId"), + @NamedQuery(name = "Observation.findByObservationTimeSeries", query = "SELECT o FROM Observation o WHERE o.observationTimeSeries = :observationTimeSeries"), @NamedQuery(name = "Observation.findByStatusChangedByUserId", query = "SELECT o FROM Observation o WHERE o.statusChangedByUserId IN(:statusChangedByUserId)"), @NamedQuery(name = "Observation.findByUserIdAndPeriod", query = "SELECT o FROM Observation o WHERE o.timeOfObservation BETWEEN :start AND :end AND o.userId IN(:userId)"), @NamedQuery(name = "Observation.findByUserIdAndStatusTypeId", query = "SELECT o FROM Observation o WHERE o.userId IN(:userId) AND o.statusTypeId= :statusTypeId"), @@ -91,6 +77,7 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse private Integer userId; private Integer lastEditedBy; private List<Gis> geoinfo; + private ObservationTimeSeries observationTimeSeries; private Integer locationPointOfInterestId; //private Double observedValue; //private Integer denominator; @@ -120,9 +107,18 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse private GISEntityUtil GISEntityUtil; private GISUtil GISUtil; + // Should be defined as an enum (WEB/APP), but PostgreSQLEnumType and hibernate6 seems to be necessary + // https://stackoverflow.com/questions/50818649/hibernate-and-java-and-postgres-enumeratedvalue-enumtype-string-caused-by + private String source; + public Observation() { + this("WEB"); + } + + public Observation(String source) { this.GISEntityUtil = new GISEntityUtil(); this.GISUtil = new GISUtil(); + this.source = source; } public Observation(Integer observationId) { @@ -272,7 +268,38 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse @Override public String toString() { - return "no.nibio.vips.logic.entity.Observation[ observationId=" + observationId + " ]"; + return "Observation{" + + "observationId=" + observationId + + ", timeOfObservation=" + timeOfObservation + + ", organism=" + organism + + ", cropOrganism=" + cropOrganism + + ", userId=" + userId + + ", lastEditedBy=" + lastEditedBy + + ", geoinfo=" + geoinfo + + ", observationTimeSeries=" + observationTimeSeries + + ", locationPointOfInterestId=" + locationPointOfInterestId + + ", observationHeading='" + observationHeading + '\'' + + ", observationText='" + observationText + '\'' + + ", statusTypeId=" + statusTypeId + + ", statusChangedByUserId=" + statusChangedByUserId + + ", statusChangedTime=" + statusChangedTime + + ", lastEditedTime=" + lastEditedTime + + ", statusRemarks='" + statusRemarks + '\'' + + ", observationData='" + observationData + '\'' + + ", isQuantified=" + isQuantified + + ", isPositive=" + isPositive + + ", broadcastMessage=" + broadcastMessage + + ", locationIsPrivate=" + locationIsPrivate + + ", polygonService=" + polygonService + + ", observationDataSchema=" + observationDataSchema + + ", user=" + user + + ", lastEditedByUser=" + lastEditedByUser + + ", location=" + location + + ", observationIllustrationSet=" + observationIllustrationSet + + ", GISEntityUtil=" + GISEntityUtil + + ", GISUtil=" + GISUtil + + ", source=" + source + + '}'; } /** @@ -531,6 +558,22 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse this.observationIllustrationSet = observationIllustrationSet; } + /** + * @return the observation time series + */ + @JoinColumn(name = "observation_time_series_id", referencedColumnName = "observation_time_series_id") + @ManyToOne + public ObservationTimeSeries getObservationTimeSeries() { + return observationTimeSeries; + } + + /** + * @param observationTimeSeries the observation time series to set + */ + public void setObservationTimeSeries(ObservationTimeSeries observationTimeSeries) { + this.observationTimeSeries = observationTimeSeries; + } + /** * @return the locationPointOfInterestId */ @@ -679,4 +722,12 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse public void setIsPositive(Boolean positive) { isPositive = positive; } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } } diff --git a/src/main/java/no/nibio/vips/logic/entity/ObservationTimeSeries.java b/src/main/java/no/nibio/vips/logic/entity/ObservationTimeSeries.java new file mode 100644 index 0000000000000000000000000000000000000000..b222732163440a174fb531b8d86a2fcfc4dd4ec4 --- /dev/null +++ b/src/main/java/no/nibio/vips/logic/entity/ObservationTimeSeries.java @@ -0,0 +1,390 @@ +/* + * Copyright (c) 2014 NIBIO <http://www.nibio.no/>. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + */ +package no.nibio.vips.logic.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import no.nibio.vips.logic.util.StringJsonUserType; +import org.hibernate.annotations.TypeDef; +import org.hibernate.annotations.TypeDefs; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; +import java.util.Date; + +@Entity +@Table(name = "observation_time_series") +@XmlRootElement +@TypeDefs({@TypeDef(name = "StringJsonObject", typeClass = StringJsonUserType.class)}) +@NamedQueries({ + @NamedQuery(name = "ObservationTimeSeries.findAll", query = "SELECT ots FROM ObservationTimeSeries ots"), + @NamedQuery(name = "ObservationTimeSeries.findByObservationTimeSeriesId", query = "SELECT ots FROM ObservationTimeSeries ots WHERE ots.observationTimeSeriesId = :id"), + @NamedQuery(name = "ObservationTimeSeries.findByOrganizationId", query = "SELECT ots FROM ObservationTimeSeries ots WHERE ots.userId IN(SELECT v.userId FROM VipsLogicUser v WHERE v.organizationId = :organizationId OR v.organizationId IN(SELECT o.organizationId FROM Organization o WHERE o.parentOrganizationId = :organizationId))"), + @NamedQuery(name = "ObservationTimeSeries.findByUserId", query = "SELECT ots FROM ObservationTimeSeries ots WHERE ots.userId IN(:userId)") +}) +public class ObservationTimeSeries implements Serializable { + + private static final long serialVersionUID = 1L; + private Integer observationTimeSeriesId; + private Organism cropOrganism; + private Organism organism; + private Integer year; + private String name; + private String description; + private Date created; + private Integer userId; + private VipsLogicUser user; // Transient + private String source; + private Date lastModified; + private Integer lastModifiedBy; + private VipsLogicUser lastModifiedByUser; // Transient + private Integer locationPointOfInterestId; + private PointOfInterest locationPointOfInterest; + private Boolean locationIsPrivate; + private PolygonService polygonService; + + public ObservationTimeSeries() { + } + + public ObservationTimeSeries(String source) { + this.source = source; + } + + /** + * @return the id of the observation time series + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Basic(optional = false) + @Column(name = "observation_time_series_id") + public Integer getObservationTimeSeriesId() { + return observationTimeSeriesId; + } + + /** + * @param id the observation time series id to set + */ + public void setObservationTimeSeriesId(Integer id) { + this.observationTimeSeriesId = id; + } + + + /** + * @return the crop organism + */ + @JoinColumn(name = "crop_organism_id", referencedColumnName = "organism_id") + @ManyToOne + public Organism getCropOrganism() { + return cropOrganism; + } + + /** + * @param cropOrganism the crop organism to set + */ + public void setCropOrganism(Organism cropOrganism) { + this.cropOrganism = cropOrganism; + } + + /** + * @return the crop organism id + */ + @Transient + public Integer getCropOrganismId() { + return this.getCropOrganism() != null ? this.getCropOrganism().getOrganismId() : null; + } + + /** + * @return the organism + */ + @JoinColumn(name = "organism_id", referencedColumnName = "organism_id") + @ManyToOne + public Organism getOrganism() { + return organism; + } + + /** + * @param organism the organism to set + */ + public void setOrganism(Organism organism) { + this.organism = organism; + } + + /** + * @return the organism id + */ + @Transient + public Integer getOrganismId() { + return this.getOrganism() != null ? this.getOrganism().getOrganismId() : null; + } + + /** + * @return the year of the observation time series + */ + @Column(name = "year") + public Integer getYear() { + return year; + } + + /** + * @param year the observation time series year to set + */ + public void setYear(Integer year) { + this.year = year; + } + + /** + * @return the name of the observation time series + */ + @Column(name = "name") + public String getName() { + return name; + } + + /** + * @param name the observation time series name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return the description of the observation time series + */ + @Column(name = "description") + public String getDescription() { + return description; + } + + /** + * @param description the observation time series text to set + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * @return the creation date of the observation time series + */ + @NotNull + @Basic(optional = false) + @Column(name = "created") + @Temporal(TemporalType.TIMESTAMP) + public Date getCreated() { + return created; + } + + /** + * @param created the creation date to set + */ + public void setCreated(Date created) { + this.created = created; + } + + @NotNull + @Basic(optional = false) + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "last_modified") + public Date getLastModified() { + return lastModified; + } + + public void setLastModified(Date lastModified) { + this.lastModified = lastModified; + } + + /** + * @return the userId + */ + @Column(name = "user_id") + public Integer getUserId() { + return userId; + } + + /** + * @param userId the userId to set + */ + public void setUserId(Integer userId) { + this.userId = userId; + } + + /** + * @return the user + */ + @Transient + @JsonIgnore + public VipsLogicUser getUser() { + return user; + } + + /** + * @param user the user to set + */ + public void setUser(VipsLogicUser user) { + this.user = user; + } + + /** + * @return from where the observation time series originally was created, either WEB or APP + */ + @Column(name = "source") + public String getSource() { + return source; + } + + /** + * @param source From where the observation time series originally was created + */ + public void setSource(String source) { + this.source = source; + } + + @Column(name = "last_modified_by") + public Integer getLastModifiedBy() { + return lastModifiedBy; + } + + /** + * @param lastModifiedBy the lastModifiedBy to set + */ + public void setLastModifiedBy(Integer lastModifiedBy) { + this.lastModifiedBy = lastModifiedBy; + } + + /** + * @return the user who last modified the observation time series + */ + @Transient + @JsonIgnore + public VipsLogicUser getLastModifiedByUser() { + return lastModifiedByUser; + } + + /** + * @param lastModifiedByUser the lastModifiedByUser to set + */ + public void setLastModifiedByUser(VipsLogicUser lastModifiedByUser) { + this.lastModifiedByUser = lastModifiedByUser; + } + + /** + * @return the locationPointOfInterestId + */ + @Column(name = "location_point_of_interest_id") + public Integer getLocationPointOfInterestId() { + return locationPointOfInterestId; + } + + /** + * @param locationPointOfInterestId the locationPointOfInterestId to set + */ + public void setLocationPointOfInterestId(Integer locationPointOfInterestId) { + this.locationPointOfInterestId = locationPointOfInterestId; + } + + /** + * @return the location + */ + @Transient + public PointOfInterest getLocationPointOfInterest() { + return locationPointOfInterest; + } + + /** + * @param locationPointOfInterest the location to set + */ + public void setLocationPointOfInterest(PointOfInterest locationPointOfInterest) { + this.locationPointOfInterest = locationPointOfInterest; + } + + /** + * @return the locationIsPrivate + */ + @Column(name = "location_is_private") + public Boolean getLocationIsPrivate() { + return locationIsPrivate != null ? locationIsPrivate : false; + } + + /** + * @param locationIsPrivate the locationIsPrivate to set + */ + public void setLocationIsPrivate(Boolean locationIsPrivate) { + this.locationIsPrivate = locationIsPrivate; + } + + /** + * @return the polygon service + */ + @JoinColumn(name = "polygon_service_id", referencedColumnName = "polygon_service_id") + @ManyToOne + public PolygonService getPolygonService() { + return this.polygonService; + } + + /** + * @param polygonService the polygon service to set + */ + public void setPolygonService(PolygonService polygonService) { + this.polygonService = polygonService; + } + + @Override + public int hashCode() { + int hash = 0; + hash += (observationTimeSeriesId != null ? observationTimeSeriesId.hashCode() : 0); + return hash; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ObservationTimeSeries that = (ObservationTimeSeries) o; + return observationTimeSeriesId.equals(that.observationTimeSeriesId); + } + + @Override + public String toString() { + return "ObservationTimeSeries{" + + "observationTimeSeriesId=" + observationTimeSeriesId + + ", cropOrganism=" + cropOrganism + + ", organism=" + organism + + ", year=" + year + + ", name='" + name + '\'' + + ", description='" + description + '\'' + + ", created=" + created + + ", userId=" + userId + + ", user=" + user + + ", source=" + source + + ", lastModified=" + lastModified + + ", lastModifiedBy=" + lastModifiedBy + + ", lastModifiedByUser=" + lastModifiedByUser + + ", locationPointOfInterestId=" + locationPointOfInterestId + + ", locationPointOfInterest=" + locationPointOfInterest + + ", locationIsPrivate=" + locationIsPrivate + + ", polygonService=" + polygonService + + '}'; + } + + public int compareTo(ObservationTimeSeries other) { + if (this.getYear() != null && other.getYear() != null) { + return other.getYear().compareTo(this.getYear()); + } + return this.getName().compareTo(other.getName()); + } +} diff --git a/src/main/java/no/nibio/vips/logic/service/ObservationService.java b/src/main/java/no/nibio/vips/logic/service/ObservationService.java index bb17d6b1dd03dbd8b9adcd22b9c9b3db4575cf15..a6a9604ba915a4f4207869b4818d19ba329175e9 100755 --- a/src/main/java/no/nibio/vips/logic/service/ObservationService.java +++ b/src/main/java/no/nibio/vips/logic/service/ObservationService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 NIBIO <http://www.nibio.no/>. + * Copyright (c) 2022 NIBIO <http://www.nibio.no/>. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -22,45 +22,11 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.ibm.icu.util.ULocale; import com.webcohesion.enunciate.metadata.rs.TypeHint; - -import java.io.IOException; -import java.net.URI; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.stream.Collectors; -import javax.ejb.EJB; -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.Consumes; -import javax.ws.rs.DELETE; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.client.Client; -import javax.ws.rs.client.ClientBuilder; -import javax.ws.rs.client.Entity; -import javax.ws.rs.client.WebTarget; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; import no.nibio.vips.gis.GISUtil; import no.nibio.vips.logic.controller.session.ObservationBean; +import no.nibio.vips.logic.controller.session.ObservationTimeSeriesBean; import no.nibio.vips.logic.controller.session.OrganismBean; import no.nibio.vips.logic.controller.session.UserBean; - import no.nibio.vips.logic.entity.*; import no.nibio.vips.logic.entity.rest.ObservationListItem; import no.nibio.vips.logic.entity.rest.PointMappingResponse; @@ -72,34 +38,52 @@ import org.jboss.resteasy.annotations.GZIP; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.Point; -import org.wololo.geojson.Feature; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.wololo.geojson.FeatureCollection; +import org.wololo.geojson.Feature; import org.wololo.geojson.GeoJSON; +import javax.ejb.EJB; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.*; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import java.io.IOException; +import java.net.URI; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.util.*; +import java.util.stream.Collectors; + /** - * @copyright 2016-2022 <a href="http://www.nibio.no/">NIBIO</a> * @author Tor-Einar Skog <tor-einar.skog@nibio.no> + * @copyright 2016-2022 <a href="http://www.nibio.no/">NIBIO</a> */ @Path("rest/observation") public class ObservationService { - + private final static Logger LOGGER = LoggerFactory.getLogger(ObservationService.class); - - @Context - private HttpServletRequest httpServletRequest; - @EJB UserBean userBean; @EJB ObservationBean observationBean; @EJB OrganismBean organismBean; + + @EJB + ObservationTimeSeriesBean observationTimeSeriesBean; @EJB MessagingBean messagingBean; - + @Context + private HttpServletRequest httpServletRequest; + /* * PostGIS tip: * How to query for observations within a bounding box @@ -107,16 +91,15 @@ public class ObservationService { * ST_SetSRID(ST_MakeBox2D(ST_MakePoint(2.9004, 57.7511), ST_MakePoint(32.4316, 71.3851)),4326), * gis_geom); * First point is SW, last is NE (but could be anything?) - */ - + */ + /** - * * @param organizationId Database ID of the organization - * @param pestId Database ID of the pest - * @param cropId Database ID of the crop + * @param pestId Database ID of the pest + * @param cropId Database ID of the crop * @param cropCategoryId Database IDs of the crop category/categories - * @param fromStr format "yyyy-MM-dd" - * @param toStr format "yyyy-MM-dd" + * @param fromStr format "yyyy-MM-dd" + * @param toStr format "yyyy-MM-dd" * @return Observation objects with all data (full tree) */ @GET @@ -132,8 +115,7 @@ public class ObservationService { @QueryParam("from") String fromStr, @QueryParam("to") String toStr, @QueryParam("isPositive") Boolean isPositive - ) - { + ) { return Response.ok().entity(getFilteredObservationsFromBackend( organizationId, pestId, @@ -144,15 +126,14 @@ public class ObservationService { isPositive )).build(); } - + /** - * * @param organizationId Database ID of the organization - * @param pestId Database ID of the pest - * @param cropId Database ID of the crop + * @param pestId Database ID of the pest + * @param cropId Database ID of the crop * @param cropCategoryId cropCategoryId Database IDs of the crop category/categories - * @param fromStr format "yyyy-MM-dd" - * @param toStr format "yyyy-MM-dd" + * @param fromStr format "yyyy-MM-dd" + * @param toStr format "yyyy-MM-dd" * @return Observation objects for which the user is authorized to observe with properties relevant for lists */ @GET @@ -170,13 +151,11 @@ public class ObservationService { @QueryParam("userUUID") String userUUID, @QueryParam("locale") String localeStr, @QueryParam("isPositive") Boolean isPositive - ) - { + ) { return Response.ok().entity(this.getFilteredObservationListItems(organizationId, pestId, cropId, cropCategoryId, fromStr, toStr, userUUID, localeStr, isPositive)).build(); } - - - + + private List<ObservationListItem> getFilteredObservationListItems( Integer organizationId, Integer pestId, @@ -187,54 +166,51 @@ public class ObservationService { String userUUID, String localeStr, Boolean isPositive - ) - { + ) { VipsLogicUser user = (VipsLogicUser) httpServletRequest.getSession().getAttribute("user"); - - if(user == null && userUUID != null) - { + + if (user == null && userUUID != null) { user = userBean.findVipsLogicUser(UUID.fromString(userUUID)); } - ULocale locale = new ULocale(localeStr != null ? localeStr : - user != null ? user.getOrganizationId().getDefaultLocale() : - userBean.getOrganization(organizationId).getDefaultLocale()); + ULocale locale = new ULocale(localeStr != null ? localeStr : + user != null ? user.getOrganizationId().getDefaultLocale() : + userBean.getOrganization(organizationId).getDefaultLocale()); List<ObservationListItem> observations = getFilteredObservationsFromBackend( - organizationId, - pestId, - cropId, - cropCategoryId, - fromStr, - toStr, - isPositive, - user - ).stream().map(obs -> { - try { - return obs.getListItem(locale.getLanguage(), - observationBean.getLocalizedObservationDataSchema( - observationBean.getObservationDataSchema(organizationId, obs.getOrganismId()), - httpServletRequest, - locale - ) - ); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - return null; - } - }).collect(Collectors.toList()); + organizationId, + pestId, + cropId, + cropCategoryId, + fromStr, + toStr, + isPositive, + user + ).stream().map(obs -> { + try { + return obs.getListItem(locale.getLanguage(), + observationBean.getLocalizedObservationDataSchema( + observationBean.getObservationDataSchema(organizationId, obs.getOrganismId()), + httpServletRequest, + locale + ) + ); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return null; + } + }).collect(Collectors.toList()); //o.setObservationDataSchema(observationBean.getObservationDataSchema(observer.getOrganizationId().getOrganizationId(), o.getOrganismId())); return observations; } - + /** - * * @param organizationId Database ID of the organization - * @param pestId Database ID of the pest - * @param cropId Database ID of the crop + * @param pestId Database ID of the pest + * @param cropId Database ID of the crop * @param cropCategoryId cropCategoryId Database IDs of the crop category/categories - * @param fromStr format "yyyy-MM-dd" - * @param toStr format "yyyy-MM-dd" + * @param fromStr format "yyyy-MM-dd" + * @param toStr format "yyyy-MM-dd" * @return Observation objects for which the user is authorized to observe with properties relevant for lists */ @GET @@ -252,24 +228,21 @@ public class ObservationService { @QueryParam("userUUID") String userUUID, @QueryParam("locale") String localeStr, @QueryParam("isPositive") Boolean isPositive - ) - { + ) { List<ObservationListItem> observations = this.getFilteredObservationListItems(organizationId, pestId, cropId, cropCategoryId, fromStr, toStr, userUUID, localeStr, isPositive); Collections.sort(observations); String retVal = "ObservationID;organismName;cropOrganismName;timeOfObservation;lat/lon;observationHeading;observationData"; GISUtil gisUtil = new GISUtil(); - for(ObservationListItem obs:observations) - { + for (ObservationListItem obs : observations) { // Get latlon from geoInfo List<Geometry> geometries = gisUtil.getGeometriesFromGeoJSON(obs.getGeoInfo()); Coordinate c = null; - if(geometries.size() == 1 && geometries.get(0) instanceof Point) - { - c = ((Point)geometries.get(0)).getCoordinate(); + if (geometries.size() == 1 && geometries.get(0) instanceof Point) { + c = ((Point) geometries.get(0)).getCoordinate(); } - retVal += "\n" + obs.getObservationId() - + ";" + obs.getOrganismName() - + ";" + obs.getCropOrganismName() + retVal += "\n" + obs.getObservationId() + + ";" + obs.getOrganismName() + + ";" + obs.getCropOrganismName() + ";" + obs.getTimeOfObservation() + ";" + (c != null ? c.getY() + "," + c.getX() : "") + ";" + obs.getObservationHeading() @@ -279,13 +252,12 @@ public class ObservationService { } /** - * * @param organizationId Database ID of the organization - * @param pestId Database ID of the pest - * @param cropId Database ID of the crop + * @param pestId Database ID of the pest + * @param cropId Database ID of the crop * @param cropCategoryId cropCategoryId Database IDs of the crop category/categories - * @param fromStr format "yyyy-MM-dd" - * @param toStr format "yyyy-MM-dd" + * @param fromStr format "yyyy-MM-dd" + * @param toStr format "yyyy-MM-dd" * @return Observation objects for which the user is authorized to observe with properties relevant for lists */ private List<Observation> getFilteredObservationsFromBackend( @@ -296,33 +268,31 @@ public class ObservationService { String fromStr, String toStr, Boolean isPositive - ) - { + ) { SimpleDateFormat format = new SimpleDateFormat(Globals.defaultDateFormat); //TODO Set correct timeZone!!! Date from = null; Date to = null; - try - { + try { from = fromStr != null ? format.parse(fromStr) : null; to = toStr != null ? format.parse(toStr) : null; + } catch (ParseException ex) { + System.out.println("ERROR"); } - catch(ParseException ex){ System.out.println("ERROR");} - + return observationBean.getFilteredObservations( - organizationId, - pestId, - cropId, - cropCategoryId, - from, - to, - isPositive + organizationId, + pestId, + cropId, + cropCategoryId, + from, + to, + isPositive ); - + } /** - * * @param organizationId * @param pestId * @param cropId @@ -330,106 +300,104 @@ public class ObservationService { * @param fromStr * @param toStr * @return - * * @responseExample application/json * { - * "type": "FeatureCollection", - * "features": [ - * { - * "type": "Feature", - * "id": 18423, - * "geometry": { - * "type": "Point", - * "coordinates": [ - * 10.782463095356452, - * 59.35794998304658, - * 0.0 - * ] - * }, - * "properties": { - * "observationId": 18446, - * "observationHeading": "NLR Øst, Huggenes: Det er funnet potettørråte i Råde", - * "organism": { - * "organismId": 14, - * "latinName": "Phytophthora infestans", - * "tradeName": "", - * "logicallyDeleted": false, - * "isPest": true, - * "isCrop": false, - * "parentOrganismId": 124, - * "hierarchyCategoryId": 120, - * "organismLocaleSet": [ - * { - * "organismLocalePK": { - * "organismId": 14, - * "locale": "nb" - * }, - * "localName": "Potettørråte" - * }, - * { - * "organismLocalePK": { - * "organismId": 14, - * "locale": "en" - * }, - * "localName": "Late blight" - * } - * ], - * "organismExternalResourceSet": [], - * "childOrganisms": null, - * "extraProperties": {}, - * "observationDataSchema": null - * }, - * "cropOrganism": { - * "organismId": 5, - * "latinName": "Solanum tuberosum", - * "tradeName": "", - * "logicallyDeleted": false, - * "isPest": false, - * "isCrop": true, - * "parentOrganismId": 4, - * "hierarchyCategoryId": 120, - * "organismLocaleSet": [ - * { - * "organismLocalePK": { - * "organismId": 5, - * "locale": "bs" - * }, - * "localName": "Potato" - * }, - * { - * "organismLocalePK": { - * "organismId": 5, - * "locale": "nb" - * }, - * "localName": "Potet" - * }, - * { - * "organismLocalePK": { - * "organismId": 5, - * "locale": "en" - * }, - * "localName": "Potato" - * }, - * { - * "organismLocalePK": { - * "organismId": 5, - * "locale": "hr" - * }, - * "localName": "Krompir" - * } - * ], - * "organismExternalResourceSet": [], - * "childOrganisms": null, - * "extraProperties": {}, - * "observationDataSchema": null - * }, - * "observationText": "Fredag 2.juli ble det gjort første funn av potettørråte i Råde. Det er noen flekker på bladene på øvre del av planten. Smitten ser ut til å ha kommet som sekundærsmitte med vinden.", - * "timeOfObservation": "2021-07-02T11:00:00+02:00" - * } - * } - * ] + * "type": "FeatureCollection", + * "features": [ + * { + * "type": "Feature", + * "id": 18423, + * "geometry": { + * "type": "Point", + * "coordinates": [ + * 10.782463095356452, + * 59.35794998304658, + * 0.0 + * ] + * }, + * "properties": { + * "observationId": 18446, + * "observationHeading": "NLR Øst, Huggenes: Det er funnet potettørråte i Råde", + * "organism": { + * "organismId": 14, + * "latinName": "Phytophthora infestans", + * "tradeName": "", + * "logicallyDeleted": false, + * "isPest": true, + * "isCrop": false, + * "parentOrganismId": 124, + * "hierarchyCategoryId": 120, + * "organismLocaleSet": [ + * { + * "organismLocalePK": { + * "organismId": 14, + * "locale": "nb" + * }, + * "localName": "Potettørråte" + * }, + * { + * "organismLocalePK": { + * "organismId": 14, + * "locale": "en" + * }, + * "localName": "Late blight" + * } + * ], + * "organismExternalResourceSet": [], + * "childOrganisms": null, + * "extraProperties": {}, + * "observationDataSchema": null + * }, + * "cropOrganism": { + * "organismId": 5, + * "latinName": "Solanum tuberosum", + * "tradeName": "", + * "logicallyDeleted": false, + * "isPest": false, + * "isCrop": true, + * "parentOrganismId": 4, + * "hierarchyCategoryId": 120, + * "organismLocaleSet": [ + * { + * "organismLocalePK": { + * "organismId": 5, + * "locale": "bs" + * }, + * "localName": "Potato" + * }, + * { + * "organismLocalePK": { + * "organismId": 5, + * "locale": "nb" + * }, + * "localName": "Potet" + * }, + * { + * "organismLocalePK": { + * "organismId": 5, + * "locale": "en" + * }, + * "localName": "Potato" + * }, + * { + * "organismLocalePK": { + * "organismId": 5, + * "locale": "hr" + * }, + * "localName": "Krompir" + * } + * ], + * "organismExternalResourceSet": [], + * "childOrganisms": null, + * "extraProperties": {}, + * "observationDataSchema": null + * }, + * "observationText": "Fredag 2.juli ble det gjort første funn av potettørråte i Råde. Det er noen flekker på bladene på øvre del av planten. Smitten ser ut til å ha kommet som sekundærsmitte med vinden.", + * "timeOfObservation": "2021-07-02T11:00:00+02:00" + * } + * } + * ] * } - * */ @GET @Path("filter/{organizationId}/geoJSON") @@ -444,37 +412,37 @@ public class ObservationService { @QueryParam("from") String fromStr, @QueryParam("to") String toStr, @QueryParam("isPositive") Boolean isPositive - - ) - { + + ) { SimpleDateFormat format = new SimpleDateFormat(Globals.defaultDateFormat); //TODO Set correct timeZone!!! Date from = null; Date to = null; - try - { + try { from = fromStr != null ? format.parse(fromStr) : null; to = toStr != null ? format.parse(toStr) : null; + } catch (ParseException ex) { + System.out.println("ERROR"); } - catch(ParseException ex){ System.out.println("ERROR");} - + List<Observation> filteredObservations = this.getFilteredObservationsFromBackend( - organizationId, - pestId, - cropId, - cropCategoryId, - fromStr, - toStr, - isPositive + organizationId, + pestId, + cropId, + cropCategoryId, + fromStr, + toStr, + isPositive ); GISEntityUtil gisUtil = new GISEntityUtil(); return Response.ok().entity(gisUtil.getGeoJSONFromObservations(filteredObservations)).build(); } - + /** * Get a list of all observed pests for one organization * Practical for building effective select lists + * * @param organizationId Database ID of organization * @return list of all observed pests for one organization */ @@ -482,30 +450,30 @@ public class ObservationService { @Path("pest/{organizationId}") @Produces("application/json;charset=UTF-8") @TypeHint(Organism[].class) - public Response getObservedPests(@PathParam("organizationId") Integer organizationId) - { + public Response getObservedPests(@PathParam("organizationId") Integer organizationId) { return Response.ok().entity(observationBean.getObservedPests(organizationId)).build(); } - + /** * Get a list of all crop cultures where observations have been made for one organization * Practical for building effective select lists * TODO: Should be cached?? + * * @param organizationId Database ID of organization - * @return + * @return */ @GET @Path("crop/{organizationId}") @Produces("application/json;charset=UTF-8") @TypeHint(Organism[].class) - public Response getObservedCrops(@PathParam("organizationId") Integer organizationId) - { + public Response getObservedCrops(@PathParam("organizationId") Integer organizationId) { return Response.ok().entity(observationBean.getObservedCrops(organizationId)).build(); } - - + + /** * Publicly available observations per organization + * * @param organizationId Database ID of organization * @return APPROVED observations */ @@ -514,13 +482,14 @@ public class ObservationService { @GZIP @Produces("application/json;charset=UTF-8") @TypeHint(Observation[].class) - public Response getObservations(@PathParam("organizationId") Integer organizationId){ + public Response getObservations(@PathParam("organizationId") Integer organizationId) { return Response.ok().entity(observationBean.getObservations(organizationId, Observation.STATUS_TYPE_ID_APPROVED)).build(); } - + /** * Get observations for a user * Requires a valid UUID to be provided in the Authorization header + * * @param observationIds Comma separated list of Observation Ids * @return Filtering by observation ids */ @@ -529,66 +498,56 @@ public class ObservationService { @Produces("application/json;charset=UTF-8") @TypeHint(Observation[].class) public Response getObservationsForUser( - @QueryParam("observationIds") String observationIds - ) - { - try - { - VipsLogicUser user = userBean.getUserFromUUID(httpServletRequest); - if(user != null) - { - List<Observation> allObs = observationBean.getObservationsForUser(user); - if(observationIds != null) - { - Set<Integer> observationIdSet = Arrays.asList(observationIds.split(",")).stream() - .map(s->Integer.valueOf(s)) - .collect(Collectors.toSet()); - return Response.ok().entity( - allObs.stream() - .filter(obs->observationIdSet.contains(obs.getObservationId())) - .collect(Collectors.toList()) - ) - .build(); - } - return Response.ok().entity(allObs).build(); - } - else - { - return Response.status(Status.UNAUTHORIZED).build(); - } - } - catch(Exception e) - { - return Response.serverError().entity(e.getMessage()).build(); - } + @QueryParam("observationIds") String observationIds + ) { + try { + VipsLogicUser user = userBean.getUserFromUUID(httpServletRequest); + if (user != null) { + List<Observation> allObs = observationBean.getObservationsForUser(user); + if (observationIds != null) { + Set<Integer> observationIdSet = Arrays.asList(observationIds.split(",")).stream() + .map(s -> Integer.valueOf(s)) + .collect(Collectors.toSet()); + return Response.ok().entity( + allObs.stream() + .filter(obs -> observationIdSet.contains(obs.getObservationId())) + .collect(Collectors.toList()) + ) + .build(); + } + return Response.ok().entity(allObs).build(); + } else { + return Response.status(Status.UNAUTHORIZED).build(); + } + } catch (Exception e) { + return Response.serverError().entity(e.getMessage()).build(); + } } - + /** * Get minimized (only synchronization info) observations for a user * Used by the Observation app * Requires a valid UUID to be provided in the Authorization header + * * @return */ @GET @Path("list/minimized/user") @Produces("application/json;charset=UTF-8") @TypeHint(ObservationSyncInfo[].class) - public Response getMinimizedObservationsForUser() - { - VipsLogicUser user = userBean.getUserFromUUID(httpServletRequest); - if(user != null) - { - return Response.ok().entity(observationBean.getObservationsForUser(user).stream() - .map(obs->new ObservationSyncInfo(obs)).collect(Collectors.toList())).build(); - } - else - { - return Response.status(Status.UNAUTHORIZED).build(); - } + public Response getMinimizedObservationsForUser() { + VipsLogicUser user = userBean.getUserFromUUID(httpServletRequest); + if (user != null) { + return Response.ok().entity(observationBean.getObservationsForUser(user).stream() + .map(obs -> new ObservationSyncInfo(obs)).collect(Collectors.toList())).build(); + } else { + return Response.status(Status.UNAUTHORIZED).build(); + } } - + /** * Publicly available observations per organization + * * @param organizationId Database id of the organization * @return APPROVED observations */ @@ -602,41 +561,33 @@ public class ObservationService { @QueryParam("season") Integer season, @QueryParam("timeOfObservationFrom") String timeOfObservationFrom, @QueryParam("timeOfObservationTo") String timeOfObservationTo - ) - { - if((timeOfObservationFrom != null && ! timeOfObservationFrom.isEmpty()) - || (timeOfObservationTo != null && ! timeOfObservationTo.isEmpty())) - { + ) { + if ((timeOfObservationFrom != null && !timeOfObservationFrom.isEmpty()) + || (timeOfObservationTo != null && !timeOfObservationTo.isEmpty())) { Date from = null; Date to = null; - try - { + try { SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); - if(timeOfObservationFrom != null && ! timeOfObservationFrom.isEmpty()) - { + if (timeOfObservationFrom != null && !timeOfObservationFrom.isEmpty()) { from = format.parse(timeOfObservationFrom); } - if(timeOfObservationTo != null && ! timeOfObservationTo.isEmpty()) - { + if (timeOfObservationTo != null && !timeOfObservationTo.isEmpty()) { to = format.parse(timeOfObservationTo); } return Response.ok().entity(observationBean.getBroadcastObservations(organizationId, from, to)).build(); - } - catch(ParseException ex) - { + } catch (ParseException ex) { return Response.status(Response.Status.BAD_REQUEST).entity("Invalid date format").build(); } - } - else - { + } else { return Response.ok().entity(observationBean.getBroadcastObservations(organizationId, season)).build(); } } /** * Get one observation + * * @param observationId Database ID of the observation - * @param userUUID UUID that identifies the user (e.g. from VIPSWeb) + * @param userUUID UUID that identifies the user (e.g. from VIPSWeb) * @return */ @GET @@ -646,54 +597,50 @@ public class ObservationService { public Response getObservation( @PathParam("observationId") Integer observationId, @QueryParam("userUUID") String userUUID - ){ + ) { // Observation needs to be masked here as well, or does it create trouble for VIPSLogic observation admin? Observation o = observationBean.getObservation(observationId); - if(o == null) - { + if (o == null) { return Response.status(Status.NOT_FOUND).build(); } // Which organization does this observation belong to? VipsLogicUser observer = userBean.getVipsLogicUser(o.getUserId()); o.setObservationDataSchema(observationBean.getObservationDataSchema(observer.getOrganizationId().getOrganizationId(), o.getOrganismId())); - + VipsLogicUser user = (VipsLogicUser) httpServletRequest.getSession().getAttribute("user"); - if(user == null && userUUID != null) - { + if (user == null && userUUID != null) { user = userBean.findVipsLogicUser(UUID.fromString(userUUID)); } // Modification of location information: // 1) If location is private, only the owner or super users/org admins may view them // slett all geoInfo - if(user == null || (! user.isSuperUser() && ! user.isOrganizationAdmin())) - { + if (user == null || (!user.isSuperUser() && !user.isOrganizationAdmin())) { // Hide completely for all users except super and orgadmin - if(o.getLocationIsPrivate() && (user == null || ! o.getUserId().equals(user.getUserId()))) - { + if (o.getLocationIsPrivate() && (user == null || !o.getUserId().equals(user.getUserId()))) { o.setGeoinfos(null); } // This means the user wants to hide the exact location, // so mask for all users except super and orgadmin - else if(o.getPolygonService() != null) - { + else if (o.getPolygonService() != null) { //System.out.println("Masking observation"); List<Observation> intermediary = new ArrayList<>(); intermediary.add(o); - intermediary = this.maskObservations(o.getPolygonService(), + intermediary = this.maskObservations(o.getPolygonService(), observationBean.getObservationsWithLocations( observationBean.getObservationsWithGeoInfo(intermediary) ) - ); + ); o = intermediary.get(0); } } - + return Response.ok().entity(o).build(); } - + /** * Polygon services are used to mask observations (privacy concerns) * They may vary greatly between organizations (different countries) + * * @param organizationId Database id of the organization * @return A list of available polygon services for the requested organization */ @@ -702,45 +649,42 @@ public class ObservationService { @Produces("application/json;charset=UTF-8") @TypeHint(PolygonService[].class) public Response getPolygonServicesForOrganization( - @PathParam("organizationId") Integer organizationId - ) - { - return Response.ok().entity(observationBean.getPolygonServicesForOrganization(organizationId)).build(); + @PathParam("organizationId") Integer organizationId + ) { + return Response.ok().entity(observationBean.getPolygonServicesForOrganization(organizationId)).build(); } - - + + /** * Deletes a gis entity and its corresponding observation + * * @param gisId Database id of the gis entity - * @return + * @return */ @DELETE @Path("gisobservation/{gisId}") - public Response deleteGisObservation(@PathParam("gisId") Integer gisId) - { + public Response deleteGisObservation(@PathParam("gisId") Integer gisId) { VipsLogicUser user = (VipsLogicUser) httpServletRequest.getSession().getAttribute("user"); // If no user, send error message back to client - if(user == null) - { + if (user == null) { return Response.status(Response.Status.UNAUTHORIZED).build(); } - if(!userBean.authorizeUser(user, - VipsLogicRole.OBSERVER, - VipsLogicRole.OBSERVATION_AUTHORITY, - VipsLogicRole.ORGANIZATION_ADMINISTRATOR, - VipsLogicRole.SUPERUSER - ) - ) - { + if (!userBean.authorizeUser(user, + VipsLogicRole.OBSERVER, + VipsLogicRole.OBSERVATION_AUTHORITY, + VipsLogicRole.ORGANIZATION_ADMINISTRATOR, + VipsLogicRole.SUPERUSER + ) + ) { return Response.status(Response.Status.FORBIDDEN).build(); } observationBean.deleteGisObservationByGis(gisId); return Response.noContent().build(); } - + /** - * * Stores an observation from geoJson + * * @param geoJSON * @return the Url of the created entity (observation) */ @@ -749,26 +693,22 @@ public class ObservationService { @Path("gisobservation") @Consumes("application/json;charset=UTF-8") @Produces("application/json;charset=UTF-8") - public Response storeGisObservation(String geoJSON) - { - try - { + public Response storeGisObservation(String geoJSON) { + try { // Create the Observation Observation observation = observationBean.getObservationFromGeoJSON(geoJSON); VipsLogicUser user = (VipsLogicUser) httpServletRequest.getSession().getAttribute("user"); // If no user, send error message back to client - if(user == null) - { + if (user == null) { return Response.status(Response.Status.UNAUTHORIZED).build(); } - if(!userBean.authorizeUser(user, - VipsLogicRole.OBSERVER, - VipsLogicRole.OBSERVATION_AUTHORITY, + if (!userBean.authorizeUser(user, + VipsLogicRole.OBSERVER, + VipsLogicRole.OBSERVATION_AUTHORITY, VipsLogicRole.ORGANIZATION_ADMINISTRATOR, VipsLogicRole.SUPERUSER - ) ) - { + ) { return Response.status(Response.Status.FORBIDDEN).build(); } observation.setUserId(user.getUserId()); @@ -777,14 +717,14 @@ public class ObservationService { observation = observationBean.storeObservation(observation); GISEntityUtil gisUtil = new GISEntityUtil(); return Response.created(URI.create("/observation/" + observation.getObservationId())).entity(gisUtil.getGeoJSONFromObservation(observation)).build(); - }catch (IOException ex) - { + } catch (IOException ex) { return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(ex).build(); } } /** * Returns the time of the first observation in the system of the pest with given Id + * * @param organismId Database id of the given organism * @return the time of the first observation in the system of the pest with given Id */ @@ -792,16 +732,16 @@ public class ObservationService { @Path("first/{organismId}") @Produces("text/plain;charset=UTF-8") @TypeHint(Date.class) - public Response getFirstObservation(@PathParam("organismId") Integer organismId) - { + public Response getFirstObservation(@PathParam("organismId") Integer organismId) { Date firstObsTime = observationBean.getFirstObservationTime(organismId); return firstObsTime != null ? Response.ok().entity(firstObsTime).build() : Response.status(404).entity("No observations of organism with id=" + organismId).build(); } - + /** * When was the last time a change was made in cropCategories or organisms? Used for sync with the Field observation * app. + * * @return last time a change was made in cropCategories or organisms * @responseExample application/json {"lastUpdated": "2021-02-08T00:00:00Z"} */ @@ -809,24 +749,22 @@ public class ObservationService { @Path("organismsystemupdated") @Produces(MediaType.APPLICATION_JSON) @TypeHint(Date.class) - public Response getDateOfLastOrganismSystemUpdate() - { - HashMap<String, Object> result = new HashMap<>(); - Instant lastUpdated = organismBean.getLatestUpdateOfOrganisms(); - result.put("lastUpdated", lastUpdated != null ? lastUpdated: "1970-01-01T00:00:00Z"); - return Response.ok().entity(result).build(); + public Response getDateOfLastOrganismSystemUpdate() { + HashMap<String, Object> result = new HashMap<>(); + Instant lastUpdated = organismBean.getLatestUpdateOfOrganisms(); + result.put("lastUpdated", lastUpdated != null ? lastUpdated : "1970-01-01T00:00:00Z"); + return Response.ok().entity(result).build(); } - + /** - * * @param organizationId Database id of the organization - * @param pestId Database id of the pest - * @param cropId Database id of the crop + * @param pestId Database id of the pest + * @param cropId Database id of the crop * @param cropCategoryId Database ids of the crop categories - * @param fromStr format "yyyy-MM-dd" - * @param toStr format "yyyy-MM-dd" - * @param user The user that requests this (used for authorization) + * @param fromStr format "yyyy-MM-dd" + * @param toStr format "yyyy-MM-dd" + * @param user The user that requests this (used for authorization) * @return A list of observations that meets the filter criteria */ private List<Observation> getFilteredObservationsFromBackend( @@ -842,17 +780,15 @@ public class ObservationService { List<Observation> filteredObservations = this.getFilteredObservationsFromBackend(organizationId, pestId, cropId, cropCategoryId, fromStr, toStr, isPositive); //filteredObservations.forEach(o->System.out.println(o.getObservationId())); // If superuser or orgadmin: Return everything, unchanged, uncensored - if(user != null && (user.isSuperUser() || user.isOrganizationAdmin())) - { + if (user != null && (user.isSuperUser() || user.isOrganizationAdmin())) { return filteredObservations; } - List<Observation> retVal = filteredObservations.stream().filter(obs->obs.getBroadcastMessage() || (isPositive == null || !isPositive)).collect(Collectors.toList()); + List<Observation> retVal = filteredObservations.stream().filter(obs -> obs.getBroadcastMessage() || (isPositive == null || !isPositive)).collect(Collectors.toList()); //retVal.forEach(o->System.out.println(o.getObservationId())); retVal = this.maskObservations(retVal); //retVal.forEach(o->System.out.println(o.getObservationId())); // If user is not logged in, return only the publicly available observations - if(user == null) - { + if (user == null) { return retVal; } // Else: This is a registered user without special privileges. Show public observations + user's own @@ -863,68 +799,63 @@ public class ObservationService { /** * Runs through the observations, check each to see if it should be masked * (for privacy reasons) through a polygon service + * * @param observations The list of observations to check * @return The list of observations, with the locations masked with the selected polygon service */ private List<Observation> maskObservations(List<Observation> observations) { //System.out.println("maskObservations(List<Observation> observations)"); - + // Placing all observations with a polygon service in the correct bucket. Map<PolygonService, List<Observation>> registeredPolygonServicesInObservationList = new HashMap<>(); - observations.stream().filter((obs) -> { return (!obs.getLocationIsPrivate() && obs.getPolygonService() != null);}).forEachOrdered((obs) -> { + observations.stream().filter((obs) -> { + return (!obs.getLocationIsPrivate() && obs.getPolygonService() != null); + }).forEachOrdered((obs) -> { List<Observation> obsWithPolyServ = registeredPolygonServicesInObservationList.getOrDefault(obs.getPolygonService(), new ArrayList<>()); obsWithPolyServ.add(obs); registeredPolygonServicesInObservationList.put(obs.getPolygonService(), obsWithPolyServ); }); - + // No buckets filled = No masking needed, return list unmodified - if(registeredPolygonServicesInObservationList.isEmpty()) - { - return observations; - } - else - { + if (registeredPolygonServicesInObservationList.isEmpty()) { + return observations; + } else { // Loop through, mask - Map<Integer,Observation> maskedObservations = new HashMap<>(); + Map<Integer, Observation> maskedObservations = new HashMap<>(); registeredPolygonServicesInObservationList.keySet().forEach((pService) -> { this.maskObservations(pService, registeredPolygonServicesInObservationList.get(pService)) - .forEach(o->maskedObservations.put(o.getObservationId(), o)); + .forEach(o -> maskedObservations.put(o.getObservationId(), o)); }); - + // Adding the rest of the observations (the ones that don't need masking) - observations.stream().filter(o->maskedObservations.get(o.getObservationId())==null).forEach(o->maskedObservations.put(o.getObservationId(), o)); + observations.stream().filter(o -> maskedObservations.get(o.getObservationId()) == null).forEach(o -> maskedObservations.put(o.getObservationId(), o)); return new ArrayList<>(maskedObservations.values()); } } /** - * * @param polygonService The polygon service that should be used for masking - * @param observations The list of observations to mask + * @param observations The list of observations to mask * @return The list of observations, with the locations masked with the selected polygon service */ - private List<Observation> maskObservations(PolygonService polygonService, List<Observation> observations) - { + private List<Observation> maskObservations(PolygonService polygonService, List<Observation> observations) { //observations.forEach(o->System.out.println(o.getObservationId())); Client client = ClientBuilder.newClient(); WebTarget target = client.target(polygonService.getGisSearchUrlTemplate()); List<ReferencedPoint> points = observations.stream() - .filter(obs -> (obs.getGeoinfos()!=null && !obs.getGeoinfos().isEmpty()) || obs.getLocation() != null) - .map(obs->{ + .filter(obs -> (obs.getGeoinfos() != null && !obs.getGeoinfos().isEmpty()) || obs.getLocation() != null) + .map(obs -> { ReferencedPoint rp = new ReferencedPoint(); rp.setId(String.valueOf(obs.getObservationId())); - if(obs.getGeoinfos() != null) - { + if (obs.getGeoinfos() != null) { rp.setLon(obs.getGeoinfos().get(0).getGisGeom().getCoordinate().x); rp.setLat(obs.getGeoinfos().get(0).getGisGeom().getCoordinate().y); - } - else - { + } else { rp.setLon(obs.getLocation().getLongitude()); rp.setLat(obs.getLocation().getLatitude()); } return rp; - }).collect(Collectors.toList()); + }).collect(Collectors.toList()); /*System.out.println("maskobservations - target.request() about to be called"); ObjectMapper oMapper = new ObjectMapper(); try { @@ -936,13 +867,11 @@ public class ObservationService { .post(Entity.entity(points.toArray(new ReferencedPoint[points.size()]), MediaType.APPLICATION_JSON), PointMappingResponse.class); // We need to loop through the observations and find corresponding featurecollections and replace those Map<Integer, Feature> indexedPolygons = new HashMap<>(); - for(Feature feature:response.getFeatureCollection().getFeatures()) - { - indexedPolygons.put((Integer)feature.getProperties().get("id"), feature); + for (Feature feature : response.getFeatureCollection().getFeatures()) { + indexedPolygons.put((Integer) feature.getProperties().get("id"), feature); } GISEntityUtil gisEntityUtil = new GISEntityUtil(); - for(Map mapping:response.getMapping()) - { + for (Map mapping : response.getMapping()) { Integer observationId = Integer.valueOf((String) mapping.get("id")); Integer borderId = (Integer) mapping.get("borderid"); observations.stream().filter((o) -> (o.getObservationId().equals(observationId))).forEachOrdered((o) -> { @@ -954,13 +883,14 @@ public class ObservationService { o.setLocationPointOfInterestId(null); }); } - + return observations; } - + /** * This service is used by the VIPS Field observation app to sync data stored locally on the smartphone with the * state of an (potentially non-existent) observation in the VIPSLogic database + * * @param observationJson Json representation of the observation(s) * @return The observation(s) in their merged state, serialized to Json */ @@ -970,144 +900,141 @@ public class ObservationService { @Produces("application/json;charset=UTF-8") @TypeHint(Observation.class) public Response syncObservationFromApp( - String observationJson - ) - { - try - { - VipsLogicUser user = userBean.getUserFromUUID(httpServletRequest); - if(user == null) - { - return Response.status(Status.UNAUTHORIZED).build(); - } - ObjectMapper oM = new ObjectMapper(); - SimpleDateFormat df = new SimpleDateFormat(Globals.defaultTimestampFormat); - try { - Map<Object,Object> mapFromApp = oM.readValue(observationJson, new TypeReference<HashMap<Object,Object>>(){}); - // Check if it is marked as deleted or not - if(mapFromApp.get("deleted") != null && ((Boolean)mapFromApp.get("deleted").equals(true))) - { - if(observationBean.getObservation((Integer)mapFromApp.get("observationId")) != null) - { - observationBean.deleteObservation((Integer)mapFromApp.get("observationId")); - return Response.ok().build(); - } - else - { - return Response.status(Status.NOT_FOUND).build(); - } - } - else - { - Observation mergeObs = ((Integer)mapFromApp.get("observationId")) > 0 ? observationBean.getObservation((Integer)mapFromApp.get("observationId")): new Observation(); - // Trying to sync a non-existing observation - if(mergeObs == null) - { - return Response.status(Status.NOT_FOUND).build(); - } - // Pest organism - mergeObs.setOrganism(organismBean.getOrganism((Integer)mapFromApp.get("organismId"))); - // Crop organism - mergeObs.setCropOrganism(organismBean.getOrganism((Integer)mapFromApp.get("cropOrganismId"))); - // Other properties - mergeObs.setTimeOfObservation(oM.convertValue(mapFromApp.get("timeOfObservation"), new TypeReference<Date>(){})); + String observationJson + ) { + LOGGER.info("In syncObservationFromApp"); + LOGGER.info(observationJson); + + try { + VipsLogicUser user = userBean.getUserFromUUID(httpServletRequest); + if (user == null) { + return Response.status(Status.UNAUTHORIZED).build(); + } + ObjectMapper oM = new ObjectMapper(); + SimpleDateFormat df = new SimpleDateFormat(Globals.defaultTimestampFormat); + try { + Map<Object, Object> mapFromApp = oM.readValue(observationJson, new TypeReference<HashMap<Object, Object>>() { + }); + // Check if it is marked as deleted or not + if (mapFromApp.get("deleted") != null && ((Boolean) mapFromApp.get("deleted").equals(true))) { + if (observationBean.getObservation((Integer) mapFromApp.get("observationId")) != null) { + observationBean.deleteObservation((Integer) mapFromApp.get("observationId")); + return Response.ok().build(); + } else { + return Response.status(Status.NOT_FOUND).build(); + } + } else { + Integer observationId = (Integer) mapFromApp.get("observationId"); + Observation mergeObs; + if(observationId > 0) { + mergeObs = observationBean.getObservation(observationId); + if (mergeObs == null) { + return Response.status(Status.NOT_FOUND).build(); + } + } else { + mergeObs = new Observation("APP"); + } + + // Observation time series + if(mapFromApp.get("observationTimeSeriesId") != null) { + Integer observationTimeSeriesId = (Integer) mapFromApp.get("observationTimeSeriesId"); + mergeObs.setObservationTimeSeries(observationTimeSeriesBean.getObservationTimeSeries(observationTimeSeriesId)); + } + // Pest organism + mergeObs.setOrganism(organismBean.getOrganism((Integer) mapFromApp.get("organismId"))); + // Crop organism + mergeObs.setCropOrganism(organismBean.getOrganism((Integer) mapFromApp.get("cropOrganismId"))); + // Other properties + mergeObs.setTimeOfObservation(oM.convertValue(mapFromApp.get("timeOfObservation"), new TypeReference<Date>() { + })); mergeObs.setIsPositive(mapFromApp.get("isPositive") != null ? (Boolean) mapFromApp.get("isPositive") : false); - mergeObs.setUserId(mapFromApp.get("userId") != null ? Integer.valueOf((Integer)mapFromApp.get("userId")): user.getUserId()); - mergeObs.setGeoinfo((String)mapFromApp.get("geoinfo")); - mergeObs.setLocationPointOfInterestId(mapFromApp.get("locationPointOfInterestId") != null ? (Integer) mapFromApp.get("locationPointOfInterestId") : null); - mergeObs.setObservationHeading(mapFromApp.get("observationHeading") != null ? (String) mapFromApp.get("observationHeading") : null); - mergeObs.setObservationText(mapFromApp.get("observationText") != null ? (String) mapFromApp.get("observationText") : null); - mergeObs.setBroadcastMessage(mapFromApp.get("broadcastMessage") != null ? (Boolean) mapFromApp.get("broadcastMessage") : false); - mergeObs.setStatusTypeId(Integer.valueOf((Integer)mapFromApp.get("statusTypeId"))); - // If the user has the role of observation approver, change to approved if set to pending - if(mergeObs.getStatusTypeId().equals(ObservationStatusType.STATUS_PENDING) && user.isObservationAuthority()) - { - mergeObs.setStatusTypeId(ObservationStatusType.STATUS_APPROVED); - } - mergeObs.setStatusChangedByUserId(mapFromApp.get("statusChangedByUserId") != null ? (Integer) mapFromApp.get("statusChangedByUserId") : null); - mergeObs.setStatusChangedTime(mapFromApp.get("timeOfObservation") != null ? oM.convertValue(mapFromApp.get("timeOfObservation"), new TypeReference<Date>(){}) : null); - mergeObs.setStatusRemarks(mapFromApp.get("statusRemarks") != null ? (String) mapFromApp.get("statusRemarks") : null); - mergeObs.setIsQuantified(mapFromApp.get("isQuantified") != null ? (Boolean) mapFromApp.get("isQuantified") : false); - mergeObs.setLocationIsPrivate(mapFromApp.get("locationIsPrivate") != null ? (Boolean) mapFromApp.get("locationIsPrivate") : false); - mergeObs.setPolygonService(mapFromApp.get("polygonService") != null && ! ((String)mapFromApp.get("polygonService")).isEmpty() ? oM.convertValue(mapFromApp.get("polygonService"), new TypeReference<PolygonService>(){}) : null); - mergeObs.setObservationDataSchema(observationBean.getObservationDataSchema(user.getOrganization_id(), mergeObs.getOrganismId())); - mergeObs.setObservationData(mapFromApp.get("observationData") != null ? mapFromApp.get("observationData").toString() : null); - mergeObs.setLastEditedBy(user.getUserId()); - mergeObs.setLastEditedTime(new Date()); - - // Input check before storing - // Location must be set - if((mergeObs.getGeoinfo() == null || mergeObs.getGeoinfo().trim().isEmpty()) && mergeObs.getLocationPointOfInterestId() == null) - { - return Response.status(Status.BAD_REQUEST).entity("The observation is missing location data.").build(); - } - - boolean sendNotification = false; - // Storing approval status - // If superusers or user with correct authorization: Set as approved and send message - if(userBean.authorizeUser(user, VipsLogicRole.OBSERVATION_AUTHORITY, VipsLogicRole.ORGANIZATION_ADMINISTRATOR, VipsLogicRole.SUPERUSER)) - { - LOGGER.debug("user properly authorized to register observations"); - LOGGER.debug("observation Id=" + mergeObs.getObservationId()); - LOGGER.debug("broadcast this message? " + mergeObs.getBroadcastMessage()); - if(mergeObs.getObservationId() == null || mergeObs.getObservationId() <= 0) - { - mergeObs.setStatusTypeId(Observation.STATUS_TYPE_ID_APPROVED); - sendNotification = mergeObs.getBroadcastMessage(); // Only send the ones intended for sending - } - } - else if(mergeObs.getObservationId() == null || mergeObs.getObservationId() <= 0) - { - mergeObs.setStatusTypeId(Observation.STATUS_TYPE_ID_PENDING); - } - - // We need to get an observation Id before storing the illustrations! - mergeObs = observationBean.storeObservation(mergeObs); - - - - // ObservationIllustrationSet - // Including data that may need to be stored - if(mapFromApp.get("observationIllustrationSet") != null) - { - List<Map<Object,Object>> illusMaps = (List<Map<Object,Object>>) mapFromApp.get("observationIllustrationSet"); - for(Map<Object,Object> illusMap:illusMaps) - { - ObservationIllustrationPK pk = oM.convertValue(illusMap.get("observationIllustrationPK"), new TypeReference<ObservationIllustrationPK>(){}); - - if(illusMap.get("deleted") != null && ((Boolean) illusMap.get("deleted")) == true) - { - observationBean.deleteObservationIllustration(mergeObs, new String[] {pk.getFileName()}); - } - else if(illusMap.get("uploaded") != null && ((Boolean) illusMap.get("uploaded")) == false && illusMap.get("imageTextData") != null) - { - mergeObs = observationBean.storeObservationIllustration(mergeObs, pk.getFileName(), (String)illusMap.get("imageTextData")); - } - } - } - LOGGER.debug("sendNotification? " + sendNotification); - - // All transactions finished, we can send notifications - // if conditions are met - if(sendNotification && ! - (System.getProperty("DISABLE_MESSAGING_SYSTEM") != null && System.getProperty("DISABLE_MESSAGING_SYSTEM").equals("true")) - ) - { - LOGGER.debug("Sending the message!"); - messagingBean.sendUniversalMessage(mergeObs); - } - - return Response.ok().entity(mergeObs).build(); - } - } catch (IOException e) { - return Response.serverError().entity(e).build(); - } - } - catch(Exception e) - { - e.printStackTrace(); - return Response.serverError().entity(e).build(); - } - + mergeObs.setUserId(mapFromApp.get("userId") != null ? (Integer) mapFromApp.get("userId") : user.getUserId()); + mergeObs.setGeoinfo((String) mapFromApp.get("geoinfo")); + mergeObs.setLocationPointOfInterestId(mapFromApp.get("locationPointOfInterestId") != null ? (Integer) mapFromApp.get("locationPointOfInterestId") : null); + mergeObs.setObservationHeading(mapFromApp.get("observationHeading") != null ? (String) mapFromApp.get("observationHeading") : null); + mergeObs.setObservationText(mapFromApp.get("observationText") != null ? (String) mapFromApp.get("observationText") : null); + mergeObs.setBroadcastMessage(mapFromApp.get("broadcastMessage") != null ? (Boolean) mapFromApp.get("broadcastMessage") : false); + mergeObs.setStatusTypeId((Integer) mapFromApp.get("statusTypeId")); + // If the user has the role of observation approver, change to approved if set to pending + if (mergeObs.getStatusTypeId().equals(ObservationStatusType.STATUS_PENDING) && user.isObservationAuthority()) { + mergeObs.setStatusTypeId(ObservationStatusType.STATUS_APPROVED); + } + mergeObs.setStatusChangedByUserId(mapFromApp.get("userId") != null ? (Integer) mapFromApp.get("userId") : null); + mergeObs.setStatusChangedTime(mapFromApp.get("timeOfObservation") != null ? oM.convertValue(mapFromApp.get("timeOfObservation"), new TypeReference<Date>() { + }) : null); + mergeObs.setStatusRemarks(mapFromApp.get("statusRemarks") != null ? (String) mapFromApp.get("statusRemarks") : null); + mergeObs.setIsQuantified(mapFromApp.get("isQuantified") != null ? (Boolean) mapFromApp.get("isQuantified") : false); + mergeObs.setLocationIsPrivate(mapFromApp.get("locationIsPrivate") != null ? (Boolean) mapFromApp.get("locationIsPrivate") : false); + Object polygonServiceValue = mapFromApp.get("polygonService"); + if(polygonServiceValue != null && !polygonServiceValue.toString().isBlank()) { + PolygonService polygonService = oM.convertValue(mapFromApp.get("polygonService"), PolygonService.class); + mergeObs.setPolygonService(polygonService); + } + + mergeObs.setObservationDataSchema(observationBean.getObservationDataSchema(user.getOrganization_id(), mergeObs.getOrganismId())); + mergeObs.setObservationData(mapFromApp.get("observationData") != null ? mapFromApp.get("observationData").toString() : null); + mergeObs.setLastEditedBy(user.getUserId()); + mergeObs.setLastEditedTime(new Date()); + + // Input check before storing + // Location must be set + if ((mergeObs.getGeoinfo() == null || mergeObs.getGeoinfo().trim().isEmpty()) && mergeObs.getLocationPointOfInterestId() == null) { + return Response.status(Status.BAD_REQUEST).entity("{\"error\": \"The observation is missing location data.\"}").type(MediaType.APPLICATION_JSON).build(); + } + + boolean sendNotification = false; + // Storing approval status + // If superusers or user with correct authorization: Set as approved and send message + if (userBean.authorizeUser(user, VipsLogicRole.OBSERVATION_AUTHORITY, VipsLogicRole.ORGANIZATION_ADMINISTRATOR, VipsLogicRole.SUPERUSER)) { + LOGGER.debug("user properly authorized to register observations"); + LOGGER.debug("observation Id=" + mergeObs.getObservationId()); + LOGGER.debug("broadcast this message? " + mergeObs.getBroadcastMessage()); + if (mergeObs.getObservationId() == null || mergeObs.getObservationId() <= 0) { + mergeObs.setStatusTypeId(Observation.STATUS_TYPE_ID_APPROVED); + sendNotification = mergeObs.getBroadcastMessage(); // Only send the ones intended for sending + } + } else if (mergeObs.getObservationId() == null || mergeObs.getObservationId() <= 0) { + mergeObs.setStatusTypeId(Observation.STATUS_TYPE_ID_PENDING); + } + + // We need to get an observation Id before storing the illustrations! + mergeObs = observationBean.storeObservation(mergeObs); + + // ObservationIllustrationSet + // Including data that may need to be stored + if (mapFromApp.get("observationIllustrationSet") != null) { + List<Map<Object, Object>> illusMaps = (List<Map<Object, Object>>) mapFromApp.get("observationIllustrationSet"); + for (Map<Object, Object> illusMap : illusMaps) { + ObservationIllustrationPK pk = oM.convertValue(illusMap.get("observationIllustrationPK"), new TypeReference<ObservationIllustrationPK>() { + }); + + if (illusMap.get("deleted") != null && ((Boolean) illusMap.get("deleted")) == true) { + observationBean.deleteObservationIllustration(mergeObs, new String[]{pk.getFileName()}); + } else if (illusMap.get("uploaded") != null && ((Boolean) illusMap.get("uploaded")) == false && illusMap.get("imageTextData") != null) { + mergeObs = observationBean.storeObservationIllustration(mergeObs, pk.getFileName(), (String) illusMap.get("imageTextData")); + } + } + } + LOGGER.info("sendNotification? " + sendNotification); + + // All transactions finished, we can send notifications + // if conditions are met + if (sendNotification && ! + (System.getProperty("DISABLE_MESSAGING_SYSTEM") != null && System.getProperty("DISABLE_MESSAGING_SYSTEM").equals("true")) + ) { + LOGGER.debug("Sending the message!"); + messagingBean.sendUniversalMessage(mergeObs); + } + + return Response.ok().entity(mergeObs).build(); + } + } catch (IOException e) { + return Response.serverError().entity(e).build(); + } + } catch (Exception e) { + LOGGER.error("Exception occurred while syncing observations from app", e); + return Response.serverError().entity(e).build(); + } + } } diff --git a/src/main/java/no/nibio/vips/logic/service/ObservationTimeSeriesService.java b/src/main/java/no/nibio/vips/logic/service/ObservationTimeSeriesService.java new file mode 100644 index 0000000000000000000000000000000000000000..dc2aa0e64b9dd06d5f22013ff74a62e169cfdf46 --- /dev/null +++ b/src/main/java/no/nibio/vips/logic/service/ObservationTimeSeriesService.java @@ -0,0 +1,258 @@ +package no.nibio.vips.logic.service; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import no.nibio.vips.logic.controller.session.ObservationTimeSeriesBean; +import no.nibio.vips.logic.controller.session.OrganismBean; +import no.nibio.vips.logic.controller.session.UserBean; +import no.nibio.vips.logic.entity.Gis; +import no.nibio.vips.logic.entity.ObservationTimeSeries; +import no.nibio.vips.logic.entity.PolygonService; +import no.nibio.vips.logic.entity.VipsLogicUser; +import no.nibio.vips.logic.entity.rest.PointMappingResponse; +import no.nibio.vips.logic.entity.rest.ReferencedPoint; +import no.nibio.vips.logic.util.GISEntityUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.wololo.geojson.Feature; + +import javax.ejb.EJB; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.*; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +@Path("rest/observationtimeseries") +public class ObservationTimeSeriesService { + + public static final String APPLICATION_JSON = "application/json;charset=UTF-8"; + private final static Logger LOGGER = LoggerFactory.getLogger(ObservationTimeSeriesService.class); + private static final String DELETED = "deleted"; + private static final String OBSERVATION_TIME_SERIES_ID = "observationTimeSeriesId"; + private static final String ORGANISM_ID = "organismId"; + private static final String CROP_ORGANISM_ID = "cropOrganismId"; + private static final String USER_ID = "userId"; + private static final String LOCATION_POINT_OF_INTEREST_ID = "locationPointOfInterestId"; + private static final String YEAR = "year"; + private static final String NAME = "name"; + private static final String DESCRIPTION = "description"; + private static final String LOCATION_IS_PRIVATE = "locationIsPrivate"; + private static final String POLYGON_SERVICE = "polygonService"; + @EJB + UserBean userBean; + @EJB + ObservationTimeSeriesBean observationTimeSeriesBean; + @EJB + OrganismBean organismBean; + + @Context + private HttpServletRequest httpServletRequest; + + @GET + @Path("list/user") + @Produces(APPLICATION_JSON) + @TypeHint(ObservationTimeSeries[].class) + public Response getObservationsTimeSeriesForUser(@QueryParam("observationTimeSeriesIds") String otsIds) { + LOGGER.info("In getObservationsTimeSeriesForUser observationTimeSeriesIds=" + otsIds); + try { + VipsLogicUser user = userBean.getUserFromUUID(httpServletRequest); + if (user != null) { + List<ObservationTimeSeries> allObs = observationTimeSeriesBean.getObservationTimeSeriesListForUser(user); + if (otsIds != null) { + Set<Integer> observationIdSet = Arrays.stream(otsIds.split(",")) + .map(Integer::valueOf) + .collect(Collectors.toSet()); + return Response.ok().entity( + allObs.stream() + .filter(obs -> observationIdSet.contains(obs.getObservationTimeSeriesId())) + .collect(Collectors.toList()) + ) + .build(); + } + return Response.ok().entity(allObs).build(); + } else { + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + } catch (Exception e) { + return Response.serverError().entity(e.getMessage()).build(); + } + } + + /** + * Get one observation time series + * + * @param observationTimeSeriesId Database ID of the observation time series + * @param userUUID UUID that identifies the user (e.g. from VIPSWeb) + * @return + */ + @GET + @Path("{observationTimeSeriesId}") + @Produces(APPLICATION_JSON) + @TypeHint(ObservationTimeSeries.class) + public Response getObservationTimeSeries(@PathParam("observationTimeSeriesId") Integer observationTimeSeriesId, @QueryParam("userUUID") String userUUID) { + ObservationTimeSeries ots = observationTimeSeriesBean.getObservationTimeSeries(observationTimeSeriesId); + if (ots == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + VipsLogicUser requester = (VipsLogicUser) httpServletRequest.getSession().getAttribute("user"); + if (requester == null && userUUID != null) { + requester = userBean.findVipsLogicUser(UUID.fromString(userUUID)); + } + + boolean requesterNotValidUser = requester == null; + boolean requesterRegularUser = requester != null && !requester.isSuperUser() && !requester.isOrganizationAdmin(); + boolean requesterNotCreator = requester != null && !ots.getUserId().equals(requester.getUserId()); + + if (requesterNotValidUser || requesterRegularUser) { + // Mask for all users except creator, super and orgadmin + if (!(ots.getLocationIsPrivate() && (requesterNotValidUser || requesterNotCreator)) && ots.getPolygonService() != null) { + observationTimeSeriesBean.enrichObservationTimeSeriesWithPointOfInterest(ots); + this.maskLocation(ots.getPolygonService(), ots); + } + } + return Response.ok().entity(ots).build(); + } + + /** + * The location (point of interest) of the given observation time series is masked using the provided polygon service. + * + * @param polygonService The polygon service that should be used for masking + * @param observationTimeSeries The observation time series to mask location for + * @return The observation time series, with location masked with the provided polygon service + */ + private void maskLocation(PolygonService polygonService, ObservationTimeSeries observationTimeSeries) { + Client client = ClientBuilder.newClient(); + WebTarget target = client.target(polygonService.getGisSearchUrlTemplate()); + ReferencedPoint rp = new ReferencedPoint(); + rp.setId(String.valueOf(observationTimeSeries.getObservationTimeSeriesId())); + rp.setLon(observationTimeSeries.getLocationPointOfInterest().getLongitude()); + rp.setLat(observationTimeSeries.getLocationPointOfInterest().getLatitude()); + + ReferencedPoint[] pointArray = {rp}; + PointMappingResponse response = target.request(MediaType.APPLICATION_JSON).post(Entity.entity(pointArray, MediaType.APPLICATION_JSON), PointMappingResponse.class); + // We need to loop through the observations and find corresponding featurecollections and replace those + Map<Integer, Feature> indexedPolygons = new HashMap<>(); + for (Feature feature : response.getFeatureCollection().getFeatures()) { + indexedPolygons.put((Integer) feature.getProperties().get(OBSERVATION_TIME_SERIES_ID), feature); + } + GISEntityUtil gisEntityUtil = new GISEntityUtil(); + for (Map mapping : response.getMapping()) { + Integer observationTimeSeriesId = Integer.valueOf((String) mapping.get(OBSERVATION_TIME_SERIES_ID)); + if (observationTimeSeries.getObservationTimeSeriesId().equals(observationTimeSeriesId)) { + Integer borderId = (Integer) mapping.get("borderid"); + Gis polygon = gisEntityUtil.getGisFromFeature(indexedPolygons.get(borderId)); + List<Gis> gis = new ArrayList<>(); + gis.add(polygon); + observationTimeSeries.setLocationPointOfInterest(null); + observationTimeSeries.setLocationPointOfInterestId(null); + } + } + } + + /** + * This service is used by the VIPS Field observation app to sync data stored locally on the smartphone with the + * state of a (potentially non-existent) observation time series in the VIPSLogic database + * + * @param jsonOts Json representation of the observation time series + * @return The observation time series in its merged state, serialized to Json + */ + @POST + @Path("syncfromapp") + @Consumes(APPLICATION_JSON) + @Produces(APPLICATION_JSON) + @TypeHint(ObservationTimeSeries.class) + public Response syncObservationTimeSeriesFromApp(String jsonOts) { + LOGGER.info("In syncObservationTimeSeriesFromApp"); + LOGGER.info(jsonOts); + VipsLogicUser user = userBean.getUserFromUUID(httpServletRequest); + if (user == null) { + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + ObjectMapper oM = new ObjectMapper(); + try { + Map<Object, Object> mapFromApp = oM.readValue(jsonOts, new TypeReference<HashMap<Object, Object>>() { + }); + + Integer otsId = (Integer) mapFromApp.get(OBSERVATION_TIME_SERIES_ID); + + // Check if the observation time series is marked as deleted + Boolean isDeleted = (Boolean) mapFromApp.getOrDefault(DELETED, false); + if (isDeleted) { + // If marked as deleted, delete the observation time series if it exists + // Observations in time series are also deleted, but the app currently prevents this. + if (observationTimeSeriesBean.getObservationTimeSeries(otsId) != null) { + observationTimeSeriesBean.deleteObservationTimeSeries(otsId); + LOGGER.info("ObservationTimeSeries with id={} deleted", otsId); + return Response.ok().build(); + } else { + LOGGER.warn("ObservationTimeSeries with id={} not found, nothing deleted", otsId); + return Response.status(Response.Status.NOT_FOUND).build(); + } + } + + Date currentDate = new Date(); + ObservationTimeSeries otsToSave; + if (otsId != null && otsId > 0) { + otsToSave = observationTimeSeriesBean.getObservationTimeSeries(otsId); + if (otsToSave == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + } else { + otsToSave = new ObservationTimeSeries("APP"); + otsToSave.setUserId(user.getUserId()); + otsToSave.setCreated(currentDate); + } + otsToSave.setCropOrganism(organismBean.getOrganism(getValueFromMap(mapFromApp, CROP_ORGANISM_ID, Integer.class))); + otsToSave.setOrganism(organismBean.getOrganism(getValueFromMap(mapFromApp, ORGANISM_ID, Integer.class))); + otsToSave.setLocationPointOfInterestId(getValueFromMap(mapFromApp, LOCATION_POINT_OF_INTEREST_ID, Integer.class)); + otsToSave.setYear(getValueFromMap(mapFromApp, YEAR, Integer.class)); + otsToSave.setName(getValueFromMap(mapFromApp, NAME, String.class)); + otsToSave.setDescription(getValueFromMap(mapFromApp, DESCRIPTION, String.class)); + otsToSave.setLocationIsPrivate(getValueFromMap(mapFromApp, LOCATION_IS_PRIVATE, Boolean.class)); + + Object polygonServiceValue = mapFromApp.get(POLYGON_SERVICE); + if (polygonServiceValue != null && !polygonServiceValue.toString().isBlank()) { + PolygonService polygonService = oM.convertValue(polygonServiceValue, PolygonService.class); + otsToSave.setPolygonService(polygonService); + } + + otsToSave.setLastModified(currentDate); + otsToSave.setLastModifiedBy(user.getUserId()); + + // Input check before storing, location must be set + if (otsToSave.getLocationPointOfInterestId() == null) { + LOGGER.error("The observation time series is missing location data"); + return Response.status(Response.Status.BAD_REQUEST).entity("The observation time series is missing location data").build(); + } + LOGGER.info("otsToSave before storing: " + otsToSave); + otsToSave = observationTimeSeriesBean.storeObservationTimeSeries(otsToSave); + return Response.ok().entity(otsToSave).build(); + } catch (IOException e) { + LOGGER.error("IOException on save ObservationTimeSeries", e); + return Response.serverError().entity(e).build(); + } + } + + private <T> T getValueFromMap(Map<Object, Object> map, String key, Class<T> clazz) { + Object value = map.get(key); + if (clazz.isInstance(value)) { + return clazz.cast(value); + } else if (clazz == Integer.class && value instanceof String) { + try { + return clazz.cast(Integer.parseInt((String) value)); + } catch (NumberFormatException e) { + return null; + } + } + return clazz == Boolean.class ? clazz.cast(false) : null; + } +} diff --git a/src/main/java/no/nibio/vips/logic/service/POIService.java b/src/main/java/no/nibio/vips/logic/service/POIService.java index e84089cc9235d2b80849ad1e51729727e6adfaed..19132a3b9146e71697b99567cb723a0acd7140b9 100644 --- a/src/main/java/no/nibio/vips/logic/service/POIService.java +++ b/src/main/java/no/nibio/vips/logic/service/POIService.java @@ -37,6 +37,7 @@ import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; +import no.nibio.vips.logic.controller.session.PointOfInterestBean; import org.jboss.resteasy.spi.HttpRequest; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Point; @@ -55,8 +56,9 @@ import no.nibio.vips.logic.entity.Organization; import no.nibio.vips.logic.entity.PointOfInterest; import no.nibio.vips.logic.entity.PointOfInterestWeatherStation; import no.nibio.vips.logic.entity.VipsLogicUser; -import no.nibio.vips.logic.util.Globals; import no.nibio.vips.logic.controller.session.SessionControllerGetter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * @copyright 2022 <a href="http://www.nibio.no/">NIBIO</a> @@ -64,6 +66,7 @@ import no.nibio.vips.logic.controller.session.SessionControllerGetter; */ @Path("rest/poi") public class POIService { + private final static Logger LOGGER = LoggerFactory.getLogger(POIService.class); @Context private HttpRequest httpRequest; @@ -182,22 +185,26 @@ public class POIService { return Response.status(Status.UNAUTHORIZED).build(); } ObjectMapper oM = new ObjectMapper(); - SimpleDateFormat df = new SimpleDateFormat(Globals.defaultTimestampFormat); try { Map<Object, Object> mapFromApp = oM.readValue(poiJson, new TypeReference<HashMap<Object, Object>>() { }); + Integer pointOfInterestId = (Integer) mapFromApp.get("pointOfInterestId"); + PointOfInterestBean pointOfInterestBean = SessionControllerGetter.getPointOfInterestBean(); // Check if it is marked as deleted or not - if (mapFromApp.get("deleted") != null && ((Boolean) mapFromApp.get("deleted").equals(true))) { - if (SessionControllerGetter.getPointOfInterestBean().getPointOfInterest((Integer) mapFromApp.get("pointOfInterestId")) != null) { - SessionControllerGetter.getPointOfInterestBean().deletePoi((Integer) mapFromApp.get("pointOfInterestId")); + if (mapFromApp.get("deleted") != null && (mapFromApp.get("deleted").equals(true))) { + if (pointOfInterestBean.getPointOfInterest(pointOfInterestId) != null) { + pointOfInterestBean.deletePoi(pointOfInterestId); + LOGGER.info("POI with id={} deleted", pointOfInterestId); return Response.ok().build(); } else { + LOGGER.error("POI with id={} not found, nothing deleted", pointOfInterestId); return Response.status(Status.NOT_FOUND).build(); } } else { - PointOfInterest mergePoi = ((Integer) mapFromApp.get("pointOfInterestId")) > 0 ? SessionControllerGetter.getPointOfInterestBean().getPointOfInterest((Integer) mapFromApp.get("pointOfInterestId")) : PointOfInterest.getInstance((Integer) mapFromApp.get("pointOfInterestTypeId")); + PointOfInterest mergePoi = pointOfInterestId > 0 ? pointOfInterestBean.getPointOfInterest(pointOfInterestId) : PointOfInterest.getInstance((Integer) mapFromApp.get("pointOfInterestTypeId")); // Trying to sync a non-existing POI if (mergePoi == null) { + LOGGER.error("POI with id={} not found, nothing updated", pointOfInterestId); return Response.status(Status.NOT_FOUND).build(); } @@ -228,11 +235,11 @@ public class POIService { Point p3d = gisUtil.createPointWGS84(coordinate); mergePoi.setGisGeom(p3d); - mergePoi = SessionControllerGetter.getPointOfInterestBean().getPointOfInterest(SessionControllerGetter.getPointOfInterestBean().storePoi(mergePoi).getPointOfInterestId()); - + mergePoi = pointOfInterestBean.getPointOfInterest(pointOfInterestBean.storePoi(mergePoi).getPointOfInterestId()); return Response.ok().entity(mergePoi).build(); } } catch (IOException e) { + LOGGER.error("Exception when syncing POI", e); return Response.serverError().entity(e).build(); } diff --git a/src/main/resources/db/migration/V17__ObservationTimeSeries.sql b/src/main/resources/db/migration/V17__ObservationTimeSeries.sql new file mode 100644 index 0000000000000000000000000000000000000000..c278854b4599ea411fecb62c26f17911e28f94b1 --- /dev/null +++ b/src/main/resources/db/migration/V17__ObservationTimeSeries.sql @@ -0,0 +1,26 @@ +CREATE TABLE IF NOT EXISTS public.observation_time_series +( + observation_time_series_id SERIAL PRIMARY KEY, + crop_organism_id integer NOT NULL, + organism_id integer NOT NULL, + year integer NOT NULL, + name character varying(1023) NOT NULL, + description text, + source character varying(6) NOT NULL DEFAULT 'WEB', + user_id integer NOT NULL, + created timestamp with time zone NOT NULL, + last_modified_by integer, + last_modified timestamp with time zone, + location_point_of_interest_id integer NOT NULL, + location_is_private boolean DEFAULT false, + polygon_service_id integer, + CONSTRAINT observation_time_series_crop_organism_id_fkey FOREIGN KEY (crop_organism_id) REFERENCES public.organism (organism_id), + CONSTRAINT observation_time_series_organism_id_fkey FOREIGN KEY (organism_id) REFERENCES public.organism (organism_id), + CONSTRAINT observation_time_series_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.vips_logic_user (user_id), + CONSTRAINT observation_time_series_last_modified_by_fkey FOREIGN KEY (last_modified_by) REFERENCES public.vips_logic_user (user_id), + CONSTRAINT observation_time_series_location_point_of_interest_id_fkey FOREIGN KEY (location_point_of_interest_id) REFERENCES public.point_of_interest (point_of_interest_id), + CONSTRAINT observation_time_series_polygon_service_id_fkey FOREIGN KEY (polygon_service_id) REFERENCES public.polygon_service (polygon_service_id) +); + +ALTER TABLE observation ADD COLUMN source character varying(6) NOT NULL DEFAULT 'WEB'; +ALTER TABLE observation ADD COLUMN observation_time_series_id integer REFERENCES public.observation_time_series(observation_time_series_id); \ No newline at end of file diff --git a/src/test/java/no/nibio/vips/logic/service/ObservationTimeSeriesServiceTest.java b/src/test/java/no/nibio/vips/logic/service/ObservationTimeSeriesServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..030e3261f192a5740979b4b4f3848aa90fe2635f --- /dev/null +++ b/src/test/java/no/nibio/vips/logic/service/ObservationTimeSeriesServiceTest.java @@ -0,0 +1,5 @@ +package no.nibio.vips.logic.service; + +public class ObservationTimeSeriesServiceTest { + +} \ No newline at end of file