From 5f22349617397743c96ec082f97d64b7b760ae40 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Tue, 15 Jul 2025 14:40:58 -0700 Subject: [PATCH 01/26] QuerySaveRowsCommand --- .../remoteapi/query/QuerySaveRowsCommand.java | 236 ++++++++++++++++++ .../query/QuerySaveRowsResponse.java | 110 ++++++++ 2 files changed, 346 insertions(+) create mode 100644 src/org/labkey/remoteapi/query/QuerySaveRowsCommand.java create mode 100644 src/org/labkey/remoteapi/query/QuerySaveRowsResponse.java diff --git a/src/org/labkey/remoteapi/query/QuerySaveRowsCommand.java b/src/org/labkey/remoteapi/query/QuerySaveRowsCommand.java new file mode 100644 index 00000000..4b00add6 --- /dev/null +++ b/src/org/labkey/remoteapi/query/QuerySaveRowsCommand.java @@ -0,0 +1,236 @@ +package org.labkey.remoteapi.query; + +import org.json.JSONObject; +import org.labkey.remoteapi.PostCommand; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class QuerySaveRowsCommand extends PostCommand +{ + private final List _commands = new ArrayList<>(); + private Map _extraContext; + private Boolean _transacted; + private Boolean _validateOnly; + + /** + * Constructs a new SaveRowsActualCommand given a controller and action name. + * + * @param commands The commands. + */ + public QuerySaveRowsCommand(Command... commands) + { + super("query", "saveRows.api"); + + addCommand(commands); + } + + public Map getExtraContext() + { + return _extraContext; + } + + public void setExtraContext(Map extraContext) + { + _extraContext = extraContext; + } + + public QuerySaveRowsCommand addCommand(Command... commands) + { + for (Command command : commands) + { + if (command != null) + _commands.add(command); + } + + return this; + } + + public List getCommands() + { + return _commands; + } + + public Boolean isTransacted() + { + return _transacted; + } + + public void setTransacted(Boolean transacted) + { + _transacted = transacted; + } + + public Boolean isValidateOnly() + { + return _validateOnly; + } + + public void setValidateOnly(Boolean validateOnly) + { + _validateOnly = validateOnly; + } + + @Override + public JSONObject getJsonObject() + { + JSONObject json = new JSONObject(); + + List commands = new ArrayList<>(); + for (Command command : getCommands()) + commands.add(command.getJsonObject()); + json.put("commands", commands); + + if (getExtraContext() != null && !getExtraContext().isEmpty()) + json.put("extraContext", getExtraContext()); + + if (isTransacted() != null) + json.put("transacted", isTransacted()); + + if (isValidateOnly() != null) + json.put("validateOnly", isValidateOnly()); + + return json; + } + + @Override + protected QuerySaveRowsResponse createResponse(String text, int status, String contentType, JSONObject json) + { + return new QuerySaveRowsResponse(text, status, contentType, json); + } + + public enum CommandType + { + Insert, + Update, + Delete + } + + public static class Command + { + SaveRowsCommand.AuditBehavior _auditBehavior; + String _auditUserComment; + final CommandType _commandType; + String _containerPath; + Map _extraContext; + List> _rows; + final String _queryName; + final String _schemaName; + Boolean _skipReselectRows; + + public Command(CommandType commandType, String schemaName, String queryName, List> rows) + { + assert null != commandType; + assert null != schemaName && !schemaName.isEmpty(); + assert null != queryName && !queryName.isEmpty(); + + _commandType = commandType; + _schemaName = schemaName; + _queryName = queryName; + _rows = rows; + } + + public JSONObject getJsonObject() + { + JSONObject json = new JSONObject(); + + json.put("command", getCommandType().name().toLowerCase()); + json.put("schemaName", getSchemaName()); + json.put("queryName", getQueryName()); + json.put("rows", getRows()); + + if (getAuditBehavior() != null) + json.put("auditBehavior", getAuditBehavior()); + + if (getAuditUserComment() != null && !getAuditUserComment().isEmpty()) + json.put("auditUserComment", getAuditUserComment()); + + if (getContainerPath() != null && !getContainerPath().isEmpty()) + json.put("containerPath", getContainerPath()); + + if (getExtraContext() != null && !getExtraContext().isEmpty()) + json.put("extraContext", getExtraContext()); + + if (isSkipReselectRows() != null) + json.put("skipReselectRows", isSkipReselectRows()); + + return json; + } + + public SaveRowsCommand.AuditBehavior getAuditBehavior() + { + return _auditBehavior; + } + + public void setAuditBehavior(SaveRowsCommand.AuditBehavior auditBehavior) + { + _auditBehavior = auditBehavior; + } + + public String getAuditUserComment() + { + return _auditUserComment; + } + + public void setAuditUserComment(String auditUserComment) + { + _auditUserComment = auditUserComment; + } + + public CommandType getCommandType() + { + return _commandType; + } + + public String getContainerPath() + { + return _containerPath; + } + + public void setContainerPath(String containerPath) + { + _containerPath = containerPath; + } + + public Map getExtraContext() + { + return _extraContext; + } + + public void setExtraContext(Map extraContext) + { + _extraContext = extraContext; + } + + public String getQueryName() + { + return _queryName; + } + + public String getSchemaName() + { + return _schemaName; + } + + public List> getRows() + { + return _rows; + } + + public void setRows(List> rows) + { + _rows = rows; + } + + public Boolean isSkipReselectRows() + { + return _skipReselectRows; + } + + public void setSkipReselectRows(Boolean skipReselectRows) + { + _skipReselectRows = skipReselectRows; + } + } +} diff --git a/src/org/labkey/remoteapi/query/QuerySaveRowsResponse.java b/src/org/labkey/remoteapi/query/QuerySaveRowsResponse.java new file mode 100644 index 00000000..03fa2f57 --- /dev/null +++ b/src/org/labkey/remoteapi/query/QuerySaveRowsResponse.java @@ -0,0 +1,110 @@ +package org.labkey.remoteapi.query; + +import org.json.JSONObject; +import org.labkey.remoteapi.CommandResponse; +import org.labkey.remoteapi.collections.CaseInsensitiveHashMap; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class QuerySaveRowsResponse extends CommandResponse +{ + private final boolean _committed; + private final int _errorCount; + private final List _results; + + public QuerySaveRowsResponse(String text, int statusCode, String contentType, JSONObject json) + { + super(text, statusCode, contentType, json); + + _committed = json.optBoolean("committed", false); + _errorCount = json.optInt("errorCount", 0); + + List results = new ArrayList<>(); + if (json.has("result")) + { + for (Object resultJson : json.getJSONArray("result")) + results.add(new Result((JSONObject) resultJson)); + } + _results = Collections.unmodifiableList(results); + } + + public boolean isCommitted() + { + return _committed; + } + + public int getErrorCount() + { + return _errorCount; + } + + public List getResults() + { + return _results; + } + + public static class Result + { + private final String _command; + private final String _containerPath; + private final String _queryName; + private final List> _rows = new ArrayList<>(); + private final int _rowsAffected; + private final String _schemaName; + private final int _transactionAuditId; + + private Result(JSONObject json) + { + _command = json.optString("command", null); + _containerPath = json.optString("containerPath", null); + _queryName = json.optString("queryName", null); + _rowsAffected = json.optInt("rowsAffected", 0); + _schemaName = json.optString("schemaName", null); + _transactionAuditId = json.optInt("transactionAuditId", 0); + + if (json.has("rows")) + { + for (Object rowJson : json.getJSONArray("rows")) + _rows.add(new CaseInsensitiveHashMap<>(((JSONObject) rowJson).toMap())); + } + } + + public String getCommand() + { + return _command; + } + + public String getContainerPath() + { + return _containerPath; + } + + public String getQueryName() + { + return _queryName; + } + + public List> getRows() + { + return _rows; + } + + public int getRowsAffected() + { + return _rowsAffected; + } + + public String getSchemaName() + { + return _schemaName; + } + + public int getTransactionAuditId() + { + return _transactionAuditId; + } + } +} From 45feb9a1cd51bc54f31bad6a0d0c314ffeb79fa0 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Tue, 15 Jul 2025 14:41:10 -0700 Subject: [PATCH 02/26] Add QuerySaveRowsCommandDemo --- .../test/QuerySaveRowsCommandDemo.java | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 src/org/labkey/remoteapi/test/QuerySaveRowsCommandDemo.java diff --git a/src/org/labkey/remoteapi/test/QuerySaveRowsCommandDemo.java b/src/org/labkey/remoteapi/test/QuerySaveRowsCommandDemo.java new file mode 100644 index 00000000..9d7776df --- /dev/null +++ b/src/org/labkey/remoteapi/test/QuerySaveRowsCommandDemo.java @@ -0,0 +1,99 @@ +package org.labkey.remoteapi.test; + +import org.labkey.remoteapi.ApiKeyCredentialsProvider; +import org.labkey.remoteapi.Connection; +import org.labkey.remoteapi.domain.CreateDomainCommand; +import org.labkey.remoteapi.domain.Domain; +import org.labkey.remoteapi.domain.PropertyDescriptor; +import org.labkey.remoteapi.query.InsertRowsCommand; +import org.labkey.remoteapi.query.QuerySaveRowsCommand; +import org.labkey.remoteapi.query.QuerySaveRowsCommand.Command; +import org.labkey.remoteapi.query.QuerySaveRowsCommand.CommandType; +import org.labkey.remoteapi.query.QuerySaveRowsResponse; +import org.labkey.remoteapi.query.SaveRowsCommand; +import org.labkey.remoteapi.query.SaveRowsResponse; +import org.labkey.remoteapi.security.CreateContainerCommand; +import org.labkey.remoteapi.security.DeleteContainerCommand; + +import java.util.List; +import java.util.Map; + +public class QuerySaveRowsCommandDemo +{ + public static void main(String[] args) throws Exception + { + String folderPath = "SaveRowsCommandDemo"; + String schemaName = "lists"; + String queryName = "Players"; + + ApiKeyCredentialsProvider credentials = new ApiKeyCredentialsProvider("xxx"); + Connection conn = new Connection("http://localhost:8080", credentials); + + // Create the project + new CreateContainerCommand(folderPath).execute(conn, "/"); + + try + { + // Create a list + { + CreateDomainCommand createdListCmd = new CreateDomainCommand("IntList", queryName); + createdListCmd.setOptions(Map.of("keyName", "JerseyNumber", "keyType", "Integer")); + + Domain domain = createdListCmd.getDomainDesign(); + domain.setFields(List.of( + new PropertyDescriptor("FirstName", "First Name", "string"), + new PropertyDescriptor("LastName", "Last Name", "string"), + new PropertyDescriptor("Team", null, "string") + )); + + createdListCmd.execute(conn, folderPath); + } + + // Add some initial data + { + InsertRowsCommand insertCmd = new InsertRowsCommand(schemaName, queryName); + insertCmd.addRow(Map.of("FirstName", "Alvin", "LastName", "David", "JerseyNumber", 21, "Team", "Seattle Mariners")); + insertCmd.addRow(Map.of("FirstName", "Jay", "LastName", "Buhner", "JerseyNumber", 19, "Team", "New York Yankees")); + insertCmd.addRow(Map.of("FirstName", "Ken", "LastName", "Phelps", "JerseyNumber", 44, "Team", "Seattle Mariners")); + + SaveRowsResponse response = insertCmd.execute(conn, folderPath); + System.out.printf(String.format("Inserted %d players%n", response.getRowsAffected().intValue())); + } + + // Execute multiple query operations using a saveRows command + { + QuerySaveRowsCommand saveCmd = new QuerySaveRowsCommand(); + + // Draft Ken Griffey Jr. + saveCmd.addCommand(new Command(CommandType.Insert, schemaName, queryName, List.of(Map.of("FirstName", "Ken", "LastName", "Griffey Jr.", "JerseyNumber", 24, "Team", "Seattle Mariners")))); + + // Trade for Jay Buhner + Command tradeJayBuhnerCommand = new Command(CommandType.Update, schemaName, queryName, List.of( + Map.of("JerseyNumber", 19, "Team", "Seattle Mariners"), + Map.of("JerseyNumber", 44, "Team", "New York Yankees") + )); + tradeJayBuhnerCommand.setAuditBehavior(SaveRowsCommand.AuditBehavior.DETAILED); + tradeJayBuhnerCommand.setAuditUserComment("Traded Jay Buhner for Ken Phelps on July 21, 1988"); + saveCmd.addCommand(tradeJayBuhnerCommand); + + // Alvin Davis retires + saveCmd.addCommand(new Command(CommandType.Delete, schemaName, queryName, List.of(Map.of("JerseyNumber", 21)))); + + QuerySaveRowsResponse response = saveCmd.execute(conn, folderPath); + System.out.printf("Executed saveRows command with %d errors and %d results%n", response.getErrorCount(), response.getResults().size()); + } + } + catch (Exception e) + { + System.out.printf("Error: %s%n", e.getMessage()); + System.out.printf("Type: %s%n", e.getClass().getName()); + System.out.println("Stack trace:"); + for (StackTraceElement element : e.getStackTrace()) + System.out.printf(" at %s%n", element.toString()); + } + finally + { + new DeleteContainerCommand().execute(conn, folderPath); + } + } +} From f8fdc822b73d4d54b5193e8c17253d1123eddcf7 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Tue, 15 Jul 2025 14:43:32 -0700 Subject: [PATCH 03/26] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8b28c3b..e2342875 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## version 6.4.0-SNAPSHOT *Released*: TBD * Update Gradle, Gradle Plugins, HttpClient, and JSONObject versions +* Add `QuerySaveRowsCommand` which wraps the `query-saveRows.api` endpoint ## version 6.3.0 *Released*: 19 June 2025 From b21527510ff69467d2a3116d8e5b20b45664aae7 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Tue, 15 Jul 2025 14:50:26 -0700 Subject: [PATCH 04/26] 6.4.0-QuerySaveRowsCommand-SNAPSHOT --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1dc76d54..a4ad9a15 100644 --- a/build.gradle +++ b/build.gradle @@ -72,7 +72,7 @@ repositories { group = "org.labkey.api" -version = "6.4.0-SNAPSHOT" +version = "6.4.0-QuerySaveRowsCommand-SNAPSHOT" dependencies { api "org.json:json:${jsonObjectVersion}" From 5a559750936a2f8ee565be7fd32bdd74b4739400 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Wed, 16 Jul 2025 09:53:41 -0700 Subject: [PATCH 05/26] Update pom.scm.url --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a4ad9a15..e5500f03 100644 --- a/build.gradle +++ b/build.gradle @@ -191,7 +191,7 @@ project.publishing { scm { connection = 'scm:git:https://github.com/LabKey/labkey-api-java' developerConnection = 'scm:git:https://github.com/LabKey/labkey-api-java' - url = 'scm:git:https://github.com/LabKey/labkey-api-java/labkey-client-api' + url = 'scm:git:https://github.com/LabKey/labkey-api-java' } } } From a6ced45b1b91677ceaf4787b419180cc50301164 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Wed, 16 Jul 2025 11:18:18 -0700 Subject: [PATCH 06/26] Rename to SaveRowsApiCommand --- ...wsCommand.java => SaveRowsApiCommand.java} | 25 ++++++++----------- ...Response.java => SaveRowsApiResponse.java} | 4 +-- ...sCommandDemo.java => SaveRowsApiDemo.java} | 14 +++++------ 3 files changed, 20 insertions(+), 23 deletions(-) rename src/org/labkey/remoteapi/query/{QuerySaveRowsCommand.java => SaveRowsApiCommand.java} (88%) rename src/org/labkey/remoteapi/query/{QuerySaveRowsResponse.java => SaveRowsApiResponse.java} (94%) rename src/org/labkey/remoteapi/test/{QuerySaveRowsCommandDemo.java => SaveRowsApiDemo.java} (90%) diff --git a/src/org/labkey/remoteapi/query/QuerySaveRowsCommand.java b/src/org/labkey/remoteapi/query/SaveRowsApiCommand.java similarity index 88% rename from src/org/labkey/remoteapi/query/QuerySaveRowsCommand.java rename to src/org/labkey/remoteapi/query/SaveRowsApiCommand.java index 4b00add6..cd32f9b0 100644 --- a/src/org/labkey/remoteapi/query/QuerySaveRowsCommand.java +++ b/src/org/labkey/remoteapi/query/SaveRowsApiCommand.java @@ -7,22 +7,16 @@ import java.util.List; import java.util.Map; -public class QuerySaveRowsCommand extends PostCommand +public class SaveRowsApiCommand extends PostCommand { private final List _commands = new ArrayList<>(); private Map _extraContext; private Boolean _transacted; private Boolean _validateOnly; - /** - * Constructs a new SaveRowsActualCommand given a controller and action name. - * - * @param commands The commands. - */ - public QuerySaveRowsCommand(Command... commands) + public SaveRowsApiCommand(Command... commands) { super("query", "saveRows.api"); - addCommand(commands); } @@ -31,12 +25,13 @@ public Map getExtraContext() return _extraContext; } - public void setExtraContext(Map extraContext) + public SaveRowsApiCommand setExtraContext(Map extraContext) { _extraContext = extraContext; + return this; } - public QuerySaveRowsCommand addCommand(Command... commands) + public SaveRowsApiCommand addCommand(Command... commands) { for (Command command : commands) { @@ -57,9 +52,10 @@ public Boolean isTransacted() return _transacted; } - public void setTransacted(Boolean transacted) + public SaveRowsApiCommand setTransacted(Boolean transacted) { _transacted = transacted; + return this; } public Boolean isValidateOnly() @@ -67,9 +63,10 @@ public Boolean isValidateOnly() return _validateOnly; } - public void setValidateOnly(Boolean validateOnly) + public SaveRowsApiCommand setValidateOnly(Boolean validateOnly) { _validateOnly = validateOnly; + return this; } @Override @@ -95,9 +92,9 @@ public JSONObject getJsonObject() } @Override - protected QuerySaveRowsResponse createResponse(String text, int status, String contentType, JSONObject json) + protected SaveRowsApiResponse createResponse(String text, int status, String contentType, JSONObject json) { - return new QuerySaveRowsResponse(text, status, contentType, json); + return new SaveRowsApiResponse(text, status, contentType, json); } public enum CommandType diff --git a/src/org/labkey/remoteapi/query/QuerySaveRowsResponse.java b/src/org/labkey/remoteapi/query/SaveRowsApiResponse.java similarity index 94% rename from src/org/labkey/remoteapi/query/QuerySaveRowsResponse.java rename to src/org/labkey/remoteapi/query/SaveRowsApiResponse.java index 03fa2f57..579b90c9 100644 --- a/src/org/labkey/remoteapi/query/QuerySaveRowsResponse.java +++ b/src/org/labkey/remoteapi/query/SaveRowsApiResponse.java @@ -9,13 +9,13 @@ import java.util.List; import java.util.Map; -public class QuerySaveRowsResponse extends CommandResponse +public class SaveRowsApiResponse extends CommandResponse { private final boolean _committed; private final int _errorCount; private final List _results; - public QuerySaveRowsResponse(String text, int statusCode, String contentType, JSONObject json) + public SaveRowsApiResponse(String text, int statusCode, String contentType, JSONObject json) { super(text, statusCode, contentType, json); diff --git a/src/org/labkey/remoteapi/test/QuerySaveRowsCommandDemo.java b/src/org/labkey/remoteapi/test/SaveRowsApiDemo.java similarity index 90% rename from src/org/labkey/remoteapi/test/QuerySaveRowsCommandDemo.java rename to src/org/labkey/remoteapi/test/SaveRowsApiDemo.java index 9d7776df..a1371861 100644 --- a/src/org/labkey/remoteapi/test/QuerySaveRowsCommandDemo.java +++ b/src/org/labkey/remoteapi/test/SaveRowsApiDemo.java @@ -6,10 +6,10 @@ import org.labkey.remoteapi.domain.Domain; import org.labkey.remoteapi.domain.PropertyDescriptor; import org.labkey.remoteapi.query.InsertRowsCommand; -import org.labkey.remoteapi.query.QuerySaveRowsCommand; -import org.labkey.remoteapi.query.QuerySaveRowsCommand.Command; -import org.labkey.remoteapi.query.QuerySaveRowsCommand.CommandType; -import org.labkey.remoteapi.query.QuerySaveRowsResponse; +import org.labkey.remoteapi.query.SaveRowsApiCommand; +import org.labkey.remoteapi.query.SaveRowsApiCommand.Command; +import org.labkey.remoteapi.query.SaveRowsApiCommand.CommandType; +import org.labkey.remoteapi.query.SaveRowsApiResponse; import org.labkey.remoteapi.query.SaveRowsCommand; import org.labkey.remoteapi.query.SaveRowsResponse; import org.labkey.remoteapi.security.CreateContainerCommand; @@ -18,7 +18,7 @@ import java.util.List; import java.util.Map; -public class QuerySaveRowsCommandDemo +public class SaveRowsApiDemo { public static void main(String[] args) throws Exception { @@ -62,7 +62,7 @@ public static void main(String[] args) throws Exception // Execute multiple query operations using a saveRows command { - QuerySaveRowsCommand saveCmd = new QuerySaveRowsCommand(); + SaveRowsApiCommand saveCmd = new SaveRowsApiCommand(); // Draft Ken Griffey Jr. saveCmd.addCommand(new Command(CommandType.Insert, schemaName, queryName, List.of(Map.of("FirstName", "Ken", "LastName", "Griffey Jr.", "JerseyNumber", 24, "Team", "Seattle Mariners")))); @@ -79,7 +79,7 @@ public static void main(String[] args) throws Exception // Alvin Davis retires saveCmd.addCommand(new Command(CommandType.Delete, schemaName, queryName, List.of(Map.of("JerseyNumber", 21)))); - QuerySaveRowsResponse response = saveCmd.execute(conn, folderPath); + SaveRowsApiResponse response = saveCmd.execute(conn, folderPath); System.out.printf("Executed saveRows command with %d errors and %d results%n", response.getErrorCount(), response.getResults().size()); } } From 56f1d3955dea95680813ccecc5c26cf9ad746982 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Wed, 16 Jul 2025 11:31:02 -0700 Subject: [PATCH 07/26] Documentation --- .../remoteapi/query/SaveRowsApiCommand.java | 49 +++++++++++++++++ .../remoteapi/query/SaveRowsApiResponse.java | 53 +++++++++++++++++++ .../remoteapi/test/SaveRowsApiDemo.java | 3 +- 3 files changed, 104 insertions(+), 1 deletion(-) diff --git a/src/org/labkey/remoteapi/query/SaveRowsApiCommand.java b/src/org/labkey/remoteapi/query/SaveRowsApiCommand.java index cd32f9b0..d5daa8ed 100644 --- a/src/org/labkey/remoteapi/query/SaveRowsApiCommand.java +++ b/src/org/labkey/remoteapi/query/SaveRowsApiCommand.java @@ -7,6 +7,55 @@ import java.util.List; import java.util.Map; +/** + * Command for executing multiple data modification operations (insert, update, delete) in a single request + * to a LabKey Server. This command allows batching multiple operations together, optionally in a transaction. + *

+ * All data exposed from a LabKey Server is organized into schemas containing queries. Each command in a batch + * specifies the schema name (e.g., 'lists' or 'study') and query name (e.g., 'People' or 'Samples') to operate on. + *

+ * The command supports several features: + *

    + *
  • Multiple operations (insert, update, delete) in a single request
  • + *
  • Optional transaction support to ensure all-or-nothing execution
  • + *
  • Validation-only mode to check operations without making changes
  • + *
  • Audit trail support with configurable detail levels
  • + *
  • Custom audit comments for tracking changes
  • + *
+ *

+ * Example usage: + *


+ * ApiKeyCredentialsProvider credentials = new ApiKeyCredentialsProvider("xxx");
+ * Connection conn = new Connection("http://localhost:8080", credentials);
+ * SaveRowsApiCommand saveCmd = new SaveRowsApiCommand();
+ *
+ * // Add new gene annotations
+ * saveCmd.addCommand(new Command(CommandType.Insert, "genome", "GeneAnnotations",
+ *     List.of(
+ *         Map.of("name", "p53 binding site", "geneName", "TP53", "start", 1000, "end", 1020),
+ *         Map.of("name", "TATA box", "geneName", "BRCA1", "start", 2500, "end", 2506)
+ *     )));
+ *
+ * // Update annotation positions
+ * Command updateCmd = new Command(CommandType.Update, "genome", "GeneAnnotations",
+ *     List.of(Map.of(
+ *         "name", "Promoter region",
+ *         "geneName", "EGFR",
+ *         "start", 5000,
+ *         "end", 5500
+ *     )));
+ * updateCmd.setAuditBehavior(SaveRowsCommand.AuditBehavior.DETAILED);
+ * updateCmd.setAuditUserComment("Updated promoter region coordinates based on new assembly");
+ * saveCmd.addCommand(updateCmd);
+ *
+ * // Delete obsolete annotation
+ * saveCmd.addCommand(new Command(CommandType.Delete, "genome", "GeneAnnotations",
+ *     List.of(Map.of("name", "Putative enhancer", "geneName", "MYC"))));
+ *
+ * // Execute all commands in a transaction
+ * SaveRowsApiResponse response = saveCmd.execute(conn, "GenomeProject");
+ * 
+ */ public class SaveRowsApiCommand extends PostCommand { private final List _commands = new ArrayList<>(); diff --git a/src/org/labkey/remoteapi/query/SaveRowsApiResponse.java b/src/org/labkey/remoteapi/query/SaveRowsApiResponse.java index 579b90c9..91db211a 100644 --- a/src/org/labkey/remoteapi/query/SaveRowsApiResponse.java +++ b/src/org/labkey/remoteapi/query/SaveRowsApiResponse.java @@ -9,6 +9,59 @@ import java.util.List; import java.util.Map; +/** + * Response object for the {@link SaveRowsApiCommand}, containing results of batch operations executed on the server. + * This response provides details about the success or failure of each command in the batch, including: + *
    + *
  • Whether the transaction was committed
  • + *
  • Number of errors encountered
  • + *
  • Detailed results for each command executed
  • + *
+ *

+ * Example usage: + *


+ * SaveRowsApiCommand cmd = new SaveRowsApiCommand();
+ * // Add commands to insert/update/delete gene annotations...
+ * SaveRowsApiResponse response = cmd.execute(connection, "GenomeProject");
+ *
+ * if (response.isCommitted())
+ * {
+ *     for (SaveRowsApiResponse.Result result : response.getResults())
+ *     {
+ *         System.out.println(String.format(
+ *             "%s operation affected %d rows in %s.%s",
+ *             result.getCommand(),
+ *             result.getRowsAffected(),
+ *             result.getSchemaName(),
+ *             result.getQueryName()
+ *         ));
+ *
+ *         // For detailed examination of affected rows
+ *         for (Map row : result.getRows())
+ *         {
+ *             System.out.println(String.format(
+ *                 "Gene %s annotation at position %d-%d",
+ *                 row.get("geneName"),
+ *                 row.get("start"),
+ *                 row.get("end")
+ *             ));
+ *         }
+ *
+ *         // Check if operation was audited
+ *         if (result.getTransactionAuditId() > 0)
+ *         {
+ *             System.out.println("Audit record created with ID: " +
+ *                 result.getTransactionAuditId());
+ *         }
+ *     }
+ * }
+ * else
+ * {
+ *     System.out.println("Transaction failed with " +
+ *         response.getErrorCount() + " errors");
+ * }
+ * 
+ */ public class SaveRowsApiResponse extends CommandResponse { private final boolean _committed; diff --git a/src/org/labkey/remoteapi/test/SaveRowsApiDemo.java b/src/org/labkey/remoteapi/test/SaveRowsApiDemo.java index a1371861..0d9c66c3 100644 --- a/src/org/labkey/remoteapi/test/SaveRowsApiDemo.java +++ b/src/org/labkey/remoteapi/test/SaveRowsApiDemo.java @@ -22,10 +22,11 @@ public class SaveRowsApiDemo { public static void main(String[] args) throws Exception { - String folderPath = "SaveRowsCommandDemo"; + String folderPath = "SaveRowsApiDemo"; String schemaName = "lists"; String queryName = "Players"; + // Furnish your own API key ApiKeyCredentialsProvider credentials = new ApiKeyCredentialsProvider("xxx"); Connection conn = new Connection("http://localhost:8080", credentials); From d715f5341845f0b574e2bd6c5d08cd4048d2ad10 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Wed, 16 Jul 2025 11:38:37 -0700 Subject: [PATCH 08/26] Fix HTML --- .../remoteapi/query/SaveRowsApiCommand.java | 50 ++++++------- .../remoteapi/query/SaveRowsApiResponse.java | 74 +++++++++---------- 2 files changed, 62 insertions(+), 62 deletions(-) diff --git a/src/org/labkey/remoteapi/query/SaveRowsApiCommand.java b/src/org/labkey/remoteapi/query/SaveRowsApiCommand.java index d5daa8ed..afb0683d 100644 --- a/src/org/labkey/remoteapi/query/SaveRowsApiCommand.java +++ b/src/org/labkey/remoteapi/query/SaveRowsApiCommand.java @@ -25,35 +25,35 @@ *

* Example usage: *


- * ApiKeyCredentialsProvider credentials = new ApiKeyCredentialsProvider("xxx");
- * Connection conn = new Connection("http://localhost:8080", credentials);
- * SaveRowsApiCommand saveCmd = new SaveRowsApiCommand();
+ *  ApiKeyCredentialsProvider credentials = new ApiKeyCredentialsProvider("xxx");
+ *  Connection conn = new Connection("http://localhost:8080", credentials);
+ *  SaveRowsApiCommand saveCmd = new SaveRowsApiCommand();
  *
- * // Add new gene annotations
- * saveCmd.addCommand(new Command(CommandType.Insert, "genome", "GeneAnnotations",
- *     List.of(
- *         Map.of("name", "p53 binding site", "geneName", "TP53", "start", 1000, "end", 1020),
- *         Map.of("name", "TATA box", "geneName", "BRCA1", "start", 2500, "end", 2506)
- *     )));
+ *  // Add new gene annotations
+ *  saveCmd.addCommand(new Command(CommandType.Insert, "genome", "GeneAnnotations",
+ *      List.of(
+ *          Map.of("name", "p53 binding site", "geneName", "TP53", "start", 1000, "end", 1020),
+ *          Map.of("name", "TATA box", "geneName", "BRCA1", "start", 2500, "end", 2506)
+ *      )));
  *
- * // Update annotation positions
- * Command updateCmd = new Command(CommandType.Update, "genome", "GeneAnnotations",
- *     List.of(Map.of(
- *         "name", "Promoter region",
- *         "geneName", "EGFR",
- *         "start", 5000,
- *         "end", 5500
- *     )));
- * updateCmd.setAuditBehavior(SaveRowsCommand.AuditBehavior.DETAILED);
- * updateCmd.setAuditUserComment("Updated promoter region coordinates based on new assembly");
- * saveCmd.addCommand(updateCmd);
+ *  // Update annotation positions
+ *  Command updateCmd = new Command(CommandType.Update, "genome", "GeneAnnotations",
+ *      List.of(Map.of(
+ *          "name", "Promoter region",
+ *          "geneName", "EGFR",
+ *          "start", 5000,
+ *          "end", 5500
+ *      )));
+ *  updateCmd.setAuditBehavior(SaveRowsCommand.AuditBehavior.DETAILED);
+ *  updateCmd.setAuditUserComment("Updated promoter region coordinates based on new assembly");
+ *  saveCmd.addCommand(updateCmd);
  *
- * // Delete obsolete annotation
- * saveCmd.addCommand(new Command(CommandType.Delete, "genome", "GeneAnnotations",
- *     List.of(Map.of("name", "Putative enhancer", "geneName", "MYC"))));
+ *  // Delete obsolete annotation
+ *  saveCmd.addCommand(new Command(CommandType.Delete, "genome", "GeneAnnotations",
+ *      List.of(Map.of("name", "Putative enhancer", "geneName", "MYC"))));
  *
- * // Execute all commands in a transaction
- * SaveRowsApiResponse response = saveCmd.execute(conn, "GenomeProject");
+ *  // Execute all commands in a transaction
+ *  SaveRowsApiResponse response = saveCmd.execute(conn, "GenomeProject");
  * 
*/ public class SaveRowsApiCommand extends PostCommand diff --git a/src/org/labkey/remoteapi/query/SaveRowsApiResponse.java b/src/org/labkey/remoteapi/query/SaveRowsApiResponse.java index 91db211a..fbec8632 100644 --- a/src/org/labkey/remoteapi/query/SaveRowsApiResponse.java +++ b/src/org/labkey/remoteapi/query/SaveRowsApiResponse.java @@ -20,46 +20,46 @@ *

* Example usage: *


- * SaveRowsApiCommand cmd = new SaveRowsApiCommand();
- * // Add commands to insert/update/delete gene annotations...
- * SaveRowsApiResponse response = cmd.execute(connection, "GenomeProject");
+ *  SaveRowsApiCommand cmd = new SaveRowsApiCommand();
+ *  // Add commands to insert/update/delete gene annotations...
+ *  SaveRowsApiResponse response = cmd.execute(connection, "GenomeProject");
  *
- * if (response.isCommitted())
- * {
- *     for (SaveRowsApiResponse.Result result : response.getResults())
- *     {
- *         System.out.println(String.format(
- *             "%s operation affected %d rows in %s.%s",
- *             result.getCommand(),
- *             result.getRowsAffected(),
- *             result.getSchemaName(),
- *             result.getQueryName()
- *         ));
+ *  if (response.isCommitted())
+ *  {
+ *      for (SaveRowsApiResponse.Result result : response.getResults())
+ *      {
+ *          System.out.println(String.format(
+ *              "%s operation affected %d rows in %s.%s",
+ *              result.getCommand(),
+ *              result.getRowsAffected(),
+ *              result.getSchemaName(),
+ *              result.getQueryName()
+ *          ));
  *
- *         // For detailed examination of affected rows
- *         for (Map row : result.getRows())
- *         {
- *             System.out.println(String.format(
- *                 "Gene %s annotation at position %d-%d",
- *                 row.get("geneName"),
- *                 row.get("start"),
- *                 row.get("end")
- *             ));
- *         }
+ *          // For detailed examination of affected rows
+ *          for (Map>String, Object> row : result.getRows())
+ *          {
+ *              System.out.println(String.format(
+ *                  "Gene %s annotation at position %d-%d",
+ *                  row.get("geneName"),
+ *                  row.get("start"),
+ *                  row.get("end")
+ *              ));
+ *          }
  *
- *         // Check if operation was audited
- *         if (result.getTransactionAuditId() > 0)
- *         {
- *             System.out.println("Audit record created with ID: " +
- *                 result.getTransactionAuditId());
- *         }
- *     }
- * }
- * else
- * {
- *     System.out.println("Transaction failed with " +
- *         response.getErrorCount() + " errors");
- * }
+ *          // Check if operation was audited
+ *          if (result.getTransactionAuditId() > 0)
+ *          {
+ *              System.out.println("Audit record created with ID: " +
+ *                result.getTransactionAuditId());
+ *          }
+ *      }
+ *  }
+ *  else
+ *  {
+ *      System.out.println("Transaction failed with " +
+ *          response.getErrorCount() + " errors");
+ *  }
  * 
*/ public class SaveRowsApiResponse extends CommandResponse From 01473008ebb2d71ea065b6e349f146b8890ed529 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Wed, 16 Jul 2025 13:23:35 -0700 Subject: [PATCH 09/26] Update setters --- .../remoteapi/query/SaveRowsApiCommand.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/org/labkey/remoteapi/query/SaveRowsApiCommand.java b/src/org/labkey/remoteapi/query/SaveRowsApiCommand.java index afb0683d..1860a824 100644 --- a/src/org/labkey/remoteapi/query/SaveRowsApiCommand.java +++ b/src/org/labkey/remoteapi/query/SaveRowsApiCommand.java @@ -209,9 +209,10 @@ public SaveRowsCommand.AuditBehavior getAuditBehavior() return _auditBehavior; } - public void setAuditBehavior(SaveRowsCommand.AuditBehavior auditBehavior) + public Command setAuditBehavior(SaveRowsCommand.AuditBehavior auditBehavior) { _auditBehavior = auditBehavior; + return this; } public String getAuditUserComment() @@ -219,9 +220,10 @@ public String getAuditUserComment() return _auditUserComment; } - public void setAuditUserComment(String auditUserComment) + public Command setAuditUserComment(String auditUserComment) { _auditUserComment = auditUserComment; + return this; } public CommandType getCommandType() @@ -234,9 +236,10 @@ public String getContainerPath() return _containerPath; } - public void setContainerPath(String containerPath) + public Command setContainerPath(String containerPath) { _containerPath = containerPath; + return this; } public Map getExtraContext() @@ -244,9 +247,10 @@ public Map getExtraContext() return _extraContext; } - public void setExtraContext(Map extraContext) + public Command setExtraContext(Map extraContext) { _extraContext = extraContext; + return this; } public String getQueryName() @@ -264,9 +268,10 @@ public List> getRows() return _rows; } - public void setRows(List> rows) + public Command setRows(List> rows) { _rows = rows; + return this; } public Boolean isSkipReselectRows() @@ -274,9 +279,10 @@ public Boolean isSkipReselectRows() return _skipReselectRows; } - public void setSkipReselectRows(Boolean skipReselectRows) + public Command setSkipReselectRows(Boolean skipReselectRows) { _skipReselectRows = skipReselectRows; + return this; } } } From e2e776fadd6c2658effdf53ba8ac510b28a39da3 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Wed, 16 Jul 2025 13:26:57 -0700 Subject: [PATCH 10/26] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2342875..ee9dd98b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## version 6.4.0-SNAPSHOT *Released*: TBD * Update Gradle, Gradle Plugins, HttpClient, and JSONObject versions -* Add `QuerySaveRowsCommand` which wraps the `query-saveRows.api` endpoint +* Add `SaveRowsApiCommand` which wraps the `query-saveRows.api` endpoint ## version 6.3.0 *Released*: 19 June 2025 From 94d9db88fadee1ed41ab9be097e9a5a9aacbd7bb Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Wed, 16 Jul 2025 13:28:12 -0700 Subject: [PATCH 11/26] Remove SaveRowsApiDemo.java --- .../remoteapi/test/SaveRowsApiDemo.java | 100 ------------------ 1 file changed, 100 deletions(-) delete mode 100644 src/org/labkey/remoteapi/test/SaveRowsApiDemo.java diff --git a/src/org/labkey/remoteapi/test/SaveRowsApiDemo.java b/src/org/labkey/remoteapi/test/SaveRowsApiDemo.java deleted file mode 100644 index 0d9c66c3..00000000 --- a/src/org/labkey/remoteapi/test/SaveRowsApiDemo.java +++ /dev/null @@ -1,100 +0,0 @@ -package org.labkey.remoteapi.test; - -import org.labkey.remoteapi.ApiKeyCredentialsProvider; -import org.labkey.remoteapi.Connection; -import org.labkey.remoteapi.domain.CreateDomainCommand; -import org.labkey.remoteapi.domain.Domain; -import org.labkey.remoteapi.domain.PropertyDescriptor; -import org.labkey.remoteapi.query.InsertRowsCommand; -import org.labkey.remoteapi.query.SaveRowsApiCommand; -import org.labkey.remoteapi.query.SaveRowsApiCommand.Command; -import org.labkey.remoteapi.query.SaveRowsApiCommand.CommandType; -import org.labkey.remoteapi.query.SaveRowsApiResponse; -import org.labkey.remoteapi.query.SaveRowsCommand; -import org.labkey.remoteapi.query.SaveRowsResponse; -import org.labkey.remoteapi.security.CreateContainerCommand; -import org.labkey.remoteapi.security.DeleteContainerCommand; - -import java.util.List; -import java.util.Map; - -public class SaveRowsApiDemo -{ - public static void main(String[] args) throws Exception - { - String folderPath = "SaveRowsApiDemo"; - String schemaName = "lists"; - String queryName = "Players"; - - // Furnish your own API key - ApiKeyCredentialsProvider credentials = new ApiKeyCredentialsProvider("xxx"); - Connection conn = new Connection("http://localhost:8080", credentials); - - // Create the project - new CreateContainerCommand(folderPath).execute(conn, "/"); - - try - { - // Create a list - { - CreateDomainCommand createdListCmd = new CreateDomainCommand("IntList", queryName); - createdListCmd.setOptions(Map.of("keyName", "JerseyNumber", "keyType", "Integer")); - - Domain domain = createdListCmd.getDomainDesign(); - domain.setFields(List.of( - new PropertyDescriptor("FirstName", "First Name", "string"), - new PropertyDescriptor("LastName", "Last Name", "string"), - new PropertyDescriptor("Team", null, "string") - )); - - createdListCmd.execute(conn, folderPath); - } - - // Add some initial data - { - InsertRowsCommand insertCmd = new InsertRowsCommand(schemaName, queryName); - insertCmd.addRow(Map.of("FirstName", "Alvin", "LastName", "David", "JerseyNumber", 21, "Team", "Seattle Mariners")); - insertCmd.addRow(Map.of("FirstName", "Jay", "LastName", "Buhner", "JerseyNumber", 19, "Team", "New York Yankees")); - insertCmd.addRow(Map.of("FirstName", "Ken", "LastName", "Phelps", "JerseyNumber", 44, "Team", "Seattle Mariners")); - - SaveRowsResponse response = insertCmd.execute(conn, folderPath); - System.out.printf(String.format("Inserted %d players%n", response.getRowsAffected().intValue())); - } - - // Execute multiple query operations using a saveRows command - { - SaveRowsApiCommand saveCmd = new SaveRowsApiCommand(); - - // Draft Ken Griffey Jr. - saveCmd.addCommand(new Command(CommandType.Insert, schemaName, queryName, List.of(Map.of("FirstName", "Ken", "LastName", "Griffey Jr.", "JerseyNumber", 24, "Team", "Seattle Mariners")))); - - // Trade for Jay Buhner - Command tradeJayBuhnerCommand = new Command(CommandType.Update, schemaName, queryName, List.of( - Map.of("JerseyNumber", 19, "Team", "Seattle Mariners"), - Map.of("JerseyNumber", 44, "Team", "New York Yankees") - )); - tradeJayBuhnerCommand.setAuditBehavior(SaveRowsCommand.AuditBehavior.DETAILED); - tradeJayBuhnerCommand.setAuditUserComment("Traded Jay Buhner for Ken Phelps on July 21, 1988"); - saveCmd.addCommand(tradeJayBuhnerCommand); - - // Alvin Davis retires - saveCmd.addCommand(new Command(CommandType.Delete, schemaName, queryName, List.of(Map.of("JerseyNumber", 21)))); - - SaveRowsApiResponse response = saveCmd.execute(conn, folderPath); - System.out.printf("Executed saveRows command with %d errors and %d results%n", response.getErrorCount(), response.getResults().size()); - } - } - catch (Exception e) - { - System.out.printf("Error: %s%n", e.getMessage()); - System.out.printf("Type: %s%n", e.getClass().getName()); - System.out.println("Stack trace:"); - for (StackTraceElement element : e.getStackTrace()) - System.out.printf(" at %s%n", element.toString()); - } - finally - { - new DeleteContainerCommand().execute(conn, folderPath); - } - } -} From 38728b3fd536a073f1ed0bea0b51fde375735bc7 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Wed, 16 Jul 2025 16:41:53 -0700 Subject: [PATCH 12/26] Issue 53243: Add "includeEmptyPermGroups" to GetGroupPermsCommand --- .../security/GetGroupPermsCommand.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/org/labkey/remoteapi/security/GetGroupPermsCommand.java b/src/org/labkey/remoteapi/security/GetGroupPermsCommand.java index db83c8a3..48bfc1fa 100644 --- a/src/org/labkey/remoteapi/security/GetGroupPermsCommand.java +++ b/src/org/labkey/remoteapi/security/GetGroupPermsCommand.java @@ -22,6 +22,7 @@ public class GetGroupPermsCommand extends GetCommand { + private boolean _includeEmptyPermGroups = true; private boolean _includeSubfolders = false; public GetGroupPermsCommand() @@ -29,6 +30,24 @@ public GetGroupPermsCommand() super("security", "getGroupPerms"); } + /** + * Determines whether groups with no effective permissions are included in the result. + * @return true or false (default is true). + */ + public boolean isIncludeEmptyPermGroups() + { + return _includeEmptyPermGroups; + } + + /** + * Sets whether groups with no effective permissions are included in the result. + * @param includeEmptyPermGroups true to include groups with no effective permissions + */ + public void setIncludeEmptyPermGroups(boolean includeEmptyPermGroups) + { + _includeEmptyPermGroups = includeEmptyPermGroups; + } + /** * Returns whether the command will recurse down the subfolders * of the folder in which the command is executed. @@ -60,6 +79,7 @@ protected Map createParameterMap() { Map params = super.createParameterMap(); params.put("includeSubfolders", isIncludeSubfolders()); + params.put("includeEmptyPermGroups", isIncludeEmptyPermGroups()); return params; } From 113fd74e2f0e5f4ad05bd68a428216efea078444 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Thu, 17 Jul 2025 09:42:22 -0700 Subject: [PATCH 13/26] addCommands --- src/org/labkey/remoteapi/query/SaveRowsApiCommand.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/org/labkey/remoteapi/query/SaveRowsApiCommand.java b/src/org/labkey/remoteapi/query/SaveRowsApiCommand.java index 1860a824..8e4970c3 100644 --- a/src/org/labkey/remoteapi/query/SaveRowsApiCommand.java +++ b/src/org/labkey/remoteapi/query/SaveRowsApiCommand.java @@ -66,7 +66,7 @@ public class SaveRowsApiCommand extends PostCommand public SaveRowsApiCommand(Command... commands) { super("query", "saveRows.api"); - addCommand(commands); + addCommands(commands); } public Map getExtraContext() @@ -80,7 +80,7 @@ public SaveRowsApiCommand setExtraContext(Map extraContext) return this; } - public SaveRowsApiCommand addCommand(Command... commands) + public SaveRowsApiCommand addCommands(Command... commands) { for (Command command : commands) { From 15cfcc5b7f402c4b4c87fcea186fc100d015ee2e Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Thu, 17 Jul 2025 09:50:39 -0700 Subject: [PATCH 14/26] SaveRowsCommand -> BaseRowsCommand --- .../{SaveRowsCommand.java => BaseRowsCommand.java} | 4 ++-- src/org/labkey/remoteapi/query/DeleteRowsCommand.java | 8 ++++---- src/org/labkey/remoteapi/query/InsertRowsCommand.java | 10 +++++----- src/org/labkey/remoteapi/query/MoveRowsCommand.java | 6 +++--- src/org/labkey/remoteapi/query/SaveRowsApiCommand.java | 6 +++--- src/org/labkey/remoteapi/query/UpdateRowsCommand.java | 10 +++++----- 6 files changed, 22 insertions(+), 22 deletions(-) rename src/org/labkey/remoteapi/query/{SaveRowsCommand.java => BaseRowsCommand.java} (98%) diff --git a/src/org/labkey/remoteapi/query/SaveRowsCommand.java b/src/org/labkey/remoteapi/query/BaseRowsCommand.java similarity index 98% rename from src/org/labkey/remoteapi/query/SaveRowsCommand.java rename to src/org/labkey/remoteapi/query/BaseRowsCommand.java index e6287765..3ff09817 100644 --- a/src/org/labkey/remoteapi/query/SaveRowsCommand.java +++ b/src/org/labkey/remoteapi/query/BaseRowsCommand.java @@ -81,7 +81,7 @@ * resp = cmdDel.execute(cn, "PROJECT_NAME"); * */ -public abstract class SaveRowsCommand extends PostCommand +public abstract class BaseRowsCommand extends PostCommand { public enum AuditBehavior { @@ -103,7 +103,7 @@ public enum AuditBehavior * @param queryName The query name. * @param actionName The action name to call (supplied by the derived class). */ - protected SaveRowsCommand(String schemaName, String queryName, String actionName) + protected BaseRowsCommand(String schemaName, String queryName, String actionName) { super("query", actionName); assert null != schemaName; diff --git a/src/org/labkey/remoteapi/query/DeleteRowsCommand.java b/src/org/labkey/remoteapi/query/DeleteRowsCommand.java index a4a5ab4e..f5291b40 100644 --- a/src/org/labkey/remoteapi/query/DeleteRowsCommand.java +++ b/src/org/labkey/remoteapi/query/DeleteRowsCommand.java @@ -20,16 +20,16 @@ * with the connection used when executing this command must have * permission to delete the data. *

