diff --git a/CHANGELOG.md b/CHANGELOG.md index d8b28c3b..c5a5884e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,18 @@ # The LabKey Remote API Library for Java - Change Log -## version 6.4.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 + * 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 diff --git a/build.gradle b/build.gradle index 1dc76d54..85bbf4e9 100644 --- a/build.gradle +++ b/build.gradle @@ -72,7 +72,7 @@ repositories { group = "org.labkey.api" -version = "6.4.0-SNAPSHOT" +version = "7.0.0-SNAPSHOT" dependencies { api "org.json:json:${jsonObjectVersion}" @@ -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' } } } diff --git a/src/org/labkey/remoteapi/query/BaseRowsCommand.java b/src/org/labkey/remoteapi/query/BaseRowsCommand.java new file mode 100644 index 00000000..815f5e7e --- /dev/null +++ b/src/org/labkey/remoteapi/query/BaseRowsCommand.java @@ -0,0 +1,302 @@ +/* + * 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.JSONArray; +import org.json.JSONObject; +import org.labkey.remoteapi.PostCommand; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * Base class for commands that make changes to rows exposed from a given + * query in a given schema. Clients should use {@link UpdateRowsCommand}, + * {@link InsertRowsCommand} or {@link DeleteRowsCommand} and not this class directly. + *

