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