16
16
/**
17
17
* Initializes the fields of a Record Class from values stored in {@link Preferences}.
18
18
*
19
+ * <p>The Preference values can be updated in the SmartDashboard and/or Shuffleboard UI; updated
20
+ * values will be stored in the flash storage for the robot.
21
+ *
19
22
* <p>Example use:
20
23
*
21
24
* {@snippet :
26
29
* DoubleSupplier powerMultiplier) {
27
30
*
28
31
* public static DriveConfiguration fromPreferences() {
29
- * DriveConfiguration defaultConfig = new DriveConfiguration(
30
- * true, 1337, () -> 3.14);
31
- * return PersistedConfiguration.fromPreferences("Drive", defaultConfig);
32
+ * return PersistedConfiguration.fromPreferences("Drive", DriveConfiguration.class);
32
33
* }
33
34
* }
34
35
* }
38
39
* populated the "Preferences" NetworkTables table. The keys would be:
39
40
*
40
41
* <ul>
41
- * <li>{@code "Drive.DriveConfiguration.addLimelightMeasurement "}
42
- * <li>{@code "Drive.DriveConfiguration. robotWeight"}
43
- * <li>{@code "Drive.DriveConfiguration. powerMultiplier"}
42
+ * <li>{@code "Drive/addVisionMeasurements "}
43
+ * <li>{@code "Drive/ robotWeight"}
44
+ * <li>{@code "Drive/ powerMultiplier"}
44
45
* </ul>
45
46
*
46
- * <p>The default values of for these Preference values will be the values provided to {@link
47
- * PersistedConfiguration#fromPreferences(String, Record)}. The values can be updated in the
48
- * SmartDashboard and/or Shuffleboard UI; updated values will be stored in the flash storage for the
49
- * robot. In the above example, if none of the above preference keys existed, preferences will be
50
- * created with the following values:
47
+ * <p>If no value is stored in Preferences for a key, the default value returned (and initialized in
48
+ * Preferences) would be the default value for the type of the record component. In the above
49
+ * example, if none of the above preference keys existed, preferences will be created with the
50
+ * following values:
51
+ *
52
+ * <ul>
53
+ * <li>{@code "Drive/addVisionMeasurements"}: {@code false}
54
+ * <li>{@code "Drive/robotWeight"}: 0
55
+ * <li>{@code "Drive/powerMultiplier"}: 0.0
56
+ * </ul>
57
+ *
58
+ * <p>The caller could specify different default values by passing an instance of the record class:
59
+ *
60
+ * {@snippet :
61
+ * public final class Drive {
62
+ *
63
+ * public record DriveConfiguration(
64
+ * boolean addVisionMeasurements, long robotWeight,
65
+ * DoubleSupplier powerMultiplier) {
66
+ *
67
+ * public static DriveConfiguration fromPreferences() {
68
+ * DriveConfiguration defaultConfig = new DriveConfiguration(
69
+ * true, 1337, () -> 3.14);
70
+ * return PersistedConfiguration.fromPreferences("Drive", defaultConfig);
71
+ * }
72
+ * }
73
+ * }
74
+ * }
75
+ *
76
+ * <p>In the above example, {@code fromPreferences()} would return a record instance with the values
77
+ * populated the "Preferences" NetworkTables table. The keys and default values would be:
51
78
*
52
79
* <ul>
53
- * <li>{@code "Drive.DriveConfiguration.addLimelightMeasurement"} : {@code true}
54
- * <li>{@code "Drive.DriveConfiguration. robotWeight"}: 1337
55
- * <li>{@code "Drive.DriveConfiguration. powerMultiplier"}: 3.14
80
+ * <li>{@code "Drive/addVisionMeasurements"} (default value : {@code true})
81
+ * <li>{@code "Drive/ robotWeight"} (default value: {@code 1337})
82
+ * <li>{@code "Drive/ powerMultiplier"} (default value: {@code 3.14})
56
83
* </ul>
57
84
*
58
85
* <p>For record classes with many component values of the same type, it is strongly recommended
64
91
* constructor to create record instances, so any parameter validation should be done in a custom
65
92
* constructor; see <a href="https://www.baeldung.com/java-records-custom-constructor">Custom
66
93
* Constructor in Java Records</a> for details.
94
+ *
95
+ * @since 1.3.0
67
96
*/
68
97
public final class PersistedConfiguration {
69
98
// The below package-scope fields are for the self-tests.
@@ -99,11 +128,41 @@ public static <T extends Record> T fromPreferences(String preferenceName, T conf
99
128
return fromPreferences (preferenceName , configWithDefaults , PATH_SEPARATOR );
100
129
}
101
130
131
+ /**
132
+ * Creates a record class instance of the provided type, with fields populated from Preferences.
133
+ *
134
+ * <p>To be stored in preferences, the type of the record components can be any of the following:
135
+ *
136
+ * <ul>
137
+ * <li>{@code boolean} or {@code BooleanSupplier} or {@code Supplier<Boolean>}
138
+ * <li>{@code int} or {@code IntSupplier} or {@code Supplier<Integer>}
139
+ * <li>{@code long} or {@code LongSupplier} or {@code Supplier<Long>}
140
+ * <li>{@code double} or {@code DoubleSupplier} or {@code Supplier<Double>}
141
+ * <li>{@code String} or {@code Supplier<String>}
142
+ * <li>{@code Record} following the above rules
143
+ * </ul>
144
+ *
145
+ * <p>The default values for the preferences will be Java defaults (for example, zero for
146
+ * integers).
147
+ *
148
+ * @param preferenceName Preference subtable to use to get the values.
149
+ * @param recordClass Type of the record instance to populate from preferences.
150
+ * @throws IllegalArgumentException If {@code preferenceName} is empty or contains a {@code '/'}.
151
+ * @throws IllegalStateException If {@code preferenceName} was used for a different record class.
152
+ */
153
+ public static <T extends Record > T fromPreferences (String preferenceName , Class <T > recordClass ) {
154
+ return fromPreferences (preferenceName , recordClass , null , PATH_SEPARATOR );
155
+ }
156
+
102
157
static <T extends Record > T fromPreferences (
103
158
String preferenceName , T configWithDefaults , char pathSeparator ) {
104
159
@ SuppressWarnings ("unchecked" )
105
160
Class <T > recordClass = (Class <T >) configWithDefaults .getClass ();
161
+ return fromPreferences (preferenceName , recordClass , configWithDefaults , pathSeparator );
162
+ }
106
163
164
+ private static <T extends Record > T fromPreferences (
165
+ String preferenceName , Class <T > recordClass , T configWithDefaults , char pathSeparator ) {
107
166
NetworkTableInstance ntInstance = NetworkTableInstance .getDefault ();
108
167
validatePreferenceName (preferenceName );
109
168
verifyNotRegisteredToAnotherClass (ntInstance , preferenceName , recordClass );
@@ -196,7 +255,9 @@ private static <T> T createFromPreferences(
196
255
params [i ] = componentValue ;
197
256
} else {
198
257
// Fetch the value from Preferences
199
- params [i ] = factory .create (component , key , componentValue );
258
+ params [i ] =
259
+ factory .create (
260
+ component , key , componentValue , /* initializePreference= */ needComponentValue );
200
261
}
201
262
i ++;
202
263
}
@@ -207,20 +268,22 @@ private static <T> T createFromPreferences(
207
268
208
269
@ FunctionalInterface
209
270
private interface PreferenceFactory {
210
- Object create (RecordComponent component , String key , Object defaultValue );
271
+ Object create (
272
+ RecordComponent component , String key , Object defaultValue , boolean initializePreference );
211
273
}
212
274
213
275
@ FunctionalInterface
214
276
private interface GenericPreferenceFactory <T > {
215
- T create (RecordComponent component , String key , T defaultValue );
277
+ T create (RecordComponent component , String key , T defaultValue , boolean initializePreference );
216
278
}
217
279
218
280
private static final Map <Type , PreferenceFactory > TYPE_TO_FACTORY = new HashMap <>();
219
281
220
282
@ SuppressWarnings ("unchecked" )
221
283
private static <T > void register (Class <T > type , GenericPreferenceFactory <T > simpleFactory ) {
222
284
PreferenceFactory factory =
223
- (component , key , defaultValue ) -> simpleFactory .create (component , key , (T ) defaultValue );
285
+ (component , key , defaultValue , initializePreference ) ->
286
+ simpleFactory .create (component , key , (T ) defaultValue , initializePreference );
224
287
TYPE_TO_FACTORY .put (type , factory );
225
288
}
226
289
@@ -248,80 +311,111 @@ private static <T> void register(Class<T> type, GenericPreferenceFactory<T> simp
248
311
249
312
/** Gets a boolean value from Preferences for the given component. */
250
313
private static boolean booleanFactory (
251
- RecordComponent component , String key , Boolean defaultValue ) {
252
- if (defaultValue != null ) {
314
+ RecordComponent component , String key , Boolean defaultValue , boolean initialize ) {
315
+ if (initialize ) {
316
+ if (defaultValue == null ) {
317
+ defaultValue = Boolean .FALSE ;
318
+ }
253
319
Preferences .initBoolean (key , defaultValue );
254
320
}
255
321
return Preferences .getBoolean (key , false );
256
322
}
257
323
258
324
/** Gets a BooleanSupplier value from Preferences for the given component. */
259
325
private static BooleanSupplier booleanSupplierFactory (
260
- RecordComponent component , String key , BooleanSupplier defaultValueSupplier ) {
261
- if (defaultValueSupplier != null ) {
262
- boolean defaultValue = defaultValueSupplier .getAsBoolean ();
326
+ RecordComponent component ,
327
+ String key ,
328
+ BooleanSupplier defaultValueSupplier ,
329
+ boolean initialize ) {
330
+ if (initialize ) {
331
+ boolean defaultValue = false ;
332
+ if (defaultValueSupplier != null ) {
333
+ defaultValue = defaultValueSupplier .getAsBoolean ();
334
+ }
263
335
Preferences .initBoolean (key , defaultValue );
264
336
}
265
337
return () -> Preferences .getBoolean (key , false );
266
338
}
267
339
268
340
/** Gets an int value from Preferences for the given component. */
269
- private static int intFactory (RecordComponent component , String key , Integer defaultValue ) {
270
- if (defaultValue != null ) {
341
+ private static int intFactory (
342
+ RecordComponent component , String key , Integer defaultValue , boolean initialize ) {
343
+ if (initialize ) {
344
+ if (defaultValue == null ) {
345
+ defaultValue = 0 ;
346
+ }
271
347
Preferences .initInt (key , defaultValue );
272
348
}
273
349
return Preferences .getInt (key , 0 );
274
350
}
275
351
276
352
/** Gets a IntSupplier value from Preferences for the given component. */
277
353
private static IntSupplier intSupplierFactory (
278
- RecordComponent component , String key , IntSupplier defaultValueSupplier ) {
279
- if (defaultValueSupplier != null ) {
280
- int defaultValue = defaultValueSupplier .getAsInt ();
354
+ RecordComponent component , String key , IntSupplier defaultValueSupplier , boolean initialize ) {
355
+ if (initialize ) {
356
+ int defaultValue = defaultValueSupplier != null ? defaultValueSupplier .getAsInt () : 0 ;
281
357
Preferences .initInt (key , defaultValue );
282
358
}
283
359
return () -> Preferences .getInt (key , 0 );
284
360
}
285
361
286
362
/** Gets a long value from Preferences for the given component. */
287
- private static long longFactory (RecordComponent component , String key , Long defaultValue ) {
288
- if (defaultValue != null ) {
363
+ private static long longFactory (
364
+ RecordComponent component , String key , Long defaultValue , boolean initialize ) {
365
+ if (initialize ) {
366
+ if (defaultValue == null ) {
367
+ defaultValue = 0L ;
368
+ }
289
369
Preferences .initLong (key , defaultValue );
290
370
}
291
371
return Preferences .getLong (key , 0 );
292
372
}
293
373
294
374
/** Gets a LongSupplier value from Preferences for the given component. */
295
375
private static LongSupplier longSupplierFactory (
296
- RecordComponent component , String key , LongSupplier defaultValueSupplier ) {
297
- if (defaultValueSupplier != null ) {
298
- long defaultValue = defaultValueSupplier .getAsLong ();
376
+ RecordComponent component ,
377
+ String key ,
378
+ LongSupplier defaultValueSupplier ,
379
+ boolean initialize ) {
380
+ if (initialize ) {
381
+ long defaultValue = defaultValueSupplier != null ? defaultValueSupplier .getAsLong () : 0 ;
299
382
Preferences .initLong (key , defaultValue );
300
383
}
301
384
return () -> Preferences .getLong (key , 0 );
302
385
}
303
386
304
387
/** Gets a double value from Preferences for the given component. */
305
- private static double doubleFactory (RecordComponent component , String key , Double defaultValue ) {
306
- if (defaultValue != null ) {
388
+ private static double doubleFactory (
389
+ RecordComponent component , String key , Double defaultValue , boolean initialize ) {
390
+ if (initialize ) {
391
+ if (defaultValue == null ) {
392
+ defaultValue = 0.0 ;
393
+ }
307
394
Preferences .initDouble (key , defaultValue );
308
395
}
309
396
return Preferences .getDouble (key , 0 );
310
397
}
311
398
312
399
/** Gets a DoubleSupplier value from Preferences for the given component. */
313
400
private static DoubleSupplier doubleSupplierFactory (
314
- RecordComponent component , String key , DoubleSupplier defaultValueSupplier ) {
315
- if (defaultValueSupplier != null ) {
316
- double defaultValue = defaultValueSupplier .getAsDouble ();
401
+ RecordComponent component ,
402
+ String key ,
403
+ DoubleSupplier defaultValueSupplier ,
404
+ boolean initialize ) {
405
+ if (initialize ) {
406
+ double defaultValue = defaultValueSupplier != null ? defaultValueSupplier .getAsDouble () : 0 ;
317
407
Preferences .initDouble (key , defaultValue );
318
408
}
319
409
return () -> Preferences .getDouble (key , 0 );
320
410
}
321
411
322
412
/** Gets a String value from Preferences for the given component. */
323
- private static String stringFactory (RecordComponent component , String key , String defaultValue ) {
324
- if (defaultValue != null ) {
413
+ private static String stringFactory (
414
+ RecordComponent component , String key , String defaultValue , boolean initialize ) {
415
+ if (initialize ) {
416
+ if (defaultValue == null ) {
417
+ defaultValue = "" ;
418
+ }
325
419
Preferences .initString (key , defaultValue );
326
420
}
327
421
return Preferences .getString (key , "" );
@@ -332,7 +426,7 @@ private static String stringFactory(RecordComponent component, String key, Strin
332
426
* boolean and float values.
333
427
*/
334
428
private static Supplier <?> supplierFactory (
335
- RecordComponent component , String key , Supplier <?> defaultValueSupplier ) {
429
+ RecordComponent component , String key , Supplier <?> defaultValueSupplier , boolean initialize ) {
336
430
Type supplierType =
337
431
((ParameterizedType ) component .getGenericType ()).getActualTypeArguments ()[0 ];
338
432
Type registeredType = SUPPLIER_TYPE_TO_REGISTERED_TYPE .get (supplierType );
@@ -344,23 +438,47 @@ private static Supplier<?> supplierFactory(
344
438
}
345
439
PreferenceFactory factory = TYPE_TO_FACTORY .get (registeredType );
346
440
347
- if (defaultValueSupplier != null ) {
348
- Object defaultValue = defaultValueSupplier .get ();
349
- if (defaultValue == null ) {
350
- warn ("Cannot store '%s' in Preferences; default value is null" , component .getName ());
351
- return defaultValueSupplier ;
441
+ if (initialize ) {
442
+ Object defaultValue = null ;
443
+ if (defaultValueSupplier != null ) {
444
+ defaultValue = defaultValueSupplier .get ();
445
+ if (defaultValue == null ) {
446
+ warn ("Cannot store '%s' in Preferences; default value is null" , component .getName ());
447
+ return defaultValueSupplier ;
448
+ }
352
449
}
353
- factory .create (component , key , defaultValue ); // Call Preferences.init{String,Double,etc}()
450
+ factory .create (
451
+ component , key , defaultValue , true ); // Call Preferences.init{String,Double,etc}()
354
452
}
355
453
356
- return () -> factory .create (component , key , null );
454
+ return () -> factory .create (component , key , null , false );
357
455
}
358
456
359
457
private static void warn (String format , Object ... args ) {
360
458
String message = String .format ("WARNING: " + format , args );
361
459
errorReporter .accept (message );
362
460
}
363
461
462
+ private static <T > T createInstanceWithJavaDefaults (Class <T > clazz )
463
+ throws ReflectiveOperationException {
464
+ if (clazz .isPrimitive ()) {
465
+ return clazz .cast (Array .get (Array .newInstance (clazz , 1 ), 0 ));
466
+ }
467
+ if (Record .class .isAssignableFrom (clazz )) {
468
+ var components = clazz .getRecordComponents ();
469
+ Object [] params = new Object [components .length ];
470
+ Class <?>[] types = new Class [components .length ];
471
+ int i = 0 ;
472
+ for (RecordComponent component : components ) {
473
+ Class <?> type = component .getType ();
474
+ types [i ] = type ;
475
+ params [i ] = createInstanceWithJavaDefaults (type );
476
+ }
477
+ return clazz .getDeclaredConstructor (types ).newInstance (params );
478
+ }
479
+ throw new IllegalArgumentException ("Unsupported type: " + clazz );
480
+ }
481
+
364
482
private PersistedConfiguration () {
365
483
throw new AssertionError ("Not instantiable" );
366
484
}
0 commit comments