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.nio.file.Path;
24  import java.util.Map;
25  
26  import de.spiritscorp.datasync.ScanType;
27  import de.spiritscorp.datasync.model.FileAttributes;
28  
29  /**
30   * Formats raw synchronization metrics, file attributes, and processing durations
31   * into localized, human-readable string representations tailored for the user interface.
32   * <p>
33   * This utility decouples presentation and text formatting logic from the core
34   * background threading and data synchronization routines, ensuring a clean separation
35   * of concerns.
36   * </p>
37   *
38   * @author Tom Spirit
39   */
40  @SuppressWarnings( "PMD.LongVariable" )
41  class LogFormatter {
42  
43  	// --- Conversion Constants for Byte Sizes ---
44  	/** The number of bytes required to reach a Gibibyte threshold. */
45  	private static final long BYTES_PER_GIB_THRESHOLD = 1_073_741_824L;
46  	/** The number of bytes required to reach a Mebibyte threshold. */
47  	private static final long BYTES_PER_MIB_THRESHOLD = 1_048_576L;
48  	/** The number of bytes required to reach a Kibibyte threshold. */
49  	private static final long BYTES_PER_KIB_THRESHOLD = 1_024L;
50  	/** Conversion divisor for calculating Mebibyte values. */
51  	private static final double CONVERSION_FACTOR_MIB = 1_048_576.0;
52  	/** Conversion divisor for calculating Kibibyte values. */
53  	private static final double CONVERSION_FACTOR_KIB = 1_024.0;
54  	/** Threshold above which the plural form "bytes" is used. */
55  	private static final long MINIMUM_BYTE_PLURAL_THRESHOLD = 1L;
56  
57  	// --- Conversion Constants for Time Metrics ---
58  	/** Number of nanoseconds in a single standard second. */
59  	private static final double NANOSECONDS_PER_SECOND = 1_000_000_000.0;
60  	/** Total number of seconds representing a two-hour duration. */
61  	private static final int SECONDS_PER_TWO_HOURS = 7200;
62  	/** Total number of seconds representing a one-hour duration. */
63  	private static final int SECONDS_PER_ONE_HOUR = 3600;
64  	/** Total number of seconds representing a single minute. */
65  	private static final int SECONDS_PER_MINUTE = 60;
66  
67  	// --- UI Layout and Processing Boundaries ---
68  	/** Maximum number of raw file entries displayed in the UI log text area. */
69  	private static final int DEFAULT_LOG_DISPLAY_LIMIT = 10_000;
70  	/** Initial number of character displayed in the UI log text area. */
71  	private static final int INITIAL_LOG_BUFFER_SIZE = 2_000;
72  	/** Divider used to calculate the display boundary loop limit. */
73  	private static final int DISPLAY_LIMIT_HALF_DIVIDER = 2;
74  
75  	/**
76  	 * Converts a raw number of bytes into a human-readable string representation using binary prefixes (KiB, MiB, GiB).
77  	 *
78  	 * @param bytes the number of bytes to format
79  	 * @return a formatted string indicating the size with appropriate units
80  	 */
81  	String getReadableBytes( final long bytes ) {
82  		if( bytes > BYTES_PER_GIB_THRESHOLD ) {
83  			return String.format( "%.3f GiB", bytes / CONVERSION_FACTOR_MIB / CONVERSION_FACTOR_KIB );
84  		}else if( bytes > BYTES_PER_MIB_THRESHOLD ) {
85  			return ( bytes / BYTES_PER_MIB_THRESHOLD ) + " MiB";
86  		}else if( bytes > BYTES_PER_KIB_THRESHOLD ) {
87  			return ( bytes / BYTES_PER_KIB_THRESHOLD ) + " KiB";
88  		}else if( bytes > MINIMUM_BYTE_PLURAL_THRESHOLD ) {
89  			return bytes + " bytes";
90  		}else {
91  			return bytes + " byte";
92  		}
93  	}
94  
95  	/**
96  	 * Formats a nanosecond duration into a human-readable runtime string partitioned into hours, minutes, and seconds.
97  	 *
98  	 * @param endTimeNano the elapsed duration in nanoseconds
99  	 * @return a localized string visualizing the total runtime
100 	 */
101 	String getTimeFormatted( final long endTimeNano ) {
102 		final double endTimeSec = endTimeNano / NANOSECONDS_PER_SECOND;
103 
104 		// Einheiten sauber aufteilen
105 		final long totalSeconds = (long) endTimeSec;
106 		final long hours = totalSeconds / SECONDS_PER_ONE_HOUR;
107 		final long minutes = totalSeconds % SECONDS_PER_ONE_HOUR / SECONDS_PER_MINUTE;
108 		final double seconds = endTimeSec % SECONDS_PER_MINUTE;
109 
110 		if( endTimeSec >= SECONDS_PER_TWO_HOURS ) {
111 			return String.format( "%d Stunden %d Minuten %.3f Sekunden", hours, minutes, seconds );
112 		}else if( endTimeSec >= SECONDS_PER_ONE_HOUR ) {
113 			return String.format( "%d Stunde %d Minuten %.3f Sekunden", hours, minutes, seconds );
114 		}else if( endTimeSec >= SECONDS_PER_MINUTE ) {
115 			return String.format( "%d Minuten %.3f Sekunden", minutes, seconds );
116 		}else {
117 			return String.format( "%.3f Sekunden", endTimeSec );
118 		}
119 	}
120 
121 	/**
122 	 * Generates a fully structured and titled overview log text from the scanned file maps,
123 	 * tailoring headers and categories based on the given synchronization execution type.
124 	 *
125 	 * @param scanType  the synchronization or backup operational mode archetype
126 	 * @param sourceMap map of pending allocations determined on the source environment
127 	 * @param destMap   map of pending allocations determined on the target environment
128 	 * @param failMap   map of files that encountered access violations or are marked for deletion
129 	 * @return a comprehensive structured text summary report ready for text area visualization
130 	 */
131 	String formatMaps( final ScanType scanType, final Map<Path, FileAttributes> sourceMap, final Map<Path, FileAttributes> destMap, final Map<Path, FileAttributes> failMap ) {
132 		final String line = System.lineSeparator();
133 		final String scanFinish = "Scan abgeschlossen!\n";
134 		final String tableSep = "------------------------------\n";
135 		final StringBuilder stringBuilder = new StringBuilder( INITIAL_LOG_BUFFER_SIZE );
136 
137 		if( scanType == ScanType.SYNCHRONIZE ) {
138 			stringBuilder.append( scanFinish ).append( "Zu kopieren in das Sourceverzeichnis:\n" ).append( tableSep );
139 			if( sourceMap != null ) {
140 				buildEntries( stringBuilder, sourceMap, DEFAULT_LOG_DISPLAY_LIMIT );
141 			}
142 			stringBuilder.append( line ).append( "Zu kopieren in das Zielverzeichnis:\n" ).append( tableSep );
143 			if( destMap != null ) {
144 				buildEntries( stringBuilder, destMap, DEFAULT_LOG_DISPLAY_LIMIT );
145 			}
146 			if( failMap != null && !failMap.isEmpty() ) {
147 				stringBuilder.append( line ).append( "Zu löschende Dateien:\n" ).append( tableSep );
148 				buildEntries( stringBuilder, failMap, DEFAULT_LOG_DISPLAY_LIMIT );
149 			}
150 		}else {
151 			stringBuilder.append( scanFinish ).append( "Zu kopierende Dateien:\n" ).append( tableSep );
152 			if( sourceMap != null ) {
153 				buildEntries( stringBuilder, sourceMap, DEFAULT_LOG_DISPLAY_LIMIT );
154 			}
155 			stringBuilder.append( line ).append( "Zu löschende Dateien:\n" ).append( tableSep );
156 			if( destMap != null ) {
157 				buildEntries( stringBuilder, destMap, DEFAULT_LOG_DISPLAY_LIMIT );
158 			}
159 			if( failMap != null && !failMap.isEmpty() ) {
160 				stringBuilder.append( line ).append( "Fehlerhafter Zugriff:\n" ).append( tableSep );
161 				buildEntries( stringBuilder, failMap, DEFAULT_LOG_DISPLAY_LIMIT );
162 			}
163 		}
164 		return stringBuilder.toString();
165 	}
166 
167 	/**
168 	 * Iterates over a map of file attributes and appends a detailed structural text summary
169 	 * for each entry to the provided {@code StringBuilder} up to a designated threshold.
170 	 *
171 	 * @param stringBuilder the buffer to append the formatted lines to
172 	 * @param printableMap  the map containing file paths and their associated attributes
173 	 * @param displayLimit  the absolute boundary to prevent UI thread lockups during rendering
174 	 * @return the updated {@code StringBuilder} containing the appended logs
175 	 */
176 	private StringBuilder buildEntries( final StringBuilder stringBuilder, final Map<Path, FileAttributes> printableMap, final int displayLimit ) {
177 		final String valueSeparator = " , ";
178 		int index = 0;
179 		for( final Map.Entry<Path, FileAttributes> entry : printableMap.entrySet() ) {
180 			final FileAttributes value = entry.getValue();
181 			stringBuilder.append( value.getFileName() ).append( valueSeparator )
182 					.append( getReadableBytes( value.getSize() ) ).append( valueSeparator )
183 					.append( value.getModTimeString() ).append( valueSeparator )
184 					.append( value.getCreateTimeString() ).append( valueSeparator )
185 					.append( value.getFileHash() ).append( "     " )
186 					.append( entry.getKey().toString() )
187 					.append( '\n' );
188 
189 			index++;
190 			if( index > ( displayLimit / DISPLAY_LIMIT_HALF_DIVIDER ) ) {
191 				break;
192 			}
193 		}
194 		return stringBuilder;
195 	}
196 
197 }