Skip to content

Commit 3ea6da8

Browse files
committed
Merge from develop and update source treatment to align with sample treatment for GitHub Issues 1309 and 1282
2 parents faa1700 + dd6832a commit 3ea6da8

47 files changed

Lines changed: 1280 additions & 542 deletions

Some content is hidden

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

api/src/org/labkey/api/ApiModule.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.labkey.api.data.AbstractForeignKey;
4747
import org.labkey.api.data.Aggregate;
4848
import org.labkey.api.data.AtomicDatabaseInteger;
49+
import org.labkey.api.data.BindingTestCase;
4950
import org.labkey.api.data.BooleanFormat;
5051
import org.labkey.api.data.BuilderObjectFactory;
5152
import org.labkey.api.data.CompareType;
@@ -508,6 +509,7 @@ public void registerServlets(ServletContext servletCtx)
508509
ApiKeyManager.TestCase.class,
509510
AppPropsTestCase.class,
510511
AtomicDatabaseInteger.TestCase.class,
512+
BindingTestCase.class,
511513
BlockingCache.BlockingCacheTest.class,
512514
CompareType.TestCase.class,
513515
ContainerDisplayColumn.TestCase.class,

api/src/org/labkey/api/action/BaseApiAction.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import org.labkey.api.view.UnauthorizedException;
4545
import org.labkey.api.view.ViewContext;
4646
import org.springframework.beans.MutablePropertyValues;
47+
import org.springframework.beans.PropertyValues;
4748
import org.springframework.mock.web.MockHttpServletRequest;
4849
import org.springframework.validation.BindException;
4950
import org.springframework.validation.Errors;
@@ -339,7 +340,7 @@ private FormAndErrors<FORM> defaultPopulateForm() throws Exception
339340
{
340341
saveRequestedApiVersion(getViewContext().getRequest(), null);
341342

342-
BindException errors = defaultBindParameters(getCommand(), getPropertyValues());
343+
BindException errors = defaultBindParameters(getPropertyValues());
343344
FORM form = (FORM)errors.getTarget();
344345

345346
return new FormAndErrors<>(form, errors);
@@ -460,6 +461,16 @@ private FormAndErrors<FORM> populateJSONObjectForm() throws Exception
460461
}
461462
saveRequestedApiVersion(getViewContext().getRequest(), jsonObj);
462463

464+
// Records are immutable, so we can't instantiate the form up front and populate it; instead collect the JSON
465+
// properties and construct the record via defaultBindParameters(). Note: record forms can't implement
466+
// ApiJsonForm since that relies on mutating an existing instance.
467+
if (getCommandClass().isRecord())
468+
{
469+
PropertyValues values = null == jsonObj ? new MutablePropertyValues() : new JsonPropertyValues(jsonObj);
470+
BindException errors = defaultBindParameters(values);
471+
return new FormAndErrors<>((FORM)errors.getTarget(), errors);
472+
}
473+
463474
FORM form = getCommand();
464475
BindException errors = populateForm(jsonObj, form);
465476
return new FormAndErrors<>(form, errors);

api/src/org/labkey/api/action/BaseViewAction.java

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.labkey.api.audit.TransactionAuditProvider;
3232
import org.labkey.api.data.Container;
3333
import org.labkey.api.data.ConvertHelper;
34+
import org.labkey.api.data.ObjectFactory;
3435
import org.labkey.api.security.User;
3536
import org.labkey.api.util.HelpTopic;
3637
import org.labkey.api.util.HttpUtil;
@@ -78,6 +79,9 @@
7879
import java.util.List;
7980
import java.util.Map;
8081
import java.util.function.Predicate;
82+
import java.util.stream.Collectors;
83+
84+
import static org.labkey.api.action.SpringActionController.ERROR_MSG;
8185

8286
public abstract class BaseViewAction<FORM> extends PermissionCheckableAction implements Validator, HasPageConfig, ContainerUser
8387
{
@@ -122,43 +126,36 @@ protected BaseViewAction()
122126
setCommandClass(typeBest);
123127
}
124128

125-
126129
protected abstract String getCommandClassMethodName();
127130

128-
129131
protected BaseViewAction(@NotNull Class<? extends FORM> commandClass)
130132
{
131133
setCommandClass(commandClass);
132134
}
133135

134-
135136
public void setProperties(PropertyValues pvs)
136137
{
137138
_pvs = pvs;
138139
}
139140

