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.
    • 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.