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.BufferedWriter;
24  import java.io.IOException;
25  import java.nio.file.Files;
26  import java.nio.file.Path;
27  import java.nio.file.StandardOpenOption;
28  import java.time.LocalDateTime;
29  import java.time.ZoneId;
30  import java.time.format.DateTimeFormatter;
31  import java.util.ArrayList;
32  import java.util.List;
33  import java.util.Objects;
34  import java.util.concurrent.locks.ReentrantLock;
35  
36  import jakarta.json.Json;
37  import jakarta.json.JsonArray;
38  import jakarta.json.JsonException;
39  import jakarta.json.JsonObject;
40  
41  import de.spiritscorp.datasync.model.FileAttributes;
42  
43  /**
44   * High-performance background logger using JSON Lines format (NDJSON).
45   * Features an automated, size-based log rotation upon initialization.
46   * Designed with Dependency Injection for optimal testability.
47   */
48  public class Logger {
49  
50  	/** Formatter for generating standardized German timestamp strings. */
51  	private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern( "dd.MM.yyyy  HH:mm:ss" );
52  
53  	/** The absolute destination path of the active log file. */
54  	private final Path baseLogPath;
55  
56  	/** Thread-safe internal memory cache containing unwritten log entries. */
57  	private final List<JsonArray> logCache = new ArrayList<>();
58  
59  	/** Concurrency lock ensuring atomic operations across background threads. */
60  	private final ReentrantLock threadLock = new ReentrantLock();
61  
62  	/**
63  	 * Public default constructor utilizing production configurations.
64  	 * Fetches the default log path and enforces a 10 MB retention limit with 5 backups.
65  	 */
66  	public Logger() {
67  		this(
68  				PreferenceManager.getInstance().getLogPath(),
69  				new Logrotater(
70  						10_485_760L, // 10 MB
71  						5 ) );
72  	}
73  
74  	/**
75  	 * Initializes the logger and executes an immediate size-based log rotation check.
76  	 *
77  	 * @param baseLogPath    The primary path to the active log file
78  	 * @param maxFileSize    The maximum allowed file size in bytes before rotation triggers
79  	 * @param maxBackupIndex The maximum number of archived log files to retain
80  	 * @throws NullPointerException if baseLogPath is null
81  	 */
82  	Logger( final Path baseLogPath, final Logrotater rotater ) {
83  		this.baseLogPath = Objects.requireNonNull( baseLogPath, "baseLogPath must not be null" );
84  
85  		rotater.executeLogRotationIfNeeded( baseLogPath );
86  	}
87  
88  	/**
89  	 * Sets a new log entry and queues it inside the volatile internal cache.
90  	 *
91  	 * @param filePath       The path where the file is/was located
92  	 * @param changeStatus   The status representing the change event
93  	 * @param fileAttributes The structural attributes of the file
94  	 */
95  	public void setEntry( final String filePath, final String changeStatus, final FileAttributes fileAttributes ) {
96  		threadLock.lock();
97  		try {
98  			final JsonObject jsonObject = Json.createObjectBuilder()
99  					.add( "Dateiname", fileAttributes.getFileName() )
100 					.add( "erstellt", fileAttributes.getCreateTimeString() )
101 					.add( "zuletzt modifiziert", fileAttributes.getModTimeString() )
102 					.add( "Größe", fileAttributes.getSize() )
103 					.add( "Fingerabdruck", ( fileAttributes.getFileHash() == null ) ? "null" : fileAttributes.getFileHash() )
104 					.build();
105 
106 			final JsonArray jsonArray = Json.createArrayBuilder()
107 					.add( filePath )
108 					.add( LocalDateTime.now( ZoneId.systemDefault() ).format( DATE_FORMATTER ) )
109 					.add( changeStatus )
110 					.add( jsonObject )
111 					.build();
112 
113 			logCache.add( jsonArray );
114 		}finally {
115 			threadLock.unlock();
116 		}
117 	}
118 
119 	/**
120 	 * Writes the cached log entries to the file system using an efficient O(1) append strategy.
121 	 * Avoids loading existing files into memory, keeping the footprint static.
122 	 */
123 	public void printStatus() {
124 		threadLock.lock();
125 		try {
126 			if( logCache.isEmpty() ) { return; }
127 
128 			try( BufferedWriter writer = Files.newBufferedWriter(
129 					baseLogPath,
130 					StandardOpenOption.CREATE,
131 					StandardOpenOption.APPEND ) ) {
132 
133 				for( final JsonArray logEntry : logCache ) {
134 					writer.write( logEntry.toString() );
135 					writer.newLine();
136 				}
137 
138 				logCache.clear();
139 			}catch( final IOException exception ) {
140 				Debug.printDebug( "[Logger] can´t write log file at -> %s", baseLogPath );
141 				Debug.printException( this.getClass(), exception );
142 			}
143 		}finally {
144 			threadLock.unlock();
145 		}
146 	}
147 
148 	/**
149 	 * Reads the log file sequentially and parses the JSON lines.
150 	 * Returns the entries in reverse order, positioning the newest events at the top for UI representation.
151 	 *
152 	 * @return A list containing all logged structures ordered from newest to oldest
153 	 */
154 	public List<JsonArray> readLogForGui() {
155 		final List<JsonArray> invertedGuiList = new ArrayList<>();
156 
157 		if( !Files.exists( baseLogPath ) ) { return invertedGuiList; }
158 
159 		threadLock.lock();
160 		String currentLine = "";
161 		try {
162 			final List<String> lines = Files.readAllLines( baseLogPath );
163 			for( int i = lines.size() - 1; i >= 0; i-- ) {
164 				currentLine = lines.get( i ).trim();
165 				if( currentLine.isEmpty() ) continue;
166 				invertedGuiList.add( Json.createArrayBuilder().add( currentLine ).build() );
167 			}
168 		}catch( final JsonException exception ) {
169 			Debug.printDebug( "[Logger] Invalid JSON in log line: %s", currentLine );
170 			Debug.printException( this.getClass(), exception );
171 		}catch( final IOException exception ) {
172 			Debug.printDebug( "[Logger] can´t read log file at -> %s", baseLogPath );
173 			Debug.printException( this.getClass(), exception );
174 		}finally {
175 			threadLock.unlock();
176 		}
177 		return invertedGuiList;
178 	}
179 }