44import dev .faststats .internal .LoggerFactory ;
55import org .jetbrains .annotations .ApiStatus ;
66import org .jetbrains .annotations .Contract ;
7+ import org .jspecify .annotations .Nullable ;
78
89import java .io .IOException ;
910import java .io .OutputStreamWriter ;
1011import java .nio .file .Files ;
1112import java .nio .file .Path ;
12- import java .util .Optional ;
1313import java .util .Properties ;
1414import java .util .UUID ;
1515import java .util .concurrent .atomic .AtomicBoolean ;
16- import java .util .function .BiPredicate ;
16+ import java .util .function .Function ;
17+ import java .util .function .Supplier ;
1718import java .util .logging .Level ;
1819
1920import static java .nio .charset .StandardCharsets .UTF_8 ;
2021
2122@ ApiStatus .Internal
2223public record SimpleConfig (
2324 UUID serverId ,
25+ boolean enabled ,
2426 boolean additionalMetrics ,
2527 boolean debug ,
26- boolean enabled ,
28+ boolean submitMetrics ,
2729 boolean errorTracking ,
2830 boolean firstRun
2931) implements Config {
32+ private static final int CONFIG_VERSION = 1 ;
3033
31- public static final String COMMENT = """
34+ private static final String COMMENT = """
3235 FastStats (https://faststats.dev) collects anonymous usage statistics.
3336 # This helps developers understand how their projects are used in the real world.
3437 #
3538 # No IP addresses, player data, or personal information is collected.
3639 # The server ID below is randomly generated and can be regenerated at any time.
3740 #
3841 # Enabling metrics has no noticeable performance impact.
39- # Keeping metrics enabled is recommended, but you can opt out by setting 'enabled=false'.
42+ # Keeping FastStats enabled is recommended.
43+ # To disable all FastStats features, set 'enabled=false'.
44+ # To disable only metrics submission, set 'submitMetrics=false'.
45+ # To disable only additional metrics, set 'submitAdditionalMetrics=false'.
46+ # To disable only error tracking, set 'submitErrors=false'.
4047 #
41- # If you suspect a developer is collecting personal data or bypassing the "enabled" option,
48+ # If you suspect a developer is collecting personal data or bypassing any opt-out option,
4249 # please report it at: https://faststats.dev/abuse
4350 #
4451 # For more information, visit: https://faststats.dev/info
@@ -55,63 +62,87 @@ public record SimpleConfig(
5562 @ Contract (mutates = "io" )
5663 public static SimpleConfig read (final Path file ) throws RuntimeException {
5764 final var properties = readOrEmpty (file );
58- final var firstRun = properties . isEmpty () ;
65+ final var firstRun = properties != null ;
5966 final var saveConfig = new AtomicBoolean (firstRun );
6067
61- final var serverId = properties .map (object -> object .getProperty ("serverId" )).map (string -> {
62- try {
63- final var trimmed = string .trim ();
64- final var corrected = trimmed .length () > 36 ? trimmed .substring (0 , 36 ) : trimmed ;
65- if (!corrected .equals (string )) saveConfig .set (true );
66- return UUID .fromString (corrected );
67- } catch (final IllegalArgumentException e ) {
68- saveConfig .set (true );
69- return UUID .randomUUID ();
70- }
71- }).orElseGet (() -> {
72- saveConfig .set (true );
73- return UUID .randomUUID ();
68+ final var serverId = parse (properties , saveConfig , "serverId" , UUID ::randomUUID , value -> {
69+ final var corrected = value .length () > 36 ? value .substring (0 , 36 ) : value ;
70+ final var uuid = UUID .fromString (corrected );
71+ if (!value .equals (uuid .toString ())) saveConfig .set (true );
72+ return uuid ;
7473 });
75-
76- final BiPredicate <String , Boolean > predicate = (key , defaultValue ) -> {
77- return properties .map (object -> object .getProperty (key )).map (Boolean ::parseBoolean ).orElseGet (() -> {
78- saveConfig .set (true );
79- return defaultValue ;
80- });
81- };
82-
83- final var enabled = predicate .test ("enabled" , true );
84- final var errorTracking = predicate .test ("submitErrors" , true );
85- final var additionalMetrics = predicate .test ("submitAdditionalMetrics" , true );
86- final var debug = predicate .test ("debug" , false );
87-
88- if (saveConfig .get ()) try {
74+ final var configVersion = parse (properties , saveConfig , "configVersion" , null , Integer ::parseInt );
75+ final boolean enabled = parse (properties , saveConfig , "enabled" , () -> true , Boolean ::parseBoolean );
76+ final boolean submitMetrics = parse (properties , saveConfig , "submitMetrics" , () -> true , Boolean ::parseBoolean );
77+ final boolean errorTracking = parse (properties , saveConfig , "submitErrors" , () -> true , Boolean ::parseBoolean );
78+ final boolean additionalMetrics = parse (properties , saveConfig , "submitAdditionalMetrics" , () -> true , Boolean ::parseBoolean );
79+ final boolean debug = parse (properties , saveConfig , "debug" , () -> true , Boolean ::parseBoolean );
80+
81+ if (saveConfig .get () && (configVersion == null || configVersion <= CONFIG_VERSION )) try {
8982 Files .createDirectories (file .getParent ());
9083 try (final var out = Files .newOutputStream (file );
9184 final var writer = new OutputStreamWriter (out , UTF_8 )) {
9285 final var store = new Properties ();
9386
94- store .setProperty ("serverId" , serverId .toString ());
9587 store .setProperty ("enabled" , Boolean .toString (enabled ));
96- store .setProperty ("submitErrors " , Boolean .toString (errorTracking ));
88+ store .setProperty ("submitMetrics " , Boolean .toString (submitMetrics ));
9789 store .setProperty ("submitAdditionalMetrics" , Boolean .toString (additionalMetrics ));
90+ store .setProperty ("submitErrors" , Boolean .toString (errorTracking ));
91+
92+ store .setProperty ("serverId" , serverId .toString ());
93+
9894 store .setProperty ("debug" , Boolean .toString (debug ));
95+ store .setProperty ("configVersion" , Integer .toString (CONFIG_VERSION ));
9996
10097 store .store (writer , COMMENT );
10198 }
10299 } catch (final IOException e ) {
103100 throw new RuntimeException ("Failed to save metrics config" , e );
104101 }
105102
106- return new SimpleConfig (serverId , additionalMetrics , debug , enabled , errorTracking , firstRun );
103+ return new SimpleConfig (
104+ serverId ,
105+ enabled ,
106+ enabled && additionalMetrics ,
107+ debug ,
108+ enabled && submitMetrics ,
109+ enabled && errorTracking ,
110+ firstRun
111+ );
112+ }
113+
114+ // fixme: this code sucks ass
115+ @ Contract (value = "_, _, _, !null, _ -> !null" )
116+ private static <T > @ Nullable T parse (
117+ @ Nullable final Properties properties ,
118+ final AtomicBoolean saveConfig ,
119+ final String key ,
120+ @ Nullable final Supplier <T > defaultValue ,
121+ final Function <String , T > parser
122+ ) {
123+ if (properties == null ) {
124+ saveConfig .set (true );
125+ return defaultValue != null ? defaultValue .get () : null ;
126+ }
127+ final var property = properties .getProperty (key );
128+ if (property == null ) {
129+ saveConfig .set (true );
130+ return defaultValue != null ? defaultValue .get () : null ;
131+ }
132+ try {
133+ return parser .apply (property .trim ());
134+ } catch (final Exception e ) {
135+ saveConfig .set (true );
136+ return defaultValue != null ? defaultValue .get () : null ;
137+ }
107138 }
108139
109- private static Optional < Properties > readOrEmpty (final Path file ) throws RuntimeException {
110- if (!Files .isRegularFile (file )) return Optional . empty () ;
140+ private static @ Nullable Properties readOrEmpty (final Path file ) throws RuntimeException {
141+ if (!Files .isRegularFile (file )) return null ;
111142 try (final var reader = Files .newBufferedReader (file , UTF_8 )) {
112143 final var properties = new Properties ();
113144 properties .load (reader );
114- return Optional . of ( properties ) ;
145+ return properties ;
115146 } catch (final IOException e ) {
116147 throw new RuntimeException ("Failed to read metrics config" , e );
117148 }
0 commit comments