140-
141141
public void setProperties(Map<?,?> m)
142142
{
143143
_pvs = new MutablePropertyValues(m);
144144
}
145145

146-
147146
/* Doesn't guarantee non-null, non-empty */
148147
public Object getProperty(String key, String d)
149148
{
150149
PropertyValue pv = _pvs.getPropertyValue(key);
151150
return pv == null ? d : pv.getValue();
152151
}
153152

154-
155153
public Object getProperty(Enum<?> key)
156154
{
157155
PropertyValue pv = _pvs.getPropertyValue(key.name());
158156
return pv == null ? null : pv.getValue();
159157
}
160158

161-
162159
public Object getProperty(String key)
163160
{
164161
PropertyValue pv = _pvs.getPropertyValue(key);
@@ -170,7 +167,6 @@ public PropertyValues getPropertyValues()
170167
return _pvs;
171168
}
172169

173-
174170
public static PropertyValues getPropertyValuesForFormBinding(PropertyValues pvs, @NotNull Predicate<String> allowBind)
175171
{
176172
if (null == pvs)
@@ -184,7 +180,6 @@ public static PropertyValues getPropertyValuesForFormBinding(PropertyValues pvs,
184180
return ret;
185181
}
186182

187-
188183
/// Some characters can be mishandled by the browser in multipart/formdata requests (e.g. doublequote and backslask).
189184
/// We support an encoding from fields to avoid these characters, see {@link PageFlowUtil#encodeFormName} and {@link PageFlowUtil#decodeFormName}.
190185
static public class ViewActionParameterPropertyValues extends ServletRequestParameterPropertyValues
@@ -218,7 +213,6 @@ public ModelAndView handleRequest(@NotNull HttpServletRequest request, @NotNull
218213
return handleRequest();
219214
}
220215

221-
222216
private void handleSpecialProperties()
223217
{
224218
_robot = PageFlowUtil.isRobotUserAgent(getViewContext().getRequest().getHeader("User-Agent"));
@@ -260,55 +254,47 @@ private boolean hasStringValue(String propertyName)
260254

261255
public abstract ModelAndView handleRequest() throws Exception;
262256

263-
264257
@Override
265258
public void setPageConfig(PageConfig page)
266259
{
267260
_pageConfig = page;
268261
}
269262

270-
271263
@Override
272264
public Container getContainer()
273265
{
274266
return getViewContext().getContainer();
275267
}
276268

277-
278269
@Override
279270
public User getUser()
280271
{
281272
return getViewContext().getUser();
282273
}
283274

284-
285275
@Override
286276
public PageConfig getPageConfig()
287277
{
288278
return _pageConfig;
289279
}
290280

291-
292281
public void setTitle(String title)
293282
{
294283
assert null != getPageConfig() : "action not initialized property";
295284
getPageConfig().setTitle(title);
296285
}
297286

298-
299287
public void setHelpTopic(String topicName)
300288
{
301289
setHelpTopic(new HelpTopic(topicName));
302290
}
303291

304-
305292
public void setHelpTopic(HelpTopic topic)
306293
{
307294
assert null != getPageConfig() : "action not initialized properly";
308295
getPageConfig().setHelpTopic(topic);
309296
}
310297

311-
312298
protected Object newInstance(Class<?> c)
313299
{
314300
try
@@ -324,7 +310,6 @@ protected Object newInstance(Class<?> c)
324310
}
325311
}
326312

327-
328313
protected @NotNull FORM getCommand(HttpServletRequest request) throws Exception
329314
{
330315
FORM command = (FORM) createCommand();
@@ -335,13 +320,11 @@ protected Object newInstance(Class<?> c)
335320
return command;
336321
}
337322

338-
339323
protected @NotNull FORM getCommand() throws Exception
340324
{
341325
return getCommand(getViewContext().getRequest());
342326
}
343327

344-
345328
//
346329
// PARAMETER BINDING
347330
//
@@ -353,6 +336,45 @@ protected Object newInstance(Class<?> c)
353336
return defaultBindParameters(form, getCommandName(), params);
354337
}
355338

