001/* Catch exceptions and handle them with the specified policy.
002
003 Copyright (c) 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
027 */
028package ptolemy.actor.lib;
029
030import java.io.IOException;
031import java.text.SimpleDateFormat;
032import java.util.ArrayList;
033import java.util.Date;
034import java.util.Iterator;
035
036import ptolemy.actor.AbstractInitializableAttribute;
037import ptolemy.actor.Actor;
038import ptolemy.actor.CompositeActor;
039import ptolemy.actor.ExecutionListener;
040import ptolemy.actor.Manager;
041import ptolemy.data.expr.FileParameter;
042import ptolemy.data.expr.StringParameter;
043import ptolemy.data.type.BaseType;
044import ptolemy.kernel.CompositeEntity;
045import ptolemy.kernel.util.Attribute;
046import ptolemy.kernel.util.ExceptionHandler;
047import ptolemy.kernel.util.IllegalActionException;
048import ptolemy.kernel.util.NameDuplicationException;
049import ptolemy.kernel.util.NamedObj;
050import ptolemy.kernel.util.Settable;
051import ptolemy.kernel.util.Workspace;
052
053///////////////////////////////////////////////////////////////////
054//// CatchExceptionAttribute
055
056/**
057 This attribute catches exceptions and attempts to handle them with the
058 specified policy.  If the exception cannot be handled, the attribute
059 indicates this to the Manager.  Status messages may be logged to a file.
060
061 @author Edward A. Lee, Elizabeth Latronico
062 @version $Id$
063 @since Ptolemy II 10.0
064 @Pt.ProposedRating Red (beth)
065 @Pt.AcceptedRating Red (beth)
066 */
067public class CatchExceptionAttribute extends AbstractInitializableAttribute
068        implements ExceptionHandler, ExecutionListener {
069
070    /** Create a new actor in the specified container with the specified
071     *  name.  The name must be unique within the container or an exception
072     *  is thrown. The container argument must not be null, or a
073     *  NullPointerException will be thrown.
074     *
075     *  @param container The container.
076     *  @param name The name of this actor within the container.
077     *  @exception IllegalActionException If this actor cannot be contained
078     *   by the proposed container (see the setContainer() method).
079     *  @exception NameDuplicationException If the name coincides with
080     *   an entity already in the container.
081     */
082    public CatchExceptionAttribute(CompositeEntity container, String name)
083            throws NameDuplicationException, IllegalActionException {
084        super(container, name);
085
086        policy = new StringParameter(this, "policy");
087        policy.setExpression(THROW);
088
089        policy.addChoice(CONTINUE);
090        policy.addChoice(THROW);
091        policy.addChoice(RESTART);
092        policy.addChoice(STOP);
093
094        logFileName = new FileParameter(this, "logFile");
095        logFileName.setExpression("");
096        logFileName.setTypeEquals(BaseType.STRING);
097        _writer = null;
098
099        exceptionMessage = new StringParameter(this, "exceptionMessage");
100        exceptionMessage.setExpression("No exceptions encountered");
101        exceptionMessage.setVisibility(Settable.NOT_EDITABLE);
102
103        statusMessage = new StringParameter(this, "statusMessage");
104        statusMessage.setExpression("No exceptions encountered");
105        statusMessage.setVisibility(Settable.NOT_EDITABLE);
106
107        _initialized = false;
108        _resetMessages = true;
109        _restartDesired = false;
110        _subscribers = new ArrayList();
111
112    }
113
114    ///////////////////////////////////////////////////////////////////
115    ////                          parameters                       ////
116
117    /** The exception message from the caught exception. */
118    public StringParameter exceptionMessage;
119
120    /** The file, if any, to log messages to. */
121    public FileParameter logFileName;
122
123    /** The error handling policy to apply if an exception occurs.
124     *
125     * One of:  Continue, Throw, Restart, Quit
126     */
127    public StringParameter policy;
128
129    /** The latest action, if any, taken by the CatchExceptionAttribute.
130     *  For example, a notification that the model has restarted.
131     *  It offers a way to provide feedback to the user.
132     */
133    public StringParameter statusMessage;
134
135    ///////////////////////////////////////////////////////////////////
136    ////                         public methods                    ////
137
138    /** React to a change in an attribute.  This method is called by
139     *  a contained attribute when its value changes.  In this base class,
140     *  the method does nothing.  In derived classes, this method may
141     *  throw an exception, indicating that the new attribute value
142     *  is invalid.  It is up to the caller to restore the attribute
143     *  to a valid value if an exception is thrown.
144     *  @param attribute The attribute that changed.
145     *  @exception IllegalActionException If the change is not acceptable
146     *   to this container (not thrown in this base class).
147     */
148    @Override
149    public void attributeChanged(Attribute attribute)
150            throws IllegalActionException {
151
152        // Open the log file attributeChanged so that exceptions occurring in
153        // preinitialize() or initialize() may logged
154
155        if (attribute == logFileName) {
156            if (logFileName != null) {
157                // Copied (with some edits) from FileWriter
158                String filenameValue = logFileName.getExpression();
159
160                if (filenameValue == null || filenameValue.equals("\"\"")) {
161                    // See $PTII/ptolemy/domains/sdf/kernel/test/auto/zeroRate_delay5.xml, which sets
162                    // the filename to a string that has two doublequotes. ""
163                    // _setWriter(null) will close any existing writer
164                    _setWriter(null);
165                } else if (!filenameValue.equals(_previousFilename)) {
166                    // New filename. Close the previous.
167                    _previousFilename = filenameValue;
168                    _setWriter(null);
169                    if (!filenameValue.trim().equals("")) {
170                        java.io.Writer writer = logFileName.openForWriting();
171                        _setWriter(writer);
172                    }
173                }
174            }
175        } else {
176            super.attributeChanged(attribute);
177        }
178    }
179
180    /** Clone the attribute into the specified workspace.
181     *  @param workspace The workspace for the new object.
182     *  @return A new actor.
183     *  @exception CloneNotSupportedException If a derived class contains
184     *   an attribute that cannot be cloned.
185     */
186    @Override
187    public Object clone(Workspace workspace) throws CloneNotSupportedException {
188        CatchExceptionAttribute newObject = (CatchExceptionAttribute) super.clone(
189                workspace);
190
191        newObject._subscribers = new ArrayList();
192        return newObject;
193    }
194
195    /** Do nothing upon execution error.  Exceptions are passed to this
196     *  attribute through handleException().  This method is required by
197     *  the ExecutionListener interface.
198     *  @param manager Ignored.
199     *  @param throwable Ignored.
200     */
201    @Override
202    public void executionError(Manager manager, Throwable throwable) {
203
204    }
205
206    /** Restart here if restart is desired.  This method is called upon
207     *  successful completion.
208     *  @param manager The manager that starts the run.
209     */
210    @Override
211    public void executionFinished(Manager manager) {
212        if (_restartDesired) {
213            Date date = new Date(System.currentTimeMillis());
214            SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
215
216            try {
217                // Start a new execution in a new thread
218                try {
219                    manager.startRun();
220                } catch (IllegalActionException e) {
221                    _writeMessage("Cannot restart model.  "
222                            + "Manager.startRun() failed.");
223                }
224
225                _writeMessage("Model restarted at " + dateFormat.format(date));
226            } catch (IOException e) {
227                statusMessage.setExpression("Error:  Cannot write to file.");
228            }
229            // Do NOT reset messages in the event of a restart
230            // This way, user can see that model was restarted
231            _resetMessages = false;
232            _restartDesired = false;
233        }
234    }
235
236    // TODO:  Figure out what makes sense for continue (if anything)
237
238    /** Handle an exception according to the specified policy:
239     *
240     *  continue: Not implemented yet
241     *   Consume the exception and return control to the director.
242     *   Could be valuable for domains like DE or modal models when new
243     *   events will arrive.  Probably not appropriate for domains like SDF
244     *   where the director follows a predefined schedule based on data flow
245     *   (since the actor throwing the exception no longer provides output to
246     *   the next actor).
247     *
248     *  throw:  Do not catch the exception.
249     *
250     *  restart:  Stop and restart the model.  Does not apply to exceptions
251     *   generated during initialize().
252     *
253     *  stop:  Stop the model.
254     *
255     *  @param context The object in which the error occurred.
256     *  @param exception The exception to be handled.
257     *  @return true if the exception is handled; false if this attribute
258     *   did not handle it
259     *  @exception IllegalActionException If thrown by the parent
260     */
261    @Override
262    public boolean handleException(NamedObj context, Throwable exception)
263            throws IllegalActionException {
264
265        // Try / catch for IOException from file writer
266
267        try {
268            // Save the exception message.  Only informational at the moment.
269            exceptionMessage.setExpression(exception.getMessage());
270            if (_writer != null) {
271                _writer.write("Exception: " + exception.getMessage() + "\n");
272                _writer.flush();
273            }
274
275            Date date = new Date(System.currentTimeMillis());
276            SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
277
278            // Handle the exception according to the specified policy
279            // TODO:  Apply different policies depending on the type of exception.
280            // How would the policy be specified then?
281
282            String policyValue = policy.stringValue();
283
284            // Set initialized to false here, unless policy is to restart, in
285            // which case set it after the current value is checked
286            if (!policyValue.equals(RESTART)) {
287                // Set _initialized here instead of in wrapup(), since
288                // wrapup() is called prior to handleException()
289                _initialized = false;
290            }
291
292            if (policyValue.equals(CONTINUE)) {
293                _writeMessage(
294                        "Execution continued at " + dateFormat.format(date));
295
296                // FIXME:  Is continue possible?  Looks like wrapup() is called
297                // automatically before handleException()
298            } else if (policyValue.equals(THROW)) {
299                _writeMessage("Exception thrown at " + dateFormat.format(date));
300
301                // Return false if an exception is thrown, since this attribute
302                // did not resolve the exception.
303                return false;
304
305            } else if (policyValue.equals(RESTART)) {
306                // Restarts the model in a new thread
307
308                // Check if the model made it through initialize().  If not, return
309                // false (thereby leaving exception unhandled)
310                if (!_initialized) {
311
312                    // Return false if an exception is thrown, since this attribute
313                    // did not resolve the exception.
314                    _writeMessage("Cannot restart: Error before or during "
315                            + "intialize()");
316                    return false;
317                }
318
319                // Set _initialized here, instead of in wrapup(), since
320                // wrapup() is called prior to handleException()
321                _initialized = false;
322
323                // Find an actor in the model; use the actor to get the manager.
324                Manager manager = null;
325
326                NamedObj toplevel = toplevel();
327                if (toplevel != null) {
328                    Iterator iterator = toplevel.containedObjectsIterator();
329                    while (iterator.hasNext()) {
330                        Object obj = iterator.next();
331                        if (obj instanceof Actor) {
332                            manager = ((Actor) obj).getManager();
333                        }
334                    }
335                }
336
337                if (manager != null) {
338                    // End execution
339                    manager.finish();
340
341                    // Wait until the manager notifies listeners of successful
342                    // completion before restarting.  Manager will call
343                    // _executionFinished().  Set a flag here indicating to restart
344                    _restartDesired = true;
345
346                } else {
347                    _writeMessage(
348                            "Cannot restart model since there is no model "
349                                    + "Manager.  Perhaps the model has no actors?");
350                    return false;
351                }
352
353            } else if (policyValue.equals(STOP)) {
354                _writeMessage("Model stopped at " + dateFormat.format(date));
355
356                // Call validate() to notify listeners of these changes
357                exceptionMessage.validate();
358                statusMessage.validate();
359
360                // wrapup() is automatically called prior to handleException(),
361                // so don't need to call it again
362            } else {
363                _writeMessage("Illegal policy encountered at: "
364                        + dateFormat.format(date));
365
366                // Throw an exception here instead of just returning false, since
367                // this is a problem with CatchExceptionAttribute
368                throw new IllegalActionException(this,
369                        "Illegal exception handling policy.");
370            }
371
372            // Call validate() to notify listeners of these changes
373            exceptionMessage.validate();
374            statusMessage.validate();
375
376            _resetMessages = false;
377
378            // Notify all ExceptionSubscribers
379            for (ExceptionSubscriber subscriber : _subscribers) {
380                subscriber.exceptionOccurred(policyValue, exception);
381            }
382
383            return true;
384        } catch (IOException ioe) {
385            statusMessage.setExpression("Error:  Cannot write to file.");
386            return false;
387        }
388    }
389
390    /** Find all of the ExceptionSubscribers in the model.
391     *
392     *  @exception IllegalActionException If thrown by parent
393     */
394    @Override
395    public void initialize() throws IllegalActionException {
396
397        // Use allAtomicEntityList here to include entities inside composite
398        // actors. Could switch to containedObjectsIterator in the future if we
399        // want to allow attributes to be ExceptionSubscribers.  (Will need to
400        // implement a deep search.  containedObjectsIterator does not look
401        // inside composite entities).
402        Iterator iterator = ((CompositeActor) toplevel()).allAtomicEntityList()
403                .iterator();
404        _subscribers.clear();
405        NamedObj obj;
406
407        while (iterator.hasNext()) {
408            obj = (NamedObj) iterator.next();
409            if (obj instanceof ExceptionSubscriber) {
410                _subscribers.add((ExceptionSubscriber) obj);
411            }
412        }
413    }
414
415    /** React to a change of state in the Manager.
416     *
417     * @param manager The model manager
418     */
419    @Override
420    public void managerStateChanged(Manager manager) {
421        if (manager.getState().equals(Manager.EXITING)) {
422            // Close file writer, if any
423            if (_writer != null) {
424                try {
425                    _writer.close();
426                } catch (IOException e) {
427                    // Can't really do anything about an exception here?
428                }
429            }
430        } else if (manager.getState().equals(Manager.INITIALIZING)) {
431            // Enable restart once all objects have been initialized
432            //_initialized is set back to false at the end of _handleException()
433            if (_resetMessages) {
434                exceptionMessage.setExpression("No exceptions encountered");
435                statusMessage.setExpression("No exceptions encountered");
436
437                // Call validate() to notify listeners of these changes
438                try {
439                    exceptionMessage.validate();
440                    statusMessage.validate();
441                } catch (IllegalActionException e) {
442                    try {
443                        _writeMessage("Error initializing status message.");
444                    } catch (IOException ioe) {
445                        statusMessage.setExpression("Error writing to file");
446                    }
447                }
448            }
449
450            _resetMessages = true;
451            _initialized = true;
452        }
453    }
454
455    /** Register this attribute with the manager.  Done here instead of in the
456     *  constructor since the director is found in order to get the manager.
457     *  The constructor for this attribute might be called before the
458     *  constructor for the director.
459     *
460     *  @exception IllegalActionException If the parent class throws it
461     */
462    @Override
463    public void preinitialize() throws IllegalActionException {
464        super.preinitialize();
465
466        // Find an actor in the model; use the actor to get the manager.
467        Manager manager = null;
468
469        NamedObj toplevel = toplevel();
470        if (toplevel != null) {
471            Iterator iterator = toplevel.containedObjectsIterator();
472            while (iterator.hasNext()) {
473                Object obj = iterator.next();
474                if (obj instanceof Actor) {
475                    manager = ((Actor) obj).getManager();
476                }
477            }
478        }
479
480        if (manager != null) {
481            manager.addExecutionListener(this);
482        } else {
483            throw new IllegalActionException(this, "Manager cannot be found. "
484                    + "Perhaps the model has no actors?");
485        }
486    }
487
488    ///////////////////////////////////////////////////////////////////
489    ////                         protected methods                 ////
490
491    /** Write the given message to the statusMessage parameter and to the log
492     * file, if open.
493     * @param message The message to write
494     * @exception IOException If there is a problem writing to the file
495     */
496    protected void _writeMessage(String message) throws IOException {
497        statusMessage.setExpression(message);
498        if (_writer != null) {
499            _writer.write(message + " \n");
500            _writer.flush();
501        }
502    }
503
504    ///////////////////////////////////////////////////////////////////
505    ////                         private methods                   ////
506
507    /** Set the writer.  If there was a previous writer, close it.
508     *  To set standard output, call this method with argument null.
509     *  @param writer The writer to write to.
510     *  @exception IllegalActionException If an IO error occurs.
511     */
512    private void _setWriter(java.io.Writer writer)
513            throws IllegalActionException {
514        // Copied (with some edits) from FileWriter.
515        try {
516            if (_writer != null && _writer != _stdOut) {
517                _writer.close();
518
519                // Since we have closed the writer, we also need to clear
520                // _previousFilename, so that a new writer will be opened for
521                // this filename if the model is executed again
522                _previousFilename = null;
523            }
524        } catch (IOException ex) {
525            throw new IllegalActionException(this, ex,
526                    "setWriter(" + writer + ") failed");
527        }
528
529        if (writer != null) {
530            _writer = writer;
531        } else {
532            _writer = _stdOut;
533        }
534    }
535
536    ///////////////////////////////////////////////////////////////////
537    ////                         public variables                  ////
538
539    /** String value for the "continue" policy. */
540    public static final String CONTINUE = "continue";
541
542    /** String value for the "restart" policy. */
543    public static final String RESTART = "restart";
544
545    /** String value for the "throw" policy. */
546    public static final String THROW = "throw";
547
548    /** String value for the "stop" policy. */
549    public static final String STOP = "stop";
550
551    ///////////////////////////////////////////////////////////////////
552    ////                         private variables                 ////
553
554    /** True if the model has been initialized but not yet wrapped up,
555     *  false otherwise.  Some policies (e.g. restart) are desirable only
556     *  for run-time exceptions.
557     */
558    private boolean _initialized;
559
560    /** The previously used filename, or null if none has been previously used.
561     */
562    private String _previousFilename = null;
563
564    /** True if the model has been started externally (e.g. by a user);
565     * false if the model has been restarted by this attribute.
566     */
567    private boolean _resetMessages;
568
569    /** True if this attribute should invoke Manager.startRun() upon successful
570     *  completion (i.e. when executionFinished() is invoked).
571     */
572    private boolean _restartDesired;
573
574    /** A list of all ExceptionSubscribers, to be notified when an exception is
575     *  caught by this class.
576     */
577    private ArrayList<ExceptionSubscriber> _subscribers;
578
579    /** Standard out as a writer. */
580    private static java.io.Writer _stdOut = null;
581
582    /** The writer to write to. */
583    private java.io.Writer _writer = null;
584}