diff --git a/NEWS.md b/NEWS.md index 941036d78..0cd9808dc 100644 --- a/NEWS.md +++ b/NEWS.md @@ -20,6 +20,7 @@ ### Features * Add module descriptor validator plugin and fix the permission names ([MODINVSTOR-1247](https://folio-org.atlassian.net/browse/MODINVSTOR-1247)) +* Implement publication period migration on big dataset, create new InstanceWithoutPubPeriod schema for request/response API ([MODINVSTOR-1271](https://folio-org.atlassian.net/browse/MODINVSTOR-1271)) ### Tech Dept * Upgrade localstack from 0.11.3 to s3-latest (=3.8.0) ([MODINVSTOR-1272](https://folio-org.atlassian.net/browse/MODINVSTOR-1272)) diff --git a/pom.xml b/pom.xml index 8916b130d..1ec2a210b 100644 --- a/pom.xml +++ b/pom.xml @@ -23,6 +23,7 @@ 3.17.0 2.24.1 2.2.0-SNAPSHOT + 2.11.0 6.0.3 5.11.2 @@ -138,6 +139,11 @@ folio-s3-client ${folio-s3-client.version} + + com.google.code.gson + gson + ${google-code-gson.version} + diff --git a/ramls/dereferenceditem-without-pub-period.json b/ramls/dereferenceditem-without-pub-period.json new file mode 100644 index 000000000..58b6789ac --- /dev/null +++ b/ramls/dereferenceditem-without-pub-period.json @@ -0,0 +1,416 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "An item record with related record data", + "type": "object", + "javaType": "org.folio.rest.jaxrs.model.DereferencedItemWithoutPubPeriod", + "properties": { + "id": { + "type": "string", + "description": "Unique ID of the item record" + }, + "instanceRecord":{ + "type": "object", + "description" : "Parent instance record without the publicationPeriod.", + "$ref": "instance-without-pub-period.json" + }, + "_version": { + "type": "integer", + "description": "Record version for optimistic locking" + }, + "hrid": { + "type": "string", + "description": "The human readable ID, also called eye readable ID. A system-assigned sequential alternate ID" + }, + "holdingsRecord": { + "type": "object", + "description": "Holdings record the item is a member of.", + "$ref": "holdings-storage/holdingsRecord.json" + }, + "formerIds": { + "type": "array", + "description": "Previous identifiers assigned to the item", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "discoverySuppress": { + "type": "boolean", + "description": "Records the fact that the record should not be displayed in a discovery system" + }, + "accessionNumber": { + "type": "string", + "description": "Also called inventar number" + }, + "barcode": { + "type": "string", + "description": "Unique inventory control number for physical resources, used largely for circulation purposes" + }, + "effectiveShelvingOrder": { + "type": "string", + "description": "A system generated normalization of the call number that allows for call number sorting in reports and search results", + "readonly": true + }, + "itemLevelCallNumber": { + "type": "string", + "description": "Call Number is an identifier assigned to an item, usually printed on a label attached to the item. The call number is used to determine the items physical position in a shelving sequence, e.g. K1 .M44. The Item level call number, is the call number on item level." + }, + "itemLevelCallNumberPrefix": { + "type": "string", + "description": "Prefix of the call number on the item level." + }, + "itemLevelCallNumberSuffix": { + "type": "string", + "description": "Suffix of the call number on the item level." + }, + "itemLevelCallNumberTypeId": { + "type": "string", + "description": "Identifies the source of the call number, e.g., LCC, Dewey, NLM, etc." + }, + "effectiveCallNumberComponents": { + "type": "object", + "description": "Elements of a full call number generated from the item or holding", + "properties": { + "callNumber": { + "type": "string", + "description": "Effective Call Number is an identifier assigned to an item or its holding and associated with the item.", + "readonly": true + }, + "prefix": { + "type": "string", + "description": "Effective Call Number Prefix is the prefix of the identifier assigned to an item or its holding and associated with the item.", + "readonly": true + }, + "suffix": { + "type": "string", + "description": "Effective Call Number Suffix is the suffix of the identifier assigned to an item or its holding and associated with the item.", + "readonly": true + }, + "typeId": { + "type": "string", + "description": "Effective Call Number Type Id is the call number type id of the item, if available, otherwise that of the holding.", + "$ref": "uuid.json", + "readonly": true + } + }, + "additionalProperties": false + }, + "volume": { + "type": "string", + "description": "Volume is intended for monographs when a multipart monograph (e.g. a biography of George Bernard Shaw in three volumes)." + }, + "enumeration": { + "type": "string", + "description": "Enumeration is the descriptive information for the numbering scheme of a serial." + }, + "chronology": { + "type": "string", + "description": "Chronology is the descriptive information for the dating scheme of a serial." + }, + "yearCaption": { + "type": "array", + "description": "In multipart monographs, a caption is a character(s) used to label a level of chronology, e.g., year 1985.", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "itemIdentifier": { + "type": "string", + "description": "Item identifier number, e.g. imported from the union catalogue (read only)." + }, + "copyNumber": { + "type": "string", + "description": "Copy number is the piece identifier. The copy number reflects if the library has a copy of a single-volume monograph; one copy of a multi-volume, (e.g. Copy 1, or C.7.)" + }, + "numberOfPieces": { + "type": "string", + "description": "Number of pieces. Used when an item is checked out or returned to verify that all parts are present (e.g. 7 CDs in a set)." + }, + "descriptionOfPieces": { + "description": "Description of item pieces.", + "type": "string" + }, + "numberOfMissingPieces": { + "type": "string", + "description": "Number of missing pieces." + }, + "missingPieces": { + "type": "string", + "description": "Description of the missing pieces. " + }, + "missingPiecesDate": { + "type": "string", + "description": "Date when the piece(s) went missing." + }, + "itemDamagedStatusId": { + "description": "Item dame status id identifier.", + "type": "string" + }, + "itemDamagedStatusDate": { + "description": "Date and time when the item was damaged.", + "type": "string" + }, + "administrativeNotes":{ + "type": "array", + "description": "Administrative notes", + "minItems": 0, + "items": { + "type": "string" + } + }, + "notes": { + "type": "array", + "description": "Notes about action, copy, binding etc.", + "items": { + "type": "object", + "properties": { + "itemNoteTypeId": { + "type": "string", + "description": "ID of the type of note" + }, + "itemNoteType": { + "description": "Type of item's note", + "type": "object", + "folio:$ref": "itemnotetype.json", + "javaType": "org.folio.rest.jaxrs.model.itemNoteTypeVirtual", + "readonly": true, + "folio:isVirtual": true, + "folio:linkBase": "item-note-types", + "folio:linkFromField": "itemNoteTypeId", + "folio:linkToField": "id", + "folio:includedElement": "itemNoteTypes.0" + }, + "note": { + "type": "string", + "description": "Text content of the note" + }, + "staffOnly": { + "type": "boolean", + "description": "If true, determines that the note should not be visible for others than staff", + "default": false + } + } + } + }, + "circulationNotes": { + "type": "array", + "description": "Notes to be displayed in circulation processes", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The id of the circulation note" + }, + "noteType": { + "type": "string", + "description": "Type of circulation process that the note applies to", + "enum": ["Check in", "Check out"] + }, + "note": { + "type": "string", + "description": "Text to display" + }, + "source": { + "type": "object", + "description": "The user who added/updated the note. The property is generated by the server and needed to support sorting. Points to /users/{id} resource.", + "properties": { + "id": { + "type": "string", + "description": "The id of the user who added/updated the note. The user information is generated by the server and needed to support sorting. Points to /users/{id} resource." + }, + "personal": { + "type": "object", + "description": "Personal information about the user", + "properties": { + "lastName": { + "description": "The user's last name", + "type": "string" + }, + "firstName": { + "description": "The user's first name", + "type": "string" + } + } + } + } + }, + "date": { + "type": "string", + "description": "Date and time the record is added/updated. The property is generated by the server and needed to support sorting." + }, + "staffOnly": { + "type": "boolean", + "description": "Flag to restrict display of this note", + "default": false + } + }, + "additionalProperties": false + } + }, + "status": { + "description": "The status of the item", + "type": "object", + "properties": { + "name": { + "description": "Name of the status e.g. Available, Checked out, In transit", + "type": "string", + "enum": [ + "Aged to lost", + "Available", + "Awaiting pickup", + "Awaiting delivery", + "Checked out", + "Claimed returned", + "Declared lost", + "In process", + "In process (non-requestable)", + "In transit", + "Intellectual item", + "Long missing", + "Lost and paid", + "Missing", + "On order", + "Paged", + "Restricted", + "Order closed", + "Unavailable", + "Unknown", + "Withdrawn" + ] + }, + "date": { + "description": "Date and time when the status was last changed", + "type": "string", + "format": "date-time", + "readonly": true + } + }, + "required": ["name"], + "additionalProperties": false + }, + "materialType": { + "description": "Item's material type", + "type": "object", + "$ref": "materialtype.json" + }, + "permanentLoanType": { + "type": "object", + "description": "The permanent loan type, is the default loan type for a given item. Loan types are tenant-defined.", + "$ref":"loantype.json" + }, + "temporaryLoanType": { + "type": "object", + "description": "Temporary loan type, is the temporary loan type for a given item.", + "$ref":"loantype.json" + }, + "permanentLocation": { + "type": "object", + "description": "Permanent item location is the default location, shelving location, or holding which is a physical place where items are stored, or an Online location.", + "$ref": "locations/location.json" + }, + "temporaryLocation": { + "type": "object", + "description": "Temporary item location is the temporarily location, shelving location, or holding which is a physical place where items are stored, or an Online location.", + "$ref": "locations/location.json" + }, + "effectiveLocation": { + "type": "object", + "description": "Read only current home location for the item.", + "$ref": "locations/location.json", + "readonly": true + }, + "electronicAccess": { + "type": "array", + "description": "References for accessing the item by URL.", + "items": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "description": "uniform resource identifier (URI) is a string of characters designed for unambiguous identification of resources" + }, + "linkText": { + "type": "string", + "description": "the value of the MARC tag field 856 2nd indicator, where the values are: no information provided, resource, version of resource, related resource, no display constant generated" + }, + "materialsSpecification": { + "type": "string", + "description": "materials specified is used to specify to what portion or aspect of the resource the electronic location and access information applies (e.g. a portion or subset of the item is electronic, or a related electronic resource is being linked to the record)" + }, + "publicNote": { + "type": "string", + "description": "URL public note to be displayed in the discovery" + }, + "relationshipId": { + "type": "string", + "description": "relationship between the electronic resource at the location identified and the item described in the record as a whole" + } + }, + "additionalProperties": false, + "required": [ + "uri" + ] + } + }, + "inTransitDestinationServicePointId": { + "description": "Service point an item is intended to be transited to (should only be present when in transit)", + "type": "string", + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, + "statisticalCodeIds": { + "type": "array", + "description": "List of statistical code IDs", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "purchaseOrderLineIdentifier": { + "type": "string", + "description": "ID referencing a remote purchase order object related to this item" + }, + "tags": { + "description": "arbitrary tags associated with this item", + "id": "tags", + "type": "object", + "$ref": "raml-util/schemas/tags.schema" + }, + "metadata": { + "type": "object", + "$ref": "raml-util/schemas/metadata.schema", + "readonly": true + }, + "lastCheckIn": { + "type": "object", + "additionalProperties": false, + "description": "Information about when an item was last scanned in the Inventory app.", + "properties": { + "dateTime": { + "type": "string", + "description": "Date and time of the last check in of the item.", + "format": "date-time" + }, + "servicePointId": { + "type": "string", + "description": "Service point ID being used by a staff member when item was scanned in Check in app.", + "$ref": "uuid.json" + }, + "staffMemberId": { + "type": "string", + "description": "ID a staff member who scanned the item", + "$ref": "uuid.json" + } + } + } + }, + "additionalProperties": false, + "required": [ + "materialType", + "instanceRecord", + "permanentLoanType", + "holdingsRecord", + "status" + ] +} diff --git a/ramls/dereferenceditems-without-pub-period.json b/ramls/dereferenceditems-without-pub-period.json new file mode 100644 index 000000000..bf5365257 --- /dev/null +++ b/ramls/dereferenceditems-without-pub-period.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "A collection of dereferenced item records", + "type": "object", + "properties": { + "dereferencedItems": { + "description": "List of dereferenced item records", + "id": "items", + "type": "array", + "items": { + "type": "object", + "$ref": "dereferenceditem-without-pub-period.json" + } + }, + "totalRecords": { + "description": "Estimated or exact total number of records", + "type": "integer" + } + }, + "required": [ + "dereferencedItems", + "totalRecords" + ] +} diff --git a/ramls/instance-set.json b/ramls/instance-set.json index c60c480a3..7d375ab2e 100644 --- a/ramls/instance-set.json +++ b/ramls/instance-set.json @@ -8,8 +8,8 @@ "$ref": "uuid.json" }, "instance": { - "description": "Instance record", - "$ref": "instance.json" + "description": "Instance record without the publicationPeriod", + "$ref": "instance-without-pub-period.json" }, "holdingsRecords": { "type": "array", diff --git a/ramls/instance-storage-batch.raml b/ramls/instance-storage-batch.raml index b28f3fc2a..d06642820 100644 --- a/ramls/instance-storage-batch.raml +++ b/ramls/instance-storage-batch.raml @@ -11,6 +11,7 @@ documentation: types: errors: !include raml-util/schemas/errors.schema instances: !include instances.json + instancesWithoutPubPeriod: !include instances-without-pub-period.json instancesBatchResponse: !include instances-batch-response.json /instance-storage/batch/instances: @@ -19,7 +20,7 @@ types: description: "Create collection of instances in one request - deprecated, use /instance-storage/sync instead" body: application/json: - type: instances + type: instancesWithoutPubPeriod responses: 201: description: "At least one Instance from the list was created" diff --git a/ramls/instance-storage.raml b/ramls/instance-storage.raml index 373a5785b..42e16acfc 100644 --- a/ramls/instance-storage.raml +++ b/ramls/instance-storage.raml @@ -9,6 +9,8 @@ documentation: content: Storage for instances in the inventory types: + instanceWithoutPubPeriod: !include instance-without-pub-period.json + instancesWithoutPubPeriod: !include instances-without-pub-period.json instance: !include instance.json instances: !include instances.json marcJson: !include marc.json @@ -61,8 +63,8 @@ resourceTypes: type: collection: exampleCollection: !include examples/instances_get.json - schemaCollection: instances - schemaItem: instance + schemaCollection: instancesWithoutPubPeriod + schemaItem: instanceWithoutPubPeriod exampleItem: !include examples/instance_get.json get: is: [pageable, @@ -90,13 +92,13 @@ resourceTypes: type: collection-item: exampleItem: !include examples/instance_get.json - schema: instance + schema: instanceWithoutPubPeriod get: responses: 200: body: application/json: - type: instance + type: instanceWithoutPubPeriod description: | Get Instance by InstanceId Instances are stored and accessed by a hash of key properties. The rules which govern diff --git a/ramls/instance-without-pub-period.json b/ramls/instance-without-pub-period.json new file mode 100644 index 000000000..b6542c778 --- /dev/null +++ b/ramls/instance-without-pub-period.json @@ -0,0 +1,508 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "An instance record without publicationPeriod", + "type": "object", + "javaType": "org.folio.rest.jaxrs.model.InstanceWithoutPubPeriod", + "properties": { + "id": { + "type": "string", + "description": "The unique ID of the instance record; a UUID", + "$ref": "uuid.json" + }, + "_version": { + "type": "integer", + "description": "Record version for optimistic locking" + }, + "hrid": { + "type": "string", + "description": "The human readable ID, also called eye readable ID. A system-assigned sequential ID which maps to the Instance ID" + }, + "matchKey": { + "type": "string", + "description" : "A unique instance identifier matching a client-side bibliographic record identification scheme, in particular for a scenario where multiple separate catalogs with no shared record identifiers contribute to the same Instance in Inventory. A match key is typically generated from select, normalized pieces of metadata in bibliographic records" + }, + "source": { + "type": "string", + "description": "The metadata source and its format of the underlying record to the instance record. (e.g. FOLIO if it's a record created in Inventory; MARC if it's a MARC record created in MARCcat or EPKB if it's a record coming from eHoldings; CONSORTIUM-MARC or CONSORTIUM-FOLIO for sharing Instances)." + }, + "title": { + "type": "string", + "description": "The primary title (or label) associated with the resource" + }, + "indexTitle": { + "type": "string", + "description": "Title normalized for browsing and searching; based on the title with articles removed" + }, + "alternativeTitles": { + "type": "array", + "description": "List of alternative titles for the resource (e.g. original language version title of a movie)", + "items": { + "type": "object", + "properties": { + "alternativeTitleTypeId": { + "type": "string", + "description": "UUID for an alternative title qualifier", + "$ref": "uuid.json" + }, + "alternativeTitle": { + "type": "string", + "description": "An alternative title for the resource" + }, + "authorityId": { + "type": "string", + "description": "UUID of authority record that controls an alternative title", + "$ref": "uuid.json" + } + } + }, + "uniqueItems": true + }, + "editions": { + "type": "array", + "description": "The edition statement, imprint and other publication source information", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "series": { + "type": "array", + "description": "List of series titles associated with the resource (e.g. Harry Potter)", + "items": { + "type": "object", + "properties": { + "value": { + "type": "string", + "description": "Series title value" + }, + "authorityId": { + "type": "string", + "description": "UUID of authority record that controls an series title", + "$ref": "uuid.json" + } + }, + "additionalProperties": false, + "required": [ + "value" + ] + }, + "uniqueItems": true + }, + "identifiers": { + "type": "array", + "description": "An extensible set of name-value pairs of identifiers associated with the resource", + "minItems": 0, + "items": { + "type": "object", + "properties": { + "value": { + "type": "string", + "description": "Resource identifier value" + }, + "identifierTypeId": { + "type": "string", + "description": "UUID of resource identifier type (e.g. ISBN, ISSN, LCCN, CODEN, Locally defined identifiers)", + "$ref": "uuid.json" + }, + "identifierTypeObject": { + "type": "object", + "description": "Information about identifier type, looked up from identifierTypeId", + "folio:$ref": "illpolicy.json", + "readonly": true, + "folio:isVirtual": true, + "folio:linkBase": "identifier-types", + "folio:linkFromField": "identifierTypeId", + "folio:linkToField": "id", + "folio:includedElement": "identifierTypes.0" + } + }, + "additionalProperties": false, + "required": [ + "value", + "identifierTypeId" + ] + } + }, + "contributors": { + "type": "array", + "description": "List of contributors", + "minItems": 0, + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Personal name, corporate name, meeting name" + }, + "contributorTypeId": { + "type": "string", + "description": "UUID for the contributor type term defined in controlled vocabulary", + "$ref": "uuid.json" + }, + "contributorTypeText": { + "type": "string", + "description": "Free text element for adding contributor type terms other that defined by the MARC code list for relators" + }, + "contributorNameTypeId": { + "type": "string", + "description": "UUID of contributor name type term defined by the MARC code list for relators", + "$ref": "uuid.json" + }, + "authorityId": { + "type": "string", + "description": "UUID of authority record that controls the contributor", + "$ref": "uuid.json" + }, + "contributorNameType": { + "type": "object", + "description": "Dereferenced contributor-name type", + "javaType": "org.folio.rest.jaxrs.model.contributorNameTypeVirtual", + "folio:$ref": "contributornametype.json", + "readonly": true, + "folio:isVirtual": true, + "folio:linkBase": "contributor-name-types", + "folio:linkFromField": "contributorNameTypeId", + "folio:linkToField": "id", + "folio:includedElement": "contributorNameTypes.0" + }, + "primary": { + "type": "boolean", + "description": "Whether this is the primary contributor" + } + }, + "additionalProperties": false, + "required": [ + "name", + "contributorNameTypeId" + ] + } + }, + "subjects": { + "type": "array", + "description": "List of subject headings", + "items": { + "type": "object", + "properties": { + "value": { + "type": "string", + "description": "Subject heading value" + }, + "authorityId": { + "type": "string", + "description": "UUID of authority record that controls a subject heading", + "$ref": "uuid.json" + }, + "sourceId": { + "type": "string", + "description": "UUID of subject source", + "$ref": "uuid.json" + }, + "typeId": { + "type": "string", + "description": "UUID of subject type", + "$ref": "uuid.json" + } + }, + "additionalProperties": false, + "required": [ + "value" + ] + }, + "uniqueItems": true + }, + "classifications": { + "type": "array", + "description": "List of classifications", + "minItems": 0, + "items": { + "type": "object", + "properties": { + "classificationNumber": { + "type": "string", + "description": "Classification (e.g. classification scheme, classification schedule)" + }, + "classificationTypeId": { + "type": "string", + "description": "UUID of classification schema (e.g. LC, Canadian Classification, NLM, National Agricultural Library, UDC, and Dewey)", + "$ref": "uuid.json" + }, + "classificationType": { + "type": "object", + "description": "Dereferenced classification schema", + "javaType": "org.folio.rest.jaxrs.model.classificationTypeVirtual", + "folio:$ref": "classificationtype.json", + "readonly": true, + "folio:isVirtual": true, + "folio:linkBase": "classification-types", + "folio:linkFromField": "classificationTypeId", + "folio:linkToField": "id", + "folio:includedElement": "classificationTypes.0" + } + }, + "additionalProperties": false, + "required": [ + "classificationNumber", + "classificationTypeId" + ] + } + }, + "publication": { + "type": "array", + "description": "List of publication items", + "items": { + "type": "object", + "properties": { + "publisher": { + "type": "string", + "description": "Name of publisher, distributor, etc." + }, + "place": { + "type": "string", + "description": "Place of publication, distribution, etc." + }, + "dateOfPublication": { + "type": "string", + "description": "Date (year YYYY) of publication, distribution, etc." + }, + "role": { + "type": "string", + "description": "The role of the publisher, distributor, etc." + } + } + } + }, + "publicationFrequency": { + "type": "array", + "description": "List of intervals at which a serial appears (e.g. daily, weekly, monthly, quarterly, etc.)", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "publicationRange": { + "type": "array", + "description": "The range of sequential designation/chronology of publication, or date range", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "electronicAccess": { + "type": "array", + "description": "List of electronic access items", + "items": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "description": "uniform resource identifier (URI) is a string of characters designed for unambiguous identification of resources" + }, + "linkText": { + "type": "string", + "description": "The value of the MARC tag field 856 2nd indicator, where the values are: no information provided, resource, version of resource, related resource, no display constant generated" + }, + "materialsSpecification": { + "type": "string", + "description": "Materials specified is used to specify to what portion or aspect of the resource the electronic location and access information applies (e.g. a portion or subset of the item is electronic, or a related electronic resource is being linked to the record)" + }, + "publicNote": { + "type": "string", + "description": "URL public note to be displayed in the discovery" + }, + "relationshipId": { + "type": "string", + "description": "UUID for the type of relationship between the electronic resource at the location identified and the item described in the record as a whole", + "$ref": "uuid.json" + } + }, + "additionalProperties": false, + "required": [ + "uri" + ] + } + }, + "dates": { + "type": "object", + "description": "Instance Dates", + "properties": { + "dateTypeId": { + "type": "string", + "description": "Date type ID", + "$ref": "uuid.json" + }, + "date1": { + "type": "string", + "description": "Date 1", + "maxLength": 4 + }, + "date2": { + "type": "string", + "description": "Date 2", + "maxLength": 4 + } + }, + "additionalProperties": false + }, + "instanceTypeId": { + "type": "string", + "description": "UUID of the unique term for the resource type whether it's from the RDA content term list of locally defined", + "$ref": "uuid.json" + }, + "instanceFormatIds": { + "type": "array", + "description": "UUIDs for the unique terms for the format whether it's from the RDA carrier term list of locally defined", + "items": { + "type": "string", + "$ref": "uuid.json" + } + }, + "instanceFormats": { + "type": "array", + "description": "List of dereferenced instance formats", + "items": { + "type": "object", + "$ref": "instanceformat.json" + }, + "readonly": true, + "folio:isVirtual": true, + "folio:linkBase": "instance-formats", + "folio:linkFromField": "instanceFormatIds", + "folio:linkToField": "id", + "folio:includedElement": "instanceFormats" + }, + "physicalDescriptions": { + "type": "array", + "description": "Physical description of the described resource, including its extent, dimensions, and such other physical details as a description of any accompanying materials and unit type and size", + "items": { + "type": "string" + } + }, + "languages": { + "type": "array", + "description": "The set of languages used by the resource", + "minItems": 0, + "items": { + "type": "string" + } + }, + "notes": { + "type": "array", + "description": "Bibliographic notes (e.g. general notes, specialized notes)", + "items": { + "type": "object", + "javaType": "org.folio.rest.jaxrs.model.InstanceNote", + "additionalProperties": false, + "properties": { + "instanceNoteTypeId": { + "description": "ID of the type of note", + "$ref": "uuid.json" + }, + "note": { + "type": "string", + "description": "Text content of the note" + }, + "staffOnly": { + "type": "boolean", + "description": "If true, determines that the note should not be visible for others than staff", + "default": false + } + } + } + }, + "administrativeNotes":{ + "type": "array", + "description": "Administrative notes", + "minItems": 0, + "items": { + "type": "string" + } + }, + "modeOfIssuanceId": { + "type": "string", + "description": "UUID of the RDA mode of issuance, a categorization reflecting whether a resource is issued in one or more parts, the way it is updated, and whether its termination is predetermined or not (e.g. monograph, sequential monograph, serial; integrating Resource, other)", + "$ref": "uuid.json" + }, + "catalogedDate": { + "type": "string", + "description": "Date or timestamp on an instance for when is was considered cataloged" + }, + "previouslyHeld": { + "type": "boolean", + "description": "Records the fact that the resource was previously held by the library for things like Hathi access, etc.", + "default": false + }, + "staffSuppress": { + "type": "boolean", + "description": "Records the fact that the record should not be displayed for others than catalogers" + }, + "discoverySuppress": { + "type": "boolean", + "description": "Records the fact that the record should not be displayed in a discovery system", + "default": false + }, + "statisticalCodeIds": { + "type": "array", + "description": "List of statistical code IDs", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "sourceRecordFormat": { + "type": "string", + "description": "Format of the instance source record, if a source record exists (e.g. FOLIO if it's a record created in Inventory, MARC if it's a MARC record created in MARCcat or EPKB if it's a record coming from eHoldings)", + "enum": ["MARC-JSON"], + "readonly": true + }, + "statusId": { + "type": "string", + "description": "UUID for the Instance status term (e.g. cataloged, uncatalogued, batch loaded, temporary, other, not yet assigned)", + "$ref": "uuid.json" + }, + "statusUpdatedDate": { + "type": "string", + "description": "Date [or timestamp] for when the instance status was updated" + }, + "tags": { + "description": "arbitrary tags associated with this instance", + "id": "tags", + "type": "object", + "$ref": "raml-util/schemas/tags.schema" + }, + "metadata": { + "type": "object", + "$ref": "raml-util/schemas/metadata.schema", + "readonly": true + }, + "holdingsRecords2": { + "type": "array", + "description": "List of holdings records", + "items": { + "type": "object", + "$ref": "holdings-storage/holdingsRecord.json" + }, + "readonly": true, + "folio:isVirtual": true, + "folio:linkBase": "holdings-storage/holdings", + "folio:linkFromField": "id", + "folio:linkToField": "instanceId", + "folio:includedElement": "holdingsRecords" + }, + "natureOfContentTermIds": { + "type": "array", + "description": "Array of UUID for the Instance nature of content (e.g. bibliography, biography, exhibition catalogue, festschrift, newspaper, proceedings, research report, thesis or website)", + "uniqueItems": true, + "items": { + "type": "string", + "description": "Single UUID for the Instance nature of content", + "$ref": "uuid.json" + } + } + }, + "additionalProperties": false, + "required": [ + "source", + "title", + "instanceTypeId" + ] +} diff --git a/ramls/instances-batch-response.json b/ramls/instances-batch-response.json index 93544d1f7..7532b7462 100644 --- a/ramls/instances-batch-response.json +++ b/ramls/instances-batch-response.json @@ -5,11 +5,11 @@ "properties": { "instances": { "id": "instancesList", - "description": "List of all successfully saved Instances", + "description": "List of all successfully saved Instances without the publicationPeriod", "type": "array", "items": { "type": "object", - "$ref": "instance.json" + "$ref": "instance-without-pub-period.json" } }, "errorMessages": { diff --git a/ramls/instances-without-pub-period.json b/ramls/instances-without-pub-period.json new file mode 100644 index 000000000..88a0ba224 --- /dev/null +++ b/ramls/instances-without-pub-period.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "A collection of instance records without publicationPeriod", + "type": "object", + "properties": { + "instances": { + "description": "List of instance records without publication period", + "id": "instances", + "type": "array", + "items": { + "$ref": "instance-without-pub-period.json", + "type" : "object" + } + }, + "totalRecords": { + "description": "Estimated or exact total number of records", + "type": "integer" + }, + "resultInfo": { + "$ref": "raml-util/schemas/resultInfo.schema", + "readonly": true + } + + }, + "required": [ + "instances", + "totalRecords" + ] +} diff --git a/ramls/instances_post.json b/ramls/instances_post.json index bb857e514..1eb06e415 100644 --- a/ramls/instances_post.json +++ b/ramls/instances_post.json @@ -1,15 +1,15 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "description": "A collection of instance records", + "description": "A collection of instance records without the publicationPeriod", "type": "object", "properties": { "instances": { - "description": "List of instance records", + "description": "List of instance records without the publicationPeriod", "id": "instances", "type": "array", "items": { - "type": "object", - "$ref": "instance.json" + "$ref": "instance-without-pub-period.json", + "type" : "object" } } }, diff --git a/ramls/inventory-view-instance-without-pub-period.json b/ramls/inventory-view-instance-without-pub-period.json new file mode 100644 index 000000000..5b02bef83 --- /dev/null +++ b/ramls/inventory-view-instance-without-pub-period.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Instance with holdings and items", + "type": "object", + "properties": { + "instanceId": { + "description": "Instance id", + "$ref": "uuid.json" + }, + "isBoundWith": { + "description": "Records the relationship between a part of a bound-with (a holdings-record) and the bound-with as a whole (the circulatable item)", + "type": "boolean" + }, + "instance": { + "description": "Instance record without the publicationPeriod", + "$ref": "instance-without-pub-period.json" + }, + "holdingsRecords": { + "type": "array", + "description": "Holdings records for the instance", + "items": { + "$ref": "holdings-storage/holdingsRecord.json" + } + }, + "items": { + "type": "array", + "description": "Items for the instance", + "items": { + "$ref": "item.json" + } + } + }, + "additionalProperties": false, + "required": ["instanceId", "instance"] +} diff --git a/ramls/inventory-view.raml b/ramls/inventory-view.raml index 1d25d5268..59a5fab6d 100644 --- a/ramls/inventory-view.raml +++ b/ramls/inventory-view.raml @@ -10,6 +10,7 @@ documentation: types: inventoryViewInstance: !include inventory-view-instance.json + inventoryViewInstanceWithoutPubPeriod: !include inventory-view-instance-without-pub-period.json errors: !include raml-util/schemas/errors.schema traits: @@ -23,7 +24,7 @@ resourceTypes: /inventory-view/instances: type: collection-stream: - schemaCollection: inventoryViewInstance + schemaCollection: inventoryViewInstanceWithoutPubPeriod exampleCollection: !include examples/inventory-view-instances.json get: description: Get instances by id with their holdings and items diff --git a/ramls/item-storage-dereferenced.raml b/ramls/item-storage-dereferenced.raml index ed618e597..1ba83dbad 100644 --- a/ramls/item-storage-dereferenced.raml +++ b/ramls/item-storage-dereferenced.raml @@ -11,6 +11,9 @@ documentation: types: dereferencedItem: !include dereferenceditem.json dereferencedItems: !include dereferenceditems.json + dereferencedItemWithoutPubPeriod: !include dereferenceditem-without-pub-period.json + dereferencedItemsWithoutPubPeriod: !include dereferenceditems-without-pub-period.json + errors: !include raml-util/schemas/errors.schema traits: @@ -27,7 +30,7 @@ resourceTypes: type: collection: exampleCollection: !include examples/items_dereferenced_get.json - schemaCollection: dereferencedItems + schemaCollection: dereferencedItemsWithoutPubPeriod get: is: [pageable, searchable: {description: "using CQL (indexes for item and material type)", @@ -37,5 +40,5 @@ resourceTypes: type: collection-item: exampleItem: !include examples/item_dereferenced_get.json - schema: dereferencedItem + schema: dereferencedItemWithoutPubPeriod get: diff --git a/src/main/java/org/folio/persist/AbstractRepository.java b/src/main/java/org/folio/persist/AbstractRepository.java index 5bc276012..077c2551a 100644 --- a/src/main/java/org/folio/persist/AbstractRepository.java +++ b/src/main/java/org/folio/persist/AbstractRepository.java @@ -4,24 +4,50 @@ import static io.vertx.core.Promise.promise; import static java.lang.String.format; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import io.vertx.core.AsyncResult; import io.vertx.core.Future; import io.vertx.core.Promise; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.json.Json; +import io.vertx.ext.web.RoutingContext; import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowSet; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import java.util.stream.Collectors; +import javax.ws.rs.core.MediaType; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.folio.HttpStatus; +import org.folio.cql2pgjson.CQL2PgJSON; +import org.folio.dbschema.ObjectMapperTool; +import org.folio.rest.jaxrs.model.Diagnostic; +import org.folio.rest.jaxrs.model.ResultInfo; import org.folio.rest.persist.Criteria.Criterion; +import org.folio.rest.persist.PgExceptionUtil; import org.folio.rest.persist.PostgresClient; import org.folio.rest.persist.PostgresClientFuturized; +import org.folio.rest.persist.PostgresClientStreamResult; import org.folio.rest.persist.SQLConnection; +import org.folio.rest.persist.cql.CQLWrapper; +import org.folio.rest.persist.facets.FacetField; +import org.folio.rest.persist.facets.FacetManager; import org.folio.rest.persist.interfaces.Results; +import org.folio.utils.ObjectConverterUtils; public abstract class AbstractRepository { + + private static final Logger logger = LogManager.getLogger(); + private static final ObjectMapper OBJECT_MAPPER = ObjectMapperTool.getMapper(); + private static final String JSON_COLUMN = "jsonb"; + protected final PostgresClientFuturized postgresClientFuturized; protected final PostgresClient postgresClient; protected final String tableName; @@ -70,7 +96,7 @@ public Future> getById(Collection records, Function> update(AsyncResult connection, String id, T entity) { final Promise> promise = promise(); - postgresClient.update(connection, tableName, entity, "jsonb", + postgresClient.update(connection, tableName, entity, JSON_COLUMN, format("WHERE id = '%s'", id), false, promise); return promise.future(); @@ -111,4 +137,103 @@ public Future> deleteAll() { public Future> deleteById(String id) { return postgresClientFuturized.deleteById(tableName, id); } + + @SuppressWarnings("java:S107") + // suppress "Methods should not have too many parameters" + public void streamGet(String table, Class clazz, + String cql, int offset, int limit, List facets, + String element, int queryTimeout, RoutingContext routingContext, + Class targetClazz) { + + var response = routingContext.response(); + try { + var facetList = FacetManager.convertFacetStrings2FacetFields(facets, JSON_COLUMN); + var wrapper = new CQLWrapper(new CQL2PgJSON(String.format("%s.%s", table, JSON_COLUMN)), cql, limit, offset); + streamGetInstances(table, clazz, wrapper, facetList, element, queryTimeout, routingContext, targetClazz); + } catch (Exception e) { + logger.error(String.format("streamGet:: Failed to retrieve data: %s", e.getMessage()), e); + response.setStatusCode(HttpStatus.HTTP_INTERNAL_SERVER_ERROR.toInt()); + response.putHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN); + response.end(e.toString()); + } + } + + @SuppressWarnings("java:S107") + // suppress "Methods should not have too many parameters" + private void streamGetInstances(String table, Class clazz, + CQLWrapper filter, List facetList, + String element, int queryTimeout, + RoutingContext routingContext, Class targetClazz) { + + var response = routingContext.response(); + postgresClient.streamGet(table, clazz, JSON_COLUMN, filter, true, null, + facetList, queryTimeout, reply -> { + if (reply.failed()) { + handleFailure(filter, reply, response); + return; + } + streamGetResult(reply.result(), element, response, targetClazz); + }); + } + + private void streamGetResult(PostgresClientStreamResult result, + String element, HttpServerResponse response, + Class targetClazz) { + response.setStatusCode(HttpStatus.HTTP_OK.toInt()); + response.setChunked(true); + response.putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); + response.write("{\n"); + response.write(String.format(" \"%s\": [%n", element)); + AtomicBoolean first = new AtomicBoolean(true); + result.exceptionHandler(res -> handleException(result, response, res)); + result.endHandler(res -> streamTrailer(response, result.resultInfo())); + result.handler(res -> handleResult(response, targetClazz, res, first)); + } + + private void handleFailure(CQLWrapper filter, + AsyncResult> reply, + HttpServerResponse response) { + var message = PgExceptionUtil.badRequestMessage(reply.cause()); + if (message == null) { + message = reply.cause().getMessage(); + } + message = String.format("%s: %s", message, filter.getQuery()); + logger.error(message, reply.cause()); + response.setStatusCode(HttpStatus.HTTP_BAD_REQUEST.toInt()); + response.putHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN); + response.end(message); + } + + private void streamTrailer(HttpServerResponse response, ResultInfo resultInfo) { + response.write("],\n"); + if (resultInfo.getTotalRecords() != null) { + response.write(String.format(" \"totalRecords\": %d,%n", resultInfo.getTotalRecords())); + } + response.end(String.format(" \"resultInfo\": %s%n}", Json.encode(resultInfo))); + } + + private void handleException(PostgresClientStreamResult result, HttpServerResponse response, Throwable res) { + var diagnostic = new Diagnostic() + .withCode(HttpStatus.HTTP_INTERNAL_SERVER_ERROR.toString()) + .withMessage(res.getMessage()); + result.resultInfo().setDiagnostics(List.of(diagnostic)); + streamTrailer(response, result.resultInfo()); + } + + private void handleResult(HttpServerResponse response, Class targetClazz, S res, AtomicBoolean first) { + String itemString; + try { + var targetObject = ObjectConverterUtils.convertObject(res, targetClazz); + itemString = OBJECT_MAPPER.writeValueAsString(targetObject); + } catch (JsonProcessingException ex) { + logger.error(String.format("handleResult:: Failed to handle streamGet result: %s", ex.getMessage()), ex); + throw new IllegalArgumentException(ex.getCause()); + } + if (first.get()) { + first.set(false); + } else { + response.write(String.format(",%n")); + } + response.write(itemString); + } } diff --git a/src/main/java/org/folio/rest/impl/InstanceBatchSyncApi.java b/src/main/java/org/folio/rest/impl/InstanceBatchSyncApi.java index fc0e56ebe..f2fa3aadb 100644 --- a/src/main/java/org/folio/rest/impl/InstanceBatchSyncApi.java +++ b/src/main/java/org/folio/rest/impl/InstanceBatchSyncApi.java @@ -8,9 +8,11 @@ import java.util.Map; import javax.ws.rs.core.Response; import org.folio.rest.annotations.Validate; +import org.folio.rest.jaxrs.model.Instances; import org.folio.rest.jaxrs.model.InstancesPost; import org.folio.rest.jaxrs.resource.InstanceStorageBatchSynchronous; import org.folio.services.instance.InstanceService; +import org.folio.utils.ObjectConverterUtils; public class InstanceBatchSyncApi implements InstanceStorageBatchSynchronous { @Validate @@ -20,8 +22,10 @@ public void postInstanceStorageBatchSynchronous(boolean upsert, InstancesPost en Handler> asyncResultHandler, Context vertxContext) { + var instances = ObjectConverterUtils.convertObject(entity, Instances.class); + new InstanceService(vertxContext, okapiHeaders) - .createInstances(entity.getInstances(), upsert, true) + .createInstances(instances.getInstances(), upsert, true) .otherwise(cause -> respond500WithTextPlain(cause.getMessage())) .onComplete(asyncResultHandler); } diff --git a/src/main/java/org/folio/rest/impl/InstanceBatchSyncUnsafeApi.java b/src/main/java/org/folio/rest/impl/InstanceBatchSyncUnsafeApi.java index 565ebacd5..5ce994149 100644 --- a/src/main/java/org/folio/rest/impl/InstanceBatchSyncUnsafeApi.java +++ b/src/main/java/org/folio/rest/impl/InstanceBatchSyncUnsafeApi.java @@ -8,9 +8,11 @@ import java.util.Map; import javax.ws.rs.core.Response; import org.folio.rest.annotations.Validate; +import org.folio.rest.jaxrs.model.Instances; import org.folio.rest.jaxrs.model.InstancesPost; import org.folio.rest.jaxrs.resource.InstanceStorageBatchSynchronousUnsafe; import org.folio.services.instance.InstanceService; +import org.folio.utils.ObjectConverterUtils; public class InstanceBatchSyncUnsafeApi implements InstanceStorageBatchSynchronousUnsafe { @Validate @@ -19,8 +21,10 @@ public void postInstanceStorageBatchSynchronousUnsafe(InstancesPost entity, Map< Handler> asyncResultHandler, Context vertxContext) { + var instances = ObjectConverterUtils.convertObject(entity, Instances.class); + new InstanceService(vertxContext, okapiHeaders) - .createInstances(entity.getInstances(), true, false) + .createInstances(instances.getInstances(), true, false) .otherwise(cause -> respond500WithTextPlain(cause.getMessage())) .onComplete(asyncResultHandler); } diff --git a/src/main/java/org/folio/rest/impl/InstanceStorageApi.java b/src/main/java/org/folio/rest/impl/InstanceStorageApi.java index ed78f7684..6d711bd1b 100644 --- a/src/main/java/org/folio/rest/impl/InstanceStorageApi.java +++ b/src/main/java/org/folio/rest/impl/InstanceStorageApi.java @@ -23,6 +23,7 @@ import org.folio.rest.jaxrs.model.Instance; import org.folio.rest.jaxrs.model.InstanceRelationship; import org.folio.rest.jaxrs.model.InstanceRelationships; +import org.folio.rest.jaxrs.model.InstanceWithoutPubPeriod; import org.folio.rest.jaxrs.model.Instances; import org.folio.rest.jaxrs.model.MarcJson; import org.folio.rest.jaxrs.model.RetrieveDto; @@ -34,10 +35,13 @@ import org.folio.rest.persist.PostgresClient; import org.folio.rest.persist.cql.CQLWrapper; import org.folio.rest.support.EndpointFailureHandler; +import org.folio.rest.support.EndpointHandler; +import org.folio.rest.support.GetInstanceStorageInstanceResponse; import org.folio.rest.tools.messages.MessageConsts; import org.folio.rest.tools.messages.Messages; import org.folio.rest.tools.utils.TenantTool; import org.folio.services.instance.InstanceService; +import org.folio.utils.ObjectConverterUtils; public class InstanceStorageApi implements InstanceStorage { private static final Logger log = LogManager.getLogger(); @@ -209,13 +213,15 @@ public void getInstanceStorageInstances(String totalRecords, int offset, int lim @Override public void postInstanceStorageInstances( - Instance entity, + InstanceWithoutPubPeriod entity, RoutingContext routingContext, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + var instance = ObjectConverterUtils.convertObject(entity, Instance.class); + new InstanceService(vertxContext, okapiHeaders) - .createInstance(entity) + .createInstance(instance) .onSuccess(response -> asyncResultHandler.handle(succeededFuture(response))) .onFailure(handleFailure(asyncResultHandler)); } @@ -267,13 +273,15 @@ public void deleteInstanceStorageInstancesByInstanceId( public void putInstanceStorageInstancesByInstanceId( String instanceId, - Instance entity, + InstanceWithoutPubPeriod entity, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + var instance = ObjectConverterUtils.convertObject(entity, Instance.class); + new InstanceService(vertxContext, okapiHeaders) - .updateInstance(instanceId, entity) + .updateInstance(instanceId, instance) .onSuccess(response -> asyncResultHandler.handle(succeededFuture(response))) .onFailure(handleFailure(asyncResultHandler)); } @@ -334,7 +342,7 @@ public void putInstanceStorageInstancesSourceRecordMarcJsonByInstanceId( return; } if (PgExceptionUtil.isForeignKeyViolation(reply.cause()) - && reply.cause().getMessage().contains(INSTANCE_SOURCE_MARC_TABLE)) { + && reply.cause().getMessage().contains(INSTANCE_SOURCE_MARC_TABLE)) { asyncResultHandler.handle(Future.succeededFuture( PutInstanceStorageInstancesSourceRecordMarcJsonByInstanceIdResponse .respond404WithTextPlain(reply.cause().getMessage()))); @@ -395,7 +403,8 @@ private void fetchInstances(String query, int limit, int offset, PreparedCql preparedCql = handleCql(query, limit, offset); PgUtil.getWithOptimizedSql(preparedCql.getTableName(), Instance.class, Instances.class, TITLE, query, offset, limit, - okapiHeaders, vertxContext, GetInstanceStorageInstancesResponse.class, asyncResultHandler); + okapiHeaders, vertxContext, GetInstanceStorageInstanceResponse.class, + EndpointHandler.handleInstances(asyncResultHandler)); } catch (Exception e) { log.error(e.getMessage(), e); asyncResultHandler.handle(io.vertx.core.Future.succeededFuture( @@ -403,9 +412,8 @@ private void fetchInstances(String query, int limit, int offset, } return; } - - PgUtil.streamGet(INSTANCE_TABLE, Instance.class, query, offset, limit, null, - "instances", routingContext, okapiHeaders, vertxContext); + new InstanceService(vertxContext, okapiHeaders) + .streamGetInstances(INSTANCE_TABLE, query, offset, limit, null, "instances", 0, routingContext); } PreparedCql handleCql(String query, int limit, int offset) throws FieldException { diff --git a/src/main/java/org/folio/rest/impl/InstanceStorageBatchApi.java b/src/main/java/org/folio/rest/impl/InstanceStorageBatchApi.java index 51594a4c3..8bfcc3a89 100644 --- a/src/main/java/org/folio/rest/impl/InstanceStorageBatchApi.java +++ b/src/main/java/org/folio/rest/impl/InstanceStorageBatchApi.java @@ -22,14 +22,17 @@ import org.apache.logging.log4j.Logger; import org.folio.rest.annotations.Validate; import org.folio.rest.jaxrs.model.Instance; +import org.folio.rest.jaxrs.model.InstanceWithoutPubPeriod; import org.folio.rest.jaxrs.model.Instances; import org.folio.rest.jaxrs.model.InstancesBatchResponse; +import org.folio.rest.jaxrs.model.InstancesWithoutPubPeriod; import org.folio.rest.jaxrs.resource.InstanceStorageBatchInstances; import org.folio.rest.persist.PgUtil; import org.folio.rest.persist.PostgresClient; import org.folio.rest.support.HridManager; import org.folio.rest.tools.utils.MetadataUtil; import org.folio.services.domainevent.InstanceDomainEventPublisher; +import org.folio.utils.ObjectConverterUtils; @SuppressWarnings("rawtypes") public class InstanceStorageBatchApi implements InstanceStorageBatchInstances { @@ -44,13 +47,15 @@ public class InstanceStorageBatchApi implements InstanceStorageBatchInstances { @Validate @Override - public void postInstanceStorageBatchInstances(Instances entity, + public void postInstanceStorageBatchInstances(InstancesWithoutPubPeriod entity, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { + var instances = ObjectConverterUtils.convertObject(entity, Instances.class); + final String statusUpdatedDate = generateStatusUpdatedDate(); - for (Instance instance : entity.getInstances()) { + for (Instance instance : instances.getInstances()) { instance.setStatusUpdatedDate(statusUpdatedDate); } @@ -60,9 +65,9 @@ public void postInstanceStorageBatchInstances(Instances entity, final InstanceDomainEventPublisher instanceDomainEventPublisher = new InstanceDomainEventPublisher(vertxContext, okapiHeaders); - MetadataUtil.populateMetadata(entity.getInstances(), okapiHeaders); - executeInBatch(entity.getInstances(), - instances -> saveInstances(instances, postgresClient)) + MetadataUtil.populateMetadata(instances.getInstances(), okapiHeaders); + executeInBatch(instances.getInstances(), + instanceList -> saveInstances(instanceList, postgresClient)) .onComplete(ar -> { InstancesBatchResponse response = constructResponse(ar.result()); @@ -170,7 +175,7 @@ private InstancesBatchResponse constructResponse(List> saveFutu if (save.failed()) { response.getErrorMessages().add(save.cause().getMessage()); } else { - response.getInstances().add(save.result()); + response.getInstances().add(ObjectConverterUtils.convertObject(save.result(), InstanceWithoutPubPeriod.class)); } }); diff --git a/src/main/java/org/folio/rest/impl/InventoryViewApi.java b/src/main/java/org/folio/rest/impl/InventoryViewApi.java index b7dd7aae9..325055ca2 100644 --- a/src/main/java/org/folio/rest/impl/InventoryViewApi.java +++ b/src/main/java/org/folio/rest/impl/InventoryViewApi.java @@ -1,7 +1,5 @@ package org.folio.rest.impl; -import static org.folio.rest.persist.PgUtil.streamGet; - import io.vertx.core.AsyncResult; import io.vertx.core.Context; import io.vertx.core.Handler; @@ -9,8 +7,8 @@ import java.util.Map; import javax.ws.rs.core.Response; import org.folio.rest.annotations.Validate; -import org.folio.rest.jaxrs.model.InventoryViewInstance; import org.folio.rest.jaxrs.resource.InventoryViewInstances; +import org.folio.services.instance.InstanceService; public class InventoryViewApi implements InventoryViewInstances { @Validate @@ -19,7 +17,8 @@ public void getInventoryViewInstances(String totalRecords, int offset, int limit RoutingContext routingContext, Map okapiHeaders, Handler> asyncResultHandler, Context vertxContext) { - streamGet("instance_holdings_item_view", InventoryViewInstance.class, query, - offset, limit, null, "instances", routingContext, okapiHeaders, vertxContext); + new InstanceService(vertxContext, okapiHeaders) + .streamGetInventoryViewInstances("instance_holdings_item_view", query, + offset, limit, null, "instances", 0, routingContext); } } diff --git a/src/main/java/org/folio/rest/impl/ItemStorageDereferencedApi.java b/src/main/java/org/folio/rest/impl/ItemStorageDereferencedApi.java index 0f6eb5f8d..6b3fb0685 100644 --- a/src/main/java/org/folio/rest/impl/ItemStorageDereferencedApi.java +++ b/src/main/java/org/folio/rest/impl/ItemStorageDereferencedApi.java @@ -17,12 +17,15 @@ import org.folio.cql2pgjson.CQL2PgJSON; import org.folio.rest.annotations.Validate; import org.folio.rest.jaxrs.model.DereferencedItem; +import org.folio.rest.jaxrs.model.DereferencedItemWithoutPubPeriod; import org.folio.rest.jaxrs.model.DereferencedItems; +import org.folio.rest.jaxrs.model.DereferencedItemsWithoutPubPeriod; import org.folio.rest.jaxrs.resource.ItemStorageDereferenced; import org.folio.rest.persist.PgUtil; import org.folio.rest.persist.PostgresClient; import org.folio.rest.persist.cql.CQLWrapper; import org.folio.util.UuidUtil; +import org.folio.utils.ObjectConverterUtils; /** * CRUD for Dereferenced Items. @@ -109,8 +112,11 @@ public void getItemStorageDereferencedItems(String totalRecords, int offset, int itemCollection.setDereferencedItems(mappedResults); itemCollection.setTotalRecords(mappedResults.size()); + var dereferencedItemsWithoutPubPeriod = ObjectConverterUtils.convertObject( + itemCollection, DereferencedItemsWithoutPubPeriod.class); asyncResultHandler.handle(Future.succeededFuture( - GetItemStorageDereferencedItemsResponse.respond200WithApplicationJson(itemCollection))); + GetItemStorageDereferencedItemsResponse.respond200WithApplicationJson( + dereferencedItemsWithoutPubPeriod))); }); } @@ -138,8 +144,12 @@ public void getItemStorageDereferencedItemsByItemId( Row row = asyncResult.result().iterator().next(); DereferencedItem item = mapToDereferencedItem(row); + var dereferencedItemWithoutPubPeriod = ObjectConverterUtils.convertObject( + item, DereferencedItemWithoutPubPeriod.class); + asyncResultHandler.handle(Future.succeededFuture( - GetItemStorageDereferencedItemsByItemIdResponse.respond200WithApplicationJson(item))); + GetItemStorageDereferencedItemsByItemIdResponse.respond200WithApplicationJson( + dereferencedItemWithoutPubPeriod))); }); } diff --git a/src/main/java/org/folio/rest/support/EndpointHandler.java b/src/main/java/org/folio/rest/support/EndpointHandler.java index de886cb48..d6edf8ef0 100644 --- a/src/main/java/org/folio/rest/support/EndpointHandler.java +++ b/src/main/java/org/folio/rest/support/EndpointHandler.java @@ -5,6 +5,10 @@ import io.vertx.core.AsyncResult; import io.vertx.core.Handler; import javax.ws.rs.core.Response; +import org.folio.HttpStatus; +import org.folio.rest.jaxrs.model.InstancesWithoutPubPeriod; +import org.folio.rest.jaxrs.resource.InstanceStorage; +import org.folio.utils.ObjectConverterUtils; public final class EndpointHandler { private EndpointHandler() { } @@ -23,4 +27,25 @@ public static Handler> handle(Handler> handleInstances(Handler> asyncResultHandler) { + return result -> { + if (result.succeeded()) { + handleSuccess(asyncResultHandler, result.result()); + } else { + EndpointFailureHandler.handleFailure(asyncResultHandler).handle(result.cause()); + } + }; + } + + private static void handleSuccess(Handler> asyncResultHandler, Response result) { + if (result.getStatus() == HttpStatus.HTTP_OK.toInt()) { + var instances = result.getEntity(); + var instancesWithoutPubPeriod = ObjectConverterUtils.convertObject(instances, InstancesWithoutPubPeriod.class); + asyncResultHandler.handle(succeededFuture( + InstanceStorage.GetInstanceStorageInstancesResponse.respond200WithApplicationJson(instancesWithoutPubPeriod))); + } else { + asyncResultHandler.handle(succeededFuture(result)); + } + } } diff --git a/src/main/java/org/folio/rest/support/GetInstanceStorageInstanceResponse.java b/src/main/java/org/folio/rest/support/GetInstanceStorageInstanceResponse.java new file mode 100644 index 000000000..77f051816 --- /dev/null +++ b/src/main/java/org/folio/rest/support/GetInstanceStorageInstanceResponse.java @@ -0,0 +1,44 @@ +package org.folio.rest.support; + +import io.vertx.core.http.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.folio.HttpStatus; +import org.folio.rest.jaxrs.model.Instances; +import org.folio.rest.jaxrs.resource.support.ResponseDelegate; + +public final class GetInstanceStorageInstanceResponse extends ResponseDelegate { + + private GetInstanceStorageInstanceResponse(Response response, Object entity) { + super(response, entity); + } + + public static GetInstanceStorageInstanceResponse respond200WithApplicationJson( + Instances entity) { + Response.ResponseBuilder responseBuilder = Response.status(HttpStatus.HTTP_OK.toInt()) + .header(HttpHeaders.CONTENT_TYPE.toString(), MediaType.APPLICATION_JSON); + responseBuilder.entity(entity); + return new GetInstanceStorageInstanceResponse(responseBuilder.build(), entity); + } + + public static GetInstanceStorageInstanceResponse respond400WithTextPlain(Object entity) { + Response.ResponseBuilder responseBuilder = Response.status(HttpStatus.HTTP_BAD_REQUEST.toInt()) + .header(HttpHeaders.CONTENT_TYPE.toString(), MediaType.TEXT_PLAIN); + responseBuilder.entity(entity); + return new GetInstanceStorageInstanceResponse(responseBuilder.build(), entity); + } + + public static GetInstanceStorageInstanceResponse respond401WithTextPlain(Object entity) { + Response.ResponseBuilder responseBuilder = Response.status(HttpStatus.HTTP_UNAUTHORIZED.toInt()) + .header(HttpHeaders.CONTENT_TYPE.toString(), MediaType.TEXT_PLAIN); + responseBuilder.entity(entity); + return new GetInstanceStorageInstanceResponse(responseBuilder.build(), entity); + } + + public static GetInstanceStorageInstanceResponse respond500WithTextPlain(Object entity) { + Response.ResponseBuilder responseBuilder = Response.status(HttpStatus.HTTP_INTERNAL_SERVER_ERROR.toInt()) + .header(HttpHeaders.CONTENT_TYPE.toString(), MediaType.TEXT_PLAIN); + responseBuilder.entity(entity); + return new GetInstanceStorageInstanceResponse(responseBuilder.build(), entity); + } +} diff --git a/src/main/java/org/folio/services/domainevent/InstanceDomainEventPublisher.java b/src/main/java/org/folio/services/domainevent/InstanceDomainEventPublisher.java index 0d9072903..b465e03c7 100644 --- a/src/main/java/org/folio/services/domainevent/InstanceDomainEventPublisher.java +++ b/src/main/java/org/folio/services/domainevent/InstanceDomainEventPublisher.java @@ -16,13 +16,15 @@ import org.apache.logging.log4j.Logger; import org.folio.persist.InstanceRepository; import org.folio.rest.jaxrs.model.Instance; +import org.folio.rest.jaxrs.model.InstanceWithoutPubPeriod; import org.folio.rest.jaxrs.model.PublishReindexRecords; +import org.folio.utils.ObjectConverterUtils; -public class InstanceDomainEventPublisher extends AbstractDomainEventPublisher { +public class InstanceDomainEventPublisher extends AbstractDomainEventPublisher { private static final String MAX_REQUEST_SIZE = "KAFKA_REINDEX_PRODUCER_MAX_REQUEST_SIZE_BYTES"; private static final Logger log = getLogger(InstanceDomainEventPublisher.class); - + private final CommonDomainEventPublisher> instanceReindexPublisher; public InstanceDomainEventPublisher(Context context, Map okapiHeaders) { @@ -41,7 +43,7 @@ public Future publishReindexInstances(String key, List return instanceReindexPublisher.publishReindexRecords(key, PublishReindexRecords.RecordType.INSTANCE, instances); } - public Future publishInstancesCreated(List instances) { + public Future publishInstancesCreated(List instances) { if (instances.isEmpty()) { log.info("No instances were created, skipping event sending"); return succeededFuture(); @@ -62,8 +64,8 @@ protected Future>> getRecordIds(Collection } @Override - protected Instance convertDomainToEvent(String instanceId, Instance domain) { - return domain; + protected InstanceWithoutPubPeriod convertDomainToEvent(String instanceId, Instance domain) { + return ObjectConverterUtils.convertObject(domain, InstanceWithoutPubPeriod.class); } @Override diff --git a/src/main/java/org/folio/services/instance/InstanceService.java b/src/main/java/org/folio/services/instance/InstanceService.java index 8a37b6a3e..3b915607d 100644 --- a/src/main/java/org/folio/services/instance/InstanceService.java +++ b/src/main/java/org/folio/services/instance/InstanceService.java @@ -20,6 +20,7 @@ import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.Promise; +import io.vertx.ext.web.RoutingContext; import java.util.List; import java.util.Map; import javax.ws.rs.core.Response; @@ -28,6 +29,9 @@ import org.folio.persist.InstanceRelationshipRepository; import org.folio.persist.InstanceRepository; import org.folio.rest.jaxrs.model.Instance; +import org.folio.rest.jaxrs.model.InstanceWithoutPubPeriod; +import org.folio.rest.jaxrs.model.InventoryViewInstance; +import org.folio.rest.jaxrs.model.InventoryViewInstanceWithoutPubPeriod; import org.folio.rest.jaxrs.resource.InstanceStorage; import org.folio.rest.persist.PostgresClient; import org.folio.rest.support.CqlQuery; @@ -39,6 +43,7 @@ import org.folio.services.consortium.ConsortiumServiceImpl; import org.folio.services.domainevent.InstanceDomainEventPublisher; import org.folio.util.StringUtil; +import org.folio.utils.ObjectConverterUtils; import org.folio.validator.CommonValidators; import org.folio.validator.NotesValidators; @@ -72,7 +77,8 @@ public Future getInstance(String id) { if (instance == null) { return GetInstanceStorageInstancesByInstanceIdResponse.respond404WithTextPlain(null); } - return GetInstanceStorageInstancesByInstanceIdResponse.respond200WithApplicationJson(instance); + var instanceWithoutPubPeriod = ObjectConverterUtils.convertObject(instance, InstanceWithoutPubPeriod.class); + return GetInstanceStorageInstancesByInstanceIdResponse.respond200WithApplicationJson(instanceWithoutPubPeriod); }); } @@ -212,6 +218,24 @@ public Future publishReindexInstanceRecords(String rangeId, String fromId, .compose(instances -> domainEventPublisher.publishReindexInstances(rangeId, instances)); } + @SuppressWarnings("java:S107") + // suppress "Methods should not have too many parameters" + public void streamGetInstances(String table, String cql, int offset, int limit, List facets, + String element, int queryTimeout, RoutingContext routingContext) { + instanceRepository.streamGet( + table, Instance.class, cql, offset, limit, facets, element, + queryTimeout, routingContext, InstanceWithoutPubPeriod.class); + } + + @SuppressWarnings("java:S107") + // suppress "Methods should not have too many parameters" + public void streamGetInventoryViewInstances(String table, String cql, int offset, int limit, List facets, + String element, int queryTimeout, RoutingContext routingContext) { + instanceRepository.streamGet( + table, InventoryViewInstance.class, cql, offset, limit, facets, element, + queryTimeout, routingContext, InventoryViewInstanceWithoutPubPeriod.class); + } + private boolean isCentralTenantId(String tenantId, ConsortiumData consortiumData) { return tenantId.equals(consortiumData.centralTenantId()); } diff --git a/src/main/java/org/folio/services/migration/MigrationName.java b/src/main/java/org/folio/services/migration/MigrationName.java new file mode 100644 index 000000000..70782d4cd --- /dev/null +++ b/src/main/java/org/folio/services/migration/MigrationName.java @@ -0,0 +1,22 @@ +package org.folio.services.migration; + +public enum MigrationName { + PUBLICATION_PERIOD_MIGRATION("publicationPeriodMigration"), + SUBJECT_SERIES_MIGRATION("subjectSeriesMigration"), + ITEM_SHELVING_ORDER_MIGRATION("itemShelvingOrderMigration"); + + private final String value; + + MigrationName(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } +} diff --git a/src/main/java/org/folio/services/migration/async/AbstractAsyncBaseMigrationService.java b/src/main/java/org/folio/services/migration/async/AbstractAsyncBaseMigrationService.java new file mode 100644 index 000000000..1c7010386 --- /dev/null +++ b/src/main/java/org/folio/services/migration/async/AbstractAsyncBaseMigrationService.java @@ -0,0 +1,59 @@ +package org.folio.services.migration.async; + +import static org.folio.persist.InstanceRepository.INSTANCE_TABLE; + +import io.vertx.core.Future; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowStream; +import java.util.List; +import java.util.stream.Collectors; +import org.folio.persist.InstanceRepository; +import org.folio.rest.jaxrs.model.Instance; +import org.folio.rest.persist.PostgresClientFuturized; +import org.folio.rest.persist.SQLConnection; + +public abstract class AbstractAsyncBaseMigrationService extends AsyncBaseMigrationService { + + private static final String WHERE_CONDITION = "id in (%s)"; + + protected final PostgresClientFuturized postgresClient; + protected final InstanceRepository instanceRepository; + + protected AbstractAsyncBaseMigrationService(String version, PostgresClientFuturized postgresClient, + InstanceRepository instanceRepository) { + super(version, postgresClient); + this.postgresClient = postgresClient; + this.instanceRepository = instanceRepository; + } + + @Override + protected Future> openStream(SQLConnection connection) { + return postgresClient.selectStream(connection, selectSql()); + } + + @Override + protected Future updateBatch(List batch, SQLConnection connection) { + var instances = batch.stream() + .map(row -> row.getJsonObject("jsonb")) + .map(json -> json.mapTo(Instance.class)) + .toList(); + return instanceRepository.updateBatch(instances, connection) + .map(notUsed -> instances.size()); + } + + protected abstract String getSelectSqlQuery(); + + private String selectSql() { + var idsForMigration = getIdsForMigration(); + var whereCondition = "false"; + + if (!idsForMigration.isEmpty()) { + var ids = idsForMigration.stream() + .map(id -> String.format("'%s'", id)) + .collect(Collectors.joining(", ")); + + whereCondition = String.format(WHERE_CONDITION, ids); + } + return String.format(getSelectSqlQuery(), postgresClient.getFullTableName(INSTANCE_TABLE), whereCondition); + } +} diff --git a/src/main/java/org/folio/services/migration/async/AsyncMigrationJobService.java b/src/main/java/org/folio/services/migration/async/AsyncMigrationJobService.java index 44d5c5c83..d5ed2a3a9 100644 --- a/src/main/java/org/folio/services/migration/async/AsyncMigrationJobService.java +++ b/src/main/java/org/folio/services/migration/async/AsyncMigrationJobService.java @@ -31,7 +31,9 @@ public final class AsyncMigrationJobService { private static final List MIGRATION_JOB_RUNNERS = List - .of(new ShelvingOrderMigrationJobRunner(), new SubjectSeriesMigrationJobRunner()); + .of(new ShelvingOrderMigrationJobRunner(), + new SubjectSeriesMigrationJobRunner(), + new PublicationPeriodMigrationJobRunner()); private static final List ACCEPTABLE_STATUSES = List .of(AsyncMigrationJob.JobStatus.IN_PROGRESS, IDS_PUBLISHED); diff --git a/src/main/java/org/folio/services/migration/async/AsyncMigrationsConsumerUtils.java b/src/main/java/org/folio/services/migration/async/AsyncMigrationsConsumerUtils.java index 4d398840d..107792b17 100644 --- a/src/main/java/org/folio/services/migration/async/AsyncMigrationsConsumerUtils.java +++ b/src/main/java/org/folio/services/migration/async/AsyncMigrationsConsumerUtils.java @@ -42,7 +42,8 @@ public static Handler> pollAsyncMigrati var availableMigrations = Set.of( new ShelvingOrderAsyncMigrationService(vertxContext, headers), - new SubjectSeriesMigrationService(vertxContext, headers)); + new SubjectSeriesMigrationService(vertxContext, headers), + new PublicationPeriodMigrationService(vertxContext, headers)); var jobService = new AsyncMigrationJobService(vertxContext, headers); var migrationEvents = buildIdsForMigrations(v.getValue()); diff --git a/src/main/java/org/folio/services/migration/async/PublicationPeriodMigrationJobRunner.java b/src/main/java/org/folio/services/migration/async/PublicationPeriodMigrationJobRunner.java new file mode 100644 index 000000000..414470419 --- /dev/null +++ b/src/main/java/org/folio/services/migration/async/PublicationPeriodMigrationJobRunner.java @@ -0,0 +1,37 @@ +package org.folio.services.migration.async; + +import static java.lang.String.format; +import static org.folio.persist.InstanceRepository.INSTANCE_TABLE; +import static org.folio.services.migration.MigrationName.PUBLICATION_PERIOD_MIGRATION; + +import io.vertx.core.Future; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowStream; +import java.util.Collections; +import java.util.List; +import org.folio.rest.jaxrs.model.AffectedEntity; +import org.folio.rest.persist.PostgresClientFuturized; +import org.folio.rest.persist.SQLConnection; + +public class PublicationPeriodMigrationJobRunner extends AbstractAsyncMigrationJobRunner { + + private static final String SELECT_SQL = """ + SELECT id FROM %s + WHERE jsonb -> 'publicationPeriod' IS NOT NULL + """; + + @Override + protected Future> openStream(PostgresClientFuturized postgresClient, SQLConnection connection) { + return postgresClient.selectStream(connection, format(SELECT_SQL, postgresClient.getFullTableName(INSTANCE_TABLE))); + } + + @Override + public String getMigrationName() { + return PUBLICATION_PERIOD_MIGRATION.getValue(); + } + + @Override + public List getAffectedEntities() { + return Collections.singletonList(AffectedEntity.INSTANCE); + } +} diff --git a/src/main/java/org/folio/services/migration/async/PublicationPeriodMigrationService.java b/src/main/java/org/folio/services/migration/async/PublicationPeriodMigrationService.java new file mode 100644 index 000000000..ba5ebd3d0 --- /dev/null +++ b/src/main/java/org/folio/services/migration/async/PublicationPeriodMigrationService.java @@ -0,0 +1,39 @@ +package org.folio.services.migration.async; + +import static org.folio.services.migration.MigrationName.PUBLICATION_PERIOD_MIGRATION; + +import io.vertx.core.Context; +import java.util.Map; +import org.folio.persist.InstanceRepository; +import org.folio.rest.persist.PgUtil; +import org.folio.rest.persist.PostgresClientFuturized; + +public class PublicationPeriodMigrationService extends AbstractAsyncBaseMigrationService { + + private static final String SELECT_SQL = """ + SELECT migrate_publication_period(jsonb) as jsonb + FROM %s + WHERE %s FOR UPDATE + """; + + public PublicationPeriodMigrationService(Context context, Map okapiHeaders) { + this(new PostgresClientFuturized(PgUtil.postgresClient(context, okapiHeaders)), + new InstanceRepository(context, okapiHeaders)); + } + + public PublicationPeriodMigrationService(PostgresClientFuturized postgresClient, + InstanceRepository instanceRepository) { + + super("28.0.0", postgresClient, instanceRepository); + } + + @Override + public String getMigrationName() { + return PUBLICATION_PERIOD_MIGRATION.getValue(); + } + + @Override + protected String getSelectSqlQuery() { + return SELECT_SQL; + } +} diff --git a/src/main/java/org/folio/services/migration/async/ShelvingOrderMigrationJobRunner.java b/src/main/java/org/folio/services/migration/async/ShelvingOrderMigrationJobRunner.java index b6234c1e9..2790ed7c8 100644 --- a/src/main/java/org/folio/services/migration/async/ShelvingOrderMigrationJobRunner.java +++ b/src/main/java/org/folio/services/migration/async/ShelvingOrderMigrationJobRunner.java @@ -1,6 +1,7 @@ package org.folio.services.migration.async; import static java.lang.String.format; +import static org.folio.services.migration.MigrationName.ITEM_SHELVING_ORDER_MIGRATION; import io.vertx.core.Future; import io.vertx.sqlclient.Row; @@ -18,7 +19,7 @@ public class ShelvingOrderMigrationJobRunner extends AbstractAsyncMigrationJobRu @Override public String getMigrationName() { - return "itemShelvingOrderMigration"; + return ITEM_SHELVING_ORDER_MIGRATION.getValue(); } @Override diff --git a/src/main/java/org/folio/services/migration/async/SubjectSeriesMigrationJobRunner.java b/src/main/java/org/folio/services/migration/async/SubjectSeriesMigrationJobRunner.java index 048db1fe3..da2a8a7ea 100644 --- a/src/main/java/org/folio/services/migration/async/SubjectSeriesMigrationJobRunner.java +++ b/src/main/java/org/folio/services/migration/async/SubjectSeriesMigrationJobRunner.java @@ -1,6 +1,7 @@ package org.folio.services.migration.async; import static java.lang.String.format; +import static org.folio.services.migration.MigrationName.SUBJECT_SERIES_MIGRATION; import io.vertx.core.Future; import io.vertx.sqlclient.Row; @@ -18,7 +19,7 @@ public class SubjectSeriesMigrationJobRunner extends AbstractAsyncMigrationJobRu @Override public String getMigrationName() { - return "subjectSeriesMigration"; + return SUBJECT_SERIES_MIGRATION.getValue(); } @Override diff --git a/src/main/java/org/folio/services/migration/async/SubjectSeriesMigrationService.java b/src/main/java/org/folio/services/migration/async/SubjectSeriesMigrationService.java index f1a6e950f..5b2929855 100644 --- a/src/main/java/org/folio/services/migration/async/SubjectSeriesMigrationService.java +++ b/src/main/java/org/folio/services/migration/async/SubjectSeriesMigrationService.java @@ -1,26 +1,17 @@ package org.folio.services.migration.async; +import static org.folio.services.migration.MigrationName.SUBJECT_SERIES_MIGRATION; + import io.vertx.core.Context; -import io.vertx.core.Future; -import io.vertx.sqlclient.Row; -import io.vertx.sqlclient.RowStream; -import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import org.folio.persist.InstanceRepository; -import org.folio.rest.jaxrs.model.Instance; import org.folio.rest.persist.PgUtil; import org.folio.rest.persist.PostgresClientFuturized; -import org.folio.rest.persist.SQLConnection; -public class SubjectSeriesMigrationService extends AsyncBaseMigrationService { +public class SubjectSeriesMigrationService extends AbstractAsyncBaseMigrationService { private static final String SELECT_SQL = "SELECT migrate_series_and_subjects(jsonb) as jsonb FROM %s WHERE %s FOR UPDATE"; - private static final String WHERE_CONDITION = "id in (%s)"; - - private final PostgresClientFuturized postgresClient; - private final InstanceRepository instanceRepository; public SubjectSeriesMigrationService(Context context, Map okapiHeaders) { this(new PostgresClientFuturized(PgUtil.postgresClient(context, okapiHeaders)), @@ -30,43 +21,16 @@ public SubjectSeriesMigrationService(Context context, Map okapiH public SubjectSeriesMigrationService(PostgresClientFuturized postgresClient, InstanceRepository instanceRepository) { - super("26.0.0", postgresClient); - this.postgresClient = postgresClient; - this.instanceRepository = instanceRepository; - } - - @Override - protected Future> openStream(SQLConnection connection) { - return postgresClient.selectStream(connection, selectSql()); - } - - @Override - protected Future updateBatch(List batch, SQLConnection connection) { - var instances = batch.stream() - .map(row -> row.getJsonObject("jsonb")) - .map(json -> json.mapTo(Instance.class)) - .toList(); - return instanceRepository.updateBatch(instances, connection) - .map(notUsed -> instances.size()); + super("26.0.0", postgresClient, instanceRepository); } @Override public String getMigrationName() { - return "subjectSeriesMigration"; + return SUBJECT_SERIES_MIGRATION.getValue(); } - private String selectSql() { - var idsForMigration = getIdsForMigration(); - var whereCondition = "false"; - - if (!idsForMigration.isEmpty()) { - var ids = idsForMigration.stream() - .map(id -> "'" + id + "'") - .collect(Collectors.joining(", ")); - - whereCondition = String.format(WHERE_CONDITION, ids); - } - - return String.format(SELECT_SQL, postgresClient.getFullTableName("instance"), whereCondition); + @Override + protected String getSelectSqlQuery() { + return SELECT_SQL; } } diff --git a/src/main/java/org/folio/services/migration/item/ItemShelvingOrderMigrationService.java b/src/main/java/org/folio/services/migration/item/ItemShelvingOrderMigrationService.java index c7ec0a821..2fb52e1fc 100644 --- a/src/main/java/org/folio/services/migration/item/ItemShelvingOrderMigrationService.java +++ b/src/main/java/org/folio/services/migration/item/ItemShelvingOrderMigrationService.java @@ -1,5 +1,7 @@ package org.folio.services.migration.item; +import static org.folio.services.migration.MigrationName.ITEM_SHELVING_ORDER_MIGRATION; + import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.sqlclient.Row; @@ -51,7 +53,7 @@ protected Future updateBatch(List batch, SQLConnection connection) @Override public String getMigrationName() { - return "itemShelvingOrderMigration"; + return ITEM_SHELVING_ORDER_MIGRATION.getValue(); } protected String selectSql() { diff --git a/src/main/java/org/folio/utils/ObjectConverterUtils.java b/src/main/java/org/folio/utils/ObjectConverterUtils.java new file mode 100644 index 000000000..b5a40168b --- /dev/null +++ b/src/main/java/org/folio/utils/ObjectConverterUtils.java @@ -0,0 +1,30 @@ +package org.folio.utils; + +import com.google.gson.Gson; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class ObjectConverterUtils { + + private static final Logger log = LoggerFactory.getLogger(ObjectConverterUtils.class); + private static final Gson GSON = new Gson(); + + private ObjectConverterUtils() { + throw new UnsupportedOperationException("Utility class"); + } + + public static T convertObject(Object source, Class targetClass) { + try { + log.debug("convertObject:: convert from {} to {}", + source.getClass().getSimpleName(), targetClass.getSimpleName()); + + var sourceJson = GSON.toJson(source); + return GSON.fromJson(sourceJson, targetClass); + } catch (Exception e) { + var errorMessage = String.format("convertObject:: Failed to convert %s to %s: %s", + source.getClass().getSimpleName(), targetClass.getSimpleName(), e.getMessage()); + log.error(errorMessage, e); + throw new IllegalArgumentException(errorMessage, e); + } + } +} diff --git a/src/main/resources/templates/db_scripts/publication-period/migratePublicationPeriod.sql b/src/main/resources/templates/db_scripts/publication-period/migratePublicationPeriod.sql index 582795944..a2f1943f0 100644 --- a/src/main/resources/templates/db_scripts/publication-period/migratePublicationPeriod.sql +++ b/src/main/resources/templates/db_scripts/publication-period/migratePublicationPeriod.sql @@ -41,89 +41,3 @@ BEGIN RETURN jsonb_data; END; $$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE STRICT; - - --- Migration Script -DO -$$ -DECLARE - trigger VARCHAR; - triggers VARCHAR[] DEFAULT ARRAY[ - 'audit_instance', - 'check_subject_references_on_insert_or_update', - 'instance_check_statistical_code_references_on_insert', - 'instance_check_statistical_code_references_on_update', - 'set_id_in_jsonb', - 'set_instance_md_json_trigger', - 'set_instance_md_trigger', - 'set_instance_ol_version_trigger', - 'set_instance_sourcerecordformat', - 'set_instance_status_updated_date', - 'update_instance_references', - 'updatecompleteupdateddate_instance']; - arr UUID[] DEFAULT ARRAY[ - '10000000-0000-0000-0000-000000000000', - '20000000-0000-0000-0000-000000000000', - '30000000-0000-0000-0000-000000000000', - '40000000-0000-0000-0000-000000000000', - '50000000-0000-0000-0000-000000000000', - '60000000-0000-0000-0000-000000000000', - '70000000-0000-0000-0000-000000000000', - '80000000-0000-0000-0000-000000000000', - '90000000-0000-0000-0000-000000000000', - 'a0000000-0000-0000-0000-000000000000', - 'b0000000-0000-0000-0000-000000000000', - 'c0000000-0000-0000-0000-000000000000', - 'd0000000-0000-0000-0000-000000000000', - 'e0000000-0000-0000-0000-000000000000', - 'f0000000-0000-0000-0000-000000000000', - 'ffffffff-ffff-ffff-ffff-ffffffffffff' - ]; - lower UUID; - cur UUID; - rowcount BIGINT; - need_migration BOOLEAN; -BEGIN - -- STEP 0: Check if migration is required - SELECT EXISTS ( - SELECT 1 - FROM ${myuniversity}_${mymodule}.instance - WHERE jsonb ? 'publicationPeriod' - LIMIT 1 - ) INTO need_migration; - - IF need_migration THEN - -- STEP 1: Disable triggers - FOREACH trigger IN ARRAY triggers LOOP - EXECUTE 'ALTER TABLE ${myuniversity}_${mymodule}.instance DISABLE TRIGGER ' - || trigger; - END LOOP; - - -- STEP 2: Do updates - lower := '00000000-0000-0000-0000-000000000000'; - FOREACH cur IN ARRAY arr LOOP - RAISE INFO 'range: % - %', lower, cur; - -- Update scripts - EXECUTE format($q$ - UPDATE ${myuniversity}_${mymodule}.instance - SET jsonb = ${myuniversity}_${mymodule}.migrate_publication_period(jsonb) - WHERE (jsonb -> 'publicationPeriod' IS NOT NULL) - AND (id > %L AND id <= %L); - $q$, lower, cur); - - GET DIAGNOSTICS rowcount = ROW_COUNT; - RAISE INFO 'updated % instances', rowcount; - - lower := cur; - END LOOP; - - -- STEP 3: Enable triggers - FOREACH trigger IN ARRAY triggers LOOP - EXECUTE 'ALTER TABLE ${myuniversity}_${mymodule}.instance ENABLE TRIGGER ' - || trigger; - END LOOP; - END IF; -END; -$$ LANGUAGE 'plpgsql'; - -DROP FUNCTION IF EXISTS ${myuniversity}_${mymodule}.migrate_publication_period(jsonb); diff --git a/src/main/resources/templates/db_scripts/schema.json b/src/main/resources/templates/db_scripts/schema.json index e3cd3a41b..e2c76f3e1 100644 --- a/src/main/resources/templates/db_scripts/schema.json +++ b/src/main/resources/templates/db_scripts/schema.json @@ -1227,6 +1227,11 @@ "run": "after", "snippetPath": "subjectIdsReferenceCheckTrigger.sql", "fromModuleVersion": "27.2.0" + }, + { + "run": "after", + "snippetPath": "publication-period/migratePublicationPeriod.sql", + "fromModuleVersion": "28.0.0" } ] } diff --git a/src/test/java/org/folio/rest/api/AsyncMigrationTest.java b/src/test/java/org/folio/rest/api/AsyncMigrationTest.java index e681a12c5..f7e2c1ff2 100644 --- a/src/test/java/org/folio/rest/api/AsyncMigrationTest.java +++ b/src/test/java/org/folio/rest/api/AsyncMigrationTest.java @@ -10,6 +10,9 @@ import static org.folio.rest.support.http.InterfaceUrls.holdingsStorageUrl; import static org.folio.rest.support.http.InterfaceUrls.instancesStorageUrl; import static org.folio.rest.support.http.InterfaceUrls.itemsStorageUrl; +import static org.folio.services.migration.MigrationName.ITEM_SHELVING_ORDER_MIGRATION; +import static org.folio.services.migration.MigrationName.PUBLICATION_PERIOD_MIGRATION; +import static org.folio.services.migration.MigrationName.SUBJECT_SERIES_MIGRATION; import static org.folio.utility.ModuleUtility.getVertx; import static org.folio.utility.RestUtility.TENANT_ID; import static org.hamcrest.MatcherAssert.assertThat; @@ -19,6 +22,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doReturn; @@ -26,14 +30,18 @@ import io.vertx.core.Context; import io.vertx.core.json.JsonObject; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.stream.IntStream; import junitparams.JUnitParamsRunner; +import lombok.SneakyThrows; import org.apache.commons.collections4.map.CaseInsensitiveMap; import org.folio.persist.AsyncMigrationJobRepository; import org.folio.rest.jaxrs.model.AsyncMigrationJob; @@ -43,18 +51,36 @@ import org.folio.rest.jaxrs.model.EffectiveCallNumberComponents; import org.folio.rest.jaxrs.model.Processed; import org.folio.rest.jaxrs.model.Published; +import org.folio.rest.persist.PostgresClient; import org.folio.rest.persist.PostgresClientFuturized; import org.folio.rest.support.sql.TestRowStream; import org.folio.services.migration.async.AsyncMigrationContext; import org.folio.services.migration.async.ShelvingOrderMigrationJobRunner; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(JUnitParamsRunner.class) public class AsyncMigrationTest extends TestBaseWithInventoryUtil { + private static final String UPDATE_JSONB_WITH_PUB_PERIOD = """ + UPDATE %s_mod_inventory_storage.instance + SET jsonb = jsonb || jsonb_set(jsonb, '{publicationPeriod}', jsonb_build_object('start', '1999', 'end', '2001')) + RETURNING id::text; + """; + private static final String SELECT_JSONB = + "SELECT jsonb FROM %s_mod_inventory_storage.instance"; + private final AsyncMigrationJobRepository repository = getRepository(); + @SneakyThrows + @Before + public void beforeEach() { + StorageTestSuite.deleteAll(itemsStorageUrl("")); + StorageTestSuite.deleteAll(holdingsStorageUrl("")); + StorageTestSuite.deleteAll(instancesStorageUrl("")); + } + private static Map okapiHeaders() { return new CaseInsensitiveMap<>(Map.of(TENANT.toLowerCase(), TENANT_ID)); } @@ -67,7 +93,7 @@ private static AsyncMigrationJob migrationJob() { return new AsyncMigrationJob() .withJobStatus(IN_PROGRESS) .withId(UUID.randomUUID().toString()) - .withMigrations(Collections.singletonList("itemShelvingOrderMigration")) + .withMigrations(Collections.singletonList(ITEM_SHELVING_ORDER_MIGRATION.getValue())) .withSubmittedDate(new Date()); } @@ -83,7 +109,7 @@ public void canMigrateItems() { .withEffectiveCallNumberComponents(new EffectiveCallNumberComponents().withCallNumber("K1 .M44"))))); var migrationJob = asyncMigration.postMigrationJob(new AsyncMigrationJobRequest() - .withMigrations(List.of("itemShelvingOrderMigration"))); + .withMigrations(List.of(ITEM_SHELVING_ORDER_MIGRATION.getValue()))); await().atMost(25, SECONDS).until(() -> asyncMigration.getMigrationJob(migrationJob.getId()) .getJobStatus() == AsyncMigrationJob.JobStatus.COMPLETED); @@ -123,7 +149,48 @@ public void canMigrateInstanceSubjectsAndSeries() { await().atMost(5, SECONDS).until(() -> countDownLatch.getCount() == 0L); var migrationJob = asyncMigration.postMigrationJob(new AsyncMigrationJobRequest() - .withMigrations(List.of("subjectSeriesMigration"))); + .withMigrations(List.of(SUBJECT_SERIES_MIGRATION.getValue()))); + + await().atMost(25, SECONDS).until(() -> asyncMigration.getMigrationJob(migrationJob.getId()) + .getJobStatus() == AsyncMigrationJob.JobStatus.COMPLETED); + + var job = asyncMigration.getMigrationJob(migrationJob.getId()); + + assertThat(job.getPublished().stream().map(Published::getCount) + .mapToInt(Integer::intValue).sum(), is(numberOfRecords)); + assertThat(job.getProcessed().stream().map(Processed::getCount) + .mapToInt(Integer::intValue).sum(), is(numberOfRecords)); + assertThat(job.getJobStatus(), is(AsyncMigrationJob.JobStatus.COMPLETED)); + assertThat(job.getSubmittedDate(), notNullValue()); + } + + @Test + public void canMigrateInstancePublicationPeriod() { + var numberOfRecords = 10; + + IntStream.range(0, numberOfRecords).parallel().forEach(v -> + instancesClient.create(new JsonObject() + .put("title", "test" + v) + .put("source", "MARC") + .put("instanceTypeId", "30fffe0e-e985-4144-b2e2-1e8179bdb41f"))); + + var countDownLatch = new CountDownLatch(1); + var query = String.format(UPDATE_JSONB_WITH_PUB_PERIOD, TENANT_ID); + postgresClient(getContext(), okapiHeaders()).execute(query) + .onSuccess(event -> countDownLatch.countDown()); + // check jsonb contains 'publicationPeriod' data + RowSet selectResult = runSql(String.format(SELECT_JSONB, TENANT_ID)); + + assertEquals(10, selectResult.rowCount()); + JsonObject jsonbData = selectResult.iterator().next().toJson().getJsonObject("jsonb"); + assertNull(jsonbData.getJsonObject("dates")); + assertNotNull(jsonbData.getJsonObject("publicationPeriod")); + + + await().atMost(5, SECONDS).until(() -> countDownLatch.getCount() == 0L); + + var migrationJob = asyncMigration.postMigrationJob(new AsyncMigrationJobRequest() + .withMigrations(List.of(PUBLICATION_PERIOD_MIGRATION.getValue()))); await().atMost(25, SECONDS).until(() -> asyncMigration.getMigrationJob(migrationJob.getId()) .getJobStatus() == AsyncMigrationJob.JobStatus.COMPLETED); @@ -136,20 +203,35 @@ public void canMigrateInstanceSubjectsAndSeries() { .mapToInt(Integer::intValue).sum(), is(numberOfRecords)); assertThat(job.getJobStatus(), is(AsyncMigrationJob.JobStatus.COMPLETED)); assertThat(job.getSubmittedDate(), notNullValue()); + + // check that the 'publicationPeriod' data has been migrated to the 'dates' data + var selectQuery = String.format(SELECT_JSONB, TENANT_ID); + RowSet result = runSql(selectQuery); + + assertEquals(10, result.rowCount()); + JsonObject entry = result.iterator().next().toJson(); + JsonObject jsonb = entry.getJsonObject("jsonb"); + JsonObject dates = jsonb.getJsonObject("dates"); + assertNotNull(dates); + assertNull(jsonb.getString("publicationPeriod")); + assertEquals("1999", dates.getString("date1")); + assertEquals("2001", dates.getString("date2")); + assertEquals("8fa6d067-41ff-4362-96a0-96b16ddce267", dates.getString("dateTypeId")); } @Test public void canGetAvailableMigrations() { AsyncMigrations migrations = asyncMigration.getMigrations(); assertNotNull(migrations); - assertEquals(Integer.valueOf(2), migrations.getTotalRecords()); - assertEquals("itemShelvingOrderMigration", migrations.getAsyncMigrations().get(0).getMigrations().get(0)); + assertEquals(Integer.valueOf(3), migrations.getTotalRecords()); + assertEquals(ITEM_SHELVING_ORDER_MIGRATION.getValue(), + migrations.getAsyncMigrations().get(0).getMigrations().get(0)); } @Test public void canGetAllAvailableMigrationJobs() { asyncMigration.postMigrationJob(new AsyncMigrationJobRequest() - .withMigrations(List.of("itemShelvingOrderMigration"))); + .withMigrations(List.of(ITEM_SHELVING_ORDER_MIGRATION.getValue()))); AsyncMigrationJobCollection migrations = asyncMigration.getAllMigrationJobs(); assertNotNull(migrations); assertFalse(migrations.getJobs().isEmpty()); @@ -167,7 +249,8 @@ public void canCancelMigration() { get(repository.save(migrationJob.getId(), migrationJob).toCompletionStage() .toCompletableFuture()); var amc = new AsyncMigrationContext(getContext(), okapiHeaders(), postgresClientFuturized); - jobRunner().startAsyncMigration(migrationJob, new AsyncMigrationContext(amc, "itemShelvingOrderMigration")); + jobRunner().startAsyncMigration(migrationJob, + new AsyncMigrationContext(amc, ITEM_SHELVING_ORDER_MIGRATION.getValue())); asyncMigration.cancelMigrationJob(migrationJob.getId()); @@ -192,4 +275,13 @@ private ShelvingOrderMigrationJobRunner jobRunner() { private AsyncMigrationJobRepository getRepository() { return new AsyncMigrationJobRepository(getContext(), okapiHeaders()); } + + @SneakyThrows + private RowSet runSql(String sql) { + return PostgresClient.getInstance(getVertx()) + .execute(sql) + .toCompletionStage() + .toCompletableFuture() + .get(TIMEOUT, TimeUnit.SECONDS); + } } diff --git a/src/test/java/org/folio/rest/api/InstanceStorageTest.java b/src/test/java/org/folio/rest/api/InstanceStorageTest.java index 112b70c27..931d33366 100644 --- a/src/test/java/org/folio/rest/api/InstanceStorageTest.java +++ b/src/test/java/org/folio/rest/api/InstanceStorageTest.java @@ -5,6 +5,7 @@ import static java.net.HttpURLConnection.HTTP_NOT_FOUND; import static java.net.HttpURLConnection.HTTP_OK; import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; import static org.folio.rest.support.HttpResponseMatchers.errorMessageContains; import static org.folio.rest.support.HttpResponseMatchers.errorParametersValueIs; import static org.folio.rest.support.HttpResponseMatchers.statusCodeIs; @@ -822,6 +823,9 @@ public void canSearchUsingMetadataDateUpdatedIndex() UUID secondInstanceId = UUID.randomUUID(); JsonObject secondInstanceToCreate = nod(secondInstanceId); + // wait 2 seconds before creating the second instance to have a different "updatedDate" field + // than the first instance. + await().pollDelay(2, SECONDS).until(() -> true); IndividualResource ir = createInstance(secondInstanceToCreate); diff --git a/src/test/java/org/folio/rest/api/PublicationPeriodMigrationTest.java b/src/test/java/org/folio/rest/api/PublicationPeriodMigrationTest.java index f66d4b087..ce7889134 100644 --- a/src/test/java/org/folio/rest/api/PublicationPeriodMigrationTest.java +++ b/src/test/java/org/folio/rest/api/PublicationPeriodMigrationTest.java @@ -18,11 +18,8 @@ import org.folio.rest.persist.PostgresClient; import org.junit.Before; import org.junit.Test; -import org.junit.jupiter.api.Disabled; -@Disabled public class PublicationPeriodMigrationTest extends MigrationTestBase { - private static final String MIGRATION_SCRIPT = loadScript("publication-period/migratePublicationPeriod.sql"); private static final String TAG_VALUE = "test-tag"; private static final String START_DATE = "1877"; private static final String END_DATE = "1880"; @@ -40,6 +37,11 @@ public class PublicationPeriodMigrationTest extends MigrationTestBase { SET jsonb = jsonb_set(jsonb, '{publicationPeriod}', jsonb_build_object('start', $1)) WHERE id = $2 """; + private static final String UPDATE_JSONB_WITH_PUB_PERIOD_MIGRATION= """ + UPDATE %s_mod_inventory_storage.instance + SET jsonb = %s_mod_inventory_storage.migrate_publication_period(jsonb) + WHERE id = $1 AND jsonb -> 'publicationPeriod' IS NOT NULL + """; @SneakyThrows @Before @@ -49,14 +51,15 @@ public void beforeEach() { } @Test - public void canMigratePublicationPeriodToMultipleDates() throws Exception { + public void canMigratePublicationPeriodToMultipleDates() { var instanceId = createInstance(); // add "publicationPeriod" object to jsonb addPublicationPeriodToJsonb(instanceId, END_DATE); //migrate "publicationPeriod" to Dates object - executeMultipleSqlStatements(MIGRATION_SCRIPT); + var updateQuery = String.format(UPDATE_JSONB_WITH_PUB_PERIOD_MIGRATION, TENANT_ID, TENANT_ID); + runSql(updateQuery, Tuple.of(instanceId)); var query = String.format(SELECT_JSONB_BY_ID, TENANT_ID); RowSet result = runSql(query, Tuple.of(instanceId)); @@ -71,14 +74,15 @@ public void canMigratePublicationPeriodToMultipleDates() throws Exception { } @Test - public void canMigratePublicationPeriodToSingleDates() throws Exception { + public void canMigratePublicationPeriodToSingleDates() { var instanceId = createInstance(); // add "publicationPeriod" object to jsonb addPublicationPeriodToJsonb(instanceId, null); //migrate "publicationPeriod" to Dates object - executeMultipleSqlStatements(MIGRATION_SCRIPT); + var updateQuery = String.format(UPDATE_JSONB_WITH_PUB_PERIOD_MIGRATION, TENANT_ID, TENANT_ID); + runSql(updateQuery, Tuple.of(instanceId)); var query = String.format(SELECT_JSONB_BY_ID, TENANT_ID); RowSet result = runSql(query, Tuple.of(instanceId)); @@ -93,11 +97,11 @@ public void canMigratePublicationPeriodToSingleDates() throws Exception { } @Test - public void canNotMigrateWhenPublicationPeriodIsNull() throws Exception { + public void canNotMigrateWhenPublicationPeriodIsNull() { var instanceId = createInstance(); - //migrate "publicationPeriod" to Dates object - executeMultipleSqlStatements(MIGRATION_SCRIPT); + var updateQuery = String.format(UPDATE_JSONB_WITH_PUB_PERIOD_MIGRATION, TENANT_ID, TENANT_ID); + runSql(updateQuery, Tuple.of(instanceId)); var query = String.format(SELECT_JSONB_BY_ID, TENANT_ID); RowSet result = runSql(query, Tuple.of(instanceId)); diff --git a/src/test/java/org/folio/rest/support/GetInstanceStorageInstanceResponseTest.java b/src/test/java/org/folio/rest/support/GetInstanceStorageInstanceResponseTest.java new file mode 100644 index 000000000..1a06daa9b --- /dev/null +++ b/src/test/java/org/folio/rest/support/GetInstanceStorageInstanceResponseTest.java @@ -0,0 +1,65 @@ +package org.folio.rest.support; + +import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static javax.ws.rs.core.MediaType.TEXT_PLAIN; +import static org.folio.HttpStatus.HTTP_BAD_REQUEST; +import static org.folio.HttpStatus.HTTP_INTERNAL_SERVER_ERROR; +import static org.folio.HttpStatus.HTTP_OK; +import static org.folio.HttpStatus.HTTP_UNAUTHORIZED; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.folio.rest.jaxrs.model.Instances; +import org.junit.Test; + +public class GetInstanceStorageInstanceResponseTest { + + @Test + public void shouldRespond200WithApplicationJson() { + var instances = new Instances(); + + var response = GetInstanceStorageInstanceResponse.respond200WithApplicationJson(instances); + + assertNotNull(response); + assertEquals(HTTP_OK.toInt(), response.getStatus()); + assertEquals(APPLICATION_JSON, response.getHeaders().getFirst(CONTENT_TYPE)); + assertEquals(instances, response.getEntity()); + } + + @Test + public void shouldRespond400WithTextPlain() { + var errorMessage = "Bad Request"; + + var response = GetInstanceStorageInstanceResponse.respond400WithTextPlain(errorMessage); + + assertNotNull(response); + assertEquals(HTTP_BAD_REQUEST.toInt(), response.getStatus()); + assertEquals(TEXT_PLAIN, response.getHeaders().getFirst(CONTENT_TYPE)); + assertEquals(errorMessage, response.getEntity()); + } + + @Test + public void shouldRespond401WithTextPlain() { + var errorMessage = "Unauthorized"; + + var response = GetInstanceStorageInstanceResponse.respond401WithTextPlain(errorMessage); + + assertNotNull(response); + assertEquals(HTTP_UNAUTHORIZED.toInt(), response.getStatus()); + assertEquals(TEXT_PLAIN, response.getHeaders().getFirst(CONTENT_TYPE)); + assertEquals(errorMessage, response.getEntity()); + } + + @Test + public void shouldRespond500WithTextPlain() { + var errorMessage = "Internal Server Error"; + + var response = GetInstanceStorageInstanceResponse.respond500WithTextPlain(errorMessage); + + assertNotNull(response); + assertEquals(HTTP_INTERNAL_SERVER_ERROR.toInt(), response.getStatus()); + assertEquals(TEXT_PLAIN, response.getHeaders().getFirst(CONTENT_TYPE)); + assertEquals(errorMessage, response.getEntity()); + } +} diff --git a/src/test/java/org/folio/services/ObjectConverterUtilsTest.java b/src/test/java/org/folio/services/ObjectConverterUtilsTest.java new file mode 100644 index 000000000..85a132ed5 --- /dev/null +++ b/src/test/java/org/folio/services/ObjectConverterUtilsTest.java @@ -0,0 +1,56 @@ +package org.folio.services; + +import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; +import lombok.SneakyThrows; +import org.folio.rest.jaxrs.model.Instance; +import org.folio.rest.jaxrs.model.InstanceWithoutPubPeriod; +import org.folio.rest.jaxrs.model.Instances; +import org.folio.rest.jaxrs.model.InstancesWithoutPubPeriod; +import org.folio.utils.ObjectConverterUtils; +import org.junit.jupiter.api.Test; + +class ObjectConverterUtilsTest { + + private static final String TITLE = "title"; + private static final String ID = "123456789"; + + @Test + void shouldConvertToInstances() { + var instanceWithoutPubPeriod = new InstanceWithoutPubPeriod(); + instanceWithoutPubPeriod.setId(ID); + instanceWithoutPubPeriod.setTitle(TITLE); + var instancesWithoutPubPeriod = new InstancesWithoutPubPeriod(); + instancesWithoutPubPeriod.setInstances(List.of(instanceWithoutPubPeriod)); + + var result = ObjectConverterUtils.convertObject(instancesWithoutPubPeriod, Instances.class); + var instances = result.getInstances(); + + assertNotNull(instances); + assertEquals(1, instances.size()); + assertEquals(instanceWithoutPubPeriod.getId(), instances.get(0).getId()); + assertEquals(instanceWithoutPubPeriod.getTitle(), instances.get(0).getTitle()); + } + + @Test + void shouldConvertToInstance() { + var instanceWithoutPubPeriod = new InstanceWithoutPubPeriod(); + instanceWithoutPubPeriod.setId(ID); + instanceWithoutPubPeriod.setTitle(TITLE); + + var result = ObjectConverterUtils.convertObject(instanceWithoutPubPeriod, Instance.class); + + assertNotNull(result); + assertEquals(instanceWithoutPubPeriod.getId(), result.getId()); + assertEquals(instanceWithoutPubPeriod.getTitle(), result.getTitle()); + } + + @Test + @SneakyThrows + void shouldThrowExceptionWhenCannotConvertToInstance() { + assertThrows(Exception.class, () -> ObjectConverterUtils.convertObject(new Object(), String.class)); + } +}