Skip to content

maintenance portlet | Implement search/replace, drop old versions, and delete pushed assets APIs #35191#35231

Open
hassandotcms wants to merge 7 commits intomainfrom
35199-maintenance-search-replace-old-versions-pushed-assets
Open

maintenance portlet | Implement search/replace, drop old versions, and delete pushed assets APIs #35191#35231
hassandotcms wants to merge 7 commits intomainfrom
35199-maintenance-search-replace-old-versions-pushed-assets

Conversation

@hassandotcms
Copy link
Copy Markdown
Member

Summary

Add 3 new REST endpoints to MaintenanceResource, replacing legacy DWR/Struts maintenance tools with modern REST APIs for the Maintenance portlet Tools tab.

New endpoints:

  • POST /api/v1/maintenance/_searchAndReplace — Database-wide find/replace across text content in contentlets, containers, templates, fields, and links. Only affects working/live versions. Flushes all caches after completion.
  • DELETE /api/v1/maintenance/_oldVersions?date=yyyy-MM-dd — Deletes all versions of versionable objects (contentlets, containers, templates, links, workflow history) older than the specified date. Uses ISO date format instead of legacy MM/dd/yyyy.
  • DELETE /api/v1/maintenance/_pushedAssets — Clears all push publishing history records, making all assets appear as "never pushed" to all endpoints.

Implementation details:

  • SearchAndReplaceForm extends Validated with @JsonCreator, validates searchString is non-empty, allows empty replaceString (delete occurrences)
  • Typed @Value.Immutable views (SearchAndReplaceResultView, DropOldVersionsResultView) with full Swagger/OpenAPI annotations
  • SecurityLogger entries on all destructive operations
  • Reuses existing assertBackendUser() pattern (requireAdmin + requiredPortlet MAINTENANCE)
  • ResponseEntityStringView reused for simple _pushedAssets response

Test plan

  • Integration tests pass: ./mvnw verify -pl :dotcms-integration -Dcoreit.test.skip=false -Dit.test=MaintenanceResourceIntegrationTest
  • POST /_searchAndReplace with valid search/replace strings returns {success: true, hasErrors: false}
  • POST /_searchAndReplace with empty searchString returns 400
  • POST /_searchAndReplace with empty replaceString succeeds (deletes occurrences)
  • DELETE /_oldVersions?date=2000-01-01 returns {deletedCount: N, success: true}
  • DELETE /_oldVersions?date=01/01/2000 returns 400 (wrong format)
  • DELETE /_oldVersions without date param returns 400
  • DELETE /_pushedAssets returns "success"
  • Non-admin users get 403 on all endpoints
  • SecurityLogger entries appear for all operations

Closes #35199, Closes #35201, Closes #35204

@claude
Copy link
Copy Markdown
Contributor

claude bot commented Apr 7, 2026

Claude finished @hassandotcms's task in 2m 18s —— View job


PR Review

  • Read changed files
  • Review MaintenanceResource.java
  • Review form/view classes
  • Review integration tests
  • Post findings

Issues

1. DBSearchAndReplace silently continues on invalid input — MaintenanceUtil.java:451-456

The guard clauses log "Returning because..." but never actually return:

if(!UtilMethods.isSet(textToSearchFor)){
    Logger.info(MaintenanceUtil.class, "Returning because text to search for is null or empty");
    // ← no return here
}
if(textToReplaceWith == null){
    Logger.info(MaintenanceUtil.class, "Returning because text to replace is null");
    // ← no return here
}
// ... proceeds to run 50+ UPDATE statements

The form validation (@NotNull, UtilMethods.isSet guard) prevents hitting this via the API, so it won't blow up today. But the utility is broken for any direct caller and the misleading log message is an active trap. This pre-exists this PR but this PR invokes the method — worth fixing the utility or at minimum noting the dependency on form validation for safety. Fix this →


2. success and hasErrors are redundant inverses — AbstractSearchAndReplaceResultView.java

success = !hasErrors always, by construction (MaintenanceResource.java:585-587). The view exposes both, but one is derivable from the other. Either collapse to a single field, or rename so they communicate different things (e.g. completedWithoutErrors + failedTableCount). As-is, a caller seeing {success: false, hasErrors: true} gets no new information from the second field.


