-
Notifications
You must be signed in to change notification settings - Fork 9
Topcat Java Overview
Brian Ritchie, June 2019 (rev. Nov 2019)
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.
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.
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.
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 ofproject.version
in the fileapp.properties
.
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
.
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!
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 asession
request to ICAT and extracts and returns theuserName
string. -
isAdmin()
: gets the userName from ICAT and checks whether it is in Topcat'sadminUserNames
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).
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'sids.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
andgenerateDataSelectionOffsets
. 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.
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.
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!
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.
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.
As the name implies, a collection of utility methods, mainly parsing of Json objects and some string conversions.
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");
}
}
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
- enum:
-
DownloadType
Long id
String facilityName
String downloadType
Boolean disabled
String message
-
EntityType
- enum:
investigation, dataset, datafile
- enum:
-
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
- enum:
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.
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.
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).
Repositories of various kinds of objects from topcat.domain: Cache, Cart, ConfVar, Download and DownloadType.
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
).
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.)
Methods to get and save ConfVars.
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.
Methods to get and retrieve DownloadTypes. (Added in 2.4.4)
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.
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.
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).