Skip to content

Commit e823690

Browse files
authored
Issue 53463: Add query-saveRows.api endpoint wrapper to Java client API (#83)
1 parent eaee99c commit e823690

14 files changed

+1050
-404
lines changed

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
# The LabKey Remote API Library for Java - Change Log
22

3-
## version 6.4.0-SNAPSHOT
3+
## version 7.1.0-SNAPSHOT
44
*Released*: TBD
5+
*
6+
7+
## version 7.0.0
8+
*Released*: 18 July 2025
59
* Update Gradle, Gradle Plugins, HttpClient, and JSONObject versions
10+
* BREAKING CHANGES
11+
* The `SaveRowsCommand` has been updated to be a command wrapper for the `query-saveRows.api`
12+
* The `SaveRowsResponse` now wraps the response from the new `SaveRowsCommand`
13+
* Rename original `SaveRowsResponse` to `RowsResponse`
14+
* Rename original `SaveRowsCommand` to `BaseRowsCommand`
15+
* Rename original `RowsResponse` to `BaseRowsResponse`
616

717
## version 6.3.0
818
*Released*: 19 June 2025

build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ repositories {
7272

7373
group = "org.labkey.api"
7474

75-
version = "6.4.0-SNAPSHOT"
75+
version = "7.0.0-SNAPSHOT"
7676

7777
dependencies {
7878
api "org.json:json:${jsonObjectVersion}"
@@ -191,7 +191,7 @@ project.publishing {
191191
scm {
192192
connection = 'scm:git:https://github.com/LabKey/labkey-api-java'
193193
developerConnection = 'scm:git:https://github.com/LabKey/labkey-api-java'
194-
url = 'scm:git:https://github.com/LabKey/labkey-api-java/labkey-client-api'
194+
url = 'scm:git:https://github.com/LabKey/labkey-api-java'
195195
}
196196
}
197197
}
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
/*
2+
* Copyright (c) 2008-2025 LabKey Corporation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.labkey.remoteapi.query;
17+
18+
import org.json.JSONArray;
19+
import org.json.JSONObject;
20+
import org.labkey.remoteapi.PostCommand;
21+
22+
import java.text.SimpleDateFormat;
23+
import java.util.ArrayList;
24+
import java.util.Date;
25+
import java.util.List;
26+
import java.util.Map;
27+
28+
/**
29+
* Base class for commands that make changes to rows exposed from a given
30+
* query in a given schema. Clients should use {@link UpdateRowsCommand},
31+
* {@link InsertRowsCommand} or {@link DeleteRowsCommand} and not this class directly.
32+
* <p>
33+
* All three of these subclasses post similar JSON to the server, so this class
34+
* does all the common work. The client must supply three things: the schemaName,
35+
* the queryName and an array of 'rows' (i.e., Maps). The rows are added via
36+
* the {@link #addRow(Map)} or {@link #setRows(List)} methods.
37+
* <p>
38+
* All data exposed from the LabKey Server is organized into a set of queries
39+
* contained in a set of schemas. A schema is simply a group of queries, identified
40+
* by a name (e.g., 'lists' or 'study'). A query is a particular table or view within
41+
* that schema (e.g., 'People' or 'Peptides'). Currently, clients may update rows in
42+
* base tables only and not in joined views. Therefore, the query name must be the
43+
* name of a table in the schema.
44+
* <p>
45+
* To view the schemas and queries exposed in a given folder, add a Query web part
46+
* to your portal page and choose the option "Show the list of tables in this schema"
47+
* in the part configuration page. Alternatively, if it is exposed, click on the Query
48+
* tab across the top of the main part of the page.
49+
* <p>
50+
* Examples:
51+
* <pre><code>
52+
* ApiKeyCredentialsProvider credentials = new ApiKeyCredentialsProvider("xxx");
53+
* Connection cn = new Connection("http://localhost:8080", credentials);
54+
*
55+
* //Insert Rows Command
56+
* InsertRowsCommand cmd = new InsertRowsCommand("lists", "People");
57+
*
58+
* Map&lt;String, Object&gt; row = new HashMap&lt;String, Object&gt;();
59+
* row.put("FirstName", "Insert");
60+
* row.put("LastName", "Test");
61+
*
62+
* cmd.addRow(row); //can add multiple rows to insert many at once
63+
* RowsResponse resp = cmd.execute(cn, "PROJECT_NAME");
64+
*
65+
* //get the newly-assigned primary key value from the first return row
66+
* int newKey = resp.getRows().get(0).get("Key");
67+
*
68+
* //Update Rows Command
69+
* UpdateRowsCommand cmdUpd = new UpdateRowsCommand("lists", "People");
70+
* row = new HashMap&lt;String, Object&gt;();
71+
* row.put("Key", newKey);
72+
* row.put("LastName", "Test UPDATED");
73+
* cmdUpd.addRow(row);
74+
* resp = cmdUpd.execute(cn, "PROJECT_NAME");
75+
*
76+
* //Delete Rows Command
77+
* DeleteRowsCommand cmdDel = new DeleteRowsCommand("lists", "People");
78+
* row = new HashMap&lt;String, Object&gt;();
79+
* row.put("Key", newKey);
80+
* cmdDel.addRow(row);
81+
* resp = cmdDel.execute(cn, "PROJECT_NAME");
82+
* </code></pre>
83+
*/
84+
public abstract class BaseRowsCommand extends PostCommand<RowsResponse>
85+
{
86+
public enum AuditBehavior
87+
{
88+
NONE,
89+
SUMMARY,
90+
DETAILED
91+
}
92+
93+
private String _schemaName;
94+
private String _queryName;
95+
private Map<String, Object> _extraContext;
96+
private List<Map<String, Object>> _rows = new ArrayList<>();
97+
private AuditBehavior _auditBehavior;
98+
private String _auditUserComment;
99+
100+
/**
101+
* Constructs a new BaseRowsCommand for a given schema, query and action name.
102+
* @param schemaName The schema name.
103+
* @param queryName The query name.
104+
* @param actionName The action name to call (supplied by the derived class).
105+
*/
106+
protected BaseRowsCommand(String schemaName, String queryName, String actionName)
107+
{
108+
super("query", actionName);
109+
assert null != schemaName;
110+
assert null != queryName;
111+
_schemaName = schemaName;
112+
_queryName = queryName;
113+
}
114+
115+
/**
116+
* Returns the schema name.
117+
* @return The schema name.
118+
*/
119+
public String getSchemaName()
120+
{
121+
return _schemaName;
122+
}
123+
124+
/**
125+
* Sets the schema name
126+
* @param schemaName The new schema name.
127+
*/
128+
public void setSchemaName(String schemaName)
129+
{
130+
_schemaName = schemaName;
131+
}
132+
133+
/**
134+
* Returns the query name
135+
* @return the query name.
136+
*/
137+
public String getQueryName()
138+
{
139+
return _queryName;
140+
}
141+
142+
/**
143+
* Sets a new query name to update
144+
* @param queryName the query name.
145+
*/
146+
public void setQueryName(String queryName)
147+
{
148+
_queryName = queryName;
149+
}
150+
151+
/**
152+
* Gets the additional extra context.
153+
* @return the extra context.
154+
*/
155+
public Map<String, Object> getExtraContext()
156+
{
157+
return _extraContext;
158+
}
159+
160+
/**
161+
* Sets the additional extra context.
162+
* @param extraContext The extra context.
163+
*/
164+
public void setExtraContext(Map<String, Object> extraContext)
165+
{
166+
_extraContext = extraContext;
167+
}
168+
169+
/**
170+
* Returns the current list of 'rows' (i.e., Maps) that will
171+
* be sent to the server.
172+
* @return The list of rows.
173+
*/
174+
public List<Map<String, Object>> getRows()
175+
{
176+
return _rows;
177+
}
178+
179+
/**
180+
* Sets the list of 'rows' (i.e., Maps) to be sent to the server.
181+
* @param rows The rows to send
182+
*/
183+
public void setRows(List<Map<String, Object>> rows)
184+
{
185+
_rows = rows;
186+
}
187+
188+
/**
189+
* Adds a row to the list of rows to be sent to the server.
190+
* @param row The row to add
191+
*/
192+
public void addRow(Map<String, Object> row)
193+
{
194+
_rows.add(row);
195+
}
196+
197+
public AuditBehavior getAuditBehavior()
198+
{
199+
return _auditBehavior;
200+
}
201+
202+
/**
203+
* Used to override the audit behavior for the schema/query.
204+
* Note that any audit behavior type that is configured via an XML file for the given schema/query
205+
* will take precedence over this value. See TableInfo.getAuditBehavior() for more details.
206+
* @param auditBehavior Valid values include "NONE", "SUMMARY", and "DETAILED"
207+
*/
208+
public void setAuditBehavior(AuditBehavior auditBehavior)
209+
{
210+
_auditBehavior = auditBehavior;
211+
}
212+
213+
public String getAuditUserComment()
214+
{
215+
return _auditUserComment;
216+
}
217+
218+
/**
219+
* Used to provide a comment that will be attached to certain detailed audit log records
220+
* @param auditUserComment The comment to attach to the detailed audit log records
221+
*/
222+
public void setAuditUserComment(String auditUserComment)
223+
{
224+
_auditUserComment = auditUserComment;
225+
}
226+
227+
/**
228+
* Dynamically builds the JSON object to send based on the current
229+
* schema name, query name and rows list.
230+
* @return The JSON object to send.
231+
*/
232+
@Override
233+
public JSONObject getJsonObject()
234+
{
235+
JSONObject json = new JSONObject();
236+
json.put("schemaName", getSchemaName());
237+
json.put("queryName", getQueryName());
238+
if (getExtraContext() != null)
239+
json.put("extraContext", getExtraContext());
240+
if (getAuditBehavior() != null)
241+
json.put("auditBehavior", getAuditBehavior());
242+
243+
stringToJson(json, "auditUserComment", getAuditUserComment());
244+
json.put("rows", rowsToJson(getRows()));
245+
246+
return json;
247+
}
248+
249+
@Override
250+
protected RowsResponse createResponse(String text, int status, String contentType, JSONObject json)
251+
{
252+
return new RowsResponse(text, status, contentType, json, this);
253+
}
254+
255+
static void stringToJson(JSONObject json, String prop, String value)
256+
{
257+
if (value != null && !value.isEmpty())
258+
{
259+
String trimmed = value.trim();
260+
if (!trimmed.isEmpty())
261+
json.put(prop, trimmed);
262+
}
263+
}
264+
265+
static JSONArray rowsToJson(List<Map<String, Object>> rows)
266+
{
267+
//unfortunately, JSON simple is so simple that it doesn't
268+
//encode maps into JSON objects on the fly,
269+
//nor dates into property JSON format
270+
JSONArray jsonRows = new JSONArray();
271+
if (null != rows && !rows.isEmpty())
272+
{
273+
SimpleDateFormat dateFormat = new SimpleDateFormat("d MMM yyyy HH:mm:ss Z");
274+
for (Map<String, Object> row : rows)
275+
{
276+
if (row instanceof JSONObject jo)
277+
{
278+
jsonRows.put(jo);
279+
}
280+
else
281+
{
282+
JSONObject jsonRow = new JSONObject();
283+
// Row map entries must be scalar values (no embedded maps or arrays)
284+
for (Map.Entry<String, Object> entry : row.entrySet())
285+
{
286+
Object value = entry.getValue();
287+
288+
if (value instanceof Date dateValue)
289+
value = dateFormat.format(dateValue);
290+
291+
// JSONObject.wrap allows us to save 'null' values.
292+
jsonRow.put(entry.getKey(), JSONObject.wrap(value));
293+
}
294+
295+
jsonRows.put(jsonRow);
296+
}
297+
}
298+
}
299+
300+
return jsonRows;
301+
}
302+
}

0 commit comments

Comments
 (0)