3131import org .labkey .api .audit .TransactionAuditProvider ;
3232import org .labkey .api .data .Container ;
3333import org .labkey .api .data .ConvertHelper ;
34+ import org .labkey .api .data .ObjectFactory ;
3435import org .labkey .api .security .User ;
3536import org .labkey .api .util .HelpTopic ;
3637import org .labkey .api .util .HttpUtil ;
7879import java .util .List ;
7980import java .util .Map ;
8081import java .util .function .Predicate ;
82+ import java .util .stream .Collectors ;
83+
84+ import static org .labkey .api .action .SpringActionController .ERROR_MSG ;
8185
8286public 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 }
0 commit comments