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 java.util.Map;
24  
25  import javafx.application.Platform;
26  import javafx.collections.FXCollections;
27  import javafx.collections.ObservableList;
28  import javafx.scene.control.Alert;
29  import javafx.scene.control.ButtonType;
30  import javafx.scene.control.ListCell;
31  import javafx.scene.control.TextInputDialog;
32  
33  import de.spiritscorp.datasync.Main;
34  import de.spiritscorp.datasync.gui.DialogService;
35  import de.spiritscorp.datasync.gui.Gui;
36  import de.spiritscorp.datasync.gui.WorkspaceView.NotifyStatus;
37  import de.spiritscorp.datasync.io.Debug;
38  import de.spiritscorp.datasync.io.Logger;
39  import de.spiritscorp.datasync.io.Preference;
40  import de.spiritscorp.datasync.io.PreferenceManager;
41  import de.spiritscorp.datasync.theme.AppTheme;
42  
43  /**
44   * Central controller implementation executing operational state translations and business action flows.
45   * * @author Tom Spirit
46   */
47  public class MainViewController implements ViewController {
48  
49  	private final Gui gui;
50  	private final SyncJobService helper;
51  	private final PreferenceManager manager;
52  	private BgController activeBgController;
53  
54  	/**
55  	 * Allocates a new controller instance tied directly to the display engine layer hook.
56  	 *
57  	 * @param gui The global display manager orchestrator application shell instance.
58  	 */
59  	public MainViewController( final Gui gui ) {
60  		this(
61  				gui,
62  				new SyncJobService( new DialogService( gui.getWindowStage() ), new UiLogFormatter() ),
63  				PreferenceManager.getInstance() );
64  		loadInitialJobList();
65  	}
66  
67  	MainViewController( final Gui gui, final SyncJobService helper, final PreferenceManager manager ) {
68  		this.gui = gui;
69  		this.helper = helper;
70  		this.manager = manager;
71  	}
72  
73  	@Override
74  	public void registerNativeShutdownHook() {
75  		Runtime.getRuntime().addShutdownHook( new Thread( () -> {
76  			// This block executes automatically if Windows/Linux sends a SIGTERM or shutdown signal
77  			Debug.printDebug( "[Exit] Host operating system shutdown signal intercepted via native runtime hook." );
78  			// Enforce rapid execution with small timeouts since the OS will forcefully kill us shortly
79  			executeCoreShutdownSequence( true );
80  			Debug.printDebug( "[Exit] BYE, BYE" );
81  		}, "DataSync-OS-Shutdown-Hook-Thread" ) );
82  		Debug.printDebug( "[Info] Native OS runtime shutdown hook successfully registered." );
83  	}
84  
85  	@Override
86  	public void handleNavigate( final Gui.ViewState state ) {
87  		gui.setViewState( state );
88  		if( !gui.getWindowStage().isShowing() ) {
89  			gui.getWindowStage().show();
90  			gui.getWindowStage().toFront();
91  		}
92  	}
93  
94  	@Override
95  	public void handleApplicationShutdown() {
96  		final Alert confirmation = new Alert( Alert.AlertType.CONFIRMATION, "Hintergrunddienste werden beendet.", ButtonType.OK, ButtonType.CANCEL );
97  		confirmation.setTitle( "Programm beenden" );
98  		confirmation.setHeaderText( "Möchten Sie DataSync wirklich schließen?" );
99  		confirmation.setContentText( "Aktive Hintergrunddienste werden wenn möglich sauber beendet." );
100 		confirmation.initOwner( gui.getWindowStage() );
101 
102 		if( confirmation.showAndWait().orElse( ButtonType.CANCEL ) == ButtonType.OK ) {
103 			Debug.printDebug( "[Exit] Complete system teardown triggered manually via user confirmation." );
104 
105 			// Tear down the JavaFX UI framework layer immediately so the window closes for the user
106 			Platform.exit();
107 			// Execute executeCoreShutdownSequence in the System.exit() hook
108 			Debug.printDebug( "[Exit] Manual graceful teardown completed. Evicting core JVM runtime context loop." );
109 			System.exit( 0 );
110 		}
111 	}
112 
113 //	@Override
114 	private void loadInitialJobList() {
115 		final ObservableList<SyncJobContext> jobList = FXCollections.observableArrayList();
116 
117 		if( manager.loadAllPreferences() ) {
118 			for( final Map.Entry<String, Preference> entry : manager.getLoadedProfiles().entrySet() ) {
119 				final SyncJobContext ctx = new SyncJobContext( entry.getKey(), entry.getValue() );
120 				ctx.setSelectedMode( entry.getValue().getScanMode().getDescription() );
121 				jobList.add( ctx );
122 			}
123 		}else {
124 			jobList.add( new SyncJobContext( "NAS Dokumenten-Spiegel", manager.createProfile( "NAS Dokus" ) ) );
125 			jobList.add( new SyncJobContext( "Lokales Code-Workspace Backup", manager.createProfile( "WorkspaceRepo" ) ) );
126 		}
127 		gui.setInitialJobConfigurations( jobList );
128 	}
129 
130 	@Override
131 	public void handleCreateNewJob() {
132 		final String name = "Sync Job " + ( gui.getJobList().size() + 1 );
133 		gui.getJobList().add( new SyncJobContext( name, manager.createProfile( name ) ) );
134 	}
135 
136 	@Override
137 	public void handleRenameJob( final ListCell<SyncJobContext> cell ) {
138 		final SyncJobContext selectedJob = cell.getItem();
139 		if( selectedJob != null ) {
140 			final String oldName = selectedJob.getJobName();
141 			final TextInputDialog dialog = new TextInputDialog( selectedJob.getJobName() );
142 			dialog.setTitle( "Task umbenennen" );
143 			dialog.setHeaderText( "Geben Sie einen neuen Namen für den Task ein:" );
144 			dialog.setContentText( "Name:" );
145 			dialog.initOwner( gui.getWindowStage() );
146 			dialog.showAndWait().ifPresent( newName -> {
147 				final String trimmedName = newName.trim();
148 				if( !trimmedName.isEmpty() && !trimmedName.equals( oldName ) ) {
149 					selectedJob.setJobName( trimmedName );
150 					gui.showStatusNotification( oldName + " wurde ersetzt und gespeichert durch" + newName, NotifyStatus.SUCESS, Main.INFO_DELAY );
151 				}else {
152 					gui.showStatusNotification( oldName + " wurde nicht ersetzt", NotifyStatus.WARNING, Main.INFO_DELAY );
153 				}
154 			} );
155 		}
156 	}
157 
158 	@Override
159 	public void handleDuplicateJob( final SyncJobContext job ) {
160 		if( job != null ) {
161 			gui.getJobList().add( new SyncJobContext( job.getJobName() + " (Kopie)", job.getPreference() ) );
162 		}
163 	}
164 
165 	@Override
166 	public void handleDeleteJob( final SyncJobContext job ) {
167 		if( job != null ) {
168 			final Alert alert = new Alert( Alert.AlertType.CONFIRMATION, "Task '" + job.getJobName() + "' wirklich unwiderruflich löschen?", ButtonType.YES, ButtonType.NO );
169 			alert.setTitle( "Task entfernen" );
170 			alert.setHeaderText( null );
171 			alert.initOwner( gui.getWindowStage() );
172 			alert.showAndWait().ifPresent( response -> {
173 				if( response == ButtonType.YES ) {
174 					gui.getJobList().remove( job );
175 					PreferenceManager.getInstance().removeProfile( job );
176 					gui.showStatusNotification( job.getJobName() + " wurde erfolgreich gelöscht", NotifyStatus.SUCESS, Main.INFO_DELAY );
177 				}else {
178 					gui.showStatusNotification( job.getJobName() + " wurde nicht gelöscht", NotifyStatus.WARNING, Main.INFO_DELAY );
179 				}
180 			} );
181 		}
182 	}
183 
184 	@Override
185 	public void handleExecuteTask( final SyncJobContext job ) {
186 		if( job != null ) {
187 			switch( job.getSelectedMode() ) {
188 				case SYNCHRONIZE -> helper.startSynchronize( job );
189 				case DUBLICATE_SCAN -> helper.startDuplicateScan( job );
190 				case DEEP_SCAN, FLAT_SCAN -> helper.startBackup( job );
191 				default -> throw new IllegalArgumentException( "Unexpected value: " + job.getSelectedMode() );
192 			}
193 		}
194 	}
195 
196 	@Override
197 	public void handleStopTask( final SyncJobContext job ) {
198 		job.cancelRunningTask( Main.EXIT_THREAD_TIMEOUT );
199 	}
200 
201 	@Override
202 	public void handleSaveSettings( final Preference localPreferences, final AppTheme targetTheme ) {
203 		if( localPreferences == null ) return;
204 
205 		// Persist structural configuration states securely to disk
206 		final boolean prefsSaved = PreferenceManager.getInstance().saveAllPreferences();
207 		if( prefsSaved ) {
208 			Debug.printDebug( "[Settings] Configuration profile assets successfully serialized to disk." );
209 		}else {
210 			Debug.printDebug( "[Settings] Critical: Failed to persist configuration profile assets." );
211 		}
212 
213 		// Adjust underlying host operating system autostart integration context matrix
214 		final boolean autostartTargetState = PreferenceManager.getInstance().isGlobalAutoStart();
215 		final boolean autostartSaved = helper.setOSAutostart( autostartTargetState );
216 		if( autostartSaved ) {
217 			Debug.printDebug( "[Settings] OS desktop autostart hooks successfully synchronized to state: " + autostartTargetState );
218 		}else {
219 			Debug.printDebug( "[Settings] Warning: Failed to apply host operating system autostart modifications." );
220 		}
221 
222 		gui.changeTheme( targetTheme );
223 		gui.setViewState( Gui.ViewState.SETTINGS );
224 
225 		// Trigger visual feedback via the global GUI proxy method using theme classes
226 		if( prefsSaved && autostartSaved ) {
227 			gui.showStatusNotification( "Settings successfully persisted to the configuration registry.", NotifyStatus.SUCESS, Main.INFO_DELAY );
228 		}else if( !prefsSaved ) {
229 			gui.showStatusNotification( "Error: Failed to write configuration payload to 'conf.json'.", NotifyStatus.ERROR, Main.INFO_DELAY );
230 		}else {
231 			gui.showStatusNotification( "Warning: Profiles saved, but host operating system autostart configuration failed.", NotifyStatus.WARNING, Main.INFO_DELAY );
232 		}
233 	}
234 
235 	@Override
236 	public void runInBackground( final boolean firstStart ) {
237 		activeBgController = new BgController( gui, this, gui.getJobList(), new Logger() );
238 		activeBgController.startBgJob( firstStart );
239 	}
240 
241 	@Override
242 	public void deleteSelectedDuplicates( final SyncJobContext jobContext ) {
243 		helper.deleteSelectedDuplicates( jobContext );
244 	}
245 
246 	/**
247 	 * Orchestrates the application teardown sequence by gracefully processing or aborting
248 	 * active tasks based on the execution context boundaries.
249 	 *
250 	 * @param dynamicGracePeriod If true, allocates an extended time buffer per thread;
251 	 *                           if false (OS shutdown), enforces tight, rapid deadlines.
252 	 */
253 	private void executeCoreShutdownSequence( final boolean dynamicGracePeriod ) {
254 		final long timeoutPerThreadMs = dynamicGracePeriod ? Main.BACKGROUND_THREAD_TIMEOUT : Main.EXIT_THREAD_TIMEOUT;
255 		Debug.printDebug( "[Exit] Internal system teardown invoked. Dynamic grace mode: %b -> %d ms", dynamicGracePeriod, timeoutPerThreadMs );
256 		// Delegate termination orchestration directly to the individual task contexts securely
257 		for( final SyncJobContext job : gui.getJobList() ) {
258 			job.cancelRunningTask( timeoutPerThreadMs );
259 		}
260 		if( !gui.isShowing() && activeBgController != null ) activeBgController.interruptBgJob( timeoutPerThreadMs );
261 		Debug.printDebug( "[Exit] Core teardown protocol finalized. Flushing runtime buffers." );
262 	}
263 }