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  public class UiLogFormatter {
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
77  	 * using binary prefixes (KiB, MiB, GiB).
78  	 *
79  	 * @param bytes the number of bytes to format
80  	 * @return a formatted string indicating the size with appropriate units
81  	 */
82  	String getReadableBytes( final long bytes ) {
83  		if( bytes > BYTES_PER_GIB_THRESHOLD ) {
84  			return String.format( "%.3f GiB", bytes / CONVERSION_FACTOR_MIB / CONVERSION_FACTOR_KIB );
85  		}else if( bytes > BYTES_PER_MIB_THRESHOLD ) {
86  			return ( bytes / BYTES_PER_MIB_THRESHOLD ) + " MiB";
87  		}else if( bytes > BYTES_PER_KIB_THRESHOLD ) {
88  			return ( bytes / BYTES_PER_KIB_THRESHOLD ) + " KiB";
89  		}else if( bytes > MINIMUM_BYTE_PLURAL_THRESHOLD ) {
90  			return bytes + " bytes";
91  		}else {
92  			return bytes + " byte";
93  		}
94  	}
95  
96  	/**
97  	 * Formats a nanosecond duration into a human-readable runtime string
98  	 * partitioned into hours, minutes, and seconds.
99  	 *
100 	 * @param endTimeNano the elapsed duration in nanoseconds
101 	 * @return a localized string visualizing the total runtime
102 	 */
103 	String getEndTimeFormatted( final long endTimeNano ) {
104 		final double endTimeSec = endTimeNano / NANOSECONDS_PER_SECOND;
105 		if( endTimeSec >= SECONDS_PER_TWO_HOURS ) {
106 			return String.format( "%d Stunden %d Minuten %.3f Sekunden Laufzeit", ( (int) endTimeSec ) / SECONDS_PER_ONE_HOUR, ( (int) endTimeSec ) % SECONDS_PER_MINUTE,
107 					endTimeSec % SECONDS_PER_MINUTE );
108 		}else if( endTimeSec >= SECONDS_PER_ONE_HOUR ) {
109 			return String.format( "%d Stunde %d Minuten %.3f Sekunden Laufzeit", ( (int) endTimeSec ) / SECONDS_PER_ONE_HOUR, ( (int) endTimeSec ) % SECONDS_PER_MINUTE,
110 					endTimeSec % SECONDS_PER_MINUTE );
111 		}else if( endTimeSec >= SECONDS_PER_MINUTE ) {
112 			return String.format( "%d Minuten %.3f Sekunden Laufzeit", ( (int) endTimeSec ) / SECONDS_PER_MINUTE, endTimeSec % SECONDS_PER_MINUTE );
113 		}else {
114 			return String.format( "%.3f Sekunden Laufzeit", endTimeSec );
115 		}
116 	}
117 
118 	/**
119 	 * Generates a fully structured and titled overview log text from the scanned file maps,
120 	 * tailoring headers and categories based on the given synchronization execution type.
121 	 *
122 	 * @param scanType  the synchronization or backup operational mode archetype
123 	 * @param sourceMap map of pending allocations determined on the source environment
124 	 * @param destMap   map of pending allocations determined on the target environment
125 	 * @param failMap   map of files that encountered access violations or are marked for deletion
126 	 * @return a comprehensive structured text summary report ready for text area visualization
127 	 */
128 	String formatMaps( final ScanType scanType, final Map<Path, FileAttributes> sourceMap, final Map<Path, FileAttributes> destMap, final Map<Path, FileAttributes> failMap ) {
129 		final String line = System.lineSeparator();
130 		final String scanFinish = "Scan abgeschlossen!\n";
131 		final String tableSep = "------------------------------\n";
132 		final StringBuilder stringBuilder = new StringBuilder( INITIAL_LOG_BUFFER_SIZE );
133 
134 		if( scanType == ScanType.SYNCHRONIZE ) {
135 			stringBuilder.append( scanFinish ).append( "Zu kopieren in das Sourceverzeichnis:\n" ).append( tableSep );
136 			if( sourceMap != null ) {
137 				buildEntries( stringBuilder, sourceMap, DEFAULT_LOG_DISPLAY_LIMIT );
138 			}
139 			stringBuilder.append( line ).append( "Zu kopieren in das Zielverzeichnis:\n" ).append( tableSep );
140 			if( destMap != null ) {
141 				buildEntries( stringBuilder, destMap, DEFAULT_LOG_DISPLAY_LIMIT );
142 			}
143 			if( failMap != null && !failMap.isEmpty() ) {
144 				stringBuilder.append( line ).append( "Zu löschende Dateien:\n" ).append( tableSep );
145 				buildEntries( stringBuilder, failMap, DEFAULT_LOG_DISPLAY_LIMIT );
146 			}
147 		}else {
148 			stringBuilder.append( scanFinish ).append( "Zu kopierende Dateien:\n" ).append( tableSep );
149 			if( sourceMap != null ) {
150 				buildEntries( stringBuilder, sourceMap, DEFAULT_LOG_DISPLAY_LIMIT );
151 			}
152 			stringBuilder.append( line ).append( "Zu löschende Dateien:\n" ).append( tableSep );
153 			if( destMap != null ) {
154 				buildEntries( stringBuilder, destMap, DEFAULT_LOG_DISPLAY_LIMIT );
155 			}
156 			if( failMap != null && !failMap.isEmpty() ) {
157 				stringBuilder.append( line ).append( "Fehlerhafter Zugriff:\n" ).append( tableSep );
158 				buildEntries( stringBuilder, failMap, DEFAULT_LOG_DISPLAY_LIMIT );
159 			}
160 		}
161 		return stringBuilder.toString();
162 	}
163 
164 	/**
165 	 * Iterates over a map of file attributes and appends a detailed structural text summary
166 	 * for each entry to the provided {@code StringBuilder} up to a designated threshold.
167 	 *
168 	 * @param stringBuilder the buffer to append the formatted lines to
169 	 * @param printableMap  the map containing file paths and their associated attributes
170 	 * @param displayLimit  the absolute boundary to prevent UI thread lockups during rendering
171 	 * @return the updated {@code StringBuilder} containing the appended logs
172 	 */
173 	private StringBuilder buildEntries( final StringBuilder stringBuilder, final Map<Path, FileAttributes> printableMap, final int displayLimit ) {
174 		final String valueSeparator = " , ";
175 		int index = 0;
176 		for( final Map.Entry<Path, FileAttributes> entry : printableMap.entrySet() ) {
177 			final FileAttributes value = entry.getValue();
178 			stringBuilder.append( value.getFileName() ).append( valueSeparator )
179 					.append( getReadableBytes( value.getSize() ) ).append( valueSeparator )
180 					.append( value.getModTimeString() ).append( valueSeparator )
181 					.append( value.getCreateTimeString() ).append( valueSeparator )
182 					.append( value.getFileHash() ).append( "     " )
183 					.append( entry.getKey().toString() )
184 					.append( '\n' );
185 
186 			index++;
187 			if( index > ( displayLimit / DISPLAY_LIMIT_HALF_DIVIDER ) ) {
188 				break;
189 			}
190 		}
191 		return stringBuilder;
192 	}
193 
194 }