diff --git a/.github/prompts/review.prompt.md b/.github/prompts/review.prompt.md deleted file mode 100644 index bc115bb7..00000000 --- a/.github/prompts/review.prompt.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -description: "Perform a code review" ---- - -## Code Review Expert: Detailed Analysis and Best Practices - -As a senior software engineer with expertise in code quality, security, and performance optimization, perform a code review of the provided git diff. - -Focus on delivering actionable feedback in the following areas: - -Critical Issues: -- Security vulnerabilities and potential exploits -- Runtime errors and logic bugs -- Performance bottlenecks and optimization opportunities -- Memory management and resource utilization -- Threading and concurrency issues -- Input validation and error handling - -Code Quality: -- Adherence to language-specific conventions and best practices -- Design patterns and architectural considerations -- Code organization and modularity -- Naming conventions and code readability -- Documentation completeness and clarity -- Test coverage and testing approach - -Maintainability: -- Code duplication and reusability -- Complexity metrics (cyclomatic complexity, cognitive complexity) -- Dependencies and coupling -- Extensibility and future-proofing -- Technical debt implications - -Provide specific recommendations with: -- Code examples for suggested improvements -- References to relevant documentation or standards -- Rationale for suggested changes -- Impact assessment of proposed modifications - -Format your review using clear sections and bullet points. Include inline code references where applicable. Output should be in markdown format. - -Note: This review should comply with the project's established coding standards and architectural guidelines. - -Create a .md file with your findings. - -## Constraints - -* **VERY IMPORTANT**: Use `git --no-pager diff --name-only master` to get changed files to be reviewed. -* **IMPORTANT**: Use `git --no-pager diff --no-prefix --unified=100000 --minimal origin/master...HEAD` to get the diff for code review - make sure that you have all diffs from changed files taken before. -* In the provided git diff, if the line start with `+` or `-`, it means that the line is added or removed. If the line starts with a space, it means that the line is unchanged. If the line starts with `@@`, it means that the line is a hunk header. - -* Avoid overwhelming the developer with too many suggestions at once. -* Use clear and concise language to ensure understanding. - -* Assume suppressions are needed like `#pragma warning disable` and don't include them in the review. -* If there are any TODO comments, make sure to address them in the review. - -* Use markdown for each suggestion, like - ``` - # Code Review for ${feature_description} - - Overview of the code changes, including the purpose of the feature, any relevant context, and the files involved. - - # Suggestions - - ## ${code_review_emoji} ${Summary of the suggestion, include necessary context to understand suggestion} - * **Priority**: ${priority: (πŸ”₯/⚠️/🟑/🟒)} - * **File**: ${relative/path/to/file} - * **Details**: ... - * **Example** (if applicable): ... - * **Suggested Change** (if applicable): (code snippet...) - ## (other suggestions...) - ... - - # Summary - ``` -* Use the following emojis to indicate the priority of the suggestions: - * πŸ”₯ Critical - * ⚠️ High - * 🟑 Medium - * 🟒 Low -* Each suggestion should be prefixed with an emoji to indicate the type of suggestion: - * πŸ”§ Change request - * ❓ Question - * ⛏️ Nitpick - * ♻️ Refactor suggestion - * πŸ’­ Thought process or concern - * πŸ‘ Positive feedback - * πŸ“ Explanatory note or fun fact - * 🌱 Observation for future consideration -* Always use file paths - -### Use Code Review Emojis - -Use code review emojis. Give the reviewee added context and clarity to follow up on code review. For example, knowing whether something really requires action (πŸ”§), highlighting nit-picky comments (⛏), flagging out of scope items for follow-up (πŸ“Œ) and clarifying items that don’t necessarily require action but are worth saying ( πŸ‘, πŸ“, πŸ€” ) - -#### Emoji Legend - -| | `:code:` | Meaning | -|:--:|:-------------------:|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| πŸ”§ | `:wrench:` | Use when this needs to be changed. This is a concern or suggested change/refactor that I feel is worth addressing. | -| ❓ | `:question:` | Use when you have a question. This should be a fully formed question with sufficient information and context that requires a response. | -| ⛏ | `:pick:` | This is a nitpick. This does not require any changes and is often better left unsaid. This may include stylistic, formatting, or organization suggestions and should likely be prevented/enforced by linting if they really matter | -| ♻️ | `:recycle:` | Suggestion for refactoring. Should include enough context to be actionable and not be considered a nitpick. | -| πŸ’­ | `:thought_balloon:` | Express concern, suggest an alternative solution, or walk through the code in my own words to make sure I understand. | -| πŸ‘ | `:+1:` | Let the author know that you really liked something! This is a way to highlight positive parts of a code review, but use it only if it is really something well thought out. | -| πŸ“ | `:memo:` | This is an explanatory note, fun fact, or relevant commentary that does not require any action. | -| 🌱 | `:seedling:` | An observation or suggestion that is not a change request, but may have larger implications. Generally something to keep in mind for the future. | \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..ace7d169 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,127 @@ +# AGENTS.md β€” mod-quick-marc + +Spring Boot 4 / Java 21 backend module that provides a REST API for editing MARC bibliographic, holdings, and authority records stored in Source Record Storage (SRS). + +--- + +## Build, test, and lint + +```bash +# Full build (unit + integration tests) +mvn clean install + +# Unit tests only (JUnit tag: unit) +mvn test + +# Integration tests only (JUnit tag: integration) +mvn verify -DskipTests + +# Single unit test class +mvn test -Dtest=MarcUtilsTest + +# Single unit test method +mvn test -Dtest=MarcUtilsTest#normalizeFixedLengthString_ShouldPreserveLeadingBlanks + +# Single integration test class +mvn failsafe:integration-test -Dit.test=RecordsEditorIT + +# Checkstyle +mvn checkstyle:check + +# Run locally with dev profile (auto-starts Docker infra) +mvn spring-boot:run -Dspring-boot.run.profiles=dev +``` + +--- + +## Architecture overview + +``` +REST controllers (org.folio.qm.controller) + └── Service layer (org.folio.qm.service) + β”œβ”€β”€ change/ – create, update record orchestration + β”œβ”€β”€ fetch/ – read record + β”œβ”€β”€ links/ – authority link suggestions + β”œβ”€β”€ validation/ – validate-only path + └── storage/ – thin wrappers around Feign clients + β”œβ”€β”€ source/ – mod-source-record-storage + β”œβ”€β”€ folio/ – mod-inventory-storage, mod-entities-links + β”œβ”€β”€ specification/ – mod-record-specifications + └── tenant/ – user, config lookups + +Conversion pipeline (org.folio.qm.convertion) + β”œβ”€β”€ converter/ – Spring Converter beans wired into ConversionService + β”‚ – SourceRecord β†’ QuickMarcView (MARCβ†’QM direction) + β”‚ – QuickMarcView β†’ marc4j Record (QMβ†’MARC direction) + β”œβ”€β”€ field/dto/ – Tag006/007/008 converters: marc4j ControlField β†’ QM FieldItem + β”œβ”€β”€ field/qm/ – Tag006/007/008 converters: QM FieldItem β†’ marc4j ControlField + └── elements/ – ControlFieldItem enums and Tag008Configuration + +External clients (org.folio.qm.client) – Feign, generated from OpenAPI specs + +Domain DTOs (org.folio.qm.domain.dto) – Generated by openapi-generator; never edit directly +``` + +**Request flow (update):** controller β†’ `UpdateRecordService` β†’ `QuickMarcRecordConverter` (QMβ†’marc4j) β†’ `SourceStorageClient` (PUT to SRS) β†’ `InventoryStorageClient` (entity sync). Response: 202 (synchronous, no Kafka queue). + +**Kafka consumer:** `SpecificationEventListener` listens to `{ENV}.*.specification-storage.specification.updated` and refreshes the `specifications` Caffeine cache. + +--- + +## Key conventions + +### MARC blank masking +MARC blanks (spaces) are represented as `\` (backslash) in the QM JSON model. The canonical constant is `Constants.BLANK_REPLACEMENT = "\\"`. + +- `MarcUtils.masqueradeBlanks(s)` β€” replaces spaces with `\` (MARCβ†’QM direction) +- `MarcUtils.restoreBlanks(s)` β€” replaces `\` with spaces (QMβ†’MARC direction) +- `MarcUtils.normalizeFixedLengthString(s, length)` β€” pads/trims to expected length, then calls `masqueradeBlanks`. Uses `StringUtils.defaultString` (not `trimToEmpty`) so leading blanks are preserved as positional data. + +### DTOs are generated β€” do not edit them +`src/main/java/org/folio/qm/domain/dto/` is produced by `openapi-generator-maven-plugin` from the YAML specs in `src/main/resources/swagger.api/`. Edit the YAML specs, not the generated Java. The generation runs during `generate-sources` phase. + +### Field converter pattern +Each special control field tag (006, 007, 008) has two converters: +- `convertion/field/dto/Tag00X…Converter` β€” MARCβ†’QM (marc4j `ControlField` β†’ `FieldItem`) +- `convertion/field/qm/Tag00X…FieldItemConverter` β€” QMβ†’MARC (`FieldItem` β†’ marc4j `VariableField`) + +Both implement a `canProcess(field, marcFormat)` method for dispatch. New tag converters follow the same pattern and must be annotated `@Component`. + +### Test tagging +- `@UnitTest` (`org.folio.spring.testing.type.UnitTest`) β€” picked up by `maven-surefire-plugin` (tag `unit`) +- `@IntegrationTest` (`org.folio.spring.testing.type.IntegrationTest`) β€” picked up by `maven-failsafe-plugin` (tag `integration`) + +Integration tests all extend `org.folio.it.BaseIT`, which spins up real PostgreSQL, Kafka, and a WireMock Okapi via Testcontainers (`@EnablePostgres`, `@EnableKafka`, `@EnableOkapi`). WireMock stubs live in `src/test/resources/mappings/` and fixtures in `src/test/resources/__files/`. + +### Test data enums +Parametrized converter tests use enum-backed test data: +- `Tag006FieldTestData`, `Tag007FieldTestData`, `Tag008FieldTestData` + +Each enum constant carries `dtoData` (raw MARC string), `leader`, and `qmContent` (expected QM map). Use `@EnumSource` to cover all cases. Cases where the reverse (QMβ†’MARC) conversion intentionally auto-modifies a value (e.g., `Date Entered` auto-fill for blank/invalid dates) must be excluded from round-trip tests via the `names`/`mode` filter on `@EnumSource`. + +### Caches +Four Caffeine caches are declared in `application.yaml`: +- `linking-rules-results` β€” authority linking rules +- `specifications` β€” MARC specifications (custom TTL/size via `folio.cache.spec.specifications.*`) +- `authorities-extended-mapping-cache` +- `consortium-central-tenant-cache` + +The `specifications` cache uses custom config keys (`folio.cache.spec.specifications.ttl=24h`, `folio.cache.spec.specifications.maximum-size=500`), not the global Caffeine spec. + +### Commit and branch conventions +- Branch names follow Jira issue IDs: `MODQM-NNN` +- Commits follow [Conventional Commits](https://folio-org.atlassian.net/wiki/spaces/FOLIJET/pages/1400654/Conventional+Commits+Guideline): `fix(scope): message`, `feat(scope): message`, `docs: message`, etc. + Use a **scope** only when the change is narrowly tied to a single feature ID from [`docs/features/`](docs/features/): + `get-record` Β· `create-record` Β· `update-record` Β· `validate-record` Β· `links-suggestions` Β· `specification-refresh` + Omit the scope when the change spans multiple features or does not belong to any of them. + Examples: `fix(get-record): preserve leading blanks in MARC 008 Date Entered` / `chore: upgrade Spring Boot` +- `NEWS.md` must be updated for every bug fix or feature under the current version section. +- PRs must follow the template in [`.github/PULL_REQUEST_TEMPLATE.md`](.github/PULL_REQUEST_TEMPLATE.md): fill in **Purpose**, **Approach**, and tick all items in the **Changes Checklist** (API changes, schema changes, interface versions, permissions, logging, unit/integration/manual testing, NEWS). + +### Sonar exclusions +`entity`, `client`, and `repository` packages are excluded from SonarQube analysis (see `pom.xml` `sonar.exclusions`). Do not add logic to these packages. + +--- + +## Feature documentation +Behavioral feature docs live in `docs/features/`. Each file uses fixed sections in this order: What it does, Why it exists, Entry point(s), Business rules, Error behavior, Caching, Configuration, Dependencies. Feature names describe observable behavior, not implementation mechanisms. diff --git a/NEWS.md b/NEWS.md index db489960..a6a65567 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,8 @@ +## v8.0.1 2026-05-12 +### Bug fixes +* Ignore `statusUpdatedDate` Instance field that is not mapped from MARC ([MODQM-514](https://folio-org.atlassian.net/browse/MODQM-514)) +* Fix MARC 008 fields shifting left when positions 00-05 contain blank characters ([MODQM-515](https://folio-org.atlassian.net/browse/MODQM-515)) + ## v8.0.0 2026-04-17 ### Breaking changes * Update QuickMARC to use generation field from SRS for optimistic locking ([MODQM-478](https://folio-org.atlassian.net/browse/MODQM-478)) diff --git a/docs/features/create-record.md b/docs/features/create-record.md index b35366f6..608e3375 100644 --- a/docs/features/create-record.md +++ b/docs/features/create-record.md @@ -7,7 +7,7 @@ updated: 2026-04-01 # Create Record ## What it does -Creates a brand-new MARC record together with its corresponding FOLIO inventory entity (Instance, Holdings, or Authority). The endpoint validates the submitted record, derives the inventory entity from the MARC content, persists both the inventory record and the SRS source record, and returns the full `quickMarcView` of the newly created record including the assigned external ID and HRID. +Creates a brand-new MARC record together with its corresponding FOLIO inventory entity (Instance, Holdings, or Authority). The endpoint validates the submitted record, derives the inventory entity from the MARC content, persists both the inventory record and the SRS source record, and returns the full `quickMarcView` of the newly created record including the assigned external ID and HRID. The `201 Created` response contains the full `quickMarcView` of the newly created record, including the assigned `parsedRecordId`, `parsedRecordDtoId`, `externalId`, and `externalHrid`. ## Why it exists Cataloguers need the ability to create original MARC records without importing from an external source. This endpoint provides a single, consistent entry point for creating all three MARC types within the quickMARC editor. @@ -15,24 +15,38 @@ Cataloguers need the ability to create original MARC records without importing f ## Entry point(s) | Method | Path | Description | |--------|------|-------------| -| POST | /records-editor/records | Creates a new MARC record and its linked FOLIO entity | +| POST | /records-editor/records | Creates a new MARC record and its linked FOLIO entity; returns `201 Created` with the full `quickMarcView` | ## Business rules and constraints -- The MARC record is validated before creation. For Bibliographic and Authority formats, validation is run against the MARC specification with the `001 MISSING_FIELD` rule skipped (the 001 field is added automatically after the FOLIO entity is created). Holdings records skip specification-based validation. -- The inventory entity is created first; the resulting external UUID and HRID are stored in the new MARC record. -- Required MARC fields are added automatically after inventory creation: + +#### Sequencing +- **Validation before creation:** The MARC record is validated before creation. For Bibliographic and Authority formats, validation is run against the MARC specification with the `001 MISSING_FIELD` rule skipped (the 001 field is added automatically after the FOLIO entity is created). Holdings records skip specification-based validation. +- **Inventory first:** The inventory entity is created first; the resulting external UUID and HRID are stored in the new MARC record. +- **SRS snapshot first:** Before creating the SRS source record, a snapshot is created in mod-source-record-storage; the source record is written within that snapshot. + +#### Field generation +- **Auto-added fields:** Required MARC fields are added automatically after inventory creation: - A `999 $i` field containing the external entity UUID is added for all record types. - - A `001` field containing the HRID is added for Instance and Holdings records. -- For Instance (Bibliographic) records, `003` fields are removed and `035` fields are normalised before the SRS record is created. -- The SRS record is created with `generation = 0`, `state = ACTUAL`, and `deleted = false`. + - A `001` field containing the HRID is added for **Instance and Holdings** records only (not Authority). +- **Bibliographic (Instance) normalisation:** `003` fields are removed and `035` fields are normalised before SRS creation. If leader position 05 (`LDR/05`) equals `d` (deleted), `staffSuppress` and `discoverySuppress` are set to `true` on the Instance. +- **Holdings normalisation:** `003` fields are removed before SRS creation. +- **Authority:** no field normalisation is applied; `001` is not added (Authority records use `authorityHrid` carried in `999 $i`). + +#### Persistence +- **Initial SRS state:** The SRS record is created with `generation = 0`, `state = ACTUAL`, and `deleted = false`. + +## Error behavior +- **400 Bad Request** – malformed or unreadable request body. +- **422 Unprocessable Content** – MARC validation failed before creation; response includes a `validationResult` payload. Nothing is persisted. +- **500 Internal Server Error** – unexpected server-side failure. ## Dependencies and interactions - **mod-inventory-storage** (instance-storage, holdings-storage) – creates the Instance or Holdings inventory entity and returns the assigned ID and HRID. - **mod-entities-links** (authority-storage) – creates the Authority inventory entity and returns the assigned ID. -- **mod-source-record-storage** – creates the new SRS record containing the raw and parsed MARC content. -- **mod-record-specifications** – MARC field specifications used for validation. +- **mod-source-record-storage** – creates a snapshot then writes the new SRS record containing the raw and parsed MARC content. +- **mod-record-specifications** – MARC field specifications used for validation (Bibliographic and Authority only; Holdings skip specification-based validation). ### Internal feature dependencies - [Validate Record](validate-record.md) – validation logic is invoked for Bibliographic and Authority records before the inventory and SRS records are created; a validation failure results in `422` and nothing is persisted. -- [Specification Cache Refresh](specification-cache-refresh.md) – the MARC specification used during validation is served from the cache that this feature keeps current. +- [Specification Cache Refresh](specification-refresh.md) – the MARC specification used during validation is served from the cache that this feature keeps current. diff --git a/docs/features/get-record.md b/docs/features/get-record.md index ef0be8f8..baf7b5ac 100644 --- a/docs/features/get-record.md +++ b/docs/features/get-record.md @@ -1,13 +1,13 @@ --- feature_id: get-record title: Get Record -updated: 2026-04-01 +updated: 2026-05-06 --- # Get Record ## What it does -Retrieves a MARC record in the quickMARC view format by the UUID of the associated external entity (Instance, Holdings, or Authority). The response includes field-level protection flags and, for Bibliographic records, authority link details populated from the entity-links service. The last editor's user information is also included when available. +Retrieves a MARC record in the quickMARC view format by the UUID of the associated external entity (Instance, Holdings, or Authority). The response includes derived identifiers (`parsedRecordId`, `parsedRecordDtoId`, `externalHrid`), version information (`sourceVersion`), discovery suppression flag, record state and last-update date, field-level protection flags, and β€” for Bibliographic records β€” authority link details populated from the entity-links service. The last editor's user information is also included when available. ## Why it exists The quickMARC editor needs to load an existing MARC record for display and editing. Clients identify a record by its external entity ID (e.g., an Instance UUID) rather than the internal SRS record ID, so this endpoint bridges that gap and enriches the raw record with editor-specific metadata before returning it. @@ -18,13 +18,29 @@ The quickMARC editor needs to load an existing MARC record for display and editi | GET | /records-editor/records | Returns a `quickMarcView` for the given external entity UUID | ## Business rules and constraints -- `externalId` query parameter is required and must be a valid UUID. -- Field protection flags are applied based on MARC field protection settings fetched from the data-import-converter-storage service; each field's `isProtected` property reflects the result. -- Authority link details (authority ID, natural ID, linking rule ID, link status, and error cause) are populated only for Bibliographic (`MARC_BIB`) records; Holdings and Authority records are returned without link data. -- The `updateInfo.updatedBy` field is populated only when the underlying SRS record carries a `metadata.updatedByUserId` and the Users service resolves that ID to a user. + +#### Input validation +- **External ID:** `externalId` query parameter is required and must be a valid UUID. + +#### Response enrichment +- **Field protection:** Flags are applied based on MARC field protection settings fetched from the data-import-converter-storage service; each field's `isProtected` property reflects the result. +- **Authority links:** Details (authority ID, natural ID, linking rule ID, link status, and error cause) are populated only for Bibliographic (`MARC_BIB`) records; Holdings and Authority records are returned without link data. +- **Last editor info:** The `updateInfo.updatedBy` field is populated only when the underlying SRS record carries a `metadata.updatedByUserId` and the Users service resolves that ID to a user. + +#### MARC field handling +- **008 blank characters:** All blank characters in the MARC 008 fixed-length field (including leading ones at positions 00-05, the Date Entered on File) are preserved and represented as `\` in the quickMARC view. Fields are padded or truncated to their expected length using `\` for any missing positions. +- **Fixed-length field lengths:** The 008 field is normalised to **40 characters** for Bibliographic and Authority records, and to **32 characters** for Holdings records. Missing positions are padded with `\`; excess characters are truncated. + +## Error behavior +- **400 Bad Request** – `externalId` query parameter is missing, not a valid UUID, or the request cannot be parsed. +- **404 Not Found** – no source record exists for the given `externalId`. +- **500 Internal Server Error** – unexpected server-side failure. + +## Caching +Authority linking rules used during response enrichment are cached in the `linking-rules-results` Caffeine cache (maximum 500 entries, 1-hour access TTL) keyed by tenant ID. The record itself is not cached. ## Dependencies and interactions -- **mod-source-record-storage** – fetches the raw SRS record by external ID. -- **data-import-converter-storage** (field protection settings) – provides MARC field protection rules applied before the record is returned. -- **mod-entities-links** (links service) – provides existing authority links for Bibliographic records. -- **mod-users** – resolves the user UUID from SRS metadata to a display name. +- **mod-source-record-storage** – fetches the raw SRS record via `GET /source-storage/source-records?externalId=&idType=EXTERNAL`. +- **data-import-converter-storage** – provides MARC field protection rules via `GET /field-protection-settings/marc?limit=1000`; applied before the record is returned. +- **mod-entities-links** (links service) – provides existing authority links for Bibliographic records via `GET /links/instances/{instanceId}`; also fetches linking rules used to populate link details. +- **mod-users** – resolves the `metadata.updatedByUserId` from the SRS record to a display name via `GET /users/{id}`. diff --git a/docs/features/links-suggestions.md b/docs/features/links-suggestions.md index e51d9d6c..cfa82205 100644 --- a/docs/features/links-suggestions.md +++ b/docs/features/links-suggestions.md @@ -7,7 +7,7 @@ updated: 2026-04-01 # Links Suggestions ## What it does -Accepts a Bibliographic MARC record and returns the same record enriched with suggested authority links on eligible fields. The service delegates to the entity-links suggestions API, which analyses the MARC fields against the authority file and returns linking candidates. If no suggestions are returned, the original record is returned unchanged. +Accepts a Bibliographic MARC record and returns the same record enriched with suggested authority links on eligible fields. The quickMARC record is converted to the internal SRS representation, forwarded to the entity-links suggestions API, and the first record in the response is converted back to `quickMarcView` format. If the suggestions API returns no records, the original submitted record is returned unchanged. The `ignoreAutoLinkingEnabled` flag is forwarded directly to the upstream API and is not evaluated locally. ## Why it exists Manually linking MARC bibliographic fields to authority records is time-consuming and error-prone. This endpoint enables the quickMARC editor to offer one-click auto-linking by fetching machine-generated authority link candidates for the fields the cataloguer is editing. @@ -18,15 +18,32 @@ Manually linking MARC bibliographic fields to authority records is time-consumin | POST | /records-editor/links/suggestion | Returns a `quickMarcView` with authority link suggestions applied | ## Business rules and constraints -- The `authoritySearchParameter` query parameter controls which authority field is used to match candidates: `ID` (authority UUID) or `NATURAL_ID` (default). When not provided, `NATURAL_ID` is used. -- The `ignoreAutoLinkingEnabled` flag, when `true`, includes fields regardless of whether their linking rule has auto-linking enabled. Default is `false`. -- The request record is converted to the internal SRS representation before being sent to the entity-links suggestions API. The first record in the response is converted back to `quickMarcView` format. -- If the suggestions response contains no records, the original submitted `quickMarcView` is returned without modification. -- Only fields that match a configured linking rule (from the linking-rules service) receive link details. + +#### Request parameters +- **Authority search parameter:** The `authoritySearchParameter` query parameter controls which authority field is used to match candidates: `ID` (authority UUID) or `NATURAL_ID` (default). When not provided, `NATURAL_ID` is used. +- **Auto-linking flag:** The `ignoreAutoLinkingEnabled` flag, when `true`, includes fields regardless of whether their linking rule has auto-linking enabled. Default is `false`. + +#### Processing +- **Format conversion:** The request record is converted to the internal SRS representation and wrapped in a single-record payload before being sent to the entity-links suggestions API (`POST /links-suggestions/marc`). The first record in the response is converted back to `quickMarcView` format. +- **Linking rules:** Field-to-authority matching and link detail population are governed by linking rules fetched from the entity-links service. Only fields that match a configured rule receive link details; this filtering is performed by the upstream service, not locally. + +#### Response behavior +- **No suggestions fallback:** If the suggestions response contains no records, the original submitted `quickMarcView` is returned without modification. + +## Error behavior +- **400 Bad Request** – malformed or unreadable request body, or missing required parameters. +- **422 Unprocessable Content** – MARC field validation failed. +- **upstream errors** – HTTP errors from the entity-links suggestions API are propagated back to the caller with the same status code. +- **500 Internal Server Error** – unexpected server-side failure. ## Caching -Linking rules are cached in the `linking-rules-results` Caffeine cache (maximum 500 entries, 1-hour access TTL). The rules are fetched from the entity-links service and reused across suggestion requests. +Linking rules are cached in the `linking-rules-results` Caffeine cache keyed by tenant ID. The cache uses a **1-hour access TTL** (`expireAfterAccess=3600s`) with a maximum of 500 entries. Rules are fetched from the entity-links service on first access per tenant and reused across suggestion requests. + +## Configuration (if applicable) +| Variable | Purpose | +|----------|---------| +| `spring.cache.caffeine.spec` | Caffeine spec for all caches including `linking-rules-results` (default: `maximumSize=500,expireAfterAccess=3600s`) | ## Dependencies and interactions -- **mod-entities-links** (links suggestions API at `/marc`) – the primary upstream service; receives the MARC record and returns it with link suggestions applied. -- **mod-entities-links** (linking rules API at `/instance-authority`) – provides the set of rules used to match bibliographic fields to authority records and to populate link details in the response. +- **mod-entities-links** (links suggestions API at `POST /links-suggestions/marc`) – receives the single-record SRS payload and returns it with link suggestions applied. +- **mod-entities-links** (linking rules API at `GET /linking-rules/instance-authority`) – provides the set of rules used to match bibliographic fields to authority records; results are served from the `linking-rules-results` cache. diff --git a/docs/features/specification-refresh.md b/docs/features/specification-refresh.md index 5cfa9b06..db7007ef 100644 --- a/docs/features/specification-refresh.md +++ b/docs/features/specification-refresh.md @@ -7,7 +7,7 @@ updated: 2026-04-01 # Specification Cache Refresh ## What it does -Listens for `specification-storage.specification.updated` Kafka events and refreshes the in-memory MARC specification cache entry for the affected tenant and profile. This ensures that subsequent validation, create, and update operations use the latest field rules without requiring a service restart. +Listens for `specification-storage.specification.updated` Kafka events and refreshes the in-memory MARC specification cache entry for the affected tenant and profile. Before fetching the updated specification, the tenant execution context is reconstructed from the Kafka message headers (`X-Okapi-Tenant`, `X-Okapi-Url`, `X-Okapi-Token`, `X-Okapi-User-Id`), falling back to the `tenantId` field in the event payload. This ensures that subsequent validation, create, and update operations use the latest field rules without requiring a service restart. ## Why it exists MARC specifications are cached to avoid repeated remote calls to the specification-storage service. When a specification is updated externally, the cache must be invalidated and reloaded so that validation reflects the new rules immediately. @@ -26,17 +26,28 @@ MARC specifications are cached to avoid repeated remote calls to the specificati 3. The fetched specification replaces the existing cache entry in the `specifications` cache under the key `{tenantId}:{profile}` (e.g., `diku:BIBLIOGRAPHIC`). Other format entries for the same or different tenants are not affected. 4. If the `specifications` cache is not present in the cache manager (misconfiguration), the update is skipped and a warning is logged. +## Error behavior +- **No retry or dead-letter handling is configured.** If the upstream specification fetch fails, the exception propagates and the cache entry is not updated; the existing cached value remains in use. +- **Missing cache:** if the `specifications` cache is absent from the cache manager (misconfiguration), the refresh is skipped silently and a warning is logged. + ## Business rules and constraints -- Only the single specification identified by `specificationId` is refreshed; all other cache entries remain valid. -- Cache population uses `put` (not evict), so the first request after the event does not experience a cache miss. -- The listener operates with the concurrency configured via `KAFKA_EVENTS_CONCURRENCY` (default: `1`). +- **Targeted refresh:** Only the single specification identified by `specificationId` is refreshed; all other cache entries remain valid. +- **No cache miss:** Cache population uses `put` (not evict), so the first request after the event does not experience a cache miss. +- **Concurrency:** The listener operates with the concurrency configured via `KAFKA_EVENTS_CONCURRENCY` (default: `1`). +- **Cache key format:** entries are stored under the key `{tenantId}:{specificationProfile}` (e.g., `diku:BIBLIOGRAPHIC`). Only the entry matching the event's specification profile is replaced. +- **Auto-commit:** the Kafka consumer is configured with `enable-auto-commit: true` and a 1-second commit interval; there is no manual offset management. -## Configuration +## Configuration (if applicable) | Variable | Purpose | |----------|---------| -| `folio.kafka.listener.specification-updated.topic-pattern` | Regex pattern for the consumed topic; defaults to `(${folio.environment}\.)(.*\.)specification-storage\.specification\.updated` | -| `folio.kafka.listener.specification-updated.group-id` | Consumer group ID; defaults to `${folio.environment}-mod-quick-marc-specification-group` | +| `folio.kafka.listener.specification-updated.topic-pattern` | Regex pattern for the consumed topic (default: `(${folio.environment}\.)(.*\.)specification-storage\.specification\.updated`) | +| `folio.kafka.listener.specification-updated.group-id` | Consumer group ID (default: `${folio.environment}-mod-quick-marc-specification-group`) | +| `folio.kafka.listener.specification-updated.shared-group` | Whether to share the consumer group across instances (default: `false`) | | `KAFKA_EVENTS_CONCURRENCY` | Number of concurrent Kafka listener threads (default: `1`) | +| `spring.kafka.consumer.enable-auto-commit` | Enables auto-commit of consumed offsets (default: `true`) | +| `spring.kafka.consumer.auto-commit-interval` | Offset commit interval when auto-commit is enabled (default: `1000ms`) | +| `folio.cache.spec.specifications.ttl` | TTL for the `specifications` cache (default: `24h`) | +| `folio.cache.spec.specifications.maximum-size` | Maximum entries in the `specifications` cache (default: `500`) | ## Dependencies and interactions - **specification-storage** – the updated specification is fetched by `specificationId` from this service immediately upon receiving the event. diff --git a/docs/features/update-record.md b/docs/features/update-record.md index d2f5f516..adf6fb76 100644 --- a/docs/features/update-record.md +++ b/docs/features/update-record.md @@ -1,13 +1,13 @@ --- feature_id: update-record title: Update Record -updated: 2026-04-01 +updated: 2026-05-06 --- # Update Record ## What it does -Accepts an edited MARC record and persists the changes by updating both the Source Record Storage (SRS) record and the corresponding FOLIO inventory record (Instance, Holdings, or Authority). The operation is asynchronous from the caller's perspective: a `202 Accepted` is returned immediately after the update is enqueued, and the SRS record's generation counter is incremented on each successful save. +Accepts an edited MARC record and persists the changes by updating both the Source Record Storage (SRS) record and the corresponding FOLIO inventory record (Instance, Holdings, or Authority). A `202 Accepted` with an empty body is returned after the update completes. The SRS record's generation counter is incremented on each successful save. ## Why it exists The quickMARC editor is the primary tool for cataloguers to correct or enrich MARC data. Changes must propagate to both the SRS (the bibliographic source of truth) and the linked inventory record to keep the catalogue consistent. Separating the write acknowledgement (202) from completion allows the UI to remain responsive. @@ -18,18 +18,39 @@ The quickMARC editor is the primary tool for cataloguers to correct or enrich MA | PUT | /records-editor/records/{id} | Updates the MARC record identified by the parsed record UUID | ## Business rules and constraints -- The `id` path parameter must equal `parsedRecordId` in the request body; a mismatch results in a `400` error. -- MARC records are validated against the MARC specification before saving. For Bibliographic and Authority formats, validation errors result in a `422` response with a `validationResult` payload listing issues by tag. Holdings records skip specification-based validation. -- An optimistic-locking check compares the `_version` (generation) supplied in the request against the stored generation; a mismatch results in a `409 Conflict`. -- When updating a Bibliographic (Instance) record, authority links embedded in the fields are extracted and written back to the entity-links service after the SRS update. -- Holdings and Instance MARC-to-FOLIO field mapping uses `SET_TO_NULL` merge semantics: fields derived from MARC that are absent in the incoming record will be set to `null` in the inventory record (non-mapped fields such as `acquisitionMethod`, `temporaryLocationId`, `statisticalCodeIds`, etc., are preserved unchanged). -- The `id`, `instanceId` (Holdings), and `version` of the existing FOLIO record are always preserved from the stored record before merge to prevent accidental overwrites. + +#### Input validation +- **ID consistency:** The `id` path parameter must equal `parsedRecordId` in the request body; a mismatch results in a `400` error. +- **Validation:** MARC records are validated against the MARC specification before saving. For Bibliographic and Authority formats, validation errors result in a `422` response with a `validationResult` payload listing issues by tag. Holdings records skip specification-based validation. + +#### Concurrency +- **Optimistic locking:** An optimistic-locking check compares the `_version` (generation) supplied in the request against the stored generation; a mismatch results in a `409 Conflict`. + +#### Data propagation +- **Authority links:** When updating a Bibliographic (Instance) record, authority links embedded in the fields are extracted and written back to the entity-links service after the SRS update. +- **MARC-type normalization:** + - **Bibliographic (Instance):** `003` fields are removed and `035` fields are normalised before saving. If the leader position 05 (`LDR/05`) equals `d` (deleted), `staffSuppress` and `discoverySuppress` are set to `true` on the inventory record. + - **Holdings:** `003` fields are removed before saving. + - **Authority:** no additional field normalization is applied. +- **Inventory merge semantics:** Holdings and Instance MARC-to-FOLIO field mapping uses `SET_TO_NULL` semantics: fields derived from MARC that are absent in the incoming record will be set to `null` in the inventory record (non-mapped fields such as `acquisitionMethod`, `temporaryLocationId`, `statisticalCodeIds`, etc., are preserved unchanged). +- **Protected FOLIO fields:** The `id`, `instanceId` (Holdings), and `version` of the existing FOLIO record are always preserved from the stored record before merge to prevent accidental overwrites. + +#### MARC field handling +- **008 Date Entered auto-fill:** The `Entered` sub-field (Date Entered on File, positions 00-05) is automatically set to the current date if the stored value is absent, all-blank (`\\\\\\\\\\\`), non-numeric, or the sentinel value `000000`. This ensures the field always carries a valid date after a save. + +## Error behavior +- **400 Bad Request** – `id` path parameter does not match `parsedRecordId` in the request body, or the request cannot be parsed. +- **404 Not Found** – source record or FOLIO inventory record not found for the given identifier. +- **409 Conflict** – optimistic-locking version mismatch (`_version` in request differs from stored generation). +- **422 Unprocessable Content** – MARC validation failed; response includes a `validationResult` payload with per-field issues. +- **500 Internal Server Error** – unexpected server-side failure. ## Dependencies and interactions -- **mod-source-record-storage** – reads the current SRS record for version check and writes the updated record. +- **mod-source-record-storage** – creates a snapshot then writes the updated SRS record on every save. - **mod-inventory-storage** (instance-storage, holdings-storage) – updates the corresponding inventory entity. - **mod-entities-links** (authority-storage) – updates the corresponding Authority inventory entity. - **mod-entities-links** (links service, Bibliographic records only) – rewrites authority links after the MARC record is saved. +- **mod-inventory-storage** (preceding-succeeding-titles) – rewrites preceding/succeeding title links after an Instance record is saved. - **mod-record-specifications** – MARC field specifications used for validation. ### Internal feature dependencies diff --git a/docs/features/validate-record.md b/docs/features/validate-record.md index 94cf1995..9dcad1ae 100644 --- a/docs/features/validate-record.md +++ b/docs/features/validate-record.md @@ -18,30 +18,47 @@ The quickMARC editor validates records on demand before the user submits a save, | POST | /records-editor/validate | Validates a `validatableRecord` and returns `validationResult` | ## Business rules and constraints -- The MARC specification used for validation is resolved by `marcFormat` from the request body (BIBLIOGRAPHIC, HOLDINGS, or AUTHORITY). -- Validation is specification-guided: issues are produced by comparing the record's fields, indicators, and subfields against the specification fetched from the specification-storage service. -- Only issues with severity `ERROR` are returned; warnings and informational notices are suppressed. -- Default field values are populated before validation to ensure system-managed fields do not produce false errors. -- Each returned `ValidationIssue` includes a `helpUrl` pointing to the field's specification page when available in the fetched specification. -- For indicator-related error codes (`INVALID_INDICATOR`, `UNDEFINED_INDICATOR`), blank indicators are represented as `\` (backslash) in the message rather than `#`. + +#### Pre-processing +- **Specification resolution:** The MARC specification used for validation is resolved by `marcFormat` from the request body (BIBLIOGRAPHIC, HOLDINGS, or AUTHORITY). +- **Default field population:** Default field values are populated before validation to ensure system-managed fields do not produce false errors. +- **Specification-guided:** Issues are produced by comparing the record's fields, indicators, and subfields against the specification fetched from the specification-storage service. + +#### Format-specific rules +- **Bibliographic:** leader positions `05` (record status), `06` (record type), `07` (bibliographic level), `08` (control type), `18` (cataloging form), and `19` (resource record level) are validated. Fields `008` and `245` must each appear exactly once. +- **Holdings:** `001`, `004`, `008`, and `852` must each appear exactly once. Leader positions are validated against the Holdings leader rule. +- **Authority:** `008` must appear exactly once; exactly one `1XX` field is required, and `010` may appear at most once. Leader positions are validated against the Authority leader rule. +- **All formats:** each field must have exactly two indicators, each a single character; required and unique-tag constraints from the MARC specification are enforced. +- **Validation is format-scoped:** only rules that declare support for the record's `marcFormat` are executed. + +#### Response format +- **Severity filter:** Only issues with severity `ERROR` are returned; warnings and informational notices are suppressed. +- **Help URLs:** Each returned `ValidationIssue` includes a `helpUrl` pointing to the field's specification page when available in the fetched specification. +- **Blank indicators:** For indicator-related error codes (`INVALID_INDICATOR`, `UNDEFINED_INDICATOR`), blank indicators are represented as `\` (backslash) in the message rather than `#`. ## Error behavior - `200 OK` – always returned when validation completes, even if issues are found (issues are in the response body). -- `400 Bad Request` – malformed or missing request body. +- `400 Bad Request` – malformed or unreadable request body. - `500 Internal Server Error` – unexpected server-side failure. ## Caching -The MARC specification is cached per format and tenant using Caffeine (`specifications` cache, maximum 500 entries, 24-hour TTL). Cache entries are refreshed without a miss window when a `specification-storage.specification.updated` Kafka event is received β€” see [Specification Cache Refresh](specification-refresh.md). +The MARC specification is cached in the `specifications` Caffeine cache, keyed by `{tenantId}:{marcFormat}` (e.g., `diku:BIBLIOGRAPHIC`). Cache entries are refreshed in place β€” without a miss window β€” when a `specification-storage.specification.updated` Kafka event is received (see [Specification Cache Refresh](specification-refresh.md)). + +| Property | Value | +|----------|-------| +| `folio.cache.spec.specifications.maximum-size` | `500` (default) | +| `folio.cache.spec.specifications.ttl` | `24h` (default) | +| `spring.cache.caffeine.spec` | `maximumSize=500,expireAfterAccess=3600s` (global default for other caches) | -## Configuration +## Configuration (if applicable) | Variable | Purpose | |----------|---------| -| `spring.cache.caffeine.spec` | Global Caffeine cache specification (default: `maximumSize=500,expireAfterAccess=3600s`) | | `folio.cache.spec.specifications.ttl` | TTL for the `specifications` cache (default: `24h`) | | `folio.cache.spec.specifications.maximum-size` | Maximum entries in the `specifications` cache (default: `500`) | +| `folio.tenant.validation.enabled` | Enables tenant-level validation; must be `true` for specification-based validation to run (default: `true`) | ## Dependencies and interactions -- **mod-record-specifications** – provides the MARC field specification used to validate the record. The specification is consumed via the `SpecificationStorageClient`. +- **mod-record-specifications** – provides the MARC field specification used to validate the record; consumed via `SpecificationStorageClient` and served from the `specifications` cache. ### Internal feature dependencies -- [Specification Cache Refresh](specification-refresh.md) – the MARC specification is served from the cache that this feature keeps current; stale cache entries are replaced in place without a miss window when the specification changes. +- [Specification Cache Refresh](specification-refresh.md) – the MARC specification is served from the cache that this feature keeps current; stale entries are replaced in place when the specification changes. diff --git a/pom.xml b/pom.xml index 403b3503..ce5e37ee 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ org.folio mod-quick-marc - 8.0.1-SNAPSHOT + 8.0.2-SNAPSHOT API for quickMARC - in-app editor for MARC records in SRS jar diff --git a/src/main/java/org/folio/qm/convertion/merger/InstanceRecordMerger.java b/src/main/java/org/folio/qm/convertion/merger/InstanceRecordMerger.java index 5d751c79..5e18d397 100644 --- a/src/main/java/org/folio/qm/convertion/merger/InstanceRecordMerger.java +++ b/src/main/java/org/folio/qm/convertion/merger/InstanceRecordMerger.java @@ -19,6 +19,7 @@ public interface InstanceRecordMerger extends FolioRecordMerger 0 and < 2^31-1"); } - var source = masqueradeBlanks(StringUtils.trimToEmpty(sourceString)); + var source = masqueradeBlanks(StringUtils.defaultString(sourceString)); var sourceLength = source.length(); if (sourceLength == length) { return source; diff --git a/src/test/java/org/folio/qm/convertion/field/qm/Tag008BibliographicFieldItemConverterTest.java b/src/test/java/org/folio/qm/convertion/field/qm/Tag008BibliographicFieldItemConverterTest.java index fd4211e6..3e0fd6e3 100644 --- a/src/test/java/org/folio/qm/convertion/field/qm/Tag008BibliographicFieldItemConverterTest.java +++ b/src/test/java/org/folio/qm/convertion/field/qm/Tag008BibliographicFieldItemConverterTest.java @@ -23,7 +23,7 @@ class Tag008BibliographicFieldItemConverterTest { mode = EnumSource.Mode.EXCLUDE, names = {"HOLDINGS", "HOLDINGS_NO_DATE_ENTERED", "HOLDINGS_WITH_GT_LEN", "HOLDINGS_WITH_LT_LEN", "AUTHORITY", "AUTHORITY_NO_DATE_ENTERED", "AUTHORITY_WITH_GT_LEN", "AUTHORITY_WITH_LT_LEN", - "BIB_BOOKS_WITH_LT_LEN"}) + "BIB_BOOKS_WITH_LT_LEN", "BIB_BOOKS_WITH_BLANK_DATE_ENTERED"}) void testConvertField(Tag008FieldTestData testData) { var actualQmField = converter.convert(new FieldItem().tag("007").content(testData.getQmContent())); assertEquals(testData.getDtoData(), ((ControlField) actualQmField).getData()); diff --git a/src/test/java/org/folio/qm/convertion/merger/InstanceRecordMergerTest.java b/src/test/java/org/folio/qm/convertion/merger/InstanceRecordMergerTest.java index 8d0484dc..6340a7cc 100644 --- a/src/test/java/org/folio/qm/convertion/merger/InstanceRecordMergerTest.java +++ b/src/test/java/org/folio/qm/convertion/merger/InstanceRecordMergerTest.java @@ -23,6 +23,8 @@ class InstanceRecordMergerTest { private static final String SOURCE_NOTE = "source-note"; private static final String TARGET_STATUS_ID = "target-status-id"; private static final String SOURCE_STATUS_ID = "source-status-id"; + private static final String TARGET_STATUS_UPDATED_DATE = "target-status-updated-date"; + private static final String SOURCE_STATUS_UPDATED_DATE = "source-status-updated-date"; private static final String TARGET_STATISTICAL_CODE_ID = "target-statistical-code-id"; private static final String SOURCE_STATISTICAL_CODE_ID = "source-statistical-code-id"; private static final String TARGET_NATURE_OF_CONTENT_ID = "target-nature-of-content-id"; @@ -56,6 +58,7 @@ void merge_positive_ignoredFieldsRemainUnchangedWhenSourceProvidesValues() { source.setNatureOfContentTermIds(List.of(SOURCE_NATURE_OF_CONTENT_ID)); source.setPreviouslyHeld(false); source.setStatusId(SOURCE_STATUS_ID); + source.setStatusUpdatedDate(SOURCE_STATUS_UPDATED_DATE); source.setAdministrativeNotes(List.of(SOURCE_NOTE)); var target = createInstanceRecord(); target.setStaffSuppress(true); @@ -64,6 +67,7 @@ void merge_positive_ignoredFieldsRemainUnchangedWhenSourceProvidesValues() { target.setNatureOfContentTermIds(new LinkedHashSet<>(Set.of(TARGET_NATURE_OF_CONTENT_ID))); target.setPreviouslyHeld(true); target.setStatusId(TARGET_STATUS_ID); + target.setStatusUpdatedDate(TARGET_STATUS_UPDATED_DATE); target.setAdministrativeNotes(List.of(TARGET_NOTE)); // Act @@ -77,6 +81,7 @@ void merge_positive_ignoredFieldsRemainUnchangedWhenSourceProvidesValues() { assertThat(target.getNatureOfContentTermIds()).containsExactly(TARGET_NATURE_OF_CONTENT_ID); assertThat(target.getPreviouslyHeld()).isTrue(); assertThat(target.getStatusId()).isEqualTo(TARGET_STATUS_ID); + assertThat(target.getStatusUpdatedDate()).isEqualTo(TARGET_STATUS_UPDATED_DATE); } @Test diff --git a/src/test/java/org/folio/qm/util/MarcUtilsTest.java b/src/test/java/org/folio/qm/util/MarcUtilsTest.java index 64db655c..ec4e37e5 100644 --- a/src/test/java/org/folio/qm/util/MarcUtilsTest.java +++ b/src/test/java/org/folio/qm/util/MarcUtilsTest.java @@ -28,7 +28,7 @@ private static Stream normalizeFixedLengthString_positive_source() { arguments(null, "\\\\\\\\\\"), arguments("", "\\\\\\\\\\"), arguments("hello", "hello"), - arguments(" hello ", "hello"), + arguments(" hello ", "\\hell"), arguments("this is a very long string", "this\\") ); } diff --git a/src/test/java/org/folio/support/testdata/Tag008FieldTestData.java b/src/test/java/org/folio/support/testdata/Tag008FieldTestData.java index d0c97053..0462923b 100644 --- a/src/test/java/org/folio/support/testdata/Tag008FieldTestData.java +++ b/src/test/java/org/folio/support/testdata/Tag008FieldTestData.java @@ -22,6 +22,8 @@ public enum Tag008FieldTestData { BIB_BOOKS_WITH_EMPTY_ITEMS("123456ghijklmnopqrabcde klmn opstuvw", "00158caa a2200073 4500", getBooksWithEmptyItemsContent()), BIB_BOOKS_WITH_LT_LEN("123456gh", "00158caa a2200073 4500", getBooksWithLtLenContent()), + BIB_BOOKS_WITH_BLANK_DATE_ENTERED(" ghijklmnopqrabcdefghijklmn opstuvw", "00158caa a2200073 4500", + getBooksWithBlankDateEnteredContent()), BIB_CONTINUING("123456ghijklmnopqrab cdefghijk lmstuvw", "00158csb a2200073 4500", getContinuingContent()), BIB_FILED("123456ghijklmnopqr ab c d stuvw", "00158cma a2200073 4500", getFiledContent()), BIB_MAPS("123456ghijklmnopqrabcdef g hi j klstuvw", "00158cea a2200073 4500", getMapsContent()), @@ -270,6 +272,12 @@ private static Map getBooksWithLtLenContent() { return content; } + private static Map getBooksWithBlankDateEnteredContent() { + var content = getBooksNoDateEnteredContent(); + content.put("Entered", "\\\\\\\\\\\\"); // 6 backslashes representing 6 blank positions (MODQM-515) + return content; + } + private static Map getContinuingContent() { Map content = new LinkedHashMap<>(); content.put("Type", "s");