Skip to content

Commit a99793f

Browse files
committed
Add PersistedConfiguration.fromPreferences(String, Class<? extends Record>)
1 parent be4a744 commit a99793f

File tree

2 files changed

+325
-55
lines changed

2 files changed

+325
-55
lines changed

lib/src/main/java/com/team2813/lib2813/preferences/PersistedConfiguration.java

+166-48
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
/**
1717
* Initializes the fields of a Record Class from values stored in {@link Preferences}.
1818
*
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+
*
1922
* <p>Example use:
2023
*
2124
* {@snippet :
@@ -26,9 +29,7 @@
2629
* DoubleSupplier powerMultiplier) {
2730
*
2831
* 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);
3233
* }
3334
* }
3435
* }
@@ -38,21 +39,47 @@
3839
* populated the "Preferences" NetworkTables table. The keys would be:
3940
*
4041
* <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"}
4445
* </ul>
4546
*
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:
5178
*
5279
* <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})
5683
* </ul>
5784
*
5885
* <p>For record classes with many component values of the same type, it is strongly recommended
@@ -64,6 +91,8 @@
6491
* constructor to create record instances, so any parameter validation should be done in a custom
6592
* constructor; see <a href="https://www.baeldung.com/java-records-custom-constructor">Custom
6693
* Constructor in Java Records</a> for details.
94+
*
95+
* @since 1.3.0
6796
*/
6897
public final class PersistedConfiguration {
6998
// 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
99128
return fromPreferences(preferenceName, configWithDefaults, PATH_SEPARATOR);
100129
}
101130

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+
102157
static <T extends Record> T fromPreferences(
103158
String preferenceName, T configWithDefaults, char pathSeparator) {
104159
@SuppressWarnings("unchecked")
105160
Class<T> recordClass = (Class<T>) configWithDefaults.getClass();
161+
return fromPreferences(preferenceName, recordClass, configWithDefaults, pathSeparator);
162+
}
106163

164+
private static <T extends Record> T fromPreferences(
165+
String preferenceName, Class<T> recordClass, T configWithDefaults, char pathSeparator) {
107166
NetworkTableInstance ntInstance = NetworkTableInstance.getDefault();
108167
validatePreferenceName(preferenceName);
109168
verifyNotRegisteredToAnotherClass(ntInstance, preferenceName, recordClass);
@@ -196,7 +255,9 @@ private static <T> T createFromPreferences(
196255
params[i] = componentValue;
197256
} else {
198257
// 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);
200261
}
201262
i++;
202263
}
@@ -207,20 +268,22 @@ private static <T> T createFromPreferences(
207268

208269
@FunctionalInterface
209270
private interface PreferenceFactory {
210-
Object create(RecordComponent component, String key, Object defaultValue);
271+
Object create(
272+
RecordComponent component, String key, Object defaultValue, boolean initializePreference);
211273
}
212274

213275
@FunctionalInterface
214276
private interface GenericPreferenceFactory<T> {
215-
T create(RecordComponent component, String key, T defaultValue);
277+
T create(RecordComponent component, String key, T defaultValue, boolean initializePreference);
216278
}
217279

218280
private static final Map<Type, PreferenceFactory> TYPE_TO_FACTORY = new HashMap<>();
219281

220282
@SuppressWarnings("unchecked")
221283
private static <T> void register(Class<T> type, GenericPreferenceFactory<T> simpleFactory) {
222284
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);
224287
TYPE_TO_FACTORY.put(type, factory);
225288
}
226289

@@ -248,80 +311,111 @@ private static <T> void register(Class<T> type, GenericPreferenceFactory<T> simp
248311

249312
/** Gets a boolean value from Preferences for the given component. */
250313
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+
}
253319
Preferences.initBoolean(key, defaultValue);
254320
}
255321
return Preferences.getBoolean(key, false);
256322
}
257323

258324
/** Gets a BooleanSupplier value from Preferences for the given component. */
259325
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+
}
263335
Preferences.initBoolean(key, defaultValue);
264336
}
265337
return () -> Preferences.getBoolean(key, false);
266338
}
267339

268340
/** 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+
}
271347
Preferences.initInt(key, defaultValue);
272348
}
273349
return Preferences.getInt(key, 0);
274350
}
275351

276352
/** Gets a IntSupplier value from Preferences for the given component. */
277353
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;
281357
Preferences.initInt(key, defaultValue);
282358
}
283359
return () -> Preferences.getInt(key, 0);
284360
}
285361

286362
/** 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+
}
289369
Preferences.initLong(key, defaultValue);
290370
}
291371
return Preferences.getLong(key, 0);
292372
}
293373

294374
/** Gets a LongSupplier value from Preferences for the given component. */
295375
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;
299382
Preferences.initLong(key, defaultValue);
300383
}
301384
return () -> Preferences.getLong(key, 0);
302385
}
303386

304387
/** 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+
}
307394
Preferences.initDouble(key, defaultValue);
308395
}
309396
return Preferences.getDouble(key, 0);
310397
}
311398

312399
/** Gets a DoubleSupplier value from Preferences for the given component. */
313400
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;
317407
Preferences.initDouble(key, defaultValue);
318408
}
319409
return () -> Preferences.getDouble(key, 0);
320410
}
321411

322412
/** 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+
}
325419
Preferences.initString(key, defaultValue);
326420
}
327421
return Preferences.getString(key, "");
@@ -332,7 +426,7 @@ private static String stringFactory(RecordComponent component, String key, Strin
332426
* boolean and float values.
333427
*/
334428
private static Supplier<?> supplierFactory(
335-
RecordComponent component, String key, Supplier<?> defaultValueSupplier) {
429+
RecordComponent component, String key, Supplier<?> defaultValueSupplier, boolean initialize) {
336430
Type supplierType =
337431
((ParameterizedType) component.getGenericType()).getActualTypeArguments()[0];
338432
Type registeredType = SUPPLIER_TYPE_TO_REGISTERED_TYPE.get(supplierType);
@@ -344,23 +438,47 @@ private static Supplier<?> supplierFactory(
344438
}
345439
PreferenceFactory factory = TYPE_TO_FACTORY.get(registeredType);
346440

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+
}
352449
}
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}()
354452
}
355453

356-
return () -> factory.create(component, key, null);
454+
return () -> factory.create(component, key, null, false);
357455
}
358456

359457
private static void warn(String format, Object... args) {
360458
String message = String.format("WARNING: " + format, args);
361459
errorReporter.accept(message);
362460
}
363461

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+
364482
private PersistedConfiguration() {
365483
throw new AssertionError("Not instantiable");
366484
}

0 commit comments

Comments
 (0)