Skip to content

Commit 3fa8a9d

Browse files
committed
feature: preferences, to be essential building block for ng
1 parent 13d08cf commit 3fa8a9d

File tree

6 files changed

+405
-0
lines changed

6 files changed

+405
-0
lines changed

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
</snapshotRepository>
5858
</distributionManagement>
5959
<modules>
60+
<module>prefs</module>
6061
<module>library</module>
6162
<module>pace</module>
6263
<module>tool</module>

prefs/pom.xml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>com.github.martinpaljak</groupId>
8+
<artifactId>gppro</artifactId>
9+
<version>25.12.02-SNAPSHOT</version>
10+
</parent>
11+
12+
<artifactId>prefs</artifactId>
13+
14+
<properties>
15+
<maven.compiler.source>17</maven.compiler.source>
16+
<maven.compiler.target>17</maven.compiler.target>
17+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
18+
</properties>
19+
<dependencies>
20+
<dependency>
21+
<groupId>org.testng</groupId>
22+
<artifactId>testng</artifactId>
23+
<scope>test</scope>
24+
</dependency>
25+
<dependency>
26+
<groupId>org.slf4j</groupId>
27+
<artifactId>slf4j-simple</artifactId>
28+
<scope>test</scope>
29+
</dependency>
30+
</dependencies>
31+
</project>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module pro.javacard.prefs {
2+
exports pro.javacard.prefs;
3+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright (c) 2025 Martin Paljak <[email protected]>
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20+
* THE SOFTWARE.
21+
*/
22+
package pro.javacard.prefs;
23+
24+
import java.lang.reflect.Type;
25+
import java.util.Objects;
26+
import java.util.function.Predicate;
27+
28+
public sealed interface Preference<V> permits Preference.Default, Preference.Parameter {
29+
String name();
30+
31+
Type type();
32+
33+
boolean readonly();
34+
35+
default Predicate<V> validator() {
36+
return x -> true;
37+
}
38+
39+
static <T> Default<T> of(String name, Class<T> type, T defaultValue, boolean readonly) {
40+
return new Default<>(name, type, defaultValue, readonly);
41+
}
42+
43+
static <T> Parameter<T> parameter(String name, Class<T> type, boolean readonly) {
44+
return new Parameter<>(name, type, readonly);
45+
}
46+
47+
record Default<V>(String name, Type type, V defaultValue, boolean readonly) implements Preference<V> {
48+
public Default {
49+
Objects.requireNonNull(defaultValue, "Must have a sane default value!");
50+
}
51+
}
52+
53+
record Parameter<V>(String name, Type type, boolean readonly) implements Preference<V> {
54+
}
55+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
* Copyright (c) 2025 Martin Paljak <[email protected]>
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20+
* THE SOFTWARE.
21+
*/
22+
package pro.javacard.prefs;
23+
24+
import java.util.*;
25+
26+
public final class Preferences {
27+
private final Map<Preference<?>, Object> values;
28+
// Registry allows to slurp in known preferences from a file.
29+
private static final Map<String, Preference<?>> REGISTRY = new HashMap<>();
30+
31+
public Preferences() {
32+
this.values = Map.of();
33+
}
34+
35+
private Preferences(Map<Preference<?>, Object> values) {
36+
// .copyOf() assures that there are no null values
37+
this.values = Map.copyOf(values);
38+
}
39+
40+
// Only allows non-null overrides for preferences that have default values
41+
public <V> Preferences with(Preference<V> key, V value) {
42+
if (value == null) {
43+
throw new IllegalArgumentException("Cannot set null outcome for preference '" + key.name() + "'");
44+
}
45+
if (!key.validator().test(value)) {
46+
throw new IllegalArgumentException("Value for preference '" + key.name() + "' fails validation: " + value);
47+
}
48+
Map<Preference<?>, Object> newValues = new HashMap<>();
49+
newValues.put(key, value);
50+
// Calling merge will take care of readonly preferences
51+
return merge(new Preferences(newValues));
52+
}
53+
54+
public <V> Preferences without(Preference<V> key) {
55+
if (key.readonly()) {
56+
throw new IllegalArgumentException("Can't remove readonly preference!");
57+
}
58+
if (!values.containsKey(key)) {
59+
return this;
60+
}
61+
Map<Preference<?>, Object> newValues = new HashMap<>(values);
62+
newValues.remove(key);
63+
return new Preferences(newValues);
64+
}
65+
66+
// Always returns non-null: either the explicit override or the default outcome
67+
@SuppressWarnings("unchecked")
68+
public <V> V get(Preference.Default<V> key) {
69+
V value = (V) values.get(key);
70+
return value != null ? value : key.defaultValue();
71+
}
72+
73+
// Returns empty Optional when using default, present Optional when explicitly overridden
74+
@SuppressWarnings("unchecked")
75+
public <V> Optional<V> valueOf(Preference<V> key) {
76+
// We don't allow null values, so the optional is empty only
77+
// if the preference is not explicitly established
78+
return Optional.ofNullable((V) values.get(key));
79+
}
80+
81+
// Add all keys from other to this
82+
public Preferences merge(Preferences other) {
83+
Map<Preference<?>, Object> newValues = new HashMap<>(this.values);
84+
for (Map.Entry<Preference<?>, Object> entry : other.values.entrySet()) {
85+
Preference<?> key = entry.getKey();
86+
if (key.readonly() && this.values.containsKey(key)) {
87+
// Skip readonly preferences that already exist in this instance
88+
//log.warn("Trying to overwrite read-only preference " + key);
89+
continue;
90+
}
91+
newValues.put(key, entry.getValue());
92+
}
93+
return new Preferences(newValues);
94+
}
95+
96+
public Set<Preference<?>> keys() {
97+
return values.keySet();
98+
}
99+
100+
public boolean isEmpty() {
101+
return values.isEmpty();
102+
}
103+
104+
public int size() {
105+
return values.size();
106+
}
107+
108+
@Override
109+
public String toString() {
110+
StringBuilder sb = new StringBuilder();
111+
sb.append("Preferences{");
112+
for (var k : values.entrySet()) {
113+
sb.append(k.getKey().name());
114+
sb.append("(");
115+
sb.append(k.getKey().type().getTypeName());
116+
sb.append(")");
117+
sb.append("=");
118+
if (k.getValue() instanceof byte[] bytes) {
119+
sb.append(HexFormat.of().formatHex(bytes));
120+
} else {
121+
sb.append(k.getValue());
122+
}
123+
sb.append(";");
124+
}
125+
sb.append("}");
126+
return sb.toString();
127+
}
128+
129+
130+
// For parameters (no default outcome)
131+
public static <T> Preference.Parameter<T> register(String name, Class<T> type, boolean readonly) {
132+
Preference.Parameter<T> preference = Preference.parameter(name, type, readonly);
133+
REGISTRY.put(preference.name(), preference);
134+
return preference;
135+
}
136+
137+
// For parameters (readonly by default)
138+
public static <T> Preference.Parameter<T> register(String name, Class<T> type) {
139+
return register(name, type, true);
140+
}
141+
142+
// For preferences with default values
143+
public static <T> Preference.Default<T> register(String name, Class<T> type, T defaultValue, boolean readonly) {
144+
Preference.Default<T> preference = Preference.of(name, type, defaultValue, readonly);
145+
REGISTRY.put(preference.name(), preference);
146+
return preference;
147+
}
148+
149+
// For preferences with default values (not readonly by default)
150+
public static <T> Preference.Default<T> register(String name, Class<T> type, T defaultValue) {
151+
return register(name, type, defaultValue, false);
152+
}
153+
}

0 commit comments

Comments
 (0)