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    /**
069     * Adds one CSV file path to the collection of files to be processed.
070     *
071     * @param csvFile The File to add.
072     * @return true if successful
073     *
074     */
075    @SuppressFBWarnings(value = "UW_UNCOND_WAIT", justification = "FindBugs incorrectly reports not guarded by conditional control flow")
076    public synchronized boolean addCsvFile(File csvFile) {
077        // Ignore null files...
078        if (csvFile == null || !excelFileExists()) {
079            return false;
080        }
081
082        // once the process starts, we can't add files to the common file
083        while (InstanceManager.getDefault(TrainCustomManifest.class).isProcessAlive() ||
084                InstanceManager.getDefault(TrainCustomSwitchList.class).isProcessAlive()) {
085            synchronized (this) {
086                try {
087                    wait(1000); // 1 sec
088                } catch (InterruptedException e) {
089                    // we don't care
090                }
091            }
092        }
093
094        fileCount++;
095        waitTimeSeconds = fileCount * Control.excelWaitTime;
096        alive = true;
097
098        File csvNamesFile = new File(InstanceManager.getDefault(OperationsManager.class).getFile(getDirectoryName()),
099                getCommonFileName());
100
101        try {
102            FileUtil.appendTextToFile(csvNamesFile, csvFile.getAbsolutePath());
103            log.debug("Queuing file {} to list", csvFile.getAbsolutePath());
104        } catch (IOException e) {
105            log.error("Unable to write to {}", csvNamesFile, e);
106            return false;
107        }
108        return true;
109    }
110
111    /**
112     * Processes the CSV files using a Custom external program that reads the
113     * file of file names.
114     *
115     * @return True if successful.
116     */
117    @SuppressFBWarnings(value = "UW_UNCOND_WAIT", justification = "FindBugs incorrectly reports not guarded by conditional control flow")
118    public synchronized boolean process() {
119
120        // check to see it the Excel program is available
121        if (!excelFileExists() || getFileName().isBlank()) {
122            return false;
123        }
124
125        // Only continue if we have some files to process.
126        if (fileCount == 0) {
127            return true; // done
128        }
129
130        // only one copy of the excel program is allowed to run.  Two copies running in parallel has issues.
131        while (InstanceManager.getDefault(TrainCustomManifest.class).isProcessAlive() ||
132                InstanceManager.getDefault(TrainCustomSwitchList.class).isProcessAlive()) {
133            synchronized (this) {
134                try {
135                    wait(1000); // 1 sec
136                } catch (InterruptedException e) {
137                    // we don't care
138                }
139            }
140        }
141
142        log.debug("Queued {} files to custom Excel program", fileCount);
143
144        // Build our command string out of these bits
145        // We need to use cmd and start to allow launching data files like
146        // Excel spreadsheets
147        // It should work OK with actual programs.
148        if (SystemType.isWindows()) {
149            String cmd = "cmd /c start " + getFileName() + " " + mcAppArg; // NOI18N
150            try {
151                process = Runtime.getRuntime().exec(cmd, null,
152                        InstanceManager.getDefault(OperationsManager.class).getFile(getDirectoryName()));
153            } catch (IOException e) {
154                log.error("Unable to execute {}", getFileName(), e);
155            }
156        } else {
157            String cmd = "open " + getFileName() + " " + mcAppArg; // NOI18N
158            try {
159                process = Runtime.getRuntime().exec(cmd, null,
160                        InstanceManager.getDefault(OperationsManager.class).getFile(getDirectoryName()));
161            } catch (IOException e) {
162                log.error("Unable to execute {}", getFileName(), e);
163            }
164        }
165        fileCount = 0;
166        return true;
167    }
168
169    public boolean excelFileExists() {
170        File file = new File(InstanceManager.getDefault(OperationsManager.class).getFile(getDirectoryName()),
171                getFileName());
172        return file.exists();
173    }
174
175    @SuppressFBWarnings(value = "UW_UNCOND_WAIT", justification = "FindBugs incorrectly reports not guarded by conditional control flow")
176    public boolean checkProcessReady() {
177        if (!isProcessAlive()) {
178            return true;
179        }
180        if (alive) {
181            log.debug("Wait time: {} seconds process ready", waitTimeSeconds);
182            long loopCount = waitTimeSeconds; // number of seconds to wait
183            while (loopCount-- > 0 && alive) {
184                synchronized (this) {
185                    try {
186                        wait(1000); // 1 sec
187                    } catch (InterruptedException e) {
188                        // TODO Auto-generated catch block
189                        log.error("Thread unexpectedly interrupted", e);
190                    }
191                }
192            }
193        }
194        return !alive;
195    }
196
197    public boolean isProcessAlive() {
198        if (process != null) {
199            return process.isAlive();
200        } else {
201            return false;
202        }
203    }
204
205    /**
206     *
207     * @return true if process completes without a timeout, false if there's a
208     *         timeout.
209     * @throws InterruptedException if process thread is interrupted
210     */
211    @SuppressFBWarnings(value = "UW_UNCOND_WAIT", justification = "FindBugs incorrectly reports not guarded by conditional control flow")
212    public boolean waitForProcessToComplete() throws InterruptedException {
213        if (process == null) {
214            return true; // process hasn't been initialized
215        }
216        boolean status = false;
217        synchronized (process) {
218            File file = new File(InstanceManager.getDefault(OperationsManager.class).getFile(getDirectoryName()),
219                    getCommonFileName());
220            if (!file.exists()) {
221                log.debug("Common file not found! Normal when processing multiple files");
222            }
223            log.debug("Waiting up to {} seconds for Excel program to complete", waitTimeSeconds);
224            status = process.waitFor(waitTimeSeconds, TimeUnit.SECONDS);
225            // printing can take a long time, wait to complete
226            if (status && file.exists()) {
227                long loopCount = waitTimeSeconds; // number of seconds to wait
228                while (loopCount-- > 0 && file.exists()) {
229                    synchronized (this) {
230                        try {
231                            wait(1000); // 1 sec
232                        } catch (InterruptedException e) {
233                            // TODO Auto-generated catch block
234                            log.error("Thread unexpectedly interrupted", e);
235                        }
236                    }
237                }
238            }
239            if (file.exists()) {
240                log.error("Common file ({}) not deleted! Wait time {} seconds", file.getPath(), waitTimeSeconds);
241                return false;
242            }
243            log.debug("Excel program complete!");
244        }
245        alive = false; // done!
246        return status;
247    }
248
249    /**
250     * Checks to see if the common file exists
251     *
252     * @return true if the common file exists
253     */
254    public boolean doesCommonFileExist() {
255        File file = new File(InstanceManager.getDefault(OperationsManager.class).getFile(getDirectoryName()),
256                getCommonFileName());
257        return file.exists();
258    }
259
260    public void load(Element options) {
261        Element mc = options.getChild(xmlElement);
262        if (mc != null) {
263            Attribute a;
264            Element directory = mc.getChild(Xml.DIRECTORY);
265            if (directory != null && (a = directory.getAttribute(Xml.NAME)) != null) {
266                setDirectoryName(a.getValue());
267            }
268            Element file = mc.getChild(Xml.RUN_FILE);
269            if (file != null && (a = file.getAttribute(Xml.NAME)) != null) {
270                mcAppName = a.getValue();
271            }
272            Element common = mc.getChild(Xml.COMMON_FILE);
273            if (common != null && (a = common.getAttribute(Xml.NAME)) != null) {
274                csvNamesFileName = a.getValue();
275            }
276        }
277    }
278
279    public void store(Element options) {
280        Element mc = new Element(xmlElement);
281        Element file = new Element(Xml.RUN_FILE);
282        file.setAttribute(Xml.NAME, getFileName());
283        Element directory = new Element(Xml.DIRECTORY);
284        directory.setAttribute(Xml.NAME, getDirectoryName());
285        Element common = new Element(Xml.COMMON_FILE);
286        common.setAttribute(Xml.NAME, getCommonFileName());
287        mc.addContent(directory);
288        mc.addContent(file);
289        mc.addContent(common);
290        options.addContent(mc);
291    }
292
293    private final static Logger log = LoggerFactory.getLogger(TrainCustomCommon.class);
294}