001/* A representative of a text file.
002
003 Copyright (c) 1998-2016 The Regents of the University of California.
004 All rights reserved.
005 Permission is hereby granted, without written agreement and without
006 license or royalty fees, to use, copy, modify, and distribute this
007 software and its documentation for any purpose, provided that the above
008 copyright notice and the following two paragraphs appear in all copies
009 of this software.
010
011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
015 SUCH DAMAGE.
016
017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
022 ENHANCEMENTS, OR MODIFICATIONS.
023
024 PT_COPYRIGHT_VERSION_2
025 COPYRIGHTENDKEY
026 */
027package ptolemy.actor.gui;
028
029import java.io.BufferedReader;
030import java.io.File;
031import java.io.IOException;
032import java.io.InputStream;
033import java.io.InputStreamReader;
034import java.lang.reflect.Constructor;
035import java.lang.reflect.Method;
036import java.net.URL;
037import java.util.Locale;
038
039import javax.swing.text.BadLocationException;
040import javax.swing.text.DefaultStyledDocument;
041import javax.swing.text.Document;
042
043import ptolemy.kernel.CompositeEntity;
044import ptolemy.kernel.util.IllegalActionException;
045import ptolemy.kernel.util.NameDuplicationException;
046import ptolemy.kernel.util.Workspace;
047
048///////////////////////////////////////////////////////////////////
049//// TextEffigy
050
051/**
052 An effigy for a text file.  If the ptolemy.user.texteditor property
053 is set to "emacs", then {@link ExternalTextEffigy} is used as an Effigy,
054 otherwise this class is used as an Effigy.
055
056 @author Edward A. Lee, contributor Zoltan Kemenczy
057 @version $Id$
058 @since Ptolemy II 1.0
059 @Pt.ProposedRating Red (neuendor)
060 @Pt.AcceptedRating Red (neuendor)
061 */
062public class TextEffigy extends Effigy {
063    /** Create a new effigy in the specified workspace with an empty string
064     *  for its name.
065     *  @param workspace The workspace for this effigy.
066     */
067    public TextEffigy(Workspace workspace) {
068        super(workspace);
069    }
070
071    /** Create a new effigy in the given directory with the given name.
072     *  @param container The directory that contains this effigy.
073     *  @param name The name of this effigy.
074     *  @exception IllegalActionException If the entity cannot be contained
075     *   by the proposed container.
076     *  @exception NameDuplicationException If the name coincides with
077     *   an entity already in the container.
078     */
079    public TextEffigy(CompositeEntity container, String name)
080            throws IllegalActionException, NameDuplicationException {
081        super(container, name);
082    }
083
084    ///////////////////////////////////////////////////////////////////
085    ////                         public methods                    ////
086
087    /** Return the syntax style to use for files with the given extension.
088     *  @param extension The file extension.
089     *  @return A syntax style, or none if the extension is not recognized.
090     */
091    public static String extensionToSyntaxStyle(String extension) {
092        extension = extension.trim().toLowerCase();
093        switch (extension) {
094        // The returned strings are defined in
095        // org.fife.ui.rsyntaxtextarea.SyntaxConstants
096        // but we don't want a hard dependence on an external
097        // library, so we have replicate those strings here.
098        case "c":
099            return "text/c";
100        case "clj":
101            return "text/clojure";
102        case "cpp":
103            return "text/cpp";
104        case "cs":
105            return "text/cs";
106        case "css":
107            return "text/css";
108        case "dtd":
109            return "text/dtd";
110        case "f":
111        case "f90":
112            return "text/fortran";
113        case "groovy":
114        case "gvy":
115        case "gy":
116            return "text/groovy";
117        case "h":
118            return "text/cpp";
119        case "htm":
120        case "html":
121            return "text/html";
122        case "java":
123            return "text/java";
124        case "js":
125        case "javascript":
126            return "text/javascript";
127        case "json":
128            return "text/json";
129        case "jsp":
130            return "text/jsp";
131        case "tex":
132        case "latex":
133            return "text/latex";
134        case "mk":
135            return "text/makefile";
136        case "pl":
137            return "text/perl";
138        case "php":
139            return "text/php";
140        case "properties":
141            return "text/properties";
142        case "py":
143        case "python":
144            return "text/python";
145        case "rby":
146        case "ruby":
147            return "text/ruby";
148        case "scala":
149            return "text/scala";
150        case "sh":
151            return "text/unix";
152        case "sql":
153            return "text/sql";
154        case "tcl":
155            return "text/tcl";
156        case "txt":
157            return "text/plain";
158        case "vb":
159            return "text/vb";
160        case "bat":
161            return "text/bat";
162        case "xml":
163            return "text/xml";
164        default:
165            return null;
166        }
167    }
168
169    /** Return the document that this is an effigy of.
170     *  @return The document, or null if none has been set.
171     *  @see #setDocument(Document)
172     */
173    public Document getDocument() {
174        return _doc;
175    }
176
177    /** Return the syntax style for the document, if one has been identified,
178     *  and null otherwise.
179     *  @return A syntax style or null.
180     */
181    public String getSyntaxStyle() {
182        return _syntaxStyle;
183    }
184
185    /** Override the base class to compare the current text in the document
186     *  against the original text.
187     *  @return True if the data has been modified.
188     */
189    @Override
190    public boolean isModified() {
191        if (_originalText == null) {
192            if (_doc.getLength() > 0) {
193                return true;
194            } else {
195                return false;
196            }
197        }
198        try {
199            if (_originalText.equals(_doc.getText(0, _doc.getLength()))) {
200                return false;
201            }
202        } catch (BadLocationException e) {
203            // This should not happen.
204            return true;
205        }
206        return true;
207    }
208
209    /** Create a new effigy in the given container containing the specified
210     *  text.  The new effigy will have a new instance of
211     *  DefaultStyledDocument associated with it.
212     *  @param container The container for the effigy.
213     *  @param text The text to insert in the effigy.
214     *  @return A new instance of SyntaxTextEffigy.
215     *  @exception Exception If the text effigy cannot be
216     *   contained by the specified container, or if the specified
217     *   text cannot be inserted into the document.
218     */
219    public static TextEffigy newTextEffigy(CompositeEntity container,
220            String text) throws Exception {
221        return newTextEffigy(container, text, null);
222    }
223
224    /** Create a new effigy in the given container containing the specified
225     *  text.  The new effigy will have a new instance of
226     *  DefaultStyledDocument associated with it.
227     *  @param container The container for the effigy.
228     *  @param text The text to insert in the effigy.
229     *  @param syntaxStyle The style of the text, for highlighting.
230     *   This can be one of the styles defined in org.fife.ui.rsyntaxtextarea.SyntaxConstants,
231     *   if that is installed,
232     *   or null or an empty string for plain text. If the style is not recognized, then
233     *   plain text will be assumed.
234     *  @return A new instance of SyntaxTextEffigy.
235     *  @exception Exception If the text effigy cannot be
236     *   contained by the specified container, or if the specified
237     *   text cannot be inserted into the document.
238     */
239    public static TextEffigy newTextEffigy(CompositeEntity container,
240            String text, String syntaxStyle) throws Exception {
241        // Create a new effigy.
242        TextEffigy effigy = new TextEffigy(container,
243                container.uniqueName("effigy"));
244        if (syntaxStyle == null || syntaxStyle.trim().equals("")) {
245            syntaxStyle = "text/plain";
246        }
247        effigy._syntaxStyle = syntaxStyle;
248        Document doc = _createDocument(syntaxStyle);
249        effigy.setDocument(doc);
250
251        if (text != null) {
252            doc.insertString(0, text, null);
253        }
254        effigy._originalText = text;
255        return effigy;
256    }
257
258    /** Create a new effigy in the given container by reading the specified
259     *  URL. If the specified URL is null, then create a blank effigy.
260     *  If the extension of the URL is one of several extensions used by
261     *  binary formats, the file is not opened and this returns null.
262     *  The new effigy will have a new instance of
263     *  DefaultStyledDocument associated with it.
264     *  @param container The container for the effigy.
265     *  @param base The base for relative file references, or null if
266     *   there are no relative file references.  This is ignored in this
267     *   class.
268     *  @param in The input URL, or null if there is none.
269     *  @return A new instance of SyntaxTextEffigy.
270     *  @exception Exception If the URL cannot be read, or if the data
271     *   is malformed in some way.
272     */
273    public static TextEffigy newTextEffigy(CompositeEntity container, URL base,
274            URL in) throws Exception {
275
276        // Check the extension: if it looks like a binary file do not open.
277        // Do not open KAR files,
278        // see http://bugzilla.ecoinformatics.org/show_bug.cgi?id=5280#c1
279        // For other extensions, determine the syntax style (a MIME type).
280        //
281        String syntaxStyle = "text/plain";
282        if (in != null) {
283            String extension = EffigyFactory.getExtension(in)
284                    .toLowerCase(Locale.getDefault());
285            String syntaxStyleFromExtension = extensionToSyntaxStyle(extension);
286            if (syntaxStyleFromExtension != null) {
287                syntaxStyle = syntaxStyleFromExtension;
288            } else if (extension.equals("jar") || extension.equals("kar")
289            // TODO: find a better way to check for binary files.
290                    || extension.equals("gz") || extension.equals("tar")
291                    || extension.equals("zip")) {
292                return null;
293            }
294        }
295
296        // Create a new effigy.
297        TextEffigy effigy = new TextEffigy(container,
298                container.uniqueName("effigy"));
299        Document doc = _createDocument(syntaxStyle);
300        effigy.setDocument(doc);
301        effigy._syntaxStyle = syntaxStyle;
302
303        if (in != null) {
304            // A URL has been given.  Read it.
305            BufferedReader reader = null;
306
307            try {
308                try {
309                    InputStream inputStream = null;
310
311                    try {
312                        inputStream = in.openStream();
313                    } catch (NullPointerException npe) {
314                        throw new IOException(
315                                "Failed to open '" + in + "', base: '" + base
316                                        + "' : openStream() threw a "
317                                        + "NullPointerException");
318                    } catch (Exception ex) {
319                        IOException exception = new IOException(
320                                "Failed to open '" + in + "\", base: \"" + base
321                                        + "\"");
322                        exception.initCause(ex);
323                        throw exception;
324                    }
325
326                    reader = new BufferedReader(
327                            new InputStreamReader(inputStream));
328
329                    // openStream throws an IOException, not a
330                    // FileNotFoundException
331                } catch (IOException ex) {
332                    try {
333                        // If we are running under WebStart, and try
334                        // view source on a .html file that is not in
335                        // ptsupport.jar, then we may end up here,
336                        // so we look for the file as a resource.
337                        URL jarURL = ptolemy.util.ClassUtilities
338                                .jarURLEntryResource(in.toString());
339                        reader = new BufferedReader(
340                                new InputStreamReader(jarURL.openStream()));
341
342                        // We were able to open the URL, so update the
343                        // original URL so that the title bar accurately
344                        // reflects the location of the file.
345                        in = jarURL;
346                    } catch (Throwable throwable) {
347                        try {
348                            // Hmm.  Might be Eclipse, where sadly the
349                            // .class files are often in a separate directory
350                            // than the .java files.  So, we look at the CLASSPATH
351                            // and for each element that names a directory, traverse
352                            // the parents directories and look for adjacent directories
353                            // that contain a "src" directory.  For example if
354                            // the classpath contains "kepler/ptolemy/target/classes/",
355                            // then we will find kepler/ptolemy/src and return it
356                            // as a URL.  See also Configuration.createPrimaryTableau()
357
358                            URL sourceURL = ptolemy.util.ClassUtilities
359                                    .sourceResource(in.toString());
360                            reader = new BufferedReader(new InputStreamReader(
361                                    sourceURL.openStream()));
362
363                            // We were able to open the URL, so update the
364                            // original URL so that the title bar accurately
365                            // reflects the location of the file.
366                            in = sourceURL;
367
368                        } catch (Throwable throwable2) {
369                            // Looking for the file as a resource did not work,
370                            // so we rethrow the original exception.
371                            throw ex;
372                        }
373                    }
374                }
375
376                String line = reader.readLine();
377
378                while (line != null) {
379                    // Translate newlines to Java form.
380                    doc.insertString(doc.getLength(), line + "\n", null);
381                    line = reader.readLine();
382                }
383            } finally {
384                if (reader != null) {
385                    reader.close();
386                }
387            }
388
389            // Check the URL to see whether it is a file,
390            // and if so, whether it is writable.
391            if (in.getProtocol().equals("file")) {
392                String filename = in.getFile();
393                File file = new File(filename);
394
395                try {
396                    if (!file.canWrite()) {
397                        effigy.setModifiable(false);
398                    }
399                } catch (SecurityException ex) {
400                    // We are in an applet or sandbox.
401                    effigy.setModifiable(false);
402                }
403            } else {
404                effigy.setModifiable(false);
405            }
406
407            effigy.uri.setURL(in);
408        } else {
409            // No document associated.  Allow modifications.
410            effigy.setModifiable(true);
411        }
412        effigy._originalText = doc.getText(0, doc.getLength());
413        return effigy;
414    }
415
416    /** Set the document that this is an effigy of.
417     *  @param document The document
418     *  @see #getDocument()
419     */
420    public void setDocument(Document document) {
421        _doc = document;
422    }
423
424    @Override
425    public void setModified(boolean modified) {
426        super.setModified(modified);
427        if (!modified) {
428            // If someone is indicating that this is no longer modified, then reset
429            // the _originalText to equal the current text.
430            try {
431                _originalText = _doc.getText(0, _doc.getLength());
432            } catch (Exception ex) {
433                // Should not occur. Ignore. Worst case is an extra prompt to apply.
434            }
435        }
436    }
437
438    /** Write the text of the document to the specified file.
439     *  @param file The file to write to.
440     *  @exception IOException If the write fails.
441     */
442    @Override
443    public void writeFile(File file) throws IOException {
444        if (_doc != null) {
445            java.io.FileWriter fileWriter = null;
446
447            try {
448                fileWriter = new java.io.FileWriter(file);
449
450                try {
451                    fileWriter.write(_doc.getText(0, _doc.getLength()));
452                } catch (BadLocationException ex) {
453                    throw new IOException(
454                            "Failed to get text from the document: " + ex);
455                }
456            } finally {
457                if (fileWriter != null) {
458                    fileWriter.close();
459                }
460            }
461        }
462    }
463
464    ///////////////////////////////////////////////////////////////////
465    ////                         protected method                  ////
466
467    /** Create a syntax document, if possible, and otherwise a plain
468     *  document.
469     *  @param syntaxStyle The syntax style.
470     *  @return A new document.
471     */
472    protected static Document _createDocument(String syntaxStyle) {
473        Document doc = null;
474        try {
475            // Attempt to create a styled document.
476            // Use reflection here to avoid a hard dependency on an external package.
477            Class docClass = Class
478                    .forName("org.fife.ui.rsyntaxtextarea.RSyntaxDocument");
479            Constructor docClassConstructor = docClass
480                    .getConstructor(String.class);
481            doc = (Document) docClassConstructor
482                    .newInstance(new Object[] { syntaxStyle });
483        } catch (Throwable ex) {
484            // Ignore and use default text editor.
485            System.out.println("Note: failed to open syntax-directed editor: "
486                    + ex.getMessage());
487        }
488        if (doc == null) {
489            doc = new DefaultStyledDocument();
490        }
491        return doc;
492    }
493
494    ///////////////////////////////////////////////////////////////////
495    ////                         private members                   ////
496
497    /** The document associated with this effigy. */
498    private Document _doc;
499
500    /** The original text, to determine whether it has been modified. */
501    private String _originalText;
502
503    /** The syntax style, if one has been identified. */
504    private String _syntaxStyle;
505
506    ///////////////////////////////////////////////////////////////////
507    ////                         inner classes                     ////
508
509    /** A factory for creating new effigies.
510     */
511    public static class Factory extends EffigyFactory {
512        /** Create a factory with the given name and container.
513         *  @param container The container.
514         *  @param name The name.
515         *  @exception IllegalActionException If the container is incompatible
516         *   with this entity.
517         *  @exception NameDuplicationException If the name coincides with
518         *   an entity already in the container.
519         */
520        public Factory(CompositeEntity container, String name)
521                throws IllegalActionException, NameDuplicationException {
522            super(container, name);
523
524            try {
525                String editorPreference = ".";
526
527                try {
528                    editorPreference = System
529                            .getProperty("ptolemy.user.texteditor", ".");
530                } catch (SecurityException security) {
531                    // Ignore, we are probably running in a sandbox
532                    // or applet.
533                }
534
535                Class effigyClass;
536
537                if (editorPreference.equals("emacs")) {
538                    effigyClass = Class
539                            .forName("ptolemy.actor.gui.ExternalTextEffigy");
540                } else {
541                    effigyClass = Class.forName("ptolemy.actor.gui.TextEffigy");
542                }
543
544                _newTextEffigyURL = effigyClass.getMethod("newTextEffigy",
545                        new Class[] { CompositeEntity.class, URL.class,
546                                URL.class });
547            } catch (ClassNotFoundException ex) {
548                throw new IllegalActionException(ex.toString());
549            } catch (NoSuchMethodException ex) {
550                throw new IllegalActionException(ex.toString());
551            }
552        }
553
554        ///////////////////////////////////////////////////////////////
555        ////                     public methods                    ////
556
557        /** Return true, indicating that this effigy factory is
558         *  capable of creating an effigy without a URL being specified.
559         *  @return True.
560         */
561        @Override
562        public boolean canCreateBlankEffigy() {
563            return true;
564        }
565
566        /** Create a new effigy in the given container by reading the specified
567         *  URL. If the specified URL is null, then create a blank effigy.
568         *  The extension of the URL is not
569         *  checked, so this will open any file.  Thus, this factory
570         *  should be last on the list of effigy factories in the
571         *  configuration.
572         *  The new effigy will have a new instance of
573         *  DefaultStyledDocument associated with it.
574         *  @param container The container for the effigy.
575         *  @param base The base for relative file references, or null if
576         *   there are no relative file references.  This is ignored in this
577         *   class.
578         *  @param in The input URL.
579         *  @return A new instance of TextEffigy.
580         *  @exception Exception If the URL cannot be read, or if the data
581         *   is malformed in some way.
582         */
583        @Override
584        public Effigy createEffigy(CompositeEntity container, URL base, URL in)
585                throws Exception {
586            // Create a new effigy.
587            try {
588                return (Effigy) _newTextEffigyURL.invoke(null,
589                        new Object[] { container, base, in });
590            } catch (java.lang.reflect.InvocationTargetException ex) {
591                throw (Exception) ex.getCause();
592                // Uncomment this for debugging
593                // throw new java.lang.reflect.InvocationTargetException(ex,
594                // " Invocation of method failed!. Method was: "
595                // + _newTextEffigyURL
596                // + "\nwith arguments( container = " + container
597                // + " base = " + base + " in = " + in + ")");
598            }
599        }
600
601        private Method _newTextEffigyURL;
602    }
603}