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 static java.util.Arrays.asList;
25  
26  import java.io.File;
27  import java.io.IOException;
28  import java.util.Collections;
29  import java.util.EventListener;
30  import java.util.HashMap;
31  import java.util.LinkedList;
32  import java.util.List;
33  import java.util.Map;
34  
35  import net.grinder.common.GrinderProperties;
36  import net.grinder.common.GrinderProperties.PersistenceException;
37  import net.grinder.console.common.ConsoleException;
38  import net.grinder.console.common.DisplayMessageConsoleException;
39  import net.grinder.console.common.Resources;
40  import net.grinder.console.distribution.AgentCacheState;
41  import net.grinder.console.distribution.FileChangeWatcher;
42  import net.grinder.console.distribution.FileChangeWatcher.FileChangedListener;
43  import net.grinder.util.ListenerSupport;
44  
45  
46  /**
47   * Editor model.
48   *
49   * @author Philip Aston
50   */
51  public final class EditorModel {
52  
53    private static final List<String> s_knownScriptTypes =
54      asList("py", "clj");
55  
56    private final Resources m_resources;
57    private final TextSource.Factory m_textSourceFactory;
58    private final AgentCacheState m_agentCacheState;
59  
60    private final ListenerSupport<Listener> m_listeners =
61      new ListenerSupport<Listener>();
62  
63    // Guarded by itself.
64    private final LinkedList<Buffer> m_bufferList = new LinkedList<Buffer>();
65  
66    // Guarded by itself.
67    private final Map<File, Buffer> m_fileBuffers =
68      Collections.synchronizedMap(new HashMap<File, Buffer>());
69  
70    // Guarded by this.
71    private int m_nextNewBufferNameIndex = 0;
72  
73    // Guarded by this.
74    private Buffer m_selectedBuffer;
75  
76    // Guarded by this.
77    private File m_selectedProperties;
78  
79    // Guarded by this.
80    private File m_selectedFile;
81  
82    // Guarded by this.
83    private ExternalEditor m_externalEditor;
84  
85    /**
86     * Constructor.
87     *
88     * @param resources ResourcesImplementation.
89     * @param textSourceFactory Factory for {@link TextSource}s.
90     * @param agentCacheState Notified when the model updates a file.
91     * @param fileChangeWatcher A FileDistribution.
92     */
93    public EditorModel(Resources resources,
94                       TextSource.Factory textSourceFactory,
95                       AgentCacheState agentCacheState,
96                       FileChangeWatcher fileChangeWatcher) {
97      m_resources = resources;
98      m_textSourceFactory = textSourceFactory;
99      m_agentCacheState = agentCacheState;
100 
101     fileChangeWatcher.addFileChangedListener(new FileChangedListener() {
102       public void filesChanged(File[] files) {
103         synchronized (m_fileBuffers) {
104           for (int i = 0; i < files.length; ++i) {
105             final Buffer buffer = getBufferForFile(files[i]);
106 
107             if (buffer != null && !buffer.isUpToDate()) {
108               fireBufferNotUpToDate(buffer);
109             }
110 
111             parseSelectedProperties(files[i]);
112           }
113         }
114       }
115     });
116   }
117 
118   /**
119    * Get the currently active buffer.
120    *
121    * @return The active buffer.
122    */
123   public Buffer getSelectedBuffer() {
124     synchronized (this) {
125       return m_selectedBuffer;
126     }
127   }
128 
129   /**
130    * Select a new buffer.
131    */
132   public void selectNewBuffer() {
133     final Buffer buffer = new BufferImplementation(m_resources,
134                                                    m_textSourceFactory.create(),
135                                                    createNewBufferName());
136     addBuffer(buffer);
137 
138     selectBuffer(buffer);
139   }
140 
141   /**
142    * Select the buffer for the given file.
143    *
144    * @param file
145    *          The file.
146    * @return The buffer.
147    * @throws ConsoleException
148    *           If a buffer could not be selected for the file.
149    */
150   public Buffer selectBufferForFile(File file) throws ConsoleException {
151     final Buffer existingBuffer = getBufferForFile(file);
152     final Buffer buffer;
153 
154     if (existingBuffer != null) {
155       buffer = existingBuffer;
156 
157       selectBuffer(buffer);
158 
159       if (!buffer.isUpToDate()) {
160         // The user's edits conflict with a file system change.
161         // We ensure the buffer is selected before firing this event because
162         // the UI might only raise out of date warnings for selected buffers.
163         fireBufferNotUpToDate(buffer);
164       }
165     }
166     else {
167       buffer = new BufferImplementation(m_resources,
168                                         m_textSourceFactory.create(),
169                                         file);
170       buffer.load();
171       addBuffer(buffer);
172 
173       m_fileBuffers.put(file, buffer);
174 
175       selectBuffer(buffer);
176     }
177 
178     return buffer;
179   }
180 
181   /**
182    * Get the buffer for the given file.
183    *
184    * @param file
185    *          The file.
186    * @return The buffer; <code>null</code> => there is no buffer for the file.
187    */
188   public Buffer getBufferForFile(File file) {
189     return m_fileBuffers.get(file);
190   }
191 
192   /**
193    * Return a copy of the current buffer list.
194    *
195    * @return The buffer list.
196    */
197   public Buffer[] getBuffers() {
198     synchronized (m_bufferList) {
199       return m_bufferList.toArray(new Buffer[m_bufferList.size()]);
200     }
201   }
202 
203   /**
204    * Return whether one of our buffers is dirty.
205    *
206    * @return <code>true</code> => a buffer is dirty.
207    */
208   public boolean isABufferDirty() {
209     final Buffer[] buffers = getBuffers();
210 
211     for (int i = 0; i < buffers.length; ++i) {
212       if (buffers[i].isDirty()) {
213         return true;
214       }
215     }
216 
217     return false;
218   }
219 
220   /**
221    * Select a buffer.
222    *
223    * @param buffer The buffer.
224    */
225   public void selectBuffer(Buffer buffer) {
226     final Buffer oldBuffer = getSelectedBuffer();
227 
228     if (buffer == null || !buffer.equals(oldBuffer)) {
229 
230       synchronized (this) {
231         m_selectedBuffer = buffer;
232       }
233 
234       if (oldBuffer != null) {
235         fireBufferStateChanged(oldBuffer);
236       }
237 
238       if (buffer != null) {
239         fireBufferStateChanged(buffer);
240       }
241     }
242   }
243 
244   /**
245    * Close a buffer.
246    *
247    * @param buffer The buffer.
248    */
249   public void closeBuffer(final Buffer buffer) {
250     final boolean removed;
251 
252     synchronized (m_bufferList) {
253       removed = m_bufferList.remove(buffer);
254     }
255 
256     if (removed) {
257       final File file = buffer.getFile();
258 
259       if (buffer.equals(getBufferForFile(file))) {
260         m_fileBuffers.remove(file);
261       }
262 
263       if (buffer.equals(getSelectedBuffer())) {
264         final Buffer bufferToSelect;
265 
266         synchronized (m_bufferList) {
267           final int numberOfBuffers = m_bufferList.size();
268 
269           bufferToSelect = numberOfBuffers > 0 ?
270               (Buffer)m_bufferList.get(numberOfBuffers - 1) : null;
271         }
272 
273         selectBuffer(bufferToSelect);
274       }
275 
276       m_listeners.apply(
277         new ListenerSupport.Informer<Listener>() {
278           public void inform(Listener l) { l.bufferRemoved(buffer); }
279         });
280     }
281   }
282 
283   /**
284    * Get the currently selected properties.
285    *
286    * @return The selected properties.
287    */
288   public File getSelectedPropertiesFile() {
289     synchronized (this) {
290       return m_selectedProperties;
291     }
292   }
293 
294   /**
295    * Set the currently selected properties.
296    *
297    * @param selectedProperties The selected properties.
298    */
299   public void setSelectedPropertiesFile(final File selectedProperties) {
300     synchronized (this) {
301       m_selectedProperties = selectedProperties;
302 
303       if (selectedProperties == null) {
304         m_selectedFile = null;
305       }
306     }
307 
308     parseSelectedProperties(selectedProperties);
309   }
310 
311   private void addBuffer(final Buffer buffer) {
312     buffer.getTextSource().addListener(new TextSource.Listener() {
313         public void textSourceChanged(boolean dirtyStateChanged) {
314           if (dirtyStateChanged) {
315             fireBufferStateChanged(buffer);
316           }
317         }
318       });
319 
320     buffer.addListener(
321       new BufferImplementation.Listener() {
322         public void bufferSaved(Buffer savedBuffer, File oldFile) {
323           final File newFile = savedBuffer.getFile();
324 
325           m_agentCacheState.setNewFileTime(newFile.lastModified());
326 
327           if (!newFile.equals(oldFile)) {
328             if (oldFile != null) {
329               m_fileBuffers.remove(oldFile);
330             }
331 
332             m_fileBuffers.put(newFile, savedBuffer);
333 
334             // Fire that bufferChanged because it is associated with a new
335             // file.
336             fireBufferStateChanged(savedBuffer);
337           }
338 
339           parseSelectedProperties(newFile);
340         }
341       }
342       );
343 
344     synchronized (m_bufferList) {
345       m_bufferList.add(buffer);
346     }
347 
348     m_listeners.apply(
349       new ListenerSupport.Informer<Listener>() {
350         public void inform(Listener l) { l.bufferAdded(buffer); }
351       });
352   }
353 
354   private void fireBufferStateChanged(final Buffer buffer) {
355     m_listeners.apply(
356       new ListenerSupport.Informer<Listener>() {
357         public void inform(Listener l) { l.bufferStateChanged(buffer); }
358       });
359   }
360 
361   /**
362    * The UI doesn't currently listen to this event, but might want to in the
363    * future.
364    */
365   private void fireBufferNotUpToDate(final Buffer buffer) {
366     m_listeners.apply(
367       new ListenerSupport.Informer<Listener>() {
368         public void inform(Listener l) { l.bufferNotUpToDate(buffer); }
369       });
370   }
371 
372   private String createNewBufferName() {
373 
374     final String prefix = m_resources.getString("newBuffer.text");
375 
376     synchronized (this) {
377       try {
378         if (m_nextNewBufferNameIndex == 0) {
379           return prefix;
380         }
381         else {
382           return prefix + " " + m_nextNewBufferNameIndex;
383         }
384       }
385       finally {
386         ++m_nextNewBufferNameIndex;
387       }
388     }
389   }
390 
391   private void parseSelectedProperties(File file) {
392 
393     if (file != null && file.equals(getSelectedPropertiesFile())) {
394       File selectedFile;
395 
396       try {
397         final GrinderProperties properties = new GrinderProperties(file);
398 
399         selectedFile =
400           properties.resolveRelativeFile(
401             properties.getFile(GrinderProperties.SCRIPT,
402                                GrinderProperties.DEFAULT_SCRIPT))
403           .getCanonicalFile();
404       }
405       catch (PersistenceException e) {
406         selectedFile = null;
407       }
408       catch (IOException e) {
409         selectedFile = null;
410       }
411 
412       synchronized (this) {
413         m_selectedFile = selectedFile;
414       }
415     }
416   }
417 
418   /**
419    * Add a new listener.
420    *
421    * @param listener The listener.
422    */
423   public void addListener(Listener listener) {
424     m_listeners.add(listener);
425   }
426 
427   /**
428    * Return whether the given file should be considered to be a script
429    * file. For now this is just based on name.
430    *
431    * @param f The file.
432    * @return <code>true</code> => its a Python file.
433    */
434   public boolean isScriptFile(File f) {
435     if (f != null  &&
436         (!f.exists() || f.isFile())) {
437 
438       final int lastDot = f.getName().lastIndexOf('.');
439 
440       if (lastDot >= 0) {
441         final String suffix = f.getName().substring(lastDot + 1).toLowerCase();
442 
443         return s_knownScriptTypes.contains(suffix);
444       }
445     }
446 
447     return false;
448   }
449 
450   /**
451    * Return whether the given file should be considered to be a grinder
452    * properties file. For now this is just based on name.
453    *
454    * @param f The file.
455    * @return <code>true</code> => its a properties file.
456    */
457   public boolean isPropertiesFile(File f) {
458     return
459       f != null &&
460       (!f.exists() || f.isFile()) &&
461       f.getName().toLowerCase().endsWith(".properties");
462   }
463 
464   /**
465    * Return whether the given file is the script file specified in the
466    * currently selected properties file.
467    *
468    * @param f The file.
469    * @return <code>true</code> => its the selected script.
470    */
471   public boolean isSelectedScript(File f) {
472     // We don't constrain selection to have a .py extension. If the
473     // user really wants to use something else, so be it.
474     synchronized (this) {
475       return f != null && f.equals(m_selectedFile);
476     }
477   }
478 
479   /**
480    * Return whether the given file should be marked as boring.
481    *
482    * @param f The file.
483    * @return a <code>true</code> => its boring.
484    */
485   public boolean isBoringFile(File f) {
486     if (f == null) {
487       return false;
488     }
489 
490     final String name = f.getName().toLowerCase();
491 
492     return
493       f.isHidden() ||
494       name.endsWith(".class") ||
495       name.startsWith("~") ||
496       name.endsWith("~") ||
497       name.startsWith("#") ||
498       name.endsWith(".exe") ||
499       name.endsWith(".gif") ||
500       name.endsWith(".jpeg") ||
501       name.endsWith(".jpg") ||
502       name.endsWith(".tiff");
503   }
504 
505 
506   /**
507    * Open the given file with the external file.
508    *
509    * @param file The file.
510    * @throws ConsoleException If the file could not be opened.
511    */
512   public void openWithExternalEditor(final File file) throws ConsoleException {
513     final ExternalEditor externalEditor;
514 
515     synchronized (this) {
516       externalEditor = m_externalEditor;
517     }
518 
519     if (externalEditor == null) {
520       throw new DisplayMessageConsoleException(m_resources,
521                                                "externalEditorNotSet.text");
522     }
523 
524     try {
525       externalEditor.open(file);
526     }
527     catch (IOException e) {
528       throw new DisplayMessageConsoleException(m_resources,
529                                                "externalEditError.text",
530                                                e);
531     }
532   }
533 
534   /**
535    * Set the external editor command line.
536    *
537    * @param command
538    *            Path to the external editor executable. <code>null</code> =>
539    *            no editor set.
540    * @param arguments
541    *            Arguments to pass to the external editor. Any <code>%f</code>
542    *            will be replaced with the absolute path of the file to edit.
543    *            If no <code>%f</code> is found, the file path will be appended
544    *            to the end of the command line.
545    */
546   public void setExternalEditor(File command, String arguments) {
547     final ExternalEditor externalEditor;
548     if (command == null) {
549       externalEditor = null;
550     }
551     else {
552       externalEditor =
553         new ExternalEditor(m_agentCacheState, this, command, arguments);
554     }
555 
556     synchronized (this) {
557       m_externalEditor = externalEditor;
558     }
559   }
560 
561   /**
562    * Interface for listeners.
563    */
564   public interface Listener extends EventListener {
565 
566     /**
567      * Called when a buffer has been added.
568      *
569      * @param buffer The buffer.
570      */
571     void bufferAdded(Buffer buffer);
572 
573     /**
574      * Called when a buffer's state has changed. I.e. the buffer has
575      * become dirty, or become clean, or has been selected, or has
576      * been unselected, or has become associated with a new file.
577      *
578      * @param buffer The buffer.
579      */
580     void bufferStateChanged(Buffer buffer);
581 
582     /**
583      * Called when an independent modification to a buffer's associated
584      * file has been detected.
585      *
586      * @param buffer The buffer.
587      */
588     void bufferNotUpToDate(Buffer buffer);
589 
590     /**
591      * Called when a buffer has been removed.
592      *
593      * @param buffer The buffer.
594      */
595     void bufferRemoved(Buffer buffer);
596   }
597 
598   /**
599    * Base {@link EditorModel.Listener} implementation that does nothing.
600    */
601   public abstract static class AbstractListener implements Listener {
602 
603     /**
604      * {@inheritDoc}
605      */
606     @Override public void bufferAdded(Buffer buffer) { }
607 
608     /**
609      * {@inheritDoc}
610      */
611     @Override public void bufferStateChanged(Buffer buffer) { }
612 
613     /**
614      * {@inheritDoc}
615      */
616     @Override public void bufferNotUpToDate(Buffer buffer) { }
617 
618     /**
619      * {@inheritDoc}
620      */
621     @Override public void bufferRemoved(Buffer buffer) { }
622   }
623 }