001package jmri.configurexml; 002 003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 004 005import java.awt.GraphicsEnvironment; 006import java.awt.event.ActionEvent; 007import java.io.BufferedReader; 008import java.io.File; 009import java.io.FileInputStream; 010import java.io.InputStreamReader; 011import java.util.UUID; 012 013import javax.swing.AbstractAction; 014import javax.swing.JFileChooser; 015 016import jmri.*; 017 018/** 019 * Determine if there have been changes made to the PanelPro data. If so, then a prompt will 020 * be displayed to store the data before the JMRI shutdown process proceeds. 021 * <p> 022 * If the JMRI application is DecoderPro, the checking does not occur. If the PanelPro tables 023 * contain only 3 time related beans and no panels, the checking does not occur. 024 * <p> 025 * The main check process uses the checkFile process which is used by the load and store tests. 026 * The current configuration is stored to a temporary file. This temp file is compared to the file 027 * that was loaded manually or via a start up action. If there are differences and the 028 * shutdown store check preference is enabled, a store request prompt is displayed. The 029 * prompt does not occur when running in headless mode. 030 * 031 * @author Dave Sand Copyright (c) 2022 032 */ 033public class StoreAndCompare extends AbstractAction { 034 035 public StoreAndCompare() { 036 this("Store and Compare"); // NOI18N 037 } 038 039 public StoreAndCompare(String s) { 040 super(s); 041 } 042 043 private static ShutdownPreferences _preferences = jmri.InstanceManager.getDefault(ShutdownPreferences.class); 044 045 @Override 046 public void actionPerformed(ActionEvent e) { 047 requestStoreIfNeeded(); 048 } 049 050 public static void requestStoreIfNeeded() { 051 if (Application.getApplicationName().equals("PanelPro")) { 052 if (_preferences.isStoreCheckEnabled()) { 053 if (dataHasChanged() && !GraphicsEnvironment.isHeadless()) { 054 jmri.configurexml.swing.StoreAndCompareDialog.showDialog(); 055 } 056 } 057 } 058 } 059 060 public static boolean dataHasChanged() { 061 var result = false; 062 063 // Get file 1 :: This will be the file used to load the layout data. 064 JFileChooser chooser = LoadStoreBaseAction.getUserFileChooser(); 065 File file1 = chooser.getSelectedFile(); 066 if (file1 == null) { 067 // No file loaded, check for possible additions. 068 return noFileChecks(); 069 } 070 071 // Get file 2 :: This is the default tmp directory with a random xml file name. 072 var tempDir = System.getProperty("java.io.tmpdir") + File.separator; 073 var fileName = UUID.randomUUID().toString(); 074 File file2 = new File(tempDir + fileName + ".xml"); 075 076 // Store the current data using the temp file. 077 jmri.ConfigureManager cm = jmri.InstanceManager.getNullableDefault(jmri.ConfigureManager.class); 078 if (cm != null) { 079 boolean stored = cm.storeUser(file2); 080 log.debug("temp file '{}' stored :: {}", file2, stored); 081 082 try { 083 result = checkFile(file1, file2); 084 } catch (Exception ex) { 085 log.debug("checkFile exception: ", ex); 086 } 087 088 if (!file2.delete()) { 089 log.warn("An error occurred while deleting temporary file {}", file2.getPath()); 090 } 091 } 092 093 return result; 094 } 095 096 /** 097 * When a file has not been loaded, there might be items that should be stored. This check 098 * is not exhaustive. 099 * <p> 100 * If ISCLOCKRUNNING is the only sensor, that is not considered a change. This also applies 101 * to the IMCURRENTTIME and IMRATEFACTOR memories. 102 * @return true if notification should occur. 103 */ 104 @SuppressFBWarnings(value = {"RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE"}, 105 justification = 106 "spotbugs did not like the protection provided by the result boolean, but the second test was declared redundant") 107 private static boolean noFileChecks() { 108 var result = false; 109 110 var tMgr = InstanceManager.getDefault(TurnoutManager.class); 111 var sMgr = InstanceManager.getDefault(SensorManager.class); 112 var mMgr = InstanceManager.getDefault(MemoryManager.class); 113 114 // Get the system prefix for internal beans using the memory manager to avoid the default prefix. 115 var systemPrefix = "I"; 116 if (mMgr != null) { 117 systemPrefix = mMgr.getSystemPrefix(); 118 } 119 120 if (tMgr == null || sMgr == null || mMgr == null) { 121 log.debug("triple manager test sets true"); 122 result = true; 123 } 124 125 if (!result && tMgr != null && tMgr.getNamedBeanSet().size() > 0) { 126 log.debug("turnout manager test sets true"); 127 result = true; 128 } 129 130 if (!result && sMgr != null) { 131 var sensorSize = sMgr.getNamedBeanSet().size(); 132 if (sensorSize > 1) { 133 log.debug("sensor > 1 sets true"); 134 result = true; 135 } else if (sensorSize == 1) { 136 if (sMgr.getBySystemName(systemPrefix + "SCLOCKRUNNING") == null) { 137 log.debug("sensor == 1 sets true"); 138 result = true; // One sensor but it is not ISCLOCKRUNNING 139 } 140 } 141 } 142 143 if (!result && mMgr != null) { 144 var memSize = mMgr.getNamedBeanSet().size(); 145 if (memSize > 2) { 146 log.debug("memory > 2 sets true"); 147 result = true; 148 } else if (memSize != 0) { 149 if (mMgr.getBySystemName(systemPrefix + "MCURRENTTIME") == null) { 150 log.debug("memory no MCURRENTTIME sets true"); 151 result = true; // Two memories but one is not IMCURRENTTIME 152 } 153 if (mMgr.getBySystemName(systemPrefix + "MRATEFACTOR") == null) { 154 log.debug("memory no MRATEFACTOR sets true"); 155 result = true; // Two memories but one is not IMRATEFACTOR 156 } 157 } 158 } 159 160 if (!result) { 161 if (InstanceManager.getDefault(jmri.jmrit.display.EditorManager.class).getList().size() > 0) { 162 log.debug("panel check sets true"); 163 result = true; // One or more panels have been added. 164 } 165 } 166 167 return result; 168 } 169 170 @SuppressFBWarnings(value = {"OS_OPEN_STREAM_EXCEPTION_PATH", "RV_DONT_JUST_NULL_CHECK_READLINE"}, 171 justification = 172 "Open streams are not a problem during JMRI shutdown." 173 + "The line represents the end of a XML comment and is not relevant") 174 public static boolean checkFile(File inFile1, File inFile2) throws Exception { 175 boolean result = false; 176 // compare files, except for certain special lines 177 BufferedReader fileStream1 = new BufferedReader( 178 new InputStreamReader(new FileInputStream(inFile1))); 179 BufferedReader fileStream2 = new BufferedReader( 180 new InputStreamReader(new FileInputStream(inFile2))); 181 182 String line1 = fileStream1.readLine(); 183 String line2 = fileStream2.readLine(); 184 185 int lineNumber1 = 0, lineNumber2 = 0; 186 String next1, next2; 187 while ((next1 = fileStream1.readLine()) != null && (next2 = fileStream2.readLine()) != null) { 188 lineNumber1++; 189 lineNumber2++; 190 191 // Do we have a multi line comment? Comments in the xml file is used by LogixNG. 192 // This only happens in the first file since store() will not store comments 193 if (next1.startsWith("<!--")) { 194 while ((next1 = fileStream1.readLine()) != null && !next1.endsWith("-->")) { 195 lineNumber1++; 196 } 197 198 // If here, we either have a line that ends with --> or we have reached endf of file 199 if (fileStream1.readLine() == null) break; 200 201 // If here, we have a line that ends with --> or we have reached end of file 202 continue; 203 } 204 205 // where the (empty) entryexitpairs line ends up seems to be non-deterministic 206 // so if we see it in either file we just skip it 207 String entryexitpairs = "<entryexitpairs class=\"jmri.jmrit.signalling.configurexml.EntryExitPairsXml\" />"; 208 if (line1.contains(entryexitpairs)) { 209 line1 = next1; 210 if ((next1 = fileStream1.readLine()) == null) { 211 break; 212 } 213 lineNumber1++; 214 } 215 if (line2.contains(entryexitpairs)) { 216 line2 = next2; 217 if ((next2 = fileStream2.readLine()) == null) { 218 break; 219 } 220 lineNumber2++; 221 } 222 223 // if we get to the file history... 224 String filehistory = "filehistory"; 225 if (line1.contains(filehistory) && line2.contains(filehistory)) { 226 break; // we're done! 227 } 228 229 boolean match = false; // assume failure (pessimist!) 230 231 String[] startsWithStrings = { 232 " <!--Written by JMRI version", 233 " <test>", // version changes over time 234 " <modifier", // version changes over time 235 " <major", // version changes over time 236 " <minor", // version changes over time 237 "<layout-config", // Linux seems to put attributes in different order 238 "<?xml-stylesheet", // Linux seems to put attributes in different order 239 " <memory systemName=\"IMCURRENTTIME\"", // time varies - old format 240 " <modifier>This line ignored</modifier>" 241 }; 242 for (String startsWithString : startsWithStrings) { 243 if (line1.startsWith(startsWithString) && line2.startsWith(startsWithString)) { 244 match = true; 245 break; 246 } 247 } 248 249 // Memory variables have a value attribute for non-null values or no attribute. 250 if (!match) { 251 var mem1 = line1.startsWith(" <memory value") || line1.startsWith(" <memory>"); 252 var mem2 = line2.startsWith(" <memory value") || line2.startsWith(" <memory>"); 253 if (mem1 && mem2) { 254 match = true; 255 } 256 } 257 258 // Screen size will vary when written out 259 if (!match) { 260 if (line1.contains(" <LayoutEditor")) { 261 // if either line contains a windowheight attribute 262 String windowheight_regexe = "( windowheight=\"[^\"]*\")"; 263 line1 = filterLineUsingRegEx(line1, windowheight_regexe); 264 line2 = filterLineUsingRegEx(line2, windowheight_regexe); 265 // if either line contains a windowheight attribute 266 String windowwidth_regexe = "( windowwidth=\"[^\"]*\")"; 267 line1 = filterLineUsingRegEx(line1, windowwidth_regexe); 268 line2 = filterLineUsingRegEx(line2, windowwidth_regexe); 269 } 270 } 271 272 // window positions will sometimes differ based on window decorations. 273 if (!match) { 274 if (line1.contains(" <LayoutEditor") || 275 line1.contains(" <switchboardeditor")) { 276 // if either line contains a y position attribute 277 String yposition_regexe = "( y=\"[^\"]*\")"; 278 line1 = filterLineUsingRegEx(line1, yposition_regexe); 279 line2 = filterLineUsingRegEx(line2, yposition_regexe); 280 // if either line contains an x position attribute 281 String xposition_regexe = "( x=\"[^\"]*\")"; 282 line1 = filterLineUsingRegEx(line1, xposition_regexe); 283 line2 = filterLineUsingRegEx(line2, xposition_regexe); 284 } 285 } 286 287 // Dates can vary when written out 288 String date_string = "<date>"; 289 if (!match && line1.contains(date_string) && line2.contains(date_string)) { 290 match = true; 291 } 292 293 if (!match) { 294 // remove fontname and fontFamily attributes from comparison 295 String fontname_regexe = "( fontname=\"[^\"]*\")"; 296 line1 = filterLineUsingRegEx(line1, fontname_regexe); 297 line2 = filterLineUsingRegEx(line2, fontname_regexe); 298 String fontFamily_regexe = "( fontFamily=\"[^\"]*\")"; 299 line1 = filterLineUsingRegEx(line1, fontFamily_regexe); 300 line2 = filterLineUsingRegEx(line2, fontFamily_regexe); 301 } 302 303 // Check if timebase is ignored 304 if (!match && line1.startsWith(" <timebase") && line2.startsWith(" <timebase")) { 305 if (_preferences.isIgnoreTimebaseEnabled()) { 306 match = true; 307 } 308 } 309 310 // Check if sensor icon label colors are ignored 311 if (!match 312 && line1.startsWith(" <sensoricon") && line2.startsWith(" <sensoricon") 313 && line1.contains("icon=\"no\"") && line2.contains("icon=\"no\"") 314 && _preferences.isIgnoreSensorColorsEnabled()) { 315 line1 = removeSensorColors(line1); 316 line2 = removeSensorColors(line2); 317 } 318 319 if (!match && !line1.equals(line2)) { 320 log.info("Match failed in StoreAndCompare:"); 321 log.info(" file1:line {}: \"{}\"", lineNumber1, line1); 322 log.info(" file2:line {}: \"{}\"", lineNumber2, line2); 323 log.info(" comparing file1:\"{}\"", inFile1.getPath()); 324 log.info(" to file2:\"{}\"", inFile2.getPath()); 325 result = true; 326 break; 327 } 328 line1 = next1; 329 line2 = next2; 330 } // while readLine() != null 331 332 fileStream1.close(); 333 fileStream2.close(); 334 335 return result; 336 } 337 338 private static String filterLineUsingRegEx(String line, String regexe) { 339 String[] splits = line.split(regexe); 340 if (splits.length == 2) { // (yes) remove it 341 line = splits[0] + splits[1]; 342 } 343 return line; 344 } 345 346 private static String removeSensorColors(String line) { 347 var leftSide = line.substring(0, line.indexOf(" red=")); 348 349 // Find the next non color attribute. "justification" is always present." 350 var index = 0; 351 if (line.indexOf("margin=") != -1) { 352 index = line.indexOf("margin="); 353 } else if (line.indexOf("borderSize=") != -1) { 354 index = line.indexOf("borderSize="); 355 } else if (line.indexOf("redBorder=") != -1) { 356 index = line.indexOf("redBorder="); 357 } else if (line.indexOf("greenBorder=") != -1) { 358 index = line.indexOf("greenBorder="); 359 } else if (line.indexOf("blueBorder=") != -1) { 360 index = line.indexOf("blueBorder="); 361 } else if (line.indexOf("fixedWidth=") != -1) { 362 index = line.indexOf("fixedWidth="); 363 } else if (line.indexOf("fixedHeight=") != -1) { 364 index = line.indexOf("fixedHeight="); 365 } else { 366 index = line.indexOf("justification="); 367 } 368 369 var rightSide = line.substring(index - 1, line.length()); 370 return leftSide + rightSide; 371 } 372 373 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(StoreAndCompare.class); 374}