001/* A representative of a ptolemy model 002 003 Copyright (c) 1998-2014 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.File; 030import java.io.IOException; 031import java.net.URI; 032import java.net.URL; 033import java.util.List; 034import java.util.Locale; 035 036import ptolemy.actor.Manager; 037import ptolemy.actor.TypedCompositeActor; 038import ptolemy.kernel.CompositeEntity; 039import ptolemy.kernel.attributes.URIAttribute; 040import ptolemy.kernel.util.Attribute; 041import ptolemy.kernel.util.ChangeListener; 042import ptolemy.kernel.util.ChangeRequest; 043import ptolemy.kernel.util.IllegalActionException; 044import ptolemy.kernel.util.NameDuplicationException; 045import ptolemy.kernel.util.NamedObj; 046import ptolemy.kernel.util.Workspace; 047import ptolemy.moml.MoMLParser; 048import ptolemy.moml.ParserAttribute; 049import ptolemy.util.ClassUtilities; 050import ptolemy.util.MessageHandler; 051import ptolemy.util.StringUtilities; 052 053/////////////////////////////////////////////////////////////////// 054//// PtolemyEffigy 055 056/** 057 An effigy for a Ptolemy II model. 058 An effigy represents model metadata, and is contained by the 059 model directory or by another effigy. This class adds to the base 060 class an association with a Ptolemy II model. The model, strictly 061 speaking, is any Ptolemy II object (an instance of NamedObj). 062 The Effigy class extends CompositeEntity, so an instance of Effigy 063 can contain entities. By convention, an effigy contains all 064 open instances of Tableau associated with the model. 065 066 @author Steve Neuendorffer and Edward A. Lee 067 @version $Id$ 068 @since Ptolemy II 1.0 069 @Pt.ProposedRating Green (eal) 070 @Pt.AcceptedRating Yellow (janneck) 071 */ 072public class PtolemyEffigy extends Effigy implements ChangeListener { 073 /** Create a new effigy in the specified workspace with an empty string 074 * for its name. 075 * @param workspace The workspace for this effigy. 076 */ 077 public PtolemyEffigy(Workspace workspace) { 078 super(workspace); 079 } 080 081 /** Create a new effigy in the given container with the given name. 082 * @param container The container that contains this effigy. 083 * @param name The name of this effigy. 084 * @exception IllegalActionException If the entity cannot be contained 085 * by the proposed container. 086 * @exception NameDuplicationException If the name coincides with 087 * an entity already in the container. 088 */ 089 public PtolemyEffigy(CompositeEntity container, String name) 090 throws IllegalActionException, NameDuplicationException { 091 super(container, name); 092 } 093 094 /////////////////////////////////////////////////////////////////// 095 //// public methods //// 096 097 /** React to the fact that a change has been successfully executed. 098 * This method does nothing. 099 * @param change The change that has been executed. 100 */ 101 @Override 102 public void changeExecuted(ChangeRequest change) { 103 // Initially, the undo facility wanted us to call 104 // setModified(true) here. However, if we do, then running an 105 // SDF Model that sets the bufferSize of a relation would 106 // result in the model being marked as modified and the user 107 // being queried about saving the model upon exit. 108 // Needed for undo. 109 //setModified(true); 110 } 111 112 /** React to the fact that a change has triggered an error by 113 * reporting the error in a top-level dialog. 114 * @param change The change that was attempted. 115 * @param exception The exception that resulted. 116 */ 117 @Override 118 public void changeFailed(ChangeRequest change, Exception exception) { 119 // Do not report if it has already been reported. 120 // NOTE: This method assumes that the context of the error handler 121 // has been set so that there is an owner for the error window. 122 if (change == null) { 123 MessageHandler.error("Change failed: ", exception); 124 } else if (!change.isErrorReported()) { 125 change.setErrorReported(true); 126 MessageHandler.error( 127 "Change failed: " + StringUtilities 128 .truncateString(change.getDescription(), 80, 1), 129 exception); 130 } 131 } 132 133 /** Clone the object into the specified workspace. This calls the 134 * base class and then clones the associated model into a new 135 * workspace, if there is one. 136 * @param workspace The workspace for the new effigy. 137 * @return A new effigy. 138 * @exception CloneNotSupportedException If a derived class contains 139 * an attribute that cannot be cloned. 140 */ 141 @Override 142 public Object clone(Workspace workspace) throws CloneNotSupportedException { 143 PtolemyEffigy newObject = (PtolemyEffigy) super.clone(workspace); 144 145 if (_model != null && !(_model instanceof Configuration)) { 146 newObject._model = (NamedObj) _model.clone(new Workspace()); 147 } 148 149 return newObject; 150 } 151 152 /** Return the ptolemy model that this is an effigy of. 153 * @return The model, or null if none has been set. 154 * @see #setModel(NamedObj) 155 */ 156 public NamedObj getModel() { 157 return _model; 158 } 159 160 /** Return the effigy that is "in charge" of this effigy. 161 * In this base class, this returns the effigy associated 162 * with the top-level of the associated model. If there is 163 * no model, or it has no effigy, then delegate to the base 164 * class. 165 * @return The effigy associated with the top-level of the 166 * model. 167 */ 168 @Override 169 public Effigy masterEffigy() { 170 if (_model != null) { 171 NamedObj toplevel = _model.toplevel(); 172 173 if (toplevel == _model) { 174 return this; 175 } 176 177 Effigy effigyForToplevel = Configuration.findEffigy(toplevel); 178 179 if (effigyForToplevel != null) { 180 return effigyForToplevel; 181 } 182 } 183 184 return super.masterEffigy(); 185 } 186 187 /** Set the ptolemy model that this is an effigy of. 188 * Register with that model as a change listener. 189 * @param model The model. 190 * @see #getModel() 191 */ 192 public void setModel(NamedObj model) { 193 if (_model != null) { 194 _model.toplevel().removeChangeListener(this); 195 } 196 197 _model = model; 198 199 if (model != null) { 200 _model.toplevel().addChangeListener(this); 201 } 202 } 203 204 /** Write the model associated with this effigy 205 * to the specified file in MoML format. 206 * Change the name of the model to match the 207 * file name, up to its first period. 208 * @param file The file to write to. 209 * @exception IOException If the write fails. 210 */ 211 @Override 212 public void writeFile(File file) throws IOException { 213 java.io.FileWriter fileWriter = null; 214 215 try { 216 fileWriter = new java.io.FileWriter(file); 217 218 String name = getModel().getName(); 219 220 String filename = file.getName(); 221 int period = filename.indexOf("."); 222 223 if (period > 0) { 224 name = filename.substring(0, period); 225 } else { 226 name = filename; 227 } 228 // If the user has a & in the file name . . . 229 // See http://bugzilla.ecoinformatics.org/show_bug.cgi?id=3901 230 name = StringUtilities.escapeForXML(name); 231 232 // If the model is not at the top level, 233 // then we have to force the writer to export 234 // the DTD, because the exportMoML() method 235 // will not do it. 236 NamedObj model = getModel(); 237 if (model.getContainer() != null) { 238 fileWriter.write("<?xml version=\"1.0\" standalone=\"no\"?>\n" 239 + "<!DOCTYPE " + _elementName + " PUBLIC " 240 + "\"-//UC Berkeley//DTD MoML 1//EN\"\n" 241 + " \"http://ptolemy.eecs.berkeley.edu" 242 + "/xml/dtd/MoML_1.dtd\">\n"); 243 } 244 model.exportMoML(fileWriter, 0, name); 245 } finally { 246 if (fileWriter != null) { 247 fileWriter.close(); 248 } 249 } 250 } 251 252 /////////////////////////////////////////////////////////////////// 253 //// protected methods //// 254 255 /** Check that the specified container is of a suitable class for 256 * this entity, i.e., ModelDirectory or PtolemyEffigy. 257 * @param container The proposed container. 258 * @exception IllegalActionException If the container is not of 259 * an acceptable class. 260 */ 261 @Override 262 protected void _checkContainer(CompositeEntity container) 263 throws IllegalActionException { 264 if (container != null && !(container instanceof ModelDirectory) 265 && !(container instanceof PtolemyEffigy)) { 266 throw new IllegalActionException(this, container, 267 "The container can only be set to an " 268 + "instance of ModelDirectory or PtolemyEffigy."); 269 } 270 } 271 272 /////////////////////////////////////////////////////////////////// 273 //// private members //// 274 // The model associated with this effigy. 275 private NamedObj _model; 276 277 /////////////////////////////////////////////////////////////////// 278 //// inner classes //// 279 280 /** A factory for creating new Ptolemy effigies. 281 */ 282 public static class Factory extends EffigyFactory { 283 /** Create a factory with the given name and container. 284 * @param container The container. 285 * @param name The name. 286 * @exception IllegalActionException If the container is incompatible 287 * with this entity. 288 * @exception NameDuplicationException If the name coincides with 289 * an entity already in the container. 290 */ 291 public Factory(CompositeEntity container, String name) 292 throws IllegalActionException, NameDuplicationException { 293 super(container, name); 294 } 295 296 /////////////////////////////////////////////////////////////// 297 //// public methods //// 298 299 /** Return true, indicating that this effigy factory is 300 * capable of creating an effigy without a URL being specified. 301 * @return True. 302 */ 303 @Override 304 public boolean canCreateBlankEffigy() { 305 return true; 306 } 307 308 /** Create a new effigy in the given container by reading the 309 * <i>input</i> URL. If the <i>input</i> URL is null, then 310 * create a blank effigy. 311 * The blank effigy will have a new model associated with it. 312 * If this effigy factory contains an entity or an attribute 313 * named "blank", then 314 * the new model will be a clone of that object. Otherwise, 315 * it will be an instance of TypedCompositeActor. 316 * If the URL does not end with extension ".xml" or ".moml" 317 * (case insensitive), then return null. If the URL points 318 * to an XML file that is not 319 * a MoML file, then also return null. A MoML file is required 320 * to have the MoML DTD designation in the first five lines. 321 * That is, it must contain a line beginning with the string 322 * "<!DOCTYPE" and ending with the string 323 * 'PUBLIC \"-//UC Berkeley//DTD MoML"'. 324 * The specified base is used to expand any relative file references 325 * within the URL. 326 * If the input URL contains a "#", then the fragment after 327 * the "#" assumed to be a dot separated path to an inner model 328 * and the inner model is opened. 329 * @param container The container for the effigy. 330 * @param base The base for relative file references, or null if 331 * there are no relative file references. 332 * @param input The input URL. 333 * @return A new instance of PtolemyEffigy, or null if the URL 334 * does not specify a Ptolemy II model. 335 * @exception Exception If the URL cannot be read, or if the data 336 * is malformed in some way. 337 */ 338 @Override 339 public Effigy createEffigy(CompositeEntity container, URL base, 340 URL input) throws Exception { 341 if (input == null) { 342 // Create a blank effigy. 343 // Use the strategy pattern so derived classes can 344 // override this. 345 PtolemyEffigy effigy = _newEffigy(container, 346 container.uniqueName("effigy")); 347 348 // If this factory contains an entity called "blank", then 349 // clone that. 350 NamedObj entity = getEntity("blank"); 351 Attribute attribute = getAttribute("blank"); 352 NamedObj newModel; 353 354 if (entity != null) { 355 newModel = (NamedObj) entity.clone(new Workspace()); 356 357 // The cloning process results an object that defers change 358 // requests. By default, we do not want to defer change 359 // requests, but more importantly, we need to execute 360 // any change requests that may have been queued 361 // during cloning. The following call does that. 362 newModel.setDeferringChangeRequests(false); 363 } else if (attribute != null) { 364 newModel = (NamedObj) attribute.clone(new Workspace()); 365 366 // The cloning process results an object that defers change 367 // requests. By default, we do not want to defer change 368 // requests, but more importantly, we need to execute 369 // any change requests that may have been queued 370 // during cloning. The following call does that. 371 newModel.setDeferringChangeRequests(false); 372 } else { 373 newModel = new TypedCompositeActor(new Workspace()); 374 } 375 376 // The model should have a parser associated with it 377 // so that undo works. The following method will create 378 // a parser, if there isn't one already. 379 // We don't need the parser, so we ignore the return value. 380 ParserAttribute.getParser(newModel); 381 382 // The name might be "blank" which is confusing. 383 // Set it to an empty string. On Save As, this will 384 // be changed to match the file name. 385 newModel.setName(""); 386 effigy.setModel(newModel); 387 return effigy; 388 } else { 389 String extension = getExtension(input) 390 .toLowerCase(Locale.getDefault()); 391 392 if (!extension.equals("xml") && !extension.equals("moml")) { 393 return null; 394 } 395 396 if (!checkForDTD(input, "<!DOCTYPE", 397 ".*PUBLIC \"-//UC Berkeley//DTD MoML.*")) { 398 return null; 399 } 400 401 // Create a blank effigy. 402 PtolemyEffigy effigy = _newEffigy(container, 403 container.uniqueName("effigy")); 404 405 MoMLParser parser = new MoMLParser(); 406 407 // Make sure that the MoMLParser._modified flag is reset 408 // If we don't call reset here, then the second time 409 // the code generator is run, we will be prompted to 410 // save the model because the first time we ran 411 // the code generator the model was marked as modified. 412 parser.reset(); 413 414 NamedObj toplevel = null; 415 416 try { 417 try { 418 long startTime = 0; 419 long endTime = 0; 420 // If the following fails, we should remove the effigy. 421 try { 422 // Report on the time it takes to open the model. 423 startTime = System.currentTimeMillis(); 424 toplevel = parser.parse(base, input); 425 endTime = System.currentTimeMillis(); 426 } catch (IOException io) { 427 // If we are running under Web Start, we 428 // might have a URL that refers to another 429 // jar file. 430 URL anotherURL = ClassUtilities 431 .jarURLEntryResource(input.toString()); 432 433 if (anotherURL != null) { 434 startTime = System.currentTimeMillis(); 435 toplevel = parser.parse(base, anotherURL); 436 endTime = System.currentTimeMillis(); 437 } else { 438 throw io; 439 } 440 } 441 442 if (toplevel != null) { 443 NamedObj model = toplevel; 444 int index = -1; 445 if ((index = input.toString().indexOf("#")) != -1) { 446 String fullName = input.toString().substring( 447 index + 1, input.toString().length()); 448 if (toplevel instanceof CompositeEntity) { 449 model = ((CompositeEntity) toplevel) 450 .getEntity(fullName); 451 } 452 } 453 try { 454 String entityClassName = StringUtilities 455 .getProperty("entityClassName"); 456 if ((entityClassName.length() > 0 457 || endTime > startTime 458 + Manager.minimumStatisticsTime) 459 && model instanceof CompositeEntity) { 460 System.out.println("Opened " + input 461 + " in " 462 + (System.currentTimeMillis() 463 - startTime) 464 + " ms."); 465 466 long statisticsStartTime = System 467 .currentTimeMillis(); 468 System.out.println(((CompositeEntity) model) 469 .statistics(entityClassName)); 470 long statisticsEndTime = System 471 .currentTimeMillis(); 472 if (statisticsEndTime 473 - statisticsStartTime > Manager.minimumStatisticsTime) { 474 System.out.println( 475 "Generating statistics took" 476 + (statisticsEndTime 477 - statisticsStartTime) 478 + " ms. "); 479 } 480 } 481 } catch (SecurityException ex) { 482 System.err.println( 483 "Warning, while trying to print timing statistics," 484 + " failed to read the entityClassName" 485 + " property (-sandbox always causes this)"); 486 } 487 effigy.setModel(model); 488 489 // A MoMLFilter may have modified the model 490 // as it was being parsed. 491 effigy.setModified(MoMLParser.isModified()); 492 493 // The effigy will handle saving the modified 494 // moml for us, so MoMLParser need 495 // not care anymore. 496 MoMLParser.setModified(false); 497 498 // Identify the URI from which the model was read 499 // by inserting an attribute into both the model 500 // and the effigy. 501 URIAttribute uriAttribute = new URIAttribute( 502 toplevel, "_uri"); 503 URI inputURI = null; 504 505 try { 506 inputURI = new URI(input.toExternalForm()); 507 } catch (java.net.URISyntaxException ex) { 508 // This is annoying, if the input has a space 509 // in it, then we cannot create a URI, 510 // but we could create a URL. 511 // If, under Windows, we call 512 // File.createTempFile(), then we are likely 513 // to get a pathname that has space. 514 // FIXME: Note that jar urls will barf if there 515 // is a %20 instead of a space. This could 516 // cause problems in Web Start 517 String inputExternalFormFixed = StringUtilities 518 .substitute(input.toExternalForm(), " ", 519 "%20"); 520 521 try { 522 inputURI = new URI(inputExternalFormFixed); 523 } catch (Exception ex2) { 524 throw new Exception("Failed to generate " 525 + "a URI from '" 526 + input.toExternalForm() 527 + "' and from '" 528 + inputExternalFormFixed + "'", ex); 529 } 530 } 531 532 uriAttribute.setURI(inputURI); 533 534 // This is used by TableauFrame in its 535 //_save() method. 536 effigy.uri.setURI(inputURI); 537 538 return effigy; 539 } else { 540 effigy.setContainer(null); 541 } 542 } catch (Throwable throwable) { 543 if (throwable instanceof StackOverflowError) { 544 Throwable newThrowable = new StackOverflowError( 545 "StackOverflowError: " 546 + "Which often indicates that a class " 547 + "could not be found, but there was " 548 + "possibly a moml file with that same " 549 + "name in the directory that referred " 550 + "to the class, so we got into a loop." 551 + "For example: We had " 552 + "actor/lib/joystick/Joystick.java " 553 + "and " 554 + "actor/lib/joystick/joystick.xml, " 555 + "but " 556 + "the .class file would not load " 557 + "because of a classpath problem, " 558 + "so we kept " 559 + "loading joystick.xml which " 560 + "referred to Joystick and because " 561 + "of Windows " 562 + "filename case insensitivity, " 563 + "we found joystick.xml, which put " 564 + "us in a loop."); 565 newThrowable.initCause(throwable); 566 throwable = newThrowable; 567 } 568 569 throwable.printStackTrace(); 570 571 // The finally clause below can result in the 572 // application exiting if there are no other 573 // effigies open. We check for that condition, 574 // and report the error here. Otherwise, we 575 // pass the error to the caller. 576 ModelDirectory dir = (ModelDirectory) effigy.topEffigy() 577 .getContainer(); 578 List effigies = dir.entityList(Effigy.class); 579 580 // We might get to here if we are running a 581 // vergil with a model specified as a command 582 // line argument and the model has an invalid 583 // parameter. 584 // We might have three effigies here: 585 // 1) .configuration.directory.configuration 586 // 2) .configuration.directory.UserLibrary 587 // 3) .configuration.directory.effigy 588 // Note that one of the effigies is the configuration 589 // itself, which does not prevent exiting the app. 590 // Hence, we handle the error if there are 3 or fewer. 591 if (effigies.size() <= 3) { 592 // FIXME: This could cause problems with 593 // systems that do not load the user 594 // library. Currently, VergilApplication 595 // loads the user library, but other 596 // applications like PtolemyApplication do 597 // not. We could check to see if the 598 // names of two of the three Effigies were 599 // .configuration.directory.configuration 600 // and.configuration.directory.user 601 // library, but this seems like overkill. 602 String errorMessage = "Failed to read " + input; 603 System.err.println(errorMessage); 604 throwable.printStackTrace(); 605 MessageHandler.error(errorMessage, throwable); 606 } else { 607 if (throwable instanceof Exception) { 608 // Let the caller handle the error. 609 throw (Exception) throwable; 610 } else { 611 // If we have a parameter that has a backslash 612 // then we might get a data.expr.TokenMgrError 613 // which is an error, so we rethrow this 614 // FIXME: createEffigy() should be 615 // declared to throw Throwable, but that 616 // results in lots of changes elsewhere. 617 throw new Exception(throwable); 618 } 619 } 620 } 621 } finally { 622 // If we failed to populate the effigy with a model, 623 // then we remove the effigy from its container. 624 if (toplevel == null) { 625 effigy.setContainer(null); 626 } 627 } 628 629 return null; 630 } 631 } 632 633 /** Create a new effigy. We use the strategy pattern here 634 * so that derived classes can easily override the exact class 635 * that is created. 636 * @param container The container for the effigy. 637 * @param name The name. 638 * @return A new effigy. 639 * @exception IllegalActionException If the entity cannot be contained 640 * by the proposed container. 641 * @exception NameDuplicationException If the name coincides with 642 * an entity already in the container. 643 */ 644 protected PtolemyEffigy _newEffigy(CompositeEntity container, 645 String name) 646 throws IllegalActionException, NameDuplicationException { 647 return new PtolemyEffigy(container, name); 648 } 649 } 650 651 /** A factory for creating new Ptolemy effigies, but without the 652 * capability of creating a new blank effigy. Use this factory 653 * in a configuration if you do not want the factory to appear 654 * in the File->New menu. 655 */ 656 public static class FactoryWithoutNew extends Factory { 657 /** Create a factory with the given name and container. 658 * @param container The container. 659 * @param name The name. 660 * @exception IllegalActionException If the container is incompatible 661 * with this entity. 662 * @exception NameDuplicationException If the name coincides with 663 * an entity already in the container. 664 */ 665 public FactoryWithoutNew(CompositeEntity container, String name) 666 throws IllegalActionException, NameDuplicationException { 667 super(container, name); 668 } 669 670 /////////////////////////////////////////////////////////////// 671 //// public methods //// 672 673 /** Return false, indicating that this effigy factory is not 674 * capable of creating an effigy without a URL being specified. 675 * @return False. 676 */ 677 @Override 678 public boolean canCreateBlankEffigy() { 679 return false; 680 } 681 } 682}