From dd084e4f693c296825492ee63b90d4b269ea15ba Mon Sep 17 00:00:00 2001 From: mvazquezficodes <145013779+mvazquezficodes@users.noreply.github.com> Date: Wed, 7 Feb 2024 15:53:26 +0100 Subject: [PATCH] Service inventory implementation (#42) * Base implementation of service-inventory * Solving erros in Service and adding Event Subscription * Adding testing and fixed some bugs * Adding event handler compatibility * Adding changes to pom * Changed swagger URL for resource catalog * testing on github * Just to make test compile * solving target commit * Delete resource-catalog/.DS_Store unwanted file * Delete service-catalog/.DS_Store unwanted file * Changing pom so it ignore resource catalog * Restoring pom * Delete common/.DS_Store unwanted file * Removing target files * Reverting changes to ServiceCategoryRef and commenting resource catalog pom for testing purposes * Changes to href on ServiceCategoryRef * Adding micronaut test to ServiceApiIT * Debuging tests * Debuging tests 2 * Debuging tests 3 * Debuging tests 4 * Debuging test 5 * Debuging test 6 service-inventory * Enabling resrouce-catalog api * Debuging tests resource-catalog 1 * Debuging resource-catalog tests 2 * Debuging test resource catalog 3 * Debuging resource catalog test 4 * solving conflicting elements * Solving conflicting lanes with main * Reverting unganted change to the application.yaml on service-catalog * Changed pom api url and ctk url to new * Solved problem with api url * Checking tests on main * New swagger api urls * Changes on .github to run test on my repository * Updating the service api url * Adding a missing Junit notation * Extra Junit notation unnintentional pushed in createBillPresentationMedia * Changes to service inventory rest (unfinished) * Solving EventHandler and test errors * Adding Service Inventory to scorpio test workflow * Debugging ServiceApiIT * Second debuggin on ServiceApiIT * Debug 3 ServiceApiIT * Debug 3 ServiceApiIT * Debug 4 updating ServiceApiIT, missing on the last commit * Debug 5 ServiceApiIT --- .github/workflows/test-brokers.yaml | 3 +- .github/workflows/test-caches.yaml | 0 .github/workflows/test.yaml | 0 README.md | 3 +- .../account/BillPresentationMediaApiIT.java | 1 + .../common/notification/EventConstants.java | 3 + pom.xml | 1 + .../ResourceCandidateApiIT.java | 1 + .../resourcecatalog/ResourceCatalogApiIT.java | 1 + .../ResourceCategoryApiIT.java | 1 + .../ResourceSpecificationApiIT.java | 1 + service-inventory/pom.xml | 273 ++++++ .../tmforum/serviceinventory/Application.java | 47 + .../serviceinventory/MappingHelper.java | 30 + .../serviceinventory/TMForumMapper.java | 73 ++ .../domain/OrderItemActionType.java | 22 + .../domain/RelatedEntityRefOrValue.java | 28 + .../domain/RelatedPlaceRefOrValue.java | 28 + .../domain/RelatedServiceOrderItem.java | 27 + .../serviceinventory/domain/Service.java | 133 +++ .../domain/ServiceRefOrValue.java | 67 ++ .../domain/ServiceRelationship.java | 17 + .../domain/ServiceStateType.java | 24 + .../rest/EventSubscriptionApiController.java | 66 ++ .../rest/ServiceApiController.java | 208 +++++ .../main/resources/application-in-memory.yaml | 11 + .../main/resources/application-orion-ld.yaml | 4 + .../src/main/resources/application-redis.yaml | 12 + .../main/resources/application-scorpio.yaml | 4 + .../src/main/resources/application.yaml | 39 + .../src/main/resources/logback.xml | 16 + .../EventSubscriptionApiIT.java | 307 +++++++ .../serviceinventory/ServiceApiIT.java | 844 ++++++++++++++++++ .../test/resources/application-in-memory.yaml | 11 + .../test/resources/application-orion-ld.yaml | 4 + .../src/test/resources/application-redis.yaml | 12 + .../test/resources/application-scorpio.yaml | 4 + .../src/test/resources/application.yaml | 39 + .../src/test/resources/logback.xml | 16 + .../tmforum/service/ServiceRelationship.java | 14 + 40 files changed, 2393 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/test-caches.yaml create mode 100644 .github/workflows/test.yaml create mode 100644 service-inventory/pom.xml create mode 100644 service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/Application.java create mode 100644 service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/MappingHelper.java create mode 100644 service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/TMForumMapper.java create mode 100644 service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/OrderItemActionType.java create mode 100644 service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/RelatedEntityRefOrValue.java create mode 100644 service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/RelatedPlaceRefOrValue.java create mode 100644 service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/RelatedServiceOrderItem.java create mode 100644 service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/Service.java create mode 100644 service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/ServiceRefOrValue.java create mode 100644 service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/ServiceRelationship.java create mode 100644 service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/ServiceStateType.java create mode 100644 service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/rest/EventSubscriptionApiController.java create mode 100644 service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/rest/ServiceApiController.java create mode 100644 service-inventory/src/main/resources/application-in-memory.yaml create mode 100644 service-inventory/src/main/resources/application-orion-ld.yaml create mode 100644 service-inventory/src/main/resources/application-redis.yaml create mode 100644 service-inventory/src/main/resources/application-scorpio.yaml create mode 100644 service-inventory/src/main/resources/application.yaml create mode 100644 service-inventory/src/main/resources/logback.xml create mode 100644 service-inventory/src/test/java/org/fiware/tmforum/serviceinventory/EventSubscriptionApiIT.java create mode 100644 service-inventory/src/test/java/org/fiware/tmforum/serviceinventory/ServiceApiIT.java create mode 100644 service-inventory/src/test/resources/application-in-memory.yaml create mode 100644 service-inventory/src/test/resources/application-orion-ld.yaml create mode 100644 service-inventory/src/test/resources/application-redis.yaml create mode 100644 service-inventory/src/test/resources/application-scorpio.yaml create mode 100644 service-inventory/src/test/resources/application.yaml create mode 100644 service-inventory/src/test/resources/logback.xml create mode 100644 service-shared-models/src/main/java/org/fiware/tmforum/service/ServiceRelationship.java diff --git a/.github/workflows/test-brokers.yaml b/.github/workflows/test-brokers.yaml index e53ef577..e38a52f5 100644 --- a/.github/workflows/test-brokers.yaml +++ b/.github/workflows/test-brokers.yaml @@ -11,6 +11,7 @@ on: push: branches: - tests + - ServiceInventory-Implementation concurrency: # Only cancel jobs for PR updates @@ -23,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - module: [ customer-bill-management, customer-management, party-catalog, party-role, product-catalog, product-inventory, product-ordering-management, resource-catalog, resource-function-activation, resource-inventory, service-catalog ] + module: [ customer-bill-management, customer-management, party-catalog, party-role, product-catalog, product-inventory, product-ordering-management, resource-catalog, resource-function-activation, resource-inventory, service-catalog, service-inventory ] # only test scorpio for speed up, orion is currently not really used with the tmforum broker: [ scorpio ] cache: [ in-memory, redis ] diff --git a/.github/workflows/test-caches.yaml b/.github/workflows/test-caches.yaml new file mode 100644 index 00000000..e69de29b diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 00000000..e69de29b diff --git a/README.md b/README.md index bf05a47d..7fffc574 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # FIWARE implementation of the TMFORUM-APIs -## Structure +## Structure + The project is setup as a maven multi-module project, to ease the development of independent implementations for each API. diff --git a/account/src/test/java/org/fiware/tmforum/account/BillPresentationMediaApiIT.java b/account/src/test/java/org/fiware/tmforum/account/BillPresentationMediaApiIT.java index fbcaf069..082c1692 100644 --- a/account/src/test/java/org/fiware/tmforum/account/BillPresentationMediaApiIT.java +++ b/account/src/test/java/org/fiware/tmforum/account/BillPresentationMediaApiIT.java @@ -272,6 +272,7 @@ public void listBillPresentationMedia200() throws Exception { } @Override + @Test public void listBillPresentationMedia400() throws Exception { HttpResponse> badRequestResponse = callAndCatch( () -> billPresentationMediaApiTestClient.listBillPresentationMedia(null, -1, null)); diff --git a/common/src/main/java/org/fiware/tmforum/common/notification/EventConstants.java b/common/src/main/java/org/fiware/tmforum/common/notification/EventConstants.java index e61c6266..670f0ecb 100644 --- a/common/src/main/java/org/fiware/tmforum/common/notification/EventConstants.java +++ b/common/src/main/java/org/fiware/tmforum/common/notification/EventConstants.java @@ -55,6 +55,7 @@ public class EventConstants { public static final String DELETE_EVENT_SUFFIX = "DeleteEvent"; public static final String CHANGE_EVENT_SUFFIX = "ChangeEvent"; public static final String INFORMATION_REQUIRED_EVENT_SUFFIX = "InformationRequiredEvent"; + public static final String EVENT_GROUP_SERVICE = "Service"; public static final Map> ALLOWED_EVENT_TYPES = Map.ofEntries( entry(EVENT_GROUP_PRODUCT, List.of(CREATE_EVENT_SUFFIX, ATTRIBUTE_VALUE_CHANGE_EVENT_SUFFIX, @@ -130,6 +131,8 @@ public class EventConstants { entry(EVENT_GROUP_PARTY_ACCOUNT, List.of(CREATE_EVENT_SUFFIX, CHANGE_EVENT_SUFFIX, DELETE_EVENT_SUFFIX)), entry(EVENT_GROUP_SETTLEMENT_ACCOUNT, List.of(CREATE_EVENT_SUFFIX, CHANGE_EVENT_SUFFIX, + DELETE_EVENT_SUFFIX)), + entry(EVENT_GROUP_SERVICE, List.of(CREATE_EVENT_SUFFIX, CHANGE_EVENT_SUFFIX, DELETE_EVENT_SUFFIX)) ); } diff --git a/pom.xml b/pom.xml index b985b1f0..6ed42be9 100644 --- a/pom.xml +++ b/pom.xml @@ -48,6 +48,7 @@ agreement usage-management account + service-inventory party-role diff --git a/resource-catalog/src/test/java/org/fiware/tmforum/resourcecatalog/ResourceCandidateApiIT.java b/resource-catalog/src/test/java/org/fiware/tmforum/resourcecatalog/ResourceCandidateApiIT.java index 83ec1585..162347e6 100644 --- a/resource-catalog/src/test/java/org/fiware/tmforum/resourcecatalog/ResourceCandidateApiIT.java +++ b/resource-catalog/src/test/java/org/fiware/tmforum/resourcecatalog/ResourceCandidateApiIT.java @@ -30,6 +30,7 @@ import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; +import java.net.URI; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/resource-catalog/src/test/java/org/fiware/tmforum/resourcecatalog/ResourceCatalogApiIT.java b/resource-catalog/src/test/java/org/fiware/tmforum/resourcecatalog/ResourceCatalogApiIT.java index ec7da190..16ebebc7 100644 --- a/resource-catalog/src/test/java/org/fiware/tmforum/resourcecatalog/ResourceCatalogApiIT.java +++ b/resource-catalog/src/test/java/org/fiware/tmforum/resourcecatalog/ResourceCatalogApiIT.java @@ -30,6 +30,7 @@ import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; +import java.net.URI; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/resource-catalog/src/test/java/org/fiware/tmforum/resourcecatalog/ResourceCategoryApiIT.java b/resource-catalog/src/test/java/org/fiware/tmforum/resourcecatalog/ResourceCategoryApiIT.java index 82b79bd7..2684dba1 100644 --- a/resource-catalog/src/test/java/org/fiware/tmforum/resourcecatalog/ResourceCategoryApiIT.java +++ b/resource-catalog/src/test/java/org/fiware/tmforum/resourcecatalog/ResourceCategoryApiIT.java @@ -30,6 +30,7 @@ import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; +import java.net.URI; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/resource-catalog/src/test/java/org/fiware/tmforum/resourcecatalog/ResourceSpecificationApiIT.java b/resource-catalog/src/test/java/org/fiware/tmforum/resourcecatalog/ResourceSpecificationApiIT.java index 04fdadf4..0bda0663 100644 --- a/resource-catalog/src/test/java/org/fiware/tmforum/resourcecatalog/ResourceSpecificationApiIT.java +++ b/resource-catalog/src/test/java/org/fiware/tmforum/resourcecatalog/ResourceSpecificationApiIT.java @@ -260,6 +260,7 @@ private static Stream> provideInvalidFeatur .featureSpecRelationship(List.of(FeatureSpecificationRelationshipVOTestExample.build() .featureId(null) .parentSpecificationId("invalid"))))); + invalidFeatureSpecs.add( new ArgumentPair<>("Feature specification with non-existent resource id on spec rel should fail.", FeatureSpecificationVOTestExample.build() diff --git a/service-inventory/pom.xml b/service-inventory/pom.xml new file mode 100644 index 00000000..06e86c70 --- /dev/null +++ b/service-inventory/pom.xml @@ -0,0 +1,273 @@ + + + 4.0.0 + org.fiware.tmforum + service-inventory + 0.1 + + + org.fiware + tmforum + 0.1 + + + + https://tmf-open-api-table-documents.s3.eu-west-1.amazonaws.com/OpenApiTable/4.0.0/swagger/TMF638_Service_Inventory_Management_API_v4.0.0_swagger.json + https://tmf-open-api-table-documents.s3.eu-west-1.amazonaws.com/OpenApiTable/4.0.0/ctk/TMF638-ServiceInventory_V4-0-0.zip + TMF638-ServiceInventory + Mac-Linux-RUNCTK.sh + /tmf-api/serviceInventoryManagement/v4 + sed -i 's/8633/8632/g' /opt/ctk/TMF638-ServiceInventory/config.json + + + + + org.fiware.tmforum + common + ${project.version} + compile + + + org.fiware.tmforum + resource-shared-models + ${project.version} + compile + + + org.fiware.tmforum + service-shared-models + ${project.version} + compile + + + + + org.projectlombok + lombok + + + org.mapstruct + mapstruct + + + + + io.micronaut + micronaut-inject + compile + + + io.micronaut + micronaut-validation + compile + + + io.micronaut + micronaut-runtime + compile + + + io.micronaut + micronaut-management + compile + + + io.micronaut.cache + micronaut-cache-caffeine + compile + + + io.micronaut + micronaut-http-client + compile + + + io.micronaut + micronaut-http-server-netty + compile + + + io.micronaut + micronaut-jackson-databind + compile + + + io.micronaut + micronaut-jackson-core + compile + + + io.micronaut.reactor + micronaut-reactor + compile + + + + + javax.inject + javax.inject + + + javax.annotation + javax.annotation-api + + + + com.google.code.findbugs + annotations + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + compile + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-params + test + + + io.micronaut.test + micronaut-test-junit5 + test + + + io.micronaut.test + micronaut-test-core + test + + + + org.mockito + mockito-all + test + + + org.mockito + mockito-junit-jupiter + test + + + org.awaitility + awaitility + test + + + + + + + src/main/resources + true + + + + + src/test/resources + true + + + + + + org.openapitools + openapi-generator-maven-plugin + ${version.org.openapitools.generator-maven-plugin} + + + openapi-service-inventory-api + generate-sources + + generate + + + ${tmforum.api.url} + org.fiware.serviceinventory.api + true + org.fiware.serviceinventory.model + micronaut + VO + ${project.build.directory} + false + + true + false + true + true + true + false + true + false + true + + + java.util.Date=java.time.Instant + + + + + + + io.kokuwa.micronaut + micronaut-openapi-codegen + ${version.io.kokuwa.micronaut.codegen} + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + io.kokuwa.maven + k3s-maven-plugin + + + maven-resources-plugin + + + io.fabric8 + docker-maven-plugin + + + + + com.google.cloud.tools + jib-maven-plugin + + + + \ No newline at end of file diff --git a/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/Application.java b/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/Application.java new file mode 100644 index 00000000..134cb03c --- /dev/null +++ b/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/Application.java @@ -0,0 +1,47 @@ +package org.fiware.tmforum.serviceinventory; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import io.micronaut.context.annotation.Bean; +import io.micronaut.context.annotation.Factory; +import io.micronaut.context.event.BeanCreatedEvent; +import io.micronaut.context.event.BeanCreatedEventListener; +import io.micronaut.runtime.Micronaut; +import lombok.RequiredArgsConstructor; +import org.fiware.serviceinventory.model.*; +import org.fiware.tmforum.common.mapping.FieldCleaningSerializer; + +import javax.inject.Singleton; +import java.time.Clock; + +/** + * Base application as starting point + */ +@Factory +public class Application { + + public static void main(String[] args) { + Micronaut.run(Application.class, args); + } + + @Bean + public Clock clock() { + return Clock.systemUTC(); + } + + @Singleton + @RequiredArgsConstructor + static class ObjectMapperBeanEventListener implements BeanCreatedEventListener { + + @Override + public ObjectMapper onCreated(BeanCreatedEvent event) { + final ObjectMapper objectMapper = event.getBean(); + SimpleModule fieldParamModule = new SimpleModule(); + // we need to register per class, in order to use the generic serializer + fieldParamModule.addSerializer(ServiceVO.class, new FieldCleaningSerializer<>()); + objectMapper.registerModule(fieldParamModule); + return objectMapper; + } + } +} + diff --git a/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/MappingHelper.java b/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/MappingHelper.java new file mode 100644 index 00000000..2b4dad8b --- /dev/null +++ b/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/MappingHelper.java @@ -0,0 +1,30 @@ +package org.fiware.tmforum.serviceinventory; + +import org.fiware.tmforum.resource.ResourceRef; +import org.fiware.tmforum.service.*; +import org.fiware.tmforum.serviceinventory.domain.ServiceRefOrValue; +import org.mapstruct.Named; + +import java.net.URI; +import java.util.Optional; + +@Named("MappingHelper") +public class MappingHelper { + + + @Named("toResourceRef") + public static ResourceRef toResourceRef(String parentId) { + if (parentId == null) { + return null; + } + return new ResourceRef(parentId); + } + + @Named("fromResourceRef") + public static String fromResourceRef(ResourceRef parentId) { + if (parentId == null) { + return null; + } + return Optional.ofNullable(parentId.getEntityId()).map(URI::toString).orElse(null); + } +} diff --git a/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/TMForumMapper.java b/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/TMForumMapper.java new file mode 100644 index 00000000..153219eb --- /dev/null +++ b/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/TMForumMapper.java @@ -0,0 +1,73 @@ +package org.fiware.tmforum.serviceinventory; + +import org.fiware.tmforum.common.domain.subscription.Subscription; +import org.fiware.tmforum.common.domain.subscription.TMForumSubscription; +import org.fiware.tmforum.resource.*; +import io.github.wistefan.mapping.MappingException; +import org.fiware.serviceinventory.model.*; +import org.fiware.tmforum.serviceinventory.domain.*; +import org.fiware.tmforum.common.mapping.IdHelper; +import org.fiware.tmforum.resource.ResourceSpecificationRef; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; + +/** + * Mapper between the internal model and api-domain objects + */ +@Mapper(componentModel = "jsr330", uses = IdHelper.class) +public interface TMForumMapper { + + // product + + @Mapping(target = "id", source = "id") + @Mapping(target = "href", source = "id") + ServiceVO map(ServiceCreateVO productCreateVO, URI id); + + ServiceVO map(Service product); + + Service map(ServiceVO productVO); + + @Mapping(target = "id", source = "id") + Service map(ServiceUpdateVO productUpdateVO, String id); + + //RelatedServiceOrderItemRef map (RelatedServiceOrderItemVO relatedServiceOrderItemVO); + + default URL map(String value) { + if (value == null) { + return null; + } + try { + return new URL(value); + } catch (MalformedURLException e) { + throw new MappingException(String.format("%s is not a URL.", value), e); + } + } + + @Mapping(target = "query", source = "rawQuery") + EventSubscriptionVO map(TMForumSubscription subscription); + + default String map(URL value) { + if (value == null) { + return null; + } + return value.toString(); + } + + default URI mapToURI(String value) { + if (value == null) { + return null; + } + return URI.create(value); + } + + default String mapFromURI(URI value) { + if (value == null) { + return null; + } + return value.toString(); + } +} diff --git a/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/OrderItemActionType.java b/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/OrderItemActionType.java new file mode 100644 index 00000000..fffb4f9f --- /dev/null +++ b/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/OrderItemActionType.java @@ -0,0 +1,22 @@ +package org.fiware.tmforum.serviceinventory.domain; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum OrderItemActionType { + + ADD("add"), + MODIFY("modify"), + DELETE("delete"), + NOCHANGE("noChange"); + + private final String value; + + OrderItemActionType(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } +} diff --git a/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/RelatedEntityRefOrValue.java b/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/RelatedEntityRefOrValue.java new file mode 100644 index 00000000..0db609f4 --- /dev/null +++ b/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/RelatedEntityRefOrValue.java @@ -0,0 +1,28 @@ +package org.fiware.tmforum.serviceinventory.domain; + +import io.github.wistefan.mapping.annotations.AttributeGetter; +import io.github.wistefan.mapping.annotations.AttributeSetter; +import io.github.wistefan.mapping.annotations.AttributeType; +import lombok.EqualsAndHashCode; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import org.fiware.tmforum.common.domain.Entity; + +import java.net.URI; +import java.util.List; + +@EqualsAndHashCode(callSuper = true) +@Data +public class RelatedEntityRefOrValue extends Entity { + + private URI id; + private URI href; + private String name; + private String role; + private String atBaseType; + private URI atSchemaLocation; + private String atType; + private String atReferredType; + +} \ No newline at end of file diff --git a/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/RelatedPlaceRefOrValue.java b/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/RelatedPlaceRefOrValue.java new file mode 100644 index 00000000..856fbbff --- /dev/null +++ b/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/RelatedPlaceRefOrValue.java @@ -0,0 +1,28 @@ +package org.fiware.tmforum.serviceinventory.domain; + +import io.github.wistefan.mapping.annotations.AttributeGetter; +import io.github.wistefan.mapping.annotations.AttributeSetter; +import io.github.wistefan.mapping.annotations.AttributeType; +import lombok.EqualsAndHashCode; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import org.fiware.tmforum.common.domain.Entity; + +import java.net.URI; +import java.util.List; + +@EqualsAndHashCode(callSuper = true) +@Data +public class RelatedPlaceRefOrValue extends Entity { + + private URI id; + private URI href; + private String name; + private String role; + private String atBaseType; + private URI atSchemaLocation; + private String atType; + private String atReferredType; + +} diff --git a/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/RelatedServiceOrderItem.java b/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/RelatedServiceOrderItem.java new file mode 100644 index 00000000..7ab48563 --- /dev/null +++ b/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/RelatedServiceOrderItem.java @@ -0,0 +1,27 @@ +package org.fiware.tmforum.serviceinventory.domain; + +import io.github.wistefan.mapping.annotations.AttributeGetter; +import io.github.wistefan.mapping.annotations.AttributeSetter; +import io.github.wistefan.mapping.annotations.AttributeType; +import lombok.EqualsAndHashCode; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import org.fiware.tmforum.common.domain.Entity; + +import java.net.URI; +import java.util.List; + +@EqualsAndHashCode(callSuper = true) +@Data +public class RelatedServiceOrderItem extends Entity { + + private String ItemId; + private String name; + private String role; + private String serviceOrderHref; + private String serviceOrderId; + private OrderItemActionType itemAction; + private String atReferredType; + +} \ No newline at end of file diff --git a/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/Service.java b/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/Service.java new file mode 100644 index 00000000..d7756519 --- /dev/null +++ b/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/Service.java @@ -0,0 +1,133 @@ +package org.fiware.tmforum.serviceinventory.domain; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.fiware.tmforum.common.domain.*; +import org.fiware.tmforum.resource.Characteristic; +import org.fiware.tmforum.resource.*; +import org.fiware.tmforum.service.*; +import io.github.wistefan.mapping.annotations.AttributeGetter; +import io.github.wistefan.mapping.annotations.AttributeSetter; +import io.github.wistefan.mapping.annotations.AttributeType; +import io.github.wistefan.mapping.annotations.MappingEnabled; + +import java.net.URI; +import java.time.Instant; +import java.util.List; + +@EqualsAndHashCode(callSuper = true) +@MappingEnabled(entityType = Service.TYPE_SERVICE) +public class Service extends EntityWithId { + + public static final String TYPE_SERVICE = "service"; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY, targetName = "href")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY, targetName = "href")})) + private URI href; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY, targetName = "category")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY, targetName = "category")})) + private String category; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY, targetName = "description")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY, targetName = "description")})) + private String description; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY, targetName = "endDate")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY, targetName = "endDate")})) + private Instant endDate; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY, targetName = "hasStarted")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY, targetName = "hasStarted")})) + private Boolean hasStarted; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY, targetName = "isBundle")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY, targetName = "isBundle")})) + private Boolean isBundle; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY, targetName = "isServiceEnabled")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY, targetName = "isServiceEnabled")})) + private Boolean isServiceEnabled; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY, targetName = "isStateful")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY, targetName = "isStateful")})) + private Boolean isStateful; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY, targetName = "name")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY, targetName = "name")})) + private String name; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY, targetName = "serviceDate")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY, targetName = "serviceDate")})) + private String serviceDate; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY, targetName = "serviceType")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY, targetName = "serviceType")})) + private String serviceType; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY, targetName = "startDate")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY, targetName = "startDate")})) + private Instant startDate; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY, targetName = "startMode")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY, targetName = "startMode")})) + private String startMode; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY_LIST, targetName = "feature")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY_LIST, targetName = "feature", targetClass = Feature.class)})) + private List feature; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY_LIST, targetName = "note")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY_LIST, targetName = "note", targetClass = Note.class)})) + private List note; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY_LIST, targetName = "place")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY_LIST, targetName = "place", targetClass = RelatedPlaceRefOrValue.class)})) + private List place; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY_LIST, targetName = "relatedEntity")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY_LIST, targetName = "relatedEntity", targetClass = RelatedEntityRefOrValue.class)})) + private List relatedEntity; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.RELATIONSHIP_LIST, targetName = "relatedParty")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.RELATIONSHIP_LIST, targetName = "relatedParty", targetClass = RelatedParty.class)})) + private List relatedParty; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY_LIST, targetName = "serviceCharacteristic")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY_LIST, targetName = "serviceCharacteristic", targetClass = Characteristic.class)})) + private List serviceCharacteristic; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY_LIST, targetName = "serviceOrderItem")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY_LIST, targetName = "serviceOrderItem", targetClass = RelatedServiceOrderItem.class)})) + private List serviceOrderItem; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY_LIST, targetName = "serviceRelationship")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY_LIST, targetName = "serviceRelationship", targetClass = ServiceRelationship.class)})) + private List serviceRelationship; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.RELATIONSHIP, targetName = "serviceSpecification")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.RELATIONSHIP, targetName = "serviceSpecification", targetClass = ServiceSpecificationRef.class)})) + private ServiceSpecificationRef serviceSpecification; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY, targetName = "state")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY, targetName = "state", targetClass = ServiceStateType.class)})) + private ServiceStateType state; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.RELATIONSHIP_LIST, targetName = "supportingResource")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.RELATIONSHIP_LIST, targetName = "supportingResource", targetClass = ResourceRef.class)})) + private List supportingResource; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY_LIST, targetName = "supportingService")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY_LIST, targetName = "supportingService", fromProperties = true, targetClass = ServiceRefOrValue.class)})) + private List supportingService; + + public Service(String id) { + super(TYPE_SERVICE, id); + } + + @Override + public String getEntityState() { + return state.getValue(); + } +} diff --git a/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/ServiceRefOrValue.java b/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/ServiceRefOrValue.java new file mode 100644 index 00000000..aa5f01d7 --- /dev/null +++ b/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/ServiceRefOrValue.java @@ -0,0 +1,67 @@ +package org.fiware.tmforum.serviceinventory.domain; + +import io.github.wistefan.mapping.annotations.AttributeGetter; +import io.github.wistefan.mapping.annotations.AttributeSetter; +import io.github.wistefan.mapping.annotations.AttributeType; +import lombok.EqualsAndHashCode; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import org.fiware.tmforum.common.domain.Entity; +import org.fiware.tmforum.common.domain.*; +import org.fiware.tmforum.resource.Characteristic; +import org.fiware.tmforum.common.validation.ReferencedEntity; +import org.fiware.tmforum.resource.*; +import org.fiware.tmforum.service.*; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.time.Instant; + +@EqualsAndHashCode(callSuper = true) +@Data +public class ServiceRefOrValue extends Entity implements ReferencedEntity { + + private URI id; + private URI href; + private String category; + private String description; + private Instant endDate; + private Boolean hasStarted; + private Boolean isBundle; + private Boolean isServiceEnabled; + private Boolean isStateful; + private String name; + private String serviceDate; + private String serviceType; + private String startDate; + private String startMode; + private List feature; + private List note; + private List place; + private List relatedEntity; + private List relatedParty; + private List serviceCharacteristic; + private List serviceOrderItem; + private List serviceRelationship; + private ServiceSpecificationRef serviceSpecification; + private ServiceStateType state; + private List supportingResource; + private List supportingService; + private String atBaseType; + private URI atSchemaLocation; + private String atType; + private String atReferredType; + + @Override + public List getReferencedTypes() { + return new ArrayList<>(List.of("service")); + } + + @Override + public URI getEntityId() { + return this.id; + } + +} diff --git a/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/ServiceRelationship.java b/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/ServiceRelationship.java new file mode 100644 index 00000000..b0a251ca --- /dev/null +++ b/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/ServiceRelationship.java @@ -0,0 +1,17 @@ +package org.fiware.tmforum.serviceinventory.domain; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.fiware.tmforum.resource.*; +import org.fiware.tmforum.common.domain.Entity; + +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +public class ServiceRelationship extends Entity { + + private String relationshipType; + private ServiceRefOrValue service; + private List serviceRelationshipCharacteristic; +} diff --git a/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/ServiceStateType.java b/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/ServiceStateType.java new file mode 100644 index 00000000..06c2ed58 --- /dev/null +++ b/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/domain/ServiceStateType.java @@ -0,0 +1,24 @@ +package org.fiware.tmforum.serviceinventory.domain; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum ServiceStateType { + + FEASIBILITYCHECKED("feasibilityChecked"), + DESIGNED("designed"), + RESERVED("reserved"), + INACTIVE("inactive"), + ACTIVE("active"), + TERMINATED("terminated"); + + private final String value; + + ServiceStateType(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } +} diff --git a/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/rest/EventSubscriptionApiController.java b/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/rest/EventSubscriptionApiController.java new file mode 100644 index 00000000..56ed05db --- /dev/null +++ b/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/rest/EventSubscriptionApiController.java @@ -0,0 +1,66 @@ +package org.fiware.tmforum.serviceinventory.rest; + +import io.github.wistefan.mapping.EntityVOMapper; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.annotation.Controller; +import lombok.extern.slf4j.Slf4j; +import org.fiware.serviceinventory.api.EventsSubscriptionApi; +import org.fiware.serviceinventory.model.EventSubscriptionInputVO; +import org.fiware.serviceinventory.model.EventSubscriptionVO; +import org.fiware.tmforum.common.configuration.GeneralProperties; +import org.fiware.tmforum.common.domain.subscription.TMForumSubscription; +import org.fiware.tmforum.common.notification.NgsiLdEventHandler; +import org.fiware.tmforum.common.notification.TMForumEventHandler; +import org.fiware.tmforum.common.querying.QueryParser; +import org.fiware.tmforum.common.repository.TmForumRepository; +import org.fiware.tmforum.common.rest.AbstractSubscriptionApiController; +import org.fiware.tmforum.common.validation.ReferenceValidationService; +import org.fiware.tmforum.serviceinventory.TMForumMapper; +import org.fiware.tmforum.serviceinventory.domain.Service; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Map; + +import static java.util.Map.entry; +import static org.fiware.tmforum.common.notification.EventConstants.EVENT_GROUP_SERVICE; + +@Slf4j +@Controller("${general.basepath:/}") +public class EventSubscriptionApiController extends AbstractSubscriptionApiController implements EventsSubscriptionApi { + private final TMForumMapper tmForumMapper; + private static final Map EVENT_GROUP_TO_ENTITY_NAME_MAPPING = Map.ofEntries( + entry(EVENT_GROUP_SERVICE, Service.TYPE_SERVICE) + ); + private static final List EVENT_GROUPS = List.of(EVENT_GROUP_SERVICE); + private static final Map> ENTITY_NAME_TO_ENTITY_CLASS_MAPPING = Map.ofEntries( + entry(Service.TYPE_SERVICE, Service.class) + ); + + public EventSubscriptionApiController(QueryParser queryParser, ReferenceValidationService validationService, + TmForumRepository repository, TMForumMapper tmForumMapper, + TMForumEventHandler tmForumEventHandler, NgsiLdEventHandler ngsiLdEventHandler, + GeneralProperties generalProperties, EntityVOMapper entityVOMapper) { + super(queryParser, validationService, repository, EVENT_GROUP_TO_ENTITY_NAME_MAPPING, + ENTITY_NAME_TO_ENTITY_CLASS_MAPPING, tmForumEventHandler, ngsiLdEventHandler, + generalProperties, entityVOMapper); + this.tmForumMapper = tmForumMapper; + } + + @Override + public Mono> registerListener( + @NonNull EventSubscriptionInputVO eventSubscriptionInputVO) { + TMForumSubscription subscription = buildSubscription(eventSubscriptionInputVO.getCallback(), + eventSubscriptionInputVO.getQuery(), EVENT_GROUPS); + + return create(subscription) + .map(tmForumMapper::map) + .map(HttpResponse::created); + } + + @Override + public Mono> unregisterListener(@NonNull String id) { + return delete(id); + } +} diff --git a/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/rest/ServiceApiController.java b/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/rest/ServiceApiController.java new file mode 100644 index 00000000..24ecc7b5 --- /dev/null +++ b/service-inventory/src/main/java/org/fiware/tmforum/serviceinventory/rest/ServiceApiController.java @@ -0,0 +1,208 @@ +package org.fiware.tmforum.serviceinventory.rest; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.annotation.Controller; +import lombok.extern.slf4j.Slf4j; +import org.fiware.serviceinventory.api.ServiceApi; +import org.fiware.serviceinventory.model.ServiceCreateVO; +import org.fiware.serviceinventory.model.ServiceUpdateVO; +import org.fiware.serviceinventory.model.ServiceVO; +import org.fiware.tmforum.common.exception.TmForumException; +import org.fiware.tmforum.common.exception.TmForumExceptionReason; +import org.fiware.tmforum.common.mapping.IdHelper; +import org.fiware.tmforum.common.notification.TMForumEventHandler; +import org.fiware.tmforum.common.querying.QueryParser; +import org.fiware.tmforum.common.repository.TmForumRepository; +import org.fiware.tmforum.common.rest.AbstractApiController; +import org.fiware.tmforum.common.validation.ReferenceValidationService; +import org.fiware.tmforum.common.validation.ReferencedEntity; +import org.fiware.tmforum.resource.*; +import org.fiware.tmforum.serviceinventory.domain.Service; +import org.fiware.tmforum.serviceinventory.TMForumMapper; +import org.fiware.tmforum.serviceinventory.domain.ServiceRelationship; +import reactor.core.publisher.Mono; + +import java.net.URI; +import java.util.*; + +@Slf4j +@Controller("${general.basepath:/}") +public class ServiceApiController extends AbstractApiController implements ServiceApi { + + private final TMForumMapper tmForumMapper; + + public ServiceApiController(QueryParser queryParser, ReferenceValidationService validationService, + TmForumRepository repository, TMForumMapper tmForumMapper, TMForumEventHandler eventHandler) { + super(queryParser, validationService, repository, eventHandler); + this.tmForumMapper = tmForumMapper; + } + + @Override + public Mono> createService(@NonNull ServiceCreateVO serviceCreateVO) { + Service service = tmForumMapper.map( + tmForumMapper.map(serviceCreateVO, + IdHelper.toNgsiLd(UUID.randomUUID().toString(), Service.TYPE_SERVICE))); + + validateInternalRefs(service); + + return create(getCheckingMono(service), Service.class) + .map(tmForumMapper::map) + .map(HttpResponse::created); + } + + private Mono getCheckingMono(Service service) { + + List> references = new ArrayList<>(); + references.add(service.getSupportingService()); + references.add(service.getRelatedParty()); + Optional.ofNullable(service.getServiceSpecification()).map(List::of).ifPresent(references::add); + Optional.ofNullable(service.getServiceSpecification()).map(List::of).ifPresent(references::add); + + Mono checkingMono = getCheckingMono(service, references); + + if (service.getServiceRelationship() != null && !service.getServiceRelationship().isEmpty()) { + List> serviceRelCheckingMonos = service.getServiceRelationship() + .stream() + .map(ServiceRelationship::getService) + .map(serviceRef -> getCheckingMono(service, List.of(List.of(serviceRef)))) + .toList(); + if (!serviceRelCheckingMonos.isEmpty()) { + Mono serviceRelCheckingMono = Mono.zip(serviceRelCheckingMonos, p -> service); + checkingMono = Mono.zip(serviceRelCheckingMono, checkingMono, (p1, p2) -> service); + } + } + + if (service.getFeature() != null && !service.getFeature().isEmpty()) { + List> featureConstraintsCheckingMonos = service.getFeature() + .stream() + .peek(feature -> validateInternalFeatureRefs(feature, service)) + .filter(feature -> feature.getConstraint() != null) + .map(feature -> getCheckingMono(service, List.of(feature.getConstraint()))) + .toList(); + if (!featureConstraintsCheckingMonos.isEmpty()) { + Mono featureConstraintsCheckingMono = Mono.zip(featureConstraintsCheckingMonos, + p -> service); + checkingMono = Mono.zip(featureConstraintsCheckingMono, checkingMono, (p1, p2) -> service); + } + } + return checkingMono + .onErrorMap(throwable -> + new TmForumException( + String.format("Was not able to create service %s", service.getId()), + throwable, + TmForumExceptionReason.INVALID_RELATIONSHIP)); + } + + private void validateInternalRefs(Service service) { + if (service.getNote() != null) { + List noteIds = service.getNote().stream().map(Note::getId).toList(); + if (noteIds.size() != new HashSet<>(noteIds).size()) { + throw new TmForumException( + String.format("Duplicate note ids are not allowed: %s", noteIds), + TmForumExceptionReason.INVALID_DATA); + } + } + if (service.getServiceCharacteristic() != null) { + service.getServiceCharacteristic() + .forEach(characteristic -> validateInternalCharacteristicRefs(characteristic, + service.getServiceCharacteristic())); + } + + } + + private void validateInternalCharacteristicRefs(Characteristic characteristic, + List characteristics) { + List charIds = characteristics + .stream() + .map(Characteristic::getId) + .toList(); + if (charIds.size() != new HashSet<>(charIds).size()) { + throw new TmForumException( + String.format("Duplicate characteristic ids are not allowed: %s", charIds), + TmForumExceptionReason.INVALID_DATA); + } + + if (characteristic.getCharacteristicRelationship() != null) { + characteristic.getCharacteristicRelationship() + .stream() + .map(CharacteristicRelationship::getId) + .filter(charRef -> !charIds.contains(charRef)) + .findFirst() + .ifPresent(missingId -> { + throw new TmForumException( + String.format("Referenced characteristic %s does not exist", missingId), + TmForumExceptionReason.INVALID_DATA); + }); + } + } + + + private void validateInternalFeatureRefs(Feature feature, Service service) { + List featureIds = service.getFeature() + .stream() + .map(Feature::getId) + .toList(); + // check for duplicate ids + if (featureIds.size() != new HashSet<>(featureIds).size()) { + throw new TmForumException(String.format("Duplicate feature ids are not allowed: %s", featureIds), + TmForumExceptionReason.INVALID_DATA); + } + if (feature.getFeatureRelationship() != null) { + feature.getFeatureRelationship() + .stream() + .map(FeatureRelationship::getId) + .filter(featureRef -> !featureIds.contains(featureRef)) + .findFirst() + .ifPresent(missingId -> { + throw new TmForumException( + String.format("Referenced feature %s does not exist", missingId), + TmForumExceptionReason.INVALID_DATA); + }); + } + if (feature.getFeatureCharacteristic() != null) { + feature.getFeatureCharacteristic() + .forEach(characteristic -> validateInternalCharacteristicRefs(characteristic, + feature.getFeatureCharacteristic())); + } + } + + @Override public Mono> deleteService(@NonNull String id) { + return delete(id); + } + + @Override public Mono>> listService(@Nullable String fields, @Nullable Integer offset, + @Nullable Integer limit) { + return list(offset, limit, Service.TYPE_SERVICE, Service.class) + .map(serviceStream -> serviceStream + .map(tmForumMapper::map) + .toList()) + .switchIfEmpty(Mono.just(List.of())) + .map(HttpResponse::ok); + } + + @Override public Mono> patchService(@NonNull String id, + @NonNull ServiceUpdateVO serviceUpdateVO) { + // non-ngsi-ld ids cannot exist. + if (!IdHelper.isNgsiLdId(id)) { + throw new TmForumException("Did not receive a valid id, such service cannot exist.", + TmForumExceptionReason.NOT_FOUND); + } + + Service service = tmForumMapper.map(serviceUpdateVO, id); + validateInternalRefs(service); + + return patch(id, service, getCheckingMono(service), Service.class) + .map(tmForumMapper::map) + .map(HttpResponse::ok); + } + + @Override public Mono> retrieveService(@NonNull String id, @Nullable String fields) { + return retrieve(id, Service.class) + .switchIfEmpty(Mono.error(new TmForumException("No such service exists.", + TmForumExceptionReason.NOT_FOUND))) + .map(tmForumMapper::map) + .map(HttpResponse::ok); + } +} diff --git a/service-inventory/src/main/resources/application-in-memory.yaml b/service-inventory/src/main/resources/application-in-memory.yaml new file mode 100644 index 00000000..572d7336 --- /dev/null +++ b/service-inventory/src/main/resources/application-in-memory.yaml @@ -0,0 +1,11 @@ +micronaut: + caches: + entities: + maximumSize: 1000 + # enough to not call twice in one call, but would prevent conflicting writes + expire-after-write: 2s + expire-after-access: 2s + subscriptions: + maximumSize: 1000 + expire-after-write: 14d + expire-after-access: 14d \ No newline at end of file diff --git a/service-inventory/src/main/resources/application-orion-ld.yaml b/service-inventory/src/main/resources/application-orion-ld.yaml new file mode 100644 index 00000000..367ab0b7 --- /dev/null +++ b/service-inventory/src/main/resources/application-orion-ld.yaml @@ -0,0 +1,4 @@ +general: + ngsildOrQueryValue: ";" + ngsildOrQueryKey: ";" + encloseQuery: true \ No newline at end of file diff --git a/service-inventory/src/main/resources/application-redis.yaml b/service-inventory/src/main/resources/application-redis.yaml new file mode 100644 index 00000000..3aebfb0d --- /dev/null +++ b/service-inventory/src/main/resources/application-redis.yaml @@ -0,0 +1,12 @@ +redis: + uri: redis://localhost:6379 + caches: + entities: + # enough to not call twice in one call, but would prevent conflicting writes + expire-after-write: 2s + expire-after-access: 2s + value-serializer: io.github.wistefan.mapping.EntityVOSerializer + subscriptions: + expire-after-write: 14d + expire-after-access: 14d + value-serializer: io.github.wistefan.mapping.EntityVOSerializer \ No newline at end of file diff --git a/service-inventory/src/main/resources/application-scorpio.yaml b/service-inventory/src/main/resources/application-scorpio.yaml new file mode 100644 index 00000000..b8947264 --- /dev/null +++ b/service-inventory/src/main/resources/application-scorpio.yaml @@ -0,0 +1,4 @@ +general: + ngsildOrQueryValue: "," + ngsildOrQueryKey: "," + encloseQuery: false \ No newline at end of file diff --git a/service-inventory/src/main/resources/application.yaml b/service-inventory/src/main/resources/application.yaml new file mode 100644 index 00000000..2381def1 --- /dev/null +++ b/service-inventory/src/main/resources/application.yaml @@ -0,0 +1,39 @@ +micronaut: + + server: + port: 8632 + + metrics: + enabled: true + export: + prometheus: + step: PT2s + descriptions: false + + http: + services: + read-timeout: 30s + ngsi: + path: ngsi-ld/v1 + url: http://localhost:1026 + read-timeout: 30 +--- +jackson: + serialization: + writeDatesAsTimestamps: false +--- +endpoints: + metrics: + enabled: true + health: + enabled: true + +--- +loggers: + levels: + ROOT: TRACE + +--- +general: + contextUrl: https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld + serverHost: http://localhost:8632 \ No newline at end of file diff --git a/service-inventory/src/main/resources/logback.xml b/service-inventory/src/main/resources/logback.xml new file mode 100644 index 00000000..f075e861 --- /dev/null +++ b/service-inventory/src/main/resources/logback.xml @@ -0,0 +1,16 @@ + + + + true + + + %cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n + + + + + + + + \ No newline at end of file diff --git a/service-inventory/src/test/java/org/fiware/tmforum/serviceinventory/EventSubscriptionApiIT.java b/service-inventory/src/test/java/org/fiware/tmforum/serviceinventory/EventSubscriptionApiIT.java new file mode 100644 index 00000000..a6c527f2 --- /dev/null +++ b/service-inventory/src/test/java/org/fiware/tmforum/serviceinventory/EventSubscriptionApiIT.java @@ -0,0 +1,307 @@ +package org.fiware.tmforum.serviceinventory; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import org.fiware.ngsi.api.EntitiesApiClient; +import org.fiware.serviceinventory.api.EventsSubscriptionApiTestClient; +import org.fiware.serviceinventory.api.EventsSubscriptionApiTestSpec; +import org.fiware.serviceinventory.model.EventSubscriptionInputVO; +import org.fiware.serviceinventory.model.EventSubscriptionInputVOTestExample; +import org.fiware.serviceinventory.model.EventSubscriptionVO; +import org.fiware.serviceinventory.model.EventSubscriptionVOTestExample; +import org.fiware.tmforum.common.configuration.GeneralProperties; +import org.fiware.tmforum.common.domain.subscription.Subscription; +import org.fiware.tmforum.common.exception.ErrorDetails; +import org.fiware.tmforum.common.test.AbstractApiIT; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@MicronautTest(packages = { "org.fiware.tmforum.serviceinventory" }) +/* + * the test currently fails for scorpio - {@link https://github.com/ScorpioBroker/ScorpioBroker/issues/456} + */ +@Requires(notEnv = "scorpio") +public class EventSubscriptionApiIT extends AbstractApiIT implements EventsSubscriptionApiTestSpec { + + private static final String ANY_CALLBACK = "https://test.com"; + + public final EventsSubscriptionApiTestClient eventsSubscriptionApiTestClient; + private EventSubscriptionInputVO eventSubscriptionInputVO; + private String message; + private EventSubscriptionVO expectedEventSubscription; + + public EventSubscriptionApiIT(EventsSubscriptionApiTestClient eventsSubscriptionApiTestClient, + EntitiesApiClient entitiesApiClient, ObjectMapper objectMapper, + GeneralProperties generalProperties) { + super(entitiesApiClient, objectMapper, generalProperties); + this.eventsSubscriptionApiTestClient = eventsSubscriptionApiTestClient; + } + + @Override + protected String getEntityType() { + return Subscription.TYPE_SUBSCRIPTION; + } + + @ParameterizedTest + @MethodSource("provideValidEventSubscriptionInputs") + public void registerListener201(String message, EventSubscriptionInputVO eventSubscriptionInputVO, + EventSubscriptionVO expectedEventSubscription) throws Exception { + this.message = message; + this.eventSubscriptionInputVO = eventSubscriptionInputVO; + this.expectedEventSubscription = expectedEventSubscription; + registerListener201(); + } + + @Override + public void registerListener201() throws Exception { + HttpResponse registerResponse = + callAndCatch(() -> eventsSubscriptionApiTestClient.registerListener(eventSubscriptionInputVO)); + assertEquals(HttpStatus.CREATED, registerResponse.getStatus(), message); + assertTrue(registerResponse.getBody().isPresent()); + expectedEventSubscription.setId(registerResponse.getBody().get().getId()); + + assertEquals(expectedEventSubscription, registerResponse.getBody().get(), message); + } + + @ParameterizedTest + @MethodSource("provideInvalidEventSubscriptionInputs") + public void registerListener400(String message, EventSubscriptionInputVO eventSubscriptionInputVO) + throws Exception { + this.message = message; + this.eventSubscriptionInputVO = eventSubscriptionInputVO; + registerListener400(); + } + + @Override + public void registerListener400() throws Exception { + HttpResponse registerResponse = callAndCatch( + () -> eventsSubscriptionApiTestClient.registerListener(eventSubscriptionInputVO)); + assertEquals(HttpStatus.BAD_REQUEST, registerResponse.getStatus(), message); + Optional optionalErrorDetails = registerResponse.getBody(ErrorDetails.class); + assertTrue(optionalErrorDetails.isPresent(), "Error details should be provided."); + } + + @Disabled("Security is handled externally, thus 401 and 403 cannot happen.") + @Override + public void registerListener401() throws Exception { + + } + + @Disabled("Security is handled externally, thus 401 and 403 cannot happen.") + @Override + public void registerListener403() throws Exception { + + } + + @Disabled("Impossible status.") + @Override + public void registerListener404() throws Exception { + + } + + @Disabled("Prohibited by the framework.") + @Override + public void registerListener405() throws Exception { + + } + + @ParameterizedTest + @MethodSource("provideEventSubscriptionInputsForDuplication") + public void registerListener409(EventSubscriptionInputVO eventSubscriptionInputVO) throws Exception { + this.eventSubscriptionInputVO = eventSubscriptionInputVO; + registerListener409(); + } + + @Override + public void registerListener409() throws Exception { + HttpResponse registerResponse = + callAndCatch(() -> eventsSubscriptionApiTestClient.registerListener(eventSubscriptionInputVO)); + assertEquals(HttpStatus.CREATED, registerResponse.getStatus(), message); + + HttpResponse duplicatedListenerRegisterResponse = + callAndCatch(() -> eventsSubscriptionApiTestClient.registerListener(eventSubscriptionInputVO)); + assertEquals(HttpStatus.CONFLICT, duplicatedListenerRegisterResponse.getStatus(), message); + } + + private static Stream provideEventSubscriptionInputsForDuplication() { + List testEntries = new ArrayList<>(); + + testEntries.add( + Arguments.of( + EventSubscriptionInputVOTestExample.build() + .query(null) + .callback(ANY_CALLBACK) + ) + ); + testEntries.add( + Arguments.of( + EventSubscriptionInputVOTestExample.build() + .query("") + .callback(ANY_CALLBACK) + ) + ); + testEntries.add( + Arguments.of( + EventSubscriptionInputVOTestExample.build() + .query("eventType=ServiceCreateEvent") + .callback(ANY_CALLBACK) + ) + ); + + return testEntries.stream(); + } + + @Override + public void registerListener500() throws Exception { + + } + + @Override + public void unregisterListener204() throws Exception { + EventSubscriptionInputVO eventSubscriptionInputVO = EventSubscriptionInputVOTestExample.build() + .query(null) + .callback(ANY_CALLBACK); + HttpResponse eventSubscriptionVOHttpResponse = + callAndCatch(() -> eventsSubscriptionApiTestClient.registerListener(eventSubscriptionInputVO)); + assertEquals(HttpStatus.CREATED, eventSubscriptionVOHttpResponse.getStatus(), + "A listener with callback should have been created."); + assertTrue(eventSubscriptionVOHttpResponse.getBody().isPresent()); + + String hubId = eventSubscriptionVOHttpResponse.getBody().get().getId(); + HttpResponse httpResponse = + callAndCatch(() -> eventsSubscriptionApiTestClient.unregisterListener(hubId)); + assertEquals(HttpStatus.NO_CONTENT, httpResponse.getStatus(), + "A listener with callback should have been deleted."); + } + + @Override + public void unregisterListener400() throws Exception { + HttpResponse httpResponse = + callAndCatch(() -> eventsSubscriptionApiTestClient.unregisterListener(null)); + assertEquals(HttpStatus.BAD_REQUEST, httpResponse.getStatus(), + "Should have returned 400 when ID is null"); + } + + @Disabled("Security is handled externally, thus 401 and 403 cannot happen.") + @Override + public void unregisterListener401() throws Exception { + + } + + @Disabled("Security is handled externally, thus 401 and 403 cannot happen.") + @Override + public void unregisterListener403() throws Exception { + + } + + @Test + @Override + public void unregisterListener404() throws Exception { + HttpResponse httpResponse = + callAndCatch(() -> eventsSubscriptionApiTestClient.unregisterListener("non-existing-id")); + assertEquals(HttpStatus.NOT_FOUND, httpResponse.getStatus(), + "Should have return 404 when non-existing ID provided."); + } + + @Disabled("Prohibited by the framework.") + @Override + public void unregisterListener405() throws Exception { + + } + + @Override + public void unregisterListener500() throws Exception { + + } + + private static Stream provideValidEventSubscriptionInputs() { + List testEntries = new ArrayList<>(); + + testEntries.add( + Arguments.of("A listener with callback should have been created.", + EventSubscriptionInputVOTestExample.build() + .query(null) + .callback(ANY_CALLBACK), + EventSubscriptionVOTestExample.build() + .query("") + .callback(ANY_CALLBACK) + ) + ); + testEntries.add( + Arguments.of("A listener with callback and simple query should have been created.", + EventSubscriptionInputVOTestExample.build() + .query("eventType=ServiceCreateEvent") + .callback(ANY_CALLBACK), + EventSubscriptionVOTestExample.build() + .query("eventType=ServiceCreateEvent") + .callback(ANY_CALLBACK) + ) + ); + testEntries.add( + Arguments.of("A listener with callback and complex query should have been created.", + EventSubscriptionInputVOTestExample.build() + .query("eventType=ServiceCreateEvent&event.service.status=created") + .callback(ANY_CALLBACK), + EventSubscriptionVOTestExample.build() + .query("eventType=ServiceCreateEvent&event.service.status=created") + .callback(ANY_CALLBACK) + ) + ); + testEntries.add( + Arguments.of("A listener with callback and complex query should have been created.", + EventSubscriptionInputVOTestExample.build() + .query("eventType=ServiceCreateEvent&event.service.status=created" + + "&fields=event.service.id,event.service.name") + .callback(ANY_CALLBACK), + EventSubscriptionVOTestExample.build() + .query("eventType=ServiceCreateEvent&event.service.status=created" + + "&fields=event.service.id,event.service.name") + .callback(ANY_CALLBACK) + ) + ); + + return testEntries.stream(); + } + + private static Stream provideInvalidEventSubscriptionInputs() { + List testEntries = new ArrayList<>(); + + testEntries.add( + Arguments.of("A query with several ANDed event types should not be created.", + EventSubscriptionInputVOTestExample.build() + .query("eventType=ServiceCreateEvent&eventType=ServiceDeleteEvent") + .callback(ANY_CALLBACK) + ) + ); + testEntries.add( + Arguments.of("A query with different ORed section conditions should not be created.", + EventSubscriptionInputVOTestExample.build() + .query("eventType=ServiceCreateEvent;event.service.name=Some") + .callback(ANY_CALLBACK) + ) + ); + testEntries.add( + Arguments.of("A query with both logical operators AND and OR should not be created.", + EventSubscriptionInputVOTestExample.build() + .query("eventType=ServiceCreateEvent;eventType=ServiceDeleteEvent&event.service.name=Some") + .callback(ANY_CALLBACK) + ) + ); + + return testEntries.stream(); + } +} diff --git a/service-inventory/src/test/java/org/fiware/tmforum/serviceinventory/ServiceApiIT.java b/service-inventory/src/test/java/org/fiware/tmforum/serviceinventory/ServiceApiIT.java new file mode 100644 index 00000000..98b48291 --- /dev/null +++ b/service-inventory/src/test/java/org/fiware/tmforum/serviceinventory/ServiceApiIT.java @@ -0,0 +1,844 @@ +package org.fiware.tmforum.serviceinventory; + +import org.fiware.serviceinventory.model.*; +import org.fiware.tmforum.common.notification.TMForumEventHandler; +import org.fiware.tmforum.common.test.AbstractApiIT; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.test.annotation.MockBean; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import org.fiware.ngsi.api.EntitiesApiClient; +import org.fiware.serviceinventory.api.ServiceApiTestClient; +import org.fiware.serviceinventory.api.ServiceApiTestSpec; +import org.fiware.tmforum.common.configuration.GeneralProperties; +import org.fiware.tmforum.common.exception.ErrorDetails; +import org.fiware.tmforum.common.test.ArgumentPair; +import org.fiware.tmforum.serviceinventory.domain.Service; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import reactor.core.publisher.Mono; + + +import java.net.URI; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@MicronautTest(packages = { "org.fiware.tmforum.serviceinventory" }) +public class ServiceApiIT extends AbstractApiIT implements ServiceApiTestSpec{ + public final ServiceApiTestClient serviceApiTestClient; + + private String message; + private String fieldsParameter; + private ServiceCreateVO serviceCreateVO; + private ServiceUpdateVO serviceUpdateVO; + private ServiceVO expectedService; + + public ServiceApiIT(ServiceApiTestClient serviceApiTestClient, EntitiesApiClient entitiesApiClient, + ObjectMapper objectMapper, GeneralProperties generalProperties) { + super(entitiesApiClient, objectMapper, generalProperties); + this.serviceApiTestClient = serviceApiTestClient; + } + + @MockBean(TMForumEventHandler.class) + public TMForumEventHandler eventHandler() { + TMForumEventHandler eventHandler = mock(TMForumEventHandler.class); + + when(eventHandler.handleCreateEvent(any())).thenReturn(Mono.empty()); + when(eventHandler.handleUpdateEvent(any(), any())).thenReturn(Mono.empty()); + + return eventHandler; + } + + @Override protected String getEntityType() { + return Service.TYPE_SERVICE; + } + + @ParameterizedTest + @MethodSource("provideValidServices") + public void createService201(String message, ServiceCreateVO serviceCreateVO, ServiceVO expectedService) + throws Exception { + this.message = message; + this.serviceCreateVO = serviceCreateVO; + this.expectedService = expectedService; + createService201(); + } + + @Override + public void createService201() throws Exception { + + HttpResponse serviceVOHttpResponse = callAndCatch( + () -> serviceApiTestClient.createService(serviceCreateVO)); + assertEquals(HttpStatus.CREATED, serviceVOHttpResponse.getStatus(), message); + String rfId = serviceVOHttpResponse.body().getId(); + expectedService.setId(rfId); + expectedService.setHref(rfId); + + assertEquals(expectedService, serviceVOHttpResponse.body(), message); + } + + private static Stream provideValidServices() { + List testEntries = new ArrayList<>(); + + testEntries.add( + Arguments.of("An empty service should have been created.", + ServiceCreateVOTestExample.build().place(null).serviceSpecification(null), + ServiceVOTestExample.build().place(null).serviceSpecification(null))); + + List notes = List.of(NoteVOTestExample.build().id("urn:note-1"), NoteVOTestExample.build().id("urn:note-2")); + testEntries.add( + Arguments.of("A service with notes should have been created.", + ServiceCreateVOTestExample.build().place(null).serviceSpecification(null).note(notes), + ServiceVOTestExample.build().place(null).serviceSpecification(null).note(notes))); + + provideValidFeatureLists() + .map(ap -> + Arguments.of( + ap.message(), + ServiceCreateVOTestExample.build().place(null).serviceSpecification(null) + .feature(ap.value()), + ServiceVOTestExample.build().place(null).serviceSpecification(null) + .feature(ap.value())) + ).forEach(testEntries::add); + + provideValidCharacteristicLists() + .map(ap -> Arguments.of(ap.message(), + ServiceCreateVOTestExample.build().place(null).serviceSpecification(null) + .serviceCharacteristic(ap.value()), + ServiceVOTestExample.build().place(null).serviceSpecification(null) + .serviceCharacteristic(ap.value()))) + .forEach(testEntries::add); + + return testEntries.stream(); + } + + private static Stream>> provideValidFeatureLists() { + List>> featureArguments = new ArrayList<>(); + + featureArguments.add(new ArgumentPair<>("A single feature without references should be valid.", + List.of(FeatureVOTestExample.build().id("urn:f-1").constraint(null).featureRelationship(null) + .featureCharacteristic(null)))); + featureArguments.add(new ArgumentPair<>("Multiple features without references should be valid.", + List.of( + FeatureVOTestExample.build().id("urn:f-1").constraint(null).featureRelationship(null) + .featureCharacteristic(null), + FeatureVOTestExample.build().id("urn:f-2").constraint(null).featureRelationship(null) + .featureCharacteristic(null)))); + featureArguments.add(new ArgumentPair<>("Features referencing should be valid.", + List.of( + FeatureVOTestExample.build().id("urn:f-1").constraint(null).featureRelationship(null) + .featureCharacteristic(null), + FeatureVOTestExample.build().id("urn:f-2").constraint(null).featureCharacteristic(null) + .featureRelationship( + List.of(FeatureRelationshipVOTestExample.build().validFor(null).id("urn:f-1")))))); + + provideValidCharacteristicLists() + .map(ap -> new ArgumentPair<>(String.format("Features should be valid - %s", ap.message()), + List.of(FeatureVOTestExample.build().id("urn:f-1").constraint(null).featureRelationship(null) + .featureCharacteristic(ap.value())))) + .forEach(featureArguments::add); + + return featureArguments.stream(); + } + + private static Stream>> provideValidCharacteristicLists() { + List>> characteristicArguments = new ArrayList<>(); + + characteristicArguments.add(new ArgumentPair<>("Single characteristics should be valid.", + List.of(CharacteristicVOTestExample.build().id("urn:c-1").characteristicRelationship(null)))); + characteristicArguments.add(new ArgumentPair<>("Mulitple characteristics should be valid.", + List.of(CharacteristicVOTestExample.build().id("urn:c-1").characteristicRelationship(null), + CharacteristicVOTestExample.build().id("urn:c-2").characteristicRelationship(null)))); + characteristicArguments.add(new ArgumentPair<>("Referencing characteristics should be valid.", + List.of(CharacteristicVOTestExample.build().id("urn:c-1").characteristicRelationship(null), + CharacteristicVOTestExample.build().id("urn:c-2") + .characteristicRelationship( + List.of(CharacteristicRelationshipVOTestExample.build().id("urn:c-1")))))); + return characteristicArguments.stream(); + } + + @ParameterizedTest + @MethodSource("provideInvalidServices") + public void createService400(String message, ServiceCreateVO invalidCreateVO) throws Exception { + this.message = message; + this.serviceCreateVO = invalidCreateVO; + createService400(); + } + + @Override + public void createService400() throws Exception { + HttpResponse creationResponse = callAndCatch( + () -> serviceApiTestClient.createService(serviceCreateVO)); + assertEquals(HttpStatus.BAD_REQUEST, creationResponse.getStatus(), message); + Optional optionalErrorDetails = creationResponse.getBody(ErrorDetails.class); + assertTrue(optionalErrorDetails.isPresent(), "Error details should be provided."); + } + + private static Stream provideInvalidServices() { + List testEntries = new ArrayList<>(); + + testEntries.add(Arguments.of("A service with invalid related parties should not be created.", + ServiceCreateVOTestExample.build() + .serviceSpecification(null) + .relatedParty(List.of(RelatedPartyVOTestExample.build())))); + testEntries.add(Arguments.of("A service with non-existent related parties should not be created.", + ServiceCreateVOTestExample.build() + .serviceSpecification(null) + .relatedParty( + List.of((RelatedPartyVOTestExample.build() + .id("urn:ngsi-ld:organisation:non-existent")))))); + + testEntries.add(Arguments.of("A service with invalid service specification should not be created.", + ServiceCreateVOTestExample.build() + .serviceSpecification(ServiceSpecificationRefVOTestExample.build()))); + testEntries.add(Arguments.of("A service with non-existent service specifications should not be created.", + ServiceCreateVOTestExample.build() + .serviceSpecification( + (ServiceSpecificationRefVOTestExample.build() + .id("urn:ngsi-ld:organisation:non-existent"))))); + + return testEntries.stream(); + } + + @Disabled("Security is handled externally, thus 401 and 403 cannot happen.") + @Test + @Override + public void createService401() throws Exception { + + } + + @Disabled("Security is handled externally, thus 401 and 403 cannot happen.") + @Test + @Override + public void createService403() throws Exception { + + } + + @Disabled("Prohibited by the framework.") + @Test + @Override + public void createService405() throws Exception { + } + + @Disabled("No implicit creation, impossible state.") + @Test + @Override + public void createService409() throws Exception { + + } + + @Override + public void createService500() throws Exception { + + } + + @Test + @Override + public void deleteService204() throws Exception { + ServiceCreateVO emptyCreate = ServiceCreateVOTestExample.build() + .serviceSpecification(null); + + HttpResponse createResponse = serviceApiTestClient.createService(emptyCreate); + assertEquals(HttpStatus.CREATED, createResponse.getStatus(), "The service should have been created first."); + + String rfId = createResponse.body().getId(); + + assertEquals(HttpStatus.NO_CONTENT, + callAndCatch(() -> serviceApiTestClient.deleteService(rfId)).getStatus(), + "The service should have been deleted."); + + assertEquals(HttpStatus.NOT_FOUND, + callAndCatch(() -> serviceApiTestClient.retrieveService(rfId, null)).status(), + "The service should not exist anymore."); + } + + @Disabled("400 is impossible to happen on deletion with the current implementation.") + @Test + @Override + public void deleteService400() throws Exception { + + } + + @Disabled("Security is handled externally, thus 401 and 403 cannot happen.") + @Test + @Override + public void deleteService401() throws Exception { + + } + + @Disabled("Security is handled externally, thus 401 and 403 cannot happen.") + @Test + @Override + public void deleteService403() throws Exception { + + } + + @Test + @Override + public void deleteService404() throws Exception { + HttpResponse notFoundResponse = callAndCatch( + () -> serviceApiTestClient.deleteService("urn:ngsi-ld:service-catalog:no-pop")); + assertEquals(HttpStatus.NOT_FOUND, + notFoundResponse.getStatus(), + "No such service-catalog should exist."); + + Optional optionalErrorDetails = notFoundResponse.getBody(ErrorDetails.class); + assertTrue(optionalErrorDetails.isPresent(), "Error details should be provided."); + + notFoundResponse = callAndCatch(() -> serviceApiTestClient.deleteService("invalid-id")); + assertEquals(HttpStatus.NOT_FOUND, + notFoundResponse.getStatus(), + "No such service-catalog should exist."); + + optionalErrorDetails = notFoundResponse.getBody(ErrorDetails.class); + assertTrue(optionalErrorDetails.isPresent(), "Error details should be provided."); + } + + @Disabled("Prohibited by the framework.") + @Test + @Override + public void deleteService405() throws Exception { + + } + + @Disabled("Impossible status.") + @Test + @Override + public void deleteService409() throws Exception { + + } + + @Override + public void deleteService500() throws Exception { + + } + + @Test + @Override + public void listService200() throws Exception { + + List expectedServices = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + ServiceCreateVO serviceCreateVO = ServiceCreateVOTestExample.build() + .serviceSpecification(null); + String id = serviceApiTestClient.createService(serviceCreateVO) + .body().getId(); + ServiceVO serviceVO = ServiceVOTestExample.build(); + serviceVO + .id(id) + .href(id) + .serviceSpecification(null) + .supportingResource(null) + .relatedParty(null); + expectedServices.add(serviceVO); + } + + HttpResponse> serviceResponse = callAndCatch( + () -> serviceApiTestClient.listService(null, null, null)); + + assertEquals(HttpStatus.OK, serviceResponse.getStatus(), "The list should be accessible."); + assertEquals(expectedServices.size(), serviceResponse.getBody().get().size(), + "All services should have been returned."); + List retrievedServices = serviceResponse.getBody().get(); + + Map retrievedMap = retrievedServices.stream() + .collect(Collectors.toMap(service -> service.getId(), + service -> service)); + + expectedServices.stream() + .forEach( + expectedService -> assertTrue( + retrievedMap.containsKey(expectedService.getId()), + String.format("All created services should be returned - Missing: %s.", + expectedService, + retrievedServices))); + expectedServices.stream().forEach( + expectedService -> assertEquals(expectedService, + retrievedMap.get(expectedService.getId()), + "The correct services should be retrieved.")); + + // get with pagination + Integer limit = 5; + HttpResponse> firstPartResponse = callAndCatch( + () -> serviceApiTestClient.listService(null, 0, limit)); + assertEquals(limit, firstPartResponse.body().size(), + "Only the requested number of entries should be returend."); + HttpResponse> secondPartResponse = callAndCatch( + () -> serviceApiTestClient.listService(null, 0 + limit, limit)); + assertEquals(limit, secondPartResponse.body().size(), + "Only the requested number of entries should be returend."); + + retrievedServices.clear(); + retrievedServices.addAll(firstPartResponse.body()); + retrievedServices.addAll(secondPartResponse.body()); + expectedServices.stream() + .forEach( + expectedService -> assertTrue( + retrievedMap.containsKey(expectedService.getId()), + String.format("All created services should be returned - Missing: %s.", + expectedService))); + expectedServices.stream().forEach( + expectedService -> assertEquals(expectedService, + retrievedMap.get(expectedService.getId()), + "The correct services should be retrieved.")); + } + + @Test + @Override + public void listService400() throws Exception { + HttpResponse> badRequestResponse = callAndCatch( + () -> serviceApiTestClient.listService(null, -1, null)); + assertEquals(HttpStatus.BAD_REQUEST, + badRequestResponse.getStatus(), + "Negative offsets are impossible."); + + Optional optionalErrorDetails = badRequestResponse.getBody(ErrorDetails.class); + assertTrue(optionalErrorDetails.isPresent(), "Error details should be provided."); + + badRequestResponse = callAndCatch(() -> serviceApiTestClient.listService(null, null, -1)); + assertEquals(HttpStatus.BAD_REQUEST, + badRequestResponse.getStatus(), + "Negative limits are impossible."); + optionalErrorDetails = badRequestResponse.getBody(ErrorDetails.class); + assertTrue(optionalErrorDetails.isPresent(), "Error details should be provided."); + } + + @Disabled("Security is handled externally, thus 401 and 403 cannot happen.") + @Test + @Override + public void listService401() throws Exception { + + } + + @Disabled("Security is handled externally, thus 401 and 403 cannot happen.") + @Test + @Override + public void listService403() throws Exception { + + } + + @Disabled("Not found is not possible here, will be answered with an empty list instead.") + @Test + @Override + public void listService404() throws Exception { + + } + + @Disabled("Prohibited by the framework.") + @Test + @Override + public void listService405() throws Exception { + + } + + @Disabled("Impossible status.") + @Test + @Override + public void listService409() throws Exception { + + } + + @Override + public void listService500() throws Exception { + + } + + @ParameterizedTest + @MethodSource("provideServiceUpdates") + public void patchService200(String message, ServiceUpdateVO serviceUpdateVO, ServiceVO expectedService) + throws Exception { + this.message = message; + this.serviceUpdateVO = serviceUpdateVO; + this.expectedService = expectedService; + patchService200(); + } + + @Override + public void patchService200() throws Exception { + //first create + ServiceCreateVO serviceCreateVO = ServiceCreateVOTestExample.build() + .serviceSpecification(null); + + HttpResponse createResponse = callAndCatch( + () -> serviceApiTestClient.createService(serviceCreateVO)); + assertEquals(HttpStatus.CREATED, createResponse.getStatus(), + "The service function should have been created first."); + + String serviceId = createResponse.body().getId(); + + HttpResponse updateResponse = callAndCatch( + () -> serviceApiTestClient.patchService(serviceId, serviceUpdateVO)); + assertEquals(HttpStatus.OK, updateResponse.getStatus(), message); + + ServiceVO updatedService = updateResponse.body(); + expectedService.href(serviceId).id(serviceId); + + assertEquals(expectedService, updatedService, message); + } + + private static Stream provideServiceUpdates() { + List testEntries = new ArrayList<>(); + + testEntries.add(Arguments.of("The description should have been updated.", + ServiceUpdateVOTestExample.build() + .serviceSpecification(null) + .description("new-description"), + ServiceVOTestExample.build() + .serviceSpecification(null) + .relatedParty(null) + .supportingResource(null) + .description("new-description"))); + + testEntries.add(Arguments.of("The name should have been updated.", + ServiceUpdateVOTestExample.build() + .serviceSpecification(null) + .name("new-name"), + ServiceVOTestExample.build() + .serviceSpecification(null) + .relatedParty(null) + .supportingResource(null) + .name("new-name"))); + + testEntries.add(Arguments.of("The isBundle should have been updated.", + ServiceUpdateVOTestExample.build() + .serviceSpecification(null) + .isBundle(false), + ServiceVOTestExample.build() + .serviceSpecification(null) + .relatedParty(null) + .supportingResource(null) + .isBundle(false))); + + Instant date = Instant.now(); + testEntries.add(Arguments.of("The startDate should have been updated.", + ServiceUpdateVOTestExample.build() + .serviceSpecification(null) + .startDate(date), + ServiceVOTestExample.build() + .serviceSpecification(null) + .relatedParty(null) + .supportingResource(null) + .startDate(date))); + + testEntries.add(Arguments.of("The endDate should have been updated.", + ServiceUpdateVOTestExample.build() + .serviceSpecification(null) + .endDate(date), + ServiceVOTestExample.build() + .serviceSpecification(null) + .relatedParty(null) + .supportingResource(null) + .endDate(date))); + + testEntries.add(Arguments.of("The characteristic should have been updated.", + ServiceUpdateVOTestExample.build() + .serviceSpecification(null) + .serviceCharacteristic(List.of(CharacteristicVOTestExample.build().name("new") + .id(null))), + ServiceVOTestExample.build() + .serviceSpecification(null) + .relatedParty(null) + .supportingResource(null) + .serviceCharacteristic(List.of(CharacteristicVOTestExample.build().name("new") + .id(null) + .characteristicRelationship(null))))); + + return testEntries.stream(); + } + + @ParameterizedTest + @MethodSource("provideInvalidUpdates") + public void patchService400(String message, ServiceUpdateVO invalidUpdateVO) throws Exception { + this.message = message; + this.serviceUpdateVO = invalidUpdateVO; + patchService400(); + } + + @Override + public void patchService400() throws Exception { + //first create + ServiceCreateVO serviceCreateVO = ServiceCreateVOTestExample.build() + .serviceSpecification(null); + + HttpResponse createResponse = callAndCatch( + () -> serviceApiTestClient.createService(serviceCreateVO)); + assertEquals(HttpStatus.CREATED, createResponse.getStatus(), + "The service function should have been created first."); + + String serviceId = createResponse.body().getId(); + + HttpResponse updateResponse = callAndCatch( + () -> serviceApiTestClient.patchService(serviceId, serviceUpdateVO)); + assertEquals(HttpStatus.BAD_REQUEST, updateResponse.getStatus(), message); + + Optional optionalErrorDetails = updateResponse.getBody(ErrorDetails.class); + assertTrue(optionalErrorDetails.isPresent(), "Some error details should be present."); + } + + private static Stream provideInvalidUpdates() { + List testEntries = new ArrayList<>(); + + testEntries.add(Arguments.of("A service with invalid related parties should not be created.", + ServiceUpdateVOTestExample.build().place(null).serviceSpecification(null) + .relatedParty(List.of(RelatedPartyVOTestExample.build())))); + testEntries.add(Arguments.of("A service with non-existent related parties should not be created.", + ServiceUpdateVOTestExample.build().place(null).serviceSpecification(null).relatedParty( + List.of((RelatedPartyVOTestExample.build().id("urn:ngsi-ld:organisation:non-existent")))))); + + testEntries.add(Arguments.of("A service with duplicate feature ids should not be created.", + ServiceUpdateVOTestExample.build().place(null).serviceSpecification(null) + .feature(List.of(FeatureVOTestExample.build().id("my-feature"), + FeatureVOTestExample.build().id("my-feature"))))); + testEntries.add(Arguments.of("A service with invalid feature references should not be created.", + ServiceUpdateVOTestExample.build().place(null).serviceSpecification(null) + .feature(List.of( + FeatureVOTestExample.build().id("my-feature"), + FeatureVOTestExample.build().featureRelationship( + List.of(FeatureRelationshipVOTestExample.build().id("non-existent"))))))); + + testEntries.add(Arguments.of("A service with duplicate service characteristic ids should not be created.", + ServiceUpdateVOTestExample.build().place(null).serviceSpecification(null) + .serviceCharacteristic(List.of(CharacteristicVOTestExample.build().id("my-characteristic"), + CharacteristicVOTestExample.build().id("my-characteristic"))))); + testEntries.add(Arguments.of("A service with invalid feature references should not be created.", + ServiceUpdateVOTestExample.build().place(null).serviceSpecification(null) + .serviceCharacteristic(List.of( + CharacteristicVOTestExample.build().id("my-feature"), + CharacteristicVOTestExample.build().characteristicRelationship( + List.of(CharacteristicRelationshipVOTestExample.build() + .id("non-existent"))))))); + + testEntries.add(Arguments.of("A service with duplicate feature characteristic ids should not be created.", + ServiceUpdateVOTestExample.build().place(null).serviceSpecification(null) + .feature(List.of(FeatureVOTestExample.build() + .featureCharacteristic( + List.of(CharacteristicVOTestExample.build().id("my-characteristic"), + CharacteristicVOTestExample.build().id("my-characteristic"))))))); + testEntries.add(Arguments.of("A service with invalid feature references should not be created.", + ServiceUpdateVOTestExample.build().place(null).serviceSpecification(null) + .feature(List.of(FeatureVOTestExample.build() + .featureCharacteristic( + List.of(CharacteristicVOTestExample.build().id("my-characteristic"), + CharacteristicVOTestExample.build() + .characteristicRelationship( + List.of(CharacteristicRelationshipVOTestExample.build() + .id("non-existent"))))))))); + + return testEntries.stream(); + } + + @Disabled("Security is handled externally, thus 401 and 403 cannot happen.") + @Test + @Override + public void patchService401() throws Exception { + + } + + @Disabled("Security is handled externally, thus 401 and 403 cannot happen.") + @Test + @Override + public void patchService403() throws Exception { + + } + + @Test + @Override + public void patchService404() throws Exception { + ServiceUpdateVO serviceUpdateVO = ServiceUpdateVOTestExample.build(); + assertEquals( + HttpStatus.NOT_FOUND, + callAndCatch(() -> serviceApiTestClient.patchService("urn:ngsi-ld:service-catalog:not-existent", + serviceUpdateVO)).getStatus(), + "Non existent service should not be updated."); + } + + @Disabled("Prohibited by the framework.") + @Test + @Override + public void patchService405() throws Exception { + + } + + @Override + public void patchService409() throws Exception { + // TODO: can this happen? + } + + @Override + public void patchService500() throws Exception { + + } + + @ParameterizedTest + @MethodSource("provideFieldParameters") + public void retrieveService200(String message, String fields, ServiceVO expectedService) throws Exception { + this.fieldsParameter = fields; + this.message = message; + this.expectedService = expectedService; + retrieveService200(); + } + + @Override + public void retrieveService200() throws Exception { + + ServiceCreateVO serviceCreateVO = ServiceCreateVOTestExample.build() + .serviceSpecification(null); + HttpResponse createResponse = callAndCatch( + () -> serviceApiTestClient.createService(serviceCreateVO)); + assertEquals(HttpStatus.CREATED, createResponse.getStatus(), message); + String id = createResponse.body().getId(); + + expectedService + .id(id) + .href(id); + + //then retrieve + HttpResponse retrievedRF = callAndCatch( + () -> serviceApiTestClient.retrieveService(id, fieldsParameter)); + assertEquals(HttpStatus.OK, retrievedRF.getStatus(), message); + assertEquals(expectedService, retrievedRF.body(), message); + } + + private static Stream provideFieldParameters() { + return Stream.of( + Arguments.of("Without a fields parameter everything should be returned.", null, + ServiceVOTestExample.build() + // get nulled without values + .relatedParty(null) + .supportingResource(null) + .serviceSpecification(null)), + Arguments.of("Only category and the mandatory parameters should have been included.", "category", + ServiceVOTestExample.build() + .relatedParty(null) + .place(null) + .serviceSpecification(null) + .serviceCharacteristic(null) + .feature(null) + .serviceRelationship(null) + .description(null) + .note(null) + .name(null) + .hasStarted(null) + .isServiceEnabled(null) + .isStateful(null) + .isBundle(null) + .serviceDate(null) + .serviceType(null) + .startMode(null) + .relatedEntity(null) + .serviceOrderItem(null) + .supportingResource(null) + .supportingService(null)), + Arguments.of( + "Only the mandatory parameters should have been included when a non-existent field was requested.", + "nothingToSeeHere", ServiceVOTestExample.build() + .relatedParty(null) + .place(null) + .category(null) + .serviceSpecification(null) + .serviceCharacteristic(null) + .feature(null) + .description(null) + .serviceRelationship(null) + .note(null) + .name(null) + .hasStarted(null) + .isServiceEnabled(null) + .isStateful(null) + .isBundle(null) + .serviceDate(null) + .serviceType(null) + .startMode(null) + .relatedEntity(null) + .serviceOrderItem(null) + .supportingResource(null) + .supportingService(null)), + Arguments.of("Only description, name and the mandatory parameters should have been included.", + "name,description", ServiceVOTestExample.build() + .relatedParty(null) + .place(null) + .serviceSpecification(null) + .serviceCharacteristic(null) + .feature(null) + .category(null) + .serviceRelationship(null) + .note(null) + .hasStarted(null) + .isServiceEnabled(null) + .isStateful(null) + .isBundle(null) + .serviceDate(null) + .serviceType(null) + .startMode(null) + .relatedEntity(null) + .serviceOrderItem(null) + .supportingResource(null) + .supportingService(null))); + } + + @Disabled("400 cannot happen, only 404") + @Test + @Override + public void retrieveService400() throws Exception { + + } + + @Disabled("Security is handled externally, thus 401 and 403 cannot happen.") + @Test + @Override + public void retrieveService401() throws Exception { + + } + + @Disabled("Security is handled externally, thus 401 and 403 cannot happen.") + @Test + @Override + public void retrieveService403() throws Exception { + + } + + @Test + @Override + public void retrieveService404() throws Exception { + HttpResponse response = callAndCatch( + () -> serviceApiTestClient.retrieveService("urn:ngsi-ld:service-function:non-existent", null)); + assertEquals(HttpStatus.NOT_FOUND, response.getStatus(), "No such service-catalog should exist."); + + Optional optionalErrorDetails = response.getBody(ErrorDetails.class); + assertTrue(optionalErrorDetails.isPresent(), "Error details should have been provided."); + } + + @Disabled("Prohibited by the framework.") + @Test + @Override + public void retrieveService405() throws Exception { + + } + + @Disabled("Conflict not possible on retrieval") + @Test + @Override + public void retrieveService409() throws Exception { + + } + + @Override + public void retrieveService500() throws Exception { + + } +} diff --git a/service-inventory/src/test/resources/application-in-memory.yaml b/service-inventory/src/test/resources/application-in-memory.yaml new file mode 100644 index 00000000..3a8c3c7e --- /dev/null +++ b/service-inventory/src/test/resources/application-in-memory.yaml @@ -0,0 +1,11 @@ +micronaut: + caches: + entities: + maximumSize: 1000 + # enough to not call twice in one call, but would prevent conflicting writes + expire-after-write: 2s + expire-after-access: 2s + subscriptions: + maximumSize: 1000 + expire-after-write: 10s + expire-after-access: 10s \ No newline at end of file diff --git a/service-inventory/src/test/resources/application-orion-ld.yaml b/service-inventory/src/test/resources/application-orion-ld.yaml new file mode 100644 index 00000000..367ab0b7 --- /dev/null +++ b/service-inventory/src/test/resources/application-orion-ld.yaml @@ -0,0 +1,4 @@ +general: + ngsildOrQueryValue: ";" + ngsildOrQueryKey: ";" + encloseQuery: true \ No newline at end of file diff --git a/service-inventory/src/test/resources/application-redis.yaml b/service-inventory/src/test/resources/application-redis.yaml new file mode 100644 index 00000000..ffad37c0 --- /dev/null +++ b/service-inventory/src/test/resources/application-redis.yaml @@ -0,0 +1,12 @@ +redis: + uri: redis://localhost:6379 + caches: + entities: + # enough to not call twice in one call, but would prevent conflicting writes + expire-after-write: 2s + expire-after-access: 2s + value-serializer: io.github.wistefan.mapping.EntityVOSerializer + subscriptions: + expire-after-write: 10s + expire-after-access: 10s + value-serializer: io.github.wistefan.mapping.EntityVOSerializer \ No newline at end of file diff --git a/service-inventory/src/test/resources/application-scorpio.yaml b/service-inventory/src/test/resources/application-scorpio.yaml new file mode 100644 index 00000000..b8947264 --- /dev/null +++ b/service-inventory/src/test/resources/application-scorpio.yaml @@ -0,0 +1,4 @@ +general: + ngsildOrQueryValue: "," + ngsildOrQueryKey: "," + encloseQuery: false \ No newline at end of file diff --git a/service-inventory/src/test/resources/application.yaml b/service-inventory/src/test/resources/application.yaml new file mode 100644 index 00000000..2381def1 --- /dev/null +++ b/service-inventory/src/test/resources/application.yaml @@ -0,0 +1,39 @@ +micronaut: + + server: + port: 8632 + + metrics: + enabled: true + export: + prometheus: + step: PT2s + descriptions: false + + http: + services: + read-timeout: 30s + ngsi: + path: ngsi-ld/v1 + url: http://localhost:1026 + read-timeout: 30 +--- +jackson: + serialization: + writeDatesAsTimestamps: false +--- +endpoints: + metrics: + enabled: true + health: + enabled: true + +--- +loggers: + levels: + ROOT: TRACE + +--- +general: + contextUrl: https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld + serverHost: http://localhost:8632 \ No newline at end of file diff --git a/service-inventory/src/test/resources/logback.xml b/service-inventory/src/test/resources/logback.xml new file mode 100644 index 00000000..f075e861 --- /dev/null +++ b/service-inventory/src/test/resources/logback.xml @@ -0,0 +1,16 @@ + + + + true + + + %cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n + + + + + + + + \ No newline at end of file diff --git a/service-shared-models/src/main/java/org/fiware/tmforum/service/ServiceRelationship.java b/service-shared-models/src/main/java/org/fiware/tmforum/service/ServiceRelationship.java new file mode 100644 index 00000000..f45d0d8b --- /dev/null +++ b/service-shared-models/src/main/java/org/fiware/tmforum/service/ServiceRelationship.java @@ -0,0 +1,14 @@ +package org.fiware.tmforum.service; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.fiware.tmforum.common.domain.Entity; + +@Data +@EqualsAndHashCode(callSuper = true) +public class ServiceRelationship extends Entity { + + private String relationshipType; + // needs to work as ref or value! + private ServiceRef service; +} \ No newline at end of file