ExternalEditor.java revision 15988:1396fb6d0279
1/*
2 * Copyright (c) 2015, 2016, 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.internal.editor.external;
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.Arrays;
37import java.util.Scanner;
38import java.util.function.Consumer;
39import java.util.stream.Collectors;
40import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
41import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
42import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
43
44/**
45 * Wrapper for controlling an external editor.
46 */
47public class ExternalEditor {
48    private final Consumer<String> errorHandler;
49    private final Consumer<String> saveHandler;
50    private final boolean wait;
51
52    private final Runnable suspendInteractiveInput;
53    private final Runnable resumeInteractiveInput;
54    private final Runnable promptForNewLineToEndWait;
55
56    private WatchService watcher;
57    private Thread watchedThread;
58    private Path dir;
59    private Path tmpfile;
60
61    /**
62     * Launch an external editor.
63     *
64     * @param cmd the command to launch (with parameters)
65     * @param initialText initial text in the editor buffer
66     * @param errorHandler handler for error messages
67     * @param saveHandler handler sent the buffer contents on save
68     * @param suspendInteractiveInput a callback to suspend caller (shell) input
69     * @param resumeInteractiveInput a callback to resume caller input
70     * @param wait true, if editor process termination cannot be used to
71     * determine when done
72     * @param promptForNewLineToEndWait a callback to prompt for newline if
73     * wait==true
74     */
75    public static void edit(String[] cmd, String initialText,
76            Consumer<String> errorHandler,
77            Consumer<String> saveHandler,
78            Runnable suspendInteractiveInput,
79            Runnable resumeInteractiveInput,
80            boolean wait,
81            Runnable promptForNewLineToEndWait) {
82        ExternalEditor ed = new ExternalEditor(errorHandler, saveHandler, suspendInteractiveInput,
83             resumeInteractiveInput, wait, promptForNewLineToEndWait);
84        ed.edit(cmd, initialText);
85    }
86
87    ExternalEditor(Consumer<String> errorHandler,
88            Consumer<String> saveHandler,
89            Runnable suspendInteractiveInput,
90            Runnable resumeInteractiveInput,
91            boolean wait,
92            Runnable promptForNewLineToEndWait) {
93        this.errorHandler = errorHandler;
94        this.saveHandler = saveHandler;
95        this.wait = wait;
96        this.suspendInteractiveInput = suspendInteractiveInput;
97        this.resumeInteractiveInput = resumeInteractiveInput;
98        this.promptForNewLineToEndWait = promptForNewLineToEndWait;
99    }
100
101    private void edit(String[] cmd, String initialText) {
102        try {
103            setupWatch(initialText);
104            launch(cmd);
105        } catch (IOException ex) {
106            errorHandler.accept(ex.getMessage());
107        }
108    }
109
110    /**
111     * Creates a WatchService and registers the given directory
112     */
113    private void setupWatch(String initialText) throws IOException {
114        this.watcher = FileSystems.getDefault().newWatchService();
115        this.dir = Files.createTempDirectory("extedit");
116        this.tmpfile = Files.createTempFile(dir, null, ".java");
117        Files.write(tmpfile, initialText.getBytes(Charset.forName("UTF-8")));
118        dir.register(watcher,
119                ENTRY_CREATE,
120                ENTRY_DELETE,
121                ENTRY_MODIFY);
122        watchedThread = new Thread(() -> {
123            for (;;) {
124                WatchKey key;
125                try {
126                    key = watcher.take();
127                } catch (ClosedWatchServiceException ex) {
128                    // The watch service has been closed, we are done
129                    break;
130                } catch (InterruptedException ex) {
131                    // tolerate an interrupt
132                    continue;
133                }
134
135                if (!key.pollEvents().isEmpty()) {
136                    saveFile();
137                }
138
139                boolean valid = key.reset();
140                if (!valid) {
141                    // The watch service has been closed, we are done
142                    break;
143                }
144            }
145        });
146        watchedThread.start();
147    }
148
149    private void launch(String[] cmd) throws IOException {
150        String[] params = Arrays.copyOf(cmd, cmd.length + 1);
151        params[cmd.length] = tmpfile.toString();
152        ProcessBuilder pb = new ProcessBuilder(params);
153        pb = pb.inheritIO();
154
155        try {
156            suspendInteractiveInput.run();
157            Process process = pb.start();
158            // wait to exit edit mode in one of these ways...
159            if (wait) {
160                // -wait option -- ignore process exit, wait for carriage-return
161                Scanner scanner = new Scanner(System.in);
162                promptForNewLineToEndWait.run();
163                scanner.nextLine();
164            } else {
165                // wait for process to exit
166                process.waitFor();
167            }
168        } catch (IOException ex) {
169            errorHandler.accept("process IO failure: " + ex.getMessage());
170        } catch (InterruptedException ex) {
171            errorHandler.accept("process interrupt: " + ex.getMessage());
172        } finally {
173            try {
174                watcher.close();
175                watchedThread.join(); //so that saveFile() is finished.
176                saveFile();
177            } catch (InterruptedException ex) {
178                errorHandler.accept("process interrupt: " + ex.getMessage());
179            } finally {
180                resumeInteractiveInput.run();
181            }
182        }
183    }
184
185    private void saveFile() {
186        try {
187            saveHandler.accept(Files.lines(tmpfile).collect(Collectors.joining("\n", "", "\n")));
188        } catch (IOException ex) {
189            errorHandler.accept("Failure in read edit file: " + ex.getMessage());
190        }
191    }
192}
193