001package jmri.jmrit.operations.trains; 002 003import java.beans.PropertyChangeEvent; 004import java.beans.PropertyChangeListener; 005import java.io.*; 006import java.nio.charset.StandardCharsets; 007import java.text.ParseException; 008import java.text.SimpleDateFormat; 009import java.time.LocalDateTime; 010import java.time.format.DateTimeFormatter; 011import java.util.*; 012 013import jmri.InstanceManager; 014import jmri.InstanceManagerAutoDefault; 015import jmri.jmrit.XmlFile; 016import jmri.jmrit.operations.setup.*; 017import jmri.jmrit.operations.setup.backup.AutoBackup; 018import jmri.jmrit.operations.setup.backup.DefaultBackup; 019 020import org.apache.commons.csv.CSVFormat; 021import org.apache.commons.csv.CSVPrinter; 022import org.slf4j.Logger; 023import org.slf4j.LoggerFactory; 024 025/** 026 * Logs train movements and status to a file. 027 * 028 * @author Daniel Boudreau Copyright (C) 2010, 2013, 2024 029 */ 030public class TrainLogger extends XmlFile implements InstanceManagerAutoDefault, PropertyChangeListener { 031 032 File _fileLogger; 033 private boolean _trainLog = false; // when true logging train movements 034 035 public TrainLogger() { 036 } 037 038 public void enableTrainLogging(boolean enable) { 039 if (enable) { 040 addTrainListeners(); 041 } else { 042 removeTrainListeners(); 043 } 044 } 045 046 private void createFile() { 047 if (!Setup.isTrainLoggerEnabled()) { 048 return; 049 } 050 if (_fileLogger != null) { 051 return; // log file has already been created 052 } // create the logging file for this session 053 try { 054 if (!checkFile(getFullLoggerFileName())) { 055 // The file/directory does not exist, create it before writing 056 _fileLogger = new java.io.File(getFullLoggerFileName()); 057 File parentDir = _fileLogger.getParentFile(); 058 if (!parentDir.exists()) { 059 if (!parentDir.mkdirs()) { 060 log.error("logger directory not created"); 061 } 062 } 063 if (_fileLogger.createNewFile()) { 064 log.debug("new file created"); 065 // add header 066 fileOut(getHeader()); 067 } 068 } else { 069 _fileLogger = new java.io.File(getFullLoggerFileName()); 070 } 071 } catch (Exception e) { 072 log.error("Exception while making logging directory", e); 073 } 074 075 } 076 077 private void store(Train train) { 078 // create train file if needed 079 createFile(); 080 // Note that train status can contain a comma 081 List<Object> line = Arrays.asList(new Object[]{train.getName(), 082 train.getDescription(), 083 train.getCurrentLocationName(), 084 train.getNextLocationName(), 085 train.getNumberCarsInTrain(), 086 train.getNumberCarsPickedUp(), 087 train.getNumberCarsSetout(), 088 train.getTrainLength(), 089 train.getTrainWeight(), 090 train.getStatus(), 091 train.getBuildFailedMessage(), 092 getDateAndTime()}); 093 fileOut(line); 094 } 095 096 ResourceBundle rb = ResourceBundle 097 .getBundle("jmri.jmrit.operations.setup.JmritOperationsSetupBundle"); 098 099 /* 100 * Adds a status line to the log file whenever the trains file is saved. 101 */ 102 private void storeFileSaved() { 103 List<Object> line = Arrays.asList(new Object[]{ 104 Bundle.getMessage("TrainLogger"), // train name 105 "", // train description 106 "", // current location 107 "", // next location name 108 "", // cars 109 "", // pulls 110 "", // drops 111 "", // length 112 "", // weight 113 Setup.isAutoSaveEnabled() ? rb.getString("AutoSave") : Bundle.getMessage("Manual"), // status 114 Bundle.getMessage("TrainsSaved"), // build messages 115 getDateAndTime()}); 116 fileOut(line); 117 } 118 119 private void storeBackupChanged(PropertyChangeEvent e) { 120 // create train file if needed 121 createFile(); 122 List<Object> line = Arrays.asList(new Object[]{ 123 Bundle.getMessage("TrainLogger"), // train name 124 "", // train description 125 "", // current location 126 "", // next location name 127 "", // cars 128 "", // pulls 129 e.getPropertyName(), // drops 130 "from:", // length 131 e.getOldValue(), // weight 132 "to:", // status 133 e.getNewValue(), // build messages 134 getDateAndTime()}); 135 fileOut(line); 136 } 137 138 private List<Object> getHeader() { 139 return Arrays.asList(new Object[]{Bundle.getMessage("Name"), 140 Bundle.getMessage("Description"), 141 Bundle.getMessage("Current"), 142 Bundle.getMessage("NextLocation"), 143 Bundle.getMessage("Cars"), 144 Bundle.getMessage("Pulls"), 145 Bundle.getMessage("Drops"), 146 Bundle.getMessage("Length"), 147 Bundle.getMessage("Weight"), 148 Bundle.getMessage("Status"), 149 Bundle.getMessage("BuildMessages"), 150 Bundle.getMessage("DateAndTime")}); 151 } 152 153 /* 154 * Appends one line to file. 155 */ 156 private void fileOut(List<Object> line) { 157 if (_fileLogger == null) { 158 log.error("Log file doesn't exist"); 159 return; 160 } 161 162 // FileOutputStream is set to append 163 try (CSVPrinter fileOut = new CSVPrinter(new BufferedWriter(new OutputStreamWriter( 164 new FileOutputStream(_fileLogger, true), StandardCharsets.UTF_8)), CSVFormat.DEFAULT)) { 165 log.debug("Log: {}", line); 166 fileOut.printRecord(line); 167 fileOut.flush(); 168 fileOut.close(); 169 } catch (IOException e) { 170 log.error("Exception while opening log file: {}", e.getLocalizedMessage()); 171 } 172 } 173 174 private void addTrainListeners() { 175 if (Setup.isTrainLoggerEnabled() && !_trainLog) { 176 log.debug("Train Logger adding train listerners"); 177 _trainLog = true; 178 List<Train> trains = InstanceManager.getDefault(TrainManager.class).getList(); 179 trains.forEach(train -> train.addPropertyChangeListener(this)); 180 // listen for new trains being added 181 InstanceManager.getDefault(TrainManager.class).addPropertyChangeListener(this); 182 // listen for backup file changes 183 InstanceManager.getDefault(DefaultBackup.class).addPropertyChangeListener(this); 184 InstanceManager.getDefault(AutoBackup.class).addPropertyChangeListener(this); 185 } 186 } 187 188 private void removeTrainListeners() { 189 log.debug("Train Logger removing train listerners"); 190 if (_trainLog) { 191 List<Train> trains = InstanceManager.getDefault(TrainManager.class).getList(); 192 trains.forEach(train -> train.removePropertyChangeListener(this)); 193 InstanceManager.getDefault(TrainManager.class).removePropertyChangeListener(this); 194 InstanceManager.getDefault(DefaultBackup.class).removePropertyChangeListener(this); 195 InstanceManager.getDefault(AutoBackup.class).removePropertyChangeListener(this); 196 } 197 _trainLog = false; 198 } 199 200 public void dispose() { 201 removeTrainListeners(); 202 } 203 204 @Override 205 public void propertyChange(PropertyChangeEvent e) { 206 if (e.getPropertyName().equals(Train.TRAIN_CURRENT_CHANGED_PROPERTY) && e.getNewValue() != null || 207 e.getPropertyName().equals(Train.STATUS_CHANGED_PROPERTY) && 208 (e.getNewValue().equals(Train.TRAIN_RESET) || 209 e.getNewValue().equals(Train.BUILDING) || 210 e.getNewValue().equals(Train.BUILD_FAILED) || 211 e.getNewValue().toString().startsWith(Train.TERMINATED))) { 212 if (Control.SHOW_PROPERTY) { 213 log.debug("Train logger sees property change for train ({}), property name: {}", e.getSource(), 214 e.getPropertyName()); 215 } 216 store((Train) e.getSource()); 217 } 218 if (e.getPropertyName().equals(TrainManager.LISTLENGTH_CHANGED_PROPERTY)) { 219 if ((Integer) e.getNewValue() > (Integer) e.getOldValue()) { 220 // a car or engine has been added 221 removeTrainListeners(); 222 addTrainListeners(); 223 } 224 } 225 if (e.getPropertyName().equals(TrainManager.TRAINS_SAVED_PROPERTY)) { 226 storeFileSaved(); 227 } 228 if (e.getPropertyName().equals(DefaultBackup.COPY_FILES_CHANGED_PROPERTY)) { 229 storeBackupChanged(e); 230 } 231 } 232 233 public String getFullLoggerFileName() { 234 return loggingDirectory + File.separator + getFileName(); 235 } 236 237 private String operationsDirectory = 238 OperationsSetupXml.getFileLocation() + OperationsSetupXml.getOperationsDirectoryName(); 239 private String loggingDirectory = operationsDirectory + File.separator + "logger" + File.separator + "trains"; // NOI18N 240 241 public String getDirectoryName() { 242 return loggingDirectory; 243 } 244 245 public void setDirectoryName(String name) { 246 loggingDirectory = name; 247 } 248 249 private String fileName; 250 251 public String getFileName() { 252 if (fileName == null) { 253 fileName = Bundle.getMessage("Trains") + "_" + getDate() + ".csv"; // NOI18N 254 } 255 return fileName; 256 } 257 258 private String getDate() { 259 LocalDateTime date = LocalDateTime.now(); 260 DateTimeFormatter simpleDateFormat = DateTimeFormatter.ofPattern("yyyy_MM_dd"); // NOI18N 261 return date.format(simpleDateFormat); 262 } 263 264 /** 265 * Return the date and time in an MS Excel friendly format yyyy/MM/dd 266 * HH:mm:ss 267 */ 268 private String getDateAndTime() { 269 String time = Calendar.getInstance().getTime().toString(); 270 SimpleDateFormat dt = new SimpleDateFormat("EEE MMM dd HH:mm:ss z yyyy"); // NOI18N 271 SimpleDateFormat dtout = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); // NOI18N 272 try { 273 return dtout.format(dt.parse(time)); 274 } catch (ParseException e) { 275 return time; // there was an issue, use the old format 276 } 277 } 278 279 private static final Logger log = LoggerFactory.getLogger(TrainLogger.class); 280}