View Javadoc

1   // Copyright (C) 2004 - 2012 Philip Aston
2   // All rights reserved.
3   //
4   // This file is part of The Grinder software distribution. Refer to
5   // the file LICENSE which is part of The Grinder distribution for
6   // licensing details. The Grinder distribution is available on the
7   // Internet at http://grinder.sourceforge.net/
8   //
9   // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
10  // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
11  // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
12  // FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
13  // COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
14  // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
15  // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
16  // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
17  // HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
18  // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
19  // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
20  // OF THE POSSIBILITY OF SUCH DAMAGE.
21  
22  package net.grinder.console.editor;
23  
24  import java.io.BufferedReader;
25  import java.io.File;
26  import java.io.FileNotFoundException;
27  import java.io.FileReader;
28  import java.io.FileWriter;
29  import java.io.IOException;
30  import java.io.PrintWriter;
31  import java.io.StringWriter;
32  import java.io.Writer;
33  import java.util.EventListener;
34  
35  import net.grinder.common.Closer;
36  import net.grinder.common.UncheckedInterruptedException;
37  import net.grinder.console.common.DisplayMessageConsoleException;
38  import net.grinder.console.common.Resources;
39  import net.grinder.util.ListenerSupport;
40  
41  
42  /**
43   * Implementation of {@link Buffer}.
44   *
45   * @author Philip Aston
46   */
47  final class BufferImplementation implements Buffer {
48  
49    private final Resources m_resources;
50    private final TextSource m_textSource;
51  
52    private final ListenerSupport<Listener> m_listeners =
53      new ListenerSupport<Listener>();
54  
55    private String m_name;
56    private File m_file;
57  
58    private long m_lastModified = -1;
59  
60    /**
61     * Constructor for buffers with no associated file.
62     *
63     * @param resources Console resources.
64     * @param textSource The text editor.
65     */
66    BufferImplementation(Resources resources,
67                         TextSource textSource,
68                         String name) {
69      m_resources = resources;
70      m_textSource = textSource;
71      m_file = null;
72      m_name = name;
73    }
74  
75    /**
76     * Constructor for buffers with an associated file.
77     *
78     * @param resources Console resources.
79     * @param textSource The text editor.
80     * @param file The file.
81     */
82    BufferImplementation(Resources resources, TextSource textSource, File file) {
83      m_resources = resources;
84      m_textSource = textSource;
85      setFile(file);
86    }
87  
88    /**
89     * Return the buffer's {@link TextSource}.
90     *
91     * @return The text source.
92     */
93    public TextSource getTextSource() {
94      return m_textSource;
95    }
96  
97    /**
98     * Update the text source from the file.
99     *
100    * @exception DisplayMessageConsoleException If the file could not
101    * be read from.
102    * @exception EditorException If an unexpected problem occurs.
103    */
104   public void load() throws DisplayMessageConsoleException, EditorException {
105     // Should never be called if there is no associated file, but
106     // check anyway.
107     if (m_file == null) {
108       throw new EditorException(
109         "Can't load a buffer that has no associated file");
110     }
111 
112     final StringWriter stringWriter = new StringWriter();
113     BufferedReader reader = null;
114 
115     try {
116       // We use a BufferedReader to canonicalise line endings
117       reader = new BufferedReader(new FileReader(m_file));
118 
119       while (true) {
120         final String line = reader.readLine();
121 
122         if (line == null) {
123           break;
124         }
125 
126         stringWriter.write(line);
127         stringWriter.write('\n');
128       }
129     }
130     catch (IOException e) {
131       UncheckedInterruptedException.ioException(e);
132 
133       throw new DisplayMessageConsoleException(
134         m_resources,
135         "fileReadError.text",
136         new Object[] { m_file,
137                        ".\n(" + extractReasonFromIOException(e) + ")",
138         },
139         e);
140     }
141     finally {
142       Closer.close(reader);
143     }
144 
145     m_textSource.setText(stringWriter.toString());
146 
147     m_lastModified = m_file.lastModified();
148   }
149 
150   /**
151    * Update the buffer's file from the text source.
152    *
153    * @exception DisplayMessageConsoleException If the file could not
154    * be written to.
155    * @exception EditorException If an unexpected problem occurs.
156    */
157   public void save() throws DisplayMessageConsoleException, EditorException {
158     // The UI should never call save if there is no associated file,
159     // but check anyway.
160     if (m_file == null) {
161       throw new EditorException(
162         "Can't save a buffer that has no associated file");
163     }
164 
165     save(m_file);
166   }
167 
168   /**
169    * Update a file from the text source and, if successful, associate
170    * the buffer with the new file.
171    *
172    * @param file The file.
173    * @exception DisplayMessageConsoleException If the file could not
174    * be written to.
175    */
176   public void save(File file) throws DisplayMessageConsoleException {
177     final File oldFile = getFile();
178 
179     Writer fileWriter = null;
180 
181     try {
182       // Calling getText() causes the text source to be set to "clean"
183       // and a buffer changed event to be fired by the EditorModel.
184       final String text = m_textSource.getText();
185 
186       // Line-oriented output using the platform line ending. In the future,
187       // we may try to preserve the predominant line ending of the original
188       // input.
189       final String[] lines = text.split("\n", -1);
190 
191       fileWriter = new FileWriter(file);
192       final PrintWriter printWriter = new PrintWriter(fileWriter);
193 
194       for (int i = 0; i < lines.length; ++i) {
195         printWriter.println(lines[i]);
196       }
197 
198       setFile(file);
199       printWriter.close();      // Close necessary to ensure last
200                                 // modified time is updated?
201       m_lastModified = m_file.lastModified();
202 
203       m_listeners.apply(
204         new ListenerSupport.Informer<Listener>() {
205           public void inform(Listener l) {
206             l.bufferSaved(BufferImplementation.this, oldFile);
207           }
208         });
209     }
210     catch (IOException e) {
211       UncheckedInterruptedException.ioException(e);
212 
213       throw new DisplayMessageConsoleException(
214         m_resources,
215         "fileWriteError.text",
216         new Object[] { m_file,
217                        "./n(" + extractReasonFromIOException(e) + ")",
218         },
219         e);
220     }
221     finally {
222       Closer.close(fileWriter);
223     }
224   }
225 
226   /**
227    * Return whether the buffer's text has been changed since the last
228    * save.
229    *
230    * @return <code>true</code> => the text has changed.
231    */
232   public boolean isDirty() {
233     return m_textSource.isDirty();
234   }
235 
236   private void setFile(File file) {
237     m_file = file;
238     m_name = file.getName();
239   }
240 
241   /**
242    * Return the buffer's associated file.
243    *
244    * @return The file. <code>null</code> if there is no associated file.
245    */
246   public File getFile() {
247     return m_file;
248   }
249 
250   /**
251    * Return whether the file has been independently modified since the
252    * last save.
253    *
254    * @return <code>true</code> => the file has changed independently
255    * of the buffer.
256    */
257   public boolean isUpToDate() {
258     return m_file == null || m_lastModified == m_file.lastModified();
259   }
260 
261   /**
262    * Get the type of the buffer.
263    *
264    * @return The buffer's type.
265    */
266   public Type getType() {
267 
268     if (m_file != null) {
269       final String name = m_file.getName();
270       final int lastDot = name.lastIndexOf('.');
271 
272       if (lastDot >= 0) {
273         final String extension = name.substring(lastDot + 1);
274         return Type.forExtension(extension);
275       }
276     }
277 
278     return Type.TEXT_BUFFER;
279   }
280 
281   /**
282    * Return display name of buffer.
283    *
284    * @return The buffer's name.
285    */
286   public String getDisplayName() {
287     return m_name;
288   }
289 
290     /**
291      * Useful for debugging.
292      *
293      * @return Description of the Buffer.
294      */
295   @Override public String toString() {
296     return "<Buffer " + hashCode() + " '" + getDisplayName() + "'>";
297   }
298 
299   /**
300    * Add a new listener.
301    *
302    * @param listener The listener.
303    */
304   public void addListener(Listener listener) {
305     m_listeners.add(listener);
306   }
307 
308   /**
309    * Interface for listeners.
310    */
311   public interface Listener extends EventListener {
312 
313     /**
314      * Called when a buffer has been saved.
315      *
316      * @param buffer The buffer.
317      * @param oldFile The File the buffer was previously associated with.
318      */
319     void bufferSaved(Buffer buffer, File oldFile);
320   }
321 
322   /**
323    * Protected for unit tests.
324    */
325   static String extractReasonFromIOException(IOException e) {
326     if (e instanceof FileNotFoundException) {
327       final String message = e.getMessage();
328 
329       final int firstParenthesis = message.indexOf('(');
330       final int secondsParenthesis = message.indexOf(')', firstParenthesis);
331 
332       if (firstParenthesis >= 0 && secondsParenthesis > firstParenthesis + 1) {
333         return message.substring(firstParenthesis + 1, secondsParenthesis);
334       }
335     }
336 
337     return "";
338   }
339 }