339+
/**
340+
* Bind request parameters to the action's command class, dispatching to record binding when that class is a Java
341+
* record. Records are immutable (no setters), so instead of instantiating the form and populating it via Spring data
342+
* binding we collect the property values into a map and construct the record via its {@link ObjectFactory}. Callers
343+
* that previously invoked {@code defaultBindParameters(getCommand(), params)} directly should prefer this method so
344+
* they transparently gain record support.
345+
*/
346+
public @NotNull BindException defaultBindParameters(PropertyValues params) throws Exception
347+
{
348+
Class<?> commandClass = getCommandClass();
349+
return commandClass.isRecord() ? bindParametersToRecord(commandClass, params, getCommandName()) : defaultBindParameters(getCommand(), params);
350+
}
351+
352+
// Simple binding for Java records: provides binding errors for missing primitive parameters and type conversions
353+
// failures. Currently, no support for array or list parameter types.
354+
public static <R> BindException bindParametersToRecord(Class<R> recordClass, PropertyValues pvs, String commandName)
355+
{
356+
// Note: We don't support record-based forms implementing HasAllowBindParameter since we must populate all
357+
// properties at record construction time and therefore can't invoke allowBindParameter() prior to that.
358+
PropertyValues m = getPropertyValuesForFormBinding(pvs, HasAllowBindParameter.getDefaultPredicate());
359+
ObjectFactory<R> factory = ObjectFactory.Registry.getFactory(recordClass);
360+
Map<String, Object> map = m.stream()
361+
.filter(pv -> pv.getValue() != null)
362+
.collect(Collectors.toMap(PropertyValue::getName, PropertyValue::getValue));
363+
BindException errors;
364+
try
365+
{
366+
R record = factory.fromMap(map);
367+
errors = new NullSafeBindException(record, commandName);
368+
}
369+
catch (IllegalArgumentException | ConversionException e)
370+
{
371+
// Missing primitive parameter or type conversion error. We have no instance to bind to, so report a global
372+
// error with details.
373+
errors = new NullSafeBindException(new Object(), commandName);
374+
errors.reject(ERROR_MSG, e.getMessage());
375+
}
376+
return errors;
377+
}
356378

357379
public static @NotNull BindException defaultBindParameters(Object form, String commandName, PropertyValues params)
358380
{
@@ -398,28 +420,28 @@ protected Object newInstance(Class<?> c)
398420
// Maybe we should propagate exception and return SC_BAD_REQUEST (in ExceptionUtil.handleException())
399421
// most POST handlers check errors.hasErrors(), but not all GET handlers do
400422
BindException errors = new BindException(command, commandName);
401-
errors.reject(SpringActionController.ERROR_MSG, "Error binding property: " + x.getPropertyName());
423+
errors.reject(ERROR_MSG, "Error binding property: " + x.getPropertyName());
402424
return errors;
403425
}
404426
catch (NumberFormatException x)
405427
{
406428
// Malformed array parameter throws this exception, unfortunately. Just reject the request. #21931
407429
BindException errors = new BindException(command, commandName);
408-
errors.reject(SpringActionController.ERROR_MSG, "Error binding array property; invalid array index (" + x.getMessage() + ")");
430+
errors.reject(ERROR_MSG, "Error binding array property; invalid array index (" + x.getMessage() + ")");
409431
return errors;
410432
}
411433
catch (NegativeArraySizeException x)
412434
{
413435
// Another malformed array parameter throws this exception. #23929
414436
BindException errors = new BindException(command, commandName);
415-
errors.reject(SpringActionController.ERROR_MSG, "Error binding array property; negative array size (" + x.getMessage() + ")");
437+
errors.reject(ERROR_MSG, "Error binding array property; negative array size (" + x.getMessage() + ")");
416438
return errors;
417439
}
418440
catch (IllegalArgumentException x)
419441
{
420442
// General bean binding problem. #23929
421443
BindException errors = new BindException(command, commandName);
422-
errors.reject(SpringActionController.ERROR_MSG, "Error binding property; (" + x.getMessage() + ")");
444+
errors.reject(ERROR_MSG, "Error binding property; (" + x.getMessage() + ")");
423445
return errors;
424446
}
425447
}

api/src/org/labkey/api/action/ConfirmAction.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ protected String getCommandClassMethodName()
112112

113113
public BindException bindParameters(PropertyValues m) throws Exception
114114
{
115-
return defaultBindParameters(getCommand(), m);
115+
return defaultBindParameters(m);
116116
}
117117

