Skip to content

Commit 2d3370c

Browse files
authored
Refactor Java remote API Command hierarchy (#48)
1 parent 5e92170 commit 2d3370c

File tree

98 files changed

+570
-611
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

98 files changed

+570
-611
lines changed

CHANGELOG.md

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,42 @@
11
# The LabKey Remote API Library for Java - Change Log
22

3-
## version 4.4.0-SNAPSHOT
3+
## version 5.1.0-SNAPSHOT
44
*Released*: TBD
55

6+
## version 5.0.0
7+
*Released*: 24 January 2023
8+
* Refactor the `Command` class hierarchy:
9+
* Add `GetCommand`, new abstract base class for all commands that use get
10+
* Make `Command` and `PostCommand` abstract
11+
* Add `SimpleGetCommand`, new concrete class used (instead of `Command`) to invoke GET API actions without creating a subclass
12+
* Add `SimplePostCommand`, new concrete class used (instead of `PostCommand`) to invoke POST API actions without creating a subclass
13+
* Refactor parameter handling for consistency:
14+
* `createParameterMap()` is now used by subclasses to create and populate a mutable map of URL parameters
15+
* `getParameters()` now returns an immutable copy of the current URL parameter map for external use, typically logging or testing
16+
* Commands no longer stash and reuse the parameter map; `createParameterMap()` always creates a new map.
17+
* `setParameters()` is now available only on `SimpleGetCommand` and `SimplePostCommand`. Custom `GetCommand` and `PostCommand`
18+
subclasses that need to specify parameters are expected to override `createParameterMap()`.
19+
* `setJsonObject()` is now available only on `SimplePostCommand`. Custom `PostCommand` subclasses that need to post JSON are
20+
expected to override `getJsonObject()`.
21+
* Stop passing command subclasses when constructing every `CommandResponse`. The two response classes that need this now implement
22+
it without burdening all other commands.
23+
* Introduce `HasRequiredVersion` interface and use it when instantiating `CommandResponse` subclasses that need required version
24+
* Remove all `Command` copy constructors. Same rationale as the earlier removal of `copy` methods.
25+
* Switch `SelectRowsCommand` and `NAbRunsCommand` to post their parameters as JSON
26+
* Fix NAbReplicate to handle `"NaN"` values
27+
* Remove `CommandException` from `getHttpRequest()` throws list
28+
* Adjust the `Demo.java` and `Test.java` tests to match current sample data and `Command` hierarchy changes
29+
630
## version 4.3.1
731
*Released*: 15 January 2023
832
* Fix regression introduced in 4.3.0: `SelectRowsCommand` should create a fresh parameter map for every invocation of `getParameters()`
933

1034
## version 4.3.0
1135
*Released*: 11 January 2023
12-
* [Issue 47030](https://www.labkey.org/home/Developer/issues/issues-details.view?issueId=47030): Switch `SelectRowsCommand` and `NAbRunsCommand` to always use POST
13-
* Add support for `includeTotalCount`, `includeMetadata`, and `ignoreFilter` flags to `BaseQueryCommand` and reconcile duplicate parameter handling code vs. SelectRowsCommand
36+
* [Issue 47030](https://www.labkey.org/home/Developer/issues/issues-details.view?issueId=47030): Switch `SelectRowsCommand`
37+
and `NAbRunsCommand` to always use POST
38+
* Add support for `includeTotalCount`, `includeMetadata`, and `ignoreFilter` flags to `BaseQueryCommand` and reconcile
39+
duplicate parameter handling code vs. SelectRowsCommand
1440
* Add support for `includeTitle` and `includeViewDataUrl` flags to `GetQueriesCommand`
1541

1642
## version 4.2.0

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ repositories {
4848

4949
group "org.labkey.api"
5050

51-
version "4.4.0-SNAPSHOT"
51+
version "5.1.0-SNAPSHOT"
5252

5353
dependencies {
5454
api "org.json:json:${jsonObjectVersion}"

src/org/labkey/remoteapi/BasicAuthCredentialsProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public BasicAuthCredentialsProvider(String email, String password)
3939
public void configureRequest(URI baseURI, HttpUriRequest request, HttpClientContext httpClientContext)
4040
{
4141
BasicCredentialsProvider provider = new BasicCredentialsProvider();
42-
AuthScope scope = new AuthScope(baseURI.getHost(), -1);
42+
AuthScope scope = new AuthScope(baseURI.getHost(), baseURI.getPort());
4343
Credentials credentials = new UsernamePasswordCredentials(_email, _password.toCharArray());
4444
provider.setCredentials(scope, credentials);
4545
httpClientContext.setCredentialsProvider(provider);

src/org/labkey/remoteapi/Command.java

Lines changed: 32 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
import org.apache.commons.logging.LogFactory;
1919
import org.apache.hc.client5.http.auth.AuthenticationException;
20-
import org.apache.hc.client5.http.classic.methods.HttpGet;
2120
import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
2221
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
2322
import org.apache.hc.core5.http.Header;
@@ -37,30 +36,26 @@
3736
import java.net.URISyntaxException;
3837
import java.nio.charset.StandardCharsets;
3938
import java.util.Collection;
39+
import java.util.Collections;
4040
import java.util.HashMap;
4141
import java.util.Map;
4242
import java.util.Scanner;
4343

4444
/**
45-
* Base class for all API commands. Typically, a developer will not
46-
* use this class directly, but will instead create one of the
47-
* classes that extend this class. For example, to select data,
48-
* create an instance of {@link org.labkey.remoteapi.query.SelectRowsCommand},
49-
* which provides helpful methods for setting options and obtaining
50-
* specific result types.
45+
* Abstract base class for all API commands. Developers interact with concrete classes that
46+
* extend this class. For example, to select data, create an instance of
47+
* {@link org.labkey.remoteapi.query.SelectRowsCommand}, which provides helpful methods for
48+
* setting options and obtaining specific result types.
5149
* <p>
52-
* However, if future versions of the LabKey Server expose new HTTP APIs
53-
* that are not yet supported with a specialized class in this library,
54-
* the developer may still invoke these APIs by creating an instance of the
55-
* {@link Command} class directly, providing the controller and action name for
56-
* the new API. Parameters may then be specified by calling the {@link #setParameters(Map)}
57-
* method, passing a populated parameter <code>Map&lt;String, Object&gt;</code>
50+
* If a developer wishes to invoke actions that are not yet supported with a specialized class
51+
* in this library, the developer may invoke these APIs by creating an instance of the
52+
* {@link SimpleGetCommand} or {@link SimplePostCommand} classes, providing the controller and
53+
* action name for the API, and setting parameters or JSON to send.
5854
* <p>
59-
* Note that this class is not thread-safe. Do not share instances of this class
60-
* or its descendants between threads, unless the descendant declares explicitly that
61-
* it is thread-safe.
55+
* Note that this class is not thread-safe. Do not share instances of this class or its
56+
* descendants between threads, unless the descendant declares explicitly that it is thread-safe.
6257
*/
63-
public class Command<ResponseType extends CommandResponse>
58+
public abstract class Command<ResponseType extends CommandResponse, RequestType extends HttpUriRequest> implements HasRequiredVersion
6459
{
6560
/**
6661
* A constant for the official JSON content type ("application/json")
@@ -77,7 +72,6 @@ public enum CommonParameters
7772

7873
private final String _controllerName;
7974
private final String _actionName;
80-
private Map<String, Object> _parameters = null;
8175
private Integer _timeout = null;
8276
private double _requiredVersion = 8.3;
8377

@@ -96,20 +90,6 @@ public Command(String controllerName, String actionName)
9690
_actionName = actionName;
9791
}
9892

99-
/**
100-
* Constructs a new Command from an existing command
101-
* @param source A source Command
102-
*/
103-
public Command(Command<ResponseType> source)
104-
{
105-
_actionName = source.getActionName();
106-
_controllerName = source.getControllerName();
107-
if (null != source.getParameters())
108-
_parameters = new HashMap<>(source.getParameters());
109-
_timeout = source._timeout;
110-
_requiredVersion = source.getRequiredVersion();
111-
}
112-
11393
/**
11494
* Returns the controller name specified when constructing this object.
11595
* @return The controller name.
@@ -129,26 +109,23 @@ public String getActionName()
129109
}
130110

131111
/**
132-
* Returns the current parameter map, or null if a map has not yet been set.
133-
* Derived classes will typically override this method to provide a parameter
134-
* map based on data members set by specialized setter methods.
135-
* @return The current parameter map.
112+
* Makes an immutable copy of the parameter map used for building the URL available to callers. Typically used for
113+
* logging and testing.
114+
* @return An immutable map of parameters used when building the URL.
136115
*/
137-
public Map<String, Object> getParameters()
116+
public final Map<String, Object> getParameters()
138117
{
139-
if (null == _parameters)
140-
_parameters = new HashMap<>();
141-
142-
return _parameters;
118+
return Collections.unmodifiableMap(createParameterMap());
143119
}
144120

145121
/**
146-
* Sets the current parameter map.
147-
* @param parameters The parameter map to use
122+
* Returns a new, mutable parameter map. Derived classes will typically override this
123+
* method to put values passed to specialized setter methods into the map.
124+
* @return The parameter map to use when building the URL.
148125
*/
149-
public void setParameters(Map<String, Object> parameters)
126+
protected Map<String, Object> createParameterMap()
150127
{
151-
_parameters = parameters;
128+
return new HashMap<>();
152129
}
153130

154131
/**
@@ -408,7 +385,6 @@ private void throwError(Response r, boolean throwByDefault) throws IOException,
408385
r._responseText = responseText;
409386
}
410387

411-
412388
/**
413389
* Creates an instance of the response class, initialized with
414390
* the response text, the HTTP status code, and parsed JSONObject.
@@ -423,7 +399,7 @@ private void throwError(Response r, boolean throwByDefault) throws IOException,
423399
*/
424400
protected ResponseType createResponse(String text, int status, String contentType, JSONObject json)
425401
{
426-
return (ResponseType)new CommandResponse(text, status, contentType, json, this);
402+
return (ResponseType)new CommandResponse(text, status, contentType, json);
427403
}
428404

429405
/**
@@ -438,12 +414,10 @@ protected ResponseType createResponse(String text, int status, String contentTyp
438414
* This and the base URL from the connection will be used to construct the
439415
* first part of the URL.
440416
* @return The constructed HttpUriRequest instance.
441-
* @throws CommandException Thrown if there is a problem encoding or parsing the URL
442417
* @throws URISyntaxException Thrown if there is a problem parsing the base URL in the connection.
443418
*/
444419

445-
// TODO: For next major release, remove CommandException from throws list
446-
protected HttpUriRequest getHttpRequest(Connection connection, String folderPath) throws CommandException, URISyntaxException
420+
protected RequestType getHttpRequest(Connection connection, String folderPath) throws URISyntaxException
447421
{
448422
//construct a URI from connection base URI, folder path, and current parameters
449423
URI uri = createURI(connection, folderPath);
@@ -452,15 +426,11 @@ protected HttpUriRequest getHttpRequest(Connection connection, String folderPath
452426
}
453427

454428
/**
455-
* Creates the appropriate HttpUriRequest instance. Override to create something
456-
* other than <code>HttpGet</code>.
429+
* Creates the appropriate HttpUriRequest instance. Override to create <code>HttpGet</code>, <code>HttpPost</code>, etc.
457430
* @param uri the uri to convert
458431
* @return The HttpUriRequest instance.
459432
*/
460-
protected HttpUriRequest createRequest(URI uri)
461-
{
462-
return new HttpGet(uri);
463-
}
433+
protected abstract RequestType createRequest(URI uri);
464434

465435
/**
466436
* Returns a full URI for this Command, including base URI, folder path, and query string.
@@ -492,26 +462,21 @@ private URI createURI(Connection connection, String folderPath) throws URISyntax
492462

493463
//add the controller name
494464
String controller = getControllerName();
495-
//for now, strip leading /. TODO: For next major release, throw
496-
if (controller.charAt(0) == '/')
497-
controller = controller.substring(1);
498-
//for now, strip trailing /. TODO: For next major release, throw
499-
if (controller.charAt(controller.length() - 1) == '/')
500-
controller = controller.substring(0, controller.length() - 1);
465+
if (controller.contains("/"))
466+
throw new IllegalArgumentException("Controller name should not contain a slash (/)");
501467
path.append(controller);
502468

503469
//add the action name + ".api"
504470
String actionName = getActionName();
505-
//for now, strip leading /. TODO: For next major release, throw
506-
if (actionName.charAt(0) == '/')
507-
actionName = actionName.substring(1);
471+
if (actionName.contains("/"))
472+
throw new IllegalArgumentException("Action name should not contain a slash (/)");
508473
path.append("-").append(actionName);
509474
if (!actionName.endsWith(".api"))
510475
path.append(".api");
511476

512477
URIBuilder builder = new URIBuilder(uri).setPath(path.toString());
513478

514-
Map<String, Object> params = getParameters();
479+
Map<String, Object> params = createParameterMap();
515480

516481
//add the required version if it's > 0
517482
if (getRequiredVersion() > 0)
@@ -566,6 +531,7 @@ protected String getParamValueAsString(Object param, String name)
566531
* Returns the required version number of this API call.
567532
* @return The required version number
568533
*/
534+
@Override
569535
public double getRequiredVersion()
570536
{
571537
return _requiredVersion;

src/org/labkey/remoteapi/CommandResponse.java

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,26 +36,24 @@ public class CommandResponse
3636
private final String _text;
3737
private final int _statusCode;
3838
private final String _contentType;
39-
private final double _requiredVersion;
4039

4140
private Map<String, Object> _data;
4241

4342
/**
4443
* Constructs a new CommandResponse, initialized with the provided
4544
* response text and status code.
46-
* @param text The response text
47-
* @param statusCode The HTTP status code
45+
*
46+
* @param text The response text
47+
* @param statusCode The HTTP status code
4848
* @param contentType The response content type
49-
* @param json The parsed JSONObject (or null if JSON was not returned).
50-
* @param sourceCommand A copy of the command that created this response
49+
* @param json The parsed JSONObject (or null if JSON was not returned)
5150
*/
52-
public CommandResponse(String text, int statusCode, String contentType, JSONObject json, Command<?> sourceCommand)
51+
public CommandResponse(String text, int statusCode, String contentType, JSONObject json)
5352
{
5453
_text = text;
5554
_statusCode = statusCode;
5655
_contentType = contentType;
5756
_data = null != json ? json.toMap() : null;
58-
_requiredVersion = sourceCommand != null ? sourceCommand.getRequiredVersion() : 0.0;
5957
}
6058

6159
/**
@@ -94,16 +92,6 @@ public String getContentType()
9492
return _contentType;
9593
}
9694

97-
/**
98-
* Returns the API version number required by the source command.
99-
* Some APIs may return data in a different format depending on the required version
100-
* @return the required API version number
101-
*/
102-
public double getRequiredVersion()
103-
{
104-
return _requiredVersion;
105-
}
106-
10795
/**
10896
* Attempts to parse the response text and return a property Map.
10997
* <p>

src/org/labkey/remoteapi/Connection.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,9 +234,7 @@ private HttpClientBuilder clientBuilder()
234234

235235
HttpClientBuilder builder = HttpClientBuilder.create()
236236
.setConnectionManager(connectionManager)
237-
.setDefaultRequestConfig(RequestConfig.custom().setResponseTimeout(getTimeout(), TimeUnit.MILLISECONDS).build())
238-
.setDefaultCookieStore(_httpClientContext.getCookieStore())
239-
.setDefaultCredentialsProvider(_httpClientContext.getCredentialsProvider());
237+
.setDefaultRequestConfig(RequestConfig.custom().setResponseTimeout(getTimeout(), TimeUnit.MILLISECONDS).build());
240238

241239
if (_proxyHost != null && _proxyPort != null)
242240
builder.setProxy(new HttpHost(_proxyHost, _proxyPort));
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.labkey.remoteapi;
2+
3+
import org.apache.hc.client5.http.classic.methods.HttpGet;
4+
5+
import java.net.URI;
6+
7+
/** Base class for all commands that use get **/
8+
public abstract class GetCommand<ResponseType extends CommandResponse> extends Command<ResponseType, HttpGet>
9+
{
10+
protected GetCommand(String controllerName, String actionName)
11+
{
12+
super(controllerName, actionName);
13+
}
14+
15+
/**
16+
* Creates the {@link HttpGet} instance used for the request. Override to modify the HttpGet object before use.
17+
* @param uri the request uri
18+
* @return The HttpUriRequest instance.
19+
*/
20+
@Override
21+
protected HttpGet createRequest(URI uri)
22+
{
23+
return new HttpGet(uri);
24+
}
25+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package org.labkey.remoteapi;
2+
3+
public interface HasRequiredVersion
4+
{
5+
double getRequiredVersion();
6+
}

0 commit comments

Comments
 (0)