+ * 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 {@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 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 + * name of a table in the schema. + *

+ * To view the schemas and queries exposed in a given folder, add a Query web part + * to your portal page and choose the option "Show the list of tables in this schema" + * in the part configuration page. Alternatively, if it is exposed, click on the Query + * tab across the top of the main part of the page. + *

+ * Examples: + *


+ *  ApiKeyCredentialsProvider credentials = new ApiKeyCredentialsProvider("xxx");
+ *  Connection cn = new Connection("http://localhost:8080", credentials);
+ *
+ *  //Insert Rows Command
+ *  InsertRowsCommand cmd = new InsertRowsCommand("lists", "People");
+ *
+ *  Map<String, Object> row = new HashMap<String, Object>();
+ *  row.put("FirstName", "Insert");
+ *  row.put("LastName", "Test");
+ *
+ *  cmd.addRow(row); //can add multiple rows to insert many at once
+ *  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");
+ *
+ *  //Update Rows Command
+ *  UpdateRowsCommand cmdUpd = new UpdateRowsCommand("lists", "People");
+ *  row = new HashMap<String, Object>();
+ *  row.put("Key", newKey);
+ *  row.put("LastName", "Test UPDATED");
+ *  cmdUpd.addRow(row);
+ *  resp = cmdUpd.execute(cn, "PROJECT_NAME");
+ *
+ *  //Delete Rows Command
+ *  DeleteRowsCommand cmdDel = new DeleteRowsCommand("lists", "People");
+ *  row = new HashMap<String, Object>();
+ *  row.put("Key", newKey);
+ *  cmdDel.addRow(row);
+ *  resp = cmdDel.execute(cn, "PROJECT_NAME");
+ * 
+ */ +public abstract class BaseRowsCommand extends PostCommand +{ + public enum AuditBehavior + { + NONE, + SUMMARY, + DETAILED + } + + private String _schemaName; + private String _queryName; + private Map _extraContext; + private List> _rows = new ArrayList<>(); + private AuditBehavior _auditBehavior; + private String _auditUserComment; + + /** + * 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). + */ + protected BaseRowsCommand(String schemaName, String queryName, String actionName) + { + super("query", actionName); + assert null != schemaName; + assert null != queryName; + _schemaName = schemaName; + _queryName = queryName; + } + + /** + * Returns the schema name. + * @return The schema name. + */ + public String getSchemaName() + { + return _schemaName; + } + + /** + * Sets the schema name + * @param schemaName The new schema name. + */ + public void setSchemaName(String schemaName) + { + _schemaName = schemaName; + } + + /** + * Returns the query name + * @return the query name. + */ + public String getQueryName() + { + return _queryName; + } + + /** + * Sets a new query name to update + * @param queryName the query name. + */ + public void setQueryName(String queryName) + { + _queryName = queryName; + } + + /** + * Gets the additional extra context. + * @return the extra context. + */ + public Map getExtraContext() + { + return _extraContext; + } + + /** + * Sets the additional extra context. + * @param extraContext The extra context. + */ + public void setExtraContext(Map extraContext) + { + _extraContext = extraContext; + } + + /** + * Returns the current list of 'rows' (i.e., Maps) that will + * be sent to the server. + * @return The list of rows. + */ + public List> getRows() + { + return _rows; + } + + /** + * Sets the list of 'rows' (i.e., Maps) to be sent to the server. + * @param rows The rows to send + */ + public void setRows(List> rows) + { + _rows = rows; + } + + /** + * Adds a row to the list of rows to be sent to the server. + * @param row The row to add + */ + public void addRow(Map row) + { + _rows.add(row); + } + + public AuditBehavior getAuditBehavior() + { + return _auditBehavior; + } + + /** + * Used to override the audit behavior for the schema/query. + * Note that any audit behavior type that is configured via an XML file for the given schema/query + * will take precedence over this value. See TableInfo.getAuditBehavior() for more details. + * @param auditBehavior Valid values include "NONE", "SUMMARY", and "DETAILED" + */ + public void setAuditBehavior(AuditBehavior auditBehavior) + { + _auditBehavior = auditBehavior; + } + + public String getAuditUserComment() + { + return _auditUserComment; + } + + /** + * Used to provide a comment that will be attached to certain detailed audit log records + * @param auditUserComment The comment to attach to the detailed audit log records + */ + public void setAuditUserComment(String auditUserComment) + { + _auditUserComment = auditUserComment; + } + + /** + * Dynamically builds the JSON object to send based on the current + * schema name, query name and rows list. + * @return The JSON object to send. + */ + @Override + public JSONObject getJsonObject() + { + JSONObject json = new JSONObject(); + json.put("schemaName", getSchemaName()); + json.put("queryName", getQueryName()); + if (getExtraContext() != null) + json.put("extraContext", getExtraContext()); + if (getAuditBehavior() != null) + json.put("auditBehavior", getAuditBehavior()); + + 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 != rows && !rows.isEmpty()) + { + SimpleDateFormat dateFormat = new SimpleDateFormat("d MMM yyyy HH:mm:ss Z"); + for (Map row : rows) + { + if (row instanceof JSONObject jo) + { + jsonRows.put(jo); + } + else + { + 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 dateValue) + value = dateFormat.format(dateValue); + + // JSONObject.wrap allows us to save 'null' values. + jsonRow.put(entry.getKey(), JSONObject.wrap(value)); + } + + jsonRows.put(jsonRow); + } + } + } + + return jsonRows; + } +} diff --git a/src/org/labkey/remoteapi/query/BaseRowsResponse.java b/src/org/labkey/remoteapi/query/BaseRowsResponse.java new file mode 100644 index 00000000..f2c68a72 --- /dev/null +++ b/src/org/labkey/remoteapi/query/BaseRowsResponse.java @@ -0,0 +1,192 @@ +/* + * 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.apache.commons.logging.LogFactory; +import org.json.JSONObject; +import org.labkey.remoteapi.CommandResponse; +import org.labkey.remoteapi.HasRequiredVersion; +import org.labkey.remoteapi.collections.CaseInsensitiveHashMap; + +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * Base class for command responses that contain an array of rows + * 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 +{ + /** + * 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. + * @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 + */ + BaseRowsResponse(String text, int statusCode, String contentType, JSONObject json, HasRequiredVersion hasRequiredVersion) + { + super(text, statusCode, contentType, json); + double requiredVersion = hasRequiredVersion.getRequiredVersion(); + fixupParsedData(requiredVersion); + caseInsensitizeRowMaps(); + } + + /** + * Returns the list of rows from the parsed response data. Note that numbers in the map values will be either of + * type Double or type Long depending on the presence of a decimal point. The most reliable way to work with them + * is to use the Number class. For example: + *

+     * for (Map<String, Object> row : response.getRows())
+     * {
+     *     Number key = (Number)row.get("Key");
+     *     // use Number.intValue(), doubleValue(), longValue(), etc to get various primitive types
+     * }
+     * 
+ * @return The list of rows (each row is a Map), or null if the rows list was not included in the response. + */ + public List> getRows() + { + return getProperty("rows"); + } + + /** + * Fixes up the parsed data. Currently, this converts string-based date literals into real Java Date objects. + */ + private void fixupParsedData(double requiredVersion) + { + if (null == getParsedData()) + return; + + // Because JSON does not have a literal representation for dates we fixup date values in the response rows for + // columns of type date. We also convert numeric values to their proper Java types based on the meta-data type + // name (int vs float). + + //build up the list of date fields + List dateFields = new ArrayList<>(); + List intFields = new ArrayList<>(); + List floatFields = new ArrayList<>(); + List> fields = getProperty("metaData.fields"); + if (null == fields) + return; + + for (Map field : fields) + { + String type = (String)field.get("type"); + if ("date".equalsIgnoreCase(type)) + dateFields.add((String)field.get("name")); + else if ("float".equalsIgnoreCase(type)) + floatFields.add((String)field.get("name")); + else if ("int".equalsIgnoreCase(type)) + intFields.add((String)field.get("name")); + } + + // If no fields to fixup, just return + if (dateFields.isEmpty() && floatFields.isEmpty() && intFields.isEmpty()) + return; + + // If no rows, just return + List> rows = getRows(); + if (null == rows || rows.isEmpty()) + return; + + // The selectRows.api returns dates in a very particular format so that JavaScript can parse them into actual + // date classes. If this format ever changes, we'll need to change the format string used here. + // CONSIDER: use a library like ConvertUtils to avoid this dependency? + DateParser dateFormat = new DateParser(); + boolean expandedFormat = requiredVersion == 9.1; + + for (Map row : rows) + { + for (String field : dateFields) + { + //in expanded format, the value is a Map with several + //possible properties, including "value" which is the column's value + String valueFieldName = expandedFormat ? "value" : field; + Map map = expandedFormat ? (Map)row.get(field) : row; + Object dateString = map.get(valueFieldName); + + if (dateString instanceof String ds) + { + //parse the string into a Java Date and reset the association + try + { + Date date = dateFormat.parse(ds); + if (null != date) + { + map.put(valueFieldName, date); + } + } + catch(ParseException e) + { + //just log it--if it doesn't parse, we can't fix it up + LogFactory.getLog(SelectRowsResponse.class).warn("Failed to parse date '" + + dateString + "': " + e); + } + } //if the value is present and a string + } //for each date field + + //floats + for (String field : floatFields) + { + String valueFieldName = expandedFormat ? "value" : field; + Map map = expandedFormat ? (Map)row.get(field) : row; + Object value = map.get(valueFieldName); + + if (value instanceof Number num) + { + map.put(valueFieldName, num.doubleValue()); + } + } + + //ints + for (String field : intFields) + { + String valueFieldName = expandedFormat ? "value" : field; + Map map = expandedFormat ? (Map)row.get(field) : row; + Object value = map.get(valueFieldName); + + if (value instanceof Number num) + { + map.put(valueFieldName, num.intValue()); + } + } + } //for each row + } //fixupParsedData() + + private void caseInsensitizeRowMaps() + { + //copy the row maps into case-insensitive hash maps + List> ciRows = new ArrayList<>(); + + if (getRows() != null) + { + for (Map row : getRows()) + { + //copy the row map into a case-insensitive hash map + ciRows.add(new CaseInsensitiveHashMap<>(row)); + } + } + + //reset the rows array + getParsedData().put("rows", ciRows); + } +} 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/RowsResponse.java b/src/org/labkey/remoteapi/query/RowsResponse.java index 298846ad..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-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. @@ -15,178 +15,66 @@ */ package org.labkey.remoteapi.query; -import org.apache.commons.logging.LogFactory; import org.json.JSONObject; -import org.labkey.remoteapi.CommandResponse; import org.labkey.remoteapi.HasRequiredVersion; -import org.labkey.remoteapi.collections.CaseInsensitiveHashMap; - -import java.text.ParseException; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Map; /** - * Base class for command responses that contain an array of rows - * and meta-data about those rows. Primarily, this class converts - * date values in the rows array to real Java Date objects. + * 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. */ -abstract class RowsResponse extends CommandResponse +public class RowsResponse extends BaseRowsResponse { /** - * Constructs a new RowsResponse given the specified 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. - * @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 + * @param contentType The Content-Type header value. + * @param json The parsed JSONObject (or null if JSON was not returned) + * @param hasRequiredVersion An object that implements HasRequiredVersion */ - RowsResponse(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); - double requiredVersion = hasRequiredVersion.getRequiredVersion(); - fixupParsedData(requiredVersion); - caseInsensitizeRowMaps(); + super(text, statusCode, contentType, json, hasRequiredVersion); } /** - * Returns the list of rows from the parsed response data. Note that numbers in the map values will be either of - * type Double or type Long depending on the presence of a decimal point. The most reliable way to work with them - * is to use the Number class. For example: - *


-     * for (Map<String, Object> row : response.getRows())
-     * {
-     *     Number key = (Number)row.get("Key");
-     *     // use Number.intValue(), doubleValue(), longValue(), etc to get various primitive types
-     * }
-     * 
- * @return The list of rows (each row is a Map), or null if the rows list was not included in the response. + * Returns the 'rowsAffected' response property. + * @return The number of rows affected by the command, or null if this property + * was not present in the response. */ - public List> getRows() + public Number getRowsAffected() { - return getProperty("rows"); + return getProperty("rowsAffected"); } /** - * Fixes up the parsed data. Currently, this converts string-based date literals into real Java Date objects. + * Returns the 'schemaName' response property. + * @return The schema name affected by the command, or null if this property + * was not present in the response. */ - private void fixupParsedData(double requiredVersion) + public String getSchemaName() { - if (null == getParsedData()) - return; - - // Because JSON does not have a literal representation for dates we fixup date values in the response rows for - // columns of type date. We also convert numeric values to their proper Java types based on the meta-data type - // name (int vs float). - - //build up the list of date fields - List dateFields = new ArrayList<>(); - List intFields = new ArrayList<>(); - List floatFields = new ArrayList<>(); - List> fields = getProperty("metaData.fields"); - if (null == fields) - return; - - for (Map field : fields) - { - String type = (String)field.get("type"); - if ("date".equalsIgnoreCase(type)) - dateFields.add((String)field.get("name")); - else if ("float".equalsIgnoreCase(type)) - floatFields.add((String)field.get("name")); - else if ("int".equalsIgnoreCase(type)) - intFields.add((String)field.get("name")); - } - - // If no fields to fixup, just return - if (dateFields.isEmpty() && floatFields.isEmpty() && intFields.isEmpty()) - return; - - // If no rows, just return - List> rows = getRows(); - if (null == rows || rows.isEmpty()) - return; - - // The selectRows.api returns dates in a very particular format so that JavaScript can parse them into actual - // date classes. If this format ever changes, we'll need to change the format string used here. - // CONSIDER: use a library like ConvertUtils to avoid this dependency? - DateParser dateFormat = new DateParser(); - boolean expandedFormat = requiredVersion == 9.1; - - for (Map row : rows) - { - for (String field : dateFields) - { - //in expanded format, the value is a Map with several - //possible properties, including "value" which is the column's value - String valueFieldName = expandedFormat ? "value" : field; - Map map = expandedFormat ? (Map)row.get(field) : row; - Object dateString = map.get(valueFieldName); - - if (dateString instanceof String ds) - { - //parse the string into a Java Date and reset the association - try - { - Date date = dateFormat.parse(ds); - if (null != date) - { - map.put(valueFieldName, date); - } - } - catch(ParseException e) - { - //just log it--if it doesn't parse, we can't fix it up - LogFactory.getLog(SelectRowsResponse.class).warn("Failed to parse date '" - + dateString + "': " + e); - } - } //if the value is present and a string - } //for each date field - - //floats - for (String field : floatFields) - { - String valueFieldName = expandedFormat ? "value" : field; - Map map = expandedFormat ? (Map)row.get(field) : row; - Object value = map.get(valueFieldName); - - if (value instanceof Number num) - { - map.put(valueFieldName, num.doubleValue()); - } - } - - //ints - for (String field : intFields) - { - String valueFieldName = expandedFormat ? "value" : field; - Map map = expandedFormat ? (Map)row.get(field) : row; - Object value = map.get(valueFieldName); - - if (value instanceof Number num) - { - map.put(valueFieldName, num.intValue()); - } - } - } //for each row - } //fixupParsedData() + return getProperty("schemaName"); + } - private void caseInsensitizeRowMaps() + /** + * Returns the 'queryName' response property. + * @return The query name affected by the command, or null if this property + * was not present in the response. + */ + public String getQueryName() { - //copy the row maps into case-insensitive hash maps - List> ciRows = new ArrayList<>(); - - if (getRows() != null) - { - for (Map row : getRows()) - { - //copy the row map into a case-insensitive hash map - ciRows.add(new CaseInsensitiveHashMap<>(row)); - } - } + return getProperty("queryName"); + } - //reset the rows array - getParsedData().put("rows", ciRows); + /** + * Returns the 'command' response property. + * @return The command executed, or null if this property + * was not present in the response. + */ + public String getCommand() + { + return getProperty("command"); } } diff --git a/src/org/labkey/remoteapi/query/SaveRowsCommand.java b/src/org/labkey/remoteapi/query/SaveRowsCommand.java index e6287765..61edb98e 100644 --- a/src/org/labkey/remoteapi/query/SaveRowsCommand.java +++ b/src/org/labkey/remoteapi/query/SaveRowsCommand.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. @@ -15,272 +15,408 @@ */ package org.labkey.remoteapi.query; -import org.json.JSONArray; import org.json.JSONObject; import org.labkey.remoteapi.PostCommand; -import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Date; import java.util.List; import java.util.Map; /** - * Base class for commands that make changes to rows exposed from a given - * query in a given schema. Clients should use {@link UpdateRowsCommand}, - * {@link InsertRowsCommand} or {@link DeleteRowsCommand} and not this class directly. + * 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 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 {@link #addRow(Map)} or {@link #setRows(List)} methods. + * 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. *

- * 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 - * 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 - * name of a table in the schema. + * 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
  • + *
*

- * To view the schemas and queries exposed in a given folder, add a Query web part - * to your portal page and choose the option "Show the list of tables in this schema" - * in the part configuration page. Alternatively, if it is exposed, click on the Query - * tab across the top of the main part of the page. - *

- * Examples: + * Example usage: *


- *  // May need to add CONTEXT_PATH for dev instances
- *  Connection cn = new Connection("http://localhost:8080", user, password);
- *
- *  //Insert Rows Command
- *  InsertRowsCommand cmd = new InsertRowsCommand("lists", "People");
+ *  ApiKeyCredentialsProvider credentials = new ApiKeyCredentialsProvider("xxx");
+ *  Connection conn = new Connection("http://localhost:8080", credentials);
+ *  SaveRowsApiCommand saveCmd = new SaveRowsApiCommand();
  *
- *  Map<String, Object> row = new HashMap<String, Object>();
- *  row.put("FirstName", "Insert");
- *  row.put("LastName", "Test");
+ *  // 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)
+ *      )));
  *
- *  cmd.addRow(row); //can add multiple rows to insert many at once
- *  SaveRowsResponse resp = cmd.execute(cn, "PROJECT_NAME");
+ *  // 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(BaseRowsCommand.AuditBehavior.DETAILED);
+ *  updateCmd.setAuditUserComment("Updated promoter region coordinates based on new assembly");
+ *  saveCmd.addCommands(updateCmd);
  *
- *  //get the newly-assigned primary key value from the first return row
- *  int newKey = resp.getRows().get(0).get("Key");
+ *  // Delete obsolete annotation
+ *  saveCmd.addCommand(new Command(CommandType.Delete, "genome", "GeneAnnotations",
+ *      List.of(Map.of("name", "Putative enhancer", "geneName", "MYC"))));
  *
- *  //Update Rows Command
- *  UpdateRowsCommand cmdUpd = new UpdateRowsCommand("lists", "People");
- *  row = new HashMap<String, Object>();
- *  row.put("Key", newKey);
- *  row.put("LastName", "Test UPDATED");
- *  cmdUpd.addRow(row);
- *  resp = cmdUpd.execute(cn, "PROJECT_NAME");
- *
- *  //Delete Rows Command
- *  DeleteRowsCommand cmdDel = new DeleteRowsCommand("lists", "People");
- *  row = new HashMap<String, Object>();
- *  row.put("Key", newKey);
- *  cmdDel.addRow(row);
- *  resp = cmdDel.execute(cn, "PROJECT_NAME");
+ *  // Execute all commands in a transaction
+ *  SaveRowsApiResponse response = saveCmd.execute(conn, "GenomeProject");
  * 
*/ -public abstract class SaveRowsCommand extends PostCommand +public class SaveRowsCommand extends PostCommand { - public enum AuditBehavior - { - NONE, - SUMMARY, - DETAILED - } - - private String _schemaName; - private String _queryName; + private final List _commands = new ArrayList<>(); private Map _extraContext; - private List> _rows = new ArrayList<>(); - private AuditBehavior _auditBehavior; - private String _auditUserComment; + private Boolean _transacted; + private Boolean _validateOnly; - /** - * Constructs a new SaveRowsCommand 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). - */ - protected SaveRowsCommand(String schemaName, String queryName, String actionName) + public SaveRowsCommand(Command... commands) { - super("query", actionName); - assert null != schemaName; - assert null != queryName; - _schemaName = schemaName; - _queryName = queryName; + super("query", "saveRows.api"); + addCommands(commands); } /** - * Returns the schema name. - * @return The schema name. + * 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 String getSchemaName() + public Map getExtraContext() { - return _schemaName; + return _extraContext; } /** - * Sets the schema name - * @param schemaName The new schema name. + * 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 void setSchemaName(String schemaName) + public SaveRowsCommand setExtraContext(Map extraContext) { - _schemaName = schemaName; + _extraContext = extraContext; + return this; } /** - * Returns the query name - * @return the query name. + * 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 String getQueryName() + public SaveRowsCommand addCommands(Command... commands) { - return _queryName; + for (Command command : commands) + { + if (command != null) + _commands.add(command); + } + return this; } /** - * Sets a new query name to update - * @param queryName the query name. + * 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 void setQueryName(String queryName) + public List getCommands() { - _queryName = queryName; + return _commands; } /** - * Gets the additional extra context. - * @return the extra context. + * 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 Map getExtraContext() + public Boolean isTransacted() { - return _extraContext; + return _transacted; } /** - * Sets the additional extra context. - * @param extraContext The extra context. + * 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 void setExtraContext(Map extraContext) + public SaveRowsCommand setTransacted(Boolean transacted) { - _extraContext = extraContext; + _transacted = transacted; + return this; } /** - * Returns the current list of 'rows' (i.e., Maps) that will - * be sent to the server. - * @return The list of rows. + * 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 List> getRows() + public Boolean isValidateOnly() { - return _rows; + return _validateOnly; } /** - * Sets the list of 'rows' (i.e., Maps) to be sent to the server. - * @param rows The rows to send + * 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 void setRows(List> rows) + public SaveRowsCommand setValidateOnly(Boolean validateOnly) { - _rows = rows; + _validateOnly = validateOnly; + return this; } - /** - * Adds a row to the list of rows to be sent to the server. - * @param row The row to add - */ - public void addRow(Map row) + @Override + public JSONObject getJsonObject() { - _rows.add(row); - } + JSONObject json = new JSONObject(); - public AuditBehavior getAuditBehavior() - { - return _auditBehavior; - } + List commands = new ArrayList<>(); + for (Command command : getCommands()) + commands.add(command.getJsonObject()); + json.put("commands", commands); - /** - * Used to override the audit behavior for the schema/query. - * Note that any audit behavior type that is configured via an XML file for the given schema/query - * will take precedence over this value. See TableInfo.getAuditBehavior() for more details. - * @param auditBehavior Valid values include "NONE", "SUMMARY", and "DETAILED" - */ - public void setAuditBehavior(AuditBehavior auditBehavior) - { - _auditBehavior = auditBehavior; + 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; } - public String getAuditUserComment() + @Override + protected SaveRowsResponse createResponse(String text, int status, String contentType, JSONObject json) { - return _auditUserComment; + return new SaveRowsResponse(text, status, contentType, json); } - /** - * Used to provide a comment that will be attached to certain detailed audit log records - * @param auditUserComment The comment to attach to the detailed audit log records - */ - public void setAuditUserComment(String auditUserComment) + public enum CommandType { - _auditUserComment = auditUserComment; + Insert, + Update, + Delete } + // 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. /** - * Dynamically builds the JSON object to send based on the current - * schema name, query name and rows list. - * @return The JSON object to send. + * Represents a single command operation of a specified type + * (e.g., insert, update, delete) to be executed within a {@link SaveRowsCommand}. */ - @Override - public JSONObject getJsonObject() + public static class Command { - JSONObject json = new JSONObject(); - json.put("schemaName", getSchemaName()); - json.put("queryName", getQueryName()); - if (getExtraContext() != null) - json.put("extraContext", getExtraContext()); - if (getAuditBehavior() != null) - json.put("auditBehavior", getAuditBehavior()); - if (getAuditUserComment() != null) - json.put("auditUserComment", getAuditUserComment()); - - //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()) + 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) { - SimpleDateFormat fmt = new SimpleDateFormat("d MMM yyyy HH:mm:ss Z"); - for(Map row : getRows()) - { - JSONObject jsonRow; - if (row instanceof JSONObject jo) //optimization - { - jsonRow = jo; - } - else - { - 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); - - // JSONObject.wrap allows us to save 'null' values. - jsonRow.put(entry.getKey(), JSONObject.wrap(value)); - } - } - jsonRows.put(jsonRow); - } + assert null != commandType; + assert null != schemaName && !schemaName.isEmpty(); + assert null != queryName && !queryName.isEmpty(); + + _commandType = commandType; + _schemaName = schemaName; + _queryName = queryName; + _rows = rows; } - json.put("rows", jsonRows); - return json; - } - @Override - protected SaveRowsResponse createResponse(String text, int status, String contentType, JSONObject json) - { - return new SaveRowsResponse(text, status, contentType, json, this); + public JSONObject 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 (getExtraContext() != null && !getExtraContext().isEmpty()) + json.put("extraContext", getExtraContext()); + + if (isSkipReselectRows() != null) + json.put("skipReselectRows", isSkipReselectRows()); + + 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; + return this; + } } } diff --git a/src/org/labkey/remoteapi/query/SaveRowsResponse.java b/src/org/labkey/remoteapi/query/SaveRowsResponse.java index 622e44b0..51c2c1c2 100644 --- a/src/org/labkey/remoteapi/query/SaveRowsResponse.java +++ b/src/org/labkey/remoteapi/query/SaveRowsResponse.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. @@ -16,65 +16,163 @@ package org.labkey.remoteapi.query; import org.json.JSONObject; -import org.labkey.remoteapi.HasRequiredVersion; +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; /** - * Response object used for commands that derive from SaveRowsCommand. - * This response object provides helper methods for accessing the important - * bits of the parsed response data. + * 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
  • + *
  • Number of errors encountered
  • + *
  • Detailed results for each command executed
  • + *
+ *

+ * Example usage: + *


+ *  SaveRowsCommand cmd = new SaveRowsCommand();
+ *  // Add commands to insert/update/delete gene annotations...
+ *  SaveRowsResponse response = cmd.execute(connection, "GenomeProject");
+ *
+ *  if (response.isCommitted())
+ *  {
+ *      for (SaveRowsResponse.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>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");
+ *  }
+ * 
*/ -public class SaveRowsResponse extends RowsResponse +public class SaveRowsResponse extends CommandResponse { - /** - * Constructs a new SaveRowsResponse 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. - * @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) + private final boolean _committed; + private final int _errorCount; + private final List _results; + + public SaveRowsResponse(String text, int statusCode, String contentType, JSONObject json) { - super(text, statusCode, contentType, json, hasRequiredVersion); + 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); } - /** - * Returns the 'rowsAffected' response property. - * @return The number of rows affected by the command, or null if this property - * was not present in the response. - */ - public Number getRowsAffected() + public boolean isCommitted() { - return getProperty("rowsAffected"); + return _committed; } - /** - * Returns the 'schemaName' response property. - * @return The schema name affected by the command, or null if this property - * was not present in the response. - */ - public String getSchemaName() + public int getErrorCount() { - return getProperty("schemaName"); + return _errorCount; } - /** - * Returns the 'queryName' response property. - * @return The query name affected by the command, or null if this property - * was not present in the response. - */ - public String getQueryName() + public List getResults() { - return getProperty("queryName"); + return _results; } - /** - * Returns the 'command' response property. - * @return The command executed, or null if this property - * was not present in the response. - */ - public String getCommand() + public static class Result { - return getProperty("command"); + 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; + } } } 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 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) { 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; } 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);