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.text.SimpleDateFormat;
031import java.util.ArrayList;
032import java.util.Date;
033import java.util.Iterator;
034import java.util.LinkedHashSet;
035import java.util.Set;
036
037import ptolemy.actor.AbstractInitializableAttribute;
038import ptolemy.actor.Actor;
039import ptolemy.actor.CompositeActor;
040import ptolemy.actor.ExecutionListener;
041import ptolemy.actor.Initializable;
042import ptolemy.actor.Manager;
043import ptolemy.actor.lib.gui.ExceptionManagerGUIFactory;
044import ptolemy.data.expr.StringParameter;
045import ptolemy.kernel.util.ExceptionHandler;
046import ptolemy.kernel.util.HierarchyListener;
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;
052import ptolemy.moml.MoMLModelAttribute;
053
054///////////////////////////////////////////////////////////////////
055//// ExceptionManager
056
057/**
058 The ExceptionManager catches exceptions and attempts to handle them with the
059 specified policy.  It notifies ExceptionSubscribers after an exception has
060 occurred and again after the handling attempt.  If the exception cannot be
061 handled, the model Manager is informed.
062
063 @author Elizabeth Latronico
064 @version $Id$
065 @since Ptolemy II 10.0
066 @Pt.ProposedRating Red (beth)
067 @Pt.AcceptedRating Red (beth)
068 */
069
070public class ExceptionManager extends MoMLModelAttribute implements
071        ExceptionHandler, ExecutionListener, Initializable, HierarchyListener {
072
073    /** Create a model attribute with the specified container and name.
074     *  @param container The specified container.
075     *  @param name The specified name.
076     *  @exception IllegalActionException If the attribute is not of an
077     *   acceptable class for the container, or if the name contains a period.
078     *  @exception NameDuplicationException If the name coincides with an
079     *   attribute already in the container.
080     */
081    public ExceptionManager(NamedObj container, String name)
082            throws NameDuplicationException, IllegalActionException {
083        super(container, name);
084
085        // Provide a default model
086        _model = new ExceptionManagerModel(this, workspace());
087
088        // TODO:  Icon should list contained commands and status of each
089        _attachText("_iconDescription", "<svg>\n"
090                + "<rect x=\"-50\" y=\"-20\" width=\"100\" height=\"40\" "
091                + "style=\"fill:blue\"/>" + "<text x=\"-40\" y=\"-5\" "
092                + "style=\"font-size:14; font-family:SansSerif; fill:white;\">"
093                + "Exception \nManager</text></svg>");
094
095        new ExceptionManagerGUIFactory(this, "_ExceptionManagerGUIFactory");
096
097        policy = new StringParameter(this, "policy");
098        policy.setExpression(CatchExceptionAttribute.THROW);
099
100        policy.addChoice(CatchExceptionAttribute.RESTART);
101        policy.addChoice(CatchExceptionAttribute.STOP);
102        policy.addChoice(CatchExceptionAttribute.THROW);
103
104        exceptionMessage = new StringParameter(this, "exceptionMessage");
105        exceptionMessage.setExpression("No exceptions encountered");
106        exceptionMessage.setVisibility(Settable.NOT_EDITABLE);
107
108        statusMessage = new StringParameter(this, "statusMessage");
109        statusMessage.setExpression("No exceptions encountered");
110        statusMessage.setVisibility(Settable.NOT_EDITABLE);
111
112        modelURL.setVisibility(Settable.NONE);
113
114        _resetMessages = true;
115        _restartDesired = false;
116        _subscribers = new ArrayList();
117    }
118
119    ///////////////////////////////////////////////////////////////////
120    ////                         public variables                  ////
121
122    /** The exception message from the caught exception. */
123    public StringParameter exceptionMessage;
124
125    /** The error handling policy to apply if an exception occurs.
126     *  One of: restart, stop, throw.  See {@link CatchExceptionAttribute}
127     */
128    public StringParameter policy;
129
130    /** The latest action, if any, taken by the CatchExceptionAttribute.
131     *  For example, a notification that the model has restarted.
132     *  It offers a way to provide feedback to the user.
133     */
134    public StringParameter statusMessage;
135
136    ///////////////////////////////////////////////////////////////////
137    ////                         public methods                    ////
138
139    /** Add the specified object to the set of objects whose
140     *  preinitialize(), initialize(), and wrapup()
141     *  methods should be invoked upon invocation of the corresponding
142     *  methods of this object.
143     *  @param initializable The object whose methods should be invoked.
144     *  @see #removeInitializable(Initializable)
145     */
146    @Override
147    public void addInitializable(Initializable initializable) {
148        if (_initializables == null) {
149            _initializables = new LinkedHashSet<Initializable>();
150        }
151        _initializables.add(initializable);
152    }
153
154    /** Clone the object into the specified workspace.
155     *  @param workspace The workspace for the new object.
156     *  @return A new NamedObj.
157     *  @exception CloneNotSupportedException If any of the attributes
158     *   cannot be cloned.
159     */
160    @Override
161    public Object clone(Workspace workspace) throws CloneNotSupportedException {
162        ExceptionManager newObject = (ExceptionManager) super.clone(workspace);
163
164        newObject._initializables = new LinkedHashSet<Initializable>();
165        newObject._initialized = false;
166        newObject._resetMessages = false;
167        newObject._restartDesired = false;
168        newObject._subscribers = new ArrayList();
169
170        return newObject;
171    }
172
173    /** Do nothing upon execution error.  Exceptions are passed to this
174     *  attribute through handleException().  This method is required by
175     *  the ExecutionListener interface.
176     */
177    @Override
178    public void executionError(Manager manager, Throwable throwable) {
179
180    }
181
182    /** Restart here if restart is desired.  This method is called upon
183     * successful completion.
184     */
185    @Override
186    public void executionFinished(Manager manager) {
187        if (_restartDesired) {
188            Date date = new Date(System.currentTimeMillis());
189            SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
190
191            // Start a new execution in a new thread
192            try {
193                manager.startRun();
194            } catch (IllegalActionException e) {
195                statusMessage.setExpression("Cannot restart model.  "
196                        + "Manager.startRun() failed.");
197            }
198
199            statusMessage.setExpression(
200                    "Model restarted at " + dateFormat.format(date));
201
202            // Do NOT reset messages in the event of a restart
203            // This way, user can see that model was restarted
204            _resetMessages = false;
205            _restartDesired = false;
206        }
207    }
208
209    /** Notify this object that the containment hierarchy above it has
210     *  changed. This method does nothing because instead we use
211     *  {@link #preinitialize()} to handle re-establishing the connections.
212     *  @exception IllegalActionException If the change is not
213     *   acceptable.
214     *   @see AbstractInitializableAttribute
215     */
216    @Override
217    public void hierarchyChanged() throws IllegalActionException {
218        // Make sure we are registered as to be initialized
219        // with the container.
220        Initializable container = _getInitializableContainer();
221        if (container != null) {
222            container.addInitializable(this);
223        }
224    }
225
226    /** Notify this object that the containment hierarchy above it will be
227     *  changed.
228     *  @exception IllegalActionException If unlinking to a published port fails.
229     *  @see AbstractInitializableAttribute
230     */
231    @Override
232    public void hierarchyWillChange() throws IllegalActionException {
233        // Unregister to be initialized with the initializable container.
234        // We will be re-registered when hierarchyChanged() is called.
235        Initializable container = _getInitializableContainer();
236        if (container != null) {
237            container.removeInitializable(this);
238        }
239    }
240
241    /** Find all of the ExceptionSubscribers in the model and save in a list.
242     *
243     *  @exception IllegalActionException If thrown by parent
244     */
245    @Override
246    public void initialize() throws IllegalActionException {
247
248        // Use allAtomicEntityList here to include entities inside composite
249        // actors. Could switch to containedObjectsIterator in the future if we
250        // want to allow attributes to be ExceptionSubscribers.  (Will need to
251        // implement a deep search.  containedObjectsIterator does not look
252        // inside composite entities).
253
254        Iterator iterator = ((CompositeActor) toplevel()).allAtomicEntityList()
255                .iterator();
256        _subscribers.clear();
257        NamedObj obj;
258
259        while (iterator.hasNext()) {
260            obj = (NamedObj) iterator.next();
261            if (obj instanceof ExceptionSubscriber) {
262                _subscribers.add((ExceptionSubscriber) obj);
263            }
264        }
265
266        // Also, check for entities inside the model contained by this attribute
267        iterator = _model.containedObjectsIterator();
268
269        while (iterator.hasNext()) {
270            obj = (NamedObj) iterator.next();
271            if (obj instanceof ExceptionSubscriber) {
272                _subscribers.add((ExceptionSubscriber) obj);
273            }
274        }
275
276        // TODO:  Figure out why setting this through the constructor is not
277        // working
278        ((ExceptionManagerModel) _model).setModelContainer(this);
279    }
280
281    /** Handle an exception according to the specified policy:
282     *
283     *  continue: Not implemented yet
284     *   Consume the exception and return control to the director.
285     *   Could be valuable for domains like DE or modal models when new
286     *   events will arrive.  Probably not appropriate for domains like SDF
287     *   where the director follows a predefined schedule based on data flow
288     *   (since the actor throwing the exception no longer provides output to
289     *   the next actor).
290     *
291     *  throw:  Do not catch the exception.
292     *
293     *  restart:  Stop and restart the model.  Does not apply to exceptions
294     *   generated during initialize().
295     *
296     *  stop:  Stop the model.
297     *
298     *  @param context The object in which the error occurred.
299     *  @param exception The exception to be handled.
300     *  @return true if the exception is handled; false if this attribute
301     *   did not handle it
302     *  @exception IllegalActionException If thrown by the parent
303     */
304
305    @Override
306    public boolean handleException(NamedObj context, Throwable exception)
307            throws IllegalActionException {
308
309        // Notify all subscribers, in the specified order, of the exception
310        // Note at this stage it is not guaranteed that the exception can be
311        // handled successfully
312
313        for (ExceptionSubscriber subscriber : _subscribers) {
314            subscriber.exceptionOccurred(policy.getValueAsString(), exception);
315        }
316
317        // Handle the exception according to the policy
318
319        // Save the exception message.  Only informational at the moment.
320        exceptionMessage.setExpression(exception.getMessage());
321
322        Date date = new Date(System.currentTimeMillis());
323        SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
324
325        // Handle the exception according to the specified policy
326
327        String policyValue = policy.stringValue();
328
329        // Set initialized to false here, unless policy is to restart, in
330        // which case set it after the current value is checked
331        if (!policyValue.equals(CatchExceptionAttribute.RESTART)) {
332            // Set _initialized here instead of in wrapup(), since
333            // wrapup() is called prior to handleException()
334            _initialized = false;
335        }
336
337        if (policyValue.equals(CatchExceptionAttribute.RESTART)) {
338            // Restarts the model in a new thread
339
340            // Check if the model made it through initialize().  If not, return
341            // false (thereby leaving exception unhandled)
342            if (!_initialized) {
343
344                // Return false if an exception is thrown, since this attribute
345                // did not resolve the exception.
346                statusMessage.setExpression("Cannot restart: Error before "
347                        + "or during intialize()");
348                for (ExceptionSubscriber subscriber : _subscribers) {
349                    subscriber.exceptionHandled(false, policyValue);
350                }
351                return false;
352            }
353
354            // Set _initialized here, instead of in wrapup(), since
355            // wrapup() is called prior to handleException()
356            _initialized = false;
357
358            // Find an actor in the model; use the actor to get the manager.
359            Manager manager = null;
360
361            NamedObj toplevel = toplevel();
362            if (toplevel != null) {
363                Iterator iterator = toplevel.containedObjectsIterator();
364                while (iterator.hasNext()) {
365                    Object obj = iterator.next();
366                    if (obj instanceof Actor) {
367                        manager = ((Actor) obj).getManager();
368                    }
369                }
370            }
371
372            if (manager != null) {
373                // End execution
374                manager.finish();
375
376                // Wait until the manager notifies listeners of successful
377                // completion before restarting.  Manager will call
378                // _executionFinished().  Set a flag here indicating to restart
379                _restartDesired = true;
380
381            } else {
382                statusMessage.setExpression("Cannot restart model since "
383                        + "there is no model Manager.  Perhaps the model has no "
384                        + "actors?");
385                for (ExceptionSubscriber subscriber : _subscribers) {
386                    subscriber.exceptionHandled(false, policyValue);
387                }
388                return false;
389            }
390
391        } else if (policyValue.equals(CatchExceptionAttribute.STOP)) {
392            statusMessage.setExpression(
393                    "Model stopped at " + dateFormat.format(date));
394
395            // Call validate() to notify listeners of these changes
396            exceptionMessage.validate();
397            statusMessage.validate();
398
399            // wrapup() is automatically called prior to handleException(),
400            // so don't need to call it again
401        } else if (policyValue.equals(CatchExceptionAttribute.THROW)) {
402            statusMessage.setExpression(
403                    "Exception thrown at " + dateFormat.format(date));
404
405            // Return false if an exception is thrown, since this attribute
406            // did not resolve the exception.
407            for (ExceptionSubscriber subscriber : _subscribers) {
408                subscriber.exceptionHandled(false, policyValue);
409            }
410            return false;
411
412        } else {
413            statusMessage.setExpression("Illegal policy encountered at: "
414                    + dateFormat.format(date));
415
416            for (ExceptionSubscriber subscriber : _subscribers) {
417                subscriber.exceptionHandled(false, policyValue);
418            }
419            // Throw an exception here instead of just returning false, since
420            // this is a problem with CatchExceptionAttribute
421            throw new IllegalActionException(this,
422                    "Illegal exception handling policy.");
423        }
424
425        // Call validate() to notify listeners of these changes
426        exceptionMessage.validate();
427        statusMessage.validate();
428
429        _resetMessages = false;
430
431        for (ExceptionSubscriber subscriber : _subscribers) {
432            subscriber.exceptionHandled(true, policyValue);
433        }
434
435        return true;
436    }
437
438    /** React to a change of state in the Manager.
439     *
440     * @param manager The model manager
441     */
442
443    @Override
444    public void managerStateChanged(Manager manager) {
445
446        if (manager.getState().equals(Manager.INITIALIZING)) {
447            // Enable restart once all objects have been initialized
448            //_initialized is set back to false at the end of _handleException()
449            if (_resetMessages) {
450                exceptionMessage.setExpression("No exceptions encountered");
451                statusMessage.setExpression("No exceptions encountered");
452
453                // Call validate() to notify listeners of these changes
454                try {
455                    exceptionMessage.validate();
456                    statusMessage.validate();
457                } catch (IllegalActionException e) {
458                    // TODO:  What to do if parameters don't validate()?
459                }
460            }
461
462            _resetMessages = true;
463            _initialized = true;
464        }
465    }
466
467    /** Remove the specified object from the list of objects whose
468     *  preinitialize(), initialize(), and wrapup()
469     *  methods should be invoked upon invocation of the corresponding
470     *  methods of this object. If the specified object is not
471     *  on the list, do nothing.
472     *  @param initializable The object whose methods should no longer be invoked.
473     *  @see #addInitializable(Initializable)
474     */
475    @Override
476    public void removeInitializable(Initializable initializable) {
477        if (_initializables != null) {
478            _initializables.remove(initializable);
479            if (_initializables.size() == 0) {
480                _initializables = null;
481            }
482        }
483    }
484
485    /** Register this attribute with the manager.  Done here instead of in the
486     *  constructor since the director is found in order to get the manager.
487     *  The constructor for this attribute might be called before the
488     *  constructor for the director.
489     *
490     *  @exception IllegalActionException If the parent class throws it
491     */
492    @Override
493    public void preinitialize() throws IllegalActionException {
494
495        // Find an actor in the model; use the actor to get the manager.
496        Manager manager = null;
497
498        NamedObj toplevel = toplevel();
499        if (toplevel != null) {
500            Iterator iterator = toplevel.containedObjectsIterator();
501            while (iterator.hasNext()) {
502                Object obj = iterator.next();
503                if (obj instanceof Actor) {
504                    manager = ((Actor) obj).getManager();
505                }
506            }
507        }
508
509        if (manager != null) {
510            manager.addExecutionListener(this);
511        } else {
512            throw new IllegalActionException(this, "Manager cannot be found. "
513                    + "Perhaps the model has no actors?");
514        }
515    }
516
517    /** Override the base class to register as an
518     *  {@link Initializable}
519     *  so that preinitialize() is invoked, and as a
520     *  {@link HierarchyListener}, so that we are notified of
521     *  changes in the hierarchy above.
522     *  @param container The proposed container.
523     *  @exception IllegalActionException If the action would result in a
524     *   recursive containment structure, or if
525     *   this entity and container are not in the same workspace.
526     *  @exception NameDuplicationException If the container already has
527     *   an entity with the name of this entity.
528     *   @see AbstractInitializableAttribute
529     */
530    @Override
531    public void setContainer(NamedObj container)
532            throws IllegalActionException, NameDuplicationException {
533        Initializable previousInitializableContainer = _getInitializableContainer();
534        NamedObj previousContainer = getContainer();
535        super.setContainer(container);
536        Initializable newInitializableContainer = _getInitializableContainer();
537        if (previousInitializableContainer != newInitializableContainer) {
538            if (previousInitializableContainer != null) {
539                previousInitializableContainer.removeInitializable(this);
540            }
541            if (newInitializableContainer != null) {
542                newInitializableContainer.addInitializable(this);
543            }
544        }
545        if (previousContainer != container) {
546            if (previousContainer != null) {
547                previousContainer.removeHierarchyListener(this);
548            }
549            if (container != null) {
550                container.addHierarchyListener(this);
551            }
552        }
553    }
554
555    /** Invoke wrapup() on registered initializables.
556     *  @exception IllegalActionException If thrown by a subclass.
557     *  @see AbstractInitializableAttribute
558     */
559    @Override
560    public void wrapup() throws IllegalActionException {
561        // Invoke initializable methods.
562        if (_initializables != null) {
563            for (Initializable initializable : _initializables) {
564                initializable.wrapup();
565            }
566        }
567    }
568
569    ///////////////////////////////////////////////////////////////////
570    ////                         protected methods                 ////
571
572    /** Return the first Initializable encountered above this
573     *  in the hierarchy that will be initialized (i.e., it is either
574     *  an atomic actor or an opaque composite actor).
575     *  @return The first Initializable above this in the hierarchy,
576     *   or null if there is none.
577     *   @see AbstractInitializableAttribute
578     */
579    protected Initializable _getInitializableContainer() {
580        NamedObj container = getContainer();
581        while (container != null) {
582            if (container instanceof Initializable) {
583                if (container instanceof CompositeActor) {
584                    if (((CompositeActor) container).isOpaque()) {
585                        return (Initializable) container;
586                    }
587                } else {
588                    return (Initializable) container;
589                }
590            }
591            container = container.getContainer();
592        }
593        return null;
594    }
595
596    // Commands
597    // - Message displayer (separate or integrate?) (probably easier to just integrate)
598    // - File logger
599    // - Emailer
600
601    ///////////////////////////////////////////////////////////////////
602    ////                         private variables                 ////
603
604    /** List of objects whose (pre)initialize() and wrapup() methods should be
605     *  slaved to these.
606     *  See {@link AbstractInitializableAttribute}
607     */
608    private transient Set<Initializable> _initializables;
609
610    /** True if the model has been initialized but not yet wrapped up;
611     *  false otherwise.  Some policies (e.g. restart) are desirable only
612     *  for run-time exceptions.
613     */
614    private boolean _initialized;
615
616    /** True if the model has been started externally (e.g. by a user);
617     * false if the model has been restarted by this attribute.
618     */
619    private boolean _resetMessages;
620
621    /** True if this attribute should invoke Manager.startRun() upon successful
622     *  completion (i.e. when executionFinished() is invoked).
623     */
624    private boolean _restartDesired;
625
626    /** A list of all ExceptionSusbcribers, to be notified when an exception is
627     *  caught by this class.
628     */
629    private ArrayList<ExceptionSubscriber> _subscribers;
630}