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}