118118
/**

api/src/org/labkey/api/action/FormApiAction.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ protected ModelAndView getPrintView(FORM form, BindException errors) throws Exce
7979

8080
protected BindException bindParameters(PropertyValues pvs) throws Exception
8181
{
82-
return SimpleViewAction.defaultBindParameters(getCommand(), getCommandName(), pvs);
82+
return defaultBindParameters(pvs);
8383
}
8484

8585
/**

api/src/org/labkey/api/action/FormViewAction.java

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,18 @@
1717
package org.labkey.api.action;
1818

1919
import org.jetbrains.annotations.NotNull;
20-
import org.labkey.api.data.ObjectFactory;
2120
import org.labkey.api.miniprofiler.MiniProfiler;
2221
import org.labkey.api.miniprofiler.Timing;
2322
import org.labkey.api.util.ExceptionUtil;
2423
import org.labkey.api.util.URLHelper;
2524
import org.labkey.api.view.HttpView;
26-
import org.springframework.beans.PropertyValue;
2725
import org.springframework.beans.PropertyValues;
2826
import org.springframework.validation.BindException;
2927
import org.springframework.validation.Errors;
3028
import org.springframework.validation.ObjectError;
3129
import org.springframework.web.servlet.ModelAndView;
3230

3331
import java.util.List;
34-
import java.util.Map;
35-
import java.util.stream.Collectors;
3632

3733
/**
3834
* Is this better than BaseCommandController? Probably not, but it understands TableViewForm.
@@ -149,22 +145,7 @@ protected String getCommandClassMethodName()
149145

150146
public BindException bindParameters(PropertyValues m) throws Exception
151147
{
152-
Class<?> commandClass = getCommandClass();
153-
return commandClass.isRecord() ? defaultBindParametersToRecord(commandClass, m) : defaultBindParameters(getCommand(), m);
154-
}
155-
156-
// Simple binding for Java records: no support for binding errors, arrays, lists, etc.
157-
private <R> BindException defaultBindParametersToRecord(Class<R> recordClass, PropertyValues pvs)
158-
{
159-
// Note: We don't support record-based forms implementing HasAllowBindParameter since we must populate all
160-
// properties at record construction time and therefore can't invoke allowBindParameter() prior to that.
161-
PropertyValues m = getPropertyValuesForFormBinding(pvs, HasAllowBindParameter.getDefaultPredicate());
162-
ObjectFactory<R> factory = ObjectFactory.Registry.getFactory(recordClass);
163-
Map<String, Object> map = m.stream()
164-
.filter(pv -> pv.getValue() != null)
165-
.collect(Collectors.toMap(PropertyValue::getName, PropertyValue::getValue));
166-
R record = factory.fromMap(map);
167-
return new NullSafeBindException(record, "Form");
148+
return defaultBindParameters(m);
168149
}
169150

170151
@Override

api/src/org/labkey/api/action/PermissionCheckableAction.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,6 @@ private void _checkActionPermissions(Set<Role> contextualRoles) throws Unauthori
258258
throw new DeprecatedActionException(actionClass);
259259
}
260260

261-
262261
private void checkPermissionsAndTermsOfUse(Set<Role> contextualRoles, boolean isSendBasic) throws UnauthorizedException
263262
{
264263
checkActionPermissions(contextualRoles);
@@ -267,7 +266,6 @@ private void checkPermissionsAndTermsOfUse(Set<Role> contextualRoles, boolean is
267266
verifyTermsOfUse(isSendBasic);
268267
}
269268

270-
271269
/**
272270
* Check if terms of use are ever required for this request. If so, enumerate all the terms-of-use providers and ask
273271
* each to verify terms are set via its custom mechanism. If a provider's terms are active and the user hasn't yet
@@ -289,7 +287,6 @@ protected void verifyTermsOfUse(boolean isSendBasic) throws RedirectException
289287
provider.verifyTermsOfUse(context, isBasicAuth);
290288
}
291289

292-
293290
/**
294291
* Actions may provide a set of {@link Role}s used during permission checking
295292
* or null if no contextual roles apply.

api/src/org/labkey/api/action/SimpleViewAction.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ protected String getCommandClassMethodName()
114114

115115
public BindException bindParameters(PropertyValues pvs) throws Exception
116116
{
117-
return defaultBindParameters(getCommand(), pvs);
117+
return defaultBindParameters(pvs);
118118
}
119119

120120
public void validate(FORM form, BindException errors)

0 commit comments

Comments
 (0)