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         *  "&lt;!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-&gt;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}