001/* A named object that represents 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.MalformedURLException; 032import java.net.URI; 033import java.net.URL; 034import java.util.Iterator; 035import java.util.List; 036 037import ptolemy.data.expr.ContainmentExtender; 038import ptolemy.kernel.ComponentEntity; 039import ptolemy.kernel.CompositeEntity; 040import ptolemy.kernel.attributes.URIAttribute; 041import ptolemy.kernel.util.Attribute; 042import ptolemy.kernel.util.IllegalActionException; 043import ptolemy.kernel.util.InternalErrorException; 044import ptolemy.kernel.util.NameDuplicationException; 045import ptolemy.kernel.util.Nameable; 046import ptolemy.kernel.util.NamedObj; 047import ptolemy.kernel.util.StringAttribute; 048import ptolemy.kernel.util.Workspace; 049import ptolemy.moml.MoMLParser; 050import ptolemy.util.StringUtilities; 051 052/////////////////////////////////////////////////////////////////// 053//// Effigy 054 055/** 056 An effigy represents model metadata, and is contained by the 057 model directory or by another effigy. The effigy, for example, 058 keeps track of where the model originated (from a URI or file) 059 and whether the model has been modified since the URI or file was 060 read. In design automation, such information is often called 061 "metadata." When we began to design this class, we called it 062 ModelModel, because it was a model of a Ptolemy II model. 063 However, this name seemed awkward, so we changed it to Effigy. 064 We also considered the name Proxy for the class. We rejected that 065 name because of the common use of the word "proxy" in distributed 066 object-oriented models. 067 <p> 068 The Effigy class extends CompositeEntity, so an instance of Effigy 069 can contain entities. By convention, an effigy contains all 070 open instances of Tableau associated with the model. It also 071 contains a string attribute named "identifier" with a value that 072 uniquely identifies the model. A typical choice (which depends on 073 the configuration) is the canonical URI for a MoML file that 074 describes the model. In the case of an effigy contained by another, 075 a typical choice is the URI of the parent effigy, a pound sign "#", 076 and a name. 077 <p> 078 An effigy may contain other effigies. The master effigy 079 in such a containment hierarchy is typically associated with a 080 URI or file. 081 Contained effigies are associated with the same file, and represent 082 structured data within the top-level representation in the file. 083 The masterEffigy() method returns that master effigy. 084 The topEffigy() method in this base class returns the same 085 master effigy. However, in derived classes, a master effigy 086 may be contained by another effigy, so the top effigy is not 087 the same as the master effigy. The top effigy is directly contained 088 by the ModelDirectory in the Configuration. 089 <p> 090 NOTE: It might seem more natural for the identifier to match the name 091 of the effigy rather than recording the identifier in a string attribute. 092 But in Ptolemy II, an entity name cannot have periods in it, and a URI 093 typically does have periods in it. 094 095 <p> To determine the Effigy of a NamedObj, use 096 {@link ptolemy.actor.gui.Configuration#findEffigy(NamedObj)}. 097 098 @author Steve Neuendorffer and Edward A. Lee 099 @version $Id$ 100 @since Ptolemy II 1.0 101 @Pt.ProposedRating Green (eal) 102 @Pt.AcceptedRating Yellow (celaine) 103 @see ModelDirectory 104 @see Tableau 105 */ 106public class Effigy extends CompositeEntity { 107 /** Create a new effigy in the specified workspace with an empty string 108 * for its name. 109 * @param workspace The workspace for this effigy. 110 */ 111 public Effigy(Workspace workspace) { 112 super(workspace); 113 114 try { 115 identifier = new StringAttribute(this, "identifier"); 116 identifier.setExpression("Unnamed"); 117 uri = new URIAttribute(this, "uri"); 118 } catch (Throwable throwable) { 119 throw new InternalErrorException(this, throwable, 120 "Can't create identifier!"); 121 } 122 } 123 124 /** Construct an effigy with the given name and container. 125 * @param container The container. 126 * @param name The name of the effigy. 127 * @exception IllegalActionException If the entity cannot be contained 128 * by the proposed container. 129 * @exception NameDuplicationException If the name coincides with 130 * an entity already in the container. 131 */ 132 public Effigy(CompositeEntity container, String name) 133 throws IllegalActionException, NameDuplicationException { 134 super(container, name); 135 identifier = new StringAttribute(this, "identifier"); 136 identifier.setExpression("Unnamed"); 137 uri = new URIAttribute(this, "uri"); 138 } 139 140 /////////////////////////////////////////////////////////////////// 141 //// public parameters //// 142 143 /** The identifier for the effigy. The default value is "Unnamed". */ 144 public StringAttribute identifier; 145 146 /** The URI for the effigy. The default value is null. */ 147 public URIAttribute uri; 148 149 /////////////////////////////////////////////////////////////////// 150 //// public methods //// 151 152 /** If the argument is the <i>identifier</i> parameter, then set 153 * the title of all contained Tableaux to the value of the parameter; 154 * if the argument is the <i>uri</i> parameter, then check to see 155 * whether it is writable, and call setModifiable() appropriately. 156 * @param attribute The attribute that changed. 157 * @exception IllegalActionException If the base class throws it. 158 */ 159 @Override 160 public void attributeChanged(Attribute attribute) 161 throws IllegalActionException { 162 if (attribute == identifier) { 163 Iterator tableaux = entityList(Tableau.class).iterator(); 164 165 while (tableaux.hasNext()) { 166 Tableau tableau = (Tableau) tableaux.next(); 167 tableau.setTitle(identifier.getExpression()); 168 } 169 } else if (attribute == uri) { 170 URI uriValue = uri.getURI(); 171 172 if (uriValue == null) { 173 // A new model, with no URI, is by default modifiable. 174 _modifiableURI = true; 175 } else { 176 String protocol = uriValue.getScheme(); 177 178 if (!protocol.equals("file")) { 179 _modifiableURI = false; 180 } else { 181 // Use just the path here in case we 182 // are passed a URI that has a fragment. 183 // If we had file:/C%7C/foo.txt#bar 184 // then bar is the fragment. Unfortunately, 185 // new File(file:/C%7C/foo.txt#bar) will fail, 186 // so we add the path. 187 String path = uriValue.getPath(); 188 if (path != null) { 189 File file = new File(path); 190 191 try { 192 if (path.indexOf("%20") == -1) { 193 _modifiableURI = file.canWrite(); 194 } else { 195 // FIXME: we need a better way to check if 196 // a URL is writable. 197 198 // Sigh. If the filename has spaces in it, 199 // then the URL will have %20s. However, 200 // the file does not have %20s. 201 // See 202 // https://chess.eecs.berkeley.edu/bugzilla/show_bug.cgi?id=153 203 path = StringUtilities.substitute(path, "%20", 204 " "); 205 file = new File(path); 206 _modifiableURI = file.canWrite(); 207 } 208 } catch (java.security.AccessControlException accessControl) { 209 // If we are running in a sandbox, then canWrite() 210 // may throw an AccessControlException. 211 _modifiableURI = false; 212 } 213 } 214 } 215 } 216 } else { 217 super.attributeChanged(attribute); 218 } 219 } 220 221 /** Close all tableaux contained by this effigy, and by any effigies 222 * it contains. 223 * @return False if the user cancels on a save query, and true 224 * if all tableaux are successfully closed. 225 */ 226 public boolean closeTableaux() { 227 Iterator effigies = entityList(Effigy.class).iterator(); 228 229 while (effigies.hasNext()) { 230 Effigy effigy = (Effigy) effigies.next(); 231 232 if (!effigy.closeTableaux()) { 233 return false; 234 } 235 } 236 237 Iterator tableaux = entityList(Tableau.class).iterator(); 238 239 while (tableaux.hasNext()) { 240 Tableau tableau = (Tableau) tableaux.next(); 241 242 if (!tableau.close()) { 243 return false; 244 } 245 } 246 247 return true; 248 } 249 250 /** Find the effigy associated with the top level of the object, and if not 251 * found but the top level has a ContainmentExtender attribute, use that 252 * attribute to find the containment extender of the top level and continue 253 * the search. 254 * 255 * @param object The object. 256 * @return The effigy, or null if not found. 257 * @exception IllegalActionException If attributes cannot be retrieved, or 258 * the container that an attribute points to is invalid. 259 */ 260 public static Effigy findToplevelEffigy(NamedObj object) 261 throws IllegalActionException { 262 // FIXME: Should topEffigy call this method? 263 NamedObj toplevel; 264 do { 265 toplevel = object.toplevel(); 266 Effigy effigy = Configuration.findEffigy(toplevel); 267 if (effigy != null) { 268 return effigy; 269 } 270 ContainmentExtender extender = (ContainmentExtender) toplevel 271 .getAttribute("_containmentExtender", 272 ContainmentExtender.class); 273 object = toplevel; 274 if (extender != null) { 275 object = extender.getExtendedContainer(); 276 } 277 } while (toplevel != object); 278 return null; 279 } 280 281 /** Get a tableau factory that offers views of this effigy, or 282 * null if none has been specified. The tableau factory can be 283 * used to create visual renditions of or editors for the 284 * associated model. It can be used to find out what sorts of 285 * views are available for the model. 286 * @return A tableau factory offering multiple views. 287 * @see #setTableauFactory(TableauFactory) 288 */ 289 public TableauFactory getTableauFactory() { 290 return _factory; 291 } 292 293 /** Return a writable file for the URI given by the <i>uri</i> 294 * parameter of this effigy, if there is one, or return 295 * null if there is not. This will return null if the file does 296 * not exist, or it exists and is not writable, or the <i>uri</i> 297 * parameter has not been set. 298 * @return A writable file, or null if one cannot be created. 299 */ 300 public File getWritableFile() { 301 File result = null; 302 URI fileURI = uri.getURI(); 303 304 if (fileURI != null) { 305 String protocol = fileURI.getScheme(); 306 307 if (protocol == null || protocol.equals("file")) { 308 File tentativeResult = new File(fileURI); 309 310 if (tentativeResult.canWrite()) { 311 result = tentativeResult; 312 } 313 } 314 } 315 316 return result; 317 } 318 319 /** Return whether the model data is modifiable. This is delegated 320 * to the effigy returned by masterEffigy(). If this is the master 321 * effigy, then whether the data is modifiable depends on whether 322 * setModifiable() has been called, and if not, on whether there 323 * is a URI associated with this effigy and whether that URI is 324 * writable. 325 * @see #masterEffigy() 326 * @return False to indicate that the model is not modifiable. 327 */ 328 public boolean isModifiable() { 329 Effigy master = masterEffigy(); 330 if (!master._modifiable) { 331 return false; 332 } else { 333 return master._modifiableURI; 334 } 335 } 336 337 /** Return the data associated with the master effigy (as 338 * returned by masterEffigy()) has been modified. 339 * This method is intended to be used to 340 * keep track of whether the data in the file or URI associated 341 * with this data has been modified. The method is called by 342 * an instance of TableauFrame to determine whether it is safe 343 * to close. 344 * @see #masterEffigy() 345 * @see #setModifiable(boolean) 346 * @return True if the data has been modified. 347 */ 348 public boolean isModified() { 349 return masterEffigy()._modified; 350 } 351 352 /** Return whether this effigy is a system effigy. System effigies 353 * are not automatically removed when they have no tableaux. 354 * @return True if the model is a system effigy. 355 */ 356 public boolean isSystemEffigy() { 357 return _isSystemEffigy; 358 } 359 360 /** Return the effigy that is "in charge" of this effigy. 361 * In this base class, this is the same as calling topEffigy(). 362 * But in derived classes, particularly PtolemyEffigy, it will 363 * be different. 364 * @see #topEffigy() 365 * @return The effigy in charge of this effigy. 366 */ 367 public Effigy masterEffigy() { 368 return topEffigy(); 369 } 370 371 /** Return the total number of open tableau for this effigy 372 * effigy and all effigies it contains. 373 * @return A non-negative integer giving the number of open tableaux. 374 */ 375 public int numberOfOpenTableaux() { 376 int result = 0; 377 List tableaux = entityList(Tableau.class); 378 result += tableaux.size(); 379 380 List containedEffigies = entityList(Effigy.class); 381 Iterator effigies = containedEffigies.iterator(); 382 383 while (effigies.hasNext()) { 384 result += ((Effigy) effigies.next()).numberOfOpenTableaux(); 385 } 386 387 return result; 388 } 389 390 /** Override the base class so that tableaux contained by this object 391 * are removed before this effigy is removed from the ModelDirectory. 392 * This causes the frames associated with those tableaux to be 393 * closed. Also, if the argument is null and there is a URI 394 * associated with this model, then purge any record of the 395 * model that the MoMLParser class is keeping so that future 396 * efforts to open the model result in re-parsing. 397 * @param container The directory in which to list this effigy. 398 * @exception IllegalActionException If the proposed container is not 399 * an instance of ModelDirectory, or if the superclass throws it. 400 * @exception NameDuplicationException If the container already has 401 * an entity with the specified name. 402 */ 403 @Override 404 public void setContainer(CompositeEntity container) 405 throws IllegalActionException, NameDuplicationException { 406 if (container == null) { 407 // Remove all tableaux. 408 Iterator tableaux = entityList(Tableau.class).iterator(); 409 410 while (tableaux.hasNext()) { 411 ComponentEntity tableau = (ComponentEntity) tableaux.next(); 412 tableau.setContainer(null); 413 } 414 415 // Remove all contained effigies as well. 416 Iterator effigies = entityList(Effigy.class).iterator(); 417 418 while (effigies.hasNext()) { 419 ComponentEntity effigy = (ComponentEntity) effigies.next(); 420 effigy.setContainer(null); 421 } 422 423 if (uri != null) { 424 try { 425 URL url = uri.getURL(); 426 MoMLParser.purgeModelRecord(url); 427 } catch (MalformedURLException e) { 428 // This might occur as a result of failure 429 // to read the URL in the first place, so we 430 // have to do nothing. 431 } 432 } 433 } 434 435 super.setContainer(container); 436 } 437 438 /** If the argument is false, the specify that that the model is not 439 * modifiable, even if the URI associated with this effigy is writable. 440 * This always sets a flag in the master effigy (as returned by 441 * masterEffigy()). 442 * If the argument is true, or if this method is never called, 443 * then whether the model is modifiable is determined by whether 444 * the URI can be written to. 445 * Notice that this does not automatically result in any tableaux 446 * that are contained switching to being uneditable. But it will 447 * prevent them from writing to the URI. 448 * @see #masterEffigy() 449 * @see #isModifiable() 450 * @see #isModified() 451 * @see #setModified(boolean) 452 * @param flag False to prevent writing to the URI. 453 */ 454 public void setModifiable(boolean flag) { 455 masterEffigy()._modifiable = flag; 456 } 457 458 /** Record whether the data associated with this effigy has been 459 * modified since it was first read or last saved. If you call 460 * this with a true argument, then subsequent calls to isModified() 461 * will return true. This is used by instances of TableauFrame. 462 * This is recorded in the entity returned by topEntity(), which 463 * is the one associated with a file. 464 * This always sets a flag in the master effigy (as returned by 465 * masterEffigy()). 466 * @see #masterEffigy() 467 * @see #isModifiable() 468 * @see #isModified() 469 * @see #setModifiable(boolean) 470 * @param modified True if the data has been modified. 471 */ 472 public void setModified(boolean modified) { 473 // NOTE: To see who is setting this true, uncomment this: 474 //if (modified == true) (new Exception("Effigy.setModified()" + this)).printStackTrace(); 475 masterEffigy()._modified = modified; 476 _modified = modified; 477 } 478 479 /** Set the effigy to be a system effigy if the given flag is true. 480 * System effigies are not removed automatically if they have no 481 * tableaux. 482 * @param isSystemEffigy True if this is to be a system effigy. 483 */ 484 public void setSystemEffigy(boolean isSystemEffigy) { 485 _isSystemEffigy = isSystemEffigy; 486 } 487 488 /** Specify a tableau factory that offers multiple views of this effigy. 489 * This can be used by a contained tableau to set up a View menu. 490 * @param factory A tableau factory offering multiple views. 491 * @see #getTableauFactory() 492 */ 493 public void setTableauFactory(TableauFactory factory) { 494 _factory = factory; 495 } 496 497 /** Make all tableaux associated with this effigy and any effigies it 498 * contains visible by raising or deiconifying them. 499 * If there is no tableau contained directly by 500 * this effigy, then create one by calling createPrimaryTableau() 501 * in the configuration. 502 * @return The first tableau encountered, or a new one if there are none. 503 */ 504 public Tableau showTableaux() { 505 Iterator effigies = entityList(Effigy.class).iterator(); 506 507 while (effigies.hasNext()) { 508 Effigy effigy = (Effigy) effigies.next(); 509 effigy.showTableaux(); 510 } 511 512 Iterator tableaux = entityList(Tableau.class).iterator(); 513 Tableau result = null; 514 515 while (tableaux.hasNext()) { 516 Tableau tableau = (Tableau) tableaux.next(); 517 tableau.show(); 518 519 if (result == null) { 520 result = tableau; 521 } 522 } 523 524 if (result == null) { 525 // Create a new tableau. 526 Configuration configuration = (Configuration) toplevel(); 527 result = configuration.createPrimaryTableau(this); 528 } 529 530 return result; 531 } 532 533 /** Return the top-level effigy that (deeply) contains this one. 534 * If this effigy is contained by another effigy, then return 535 * the result of calling this method on that other effigy; 536 * otherwise, return this effigy. 537 * @return The top-level effigy that (deeply) contains this one. 538 */ 539 public Effigy topEffigy() { 540 Nameable container = getContainer(); 541 542 // FIXME: Should topEffigy Effigy.findToplevelEffigy? 543 if (container instanceof Effigy) { 544 return ((Effigy) container).topEffigy(); 545 } else { 546 return this; 547 } 548 } 549 550 /** Write the model associated with this effigy 551 * to the specified file. This base class throws 552 * an exception, since it does not know how to write model data. 553 * Derived classes should override this method to write model 554 * data. 555 * @param file The file to write to. 556 * @exception IOException If the write fails. 557 */ 558 public void writeFile(File file) throws IOException { 559 throw new IOException("I do not know how to write this model data."); 560 } 561 562 /////////////////////////////////////////////////////////////////// 563 //// protected methods //// 564 565 /** Check that the specified container is of a suitable class for 566 * this entity, i.e., ModelDirectory or Effigy. 567 * @param container The proposed container. 568 * @exception IllegalActionException If the container is not of 569 * an acceptable class. 570 */ 571 protected void _checkContainer(CompositeEntity container) 572 throws IllegalActionException { 573 if (container != null && !(container instanceof ModelDirectory) 574 && !(container instanceof Effigy)) { 575 throw new IllegalActionException(this, container, 576 "The container can only be set to an " 577 + "instance of ModelDirectory or Effigy."); 578 } 579 } 580 581 /** Remove the specified entity from this container. If this effigy 582 * is a system effigy and there are no remaining tableaux 583 * contained by this effigy or any effigy it contains, then remove 584 * this object from its container. 585 * @param entity The tableau to remove. 586 */ 587 @Override 588 protected void _removeEntity(ComponentEntity entity) { 589 super._removeEntity(entity); 590 591 if (numberOfOpenTableaux() == 0 && !isSystemEffigy()) { 592 try { 593 setContainer(null); 594 } catch (Exception ex) { 595 throw new InternalErrorException(this, ex, 596 "Cannot remove effigy!"); 597 } 598 } 599 } 600 601 /////////////////////////////////////////////////////////////////// 602 //// private members //// 603 // A tableau factory offering multiple views. 604 private TableauFactory _factory = null; 605 606 // Indicator that the effigy is a system effigy. 607 private boolean _isSystemEffigy = false; 608 609 /** Indicator that the data represented in the window has been modified. */ 610 private boolean _modified = false; 611 612 /** Indicator that the URI must not be written to (if false). */ 613 private boolean _modifiable = true; 614 615 /** Indicator that the URI can be written to. */ 616 private boolean _modifiableURI = true; 617}