Skip to content

Topcat Java Overview

Brian Ritchie edited this page Nov 18, 2019 · 1 revision

Brian Ritchie, June 2019 (rev. Nov 2019)

Introduction

The new version of Topcat will still require a server-side application, which may be similar to the current code. So it would be useful to document it. At present, only the REST API is documented, on a per-method basis; an overview might be useful as well.

I must admit that there are parts of this code that I have rarely, if ever, looked at. I assume most of them are fine as they are.

REST API Overview

Detailed documentation of the REST API methods is included in each release of Topcat. That for 2.4.6 (the current release at time of writing) can be found here.

The API provides several main groups of functions:

  • general status requests: ping, version, configuration variable values
  • user operations:
    • cart retrieval
    • add/delete cart items
    • cart submission
    • downloads retrieval
    • download status changes / deletion
    • query download type status
    • entity size requests
  • administrative management:
    • access control
    • set configuration variables
    • downloads management (across multiple users)
    • enable/disable download types
    • size cache clearance

Most of these operations are used by Topcat's browser client (cache clearance is one exception). There are other tools that use the API, such as pollcat and the topcat_admin python script; and ingestion processes for specific facilities may use it too.

The server side stores details of each user's cart, so that it will persist across user sessions; and manages the status of user download requests (including monitoring their progress through the IDS). Note that deleting a download does not remove it from the database; it is just flagged as Deleted. Most user operations will ignore deleted downloads.

In general, a user may have a separate cart for each facility. The browser side presents this as a single cart, but on the server side they are logically distinct.

The Admin downloads API gives admin users (as defined by the adminUserNames list in topcat.properties) an overview of downloads across all users, and allows them to take charge of any downloads that are stuck in some way.

The ability to disable or re-enable download types was added in release 2.4.4. This allows a download type to be disabled, for example if its storage space is nearing capacity. The disable operation takes a message that will be displayed to users who attempt to use the download type. At present, the Topcat browser admin tab does not support this; but it is available in the topcat_admin python script.

