1/*
2 * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package jdk.nashorn.tools.jjs;
27
28import java.io.IOException;
29import java.nio.charset.Charset;
30import java.nio.file.ClosedWatchServiceException;
31import java.nio.file.FileSystems;
32import java.nio.file.Files;
33import java.nio.file.Path;
34import java.nio.file.WatchKey;
35import java.nio.file.WatchService;
36import java.util.List;
37import java.util.function.Consumer;
38import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
39import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
40import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
41
42final class ExternalEditor {
43    private final Consumer<String> errorHandler;
44    private final Consumer<String> saveHandler;
45    private final Console input;
46
47    private WatchService watcher;
48    private Thread watchedThread;
49    private Path dir;
50    private Path tmpfile;
51
52    ExternalEditor(final Consumer<String> errorHandler, final Consumer<String> saveHandler, final Console input) {
53        this.errorHandler = errorHandler;
54        this.saveHandler = saveHandler;
55        this.input = input;
56    }
57
58    private void edit(final String cmd, final String initialText) {
59        try {
60            setupWatch(initialText);
61            launch(cmd);
62        } catch (IOException ex) {
63            errorHandler.accept(ex.getMessage());
64        }
65    }
66
67    /**
68     * Creates a WatchService and registers the given directory
69     */
70    private void setupWatch(final String initialText) throws IOException {
71        this.watcher = FileSystems.getDefault().newWatchService();
72        this.dir = Files.createTempDirectory("REPL");
73        this.tmpfile = Files.createTempFile(dir, null, ".js");
74        Files.write(tmpfile, initialText.getBytes(Charset.forName("UTF-8")));
75        dir.register(watcher,
76                ENTRY_CREATE,
77                ENTRY_DELETE,
78                ENTRY_MODIFY);
79        watchedThread = new Thread(() -> {
80            for (;;) {
81                WatchKey key;
82                try {
83                    key = watcher.take();
84                } catch (final ClosedWatchServiceException ex) {
85                    break;
86                } catch (final InterruptedException ex) {
87                    continue; // tolerate an intrupt
88                }
89
90                if (!key.pollEvents().isEmpty()) {
91                    if (!input.terminalEditorRunning()) {
92                        saveFile();
93                    }
94                }
95
96                boolean valid = key.reset();
97                if (!valid) {
98                    errorHandler.accept("Invalid key");
99                    break;
100                }
101            }
102        });
103        watchedThread.start();
104    }
105
106    private void launch(final String cmd) throws IOException {
107        ProcessBuilder pb = new ProcessBuilder(cmd, tmpfile.toString());
108        pb = pb.inheritIO();
109
110        try {
111            input.suspend();
112            Process process = pb.start();
113            process.waitFor();
114        } catch (final IOException ex) {
115            errorHandler.accept("process IO failure: " + ex.getMessage());
116        } catch (final InterruptedException ex) {
117            errorHandler.accept("process interrupt: " + ex.getMessage());
118        } finally {
119            try {
120                watcher.close();
121                watchedThread.join(); //so that saveFile() is finished.
122                saveFile();
123            } catch (InterruptedException ex) {
124                errorHandler.accept("process interrupt: " + ex.getMessage());
125            } finally {
126                input.resume();
127            }
128        }
129    }
130
131    private void saveFile() {
132        List<String> lines;
133        try {
134            lines = Files.readAllLines(tmpfile);
135        } catch (final IOException ex) {
136            errorHandler.accept("Failure read edit file: " + ex.getMessage());
137            return ;
138        }
139        StringBuilder sb = new StringBuilder();
140        for (String ln : lines) {
141            sb.append(ln);
142            sb.append('\n');
143        }
144        saveHandler.accept(sb.toString());
145    }
146
147    static void edit(final String cmd, final Consumer<String> errorHandler, final String initialText,
148            final Consumer<String> saveHandler, final Console input) {
149        ExternalEditor ed = new ExternalEditor(errorHandler,  saveHandler, input);
150        ed.edit(cmd, initialText);
151    }
152}
153