View Javadoc
1   package de.spiritscorp.datasync.controller;
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 javafx.application.Platform;
24  import javafx.collections.FXCollections;
25  import javafx.collections.ObservableList;
26  
27  import de.spiritscorp.datasync.gui.DialogService;
28  import de.spiritscorp.datasync.gui.Gui;
29  import de.spiritscorp.datasync.gui.NotifyStatus;
30  import de.spiritscorp.datasync.io.Debug;
31  import de.spiritscorp.datasync.io.Logger;
32  import de.spiritscorp.datasync.io.Preference;
33  import de.spiritscorp.datasync.io.PreferenceManager;
34  import de.spiritscorp.datasync.theme.AppTheme;
35  
36  /**
37   * Central controller implementation executing operational state translations and business action flows.<br>
38   *
39   * Acting as the primary orchestrator (Mediator pattern), this component decouples the reactive
40   * JavaFX user interface layer from the transactional backend service domains and persistence engines.
41   * It intercepts user-driven view interactions, coordinates lifecycle mutations of background synchronization
42   * tasks, and dispatches state-change signals across the active runtime workspace.
43   *
44   * @author Tom Spirit
45   * @since 1.2.0
46   */
47  public class MainViewController implements ViewController {
48  
49  	/** The timeout limit in milliseconds for asynchronous background processes (e.g., automated file or task scans). */
50  	public static final int BG_TIMEOUT = 20_000;
51  	/** The timeout limit in milliseconds for regular, worker threads before a forced termination is triggered. */
52  	public static final int EXIT_TIMEOUT = 10_000;
53  
54  	/** The primary user interface orchestration shell managing view states, layouts, and volatile notifications. */
55  	private final Gui gui;
56  	/** The centralized dialog orchestration service managing modal view lifecycles and user confirmation flows. */
57  	private final DialogService dialogService;
58  	/** The core service layer processing execution requests and lifecycle validations for synchronization tasks. */
59  	private final SyncJobService helper;
60  	/** The central preference manager coordinating serialization, persistence, and registration of global and job-specific profiles. */
61  	private final PreferenceManager manager;
62  	/** The execution controller overseeing scheduled background worker threads and daemon interval routines. */
63  	private BgController bgController;
64  
65  	/**
66  	 * Constructs a primary controller instance, automatically provisioning the underlying dialog subsystem using the active window stage hook.
67  	 *
68  	 * @param gui The primary user interface orchestration shell managing view states and layouts.
69  	 */
70  	public MainViewController( final Gui gui ) {
71  		this(
72  				gui,
73  				new DialogService( gui.getWindowStage() ) );
74  	}
75  
76  	/**
77  	 * Allocates a new controller instance tied directly to the display engine layer hook, bootstrapping the intermediate execution and configuration service layers.
78  	 *
79  	 * @param gui           The global display manager orchestrator application shell instance.
80  	 * @param dialogService The centralized dialog orchestration service managing modal view lifecycles.
81  	 */
82  	MainViewController( final Gui gui, final DialogService dialogService ) {
83  		this(
84  				gui,
85  				new SyncJobService( dialogService, new LogFormatter() ),
86  				PreferenceManager.getInstance(),
87  				dialogService );
88  		loadInitialJobList();
89  	}
90  
91  	/**
92  	 * For TESTING
93  	 * <br>
94  	 * Constructs a new central view controller fully decoupled and initialized with its core
95  	 * architectural dependencies for isolated execution tracking.
96  	 *
97  	 * @param gui           The visual layout shell managing view hierarchies and user interaction states.
98  	 * @param helper        The core service layer orchestrating synchronization job execution routines.
99  	 * @param manager       The central preference authority handling profile configuration persistence.
100 	 * @param dialogService The centralized dialog orchestration service managing modal view lifecycles.
101 	 */
102 	MainViewController( final Gui gui, final SyncJobService helper, final PreferenceManager manager, final DialogService dialogService ) {
103 		this.dialogService = dialogService;
104 		this.gui = gui;
105 		this.helper = helper;
106 		this.manager = manager;
107 	}
108 
109 	@SuppressWarnings( { "java:S106" } )
110 	@Override
111 	public void registerNativeShutdownHook() {
112 		Runtime.getRuntime().addShutdownHook( new Thread( () -> {
113 			// This block executes automatically if Windows/Linux sends a SIGTERM or shutdown signal
114 			Debug.printDebug( "[Exit] Host operating system shutdown signal intercepted via native runtime hook." );
115 			// Enforce rapid execution with small timeouts since the OS will forcefully kill us shortly
116 			executeShutdown( false );
117 			Debug.printDebug( "[Exit] BYE, BYE" );
118 			System.out.flush();
119 			System.err.flush();
120 		}, "DataSync-OS-Shutdown-Hook-Thread" ) );
121 		Debug.printDebug( "[Info] Native OS runtime shutdown hook successfully registered." );
122 	}
123 
124 	@Override
125 	public void handleApplicationShutdown() {
126 		if( dialogService.promptOkChancel( "Programm beenden", "Möchten sie DataSync wirklich schließen?", "Aktive Hintergrunddienste werden wenn möglich sauber beendet." ) ) {
127 			Debug.printDebug( "[Exit] Complete system teardown triggered manually via user confirmation." );
128 
129 			// Tear down the JavaFX UI framework layer immediately so the window closes for the user
130 			Platform.exit();
131 			// Execute executeCoreShutdownSequence in the System.exit() hook
132 			Debug.printDebug( "[Exit] Manual graceful teardown completed. Evicting core JVM runtime context loop." );
133 			System.exit( 0 ); // NOPMD Kill all
134 		}
135 	}
136 
137 	@Override
138 	public void runInBackground( final boolean firstStart ) {
139 		bgController = new BgController( gui, this, gui.getJobList(), new Logger() );
140 		bgController.startBgJob( firstStart );
141 	}
142 
143 	@Override
144 	public void handleAutostart( final boolean autostart ) {
145 		manager.setGlobalAutoStart( autostart );
146 		if( helper.setOSAutostart( autostart ) && manager.saveAllPreferences() ) {
147 			Debug.printDebug( "[Settings] OS desktop autostart hooks successfully synchronized to state: " + autostart );
148 		}else {
149 			Debug.printDebug( "[Settings] Warning: Failed to apply host operating system autostart modifications." );
150 		}
151 	}
152 
153 	@Override
154 	public void handleNavigate( final Gui.ViewState state ) {
155 		gui.setViewState( state );
156 		if( !gui.getWindowStage().isShowing() ) {
157 			gui.getWindowStage().show();
158 			gui.getWindowStage().toFront();
159 		}
160 	}
161 
162 	@Override
163 	public void handleCreateNewJob() {
164 		final String name = "Sync Job " + ( gui.getJobList().size() + 1 );
165 		gui.getJobList().add( new SyncJobContext( name, manager.createProfile( name, true ) ) );
166 		gui.showStatusNotification( name + " wurde erstellt und gespeichert", NotifyStatus.SUCCESS, Gui.INFO_DELAY );
167 	}
168 
169 	@Override
170 	public void handleRenameJob( final SyncJobContext job ) {
171 		if( job != null ) {
172 			final String oldName = job.getJobName();
173 			final String newName = dialogService.promptTextInput( "Task umbenennen: " + oldName, "Geben Sie einen neuen Namen für den Task ein", "Neuer Name:" );
174 //		Check if not the same and don`t exists
175 			if( !newName.isBlank() && !newName.equals( oldName ) && manager.getProfile( newName ) == null ) {
176 				manager.renameProfile( oldName, newName, job.getPreference() );
177 				job.setJobName( newName );
178 				gui.showStatusNotification( oldName + " wurde ersetzt und gespeichert durch " + newName, NotifyStatus.SUCCESS, Gui.INFO_DELAY );
179 			}else {
180 				gui.showStatusNotification( oldName + " wurde nicht ersetzt", NotifyStatus.WARNING, Gui.INFO_DELAY );
181 			}
182 		}
183 	}
184 
185 	@Override
186 	public void handleDuplicateJob( final SyncJobContext job ) {
187 		if( job != null ) {
188 			final String newName = job.getJobName() + " (Kopie)";
189 			final Preference pref = job.getPreference();
190 			gui.getJobList().addLast( new SyncJobContext( newName, manager.setNewProfile( newName, pref ) ) );
191 			gui.showStatusNotification( newName + " wurde erstellt und gespeichert", NotifyStatus.SUCCESS, Gui.INFO_DELAY );
192 		}else {
193 			gui.showStatusNotification( "Fehler: Job ist unbekannt", NotifyStatus.ERROR, Gui.INFO_DELAY );
194 		}
195 	}
196 
197 	@Override
198 	public void deleteSelectedDuplicates( final SyncJobContext jobContext ) {
199 		helper.deleteSelectedDuplicates( jobContext );
200 	}
201 
202 	@Override
203 	public void handleDeleteJob( final SyncJobContext job ) {
204 		if( job != null ) {
205 			final String jobName = job.getJobName();
206 			if( dialogService.promptYesNo( "Task entfernen", null, "Task '" + job.getJobName() + "' wirklich unwiderruflich löschen?" ) ) {
207 				gui.getJobList().remove( job );
208 				manager.removeProfile( jobName );
209 				gui.showStatusNotification( jobName + " wurde erfolgreich gelöscht", NotifyStatus.SUCCESS, Gui.INFO_DELAY );
210 			}else {
211 				gui.showStatusNotification( jobName + " wurde nicht gelöscht", NotifyStatus.WARNING, Gui.INFO_DELAY );
212 			}
213 		}
214 	}
215 
216 	@Override
217 	public void handleDragJob( final int newIdx, final int draggedIdx ) {
218 		final SyncJobContext item = gui.getJobList().remove( draggedIdx );
219 		if( item != null ) {
220 			gui.getJobList().add( newIdx, item );
221 			manager.moveProfile( newIdx, draggedIdx, item.getPreference() );
222 		}
223 	}
224 
225 	@Override
226 	public void handleExecuteTask( final SyncJobContext job ) {
227 		if( job != null ) {
228 			switch( job.getSelectedMode() ) { // NOPMD default is here ok
229 				case SYNCHRONIZE -> helper.startSynchronize( job );
230 				case DUBLICATE_SCAN -> helper.startDuplicateScan( job );
231 				case DEEP_SCAN, FLAT_SCAN -> helper.startBackup( job );
232 				default -> throw new IllegalArgumentException( "Unexpected value: " + job.getSelectedMode() );
233 			}
234 		}
235 	}
236 
237 	@Override
238 	public void handleStopTask( final SyncJobContext job ) {
239 		job.cancelRunningTask( EXIT_TIMEOUT );
240 	}
241 
242 	@Override
243 	public void handleSaveSettings( final AppTheme targetTheme ) {
244 		gui.changeTheme( targetTheme );
245 		gui.setViewState( Gui.ViewState.SETTINGS );
246 
247 		// Persist structural configuration states securely to disk
248 		if( manager.saveAllPreferences() ) {
249 			gui.showStatusNotification( "Die Einstellungen wurden erfolgreich in der Konfiguration gespeichert.", NotifyStatus.SUCCESS, Gui.INFO_DELAY );
250 			Debug.printDebug( "[Settings] Configuration profile assets successfully serialized to disk." );
251 		}else {
252 			gui.showStatusNotification( "Fehler: Die Konfigurationsdaten konnten nicht in 'conf.json' geschrieben werden.", NotifyStatus.ERROR, Gui.INFO_DELAY );
253 			Debug.printDebug( "[Settings Error] Critical: Failed to persist configuration profile assets." );
254 		}
255 	}
256 
257 	/**
258 	 * Orchestrates the application teardown sequence by gracefully processing or aborting
259 	 * active tasks based on the execution context boundaries.
260 	 *
261 	 * @param gracePeriod If true, allocates an extended time buffer per thread;
262 	 *                    if false (OS shutdown), enforces tight, rapid deadlines.
263 	 */
264 	private void executeShutdown( final boolean gracePeriod ) {
265 		final long timeout = gracePeriod ? BG_TIMEOUT : EXIT_TIMEOUT;
266 		Debug.printDebug( "[Exit] Internal system teardown invoked. Dynamic grace mode: %b -> %d ms", gracePeriod, timeout );
267 		// Delegate termination orchestration directly to the individual task contexts securely
268 		for( final SyncJobContext job : gui.getJobList() ) {
269 			job.cancelRunningTask( timeout );
270 		}
271 		if( !gui.isShowing() && bgController != null ) bgController.interruptBgJob( timeout );
272 		Debug.printDebug( "[Exit] Core teardown protocol finalized. Flushing runtime buffers." );
273 	}
274 
275 	/**
276 	 * Bootstraps the primary synchronization job registry during application startup.
277 	 * <br>
278 	 * This method attempts to load and deserialize all previously persisted task profiles
279 	 * from the underlying configuration storage. If existing preferences are successfully recovered,
280 	 * they are mapped into operational context instances. If no data is found or initialization
281 	 * fails (e.g., during the first application launch), a pre-configured set of standard fallback
282 	 * profiles is generated to guarantee a seamless out-of-the-box user experience.
283 	 *
284 	 */
285 	private void loadInitialJobList() {
286 		final ObservableList<SyncJobContext> jobList = FXCollections.observableArrayList();
287 
288 		if( manager.loadAllPreferences() ) {
289 			manager.getLoadedProfiles().stream()
290 					.map( entry -> {
291 						final SyncJobContext ctx = new SyncJobContext( entry.getJobName(), entry );
292 						ctx.setSelectedMode( entry.getScanMode().getDescription() );
293 						return ctx;
294 					} )
295 					.forEach( jobList::add );
296 		}else {
297 			jobList.add( new SyncJobContext( "NAS Dokumente", manager.createProfile( "NAS  Dokumente", false ) ) );
298 			jobList.add( new SyncJobContext( "Lokales Workspace Backup", manager.createProfile( "Lokales Workspace Backup", false ) ) );
299 		}
300 		gui.setInitialJobConfigurations( jobList );
301 	}
302 }