View Javadoc
1   package de.spiritscorp.datasync.io;
2   
3   /*-
4    * 		Data Sync
5    *
6    * 		Copyright ©   2022    The Spirit
7    * 		@email                        thespirit@spiritscorp.network
8    *
9    * 		This program is free software; you can redistribute it and/or modify
10   * 		it under the terms of the GNU General Public License as published by
11   * 		the Free Software Foundation; either version 3 of the License, or
12   * 		(at your option) any later version.
13   *
14   * 		This program is distributed in the hope that it will be useful,
15   * 		but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * 		MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
17   * 		See the GNU General Public License for more details.
18   *
19   * 		You should have received a copy of the GNU General Public License
20   * 		along with this program. If not, see <http://www.gnu.org/licenses/>.
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   * Thread-safe global configurations orchestrator managing persistent JSON configurations.
50   * Coordinates multi-profile I/O read/write operations for independent replication synchronization tasks
51   * and global application states. Implements the Singleton pattern to ensure centralized state control.
52   *
53   */
54  public final class PreferenceManager {
55  
56  	/**
57  	 * Timeout duration in seconds for acquiring the profile lock.
58  	 */
59  	private static final long LOCK_TIME = 1;
60  	/**
61  	 * Root directory for application data storage.
62  	 */
63  	/*package*/ static final Path DATASYNC_HOME = Paths.get( System.getProperty( "user.home" ), "DataSync" );
64  	/**
65  	 * Root directory for application data storage.
66  	 */
67  	private Path rootPath = DATASYNC_HOME;
68  	/**
69  	 * Path to the JSON configuration file containing profiles and global settings.
70  	 */
71  	private Path configPath = rootPath.resolve( "conf.json" );
72  	/**
73  	 * Path to the standard JSON log file.
74  	 */
75  	private Path logPath = rootPath.resolve( "log.json" );
76  	/**
77  	 * Path to the standard debug log text file.
78  	 */
79  	private Path debugPath = rootPath.resolve( "debug.log" );
80  	/**
81  	 * Path to the error log text file.
82  	 */
83  	private Path errorPath = rootPath.resolve( "debug.err" );
84  
85  	/**
86  	 * Thread-safe map storing the loaded automation profiles indexed by their job name.
87  	 */
88  	private final Map<String, Preference> loadedProfiles = new ConcurrentHashMap<>();
89  
90  	/**
91  	 * The global Singleton instance of the PreferenceManager.
92  	 */
93  	private static final PreferenceManager INSTANCE = new PreferenceManager();
94  
95  	/**
96  	 * Lock to ensure thread-safe operations on profile configurations.
97  	 */
98  	private final ReentrantLock profileLock = new ReentrantLock();
99  
100 	/**
101 	 * Global flag indicating if application launch on systemboot.
102 	 */
103 	private boolean globalAutoStart;
104 
105 	/**
106 	 * The currently active visual theme of the application.
107 	 */
108 	private AppTheme theme;
109 
110 	/**
111 	 * Enforces non-instantiability outside the Singleton lifecycle context.
112 	 */
113 	private PreferenceManager() {
114 	}
115 
116 	/**
117 	 * Gets the global Singleton instance.
118 	 *
119 	 * @return The singleton manager instance.
120 	 */
121 	public static PreferenceManager getInstance() { return INSTANCE; }
122 
123 	/**
124 	 * Reconfigures and overrides the global operational ecosystem workspace root coordinates.
125 	 * Re-initializes all system-dependent structural path mappings dynamically.
126 	 * <p>
127 	 * Fallback at <b>(user.home)/DataSync</b> if the target path is not writeable or not valid
128 	 *
129 	 * @param customRoot The new target base directory path, or null to retain the home default context.
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 	 * Allocates and provisions a new, distinct profile configuration data scope unit.
155 	 * Automatically appends the freshly constructed tracking unit block into active memory structures.
156 	 *
157 	 * @param jobName Unique target workspace identifier.
158 	 *
159 	 * @return An unpopulated, isolated configuration state segment tracker.
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 	 * Retrieves an allocated configuration tracking state context signature via its workspace identifier.
182 	 *
183 	 * @param jobName The unique profile registry lookup key.
184 	 * @return The matching configuration instance state, or null if no mapping tracks the parameter.
185 	 */
186 	public Preference getProfile( final String jobName ) {
187 		return loadedProfiles.get( jobName );
188 	}
189 
190 	/**
191 	 * Atomically handles profile rename routines inside the synchronization runtime context.
192 	 * Mutates the tracking key structural state identifier map and flushes changes to disk immediately.
193 	 *
194 	 * @param oldName Original task profile identifier key.
195 	 * @param newName Target replacement unique identifier string.
196 	 * @param pref    Associated configuration parameters data segment instance.
197 	 * @return true if persistence succeeded; false if parameters were invalid or execution failed.
198 	 */
199 	public boolean renameProfile( final String oldName, final String newName, final Preference pref ) {
200 		boolean success = false;
201 		try {
202 			// Try to acquire the lock within a 1-second timeout to prevent deadlocks
203 			if( profileLock.tryLock( LOCK_TIME, TimeUnit.SECONDS ) ) {
204 				try {
205 					// Execute only when all inputs are valid
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 					// Always ensure the lock is released if it was successfully acquired
214 					profileLock.unlock();
215 				}
216 			}else {
217 				Debug.printError( "[Error] renameProfile() -> Profiles are allready locked" );
218 			}
219 		}catch( final InterruptedException e ) {
220 			// Restore interrupted status if the thread was interrupted while waiting for the lock
221 			Thread.currentThread().interrupt();
222 		}
223 		return success;
224 	}
225 
226 	/**
227 	 * Atomically removes an active synchronization job context tracking assignment.
228 	 * Evicts cached properties from memory and updates the primary configuration storage file.
229 	 *
230 	 * @param job The high-level UI task context container targeted for decommissioning.
231 	 * @return true if structural extraction and serialization completed successfully.
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 	 * Exposes the active, in-memory configuration profile map registry.
256 	 * Wrapped in an unmodifiable view to preserve structural mutation thread safety bounds.
257 	 *
258 	 * @return An unmodifiable structural read-only view tracking live preference profiles.
259 	 */
260 	public Map<String, Preference> getLoadedProfiles() { return Collections.unmodifiableMap( loadedProfiles ); }
261 
262 	/**
263 	 * Compiles all active in-memory profile matrices and flushes them into a single unified JSON structure.
264 	 * Truncates any existing configuration state assets dynamically during filesystem stream allocation.
265 	 *
266 	 * @return true if structural file flushing and underlying persistence executed without errors.
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 					// Embed global properties
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 					// Append dynamic profile segments
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 	 * Hydrates the global storage matrix layer and multi-job profile cache records from the persistent disk token.
316 	 * Validates individual structural segments during structural ingestion parsing.
317 	 *
318 	 * @return true if filesystem parsing completed entirely; false if tracking token was absent or corrupt.
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 						// Extract global runtime parameters
330 						if( !extractGlobal( rootObj ) ) {
331 							Debug.printDebug( "[Warn] load globals incompleted" );
332 						}
333 						loadedProfiles.clear();
334 
335 						// Extract distinct automation tasks profiles
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 	// --- Global Configuration Accessors ---
399 
400 	/**
401 	 * Evaluates whether the application ecosystem is provisioned to launch automatically
402 	 * upon host operating system startup sequences.
403 	 *
404 	 * @return true if the global background autostart configuration sequence is enabled.
405 	 */
406 	public boolean isGlobalAutoStart() { return globalAutoStart; }
407 
408 	/**
409 	 * Modifies the global automation startup property state parameter.
410 	 * This execution updates the structural configuration variable memory cache layers.
411 	 *
412 	 * @param globalAutoStart Target state flag to determine automated deployment behavior.
413 	 */
414 	public void setGlobalAutoStart( final boolean globalAutoStart ) { this.globalAutoStart = globalAutoStart; }
415 
416 	// --- Instanced System Properties Accessors (Formerly Static) ---
417 
418 	/**
419 	 * Retrieves the persistent dynamic configuration storage path target locator.
420 	 *
421 	 * @return The absolute filesystem path directing to the primary 'conf.json' token.
422 	 */
423 	public Path getConfigPath() { return configPath; }
424 
425 	/**
426 	 * Retrieves the persistent operational event synchronization logging path locator.
427 	 *
428 	 * @return The absolute filesystem path directing to the structural 'log.json' entity.
429 	 */
430 	public Path getLogPath() { return logPath; }
431 
432 	/**
433 	 * Retrieves the system console output debug log tracking path locator.
434 	 *
435 	 * @return The absolute filesystem path directing to the standard runtime 'debug.log' file.
436 	 */
437 	public Path getDebugPath() { return debugPath; }
438 
439 	/**
440 	 * Retrieves the localized tracking error diagnostic path locator.
441 	 *
442 	 * @return The absolute filesystem path directing to the critical runtime 'debug.err' stream dump.
443 	 */
444 	public Path getErrorPath() { return errorPath; }
445 
446 	/**
447 	 * Sets the visual theme of the application.
448 	 *
449 	 */
450 	public void setTheme( final AppTheme theme ) { this.theme = theme; }
451 
452 	/**
453 	 * Gets the currently configured application theme.
454 	 *
455 	 * @return the active AppTheme
456 	 */
457 	public AppTheme getTheme() { return theme; }
458 
459 	public boolean isCustomConfigDir() { return !DATASYNC_HOME.equals( rootPath ); }
460 }