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}