001package jmri.jmrit.operations.trains.excel;
002
003import java.io.File;
004import java.io.IOException;
005import java.util.concurrent.TimeUnit;
006
007import org.jdom2.Attribute;
008import org.jdom2.Element;
009import org.slf4j.Logger;
010import org.slf4j.LoggerFactory;
011
012import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
013import jmri.InstanceManager;
014import jmri.jmrit.operations.OperationsManager;
015import jmri.jmrit.operations.setup.Control;
016import jmri.jmrit.operations.trains.TrainManagerXml;
017import jmri.util.FileUtil;
018import jmri.util.SystemType;
019
020public abstract class TrainCustomCommon {
021
022    protected final String xmlElement;
023    protected String directoryName;
024    private String mcAppName = "MC4JMRI.xls"; // NOI18N
025    private final String mcAppArg = ""; // NOI18N
026    private String csvNamesFileName = "CSVFilesFile.txt"; // NOI18N
027    private int fileCount = 0;
028    private long waitTimeSeconds = 0;
029    private Process process;
030    private boolean alive = false; // when true files to be processed
031
032    protected TrainCustomCommon(String dirName, String xmlElement) {
033        directoryName = dirName;
034        this.xmlElement = xmlElement;
035    }
036
037    public String getFileName() {
038        return mcAppName;
039    }
040
041    public void setFileName(String name) {
042        if (!getFileName().equals(name)) {
043            mcAppName = name;
044            InstanceManager.getDefault(TrainManagerXml.class).setDirty(true);
045        }
046    }
047
048    public String getCommonFileName() {
049        return csvNamesFileName;
050    }
051
052    public void setCommonFileName(String name) {
053        csvNamesFileName = name;
054    }
055
056    public String getDirectoryName() {
057        return directoryName;
058    }
059
060    public void setDirectoryName(String name) {
061        directoryName = name;
062    }
063
064    public String getDirectoryPathName() {
065        return InstanceManager.getDefault(OperationsManager.class).getFile(getDirectoryName()).getPath();
066    }
067
068    public boolean doesExcelFileExist() {
069        File file = getExcelFile();
070        return file.exists();
071    }
072
073    public File getExcelFile() {
074        return new File(InstanceManager.getDefault(OperationsManager.class).getFile(getDirectoryName()),
075                getFileName());
076    }
077
078    /**
079     * Checks to see if the common file exists
080     *
081     * @return true if the common file exists
082     */
083    public boolean doesCommonFileExist() {
084        File file = getCommonFile();
085        return file.exists();
086    }
087
088    /**
089     * The common file contains a list of CSV files that need to be processed.
090     * 
091     * @return the common file.
092     */
093    public File getCommonFile() {
094        return new File(InstanceManager.getDefault(OperationsManager.class).getFile(getDirectoryName()),
095                getCommonFileName());
096    }
097
098    /**
099     * Adds one CSV file path to the collection of files to be processed.
100     *
101     * @param csvFile The File to add.
102     * @return true if successful
103     *
104     */
105    @SuppressFBWarnings(value = "UW_UNCOND_WAIT", justification = "FindBugs incorrectly reports not guarded by conditional control flow")
106    public synchronized boolean addCsvFile(File csvFile) {
107        // Ignore null files...
108        if (csvFile == null || !doesExcelFileExist()) {
109            return false;
110        }
111
112        // once the process starts, we can't add files to the common file
113        while (InstanceManager.getDefault(TrainCustomManifest.class).isProcessAlive() ||
114                InstanceManager.getDefault(TrainCustomSwitchList.class).isProcessAlive()) {
115            synchronized (this) {
116                try {
117                    wait(1000); // 1 sec
118                } catch (InterruptedException e) {
119                    // we don't care
120                }
121            }
122        }
123
124        if (fileCount == 0 && doesCommonFileExist()) {
125            log.warn("CSV common file exists!");
126        }
127
128        fileCount++;
129        waitTimeSeconds = fileCount * Control.excelWaitTime;
130        alive = true;
131
132        File csvNamesFile = getCommonFile();
133
134        try {
135            FileUtil.appendTextToFile(csvNamesFile, csvFile.getAbsolutePath());
136            log.debug("Queuing file {} to list", csvFile.getAbsolutePath());
137        } catch (IOException e) {
138            log.error("Unable to write to {}, {}", csvNamesFile, e.getLocalizedMessage());
139            return false;
140        }
141        return true;
142    }
143
144    /**
145     * Processes the CSV files using a Custom external program that reads the
146     * file of file names.
147     *
148     * @return True if successful.
149     */
150    @SuppressFBWarnings(value = "UW_UNCOND_WAIT", justification = "FindBugs incorrectly reports not guarded by conditional control flow")
151    public synchronized boolean process() {
152
153        // check to see it the Excel program is available
154        if (!doesExcelFileExist() || getFileName().isBlank()) {
155            return false;
156        }
157
158        // Only continue if we have some files to process.
159        if (fileCount == 0) {
160            return true; // done
161        }
162
163        // only one copy of the excel program is allowed to run.  Two copies running in parallel has issues.
164        while (InstanceManager.getDefault(TrainCustomManifest.class).isProcessAlive() ||
165                InstanceManager.getDefault(TrainCustomSwitchList.class).isProcessAlive()) {
166            synchronized (this) {
167                try {
168                    wait(1000); // 1 sec
169                } catch (InterruptedException e) {
170                    // we don't care
171                }
172            }
173        }
174
175        log.debug("Queued {} files to custom Excel program", fileCount);
176
177        // Build our command string out of these bits
178        // We need to use cmd and start to allow launching data files like
179        // Excel spreadsheets
180        // It should work OK with actual programs.
181        if (SystemType.isWindows()) {
182            String[] cmd = {"cmd", "/c", "start", getFileName(), mcAppArg}; // NOI18N
183            try {
184                process = Runtime.getRuntime().exec(cmd, null,
185                        InstanceManager.getDefault(OperationsManager.class).getFile(getDirectoryName()));
186            } catch (IOException e) {
187                log.error("Unable to execute: {}, {}", getFileName(), e.getLocalizedMessage());
188            }
189        } else {
190            String[] cmd = {"open", getFileName(), mcAppArg}; // NOI18N
191            try {
192                process = Runtime.getRuntime().exec(cmd, null,
193                        InstanceManager.getDefault(OperationsManager.class).getFile(getDirectoryName()));
194            } catch (IOException e) {
195                log.error("Unable to execute {}, {}", getFileName(), e.getLocalizedMessage());
196            }
197        }
198        fileCount = 0;
199        return true;
200    }
201
202
203    @SuppressFBWarnings(value = "UW_UNCOND_WAIT", justification = "FindBugs incorrectly reports not guarded by conditional control flow")
204    public boolean checkProcessReady() {
205        if (!isProcessAlive()) {
206            return true;
207        }
208        if (alive) {
209            log.debug("Wait time: {} seconds process ready", waitTimeSeconds);
210            long loopCount = waitTimeSeconds; // number of seconds to wait
211            while (loopCount-- > 0 && alive) {
212                synchronized (this) {
213                    try {
214                        wait(1000); // 1 sec
215                    } catch (InterruptedException e) {
216                        // TODO Auto-generated catch block
217                        log.error("Thread unexpectedly interrupted", e);
218                    }
219                }
220            }
221        }
222        return !alive;
223    }
224
225    public boolean isProcessAlive() {
226        if (process != null) {
227            return process.isAlive();
228        } else {
229            return false;
230        }
231    }
232
233    /**
234     *
235     * @return true if process completes without a timeout, false if there's a
236     *         timeout.
237     * @throws InterruptedException if process thread is interrupted
238     */
239    @SuppressFBWarnings(value = "UW_UNCOND_WAIT", justification = "FindBugs incorrectly reports not guarded by conditional control flow")
240    public boolean waitForProcessToComplete() throws InterruptedException {
241        if (process == null) {
242            return true; // process hasn't been initialized
243        }
244        boolean status = false;
245        synchronized (process) {
246            File file = getCommonFile();
247            if (!file.exists()) {
248                log.debug("Common file not found! Normal when processing multiple files");
249            }
250            log.debug("Waiting up to {} seconds for Excel program to complete", waitTimeSeconds);
251            status = process.waitFor(waitTimeSeconds, TimeUnit.SECONDS);
252            // printing can take a long time, wait to complete
253            if (status && file.exists()) {
254                wait(file);
255            }
256            if (file.exists()) {
257                log.error("Excel program failed to complete processing!");
258                log.error("Common file ({}) not deleted! Wait time {} seconds", file.getPath(), waitTimeSeconds);
259                return false;
260            } else {
261                log.info("Success the Excel program finished processing the CSV file");
262            }
263        }
264        alive = false; // done!
265        return status;
266    }
267
268    /*
269     * Wait for common file to not exist
270     */
271    private void wait(File file) {
272        long loopCount = waitTimeSeconds; // number of seconds to wait
273        while (loopCount-- > 0 && file.exists()) {
274            // report after 15 seconds that we're waiting
275            if (loopCount == waitTimeSeconds - 15) {
276                log.info("Waiting for CVS file to be processed by Excel program");
277            }
278            synchronized (this) {
279                try {
280                    wait(1000); // 1 sec
281                } catch (InterruptedException e) {
282                    // TODO Auto-generated catch block
283                    log.error("Thread unexpectedly interrupted", e);
284                }
285            }
286        }
287    }
288
289    public void load(Element options) {
290        Element mc = options.getChild(xmlElement);
291        if (mc != null) {
292            Attribute a;
293            Element directory = mc.getChild(Xml.DIRECTORY);
294            if (directory != null && (a = directory.getAttribute(Xml.NAME)) != null) {
295                setDirectoryName(a.getValue());
296            }
297            Element file = mc.getChild(Xml.RUN_FILE);
298            if (file != null && (a = file.getAttribute(Xml.NAME)) != null) {
299                mcAppName = a.getValue();
300            }
301            Element common = mc.getChild(Xml.COMMON_FILE);
302            if (common != null && (a = common.getAttribute(Xml.NAME)) != null) {
303                csvNamesFileName = a.getValue();
304            }
305        }
306    }
307
308    public void store(Element options) {
309        Element mc = new Element(xmlElement);
310        Element file = new Element(Xml.RUN_FILE);
311        file.setAttribute(Xml.NAME, getFileName());
312        Element directory = new Element(Xml.DIRECTORY);
313        directory.setAttribute(Xml.NAME, getDirectoryName());
314        Element common = new Element(Xml.COMMON_FILE);
315        common.setAttribute(Xml.NAME, getCommonFileName());
316        mc.addContent(directory);
317        mc.addContent(file);
318        mc.addContent(common);
319        options.addContent(mc);
320    }
321
322    private final static Logger log = LoggerFactory.getLogger(TrainCustomCommon.class);
323}