From 0087ec43d06564b69957e486ad5b2bfc20e8b566 Mon Sep 17 00:00:00 2001 From: nebay-abraha Date: Thu, 14 May 2026 14:35:24 +0100 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20bump=20to=201.100.0=20=E2=80=94=20n?= =?UTF-8?q?ew=20models,=20breaking=20cleanups,=20and=20Jackson=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Breaking changes: - AbstractExportPost: rename scope()/format() to getScopeString()/getFormatString() - ApiShareInfo.sharedItemName: corrected type from Long to String - FieldPut: remove shadowed content field; now inherits from FieldPost - ActivitySearchResult, DocumentSearchResult, FileSearchResult, FormSearchResult, ShareSearchResult: change from @Value to @Data @NoArgsConstructor for correct Jackson deserialisation Added: - UserInfo model (sysadmin user-listing endpoint) - UserSearchResult model (paginated UserInfo wrapper) - GroupSearchResult model (paginated GroupInfo wrapper) - AGENTS.md: single source of truth for contributors and AI agents Fixed: - @JsonIgnoreProperties(ignoreUnknown = true) on all response POJOs via IdentifiableNameable and individually on non-hierarchy classes - User.java: replace @Value @NoArgsConstructor with @Data @NoArgsConstructor - ISO8601DateSerialiser: replace per-call SimpleDateFormat with thread-safe static DateTimeFormatter - ApiShareInfo: split multi-field Long declaration; sharedItemName is now a separate String field - Removed stale swagger-codegen DO NOT EDIT headers from source files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 277 ++++++++++++++++++ CHANGELOG.md | 25 +- pom.xml | 2 +- .../api/clientmodel/AbstractExportPost.java | 6 +- .../api/clientmodel/Activity.java | 2 + .../api/clientmodel/ActivitySearchResult.java | 12 +- .../api/clientmodel/ApiFile.java | 24 -- .../api/clientmodel/ApiShareInfo.java | 11 +- .../api/clientmodel/ApiSharingResult.java | 2 + .../api/clientmodel/DocumentInfo.java | 24 -- .../api/clientmodel/DocumentSearchResult.java | 36 +-- .../api/clientmodel/ExportJob.java | 2 + .../api/clientmodel/ExportJobResult.java | 4 +- .../researchspace/api/clientmodel/Field.java | 24 -- .../api/clientmodel/FieldPut.java | 19 +- .../api/clientmodel/FileSearchResult.java | 36 +-- .../researchspace/api/clientmodel/Folder.java | 24 -- .../api/clientmodel/FolderTreeItemInfo.java | 25 -- .../clientmodel/FolderTreeItemListing.java | 4 + .../api/clientmodel/FormSearchResult.java | 18 +- .../api/clientmodel/GroupSearchResult.java | 22 ++ .../api/clientmodel/IdentifiableNameable.java | 4 +- .../api/clientmodel/LinkItem.java | 26 +- .../api/clientmodel/MoveRequest.java | 6 +- .../api/clientmodel/PaginatedResultList.java | 28 +- .../api/clientmodel/ShareSearchResult.java | 36 +-- .../researchspace/api/clientmodel/Status.java | 2 + .../researchspace/api/clientmodel/User.java | 46 +-- .../api/clientmodel/UserGroupInfo.java | 4 + .../api/clientmodel/UserInfo.java | 26 ++ .../api/clientmodel/UserPost.java | 1 + .../api/clientmodel/UserSearchResult.java | 23 ++ .../api/jackson/ISO8601DateSerialiser.java | 49 ++-- .../clientmodel/ActivitySearchResultTest.java | 13 + .../clientmodel/DocumentSearchQueryTest.java | 3 +- .../clientmodel/DocumentSearchResultTest.java | 7 +- .../api/clientmodel/DocumentTest.java | 17 +- .../api/clientmodel/FileSearchResultTest.java | 7 +- .../FolderTreeItemListingTest.java | 40 +++ .../api/clientmodel/FormPostTest.java | 14 +- .../api/clientmodel/GroupInfoTest.java | 55 ++++ .../api/clientmodel/GroupPostTest.java | 12 +- .../api/clientmodel/MoveRequestTest.java | 57 ++++ .../api/clientmodel/UserTest.java | 57 ++++ src/test/resources/FolderTreeItemListing.json | 42 +++ src/test/resources/User.json | 8 + 46 files changed, 821 insertions(+), 361 deletions(-) create mode 100644 AGENTS.md create mode 100644 src/main/java/com/researchspace/api/clientmodel/GroupSearchResult.java create mode 100644 src/main/java/com/researchspace/api/clientmodel/UserInfo.java create mode 100644 src/main/java/com/researchspace/api/clientmodel/UserSearchResult.java create mode 100644 src/test/java/com/researchspace/api/clientmodel/FolderTreeItemListingTest.java create mode 100644 src/test/java/com/researchspace/api/clientmodel/GroupInfoTest.java create mode 100644 src/test/java/com/researchspace/api/clientmodel/MoveRequestTest.java create mode 100644 src/test/java/com/researchspace/api/clientmodel/UserTest.java create mode 100644 src/test/resources/FolderTreeItemListing.json create mode 100644 src/test/resources/User.json diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d950668 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,277 @@ +# AGENTS.md — rspace-client-java-model + +> Single source of truth for AI agents and human contributors. +> When the agent produces incorrect output, make the rule more explicit here. +> Never patch via one-off prompt instructions. + +--- + +## Purpose + +Pure POJO library. No HTTP. No Spring. No business logic. +Provides request and response model classes for the RSpace REST API used by: +- `rspace-api-acceptance` — the API client and acceptance test repo +- `rspace-playwright-java` — the UI E2E test framework (via `ApiUtils`) + +--- + +## Module layout + +``` +src/main/java/ + com.researchspace.api.clientmodel/ ← ELN request + response POJOs + com.researchspace.api.clientmodel.inventory/ ← Inventory base types only + com.researchspace.api.jackson/ ← Custom Jackson serialisers + +src/test/java/ + com.researchspace.api.clientmodel/ ← Unit tests: serialisation + builder contracts +``` + +--- + +## Class taxonomy — know which pattern applies before creating a new class + +### Response POJOs (what the server sends back) + +Used for deserialisation. Must survive unknown fields from future server releases. + +| Class | Extends | Lombok | Extra | +|---|---|---|---| +| `Document` | `DocumentInfo` | `@Data @NoArgsConstructor` | — | +| `DocumentInfo` | `IdentifiableNameable` | `@Data @NoArgsConstructor` | — | +| `Folder` | `IdentifiableNameable` | `@Data @NoArgsConstructor` | — | +| `ApiFile` | `IdentifiableNameable` | `@Data @NoArgsConstructor` | — | +| `Form` | `FormInfo` | `@Data @NoArgsConstructor @SuperBuilder` | `@JsonPropertyOrder` | +| `FormInfo` | `IdentifiableNameable` | `@Data @NoArgsConstructor @SuperBuilder` | `@JsonPropertyOrder` | +| `GroupInfo` | `IdentifiableNameable` | `@Data @NoArgsConstructor` | — | +| `User` | — | **see BUGS section** | — | +| `ActivitySearchResult` | `PaginatedResultList` | `@Value` | — | +| `DocumentSearchResult` | `PaginatedResultList` | `@Value` | — | +| `FileSearchResult` | `PaginatedResultList` | `@Value` | — | +| `FormSearchResult` | `PaginatedResultList` | `@Value` | — | +| `ShareSearchResult` | `PaginatedResultList` | `@Value` | — | + +### Request POJOs (what the client sends to the server) + +Used for serialisation only. `@Builder` pattern for clean construction at call sites. + +| Class | Lombok | +|---|---| +| `DocumentPost` | `@Data @Builder` + `@Singular` on `fields` | +| `FolderPost` | `@Data @Builder` | +| `FormPost.Form` (inner) | `@Data @Builder` + `@Singular` on `fields` | +| `GroupPost` | `@Data @AllArgsConstructor @NoArgsConstructor @Builder` + `@Singular` on `users` | +| `UserPost` | `@Data @AllArgsConstructor @NoArgsConstructor @Builder` | +| `SharePost` | `@Data @Builder @AllArgsConstructor @NoArgsConstructor` | +| `MoveRequest` | `@Data @NoArgsConstructor @AllArgsConstructor @Builder` | + +### Inheritance base classes — never instantiate directly + +``` +Linkable ← _links list + getLinkByType() + └── PaginatedResultList ← totalHits, pageNumber + └── IdentifiableNameable ← id, globalId, name + └── DocumentInfo ← created, lastModified, tags, form, owner, parentFolderId + └── Document ← fields: List + └── FormInfo ← stableId, version, formState, accessControl, tags + └── Form ← fields: List + └── GroupInfo ← type, sharedFolderId, members + └── ApiFile ← contentType, size, caption, created, version + └── RecordInfo (inventory) ← description, tags, sharingMode, barcodes +``` + +### Form field hierarchy — polymorphic deserialisation via `@JsonTypeInfo` + +`FormField` is abstract and uses `@JsonSubTypes` to deserialise by `"type"` property. +Concrete types: `StringFormField`, `TextFormField`, `NumberFormField`, +`RadioFormField`, `ChoiceFormField`, `DateFormField`, `TimeFormField`. + +When deserialising a `Form` containing fields, Jackson dispatches on the `"type"` string. +The discriminator values are capitalised: `"String"`, `"Text"`, `"Number"`, `"Radio"`, +`"Choice"`, `"Date"`, `"Time"`. + +`FormPost.FormFieldPost` (inner class hierarchy) is the **request** counterpart — +it has the same field names but `@Builder` constructors and no `@JsonSubTypes`. +The two hierarchies are parallel — never mix them. + +### Inventory types — split across two packages + +``` +com.researchspace.api.clientmodel.inventory/ ← in THIS repo + RecordInfo ← abstract base for all inventory items + Barcode ← barcode attached to an item + SharedWith ← group-level sharing details + TagInfo ← ontology tag + +com.researchspace.api.acceptance.invmodel/ ← in rspace-api-acceptance + RSSample extends RecordInfo + RSInvContainer extends RecordInfo + Subsample extends RecordInfo + SamplePost extends RecordInfo + ApiSampleTemplate + ... (all concrete inventory types) +``` + +Never put concrete inventory response types (`RSSample`, `RSInvContainer` etc.) +in this repo — they belong in `rspace-api-acceptance`. +Only add to `inventory/` if it is a base type used by multiple concrete classes. + +--- + +## Lombok rules + +**Response POJOs** use: `@Data @NoArgsConstructor` (+ `@EqualsAndHashCode(callSuper=true)` for subclasses) +**Request POJOs** use: `@Data @Builder @NoArgsConstructor @AllArgsConstructor` +**Abstract base classes** use: `@Data @NoArgsConstructor @SuperBuilder` +**Immutable search results** use: `@Value @EqualsAndHashCode(callSuper=true)` — see note below + +### `@Value` vs `@Data` — know the difference + +`@Value` generates an immutable class: all fields `final`, no setters, all-args constructor. +Jackson cannot deserialise into a `@Value` class without a custom deserialiser because +there are no setters and the all-args constructor requires every field by position. + +**Only use `@Value` for classes where you are certain Jackson will never try to deserialise them.** + +Current `@Value` classes: `ActivitySearchResult`, `DocumentSearchResult`, +`FileSearchResult`, `FormSearchResult`, `ShareSearchResult`. +These work because `AbstractModelTest.readFileToClass()` disables `FAIL_ON_UNKNOWN_PROPERTIES` +and the fields are initialised to `new ArrayList<>()` which Jackson appends to. +This is fragile — see IMPROVEMENT.md item 1.1 for the correct fix. + +**`User.java` uses `@Value @NoArgsConstructor` which is a bug — see IMPROVEMENT.md item 1.3.** + +### `@Singular` — use for list fields in request builder classes + +`@Singular` on a `List` field in a `@Builder` class allows single-item `.fieldName(item)` +chaining in the builder. Used in: `DocumentPost.fields`, `GroupPost.users`, +`ActivitySearchQuery.domains/actions/usernames`, `SharePost.itemsToShare/groups/users`. + +```java +// Correct — @Singular allows this +DocumentPost.builder() + .field(new FieldPost("content1")) + .field(new FieldPost("content2")) + .build(); + +// Without @Singular you'd need .fields(List.of(...)) +``` + +--- + +## Jackson rules + +### Always verify new fields with `AbstractModelTest.readFileToClass()` + +Every new response POJO field must be verified by: +1. Finding or creating a JSON fixture in `src/test/resources/` +2. Writing a test that deserialises the fixture and asserts the field value +3. Confirming the field name matches exactly what the server returns + +The `AbstractModelTest` configures Jackson with: +- `READ_ENUMS_USING_TO_STRING` — enums deserialise via `toString()` not `name()` +- `WRITE_ENUMS_USING_TO_STRING` +- `FAIL_ON_UNKNOWN_PROPERTIES` disabled + +The production `BaseApiClientImpl` in `rspace-api-acceptance` configures the same +enum handling but does NOT yet disable `FAIL_ON_UNKNOWN_PROPERTIES` — see +`rspace-api-acceptance` IMPROVEMENT.md item 1.2. + +### `ISO8601DateSerialiser` — use for date fields sent TO the server + +`DateFormField`, `DateFieldPost` use `@JsonSerialize(using = ISO8601DateSerialiser.class)` +to serialise `Date` values as `"yyyy-MM-dd"` strings. +Use this serialiser for any new date field that the server expects in that format. +Do not use it for response date fields — those are deserialised as standard epoch millis. + +### `@JsonProperty("_links")` — already on `Linkable` + +`Linkable.links` is already annotated `@JsonProperty("_links")`. +Do not re-annotate it in subclasses. + +### `@JsonPropertyOrder` — use for types that appear in Swagger examples + +`Form`, `FormInfo`, and all `FormField` subclasses have `@JsonPropertyOrder` so that +serialised JSON matches the Swagger documentation order. Add `@JsonPropertyOrder` +to new types that will appear in API documentation or test fixtures. + +--- + +## Validation annotations + +`@Size`, `@NotNull`, `@Pattern`, `@Min` from `javax.validation` are declared on +**request POJOs** only — they document what the server expects but are not enforced +client-side (there is no validation runner). They serve as in-code documentation. + +The dependency is `javax.validation:validation-api:1.1.0.Final`. Do not upgrade to +`jakarta.validation` — this would break compatibility with Spring 5 in `rspace-api-acceptance`. + +--- + +## Testing rules + +All tests use **JUnit 5** (`org.junit.jupiter.api.Test`, `@BeforeEach`, `@AfterEach`). +This is consistent throughout the test suite — do not use JUnit 4 here. + +### Two test patterns — both are in use + +**JSON fixture roundtrip** (via `AbstractModelTest`): +```java +// Place fixture in src/test/resources/MyType.json +// Extend AbstractModelTest +MyType result = readFileToClass(new File("src/test/resources/MyType.json"), MyType.class); +assertNotNull(result.getSomeField()); +assertEquals("expectedValue", result.getSomeField()); +``` + +**Serialisation roundtrip** (via inline `ObjectMapper`): +```java +// Used in FormFieldTest, AccessControlTest — tests that classes serialise correctly +ObjectMapper om = new ObjectMapper(); +om.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); +String json = om.writeValueAsString(myObject); +MyType deserialized = om.readValue(json, MyType.class); +assertEquals(expected, deserialized.getSomeField()); +``` + +### Every new POJO needs at least one test + +Minimum: a test that verifies the class can be deserialised from a representative JSON fixture. +If the class has builder methods (`addField()`, `appendContent()`, `appendFileReference()`), +test those too. + +### JSON fixtures live in `src/test/resources/` + +Existing fixtures: `Document.json`, `DocumentSearchResult.json`, `DocumentSearchQuery.json`, +`ActivitySearchResult.json`, `FileSearchResult.json`, `File.json`, `Folder.json`, +`FormSearchResult.json`, `Form.json`, `completedJob.json`. + +When creating a fixture: use real server response JSON where possible, or construct +a minimal valid JSON that exercises the fields you care about. + +--- + +## Adding a new class + +1. **Decide which category** it belongs to (response POJO, request POJO, base class, inventory base) +2. **Choose Lombok annotations** from the taxonomy table above — do not invent new combinations +3. **Add `@JsonIgnoreProperties(ignoreUnknown = true)`** on response POJOs — see IMPROVEMENT.md +4. **Add a JSON fixture** in `src/test/resources/` or use serialisation roundtrip test +5. **Write a unit test** in `src/test/java/com/researchspace/api/clientmodel/` +6. **Verify against dev source** before adding fields not in the current Swagger: + `https://github.com/rspace-os/rspace-web/tree/main/src/main/java/com/researchspace/api/v1` + +--- + +## What the agent MUST NOT do + +- Add Spring, RestTemplate, or HTTP dependencies to this module +- Add concrete inventory response types (`RSSample`, `RSInvContainer` etc.) — those belong in `rspace-api-acceptance` +- Use `@Value` on a new class that needs Jackson deserialisation +- Copy the swagger-codegen `DO NOT EDIT` header comment — those files have been edited +- Add a field to a response POJO without a corresponding test that verifies it deserialises +- Use `jakarta.validation` annotations — use `javax.validation` only +- Upgrade `validation-api` past `1.1.0.Final` without checking compatibility +- Create new Lombok annotation combinations not in the taxonomy table above + +> Last updated: 2026-05-05 diff --git a/CHANGELOG.md b/CHANGELOG.md index e508bb8..6b9e682 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,29 @@ # Changelog All notable changes to this project will be documented in this file. +## [1.100.0] + +### Breaking Changes + +- **`AbstractExportPost`**: renamed `scope()` → `getScopeString()` and `format()` → `getFormatString()` to avoid confusion with Lombok-generated getters. Update any callers of the old method names. +- **`ApiShareInfo.sharedItemName`**: field type corrected from `Long` to `String`. Any code storing or passing `getSharedItemName()` as a `Long` must be updated. +- **`FieldPut`**: removed the shadowed `content` field; `FieldPut` now inherits `content` from `FieldPost`. The `@AllArgsConstructor`-generated constructor signature changes from `FieldPut(String content, Long id)` to `FieldPut(Long id)`. Replace any positional constructor calls with the no-args constructor + `setContent()`. +- **`ActivitySearchResult`, `DocumentSearchResult`, `FileSearchResult`, `FormSearchResult`, `ShareSearchResult`**: changed from `@Value` (immutable, all-args constructor) to `@Data @NoArgsConstructor` (mutable, setters). These are response POJOs and should not be constructed directly by callers, but any code that relied on the `@Value`-generated constructor will no longer compile. + +### Added + +- `UserInfo` model — full user information as returned by the sysadmin user-listing endpoint. +- `UserSearchResult` model — paginated wrapper for `UserInfo` lists. +- `GroupSearchResult` model — paginated wrapper for `GroupInfo` lists returned by the sysadmin group-listing endpoint. + +### Fixed + +- `@JsonIgnoreProperties(ignoreUnknown = true)` added to all response POJOs (via `IdentifiableNameable` and individually on non-hierarchy classes). Prevents `UnrecognizedPropertyException` when the server adds new fields in future releases. +- `User.java`: replaced contradictory `@Value @NoArgsConstructor` combination with `@Data @NoArgsConstructor`. All six fields now deserialise correctly from JSON. +- Removed stale swagger-codegen `DO NOT EDIT` headers from 12 source files that have been manually maintained. +- `ISO8601DateSerialiser`: replaced per-call `SimpleDateFormat` allocation with a thread-safe static `DateTimeFormatter`. +- `ApiShareInfo`: split the multi-field `Long` declaration; `sharedItemName` is now a separate `String` field. + ## [1.99.1] ### Added @@ -32,7 +55,7 @@ All notable changes to this project will be documented in this file. ## [1.98.1] ### Added -- Introduced support for document: +- Introduced support for a document: - `MoveRequest` model ### Changed diff --git a/pom.xml b/pom.xml index 628e493..deacf73 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 rspace-client-java-model - 1.99.1 + 1.100.0 rspace-client-java-model com.github.rspace-os diff --git a/src/main/java/com/researchspace/api/clientmodel/AbstractExportPost.java b/src/main/java/com/researchspace/api/clientmodel/AbstractExportPost.java index e11721d..fbd2ac2 100644 --- a/src/main/java/com/researchspace/api/clientmodel/AbstractExportPost.java +++ b/src/main/java/com/researchspace/api/clientmodel/AbstractExportPost.java @@ -12,11 +12,11 @@ public class AbstractExportPost { private ExportScope scope = ExportScope.USER; private ExportFormat format = ExportFormat.HTML; - public String scope () { + public String getScopeString() { return scope.name().toLowerCase(); } - - public String format () { + + public String getFormatString() { return format.name().toLowerCase(); } diff --git a/src/main/java/com/researchspace/api/clientmodel/Activity.java b/src/main/java/com/researchspace/api/clientmodel/Activity.java index 4b6d82c..b1cac6f 100644 --- a/src/main/java/com/researchspace/api/clientmodel/Activity.java +++ b/src/main/java/com/researchspace/api/clientmodel/Activity.java @@ -3,6 +3,7 @@ import java.util.Date; import java.util.Map; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; import lombok.EqualsAndHashCode; /** @@ -12,6 +13,7 @@ */ @Data @EqualsAndHashCode(of={"timestamp", "username", "domain","action"}) +@JsonIgnoreProperties(ignoreUnknown = true) public class Activity { private Date timestamp; diff --git a/src/main/java/com/researchspace/api/clientmodel/ActivitySearchResult.java b/src/main/java/com/researchspace/api/clientmodel/ActivitySearchResult.java index 28261fd..8d5becd 100644 --- a/src/main/java/com/researchspace/api/clientmodel/ActivitySearchResult.java +++ b/src/main/java/com/researchspace/api/clientmodel/ActivitySearchResult.java @@ -4,16 +4,20 @@ import java.util.ArrayList; import java.util.List; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; import lombok.ToString; -import lombok.Value; /** * ActivitySearchResults */ -@Value -@EqualsAndHashCode(callSuper=true) -@ToString(callSuper=true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) public class ActivitySearchResult extends PaginatedResultList { private List activities = new ArrayList<>(); diff --git a/src/main/java/com/researchspace/api/clientmodel/ApiFile.java b/src/main/java/com/researchspace/api/clientmodel/ApiFile.java index 0d9ed5d..5f436f0 100644 --- a/src/main/java/com/researchspace/api/clientmodel/ApiFile.java +++ b/src/main/java/com/researchspace/api/clientmodel/ApiFile.java @@ -1,27 +1,3 @@ -/** - * RSpace API - * Access your RSpace documents programmatically. All requests require authentication using an API key set as the value of the header `RSpace-API-Key`. - * - * OpenAPI spec version: 0.1 - * Contact: support@researchspace.com - * - * NOTE: This class is auto generated by the swagger code generator program. - * https://github.com/swagger-api/swagger-codegen.git - * Do not edit the class manually. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package com.researchspace.api.clientmodel; import java.util.Date; diff --git a/src/main/java/com/researchspace/api/clientmodel/ApiShareInfo.java b/src/main/java/com/researchspace/api/clientmodel/ApiShareInfo.java index 1baf4cb..5df4ddd 100644 --- a/src/main/java/com/researchspace/api/clientmodel/ApiShareInfo.java +++ b/src/main/java/com/researchspace/api/clientmodel/ApiShareInfo.java @@ -2,6 +2,7 @@ +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @@ -13,10 +14,14 @@ @Data @EqualsAndHashCode(callSuper = false) @NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) public class ApiShareInfo { - - private Long id, sharedItemId,sharedItemName; - private String sharedTargetType, permission; + + private Long id; + private Long sharedItemId; + private String sharedItemName; + private String sharedTargetType; + private String permission; } diff --git a/src/main/java/com/researchspace/api/clientmodel/ApiSharingResult.java b/src/main/java/com/researchspace/api/clientmodel/ApiSharingResult.java index 306a570..f5385ce 100644 --- a/src/main/java/com/researchspace/api/clientmodel/ApiSharingResult.java +++ b/src/main/java/com/researchspace/api/clientmodel/ApiSharingResult.java @@ -2,6 +2,7 @@ import java.util.List; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @@ -9,6 +10,7 @@ @Data @EqualsAndHashCode(callSuper = false) @NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) public class ApiSharingResult { diff --git a/src/main/java/com/researchspace/api/clientmodel/DocumentInfo.java b/src/main/java/com/researchspace/api/clientmodel/DocumentInfo.java index 7922cb4..77c8985 100644 --- a/src/main/java/com/researchspace/api/clientmodel/DocumentInfo.java +++ b/src/main/java/com/researchspace/api/clientmodel/DocumentInfo.java @@ -1,27 +1,3 @@ -/** - * RSpace API - * Access your RSpace documents programmatically. All requests require authentication using an API key set as the value of the header `RSpace-API-Key`. - * - * OpenAPI spec version: 0.1 - * Contact: support@researchspace.com - * - * NOTE: This class is auto generated by the swagger code generator program. - * https://github.com/swagger-api/swagger-codegen.git - * Do not edit the class manually. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package com.researchspace.api.clientmodel; import java.util.Date; diff --git a/src/main/java/com/researchspace/api/clientmodel/DocumentSearchResult.java b/src/main/java/com/researchspace/api/clientmodel/DocumentSearchResult.java index f98bb72..bbbb924 100644 --- a/src/main/java/com/researchspace/api/clientmodel/DocumentSearchResult.java +++ b/src/main/java/com/researchspace/api/clientmodel/DocumentSearchResult.java @@ -1,42 +1,22 @@ -/** - * RSpace API - * Access your RSpace documents programmatically. All requests require authentication using an API key set as the value of the header `RSpace-API-Key`. - * - * OpenAPI spec version: 0.1 - * Contact: support@researchspace.com - * - * NOTE: This class is auto generated by the swagger code generator program. - * https://github.com/swagger-api/swagger-codegen.git - * Do not edit the class manually. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package com.researchspace.api.clientmodel; import java.util.ArrayList; import java.util.List; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; import lombok.ToString; -import lombok.Value; /** * DocumentSearchResults */ -@Value -@EqualsAndHashCode(callSuper=true) -@ToString(callSuper=true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) public class DocumentSearchResult extends PaginatedResultList { List documents = new ArrayList<>(); diff --git a/src/main/java/com/researchspace/api/clientmodel/ExportJob.java b/src/main/java/com/researchspace/api/clientmodel/ExportJob.java index fe1616c..5498e7c 100644 --- a/src/main/java/com/researchspace/api/clientmodel/ExportJob.java +++ b/src/main/java/com/researchspace/api/clientmodel/ExportJob.java @@ -1,5 +1,6 @@ package com.researchspace.api.clientmodel; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; import lombok.EqualsAndHashCode; /** @@ -9,6 +10,7 @@ */ @Data @EqualsAndHashCode(callSuper=true) +@JsonIgnoreProperties(ignoreUnknown = true) public class ExportJob extends Job { private ExportJobResult result; diff --git a/src/main/java/com/researchspace/api/clientmodel/ExportJobResult.java b/src/main/java/com/researchspace/api/clientmodel/ExportJobResult.java index 4169c1f..36d3154 100644 --- a/src/main/java/com/researchspace/api/clientmodel/ExportJobResult.java +++ b/src/main/java/com/researchspace/api/clientmodel/ExportJobResult.java @@ -2,9 +2,8 @@ import java.util.Date; -import lombok.AccessLevel; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; -import lombok.Setter; /** * An export job result @@ -13,6 +12,7 @@ * */ @Data +@JsonIgnoreProperties(ignoreUnknown = true) public class ExportJobResult implements Result { private String checksum; diff --git a/src/main/java/com/researchspace/api/clientmodel/Field.java b/src/main/java/com/researchspace/api/clientmodel/Field.java index d2903f4..7f46d2b 100644 --- a/src/main/java/com/researchspace/api/clientmodel/Field.java +++ b/src/main/java/com/researchspace/api/clientmodel/Field.java @@ -1,27 +1,3 @@ -/** - * RSpace API - * Access your RSpace documents programmatically. All requests require authentication using an API key set as the value of the header `RSpace-API-Key`. - * - * OpenAPI spec version: 0.1 - * Contact: support@researchspace.com - * - * NOTE: This class is auto generated by the swagger code generator program. - * https://github.com/swagger-api/swagger-codegen.git - * Do not edit the class manually. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package com.researchspace.api.clientmodel; import java.util.ArrayList; diff --git a/src/main/java/com/researchspace/api/clientmodel/FieldPut.java b/src/main/java/com/researchspace/api/clientmodel/FieldPut.java index 4d497ad..c1c6cb4 100644 --- a/src/main/java/com/researchspace/api/clientmodel/FieldPut.java +++ b/src/main/java/com/researchspace/api/clientmodel/FieldPut.java @@ -1,6 +1,5 @@ package com.researchspace.api.clientmodel; -import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @@ -8,22 +7,28 @@ @Data @EqualsAndHashCode(callSuper=false) -@AllArgsConstructor @NoArgsConstructor /** - * Extends FieldPost with a field Id property to specify the Field whose content is to be updated. + * Extends FieldPost with a field Id property to specify the Field, whose content is to be updated. * @author rspace * */ public class FieldPut extends FieldPost { - /** - * Optional content, can be empty. - */ - private String content=""; /** * Cannot be null */ @NonNull private Long id; + /** Constructor for updating a specific field by ID only (content left as default). */ + public FieldPut(Long id) { + this.id = id; + } + + /** Constructor for updating the content of a specific field by ID. */ + public FieldPut(String content, Long id) { + super(content); + this.id = id; + } + } diff --git a/src/main/java/com/researchspace/api/clientmodel/FileSearchResult.java b/src/main/java/com/researchspace/api/clientmodel/FileSearchResult.java index 2f51b2a..237309d 100644 --- a/src/main/java/com/researchspace/api/clientmodel/FileSearchResult.java +++ b/src/main/java/com/researchspace/api/clientmodel/FileSearchResult.java @@ -1,42 +1,22 @@ -/** - * RSpace API - * Access your RSpace documents programmatically. All requests require authentication using an API key set as the value of the header `RSpace-API-Key`. - * - * OpenAPI spec version: 0.1 - * Contact: support@researchspace.com - * - * NOTE: This class is auto generated by the swagger code generator program. - * https://github.com/swagger-api/swagger-codegen.git - * Do not edit the class manually. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package com.researchspace.api.clientmodel; import java.util.ArrayList; import java.util.List; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; import lombok.EqualsAndHashCode; -import lombok.Value; +import lombok.NoArgsConstructor; /** * FileSearchResult */ -@Value -@EqualsAndHashCode(callSuper=true) +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) public class FileSearchResult extends PaginatedResultList { - private List files = new ArrayList(); + private List files = new ArrayList<>(); } diff --git a/src/main/java/com/researchspace/api/clientmodel/Folder.java b/src/main/java/com/researchspace/api/clientmodel/Folder.java index fc36300..2ab7349 100644 --- a/src/main/java/com/researchspace/api/clientmodel/Folder.java +++ b/src/main/java/com/researchspace/api/clientmodel/Folder.java @@ -1,27 +1,3 @@ -/** - * RSpace API - * Access your RSpace documents programmatically. All requests require authentication using an API key set as the value of the header `RSpace-API-Key`. - * - * OpenAPI spec version: 0.1 - * Contact: support@researchspace.com - * - * NOTE: This class is auto generated by the swagger code generator program. - * https://github.com/swagger-api/swagger-codegen.git - * Do not edit the class manually. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package com.researchspace.api.clientmodel; import java.util.Date; diff --git a/src/main/java/com/researchspace/api/clientmodel/FolderTreeItemInfo.java b/src/main/java/com/researchspace/api/clientmodel/FolderTreeItemInfo.java index b39947a..4ad4751 100644 --- a/src/main/java/com/researchspace/api/clientmodel/FolderTreeItemInfo.java +++ b/src/main/java/com/researchspace/api/clientmodel/FolderTreeItemInfo.java @@ -2,31 +2,6 @@ import java.util.Date; -/** - * RSpace API - * Access your RSpace documents programmatically. All requests require authentication using an API key set as the value of the header `RSpace-API-Key`. - * - * OpenAPI spec version: 0.1 - * Contact: support@researchspace.com - * - * NOTE: This class is auto generated by the swagger code generator program. - * https://github.com/swagger-api/swagger-codegen.git - * Do not edit the class manually. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; diff --git a/src/main/java/com/researchspace/api/clientmodel/FolderTreeItemListing.java b/src/main/java/com/researchspace/api/clientmodel/FolderTreeItemListing.java index 4fde3fa..9f392e5 100644 --- a/src/main/java/com/researchspace/api/clientmodel/FolderTreeItemListing.java +++ b/src/main/java/com/researchspace/api/clientmodel/FolderTreeItemListing.java @@ -3,13 +3,17 @@ import java.util.ArrayList; import java.util.List; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; @Data @EqualsAndHashCode(callSuper=true) +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) public class FolderTreeItemListing extends PaginatedResultList { /** diff --git a/src/main/java/com/researchspace/api/clientmodel/FormSearchResult.java b/src/main/java/com/researchspace/api/clientmodel/FormSearchResult.java index 1093974..fbfe0c9 100644 --- a/src/main/java/com/researchspace/api/clientmodel/FormSearchResult.java +++ b/src/main/java/com/researchspace/api/clientmodel/FormSearchResult.java @@ -1,15 +1,19 @@ package com.researchspace.api.clientmodel; -import lombok.EqualsAndHashCode; -import lombok.ToString; -import lombok.Value; - import java.util.ArrayList; import java.util.List; -@Value -@EqualsAndHashCode(callSuper=true) -@ToString(callSuper=true) +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) public class FormSearchResult extends PaginatedResultList { List forms = new ArrayList<>(); diff --git a/src/main/java/com/researchspace/api/clientmodel/GroupSearchResult.java b/src/main/java/com/researchspace/api/clientmodel/GroupSearchResult.java new file mode 100644 index 0000000..2079d4f --- /dev/null +++ b/src/main/java/com/researchspace/api/clientmodel/GroupSearchResult.java @@ -0,0 +1,22 @@ +package com.researchspace.api.clientmodel; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.ArrayList; +import java.util.List; + +/** + * Paginated list of groups returned by the sysadmin group-listing endpoint. + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class GroupSearchResult extends PaginatedResultList { + private List groups = new ArrayList<>(); +} diff --git a/src/main/java/com/researchspace/api/clientmodel/IdentifiableNameable.java b/src/main/java/com/researchspace/api/clientmodel/IdentifiableNameable.java index 1fe534c..8b8701d 100644 --- a/src/main/java/com/researchspace/api/clientmodel/IdentifiableNameable.java +++ b/src/main/java/com/researchspace/api/clientmodel/IdentifiableNameable.java @@ -1,15 +1,15 @@ package com.researchspace.api.clientmodel; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.*; import lombok.experimental.SuperBuilder; -import java.util.List; - @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor @AllArgsConstructor @SuperBuilder +@JsonIgnoreProperties(ignoreUnknown = true) public abstract class IdentifiableNameable extends Linkable { private Long id; private String globalId; diff --git a/src/main/java/com/researchspace/api/clientmodel/LinkItem.java b/src/main/java/com/researchspace/api/clientmodel/LinkItem.java index c1d5b9f..4e88d43 100644 --- a/src/main/java/com/researchspace/api/clientmodel/LinkItem.java +++ b/src/main/java/com/researchspace/api/clientmodel/LinkItem.java @@ -1,29 +1,6 @@ -/** - * RSpace API - * Access your RSpace documents programmatically. All requests require authentication using an API key set as the value of the header `RSpace-API-Key`. - * - * OpenAPI spec version: 0.1 - * Contact: support@researchspace.com - * - * NOTE: This class is auto generated by the swagger code generator program. - * https://github.com/swagger-api/swagger-codegen.git - * Do not edit the class manually. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package com.researchspace.api.clientmodel; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -34,6 +11,7 @@ @Data @AllArgsConstructor @NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) public class LinkItem { public static final String NEXT_REL = "next"; diff --git a/src/main/java/com/researchspace/api/clientmodel/MoveRequest.java b/src/main/java/com/researchspace/api/clientmodel/MoveRequest.java index 134e299..90dc79e 100644 --- a/src/main/java/com/researchspace/api/clientmodel/MoveRequest.java +++ b/src/main/java/com/researchspace/api/clientmodel/MoveRequest.java @@ -1,14 +1,15 @@ package com.researchspace.api.clientmodel; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; /** - * Request to move a document or notebook to a target folder + * Request to move a document or notebook to a target folder. + * The server API field name is {@code docId}; the Java field is {@code recordId}. * - * @author rspac * @since 1.98.0 */ @Data @@ -17,6 +18,7 @@ @Builder public class MoveRequest { + @JsonProperty("docId") private Long recordId; private long sourceFolderId; private Long targetFolderId; diff --git a/src/main/java/com/researchspace/api/clientmodel/PaginatedResultList.java b/src/main/java/com/researchspace/api/clientmodel/PaginatedResultList.java index 3baa611..3e20388 100644 --- a/src/main/java/com/researchspace/api/clientmodel/PaginatedResultList.java +++ b/src/main/java/com/researchspace/api/clientmodel/PaginatedResultList.java @@ -1,27 +1,3 @@ -/** - * RSpace API - * Access your RSpace documents programmatically. All requests require authentication using an API key set as the value of the header `RSpace-API-Key`. - * - * OpenAPI spec version: 0.1 - * Contact: support@researchspace.com - * - * NOTE: This class is auto generated by the swagger code generator program. - * https://github.com/swagger-api/swagger-codegen.git - * Do not edit the class manually. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package com.researchspace.api.clientmodel; import lombok.Data; @@ -35,7 +11,7 @@ @EqualsAndHashCode(callSuper=true) @ToString(callSuper=true) public abstract class PaginatedResultList extends Linkable { - - private Long totalHits = null; + private Long totalHits = null; private Integer pageNumber = null; + private Integer pageSize = null; } diff --git a/src/main/java/com/researchspace/api/clientmodel/ShareSearchResult.java b/src/main/java/com/researchspace/api/clientmodel/ShareSearchResult.java index f467fb4..407ec3b 100644 --- a/src/main/java/com/researchspace/api/clientmodel/ShareSearchResult.java +++ b/src/main/java/com/researchspace/api/clientmodel/ShareSearchResult.java @@ -1,40 +1,20 @@ -/** - * RSpace API - * Access your RSpace documents programmatically. All requests require authentication using an API key set as the value of the header `RSpace-API-Key`. - * - * OpenAPI spec version: 0.1 - * Contact: support@researchspace.com - * - * NOTE: This class is auto generated by the swagger code generator program. - * https://github.com/swagger-api/swagger-codegen.git - * Do not edit the class manually. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package com.researchspace.api.clientmodel; import java.util.ArrayList; import java.util.List; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; import lombok.EqualsAndHashCode; -import lombok.Value; +import lombok.NoArgsConstructor; /** - * FileSearchResult + * ShareSearchResult */ -@Value -@EqualsAndHashCode(callSuper=true) +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) public class ShareSearchResult extends PaginatedResultList { private List shares = new ArrayList<>(); diff --git a/src/main/java/com/researchspace/api/clientmodel/Status.java b/src/main/java/com/researchspace/api/clientmodel/Status.java index 1f0db51..e36a932 100644 --- a/src/main/java/com/researchspace/api/clientmodel/Status.java +++ b/src/main/java/com/researchspace/api/clientmodel/Status.java @@ -1,5 +1,6 @@ package com.researchspace.api.clientmodel; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -7,6 +8,7 @@ @Data @NoArgsConstructor @AllArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) public class Status { private String message; diff --git a/src/main/java/com/researchspace/api/clientmodel/User.java b/src/main/java/com/researchspace/api/clientmodel/User.java index 3576190..46bcf2a 100644 --- a/src/main/java/com/researchspace/api/clientmodel/User.java +++ b/src/main/java/com/researchspace/api/clientmodel/User.java @@ -1,45 +1,23 @@ -/** - * RSpace API - * Access your RSpace documents programmatically. All requests require authentication using an API key set as the value of the header `RSpace-API-Key`. - * - * OpenAPI spec version: 0.1 - * Contact: support@researchspace.com - * - * NOTE: This class is auto generated by the swagger code generator program. - * https://github.com/swagger-api/swagger-codegen.git - * Do not edit the class manually. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package com.researchspace.api.clientmodel; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; import lombok.NoArgsConstructor; -import lombok.Value; /** - * Representation of a User, this is read only as there is no API methods to alter user properties. + * Representation of a User. Read only — there are no API methods to alter user properties. * @since 1.1.0 */ -@Value +@Data @NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) public class User { - - private Long id = null; - private String username = null; - private String email = null; - private String firstName = null; - private String lastName = null; - private Long homeFolderId = null; + + private Long id; + private String username; + private String email; + private String firstName; + private String lastName; + private Long homeFolderId; } diff --git a/src/main/java/com/researchspace/api/clientmodel/UserGroupInfo.java b/src/main/java/com/researchspace/api/clientmodel/UserGroupInfo.java index f9bd919..89727c4 100644 --- a/src/main/java/com/researchspace/api/clientmodel/UserGroupInfo.java +++ b/src/main/java/com/researchspace/api/clientmodel/UserGroupInfo.java @@ -1,8 +1,12 @@ package com.researchspace.api.clientmodel; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) public class UserGroupInfo { private Long id; diff --git a/src/main/java/com/researchspace/api/clientmodel/UserInfo.java b/src/main/java/com/researchspace/api/clientmodel/UserInfo.java new file mode 100644 index 0000000..7936087 --- /dev/null +++ b/src/main/java/com/researchspace/api/clientmodel/UserInfo.java @@ -0,0 +1,26 @@ +package com.researchspace.api.clientmodel; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Full user information as returned by the sysadmin user-listing endpoint. + * @since 1.5 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class UserInfo extends IdentifiableNameable { + + private String email; + private String firstName; + private String lastName; + private UserRole role; + private String affiliation; + private boolean enabled; + private Long homeFolderId; + +} diff --git a/src/main/java/com/researchspace/api/clientmodel/UserPost.java b/src/main/java/com/researchspace/api/clientmodel/UserPost.java index 57713b1..19ee501 100644 --- a/src/main/java/com/researchspace/api/clientmodel/UserPost.java +++ b/src/main/java/com/researchspace/api/clientmodel/UserPost.java @@ -34,6 +34,7 @@ public class UserPost { @Size(min=1) private String affiliation; + // Server accepts 16–32 alphanumeric characters; verify exact constraint in com.researchspace.api.v1.model.ApiUserPost @Size(min = 16, max = 32) private String apiKey; diff --git a/src/main/java/com/researchspace/api/clientmodel/UserSearchResult.java b/src/main/java/com/researchspace/api/clientmodel/UserSearchResult.java new file mode 100644 index 0000000..dc575af --- /dev/null +++ b/src/main/java/com/researchspace/api/clientmodel/UserSearchResult.java @@ -0,0 +1,23 @@ +package com.researchspace.api.clientmodel; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * Paginated list of users returned by the sysadmin user-listing endpoint. + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class UserSearchResult extends PaginatedResultList { + private List users = new ArrayList<>(); + +} diff --git a/src/main/java/com/researchspace/api/jackson/ISO8601DateSerialiser.java b/src/main/java/com/researchspace/api/jackson/ISO8601DateSerialiser.java index 97bff5b..6235040 100644 --- a/src/main/java/com/researchspace/api/jackson/ISO8601DateSerialiser.java +++ b/src/main/java/com/researchspace/api/jackson/ISO8601DateSerialiser.java @@ -1,41 +1,36 @@ package com.researchspace.api.jackson; import java.io.IOException; -import java.text.SimpleDateFormat; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.Date; import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.ser.std.StdSerializer; + /** - * Serialises a long millis-since-epoch to simple ISO date format with no time. + * Serialises a Date to simple ISO date format (yyyy-MM-dd) with no time component. * @author rspace - * */ public class ISO8601DateSerialiser extends StdSerializer { - - static final String ISO8601_FORMAT_DATE = "yyyy-MM-dd"; - /** - * - */ - private static final long serialVersionUID = 1L; - - - public ISO8601DateSerialiser() { - this(null); - } - - public ISO8601DateSerialiser(Class t) { - super(t); - } - - @Override - public void serialize(Date value, JsonGenerator gen, SerializerProvider arg2) - throws IOException, JsonProcessingException { - SimpleDateFormat iso8601 = new SimpleDateFormat(ISO8601_FORMAT_DATE); - - gen.writeString(iso8601.format(value)); - } + + private static final long serialVersionUID = 1L; + private static final DateTimeFormatter FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneId.systemDefault()); + + public ISO8601DateSerialiser() { + this(null); + } + + public ISO8601DateSerialiser(Class t) { + super(t); + } + + @Override + public void serialize(Date value, JsonGenerator gen, SerializerProvider provider) + throws IOException { + gen.writeString(FORMATTER.format(value.toInstant())); + } } diff --git a/src/test/java/com/researchspace/api/clientmodel/ActivitySearchResultTest.java b/src/test/java/com/researchspace/api/clientmodel/ActivitySearchResultTest.java index 71d7e12..6ca0269 100644 --- a/src/test/java/com/researchspace/api/clientmodel/ActivitySearchResultTest.java +++ b/src/test/java/com/researchspace/api/clientmodel/ActivitySearchResultTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.File; import java.io.IOException; @@ -12,7 +13,9 @@ import org.junit.jupiter.api.Test; import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; public class ActivitySearchResultTest extends AbstractModelTest { @@ -26,6 +29,16 @@ public void setUp() throws Exception { public void tearDown() throws Exception { } + @Test + void testOidFieldSerialisesCorrectly() throws JsonProcessingException { + ActivitySearchQuery q = ActivitySearchQuery.builder() + .oid("SD12345") + .domain(ActivityDomain.RECORD) + .build(); + String json = new ObjectMapper().writeValueAsString(q); + assertTrue(json.contains("\"oid\":\"SD12345\"")); + } + @Test public void test() throws JsonParseException, JsonMappingException, IOException { ActivitySearchResult searchRes = readFileToClass(ActivitySearchResultJson, ActivitySearchResult.class); diff --git a/src/test/java/com/researchspace/api/clientmodel/DocumentSearchQueryTest.java b/src/test/java/com/researchspace/api/clientmodel/DocumentSearchQueryTest.java index de28151..ad174ea 100644 --- a/src/test/java/com/researchspace/api/clientmodel/DocumentSearchQueryTest.java +++ b/src/test/java/com/researchspace/api/clientmodel/DocumentSearchQueryTest.java @@ -27,9 +27,8 @@ public void tearDown() throws Exception { @Test public void test() throws JsonParseException, JsonMappingException, IOException { - System.err.println(SearchOperator.AND.toString()); DocumentSearchQuery query = readFileToClass(DocumentSearchQueryJson, DocumentSearchQuery.class); - assertEquals(2,query.getTerms().size()); + assertEquals(2, query.getTerms().size()); assertEquals(SearchOperator.OR, query.getOperator()); diff --git a/src/test/java/com/researchspace/api/clientmodel/DocumentSearchResultTest.java b/src/test/java/com/researchspace/api/clientmodel/DocumentSearchResultTest.java index 54357b3..fdfceea 100644 --- a/src/test/java/com/researchspace/api/clientmodel/DocumentSearchResultTest.java +++ b/src/test/java/com/researchspace/api/clientmodel/DocumentSearchResultTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import java.io.File; import java.io.IOException; @@ -29,9 +30,9 @@ public void tearDown() throws Exception { @Test public void test() throws JsonParseException, JsonMappingException, IOException { DocumentSearchResult searchTerm = readFileToClass(DocumentSearchResultJson, DocumentSearchResult.class); - assertEquals(0,searchTerm.getPageNumber().intValue()); - assertEquals(8,searchTerm.getTotalHits().intValue()); - System.err.println(searchTerm); + assertEquals(0, searchTerm.getPageNumber().intValue()); + assertEquals(8, searchTerm.getTotalHits().intValue()); + assertNotNull(searchTerm.getDocuments()); } } diff --git a/src/test/java/com/researchspace/api/clientmodel/DocumentTest.java b/src/test/java/com/researchspace/api/clientmodel/DocumentTest.java index faa8b1f..4d81ad2 100644 --- a/src/test/java/com/researchspace/api/clientmodel/DocumentTest.java +++ b/src/test/java/com/researchspace/api/clientmodel/DocumentTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import java.io.File; import java.io.IOException; @@ -29,8 +30,20 @@ public void tearDown() throws Exception { @Test public void test() throws JsonParseException, JsonMappingException, IOException { Document d = readFileToClass(documentJson, Document.class); - assertEquals(2,d.getFields().size()); - System.err.println(d); + assertEquals(2, d.getFields().size()); + assertEquals(23L, d.getId()); + assertEquals("SD23", d.getGlobalId()); + assertEquals("MyExperiment", d.getName()); + assertNotNull(d.getCreated()); + assertNotNull(d.getLastModified()); + assertNotNull(d.getOwner()); + assertEquals(1L, d.getOwner().getId()); + assertEquals("bsmith", d.getOwner().getUsername()); + assertNotNull(d.getForm()); + assertEquals(123L, d.getForm().getId()); + assertEquals(12L, d.getParentFolderId()); + assertNotNull(d.getFields().get(0).getType()); + assertNotNull(d.getFields().get(0).getName()); } } diff --git a/src/test/java/com/researchspace/api/clientmodel/FileSearchResultTest.java b/src/test/java/com/researchspace/api/clientmodel/FileSearchResultTest.java index 92b4903..f60cb15 100644 --- a/src/test/java/com/researchspace/api/clientmodel/FileSearchResultTest.java +++ b/src/test/java/com/researchspace/api/clientmodel/FileSearchResultTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import java.io.File; import java.io.IOException; @@ -29,9 +30,9 @@ public void tearDown() throws Exception { @Test public void test() throws JsonParseException, JsonMappingException, IOException { FileSearchResult d = readFileToClass(FileSearchResultJson, FileSearchResult.class); - assertEquals(0,d.getPageNumber().intValue()); - assertEquals(8,d.getTotalHits().intValue()); - System.err.println(d); + assertEquals(0, d.getPageNumber().intValue()); + assertEquals(8, d.getTotalHits().intValue()); + assertNotNull(d.getFiles()); } } diff --git a/src/test/java/com/researchspace/api/clientmodel/FolderTreeItemListingTest.java b/src/test/java/com/researchspace/api/clientmodel/FolderTreeItemListingTest.java new file mode 100644 index 0000000..41ad4c6 --- /dev/null +++ b/src/test/java/com/researchspace/api/clientmodel/FolderTreeItemListingTest.java @@ -0,0 +1,40 @@ +package com.researchspace.api.clientmodel; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.File; +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +public class FolderTreeItemListingTest extends AbstractModelTest { + + File listingJson = new File("src/test/resources/FolderTreeItemListing.json"); + + @Test + void testFolderTreeListingDeserialises() throws IOException { + FolderTreeItemListing listing = readFileToClass(listingJson, FolderTreeItemListing.class); + assertNotNull(listing); + assertEquals(2L, listing.getTotalHits()); + assertEquals(0, listing.getPageNumber()); + assertEquals(1L, listing.getParentId()); + assertFalse(listing.getRecords().isEmpty()); + } + + @Test + void testFolderItemFields() throws IOException { + FolderTreeItemListing listing = readFileToClass(listingJson, FolderTreeItemListing.class); + FolderTreeItemInfo first = listing.getRecords().get(0); + assertEquals(10L, first.getId()); + assertEquals("FL10", first.getGlobalId()); + assertEquals("Subfolder A", first.getName()); + assertEquals("FOLDER", first.getType()); + assertNotNull(first.getCreated()); + assertNotNull(first.getLastModified()); + assertEquals(1L, first.getParentFolderId()); + assertNotNull(first.getOwner()); + assertEquals("bsmith", first.getOwner().getUsername()); + } +} diff --git a/src/test/java/com/researchspace/api/clientmodel/FormPostTest.java b/src/test/java/com/researchspace/api/clientmodel/FormPostTest.java index 971f7d7..d222690 100644 --- a/src/test/java/com/researchspace/api/clientmodel/FormPostTest.java +++ b/src/test/java/com/researchspace/api/clientmodel/FormPostTest.java @@ -8,6 +8,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.researchspace.api.clientmodel.FormPost.ChoiceFieldPost; @@ -44,6 +47,15 @@ public void test() throws JsonProcessingException { .field(RadioFieldPost.builder().name("radios").options(radios).defaultOption(defaultRadio).build()) .build(); ObjectMapper reader = new ObjectMapper(); - System.err.println(reader.writeValueAsString(toSubmit)); + String json = reader.writeValueAsString(toSubmit); + assertNotNull(json); + assertTrue(json.contains("\"name\":\"formName\"")); + assertTrue(json.contains("\"type\":\"Number\"")); + assertTrue(json.contains("\"type\":\"Date\"")); + assertTrue(json.contains("\"type\":\"String\"")); + assertTrue(json.contains("\"type\":\"Text\"")); + assertTrue(json.contains("\"type\":\"Choice\"")); + assertTrue(json.contains("\"type\":\"Time\"")); + assertTrue(json.contains("\"type\":\"Radio\"")); } } diff --git a/src/test/java/com/researchspace/api/clientmodel/GroupInfoTest.java b/src/test/java/com/researchspace/api/clientmodel/GroupInfoTest.java new file mode 100644 index 0000000..8db3b28 --- /dev/null +++ b/src/test/java/com/researchspace/api/clientmodel/GroupInfoTest.java @@ -0,0 +1,55 @@ +package com.researchspace.api.clientmodel; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class GroupInfoTest { + + private ObjectMapper om = new ObjectMapper() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + + @Test + void testGroupInfoDeserialisation() throws IOException { + String json = "{" + + "\"id\":10," + + "\"globalId\":\"GP10\"," + + "\"name\":\"My Lab Group\"," + + "\"type\":\"LAB_GROUP\"," + + "\"sharedFolderId\":99," + + "\"members\":[" + + " {\"id\":1,\"username\":\"pi_user\",\"role\":\"PI\"}" + + "]," + + "\"_links\":[]" + + "}"; + GroupInfo group = om.readValue(json, GroupInfo.class); + assertEquals(10L, group.getId()); + assertEquals("GP10", group.getGlobalId()); + assertEquals("My Lab Group", group.getName()); + assertEquals("LAB_GROUP", group.getType()); + assertEquals(99L, group.getSharedFolderId()); + assertEquals(1, group.getMembers().size()); + assertEquals("PI", group.getMembers().get(0).getRole()); + assertEquals(1L, group.getMembers().get(0).getId()); + assertEquals("pi_user", group.getMembers().get(0).getUsername()); + } + + @Test + void testGroupInfoIgnoresUnknownFields() throws IOException { + String json = "{" + + "\"id\":5," + + "\"name\":\"Group\"," + + "\"futureField\":\"value\"," + + "\"members\":[]" + + "}"; + GroupInfo group = om.readValue(json, GroupInfo.class); + assertNotNull(group); + assertEquals(5L, group.getId()); + } +} diff --git a/src/test/java/com/researchspace/api/clientmodel/GroupPostTest.java b/src/test/java/com/researchspace/api/clientmodel/GroupPostTest.java index 64d9d2d..b5881ee 100644 --- a/src/test/java/com/researchspace/api/clientmodel/GroupPostTest.java +++ b/src/test/java/com/researchspace/api/clientmodel/GroupPostTest.java @@ -1,6 +1,8 @@ package com.researchspace.api.clientmodel; +import static org.junit.jupiter.api.Assertions.assertEquals; + import org.junit.jupiter.api.Test; import com.researchspace.api.clientmodel.UserGroupPost.RoleInGroup; @@ -13,11 +15,17 @@ public void groupPostUsage() { .email("email").password("password") .firstName("first").lastName("last") .role(UserRole.ROLE_PI).build(); - // create user, then can create group. - + assertEquals("pi", userPost.getUsername()); + assertEquals(UserRole.ROLE_PI, userPost.getRole()); + GroupPost grpPost = GroupPost.builder().displayName("groupName").type(GroupType.LAB_GROUP) .user(UserGroupPost.builder().username("pi").roleInGroup(RoleInGroup.PI).build()) .user(UserGroupPost.builder().username("pi").roleInGroup(RoleInGroup.RS_LAB_ADMIN).build()) .build(); + assertEquals("groupName", grpPost.getDisplayName()); + assertEquals(GroupType.LAB_GROUP, grpPost.getType()); + assertEquals(2, grpPost.getUsers().size()); + assertEquals(RoleInGroup.PI, grpPost.getUsers().get(0).getRoleInGroup()); + assertEquals(RoleInGroup.RS_LAB_ADMIN, grpPost.getUsers().get(1).getRoleInGroup()); } } diff --git a/src/test/java/com/researchspace/api/clientmodel/MoveRequestTest.java b/src/test/java/com/researchspace/api/clientmodel/MoveRequestTest.java new file mode 100644 index 0000000..85c3f31 --- /dev/null +++ b/src/test/java/com/researchspace/api/clientmodel/MoveRequestTest.java @@ -0,0 +1,57 @@ +package com.researchspace.api.clientmodel; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class MoveRequestTest { + + @Test + void testMoveRequestBuilder() { + MoveRequest req = MoveRequest.builder() + .recordId(42L) + .sourceFolderId(10L) + .targetFolderId(20L) + .currentGrandparentId(5L) + .reason("moving to new location") + .build(); + + assertEquals(42L, req.getRecordId()); + assertEquals(10L, req.getSourceFolderId()); + assertEquals(20L, req.getTargetFolderId()); + assertEquals(5L, req.getCurrentGrandparentId()); + assertEquals("moving to new location", req.getReason()); + } + + @Test + void testMoveRequestSerialisesCorrectly() throws JsonProcessingException { + MoveRequest req = MoveRequest.builder() + .recordId(1L) + .targetFolderId(2L) + .build(); + String json = new ObjectMapper().writeValueAsString(req); + // server API field is "docId"; @JsonProperty maps recordId → docId + assertTrue(json.contains("\"docId\":1"), "Expected docId in JSON but got: " + json); + assertTrue(json.contains("\"targetFolderId\":2"), "Expected targetFolderId in JSON but got: " + json); + } + + @Test + void testMoveRequestRoundtrip() throws IOException { + MoveRequest original = MoveRequest.builder() + .recordId(99L) + .sourceFolderId(5L) + .targetFolderId(7L) + .reason("test move") + .build(); + ObjectMapper om = new ObjectMapper(); + String json = om.writeValueAsString(original); + MoveRequest deserialized = om.readValue(json, MoveRequest.class); + assertEquals(original, deserialized); + } +} diff --git a/src/test/java/com/researchspace/api/clientmodel/UserTest.java b/src/test/java/com/researchspace/api/clientmodel/UserTest.java new file mode 100644 index 0000000..df5f4ea --- /dev/null +++ b/src/test/java/com/researchspace/api/clientmodel/UserTest.java @@ -0,0 +1,57 @@ +package com.researchspace.api.clientmodel; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.File; +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class UserTest extends AbstractModelTest { + + File userJson = new File("src/test/resources/User.json"); + + @Test + void testUserDeserialisationFromFixture() throws IOException { + User user = readFileToClass(userJson, User.class); + assertEquals(42L, user.getId()); + assertEquals("testuser", user.getUsername()); + assertEquals("test@example.com", user.getEmail()); + assertEquals("Test", user.getFirstName()); + assertEquals("User", user.getLastName()); + assertEquals(100L, user.getHomeFolderId()); + } + + @Test + void testUserIgnoresUnknownFields() throws IOException { + String json = "{\"id\":1,\"username\":\"user\",\"unknownFutureField\":\"some value\"}"; + User user = new ObjectMapper() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .readValue(json, User.class); + assertNotNull(user); + assertEquals(1L, user.getId()); + } + + @Test + void testAllFieldsDeserialise() throws IOException { + String json = "{" + + "\"id\":7," + + "\"username\":\"jdoe\"," + + "\"email\":\"jdoe@lab.org\"," + + "\"firstName\":\"Jane\"," + + "\"lastName\":\"Doe\"," + + "\"homeFolderId\":55" + + "}"; + User user = new ObjectMapper().readValue(json, User.class); + assertEquals(7L, user.getId()); + assertEquals("jdoe", user.getUsername()); + assertEquals("jdoe@lab.org", user.getEmail()); + assertEquals("Jane", user.getFirstName()); + assertEquals("Doe", user.getLastName()); + assertEquals(55L, user.getHomeFolderId()); + } +} diff --git a/src/test/resources/FolderTreeItemListing.json b/src/test/resources/FolderTreeItemListing.json new file mode 100644 index 0000000..3e57379 --- /dev/null +++ b/src/test/resources/FolderTreeItemListing.json @@ -0,0 +1,42 @@ +{ + "totalHits": 2, + "pageNumber": 0, + "parentId": 1, + "records": [ + { + "id": 10, + "globalId": "FL10", + "name": "Subfolder A", + "type": "FOLDER", + "created": "2023-01-01T00:00:00.000Z", + "lastModified": "2023-06-01T00:00:00.000Z", + "parentFolderId": 1, + "owner": { + "id": 1, + "username": "bsmith", + "email": "bsmith@example.com", + "firstName": "Bob", + "lastName": "Smith" + }, + "_links": [] + }, + { + "id": 11, + "globalId": "NB11", + "name": "My Notebook", + "type": "NOTEBOOK", + "created": "2023-02-01T00:00:00.000Z", + "lastModified": "2023-07-01T00:00:00.000Z", + "parentFolderId": 1, + "owner": { + "id": 1, + "username": "bsmith", + "email": "bsmith@example.com", + "firstName": "Bob", + "lastName": "Smith" + }, + "_links": [] + } + ], + "_links": [] +} diff --git a/src/test/resources/User.json b/src/test/resources/User.json new file mode 100644 index 0000000..b3b93d1 --- /dev/null +++ b/src/test/resources/User.json @@ -0,0 +1,8 @@ +{ + "id": 42, + "username": "testuser", + "email": "test@example.com", + "firstName": "Test", + "lastName": "User", + "homeFolderId": 100 +} From afd29618eeac9f96d318c80754207e3029f0cc57 Mon Sep 17 00:00:00 2001 From: nebay-abraha Date: Thu, 14 May 2026 15:45:06 +0100 Subject: [PATCH 2/6] fix: address PR #14 review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UserInfo: add missing username field + @ToString(callSuper=true) - UserInfoTest, UserSearchResultTest, GroupSearchResultTest: new deserialisation tests with JSON fixtures (UserInfo.json, UserSearchResult.json, GroupSearchResult.json) - ISO8601DateSerialiser: pin timezone to UTC (was systemDefault); add Javadoc explaining the UTC behaviour - FieldPut: move class Javadoc before annotations (was misplaced) - CHANGELOG: reword FieldPut breaking change — constructor preserved, describe actual shadow-removal semantics - DocumentSearchResult.json: add pageSize field; assert on it in test - AGENTS.md: update taxonomy table (search results -> @Data, User fixed, add UserInfo/UserSearchResult/GroupSearchResult); update Lombok rules section to remove stale @Value guidance; add pageSize to PaginatedResultList inheritance note - UserPost: remove unverified 'alphanumeric' claim from apiKey comment - MoveRequestTest: fix British spelling (deserialized -> deserialised) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 34 ++++++++---------- CHANGELOG.md | 2 +- .../api/clientmodel/FieldPut.java | 8 ++--- .../api/clientmodel/UserInfo.java | 4 ++- .../api/clientmodel/UserPost.java | 2 +- .../api/jackson/ISO8601DateSerialiser.java | 12 ++++--- .../clientmodel/DocumentSearchResultTest.java | 1 + .../clientmodel/GroupSearchResultTest.java | 29 +++++++++++++++ .../api/clientmodel/MoveRequestTest.java | 4 +-- .../api/clientmodel/UserInfoTest.java | 35 +++++++++++++++++++ .../api/clientmodel/UserSearchResultTest.java | 25 +++++++++++++ src/test/resources/DocumentSearchResult.json | 1 + src/test/resources/GroupSearchResult.json | 16 +++++++++ src/test/resources/UserInfo.json | 13 +++++++ src/test/resources/UserSearchResult.json | 16 +++++++++ 15 files changed, 168 insertions(+), 34 deletions(-) create mode 100644 src/test/java/com/researchspace/api/clientmodel/GroupSearchResultTest.java create mode 100644 src/test/java/com/researchspace/api/clientmodel/UserInfoTest.java create mode 100644 src/test/java/com/researchspace/api/clientmodel/UserSearchResultTest.java create mode 100644 src/test/resources/GroupSearchResult.json create mode 100644 src/test/resources/UserInfo.json create mode 100644 src/test/resources/UserSearchResult.json diff --git a/AGENTS.md b/AGENTS.md index d950668..9ffae9c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,12 +44,15 @@ Used for deserialisation. Must survive unknown fields from future server release | `Form` | `FormInfo` | `@Data @NoArgsConstructor @SuperBuilder` | `@JsonPropertyOrder` | | `FormInfo` | `IdentifiableNameable` | `@Data @NoArgsConstructor @SuperBuilder` | `@JsonPropertyOrder` | | `GroupInfo` | `IdentifiableNameable` | `@Data @NoArgsConstructor` | — | -| `User` | — | **see BUGS section** | — | -| `ActivitySearchResult` | `PaginatedResultList` | `@Value` | — | -| `DocumentSearchResult` | `PaginatedResultList` | `@Value` | — | -| `FileSearchResult` | `PaginatedResultList` | `@Value` | — | -| `FormSearchResult` | `PaginatedResultList` | `@Value` | — | -| `ShareSearchResult` | `PaginatedResultList` | `@Value` | — | +| `User` | — | `@Data @NoArgsConstructor` | — | +| `ActivitySearchResult` | `PaginatedResultList` | `@Data @NoArgsConstructor` | — | +| `DocumentSearchResult` | `PaginatedResultList` | `@Data @NoArgsConstructor` | — | +| `FileSearchResult` | `PaginatedResultList` | `@Data @NoArgsConstructor` | — | +| `FormSearchResult` | `PaginatedResultList` | `@Data @NoArgsConstructor` | — | +| `ShareSearchResult` | `PaginatedResultList` | `@Data @NoArgsConstructor` | — | +| `UserInfo` | `IdentifiableNameable` | `@Data @NoArgsConstructor` | — | +| `UserSearchResult` | `PaginatedResultList` | `@Data @NoArgsConstructor` | — | +| `GroupSearchResult` | `PaginatedResultList` | `@Data @NoArgsConstructor` | — | ### Request POJOs (what the client sends to the server) @@ -69,7 +72,7 @@ Used for serialisation only. `@Builder` pattern for clean construction at call s ``` Linkable ← _links list + getLinkByType() - └── PaginatedResultList ← totalHits, pageNumber + └── PaginatedResultList ← totalHits, pageNumber, pageSize └── IdentifiableNameable ← id, globalId, name └── DocumentInfo ← created, lastModified, tags, form, owner, parentFolderId └── Document ← fields: List @@ -123,23 +126,14 @@ Only add to `inventory/` if it is a base type used by multiple concrete classes. **Response POJOs** use: `@Data @NoArgsConstructor` (+ `@EqualsAndHashCode(callSuper=true)` for subclasses) **Request POJOs** use: `@Data @Builder @NoArgsConstructor @AllArgsConstructor` **Abstract base classes** use: `@Data @NoArgsConstructor @SuperBuilder` -**Immutable search results** use: `@Value @EqualsAndHashCode(callSuper=true)` — see note below +**Search result list wrappers** use: `@Data @NoArgsConstructor @EqualsAndHashCode(callSuper=true)` -### `@Value` vs `@Data` — know the difference +### `@Value` — do not use for response POJOs `@Value` generates an immutable class: all fields `final`, no setters, all-args constructor. -Jackson cannot deserialise into a `@Value` class without a custom deserialiser because -there are no setters and the all-args constructor requires every field by position. +Jackson cannot deserialise into a `@Value` class without a custom deserialiser. -**Only use `@Value` for classes where you are certain Jackson will never try to deserialise them.** - -Current `@Value` classes: `ActivitySearchResult`, `DocumentSearchResult`, -`FileSearchResult`, `FormSearchResult`, `ShareSearchResult`. -These work because `AbstractModelTest.readFileToClass()` disables `FAIL_ON_UNKNOWN_PROPERTIES` -and the fields are initialised to `new ArrayList<>()` which Jackson appends to. -This is fragile — see IMPROVEMENT.md item 1.1 for the correct fix. - -**`User.java` uses `@Value @NoArgsConstructor` which is a bug — see IMPROVEMENT.md item 1.3.** +**Never use `@Value` on any class that Jackson needs to deserialise.** ### `@Singular` — use for list fields in request builder classes diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b9e682..dac5a33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ All notable changes to this project will be documented in this file. - **`AbstractExportPost`**: renamed `scope()` → `getScopeString()` and `format()` → `getFormatString()` to avoid confusion with Lombok-generated getters. Update any callers of the old method names. - **`ApiShareInfo.sharedItemName`**: field type corrected from `Long` to `String`. Any code storing or passing `getSharedItemName()` as a `Long` must be updated. -- **`FieldPut`**: removed the shadowed `content` field; `FieldPut` now inherits `content` from `FieldPost`. The `@AllArgsConstructor`-generated constructor signature changes from `FieldPut(String content, Long id)` to `FieldPut(Long id)`. Replace any positional constructor calls with the no-args constructor + `setContent()`. +- **`FieldPut`**: removed the locally-shadowed `content` field; `FieldPut` now inherits `content` from `FieldPost`. The explicit two-arg constructor `FieldPut(String content, Long id)` is preserved, so existing callers will continue to compile. However, `equals()`/`hashCode()` no longer compare two separate `content` fields, and any code that set the shadowed field via reflection will need to update to the inherited field. - **`ActivitySearchResult`, `DocumentSearchResult`, `FileSearchResult`, `FormSearchResult`, `ShareSearchResult`**: changed from `@Value` (immutable, all-args constructor) to `@Data @NoArgsConstructor` (mutable, setters). These are response POJOs and should not be constructed directly by callers, but any code that relied on the `@Value`-generated constructor will no longer compile. ### Added diff --git a/src/main/java/com/researchspace/api/clientmodel/FieldPut.java b/src/main/java/com/researchspace/api/clientmodel/FieldPut.java index c1c6cb4..bccda1d 100644 --- a/src/main/java/com/researchspace/api/clientmodel/FieldPut.java +++ b/src/main/java/com/researchspace/api/clientmodel/FieldPut.java @@ -5,14 +5,12 @@ import lombok.NoArgsConstructor; import lombok.NonNull; +/** + * Extends FieldPost with a field Id property to specify the Field whose content is to be updated. + */ @Data @EqualsAndHashCode(callSuper=false) @NoArgsConstructor -/** - * Extends FieldPost with a field Id property to specify the Field, whose content is to be updated. - * @author rspace - * - */ public class FieldPut extends FieldPost { /** * Cannot be null diff --git a/src/main/java/com/researchspace/api/clientmodel/UserInfo.java b/src/main/java/com/researchspace/api/clientmodel/UserInfo.java index 7936087..0aca96e 100644 --- a/src/main/java/com/researchspace/api/clientmodel/UserInfo.java +++ b/src/main/java/com/researchspace/api/clientmodel/UserInfo.java @@ -4,17 +4,19 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.ToString; /** * Full user information as returned by the sysadmin user-listing endpoint. - * @since 1.5 */ @Data @EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) public class UserInfo extends IdentifiableNameable { + private String username; private String email; private String firstName; private String lastName; diff --git a/src/main/java/com/researchspace/api/clientmodel/UserPost.java b/src/main/java/com/researchspace/api/clientmodel/UserPost.java index 19ee501..e948d4c 100644 --- a/src/main/java/com/researchspace/api/clientmodel/UserPost.java +++ b/src/main/java/com/researchspace/api/clientmodel/UserPost.java @@ -34,7 +34,7 @@ public class UserPost { @Size(min=1) private String affiliation; - // Server accepts 16–32 alphanumeric characters; verify exact constraint in com.researchspace.api.v1.model.ApiUserPost + // Server constraint: 16–32 characters; see com.researchspace.api.v1.model.ApiUserPost for exact validation @Size(min = 16, max = 32) private String apiKey; diff --git a/src/main/java/com/researchspace/api/jackson/ISO8601DateSerialiser.java b/src/main/java/com/researchspace/api/jackson/ISO8601DateSerialiser.java index 6235040..95da4a3 100644 --- a/src/main/java/com/researchspace/api/jackson/ISO8601DateSerialiser.java +++ b/src/main/java/com/researchspace/api/jackson/ISO8601DateSerialiser.java @@ -1,7 +1,7 @@ package com.researchspace.api.jackson; import java.io.IOException; -import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.util.Date; @@ -10,14 +10,18 @@ import com.fasterxml.jackson.databind.ser.std.StdSerializer; /** - * Serialises a Date to simple ISO date format (yyyy-MM-dd) with no time component. - * @author rspace + * Serialises a {@link Date} to ISO date format {@code yyyy-MM-dd} with no time component. + *

