diff --git a/.github/workflows/parallel-test.yml b/.github/workflows/parallel-test.yml index 8e03ad8a..911439cf 100644 --- a/.github/workflows/parallel-test.yml +++ b/.github/workflows/parallel-test.yml @@ -35,7 +35,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, usage-management, service-inventory, agreement, account, quote ] + module: [ customer-bill-management, customer-management, document-management, party-catalog, party-role, product-catalog, product-inventory, product-ordering-management, resource-catalog, resource-function-activation, resource-inventory, service-catalog, usage-management, service-inventory, agreement, account, quote ] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/test-brokers.yaml b/.github/workflows/test-brokers.yaml index b8da3c80..f9feef73 100644 --- a/.github/workflows/test-brokers.yaml +++ b/.github/workflows/test-brokers.yaml @@ -24,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, service-inventory, quote ] + module: [ customer-bill-management, customer-management, document-management, party-catalog, party-role, product-catalog, product-inventory, product-ordering-management, resource-catalog, resource-function-activation, resource-inventory, service-catalog, service-inventory, quote ] # only test scorpio for speed up, orion is currently not really used with the tmforum broker: [ scorpio, orion-ld ] cache: [ in-memory, redis ] diff --git a/all-in-one/pom.xml b/all-in-one/pom.xml index d360b469..e4921540 100644 --- a/all-in-one/pom.xml +++ b/all-in-one/pom.xml @@ -40,6 +40,13 @@ ${project.version} + + + io.minio + minio + 8.5.7 + + org.projectlombok @@ -179,6 +186,7 @@ ${project.parent.basedir}/service-catalog/src/main/java ${project.parent.basedir}/service-inventory/src/main/java ${project.parent.basedir}/usage-management/src/main/java + ${project.parent.basedir}/document-management/src/main/java @@ -404,6 +412,17 @@ org.fiware.usagemanagement.model + + document-management + generate-sources + generate + + false + ${project.parent.basedir}/api/tm-forum/document-management/api.json + org.fiware.document.api + org.fiware.document.model + + customer-bill-management-ext-applied diff --git a/all-in-one/src/main/resources/application.yaml b/all-in-one/src/main/resources/application.yaml index 1ac1ea33..c148b694 100644 --- a/all-in-one/src/main/resources/application.yaml +++ b/all-in-one/src/main/resources/application.yaml @@ -80,4 +80,15 @@ api: software-management: basepath: /tmf-api/softwareCompute/v4 usage-management: - basepath: /tmf-api/usageManagement/v4 \ No newline at end of file + basepath: /tmf-api/usageManagement/v4 + document-management: + basepath: /tmf-api/documentManagement/v4 + +--- +s3: + endpoint: "http://localhost:9000" + accessKey: "minioadmin" + secretKey: "minioadmin" + bucket: "document-attachments" + maxContentSize: 10485760 + # region: "" # Required for AWS S3; omit for MinIO, IONOS, and other providers \ No newline at end of file diff --git a/api/tm-forum/document-management/api.json b/api/tm-forum/document-management/api.json new file mode 100644 index 00000000..f492373f --- /dev/null +++ b/api/tm-forum/document-management/api.json @@ -0,0 +1,3740 @@ +{ + "swagger": "2.0", + "info": { + "title": "Document", + "description": "TMF667 Document API describes the meta-data of a Document, such as the name, creationDate and lifecycle status. The (typically binary) body of this document (such as a Word.doc, PDF, Video clip, or Image) will be held in the associated Attachment(s) either by Ref or Value. If by value - the binary content is held in the Attachment.content. If by reference, the Attachment.url might point to a (file:) or remote (http:) pointer to the Document media.\\n\\n A Document may be associated with a DocumentSpecification, which will detail the characteristics of that type of Document (an Image may have a width, height and format; a Video may have a length and format).\\n A Document has a collection of RelatedParty's, for roles such as author, reviewer, publisher, and a lifecycle status to take the document through a simple set of production stages.", + "version": "4.0.0" + }, + "host": "serverRoot", + "basePath": "/tmf-api/document/v4/", + "schemes": [ + "https" + ], + "consumes": [ + "application/json;charset=utf-8" + ], + "produces": [ + "application/json;charset=utf-8" + ], + "tags": [ + { + "name": "document" + }, + { + "name": "documentSpecification" + }, + { + "name": "notification listeners (client side)" + }, + { + "name": "events subscription" + } + ], + "paths": { + "/document": { + "get": { + "operationId": "listDocument", + "summary": "List or find Document objects", + "description": "This operation list or find Document entities", + "tags": [ + "document" + ], + "parameters": [ + { + "name": "fields", + "description": "Comma-separated properties to be provided in response", + "required": false, + "in": "query", + "type": "string" + }, + { + "name": "offset", + "description": "Requested index for start of resources to be provided in response", + "required": false, + "in": "query", + "type": "integer" + }, + { + "name": "limit", + "description": "Requested number of resources to be provided in response", + "required": false, + "in": "query", + "type": "integer" + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Result-Count": { + "description": "Actual number of items returned in the response body", + "type": "integer" + }, + "X-Total-Count": { + "description": "Total number of items matching criteria", + "type": "integer" + } + }, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Document" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "405": { + "description": "Method Not allowed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "post": { + "operationId": "createDocument", + "summary": "Creates a Document", + "description": "This operation creates a Document entity.", + "tags": [ + "document" + ], + "parameters": [ + { + "name": "document", + "description": "The Document to be created", + "required": true, + "schema": { + "$ref": "#/definitions/Document_Create" + }, + "in": "body" + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/Document" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "405": { + "description": "Method Not allowed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/document/{id}": { + "get": { + "operationId": "retrieveDocument", + "summary": "Retrieves a Document by ID", + "description": "This operation retrieves a Document entity. Attribute selection is enabled for all first level attributes.", + "tags": [ + "document" + ], + "parameters": [ + { + "name": "id", + "description": "Identifier of the Document", + "required": true, + "type": "string", + "in": "path" + }, + { + "name": "fields", + "description": "Comma-separated properties to provide in response", + "required": false, + "type": "string", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/Document" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "405": { + "description": "Method Not allowed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "patch": { + "operationId": "patchDocument", + "summary": "Updates partially a Document", + "description": "This operation updates partially a Document entity.", + "tags": [ + "document" + ], + "parameters": [ + { + "name": "id", + "description": "Identifier of the Document", + "required": true, + "type": "string", + "in": "path" + }, + { + "name": "document", + "description": "The Document to be updated", + "required": true, + "schema": { + "$ref": "#/definitions/Document_Update" + }, + "in": "body" + } + ], + "responses": { + "200": { + "description": "Updated", + "schema": { + "$ref": "#/definitions/Document" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "405": { + "description": "Method Not allowed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "delete": { + "operationId": "deleteDocument", + "summary": "Deletes a Document", + "description": "This operation deletes a Document entity.", + "tags": [ + "document" + ], + "parameters": [ + { + "name": "id", + "description": "Identifier of the Document", + "required": true, + "type": "string", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Deleted" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "405": { + "description": "Method Not allowed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/documentSpecification": { + "get": { + "operationId": "listDocumentSpecification", + "summary": "List or find DocumentSpecification objects", + "description": "This operation list or find DocumentSpecification entities", + "tags": [ + "documentSpecification" + ], + "parameters": [ + { + "name": "fields", + "description": "Comma-separated properties to be provided in response", + "required": false, + "in": "query", + "type": "string" + }, + { + "name": "offset", + "description": "Requested index for start of resources to be provided in response", + "required": false, + "in": "query", + "type": "integer" + }, + { + "name": "limit", + "description": "Requested number of resources to be provided in response", + "required": false, + "in": "query", + "type": "integer" + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Result-Count": { + "description": "Actual number of items returned in the response body", + "type": "integer" + }, + "X-Total-Count": { + "description": "Total number of items matching criteria", + "type": "integer" + } + }, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/DocumentSpecification" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "405": { + "description": "Method Not allowed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "post": { + "operationId": "createDocumentSpecification", + "summary": "Creates a DocumentSpecification", + "description": "This operation creates a DocumentSpecification entity.", + "tags": [ + "documentSpecification" + ], + "parameters": [ + { + "name": "documentSpecification", + "description": "The DocumentSpecification to be created", + "required": true, + "schema": { + "$ref": "#/definitions/DocumentSpecification_Create" + }, + "in": "body" + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/DocumentSpecification" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "405": { + "description": "Method Not allowed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/documentSpecification/{id}": { + "get": { + "operationId": "retrieveDocumentSpecification", + "summary": "Retrieves a DocumentSpecification by ID", + "description": "This operation retrieves a DocumentSpecification entity. Attribute selection is enabled for all first level attributes.", + "tags": [ + "documentSpecification" + ], + "parameters": [ + { + "name": "id", + "description": "Identifier of the DocumentSpecification", + "required": true, + "type": "string", + "in": "path" + }, + { + "name": "fields", + "description": "Comma-separated properties to provide in response", + "required": false, + "type": "string", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/DocumentSpecification" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "405": { + "description": "Method Not allowed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "patch": { + "operationId": "patchDocumentSpecification", + "summary": "Updates partially a DocumentSpecification", + "description": "This operation updates partially a DocumentSpecification entity.", + "tags": [ + "documentSpecification" + ], + "parameters": [ + { + "name": "id", + "description": "Identifier of the DocumentSpecification", + "required": true, + "type": "string", + "in": "path" + }, + { + "name": "documentSpecification", + "description": "The DocumentSpecification to be updated", + "required": true, + "schema": { + "$ref": "#/definitions/DocumentSpecification_Update" + }, + "in": "body" + } + ], + "responses": { + "200": { + "description": "Updated", + "schema": { + "$ref": "#/definitions/DocumentSpecification" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "405": { + "description": "Method Not allowed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "delete": { + "operationId": "deleteDocumentSpecification", + "summary": "Deletes a DocumentSpecification", + "description": "This operation deletes a DocumentSpecification entity.", + "tags": [ + "documentSpecification" + ], + "parameters": [ + { + "name": "id", + "description": "Identifier of the DocumentSpecification", + "required": true, + "type": "string", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Deleted" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "405": { + "description": "Method Not allowed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/hub": { + "post": { + "operationId": "registerListener", + "summary": "Register a listener", + "description": "Sets the communication endpoint address the service instance must use to deliver information about its health state, execution state, failures and metrics.", + "tags": [ + "events subscription" + ], + "parameters": [ + { + "name": "data", + "schema": { + "$ref": "#/definitions/EventSubscriptionInput" + }, + "required": true, + "in": "body", + "description": "Data containing the callback endpoint to deliver the information" + } + ], + "responses": { + "201": { + "description": "Subscribed", + "schema": { + "$ref": "#/definitions/EventSubscription" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "405": { + "description": "Method Not allowed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/hub/{id}": { + "delete": { + "operationId": "unregisterListener", + "summary": "Unregister a listener", + "description": "Resets the communication endpoint address the service instance must use to deliver information about its health state, execution state, failures and metrics.", + "tags": [ + "events subscription" + ], + "parameters": [ + { + "name": "id", + "type": "string", + "required": true, + "in": "path", + "description": "The id of the registered listener" + } + ], + "responses": { + "204": { + "description": "Deleted" + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "405": { + "description": "Method not allowed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/listener/documentCreateEvent": { + "post": { + "operationId": "listenToDocumentCreateEvent", + "summary": "Client listener for entity DocumentCreateEvent", + "description": "Example of a client listener for receiving the notification DocumentCreateEvent", + "tags": [ + "notification listeners (client side)" + ], + "parameters": [ + { + "name": "data", + "required": true, + "in": "body", + "description": "The event data", + "schema": { + "$ref": "#/definitions/DocumentCreateEvent" + } + } + ], + "responses": { + "201": { + "description": "Notified", + "schema": { + "$ref": "#/definitions/EventSubscription" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "405": { + "description": "Method Not allowed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/listener/documentChangeEvent": { + "post": { + "operationId": "listenToDocumentChangeEvent", + "summary": "Client listener for entity DocumentChangeEvent", + "description": "Example of a client listener for receiving the notification DocumentChangeEvent", + "tags": [ + "notification listeners (client side)" + ], + "parameters": [ + { + "name": "data", + "required": true, + "in": "body", + "description": "The event data", + "schema": { + "$ref": "#/definitions/DocumentChangeEvent" + } + } + ], + "responses": { + "201": { + "description": "Notified", + "schema": { + "$ref": "#/definitions/EventSubscription" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "405": { + "description": "Method Not allowed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/listener/documentBatchEvent": { + "post": { + "operationId": "listenToDocumentBatchEvent", + "summary": "Client listener for entity DocumentBatchEvent", + "description": "Example of a client listener for receiving the notification DocumentBatchEvent", + "tags": [ + "notification listeners (client side)" + ], + "parameters": [ + { + "name": "data", + "required": true, + "in": "body", + "description": "The event data", + "schema": { + "$ref": "#/definitions/DocumentBatchEvent" + } + } + ], + "responses": { + "201": { + "description": "Notified", + "schema": { + "$ref": "#/definitions/EventSubscription" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "405": { + "description": "Method Not allowed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/listener/documentDeleteEvent": { + "post": { + "operationId": "listenToDocumentDeleteEvent", + "summary": "Client listener for entity DocumentDeleteEvent", + "description": "Example of a client listener for receiving the notification DocumentDeleteEvent", + "tags": [ + "notification listeners (client side)" + ], + "parameters": [ + { + "name": "data", + "required": true, + "in": "body", + "description": "The event data", + "schema": { + "$ref": "#/definitions/DocumentDeleteEvent" + } + } + ], + "responses": { + "201": { + "description": "Notified", + "schema": { + "$ref": "#/definitions/EventSubscription" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "405": { + "description": "Method Not allowed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/listener/documentSpecificationCreateEvent": { + "post": { + "operationId": "listenToDocumentSpecificationCreateEvent", + "summary": "Client listener for entity DocumentSpecificationCreateEvent", + "description": "Example of a client listener for receiving the notification DocumentSpecificationCreateEvent", + "tags": [ + "notification listeners (client side)" + ], + "parameters": [ + { + "name": "data", + "required": true, + "in": "body", + "description": "The event data", + "schema": { + "$ref": "#/definitions/DocumentSpecificationCreateEvent" + } + } + ], + "responses": { + "201": { + "description": "Notified", + "schema": { + "$ref": "#/definitions/EventSubscription" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "405": { + "description": "Method Not allowed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/listener/documentSpecificationAttributeValueChangeEvent": { + "post": { + "operationId": "listenToDocumentSpecificationAttributeValueChangeEvent", + "summary": "Client listener for entity DocumentSpecificationAttributeValueChangeEvent", + "description": "Example of a client listener for receiving the notification DocumentSpecificationAttributeValueChangeEvent", + "tags": [ + "notification listeners (client side)" + ], + "parameters": [ + { + "name": "data", + "required": true, + "in": "body", + "description": "The event data", + "schema": { + "$ref": "#/definitions/DocumentSpecificationAttributeValueChangeEvent" + } + } + ], + "responses": { + "201": { + "description": "Notified", + "schema": { + "$ref": "#/definitions/EventSubscription" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "405": { + "description": "Method Not allowed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/listener/documentSpecificationDeleteEvent": { + "post": { + "operationId": "listenToDocumentSpecificationDeleteEvent", + "summary": "Client listener for entity DocumentSpecificationDeleteEvent", + "description": "Example of a client listener for receiving the notification DocumentSpecificationDeleteEvent", + "tags": [ + "notification listeners (client side)" + ], + "parameters": [ + { + "name": "data", + "required": true, + "in": "body", + "description": "The event data", + "schema": { + "$ref": "#/definitions/DocumentSpecificationDeleteEvent" + } + } + ], + "responses": { + "201": { + "description": "Notified", + "schema": { + "$ref": "#/definitions/EventSubscription" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "405": { + "description": "Method Not allowed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + } + }, + "definitions": { + "Addressable": { + "type": "object", + "description": "Base schema for adressable entities", + "properties": { + "id": { + "type": "string", + "description": "unique identifier" + }, + "href": { + "type": "string", + "format": "uri", + "description": "Hyperlink reference" + } + } + }, + "Any": {}, + "AssociationSpecificationRef": { + "type": "object", + "description": "reference to an AssociationSpecification object", + "properties": { + "id": { + "type": "string", + "description": "unique identifier" + }, + "href": { + "type": "string", + "format": "uri", + "description": "Hyperlink reference" + }, + "name": { + "type": "string", + "description": "Name of the related entity." + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + }, + "@referredType": { + "type": "string", + "description": "The actual type of the target instance when needed for disambiguation." + } + }, + "required": [ + "id" + ] + }, + "Attachment": { + "type": "object", + "description": "Complements the description of an element (for instance a product) through video, pictures...", + "properties": { + "id": { + "example": "4aafacbd-11ff-4dc8-b445-305f2215715f", + "type": "string", + "description": "Unique identifier for this particular attachment" + }, + "href": { + "example": "http://host/Attachment/4aafacbd-11ff-4dc8-b445-305f2215715f", + "type": "string", + "format": "uri", + "description": "URI for this Attachment" + }, + "attachmentType": { + "example": "video", + "type": "string", + "description": "Attachment type such as video, picture" + }, + "content": { + "type": "string", + "format": "base64", + "description": "The actual contents of the attachment object, if embedded, encoded as base64" + }, + "description": { + "example": "Photograph of the Product", + "type": "string", + "description": "A narrative text describing the content of the attachment" + }, + "mimeType": { + "example": "video/mp4", + "type": "string", + "description": "Attachment mime type such as extension file for video, picture and document" + }, + "name": { + "example": "iPhone 14 video", + "type": "string", + "description": "The name of the attachment" + }, + "url": { + "example": "http://host/Content/4aafacbd-11ff-4dc8-b445-305f2215715f", + "type": "string", + "format": "uri", + "description": "Remote reference to the content if web-addressable" + }, + "size": { + "$ref": "#/definitions/Quantity", + "description": "The size of the attachment" + }, + "validFor": { + "$ref": "#/definitions/TimePeriod", + "description": "The period of time for which the attachment is valid" + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + } + } + }, + "AttachmentRef": { + "type": "object", + "description": "Attachment reference. An attachment complements the description of an element (for instance a product) through video, pictures", + "properties": { + "id": { + "type": "string", + "description": "Unique-Identifier for this attachment" + }, + "href": { + "type": "string", + "format": "uri", + "description": "URL serving as reference for the attachment resource" + }, + "description": { + "type": "string", + "description": "A narrative text describing the content of the attachment" + }, + "name": { + "type": "string", + "description": "Name of the related entity." + }, + "url": { + "type": "string", + "format": "uri", + "description": "Link to the attachment media/content" + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + }, + "@referredType": { + "type": "string", + "description": "The actual type of the target instance when needed for disambiguation." + } + }, + "required": [ + "id" + ] + }, + "AttachmentRefOrValue": { + "type": "object", + "description": "An attachment by value or by reference. An attachment complements the description of an element, for example through a document, a video, a picture.", + "properties": { + "id": { + "example": "4aafacbd-11ff-4dc8-b445-305f2215715f", + "type": "string", + "description": "Unique identifier for this particular attachment" + }, + "href": { + "example": "http://host/Attachment/4aafacbd-11ff-4dc8-b445-305f2215715f", + "type": "string", + "format": "uri", + "description": "URI for this Attachment" + }, + "attachmentType": { + "example": "video", + "type": "string", + "description": "Attachment type such as video, picture" + }, + "content": { + "type": "string", + "format": "base64", + "description": "The actual contents of the attachment object, if embedded, encoded as base64" + }, + "description": { + "example": "Photograph of the Product", + "type": "string", + "description": "A narrative text describing the content of the attachment" + }, + "mimeType": { + "example": "video/mp4", + "type": "string", + "description": "Attachment mime type such as extension file for video, picture and document" + }, + "name": { + "example": "iPhone 14 video", + "type": "string", + "description": "The name of the attachment" + }, + "url": { + "example": "http://host/Content/4aafacbd-11ff-4dc8-b445-305f2215715f", + "type": "string", + "format": "uri", + "description": "Remote reference to the content if web-addressable" + }, + "size": { + "$ref": "#/definitions/Quantity", + "description": "The size of the attachment" + }, + "validFor": { + "$ref": "#/definitions/TimePeriod", + "description": "The period of time for which the attachment is valid" + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + }, + "@referredType": { + "type": "string", + "description": "The actual type of the target instance when needed for disambiguation." + } + } + }, + "CategoryRef": { + "type": "object", + "description": "The category for grouping recommendations", + "properties": { + "id": { + "type": "string", + "description": "unique identifier" + }, + "href": { + "type": "string", + "format": "uri", + "description": "Hyperlink reference" + }, + "name": { + "type": "string", + "description": "Name of the related entity." + }, + "version": { + "type": "string", + "description": "Category version" + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + }, + "@referredType": { + "type": "string", + "description": "The actual type of the target instance when needed for disambiguation." + } + }, + "required": [ + "id" + ] + }, + "Characteristic": { + "type": "object", + "description": "Describes a given characteristic of an object or entity through a name/value pair.", + "required": [ + "name", + "value" + ], + "properties": { + "id": { + "type": "string", + "description": "Unique identifier of the characteristic" + }, + "name": { + "type": "string", + "description": "Name of the characteristic" + }, + "valueType": { + "type": "string", + "description": "Data type of the value of the characteristic" + }, + "characteristicRelationship": { + "type": "array", + "items": { + "$ref": "#/definitions/CharacteristicRelationship" + } + }, + "value": { + "$ref": "#/definitions/Any", + "description": "The value of the characteristic" + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + } + } + }, + "CharacteristicRelationship": { + "type": "object", + "description": "Another Characteristic that is related to the current Characteristic;", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier of the characteristic" + }, + "href": { + "type": "string", + "format": "uri", + "description": "Hyperlink reference" + }, + "relationshipType": { + "type": "string", + "description": "The type of relationship" + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + } + } + }, + "CharacteristicSpecification": { + "type": "object", + "description": "This class defines a characteristic specification.", + "properties": { + "id": { + "type": "string", + "description": "Unique ID for the characteristic" + }, + "configurable": { + "type": "boolean", + "description": "If true, the Boolean indicates that the target Characteristic is configurable" + }, + "description": { + "type": "string", + "description": "A narrative that explains the CharacteristicSpecification." + }, + "extensible": { + "type": "boolean", + "description": "An indicator that specifies that the values for the characteristic can be extended by adding new values when instantiating a characteristic for a resource." + }, + "isUnique": { + "type": "boolean", + "description": "Specifies if the value of this characteristic is unique across all entities instantiated from the specification that uses this characteristc. For example, consider a ProductSpecification for a set-top box, with a CharacteristicSpecification cardID. Each set-top box must have a different value for cardID, so this isUnique attribute would be set to true for the characteristic." + }, + "maxCardinality": { + "type": "integer", + "description": "The maximum number of instances a CharacteristicValue can take on. For example, zero to five phone numbers in a group calling plan, where five is the value for the maxCardinality." + }, + "minCardinality": { + "type": "integer", + "description": "The minimum number of instances a CharacteristicValue can take on. For example, zero to five phone numbers in a group calling plan, where zero is the value for the minCardinality." + }, + "name": { + "type": "string", + "description": "A word, term, or phrase by which this characteristic specification is known and distinguished from other characteristic specifications." + }, + "regex": { + "type": "string", + "description": "A rule or principle represented in regular expression used to derive the value of a characteristic value." + }, + "valueType": { + "type": "string", + "description": "A kind of value that the characteristic can take on, such as numeric, text and so forth" + }, + "charSpecRelationship": { + "type": "array", + "items": { + "$ref": "#/definitions/CharacteristicSpecificationRelationship" + }, + "description": "An aggregation, migration, substitution, dependency or exclusivity relationship between/among Specification Characteristics." + }, + "characteristicValueSpecification": { + "type": "array", + "items": { + "$ref": "#/definitions/CharacteristicValueSpecification" + }, + "description": "A CharacteristicValueSpecification object is used to define a set of attributes, each of which can be assigned to a corresponding set of attributes in a CharacteristicSpecification object. The values of the attributes in the CharacteristicValueSpecification object describe the values of the attributes that a corresponding Characteristic object can take on." + }, + "validFor": { + "$ref": "#/definitions/TimePeriod", + "description": "The period of time for which a characteristic is applicable." + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + }, + "@valueSchemaLocation": { + "type": "string", + "description": "This (optional) field provides a link to the schema describing the value type." + } + } + }, + "CharacteristicSpecificationBase": { + "type": "object", + "description": "This class defines a characteristic specification.", + "properties": { + "id": { + "type": "string", + "description": "Unique ID for the characteristic" + }, + "configurable": { + "type": "boolean", + "description": "If true, the Boolean indicates that the target Characteristic is configurable" + }, + "description": { + "type": "string", + "description": "A narrative that explains the CharacteristicSpecification." + }, + "extensible": { + "type": "boolean", + "description": "An indicator that specifies that the values for the characteristic can be extended by adding new values when instantiating a characteristic for a resource." + }, + "isUnique": { + "type": "boolean", + "description": "Specifies if the value of this characteristic is unique across all entities instantiated from the specification that uses this characteristc. For example, consider a ProductSpecification for a set-top box, with a CharacteristicSpecification cardID. Each set-top box must have a different value for cardID, so this isUnique attribute would be set to true for the characteristic." + }, + "maxCardinality": { + "type": "integer", + "description": "The maximum number of instances a CharacteristicValue can take on. For example, zero to five phone numbers in a group calling plan, where five is the value for the maxCardinality." + }, + "minCardinality": { + "type": "integer", + "description": "The minimum number of instances a CharacteristicValue can take on. For example, zero to five phone numbers in a group calling plan, where zero is the value for the minCardinality." + }, + "name": { + "type": "string", + "description": "A word, term, or phrase by which this characteristic specification is known and distinguished from other characteristic specifications." + }, + "regex": { + "type": "string", + "description": "A rule or principle represented in regular expression used to derive the value of a characteristic value." + }, + "valueType": { + "type": "string", + "description": "A kind of value that the characteristic can take on, such as numeric, text and so forth" + }, + "validFor": { + "$ref": "#/definitions/TimePeriod", + "description": "The period of time for which a characteristic is applicable." + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + }, + "@valueSchemaLocation": { + "type": "string", + "description": "This (optional) field provides a link to the schema describing the value type." + } + } + }, + "CharacteristicSpecificationRelationship": { + "type": "object", + "description": "An aggregation, migration, substitution, dependency or exclusivity relationship between/among Characteristic specifications. The specification characteristic is embedded within the specification whose ID and href are in this entity, and identified by its ID.", + "properties": { + "characteristicSpecificationId": { + "type": "string", + "description": "Unique identifier of the characteristic within the specification" + }, + "name": { + "type": "string", + "description": "Name of the target characteristic within the specification" + }, + "parentSpecificationHref": { + "type": "string", + "format": "uri", + "description": "Hyperlink reference to the parent specification containing the target characteristic" + }, + "parentSpecificationId": { + "type": "string", + "description": "Unique identifier of the parent specification containing the target characteristic" + }, + "relationshipType": { + "type": "string", + "description": "Type of relationship such as aggregation, migration, substitution, dependency, exclusivity" + }, + "validFor": { + "$ref": "#/definitions/TimePeriod", + "description": "The period for which the object is valid" + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + } + } + }, + "CharacteristicValueSpecification": { + "type": "object", + "description": "specification of a value (number or text or an object) that can be assigned to a Characteristic.", + "properties": { + "isDefault": { + "type": "boolean", + "description": "If true, the Boolean Indicates if the value is the default value for a characteristic" + }, + "rangeInterval": { + "type": "string", + "description": "An indicator that specifies the inclusion or exclusion of the valueFrom and valueTo attributes. If applicable, possible values are \"open\", \"closed\", \"closedBottom\" and \"closedTop\"." + }, + "regex": { + "type": "string", + "description": "A regular expression constraint for given value" + }, + "unitOfMeasure": { + "type": "string", + "description": "A length, surface, volume, dry measure, liquid measure, money, weight, time, and the like. In general, a determinate quantity or magnitude of the kind designated, taken as a standard of comparison for others of the same kind, in assigning to them numerical values, as 1 foot, 1 yard, 1 mile, 1 square foot." + }, + "valueFrom": { + "type": "integer", + "description": "The low range value that a characteristic can take on" + }, + "valueTo": { + "type": "integer", + "description": "The upper range value that a characteristic can take on" + }, + "valueType": { + "type": "string", + "description": "A kind of value that the characteristic value can take on, such as numeric, text and so forth" + }, + "validFor": { + "$ref": "#/definitions/TimePeriod", + "description": "The period of time for which a value is applicable." + }, + "value": { + "$ref": "#/definitions/Any", + "description": "A discrete value that the characteristic can take on, or the actual value of the characteristic" + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + } + } + }, + "ConstraintRef": { + "type": "object", + "description": "Constraint reference. The Constraint resource represents a policy/rule applied to an entity or entity spec.", + "properties": { + "id": { + "type": "string", + "description": "unique identifier" + }, + "href": { + "type": "string", + "format": "uri", + "description": "Hyperlink reference" + }, + "name": { + "type": "string", + "description": "Name of the related entity." + }, + "version": { + "type": "string", + "description": "constraint version" + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + }, + "@referredType": { + "type": "string", + "description": "The actual type of the target instance when needed for disambiguation." + } + }, + "required": [ + "id" + ] + }, + "Document": { + "type": "object", + "description": "A Document describes the meta-data of a Document - such as the name, creationDate and lifecycle status.\nThe (typically binary) body of this document (such as a Word.doc, PDF, Video clip, or Image) will be held in the associated Attachment(s) either by Ref or Value. If by value - the binary content is held in the Attachment.content. If by reference, the Attachment.url might point to a (file:) or remote (http:) pointer to the Document media.\n\nA Document may be associated with a DocumentSpecification, which will detail the characteristics of that type of Document (an Image may have a width, height and format; a Video may have a length and format).\nA Document has a collection of RelatedParty's, for roles such as author, reviewer, publisher; and a lifecycle status to take the document through a simple set of production stages.", + "properties": { + "id": { + "type": "string", + "description": "unique identifier" + }, + "href": { + "type": "string", + "format": "uri", + "description": "Hyperlink reference" + }, + "creationDate": { + "type": "string", + "format": "date-time", + "description": "The date and time the document was created. A date and time formatted in compliance with the ISO8601 standard must be used." + }, + "description": { + "type": "string", + "description": "free-text description of the document" + }, + "documentType": { + "type": "string", + "description": "Name of the document type" + }, + "lastUpdate": { + "type": "string", + "format": "date-time", + "description": "The date and time the document was last modified. A date and time formatted in compliance with the ISO8601 standard must be used." + }, + "name": { + "type": "string", + "description": "A string used to give a name to the document" + }, + "version": { + "type": "string", + "description": "A particular form or variety of an artefact that is different from others or from the original. The form represents differences in properties that characterize an artefact, that are not enough to warrant creating a new artefact" + }, + "attachment": { + "type": "array", + "items": { + "$ref": "#/definitions/AttachmentRefOrValue" + } + }, + "category": { + "type": "array", + "items": { + "$ref": "#/definitions/CategoryRef" + } + }, + "characteristic": { + "type": "array", + "items": { + "$ref": "#/definitions/Characteristic" + } + }, + "document": { + "type": "array", + "items": { + "$ref": "#/definitions/DocumentRef" + } + }, + "documentSpecification": { + "$ref": "#/definitions/DocumentSpecificationRefOrValue" + }, + "externalIdentifier": { + "type": "array", + "items": { + "$ref": "#/definitions/ExternalIdentifier" + } + }, + "relatedEntity": { + "type": "array", + "items": { + "$ref": "#/definitions/RelatedEntity" + } + }, + "relatedParty": { + "type": "array", + "items": { + "$ref": "#/definitions/RelatedParty" + } + }, + "status": { + "$ref": "#/definitions/DocumentStatusType", + "description": "The life cycle state of the document" + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + } + } + }, + "Document_Create": { + "type": "object", + "description": "A Document describes the meta-data of a Document - such as the name, creationDate and lifecycle status.\nThe (typically binary) body of this document (such as a Word.doc, PDF, Video clip, or Image) will be held in the associated Attachment(s) either by Ref or Value. If by value - the binary content is held in the Attachment.content. If by reference, the Attachment.url might point to a (file:) or remote (http:) pointer to the Document media.\n\nA Document may be associated with a DocumentSpecification, which will detail the characteristics of that type of Document (an Image may have a width, height and format; a Video may have a length and format).\nA Document has a collection of RelatedParty's, for roles such as author, reviewer, publisher; and a lifecycle status to take the document through a simple set of production stages.\nSkipped properties: id,href", + "required": [ + "name" + ], + "properties": { + "creationDate": { + "type": "string", + "format": "date-time", + "description": "The date and time the document was created. A date and time formatted in compliance with the ISO8601 standard must be used." + }, + "description": { + "type": "string", + "description": "free-text description of the document" + }, + "documentType": { + "type": "string", + "description": "Name of the document type" + }, + "lastUpdate": { + "type": "string", + "format": "date-time", + "description": "The date and time the document was last modified. A date and time formatted in compliance with the ISO8601 standard must be used." + }, + "name": { + "type": "string", + "description": "A string used to give a name to the document" + }, + "version": { + "type": "string", + "description": "A particular form or variety of an artefact that is different from others or from the original. The form represents differences in properties that characterize an artefact, that are not enough to warrant creating a new artefact" + }, + "attachment": { + "type": "array", + "items": { + "$ref": "#/definitions/AttachmentRefOrValue" + } + }, + "category": { + "type": "array", + "items": { + "$ref": "#/definitions/CategoryRef" + } + }, + "characteristic": { + "type": "array", + "items": { + "$ref": "#/definitions/Characteristic" + } + }, + "document": { + "type": "array", + "items": { + "$ref": "#/definitions/DocumentRef" + } + }, + "documentSpecification": { + "$ref": "#/definitions/DocumentSpecificationRefOrValue" + }, + "externalIdentifier": { + "type": "array", + "items": { + "$ref": "#/definitions/ExternalIdentifier" + } + }, + "relatedEntity": { + "type": "array", + "items": { + "$ref": "#/definitions/RelatedEntity" + } + }, + "relatedParty": { + "type": "array", + "items": { + "$ref": "#/definitions/RelatedParty" + } + }, + "status": { + "$ref": "#/definitions/DocumentStatusType", + "description": "The life cycle state of the document" + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + } + } + }, + "Document_Update": { + "type": "object", + "description": "A Document describes the meta-data of a Document - such as the name, creationDate and lifecycle status.\nThe (typically binary) body of this document (such as a Word.doc, PDF, Video clip, or Image) will be held in the associated Attachment(s) either by Ref or Value. If by value - the binary content is held in the Attachment.content. If by reference, the Attachment.url might point to a (file:) or remote (http:) pointer to the Document media.\n\nA Document may be associated with a DocumentSpecification, which will detail the characteristics of that type of Document (an Image may have a width, height and format; a Video may have a length and format).\nA Document has a collection of RelatedParty's, for roles such as author, reviewer, publisher; and a lifecycle status to take the document through a simple set of production stages.\nSkipped properties: id,href,@type,@baseType,@schemaLocation", + "properties": { + "creationDate": { + "type": "string", + "format": "date-time", + "description": "The date and time the document was created. A date and time formatted in compliance with the ISO8601 standard must be used." + }, + "description": { + "type": "string", + "description": "free-text description of the document" + }, + "documentType": { + "type": "string", + "description": "Name of the document type" + }, + "lastUpdate": { + "type": "string", + "format": "date-time", + "description": "The date and time the document was last modified. A date and time formatted in compliance with the ISO8601 standard must be used." + }, + "name": { + "type": "string", + "description": "A string used to give a name to the document" + }, + "version": { + "type": "string", + "description": "A particular form or variety of an artefact that is different from others or from the original. The form represents differences in properties that characterize an artefact, that are not enough to warrant creating a new artefact" + }, + "attachment": { + "type": "array", + "items": { + "$ref": "#/definitions/AttachmentRefOrValue" + } + }, + "category": { + "type": "array", + "items": { + "$ref": "#/definitions/CategoryRef" + } + }, + "characteristic": { + "type": "array", + "items": { + "$ref": "#/definitions/Characteristic" + } + }, + "document": { + "type": "array", + "items": { + "$ref": "#/definitions/DocumentRef" + } + }, + "documentSpecification": { + "$ref": "#/definitions/DocumentSpecificationRefOrValue" + }, + "externalIdentifier": { + "type": "array", + "items": { + "$ref": "#/definitions/ExternalIdentifier" + } + }, + "relatedEntity": { + "type": "array", + "items": { + "$ref": "#/definitions/RelatedEntity" + } + }, + "relatedParty": { + "type": "array", + "items": { + "$ref": "#/definitions/RelatedParty" + } + }, + "status": { + "$ref": "#/definitions/DocumentStatusType", + "description": "The life cycle state of the document" + } + } + }, + "DocumentRef": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Identifier of the document" + }, + "href": { + "type": "string", + "description": "The URI for the object itself." + }, + "name": { + "type": "string", + "description": "Name of the related entity." + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + }, + "@referredType": { + "type": "string", + "description": "The actual type of the target instance when needed for disambiguation." + } + }, + "required": [ + "id" + ] + }, + "DocumentSpecification": { + "type": "object", + "description": "A DocumentSpecification provides the characteristics and constraints that describe a particular type of Document. It follows the EntitySpecification pattern, so allows you to define the characteristics expected, as well as the nature of the values of those characteristics (eg: integer range [valueFrom/valueTo], regular expression or unitOfMeasure).", + "properties": { + "id": { + "type": "string", + "description": "unique identifier" + }, + "href": { + "type": "string", + "format": "uri", + "description": "Hyperlink reference" + }, + "description": { + "type": "string", + "description": "Description of the specification" + }, + "isBundle": { + "type": "boolean", + "description": "isBundle determines whether specification represents a single specification (false), or a bundle of specifications (true)." + }, + "lastUpdate": { + "type": "string", + "format": "date-time", + "description": "Date and time of the last update of the specification" + }, + "name": { + "example": "Image Specification", + "type": "string", + "description": "The name of the DocumentSpecification" + }, + "version": { + "example": "1.0.0", + "type": "string", + "description": "The version of the DocumentSpecification" + }, + "attachment": { + "type": "array", + "items": { + "$ref": "#/definitions/AttachmentRefOrValue" + }, + "description": "Attachments that may be of relevance to this specification, such as picture, document, media" + }, + "constraint": { + "type": "array", + "items": { + "$ref": "#/definitions/ConstraintRef" + }, + "description": "This is a list of constraint references applied to this specification" + }, + "entitySpecRelationship": { + "type": "array", + "items": { + "$ref": "#/definitions/EntitySpecificationRelationship" + }, + "description": "Relationship to another specification" + }, + "lifecycleStatus": { + "example": "approved", + "$ref": "#/definitions/DocumentSpecificationStatusType", + "description": "The current lifecycle status of this DocumentSpecification" + }, + "relatedParty": { + "type": "array", + "items": { + "$ref": "#/definitions/RelatedParty" + }, + "description": "Parties who manage or otherwise have an interest in this specification" + }, + "specCharacteristic": { + "type": "array", + "items": { + "$ref": "#/definitions/CharacteristicSpecification" + }, + "description": "List of characteristics that the entity can take" + }, + "targetEntitySchema": { + "$ref": "#/definitions/TargetEntitySchema", + "description": "Pointer to a schema that defines the target entity" + }, + "validFor": { + "$ref": "#/definitions/TimePeriod", + "description": "The period for which this REST resource is valid" + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + } + } + }, + "DocumentSpecification_Create": { + "type": "object", + "description": "A DocumentSpecification provides the characteristics and constraints that describe a particular type of Document. It follows the EntitySpecification pattern, so allows you to define the characteristics expected, as well as the nature of the values of those characteristics (eg: integer range [valueFrom/valueTo], regular expression or unitOfMeasure).\nSkipped properties: id,href", + "required": [ + "name" + ], + "properties": { + "description": { + "type": "string", + "description": "Description of the specification" + }, + "isBundle": { + "type": "boolean", + "description": "isBundle determines whether specification represents a single specification (false), or a bundle of specifications (true)." + }, + "lastUpdate": { + "type": "string", + "format": "date-time", + "description": "Date and time of the last update of the specification" + }, + "name": { + "example": "Image Specification", + "type": "string", + "description": "The name of the DocumentSpecification" + }, + "version": { + "example": "1.0.0", + "type": "string", + "description": "The version of the DocumentSpecification" + }, + "attachment": { + "type": "array", + "items": { + "$ref": "#/definitions/AttachmentRefOrValue" + }, + "description": "Attachments that may be of relevance to this specification, such as picture, document, media" + }, + "constraint": { + "type": "array", + "items": { + "$ref": "#/definitions/ConstraintRef" + }, + "description": "This is a list of constraint references applied to this specification" + }, + "entitySpecRelationship": { + "type": "array", + "items": { + "$ref": "#/definitions/EntitySpecificationRelationship" + }, + "description": "Relationship to another specification" + }, + "lifecycleStatus": { + "example": "approved", + "$ref": "#/definitions/DocumentSpecificationStatusType", + "description": "The current lifecycle status of this DocumentSpecification" + }, + "relatedParty": { + "type": "array", + "items": { + "$ref": "#/definitions/RelatedParty" + }, + "description": "Parties who manage or otherwise have an interest in this specification" + }, + "specCharacteristic": { + "type": "array", + "items": { + "$ref": "#/definitions/CharacteristicSpecification" + }, + "description": "List of characteristics that the entity can take" + }, + "targetEntitySchema": { + "$ref": "#/definitions/TargetEntitySchema", + "description": "Pointer to a schema that defines the target entity" + }, + "validFor": { + "$ref": "#/definitions/TimePeriod", + "description": "The period for which this REST resource is valid" + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + } + } + }, + "DocumentSpecification_Update": { + "type": "object", + "description": "A DocumentSpecification provides the characteristics and constraints that describe a particular type of Document. It follows the EntitySpecification pattern, so allows you to define the characteristics expected, as well as the nature of the values of those characteristics (eg: integer range [valueFrom/valueTo], regular expression or unitOfMeasure).\nSkipped properties: id,href,@type,@baseType", + "properties": { + "description": { + "type": "string", + "description": "Description of the specification" + }, + "isBundle": { + "type": "boolean", + "description": "isBundle determines whether specification represents a single specification (false), or a bundle of specifications (true)." + }, + "lastUpdate": { + "type": "string", + "format": "date-time", + "description": "Date and time of the last update of the specification" + }, + "name": { + "example": "Image Specification", + "type": "string", + "description": "The name of the DocumentSpecification" + }, + "version": { + "example": "1.0.0", + "type": "string", + "description": "The version of the DocumentSpecification" + }, + "attachment": { + "type": "array", + "items": { + "$ref": "#/definitions/AttachmentRefOrValue" + }, + "description": "Attachments that may be of relevance to this specification, such as picture, document, media" + }, + "constraint": { + "type": "array", + "items": { + "$ref": "#/definitions/ConstraintRef" + }, + "description": "This is a list of constraint references applied to this specification" + }, + "entitySpecRelationship": { + "type": "array", + "items": { + "$ref": "#/definitions/EntitySpecificationRelationship" + }, + "description": "Relationship to another specification" + }, + "lifecycleStatus": { + "example": "approved", + "$ref": "#/definitions/DocumentSpecificationStatusType", + "description": "The current lifecycle status of this DocumentSpecification" + }, + "relatedParty": { + "type": "array", + "items": { + "$ref": "#/definitions/RelatedParty" + }, + "description": "Parties who manage or otherwise have an interest in this specification" + }, + "specCharacteristic": { + "type": "array", + "items": { + "$ref": "#/definitions/CharacteristicSpecification" + }, + "description": "List of characteristics that the entity can take" + }, + "targetEntitySchema": { + "$ref": "#/definitions/TargetEntitySchema", + "description": "Pointer to a schema that defines the target entity" + }, + "validFor": { + "$ref": "#/definitions/TimePeriod", + "description": "The period for which this REST resource is valid" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + } + } + }, + "DocumentSpecificationRef": { + "type": "object", + "description": "A DocumentSpecificationRef provides a reference to a DocumentSpefication", + "properties": { + "id": { + "type": "string", + "description": "unique identifier" + }, + "href": { + "type": "string", + "format": "uri", + "description": "Hyperlink reference" + }, + "name": { + "type": "string", + "description": "Name of the related entity." + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + }, + "@referredType": { + "type": "string", + "description": "The actual type of the target instance when needed for disambiguation." + } + }, + "required": [ + "id" + ] + }, + "DocumentSpecificationRefOrValue": { + "type": "object", + "description": "A DocumentSpecificationRefOrValue where you can select between a DocumentSpecification (by Value) or a DocumentSpecificationRef (by Reference)", + "properties": { + "id": { + "type": "string", + "description": "unique identifier" + }, + "href": { + "type": "string", + "format": "uri", + "description": "Hyperlink reference" + }, + "description": { + "type": "string", + "description": "Description of the specification" + }, + "isBundle": { + "type": "boolean", + "description": "isBundle determines whether specification represents a single specification (false), or a bundle of specifications (true)." + }, + "lastUpdate": { + "type": "string", + "format": "date-time", + "description": "Date and time of the last update of the specification" + }, + "name": { + "type": "string", + "description": "Name given to the specification" + }, + "version": { + "type": "string", + "description": "specification version" + }, + "attachment": { + "type": "array", + "items": { + "$ref": "#/definitions/AttachmentRefOrValue" + }, + "description": "Attachments that may be of relevance to this specification, such as picture, document, media" + }, + "constraint": { + "type": "array", + "items": { + "$ref": "#/definitions/ConstraintRef" + }, + "description": "This is a list of constraint references applied to this specification" + }, + "entitySpecRelationship": { + "type": "array", + "items": { + "$ref": "#/definitions/EntitySpecificationRelationship" + }, + "description": "Relationship to another specification" + }, + "lifecycleStatus": { + "example": "approved", + "$ref": "#/definitions/DocumentSpecificationStatusType", + "description": "The current lifecycle status of this DocumentSpecification" + }, + "relatedParty": { + "type": "array", + "items": { + "$ref": "#/definitions/RelatedParty" + }, + "description": "Parties who manage or otherwise have an interest in this specification" + }, + "specCharacteristic": { + "type": "array", + "items": { + "$ref": "#/definitions/CharacteristicSpecification" + }, + "description": "List of characteristics that the entity can take" + }, + "targetEntitySchema": { + "$ref": "#/definitions/TargetEntitySchema", + "description": "Pointer to a schema that defines the target entity" + }, + "validFor": { + "$ref": "#/definitions/TimePeriod", + "description": "The period for which this REST resource is valid" + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + }, + "@referredType": { + "type": "string", + "description": "The actual type of the target instance when needed for disambiguation." + } + } + }, + "DocumentSpecificationStatusType": { + "type": "string", + "description": "Possible values for the status of a DocumentSpecification", + "enum": [ + "created", + "reviewed", + "approved", + "published", + "archived", + "deleted" + ] + }, + "DocumentStatusType": { + "type": "string", + "description": "Possible values for the status of a Document", + "enum": [ + "created", + "reviewed", + "approved", + "published", + "archived", + "deleted" + ] + }, + "Entity": { + "type": "object", + "description": "Base entity schema for use in TMForum Open-APIs", + "properties": { + "id": { + "type": "string", + "description": "unique identifier" + }, + "href": { + "type": "string", + "format": "uri", + "description": "Hyperlink reference" + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + } + } + }, + "EntityRef": { + "type": "object", + "description": "Entity reference schema to be use for all entityRef class.", + "properties": { + "id": { + "type": "string", + "description": "unique identifier" + }, + "href": { + "type": "string", + "format": "uri", + "description": "Hyperlink reference" + }, + "name": { + "type": "string", + "description": "Name of the related entity." + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + }, + "@referredType": { + "type": "string", + "description": "The actual type of the target instance when needed for disambiguation." + } + }, + "required": [ + "id" + ] + }, + "EntitySpecification": { + "type": "object", + "description": "EntitySpecification is a class that offers characteristics to describe a type of entity. Entities are generic constructs that may be used to describe bespoke business entities that are not effectively covered by the existing SID model.\nFunctionally, the entity specification acts as a template by which entities may be instantiated and described. By sharing the same specification, these entities would therefore share the same set of characteristics.\nNote: The \u2018configurable\u2019 attribute on the specCharacteristics determines if an entity instantiated from the entity specification can override the value of the attribute. When set to false, the entity instance may not define a value that differs from the value in the specification.", + "properties": { + "id": { + "type": "string", + "description": "unique identifier" + }, + "href": { + "type": "string", + "format": "uri", + "description": "Hyperlink reference" + }, + "description": { + "type": "string", + "description": "Description of the specification" + }, + "isBundle": { + "type": "boolean", + "description": "isBundle determines whether specification represents a single specification (false), or a bundle of specifications (true)." + }, + "lastUpdate": { + "type": "string", + "format": "date-time", + "description": "Date and time of the last update of the specification" + }, + "lifecycleStatus": { + "type": "string", + "description": "Used to indicate the current lifecycle status of this catalog item" + }, + "name": { + "type": "string", + "description": "Name given to the specification" + }, + "version": { + "type": "string", + "description": "specification version" + }, + "attachment": { + "type": "array", + "items": { + "$ref": "#/definitions/AttachmentRefOrValue" + }, + "description": "Attachments that may be of relevance to this specification, such as picture, document, media" + }, + "constraint": { + "type": "array", + "items": { + "$ref": "#/definitions/ConstraintRef" + }, + "description": "This is a list of constraint references applied to this specification" + }, + "entitySpecRelationship": { + "type": "array", + "items": { + "$ref": "#/definitions/EntitySpecificationRelationship" + }, + "description": "Relationship to another specification" + }, + "relatedParty": { + "type": "array", + "items": { + "$ref": "#/definitions/RelatedParty" + }, + "description": "Parties who manage or otherwise have an interest in this specification" + }, + "specCharacteristic": { + "type": "array", + "items": { + "$ref": "#/definitions/CharacteristicSpecification" + }, + "description": "List of characteristics that the entity can take" + }, + "targetEntitySchema": { + "$ref": "#/definitions/TargetEntitySchema", + "description": "Pointer to a schema that defines the target entity" + }, + "validFor": { + "$ref": "#/definitions/TimePeriod", + "description": "The period for which this REST resource is valid" + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + } + } + }, + "EntitySpecificationRelationship": { + "type": "object", + "description": "A migration, substitution, dependency or exclusivity relationship between/among entity specifications.", + "required": [ + "id", + "relationshipType" + ], + "properties": { + "id": { + "type": "string", + "description": "unique identifier" + }, + "href": { + "type": "string", + "format": "uri", + "description": "Hyperlink reference" + }, + "name": { + "type": "string", + "description": "Name of the related entity." + }, + "relationshipType": { + "type": "string", + "description": "Type of relationship such as migration, substitution, dependency, exclusivity" + }, + "role": { + "type": "string", + "description": "The association role for this entity specification" + }, + "associationSpec": { + "$ref": "#/definitions/AssociationSpecificationRef", + "description": "A specification for an association used by this relationship" + }, + "validFor": { + "$ref": "#/definitions/TimePeriod", + "description": "The period for which the entitySpecRelationship is valid" + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + }, + "@referredType": { + "type": "string", + "description": "The actual type of the target instance when needed for disambiguation." + } + } + }, + "Extensible": { + "type": "object", + "description": "Base Extensible schema for use in TMForum Open-APIs", + "properties": { + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + } + } + }, + "ExternalIdentifier": { + "type": "object", + "description": "An identification of an entity that is owned by or originates in a software system different from the current system, for example a ProductOrder handed off from a commerce platform into an order handling system. The structure identifies the system itself, the nature of the entity within the system (e.g. class name) and the unique ID of the entity within the system. It is anticipated that multiple external IDs can be held for a single entity, e.g. if the entity passed through multiple systems on the way to the current system. In this case the consumer is expected to sequence the IDs in the array in reverse order of provenance, i.e. most recent system first in the list.", + "required": [ + "id" + ], + "properties": { + "id": { + "example": "MC2255771199555", + "type": "string", + "description": "identification of the entity within the external system." + }, + "href": { + "type": "string", + "format": "uri", + "description": "Hyperlink reference" + }, + "externalIdentifierType": { + "example": "ProductOrder", + "type": "string", + "description": "Type of the identification, typically would be the type of the entity within the external system" + }, + "owner": { + "example": "MagentoCommerce", + "type": "string", + "description": "Name of the external system that owns the entity." + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + } + } + }, + "Quantity": { + "type": "object", + "description": "An amount in a given unit", + "properties": { + "amount": { + "default": 1, + "type": "number", + "format": "float", + "description": "Numeric value in a given unit" + }, + "units": { + "type": "string", + "description": "Unit" + } + } + }, + "RelatedEntity": { + "type": "object", + "description": "A reference to an entity, where the type of the entity is not known in advance.", + "required": [ + "@referredType", + "id", + "role" + ], + "properties": { + "id": { + "type": "string", + "description": "unique identifier" + }, + "href": { + "type": "string", + "format": "uri", + "description": "Hyperlink reference" + }, + "name": { + "type": "string", + "description": "Name of the related entity." + }, + "role": { + "type": "string", + "description": "The role of an entity." + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + }, + "@referredType": { + "type": "string", + "description": "The actual type of the target instance when needed for disambiguation." + } + } + }, + "RelatedParty": { + "type": "object", + "description": "Related Entity reference. A related party defines party or party role linked to a specific entity.", + "required": [ + "@referredType", + "id" + ], + "properties": { + "id": { + "type": "string", + "description": "unique identifier" + }, + "href": { + "type": "string", + "format": "uri", + "description": "Hyperlink reference" + }, + "name": { + "type": "string", + "description": "Name of the related entity." + }, + "role": { + "type": "string", + "description": "Role played by the related party" + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class" + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class Extensible name" + }, + "@referredType": { + "type": "string", + "description": "The actual type of the target instance when needed for disambiguation." + } + } + }, + "TargetEntitySchema": { + "type": "object", + "description": "The reference object to the schema and type of target entity which is described by a specification", + "required": [ + "@schemaLocation", + "@type" + ], + "properties": { + "@schemaLocation": { + "type": "string", + "description": "This field provides a link to the schema describing the target entity" + }, + "@type": { + "type": "string", + "description": "Class type of the target entity" + } + } + }, + "TimePeriod": { + "type": "object", + "description": "A period of time, either as a deadline (endDateTime only) a startDateTime only, or both", + "properties": { + "endDateTime": { + "example": "1985-04-12T23:20:50.52Z", + "type": "string", + "format": "date-time", + "description": "End of the time period, using IETC-RFC-3339 format" + }, + "startDateTime": { + "example": "1985-04-12T23:20:50.52Z", + "type": "string", + "format": "date-time", + "description": "Start of the time period, using IETC-RFC-3339 format" + } + } + }, + "EventSubscription": { + "type": "object", + "description": "Sets the communication endpoint address the service instance must use to deliver notification information", + "required": [ + "id", + "callback" + ], + "properties": { + "id": { + "type": "string", + "description": "Id of the listener" + }, + "callback": { + "type": "string", + "description": "The callback being registered." + }, + "query": { + "type": "string", + "description": "additional data to be passed" + } + } + }, + "EventSubscriptionInput": { + "type": "object", + "description": "Sets the communication endpoint address the service instance must use to deliver notification information", + "required": [ + "callback" + ], + "properties": { + "callback": { + "type": "string", + "description": "The callback being registered." + }, + "query": { + "type": "string", + "description": "additional data to be passed" + } + } + }, + "DocumentCreateEvent": { + "type": "object", + "description": "The notification data structure", + "properties": { + "event": { + "description": "The event payload linked to the involved resource object", + "$ref": "#/definitions/DocumentCreateEventPayload" + }, + "eventId": { + "type": "string", + "description": "The identifier of the notification." + }, + "eventTime": { + "type": "string", + "format": "date-time", + "description": "Time of the event occurrence." + }, + "eventType": { + "type": "string", + "description": "The type of the notification." + }, + "correlationId": { + "type": "string", + "description": "The correlation id for this event." + }, + "domain": { + "type": "string", + "description": "The domain of the event." + }, + "title": { + "type": "string", + "description": "The title of the event." + }, + "description": { + "type": "string", + "description": "An explnatory of the event." + }, + "priority": { + "type": "string", + "description": "A priority." + }, + "timeOcurred": { + "type": "string", + "format": "date-time", + "description": "The time the event occured." + } + } + }, + "DocumentCreateEventPayload": { + "type": "object", + "description": "The event data structure", + "properties": { + "document": { + "description": "The involved resource data for the event", + "$ref": "#/definitions/Document" + } + } + }, + "DocumentChangeEvent": { + "type": "object", + "description": "The notification data structure", + "properties": { + "event": { + "description": "The event payload linked to the involved resource object", + "$ref": "#/definitions/DocumentChangeEventPayload" + }, + "eventId": { + "type": "string", + "description": "The identifier of the notification." + }, + "eventTime": { + "type": "string", + "format": "date-time", + "description": "Time of the event occurrence." + }, + "eventType": { + "type": "string", + "description": "The type of the notification." + }, + "correlationId": { + "type": "string", + "description": "The correlation id for this event." + }, + "domain": { + "type": "string", + "description": "The domain of the event." + }, + "title": { + "type": "string", + "description": "The title of the event." + }, + "description": { + "type": "string", + "description": "An explnatory of the event." + }, + "priority": { + "type": "string", + "description": "A priority." + }, + "timeOcurred": { + "type": "string", + "format": "date-time", + "description": "The time the event occured." + } + } + }, + "DocumentChangeEventPayload": { + "type": "object", + "description": "The event data structure", + "properties": { + "document": { + "description": "The involved resource data for the event", + "$ref": "#/definitions/Document" + } + } + }, + "DocumentBatchEvent": { + "type": "object", + "description": "The notification data structure", + "properties": { + "event": { + "description": "The event payload linked to the involved resource object", + "$ref": "#/definitions/DocumentBatchEventPayload" + }, + "eventId": { + "type": "string", + "description": "The identifier of the notification." + }, + "eventTime": { + "type": "string", + "format": "date-time", + "description": "Time of the event occurrence." + }, + "eventType": { + "type": "string", + "description": "The type of the notification." + }, + "correlationId": { + "type": "string", + "description": "The correlation id for this event." + }, + "domain": { + "type": "string", + "description": "The domain of the event." + }, + "title": { + "type": "string", + "description": "The title of the event." + }, + "description": { + "type": "string", + "description": "An explnatory of the event." + }, + "priority": { + "type": "string", + "description": "A priority." + }, + "timeOcurred": { + "type": "string", + "format": "date-time", + "description": "The time the event occured." + } + } + }, + "DocumentBatchEventPayload": { + "type": "object", + "description": "The event data structure", + "properties": { + "document": { + "description": "The involved resource data for the event", + "$ref": "#/definitions/Document" + } + } + }, + "DocumentDeleteEvent": { + "type": "object", + "description": "The notification data structure", + "properties": { + "event": { + "description": "The event payload linked to the involved resource object", + "$ref": "#/definitions/DocumentDeleteEventPayload" + }, + "eventId": { + "type": "string", + "description": "The identifier of the notification." + }, + "eventTime": { + "type": "string", + "format": "date-time", + "description": "Time of the event occurrence." + }, + "eventType": { + "type": "string", + "description": "The type of the notification." + }, + "correlationId": { + "type": "string", + "description": "The correlation id for this event." + }, + "domain": { + "type": "string", + "description": "The domain of the event." + }, + "title": { + "type": "string", + "description": "The title of the event." + }, + "description": { + "type": "string", + "description": "An explnatory of the event." + }, + "priority": { + "type": "string", + "description": "A priority." + }, + "timeOcurred": { + "type": "string", + "format": "date-time", + "description": "The time the event occured." + } + } + }, + "DocumentDeleteEventPayload": { + "type": "object", + "description": "The event data structure", + "properties": { + "document": { + "description": "The involved resource data for the event", + "$ref": "#/definitions/Document" + } + } + }, + "DocumentSpecificationCreateEvent": { + "type": "object", + "description": "The notification data structure", + "properties": { + "event": { + "description": "The event payload linked to the involved resource object", + "$ref": "#/definitions/DocumentSpecificationCreateEventPayload" + }, + "eventId": { + "type": "string", + "description": "The identifier of the notification." + }, + "eventTime": { + "type": "string", + "format": "date-time", + "description": "Time of the event occurrence." + }, + "eventType": { + "type": "string", + "description": "The type of the notification." + }, + "correlationId": { + "type": "string", + "description": "The correlation id for this event." + }, + "domain": { + "type": "string", + "description": "The domain of the event." + }, + "title": { + "type": "string", + "description": "The title of the event." + }, + "description": { + "type": "string", + "description": "An explnatory of the event." + }, + "priority": { + "type": "string", + "description": "A priority." + }, + "timeOcurred": { + "type": "string", + "format": "date-time", + "description": "The time the event occured." + } + } + }, + "DocumentSpecificationCreateEventPayload": { + "type": "object", + "description": "The event data structure", + "properties": { + "documentSpecification": { + "description": "The involved resource data for the event", + "$ref": "#/definitions/DocumentSpecification" + } + } + }, + "DocumentSpecificationAttributeValueChangeEvent": { + "type": "object", + "description": "The notification data structure", + "properties": { + "eventId": { + "type": "string", + "description": "The identifier of the notification." + }, + "eventTime": { + "type": "string", + "format": "date-time", + "description": "Time of the event occurrence." + }, + "eventType": { + "type": "string", + "description": "The type of the notification." + }, + "correlationId": { + "type": "string", + "description": "The correlation id for this event." + }, + "domain": { + "type": "string", + "description": "The domain of the event." + }, + "title": { + "type": "string", + "description": "The title of the event." + }, + "description": { + "type": "string", + "description": "An explnatory of the event." + }, + "priority": { + "type": "string", + "description": "A priority." + }, + "timeOcurred": { + "type": "string", + "format": "date-time", + "description": "The time the event occured." + }, + "fieldPath": { + "type": "string", + "description": "The path identifying the object field concerned by this notification." + }, + "event": { + "description": "The event payload linked to the involved resource object", + "$ref": "#/definitions/DocumentSpecificationAttributeValueChangeEventPayload" + } + } + }, + "DocumentSpecificationAttributeValueChangeEventPayload": { + "type": "object", + "description": "The event data structure", + "properties": { + "documentSpecification": { + "description": "The involved resource data for the event", + "$ref": "#/definitions/DocumentSpecification" + } + } + }, + "DocumentSpecificationDeleteEvent": { + "type": "object", + "description": "The notification data structure", + "properties": { + "event": { + "description": "The event payload linked to the involved resource object", + "$ref": "#/definitions/DocumentSpecificationDeleteEventPayload" + }, + "eventId": { + "type": "string", + "description": "The identifier of the notification." + }, + "eventTime": { + "type": "string", + "format": "date-time", + "description": "Time of the event occurrence." + }, + "eventType": { + "type": "string", + "description": "The type of the notification." + }, + "correlationId": { + "type": "string", + "description": "The correlation id for this event." + }, + "domain": { + "type": "string", + "description": "The domain of the event." + }, + "title": { + "type": "string", + "description": "The title of the event." + }, + "description": { + "type": "string", + "description": "An explnatory of the event." + }, + "priority": { + "type": "string", + "description": "A priority." + }, + "timeOcurred": { + "type": "string", + "format": "date-time", + "description": "The time the event occured." + } + } + }, + "DocumentSpecificationDeleteEventPayload": { + "type": "object", + "description": "The event data structure", + "properties": { + "documentSpecification": { + "description": "The involved resource data for the event", + "$ref": "#/definitions/DocumentSpecification" + } + } + }, + "Error": { + "description": "Used when an API throws an Error, typically with a HTTP error response-code (3xx, 4xx, 5xx)", + "type": "object", + "required": [ + "code", + "reason" + ], + "properties": { + "code": { + "type": "string", + "description": "Application relevant detail, defined in the API or a common list." + }, + "reason": { + "type": "string", + "description": "Explanation of the reason for the error which can be shown to a client user." + }, + "message": { + "type": "string", + "description": "More details and corrective actions related to the error which can be shown to a client user." + }, + "status": { + "type": "string", + "description": "HTTP Error code extension" + }, + "referenceError": { + "type": "string", + "format": "uri", + "description": "URI of documentation describing the error." + }, + "@baseType": { + "type": "string", + "description": "When sub-classing, this defines the super-class." + }, + "@schemaLocation": { + "type": "string", + "format": "uri", + "description": "A URI to a JSON-Schema file that defines additional attributes and relationships" + }, + "@type": { + "type": "string", + "description": "When sub-classing, this defines the sub-class entity name." + } + } + } + } +} \ No newline at end of file diff --git a/document-management/pom.xml b/document-management/pom.xml new file mode 100644 index 00000000..857cafed --- /dev/null +++ b/document-management/pom.xml @@ -0,0 +1,279 @@ + + + 4.0.0 + org.fiware.tmforum + document-management + 0.1 + + + org.fiware + tmforum + 0.1 + + + + ${project.parent.basedir}/api/tm-forum/${project.artifactId}/api.json + + + + + org.fiware.tmforum + common + ${project.version} + compile + + + org.fiware.tmforum + service-shared-models + ${project.version} + compile + + + + + io.minio + minio + ${version.io.minio.minio} + + + + + 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 + + + jakarta.validation + jakarta.validation-api + + + com.networknt + json-schema-validator + + + + + jakarta.annotation + jakarta.annotation-api + + + + + 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-core + ${version.org.mockito.mockito-junit-jupiter} + 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 + + false + ${tmforum.api.url} + org.fiware.document.api + org.fiware.document.model + + + + + 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 + + + + + + + skip-minio + + + skipMinio + true + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + org.apache.maven.plugins + maven-failsafe-plugin + + true + + + + + + + diff --git a/document-management/src/main/java/org/fiware/tmforum/documentmanagement/Application.java b/document-management/src/main/java/org/fiware/tmforum/documentmanagement/Application.java new file mode 100644 index 00000000..75ad6e3a --- /dev/null +++ b/document-management/src/main/java/org/fiware/tmforum/documentmanagement/Application.java @@ -0,0 +1,35 @@ +package org.fiware.tmforum.documentmanagement; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +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.document.model.DocumentSpecificationVO; +import org.fiware.tmforum.common.mapping.FieldCleaningSerializer; + +import javax.inject.Singleton; + +@Factory +public class Application { + + public static void main(String[] args) { + Micronaut.run(Application.class, args); + } + + @Singleton + @RequiredArgsConstructor + static class ObjectMapperBeanEventListener implements BeanCreatedEventListener { + + @Override + public ObjectMapper onCreated(BeanCreatedEvent event) { + final ObjectMapper objectMapper = event.getBean(); + SimpleModule fieldParamModule = new SimpleModule(); + fieldParamModule.addSerializer(DocumentSpecificationVO.class, new FieldCleaningSerializer<>()); + objectMapper.registerModule(fieldParamModule); + return objectMapper; + } + } +} diff --git a/document-management/src/main/java/org/fiware/tmforum/documentmanagement/AttachmentService.java b/document-management/src/main/java/org/fiware/tmforum/documentmanagement/AttachmentService.java new file mode 100644 index 00000000..d95e1316 --- /dev/null +++ b/document-management/src/main/java/org/fiware/tmforum/documentmanagement/AttachmentService.java @@ -0,0 +1,68 @@ +package org.fiware.tmforum.documentmanagement; + +import org.fiware.tmforum.common.domain.AttachmentRefOrValue; +import reactor.core.publisher.Mono; + +import java.util.List; + +/** + * Service interface for managing document attachments. + * + *

