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