Class AdminWebService

java.lang.Object
ubic.gemma.rest.AdminWebService

@Service @Path("/admin") public class AdminWebService extends Object
Admin-only system monitoring surface for the gemma-curation-ui admin panel. Replaces the legacy gemma-web SystemMonitorController DWR calls (getCacheStatus, clearAllCaches, clearCache, getHibernateStatus) with structured JSON-returning endpoints.

All endpoints require GROUP_ADMIN authority.

Note: the legacy CacheMonitor.enableStatistics / disableStatistics entry points are stubs on the current post-EhCache-2 build (see CacheMonitorImpl) and are not exposed here. The legacy resetHibernateStatus is also not exposed pending a UX decision on whether the admin panel needs a Hibernate-stats reset button at all.

Author:
phase 3 admin-panel wiring
  • Constructor Details

  • Method Details

    • getCaches

      @GET @Path("/caches") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public ResponseDataObject<AdminWebService.CacheListResponse> getCaches()
      Lists the registered Spring caches by name. Replaces the HTML-returning legacy SystemMonitorController.getCacheStatus().
    • clearAllCaches

      @DELETE @Path("/caches") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public jakarta.ws.rs.core.Response clearAllCaches()
      Clears every registered cache. Replaces the legacy SystemMonitorController.clearAllCaches() DWR call.
    • clearCache

      @DELETE @Path("/caches/{cacheName}") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public jakarta.ws.rs.core.Response clearCache(@PathParam("cacheName") String cacheName)
      Clears a single named cache. Replaces the legacy SystemMonitorController.clearCache(name) DWR call.
    • getHibernateStats

      @GET @Path("/hibernate/stats") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public ResponseDataObject<AdminWebService.HibernateStatsResponse> getHibernateStats()
      Returns a structured snapshot of Hibernate statistics. Replaces the HTML-returning legacy SystemMonitorController.getHibernateStatus().
    • resetHibernateStats

      @POST @Path("/hibernate/reset") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public jakarta.ws.rs.core.Response resetHibernateStats()
      Resets the Hibernate statistics counters to zero. Replaces the legacy SystemMonitorController.resetHibernateStatus() DWR call.
    • getJobs

      @GET @Path("/jobs") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public ResponseDataObject<AdminWebService.JobsListResponse> getJobs()
      Aggregated admin view of the in-memory background task queue. Returns the per-task TaskStatusValueObject snapshots plus counts of tasks in each status. The underlying task store is in-memory only and tasks are evicted ~10 minutes after completion.
    • importGeoBatch

      @POST @Path("/tasks/import-geo") @Consumes("application/json") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_CURATOR')") public jakarta.ws.rs.core.Response importGeoBatch(@Nullable AdminWebService.ImportGeoBatchRequest body)
      Batch GEO accession import. Ports the bulk path of ubic.gemma.apps.LoadExpressionDataCli: iterate the accession list and submit one ExpressionExperimentLoadTaskCommand per accession, returning the resulting task-id list so the caller can poll each one through /tasks/{taskId}.

      The optional flags on the request body (loadPlatformOnly, suppressMatching, etc.) are applied to every accession in the batch. For one-off imports use POST /datasets/import.

    • submitMultifunctionalityRecompute

      @POST @Path("/tasks/multifunctionality") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_CURATOR')") public jakarta.ws.rs.core.Response submitMultifunctionalityRecompute(@QueryParam("taxon") TaxonArg<?> taxonArg)
      Async port of MultifunctionalityCli: recompute per-gene multifunctionality scores for a single taxon. Submits a MultifunctionalityTaskCommand; the caller polls /tasks/{taskId} for completion.

      Taxon identifier may be the common name (e.g. human), scientific name, NCBI ID, or Gemma taxon ID — same shape as elsewhere in the REST API (TaxonArg.valueOf(String)).

    • regeneratePlatformReport

      @POST @Path("/platforms/{platform}/report") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_CURATOR')") public ResponseDataObject<ArrayDesignValueObject> regeneratePlatformReport(@PathParam("platform") PlatformArg<?> platformArg)
      Regenerate the cached report for ONE platform, synchronously.

      The report holds the per-platform element / sequence / alignment / gene counts that GET /platforms serves as numberOfGenes and numberOfMappedElements. They are never computed per request — counting distinct genes for one large platform measures ~1.7s against production — so they are read from a file that something has to write. On a production node nothing does: the Quartz trigger that refreshes them monthly (SchedulerConfig.arrayDesignReportTrigger) is gated on the scheduler profile, which production does not run.

      Synchronous because a single platform is a couple of seconds and the caller wants the new numbers back. Use POST /admin/tasks/platform-reports for the whole corpus.

    • submitPlatformReportsRegeneration

      @POST @Path("/tasks/platform-reports") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_CURATOR')") public jakarta.ws.rs.core.Response submitPlatformReportsRegeneration()
      Submit an async regeneration of the cached reports for EVERY platform.

      The bulk counterpart of regeneratePlatformReport(PlatformArg); the corpus-wide run is far too long to hold a request open. Mirrors the other admin task endpoints: returns 202 with the job id, poll /tasks/{taskId}.

    • getSearchIndices

      @GET @Path("/search/indices") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public ResponseDataObject<AdminWebService.SearchIndicesResponse> getSearchIndices()
      Per-@Indexed-entity Hibernate Search 7 index status. Replaces the legacy gemma-web indexer.js flow that pinged the indexer directly to discover what was indexable. The new UI uses this read-only view to surface index sizes / on-disk paths; rebuild actions stay in the CLI (IndexGemmaCLI).
    • reindexSearchIndices

      @POST @Path("/search/indices") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public jakarta.ws.rs.core.Response reindexSearchIndices(@QueryParam("entity") @Nullable String entity)
      Trigger a Hibernate Search 7 mass-reindex for one entity (or all of them).

      Destructive: HS 7's mass-indexer purges the existing on-disk Lucene index for the entity before rebuilding (purgeAllOnStart(true)). Runs asynchronously on a background thread; this endpoint returns 202 Accepted as soon as the work is queued. Use getSearchIndices() to monitor doc-count progress and the reindexStatus field per entity.

      Concurrent reindex requests are rejected with 409 Conflict — the mass-indexer is single-flight per JVM so two parallel calls would purge each other's just-written segments.

      Parameters:
      entity - user-facing entity name (one of genes, datasets, platforms, bibliographicReferences, probes, sequences, datasetGroups, geneSets); or omit to reindex all indexable roots sequentially.
    • getSystem

      @GET @Path("/system") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public ResponseDataObject<AdminWebService.SystemSnapshotResponse> getSystem()
      Process-level memory / GC / thread / load snapshot. Complements the anonymous /info endpoint (build + JVM identity + OS identity) with the live, admin-only resource numbers the legacy systemStats.jsp hand-rolled. Single read; no historical series — that's what /metrics is for.
    • getSessions

      @GET @Path("/sessions") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public ResponseDataObject<AdminWebService.SessionsResponse> getSessions()
      Authenticated session listing. The legacy activeUsers.jsp surfaced a count via SecurityController.getAuthenticatedUserCount and a JSP comment promising a table of users that was never built. This endpoint delivers that table: distinct authenticated principals (across both browser and basic-auth callers), each with the count of currently-tracked sessions, the most recent request time, and any granted GROUP_* authorities.
    • getAllCurationLocks

      @GET @Path("/curation-locks") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public ResponseDataObject<List<AdminWebService.CurationLockValueObject>> getAllCurationLocks()
      Every advisory curation lock currently held, corpus-wide. Exists for an admin release panel: the per-dataset lock surface (see DatasetsWebService's /curation/lock* routes) answers "is this one dataset locked", scoped to what the caller can already read, but there was no way to ask "what is locked right now" without already holding the list of dataset ids to check — which is exactly the thing an admin deciding whether to force-release something does not have.

      Unlike the per-dataset routes, this bypasses per-dataset ACL by construction — it names datasets the caller may not otherwise be able to read — so it is GROUP_ADMIN-gated like the rest of this resource, not isAuthenticated() like the scoped ones.

      CURATION_LOCK holds one row per currently-locked dataset, not one per dataset in the corpus, so the answer is bounded by how much curation/agent activity is in flight right now.

    • getOntologies

      @GET @Path("/ontologies") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public ResponseDataObject<AdminWebService.OntologiesResponse> getOntologies(@QueryParam("includeTermCount") @DefaultValue("false") boolean includeTermCount)
      Per-ontology load status. Enumerates every OntologyService bean (Mondo, PATO, CHEBI, Uberon, CellType, the unified TDB, etc.) and reports each one's enable / load / initialization-thread state plus its inference and search settings. Term counts are skipped by default because getAllURIs() can be expensive on large ontologies; opt in with ?includeTermCount=true.
    • refreshOntology

      @POST @Path("/ontologies/{name}/refresh") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public jakarta.ws.rs.core.Response refreshOntology(@PathParam("name") String name, @QueryParam("forceIndexing") @DefaultValue("false") boolean forceIndexing)
      Refresh a single ontology in-process: re-run initialize(forceLoad=true) on a background thread so the source is re-fetched, the model is rebuilt, and the in-memory state is atomically swapped without a container restart. Returns 202 immediately; the caller polls getOntologies(boolean) to watch the initializing flag flip back to false.

      Matches the ontology through OntologyServiceResolver, which accepts the well-known abbreviation (CLO, HPO, TGEMO, …), the identifier, the implementing class name, or the dc:title, ignoring case and punctuation. Every ontology is therefore refreshable, including the ones whose dc:title is absent or contains spaces. 404 if no bean matches, 409 if a refresh is already in flight on that bean.

      For the slim-CHEBI path the refresh re-runs the loadModel override, which checks the seed-hash sidecar and re-extracts the slim if the corpus has drifted.

    • rebuildOntologySlim

      @POST @Path("/ontologies/{name}/rebuild-slim") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public jakarta.ws.rs.core.Response rebuildOntologySlim(@PathParam("name") String name)
      Rebuild the slim-cache OWL for an ontology that supports it (currently CHEBI only). The service must already be loaded so the extractor can read the on-disk source. Returns 202 immediately and the extraction runs on a daemon thread; poll getOntologies(boolean) to watch the result land (a fresh slim file at ${ontology.cache.dir}/ontology/chebiOntology-slim.owl).

      Memory note: STAR module extraction via OWL-API holds the full CHEBI in heap during the run (~3 GB peak after this commit's source-release fix). Invoke on a host with that headroom, and not during another resource-intensive operation.

    • getObsoleteTerms

      @GET @Path("/ontologies/obsolete-terms") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public ResponseDataObject<List<ObsoleteTermUsage>> getObsoleteTerms(@QueryParam("timeoutSeconds") @DefaultValue("120") Integer timeoutSeconds)
      In-application port of FindObsoleteTermsCli: which obsolete ontology terms do Gemma's annotations still use, and what does each owning ontology say should replace them.

      The CLI existed because the check needed ontologies in memory and a CLI had to load them itself — which is why it refuses to run unless load.ontologies=false and spends its first stretch warming up. A running application already holds them, so the only work left here is one grouped query over CHARACTERISTIC plus a lookup per distinct URI.

      Read-only. Correcting the terms is a separate, deliberate action: see autoCorrectable on each row for whether a correction could be derived from the ontology at all.

    • applyObsoleteTermCorrections

      @POST @Path("/ontologies/obsolete-terms/apply") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_CURATOR')") public jakarta.ws.rs.core.Response applyObsoleteTermCorrections(@QueryParam("dryRun") @DefaultValue("true") Boolean dryRun, @QueryParam("uris") List<String> uris, @QueryParam("timeoutSeconds") @DefaultValue("600") Integer timeoutSeconds)
      Rewrite annotations that use an obsolete ontology term to the successor its ontology asserts.

      Dry run unless dryRun=false is passed explicitly. The default is the safe one because this writes to production annotations, and a dry run returns the counts a live run would produce, so there is no reason to skip the rehearsal.

      Only autoCorrectable terms are touched — those whose replacement was derived from the ontology rather than decided by a person. Terms offering only oboInOwl:consider candidates are never corrected here; see GET /admin/ontologies/obsolete-terms for what they are and why.

    • getDbPool

      @GET @Path("/db/pool") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public jakarta.ws.rs.core.Response getDbPool()
      HikariCP pool snapshot. Reports the live connection census plus the configured upper bound, so the admin panel can show "12 / 50 active" at a glance and surface "threads awaiting" when the pool is saturated.
    • getCurationAgentHealth

      @GET @Path("/curation-agent/health") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_CURATOR')") public ResponseDataObject<AdminWebService.CurationAgentHealthResponse> getCurationAgentHealth()
      Out-of-process liveness probe for the gemma-curation-agents Python service. Configured via gemma.curationAgent.healthUrl (unset = endpoint reports "not configured" with 200, so the admin UI can render a neutral pill instead of an alarming red one).
    • grabGeoRecords

      @POST @Path("/tasks/geo-grab") @Consumes("application/json") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_CURATOR')") public ResponseDataObject<AdminWebService.GeoGrabResponse> grabGeoRecords(AdminWebService.GeoGrabRequest req)
      Scrape GEO record metadata by accession without importing into Gemma. Port of GeoGrabberCli's -e / --acc mode. Synchronous: NCBI E-utilities responses are sub-second per accession in the typical case, so the curation-UI can call this on-demand to preview a GEO record before triggering a full import.

      Returns one AdminWebService.GeoRecordValueObject per requested accession that GEO successfully returns; accessions GEO doesn't know about are silently dropped (matching the CLI's behavior).

    • submitGeoScrape

      @POST @Deprecated @Path("/tasks/geo-scrape") @Consumes("application/json") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_CURATOR')") public jakarta.ws.rs.core.Response submitGeoScrape(@Nullable AdminWebService.GeoScrapeRequest body)
      Deprecated.
      The curation agent scrapes GEO itself (scrape_geo_and_open_triage.py) and opens its own triage ticket. Still functional; see GeoScrapeService for what an agent-side replacement has to reproduce -- the preboarded rows, the watermark, and ONE batch ticket.
    • getLastGeoScrape

      @GET @Deprecated @Path("/geo-scrape/last") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public ResponseDataObject<AdminWebService.GeoScrapeWatermarkValueObject> getLastGeoScrape()
      Deprecated.
      Reads the watermark written by the deprecated in-Gemma scrape. An agent that scrapes on its own side is the author of its own run records; this only ever sees runs Gemma performed.
    • getCurationStatus

      @GET @Path("/curation-status") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_CURATOR')") public ResponseDataObject<AdminWebService.CurationStatusResponse> getCurationStatus()
      Snapshot of the annotation-set -> ticket lifecycle: per-role AnnotationSet counts in the recent windows, open-ticket counts by TicketType, distinct agent run id count, and latest-createdAt timestamp.

      Backs the curation-UI "what's the Python agent doing right now" indicator. Counts are computed with bounded aggregates against the ANNOTATION_SET and TICKET tables — no per-row fetch.

    • getUsers

      @GET @Path("/users") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public ResponseDataObject<AdminWebService.UsersListResponse> getUsers(@QueryParam("includeDeleted") @DefaultValue("false") boolean includeDeleted)
      Admin user listing. Soft-deleted users (DELETED_AT IS NOT NULL) are hidden by default; pass ?includeDeleted=true to surface them.
    • createUser

      @POST @Path("/users") @Consumes("application/json") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public jakarta.ws.rs.core.Response createUser(AdminWebService.CreateUserRequest req)
      Create a new active user with a server-generated one-time temporary password. The plaintext password is returned in the response body — pass it to the new user out-of-band. It is not stored anywhere recoverable; if lost, an admin must reset it.
    • patchUser

      @PATCH @Path("/users/{username}") @Consumes("application/json") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public ResponseDataObject<AdminWebService.UserValueObject> patchUser(@PathParam("username") String username, AdminWebService.UpdateUserRequest req)
      Partial update — toggle the enabled flag (lock/unlock) and/or admin role. Other User fields (email, password, name) are not touched by this endpoint; those go through the user-profile flow.
    • resetUserPassword

      @POST @Path("/users/{username}/password") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public ResponseDataObject<AdminWebService.ResetPasswordResponse> resetUserPassword(@PathParam("username") String username)
      Administrative password reset — set a user's password to a fresh server-generated one-time temporary password. Does not require the user's current password (this is the recovery path for a locked-out or forgetful user). The plaintext temp password is returned once; pass it to the user out-of-band. The user should then change it via the self-service PUT /users/me/password flow. Leaves the account enabled; distinct from the email-confirmation reset flow.
    • deleteUser

      @DELETE @Path("/users/{username}") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public jakarta.ws.rs.core.Response deleteUser(@PathParam("username") String username)
      Soft delete — marks the account as deleted, disables it, and preserves the row so dependent references (ACL sids, audit-event authorship FKs) don't dangle. Hard delete is intentionally not exposed via REST.
    • addBlacklistEntry

      @POST @Path("/blacklist") @Consumes("application/json") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_CURATOR')") public jakarta.ws.rs.core.Response addBlacklistEntry(@Nullable AdminWebService.BlacklistRequest body)
      Adds a single blacklist entry. Port of the -accession/-reason arm of BlacklistCli.doAuthenticatedWork(): validates the accession, looks up the GEO ExternalDatabase, and creates either a BlacklistedPlatform (GPL*) or BlacklistedExperiment (GSE*) row with the supplied reason.
    • deleteBlacklistEntry

      @DELETE @Path("/blacklist/{accession}") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_CURATOR')") public jakarta.ws.rs.core.Response deleteBlacklistEntry(@PathParam("accession") String accession)
      Removes a blacklist entry by its accession. Port of the -accession -undo arm of BlacklistCli. Returns 204 on success, 404 when the accession is not on the blacklist.
    • listBlacklistEntries

      @GET @Path("/blacklist") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_CURATOR')") public ResponseDataObject<AdminWebService.BlacklistListResponse> listBlacklistEntries(@QueryParam("limit") @DefaultValue("100") int limit, @QueryParam("offset") @DefaultValue("0") int offset)
      Lists current blacklist entries. Convenience sibling for the curation-UI; the CLI has no equivalent. The underlying service exposes only loadAll(), so pagination is applied in-process: results are sorted by accession (alphabetic, nulls last) and sliced by offset/limit.