001package jmri.jmrit.operations.rollingstock; 002 003import java.beans.PropertyChangeEvent; 004import java.beans.PropertyChangeListener; 005import java.io.*; 006import java.nio.charset.StandardCharsets; 007import java.time.LocalDateTime; 008import java.time.format.DateTimeFormatter; 009import java.util.Arrays; 010import java.util.List; 011 012import jmri.InstanceManager; 013import jmri.InstanceManagerAutoDefault; 014import jmri.jmrit.XmlFile; 015import jmri.jmrit.operations.OperationsXml; 016import jmri.jmrit.operations.rollingstock.cars.Car; 017import jmri.jmrit.operations.rollingstock.cars.CarManager; 018import jmri.jmrit.operations.rollingstock.engines.Engine; 019import jmri.jmrit.operations.rollingstock.engines.EngineManager; 020import jmri.jmrit.operations.setup.*; 021 022import org.apache.commons.csv.CSVFormat; 023import org.apache.commons.csv.CSVPrinter; 024import org.slf4j.Logger; 025import org.slf4j.LoggerFactory; 026 027/** 028 * Logs rolling stock movements by writing their locations to a file. 029 * 030 * @author Daniel Boudreau Copyright (C) 2010, 2016 031 */ 032public class RollingStockLogger extends XmlFile implements InstanceManagerAutoDefault, PropertyChangeListener { 033 034 private boolean engLog = false; // when true logging engine movements 035 private boolean carLog = false; // when true logging car movements 036 037 public RollingStockLogger() { 038 // nothing to do 039 } 040 041 public void enableCarLogging(boolean enable) { 042 if (enable) { 043 addCarListeners(); 044 } else { 045 removeCarListeners(); 046 } 047 } 048 049 public void enableEngineLogging(boolean enable) { 050 if (enable) { 051 addEngineListeners(); 052 } else { 053 removeEngineListeners(); 054 } 055 } 056 057 private boolean mustHaveTrack = true; // when true only updates that have a track are saved 058 059 private void store(RollingStock rs) { 060 061 if (rs.getTrack() == null && mustHaveTrack) { 062 return; 063 } 064 065 String carLoad = ""; 066 String carFinalDest = ""; 067 String carFinalDestTrack = ""; 068 if (Car.class.isInstance(rs)) { 069 Car car = (Car) rs; 070 carLoad = car.getLoadName(); 071 carFinalDest = car.getFinalDestinationName(); 072 carFinalDestTrack = car.getFinalDestinationTrackName(); 073 } 074 075 List<Object> line = Arrays.asList(new Object[]{rs.getNumber(), 076 rs.getRoadName(), 077 rs.getTypeName(), 078 carLoad, 079 rs.getLocationName(), 080 rs.getTrackName(), 081 carFinalDest, 082 carFinalDestTrack, 083 rs.getTrainName(), 084 rs.getMoves(), 085 getTime()}); 086 087 fileOut(line); // append line to common file 088 fileOut(line, rs); // append line to individual file 089 } 090 091 /* 092 * Appends one line to common log file. 093 */ 094 private void fileOut(List<Object> line) { 095 fileOut(line, getFile()); 096 } 097 098 /* 099 * Appends one line to the rolling stock's individual file. 100 */ 101 private void fileOut(List<Object> line, RollingStock rs) { 102 fileOut(line, getFile(rs)); 103 } 104 105 private void fileOut(List<Object> line, File file) { 106 // FileOutputStream is set to append 107 try (CSVPrinter fileOut = new CSVPrinter(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, true), 108 StandardCharsets.UTF_8)), CSVFormat.DEFAULT)) { 109 log.debug("Log: {}", line); 110 fileOut.printRecord(line); 111 } catch (IOException e) { 112 log.error("Exception while opening log file: {}", e.getLocalizedMessage()); 113 } 114 } 115 116 /* 117 * Returns the common log file for all rolling stock 118 */ 119 private File getFile() { 120 File fileLogger = null; 121 if (Setup.isEngineLoggerEnabled() || Setup.isCarLoggerEnabled()) { 122 try { 123 if (!checkFile(getFullLoggerFileName())) { 124 // The file/directory does not exist, create it before writing 125 fileLogger = new java.io.File(getFullLoggerFileName()); 126 File parentDir = fileLogger.getParentFile(); 127 if (!parentDir.exists()) { 128 if (!parentDir.mkdirs()) { 129 log.error("logger directory not created"); 130 } 131 } 132 if (fileLogger.createNewFile()) { 133 log.debug("new file created"); 134 // add header 135 fileOut(getHeader()); 136 } 137 } else { 138 fileLogger = new java.io.File(getFullLoggerFileName()); 139 } 140 } catch (IOException e) { 141 log.error("Exception while making logging directory: {}", e.getLocalizedMessage()); 142 } 143 } 144 return fileLogger; 145 } 146 147 private List<Object> getHeader() { 148 return Arrays.asList(new Object[]{Bundle.getMessage("Number"), 149 Bundle.getMessage("Road"), 150 Bundle.getMessage("Type"), 151 Bundle.getMessage("Load"), 152 Bundle.getMessage("Location"), 153 Bundle.getMessage("Track"), 154 Bundle.getMessage("FinalDestination"), 155 Bundle.getMessage("Track"), 156 Bundle.getMessage("Train"), 157 Bundle.getMessage("Moves"), 158 Bundle.getMessage("DateAndTime")}); 159 } 160 161 /* 162 * Gets the individual log file for a specific car or loco. 163 */ 164 private File getFile(RollingStock rs) { 165 File file = null; 166 if (Setup.isEngineLoggerEnabled() || Setup.isCarLoggerEnabled()) { 167 // create the logging file for this rolling stock 168 try { 169 if (!checkFile(getFullLoggerFileName(rs))) { 170 // The file/directory does not exist, create it before writing 171 file = new java.io.File(getFullLoggerFileName(rs)); 172 File parentDir = file.getParentFile(); 173 if (!parentDir.exists()) { 174 if (!parentDir.mkdirs()) { 175 log.error("logger directory not created"); 176 } 177 } 178 if (file.createNewFile()) { 179 log.debug("new file created"); 180 // add header 181 fileOut(getHeader(), rs); 182 } 183 } else { 184 file = new java.io.File(getFullLoggerFileName(rs)); 185 } 186 } catch (IOException e) { 187 log.error("Exception while making logging directory: {}", e.getLocalizedMessage()); 188 } 189 } 190 return file; 191 } 192 193 public String getFullLoggerFileName() { 194 return loggingDirectory + File.separator + getFileName(); 195 } 196 197 private String operationsDirectory = 198 OperationsSetupXml.getFileLocation() + OperationsSetupXml.getOperationsDirectoryName(); 199 200 private String loggingDirectory = operationsDirectory + File.separator + "logger"; // NOI18N 201 202 public String getDirectoryName() { 203 return loggingDirectory; 204 } 205 206 public void setDirectoryName(String name) { 207 loggingDirectory = name; 208 } 209 210 // Use the same common file even if the session crosses midnight 211 private String fileName; 212 213 public String getFileName() { 214 if (fileName == null) { 215 fileName = getDate() + ".csv"; // NOI18N 216 } 217 return fileName; 218 } 219 220 /** 221 * Individual files for each rolling stock stored in a directory called 222 * "rollingStock" inside the "logger" directory. 223 * @param rs The RollingStock to log. 224 * @return Full path name of log file. 225 * 226 */ 227 public String getFullLoggerFileName(RollingStock rs) { 228 if (!OperationsXml.checkFileName(rs.toString())) { // NOI18N 229 log.error("Rolling stock name ({}) must not contain reserved characters", rs); 230 return loggingDirectory + File.separator + "rollingStock" + File.separator + "ERROR" + ".csv"; // NOI18N 231 } 232 String rsName = rs.toString(); 233 // put clones in the same file as original car 234 if (rs.isClone()) { 235 rsName = rs.getRoadName() + " " + rs.getNumber().split(Car.CLONE_REGEX)[0]; 236 } 237 return loggingDirectory + File.separator + "rollingStock" + File.separator + rsName + ".csv"; // NOI18N 238 } 239 240 private String getDate() { 241 LocalDateTime date = LocalDateTime.now(); 242 DateTimeFormatter simpleDateFormat = DateTimeFormatter.ofPattern("yyyy_MM_dd"); // NOI18N 243 return date.format(simpleDateFormat); 244 } 245 246 /** 247 * Return the date and time in an MS Excel friendly format yyyy/MM/dd 248 * HH:mm:ss 249 * 250 */ 251 private String getTime() { 252 LocalDateTime date = LocalDateTime.now(); 253 DateTimeFormatter simpleDateFormat = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"); // NOI18N 254 return date.format(simpleDateFormat); 255 } 256 257 private void addCarListeners() { 258 if (Setup.isCarLoggerEnabled() && !carLog) { 259 log.debug("Rolling Stock Logger adding car listerners"); 260 carLog = true; 261 List<Car> cars = InstanceManager.getDefault(CarManager.class).getList(); 262 cars.forEach(car -> car.addPropertyChangeListener(this)); 263 // listen for new rolling stock being added 264 InstanceManager.getDefault(CarManager.class).addPropertyChangeListener(this); 265 } 266 } 267 268 private void addEngineListeners() { 269 if (Setup.isEngineLoggerEnabled() && !engLog) { 270 engLog = true; 271 log.debug("Rolling Stock Logger adding engine listerners"); 272 List<Engine> engines = InstanceManager.getDefault(EngineManager.class).getList(); 273 engines.forEach(engine -> engine.addPropertyChangeListener(this)); 274 // listen for new rolling stock being added 275 InstanceManager.getDefault(EngineManager.class).addPropertyChangeListener(this); 276 } 277 } 278 279 private void removeCarListeners() { 280 if (carLog) { 281 log.debug("Rolling Stock Logger removing car listerners"); 282 List<Car> cars = InstanceManager.getDefault(CarManager.class).getList(); 283 cars.forEach(car -> car.removePropertyChangeListener(this)); 284 InstanceManager.getDefault(CarManager.class).removePropertyChangeListener(this); 285 } 286 carLog = false; 287 } 288 289 private void removeEngineListeners() { 290 if (engLog) { 291 log.debug("Rolling Stock Logger removing engine listerners"); 292 List<Engine> engines = InstanceManager.getDefault(EngineManager.class).getList(); 293 engines.forEach(engine -> engine.removePropertyChangeListener(this)); 294 InstanceManager.getDefault(EngineManager.class).removePropertyChangeListener(this); 295 } 296 engLog = false; 297 } 298 299 public void dispose() { 300 removeCarListeners(); 301 removeEngineListeners(); 302 } 303 304 @Override 305 public void propertyChange(PropertyChangeEvent e) { 306 if (e.getPropertyName().equals(RollingStock.TRACK_CHANGED_PROPERTY)) { 307 if (Control.SHOW_PROPERTY) { 308 log.debug("Logger sees property change for car {}", e.getSource()); 309 } 310 store((RollingStock) e.getSource()); 311 } 312 if (e.getPropertyName().equals(RollingStockManager.LISTLENGTH_CHANGED_PROPERTY)) { 313 if ((Integer) e.getNewValue() > (Integer) e.getOldValue()) { 314 // a car or engine has been added 315 if (e.getSource().getClass().equals(CarManager.class)) { 316 removeCarListeners(); 317 addCarListeners(); 318 } else if (e.getSource().getClass().equals(EngineManager.class)) { 319 removeEngineListeners(); 320 addEngineListeners(); 321 } 322 } 323 } 324 } 325 326 private static final Logger log = LoggerFactory.getLogger(RollingStockLogger.class); 327}