- * For details on schemas and queries, and example code, see the {@link SaveRowsCommand}. + * For details on schemas and queries, and example code, see the {@link BaseRowsCommand}. */ -public class DeleteRowsCommand extends SaveRowsCommand +public class DeleteRowsCommand extends BaseRowsCommand { /** * Constructs a DeleteRowsCommand for the given schemaName and queryName. - * See the {@link SaveRowsCommand} for more details. + * See the {@link BaseRowsCommand} for more details. * @param schemaName The schemaName * @param queryName The queryName. - * @see SaveRowsCommand + * @see BaseRowsCommand */ public DeleteRowsCommand(String schemaName, String queryName) { diff --git a/src/org/labkey/remoteapi/query/InsertRowsCommand.java b/src/org/labkey/remoteapi/query/InsertRowsCommand.java index 323db104..ad21d317 100644 --- a/src/org/labkey/remoteapi/query/InsertRowsCommand.java +++ b/src/org/labkey/remoteapi/query/InsertRowsCommand.java @@ -20,17 +20,17 @@ * The user associated with the connection used when executing this * command must have permission to insert data into the specified query. *

- * For details on schemas and queries, and example code, see the {@link SaveRowsCommand}. - * @see SaveRowsCommand + * For details on schemas and queries, and example code, see the {@link BaseRowsCommand}. + * @see BaseRowsCommand */ -public class InsertRowsCommand extends SaveRowsCommand +public class InsertRowsCommand extends BaseRowsCommand { /** * Constructs an InsertRowsCommand for the given schemaName and queryName. - * See the {@link SaveRowsCommand} for more details. + * See the {@link BaseRowsCommand} for more details. * @param schemaName The schemaName * @param queryName The queryName. - * @see SaveRowsCommand + * @see BaseRowsCommand */ public InsertRowsCommand(String schemaName, String queryName) { diff --git a/src/org/labkey/remoteapi/query/MoveRowsCommand.java b/src/org/labkey/remoteapi/query/MoveRowsCommand.java index bab0f531..3f5eded8 100644 --- a/src/org/labkey/remoteapi/query/MoveRowsCommand.java +++ b/src/org/labkey/remoteapi/query/MoveRowsCommand.java @@ -23,17 +23,17 @@ * permission to update data for the source container and insert data * for the target container. */ -public class MoveRowsCommand extends SaveRowsCommand +public class MoveRowsCommand extends BaseRowsCommand { private final String _targetContainerPath; /** * Constructs a MoveRowsCommand for the given targetContainerPath, schemaName, and queryName. - * See the {@link SaveRowsCommand} for more details. + * See the {@link BaseRowsCommand} for more details. * @param targetContainerPath The targetContainerPath * @param schemaName The schemaName * @param queryName The queryName. - * @see SaveRowsCommand + * @see BaseRowsCommand */ public MoveRowsCommand(String targetContainerPath, String schemaName, String queryName) { diff --git a/src/org/labkey/remoteapi/query/SaveRowsApiCommand.java b/src/org/labkey/remoteapi/query/SaveRowsApiCommand.java index 8e4970c3..fa9cc3e6 100644 --- a/src/org/labkey/remoteapi/query/SaveRowsApiCommand.java +++ b/src/org/labkey/remoteapi/query/SaveRowsApiCommand.java @@ -155,7 +155,7 @@ public enum CommandType public static class Command { - SaveRowsCommand.AuditBehavior _auditBehavior; + BaseRowsCommand.AuditBehavior _auditBehavior; String _auditUserComment; final CommandType _commandType; String _containerPath; @@ -204,12 +204,12 @@ public JSONObject getJsonObject() return json; } - public SaveRowsCommand.AuditBehavior getAuditBehavior() + public BaseRowsCommand.AuditBehavior getAuditBehavior() { return _auditBehavior; } - public Command setAuditBehavior(SaveRowsCommand.AuditBehavior auditBehavior) + public Command setAuditBehavior(BaseRowsCommand.AuditBehavior auditBehavior) { _auditBehavior = auditBehavior; return this; diff --git a/src/org/labkey/remoteapi/query/UpdateRowsCommand.java b/src/org/labkey/remoteapi/query/UpdateRowsCommand.java index ae16d301..8931d08b 100644 --- a/src/org/labkey/remoteapi/query/UpdateRowsCommand.java +++ b/src/org/labkey/remoteapi/query/UpdateRowsCommand.java @@ -20,17 +20,17 @@ * The user associated with the connection used when executing this * command must have permission to update data into the specified query. *

- * For details on schemas and queries, and example code, see the {@link SaveRowsCommand}. - * @see SaveRowsCommand + * For details on schemas and queries, and example code, see the {@link BaseRowsCommand}. + * @see BaseRowsCommand */ -public class UpdateRowsCommand extends SaveRowsCommand +public class UpdateRowsCommand extends BaseRowsCommand { /** * Constructs an UpdateRowsCommand for the given schemaName and queryName. - * See the {@link SaveRowsCommand} for more details. + * See the {@link BaseRowsCommand} for more details. * @param schemaName The schemaName * @param queryName The queryName. - * @see SaveRowsCommand + * @see BaseRowsCommand */ public UpdateRowsCommand(String schemaName, String queryName) { From 47c782fdf7ee55867166454337f2c0170fcc3d9e Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Thu, 17 Jul 2025 09:52:26 -0700 Subject: [PATCH 15/26] RowsResponse -> BaseRowsResponse --- .../query/{RowsResponse.java => BaseRowsResponse.java} | 4 ++-- src/org/labkey/remoteapi/query/SaveRowsResponse.java | 2 +- src/org/labkey/remoteapi/query/SelectRowsResponse.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename src/org/labkey/remoteapi/query/{RowsResponse.java => BaseRowsResponse.java} (97%) diff --git a/src/org/labkey/remoteapi/query/RowsResponse.java b/src/org/labkey/remoteapi/query/BaseRowsResponse.java similarity index 97% rename from src/org/labkey/remoteapi/query/RowsResponse.java rename to src/org/labkey/remoteapi/query/BaseRowsResponse.java index 298846ad..548b890f 100644 --- a/src/org/labkey/remoteapi/query/RowsResponse.java +++ b/src/org/labkey/remoteapi/query/BaseRowsResponse.java @@ -32,7 +32,7 @@ * and meta-data about those rows. Primarily, this class converts * date values in the rows array to real Java Date objects. */ -abstract class RowsResponse extends CommandResponse +abstract class BaseRowsResponse extends CommandResponse { /** * Constructs a new RowsResponse given the specified text and status code. @@ -42,7 +42,7 @@ abstract class RowsResponse extends CommandResponse * @param json The parsed JSONObject (or null if JSON was not returned. * @param hasRequiredVersion An object that implements HasRequiredVersion, such as the command that created this response */ - RowsResponse(String text, int statusCode, String contentType, JSONObject json, HasRequiredVersion hasRequiredVersion) + BaseRowsResponse(String text, int statusCode, String contentType, JSONObject json, HasRequiredVersion hasRequiredVersion) { super(text, statusCode, contentType, json); double requiredVersion = hasRequiredVersion.getRequiredVersion(); diff --git a/src/org/labkey/remoteapi/query/SaveRowsResponse.java b/src/org/labkey/remoteapi/query/SaveRowsResponse.java index 622e44b0..1fbcb504 100644 --- a/src/org/labkey/remoteapi/query/SaveRowsResponse.java +++ b/src/org/labkey/remoteapi/query/SaveRowsResponse.java @@ -23,7 +23,7 @@ * This response object provides helper methods for accessing the important * bits of the parsed response data. */ -public class SaveRowsResponse extends RowsResponse +public class SaveRowsResponse extends BaseRowsResponse { /** * Constructs a new SaveRowsResponse given the response text and status code diff --git a/src/org/labkey/remoteapi/query/SelectRowsResponse.java b/src/org/labkey/remoteapi/query/SelectRowsResponse.java index dbb449dc..f723d301 100644 --- a/src/org/labkey/remoteapi/query/SelectRowsResponse.java +++ b/src/org/labkey/remoteapi/query/SelectRowsResponse.java @@ -28,7 +28,7 @@ * of the parsed response data. * @see SelectRowsCommand */ -public class SelectRowsResponse extends RowsResponse +public class SelectRowsResponse extends BaseRowsResponse { /** * An enumeration of the possible column data types From a70125356b246b3c5ba2eaa6911c38d87b7e6f53 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Thu, 17 Jul 2025 09:55:20 -0700 Subject: [PATCH 16/26] SaveRowsResponse -> RowsResponse --- src/org/labkey/remoteapi/query/BaseRowsCommand.java | 6 +++--- .../query/{SaveRowsResponse.java => RowsResponse.java} | 4 ++-- src/org/labkey/remoteapi/test/Test.java | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) rename src/org/labkey/remoteapi/query/{SaveRowsResponse.java => RowsResponse.java} (92%) diff --git a/src/org/labkey/remoteapi/query/BaseRowsCommand.java b/src/org/labkey/remoteapi/query/BaseRowsCommand.java index 3ff09817..f48bf580 100644 --- a/src/org/labkey/remoteapi/query/BaseRowsCommand.java +++ b/src/org/labkey/remoteapi/query/BaseRowsCommand.java @@ -81,7 +81,7 @@ * resp = cmdDel.execute(cn, "PROJECT_NAME"); * */ -public abstract class BaseRowsCommand extends PostCommand +public abstract class BaseRowsCommand extends PostCommand { public enum AuditBehavior { @@ -279,8 +279,8 @@ public JSONObject getJsonObject() } @Override - protected SaveRowsResponse createResponse(String text, int status, String contentType, JSONObject json) + protected RowsResponse createResponse(String text, int status, String contentType, JSONObject json) { - return new SaveRowsResponse(text, status, contentType, json, this); + return new RowsResponse(text, status, contentType, json, this); } } diff --git a/src/org/labkey/remoteapi/query/SaveRowsResponse.java b/src/org/labkey/remoteapi/query/RowsResponse.java similarity index 92% rename from src/org/labkey/remoteapi/query/SaveRowsResponse.java rename to src/org/labkey/remoteapi/query/RowsResponse.java index 1fbcb504..6cb606d3 100644 --- a/src/org/labkey/remoteapi/query/SaveRowsResponse.java +++ b/src/org/labkey/remoteapi/query/RowsResponse.java @@ -23,7 +23,7 @@ * This response object provides helper methods for accessing the important * bits of the parsed response data. */ -public class SaveRowsResponse extends BaseRowsResponse +public class RowsResponse extends BaseRowsResponse { /** * Constructs a new SaveRowsResponse given the response text and status code @@ -33,7 +33,7 @@ public class SaveRowsResponse extends BaseRowsResponse * @param json The parsed JSONObject (or null if JSON was not returned) * @param hasRequiredVersion An object that implements HasRequiredVersion */ - public SaveRowsResponse(String text, int statusCode, String contentType, JSONObject json, HasRequiredVersion hasRequiredVersion) + public RowsResponse(String text, int statusCode, String contentType, JSONObject json, HasRequiredVersion hasRequiredVersion) { super(text, statusCode, contentType, json, hasRequiredVersion); } diff --git a/src/org/labkey/remoteapi/test/Test.java b/src/org/labkey/remoteapi/test/Test.java index e4a2e4ac..99a74e06 100644 --- a/src/org/labkey/remoteapi/test/Test.java +++ b/src/org/labkey/remoteapi/test/Test.java @@ -33,7 +33,7 @@ import org.labkey.remoteapi.query.GetSchemasCommand; import org.labkey.remoteapi.query.GetSchemasResponse; import org.labkey.remoteapi.query.InsertRowsCommand; -import org.labkey.remoteapi.query.SaveRowsResponse; +import org.labkey.remoteapi.query.RowsResponse; import org.labkey.remoteapi.query.SelectRowsCommand; import org.labkey.remoteapi.query.SelectRowsResponse; import org.labkey.remoteapi.query.Sort; @@ -124,7 +124,7 @@ public static void crudTest(Connection cn, String folder) throws Exception row.put("Last", "Test Inserted Value"); cmdins.addRow(row); - SaveRowsResponse resp = cmdins.execute(cn, folder); + RowsResponse resp = cmdins.execute(cn, folder); //make sure row count is one greater srresp = cmdsel.execute(cn, folder); From d38954d96cbaee4f10343ce79722ffae9ed81af5 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Thu, 17 Jul 2025 09:58:05 -0700 Subject: [PATCH 17/26] SaveRowsApi -> SaveRows --- ...eRowsApiCommand.java => SaveRowsCommand.java} | 16 ++++++++-------- ...owsApiResponse.java => SaveRowsResponse.java} | 12 ++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) rename src/org/labkey/remoteapi/query/{SaveRowsApiCommand.java => SaveRowsCommand.java} (93%) rename src/org/labkey/remoteapi/query/{SaveRowsApiResponse.java => SaveRowsResponse.java} (90%) diff --git a/src/org/labkey/remoteapi/query/SaveRowsApiCommand.java b/src/org/labkey/remoteapi/query/SaveRowsCommand.java similarity index 93% rename from src/org/labkey/remoteapi/query/SaveRowsApiCommand.java rename to src/org/labkey/remoteapi/query/SaveRowsCommand.java index fa9cc3e6..97d26ceb 100644 --- a/src/org/labkey/remoteapi/query/SaveRowsApiCommand.java +++ b/src/org/labkey/remoteapi/query/SaveRowsCommand.java @@ -56,14 +56,14 @@ * SaveRowsApiResponse response = saveCmd.execute(conn, "GenomeProject"); * */ -public class SaveRowsApiCommand extends PostCommand +public class SaveRowsCommand extends PostCommand { private final List _commands = new ArrayList<>(); private Map _extraContext; private Boolean _transacted; private Boolean _validateOnly; - public SaveRowsApiCommand(Command... commands) + public SaveRowsCommand(Command... commands) { super("query", "saveRows.api"); addCommands(commands); @@ -74,13 +74,13 @@ public Map getExtraContext() return _extraContext; } - public SaveRowsApiCommand setExtraContext(Map extraContext) + public SaveRowsCommand setExtraContext(Map extraContext) { _extraContext = extraContext; return this; } - public SaveRowsApiCommand addCommands(Command... commands) + public SaveRowsCommand addCommands(Command... commands) { for (Command command : commands) { @@ -101,7 +101,7 @@ public Boolean isTransacted() return _transacted; } - public SaveRowsApiCommand setTransacted(Boolean transacted) + public SaveRowsCommand setTransacted(Boolean transacted) { _transacted = transacted; return this; @@ -112,7 +112,7 @@ public Boolean isValidateOnly() return _validateOnly; } - public SaveRowsApiCommand setValidateOnly(Boolean validateOnly) + public SaveRowsCommand setValidateOnly(Boolean validateOnly) { _validateOnly = validateOnly; return this; @@ -141,9 +141,9 @@ public JSONObject getJsonObject() } @Override - protected SaveRowsApiResponse createResponse(String text, int status, String contentType, JSONObject json) + protected SaveRowsResponse createResponse(String text, int status, String contentType, JSONObject json) { - return new SaveRowsApiResponse(text, status, contentType, json); + return new SaveRowsResponse(text, status, contentType, json); } public enum CommandType diff --git a/src/org/labkey/remoteapi/query/SaveRowsApiResponse.java b/src/org/labkey/remoteapi/query/SaveRowsResponse.java similarity index 90% rename from src/org/labkey/remoteapi/query/SaveRowsApiResponse.java rename to src/org/labkey/remoteapi/query/SaveRowsResponse.java index fbec8632..f85a2c53 100644 --- a/src/org/labkey/remoteapi/query/SaveRowsApiResponse.java +++ b/src/org/labkey/remoteapi/query/SaveRowsResponse.java @@ -10,7 +10,7 @@ import java.util.Map; /** - * Response object for the {@link SaveRowsApiCommand}, containing results of batch operations executed on the server. + * Response object for the {@link SaveRowsCommand}, containing results of batch operations executed on the server. * This response provides details about the success or failure of each command in the batch, including: *

    *
  • Whether the transaction was committed
  • @@ -20,13 +20,13 @@ *

    * Example usage: *

    
    - *  SaveRowsApiCommand cmd = new SaveRowsApiCommand();
    + *  SaveRowsCommand cmd = new SaveRowsCommand();
      *  // Add commands to insert/update/delete gene annotations...
    - *  SaveRowsApiResponse response = cmd.execute(connection, "GenomeProject");
    + *  SaveRowsResponse response = cmd.execute(connection, "GenomeProject");
      *
      *  if (response.isCommitted())
      *  {
    - *      for (SaveRowsApiResponse.Result result : response.getResults())
    + *      for (SaveRowsResponse.Result result : response.getResults())
      *      {
      *          System.out.println(String.format(
      *              "%s operation affected %d rows in %s.%s",
    @@ -62,13 +62,13 @@
      *  }
      * 
    */ -public class SaveRowsApiResponse extends CommandResponse +public class SaveRowsResponse extends CommandResponse { private final boolean _committed; private final int _errorCount; private final List _results; - public SaveRowsApiResponse(String text, int statusCode, String contentType, JSONObject json) + public SaveRowsResponse(String text, int statusCode, String contentType, JSONObject json) { super(text, statusCode, contentType, json); From 80c03526ecd23ed8b7dd40fec17ab97250815225 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Thu, 17 Jul 2025 10:25:59 -0700 Subject: [PATCH 18/26] Extend BaseRowsCommand --- .../remoteapi/query/SaveRowsCommand.java | 110 +++++++----------- 1 file changed, 41 insertions(+), 69 deletions(-) diff --git a/src/org/labkey/remoteapi/query/SaveRowsCommand.java b/src/org/labkey/remoteapi/query/SaveRowsCommand.java index 97d26ceb..26fdb8b6 100644 --- a/src/org/labkey/remoteapi/query/SaveRowsCommand.java +++ b/src/org/labkey/remoteapi/query/SaveRowsCommand.java @@ -1,8 +1,11 @@ package org.labkey.remoteapi.query; import org.json.JSONObject; +import org.labkey.remoteapi.CommandException; +import org.labkey.remoteapi.Connection; import org.labkey.remoteapi.PostCommand; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -153,136 +156,105 @@ public enum CommandType Delete } - public static class Command + public static class Command extends BaseRowsCommand { - BaseRowsCommand.AuditBehavior _auditBehavior; - String _auditUserComment; final CommandType _commandType; String _containerPath; - Map _extraContext; - List> _rows; - final String _queryName; - final String _schemaName; Boolean _skipReselectRows; public Command(CommandType commandType, String schemaName, String queryName, List> rows) { + super(schemaName, queryName, null); assert null != commandType; - assert null != schemaName && !schemaName.isEmpty(); - assert null != queryName && !queryName.isEmpty(); - _commandType = commandType; - _schemaName = schemaName; - _queryName = queryName; - _rows = rows; + setRows(rows); } public JSONObject getJsonObject() { - JSONObject json = new JSONObject(); - + JSONObject json = super.getJsonObject(); json.put("command", getCommandType().name().toLowerCase()); - json.put("schemaName", getSchemaName()); - json.put("queryName", getQueryName()); - json.put("rows", getRows()); - - if (getAuditBehavior() != null) - json.put("auditBehavior", getAuditBehavior()); - - if (getAuditUserComment() != null && !getAuditUserComment().isEmpty()) - json.put("auditUserComment", getAuditUserComment()); if (getContainerPath() != null && !getContainerPath().isEmpty()) json.put("containerPath", getContainerPath()); - if (getExtraContext() != null && !getExtraContext().isEmpty()) - json.put("extraContext", getExtraContext()); - if (isSkipReselectRows() != null) json.put("skipReselectRows", isSkipReselectRows()); return json; } - public BaseRowsCommand.AuditBehavior getAuditBehavior() - { - return _auditBehavior; - } - - public Command setAuditBehavior(BaseRowsCommand.AuditBehavior auditBehavior) - { - _auditBehavior = auditBehavior; - return this; - } - - public String getAuditUserComment() + public CommandType getCommandType() { - return _auditUserComment; + return _commandType; } - public Command setAuditUserComment(String auditUserComment) + public String getContainerPath() { - _auditUserComment = auditUserComment; - return this; + return _containerPath; } - public CommandType getCommandType() + public void setContainerPath(String containerPath) { - return _commandType; + _containerPath = containerPath; } - public String getContainerPath() + public Boolean isSkipReselectRows() { - return _containerPath; + return _skipReselectRows; } - public Command setContainerPath(String containerPath) + public void setSkipReselectRows(Boolean skipReselectRows) { - _containerPath = containerPath; - return this; + _skipReselectRows = skipReselectRows; } - public Map getExtraContext() + @Override + public String getActionName() { - return _extraContext; + throw new UnsupportedOperationException(unsupportedMethodMessage()); } - public Command setExtraContext(Map extraContext) + @Override + public String getControllerName() { - _extraContext = extraContext; - return this; + throw new UnsupportedOperationException(unsupportedMethodMessage()); } - public String getQueryName() + @Override + public RowsResponse execute(Connection connection, String folderPath) throws IOException, CommandException { - return _queryName; + throw new UnsupportedOperationException(unsupportedMethodMessage()); } - public String getSchemaName() + @Override + public double getRequiredVersion() { - return _schemaName; + throw new UnsupportedOperationException(unsupportedMethodMessage()); } - public List> getRows() + @Override + public void setRequiredVersion(double requiredVersion) { - return _rows; + throw new UnsupportedOperationException(unsupportedMethodMessage()); } - public Command setRows(List> rows) + @Override + public Integer getTimeout() { - _rows = rows; - return this; + throw new UnsupportedOperationException(unsupportedMethodMessage()); } - public Boolean isSkipReselectRows() + @Override + public void setTimeout(Integer timeout) { - return _skipReselectRows; + throw new UnsupportedOperationException(unsupportedMethodMessage()); } - public Command setSkipReselectRows(Boolean skipReselectRows) + private String unsupportedMethodMessage() { - _skipReselectRows = skipReselectRows; - return this; + // "Command does not support methodName()." + return Command.class.getSimpleName() + " does not support " + new Throwable().getStackTrace()[1].getMethodName() + "()."; } } } From 961d667b5df6971ce4adc81e3ae1289d63b00499 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Thu, 17 Jul 2025 12:49:31 -0700 Subject: [PATCH 19/26] Remove extension of BaseRowsCommand --- .../remoteapi/query/BaseRowsCommand.java | 58 +++++---- .../remoteapi/query/SaveRowsCommand.java | 116 +++++++++++------- 2 files changed, 109 insertions(+), 65 deletions(-) diff --git a/src/org/labkey/remoteapi/query/BaseRowsCommand.java b/src/org/labkey/remoteapi/query/BaseRowsCommand.java index f48bf580..cd613ae3 100644 --- a/src/org/labkey/remoteapi/query/BaseRowsCommand.java +++ b/src/org/labkey/remoteapi/query/BaseRowsCommand.java @@ -239,48 +239,64 @@ public JSONObject getJsonObject() json.put("extraContext", getExtraContext()); if (getAuditBehavior() != null) json.put("auditBehavior", getAuditBehavior()); - if (getAuditUserComment() != null) - json.put("auditUserComment", getAuditUserComment()); + stringToJson(json, "auditUserComment", getAuditUserComment()); + json.put("rows", rowsToJson(getRows())); + + return json; + } + + @Override + protected RowsResponse createResponse(String text, int status, String contentType, JSONObject json) + { + return new RowsResponse(text, status, contentType, json, this); + } + + static void stringToJson(JSONObject json, String prop, String value) + { + if (value != null && !value.isEmpty()) + { + String trimmed = value.trim(); + if (!trimmed.isEmpty()) + json.put(prop, trimmed); + } + } + + static JSONArray rowsToJson(List> rows) + { //unfortunately, JSON simple is so simple that it doesn't //encode maps into JSON objects on the fly, //nor dates into property JSON format JSONArray jsonRows = new JSONArray(); - if(null != getRows()) + if (null != rows && !rows.isEmpty()) { - SimpleDateFormat fmt = new SimpleDateFormat("d MMM yyyy HH:mm:ss Z"); - for(Map row : getRows()) + SimpleDateFormat dateFormat = new SimpleDateFormat("d MMM yyyy HH:mm:ss Z"); + for (Map row : rows) { - JSONObject jsonRow; - if (row instanceof JSONObject jo) //optimization + if (row instanceof JSONObject jo) { - jsonRow = jo; + jsonRows.put(jo); } else { - jsonRow = new JSONObject(); - //row map entries must be scalar values (no embedded maps or arrays) - for(Map.Entry entry : row.entrySet()) + JSONObject jsonRow = new JSONObject(); + // Row map entries must be scalar values (no embedded maps or arrays) + for (Map.Entry entry : row.entrySet()) { Object value = entry.getValue(); - if(value instanceof Date) - value = fmt.format((Date)value); + if (value instanceof Date dateValue) + value = dateFormat.format(dateValue); // JSONObject.wrap allows us to save 'null' values. jsonRow.put(entry.getKey(), JSONObject.wrap(value)); } + + jsonRows.put(jsonRow); } - jsonRows.put(jsonRow); } } - json.put("rows", jsonRows); - return json; - } - @Override - protected RowsResponse createResponse(String text, int status, String contentType, JSONObject json) - { - return new RowsResponse(text, status, contentType, json, this); + return jsonRows; } } diff --git a/src/org/labkey/remoteapi/query/SaveRowsCommand.java b/src/org/labkey/remoteapi/query/SaveRowsCommand.java index 26fdb8b6..bf2ce18b 100644 --- a/src/org/labkey/remoteapi/query/SaveRowsCommand.java +++ b/src/org/labkey/remoteapi/query/SaveRowsCommand.java @@ -1,11 +1,8 @@ package org.labkey.remoteapi.query; import org.json.JSONObject; -import org.labkey.remoteapi.CommandException; -import org.labkey.remoteapi.Connection; import org.labkey.remoteapi.PostCommand; -import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -49,7 +46,7 @@ * ))); * updateCmd.setAuditBehavior(SaveRowsCommand.AuditBehavior.DETAILED); * updateCmd.setAuditUserComment("Updated promoter region coordinates based on new assembly"); - * saveCmd.addCommand(updateCmd); + * saveCmd.addCommands(updateCmd); * * // Delete obsolete annotation * saveCmd.addCommand(new Command(CommandType.Delete, "genome", "GeneAnnotations", @@ -156,27 +153,50 @@ public enum CommandType Delete } - public static class Command extends BaseRowsCommand + // N.B. You may be inclined to have this share implementation with BaseRowsCommand; however, I would caution + // against doing so. This class does not represent a command like a PostCommand or a GetCommand but rather + // aligns with the "commands" made on a request to the save rows endpoint. + public static class Command { + BaseRowsCommand.AuditBehavior _auditBehavior; + String _auditUserComment; final CommandType _commandType; String _containerPath; + Map _extraContext; + List> _rows; + final String _queryName; + final String _schemaName; Boolean _skipReselectRows; public Command(CommandType commandType, String schemaName, String queryName, List> rows) { - super(schemaName, queryName, null); assert null != commandType; + assert null != schemaName && !schemaName.isEmpty(); + assert null != queryName && !queryName.isEmpty(); + _commandType = commandType; - setRows(rows); + _schemaName = schemaName; + _queryName = queryName; + _rows = rows; } public JSONObject getJsonObject() { - JSONObject json = super.getJsonObject(); + JSONObject json = new JSONObject(); + json.put("command", getCommandType().name().toLowerCase()); + json.put("schemaName", getSchemaName()); + json.put("queryName", getQueryName()); + json.put("rows", BaseRowsCommand.rowsToJson(getRows())); + + if (getAuditBehavior() != null) + json.put("auditBehavior", getAuditBehavior()); + + BaseRowsCommand.stringToJson(json, "auditUserComment", getAuditUserComment()); + BaseRowsCommand.stringToJson(json, "containerPath", getContainerPath()); - if (getContainerPath() != null && !getContainerPath().isEmpty()) - json.put("containerPath", getContainerPath()); + if (getExtraContext() != null && !getExtraContext().isEmpty()) + json.put("extraContext", getExtraContext()); if (isSkipReselectRows() != null) json.put("skipReselectRows", isSkipReselectRows()); @@ -184,77 +204,85 @@ public JSONObject getJsonObject() return json; } - public CommandType getCommandType() + public BaseRowsCommand.AuditBehavior getAuditBehavior() { - return _commandType; + return _auditBehavior; } - public String getContainerPath() + public Command setAuditBehavior(BaseRowsCommand.AuditBehavior auditBehavior) { - return _containerPath; + _auditBehavior = auditBehavior; + return this; } - public void setContainerPath(String containerPath) + public String getAuditUserComment() { - _containerPath = containerPath; + return _auditUserComment; } - public Boolean isSkipReselectRows() + public Command setAuditUserComment(String auditUserComment) { - return _skipReselectRows; + _auditUserComment = auditUserComment; + return this; } - public void setSkipReselectRows(Boolean skipReselectRows) + public CommandType getCommandType() { - _skipReselectRows = skipReselectRows; + return _commandType; } - @Override - public String getActionName() + public String getContainerPath() { - throw new UnsupportedOperationException(unsupportedMethodMessage()); + return _containerPath; } - @Override - public String getControllerName() + public Command setContainerPath(String containerPath) { - throw new UnsupportedOperationException(unsupportedMethodMessage()); + _containerPath = containerPath; + return this; } - @Override - public RowsResponse execute(Connection connection, String folderPath) throws IOException, CommandException + public Map getExtraContext() { - throw new UnsupportedOperationException(unsupportedMethodMessage()); + return _extraContext; } - @Override - public double getRequiredVersion() + public Command setExtraContext(Map extraContext) { - throw new UnsupportedOperationException(unsupportedMethodMessage()); + _extraContext = extraContext; + return this; } - @Override - public void setRequiredVersion(double requiredVersion) + public String getQueryName() { - throw new UnsupportedOperationException(unsupportedMethodMessage()); + return _queryName; } - @Override - public Integer getTimeout() + public String getSchemaName() { - throw new UnsupportedOperationException(unsupportedMethodMessage()); + return _schemaName; } - @Override - public void setTimeout(Integer timeout) + public List> getRows() { - throw new UnsupportedOperationException(unsupportedMethodMessage()); + return _rows; } - private String unsupportedMethodMessage() + public Command setRows(List> rows) { - // "Command does not support methodName()." - return Command.class.getSimpleName() + " does not support " + new Throwable().getStackTrace()[1].getMethodName() + "()."; + _rows = rows; + return this; + } + + public Boolean isSkipReselectRows() + { + return _skipReselectRows; + } + + public Command setSkipReselectRows(Boolean skipReselectRows) + { + _skipReselectRows = skipReselectRows; + return this; } } } From 22f1d16cbd05fd9e316bf094f393514af1cada2e Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Thu, 17 Jul 2025 12:58:47 -0700 Subject: [PATCH 20/26] Fixup comment references --- .../labkey/remoteapi/query/BaseRowsCommand.java | 14 +++++++------- .../labkey/remoteapi/query/BaseRowsResponse.java | 2 +- src/org/labkey/remoteapi/query/RowsResponse.java | 4 ++-- .../labkey/remoteapi/query/SaveRowsCommand.java | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/org/labkey/remoteapi/query/BaseRowsCommand.java b/src/org/labkey/remoteapi/query/BaseRowsCommand.java index cd613ae3..7681b01d 100644 --- a/src/org/labkey/remoteapi/query/BaseRowsCommand.java +++ b/src/org/labkey/remoteapi/query/BaseRowsCommand.java @@ -32,14 +32,14 @@ *

    * All three of these subclasses post similar JSON to the server, so this class * does all the common work. The client must supply three things: the schemaName, - * the queryName and an array of 'rows' (i.e. Maps). The rows are added via + * the queryName and an array of 'rows' (i.e., Maps). The rows are added via * the {@link #addRow(Map)} or {@link #setRows(List)} methods. *

    * All data exposed from the LabKey Server is organized into a set of queries * contained in a set of schemas. A schema is simply a group of queries, identified - * by a name (e.g., 'lists' or 'study'). A query is particular table or view within + * by a name (e.g., 'lists' or 'study'). A query is a particular table or view within * that schema (e.g., 'People' or 'Peptides'). Currently, clients may update rows in - * base tables only, and not in joined views. Therefore the query name must be the + * base tables only and not in joined views. Therefore, the query name must be the * name of a table in the schema. *

    * To view the schemas and queries exposed in a given folder, add a Query web part @@ -49,8 +49,8 @@ *

    * Examples: *

    
    - *  // May need to add CONTEXT_PATH for dev instances
    - *  Connection cn = new Connection("http://localhost:8080", user, password);
    + *  ApiKeyCredentialsProvider credentials = new ApiKeyCredentialsProvider("xxx");
    + *  Connection cn = new Connection("http://localhost:8080", credentials);
      *
      *  //Insert Rows Command
      *  InsertRowsCommand cmd = new InsertRowsCommand("lists", "People");
    @@ -60,7 +60,7 @@
      *  row.put("LastName", "Test");
      *
      *  cmd.addRow(row); //can add multiple rows to insert many at once
    - *  SaveRowsResponse resp = cmd.execute(cn, "PROJECT_NAME");
    + *  RowsResponse resp = cmd.execute(cn, "PROJECT_NAME");
      *
      *  //get the newly-assigned primary key value from the first return row
      *  int newKey = resp.getRows().get(0).get("Key");
    @@ -98,7 +98,7 @@ public enum AuditBehavior
         private String _auditUserComment;
     
         /**
    -     * Constructs a new SaveRowsCommand for a given schema, query and action name.
    +     * Constructs a new BaseRowsCommand for a given schema, query and action name.
          * @param schemaName The schema name.
          * @param queryName The query name.
          * @param actionName The action name to call (supplied by the derived class).
    diff --git a/src/org/labkey/remoteapi/query/BaseRowsResponse.java b/src/org/labkey/remoteapi/query/BaseRowsResponse.java
    index 548b890f..8d59468d 100644
    --- a/src/org/labkey/remoteapi/query/BaseRowsResponse.java
    +++ b/src/org/labkey/remoteapi/query/BaseRowsResponse.java
    @@ -35,7 +35,7 @@
     abstract class BaseRowsResponse extends CommandResponse
     {
         /**
    -     * Constructs a new RowsResponse given the specified text and status code.
    +     * Constructs a new BaseRowsResponse given the specified text and status code.
          * @param text The response text.
          * @param statusCode The HTTP status code.
          * @param contentType the Content-Type header value.
    diff --git a/src/org/labkey/remoteapi/query/RowsResponse.java b/src/org/labkey/remoteapi/query/RowsResponse.java
    index 6cb606d3..01881224 100644
    --- a/src/org/labkey/remoteapi/query/RowsResponse.java
    +++ b/src/org/labkey/remoteapi/query/RowsResponse.java
    @@ -19,14 +19,14 @@
     import org.labkey.remoteapi.HasRequiredVersion;
     
     /**
    - * Response object used for commands that derive from SaveRowsCommand.
    + * Response object used for command responses that derive from {@link BaseRowsResponse}.
      * This response object provides helper methods for accessing the important
      * bits of the parsed response data.
      */
     public class RowsResponse extends BaseRowsResponse
     {
         /**
    -     * Constructs a new SaveRowsResponse given the response text and status code
    +     * Constructs a new RowsResponse given the response text and status code
          * @param text The response text.
          * @param statusCode The HTTP status code.
          * @param contentType The Content-Type header value.
    diff --git a/src/org/labkey/remoteapi/query/SaveRowsCommand.java b/src/org/labkey/remoteapi/query/SaveRowsCommand.java
    index bf2ce18b..34fe616c 100644
    --- a/src/org/labkey/remoteapi/query/SaveRowsCommand.java
    +++ b/src/org/labkey/remoteapi/query/SaveRowsCommand.java
    @@ -44,7 +44,7 @@
      *          "start", 5000,
      *          "end", 5500
      *      )));
    - *  updateCmd.setAuditBehavior(SaveRowsCommand.AuditBehavior.DETAILED);
    + *  updateCmd.setAuditBehavior(BaseRowsCommand.AuditBehavior.DETAILED);
      *  updateCmd.setAuditUserComment("Updated promoter region coordinates based on new assembly");
      *  saveCmd.addCommands(updateCmd);
      *
    
    From 15176b896c5455e573f1e8aea661faef7025f164 Mon Sep 17 00:00:00 2001
    From: labkey-nicka 
    Date: Thu, 17 Jul 2025 13:14:07 -0700
    Subject: [PATCH 21/26] Comments
    
    ---
     .../remoteapi/query/SaveRowsCommand.java      | 121 +++++++++++++++++-
     1 file changed, 120 insertions(+), 1 deletion(-)
    
    diff --git a/src/org/labkey/remoteapi/query/SaveRowsCommand.java b/src/org/labkey/remoteapi/query/SaveRowsCommand.java
    index 34fe616c..6d7a720a 100644
    --- a/src/org/labkey/remoteapi/query/SaveRowsCommand.java
    +++ b/src/org/labkey/remoteapi/query/SaveRowsCommand.java
    @@ -69,17 +69,32 @@ public SaveRowsCommand(Command... commands)
             addCommands(commands);
         }
     
    +    /**
    +     * Returns the extra context map containing additional parameters for the save operation.
    +     * This context can be used to pass additional information to the server during the save process.
    +     * @return Map containing extra context parameters, or null if no extra context is set
    +     */
         public Map getExtraContext()
         {
             return _extraContext;
         }
     
    +    /**
    +     * Sets additional context parameters for the save operation.
    +     * @param extraContext Map containing extra parameters to be passed to the server
    +     * @return This SaveRowsCommand instance for method chaining
    +     */
         public SaveRowsCommand setExtraContext(Map extraContext)
         {
             _extraContext = extraContext;
             return this;
         }
     
    +    /**
    +     * Adds one or more Command objects to the set of commands to be executed by this SaveRowsCommand.
    +     * @param commands The commands to add to this SaveRowsCommand.
    +     * @return This SaveRowsCommand instance for method chaining
    +     */
         public SaveRowsCommand addCommands(Command... commands)
         {
             for (Command command : commands)
    @@ -87,31 +102,61 @@ public SaveRowsCommand addCommands(Command... commands)
                 if (command != null)
                     _commands.add(command);
             }
    -
             return this;
         }
     
    +    /**
    +     * Returns the list of Command objects representing the batch operations to be executed.
    +     * Each Command in the list represents a single insert, update, or delete operation.
    +     * @return List of Command objects to be executed
    +     */
         public List getCommands()
         {
             return _commands;
         }
     
    +    /**
    +     * Checks if the operations should be executed in a transaction.
    +     * When true, all operations will be executed atomically - either all succeed or all fail.
    +     * @return Boolean indicating if operations should be transacted, or null for default behavior
    +     */
         public Boolean isTransacted()
         {
             return _transacted;
         }
     
    +    /**
    +     * Sets whether the operations should be executed in a transaction.
    +     * @param transacted When true, all operations will be executed atomically.
    +     *                   When false, operations may partially succeed.
    +     *                   When null, uses server default behavior.
    +     * @return This SaveRowsCommand instance for method chaining
    +     */
         public SaveRowsCommand setTransacted(Boolean transacted)
         {
             _transacted = transacted;
             return this;
         }
     
    +    /**
    +     * Checks if this is a validation-only operation.
    +     * When true, the server will validate the operations without making any actual changes.
    +     * @return Boolean When true, validates operations without making changes.
    +     *                 When false, executes operations normally.
    +     *                 When null, uses server default behavior.
    +     */
         public Boolean isValidateOnly()
         {
             return _validateOnly;
         }
     
    +    /**
    +     * Sets whether this should be a validation-only operation.
    +     * @param validateOnly When true, validates operations without making changes.
    +     *                     When false, executes operations normally.
    +     *                     When null, uses server default behavior.
    +     * @return This SaveRowsCommand instance for method chaining
    +     */
         public SaveRowsCommand setValidateOnly(Boolean validateOnly)
         {
             _validateOnly = validateOnly;
    @@ -156,6 +201,10 @@ public enum CommandType
         // N.B. You may be inclined to have this share implementation with BaseRowsCommand; however, I would caution
         // against doing so. This class does not represent a command like a PostCommand or a GetCommand but rather
         // aligns with the "commands" made on a request to the save rows endpoint.
    +    /**
    +     * Represents a single command operation of a specified type
    +     * (e.g., insert, update, delete) to be executed within a {@link SaveRowsCommand}.
    +     */
         public static class Command
         {
             BaseRowsCommand.AuditBehavior _auditBehavior;
    @@ -204,81 +253,151 @@ public JSONObject getJsonObject()
                 return json;
             }
     
    +        /**
    +         * Gets the audit behavior setting for this command.
    +         * Determines the level of detail in the audit log for this operation.
    +         * @return The current audit behavior setting, or null if using default behavior
    +         */
             public BaseRowsCommand.AuditBehavior getAuditBehavior()
             {
                 return _auditBehavior;
             }
     
    +        /**
    +         * Sets the audit behavior for this command.
    +         * @param auditBehavior The desired audit behavior
    +         * @return This Command instance for method chaining
    +         */
             public Command setAuditBehavior(BaseRowsCommand.AuditBehavior auditBehavior)
             {
                 _auditBehavior = auditBehavior;
                 return this;
             }
     
    +        /**
    +         * Gets the user-provided comment that will be included in the audit log.
    +         * @return The audit comment, or null if none was set
    +         */
             public String getAuditUserComment()
             {
                 return _auditUserComment;
             }
     
    +        /**
    +         * Sets a user comment to be included in the audit log for this command.
    +         * @param auditUserComment The comment to include in the audit log
    +         * @return This Command instance for method chaining
    +         */
             public Command setAuditUserComment(String auditUserComment)
             {
                 _auditUserComment = auditUserComment;
                 return this;
             }
     
    +        /**
    +         * Gets the type of operation this command represents.
    +         * @return The CommandType for this command
    +         */
             public CommandType getCommandType()
             {
                 return _commandType;
             }
     
    +        /**
    +         * Gets the container path where this command should be executed.
    +         * @return The container path, or null if using the default container
    +         */
             public String getContainerPath()
             {
                 return _containerPath;
             }
     
    +        /**
    +         * Sets the container path where this command should be executed.
    +         * @param containerPath The target container path
    +         * @return This Command instance for method chaining
    +         */
             public Command setContainerPath(String containerPath)
             {
                 _containerPath = containerPath;
                 return this;
             }
     
    +        /**
    +         * Gets additional context parameters specific to this command.
    +         * @return Map of extra context parameters, or null if none are set
    +         */
             public Map getExtraContext()
             {
                 return _extraContext;
             }
     
    +        /**
    +         * Sets additional context parameters for this specific command.
    +         * @param extraContext Map of extra parameters to be passed with this command
    +         * @return This Command instance for method chaining
    +         */
             public Command setExtraContext(Map extraContext)
             {
                 _extraContext = extraContext;
                 return this;
             }
     
    +        /**
    +         * Gets the name of the query this command operates on.
    +         * @return The query name
    +         */
             public String getQueryName()
             {
                 return _queryName;
             }
     
    +        /**
    +         * Gets the name of the schema containing the query.
    +         * @return The schema name
    +         */
             public String getSchemaName()
             {
                 return _schemaName;
             }
     
    +        /**
    +         * Gets the list of rows to be processed by this command.
    +         * Each row is represented as a Map of column names to values.
    +         * @return List of rows to be processed
    +         */
             public List> getRows()
             {
                 return _rows;
             }
     
    +        /**
    +         * Sets the list of rows to be processed by this command.
    +         * @param rows List of maps where each map represents a row with column names as keys
    +         * @return This Command instance for method chaining
    +         */
             public Command setRows(List> rows)
             {
                 _rows = rows;
                 return this;
             }
     
    +        /**
    +         * Checks if the command should skip re-selecting rows after the operation.
    +         * @return Boolean indicating whether to skip row re-selection or null for default behavior
    +         */
             public Boolean isSkipReselectRows()
             {
                 return _skipReselectRows;
             }
     
    +        /**
    +         * Sets whether to skip re-selecting rows after the operation completes.
    +         * @param skipReselectRows When true, skips row reselection after the operation
    +         *                         When false, performs row reselection
    +         *                         When null, uses server default behavior
    +         * @return This Command instance for method chaining
    +         */
             public Command setSkipReselectRows(Boolean skipReselectRows)
             {
                 _skipReselectRows = skipReselectRows;
    
    From 4ff606d3376f16104170e054a12a95ff54569f15 Mon Sep 17 00:00:00 2001
    From: labkey-nicka 
    Date: Thu, 17 Jul 2025 16:36:46 -0700
    Subject: [PATCH 22/26] Update CHANGELOG.md
    
    ---
     CHANGELOG.md | 9 +++++++--
     1 file changed, 7 insertions(+), 2 deletions(-)
    
    diff --git a/CHANGELOG.md b/CHANGELOG.md
    index ee9dd98b..e69855a4 100644
    --- a/CHANGELOG.md
    +++ b/CHANGELOG.md
    @@ -1,9 +1,14 @@
     # The LabKey Remote API Library for Java - Change Log
     
    -## version 6.4.0-SNAPSHOT
    +## version 7.0.0-SNAPSHOT
     *Released*: TBD
     * Update Gradle, Gradle Plugins, HttpClient, and JSONObject versions
    -* Add `SaveRowsApiCommand` which wraps the `query-saveRows.api` endpoint
    +* BREAKING CHANGES
    +  * The `SaveRowsCommand` has been updated to be a command wrapper for the `query-saveRows.api`
    +  * The `SaveRowsResponse` now wraps the response from the new `SaveRowsCommand`
    +  * Rename original `SaveRowsResponse` to `RowsResponse`
    +  * Rename original `SaveRowsCommand` to `BaseRowsCommand`
    +  * Rename original `RowsResponse` to `BaseRowsResponse`
     
     ## version 6.3.0
     *Released*: 19 June 2025
    
    From ce5b097243431fd6853d5f236e5e56cf4169aa3f Mon Sep 17 00:00:00 2001
    From: labkey-nicka 
    Date: Fri, 18 Jul 2025 09:37:02 -0700
    Subject: [PATCH 23/26] Copyright
    
    ---
     .../labkey/remoteapi/query/BaseRowsCommand.java   |  2 +-
     .../labkey/remoteapi/query/BaseRowsResponse.java  |  2 +-
     src/org/labkey/remoteapi/query/RowsResponse.java  |  2 +-
     .../labkey/remoteapi/query/SaveRowsCommand.java   | 15 +++++++++++++++
     .../labkey/remoteapi/query/SaveRowsResponse.java  | 15 +++++++++++++++
     5 files changed, 33 insertions(+), 3 deletions(-)
    
    diff --git a/src/org/labkey/remoteapi/query/BaseRowsCommand.java b/src/org/labkey/remoteapi/query/BaseRowsCommand.java
    index 7681b01d..815f5e7e 100644
    --- a/src/org/labkey/remoteapi/query/BaseRowsCommand.java
    +++ b/src/org/labkey/remoteapi/query/BaseRowsCommand.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2008-2016 LabKey Corporation
    + * Copyright (c) 2008-2025 LabKey Corporation
      *
      * Licensed under the Apache License, Version 2.0 (the "License");
      * you may not use this file except in compliance with the License.
    diff --git a/src/org/labkey/remoteapi/query/BaseRowsResponse.java b/src/org/labkey/remoteapi/query/BaseRowsResponse.java
    index 8d59468d..2b1382c5 100644
    --- a/src/org/labkey/remoteapi/query/BaseRowsResponse.java
    +++ b/src/org/labkey/remoteapi/query/BaseRowsResponse.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2008-2016 LabKey Corporation
    + * Copyright (c) 2008-2025 LabKey Corporation
      *
      * Licensed under the Apache License, Version 2.0 (the "License");
      * you may not use this file except in compliance with the License.
    diff --git a/src/org/labkey/remoteapi/query/RowsResponse.java b/src/org/labkey/remoteapi/query/RowsResponse.java
    index 01881224..4048d5be 100644
    --- a/src/org/labkey/remoteapi/query/RowsResponse.java
    +++ b/src/org/labkey/remoteapi/query/RowsResponse.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2008-2009 LabKey Corporation
    + * Copyright (c) 2008-2025 LabKey Corporation
      *
      * Licensed under the Apache License, Version 2.0 (the "License");
      * you may not use this file except in compliance with the License.
    diff --git a/src/org/labkey/remoteapi/query/SaveRowsCommand.java b/src/org/labkey/remoteapi/query/SaveRowsCommand.java
    index 6d7a720a..61edb98e 100644
    --- a/src/org/labkey/remoteapi/query/SaveRowsCommand.java
    +++ b/src/org/labkey/remoteapi/query/SaveRowsCommand.java
    @@ -1,3 +1,18 @@
    +/*
    + * Copyright (c) 2008-2025 LabKey Corporation
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *     http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
     package org.labkey.remoteapi.query;
     
     import org.json.JSONObject;
    diff --git a/src/org/labkey/remoteapi/query/SaveRowsResponse.java b/src/org/labkey/remoteapi/query/SaveRowsResponse.java
    index f85a2c53..51c2c1c2 100644
    --- a/src/org/labkey/remoteapi/query/SaveRowsResponse.java
    +++ b/src/org/labkey/remoteapi/query/SaveRowsResponse.java
    @@ -1,3 +1,18 @@
    +/*
    + * Copyright (c) 2008-2025 LabKey Corporation
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *     http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
     package org.labkey.remoteapi.query;
     
     import org.json.JSONObject;
    
    From 084369d9a7e00370bed24bc144970893ece5f4fc Mon Sep 17 00:00:00 2001
    From: labkey-nicka 
    Date: Fri, 18 Jul 2025 09:37:58 -0700
    Subject: [PATCH 24/26] hyphen
    
    ---
     src/org/labkey/remoteapi/query/BaseRowsResponse.java | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/src/org/labkey/remoteapi/query/BaseRowsResponse.java b/src/org/labkey/remoteapi/query/BaseRowsResponse.java
    index 2b1382c5..f2c68a72 100644
    --- a/src/org/labkey/remoteapi/query/BaseRowsResponse.java
    +++ b/src/org/labkey/remoteapi/query/BaseRowsResponse.java
    @@ -29,7 +29,7 @@
     
     /**
      * Base class for command responses that contain an array of rows
    - * and meta-data about those rows. Primarily, this class converts
    + * and metadata about those rows. Primarily, this class converts
      * date values in the rows array to real Java Date objects.
      */
     abstract class BaseRowsResponse extends CommandResponse
    
    From d3a9ce7cac4b09475c9fc16ad5d302b6c9bf0801 Mon Sep 17 00:00:00 2001
    From: labkey-nicka 
    Date: Fri, 18 Jul 2025 10:30:36 -0700
    Subject: [PATCH 25/26] Prepare for v7.0.0
    
    ---
     CHANGELOG.md | 2 +-
     build.gradle | 2 +-
     2 files changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/CHANGELOG.md b/CHANGELOG.md
    index e69855a4..a4acb365 100644
    --- a/CHANGELOG.md
    +++ b/CHANGELOG.md
    @@ -1,7 +1,7 @@
     # The LabKey Remote API Library for Java - Change Log
     
     ## version 7.0.0-SNAPSHOT
    -*Released*: TBD
    +*Released*: 18 July 2025
     * Update Gradle, Gradle Plugins, HttpClient, and JSONObject versions
     * BREAKING CHANGES
       * The `SaveRowsCommand` has been updated to be a command wrapper for the `query-saveRows.api`
    diff --git a/build.gradle b/build.gradle
    index e5500f03..85bbf4e9 100644
    --- a/build.gradle
    +++ b/build.gradle
    @@ -72,7 +72,7 @@ repositories {
     
     group = "org.labkey.api"
     
    -version = "6.4.0-QuerySaveRowsCommand-SNAPSHOT"
    +version = "7.0.0-SNAPSHOT"
     
     dependencies {
         api "org.json:json:${jsonObjectVersion}"
    
    From f4fbb25cf4bbcb1a36d2ee11288f0ea7ebec11de Mon Sep 17 00:00:00 2001
    From: labkey-nicka 
    Date: Fri, 18 Jul 2025 10:34:17 -0700
    Subject: [PATCH 26/26] Add empty section
    
    ---
     CHANGELOG.md | 6 +++++-
     1 file changed, 5 insertions(+), 1 deletion(-)
    
    diff --git a/CHANGELOG.md b/CHANGELOG.md
    index a4acb365..c5a5884e 100644
    --- a/CHANGELOG.md
    +++ b/CHANGELOG.md
    @@ -1,6 +1,10 @@
     # The LabKey Remote API Library for Java - Change Log
     
    -## version 7.0.0-SNAPSHOT
    +## version 7.1.0-SNAPSHOT
    +*Released*: TBD
    +*
    +
    +## version 7.0.0
     *Released*: 18 July 2025
     * Update Gradle, Gradle Plugins, HttpClient, and JSONObject versions
     * BREAKING CHANGES