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.BufferedReader;
24  import java.io.BufferedWriter;
25  import java.io.IOException;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.nio.file.Paths;
29  import java.nio.file.StandardOpenOption;
30  import java.util.ArrayList;
31  import java.util.List;
32  import java.util.Map;
33  
34  import jakarta.json.Json;
35  import jakarta.json.JsonArray;
36  import jakarta.json.JsonArrayBuilder;
37  import jakarta.json.JsonObject;
38  import jakarta.json.JsonObjectBuilder;
39  import jakarta.json.JsonValue;
40  
41  import de.spiritscorp.datasync.BgTime;
42  import de.spiritscorp.datasync.ScanType;
43  import de.spiritscorp.datasync.model.FileAttributes;
44  import de.spiritscorp.datasync.model.Model;
45  
46  /**
47   * Isolated parameters state tracker mapped to a dedicated profile workspace scope.
48   *
49   * @author Tom Spirit
50   * @version 2.0.0
51   */
52  public final class Preference {
53  
54  	private String jobName;
55  	private final IOSyncMap ioSyncMap;
56  	private final Path jobScanTimePath;
57  
58  	private ArrayList<Path> sourcePaths;
59  	private ArrayList<Path> destPaths;
60  	private final Map<Path, FileAttributes> syncMap;
61  
62  	private static final String TRASHBIN_STRING = "Papierkorb";
63  	private Path trashbinPath;
64  
65  	private ScanType scanMode = ScanType.FLAT_SCAN;
66  	private BgTime bgTime = BgTime.DAYLY;
67  
68  	private boolean logOn = true;
69  	private boolean trashbin = true;
70  	private boolean subDir;
71  	private boolean autoDel;
72  	private boolean autoSync;
73  	private boolean bgSync;
74  
75  	private Preference( final String jobName ) {
76  		this.jobName = jobName;
77  
78  		syncMap = Model.createMap();
79  		destPaths = new ArrayList<>( List.of( PreferenceManager.DATASYNC_HOME ) );
80  		sourcePaths = new ArrayList<>( List.of( PreferenceManager.DATASYNC_HOME ) );
81  		trashbinPath = destPaths.getFirst().resolve( TRASHBIN_STRING );
82  
83  		final String safeName = jobName.replaceAll( "[^a-zA-Z0-9-_]", "_" );
84  		this.ioSyncMap = new IOSyncMap( safeName );
85  		this.jobScanTimePath = PreferenceManager.getInstance().getRootPath().resolve( "lastScanTime_" + safeName );
86  	}
87  
88  	/**
89  	 * Allocation factory generating tracking units separated by specific job profiles keys.
90  	 *
91  	 * @param jobName Unique target workspace identifier.
92  	 *
93  	 * @return Isolated configuration state scope.
94  	 */
95  	static Preference createSinglePreference( final String jobName ) {
96  		return new Preference( jobName );
97  	}
98  
99  	/**
100 	 * Translates local fields variables entries directly into a JSON entity.
101 	 */
102 	JsonObject serialize() {
103 		final JsonObjectBuilder builder = Json.createObjectBuilder();
104 
105 		final JsonArrayBuilder srcArr = Json.createArrayBuilder();
106 		for( final Path path : sourcePaths )
107 			srcArr.add( path.toString() );
108 
109 		final JsonArrayBuilder destArr = Json.createArrayBuilder();
110 		for( final Path path : destPaths )
111 			destArr.add( path.toString() );
112 
113 		builder.add( "sourcePaths", srcArr.build() )
114 				.add( "destPaths", destArr.build() )
115 				.add( "trashbinPath", trashbinPath.toString() )
116 				.add( "scanMode", scanMode.getDescription() )
117 				.add( "bgTime", bgTime.getName() )
118 				.add( "logOn", logOn )
119 				.add( "subDir", subDir )
120 				.add( "autoDel", autoDel )
121 				.add( "autoSync", autoSync )
122 				.add( "bgSync", bgSync )
123 				.add( "trashbin", trashbin );
124 
125 		return builder.build();
126 	}
127 
128 	/**
129 	 * Hydrates system state metrics back out from serialized profile blocks.
130 	 * Validates path existence, structural availability, and sanitizes incoming
131 	 * data types during structural parsing to prevent parsing exceptions.
132 	 *
133 	 * @param jsonObject The serialized JSON object payload mapping the profile configuration.
134 	 * @throws ConfigException If the structural state context is completely unrecoverable or malformed.
135 	 */
136 	void deserialize( final JsonObject jsonObject ) throws ConfigException {
137 		if( jsonObject == null ) { throw new ConfigException( "JSON payload context is null." ); }
138 
139 		try {
140 			// --- SOURCE PATHS VALIDATION ---
141 			this.sourcePaths.clear();
142 			final JsonArray srcArr = jsonObject.getJsonArray( "sourcePaths" );
143 			if( srcArr != null ) {
144 				for( final JsonValue jsonValue : srcArr ) {
145 					// Strip literal quotes from stringification boundaries
146 					final Path path = Paths.get( jsonValue.toString().replace( "\"", "" ) );
147 					// Rigid validation requirement: Source directories MUST physically exist
148 					if( Files.exists( path ) && Files.isDirectory( path ) ) {
149 						this.sourcePaths.add( path );
150 					}else {
151 						Debug.printDebug( "[Preference] Warning: Source path no longer available on filesystem: " + path );
152 					}
153 				}
154 			}
155 
156 			// --- DESTINATION PATHS VALIDATION ---
157 			this.destPaths.clear();
158 			final JsonArray destArr = jsonObject.getJsonArray( "destPaths" );
159 			if( destArr != null ) {
160 				for( final JsonValue jsonValue : destArr ) {
161 					final Path path = Paths.get( jsonValue.toString().replace( "\"", "" ) );
162 					// Target paths are allowed to be offline temporarily (e.g., disconnected network mounts)
163 					this.destPaths.add( path );
164 					// Fallback
165 					this.trashbinPath = path;
166 				}
167 			}
168 
169 			if( jsonObject.containsKey( "trashbinPath" ) ) {
170 				this.trashbinPath = Paths.get( jsonObject.getString( "trashbinPath" ) );
171 			}
172 
173 			// --- PATH FALLBACK LOGIC ---
174 			// Resilient protection layer: If structural filters left arrays empty, fall back to datasync home coordinates
175 			if( sourcePaths.isEmpty() || destPaths.isEmpty() ) {
176 				final Path userHome = PreferenceManager.DATASYNC_HOME;
177 				if( sourcePaths.isEmpty() ) sourcePaths.add( userHome );
178 				if( destPaths.isEmpty() ) destPaths.add( userHome );
179 				this.trashbinPath = userHome.resolve( TRASHBIN_STRING );
180 			}
181 
182 			// --- PRIMITIVE ENUM CONFIGS (With Type-Checking & Fallbacks) ---
183 			if( jsonObject.containsKey( "scanMode" ) ) {
184 				final ScanType parsed = ScanType.get( jsonObject.getString( "scanMode" ) );
185 				this.scanMode = ( parsed != null ) ? parsed : ScanType.FLAT_SCAN;
186 			}
187 			if( jsonObject.containsKey( "bgTime" ) ) {
188 				final BgTime parsed = BgTime.get( jsonObject.getString( "bgTime" ) );
189 				this.bgTime = ( parsed != null ) ? parsed : BgTime.DAYLY;
190 			}
191 
192 			// --- RESILIENT BOOLEAN INJECTION LAYER ---
193 			// Intercepts structural ClassCastExceptions if human operators modified primitive types illegally
194 			this.logOn = getSafeBoolean( jsonObject, "logOn", true );
195 			this.trashbin = getSafeBoolean( jsonObject, "trashbin", true );
196 			this.subDir = getSafeBoolean( jsonObject, "subDir", false );
197 			this.autoDel = getSafeBoolean( jsonObject, "autoDel", false );
198 			this.autoSync = getSafeBoolean( jsonObject, "autoSync", false );
199 			this.bgSync = getSafeBoolean( jsonObject, "bgSync", false );
200 
201 			// Trigger downstream initialization matrix cache tracking parsing
202 			ioSyncMap.loadSyncMap( syncMap );
203 
204 		}catch( final Exception exception ) {
205 			// Wrap any nested unhandled parsing or type violations into a predictable lifecycle runtime exception
206 			throw new ConfigException( "JSON payload structure is corrupt or mismatched.", exception );
207 		}
208 	}
209 
210 	/**
211 	 * Extracts a primitive boolean property from a JSON node while guaranteeing type safety bounds.
212 	 * Prevents application or thread failure sequences if string litterals or garbage attributes
213 	 * occupy the specified property target location.
214 	 *
215 	 * @param json         The serialized source JSON payload structural map.
216 	 * @param key          The expected parameter identifier token key.
217 	 * @param defaultValue The architectural primitive value fallback state if the verification sequence fails.
218 	 * @return The evaluated value assigned to the target property key, or the predefined default.
219 	 */
220 	private boolean getSafeBoolean( final JsonObject json, final String key, final boolean defaultValue ) {
221 		if( !json.containsKey( key ) ) { return defaultValue; }
222 		try {
223 			return json.getBoolean( key );
224 		}catch( ClassCastException _ ) {
225 			Debug.printDebug( "[Preference] Warning: Invalid data type mapping tracking key '%s'. Falling back to default: %s", key, defaultValue );
226 			return defaultValue;
227 		}
228 	}
229 
230 	/**
231 	 * Persists the current timestamp as the last successful scan execution time.
232 	 */
233 	public void saveLastScanTime() {
234 		try( BufferedWriter writer = Files.newBufferedWriter( jobScanTimePath,
235 				StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING ) ) {
236 			writer.write( String.valueOf( System.currentTimeMillis() ) );
237 		}catch( final IOException exception ) {
238 			Debug.printDebug( "[Preference Error] IO write failed at '%s' message: %s", jobScanTimePath.toString(), exception.getMessage() );
239 			Debug.printException( this.getClass(), exception );
240 		}
241 	}
242 
243 	/**
244 	 * Retrieves the last recorded scan execution timestamp.
245 	 *
246 	 * @return Unix epoch millis, or 0 if no timer token exists yet.
247 	 */
248 	public long getLastScanTime() {
249 		if( !Files.exists( jobScanTimePath ) ) { return 0; }
250 		try( BufferedReader reader = Files.newBufferedReader( jobScanTimePath ) ) {
251 			final String line = reader.readLine();
252 			return ( line != null ) ? Long.parseLong( line.trim() ) : 0;
253 		}catch( final IOException | NumberFormatException exception ) {
254 			Debug.printDebug( "[Preference Error] IO read failed at '%s' message: %s", jobScanTimePath.toString(), exception.getMessage() );
255 			Debug.printException( this.getClass(), exception );
256 			return 0;
257 		}
258 	}
259 
260 	/**
261 	 * Removes the profile by deleting its associated persistent data, including
262 	 * both the last scan time file and the synchronization map.
263 	 * <p>
264 	 * This method ensures that both deletion operations are attempted, regardless
265 	 * of whether the first operation succeeded.
266 	 *
267 	 * @return {@code true} if both the last scan file and the synchronization map
268 	 *         were successfully removed (or did not exist); {@code false} if either
269 	 *         operation encountered an error.
270 	 */
271 	public boolean removeProfile() {
272 		final boolean rmScanFile = removeLastScanFile();
273 		final boolean rmSyncFile = ioSyncMap.deleteSyncMap();
274 		return rmScanFile && rmSyncFile;
275 	}
276 
277 	/**
278 	 * Deletes the file tracking the last scan time from disk if it exists.
279 	 *
280 	 * @return {@code true} if the file was successfully deleted or did not exist;
281 	 *         {@code false} if an I/O error occurred during deletion.
282 	 */
283 	private boolean removeLastScanFile() {
284 		try {
285 			if( Files.deleteIfExists( jobScanTimePath ) ) {
286 				Debug.printDebug( "[Preferenced] Successfully deleted last scan time file: %s", jobScanTimePath.getFileName().toString() );
287 			}
288 			return true;
289 		}catch( IOException exception ) {
290 			Debug.printDebug( "[Preference Error] IO remove last scan file failed at '%s' message: %s", jobScanTimePath, exception.getMessage() );
291 			Debug.printException( this.getClass(), exception );
292 			return false;
293 		}
294 	}
295 
296 	/**
297 	 * Persists the current state of the synchronization map to storage.
298 	 * <p>
299 	 * Actual I/O operations are delegated to the underlying {@code ioSyncMap} handler.
300 	 */
301 	public void writeSyncMap() {
302 		ioSyncMap.writeSyncMap( syncMap );
303 	}
304 
305 	// --- Setters and Getters ---
306 	public String getJobName() { return jobName; }
307 
308 	void setJobNameFromManager( final String newName ) {
309 		this.ioSyncMap.deleteSyncMap();
310 		removeLastScanFile();
311 		this.jobName = newName;
312 	}
313 
314 	public ArrayList<Path> getSourcePaths() { return sourcePaths; }
315 
316 	public void setSourcePaths( final ArrayList<Path> sourcePaths ) {
317 		this.sourcePaths = sourcePaths;
318 		this.ioSyncMap.deleteSyncMap();
319 	}
320 
321 	public void setSourcePath( final Path sourcePath ) {
322 		this.sourcePaths.add( sourcePath );
323 	}
324 
325 	public void removeSourcePath( final Path sourcePath ) {
326 		this.sourcePaths.remove( sourcePath );
327 	}
328 
329 	public ArrayList<Path> getDestPaths() { return destPaths; }
330 
331 	public void setDestPaths( final ArrayList<Path> destPaths ) {
332 		this.destPaths = destPaths;
333 		this.trashbinPath = destPaths.get( 0 ).resolve( TRASHBIN_STRING );
334 		ioSyncMap.deleteSyncMap();
335 	}
336 
337 	public Map<Path, FileAttributes> getSyncMap() { return syncMap; }
338 
339 	public ScanType getScanMode() { return scanMode; }
340 
341 	public void setScanMode( final ScanType scanMode ) { this.scanMode = scanMode; }
342 
343 	public BgTime getBgTime() { return bgTime; }
344 
345 	public void setBgTime( final BgTime time ) { this.bgTime = time; }
346 
347 	public boolean isLogOn() { return logOn; }
348 
349 	public void setLogOn( final boolean logOn ) { this.logOn = logOn; }
350 
351 	public boolean isSubDir() { return subDir; }
352 
353 	public void setSubDir( final boolean subDir ) { this.subDir = subDir; }
354 
355 	public boolean isAutoDel() { return autoDel; }
356 
357 	public void setAutoDel( final boolean autoDel ) { this.autoDel = autoDel; }
358 
359 	public boolean isAutoSync() { return autoSync; }
360 
361 	public void setAutoSync( final boolean autoSync ) { this.autoSync = autoSync; }
362 
363 	public boolean isBgSync() { return bgSync; }
364 
365 	public void setBgSync( final boolean bgSync ) { this.bgSync = bgSync; }
366 
367 	public boolean isTrashbin() { return trashbin; }
368 
369 	public void setTrashbin( final boolean trashbin ) { this.trashbin = trashbin; }
370 
371 	public Path getTrashbinPath() { return trashbinPath; }
372 }