001/* An actor that outputs strings read from a text file or URL.
002
003 @Copyright (c) 2002-2014 The Regents of the University of California.
004 All rights reserved.
005
006 Permission is hereby granted, without written agreement and without
007 license or royalty fees, to use, copy, modify, and distribute this
008 software and its documentation for any purpose, provided that the
009 above copyright notice and the following two paragraphs appear in all
010 copies of this software.
011
012 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
013 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
014 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
015 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
016 SUCH DAMAGE.
017
018 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
019 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
020 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
021 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
022 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
023 ENHANCEMENTS, OR MODIFICATIONS.
024
025 PT_COPYRIGHT_VERSION 2
026 COPYRIGHTENDKEY
027 */
028package ptolemy.actor.lib.io;
029
030import java.io.BufferedReader;
031import java.io.IOException;
032
033import ptolemy.actor.TypedIOPort;
034import ptolemy.actor.lib.Source;
035import ptolemy.actor.parameters.FilePortParameter;
036import ptolemy.data.BooleanToken;
037import ptolemy.data.IntToken;
038import ptolemy.data.StringToken;
039import ptolemy.data.expr.FileParameter;
040import ptolemy.data.expr.Parameter;
041import ptolemy.data.expr.SingletonParameter;
042import ptolemy.data.type.BaseType;
043import ptolemy.kernel.CompositeEntity;
044import ptolemy.kernel.util.Attribute;
045import ptolemy.kernel.util.IllegalActionException;
046import ptolemy.kernel.util.NameDuplicationException;
047import ptolemy.kernel.util.Workspace;
048
049///////////////////////////////////////////////////////////////////
050//// LineReader
051
052/**
053 <p>This actor reads a file or URL, one line at a time, and outputs each line
054 as a string.  The file or URL is specified using any form acceptable
055 to FileParameter. Before an end of file is reached, the <i>endOfFile</i>
056 output produces <i>false</i>.  In the iteration where the last line
057 of the file is read and produced on the <i>output</i> port, this actor
058 produces <i>true</i> on the <i>endOfFile</i> port. In that iteration,
059 postfire() returns false.  If the actor is iterated again, after the end
060 of file, then prefire() and postfire() will both return false, <i>output</i>
061 will produce the string "EOF", and <i>endOfFile</i> will produce <i>true</i>.
062 </p>
063 <p>
064 In some domains (such as SDF), returning false in postfire()
065 causes the model to cease executing.
066 In other domains (such as DE), this causes the director to avoid
067 further firings of this actor.  So usually, the actor will not be
068 invoked again after the end of file is reached.</p>
069 <p>
070 This actor reads ahead in the file so that it can produce an output
071 <i>true</i> on <i>endOfFile</i> in the same iteration where it outputs
072 the last line.</p>
073 <p>
074 This actor can skip some lines at the beginning of the file or URL, with
075 the number specified by the <i>numberOfLinesToSkip</i> parameter. The
076 default value of this parameter is 0.</p>
077 <p>
078 If you need to reset this line reader to start again at the beginning
079 of the file, the way to do this is to call initialize() during the run
080 of the model.  This can be done, for example, using a modal model
081 with a transition where reset is enabled.</p>
082
083 @see FileParameter
084 @author  Edward A. Lee, Yuhong Xiong
085 @version $Id$
086 @since Ptolemy II 2.2
087 @Pt.ProposedRating Green (eal)
088 @Pt.AcceptedRating Yellow (cxh)
089 */
090public class LineReader extends Source {
091    /** Construct an actor with the given container and name.
092     *  @param container The container.
093     *  @param name The name of this actor.
094     *  @exception IllegalActionException If the actor cannot be contained
095     *   by the proposed container.
096     *  @exception NameDuplicationException If the container already has an
097     *   actor with this name.
098     */
099    public LineReader(CompositeEntity container, String name)
100            throws IllegalActionException, NameDuplicationException {
101        super(container, name);
102
103        output.setTypeEquals(BaseType.STRING);
104
105        endOfFile = new TypedIOPort(this, "endOfFile", false, true);
106        endOfFile.setTypeEquals(BaseType.BOOLEAN);
107
108        fileOrURL = new FilePortParameter(this, "fileOrURL");
109        // Parameter to get Vergil to label the fileOrURL port.
110        new SingletonParameter(fileOrURL.getPort(), "_showName")
111                .setToken(BooleanToken.TRUE);
112
113        numberOfLinesToSkip = new Parameter(this, "numberOfLinesToSkip");
114        numberOfLinesToSkip.setExpression("0");
115        numberOfLinesToSkip.setTypeEquals(BaseType.INT);
116
117        _attachText("_iconDescription", "<svg>\n" + "<rect x=\"-25\" y=\"-20\" "
118                + "width=\"50\" height=\"40\" " + "style=\"fill:white\"/>\n"
119                + "<polygon points=\"-15,-10 -12,-10 -8,-14 -1,-14 3,-10"
120                + " 15,-10 15,10, -15,10\" " + "style=\"fill:red\"/>\n"
121                + "</svg>\n");
122    }
123
124    ///////////////////////////////////////////////////////////////////
125    ////                     ports and parameters                  ////
126
127    /** An output port that produces <i>false</i> until the end of file
128     *  is reached, at which point it produces <i>true</i>. The type
129     *  is boolean.
130     */
131    public TypedIOPort endOfFile;
132
133    /** The file name or URL from which to read.  This is a string with
134     *  any form accepted by FileParameter.
135     *  @see FileParameter
136     */
137    public FilePortParameter fileOrURL;
138
139    /** The number of lines to skip at the beginning of the file or URL.
140     *  This parameter contains an IntToken, initially with value 0.
141     *  The value of this parameter must be non-negative.
142     */
143    public Parameter numberOfLinesToSkip;
144
145    ///////////////////////////////////////////////////////////////////
146    ////                         public methods                    ////
147
148    /** If the specified attribute is <i>fileOrURL</i> and there is an
149     *  open file being read, then close that file and open the new one;
150     *  if the attribute is <i>numberOfLinesToSkip</i> and its value is
151     *  negative, then throw an exception.  In the case of <i>fileOrURL</i>,
152     *  do nothing if the file name is the same as the previous value of
153     *  this attribute.
154     *  @param attribute The attribute that has changed.
155     *  @exception IllegalActionException If the specified attribute
156     *   is <i>fileOrURL</i> and the file cannot be opened, or the previously
157     *   opened file cannot be closed; or if the attribute is
158     *   <i>numberOfLinesToSkip</i> and its value is negative.
159     */
160    @Override
161    public void attributeChanged(Attribute attribute)
162            throws IllegalActionException {
163        if (attribute == fileOrURL) {
164            // NOTE: We do not want to close the file if the file
165            // has not in fact changed.  We check this by just comparing
166            // name, which is not perfect...
167            String newFileOrURL = ((StringToken) fileOrURL.getToken())
168                    .stringValue();
169
170            if (_previousFileOrURL != null
171                    && !newFileOrURL.equals(_previousFileOrURL)) {
172                if (_debugging) {
173                    _debug("Closing file: " + _previousFileOrURL);
174                }
175                _previousFileOrURL = newFileOrURL;
176                fileOrURL.close();
177
178                // Ignore if the fileOrUL is blank.
179                if (newFileOrURL.trim().equals("")) {
180                    _reader = null;
181                } else {
182                    if (_debugging) {
183                        _debug("Opening file: " + newFileOrURL);
184                    }
185                    _reader = fileOrURL.openForReading();
186                }
187            }
188        } else if (attribute == numberOfLinesToSkip) {
189            int linesToSkip = ((IntToken) numberOfLinesToSkip.getToken())
190                    .intValue();
191
192            if (linesToSkip < 0) {
193                throw new IllegalActionException(this,
194                        "The number of lines " + "to skip cannot be negative.");
195            }
196        } else {
197            super.attributeChanged(attribute);
198        }
199    }
200
201    /** Clone the actor into the specified workspace.
202     *  @param workspace The workspace for the new object.
203     *  @return A new actor.
204     *  @exception CloneNotSupportedException If a derived class contains
205     *   an attribute that cannot be cloned.
206     */
207    @Override
208    public Object clone(Workspace workspace) throws CloneNotSupportedException {
209        LineReader newObject = (LineReader) super.clone(workspace);
210        newObject._currentLine = null;
211        newObject._reader = null;
212        return newObject;
213    }
214
215    /** Output the data read in the preinitialize() or in the previous
216     *  invocation of postfire(), if there is any.
217     *  @exception IllegalActionException If there's no director.
218     */
219    @Override
220    public void fire() throws IllegalActionException {
221        super.fire();
222        fileOrURL.update();
223        if (_firstFiring) {
224            _openAndReadFirstTwoLines();
225            _firstFiring = false;
226        }
227        if (_currentLine != null) {
228            output.broadcast(new StringToken(_currentLine));
229        } else {
230            throw new IllegalActionException(this, "File is empty.");
231        }
232        if (_nextLine == null) {
233            endOfFile.broadcast(BooleanToken.TRUE);
234        } else {
235            endOfFile.broadcast(BooleanToken.FALSE);
236        }
237    }
238
239    /** If this is called after prefire() has been called but before
240     *  wrapup() has been called, then close any
241     *  open file re-open it, skip the number of lines given by the
242     *  <i>numberOfLinesToSkip</i> parameter, and read the first line to
243     *  be produced in the next invocation of prefire(). This occurs if
244     *  this actor is re-initialized during a run of the model.
245     *  @exception IllegalActionException If the file or URL cannot be
246     *   opened, or if the lines to be skipped and the first line to be
247     *   sent out in the fire() method cannot be read.
248     */
249    @Override
250    public void initialize() throws IllegalActionException {
251        super.initialize();
252        // In case the file has been previously opened, close
253        // it and the reopen it.
254        fileOrURL.close();
255        _reader = null;
256        _firstFiring = true;
257    }
258
259    /** Read the next line from the file.
260     *  If the current line is the last line in the file,
261     *  then return false.
262     *  @exception IllegalActionException If there is a problem reading
263     *   the file.
264     */
265    @Override
266    public boolean postfire() throws IllegalActionException {
267        boolean returnValue = super.postfire();
268        if (_reader == null) {
269            return false;
270        }
271        _currentLine = _nextLine;
272        // If the next line is null, then the current iteration
273        // is processing the last line. Request to not be refired.
274        if (_nextLine == null) {
275            returnValue = false;
276        }
277        try {
278            _nextLine = _reader.readLine();
279            if (_debugging) {
280                _debug("Read line: " + _nextLine);
281            }
282        } catch (IOException ex) {
283            throw new IllegalActionException(this, ex, "Postfire failed");
284        }
285        return returnValue;
286    }
287
288    /** Close the reader if there is one.
289     *  @exception IllegalActionException If an IO error occurs.
290     */
291    @Override
292    public void wrapup() throws IllegalActionException {
293        fileOrURL.close();
294        _reader = null;
295        _firstFiring = true;
296    }
297
298    ///////////////////////////////////////////////////////////////////
299    ////                         protected members                 ////
300
301    /** Cache of most recently read data. */
302    protected String _currentLine;
303
304    /** The next line after the current line. */
305    protected String _nextLine;
306
307    /** The current reader for the input file. */
308    protected BufferedReader _reader;
309
310    ///////////////////////////////////////////////////////////////////
311    ////                         protected methods                 ////
312
313    /** Open the file and read the first line, putting its value into
314     *  the _currentLine variable. Also, read the second line, putting
315     *  its value in the _nextLine variable.
316     *  @exception IllegalActionException If the file cannot be read.
317     */
318    protected void _openAndReadFirstTwoLines() throws IllegalActionException {
319        if (_debugging) {
320            _debug("Opening file: "
321                    + ((StringToken) fileOrURL.getToken()).stringValue());
322        }
323        _reader = fileOrURL.openForReading();
324
325        if (_reader == null) {
326            throw new IllegalActionException(this,
327                    "Failed to read file: " + fileOrURL.getDisplayName());
328        }
329
330        try {
331            // Read (numberOfLinesToSkip + 1) lines
332            int numberOfLines = ((IntToken) numberOfLinesToSkip.getToken())
333                    .intValue();
334
335            for (int i = 0; i <= numberOfLines; i++) {
336                _currentLine = _reader.readLine();
337
338                if (_debugging && i < numberOfLines) {
339                    _debug("Skipping line: " + _currentLine);
340                }
341
342                if (_currentLine == null) {
343                    throw new IllegalActionException(this,
344                            "The file '" + fileOrURL.stringValue()
345                                    + "' does not " + "have any data.");
346                }
347            }
348            _nextLine = _reader.readLine();
349
350            if (_debugging) {
351                _debug("Read line: " + _nextLine);
352            }
353        } catch (IOException ex) {
354            throw new IllegalActionException(this, ex,
355                    "Failed to read file in preinitialize().");
356        }
357    }
358
359    ///////////////////////////////////////////////////////////////////
360    ////                       protected members                   ////
361
362    /** First firing indicator. */
363    protected boolean _firstFiring;
364
365    ///////////////////////////////////////////////////////////////////
366    ////                         private members                   ////
367
368    /** Previous value of fileOrURL parameter. */
369    private String _previousFileOrURL;
370}