Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +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
* 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
Expand Down
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down Expand Up @@ -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'
}
}
}
Expand Down
302 changes: 302 additions & 0 deletions src/org/labkey/remoteapi/query/BaseRowsCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
/*
* Copyright (c) 2008-2016 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* Examples:
* <pre><code>
* ApiKeyCredentialsProvider credentials = new ApiKeyCredentialsProvider("xxx");
* Connection cn = new Connection("http://localhost:8080", credentials);
*
* //Insert Rows Command
* InsertRowsCommand cmd = new InsertRowsCommand("lists", "People");
*
* Map&lt;String, Object&gt; row = new HashMap&lt;String, Object&gt;();
* 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&lt;String, Object&gt;();
* 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&lt;String, Object&gt;();
* row.put("Key", newKey);
* cmdDel.addRow(row);
* resp = cmdDel.execute(cn, "PROJECT_NAME");
* </code></pre>
*/
public abstract class BaseRowsCommand extends PostCommand<RowsResponse>
{
public enum AuditBehavior
{
NONE,
SUMMARY,
DETAILED
}

private String _schemaName;
private String _queryName;
private Map<String, Object> _extraContext;
private List<Map<String, Object>> _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<String, Object> getExtraContext()
{
return _extraContext;
}

/**
* Sets the additional extra context.
* @param extraContext The extra context.
*/
public void setExtraContext(Map<String, Object> 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<Map<String, Object>> 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<Map<String, Object>> 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<String, Object> 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<Map<String, Object>> 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<String, Object> 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<String, Object> 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;
}
}
Loading