From 7bd09062c6f7334207af6db295492eda3978467f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roman=20Nagli=C4=8D?= Date: Thu, 25 Jan 2024 14:17:23 +0100 Subject: [PATCH 1/3] feat(U-6947614384): Handle Service Definition Changes New entities, ServiceGroup and ServiceKeyword, are created for improved classification and searchability of services. Relationships between these entities and existing Service entity have also been established. The service creation process now includes entity population from existing data, further enhancing service categorization and search functionality. Unit tests have been updated to validate these changes. --- app/src/main/java/app/Bootstrap.java | 57 +++++--- app/src/main/java/app/RootController.java | 24 ++-- .../main/java/app/model/service/Service.java | 73 ++++++++-- .../app/model/service/group/ServiceGroup.java | 42 ++++++ .../service/group/ServiceGroupRepository.java | 11 ++ .../model/service/keyword/ServiceKeyword.java | 45 ++++++ .../keyword/ServiceKeywordRepository.java | 25 ++++ .../ServiceDefinitionConverter.java | 39 ++++++ .../ServiceDefinitionEntity.java | 81 +++++++++++ ...ssingActiveServiceDefinitionException.java | 5 + .../model/servicerequest/ServiceRequest.java | 14 +- .../app/service/service/ServiceService.java | 36 +++-- .../servicerequest/ServiceRequestService.java | 131 +++++++++--------- app/src/main/resources/application.yml | 1 + settings.gradle | 2 +- 15 files changed, 472 insertions(+), 114 deletions(-) create mode 100644 app/src/main/java/app/model/service/group/ServiceGroup.java create mode 100644 app/src/main/java/app/model/service/group/ServiceGroupRepository.java create mode 100644 app/src/main/java/app/model/service/keyword/ServiceKeyword.java create mode 100644 app/src/main/java/app/model/service/keyword/ServiceKeywordRepository.java create mode 100644 app/src/main/java/app/model/service/servicedefinition/ServiceDefinitionConverter.java create mode 100644 app/src/main/java/app/model/service/servicedefinition/ServiceDefinitionEntity.java create mode 100644 app/src/main/java/app/model/servicerequest/MissingActiveServiceDefinitionException.java diff --git a/app/src/main/java/app/Bootstrap.java b/app/src/main/java/app/Bootstrap.java index 3aa3a5f5..6b8abcf3 100644 --- a/app/src/main/java/app/Bootstrap.java +++ b/app/src/main/java/app/Bootstrap.java @@ -19,25 +19,31 @@ import app.model.service.Service; import app.model.service.ServiceRepository; import app.model.service.ServiceType; -import app.model.service.servicedefinition.*; +import app.model.service.group.ServiceGroup; +import app.model.service.group.ServiceGroupRepository; +import app.model.service.keyword.ServiceKeyword; +import app.model.service.keyword.ServiceKeywordRepository; +import app.model.service.servicedefinition.AttributeDataType; +import app.model.service.servicedefinition.AttributeValue; +import app.model.service.servicedefinition.ServiceDefinition; +import app.model.service.servicedefinition.ServiceDefinitionAttribute; +import app.model.service.servicedefinition.ServiceDefinitionEntity; import app.model.servicerequest.ServiceRequest; import app.model.servicerequest.ServiceRequestStatus; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.context.annotation.Requires; import io.micronaut.core.convert.format.MapFormat; import io.micronaut.core.util.StringUtils; import io.micronaut.runtime.event.annotation.EventListener; import io.micronaut.runtime.server.event.ServerStartupEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; +import javax.transaction.Transactional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @Requires(property = "app-data.bootstrap.data.enabled", value = StringUtils.TRUE) @ConfigurationProperties("app-data.bootstrap") @@ -48,14 +54,20 @@ public class Bootstrap { private final ServiceRepository serviceRepository; private final JurisdictionRepository jurisdictionRepository; + private final ServiceGroupRepository groupRepository; + private final ServiceKeywordRepository keywordRepository; private static final Logger LOG = LoggerFactory.getLogger(Bootstrap.class); - public Bootstrap(ServiceRepository serviceRepository, JurisdictionRepository jurisdictionRepository) { + public Bootstrap(ServiceRepository serviceRepository, JurisdictionRepository jurisdictionRepository, + ServiceGroupRepository groupRepository, ServiceKeywordRepository keywordRepository) { this.serviceRepository = serviceRepository; this.jurisdictionRepository = jurisdictionRepository; + this.groupRepository = groupRepository; + this.keywordRepository = keywordRepository; } @EventListener + @Transactional public void devData(ServerStartupEvent event) { if(data != null) { @@ -106,7 +118,20 @@ private void processAndStoreServices(List> services, boolean mult service.setServiceCode((String) svc.get("serviceCode")); service.setDescription((String) svc.get("description")); service.setType(ServiceType.valueOf(((String) svc.get("type")).toUpperCase())); - + List groups = (List) svc.get("groups"); + if (groups != null) { + groups.forEach(groupName -> { + ServiceGroup group = groupRepository.findByName(groupName).orElse(new ServiceGroup(groupName)); + service.getServiceGroups().add(group); + }); + } + List keywords = (List) svc.get("keywords"); + if (keywords != null) { + keywords.forEach(keywordName -> { + ServiceKeyword keyword = keywordRepository.findByName(keywordName).orElse(new ServiceKeyword(keywordName)); + service.getServiceKeywords().add(keyword); + }); + } Jurisdiction jurisdiction; if (multiJurisdictionIsSupported) { String jurisdictionStr = (String) svc.get("jurisdiction"); @@ -152,16 +177,16 @@ private void processAndStoreServices(List> services, boolean mult return serviceDefinitionAttribute; }).collect(Collectors.toList())); - ObjectMapper objectMapper = new ObjectMapper(); - try { - service.setServiceDefinitionJson(objectMapper.writeValueAsString(serviceDefinition)); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } + ServiceDefinitionEntity sde = new ServiceDefinitionEntity(); + sde.setActive(true); + sde.setVersion("1.0"); - Service savedService = serviceRepository.save(service); + sde.setDefinition(serviceDefinition); + service.getServiceDefinitions().add(sde); + sde.setService(service); + } + Service savedService = serviceRepository.update(service); if (svc.containsKey("serviceRequests")) { List serviceRequests = (List) svc.get("serviceRequests"); diff --git a/app/src/main/java/app/RootController.java b/app/src/main/java/app/RootController.java index c33648f2..2ccdfa6f 100644 --- a/app/src/main/java/app/RootController.java +++ b/app/src/main/java/app/RootController.java @@ -18,14 +18,17 @@ import app.dto.download.DownloadRequestsArgumentsDTO; import app.dto.service.ServiceDTO; import app.dto.service.ServiceList; -import app.dto.servicerequest.*; +import app.dto.servicerequest.GetServiceRequestsDTO; +import app.dto.servicerequest.PostRequestServiceRequestDTO; +import app.dto.servicerequest.PostResponseServiceRequestDTO; +import app.dto.servicerequest.ServiceRequestDTO; +import app.dto.servicerequest.ServiceRequestList; import app.model.service.servicedefinition.ServiceDefinition; import app.service.discovery.DiscoveryEndpointService; import app.service.jurisdiction.JurisdictionService; import app.service.service.ServiceService; import app.service.servicerequest.ServiceRequestService; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; @@ -36,17 +39,22 @@ import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpResponse; import io.micronaut.http.MediaType; -import io.micronaut.http.annotation.*; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Consumes; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.annotation.Produces; +import io.micronaut.http.annotation.RequestBean; import io.micronaut.http.server.types.files.StreamedFile; import io.micronaut.scheduling.TaskExecutors; import io.micronaut.scheduling.annotation.ExecuteOn; import io.micronaut.security.annotation.Secured; import io.micronaut.security.rules.SecurityRule; - -import javax.validation.Valid; import java.net.MalformedURLException; import java.util.List; import java.util.Map; +import javax.validation.Valid; @Controller("/api") @Secured(SecurityRule.IS_ANONYMOUS) @@ -139,7 +147,7 @@ public HttpResponse indexXml(@Valid Pageable pageable, @Nullable String @Get(uris = {"/services/{serviceCode}{?jurisdiction_id}", "/services/{serviceCode}.json{?jurisdiction_id}"}) @Produces(MediaType.APPLICATION_JSON) @ExecuteOn(TaskExecutors.IO) - public String getServiceDefinitionJson(String serviceCode, @Nullable String jurisdiction_id) { + public ServiceDefinition getServiceDefinitionJson(String serviceCode, @Nullable String jurisdiction_id) { List errors = jurisdictionService.validateJurisdictionSupport(jurisdiction_id); if (!errors.isEmpty()) { return null; @@ -158,9 +166,7 @@ public String getServiceDefinitionXml(String serviceCode, @Nullable String juris } XmlMapper xmlMapper = XmlMapper.xmlBuilder().build(); - ObjectMapper objectMapper = new ObjectMapper(); - String serviceDefinitionStr = serviceService.getServiceDefinition(serviceCode, jurisdiction_id); - ServiceDefinition serviceDefinition = objectMapper.readValue(serviceDefinitionStr, ServiceDefinition.class); + ServiceDefinition serviceDefinition = serviceService.getServiceDefinition(serviceCode, jurisdiction_id); return xmlMapper.writeValueAsString(serviceDefinition); } diff --git a/app/src/main/java/app/model/service/Service.java b/app/src/main/java/app/model/service/Service.java index 09451e21..0e72450c 100644 --- a/app/src/main/java/app/model/service/Service.java +++ b/app/src/main/java/app/model/service/Service.java @@ -15,13 +15,20 @@ package app.model.service; import app.model.jurisdiction.Jurisdiction; +import app.model.service.group.ServiceGroup; +import app.model.service.keyword.ServiceKeyword; +import app.model.service.servicedefinition.ServiceDefinition; +import app.model.service.servicedefinition.ServiceDefinitionEntity; import app.model.servicerequest.ServiceRequest; +import java.util.HashSet; +import java.util.Set; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; import javax.persistence.*; import java.util.ArrayList; import java.util.List; +import org.hibernate.annotations.Where; @Entity @Table(name = "services") @@ -31,20 +38,31 @@ public class Service { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(unique = true) private String serviceCode; @ManyToOne @JoinColumn(name = "jurisdiction_id") private Jurisdiction jurisdiction; - @Column(columnDefinition = "TEXT") - private String serviceDefinitionJson; - @Column(nullable = false, columnDefinition = "TEXT") private String serviceName; @Column(columnDefinition = "TEXT") private String description; + @Column(columnDefinition = "TEXT") + private String keywords; + @ManyToMany(cascade = {CascadeType.ALL}, fetch = FetchType.EAGER) + @JoinTable(name="service_service_groups", + joinColumns={@JoinColumn(name="service_id")}, + inverseJoinColumns={@JoinColumn(name="service_group_id")}) + private Set serviceGroups = new HashSet<>(); + + @ManyToMany(cascade = {CascadeType.ALL}, fetch = FetchType.EAGER) + @JoinTable(name="service_service_keywords", + joinColumns={@JoinColumn(name="service_id")}, + inverseJoinColumns={@JoinColumn(name="service_keyword_id")}) + private Set serviceKeywords = new HashSet<>(); private boolean metadata = false; @@ -55,6 +73,12 @@ public class Service { @OnDelete(action = OnDeleteAction.CASCADE) private List serviceRequests = new ArrayList<>(); + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true, mappedBy = "") + @OnDelete(action = OnDeleteAction.CASCADE) + @JoinColumn(name = "service_id") + @Where(clause = "active = true") + private List serviceDefinitions = new ArrayList<>(); + public Service(String serviceName) { this.serviceName = serviceName; @@ -110,14 +134,10 @@ public void setType(ServiceType type) { this.type = type; } - public String getServiceDefinitionJson() { - return serviceDefinitionJson; - } - - public void setServiceDefinitionJson(String serviceDefinitionJson) { - this.serviceDefinitionJson = serviceDefinitionJson; + public ServiceDefinition getServiceDefinitionJson() { + if (serviceDefinitions.isEmpty()) return null; + return serviceDefinitions.get(0).getDefinition(); } - public Long getId() { return id; } @@ -137,4 +157,37 @@ public void setServiceRequests(List serviceRequests) { public void addServiceRequest(ServiceRequest serviceRequest) { serviceRequests.add(serviceRequest); } + + public List getServiceDefinitions() { + return serviceDefinitions; + } + + public void setServiceDefinitions( + List serviceDefinitions) { + this.serviceDefinitions = serviceDefinitions; + } + + public String getKeywords() { + return keywords; + } + + public void setKeywords(String keywords) { + this.keywords = keywords; + } + + public Set getServiceGroups() { + return serviceGroups; + } + + public void setServiceGroups(Set serviceGroups) { + this.serviceGroups = serviceGroups; + } + + public Set getServiceKeywords() { + return serviceKeywords; + } + + public void setServiceKeywords(Set serviceKeywords) { + this.serviceKeywords = serviceKeywords; + } } diff --git a/app/src/main/java/app/model/service/group/ServiceGroup.java b/app/src/main/java/app/model/service/group/ServiceGroup.java new file mode 100644 index 00000000..603ab4b7 --- /dev/null +++ b/app/src/main/java/app/model/service/group/ServiceGroup.java @@ -0,0 +1,42 @@ +package app.model.service.group; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "service_groups") +public class ServiceGroup { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + private String name; + + public ServiceGroup() { + } + + public ServiceGroup(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} \ No newline at end of file diff --git a/app/src/main/java/app/model/service/group/ServiceGroupRepository.java b/app/src/main/java/app/model/service/group/ServiceGroupRepository.java new file mode 100644 index 00000000..3b4062b1 --- /dev/null +++ b/app/src/main/java/app/model/service/group/ServiceGroupRepository.java @@ -0,0 +1,11 @@ +package app.model.service.group; + +import io.micronaut.data.annotation.Repository; +import io.micronaut.data.repository.PageableRepository; +import java.util.Optional; + +@Repository +public interface ServiceGroupRepository extends PageableRepository { + + Optional findByName(String name); +} diff --git a/app/src/main/java/app/model/service/keyword/ServiceKeyword.java b/app/src/main/java/app/model/service/keyword/ServiceKeyword.java new file mode 100644 index 00000000..4fc1e4ea --- /dev/null +++ b/app/src/main/java/app/model/service/keyword/ServiceKeyword.java @@ -0,0 +1,45 @@ +package app.model.service.keyword; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "service_keywords") +public class ServiceKeyword { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + private String name; + + public ServiceKeyword() { + } + + public ServiceKeyword(Long id) { + this.id = id; + } + + public ServiceKeyword(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/app/src/main/java/app/model/service/keyword/ServiceKeywordRepository.java b/app/src/main/java/app/model/service/keyword/ServiceKeywordRepository.java new file mode 100644 index 00000000..5e4dbae9 --- /dev/null +++ b/app/src/main/java/app/model/service/keyword/ServiceKeywordRepository.java @@ -0,0 +1,25 @@ +// Copyright 2023 Libre311 Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package app.model.service.keyword; + +import io.micronaut.data.annotation.Repository; +import io.micronaut.data.repository.PageableRepository; +import java.util.Optional; + +@Repository +public interface ServiceKeywordRepository extends PageableRepository { + + Optional findByName(String name); +} diff --git a/app/src/main/java/app/model/service/servicedefinition/ServiceDefinitionConverter.java b/app/src/main/java/app/model/service/servicedefinition/ServiceDefinitionConverter.java new file mode 100644 index 00000000..e2ce07ea --- /dev/null +++ b/app/src/main/java/app/model/service/servicedefinition/ServiceDefinitionConverter.java @@ -0,0 +1,39 @@ +package app.model.service.servicedefinition; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.inject.Singleton; +import javax.persistence.AttributeConverter; + +@Singleton +public class ServiceDefinitionConverter implements + AttributeConverter { + private final ObjectMapper objectMapper; + + public ServiceDefinitionConverter(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public String convertToDatabaseColumn(ServiceDefinition serviceDefinition) { + if (serviceDefinition != null) { + try { + return objectMapper.writeValueAsString(serviceDefinition); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return null; + } + + @Override + public ServiceDefinition convertToEntityAttribute(String s) { + if (s != null && !s.isBlank()) { + try { + return objectMapper.readValue(s, ServiceDefinition.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return null; + } +} diff --git a/app/src/main/java/app/model/service/servicedefinition/ServiceDefinitionEntity.java b/app/src/main/java/app/model/service/servicedefinition/ServiceDefinitionEntity.java new file mode 100644 index 00000000..3c191807 --- /dev/null +++ b/app/src/main/java/app/model/service/servicedefinition/ServiceDefinitionEntity.java @@ -0,0 +1,81 @@ +package app.model.service.servicedefinition; + + +import app.model.service.Service; +import javax.persistence.Column; +import javax.persistence.Convert; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +@Entity +@Table(name = "service_definitions") +public class ServiceDefinitionEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String version; + + private Boolean active; + + @Column(columnDefinition = "TEXT") + @Convert(converter = ServiceDefinitionConverter.class) + private ServiceDefinition definition; + + @ManyToOne + private Service service; + + public ServiceDefinitionEntity() { + } + + public ServiceDefinitionEntity(String version, Boolean active, ServiceDefinition definition) { + this.version = version; + this.active = active; + this.definition = definition; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public Boolean getActive() { + return active; + } + + public void setActive(Boolean active) { + this.active = active; + } + + public ServiceDefinition getDefinition() { + return definition; + } + + public void setDefinition(ServiceDefinition definition) { + this.definition = definition; + } + + public Service getService() { + return service; + } + + public void setService(Service service) { + this.service = service; + } +} diff --git a/app/src/main/java/app/model/servicerequest/MissingActiveServiceDefinitionException.java b/app/src/main/java/app/model/servicerequest/MissingActiveServiceDefinitionException.java new file mode 100644 index 00000000..8b4c8843 --- /dev/null +++ b/app/src/main/java/app/model/servicerequest/MissingActiveServiceDefinitionException.java @@ -0,0 +1,5 @@ +package app.model.servicerequest; + +public class MissingActiveServiceDefinitionException extends RuntimeException { + +} diff --git a/app/src/main/java/app/model/servicerequest/ServiceRequest.java b/app/src/main/java/app/model/servicerequest/ServiceRequest.java index b6715eb6..1b216097 100644 --- a/app/src/main/java/app/model/servicerequest/ServiceRequest.java +++ b/app/src/main/java/app/model/servicerequest/ServiceRequest.java @@ -16,6 +16,7 @@ import app.model.jurisdiction.Jurisdiction; import app.model.service.Service; +import app.model.service.servicedefinition.ServiceDefinitionEntity; import io.micronaut.core.annotation.Nullable; import io.micronaut.data.annotation.DateCreated; import io.micronaut.data.annotation.DateUpdated; @@ -42,6 +43,10 @@ public class ServiceRequest { @JoinColumn(name = "jurisdiction_id") private Jurisdiction jurisdiction; + @ManyToOne + @JoinColumn(name = "service_definition_id") + private ServiceDefinitionEntity serviceDefinition; + @Nullable @Column(columnDefinition = "TEXT") private String attributesJson; @@ -345,7 +350,6 @@ public Instant getClosedDate() { public void setClosedDate(@Nullable Instant closedDate) { this.closedDate = closedDate; } - public void setExpectedDate(@Nullable Instant expectedDate) { this.expectedDate = expectedDate; } @@ -354,6 +358,14 @@ public void setExpectedDate(@Nullable Instant expectedDate) { public String getAgencyEmail() { return agencyEmail; } + public ServiceDefinitionEntity getServiceDefinition() { + return serviceDefinition; + } + + public void setServiceDefinition( + ServiceDefinitionEntity serviceDefinition) { + this.serviceDefinition = serviceDefinition; + } public void setAgencyEmail(@Nullable String agencyEmail) { this.agencyEmail = agencyEmail; diff --git a/app/src/main/java/app/service/service/ServiceService.java b/app/src/main/java/app/service/service/ServiceService.java index 5ca7c33b..bf96f9e3 100644 --- a/app/src/main/java/app/service/service/ServiceService.java +++ b/app/src/main/java/app/service/service/ServiceService.java @@ -22,6 +22,7 @@ import app.model.service.Service; import app.model.service.ServiceRepository; import app.model.service.servicedefinition.ServiceDefinition; +import app.model.service.servicedefinition.ServiceDefinitionEntity; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.micronaut.data.model.Page; @@ -54,7 +55,7 @@ public Page findAll(Pageable pageable, String jurisdictionId) { return servicePage.map(ServiceDTO::new); } - public String getServiceDefinition(String serviceCode, String jurisdictionId) { + public ServiceDefinition getServiceDefinition(String serviceCode, String jurisdictionId) { Optional serviceOptional; if (jurisdictionId == null) { serviceOptional = serviceRepository.findByServiceCode(serviceCode); @@ -65,7 +66,7 @@ public String getServiceDefinition(String serviceCode, String jurisdictionId) { if (serviceOptional.isEmpty()) { LOG.error("Service not found."); return null; - } else if (serviceOptional.get().getServiceDefinitionJson() == null) { + } else if (serviceOptional.get().getServiceDefinitions().isEmpty()) { LOG.error("Service Definition is null."); return null; } @@ -96,9 +97,10 @@ public ServiceDTO createService(CreateServiceDTO serviceDTO) { Service service = new Service(); if (serviceDTO.getServiceDefinitionJson() != null) { - if (serviceDefinitionFormatIsValid(serviceDTO.getServiceDefinitionJson())) { + ServiceDefinition sd = serviceDefinitionFormatIsValid(serviceDTO.getServiceDefinitionJson()); + if (sd != null) { service.setMetadata(true); - service.setServiceDefinitionJson(serviceDTO.getServiceDefinitionJson()); + service.getServiceDefinitions().add(new ServiceDefinitionEntity("1", true, sd)); } else { LOG.error("Service Definition JSON format is invalid."); return null; @@ -142,9 +144,18 @@ public ServiceDTO updateService(Long serviceId, UpdateServiceDTO serviceDTO) { service.setServiceName(serviceDTO.getServiceName()); } if (serviceDTO.getServiceDefinitionJson() != null) { - if (serviceDefinitionFormatIsValid(serviceDTO.getServiceDefinitionJson())) { + ServiceDefinition sd = serviceDefinitionFormatIsValid(serviceDTO.getServiceDefinitionJson()); + if (sd != null) { service.setMetadata(true); - service.setServiceDefinitionJson(serviceDTO.getServiceDefinitionJson()); + ServiceDefinitionEntity existingSde = null; + if (service.getServiceDefinitions().isEmpty()) { + existingSde = new ServiceDefinitionEntity("1", true, sd); + } else { + existingSde = service.getServiceDefinitions().get(0); + existingSde.setActive(false); + } + service.getServiceDefinitions().add(new ServiceDefinitionEntity( + getNextVersionNumber(existingSde), true, sd)); } else { LOG.error("Service Definition JSON format is invalid."); return null; @@ -154,6 +165,10 @@ public ServiceDTO updateService(Long serviceId, UpdateServiceDTO serviceDTO) { return new ServiceDTO(serviceRepository.update(service)); } + private static String getNextVersionNumber(ServiceDefinitionEntity existingSde) { + return ""+(Long.parseLong(existingSde.getVersion()) + 1); + } + private boolean serviceCodeAlreadyExists(boolean jurisdictionSupportEnabled, String serviceCode, String jurisdictionId) { boolean serviceCodeAlreadyExists; if (jurisdictionSupportEnabled) { @@ -168,12 +183,13 @@ private boolean serviceCodeAlreadyExists(boolean jurisdictionSupportEnabled, Str return false; } - private boolean serviceDefinitionFormatIsValid(String serviceDefinitionJson) { + private ServiceDefinition serviceDefinitionFormatIsValid(String serviceDefinitionJson) { + ServiceDefinition sd; try { - (new ObjectMapper()).readValue(serviceDefinitionJson, ServiceDefinition.class); + sd = (new ObjectMapper()).readValue(serviceDefinitionJson, ServiceDefinition.class); } catch (JsonProcessingException e) { - return false; + return null; } - return true; + return sd; } } diff --git a/app/src/main/java/app/service/servicerequest/ServiceRequestService.java b/app/src/main/java/app/service/servicerequest/ServiceRequestService.java index 3c0c6879..71f99ddd 100644 --- a/app/src/main/java/app/service/servicerequest/ServiceRequestService.java +++ b/app/src/main/java/app/service/servicerequest/ServiceRequestService.java @@ -23,6 +23,8 @@ import app.model.service.servicedefinition.AttributeValue; import app.model.service.servicedefinition.ServiceDefinition; import app.model.service.servicedefinition.ServiceDefinitionAttribute; +import app.model.service.servicedefinition.ServiceDefinitionEntity; +import app.model.servicerequest.MissingActiveServiceDefinitionException; import app.model.servicerequest.ServiceRequest; import app.model.servicerequest.ServiceRequestRepository; import app.model.servicerequest.ServiceRequestStatus; @@ -86,15 +88,15 @@ private static ServiceRequestDTO convertToDTO(ServiceRequest serviceRequest) { } public PostResponseServiceRequestDTO createServiceRequest(HttpRequest request, PostRequestServiceRequestDTO serviceRequestDTO) { - if (!reCaptchaService.verifyReCaptcha(serviceRequestDTO.getgRecaptchaResponse())) { - LOG.error("ReCaptcha verification failed."); - return null; - } + if (!reCaptchaService.verifyReCaptcha(serviceRequestDTO.getgRecaptchaResponse())) { + LOG.error("ReCaptcha verification failed."); + return null; + } - if (!validMediaUrl(serviceRequestDTO.getMediaUrl())) { - LOG.error("Media URL is invalid."); - return null; - } + if (!validMediaUrl(serviceRequestDTO.getMediaUrl())) { + LOG.error("Media URL is invalid."); + return null; + } Optional serviceByServiceCodeOptional; if (serviceRequestDTO.getJurisdictionId() == null) { @@ -104,55 +106,64 @@ public PostResponseServiceRequestDTO createServiceRequest(HttpRequest request serviceRequestDTO.getServiceCode(), serviceRequestDTO.getJurisdictionId()); } - if (serviceByServiceCodeOptional.isEmpty()) { - LOG.error("Corresponding service not found."); - return null; // todo return 'corresponding service not found - } - - if (serviceRequestDTO.getJurisdictionId() != null && - !serviceRequestDTO.getJurisdictionId().equals(serviceByServiceCodeOptional.get().getJurisdiction().getId())) { - LOG.error("Mismatch between jurisdiction_id provided and Service's associated jurisdiction."); - return null; - } - - // validate if a location is provided - boolean latLongProvided = StringUtils.hasText(serviceRequestDTO.getLatitude()) && - StringUtils.hasText(serviceRequestDTO.getLongitude()); - - if (!latLongProvided && - StringUtils.isEmpty(serviceRequestDTO.getAddressString()) && - StringUtils.isEmpty(serviceRequestDTO.getAddressId())) { - LOG.error("Address or lat/long not provided."); - return null; // todo throw exception - } - - // validate if additional attributes are required - List requestAttributes = null; - Service service = serviceByServiceCodeOptional.get(); - if (service.isMetadata()) { - // get service definition - String serviceDefinitionJson = service.getServiceDefinitionJson(); - if (serviceDefinitionJson == null || serviceDefinitionJson.isBlank()) { - LOG.error("Service definition does not exists despite service requiring it."); - return null; // should not be in this state and admin needs to be aware. - } - - requestAttributes = buildUserResponseAttributesFromRequest(request, serviceDefinitionJson); - if (requestAttributes.isEmpty()) { - LOG.error("Submitted Service Request does not contain any attribute values."); - return null; // todo throw exception - must provide attributes - } - if (!requestAttributesHasAllRequiredServiceDefinitionAttributes(serviceDefinitionJson, requestAttributes)) { - LOG.error("Submitted Service Request does not contain required attribute values."); - return null; // todo throw exception (validation) - } - } + if (serviceByServiceCodeOptional.isEmpty()) { + LOG.error("Corresponding service not found."); + return null; // todo return 'corresponding service not found + } + + if (serviceRequestDTO.getJurisdictionId() != null && + !serviceRequestDTO.getJurisdictionId() + .equals(serviceByServiceCodeOptional.get().getJurisdiction().getId())) { + LOG.error( + "Mismatch between jurisdiction_id provided and Service's associated jurisdiction."); + return null; + } + + // validate if a location is provided + boolean latLongProvided = StringUtils.hasText(serviceRequestDTO.getLatitude()) && + StringUtils.hasText(serviceRequestDTO.getLongitude()); + + if (!latLongProvided && + StringUtils.isEmpty(serviceRequestDTO.getAddressString()) && + StringUtils.isEmpty(serviceRequestDTO.getAddressId())) { + LOG.error("Address or lat/long not provided."); + return null; // todo throw exception + } + + // validate if additional attributes are required + List requestAttributes = null; + Service service = serviceByServiceCodeOptional.get(); + if (service.isMetadata()) { + // get service definition + List serviceDefinitions = service.getServiceDefinitions(); + if (serviceDefinitions.isEmpty() || serviceDefinitions.get(0).getDefinition() == null) { + LOG.error("Service definition does not exists despite service requiring it."); + return null; // should not be in this state and admin needs to be aware. + } + ServiceDefinition serviceDefinition = serviceDefinitions.get(0).getDefinition(); + requestAttributes = buildUserResponseAttributesFromRequest(request, serviceDefinition); + if (requestAttributes.isEmpty()) { + LOG.error("Submitted Service Request does not contain any attribute values."); + return null; // todo throw exception - must provide attributes + } + if (!requestAttributesHasAllRequiredServiceDefinitionAttributes(serviceDefinition, + requestAttributes)) { + LOG.error("Submitted Service Request does not contain required attribute values."); + return null; // todo throw exception (validation) + } + } ServiceRequest serviceRequest = transformDtoToServiceRequest(serviceRequestDTO, service); if (requestAttributes != null) { ObjectMapper objectMapper = new ObjectMapper(); try { serviceRequest.setAttributesJson(objectMapper.writeValueAsString(requestAttributes)); + serviceRequest.setServiceDefinition( + service.getServiceDefinitions().stream() + .filter(sde -> sde.getActive()).findFirst().orElseThrow( + ()-> new MissingActiveServiceDefinitionException() + ) + ); } catch (JsonProcessingException e) { throw new RuntimeException(e); } @@ -166,13 +177,10 @@ private boolean validMediaUrl(String mediaUrl) { return mediaUrl.startsWith(storageUrlUtil.getBucketUrlString()); } - private boolean requestAttributesHasAllRequiredServiceDefinitionAttributes(String serviceDefinitionJson, List requestAttributes) { + private boolean requestAttributesHasAllRequiredServiceDefinitionAttributes(ServiceDefinition serviceDefinition, List requestAttributes) { // deserialize - ObjectMapper objectMapper = new ObjectMapper(); boolean containsAllRequiredAttrs = false; - try { // collect all required attributes - ServiceDefinition serviceDefinition = objectMapper.readValue(serviceDefinitionJson, ServiceDefinition.class); List requiredCodes = serviceDefinition.getAttributes().stream() .filter(ServiceDefinitionAttribute::isRequired) .map(ServiceDefinitionAttribute::getCode) @@ -184,22 +192,11 @@ private boolean requestAttributesHasAllRequiredServiceDefinitionAttributes(Strin .collect(Collectors.toList()); containsAllRequiredAttrs = requestCodes.containsAll(requiredCodes); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } return containsAllRequiredAttrs; } - private List buildUserResponseAttributesFromRequest(HttpRequest request, String serviceDefinitionJson) { - ObjectMapper objectMapper = new ObjectMapper(); - ServiceDefinition serviceDefinition; - try { - serviceDefinition = objectMapper.readValue(serviceDefinitionJson, ServiceDefinition.class); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - + private List buildUserResponseAttributesFromRequest(HttpRequest request, ServiceDefinition serviceDefinition) { Optional body = request.getBody(Map.class); List attributes = new ArrayList<>(); diff --git a/app/src/main/resources/application.yml b/app/src/main/resources/application.yml index ae29af6c..1acf2241 100644 --- a/app/src/main/resources/application.yml +++ b/app/src/main/resources/application.yml @@ -70,6 +70,7 @@ jpa: hibernate: hbm2ddl: auto: ${LIBRE311_AUTO_SCHEMA_GEN:update} +# show_sql: true netty: default: allocator: diff --git a/settings.gradle b/settings.gradle index 65dd9dd6..3e0ca93a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include 'app', 'frontend' \ No newline at end of file +include 'app' \ No newline at end of file From 5de2245095e97531764a323995af7d5a30921873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roman=20Nagli=C4=8D?= Date: Thu, 25 Jan 2024 16:34:13 +0100 Subject: [PATCH 2/3] feat(U-6947614384): Handle Service Definition Changes Service can have same serviceNames for different jurisdictions. Fix after merge --- app/src/main/java/app/model/service/Service.java | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/app/model/service/Service.java b/app/src/main/java/app/model/service/Service.java index 0e72450c..7c1ba5f3 100644 --- a/app/src/main/java/app/model/service/Service.java +++ b/app/src/main/java/app/model/service/Service.java @@ -38,7 +38,6 @@ public class Service { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(unique = true) private String serviceCode; @ManyToOne From 6fc5a75826fe2faad9d737ad52a7c7940657213b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roman=20Nagli=C4=8D?= Date: Thu, 25 Jan 2024 18:29:21 +0100 Subject: [PATCH 3/3] feat(U-6947614384): Handle Service Definition Changes Replaced 'getServiceGroups()' method with an 'addServiceGroup()' method in the Service class to allow managing service groups more effectively. Also updated appropriate calls in the Bootstrap class. This reduces the chance of duplication in service names across different jurisdictions. --- app/src/main/java/app/Bootstrap.java | 2 +- app/src/main/java/app/model/service/Service.java | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/app/Bootstrap.java b/app/src/main/java/app/Bootstrap.java index 6b8abcf3..562f0962 100644 --- a/app/src/main/java/app/Bootstrap.java +++ b/app/src/main/java/app/Bootstrap.java @@ -122,7 +122,7 @@ private void processAndStoreServices(List> services, boolean mult if (groups != null) { groups.forEach(groupName -> { ServiceGroup group = groupRepository.findByName(groupName).orElse(new ServiceGroup(groupName)); - service.getServiceGroups().add(group); + service.addServiceGroup(group); }); } List keywords = (List) svc.get("keywords"); diff --git a/app/src/main/java/app/model/service/Service.java b/app/src/main/java/app/model/service/Service.java index 7c1ba5f3..6cb51504 100644 --- a/app/src/main/java/app/model/service/Service.java +++ b/app/src/main/java/app/model/service/Service.java @@ -174,8 +174,10 @@ public void setKeywords(String keywords) { this.keywords = keywords; } - public Set getServiceGroups() { - return serviceGroups; + public void addServiceGroup(ServiceGroup serviceGroup) { + if (serviceGroups.isEmpty()) { + serviceGroups.add(serviceGroup); + } } public void setServiceGroups(Set serviceGroups) {