Implementations may back this with different storage systems (e.g. S3-compatible object + * storage, local filesystem, a third-party DMS). When no implementation is configured the + * DocumentManagement API still operates normally, but rejects any attachment that carries inline + * content — pure URL/href references are always accepted regardless of the backing store. + */ +public interface AttachmentService { + + /** + * Validates that the attachment's inline content (if any) is acceptable for storage — + * e.g. correct encoding and within the size limit. Throws a {@code TmForumException} on + * violation. Attachments that carry only a URL reference are always considered valid. + * + * @param attachment the attachment to validate + */ + void validateAttachmentContent(AttachmentRefOrValue attachment); + + /** + * Offloads inline base64 content from the given attachments to the backing store. The + * {@code content} field is cleared and the {@code url} field is set to an implementation- + * specific internal reference that can later be resolved by {@link #resolveAttachments}. + * Attachments that already carry a managed URL or that have no content are returned unchanged. + * + * @param attachments list of attachments to process + * @param entityId the owning entity's ID, used to namespace stored objects + * @return a {@code Mono} emitting the processed list + */ + Mono> offloadAttachments(List attachments, String entityId); + + /** + * Resolves the internal storage reference in the {@code url} field of each attachment to an + * accessible URL (e.g. a pre-signed download link) and returns a copy with {@code url} updated + * accordingly. Attachments whose {@code url} is not managed by this service are returned unchanged. + * + * @param attachments list of attachments to resolve + * @return a {@code Mono} emitting the resolved list + */ + Mono> resolveAttachments(List attachments); + + /** + * Deletes any stored objects that are referenced by the given attachments. Attachments + * without a retrieval reference are silently ignored. + * + * @param attachments list of attachments whose stored content should be removed + * @return a {@code Mono} that completes when all deletions are done + */ + Mono deleteAttachments(List attachments); + + /** + * Deletes stored objects for attachments that were present in {@code existing} but are no + * longer present in {@code updated}. Used during PATCH to clean up orphaned objects when + * the client replaces or removes attachments. + * + * @param existing attachments currently persisted for the entity + * @param updated attachments supplied in the PATCH request + * @return a {@code Mono} that completes when all orphaned deletions are done + */ + Mono deleteOrphanedAttachments(List existing, List updated); +} diff --git a/document-management/src/main/java/org/fiware/tmforum/documentmanagement/DocumentManagementEventMapper.java b/document-management/src/main/java/org/fiware/tmforum/documentmanagement/DocumentManagementEventMapper.java new file mode 100644 index 00000000..743d57ef --- /dev/null +++ b/document-management/src/main/java/org/fiware/tmforum/documentmanagement/DocumentManagementEventMapper.java @@ -0,0 +1,39 @@ +package org.fiware.tmforum.documentmanagement; + +import lombok.RequiredArgsConstructor; +import org.fiware.document.model.DocumentSpecificationVO; +import org.fiware.tmforum.common.exception.TmForumException; +import org.fiware.tmforum.common.exception.TmForumExceptionReason; +import org.fiware.tmforum.common.mapping.EventMapping; +import org.fiware.tmforum.common.notification.ModuleEventMapper; +import org.fiware.tmforum.documentmanagement.domain.DocumentSpecification; + +import javax.inject.Singleton; +import java.util.Map; + +import static java.util.Map.entry; + +@RequiredArgsConstructor +@Singleton +public class DocumentManagementEventMapper implements ModuleEventMapper { + + private final TMForumMapper tmForumMapper; + + @Override + public Map getEntityClassMapping() { + return Map.ofEntries( + entry(DocumentSpecification.TYPE_DOCUMENT_SPECIFICATION, + new EventMapping(DocumentSpecificationVO.class, DocumentSpecification.class)) + ); + } + + @Override + public Object mapPayload(Object rawPayload, Class rawClass) { + if (rawClass == DocumentSpecification.class) { + return tmForumMapper.map((DocumentSpecification) rawPayload); + } + throw new TmForumException( + String.format("Event-Payload %s is not supported.", rawPayload), + TmForumExceptionReason.INVALID_DATA); + } +} diff --git a/document-management/src/main/java/org/fiware/tmforum/documentmanagement/TMForumMapper.java b/document-management/src/main/java/org/fiware/tmforum/documentmanagement/TMForumMapper.java new file mode 100644 index 00000000..d6b5f8f3 --- /dev/null +++ b/document-management/src/main/java/org/fiware/tmforum/documentmanagement/TMForumMapper.java @@ -0,0 +1,101 @@ +package org.fiware.tmforum.documentmanagement; + +import io.github.wistefan.mapping.MappingException; +import org.fiware.document.model.*; +import org.fiware.tmforum.common.domain.AttachmentRefOrValue; +import org.fiware.tmforum.common.domain.ConstraintRef; +import org.fiware.tmforum.common.domain.subscription.TMForumSubscription; +import org.fiware.tmforum.common.mapping.BaseMapper; +import org.fiware.tmforum.common.mapping.IdHelper; +import org.fiware.tmforum.documentmanagement.domain.DocumentSpecification; +import org.fiware.tmforum.service.CharacteristicSpecification; +import org.fiware.tmforum.service.CharacteristicValueSpecification; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; + +@Mapper(componentModel = "jsr330", uses = {IdHelper.class}) +public abstract class TMForumMapper extends BaseMapper { + + // DocumentSpecification mappings + + @Mapping(target = "id", source = "id") + @Mapping(target = "href", source = "id") + public abstract DocumentSpecificationVO map(DocumentSpecificationCreateVO createVO, URI id); + + public abstract DocumentSpecificationVO map(DocumentSpecification documentSpecification); + + @Mapping(target = "href", source = "id") + public abstract DocumentSpecification map(DocumentSpecificationVO documentSpecificationVO); + + @Mapping(target = "id", source = "id") + @Mapping(target = "href", source = "id") + public abstract DocumentSpecification map(DocumentSpecificationUpdateVO updateVO, String id); + + // ConstraintRef mappings + + public abstract ConstraintRefVO map(ConstraintRef constraintRef); + + public abstract ConstraintRef map(ConstraintRefVO constraintRefVO); + + // CharacteristicSpecification mappings + + @Mapping(target = "id", source = "tmfId") + public abstract CharacteristicSpecificationVO map(CharacteristicSpecification characteristicSpecification); + + @Mapping(target = "tmfId", source = "id") + public abstract CharacteristicSpecification map(CharacteristicSpecificationVO characteristicSpecificationVO); + + @Mapping(target = "tmfValue", source = "value") + public abstract CharacteristicValueSpecification map(CharacteristicValueSpecificationVO characteristicVO); + + @Mapping(target = "value", source = "tmfValue") + public abstract CharacteristicValueSpecificationVO map(CharacteristicValueSpecification characteristic); + + // AttachmentRefOrValue mappings + + public abstract AttachmentRefOrValue map(AttachmentRefOrValueVO attachmentVO); + + public abstract AttachmentRefOrValueVO map(AttachmentRefOrValue attachment); + + // URL/URI helper methods + + public 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); + } + } + + public String map(URL value) { + if (value == null) { + return null; + } + return value.toString(); + } + + public URI map(URI value) { + return value; + } + + public URI mapToURI(String value) { + if (value == null) { + return null; + } + return URI.create(value); + } + + public String mapFromURI(URI value) { + if (value == null) { + return null; + } + return value.toString(); + } +} diff --git a/document-management/src/main/java/org/fiware/tmforum/documentmanagement/domain/DocumentSpecification.java b/document-management/src/main/java/org/fiware/tmforum/documentmanagement/domain/DocumentSpecification.java new file mode 100644 index 00000000..77d6b47f --- /dev/null +++ b/document-management/src/main/java/org/fiware/tmforum/documentmanagement/domain/DocumentSpecification.java @@ -0,0 +1,93 @@ +package org.fiware.tmforum.documentmanagement.domain; + +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 lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.fiware.tmforum.common.domain.AttachmentRefOrValue; +import org.fiware.tmforum.common.domain.ConstraintRef; +import org.fiware.tmforum.common.domain.EntityWithId; +import org.fiware.tmforum.common.domain.RelatedParty; +import org.fiware.tmforum.common.domain.TimePeriod; +import org.fiware.tmforum.service.CharacteristicSpecification; +import org.fiware.tmforum.service.EntitySpecificationRelationship; +import org.fiware.tmforum.service.TargetEntitySchema; + +import java.net.URI; +import java.time.Instant; +import java.util.List; + +@EqualsAndHashCode(callSuper = true) +@MappingEnabled(entityType = DocumentSpecification.TYPE_DOCUMENT_SPECIFICATION) +public class DocumentSpecification extends EntityWithId { + + public static final String TYPE_DOCUMENT_SPECIFICATION = "document-specification"; + + @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 = "name")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY, targetName = "name")})) + private String name; + + @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 = "version")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY, targetName = "version")})) + private String version; + + @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 = "lastUpdate")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY, targetName = "lastUpdate")})) + private Instant lastUpdate; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY, targetName = "lifecycleStatus")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY, targetName = "lifecycleStatus")})) + private String lifecycleStatus; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY_LIST, targetName = "attachment")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY_LIST, targetName = "attachment", targetClass = AttachmentRefOrValue.class)})) + private List attachment; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.RELATIONSHIP_LIST, targetName = "relatedParty")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.RELATIONSHIP_LIST, targetName = "relatedParty", targetClass = RelatedParty.class, fromProperties = true)})) + private List relatedParty; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.RELATIONSHIP_LIST, targetName = "constraint")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.RELATIONSHIP_LIST, targetName = "constraint", targetClass = ConstraintRef.class)})) + private List constraint; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.RELATIONSHIP_LIST, targetName = "entitySpecRelationship")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.RELATIONSHIP_LIST, targetName = "entitySpecRelationship", targetClass = EntitySpecificationRelationship.class)})) + private List entitySpecRelationship; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY_LIST, targetName = "specCharacteristic")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY_LIST, targetName = "specCharacteristic", targetClass = CharacteristicSpecification.class)})) + private List specCharacteristic; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY, targetName = "targetEntitySchema")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY, targetName = "targetEntitySchema")})) + private TargetEntitySchema targetEntitySchema; + + @Getter(onMethod = @__({@AttributeGetter(value = AttributeType.PROPERTY, targetName = "validFor")})) + @Setter(onMethod = @__({@AttributeSetter(value = AttributeType.PROPERTY, targetName = "validFor")})) + private TimePeriod validFor; + + public DocumentSpecification(String id) { + super(TYPE_DOCUMENT_SPECIFICATION, id); + } + + @Override + public String getEntityState() { + return lifecycleStatus; + } +} diff --git a/document-management/src/main/java/org/fiware/tmforum/documentmanagement/rest/DocumentSpecificationApiController.java b/document-management/src/main/java/org/fiware/tmforum/documentmanagement/rest/DocumentSpecificationApiController.java new file mode 100644 index 00000000..9d095ce4 --- /dev/null +++ b/document-management/src/main/java/org/fiware/tmforum/documentmanagement/rest/DocumentSpecificationApiController.java @@ -0,0 +1,224 @@ +package org.fiware.tmforum.documentmanagement.rest; + +import io.micronaut.http.HttpResponse; +import io.micronaut.http.annotation.Controller; +import lombok.extern.slf4j.Slf4j; +import org.fiware.document.api.DocumentSpecificationApi; +import org.fiware.document.model.DocumentSpecificationCreateVO; +import org.fiware.document.model.DocumentSpecificationUpdateVO; +import org.fiware.document.model.DocumentSpecificationVO; +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.common.domain.AttachmentRefOrValue; +import org.fiware.tmforum.documentmanagement.AttachmentService; +import org.fiware.tmforum.documentmanagement.TMForumMapper; +import org.fiware.tmforum.documentmanagement.domain.DocumentSpecification; +import reactor.core.publisher.Mono; + +import javax.annotation.Nullable; +import java.time.Clock; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Slf4j +@Controller("${api.document-management.basepath:/}") +public class DocumentSpecificationApiController extends AbstractApiController + implements DocumentSpecificationApi { + + private final TMForumMapper tmForumMapper; + private final Clock clock; + @Nullable + private final AttachmentService attachmentService; + + public DocumentSpecificationApiController( + QueryParser queryParser, + ReferenceValidationService validationService, + TmForumRepository repository, + TMForumMapper tmForumMapper, + Clock clock, + TMForumEventHandler eventHandler, + @Nullable AttachmentService attachmentService) { + super(queryParser, validationService, repository, eventHandler); + this.tmForumMapper = tmForumMapper; + this.clock = clock; + this.attachmentService = attachmentService; + } + + @Override + public Mono> createDocumentSpecification( + DocumentSpecificationCreateVO createVO) { + + if (createVO.getName() == null || createVO.getName().trim().isEmpty()) { + throw new TmForumException( + "Name field is required and must not be blank to create a document specification.", + TmForumExceptionReason.INVALID_DATA); + } + + DocumentSpecification docSpec = tmForumMapper.map( + tmForumMapper.map(createVO, + IdHelper.toNgsiLd(UUID.randomUUID().toString(), + DocumentSpecification.TYPE_DOCUMENT_SPECIFICATION))); + + docSpec.setLastUpdate(clock.instant()); + + List attachments = docSpec.getAttachment(); + if (attachmentService == null) { + rejectInlineContent(attachments); + } else if (attachments != null) { + attachments.forEach(attachmentService::validateAttachmentContent); + } + + Mono preparedSpec = attachmentService != null + ? attachmentService.offloadAttachments(attachments, docSpec.getId().toString()) + .doOnNext(docSpec::setAttachment) + .thenReturn(docSpec) + : Mono.just(docSpec); + + return preparedSpec + .flatMap(spec -> create(getCheckingMono(spec), DocumentSpecification.class)) + .map(tmForumMapper::map) + .map(HttpResponse::created); + } + + @Override + public Mono> deleteDocumentSpecification(String id) { + if (!IdHelper.isNgsiLdId(id)) { + throw new TmForumException( + "Did not receive a valid id, such document specification cannot exist.", + TmForumExceptionReason.NOT_FOUND); + } + + // First retrieve to get attachment info for S3 cleanup + return retrieve(id, DocumentSpecification.class) + .flatMap(docSpec -> { + Mono deletionStep = attachmentService != null + ? attachmentService.deleteAttachments(docSpec.getAttachment()) + : Mono.empty(); + return deletionStep.then(delete(id)); + }) + .switchIfEmpty(Mono.defer(() -> delete(id))); + } + + @Override + public Mono>> listDocumentSpecification( + @Nullable String fields, + @Nullable Integer offset, + @Nullable Integer limit) { + // List returns retrieval info only (no hydration) + return list(offset, limit, DocumentSpecification.TYPE_DOCUMENT_SPECIFICATION, DocumentSpecification.class) + .map(stream -> stream.map(tmForumMapper::map).toList()) + .switchIfEmpty(Mono.just(List.of())) + .map(HttpResponse::ok); + } + + @Override + public Mono> patchDocumentSpecification( + String id, + DocumentSpecificationUpdateVO updateVO) { + if (!IdHelper.isNgsiLdId(id)) { + throw new TmForumException( + "Did not receive a valid id, such document specification cannot exist.", + TmForumExceptionReason.NOT_FOUND); + } + + DocumentSpecification updatedSpec = tmForumMapper.map(updateVO, id); + List newAttachments = updatedSpec.getAttachment(); + + if (attachmentService == null) { + rejectInlineContent(newAttachments); + return retrieve(id, DocumentSpecification.class) + .flatMap(existing -> { + if (updatedSpec.getAtSchemaLocation() == null) { + updatedSpec.setAtSchemaLocation(existing.getAtSchemaLocation()); + } + return patch(id, updatedSpec, getCheckingMono(updatedSpec), DocumentSpecification.class); + }) + .switchIfEmpty(Mono.defer(() -> patch(id, updatedSpec, getCheckingMono(updatedSpec), DocumentSpecification.class))) + .map(tmForumMapper::map) + .map(HttpResponse::ok); + } + + if (newAttachments != null) { + newAttachments.forEach(attachmentService::validateAttachmentContent); + } + + return retrieve(id, DocumentSpecification.class) + .flatMap(existing -> { + if (updatedSpec.getAtSchemaLocation() == null) { + updatedSpec.setAtSchemaLocation(existing.getAtSchemaLocation()); + } + if (newAttachments == null || newAttachments.isEmpty()) { + return Mono.just(updatedSpec); + } + return attachmentService.deleteOrphanedAttachments(existing.getAttachment(), newAttachments) + .then(attachmentService.offloadAttachments(newAttachments, id)) + .doOnNext(updatedSpec::setAttachment) + .thenReturn(updatedSpec); + }) + .switchIfEmpty(Mono.just(updatedSpec)) + .flatMap(spec -> patch(id, spec, getCheckingMono(spec), DocumentSpecification.class)) + .map(tmForumMapper::map) + .map(HttpResponse::ok); + } + + @Override + public Mono> retrieveDocumentSpecification( + String id, + @Nullable String fields) { + if (!IdHelper.isNgsiLdId(id)) { + throw new TmForumException( + "Did not receive a valid id, such document specification cannot exist.", + TmForumExceptionReason.NOT_FOUND); + } + + return retrieve(id, DocumentSpecification.class) + .switchIfEmpty(Mono.error(new TmForumException( + "No such document specification exists.", + TmForumExceptionReason.NOT_FOUND))) + .flatMap(docSpec -> { + if (attachmentService != null) { + return attachmentService.resolveAttachments(docSpec.getAttachment()) + .doOnNext(docSpec::setAttachment) + .thenReturn(docSpec); + } + return Mono.just(docSpec); + }) + .map(tmForumMapper::map) + .map(HttpResponse::ok); + } + + private void rejectInlineContent(List attachments) { + if (attachments == null) { + return; + } + attachments.stream() + .filter(att -> att.getContent() != null && !att.getContent().isEmpty()) + .findFirst() + .ifPresent(att -> { + throw new TmForumException( + "Attachments with inline content are not supported when no AttachmentService is configured. Provide a URL reference instead.", + TmForumExceptionReason.INVALID_DATA); + }); + } + + private Mono getCheckingMono(DocumentSpecification docSpec) { + List> references = new ArrayList<>(); + references.add(docSpec.getRelatedParty()); + references.add(docSpec.getConstraint()); + references.add(docSpec.getEntitySpecRelationship()); + + return getCheckingMono(docSpec, references) + .onErrorMap(throwable -> new TmForumException( + String.format("Was not able to create document specification %s", docSpec.getId()), + throwable, + TmForumExceptionReason.INVALID_RELATIONSHIP)); + } +} diff --git a/document-management/src/main/java/org/fiware/tmforum/documentmanagement/s3/S3AttachmentService.java b/document-management/src/main/java/org/fiware/tmforum/documentmanagement/s3/S3AttachmentService.java new file mode 100644 index 00000000..9623fa5a --- /dev/null +++ b/document-management/src/main/java/org/fiware/tmforum/documentmanagement/s3/S3AttachmentService.java @@ -0,0 +1,335 @@ +package org.fiware.tmforum.documentmanagement.s3; + +import io.micronaut.context.annotation.Requires; +import org.fiware.tmforum.documentmanagement.AttachmentService; +import io.minio.BucketExistsArgs; +import io.minio.GetPresignedObjectUrlArgs; +import io.minio.MakeBucketArgs; +import io.minio.MinioClient; +import io.minio.PutObjectArgs; +import io.minio.RemoveObjectArgs; +import io.minio.http.Method; +import jakarta.annotation.PostConstruct; +import jakarta.inject.Singleton; +import lombok.extern.slf4j.Slf4j; +import org.fiware.tmforum.common.domain.AttachmentRefOrValue; +import org.fiware.tmforum.common.domain.Quantity; +import org.fiware.tmforum.common.domain.TimePeriod; +import org.fiware.tmforum.common.exception.TmForumException; +import org.fiware.tmforum.common.exception.TmForumExceptionReason; + +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import java.io.ByteArrayInputStream; +import java.net.URL; +import java.util.Base64; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * {@link AttachmentService} implementation backed by an S3-compatible object store (e.g. MinIO, + * AWS S3, IONOS Object Storage). + * + *

Inline base64 attachment content is decoded, validated against the configured size limit, + * and uploaded to the configured bucket. The {@code content} field is cleared and the {@code url} + * field is set to an internal S3 path ({@code endpoint/bucket/key}). On retrieval the internal path + * is resolved to a pre-signed download URL so the raw bytes are never persisted in the context broker. + * + *

This bean is only instantiated when {@code s3.enabled=true} is set. When it is absent the + * API falls back to accepting pure URL/href references only. + */ +@Singleton +@Requires(property = "s3.enabled", value = "true") +@Slf4j +public class S3AttachmentService implements AttachmentService { + + private static final String DEFAULT_CONTENT_TYPE = "application/octet-stream"; + private static final int PRESIGNED_URL_EXPIRY_SECONDS = 3600; + + private MinioClient s3Client; + private final S3Configuration config; + + public S3AttachmentService(S3Configuration config) { + this.config = config; + } + + /** + * Initialises the MinIO client from configuration and ensures the target bucket exists. + * Called automatically by the Micronaut container after dependency injection. + * + * @throws RuntimeException if the MinIO client cannot be created + */ + @PostConstruct + public void init() { + log.info("Initializing S3AttachmentService: endpoint={}, bucket={}, maxContentSize={}", + config.getEndpoint(), config.getBucket(), config.getMaxContentSize()); + + try { + MinioClient.Builder builder = MinioClient.builder() + .endpoint(config.getEndpoint()) + .credentials(config.getAccessKey(), config.getSecretKey()); + if (config.getRegion() != null && !config.getRegion().isBlank()) { + builder.region(config.getRegion()); + } + this.s3Client = builder.build(); + log.info("S3 client created successfully"); + } catch (Exception e) { + log.error("Failed to create S3 client: {}", e.getMessage(), e); + throw new TmForumException("Failed to initialize S3 client", e, TmForumExceptionReason.UNKNOWN); + } + try { + ensureBucketExists(); + } catch (Exception e) { + log.warn("Could not ensure bucket exists on startup: {}. Will retry on first upload.", e.getMessage()); + } + } + + private void ensureBucketExists() throws Exception { + boolean exists = s3Client.bucketExists( + BucketExistsArgs.builder().bucket(config.getBucket()).build()); + if (!exists) { + s3Client.makeBucket( + MakeBucketArgs.builder().bucket(config.getBucket()).build()); + log.info("Created S3 bucket: {}", config.getBucket()); + } + } + + /** {@inheritDoc} */ + @Override + public void validateAttachmentContent(AttachmentRefOrValue attachment) { + String content = attachment.getContent(); + if (content == null || content.isEmpty()) { + return; + } + byte[] decoded; + try { + decoded = Base64.getDecoder().decode(content); + } catch (IllegalArgumentException e) { + throw new TmForumException("Attachment content is not valid base64.", TmForumExceptionReason.INVALID_DATA); + } + if (decoded.length > config.getMaxContentSize()) { + throw new TmForumException( + String.format("Attachment content exceeds maximum size of %d bytes (got %d bytes)", + config.getMaxContentSize(), decoded.length), + TmForumExceptionReason.INVALID_DATA); + } + } + + /** {@inheritDoc} */ + @Override + public Mono> offloadAttachments(List attachments, String entityId) { + if (attachments == null || attachments.isEmpty()) { + return Mono.justOrEmpty(attachments); + } + + return Mono.fromCallable(() -> attachments.stream() + .map(att -> processAttachmentForOffload(att, entityId)) + .collect(Collectors.toList())) + .subscribeOn(Schedulers.boundedElastic()); + } + + /** {@inheritDoc} */ + @Override + public Mono> resolveAttachments(List attachments) { + if (attachments == null || attachments.isEmpty()) { + return Mono.justOrEmpty(attachments); + } + + return Mono.fromCallable(() -> attachments.stream() + .map(this::resolveAttachment) + .collect(Collectors.toList())) + .subscribeOn(Schedulers.boundedElastic()); + } + + /** {@inheritDoc} */ + @Override + public Mono deleteAttachments(List attachments) { + if (attachments == null || attachments.isEmpty()) { + return Mono.empty(); + } + + return Mono.fromRunnable(() -> attachments.stream() + .map(AttachmentRefOrValue::getUrl) + .filter(this::isManagedUrl) + .map(this::extractKey) + .forEach(key -> { + try { + deleteFromS3(key); + } catch (Exception e) { + log.warn("Failed to delete S3 object {}", key, e); + } + })) + .subscribeOn(Schedulers.boundedElastic()) + .then(); + } + + /** {@inheritDoc} */ + @Override + public Mono deleteOrphanedAttachments(List existing, List updated) { + if (existing == null || existing.isEmpty()) { + return Mono.empty(); + } + List updatedUrls = updated == null ? List.of() : updated.stream() + .filter(a -> a.getUrl() != null) + .map(a -> a.getUrl().toString()) + .toList(); + + return Mono.fromRunnable(() -> existing.stream() + .filter(a -> isManagedUrl(a.getUrl())) + .filter(a -> !updatedUrls.contains(a.getUrl().toString())) + .map(a -> extractKey(a.getUrl())) + .forEach(key -> { + try { + deleteFromS3(key); + } catch (Exception e) { + log.warn("Failed to delete orphaned S3 object {}", key, e); + } + })) + .subscribeOn(Schedulers.boundedElastic()) + .then(); + } + + private AttachmentRefOrValue processAttachmentForOffload(AttachmentRefOrValue attachment, String entityId) { + String content = attachment.getContent(); + if (content == null || content.isEmpty()) { + return attachment; + } + + // Skip if this attachment already has a managed URL + if (isManagedUrl(attachment.getUrl())) { + return attachment; + } + + byte[] decoded; + try { + decoded = Base64.getDecoder().decode(content); + } catch (IllegalArgumentException e) { + log.warn("Content is not valid base64, skipping offload"); + return attachment; + } + + String key = generateKey(entityId, attachment); + uploadToS3(key, decoded, attachment.getMimeType()); + + AttachmentRefOrValue modified = copyAttachment(attachment); + modified.setContent(null); + try { + modified.setUrl(new URL(config.getEndpoint() + "/" + config.getBucket() + "/" + key)); + } catch (Exception e) { + throw new TmForumException("Failed to construct S3 URL for attachment: " + e.getMessage(), + TmForumExceptionReason.INVALID_DATA); + } + return modified; + } + + private AttachmentRefOrValue resolveAttachment(AttachmentRefOrValue attachment) { + if (!isManagedUrl(attachment.getUrl())) { + return attachment; + } + + try { + String key = extractKey(attachment.getUrl()); + String presignedUrl = s3Client.getPresignedObjectUrl( + GetPresignedObjectUrlArgs.builder() + .method(Method.GET) + .bucket(config.getBucket()) + .object(key) + .expiry(PRESIGNED_URL_EXPIRY_SECONDS) + .build()); + AttachmentRefOrValue resolved = copyAttachment(attachment); + resolved.setUrl(new URL(presignedUrl)); + return resolved; + } catch (Exception e) { + log.error("Failed to resolve attachment URL from S3: {}", e.getMessage()); + return attachment; + } + } + + private boolean isManagedUrl(URL url) { + if (url == null) { + return false; + } + String prefix = config.getEndpoint() + "/" + config.getBucket() + "/"; + return url.toString().startsWith(prefix); + } + + private String extractKey(URL url) { + String prefix = config.getEndpoint() + "/" + config.getBucket() + "/"; + return url.toString().substring(prefix.length()); + } + + private String generateKey(String entityId, AttachmentRefOrValue attachment) { + String name = attachment.getName(); + if (name == null || name.isEmpty()) { + name = "attachment"; + } + // Clean the name for S3 key + name = name.replaceAll("[^a-zA-Z0-9._-]", "_"); + return String.format("%s/%s-%s", entityId, UUID.randomUUID().toString(), name); + } + + private void uploadToS3(String key, byte[] content, String mimeType) { + try { + ensureBucketExists(); + + String contentType = mimeType != null ? mimeType : DEFAULT_CONTENT_TYPE; + + s3Client.putObject( + PutObjectArgs.builder() + .bucket(config.getBucket()) + .object(key) + .stream(new ByteArrayInputStream(content), content.length, -1) + .contentType(contentType) + .build() + ); + log.debug("Uploaded to S3: {}/{}", config.getBucket(), key); + } catch (Exception e) { + throw new TmForumException( + "Failed to upload attachment to S3: " + e.getMessage(), + TmForumExceptionReason.INVALID_DATA); + } + } + + private void deleteFromS3(String key) { + try { + s3Client.removeObject( + RemoveObjectArgs.builder() + .bucket(config.getBucket()) + .object(key) + .build() + ); + log.debug("Deleted from S3: {}/{}", config.getBucket(), key); + } catch (Exception e) { + log.warn("Failed to delete S3 object {}", key, e); + } + } + + private AttachmentRefOrValue copyAttachment(AttachmentRefOrValue source) { + AttachmentRefOrValue copy = new AttachmentRefOrValue(); + copy.setTmfId(source.getTmfId()); + copy.setHref(source.getHref()); + copy.setAttachmentType(source.getAttachmentType()); + copy.setContent(source.getContent()); + copy.setDescription(source.getDescription()); + copy.setMimeType(source.getMimeType()); + copy.setUrl(source.getUrl()); + if (source.getSize() != null) { + Quantity sizeCopy = new Quantity(); + sizeCopy.setAmount(source.getSize().getAmount()); + sizeCopy.setUnits(source.getSize().getUnits()); + copy.setSize(sizeCopy); + } + if (source.getValidFor() != null) { + TimePeriod validForCopy = new TimePeriod(); + validForCopy.setStartDateTime(source.getValidFor().getStartDateTime()); + validForCopy.setEndDateTime(source.getValidFor().getEndDateTime()); + copy.setValidFor(validForCopy); + } + copy.setName(source.getName()); + copy.setAtReferredType(source.getAtReferredType()); + copy.setAtSchemaLocation(source.getAtSchemaLocation()); + return copy; + } +} diff --git a/document-management/src/main/java/org/fiware/tmforum/documentmanagement/s3/S3Configuration.java b/document-management/src/main/java/org/fiware/tmforum/documentmanagement/s3/S3Configuration.java new file mode 100644 index 00000000..437f1c77 --- /dev/null +++ b/document-management/src/main/java/org/fiware/tmforum/documentmanagement/s3/S3Configuration.java @@ -0,0 +1,70 @@ +package org.fiware.tmforum.documentmanagement.s3; + +import io.micronaut.context.annotation.ConfigurationProperties; +import lombok.Getter; +import lombok.Setter; + +/** + * Configuration properties for the S3-compatible object storage backend, bound from the + * {@code s3.*} namespace in {@code application.yaml}. + * + *

S3 support is opt-in: set {@code s3.enabled=true} together with the connection properties + * to activate the {@link S3AttachmentService}. When disabled, the DocumentManagement API + * still operates normally but rejects attachments with inline content. + * + *

Example configuration for a local MinIO instance: + *

+ * s3:
+ *   enabled: true
+ *   endpoint: http://minio:9000
+ *   access-key: minioadmin
+ *   secret-key: minioadmin
+ *   bucket: document-attachments
+ * 
+ */ +@ConfigurationProperties("s3") +@Getter +@Setter +public class S3Configuration { + + /** + * Whether the S3 storage backend is enabled. When {@code false} (the default) the + * {@link S3AttachmentService} bean is not instantiated and attachments with inline + * content are rejected by the API. + */ + private boolean enabled = false; + + /** + * URL of the S3-compatible endpoint, e.g. {@code http://minio:9000} for MinIO or + * {@code https://s3.amazonaws.com} for AWS S3. Defaults to {@code http://localhost:9000}. + */ + private String endpoint = "http://localhost:9000"; + + /** + * Access key (username) used to authenticate with the S3 endpoint. + */ + private String accessKey = "minioadmin"; + + /** + * Secret key (password) used to authenticate with the S3 endpoint. + */ + private String secretKey = "minioadmin"; + + /** + * Name of the S3 bucket where attachment objects are stored. The bucket is created + * automatically on startup if it does not already exist. + */ + private String bucket = "document-attachments"; + + /** + * Maximum allowed size in bytes for a single attachment's inline content. Requests + * that exceed this limit are rejected with a 422. Defaults to 10 MB. + */ + private long maxContentSize = 10 * 1024 * 1024; + + /** + * S3 region identifier (e.g. {@code us-east-1} for AWS, {@code eu-central-3} for IONOS). + * Required for AWS S3 and IONOS Object Storage. Leave unset for local MinIO instances. + */ + private String region; +} diff --git a/document-management/src/main/resources/application.yaml b/document-management/src/main/resources/application.yaml new file mode 100644 index 00000000..e7fbde68 --- /dev/null +++ b/document-management/src/main/resources/application.yaml @@ -0,0 +1,60 @@ +micronaut: + + server: + port: 8667 + + 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: WARN + +--- +general: + contextUrl: https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld + serverHost: http://localhost:8667 + +api: + document-management: + basepath: ${general.basepath:/} + +mapping: + strictRelationships: false + api_extension_enabled: true + +s3: + # Any S3-compatible provider is supported. Examples: + # MinIO (local/k3s): endpoint: "http://localhost:9000" + # AWS S3: endpoint: "https://s3.amazonaws.com", region: "us-east-1" + # IONOS: endpoint: "https://s3-eu-central-1.ionoscloud.com" + enabled: false + endpoint: "http://localhost:9000" + accessKey: "minioadmin" + secretKey: "minioadmin" + bucket: "document-attachments" + maxContentSize: 10485760 + # region: "" # Required for AWS S3; omit for MinIO, IONOS, and other providers diff --git a/document-management/src/main/resources/logback.xml b/document-management/src/main/resources/logback.xml new file mode 100644 index 00000000..bd177f67 --- /dev/null +++ b/document-management/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 + + + + + + + + diff --git a/document-management/src/test/java/org/fiware/tmforum/documentmanagement/DocumentSpecificationApiIT.java b/document-management/src/test/java/org/fiware/tmforum/documentmanagement/DocumentSpecificationApiIT.java new file mode 100644 index 00000000..99c57495 --- /dev/null +++ b/document-management/src/test/java/org/fiware/tmforum/documentmanagement/DocumentSpecificationApiIT.java @@ -0,0 +1,577 @@ +package org.fiware.tmforum.documentmanagement; + +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.document.api.DocumentSpecificationApiTestClient; +import org.fiware.document.api.DocumentSpecificationApiTestSpec; +import org.fiware.document.model.AttachmentRefOrValueVO; +import org.fiware.document.model.DocumentSpecificationCreateVO; +import org.fiware.document.model.DocumentSpecificationStatusTypeVO; +import org.fiware.document.model.DocumentSpecificationUpdateVO; +import org.fiware.document.model.DocumentSpecificationVO; +import org.fiware.document.model.RelatedPartyVO; +import org.fiware.ngsi.api.EntitiesApiClient; +import org.fiware.tmforum.common.configuration.GeneralProperties; +import org.fiware.tmforum.common.exception.ErrorDetails; +import org.fiware.tmforum.common.domain.AttachmentRefOrValue; +import org.fiware.tmforum.common.notification.TMForumEventHandler; +import org.fiware.tmforum.common.test.AbstractApiIT; +import org.fiware.tmforum.documentmanagement.domain.DocumentSpecification; +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.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@MicronautTest(packages = {"org.fiware.tmforum.documentmanagement"}) +public class DocumentSpecificationApiIT extends AbstractApiIT implements DocumentSpecificationApiTestSpec { + + public final DocumentSpecificationApiTestClient documentSpecificationApiTestClient; + + private String message; + private DocumentSpecificationCreateVO documentSpecificationCreateVO; + private DocumentSpecificationVO expectedDocSpec; + + public DocumentSpecificationApiIT( + DocumentSpecificationApiTestClient documentSpecificationApiTestClient, + EntitiesApiClient entitiesApiClient, + ObjectMapper objectMapper, + GeneralProperties generalProperties) { + super(entitiesApiClient, objectMapper, generalProperties); + this.documentSpecificationApiTestClient = documentSpecificationApiTestClient; + } + + @Override + protected String getEntityType() { + return DocumentSpecification.TYPE_DOCUMENT_SPECIFICATION; + } + + @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; + } + + @MockBean(AttachmentService.class) + public AttachmentService attachmentService() { + AttachmentService attachmentService = mock(AttachmentService.class); + when(attachmentService.offloadAttachments(any(), any())).thenAnswer(i -> { + List list = i.getArgument(0); + return list == null ? Mono.empty() : Mono.just(list); + }); + when(attachmentService.resolveAttachments(any())).thenAnswer(i -> { + List list = i.getArgument(0); + return list == null ? Mono.empty() : Mono.just(list); + }); + when(attachmentService.deleteAttachments(any())).thenReturn(Mono.empty()); + when(attachmentService.deleteOrphanedAttachments(any(), any())).thenReturn(Mono.empty()); + return attachmentService; + } + + @ParameterizedTest + @MethodSource("provideValidDocumentSpecifications") + public void createDocumentSpecification201(String message, DocumentSpecificationCreateVO createVO, + DocumentSpecificationVO expectedDocSpec) throws Exception { + this.message = message; + this.documentSpecificationCreateVO = createVO; + this.expectedDocSpec = expectedDocSpec; + createDocumentSpecification201(); + } + + @Override + public void createDocumentSpecification201() throws Exception { + HttpResponse response = callAndCatch( + () -> documentSpecificationApiTestClient.createDocumentSpecification(null, documentSpecificationCreateVO)); + + assertEquals(HttpStatus.CREATED, response.getStatus(), message); + DocumentSpecificationVO body = response.body(); + assertNotNull(body, message); + assertNotNull(body.getId(), message); + assertEquals(expectedDocSpec.getName(), body.getName(), message); + if (expectedDocSpec.getDescription() != null) { + assertEquals(expectedDocSpec.getDescription(), body.getDescription(), message); + } + if (expectedDocSpec.getVersion() != null) { + assertEquals(expectedDocSpec.getVersion(), body.getVersion(), message); + } + if (expectedDocSpec.getLifecycleStatus() != null) { + assertEquals(expectedDocSpec.getLifecycleStatus(), body.getLifecycleStatus(), message); + } + } + + private static Stream provideValidDocumentSpecifications() { + List testEntries = new ArrayList<>(); + + DocumentSpecificationCreateVO simpleCreateVO = new DocumentSpecificationCreateVO(); + simpleCreateVO.setName("Test Document Specification"); + simpleCreateVO.setDescription("A test document specification"); + simpleCreateVO.setVersion("1.0.0"); + DocumentSpecificationVO simpleExpected = new DocumentSpecificationVO(); + simpleExpected.setName("Test Document Specification"); + simpleExpected.setDescription("A test document specification"); + simpleExpected.setVersion("1.0.0"); + testEntries.add(Arguments.of("A simple document specification should be created.", simpleCreateVO, simpleExpected)); + + DocumentSpecificationCreateVO withLifecycleVO = new DocumentSpecificationCreateVO(); + withLifecycleVO.setName("Document with Lifecycle"); + withLifecycleVO.setLifecycleStatus(DocumentSpecificationStatusTypeVO.APPROVED); + DocumentSpecificationVO lifecycleExpected = new DocumentSpecificationVO(); + lifecycleExpected.setName("Document with Lifecycle"); + lifecycleExpected.setLifecycleStatus(DocumentSpecificationStatusTypeVO.APPROVED); + testEntries.add(Arguments.of("A document specification with lifecycle status should be created.", withLifecycleVO, lifecycleExpected)); + + return testEntries.stream(); + } + + @Test + public void createDocumentSpecificationWithUrlAttachment201() throws Exception { + DocumentSpecificationCreateVO createVO = new DocumentSpecificationCreateVO(); + createVO.setName("Document with Attachment"); + createVO.setVersion("1.0.0"); + + AttachmentRefOrValueVO attachment = new AttachmentRefOrValueVO(); + attachment.setName("test-file.txt"); + attachment.setMimeType("text/plain"); + attachment.setUrl(URI.create("https://example.com/test-file.txt")); + createVO.setAttachment(List.of(attachment)); + + HttpResponse response = callAndCatch( + () -> documentSpecificationApiTestClient.createDocumentSpecification(null, createVO)); + + assertEquals(HttpStatus.CREATED, response.getStatus(), "Document specification with URL attachment should be created."); + assertNotNull(response.body()); + assertNotNull(response.body().getAttachment()); + assertFalse(response.body().getAttachment().isEmpty()); + assertEquals("https://example.com/test-file.txt", + response.body().getAttachment().get(0).getUrl().toString(), + "URL attachment should be preserved as-is."); + } + + @Test + public void createDocumentSpecificationWithInlineContent201() throws Exception { + DocumentSpecificationCreateVO createVO = new DocumentSpecificationCreateVO(); + createVO.setName("Document with Inline Content"); + + AttachmentRefOrValueVO attachment = new AttachmentRefOrValueVO(); + attachment.setName("test-file.txt"); + attachment.setContent(Base64.getEncoder().encodeToString("Hello World".getBytes())); + createVO.setAttachment(List.of(attachment)); + + HttpResponse response = callAndCatch( + () -> documentSpecificationApiTestClient.createDocumentSpecification(null, createVO)); + + assertEquals(HttpStatus.CREATED, response.getStatus(), + "Inline content should be accepted when an AttachmentService is configured."); + } + + @Test + @Override + public void createDocumentSpecification400() throws Exception { + // Test creation with missing required field (name) + DocumentSpecificationCreateVO createVO = new DocumentSpecificationCreateVO(); + createVO.setDescription("Missing name field"); + + HttpResponse response = callAndCatch( + () -> documentSpecificationApiTestClient.createDocumentSpecification(null, createVO)); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatus(), "Creation without name should fail."); + + Optional optionalErrorDetails = response.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 createDocumentSpecification401() throws Exception { + } + + @Disabled("Security is handled externally, thus 401 and 403 cannot happen.") + @Test + @Override + public void createDocumentSpecification403() throws Exception { + } + + @Disabled("Prohibited by the framework.") + @Test + @Override + public void createDocumentSpecification405() throws Exception { + } + + @Disabled("DocumentSpecification doesn't have 'implicit' entities and id is generated, no conflict possible.") + @Test + @Override + public void createDocumentSpecification409() throws Exception { + } + + @Override + public void createDocumentSpecification500() throws Exception { + } + + @Test + @Override + public void deleteDocumentSpecification204() throws Exception { + // First create a document specification + DocumentSpecificationCreateVO createVO = new DocumentSpecificationCreateVO(); + createVO.setName("Document to Delete"); + createVO.setVersion("1.0.0"); + + HttpResponse createResponse = callAndCatch( + () -> documentSpecificationApiTestClient.createDocumentSpecification(null, createVO)); + + assertEquals(HttpStatus.CREATED, createResponse.getStatus(), "The document specification should have been created first."); + String id = createResponse.body().getId(); + + // Then delete it + HttpResponse deleteResponse = callAndCatch( + () -> documentSpecificationApiTestClient.deleteDocumentSpecification(null, id)); + + assertEquals(HttpStatus.NO_CONTENT, deleteResponse.getStatus(), "The document specification should have been deleted."); + + // Verify it no longer exists + assertEquals(HttpStatus.NOT_FOUND, + callAndCatch(() -> documentSpecificationApiTestClient.retrieveDocumentSpecification(null, id, null)).getStatus(), + "The document specification should not exist anymore."); + } + + @Disabled("400 is impossible to happen on deletion with the current implementation.") + @Test + @Override + public void deleteDocumentSpecification400() throws Exception { + } + + @Disabled("Security is handled externally, thus 401 and 403 cannot happen.") + @Test + @Override + public void deleteDocumentSpecification401() throws Exception { + } + + @Disabled("Security is handled externally, thus 401 and 403 cannot happen.") + @Test + @Override + public void deleteDocumentSpecification403() throws Exception { + } + + @Test + @Override + public void deleteDocumentSpecification404() throws Exception { + HttpResponse notFoundResponse = callAndCatch( + () -> documentSpecificationApiTestClient.deleteDocumentSpecification(null, + "urn:ngsi-ld:document-specification:non-existent")); + + assertEquals(HttpStatus.NOT_FOUND, notFoundResponse.getStatus(), "No such document specification should exist."); + + Optional optionalErrorDetails = notFoundResponse.getBody(ErrorDetails.class); + assertTrue(optionalErrorDetails.isPresent(), "Error details should be provided."); + + // Also test with invalid id format + notFoundResponse = callAndCatch( + () -> documentSpecificationApiTestClient.deleteDocumentSpecification(null, "invalid-id")); + assertEquals(HttpStatus.NOT_FOUND, notFoundResponse.getStatus(), "Invalid ID should return not found."); + + optionalErrorDetails = notFoundResponse.getBody(ErrorDetails.class); + assertTrue(optionalErrorDetails.isPresent(), "Error details should be provided."); + } + + @Disabled("Prohibited by the framework.") + @Test + @Override + public void deleteDocumentSpecification405() throws Exception { + } + + @Disabled("Impossible status.") + @Test + @Override + public void deleteDocumentSpecification409() throws Exception { + } + + @Override + public void deleteDocumentSpecification500() throws Exception { + } + + @Test + @Override + public void listDocumentSpecification200() throws Exception { + List expectedSpecs = new ArrayList<>(); + + // Create document specifications + for (int i = 0; i < 10; i++) { + DocumentSpecificationCreateVO createVO = new DocumentSpecificationCreateVO(); + createVO.setName("Document " + i); + createVO.setVersion("1.0.0"); + + HttpResponse createResponse = callAndCatch( + () -> documentSpecificationApiTestClient.createDocumentSpecification(null, createVO)); + assertEquals(HttpStatus.CREATED, createResponse.getStatus()); + expectedSpecs.add(createResponse.body()); + } + + // List all + HttpResponse> listResponse = callAndCatch( + () -> documentSpecificationApiTestClient.listDocumentSpecification(null, null, null, null)); + + assertEquals(HttpStatus.OK, listResponse.getStatus(), "The list should be accessible."); + assertEquals(expectedSpecs.size(), listResponse.body().size(), "All document specifications should have been returned."); + + // Test pagination + Integer limit = 5; + HttpResponse> firstPartResponse = callAndCatch( + () -> documentSpecificationApiTestClient.listDocumentSpecification(null, null, 0, limit)); + assertEquals(limit, firstPartResponse.body().size(), "Only the requested number of entries should be returned."); + + HttpResponse> secondPartResponse = callAndCatch( + () -> documentSpecificationApiTestClient.listDocumentSpecification(null, null, limit, limit)); + assertEquals(limit, secondPartResponse.body().size(), "Only the requested number of entries should be returned."); + } + + @Test + @Override + public void listDocumentSpecification400() throws Exception { + HttpResponse> badRequestResponse = callAndCatch( + () -> documentSpecificationApiTestClient.listDocumentSpecification(null, 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( + () -> documentSpecificationApiTestClient.listDocumentSpecification(null, 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 listDocumentSpecification401() throws Exception { + } + + @Disabled("Security is handled externally, thus 401 and 403 cannot happen.") + @Test + @Override + public void listDocumentSpecification403() throws Exception { + } + + @Disabled("Not found is not possible here, will be answered with an empty list instead.") + @Test + @Override + public void listDocumentSpecification404() throws Exception { + } + + @Disabled("Prohibited by the framework.") + @Test + @Override + public void listDocumentSpecification405() throws Exception { + } + + @Disabled("Impossible status.") + @Test + @Override + public void listDocumentSpecification409() throws Exception { + } + + @Override + public void listDocumentSpecification500() throws Exception { + } + + @Test + @Override + public void patchDocumentSpecification200() throws Exception { + DocumentSpecificationCreateVO createVO = new DocumentSpecificationCreateVO(); + createVO.setName("Original Name"); + createVO.setVersion("1.0.0"); + HttpResponse createResponse = callAndCatch( + () -> documentSpecificationApiTestClient.createDocumentSpecification(null, createVO)); + assertEquals(HttpStatus.CREATED, createResponse.getStatus()); + String id = createResponse.body().getId(); + + DocumentSpecificationUpdateVO updateVO = new DocumentSpecificationUpdateVO(); + updateVO.setDescription("Updated description"); + HttpResponse patchResponse = callAndCatch( + () -> documentSpecificationApiTestClient.patchDocumentSpecification(null, id, updateVO)); + + assertEquals(HttpStatus.OK, patchResponse.getStatus(), "Patch should succeed."); + assertNotNull(patchResponse.body()); + assertEquals("Updated description", patchResponse.body().getDescription()); + } + + @Test + @Override + public void patchDocumentSpecification400() throws Exception { + DocumentSpecificationCreateVO createVO = new DocumentSpecificationCreateVO(); + createVO.setName("Document for Patch 400"); + HttpResponse createResponse = callAndCatch( + () -> documentSpecificationApiTestClient.createDocumentSpecification(null, createVO)); + assertEquals(HttpStatus.CREATED, createResponse.getStatus()); + String id = createResponse.body().getId(); + + RelatedPartyVO nonExistentParty = new RelatedPartyVO(); + nonExistentParty.setId("urn:ngsi-ld:organization:non-existent"); + DocumentSpecificationUpdateVO updateVO = new DocumentSpecificationUpdateVO(); + updateVO.setRelatedParty(List.of(nonExistentParty)); + HttpResponse response = callAndCatch( + () -> documentSpecificationApiTestClient.patchDocumentSpecification(null, id, updateVO)); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatus(), + "Patch with a non-existent relatedParty reference should fail with 400."); + } + + @Disabled("Security is handled externally, thus 401 and 403 cannot happen.") + @Test + @Override + public void patchDocumentSpecification401() throws Exception { + } + + @Disabled("Security is handled externally, thus 401 and 403 cannot happen.") + @Test + @Override + public void patchDocumentSpecification403() throws Exception { + } + + @Test + @Override + public void patchDocumentSpecification404() throws Exception { + DocumentSpecificationUpdateVO updateVO = new DocumentSpecificationUpdateVO(); + updateVO.setDescription("Updated description"); + HttpResponse response = callAndCatch( + () -> documentSpecificationApiTestClient.patchDocumentSpecification(null, + "urn:ngsi-ld:document-specification:non-existent", updateVO)); + + assertEquals(HttpStatus.NOT_FOUND, response.getStatus(), "No such document specification should exist."); + } + + @Disabled("Prohibited by the framework.") + @Test + @Override + public void patchDocumentSpecification405() throws Exception { + } + + @Disabled("DocumentSpecification doesn't have 'implicit' entities and id is generated, no conflict possible.") + @Test + @Override + public void patchDocumentSpecification409() throws Exception { + } + + @Override + public void patchDocumentSpecification500() throws Exception { + } + + @Test + @Override + public void retrieveDocumentSpecification200() throws Exception { + // Create a document specification + DocumentSpecificationCreateVO createVO = new DocumentSpecificationCreateVO(); + createVO.setName("Document to Retrieve"); + createVO.setVersion("1.0.0"); + + HttpResponse createResponse = callAndCatch( + () -> documentSpecificationApiTestClient.createDocumentSpecification(null, createVO)); + + assertEquals(HttpStatus.CREATED, createResponse.getStatus(), "The document specification should have been created first."); + String id = createResponse.body().getId(); + + // Retrieve it + HttpResponse retrieveResponse = callAndCatch( + () -> documentSpecificationApiTestClient.retrieveDocumentSpecification(null, id, null)); + + assertEquals(HttpStatus.OK, retrieveResponse.getStatus(), "The retrieval should be ok."); + assertNotNull(retrieveResponse.body()); + assertEquals(id, retrieveResponse.body().getId()); + assertEquals("Document to Retrieve", retrieveResponse.body().getName()); + } + + @Test + public void retrieveDocumentSpecificationWithUrlAttachment() throws Exception { + DocumentSpecificationCreateVO createVO = new DocumentSpecificationCreateVO(); + createVO.setName("Document with URL Attachment"); + createVO.setVersion("1.0.0"); + + AttachmentRefOrValueVO attachment = new AttachmentRefOrValueVO(); + attachment.setName("test-file.txt"); + attachment.setMimeType("text/plain"); + attachment.setUrl(URI.create("https://example.com/test-file.txt")); + createVO.setAttachment(List.of(attachment)); + + HttpResponse createResponse = callAndCatch( + () -> documentSpecificationApiTestClient.createDocumentSpecification(null, createVO)); + assertEquals(HttpStatus.CREATED, createResponse.getStatus()); + String id = createResponse.body().getId(); + + HttpResponse retrieveResponse = callAndCatch( + () -> documentSpecificationApiTestClient.retrieveDocumentSpecification(null, id, null)); + + assertEquals(HttpStatus.OK, retrieveResponse.getStatus()); + assertNotNull(retrieveResponse.body().getAttachment()); + assertFalse(retrieveResponse.body().getAttachment().isEmpty()); + assertEquals("https://example.com/test-file.txt", + retrieveResponse.body().getAttachment().get(0).getUrl().toString(), + "URL attachment should be returned unchanged when no AttachmentService is configured."); + } + + @Disabled("400 cannot happen, only 404") + @Test + @Override + public void retrieveDocumentSpecification400() throws Exception { + } + + @Disabled("Security is handled externally, thus 401 and 403 cannot happen.") + @Test + @Override + public void retrieveDocumentSpecification401() throws Exception { + } + + @Disabled("Security is handled externally, thus 401 and 403 cannot happen.") + @Test + @Override + public void retrieveDocumentSpecification403() throws Exception { + } + + @Test + @Override + public void retrieveDocumentSpecification404() throws Exception { + HttpResponse response = callAndCatch( + () -> documentSpecificationApiTestClient.retrieveDocumentSpecification(null, + "urn:ngsi-ld:document-specification:non-existent", null)); + + assertEquals(HttpStatus.NOT_FOUND, response.getStatus(), "No such document specification 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 retrieveDocumentSpecification405() throws Exception { + } + + @Disabled("Conflict not possible on retrieval") + @Test + @Override + public void retrieveDocumentSpecification409() throws Exception { + } + + @Override + public void retrieveDocumentSpecification500() throws Exception { + } +} diff --git a/document-management/src/test/java/org/fiware/tmforum/documentmanagement/DocumentSpecificationNoS3IT.java b/document-management/src/test/java/org/fiware/tmforum/documentmanagement/DocumentSpecificationNoS3IT.java new file mode 100644 index 00000000..cc531525 --- /dev/null +++ b/document-management/src/test/java/org/fiware/tmforum/documentmanagement/DocumentSpecificationNoS3IT.java @@ -0,0 +1,91 @@ +package org.fiware.tmforum.documentmanagement; + +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 io.micronaut.context.annotation.Property; +import org.fiware.document.api.DocumentSpecificationApiTestClient; +import org.fiware.document.model.AttachmentRefOrValueVO; +import org.fiware.document.model.DocumentSpecificationCreateVO; +import org.fiware.document.model.DocumentSpecificationUpdateVO; +import org.fiware.document.model.DocumentSpecificationVO; +import org.fiware.ngsi.api.EntitiesApiClient; +import org.fiware.tmforum.common.configuration.GeneralProperties; +import org.fiware.tmforum.common.notification.TMForumEventHandler; +import org.fiware.tmforum.common.test.AbstractApiIT; +import org.fiware.tmforum.documentmanagement.domain.DocumentSpecification; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import java.util.Base64; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@MicronautTest(packages = {"org.fiware.tmforum.documentmanagement"}) +@Property(name = "s3.enabled", value = "false") +public class DocumentSpecificationNoS3IT extends AbstractApiIT { + + private final DocumentSpecificationApiTestClient testClient; + + public DocumentSpecificationNoS3IT( + DocumentSpecificationApiTestClient testClient, + EntitiesApiClient entitiesApiClient, + GeneralProperties generalProperties, + com.fasterxml.jackson.databind.ObjectMapper objectMapper) { + super(entitiesApiClient, objectMapper, generalProperties); + this.testClient = testClient; + } + + @Override + protected String getEntityType() { + return DocumentSpecification.TYPE_DOCUMENT_SPECIFICATION; + } + + @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; + } + + @Test + public void createWithInlineContent_rejected() throws Exception { + DocumentSpecificationCreateVO createVO = new DocumentSpecificationCreateVO(); + createVO.setName("Document with Inline Content"); + AttachmentRefOrValueVO attachment = new AttachmentRefOrValueVO(); + attachment.setContent(Base64.getEncoder().encodeToString("Hello World".getBytes())); + createVO.setAttachment(List.of(attachment)); + + HttpResponse response = callAndCatch( + () -> testClient.createDocumentSpecification(null, createVO)); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatus(), + "Inline content must be rejected when no AttachmentService is configured."); + } + + @Test + public void patchWithInlineContent_rejected() throws Exception { + DocumentSpecificationCreateVO createVO = new DocumentSpecificationCreateVO(); + createVO.setName("Document for Patch"); + HttpResponse created = callAndCatch( + () -> testClient.createDocumentSpecification(null, createVO)); + assertEquals(HttpStatus.CREATED, created.getStatus()); + + AttachmentRefOrValueVO attachment = new AttachmentRefOrValueVO(); + attachment.setContent(Base64.getEncoder().encodeToString("data".getBytes())); + DocumentSpecificationUpdateVO updateVO = new DocumentSpecificationUpdateVO(); + updateVO.setAttachment(List.of(attachment)); + + HttpResponse response = callAndCatch( + () -> testClient.patchDocumentSpecification(null, created.body().getId(), updateVO)); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatus(), + "Inline content must be rejected when no AttachmentService is configured."); + } +} diff --git a/document-management/src/test/java/org/fiware/tmforum/documentmanagement/S3AttachmentServiceTest.java b/document-management/src/test/java/org/fiware/tmforum/documentmanagement/S3AttachmentServiceTest.java new file mode 100644 index 00000000..012e2fe8 --- /dev/null +++ b/document-management/src/test/java/org/fiware/tmforum/documentmanagement/S3AttachmentServiceTest.java @@ -0,0 +1,341 @@ +package org.fiware.tmforum.documentmanagement; + +import io.minio.GetPresignedObjectUrlArgs; +import io.minio.MinioClient; +import io.minio.PutObjectArgs; +import io.minio.RemoveObjectArgs; +import org.fiware.tmforum.common.domain.AttachmentRefOrValue; +import org.fiware.tmforum.common.domain.Quantity; +import org.fiware.tmforum.common.domain.TimePeriod; +import org.fiware.tmforum.common.exception.TmForumException; +import org.fiware.tmforum.documentmanagement.s3.S3AttachmentService; +import org.fiware.tmforum.documentmanagement.s3.S3Configuration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.lang.reflect.Field; +import java.net.URL; +import java.time.Instant; +import java.util.Base64; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class S3AttachmentServiceTest { + + private static final String ENDPOINT = "http://localhost:9000"; + private static final String BUCKET = "test-bucket"; + private static final String PRESIGNED_URL = "http://localhost:9000/test-bucket/entity-1/uuid-file.txt?presigned=true"; + + /** + * Subclass that skips the MinIO @PostConstruct initialisation so the service + * can be unit-tested without a running S3 endpoint. + */ + private static class TestableS3AttachmentService extends S3AttachmentService { + TestableS3AttachmentService(S3Configuration config) { + super(config); + } + + @Override + public void init() { + // skip MinIO connection for unit tests + } + } + + @Mock + private MinioClient s3Client; + + private S3Configuration config; + private S3AttachmentService service; + + @BeforeEach + void setUp() throws Exception { + config = new S3Configuration(); + config.setEndpoint(ENDPOINT); + config.setAccessKey("minioadmin"); + config.setSecretKey("minioadmin"); + config.setBucket(BUCKET); + config.setMaxContentSize(1024 * 1024); + + service = new TestableS3AttachmentService(config); + + Field minioClientField = S3AttachmentService.class.getDeclaredField("s3Client"); + minioClientField.setAccessible(true); + minioClientField.set(service, s3Client); + } + + // --- validateAttachmentContent --- + + @Test + void validateAttachmentContent_nullContent_doesNotThrow() { + AttachmentRefOrValue attachment = new AttachmentRefOrValue(); + attachment.setContent(null); + assertDoesNotThrow(() -> service.validateAttachmentContent(attachment), + "null content should be accepted without validation"); + } + + @Test + void validateAttachmentContent_emptyContent_doesNotThrow() { + AttachmentRefOrValue attachment = new AttachmentRefOrValue(); + attachment.setContent(""); + assertDoesNotThrow(() -> service.validateAttachmentContent(attachment), + "empty content should be accepted without validation"); + } + + @Test + void validateAttachmentContent_validBase64WithinLimit_doesNotThrow() { + AttachmentRefOrValue attachment = new AttachmentRefOrValue(); + attachment.setContent(Base64.getEncoder().encodeToString("Hello World".getBytes())); + assertDoesNotThrow(() -> service.validateAttachmentContent(attachment), + "valid base64 content within size limit should pass validation"); + } + + @Test + void validateAttachmentContent_invalidBase64_throwsTmForumException() { + AttachmentRefOrValue attachment = new AttachmentRefOrValue(); + attachment.setContent("not-valid-base64!!!"); + assertThrows(TmForumException.class, () -> service.validateAttachmentContent(attachment), + "invalid base64 content should throw TmForumException"); + } + + @Test + void validateAttachmentContent_contentExceedsMaxSize_throwsTmForumException() { + config.setMaxContentSize(4); + AttachmentRefOrValue attachment = new AttachmentRefOrValue(); + attachment.setContent(Base64.getEncoder().encodeToString(new byte[10])); + assertThrows(TmForumException.class, () -> service.validateAttachmentContent(attachment), + "content exceeding max size should throw TmForumException"); + } + + // --- offloadAttachments --- + + @Test + void offloadAttachments_nullList_returnsNull() { + assertNull(service.offloadAttachments(null, "entity-1").block(), + "null attachment list should return null Mono"); + } + + @Test + void offloadAttachments_emptyList_returnsEmptyList() { + List result = service.offloadAttachments(List.of(), "entity-1").block(); + assertNotNull(result, "empty list should return an empty list, not null"); + assertTrue(result.isEmpty(), "result should be empty"); + } + + @Test + void offloadAttachments_nullContent_attachmentUnchanged() { + AttachmentRefOrValue attachment = new AttachmentRefOrValue(); + attachment.setContent(null); + + List result = service.offloadAttachments(List.of(attachment), "entity-1").block(); + + assertNotNull(result, "result should not be null"); + assertNull(result.get(0).getContent(), "content should remain null"); + assertNull(result.get(0).getUrl(), "url should remain null when no content to offload"); + verifyNoInteractions(s3Client); + } + + @Test + void offloadAttachments_withContent_uploadsToS3ClearsContentAndSetsUrl() throws Exception { + AttachmentRefOrValue attachment = new AttachmentRefOrValue(); + attachment.setName("file.txt"); + attachment.setMimeType("text/plain"); + attachment.setContent(Base64.getEncoder().encodeToString("file content".getBytes())); + + List result = service.offloadAttachments(List.of(attachment), "entity-1").block(); + + assertNotNull(result, "result should not be null"); + AttachmentRefOrValue processed = result.get(0); + assertNull(processed.getContent(), "inline content should be cleared after offload"); + assertNotNull(processed.getUrl(), "url should be set to the internal S3 path"); + assertTrue(processed.getUrl().toString().startsWith(ENDPOINT + "/" + BUCKET + "/"), + "url should be prefixed with endpoint/bucket/"); + verify(s3Client).putObject(any(PutObjectArgs.class)); + } + + @Test + void offloadAttachments_alreadyManagedUrl_skipsUpload() throws Exception { + URL managedUrl = new URL(ENDPOINT + "/" + BUCKET + "/entity-1/existing-file.txt"); + AttachmentRefOrValue attachment = new AttachmentRefOrValue(); + attachment.setUrl(managedUrl); + attachment.setContent(Base64.getEncoder().encodeToString("data".getBytes())); + + List result = service.offloadAttachments(List.of(attachment), "entity-1").block(); + + assertNotNull(result, "result should not be null"); + assertEquals(managedUrl, result.get(0).getUrl(), "managed url should remain unchanged"); + verifyNoInteractions(s3Client); + } + + // --- resolveAttachments --- + + @Test + void resolveAttachments_nullList_returnsNull() { + assertNull(service.resolveAttachments(null).block(), + "null attachment list should return null Mono"); + } + + @Test + void resolveAttachments_nullUrl_attachmentUnchanged() { + AttachmentRefOrValue attachment = new AttachmentRefOrValue(); + attachment.setUrl(null); + + List result = service.resolveAttachments(List.of(attachment)).block(); + + assertNotNull(result, "result should not be null"); + assertNull(result.get(0).getUrl(), "url should remain null when not managed"); + verifyNoInteractions(s3Client); + } + + @Test + void resolveAttachments_nonManagedUrl_attachmentUnchanged() throws Exception { + URL externalUrl = new URL("https://example.com/file.pdf"); + AttachmentRefOrValue attachment = new AttachmentRefOrValue(); + attachment.setUrl(externalUrl); + + List result = service.resolveAttachments(List.of(attachment)).block(); + + assertNotNull(result, "result should not be null"); + assertEquals(externalUrl, result.get(0).getUrl(), "non-managed url should be returned unchanged"); + verifyNoInteractions(s3Client); + } + + @Test + void resolveAttachments_managedUrl_replacesWithPresignedUrl() throws Exception { + URL managedUrl = new URL(ENDPOINT + "/" + BUCKET + "/entity-1/uuid-file.txt"); + AttachmentRefOrValue attachment = new AttachmentRefOrValue(); + attachment.setUrl(managedUrl); + + when(s3Client.getPresignedObjectUrl(any(GetPresignedObjectUrlArgs.class))) + .thenReturn(PRESIGNED_URL); + + List result = service.resolveAttachments(List.of(attachment)).block(); + + assertNotNull(result, "result should not be null"); + assertEquals(new URL(PRESIGNED_URL), result.get(0).getUrl(), "managed url should be replaced with presigned url"); + verify(s3Client).getPresignedObjectUrl(any(GetPresignedObjectUrlArgs.class)); + } + + // --- deleteAttachments --- + + @Test + void deleteAttachments_nullList_noS3Calls() { + service.deleteAttachments(null).block(); + verifyNoInteractions(s3Client); + } + + @Test + void deleteAttachments_nonManagedUrl_noS3Calls() throws Exception { + AttachmentRefOrValue attachment = new AttachmentRefOrValue(); + attachment.setUrl(new URL("https://example.com/file.pdf")); + + service.deleteAttachments(List.of(attachment)).block(); + + verifyNoInteractions(s3Client); + } + + @Test + void deleteAttachments_managedUrl_deletesFromS3() throws Exception { + AttachmentRefOrValue attachment = new AttachmentRefOrValue(); + attachment.setUrl(new URL(ENDPOINT + "/" + BUCKET + "/entity-1/uuid-file.txt")); + + service.deleteAttachments(List.of(attachment)).block(); + + verify(s3Client).removeObject(any(RemoveObjectArgs.class)); + } + + @Test + void deleteAttachments_s3DeleteFails_doesNotThrow() throws Exception { + AttachmentRefOrValue attachment = new AttachmentRefOrValue(); + attachment.setUrl(new URL(ENDPOINT + "/" + BUCKET + "/entity-1/uuid-file.txt")); + + doThrow(new RuntimeException("S3 unavailable")).when(s3Client).removeObject(any(RemoveObjectArgs.class)); + + assertDoesNotThrow(() -> service.deleteAttachments(List.of(attachment)).block(), + "S3 delete failure should be swallowed and not propagate to the caller"); + } + + // --- deleteOrphanedAttachments --- + + @Test + void deleteOrphanedAttachments_nullExisting_noS3Calls() { + service.deleteOrphanedAttachments(null, List.of()).block(); + verifyNoInteractions(s3Client); + } + + @Test + void deleteOrphanedAttachments_emptyExisting_noS3Calls() { + service.deleteOrphanedAttachments(List.of(), List.of()).block(); + verifyNoInteractions(s3Client); + } + + @Test + void deleteOrphanedAttachments_managedUrlNotInUpdated_deletesFromS3() throws Exception { + AttachmentRefOrValue existing = new AttachmentRefOrValue(); + existing.setUrl(new URL(ENDPOINT + "/" + BUCKET + "/entity-1/old-file.txt")); + + service.deleteOrphanedAttachments(List.of(existing), List.of()).block(); + + verify(s3Client).removeObject(any(RemoveObjectArgs.class)); + } + + @Test + void deleteOrphanedAttachments_managedUrlStillInUpdated_doesNotDelete() throws Exception { + URL managedUrl = new URL(ENDPOINT + "/" + BUCKET + "/entity-1/kept-file.txt"); + AttachmentRefOrValue existing = new AttachmentRefOrValue(); + existing.setUrl(managedUrl); + AttachmentRefOrValue updated = new AttachmentRefOrValue(); + updated.setUrl(managedUrl); + + service.deleteOrphanedAttachments(List.of(existing), List.of(updated)).block(); + + verifyNoInteractions(s3Client); + } + + @Test + void deleteOrphanedAttachments_nonManagedUrl_doesNotDelete() throws Exception { + AttachmentRefOrValue existing = new AttachmentRefOrValue(); + existing.setUrl(new URL("https://example.com/file.pdf")); + + service.deleteOrphanedAttachments(List.of(existing), List.of()).block(); + + verifyNoInteractions(s3Client); + } + + // --- deep copy --- + + @Test + void offloadAttachments_deepCopiesValidForAndSize() throws Exception { + TimePeriod validFor = new TimePeriod(); + validFor.setStartDateTime(Instant.parse("2024-01-01T00:00:00Z")); + validFor.setEndDateTime(Instant.parse("2025-01-01T00:00:00Z")); + Quantity size = new Quantity(); + size.setAmount(1.5f); + size.setUnits("MB"); + + AttachmentRefOrValue attachment = new AttachmentRefOrValue(); + attachment.setContent(Base64.getEncoder().encodeToString("data".getBytes())); + attachment.setName("file.txt"); + attachment.setValidFor(validFor); + attachment.setSize(size); + + List result = service.offloadAttachments(List.of(attachment), "entity-1").block(); + + assertNotNull(result, "result should not be null"); + AttachmentRefOrValue copy = result.get(0); + + assertNotSame(validFor, copy.getValidFor(), "validFor should be a new instance, not the same reference"); + assertEquals(validFor.getStartDateTime(), copy.getValidFor().getStartDateTime(), "validFor startDateTime should be preserved"); + assertEquals(validFor.getEndDateTime(), copy.getValidFor().getEndDateTime(), "validFor endDateTime should be preserved"); + + assertNotSame(size, copy.getSize(), "size should be a new instance, not the same reference"); + assertEquals(size.getAmount(), copy.getSize().getAmount(), "size amount should be preserved"); + assertEquals(size.getUnits(), copy.getSize().getUnits(), "size units should be preserved"); + } +} diff --git a/k3s/minio/minio.yaml b/k3s/minio/minio.yaml new file mode 100644 index 00000000..9e89ddbd --- /dev/null +++ b/k3s/minio/minio.yaml @@ -0,0 +1,90 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: minio + labels: + app.kubernetes.io/name: minio + app.kubernetes.io/instance: minio +spec: + type: LoadBalancer + ports: + - port: 9000 + targetPort: 9000 + protocol: TCP + name: api + - port: 9001 + targetPort: 9001 + protocol: TCP + name: console + selector: + app.kubernetes.io/name: minio + app.kubernetes.io/instance: minio +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: minio + labels: + app.kubernetes.io/name: minio + app.kubernetes.io/instance: minio +spec: + replicas: 1 + revisionHistoryLimit: 3 + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app.kubernetes.io/name: minio + app.kubernetes.io/instance: minio + template: + metadata: + labels: + app.kubernetes.io/name: minio + app.kubernetes.io/instance: minio + spec: + serviceAccountName: default + containers: + - name: minio + image: "minio/minio:latest" + imagePullPolicy: IfNotPresent + args: + - server + - /data + - --console-address + - ":9001" + ports: + - name: api + containerPort: 9000 + protocol: TCP + - name: console + containerPort: 9001 + protocol: TCP + env: + - name: MINIO_ROOT_USER + value: "minioadmin" + - name: MINIO_ROOT_PASSWORD + value: "minioadmin" + livenessProbe: + httpGet: + path: /minio/health/live + port: 9000 + initialDelaySeconds: 10 + periodSeconds: 10 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /minio/health/ready + port: 9000 + initialDelaySeconds: 10 + periodSeconds: 10 + failureThreshold: 3 + volumeMounts: + - name: data + mountPath: /data + volumes: + - name: data + emptyDir: {} diff --git a/pom.xml b/pom.xml index 35ff0c49..ad304f0b 100644 --- a/pom.xml +++ b/pom.xml @@ -56,6 +56,7 @@ party-role data-migrator all-in-one + document-management @@ -128,12 +129,13 @@ 3.2.0 3.0.1u2 + 8.5.7 myToken ${project.parent.build.directory}/jacoco/${artifactId} ${jacoco.reportFolder}/test.exec ${project.parent.basedir}/ ${project.build.directory}/site/spotbugs - + orion-ld in-memory @@ -222,6 +224,11 @@ json-schema-validator 1.5.4
+ + io.minio + minio + ${version.io.minio.minio} + @@ -630,6 +637,8 @@ 1026:1026 6379:6379 3000:3000 + 9000:9000 + 9001:9001 @@ -643,6 +652,16 @@ ${project.parent.basedir}/k3s/schema-provider + + apply-minio + pre-integration-test + + apply + + + ${project.parent.basedir}/k3s/minio + + clean post-integration-test @@ -680,6 +699,8 @@ 1026:1026 6379:6379 3000:3000 + 9000:9000 + 9001:9001 @@ -693,6 +714,15 @@ ${project.parent.basedir}/k3s/schema-provider + + apply-minio + + apply + + + ${project.parent.basedir}/k3s/minio + +