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