Calculation of entity (investigation/dataset/datafile) sizes can be expensive, so the server side provides an API that stores previously-calculated entity sizes. (As an aside, note that size caching also happens in Topcat's browser side.)

During ingest an entity's size can change, and an early size request may result in an incorrect value being cached. For this reason, the admin API includes an operation to clear the cache entry for a specific entity, which will force recalculation on any subsequent request. This is expected to be used by the ingest process, for example once ingestion of an investigation is complete.

The only use of configuration variables in the browser client at present is to set and check for "front-page" notification messages (variables serviceStatus and maintenanceMode).

The REST API is implemented across multiple classes (all within topcat.web.rest):

  • AdminResource
  • GeneralResource
  • UserResource

It is common for GET and POST operations to be implemented in different classes. As the generated documentation shows, the Java method name for an operation is often not closely related to the operation name!

The generated documentation does not map each REST operation to its implementation, so I will do this here. All URL paths are shown relative to the Topcat base url (e.g. https://topcat.example.com/topcat/):

  • General:
    • GET confVars/{name}/ : GeneralResource.getConfVar()
    • GET ping : GeneralResource.ping()
    • GET version : GeneralResource.getVersion()
  • Admin:
    • DELETE admin/clearSize/(entityType}/{id}/ : AdminResource.clearCachedSize()
    • PUT admin/confVars/{name} : AdminResource.setConfVar()
    • GET admin/downloads/ : AdminResource.getDownloads()
    • GET admin/isValidSession/ : AdminResource.isValidSession()
    • PUT admin/download/{id}/isDeleted/ : AdminResource.deleteDownload()
    • PUT admin/download/id/status/ : AdminResource.setDownloadStatus()
    • PUT admin/downloadType/{type}/status/ : AdminResource.setDownloadTypeStatus()
  • User:
    • GET user/downloads/ : UserResource.getDownloads()
    • GET user/getSize/ : UserResource.getSize()
    • GET user/cart/{facilityName}/ : UserResource.getCart()
    • POST user/cart/{facilityName}/cartItems/ : UserResource.addCartItems()
    • DELETE user/cart/{facilityName}/cartItems/ : UserResource.deleteCartItems()
    • POST user/cart/{facilityName}/submit/ : UserResource.submitCart()
    • PUT user/download/{id}/isDeleted/ : UserResource.deleteDownload()
    • PUT user/download/{id}/status/ : UserResource.setDownloadStatus()
    • GET user/downloadType/{type}/status/ : UserResource.getDownloadTypeStatus()

The implementation details are discussed (in part) below, under the topcat.web.rest package.

A note about passed queries

The admin and user operations to retrieve a list of downloads take an optional queryOffset argument, which can be any JPQL expression (with some syntax extensions) to append to the SELECT clause used internally. In theory, this could open ICAT to SQL injection; but any attacker would have first to obtain or generate a valid sessionId for a user with sufficient privileges in ICAT. The set of queryOffsets used by the client side is very small, and could probably be replaced by boilerplated choices; but the browser client's use of the ICAT entityManager interface (which takes an arbitrary JPQL query) is probably a greater concern.

For completeness, I believe that these are all the ways in which queryOffset is being used in the browser client:

  • all uses go via admin.downloads() or user.downloads(), which modify the queryOffset by prefixing with where download.facilityName = <facilityName> AND <queryOffset>

    • note: queryOffset arg to these can be a LIST of strings (passed to helpers.buildQuery to evaluate)
  • several places "just" use where download.isDeleted = false

  • download cart: where download.id = {someID}

  • index controller's pingSmartClient: where download.isDeleted = false and download.transport = 'smartclient' and download.status != org.icatproject.topcat.domain.DownloadStatus.COMPLETE

  • logout.controller: download.isDeleted = false and download.status = org.icatproject.topcat.domain.DownloadStatus.PREPARING

  • The admin downloads page constructs a queryOffset that applies the column filters and sorts, and limits to the current pageful; that is:

	   1 = 1
	   [and download.<dateField> between {ts <from>} and {ts <to>}]*
	   [and UPPER(download.<stringField>) like concat('%', <filter>.toUpperCase,'%')]*
	   [order by [download.<sortColumn> <sortDirection>]*]
	   limit <(page-1)*pageSize>,<pageSize>
  • The Downloads dialog appears to apply its filtering and sorting to the list of downloads that it receives from "somewhere else" - I have yet to find precisely where this happens, but it's probably one of the other cases identified above.

Class Overview - by package

org.icatproject.topcat

Constants.java

Defines two constants:

  • ICAT_VERSION, currently "4.5", which is probably out of date! I don't know how/where this is used.
  • API_VERSION, which is set in static code from the value of project.version in the file app.properties.

FacilityMap.java

Manages a "facility map" which maintains a mapping from facility names to ICAT/IDS urls, and from facility download type (names) to IDS urls. This is populated from topcat.properties (properties facilities.list, facility.{name}.icatUrl, facility.{name}.idsUrl and facility.{name}.downloadType.{type}). It enables / requires REST API callers to supply a facility name (sometimes with a download type name) rather than a bare ICAT/IDS url (which used to be the case until Topcat 2.4.0).

During startup, this class may throw an InternalException if it detects configuration errors. It will throw an InternalException if requested for details of an unknown facility name. If asked for an unknown download type (for a known facility), it will return the facility's "base" idsUrl.

In the browser-side configuration (topcat.json) it is possible to define only the idsUrl for a facility; in that case, the client will (attempt to) determine the icatUrl by querying the IDS. There is no such support on the server side, and both URLs must be defined in topcat.properties.

GenericExceptionMapper.java

Handles runtime exceptions thrown by Jersey. I have never really looked at this. I see that it uses the ErrorMessage entity from topcat.domain, but I don't really know why!

IcatClient.java

A wrapper for ICAT's own IcatClient that provides Topcat-specific functionality.

(My notes from the early days of working on Topcat say that the main reason why Topcat has its own IcatClient and IdsClient is that the originals did not prevent the construction of overlong URLs generated from long lists of entityIds. The Topcat implementations use chunking based on the physical length of the arguments lists, rather than the number of IDs.)

Main methods:

  • getUserName(): sends a session request to ICAT and extracts and returns the userName string.
  • isAdmin(): gets the userName from ICAT and checks whether it is in Topcat's adminUserNames property.
  • getFullName(): returns the user's fullName, or userName if no fullName is defined.
  • getEntities(): takes an entityType (investigation, dataset or datafile) and a list of entity IDs, constructs a query (or multiple queries, if there are many IDs), submits this to ICAT's entityManager, and returns a list of JSON objects of the results. If only all Topcat entity queries could use this! Unfortunately, it does not handle sub-entity inclusions (which may be required by Topcat's browse views).

IdsClient.java

A wrapper for ICAT's own IDS client that provides Topcat-specific functionality. Manages a cache of getSize requests. This is done because IDS size calculations for large datasets or investigations can be expensive (though are perhaps less so now than when caching was added). Configuration to control the cache behaviour for investigations was added recently: it is possible to set a lifetime on cached investigation sizes, or to disable the caching of zeroes completely.

Main methods:

  • constructor: note the use of a private parseTimeout method here. Topcat's ids.timeout property was recently extended to allow values such as "600s" (600 seconds) and "5m" (5 minutes) in addition to the default interpretation as milliseconds.
  • prepareData(): takes lists of investigation/dataset/datafile IDs, constructs and submits a prepareData request to the IDS, and returns the response (which normally includes a preparedId).
  • isPrepared(): takes a preparedId, queries the IDS, and returns a Boolean based on the response.
  • isTwoLevel(): a fairly direct query to the IDS.
  • getSize(sessionId,investigationIds,datasetIds,datafileIds): queries the IDS for the (summed) size of the supplied IDs. When the ID lists are too long for a single URL, this send multiple "chunked" queries and sums the results. (The chunk constructions are particularly convoluted and implemented in two private methods, chunkOffsets and generateDataSelectionOffsets. Ideally, no-one should ever have to look at this code again!)
  • getSize(cacheRepository,sessionId,entityType,entityId): determines the size of a single entity. If a (suitably recent) cached value exists, that is returned, otherwise the other getSize() method is used to query the IDS. The returned value may be cached (note that the other getSize method does not cache its value, as it is not calculated for a single entity in general). This version of getSize() is used by the UserResource class to implement the REST getSize request.

IdsUploadProxy.java

This implements Topcat's upload mechanism, as a webservice on the endpoint /topcat/ws/user/upload. I have not studied this code in detail. It sends a request to the IDS's /ids/put API.

Properties.java

A Properties manager. Constructor loads properties from topcat.properties. I note that if any exception is thrown when loading the properties file, the code logs it but attempts to continue. A lack of any defined properties will almost certainly cause further problems fairly soon!

StatusCheck.java

A Singleton whose main purpose is to define a timed method to check and update the status of download requests.

I have spent a lot of time looking at this class, mainly because Glassfish tends to expunge the timer function if it fails (throws an error) several times in a row. When this happens, Topcat is no longer informed of changes in status of downloads; and at present the only solution is to redeploy Topcat so that the timed method is restarted. (We are about to try reconfiguring glassfish so that it restarts any expunged timers.) It is probably worth describing this code in some detail.

The main method is poll(). It is currently scheduled to run once per second, but uses a semaphore (AtomicBoolean) to prevent new instances starting if a previous instance is still running. (Or rather, it tries to do this: Frazer Barnsley has pointed out that Glassfish's thread management probably has collision avoidance built-in; this means that the AtomicBoolean is redundant - but any collision attempt may count as a failure that makes the timer more likely to be expunged.)

poll() runs a query (on the EntityManager) to obtain the current set of downloads:

    select download from Download download 
    where download.isDeleted != true 
      and download.status != org.icatproject.topcat.domain.DownloadStatus.EXPIRED 
      and (download.status = org.icatproject.topcat.domain.DownloadStatus.PREPARING 
           or (download.status = org.icatproject.topcat.domain.DownloadStatus.RESTORING 
               and download.transport in ('https','http')) 
           or (download.email != null and download.isEmailSent = false))

Notes:

  • excludes deleted downloads
  • excludes EXPIRED downloads
  • includes PREPARING downloads (new requests)
  • includes RESTORING downloads with download type http or https (but not RESTORING downloads of other transport types)
  • includes downloads for which an email is defined but has not been sent (note: COMPLETED downloads with no email are excluded)

For each download retrieved: if the status is PREPARING, prepareDownload() is called.

Thus, the PREPARING status really means "ready to start preparing" - and need not mean that preparing has actually started. In Topcat, when downloads appear to be stuck at PREPARING, this is often an indication that the StatusCheck timer thread has been expunged.

prepareDownload() sends a prepareData request to the (Topcat) IdsClient, and sets the download's preparedId to the response value. It calculates the download size (using IdsClient.getSize()) and sets the download status either to RESTORING (for a two-level IDS, or for a download type other than http/https) or to COMPLETE. (So single-level http/https download requests are always marked as COMPLETE at this point). There is a lot of error-handling (and lots of logging); most notably, any TopcatException results in the download status being set to EXPIRED.

This is another case to watch out for: I believe that downloads can be flagged as expired if contact with the IDS is lost temporarily; not just by a failure in prepareData, but also in performCheck - see below.

For downloads with other status values than PREPARING, if the download was created more than poll.delay seconds ago, then if it has never been checked, or was last checked more than poll.interval.wait seconds ago, performCheck() is called. (That is, poll.delay defines the minimum delay between a download being submitted and being checked; and poll,interval.wait defines the minimum gap between subsequent checks.)

performCheck() sends a "download is ready" email if the download is COMPLETE and requires an email but none has been sent. Otherwise, if the download type is http or https and if the IdsClient reports that the download is prepared, then the download status is set to COMPLETE and email is sent if required. In all other cases, the lastCheck time for the download is set. As with prepareDownload() there is a lot of error-handling and logging, and again any TopcatException results in the download being set to EXPIRED.

The method to send emails, sendDownloadReadyEmail, allows the strings defined by mail.subject and mail.body (in topcat.properties) to contain a number of parameters (email, userName, facilityName, ...) that are substituted here. An example mail.body from topcat.properties.example shows the expected format:

# The email body message for https downloads. All subject tokens as above are available.
mail.body.globus=Hi ${userName}, \n\nYour ${size} Globus download ${fileName} is ready. Please see https:/example.com/#/globus-faq for more information on how to download using Globus.\n\nThank you for using TopCAT

NOTE In November 2018, a change was introduced to Topcat that allows the mail configuration in topcat-setup.properties to be omitted; this simplified the configuration of simple installations. Unfortunately, it has since come to light that as StatusCheck has a static dependency on a mail Session within the container, if email configuration is omitted the creation of the singleton StatusCheck instance will fail, and so there will be no scheduled checking of download statuses. This turns out to be unimportant for single-tier http/https downloads (which is how most simple systems are set up); but it is essential that the mail properties are defined in topcat-setup.properties when Topcat is configured to use two-tier transport types. This is the case even if email is not required; dummy values (as in topcat-setup.properties.example) can be used.

Example use-cases

Here are some use-cases that demonstrate typical examples of how StatusCheck works (or is intended to work).

First, consider a download (using any transport type) from a single-tier IDS. When submitCart() is called, it creates a new Download from the cart, and (because the IDS is single-tier) it sets the download's status to COMPLETE, and its isEmailSent flag to false. In this case, the download is only considered by StatusCheck if the download's email is non-null; then it will change the flag to true and if email is enabled send the email.

Second, consider an http/https download from a two-tier IDS. Here, submitCart() will set the Download's initial status to PREPARING. StatusCheck will ignore the download until more than poll.delay seconds have passed since the download's creation. On the first run after this, it will run prepareDownload() on it. This will call prepareData() on the IDS, and (if all goes well) receive a preparedId, which is added to the download, whose status is now set to RESTORING. On subsequent runs, StatusCheck will (in effect) wait for poll.interval.wait seconds, then run performCheck() on the download. As this is an http/https download, this will call isPrepared() on the IDS; if the IDS returns true, then the download's status will be set to COMPLETE and (if required) an email will be sent.

Third, consider a download of a non-http/https transport type from a two-tier IDS. The behaviour here is similar to the http/https two-tier case, with the crucial difference that as the transport type is not http/https, performCheck() will never send an isPrepared() call to the IDS. Instead, we rely on some separate process to determine the download's preparation status and to update the download to COMPLETE. (In STFC, the "separate process" is pollcat.) Once the download is COMPLETE, StatusCheck will only consider it if email needs to be sent.

Utils.java

As the name implies, a collection of utility methods, mainly parsing of Json objects and some string conversions.

InstallTrustManager.java

This class is not part of the Topcat distribution (or is not meant to be - I have added it by accident on occasion!) It installs a TrustManager that does not validate certificate chains, which can be useful when using a test system that does not have access to correct CA keys. I use it in my local Vagrant build, but it should not creep into the Topcat distribution, and should never be used in production systems! When I do a release to icatproject.org, I try to make sure that this class is hidden - usually by changing the suffix to .java-hidden. Nonetheless, I have found it very useful, so will include the (fairly short) source here verbatim:

package org.icatproject.topcat;

import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.net.ssl.HttpsURLConnection;
import java.security.cert.X509Certificate;
import java.security.SecureRandom;
import javax.net.ssl.SSLContext;

public class InstallTrustManager {
   public static void installTrustManager() {
        // Create a trust manager that does not validate certificate chains
        // Equivalent to --no-certificate-check in wget
        // Only needed if system does not have access to correct CA keys
        TrustManager[] trustAllCerts = new TrustManager[]{
            new X509TrustManager() {
                public X509Certificate[] getAcceptedIssuers() {
                    return null;
                }
                public void checkClientTrusted(X509Certificate[] certs, String authType) {
                }
                public void checkServerTrusted(X509Certificate[] certs, String authType) {
                }
            }
        };
 
        // Install the all-trusting trust manager
        try {
            SSLContext sc = SSLContext.getInstance("SSL");
            sc.init(null, trustAllCerts, new SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
        } catch (Exception e) {
            System.err.println(e.getClass().getSimpleName() + " setting trust manager: " + e.getMessage());
        }
        // log message
        System.out.println("Trust manager set up successfully");
 
    }
}

topcat.domain

Java persistence entity classes: Cache, Cart, ConfVar, Download, etc. I am not sure these require much in the way of documentation: for the most part they are "just" definitions of columns and relationships, with getters, setters and a toString method. I will list the columns for each entity.

  • Cache
    • String key
    • byte[] serializedValue
    • Date lastAccessTime
    • Date creationTime

Cache objects record their creation time on initialisation. This can be used later on retrieval to decide whether the value is too old.

  • Cart

    • Long id
    • String facilityName
    • String userName
    • List<CartItem> cartItems (@OneToMany)
    • Date createdAt
    • Date updatedAt
  • CartItem

    • Long id
    • EntityType entityType
    • Long entityId
    • String name
    • List<ParentEntity> parentEntities (@OneToMany)
    • Cart cart (@ManyToOne)

See UserResource's addCartItems for an explanation of the parentEntities.

  • ConfVar

    • String name
    • String value
  • Download

    • Long id
    • String facilityName
    • String userName
    • String fullName
    • String transport
    • String fileName
    • String preparedId
    • String sessionId
    • String email
    • Boolean isEmailSent
    • DownloadStatus status
    • long size
    • Boolean isTwoLevel
    • List<DownloadItem> downloadItems (@OneToMany)
    • Date createdAt
    • Boolean isDeleted
    • Date deletedAt
    • Date completedAt

The Download class is slightly more interesting in that it defines a number of named queries that are used elsewhere:

@NamedQueries({
        @NamedQuery(name = "Download.findAll", query = "SELECT d FROM Download d where d.isDeleted = false"),
        @NamedQuery(name = "Download.findById", query = "SELECT d FROM Download d WHERE d.id = :id AND d.isDeleted = false "),
        @NamedQuery(name = "Download.findByPreparedId", query = "SELECT d FROM Download d WHERE d.preparedId = :preparedId AND d.isDeleted = false"),
        @NamedQuery(name = "Download.deleteById", query = "DELETE FROM Download d WHERE d.id = :id AND d.isDeleted = false")
})

It also defines three methods - getInvestigationIds, getDatasetIds and getDatafileIds - that are not (quite) simple getters in that they filter by entity type.

DownloadItem has similar named queries.

  • DownloadItem

    • Long id
    • EntityType entityType
    • Long entityId
    • Download download (@ManyToOne)
  • DownloadStatus

    • enum: RESTORING, COMPLETE, EXPIRED, PAUSED, PREPARING
  • DownloadType

    • Long id
    • String facilityName
    • String downloadType
    • Boolean disabled
    • String message
  • EntityType

    • enum: investigation, dataset, datafile
  • ErrorMessage (not an entity; @XmlRootElement)

    • int status
    • String code
    • String message
    • String developerMessage
  • ParentEntity

    • Long id
    • EntityType entityType
    • Long entityId
    • CartItem cartItem (@ManyToOne)
  • Status

    • enum: ONLINE, ARCHIVE, RESTORING

topcat.exceptions

This package defines the base TopcatException class and a number of extensions:

  • AuthenticationException
  • BadRequestException
  • ForbiddenException
  • IcatException
  • InternalException
  • NotFoundException

TopcatExceptionMapper extends javax.ws.rs.ext.ExceptionMapper (whatever that does). This uses the ErrorMessage entity from topcat.domain.

topcat.filter

This is under-the-bonnet stuff that I have largely ignored. I observe that the comment in ContentTypeFilter says it is a "nasty hack" to work around a bug in Jersey 2.0 which is "supposedly fixed in 2.2" and it should be removed when not needed! I have no idea whether or not it is still needed.

topcat.httpclient

HttpClient is a wrapper for a client to other components (e.g. ICAT/IDS clients). It defines various get, post, put and head methods in terms of a general (private) send method. I've never really looked at it.

Response is a simple container for components of an HTTP request response (code, headers and body).

topcat.repository

Repositories of various kinds of objects from topcat.domain: Cache, Cart, ConfVar, Download and DownloadType.

CacheRepository.java

Stores and retrieves (Serializable) objects against keys. I don't think this needs much detailed documentation. One version of the get() method takes a seconds argument that specifies a maximum age: if the stored value is older than this, it returns null (same as a request for an unknown key). The caller must then recalculate the value (and possibly store it in the cache).

A timed method, prune(), set to run every hour, removes the least-recently-accessed items once the cache exceeds a configurable size limit (maxCacheSize in topcat.properties).

CartRepository.java

Defines a getCartVar(userName,facilityName) method that queries the EntityManager. (There is no corresponding method to save a Cart; see UserResource for details of cart creation / updates.)

ConfVarRepository.java

Methods to get and save ConfVars.

DownloadRepository.java

Methods to get and save Downloads.

The getDownloads() method takes a params map, which is assumed to contain userName and queryOffset strings. The queryOffset is passed through some fiddly regexp processing to strip off any leading "WHERE" and (I think!) to process any LIMIT clauses. The match results are used in the final query construction, which is then sent to the EntityManager to return a list of Downloads.

DownloadTypeRepository.java

Methods to get and retrieve DownloadTypes. (Added in 2.4.4)

topcat.web.rest

Classes that implement the REST API: AdminResource, GeneralResource and UserResource.
Covered under the description of the REST API above. The implementations are largely straightforward; I will add notes on the "more interesting" parts.

AdminResource.java

Most methods are guarded by a call to a private onlyAllowAdmin method, which throws a ForbiddenException if the icatUrl or sessionId are not defined, or if the IcatClient (Topcat's) does not report that the (user associated with the) sessionId has admin access.

The private method getIcatUrl() takes a facility name and uses the FacilityMap to retrieve the corresponding icatUrl. The main reason for defining this method locally is to generate class-specific logging when the facility name is null (and then return null). This may not be necessary: I added this at a time when it appeared that there may be some valid cases where the facility name (or the original icatUrl) may be null; but I now suspect that there are no such cases. There is similar code in UserResource except that the null facility case always throws BadRequestException.

UserResource.java

setDownloadStatus() will throw a ForbiddenException if the caller requests to set a download's status to Deleted, but the current userName does not match the name stored with the download.

I haven't really looked at the cart methods until now - because I've never needed to! They are more complex than I realised.

addCartItems() is quite involved, because it needs to merge the supplied list of items (a single form parameter possibly containing comma-separated pairs of entity types and IDs) with any previous items in the cart. To do this, the code first builds separate lists of investigation/dataset/datafile IDs from the current cart; then it compares each pair in the items string against these to determine whether or not it needs to be added. The new entities are added to the cart (using a separate addEntitiesToCart() method, itself quite involved), and the changes are committed to the EntityManager. We are not done yet though, for next the code looks at each cart item in turn and removes it if its parent entity is also in the cart. (This is why each CartItem records its parents.)

addEntitiesToCart() creates a new CartItem for each entity; but it also has to create and add ParentEntities to the CartItem (an Investigation for datasets, and an Investigation and a Dataset for datafiles).

deleteCartItems() allows an items value of "*", in which case it removes everything. Otherwise, the items string can contain comma-separated values, each of which is either a type/ID pair (which is processed similarly to addCartItems()) or a single ID (which is interpreted as the internal ID of a CartItem).

The "*" form of the items parameter is not documented in the REST API. I don't know if it is actually used anywhere.

submitCart() handles a request to download the contents of the current cart. It creates a new Download, and adds new DownloadItems for each cart item. If the IdsClient for the specified download type is two-level, the download status is set to PREPARING (which will trigger StatusCheck.poll() into action); otherwise idsClient.prepareData() is called directly, and the download status is set to COMPLETE. The cart is then removed from the EntityManager, and a (JSON object representing) an empty cart (but with the downloadId set) is returned.

The private methods getIcatUrl(), getIdsUrl() and getDownloadUrl() are similar to AdminResource.getIcatUrl(), though the code to test for a null facilityName is a separate method, and treats a null value as an error (throwing a BadRequestException).