001// Warnings objectInputStream here about changes to this structure and how it will affect old/new programs:
002//https://howtodoinjava.com/java/serialization/a-mini-guide-for-implementing-serializable-interface-objectInputStream-java/
003
004package jmri.jmrit.ctc.configurexml;
005
006import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
007import java.io.BufferedReader;
008import java.io.BufferedWriter;
009import java.io.File;
010import java.io.FileReader;
011import java.io.FileWriter;
012import java.io.IOException;
013import java.io.Serializable;
014import java.util.HashMap;
015import jmri.jmrit.ctc.CTCFiles;
016
017/**
018 *
019 * @author Gregory J. Bedlek Copyright (C) 2018, 2019
020 */
021public class ImportCodeButtonHandlerData implements Serializable {
022    private final static int FILE_VERSION = 6;
023    public static final int SWITCH_NOT_SLAVED = -1;
024
025    public enum LOCK_IMPLEMENTATION {
026// The values in paren's are the RadioGroup values set by "CommonSubs.numberButtonGroup",
027// gotten by calling "CommonSubs.getButtonSelectedInt".
028        GREGS(0), OTHER(1);
029        private final int _mRadioGroupValue;
030        private final static HashMap<Integer, LOCK_IMPLEMENTATION> map = new HashMap<>();
031        private LOCK_IMPLEMENTATION (int radioGroupValue) { _mRadioGroupValue = radioGroupValue; }
032        static { for (LOCK_IMPLEMENTATION value : LOCK_IMPLEMENTATION.values()) { map.put(value._mRadioGroupValue, value); }}
033    }
034
035    public enum TURNOUT_TYPE {
036// The values in paren's are the RadioGroup values set by "CommonSubs.numberButtonGroup",
037// gotten by calling "CommonSubs.getButtonSelectedInt".
038        TURNOUT(0), CROSSOVER(1), DOUBLE_CROSSOVER(2);
039        private final int _mRadioGroupValue;
040        private final static HashMap<Integer, TURNOUT_TYPE> map = new HashMap<>();
041        private TURNOUT_TYPE (int radioGroupValue) { _mRadioGroupValue = radioGroupValue; }
042        static { for (TURNOUT_TYPE value : TURNOUT_TYPE.values()) { map.put(value._mRadioGroupValue, value); }}
043    }
044
045    public ImportCodeButtonHandlerData() {
046        _mOSSectionSwitchSlavedToUniqueID = SWITCH_NOT_SLAVED;
047        _mSWDI_GUITurnoutType = ImportCodeButtonHandlerData.TURNOUT_TYPE.TURNOUT;
048        _mTUL_LockImplementation = LOCK_IMPLEMENTATION.GREGS;
049    }
050    private static final long serialVersionUID = 1L;
051
052//  This number NEVER changes, and is how this object is uniquely identified:
053    public int _mUniqueID = -1;         // FORCE serialization to write out the FIRST unique number 0 into the XML file (to make me happy!)
054//  Used by the Editor only:
055    public int _mSwitchNumber;         // Switch Indicators and lever #
056    public int _mSignalEtcNumber;      // Signal Indicators, lever, locktoggle, callon and code button number
057//  PRESENTLY (as of 10/18/18) these are ONLY used by the edit routines to TEMPORARILY get a copy.  The
058//  data is NEVER stored anywhere.  I say this because "_mUniqueID" MUST have another unique number if it is EVER
059//  stored anywhere!  For example: take the source # and add 5,000,000 to it each time.  Even copies of copies would
060//  get unique numbers!  If the user ever creates 5,000,000 objects, they must be GOD!
061
062//  Version of this file for supporting upgrade paths from prior versions:
063    public int                  _mFileVersion;
064//  Data used by the runtime (JMRI) and Editor systems:
065    public String               _mCodeButtonInternalSensor;
066    public String               _mOSSectionOccupiedExternalSensor;              // Required
067    public String               _mOSSectionOccupiedExternalSensor2;             // Optional
068    public int                  _mOSSectionSwitchSlavedToUniqueID;
069    public int                  _mGUIColumnNumber;
070    public boolean              _mGUIGeneratedAtLeastOnceAlready;
071    public int                  _mCodeButtonDelayTime;
072//  Signal Direction Indicators:
073    public boolean              _mSIDI_Enabled;
074    public String               _mSIDI_LeftInternalSensor;
075    public String               _mSIDI_NormalInternalSensor;
076    public String               _mSIDI_RightInternalSensor;
077    public int                  _mSIDI_CodingTimeInMilliseconds;
078    public int                  _mSIDI_TimeLockingTimeInMilliseconds;
079    public String               _mSIDI_LeftRightTrafficSignalsCSVList;
080    public String               _mSIDI_RightLeftTrafficSignalsCSVList;
081//  Signal Direction Lever:
082    public boolean              _mSIDL_Enabled;
083    public String               _mSIDL_LeftInternalSensor;
084    public String               _mSIDL_NormalInternalSensor;
085    public String               _mSIDL_RightInternalSensor;
086//  Switch Direction Indicators:
087    public boolean              _mSWDI_Enabled;
088    public String               _mSWDI_NormalInternalSensor;
089    public String               _mSWDI_ReversedInternalSensor;
090    public String               _mSWDI_ExternalTurnout;
091    public int                  _mSWDI_CodingTimeInMilliseconds;
092    public boolean              _mSWDI_FeedbackDifferent;
093    public TURNOUT_TYPE         _mSWDI_GUITurnoutType;
094    public boolean              _mSWDI_GUITurnoutLeftHand;
095    public boolean              _mSWDI_GUICrossoverLeftHand;
096//  Switch Direction Lever:
097    public boolean              _mSWDL_Enabled;
098    public String               _mSWDL_InternalSensor;
099//  Call On:
100    public boolean              _mCO_Enabled;
101    public String               _mCO_CallOnToggleInternalSensor;
102    public String               _mCO_GroupingsListString;
103//  Traffic Locking:
104    public boolean              _mTRL_Enabled;
105    public String               _mTRL_LeftTrafficLockingRulesSSVList;
106    public String               _mTRL_RightTrafficLockingRulesSSVList;
107//  Turnout Locking:
108    public boolean              _mTUL_Enabled;
109    public String               _mTUL_DispatcherInternalSensorLockToggle;
110    public String               _mTUL_ExternalTurnout;
111    public boolean              _mTUL_ExternalTurnoutFeedbackDifferent;
112    public String               _mTUL_DispatcherInternalSensorUnlockedIndicator;
113    public boolean              _mTUL_NoDispatcherControlOfSwitch;
114    public boolean              _mTUL_ndcos_WhenLockedSwitchStateIsClosed;
115    public LOCK_IMPLEMENTATION  _mTUL_LockImplementation;
116    public String               _mTUL_AdditionalExternalTurnout1;
117    public boolean              _mTUL_AdditionalExternalTurnout1FeedbackDifferent;
118    public String               _mTUL_AdditionalExternalTurnout2;
119    public boolean              _mTUL_AdditionalExternalTurnout2FeedbackDifferent;
120    public String               _mTUL_AdditionalExternalTurnout3;
121    public boolean              _mTUL_AdditionalExternalTurnout3FeedbackDifferent;
122//  Indication Locking (Signals):
123    public boolean              _mIL_Enabled;
124    public String               _mIL_ListOfCSVSignalNames;
125
126    public void upgradeSelf() {
127        for (int oldVersion = _mFileVersion; oldVersion < FILE_VERSION; oldVersion++) {
128            switch(oldVersion) {
129                case 0:     // 0->1: Get rid of ALL Traffic locking stuff.  Incompatible with prior version.
130                case 1:     // 1->2: Get rid of ALL Traffic locking stuff.  Incompatible with prior version.
131                case 2:     // 2->3: Get rid of ALL Traffic locking stuff.  Incompatible with prior version.
132                case 3:     // 3->4: Get rid of ALL Traffic locking stuff.  Incompatible with prior version.
133                    _mTRL_Enabled = false;
134                    _mTRL_LeftTrafficLockingRulesSSVList = "";
135                    _mTRL_RightTrafficLockingRulesSSVList = "";
136                    break;
137                default:    // 4->5, 5->6: Do NOTHING!
138                    break;
139            }
140        }
141        _mFileVersion = FILE_VERSION;       // Now at this version
142    }
143
144/*  When I change a variable name but want to keep the contents, I need to
145    pre-process the file BEFORE I turn it over to serialization.  That is done
146    here:
147    NOTE:
148    This is ALWAYS done BEFORE the normal "upgradeSelf"!  And if it matches a version to change,
149    it ALWAYS increments the file version by one!  Therefore "upgradeSelf" will see one greater!
150    So if you want to do BOTH, then you need to increase file version by 2, and insure that the
151    first increment is processed by this:
152*/
153    private final static String FILE_VERSION_STRING = "<string>_mFileVersion</string>"; // NOI18N
154    private final static String LESS_THAN_SIGN = "<";                                   // NOI18N
155    private static final String TEMPORARY_EXTENSION = ".xmlTMP";                        // NOI18N
156
157//  Regarding "@SuppressFBWarnings": My attitude is that if the input file is screwed up, do nothing!:
158    @SuppressFBWarnings(value = "NP_IMMEDIATE_DEREFERENCE_OF_READLINE", justification = "I'm already catching 'NullPointerException', it's ok!")
159    static public void preprocessingUpgradeSelf(String filename) {
160//  First, get the existing _mFileVersion from the file to see if we need to work on it:
161        int fileVersion = -1;       // Indicate none found.
162        try (BufferedReader bufferedReader = new BufferedReader(new FileReader(filename))) {
163            String aLine;
164            while (!(aLine = bufferedReader.readLine()).contains(FILE_VERSION_STRING)) {}   // Skip to the line IF it exists.
165            bufferedReader.readLine();  // Ignore <void method="set">
166            bufferedReader.readLine();  // Ignore <object idref="CodeButtonHandlerData18"/>
167            aLine = bufferedReader.readLine().trim();  // Get something like <int>4</int>
168            if (aLine.startsWith(INT_START_STRING)) {
169                aLine = aLine.substring(5);     // Get rid of it.
170                fileVersion = Integer.parseInt(aLine.substring(0, aLine.indexOf(LESS_THAN_SIGN)));
171            }
172        } catch (IOException | NumberFormatException | NullPointerException e) {}
173        if (fileVersion < 0) return;    // Safety: Nothing found, ignore it (though we should have found and parsed it!)
174        switch (fileVersion) {
175            case 4:
176                upgradeVersion4FileTo5(filename);
177                break;
178            case 5:
179                upgradeVersion5FileTo6(filename);
180                break;
181            default:
182                break;
183        }
184    }
185
186    @SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED_BAD_PRACTICE", justification = "Any problems, I don't care, it's too late by this point")
187    static private void upgradeVersion4FileTo5(String filename) {
188        String temporaryFilename = CTCFiles.changeExtensionTo(filename, TEMPORARY_EXTENSION);
189        (new File(temporaryFilename)).delete();   // Just delete it for safety before we start:
190        boolean hadAChange = false;
191        try (BufferedReader bufferedReader = new BufferedReader(new FileReader(filename));
192             BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(temporaryFilename))) {
193            String aLine = null;
194            while ((aLine = bufferedReader.readLine()) != null) { // Not EOF:
195                if ((aLine = checkFileVersion(bufferedReader, bufferedWriter, aLine, "4", "5")) == null) { hadAChange = true; continue; } // Was processed.
196                if ((aLine = checkForRefactor(bufferedWriter, aLine, "_mSWDI_ActualTurnout",                      "_mSWDI_ExternalTurnout")) == null) { hadAChange = true; continue; }  // NOI18N Was processed.
197                if ((aLine = checkForRefactor(bufferedWriter, aLine, "_mTUL_ActualTurnout",                       "_mTUL_ExternalTurnout")) == null) { hadAChange = true; continue; }  // NOI18N Was processed.
198                if ((aLine = checkForRefactor(bufferedWriter, aLine, "_mTUL_ActualTurnoutFeedbackDifferent",      "_mTUL_ExternalTurnoutFeedbackDifferent")) == null) { hadAChange = true; continue; }  // NOI18N Was processed.
199                if ((aLine = checkForRefactor(bufferedWriter, aLine, "_mTUL_AdditionalTurnout1",                  "_mTUL_AdditionalExternalTurnout1")) == null) { hadAChange = true; continue; }  // NOI18N Was processed.
200                if ((aLine = checkForRefactor(bufferedWriter, aLine, "_mTUL_AdditionalTurnout1FeedbackDifferent", "_mTUL_AdditionalExternalTurnout1FeedbackDifferent")) == null) { hadAChange = true; continue; }  // NOI18N Was processed.
201                if ((aLine = checkForRefactor(bufferedWriter, aLine, "_mTUL_AdditionalTurnout2",                  "_mTUL_AdditionalExternalTurnout2")) == null) { hadAChange = true; continue; }  // NOI18N Was processed.
202                if ((aLine = checkForRefactor(bufferedWriter, aLine, "_mTUL_AdditionalTurnout2FeedbackDifferent", "_mTUL_AdditionalExternalTurnout2FeedbackDifferent")) == null) { hadAChange = true; continue; }  // NOI18N Was processed.
203                if ((aLine = checkForRefactor(bufferedWriter, aLine, "_mTUL_AdditionalTurnout3",                  "_mTUL_AdditionalExternalTurnout3")) == null) { hadAChange = true; continue; }  // NOI18N Was processed.
204                if ((aLine = checkForRefactor(bufferedWriter, aLine, "_mTUL_AdditionalTurnout3FeedbackDifferent", "_mTUL_AdditionalExternalTurnout3FeedbackDifferent")) == null) { hadAChange = true; continue; }  // NOI18N Was processed.
205                writeLine(bufferedWriter, aLine);
206            }
207//  Regarding commented out code (due to SpotBugs):
208//  I'm a safety "nut".  I will do such things in case other code is someday inserted
209//  between the above "check for != null" and here.  But to satisfy SpotBugs:
210            if (/*aLine == null && */hadAChange) { // Do the two step:
211                bufferedReader.close();
212                bufferedWriter.close();
213                File oldFile = new File(filename);
214                oldFile.delete();                   // Delete existing old file.
215                (new File(temporaryFilename)).renameTo(oldFile);    // Rename temporary filename to proper final file.
216            }
217        } catch (IOException e) {}  // Any other error(s) just cleans up:
218        (new File(temporaryFilename)).delete();        // If we get here, just clean up.
219    }
220
221    /**
222     * This routine was written because CSVPrinter at some point began putting "\r\n" at the
223     * end of lines returned by "toString()" based upon the version of the Java Library that
224     * was present.  This corrupted my data internally, and a user out in the field got caught
225     * by this change in the Java library.  The result was lines like:
226     * Hurricane X-over Track 1 West,LEFTTRAFFIC,Flashing Red,,Hurricane Track 1,IS5:SWNI,IS3:SWNI,IS1:SWNI,,,;;;;Hurricane X-over Track 1 West,LEFTTRAFFIC,Flashing Red,,Hurricane Track 2,IS5:SWNI,IS3:SWNI,IS1:SWRI,,,;;;;Hurricane X-over Track 1 East,RIGHTTRAFFIC,Flashing Red,,East Hurricane Track 1,IS1:SWNI,IS3:SWNI,IS5:SWNI,,,;;;;Hurricane X-over Track 1 East,RIGHTTRAFFIC,Flashing Red,,East Hurricane Track 2,IS1:SWNI,IS3:SWNI,IS5:SWRI,IS7:SWNI,,
227     * You'll notice here (and other places in the line) that there are multiple ;;;; in a row                ^^^^ ...-> also
228     * caused by this.  What I will do here is fix any multiple ;;;; to a single ;:
229     * @param filename The .xml file to convert from version 5 format to version 6 format.
230     */
231    @SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED_BAD_PRACTICE", justification = "Any problems, I don't care, it's too late by this point")
232    static private void upgradeVersion5FileTo6(String filename) {
233        String temporaryFilename = CTCFiles.changeExtensionTo(filename, TEMPORARY_EXTENSION);
234        (new File(temporaryFilename)).delete();   // Just delete it for safety before we start:
235        boolean hadAChange = false;
236        try (BufferedReader bufferedReader = new BufferedReader(new FileReader(filename)); BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(temporaryFilename))) {
237            String aLine = null;
238            while ((aLine = bufferedReader.readLine()) != null) { // Not EOF:
239                if ((aLine = checkFileVersion(bufferedReader, bufferedWriter, aLine, "5", "6")) == null) { hadAChange = true; continue; } // Was processed.
240                if ((aLine = checkForMultipleSemiColons(bufferedWriter, aLine)) == null) { hadAChange = true; continue; }  // NOI18N Was processed.
241                writeLine(bufferedWriter, aLine);
242            }
243//  Regarding commented out code (due to SpotBugs):
244//  I'm a safety "nut".  I will do such things in case other code is someday inserted
245//  between the above "check for != null" and here.  But to satisfy SpotBugs:
246            if (/*aLine == null && */hadAChange) { // Do the two step:
247                bufferedReader.close();
248                bufferedWriter.close();
249                File oldFile = new File(filename);
250                oldFile.delete();                   // Delete existing old file.
251                (new File(temporaryFilename)).renameTo(oldFile);    // Rename temporary filename to proper final file.
252            }
253        } catch (IOException e) {}  // Any other error(s) just cleans up:
254        (new File(temporaryFilename)).delete();        // If we get here, just clean up.
255    }
256
257
258/*
259    Returns:    null if we processed it or it was the wrong format, and in either case WROTE the line(s) out indicating that we handled it.
260        or
261                The original aLine passed and NOTHING written, so that other(s) can check it further.
262*/
263    private final static String INT_START_STRING = "<int>"; // NOI18N
264    private final static String INT_END_STRING = "</int>";  // NOI18N
265    static private String checkFileVersion(BufferedReader bufferedReader, BufferedWriter bufferedWriter, String aLine, String oldVersion, String newVersion) throws IOException {
266        if (aLine.contains(FILE_VERSION_STRING)) {
267            writeLine(bufferedWriter, aLine);
268            writeLine(bufferedWriter, bufferedReader.readLine());   // Ignore <void method="set">
269            writeLine(bufferedWriter, bufferedReader.readLine());   // Ignore <object idref="CodeButtonHandlerData18"/>
270            aLine = bufferedReader.readLine();  // Get something like <int>4</int>
271            if (aLine != null) {
272                int intStart = aLine.indexOf(INT_START_STRING + oldVersion + INT_END_STRING);
273                if (intStart >= 0) { // Found, replace:
274                    writeLine(bufferedWriter, aLine.substring(0, intStart) + INT_START_STRING + newVersion + INT_END_STRING);
275                } else {
276                    writeLine(bufferedWriter, aLine);
277                }
278            }
279            return null;
280        }
281        return aLine;   // Line wasn't for us!
282    }
283
284    private final static String STRING_START_STRING = "<string>";   // NOI18N
285    private final static String STRING_END_STRING = "</string>";    // NOI18N
286    static private String checkForRefactor(BufferedWriter bufferedWriter, String aLine, String oldName, String newName) throws IOException {
287        int intStart = aLine.indexOf(STRING_START_STRING + oldName + STRING_END_STRING);
288        if (intStart >= 0) { // Found, replace:
289            writeLine(bufferedWriter, aLine.substring(0, intStart) + STRING_START_STRING + newName + STRING_END_STRING);
290            return null;
291        }
292        return aLine;
293    }
294
295    static private String checkForMultipleSemiColons(BufferedWriter bufferedWriter, String aLine) throws IOException {
296        int intStart = aLine.indexOf(STRING_START_STRING);
297        int intEnd = aLine.indexOf(STRING_END_STRING);
298        if (intStart >=0 && intEnd >=0 && intStart < intEnd) { // Insure a line we might look at:
299           while (aLine.contains(";;")) {
300               aLine= aLine.replace(";;", ";");
301           }
302        }
303        if (intStart >= 0) { // Found, replace:
304            writeLine(bufferedWriter, aLine);
305            return null;
306        }
307        return aLine;
308    }
309
310
311    static private void writeLine(BufferedWriter bufferedWriter, String aLine) throws IOException {
312        bufferedWriter.write(aLine); bufferedWriter.newLine();
313    }
314}