001/* Check that an assertion predicate is satisfied, and throw an exception if not.
002
003 Copyright (c) 2013-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.Writer;
031import java.util.HashMap;
032import java.util.List;
033
034import ptolemy.actor.TypedIOPort;
035import ptolemy.data.BooleanToken;
036import ptolemy.data.expr.SingletonParameter;
037import ptolemy.data.expr.StringParameter;
038import ptolemy.data.type.BaseType;
039import ptolemy.kernel.CompositeEntity;
040import ptolemy.kernel.Port;
041import ptolemy.kernel.util.IllegalActionException;
042import ptolemy.kernel.util.InternalErrorException;
043import ptolemy.kernel.util.KernelException;
044import ptolemy.kernel.util.NameDuplicationException;
045import ptolemy.kernel.util.Workspace;
046
047/** Check that an assertion predicate is satisfied, and throw an exception if not.
048 *  To use this actor, add any number of input ports.
049 *  Corresponding output ports will be automatically added.
050 *  Specify an expression that references the inputs and yields a boolean result.
051 *  When the actor fires, if the expression evaluates to false, then the actor
052 *  will throw an exception with the message given by the {@link #message} parameter.
053 *  Otherwise, it will copy the inputs to the corresponding output ports.
054 *
055 *  @author Ilge Akkaya, David Broman, Edward A. Lee
056 *  @version $Id$
057 *  @since Ptolemy II 10.0
058 *  @Pt.ProposedRating Yellow (eal)
059 *  @Pt.AcceptedRating Red (eal)
060 */
061public class Assert extends Expression {
062
063    /** Construct an instance of Assert.
064     *  @param container The container.
065     *  @param name The name of this actor.
066     *  @exception IllegalActionException If the actor cannot be contained
067     *   by the proposed container.
068     *  @exception NameDuplicationException If the container already has an
069     *   actor with this name.
070     */
071    public Assert(CompositeEntity container, String name)
072            throws NameDuplicationException, IllegalActionException {
073        super(container, name);
074
075        // Hide the output port.
076        SingletonParameter showName = new SingletonParameter(output, "_hide");
077        showName.setPersistent(false);
078        showName.setExpression("true");
079
080        // Set the type of the output port to ensure that the expression is predicate.
081        output.setTypeEquals(BaseType.BOOLEAN);
082
083        message = new StringParameter(this, "message");
084        message.setExpression("Assertion failed.");
085    }
086
087    ///////////////////////////////////////////////////////////////////
088    ////                     ports and parameters                  ////
089
090    /** The error message to display when the assertion is violated.
091     *  This is a string that defaults to "Assertion failed.".
092     */
093    public StringParameter message;
094
095    ///////////////////////////////////////////////////////////////////
096    ////                         public methods                    ////
097
098    /** Clone the actor into the specified workspace.
099     *  @param workspace The workspace for the new object.
100     *  @return A new actor.
101     *  @exception CloneNotSupportedException If a derived class contains
102     *   an attribute that cannot be cloned.
103     */
104    @Override
105    public Object clone(Workspace workspace) throws CloneNotSupportedException {
106        Assert newObject = null;
107        // NOTE: The following flag will be copied into the clone.
108        _cloning = true;
109        try {
110            newObject = (Assert) super.clone(workspace);
111            newObject._outputPortMap = new HashMap<String, TypedIOPort>();
112            // Reconstruct the output port map.
113            List<TypedIOPort> inputs = newObject.inputPortList();
114            for (TypedIOPort input : inputs) {
115                String name = input.getName();
116                String outputPortName = _OUTPUT_PORT_PREFIX + name;
117                TypedIOPort output = (TypedIOPort) newObject
118                        .getPort(outputPortName);
119                newObject._outputPortMap.put(name, output);
120            }
121        } finally {
122            _cloning = false;
123            if (newObject == null) {
124                throw new CloneNotSupportedException(
125                        "super.clone(Workspace) returned null?");
126            } else {
127                newObject._cloning = false;
128            }
129        }
130        return newObject;
131    }
132
133    /** Override the base class to check the result of the evaluation
134     *  of the expression. If the result is false, throw an exception.
135     *  Otherwise, copy the inputs to the corresponding outputs.
136     *  @exception IllegalActionException If the expression evaluates to false,
137     *   or if the superclass throws it.
138     */
139    @Override
140    public void fire() throws IllegalActionException {
141        super.fire();
142
143        if (!((BooleanToken) _result).booleanValue()) {
144            StringBuffer info = new StringBuffer();
145            info.append(message.stringValue());
146            info.append("\nAssertion: ");
147            info.append(expression.getExpression());
148            info.append("\nInput values:\n");
149            for (String name : _tokenMap.keySet()) {
150                info.append("  ");
151                info.append(name);
152                info.append(" = ");
153                info.append(_tokenMap.get(name).toString());
154                info.append("\n");
155            }
156            throw new IllegalActionException(this, info.toString());
157        }
158
159        // If we get here, assertion has passed.
160        // Copy the inputs to the outputs.
161        for (String inputName : _tokenMap.keySet()) {
162            TypedIOPort outputPort = _outputPortMap.get(inputName);
163            // NOTE: Expression does not seem to allow an input port to be a multiport.
164            // If that changes, then we need to iterate here over all input channels.
165            outputPort.send(0, _tokenMap.get(inputName));
166        }
167    }
168
169    /** Override the base class to create a specialized port.
170     *  @param name The name for the new port.
171     *  @return The new port.
172     *  @exception NameDuplicationException If the actor already has a port
173     *   with the specified name.
174     */
175    @Override
176    public Port newPort(String name) throws NameDuplicationException {
177        try {
178            return new AssertPort(this, name);
179        } catch (IllegalActionException e) {
180            throw new InternalErrorException(e);
181        }
182    }
183
184    ///////////////////////////////////////////////////////////////////
185    ////                         protected methods                 ////
186
187    /** Override the base class to create an output port corresponding
188     *  to each new input port added. The output port will have as its
189     *  display name the name of the input port. It will not be persistent
190     *  (will not be exported to the MoML file).
191     *  @param port The port to add to this entity.
192     *  @exception IllegalActionException If the port has no name.
193     *  @exception NameDuplicationException If the port name collides with a
194     *   name already in the entity.
195     */
196    @Override
197    protected void _addPort(TypedIOPort port)
198            throws IllegalActionException, NameDuplicationException {
199        super._addPort(port);
200
201        if (_creatingOutputPort || _cloning) {
202            return;
203        }
204
205        final String name = port.getName();
206        if (name.equals("output") || name.startsWith(_OUTPUT_PORT_PREFIX)) {
207            return;
208        }
209        // NOTE: Don't want to do this if this Assert is a clone
210        // under construction because later the superclass will try
211        // to clone the output port and will fail.
212        // The _cloning flag above tells us that.
213        _createOutputPort(port);
214    }
215
216    /** Override the base class to remove the corresponding
217     *  output port, if the specified port is an input port, or
218     *  the corresponding input port, if the specified port is an
219     *  output port.
220     *  @param port The port to remove from this entity.
221     *   name already in the entity.
222     */
223    @Override
224    protected void _removePort(Port port) {
225        super._removePort(port);
226        String name = port.getName();
227
228        // Remove the corresponding output port.
229        // NOTE: If a corresponding output port exists, then remove
230        // it whether this is an output port or not. The user may
231        // have accidentally added an output port.
232        String outputPortName = _OUTPUT_PORT_PREFIX + name;
233        Port outputPort = getPort(outputPortName);
234        if (outputPort != null) {
235            try {
236                outputPort.setContainer(null);
237            } catch (KernelException e) {
238                throw new InternalErrorException(e);
239            }
240        }
241        // If this port name matches the pattern of an output
242        // port, then remove the corresponding input port, if it exists.
243        if (name.startsWith(_OUTPUT_PORT_PREFIX)) {
244            // Remove the corresponding input port.
245            String inputName = name.substring(_OUTPUT_PORT_PREFIX.length());
246            Port inputPort = getPort(inputName);
247            if (inputPort != null) {
248                try {
249                    inputPort.setContainer(null);
250                } catch (KernelException e) {
251                    throw new InternalErrorException(e);
252                }
253            }
254        }
255    }
256
257    ///////////////////////////////////////////////////////////////////
258    ////                         private methods                   ////
259
260    /** Create the corresponding output port for the given input port.
261     *  @param port The input port.
262     *  @exception InternalErrorException If something goes wrong.
263     */
264    private void _createOutputPort(final TypedIOPort port) {
265        // Show the name of the input port.
266        SingletonParameter showName;
267        _creatingOutputPort = true;
268        try {
269            showName = new SingletonParameter(port, "_showName");
270            showName.setPersistent(false);
271            showName.setExpression("true");
272
273            String name = port.getName();
274
275            // If there is already a port with the correct name, use that.
276            String outputPortName = _OUTPUT_PORT_PREFIX + name;
277            TypedIOPort outputPort = (TypedIOPort) Assert.this
278                    .getPort(outputPortName);
279            if (outputPort == null) {
280                outputPort = new TypedIOPort(Assert.this, outputPortName, false,
281                        true) {
282                    // Make sure that this output port _never_ appears in MoML.
283                    // If it is allowed to appear, subtle bugs will arise, for example
284                    // when copying and pasting in actor-oriented classes.
285                    @Override
286                    public void exportMoML(Writer output, int depth,
287                            String name) {
288                    }
289                };
290                // Display name should match the input port name.
291                outputPort.setDisplayName(name);
292                showName = new SingletonParameter(outputPort, "_showName");
293                showName.setExpression("true");
294            }
295            _outputPortMap.put(name, outputPort);
296        } catch (KernelException e) {
297            throw new InternalErrorException(e);
298        } finally {
299            _creatingOutputPort = false;
300        }
301    }
302
303    ///////////////////////////////////////////////////////////////////
304    ////                         private variables                 ////
305
306    /** Flag indicating that we are being cloned.
307     *  Note that this flag will be cloned into the clone, so it needs
308     *  to be reset in the both the clone and clonee.
309     */
310    private boolean _cloning = false;
311
312    /** Flag indicating that we are adding a corresponding output port. */
313    private boolean _creatingOutputPort;
314
315    /** Map from input port name to the corresponding output port. */
316    private HashMap<String, TypedIOPort> _outputPortMap = new HashMap<String, TypedIOPort>();
317
318    /** Prefix given to output port names. */
319    private final static String _OUTPUT_PORT_PREFIX = "_correspondingOutputPort_";
320
321    ///////////////////////////////////////////////////////////////////
322    ////                         inner classes                     ////
323
324    /** Class for ports created by the user for this actor.
325     *  These should all be input ports, in theory.
326     *  This class ensures that if you change the name of the
327     *  port, then the name and displayName of the corresponding output
328     *  port are both changed.
329     */
330    public static class AssertPort extends TypedIOPort {
331        /** Construct a port for this actor.
332         *  @param container The container.
333         *  @param name The name.
334         *  @exception IllegalActionException If the port cannot be contained
335         *   by the proposed container.
336         *  @exception NameDuplicationException If the container already has a
337         *  port with this name.
338         */
339        public AssertPort(Assert container, String name)
340                throws IllegalActionException, NameDuplicationException {
341            super(container, name);
342        }
343
344        // Override setName() to also change the name of the corresponding
345        // output port.
346        @Override
347        public void setName(final String name)
348                throws IllegalActionException, NameDuplicationException {
349            final String oldName = getName();
350            super.setName(name);
351            // No need to do anything for the first name setting
352            // or if the name is not changing.
353            if (oldName != null && !oldName.equals(name)) {
354                // FIXME: The port dialog complains about this!
355                // But the operation succeeds.
356                Assert container = (Assert) getContainer();
357                TypedIOPort outputPort = container._outputPortMap.get(oldName);
358                if (outputPort != null) {
359                    outputPort.setName(_OUTPUT_PORT_PREFIX + name);
360                    outputPort.setDisplayName(name);
361                    container._outputPortMap.remove(oldName);
362                    container._outputPortMap.put(name, outputPort);
363                }
364            }
365        }
366    }
367}