3. HTTP 200 with success: false on partial failure — MaintenanceResource.java:578-588

When some DB tables fail the search/replace, the response is 200 OK with {success: false, hasErrors: true}. The Swagger docs acknowledge this, but it means clients have to inspect the body to detect partial failure. Worth considering 207 Multi-Status or 500 here, especially since the operation is irreversible and partial updates leave data in a mixed state. At minimum, the Swagger description for the 200 case should call out the partial-failure semantic more prominently.


4. DropOldContentletRunner.deleteOldContent() unchecked exception bypasses the -1 guard — MaintenanceResource.java:655-659

CMSMaintenanceFactory.deleteOldAssetVersions calls DropOldContentletRunner.deleteOldContent() at the top (before the try/catch loop). If that call throws, the exception propagates straight through dropOldVersions — the deleted < 0 check is never reached. The result is an uncaught server-side exception instead of the documented 500 path. This is a pre-existing gap in the factory, but worth a try/catch or explicit note since the resource claims to handle all failure modes.


5. SearchAndReplaceForm raises different exception types for two "missing field" inputs

  • replaceString = null (missing from JSON) → ValidationException via @NotNull + super.checkValid()
  • searchString = "" (empty) → BadRequestException via the explicit UtilMethods.isSet guard

Both are client errors, but they serialize differently. If the REST exception mapper treats them the same way this is harmless, but it's worth verifying — or aligning both to the same exception type. Fix this →


6. Minor: @NotNull on replaceString annotation conflicts with its stated intent

The @Schema description says "Can be empty to delete all occurrences" — so an empty string is valid. But the @NotNull javax constraint + requiredMode = REQUIRED annotates the field as if presence is mandatory. This is technically correct (clients must send "replaceString": "", not omit the key), but it contradicts the @NotNull message "replaceString is required". Consider using @NotNull(message = "replaceString must not be null; use empty string to delete occurrences") for clarity.


Clean areas

  • Auth guard (assertBackendUser) is applied consistently to all three new endpoints.
  • dropOldVersions date parsing via LocalDate.parse + explicit UTC conversion is correct.
  • SecurityLogger entries are present on all destructive operations.
  • deletePushedAssets correctly surfaces DotRuntimeException on failure.
  • Integration test coverage is solid: null body, empty searchString, wrong date format, garbage date, and non-admin rejection are all covered.

@hassandotcms hassandotcms changed the base branch from 35191-feature-maintenance-tools-rest-apis to main April 7, 2026 13:08
@github-actions github-actions bot added the Area : Backend PR changes Java/Maven backend code label Apr 7, 2026
@hassandotcms hassandotcms force-pushed the 35199-maintenance-search-replace-old-versions-pushed-assets branch from b797ebe to 65d3572 Compare April 7, 2026 14:53
…s, and delete pushed assets #35191

Add three new endpoints to MaintenanceResource:
- POST /_searchAndReplace: database-wide find/replace across content tables
- DELETE /_oldVersions: drop versionable objects older than a given date
- DELETE /_pushedAssets: clear all push publishing history

Includes Validated forms, @Value.Immutable response views, full Swagger
annotations, and SecurityLogger entries for all destructive operations.
…ions, and pushed assets #35191

Tests cover:
- _searchAndReplace: admin success, non-admin rejection, null/empty form validation,
  empty replaceString allowed
- _oldVersions: admin success, non-admin rejection, missing date, invalid date formats
- _pushedAssets: admin success, non-admin rejection
@hassandotcms hassandotcms force-pushed the 35199-maintenance-search-replace-old-versions-pushed-assets branch from 9c70839 to fb599c9 Compare April 16, 2026 15:54

if (deleted < 0) {
throw new DotRuntimeException(
"Failed to delete old asset versions — check server logs for details");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's include the value of dateStr in the message.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Safe To Rollback Area : Backend PR changes Java/Maven backend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[TASK] Implement delete pushed assets endpoint [TASK] Implement drop old versions endpoint [TASK] Implement search and replace endpoint

2 participants