+ * Dates are always serialised in UTC. A {@code Date} value is converted to its UTC calendar + * date before formatting, so callers must ensure the logical date they intend to send matches + * the UTC date of the {@code Date} instance (e.g. set to midnight UTC). + *

*/ public class ISO8601DateSerialiser extends StdSerializer { private static final long serialVersionUID = 1L; private static final DateTimeFormatter FORMATTER = - DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneId.systemDefault()); + DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneOffset.UTC); public ISO8601DateSerialiser() { this(null); diff --git a/src/test/java/com/researchspace/api/clientmodel/DocumentSearchResultTest.java b/src/test/java/com/researchspace/api/clientmodel/DocumentSearchResultTest.java index fdfceea..c059d1c 100644 --- a/src/test/java/com/researchspace/api/clientmodel/DocumentSearchResultTest.java +++ b/src/test/java/com/researchspace/api/clientmodel/DocumentSearchResultTest.java @@ -32,6 +32,7 @@ public void test() throws JsonParseException, JsonMappingException, IOException DocumentSearchResult searchTerm = readFileToClass(DocumentSearchResultJson, DocumentSearchResult.class); assertEquals(0, searchTerm.getPageNumber().intValue()); assertEquals(8, searchTerm.getTotalHits().intValue()); + assertEquals(10, searchTerm.getPageSize().intValue()); assertNotNull(searchTerm.getDocuments()); } diff --git a/src/test/java/com/researchspace/api/clientmodel/GroupSearchResultTest.java b/src/test/java/com/researchspace/api/clientmodel/GroupSearchResultTest.java new file mode 100644 index 0000000..0d1a262 --- /dev/null +++ b/src/test/java/com/researchspace/api/clientmodel/GroupSearchResultTest.java @@ -0,0 +1,29 @@ +package com.researchspace.api.clientmodel; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.File; +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +public class GroupSearchResultTest extends AbstractModelTest { + + private final File fixture = new File("src/test/resources/GroupSearchResult.json"); + + @Test + void testGroupSearchResultDeserialisesFromFixture() throws IOException { + GroupSearchResult result = readFileToClass(fixture, GroupSearchResult.class); + assertEquals(1L, result.getTotalHits().longValue()); + assertEquals(0, result.getPageNumber().intValue()); + assertEquals(20, result.getPageSize().intValue()); + assertNotNull(result.getGroups()); + assertEquals(1, result.getGroups().size()); + GroupInfo group = result.getGroups().get(0); + assertEquals(10L, group.getId()); + assertEquals("GP10", group.getGlobalId()); + assertEquals("My Lab Group", group.getName()); + assertEquals("LAB_GROUP", group.getType()); + } +} diff --git a/src/test/java/com/researchspace/api/clientmodel/MoveRequestTest.java b/src/test/java/com/researchspace/api/clientmodel/MoveRequestTest.java index 85c3f31..dd7d030 100644 --- a/src/test/java/com/researchspace/api/clientmodel/MoveRequestTest.java +++ b/src/test/java/com/researchspace/api/clientmodel/MoveRequestTest.java @@ -51,7 +51,7 @@ void testMoveRequestRoundtrip() throws IOException { .build(); ObjectMapper om = new ObjectMapper(); String json = om.writeValueAsString(original); - MoveRequest deserialized = om.readValue(json, MoveRequest.class); - assertEquals(original, deserialized); + MoveRequest deserialised = om.readValue(json, MoveRequest.class); + assertEquals(original, deserialised); } } diff --git a/src/test/java/com/researchspace/api/clientmodel/UserInfoTest.java b/src/test/java/com/researchspace/api/clientmodel/UserInfoTest.java new file mode 100644 index 0000000..3b67187 --- /dev/null +++ b/src/test/java/com/researchspace/api/clientmodel/UserInfoTest.java @@ -0,0 +1,35 @@ +package com.researchspace.api.clientmodel; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +public class UserInfoTest extends AbstractModelTest { + + private final File fixture = new File("src/test/resources/UserInfo.json"); + + @Test + void testUserInfoDeserialisesFromFixture() throws IOException { + UserInfo user = readFileToClass(fixture, UserInfo.class); + assertEquals(101L, user.getId()); + assertEquals("U101", user.getGlobalId()); + assertEquals("jsmith", user.getUsername()); + assertEquals("jsmith@lab.org", user.getEmail()); + assertEquals("John", user.getFirstName()); + assertEquals("Smith", user.getLastName()); + assertEquals("Acme Labs", user.getAffiliation()); + assertTrue(user.isEnabled()); + assertEquals(200L, user.getHomeFolderId()); + } + + @Test + void testUserInfoIgnoresUnknownFields() throws IOException { + UserInfo user = readFileToClass(fixture, UserInfo.class); + // fixture has unknown fields if added in future — @JsonIgnoreProperties ensures no exception + assertEquals("jsmith", user.getUsername()); + } +} diff --git a/src/test/java/com/researchspace/api/clientmodel/UserSearchResultTest.java b/src/test/java/com/researchspace/api/clientmodel/UserSearchResultTest.java new file mode 100644 index 0000000..ac54021 --- /dev/null +++ b/src/test/java/com/researchspace/api/clientmodel/UserSearchResultTest.java @@ -0,0 +1,25 @@ +package com.researchspace.api.clientmodel; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.File; +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +public class UserSearchResultTest extends AbstractModelTest { + + private final File fixture = new File("src/test/resources/UserSearchResult.json"); + + @Test + void testUserSearchResultDeserialisesFromFixture() throws IOException { + UserSearchResult result = readFileToClass(fixture, UserSearchResult.class); + assertEquals(1L, result.getTotalHits().longValue()); + assertEquals(0, result.getPageNumber().intValue()); + assertEquals(20, result.getPageSize().intValue()); + assertNotNull(result.getUsers()); + assertEquals(1, result.getUsers().size()); + assertEquals("admin", result.getUsers().get(0).getUsername()); + } +} diff --git a/src/test/resources/DocumentSearchResult.json b/src/test/resources/DocumentSearchResult.json index ade6081..afecfcc 100644 --- a/src/test/resources/DocumentSearchResult.json +++ b/src/test/resources/DocumentSearchResult.json @@ -1,6 +1,7 @@ { "totalHits" : 8, "pageNumber" : 0, + "pageSize" : 10, "documents" : [ { "id" : 476, "globalId" : "SD476", diff --git a/src/test/resources/GroupSearchResult.json b/src/test/resources/GroupSearchResult.json new file mode 100644 index 0000000..9024609 --- /dev/null +++ b/src/test/resources/GroupSearchResult.json @@ -0,0 +1,16 @@ +{ + "totalHits": 1, + "pageNumber": 0, + "pageSize": 20, + "groups": [ + { + "id": 10, + "globalId": "GP10", + "name": "My Lab Group", + "type": "LAB_GROUP", + "sharedFolderId": 99, + "members": [] + } + ], + "_links": [] +} diff --git a/src/test/resources/UserInfo.json b/src/test/resources/UserInfo.json new file mode 100644 index 0000000..4b3c663 --- /dev/null +++ b/src/test/resources/UserInfo.json @@ -0,0 +1,13 @@ +{ + "id": 101, + "globalId": "U101", + "name": "jsmith", + "username": "jsmith", + "email": "jsmith@lab.org", + "firstName": "John", + "lastName": "Smith", + "affiliation": "Acme Labs", + "enabled": true, + "homeFolderId": 200, + "_links": [] +} diff --git a/src/test/resources/UserSearchResult.json b/src/test/resources/UserSearchResult.json new file mode 100644 index 0000000..1e4f6e9 --- /dev/null +++ b/src/test/resources/UserSearchResult.json @@ -0,0 +1,16 @@ +{ + "totalHits": 1, + "pageNumber": 0, + "pageSize": 20, + "users": [ + { + "id": 1, + "username": "admin", + "email": "admin@example.com", + "firstName": "Admin", + "lastName": "User", + "homeFolderId": 5 + } + ], + "_links": [] +} From d5ddd8c794be14a01fa139bb643d256c02b2c6dd Mon Sep 17 00:00:00 2001 From: nebay-abraha Date: Thu, 14 May 2026 17:08:37 +0100 Subject: [PATCH 3/6] fix: address all PR #14 review issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AbstractExportPost: add @JsonIgnore to getScopeString()/getFormatString() so Jackson does not serialise them as unintended scopeString/formatString properties in export request payloads - UserSearchResult: change List to List to match the sysadmin endpoint response; update fixture and test accordingly - DocumentSearchResult.documents: add missing private modifier - FormSearchResult.forms: add missing private modifier - FieldPut: fix @EqualsAndHashCode(callSuper=false) → callSuper=true so inherited content field participates in equality checks - ShareSearchResult, FileSearchResult, FolderTreeItemListing: add @ToString(callSuper=true) for consistency with other PaginatedResultList subclasses - UserInfo.json: add role field (ROLE_USER) to exercise enum deserialisation - UserInfoTest: replace duplicate fixture-read test with a genuine unknown- fields test using inline JSON; add role assertion; add null-role test - UserSearchResult.json: update to UserInfo-shaped objects with all fields - UserSearchResultTest: assert UserInfo-specific fields including role - CHANGELOG: move User.java @Value→@Data to Breaking Changes section; add ISO8601DateSerialiser UTC timezone change as a breaking change; remove User.java entry from Fixed section - AGENTS.md: remove stale IMPROVEMENT.md cross-repo references Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 5 ++-- CHANGELOG.md | 5 ++-- .../api/clientmodel/AbstractExportPost.java | 3 +++ .../api/clientmodel/DocumentSearchResult.java | 2 +- .../api/clientmodel/FieldPut.java | 2 +- .../api/clientmodel/FileSearchResult.java | 2 ++ .../clientmodel/FolderTreeItemListing.java | 2 ++ .../api/clientmodel/FormSearchResult.java | 2 +- .../api/clientmodel/ShareSearchResult.java | 2 ++ .../api/clientmodel/UserSearchResult.java | 2 +- .../api/clientmodel/UserInfoTest.java | 25 ++++++++++++++++--- .../api/clientmodel/UserSearchResultTest.java | 12 ++++++++- src/test/resources/UserInfo.json | 1 + src/test/resources/UserSearchResult.json | 15 +++++++---- 14 files changed, 62 insertions(+), 18 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9ffae9c..bb62583 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -168,8 +168,7 @@ The `AbstractModelTest` configures Jackson with: - `FAIL_ON_UNKNOWN_PROPERTIES` disabled The production `BaseApiClientImpl` in `rspace-api-acceptance` configures the same -enum handling but does NOT yet disable `FAIL_ON_UNKNOWN_PROPERTIES` — see -`rspace-api-acceptance` IMPROVEMENT.md item 1.2. +enum handling but does NOT yet disable `FAIL_ON_UNKNOWN_PROPERTIES`. ### `ISO8601DateSerialiser` — use for date fields sent TO the server @@ -249,7 +248,7 @@ a minimal valid JSON that exercises the fields you care about. 1. **Decide which category** it belongs to (response POJO, request POJO, base class, inventory base) 2. **Choose Lombok annotations** from the taxonomy table above — do not invent new combinations -3. **Add `@JsonIgnoreProperties(ignoreUnknown = true)`** on response POJOs — see IMPROVEMENT.md +3. **Add `@JsonIgnoreProperties(ignoreUnknown = true)`** on response POJOs 4. **Add a JSON fixture** in `src/test/resources/` or use serialisation roundtrip test 5. **Write a unit test** in `src/test/java/com/researchspace/api/clientmodel/` 6. **Verify against dev source** before adding fields not in the current Swagger: diff --git a/CHANGELOG.md b/CHANGELOG.md index dac5a33..32e8c5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to this project will be documented in this file. - **`ApiShareInfo.sharedItemName`**: field type corrected from `Long` to `String`. Any code storing or passing `getSharedItemName()` as a `Long` must be updated. - **`FieldPut`**: removed the locally-shadowed `content` field; `FieldPut` now inherits `content` from `FieldPost`. The explicit two-arg constructor `FieldPut(String content, Long id)` is preserved, so existing callers will continue to compile. However, `equals()`/`hashCode()` no longer compare two separate `content` fields, and any code that set the shadowed field via reflection will need to update to the inherited field. - **`ActivitySearchResult`, `DocumentSearchResult`, `FileSearchResult`, `FormSearchResult`, `ShareSearchResult`**: changed from `@Value` (immutable, all-args constructor) to `@Data @NoArgsConstructor` (mutable, setters). These are response POJOs and should not be constructed directly by callers, but any code that relied on the `@Value`-generated constructor will no longer compile. +- **`User.java`**: replaced `@Value @NoArgsConstructor` with `@Data @NoArgsConstructor`. The `@Value`-generated all-args constructor is removed; any code using positional construction (`new User(id, username, …)`) will no longer compile. +- **`ISO8601DateSerialiser`**: dates are now always serialised in UTC (previously used the JVM default timezone). Any `Date` value whose local-timezone calendar date differs from its UTC calendar date will now serialise differently. Ensure all `Date` values passed to `DateFormField` represent midnight UTC if exact date identity matters. ### Added @@ -19,9 +21,8 @@ All notable changes to this project will be documented in this file. ### Fixed - `@JsonIgnoreProperties(ignoreUnknown = true)` added to all response POJOs (via `IdentifiableNameable` and individually on non-hierarchy classes). Prevents `UnrecognizedPropertyException` when the server adds new fields in future releases. -- `User.java`: replaced contradictory `@Value @NoArgsConstructor` combination with `@Data @NoArgsConstructor`. All six fields now deserialise correctly from JSON. - Removed stale swagger-codegen `DO NOT EDIT` headers from 12 source files that have been manually maintained. -- `ISO8601DateSerialiser`: replaced per-call `SimpleDateFormat` allocation with a thread-safe static `DateTimeFormatter`. +- `ISO8601DateSerialiser`: replaced per-call `SimpleDateFormat` allocation with a thread-safe static `DateTimeFormatter`. See Breaking Changes for the associated timezone behaviour change. - `ApiShareInfo`: split the multi-field `Long` declaration; `sharedItemName` is now a separate `String` field. ## [1.99.1] diff --git a/src/main/java/com/researchspace/api/clientmodel/AbstractExportPost.java b/src/main/java/com/researchspace/api/clientmodel/AbstractExportPost.java index fbd2ac2..c0047f1 100644 --- a/src/main/java/com/researchspace/api/clientmodel/AbstractExportPost.java +++ b/src/main/java/com/researchspace/api/clientmodel/AbstractExportPost.java @@ -1,5 +1,6 @@ package com.researchspace.api.clientmodel; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -12,10 +13,12 @@ public class AbstractExportPost { private ExportScope scope = ExportScope.USER; private ExportFormat format = ExportFormat.HTML; + @JsonIgnore public String getScopeString() { return scope.name().toLowerCase(); } + @JsonIgnore public String getFormatString() { return format.name().toLowerCase(); } diff --git a/src/main/java/com/researchspace/api/clientmodel/DocumentSearchResult.java b/src/main/java/com/researchspace/api/clientmodel/DocumentSearchResult.java index bbbb924..3f47726 100644 --- a/src/main/java/com/researchspace/api/clientmodel/DocumentSearchResult.java +++ b/src/main/java/com/researchspace/api/clientmodel/DocumentSearchResult.java @@ -19,6 +19,6 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class DocumentSearchResult extends PaginatedResultList { - List documents = new ArrayList<>(); + private List documents = new ArrayList<>(); } diff --git a/src/main/java/com/researchspace/api/clientmodel/FieldPut.java b/src/main/java/com/researchspace/api/clientmodel/FieldPut.java index bccda1d..a8028e6 100644 --- a/src/main/java/com/researchspace/api/clientmodel/FieldPut.java +++ b/src/main/java/com/researchspace/api/clientmodel/FieldPut.java @@ -9,7 +9,7 @@ * Extends FieldPost with a field Id property to specify the Field whose content is to be updated. */ @Data -@EqualsAndHashCode(callSuper=false) +@EqualsAndHashCode(callSuper=true) @NoArgsConstructor public class FieldPut extends FieldPost { /** diff --git a/src/main/java/com/researchspace/api/clientmodel/FileSearchResult.java b/src/main/java/com/researchspace/api/clientmodel/FileSearchResult.java index 237309d..0989bdc 100644 --- a/src/main/java/com/researchspace/api/clientmodel/FileSearchResult.java +++ b/src/main/java/com/researchspace/api/clientmodel/FileSearchResult.java @@ -7,12 +7,14 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.ToString; /** * FileSearchResult */ @Data @EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) public class FileSearchResult extends PaginatedResultList { diff --git a/src/main/java/com/researchspace/api/clientmodel/FolderTreeItemListing.java b/src/main/java/com/researchspace/api/clientmodel/FolderTreeItemListing.java index 9f392e5..16ba14c 100644 --- a/src/main/java/com/researchspace/api/clientmodel/FolderTreeItemListing.java +++ b/src/main/java/com/researchspace/api/clientmodel/FolderTreeItemListing.java @@ -9,9 +9,11 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.ToString; @Data @EqualsAndHashCode(callSuper=true) +@ToString(callSuper=true) @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) public class FolderTreeItemListing extends PaginatedResultList { diff --git a/src/main/java/com/researchspace/api/clientmodel/FormSearchResult.java b/src/main/java/com/researchspace/api/clientmodel/FormSearchResult.java index fbfe0c9..2ca3541 100644 --- a/src/main/java/com/researchspace/api/clientmodel/FormSearchResult.java +++ b/src/main/java/com/researchspace/api/clientmodel/FormSearchResult.java @@ -16,6 +16,6 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class FormSearchResult extends PaginatedResultList { - List forms = new ArrayList<>(); + private List forms = new ArrayList<>(); } diff --git a/src/main/java/com/researchspace/api/clientmodel/ShareSearchResult.java b/src/main/java/com/researchspace/api/clientmodel/ShareSearchResult.java index 407ec3b..329ebd2 100644 --- a/src/main/java/com/researchspace/api/clientmodel/ShareSearchResult.java +++ b/src/main/java/com/researchspace/api/clientmodel/ShareSearchResult.java @@ -7,12 +7,14 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.ToString; /** * ShareSearchResult */ @Data @EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) public class ShareSearchResult extends PaginatedResultList { diff --git a/src/main/java/com/researchspace/api/clientmodel/UserSearchResult.java b/src/main/java/com/researchspace/api/clientmodel/UserSearchResult.java index dc575af..8f62199 100644 --- a/src/main/java/com/researchspace/api/clientmodel/UserSearchResult.java +++ b/src/main/java/com/researchspace/api/clientmodel/UserSearchResult.java @@ -18,6 +18,6 @@ @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) public class UserSearchResult extends PaginatedResultList { - private List users = new ArrayList<>(); + private List users = new ArrayList<>(); } diff --git a/src/test/java/com/researchspace/api/clientmodel/UserInfoTest.java b/src/test/java/com/researchspace/api/clientmodel/UserInfoTest.java index 3b67187..1471727 100644 --- a/src/test/java/com/researchspace/api/clientmodel/UserInfoTest.java +++ b/src/test/java/com/researchspace/api/clientmodel/UserInfoTest.java @@ -1,6 +1,8 @@ package com.researchspace.api.clientmodel; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.File; @@ -8,6 +10,9 @@ import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + public class UserInfoTest extends AbstractModelTest { private final File fixture = new File("src/test/resources/UserInfo.json"); @@ -21,6 +26,7 @@ void testUserInfoDeserialisesFromFixture() throws IOException { assertEquals("jsmith@lab.org", user.getEmail()); assertEquals("John", user.getFirstName()); assertEquals("Smith", user.getLastName()); + assertEquals(UserRole.ROLE_USER, user.getRole()); assertEquals("Acme Labs", user.getAffiliation()); assertTrue(user.isEnabled()); assertEquals(200L, user.getHomeFolderId()); @@ -28,8 +34,21 @@ void testUserInfoDeserialisesFromFixture() throws IOException { @Test void testUserInfoIgnoresUnknownFields() throws IOException { - UserInfo user = readFileToClass(fixture, UserInfo.class); - // fixture has unknown fields if added in future — @JsonIgnoreProperties ensures no exception - assertEquals("jsmith", user.getUsername()); + String json = "{\"id\":1,\"globalId\":\"U1\",\"name\":\"testuser\",\"username\":\"testuser\",\"unknownFutureField\":\"some value\"}"; + UserInfo user = new ObjectMapper() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .readValue(json, UserInfo.class); + assertNotNull(user); + assertEquals(1L, user.getId()); + assertEquals("testuser", user.getUsername()); + } + + @Test + void testUserInfoRoleIsNullWhenNotPresent() throws IOException { + String json = "{\"id\":2,\"username\":\"noRole\"}"; + UserInfo user = new ObjectMapper() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .readValue(json, UserInfo.class); + assertNull(user.getRole()); } } diff --git a/src/test/java/com/researchspace/api/clientmodel/UserSearchResultTest.java b/src/test/java/com/researchspace/api/clientmodel/UserSearchResultTest.java index ac54021..0bcd44b 100644 --- a/src/test/java/com/researchspace/api/clientmodel/UserSearchResultTest.java +++ b/src/test/java/com/researchspace/api/clientmodel/UserSearchResultTest.java @@ -20,6 +20,16 @@ void testUserSearchResultDeserialisesFromFixture() throws IOException { assertEquals(20, result.getPageSize().intValue()); assertNotNull(result.getUsers()); assertEquals(1, result.getUsers().size()); - assertEquals("admin", result.getUsers().get(0).getUsername()); + + UserInfo user = result.getUsers().get(0); + assertEquals(1L, user.getId()); + assertEquals("U1", user.getGlobalId()); + assertEquals("jdoe", user.getUsername()); + assertEquals("jdoe@lab.org", user.getEmail()); + assertEquals("Jane", user.getFirstName()); + assertEquals("Doe", user.getLastName()); + assertEquals(UserRole.ROLE_PI, user.getRole()); + assertEquals("Research Lab", user.getAffiliation()); + assertEquals(10L, user.getHomeFolderId()); } } diff --git a/src/test/resources/UserInfo.json b/src/test/resources/UserInfo.json index 4b3c663..622f704 100644 --- a/src/test/resources/UserInfo.json +++ b/src/test/resources/UserInfo.json @@ -6,6 +6,7 @@ "email": "jsmith@lab.org", "firstName": "John", "lastName": "Smith", + "role": "ROLE_USER", "affiliation": "Acme Labs", "enabled": true, "homeFolderId": 200, diff --git a/src/test/resources/UserSearchResult.json b/src/test/resources/UserSearchResult.json index 1e4f6e9..e2c0c36 100644 --- a/src/test/resources/UserSearchResult.json +++ b/src/test/resources/UserSearchResult.json @@ -5,11 +5,16 @@ "users": [ { "id": 1, - "username": "admin", - "email": "admin@example.com", - "firstName": "Admin", - "lastName": "User", - "homeFolderId": 5 + "globalId": "U1", + "name": "jdoe", + "username": "jdoe", + "email": "jdoe@lab.org", + "firstName": "Jane", + "lastName": "Doe", + "role": "ROLE_PI", + "affiliation": "Research Lab", + "enabled": true, + "homeFolderId": 10 } ], "_links": [] From 051c67ffeef3955563460f9dd266c418503643f1 Mon Sep 17 00:00:00 2001 From: nebay-abraha Date: Thu, 14 May 2026 17:14:34 +0100 Subject: [PATCH 4/6] fix: address remaining PR #14 review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UserInfo: clarify getName() vs getUsername() in Javadoc — name is the display name (from IdentifiableNameable), username is the login identifier - UserPost: remove unverifiable cross-repo reference from apiKey comment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../java/com/researchspace/api/clientmodel/UserInfo.java | 5 +++++ .../java/com/researchspace/api/clientmodel/UserPost.java | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/researchspace/api/clientmodel/UserInfo.java b/src/main/java/com/researchspace/api/clientmodel/UserInfo.java index 0aca96e..197a23c 100644 --- a/src/main/java/com/researchspace/api/clientmodel/UserInfo.java +++ b/src/main/java/com/researchspace/api/clientmodel/UserInfo.java @@ -8,6 +8,11 @@ /** * Full user information as returned by the sysadmin user-listing endpoint. + *

+ * Note: {@code name} (inherited from {@link IdentifiableNameable}) is the display name of the user, + * while {@code username} is the unique login identifier used for authentication and API calls. + * Both fields are populated from the server response. + *

*/ @Data @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/researchspace/api/clientmodel/UserPost.java b/src/main/java/com/researchspace/api/clientmodel/UserPost.java index e948d4c..3a472f6 100644 --- a/src/main/java/com/researchspace/api/clientmodel/UserPost.java +++ b/src/main/java/com/researchspace/api/clientmodel/UserPost.java @@ -34,7 +34,7 @@ public class UserPost { @Size(min=1) private String affiliation; - // Server constraint: 16–32 characters; see com.researchspace.api.v1.model.ApiUserPost for exact validation + // Server accepts 16–32 characters for the API key field. @Size(min = 16, max = 32) private String apiKey; From da97e4f874ea2ffd020f6126256d53b5ff50c41f Mon Sep 17 00:00:00 2001 From: nebay-abraha Date: Thu, 11 Jun 2026 18:25:34 +0100 Subject: [PATCH 5/6] address PR review --- .gitignore | 3 +- AGENTS.md | 270 ------------------ CHANGELOG.md | 188 ++++++------ CLAUDE.md | 159 +++++++++++ .../api/clientmodel/AbstractExportPost.java | 4 +- .../api/clientmodel/UserPost.java | 1 - .../clientmodel/ActivitySearchResultTest.java | 1 + .../api/clientmodel/FileSearchResultTest.java | 1 + .../FolderTreeItemListingTest.java | 1 + .../api/clientmodel/FormSearchResultTest.java | 2 +- .../api/clientmodel/UserInfoTest.java | 4 +- src/test/resources/ActivitySearchResult.json | 1 + src/test/resources/FileSearchResult.json | 1 + src/test/resources/FolderTreeItemListing.json | 1 + src/test/resources/FormSearchResult.json | 1 + 15 files changed, 274 insertions(+), 364 deletions(-) delete mode 100644 AGENTS.md create mode 100644 CLAUDE.md diff --git a/.gitignore b/.gitignore index a12d458..82d50ba 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,5 @@ hs_err_pid* /.gradle/ /target/* .idea/* -.vscode/ \ No newline at end of file +.vscode/ +.claude/ \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index bb62583..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,270 +0,0 @@ -# AGENTS.md — rspace-client-java-model - -> Single source of truth for AI agents and human contributors. -> When the agent produces incorrect output, make the rule more explicit here. -> Never patch via one-off prompt instructions. - ---- - -## Purpose - -Pure POJO library. No HTTP. No Spring. No business logic. -Provides request and response model classes for the RSpace REST API used by: -- `rspace-api-acceptance` — the API client and acceptance test repo -- `rspace-playwright-java` — the UI E2E test framework (via `ApiUtils`) - ---- - -## Module layout - -``` -src/main/java/ - com.researchspace.api.clientmodel/ ← ELN request + response POJOs - com.researchspace.api.clientmodel.inventory/ ← Inventory base types only - com.researchspace.api.jackson/ ← Custom Jackson serialisers - -src/test/java/ - com.researchspace.api.clientmodel/ ← Unit tests: serialisation + builder contracts -``` - ---- - -## Class taxonomy — know which pattern applies before creating a new class - -### Response POJOs (what the server sends back) - -Used for deserialisation. Must survive unknown fields from future server releases. - -| Class | Extends | Lombok | Extra | -|---|---|---|---| -| `Document` | `DocumentInfo` | `@Data @NoArgsConstructor` | — | -| `DocumentInfo` | `IdentifiableNameable` | `@Data @NoArgsConstructor` | — | -| `Folder` | `IdentifiableNameable` | `@Data @NoArgsConstructor` | — | -| `ApiFile` | `IdentifiableNameable` | `@Data @NoArgsConstructor` | — | -| `Form` | `FormInfo` | `@Data @NoArgsConstructor @SuperBuilder` | `@JsonPropertyOrder` | -| `FormInfo` | `IdentifiableNameable` | `@Data @NoArgsConstructor @SuperBuilder` | `@JsonPropertyOrder` | -| `GroupInfo` | `IdentifiableNameable` | `@Data @NoArgsConstructor` | — | -| `User` | — | `@Data @NoArgsConstructor` | — | -| `ActivitySearchResult` | `PaginatedResultList` | `@Data @NoArgsConstructor` | — | -| `DocumentSearchResult` | `PaginatedResultList` | `@Data @NoArgsConstructor` | — | -| `FileSearchResult` | `PaginatedResultList` | `@Data @NoArgsConstructor` | — | -| `FormSearchResult` | `PaginatedResultList` | `@Data @NoArgsConstructor` | — | -| `ShareSearchResult` | `PaginatedResultList` | `@Data @NoArgsConstructor` | — | -| `UserInfo` | `IdentifiableNameable` | `@Data @NoArgsConstructor` | — | -| `UserSearchResult` | `PaginatedResultList` | `@Data @NoArgsConstructor` | — | -| `GroupSearchResult` | `PaginatedResultList` | `@Data @NoArgsConstructor` | — | - -### Request POJOs (what the client sends to the server) - -Used for serialisation only. `@Builder` pattern for clean construction at call sites. - -| Class | Lombok | -|---|---| -| `DocumentPost` | `@Data @Builder` + `@Singular` on `fields` | -| `FolderPost` | `@Data @Builder` | -| `FormPost.Form` (inner) | `@Data @Builder` + `@Singular` on `fields` | -| `GroupPost` | `@Data @AllArgsConstructor @NoArgsConstructor @Builder` + `@Singular` on `users` | -| `UserPost` | `@Data @AllArgsConstructor @NoArgsConstructor @Builder` | -| `SharePost` | `@Data @Builder @AllArgsConstructor @NoArgsConstructor` | -| `MoveRequest` | `@Data @NoArgsConstructor @AllArgsConstructor @Builder` | - -### Inheritance base classes — never instantiate directly - -``` -Linkable ← _links list + getLinkByType() - └── PaginatedResultList ← totalHits, pageNumber, pageSize - └── IdentifiableNameable ← id, globalId, name - └── DocumentInfo ← created, lastModified, tags, form, owner, parentFolderId - └── Document ← fields: List - └── FormInfo ← stableId, version, formState, accessControl, tags - └── Form ← fields: List - └── GroupInfo ← type, sharedFolderId, members - └── ApiFile ← contentType, size, caption, created, version - └── RecordInfo (inventory) ← description, tags, sharingMode, barcodes -``` - -### Form field hierarchy — polymorphic deserialisation via `@JsonTypeInfo` - -`FormField` is abstract and uses `@JsonSubTypes` to deserialise by `"type"` property. -Concrete types: `StringFormField`, `TextFormField`, `NumberFormField`, -`RadioFormField`, `ChoiceFormField`, `DateFormField`, `TimeFormField`. - -When deserialising a `Form` containing fields, Jackson dispatches on the `"type"` string. -The discriminator values are capitalised: `"String"`, `"Text"`, `"Number"`, `"Radio"`, -`"Choice"`, `"Date"`, `"Time"`. - -`FormPost.FormFieldPost` (inner class hierarchy) is the **request** counterpart — -it has the same field names but `@Builder` constructors and no `@JsonSubTypes`. -The two hierarchies are parallel — never mix them. - -### Inventory types — split across two packages - -``` -com.researchspace.api.clientmodel.inventory/ ← in THIS repo - RecordInfo ← abstract base for all inventory items - Barcode ← barcode attached to an item - SharedWith ← group-level sharing details - TagInfo ← ontology tag - -com.researchspace.api.acceptance.invmodel/ ← in rspace-api-acceptance - RSSample extends RecordInfo - RSInvContainer extends RecordInfo - Subsample extends RecordInfo - SamplePost extends RecordInfo - ApiSampleTemplate - ... (all concrete inventory types) -``` - -Never put concrete inventory response types (`RSSample`, `RSInvContainer` etc.) -in this repo — they belong in `rspace-api-acceptance`. -Only add to `inventory/` if it is a base type used by multiple concrete classes. - ---- - -## Lombok rules - -**Response POJOs** use: `@Data @NoArgsConstructor` (+ `@EqualsAndHashCode(callSuper=true)` for subclasses) -**Request POJOs** use: `@Data @Builder @NoArgsConstructor @AllArgsConstructor` -**Abstract base classes** use: `@Data @NoArgsConstructor @SuperBuilder` -**Search result list wrappers** use: `@Data @NoArgsConstructor @EqualsAndHashCode(callSuper=true)` - -### `@Value` — do not use for response POJOs - -`@Value` generates an immutable class: all fields `final`, no setters, all-args constructor. -Jackson cannot deserialise into a `@Value` class without a custom deserialiser. - -**Never use `@Value` on any class that Jackson needs to deserialise.** - -### `@Singular` — use for list fields in request builder classes - -`@Singular` on a `List` field in a `@Builder` class allows single-item `.fieldName(item)` -chaining in the builder. Used in: `DocumentPost.fields`, `GroupPost.users`, -`ActivitySearchQuery.domains/actions/usernames`, `SharePost.itemsToShare/groups/users`. - -```java -// Correct — @Singular allows this -DocumentPost.builder() - .field(new FieldPost("content1")) - .field(new FieldPost("content2")) - .build(); - -// Without @Singular you'd need .fields(List.of(...)) -``` - ---- - -## Jackson rules - -### Always verify new fields with `AbstractModelTest.readFileToClass()` - -Every new response POJO field must be verified by: -1. Finding or creating a JSON fixture in `src/test/resources/` -2. Writing a test that deserialises the fixture and asserts the field value -3. Confirming the field name matches exactly what the server returns - -The `AbstractModelTest` configures Jackson with: -- `READ_ENUMS_USING_TO_STRING` — enums deserialise via `toString()` not `name()` -- `WRITE_ENUMS_USING_TO_STRING` -- `FAIL_ON_UNKNOWN_PROPERTIES` disabled - -The production `BaseApiClientImpl` in `rspace-api-acceptance` configures the same -enum handling but does NOT yet disable `FAIL_ON_UNKNOWN_PROPERTIES`. - -### `ISO8601DateSerialiser` — use for date fields sent TO the server - -`DateFormField`, `DateFieldPost` use `@JsonSerialize(using = ISO8601DateSerialiser.class)` -to serialise `Date` values as `"yyyy-MM-dd"` strings. -Use this serialiser for any new date field that the server expects in that format. -Do not use it for response date fields — those are deserialised as standard epoch millis. - -### `@JsonProperty("_links")` — already on `Linkable` - -`Linkable.links` is already annotated `@JsonProperty("_links")`. -Do not re-annotate it in subclasses. - -### `@JsonPropertyOrder` — use for types that appear in Swagger examples - -`Form`, `FormInfo`, and all `FormField` subclasses have `@JsonPropertyOrder` so that -serialised JSON matches the Swagger documentation order. Add `@JsonPropertyOrder` -to new types that will appear in API documentation or test fixtures. - ---- - -## Validation annotations - -`@Size`, `@NotNull`, `@Pattern`, `@Min` from `javax.validation` are declared on -**request POJOs** only — they document what the server expects but are not enforced -client-side (there is no validation runner). They serve as in-code documentation. - -The dependency is `javax.validation:validation-api:1.1.0.Final`. Do not upgrade to -`jakarta.validation` — this would break compatibility with Spring 5 in `rspace-api-acceptance`. - ---- - -## Testing rules - -All tests use **JUnit 5** (`org.junit.jupiter.api.Test`, `@BeforeEach`, `@AfterEach`). -This is consistent throughout the test suite — do not use JUnit 4 here. - -### Two test patterns — both are in use - -**JSON fixture roundtrip** (via `AbstractModelTest`): -```java -// Place fixture in src/test/resources/MyType.json -// Extend AbstractModelTest -MyType result = readFileToClass(new File("src/test/resources/MyType.json"), MyType.class); -assertNotNull(result.getSomeField()); -assertEquals("expectedValue", result.getSomeField()); -``` - -**Serialisation roundtrip** (via inline `ObjectMapper`): -```java -// Used in FormFieldTest, AccessControlTest — tests that classes serialise correctly -ObjectMapper om = new ObjectMapper(); -om.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); -String json = om.writeValueAsString(myObject); -MyType deserialized = om.readValue(json, MyType.class); -assertEquals(expected, deserialized.getSomeField()); -``` - -### Every new POJO needs at least one test - -Minimum: a test that verifies the class can be deserialised from a representative JSON fixture. -If the class has builder methods (`addField()`, `appendContent()`, `appendFileReference()`), -test those too. - -### JSON fixtures live in `src/test/resources/` - -Existing fixtures: `Document.json`, `DocumentSearchResult.json`, `DocumentSearchQuery.json`, -`ActivitySearchResult.json`, `FileSearchResult.json`, `File.json`, `Folder.json`, -`FormSearchResult.json`, `Form.json`, `completedJob.json`. - -When creating a fixture: use real server response JSON where possible, or construct -a minimal valid JSON that exercises the fields you care about. - ---- - -## Adding a new class - -1. **Decide which category** it belongs to (response POJO, request POJO, base class, inventory base) -2. **Choose Lombok annotations** from the taxonomy table above — do not invent new combinations -3. **Add `@JsonIgnoreProperties(ignoreUnknown = true)`** on response POJOs -4. **Add a JSON fixture** in `src/test/resources/` or use serialisation roundtrip test -5. **Write a unit test** in `src/test/java/com/researchspace/api/clientmodel/` -6. **Verify against dev source** before adding fields not in the current Swagger: - `https://github.com/rspace-os/rspace-web/tree/main/src/main/java/com/researchspace/api/v1` - ---- - -## What the agent MUST NOT do - -- Add Spring, RestTemplate, or HTTP dependencies to this module -- Add concrete inventory response types (`RSSample`, `RSInvContainer` etc.) — those belong in `rspace-api-acceptance` -- Use `@Value` on a new class that needs Jackson deserialisation -- Copy the swagger-codegen `DO NOT EDIT` header comment — those files have been edited -- Add a field to a response POJO without a corresponding test that verifies it deserialises -- Use `jakarta.validation` annotations — use `javax.validation` only -- Upgrade `validation-api` past `1.1.0.Final` without checking compatibility -- Create new Lombok annotation combinations not in the taxonomy table above - -> Last updated: 2026-05-05 diff --git a/CHANGELOG.md b/CHANGELOG.md index 32e8c5a..fd2bf7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,86 +1,102 @@ -# Changelog -All notable changes to this project will be documented in this file. - -## [1.100.0] - -### Breaking Changes - -- **`AbstractExportPost`**: renamed `scope()` → `getScopeString()` and `format()` → `getFormatString()` to avoid confusion with Lombok-generated getters. Update any callers of the old method names. -- **`ApiShareInfo.sharedItemName`**: field type corrected from `Long` to `String`. Any code storing or passing `getSharedItemName()` as a `Long` must be updated. -- **`FieldPut`**: removed the locally-shadowed `content` field; `FieldPut` now inherits `content` from `FieldPost`. The explicit two-arg constructor `FieldPut(String content, Long id)` is preserved, so existing callers will continue to compile. However, `equals()`/`hashCode()` no longer compare two separate `content` fields, and any code that set the shadowed field via reflection will need to update to the inherited field. -- **`ActivitySearchResult`, `DocumentSearchResult`, `FileSearchResult`, `FormSearchResult`, `ShareSearchResult`**: changed from `@Value` (immutable, all-args constructor) to `@Data @NoArgsConstructor` (mutable, setters). These are response POJOs and should not be constructed directly by callers, but any code that relied on the `@Value`-generated constructor will no longer compile. -- **`User.java`**: replaced `@Value @NoArgsConstructor` with `@Data @NoArgsConstructor`. The `@Value`-generated all-args constructor is removed; any code using positional construction (`new User(id, username, …)`) will no longer compile. -- **`ISO8601DateSerialiser`**: dates are now always serialised in UTC (previously used the JVM default timezone). Any `Date` value whose local-timezone calendar date differs from its UTC calendar date will now serialise differently. Ensure all `Date` values passed to `DateFormField` represent midnight UTC if exact date identity matters. - -### Added - -- `UserInfo` model — full user information as returned by the sysadmin user-listing endpoint. -- `UserSearchResult` model — paginated wrapper for `UserInfo` lists. -- `GroupSearchResult` model — paginated wrapper for `GroupInfo` lists returned by the sysadmin group-listing endpoint. - -### Fixed - -- `@JsonIgnoreProperties(ignoreUnknown = true)` added to all response POJOs (via `IdentifiableNameable` and individually on non-hierarchy classes). Prevents `UnrecognizedPropertyException` when the server adds new fields in future releases. -- Removed stale swagger-codegen `DO NOT EDIT` headers from 12 source files that have been manually maintained. -- `ISO8601DateSerialiser`: replaced per-call `SimpleDateFormat` allocation with a thread-safe static `DateTimeFormatter`. See Breaking Changes for the associated timezone behaviour change. -- `ApiShareInfo`: split the multi-field `Long` declaration; `sharedItemName` is now a separate `String` field. - -## [1.99.1] - -### Added - -- Add support for form features: - - `AccessControl` model - Represents permissions for view/edit access to forms - - `Form` model - Complete form definition including field definitions - - `FormField` abstract model – for all form field types - - `FormState` enum - Enumeration for form publishing states (NEW, PUBLISHED, UNPUBLISHED, OLD) - - `ChoiceFormField` model - Checkbox field with multiple selection support - - `DateFormField` model - Date input field with min/max validation - - `NumberFormField` model - Numeric field with range and decimal places - - `RadioFormField` model - Radio button field for single selection - - `StringFormField` model - Short text input field (max 255 characters) - - `TextFormField` model - Long text field supporting HTML content - - `TimeFormField` model - Time input field storing millisecond values - -### Changed - -- Enhanced `FormInfo` model with additional properties: - - Added `formState` property for tracking publishing status - - Added `accessControl` property for permission management - - Added `tags` property for form categorization - - Added `iconId` property for form visual identification - -## [1.99.0] -- switch from rspace-os-parent to rspace-parent as parent pom - -## [1.98.1] - -### Added -- Introduced support for a document: - - `MoveRequest` model - -### Changed -- Upgrade dependency: lombok 1.18.42. - -## [1.98.0] - -### Added -- Introduced support for Inventory features: - - `Barcode` model - - `TagInfo` model - - `RecordInfo` model - - `SharedWith` model -- Added the ability to represent sharing mode, barcodes, and tags for Inventory. - -## [1.97.0] - -- Support newly added audit (activity) actions: DUPLICATE and RENAME -- Update list of possible audit (activity) domains - -## [1.95.0] -- Compile with java 17 -- Switch to using parent pom from rspace-os-parent project -- Change of produced artifact to from com.researchspace to com.github.rspace-os - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# Changelog +All notable changes to this project will be documented in this file. + +## [1.100.0] + +### Breaking Changes + +- **`AbstractExportPost`** — `scope()` renamed to `getScopeAsString()`, `format()` renamed to `getFormatAsString()`. + Update any callers of the old method names. + +- **`ApiShareInfo.sharedItemName`** — field type corrected from `Long` to `String`. + Update any code that stores or passes `getSharedItemName()` as a `Long`. + +- **`FieldPut`** — locally-shadowed `content` field removed; `FieldPut` now inherits `content` from `FieldPost`. + The two-arg constructor `FieldPut(String content, Long id)` is preserved so existing callers still compile. + However, `equals()`/`hashCode()` no longer compare two separate `content` fields, and any code that set + the shadowed field via reflection must switch to the inherited field. + +- **`ActivitySearchResult`, `DocumentSearchResult`, `FileSearchResult`, `FormSearchResult`, `ShareSearchResult`** — + changed from `@Value` (immutable, all-args constructor) to `@Data @NoArgsConstructor` (mutable, no all-args constructor). + These are response POJOs and should not be constructed directly, but any code using the `@Value`-generated + constructor will no longer compile. + +- **`User`** — replaced `@Value @NoArgsConstructor` with `@Data @NoArgsConstructor`. + Any code using positional construction (`new User(id, username, …)`) will no longer compile. + +- **`ISO8601DateSerialiser`** — dates are now always serialised in UTC (previously used the JVM default timezone). + A `Date` value whose local-timezone calendar date differs from its UTC date will now serialise differently. + Ensure all `Date` values passed to `DateFormField` represent midnight UTC if exact date identity matters. + +### Added + +- `UserInfo` — full user information returned by the sysadmin user-listing endpoint. +- `UserSearchResult` — paginated wrapper for `UserInfo` lists. +- `GroupSearchResult` — paginated wrapper for `GroupInfo` lists returned by the sysadmin group-listing endpoint. + +### Fixed + +- `@JsonIgnoreProperties(ignoreUnknown = true)` added to all response POJOs. Prevents `UnrecognizedPropertyException` + when the server adds new fields in future releases. +- `ISO8601DateSerialiser` — replaced per-call `SimpleDateFormat` with a thread-safe static `DateTimeFormatter`. +- Removed stale swagger-codegen `DO NOT EDIT` headers from 12 source files. + +## [1.99.1] + +### Added + +- Add support for form features: + - `AccessControl` model - Represents permissions for view/edit access to forms + - `Form` model - Complete form definition including field definitions + - `FormField` abstract model – for all form field types + - `FormState` enum - Enumeration for form publishing states (NEW, PUBLISHED, UNPUBLISHED, OLD) + - `ChoiceFormField` model - Checkbox field with multiple selection support + - `DateFormField` model - Date input field with min/max validation + - `NumberFormField` model - Numeric field with range and decimal places + - `RadioFormField` model - Radio button field for single selection + - `StringFormField` model - Short text input field (max 255 characters) + - `TextFormField` model - Long text field supporting HTML content + - `TimeFormField` model - Time input field storing millisecond values + +### Changed + +- Enhanced `FormInfo` model with additional properties: + - Added `formState` property for tracking publishing status + - Added `accessControl` property for permission management + - Added `tags` property for form categorization + - Added `iconId` property for form visual identification + +## [1.99.0] +- switch from rspace-os-parent to rspace-parent as parent pom + +## [1.98.1] + +### Added +- Introduced support for a document: + - `MoveRequest` model + +### Changed +- Upgrade dependency: lombok 1.18.42. + +## [1.98.0] + +### Added +- Introduced support for Inventory features: + - `Barcode` model + - `TagInfo` model + - `RecordInfo` model + - `SharedWith` model +- Added the ability to represent sharing mode, barcodes, and tags for Inventory. + +## [1.97.0] + +- Support newly added audit (activity) actions: DUPLICATE and RENAME +- Update list of possible audit (activity) domains + +## [1.95.0] +- Compile with java 17 +- Switch to using parent pom from rspace-os-parent project +- Change of produced artifact to from com.researchspace to com.github.rspace-os + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..089b45b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,159 @@ +# rspace-client-java-model + +Pure POJO library. No HTTP. No Spring. No business logic. +Provides request and response model classes for the RSpace REST API, consumed by: +- `rspace-api-acceptance` — the API client and acceptance test repo +- `rspace-playwright-java` — the UI E2E test framework (via `ApiUtils`) + +**Cross-repo rule:** Any change to this library that renames, removes, or changes the type of a field or method must also be checked (and fixed if broken) in both `rspace-api-acceptance` and `rspace-playwright-java` before the PR is merged. + +--- + +## Module layout + +``` +src/main/java/ + com.researchspace.api.clientmodel/ ← ELN request + response POJOs + com.researchspace.api.clientmodel.inventory/ ← Inventory base types only + com.researchspace.api.jackson/ ← Custom Jackson serialisers + +src/test/java/ + com.researchspace.api.clientmodel/ ← Unit tests: serialisation + builder contracts +``` + +### Inheritance hierarchy — never instantiate base classes directly + +``` +Linkable ← _links list + getLinkByType() + └── PaginatedResultList ← totalHits, pageNumber, pageSize + └── IdentifiableNameable ← id, globalId, name + └── DocumentInfo ← created, lastModified, tags, form, owner, parentFolderId + └── Document ← fields: List + └── FormInfo ← stableId, version, formState, accessControl, tags + └── Form ← fields: List + └── GroupInfo ← type, sharedFolderId, members + └── ApiFile ← contentType, size, caption, created, version + └── RecordInfo (inventory) ← description, tags, sharingMode, barcodes +``` + + +## Lombok rules + +- **Response POJOs**: `@Data @NoArgsConstructor` (+ `@EqualsAndHashCode(callSuper=true) @ToString(callSuper=true)` for subclasses) +- **Request POJOs**: `@Data @Builder @NoArgsConstructor @AllArgsConstructor` +- **Abstract base classes**: `@Data @NoArgsConstructor @SuperBuilder` +- **Search result wrappers**: `@Data @NoArgsConstructor @EqualsAndHashCode(callSuper=true) @ToString(callSuper=true)` + +Do not invent new Lombok combinations not listed here. + +**Never use `@Value` on a class that Jackson needs to deserialise.** `@Value` makes all fields `final` with no setters and generates an all-args constructor; Jackson cannot deserialise into it without a custom deserialiser. + +**`@Singular`** on a `List` field in a `@Builder` class enables single-item chaining: +```java +DocumentPost.builder() + .field(new FieldPost("content1")) + .field(new FieldPost("content2")) + .build(); +``` +Used in: `DocumentPost.fields`, `GroupPost.users`, `ActivitySearchQuery.domains/actions/usernames`, `SharePost.itemsToShare/groups/users`. + +--- + +## Jackson rules + +### `@JsonIgnoreProperties(ignoreUnknown = true)` + +Add this to every response POJO. `IdentifiableNameable` already carries it, so its subclasses inherit it — but also add it explicitly to non-hierarchy response classes (`User`, `Status`, `ApiShareInfo`, etc.) for clarity. + +### `@JsonProperty` on field name mismatches + +Use `@JsonProperty("serverFieldName")` when the Java field name differs from what the server sends. Example: `MoveRequest.recordId` is annotated `@JsonProperty("docId")` because the server API field is `docId`. + +### `ISO8601DateSerialiser` — for date fields sent TO the server + +`DateFormField` and `DateFieldPost` use `@JsonSerialize(using = ISO8601DateSerialiser.class)` to serialise `Date` values as `"yyyy-MM-dd"` strings in UTC. Use this serialiser for any new date field the server expects in that format. Do not use it for response date fields — those deserialise as standard epoch millis. + +Dates are always serialised in UTC. Callers must ensure the logical date they intend to send matches the UTC date of the `Date` instance (e.g. set to midnight UTC). + +### `@JsonProperty("_links")` is on `Linkable` + +Do not re-annotate `_links` in subclasses. + +### `@JsonPropertyOrder` — for types in Swagger examples + +`Form`, `FormInfo`, and all `FormField` subclasses use `@JsonPropertyOrder` so serialised JSON matches documentation order. Add it to new types that will appear in API docs or test fixtures. + +--- + +## Testing rules + +All tests use **JUnit 5** (`org.junit.jupiter.api.Test`). Do not use JUnit 4. + +Use **British spelling** throughout: `serialised`, `deserialised`, `initialised`, etc. This is consistent with the class name `ISO8601DateSerialiser` and all existing test method names. + +### Two test patterns + +**JSON fixture roundtrip** (via `AbstractModelTest`) — use for response POJOs: +```java +MyType result = readFileToClass(new File("src/test/resources/MyType.json"), MyType.class); +assertEquals("expectedValue", result.getSomeField()); +``` + +**Serialisation roundtrip** (inline `ObjectMapper`) — use for request POJOs and serialisation contracts: +```java +ObjectMapper om = new ObjectMapper(); +String json = om.writeValueAsString(myObject); +MyType deserialised = om.readValue(json, MyType.class); +assertEquals(expected, deserialised.getSomeField()); +``` + +### Testing `@JsonIgnoreProperties` correctly + +To verify that `@JsonIgnoreProperties(ignoreUnknown = true)` works on a class, use a plain `new ObjectMapper()` with no mapper-level configuration changes. If you disable `FAIL_ON_UNKNOWN_PROPERTIES` on the mapper instead, the test doesn't prove the annotation works: + +```java +// Correct — tests the annotation +String json = "{\"id\":1,\"unknownFutureField\":\"x\"}"; +MyType obj = new ObjectMapper().readValue(json, MyType.class); + +// Wrong — tests the mapper setting, not the annotation +MyType obj = new ObjectMapper() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .readValue(json, MyType.class); +``` + +### Every new POJO needs at least one test + +Minimum: deserialise from a representative JSON fixture and assert on the fields. If the class has builder methods (`addField()`, `appendContent()`), test those too. + +### JSON fixtures + +Location: `src/test/resources/`. Every `PaginatedResultList` fixture must include `totalHits`, `pageNumber`, and `pageSize`. + +Existing fixtures: `Document.json`, `DocumentSearchResult.json`, `DocumentSearchQuery.json`, `ActivitySearchResult.json`, `FileSearchResult.json`, `File.json`, `Folder.json`, `FormSearchResult.json`, `Form.json`, `FolderTreeItemListing.json`, `GroupSearchResult.json`, `User.json`, `UserInfo.json`, `UserSearchResult.json`, `completedJob.json`. + +--- + +## Adding a new class — checklist + +1. Decide which category: response POJO, request POJO, base class, or inventory base +2. Choose Lombok annotations from the taxonomy table — do not invent new combinations +3. Add `@JsonIgnoreProperties(ignoreUnknown = true)` if it is a response POJO +4. Add a JSON fixture in `src/test/resources/` (or use a serialisation roundtrip test for request POJOs) +5. Write a unit test asserting on fields +6. Verify field names against the server source before adding fields not in the current Swagger: `https://github.com/rspace-os/rspace-web/tree/main/src/main/java/com/researchspace/api/v1` +7. Check `rspace-api-acceptance` and `rspace-playwright-java` for any callers affected by the change + +--- + +## Constraints + +- No Spring, RestTemplate, or HTTP dependencies in this module +- No concrete inventory response types (`RSSample`, `RSInvContainer`, etc.) — those belong in `rspace-api-acceptance` +- No `@Value` on classes that Jackson needs to deserialise +- No swagger-codegen `DO NOT EDIT` header comments — those files are manually maintained +- No `jakarta.validation` — use `javax.validation` only +- No new field on a response POJO without a test that verifies it deserialises correctly +- No new Lombok combinations not in the taxonomy table + +--- diff --git a/src/main/java/com/researchspace/api/clientmodel/AbstractExportPost.java b/src/main/java/com/researchspace/api/clientmodel/AbstractExportPost.java index c0047f1..265b551 100644 --- a/src/main/java/com/researchspace/api/clientmodel/AbstractExportPost.java +++ b/src/main/java/com/researchspace/api/clientmodel/AbstractExportPost.java @@ -14,12 +14,12 @@ public class AbstractExportPost { private ExportFormat format = ExportFormat.HTML; @JsonIgnore - public String getScopeString() { + public String getScopeAsString() { return scope.name().toLowerCase(); } @JsonIgnore - public String getFormatString() { + public String getFormatAsString() { return format.name().toLowerCase(); } diff --git a/src/main/java/com/researchspace/api/clientmodel/UserPost.java b/src/main/java/com/researchspace/api/clientmodel/UserPost.java index 3a472f6..57713b1 100644 --- a/src/main/java/com/researchspace/api/clientmodel/UserPost.java +++ b/src/main/java/com/researchspace/api/clientmodel/UserPost.java @@ -34,7 +34,6 @@ public class UserPost { @Size(min=1) private String affiliation; - // Server accepts 16–32 characters for the API key field. @Size(min = 16, max = 32) private String apiKey; diff --git a/src/test/java/com/researchspace/api/clientmodel/ActivitySearchResultTest.java b/src/test/java/com/researchspace/api/clientmodel/ActivitySearchResultTest.java index 6ca0269..ca83554 100644 --- a/src/test/java/com/researchspace/api/clientmodel/ActivitySearchResultTest.java +++ b/src/test/java/com/researchspace/api/clientmodel/ActivitySearchResultTest.java @@ -44,6 +44,7 @@ public void test() throws JsonParseException, JsonMappingException, IOException ActivitySearchResult searchRes = readFileToClass(ActivitySearchResultJson, ActivitySearchResult.class); assertEquals(0,searchRes.getPageNumber().intValue()); assertEquals(1,searchRes.getTotalHits().intValue()); + assertEquals(10,searchRes.getPageSize().intValue()); assertNotNull(searchRes.getActivities().get(0).getTimestamp()); Map payload = searchRes.getActivities().get(0).getPayload(); assertNotNull(payload.get("data")); diff --git a/src/test/java/com/researchspace/api/clientmodel/FileSearchResultTest.java b/src/test/java/com/researchspace/api/clientmodel/FileSearchResultTest.java index f60cb15..efb443c 100644 --- a/src/test/java/com/researchspace/api/clientmodel/FileSearchResultTest.java +++ b/src/test/java/com/researchspace/api/clientmodel/FileSearchResultTest.java @@ -32,6 +32,7 @@ public void test() throws JsonParseException, JsonMappingException, IOException FileSearchResult d = readFileToClass(FileSearchResultJson, FileSearchResult.class); assertEquals(0, d.getPageNumber().intValue()); assertEquals(8, d.getTotalHits().intValue()); + assertEquals(10, d.getPageSize().intValue()); assertNotNull(d.getFiles()); } diff --git a/src/test/java/com/researchspace/api/clientmodel/FolderTreeItemListingTest.java b/src/test/java/com/researchspace/api/clientmodel/FolderTreeItemListingTest.java index 41ad4c6..95376c9 100644 --- a/src/test/java/com/researchspace/api/clientmodel/FolderTreeItemListingTest.java +++ b/src/test/java/com/researchspace/api/clientmodel/FolderTreeItemListingTest.java @@ -19,6 +19,7 @@ void testFolderTreeListingDeserialises() throws IOException { assertNotNull(listing); assertEquals(2L, listing.getTotalHits()); assertEquals(0, listing.getPageNumber()); + assertEquals(10, listing.getPageSize().intValue()); assertEquals(1L, listing.getParentId()); assertFalse(listing.getRecords().isEmpty()); } diff --git a/src/test/java/com/researchspace/api/clientmodel/FormSearchResultTest.java b/src/test/java/com/researchspace/api/clientmodel/FormSearchResultTest.java index b2ccd0f..6d41bfa 100644 --- a/src/test/java/com/researchspace/api/clientmodel/FormSearchResultTest.java +++ b/src/test/java/com/researchspace/api/clientmodel/FormSearchResultTest.java @@ -16,7 +16,7 @@ void test() throws IOException { FormSearchResult formSearchResult = readFileToClass(formSearchResultJson, FormSearchResult.class); assertEquals(0, formSearchResult.getPageNumber().intValue()); assertEquals(5, formSearchResult.getTotalHits().intValue()); - System.err.println(formSearchResult); + assertEquals(10, formSearchResult.getPageSize().intValue()); } } \ No newline at end of file diff --git a/src/test/java/com/researchspace/api/clientmodel/UserInfoTest.java b/src/test/java/com/researchspace/api/clientmodel/UserInfoTest.java index 1471727..398c499 100644 --- a/src/test/java/com/researchspace/api/clientmodel/UserInfoTest.java +++ b/src/test/java/com/researchspace/api/clientmodel/UserInfoTest.java @@ -35,9 +35,7 @@ void testUserInfoDeserialisesFromFixture() throws IOException { @Test void testUserInfoIgnoresUnknownFields() throws IOException { String json = "{\"id\":1,\"globalId\":\"U1\",\"name\":\"testuser\",\"username\":\"testuser\",\"unknownFutureField\":\"some value\"}"; - UserInfo user = new ObjectMapper() - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .readValue(json, UserInfo.class); + UserInfo user = new ObjectMapper().readValue(json, UserInfo.class); assertNotNull(user); assertEquals(1L, user.getId()); assertEquals("testuser", user.getUsername()); diff --git a/src/test/resources/ActivitySearchResult.json b/src/test/resources/ActivitySearchResult.json index 501e7f0..4baae12 100644 --- a/src/test/resources/ActivitySearchResult.json +++ b/src/test/resources/ActivitySearchResult.json @@ -1,6 +1,7 @@ { "totalHits": 1, "pageNumber": 0, + "pageSize": 10, "activities": [ { "username": "bob1234", diff --git a/src/test/resources/FileSearchResult.json b/src/test/resources/FileSearchResult.json index f88c83e..819609c 100644 --- a/src/test/resources/FileSearchResult.json +++ b/src/test/resources/FileSearchResult.json @@ -1,6 +1,7 @@ { "totalHits" : 8, "pageNumber" : 0, + "pageSize" : 10, "files":[ { "id": 1246, diff --git a/src/test/resources/FolderTreeItemListing.json b/src/test/resources/FolderTreeItemListing.json index 3e57379..3f6a70a 100644 --- a/src/test/resources/FolderTreeItemListing.json +++ b/src/test/resources/FolderTreeItemListing.json @@ -1,6 +1,7 @@ { "totalHits": 2, "pageNumber": 0, + "pageSize": 10, "parentId": 1, "records": [ { diff --git a/src/test/resources/FormSearchResult.json b/src/test/resources/FormSearchResult.json index c1e6f54..af514b5 100644 --- a/src/test/resources/FormSearchResult.json +++ b/src/test/resources/FormSearchResult.json @@ -1,6 +1,7 @@ { "totalHits": 5, "pageNumber": 0, + "pageSize": 10, "forms": [ { "id": 32785, From c014424caa4b130dc130dccc7d4a693fa960d4c3 Mon Sep 17 00:00:00 2001 From: nebay-abraha Date: Thu, 11 Jun 2026 19:32:39 +0100 Subject: [PATCH 6/6] updated CLAUDE.md and added missing models --- CHANGELOG.md | 3 + CLAUDE.md | 66 ------------------- .../api/clientmodel/DocumentShareEntry.java | 26 ++++++++ .../api/clientmodel/DocumentShares.java | 23 +++++++ .../clientmodel/SharePermissionUpdate.java | 20 ++++++ .../api/clientmodel/DocumentSharesTest.java | 37 +++++++++++ .../SharePermissionUpdateTest.java | 29 ++++++++ src/test/resources/DocumentShares.json | 19 ++++++ 8 files changed, 157 insertions(+), 66 deletions(-) create mode 100644 src/main/java/com/researchspace/api/clientmodel/DocumentShareEntry.java create mode 100644 src/main/java/com/researchspace/api/clientmodel/DocumentShares.java create mode 100644 src/main/java/com/researchspace/api/clientmodel/SharePermissionUpdate.java create mode 100644 src/test/java/com/researchspace/api/clientmodel/DocumentSharesTest.java create mode 100644 src/test/java/com/researchspace/api/clientmodel/SharePermissionUpdateTest.java create mode 100644 src/test/resources/DocumentShares.json diff --git a/CHANGELOG.md b/CHANGELOG.md index fd2bf7e..089a87a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,9 @@ All notable changes to this project will be documented in this file. - `UserInfo` — full user information returned by the sysadmin user-listing endpoint. - `UserSearchResult` — paginated wrapper for `UserInfo` lists. - `GroupSearchResult` — paginated wrapper for `GroupInfo` lists returned by the sysadmin group-listing endpoint. +- `SharePermissionUpdate` — request body for `PUT /share` (update permission of an existing share). +- `DocumentShares` — response for `GET /share/document/{id}`; contains `directShares` and `notebookShares` as `List`. +- `DocumentShareEntry` — a single entry within a `DocumentShares` response. ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index 089b45b..cd764ea 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,61 +36,10 @@ Linkable ← _links list + getLinkByType() └── RecordInfo (inventory) ← description, tags, sharingMode, barcodes ``` - -## Lombok rules - -- **Response POJOs**: `@Data @NoArgsConstructor` (+ `@EqualsAndHashCode(callSuper=true) @ToString(callSuper=true)` for subclasses) -- **Request POJOs**: `@Data @Builder @NoArgsConstructor @AllArgsConstructor` -- **Abstract base classes**: `@Data @NoArgsConstructor @SuperBuilder` -- **Search result wrappers**: `@Data @NoArgsConstructor @EqualsAndHashCode(callSuper=true) @ToString(callSuper=true)` - -Do not invent new Lombok combinations not listed here. - -**Never use `@Value` on a class that Jackson needs to deserialise.** `@Value` makes all fields `final` with no setters and generates an all-args constructor; Jackson cannot deserialise into it without a custom deserialiser. - -**`@Singular`** on a `List` field in a `@Builder` class enables single-item chaining: -```java -DocumentPost.builder() - .field(new FieldPost("content1")) - .field(new FieldPost("content2")) - .build(); -``` -Used in: `DocumentPost.fields`, `GroupPost.users`, `ActivitySearchQuery.domains/actions/usernames`, `SharePost.itemsToShare/groups/users`. - ---- - -## Jackson rules - -### `@JsonIgnoreProperties(ignoreUnknown = true)` - -Add this to every response POJO. `IdentifiableNameable` already carries it, so its subclasses inherit it — but also add it explicitly to non-hierarchy response classes (`User`, `Status`, `ApiShareInfo`, etc.) for clarity. - -### `@JsonProperty` on field name mismatches - -Use `@JsonProperty("serverFieldName")` when the Java field name differs from what the server sends. Example: `MoveRequest.recordId` is annotated `@JsonProperty("docId")` because the server API field is `docId`. - -### `ISO8601DateSerialiser` — for date fields sent TO the server - -`DateFormField` and `DateFieldPost` use `@JsonSerialize(using = ISO8601DateSerialiser.class)` to serialise `Date` values as `"yyyy-MM-dd"` strings in UTC. Use this serialiser for any new date field the server expects in that format. Do not use it for response date fields — those deserialise as standard epoch millis. - -Dates are always serialised in UTC. Callers must ensure the logical date they intend to send matches the UTC date of the `Date` instance (e.g. set to midnight UTC). - -### `@JsonProperty("_links")` is on `Linkable` - -Do not re-annotate `_links` in subclasses. - -### `@JsonPropertyOrder` — for types in Swagger examples - -`Form`, `FormInfo`, and all `FormField` subclasses use `@JsonPropertyOrder` so serialised JSON matches documentation order. Add it to new types that will appear in API docs or test fixtures. - ---- - ## Testing rules All tests use **JUnit 5** (`org.junit.jupiter.api.Test`). Do not use JUnit 4. -Use **British spelling** throughout: `serialised`, `deserialised`, `initialised`, etc. This is consistent with the class name `ISO8601DateSerialiser` and all existing test method names. - ### Two test patterns **JSON fixture roundtrip** (via `AbstractModelTest`) — use for response POJOs: @@ -107,21 +56,6 @@ MyType deserialised = om.readValue(json, MyType.class); assertEquals(expected, deserialised.getSomeField()); ``` -### Testing `@JsonIgnoreProperties` correctly - -To verify that `@JsonIgnoreProperties(ignoreUnknown = true)` works on a class, use a plain `new ObjectMapper()` with no mapper-level configuration changes. If you disable `FAIL_ON_UNKNOWN_PROPERTIES` on the mapper instead, the test doesn't prove the annotation works: - -```java -// Correct — tests the annotation -String json = "{\"id\":1,\"unknownFutureField\":\"x\"}"; -MyType obj = new ObjectMapper().readValue(json, MyType.class); - -// Wrong — tests the mapper setting, not the annotation -MyType obj = new ObjectMapper() - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .readValue(json, MyType.class); -``` - ### Every new POJO needs at least one test Minimum: deserialise from a representative JSON fixture and assert on the fields. If the class has builder methods (`addField()`, `appendContent()`), test those too. diff --git a/src/main/java/com/researchspace/api/clientmodel/DocumentShareEntry.java b/src/main/java/com/researchspace/api/clientmodel/DocumentShareEntry.java new file mode 100644 index 0000000..88dea6c --- /dev/null +++ b/src/main/java/com/researchspace/api/clientmodel/DocumentShareEntry.java @@ -0,0 +1,26 @@ +package com.researchspace.api.clientmodel; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * A single share entry within a {@link DocumentShares} response. + */ +@Data +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class DocumentShareEntry { + + private Long shareId; + private Long sharerId; + private String sharerName; + private Long recipientId; + private String recipientName; + private String recipientType; + private String permission; + private Long parentId; + private String path; + private Long grandparentId; + +} diff --git a/src/main/java/com/researchspace/api/clientmodel/DocumentShares.java b/src/main/java/com/researchspace/api/clientmodel/DocumentShares.java new file mode 100644 index 0000000..9cce002 --- /dev/null +++ b/src/main/java/com/researchspace/api/clientmodel/DocumentShares.java @@ -0,0 +1,23 @@ +package com.researchspace.api.clientmodel; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +/** + * Response for GET /share/document/{id} — all shares for a document, notebook or snippet. + */ +@Data +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class DocumentShares { + + private Long sharedDocId; + private String sharedDocName; + private List directShares = new ArrayList<>(); + private List notebookShares = new ArrayList<>(); + +} diff --git a/src/main/java/com/researchspace/api/clientmodel/SharePermissionUpdate.java b/src/main/java/com/researchspace/api/clientmodel/SharePermissionUpdate.java new file mode 100644 index 0000000..3b605a0 --- /dev/null +++ b/src/main/java/com/researchspace/api/clientmodel/SharePermissionUpdate.java @@ -0,0 +1,20 @@ +package com.researchspace.api.clientmodel; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Request body for PUT /share — updates the permission level of an existing share. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SharePermissionUpdate { + + private Long shareId; + private String permission; + +} diff --git a/src/test/java/com/researchspace/api/clientmodel/DocumentSharesTest.java b/src/test/java/com/researchspace/api/clientmodel/DocumentSharesTest.java new file mode 100644 index 0000000..74b71da --- /dev/null +++ b/src/test/java/com/researchspace/api/clientmodel/DocumentSharesTest.java @@ -0,0 +1,37 @@ +package com.researchspace.api.clientmodel; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.io.InputStream; + +import static org.junit.jupiter.api.Assertions.*; + +class DocumentSharesTest { + + @Test + void deserialiseFromFixture() throws Exception { + try (InputStream is = getClass().getResourceAsStream("/DocumentShares.json")) { + assertNotNull(is, "DocumentShares.json fixture not found"); + DocumentShares result = new ObjectMapper().readValue(is, DocumentShares.class); + assertEquals(100L, result.getSharedDocId()); + assertEquals("My Document", result.getSharedDocName()); + assertEquals(1, result.getDirectShares().size()); + assertTrue(result.getNotebookShares().isEmpty()); + DocumentShareEntry entry = result.getDirectShares().get(0); + assertEquals(1L, entry.getShareId()); + assertEquals("Smith Group", entry.getRecipientName()); + assertEquals("GROUP", entry.getRecipientType()); + assertEquals("READ", entry.getPermission()); + } + } + + @Test + void ignoresUnknownFields() throws Exception { + String json = "{\"sharedDocId\":1,\"directShares\":[],\"notebookShares\":[],\"unknownFutureField\":\"x\"}"; + DocumentShares result = new ObjectMapper().readValue(json, DocumentShares.class); + assertEquals(1L, result.getSharedDocId()); + assertTrue(result.getDirectShares().isEmpty()); + } + +} diff --git a/src/test/java/com/researchspace/api/clientmodel/SharePermissionUpdateTest.java b/src/test/java/com/researchspace/api/clientmodel/SharePermissionUpdateTest.java new file mode 100644 index 0000000..ffb960e --- /dev/null +++ b/src/test/java/com/researchspace/api/clientmodel/SharePermissionUpdateTest.java @@ -0,0 +1,29 @@ +package com.researchspace.api.clientmodel; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class SharePermissionUpdateTest { + + @Test + void roundTrip() throws Exception { + SharePermissionUpdate req = SharePermissionUpdate.builder() + .shareId(42L).permission("WRITE").build(); + ObjectMapper mapper = new ObjectMapper(); + String json = mapper.writeValueAsString(req); + SharePermissionUpdate result = mapper.readValue(json, SharePermissionUpdate.class); + assertEquals(req, result); + } + + @Test + void noArgConstructorAndSetters() { + SharePermissionUpdate req = new SharePermissionUpdate(); + req.setShareId(7L); + req.setPermission("READ"); + assertEquals(7L, req.getShareId()); + assertEquals("READ", req.getPermission()); + } + +} diff --git a/src/test/resources/DocumentShares.json b/src/test/resources/DocumentShares.json new file mode 100644 index 0000000..ae5102f --- /dev/null +++ b/src/test/resources/DocumentShares.json @@ -0,0 +1,19 @@ +{ + "sharedDocId": 100, + "sharedDocName": "My Document", + "directShares": [ + { + "shareId": 1, + "sharerId": 10, + "sharerName": "jsmith", + "recipientId": 20, + "recipientName": "Smith Group", + "recipientType": "GROUP", + "permission": "READ", + "parentId": 30, + "path": "/Shared/Smith Group", + "grandparentId": null + } + ], + "notebookShares": [] +}