1 package de.spiritscorp.datasync.io;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 import java.io.IOException;
24 import java.lang.reflect.InvocationTargetException;
25 import java.nio.file.Files;
26 import java.nio.file.LinkOption;
27 import java.nio.file.Path;
28 import java.nio.file.Paths;
29 import java.nio.file.StandardOpenOption;
30 import java.util.Collections;
31 import java.util.HashMap;
32 import java.util.Map;
33 import java.util.concurrent.ConcurrentHashMap;
34 import java.util.concurrent.TimeUnit;
35 import java.util.concurrent.locks.ReentrantLock;
36
37 import de.spiritscorp.datasync.controller.SyncJobContext;
38 import de.spiritscorp.datasync.theme.AppTheme;
39 import de.spiritscorp.datasync.theme.DarkSlateTheme;
40 import jakarta.json.Json;
41 import jakarta.json.JsonObject;
42 import jakarta.json.JsonObjectBuilder;
43 import jakarta.json.JsonReader;
44 import jakarta.json.JsonWriter;
45 import jakarta.json.JsonWriterFactory;
46 import jakarta.json.stream.JsonGenerator;
47
48
49
50
51
52
53
54 public final class PreferenceManager {
55
56
57
58
59 private static final long LOCK_TIME = 1;
60
61
62
63 static final Path DATASYNC_HOME = Paths.get( System.getProperty( "user.home" ), "DataSync" );
64
65
66
67 private Path rootPath = DATASYNC_HOME;
68
69
70
71 private Path configPath = rootPath.resolve( "conf.json" );
72
73
74
75 private Path logPath = rootPath.resolve( "log.json" );
76
77
78
79 private Path debugPath = rootPath.resolve( "debug.log" );
80
81
82
83 private Path errorPath = rootPath.resolve( "debug.err" );
84
85
86
87
88 private final Map<String, Preference> loadedProfiles = new ConcurrentHashMap<>();
89
90
91
92
93 private static final PreferenceManager INSTANCE = new PreferenceManager();
94
95
96
97
98 private final ReentrantLock profileLock = new ReentrantLock();
99
100
101
102
103 private boolean globalAutoStart;
104
105
106
107
108 private AppTheme theme;
109
110
111
112
113 private PreferenceManager() {
114 }
115
116
117
118
119
120
121 public static PreferenceManager getInstance() { return INSTANCE; }
122
123
124
125
126
127
128
129
130
131 public void initGlobalRootConfigPath( final Path customRoot ) {
132 try {
133 if( profileLock.tryLock( LOCK_TIME, TimeUnit.SECONDS ) ) {
134 try {
135 if( customRoot != null && Files.exists( customRoot, LinkOption.NOFOLLOW_LINKS ) ) {
136 rootPath = customRoot.toAbsolutePath().normalize();
137 configPath = customRoot.resolve( "conf.json" );
138 logPath = customRoot.resolve( "log.json" );
139 debugPath = customRoot.resolve( "debug.log" );
140 errorPath = customRoot.resolve( "debug.err" );
141 }
142 }finally {
143 profileLock.unlock();
144 }
145 }else {
146 Debug.printError( "[Error] initGlobalRootConfigPath() -> Profiles are allready locked" );
147 }
148 }catch( final InterruptedException e ) {
149 Thread.currentThread().interrupt();
150 }
151 }
152
153
154
155
156
157
158
159
160
161 public Preference createProfile( final String jobName ) {
162 try {
163 if( profileLock.tryLock( LOCK_TIME, TimeUnit.SECONDS ) ) {
164 try {
165 final Preference pref = Preference.createSinglePreference( jobName );
166 loadedProfiles.put( pref.getJobName(), pref );
167 if( saveAllPreferences() ) return pref;
168 }finally {
169 profileLock.unlock();
170 }
171 }else {
172 Debug.printError( "[Error] createProfiles() -> Profiles are allready locked" );
173 }
174 }catch( final InterruptedException e ) {
175 Thread.currentThread().interrupt();
176 }
177 return null;
178 }
179
180
181
182
183
184
185
186 public Preference getProfile( final String jobName ) {
187 return loadedProfiles.get( jobName );
188 }
189
190
191
192
193
194
195
196
197
198
199 public boolean renameProfile( final String oldName, final String newName, final Preference pref ) {
200 boolean success = false;
201 try {
202
203 if( profileLock.tryLock( LOCK_TIME, TimeUnit.SECONDS ) ) {
204 try {
205
206 if( oldName != null && newName != null && pref != null && !oldName.equals( newName ) ) {
207 loadedProfiles.remove( oldName );
208 pref.setJobNameFromManager( newName );
209 loadedProfiles.put( newName, pref );
210 success = saveAllPreferences();
211 }
212 }finally {
213
214 profileLock.unlock();
215 }
216 }else {
217 Debug.printError( "[Error] renameProfile() -> Profiles are allready locked" );
218 }
219 }catch( final InterruptedException e ) {
220
221 Thread.currentThread().interrupt();
222 }
223 return success;
224 }
225
226
227
228
229
230
231
232
233 public boolean removeProfile( final SyncJobContext job ) {
234 boolean success = false;
235 try {
236 if( profileLock.tryLock( LOCK_TIME, TimeUnit.SECONDS ) ) {
237 try {
238 if( job != null ) {
239 loadedProfiles.remove( job.getJobName(), job.getPreference() );
240 success = saveAllPreferences();
241 }
242 }finally {
243 profileLock.unlock();
244 }
245 }else {
246 Debug.printError( "[Error] removeProfile() -> Profiles are allready locked" );
247 }
248 }catch( final InterruptedException e ) {
249 Thread.currentThread().interrupt();
250 }
251 return success;
252 }
253
254
255
256
257
258
259
260 public Map<String, Preference> getLoadedProfiles() { return Collections.unmodifiableMap( loadedProfiles ); }
261
262
263
264
265
266
267
268 public synchronized boolean saveAllPreferences() {
269 try {
270 if( profileLock.tryLock( LOCK_TIME, TimeUnit.SECONDS ) ) {
271 try {
272 if( !Files.exists( rootPath ) ) {
273 Files.createDirectories( rootPath );
274 }
275
276 final JsonObjectBuilder rootBuilder = Json.createObjectBuilder();
277
278
279 final JsonObject globalDoc = Json.createObjectBuilder()
280 .add( "autoStart", globalAutoStart )
281 .add( "theme", theme.getClass().getName() )
282 .build();
283 rootBuilder.add( "globalSettings", globalDoc );
284
285
286 for( final Map.Entry<String, Preference> entry : loadedProfiles.entrySet() ) {
287 rootBuilder.add( entry.getKey(), entry.getValue().serialize() );
288 }
289
290 final Map<String, Object> writerConfig = new HashMap<>();
291 writerConfig.put( JsonGenerator.PRETTY_PRINTING, true );
292 final JsonWriterFactory factory = Json.createWriterFactory( writerConfig );
293
294 try( JsonWriter writer = factory.createWriter( Files.newOutputStream( configPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING ) ) ) {
295 writer.write( rootBuilder.build() );
296 return true;
297 }
298 }catch( final IOException e ) {
299 Debug.printDebug( "[Error] Critical: Failed to serialize active memory states to 'conf.json'. Reason: %s", e.getMessage() );
300 Debug.printException( this.getClass(), e );
301 return false;
302 }finally {
303 profileLock.unlock();
304 }
305 }else {
306 Debug.printError( "[Error] removeProfile() -> Profiles are allready locked" );
307 }
308 }catch( final InterruptedException e ) {
309 Thread.currentThread().interrupt();
310 }
311 return false;
312 }
313
314
315
316
317
318
319
320 public boolean loadAllPreferences() {
321 try {
322 if( profileLock.tryLock( LOCK_TIME, TimeUnit.SECONDS ) ) {
323 try {
324
325 try( JsonReader reader = Json.createReader( Files.newInputStream( configPath ) ) ) {
326 final JsonObject rootObj = reader.readObject();
327 if( rootObj.isEmpty() ) return false;
328
329
330 if( !extractGlobal( rootObj ) ) {
331 Debug.printDebug( "[Warn] load globals incompleted" );
332 }
333 loadedProfiles.clear();
334
335
336 if( !extractProfiles( rootObj ) ) {
337 Debug.printDebug( "[Warn] load profiles incompleted" );
338 }
339 return true;
340 }catch( final ClassCastException | IOException e ) {
341 Debug.printDebug( "[Error] Critical: Failed to load profiles. Reason: %s", e.getMessage() );
342 Debug.printException( this.getClass(), e );
343 return false;
344 }
345 }finally {
346 profileLock.unlock();
347 }
348 }else {
349 Debug.printError( "[Error] loadAllPreferences() -> Profiles are allready locked" );
350 }
351 }catch( final InterruptedException e ) {
352 Thread.currentThread().interrupt();
353 }
354 return false;
355 }
356
357 private boolean extractGlobal( final JsonObject rootObj ) {
358 if( rootObj.containsKey( "globalSettings" ) ) {
359 final JsonObject globalDoc = rootObj.getJsonObject( "globalSettings" );
360 if( globalDoc == null ) return false;
361 this.globalAutoStart = globalDoc.getBoolean( "autoStart", false );
362 if( globalDoc.containsKey( "theme" ) ) {
363 final String className = globalDoc.getString( "theme" );
364 try {
365 final Class<?> themeClass = Class.forName( className );
366 this.theme = (AppTheme) themeClass.getDeclaredConstructor().newInstance();
367 return true;
368 }catch( final ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException
369 | InvocationTargetException | NoSuchMethodException | SecurityException e ) {
370 Debug.printDebug( "[Error] Falling back to default. Failed to instantiate theme class: %s", e.getMessage() );
371 Debug.printException( getClass(), e );
372 }
373 }else {
374 Debug.printDebug( "[Warn] No value for instantiate theme class. Falling back to default." );
375 }
376 }
377 this.theme = new DarkSlateTheme();
378 return false;
379 }
380
381 private boolean extractProfiles( final JsonObject rootObj ) {
382 for( final String jobName : rootObj.keySet() ) {
383 if( jobName.equals( "globalSettings" ) ) continue;
384 final JsonObject jobData = rootObj.getJsonObject( jobName );
385 final Preference pref = Preference.createSinglePreference( jobName );
386 try {
387 pref.deserialize( jobData );
388 loadedProfiles.put( jobName, pref );
389 }catch( final ConfigException e ) {
390 Debug.printDebug( "[Error] Critical: Failed to load job profile '%s'. Skipping entry. Reason: %s", jobName, e.getMessage() );
391 Debug.printException( this.getClass(), e );
392 return false;
393 }
394 }
395 return !loadedProfiles.isEmpty();
396 }
397
398
399
400
401
402
403
404
405
406 public boolean isGlobalAutoStart() { return globalAutoStart; }
407
408
409
410
411
412
413
414 public void setGlobalAutoStart( final boolean globalAutoStart ) { this.globalAutoStart = globalAutoStart; }
415
416
417
418
419
420
421
422
423 public Path getConfigPath() { return configPath; }
424
425
426
427
428
429
430 public Path getLogPath() { return logPath; }
431
432
433
434
435
436
437 public Path getDebugPath() { return debugPath; }
438
439
440
441
442
443
444 public Path getErrorPath() { return errorPath; }
445
446
447
448
449
450 public void setTheme( final AppTheme theme ) { this.theme = theme; }
451
452
453
454
455
456
457 public AppTheme getTheme() { return theme; }
458
459 public boolean isCustomConfigDir() { return !DATASYNC_HOME.equals( rootPath ); }
460 }