001/* A file parameter that has an associated port.
002
003 Copyright (c) 2004-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.parameters;
029
030import java.io.BufferedReader;
031import java.io.File;
032import java.io.IOException;
033import java.io.Writer;
034import java.net.URI;
035import java.net.URL;
036
037import ptolemy.data.StringToken;
038import ptolemy.data.type.BaseType;
039import ptolemy.kernel.attributes.FileOrURLAccessor;
040import ptolemy.kernel.attributes.URIAttribute;
041import ptolemy.kernel.util.IllegalActionException;
042import ptolemy.kernel.util.NameDuplicationException;
043import ptolemy.kernel.util.NamedObj;
044import ptolemy.kernel.util.Workspace;
045import ptolemy.util.ClassUtilities;
046import ptolemy.util.FileUtilities;
047
048///////////////////////////////////////////////////////////////////
049//// FilePortParameter
050
051/**
052 This file parameter creates an associated port that can be used to update
053 the current value of the parameter. The value of this
054 parameter, accessed by getExpression(), is a string that names a file
055 or URL, possibly containing references to variables defined in scope
056 using the syntax $ID, ${ID} or $(ID). The value returned by getToken()
057 is name of the file with such references resolved.
058 <p>
059 If the model containing this port
060 parameter has been saved to a MoML file, then the file name can be
061 given relative to the directory containing that MoML file.
062 If the model has not been saved to a file,
063 then the classpath is used for identifying relative file names.
064 <p>
065 Files can be given relative to a <i>base</i>, where the base is
066 the URI of the first container above this one that has a URIAttribute.
067 Normally, this URI specifies the file or URL containing the model
068 definition. Thus, files that are referred to here can be kept in the
069 same directory as the model, or in a related directory, and can
070 moved together with the model.
071 <p>
072 The following special file names are understood:
073 <ul>
074 <li> System.in: Standard input.
075 <li> System.out: Standard output.
076 </ul>
077 Note, however, that these file names cannot be converted to URLs
078 using the asURL() method.
079 A file name can also contain the following strings that start
080 with "$", which get substituted
081 with the appropriate values.
082 <table>
083 <caption>Variables that get substituted.</caption>
084 <tr>
085 <th>String</th>
086 <th>Description</th>
087 <th>Property</th>
088 </tr>
089 <tr>
090 <td><code>$CWD</code></td>
091 <td>The current working directory</td>
092 <td><code>user.dir</code></td>
093 </tr>
094 <tr>
095 <td><code>$HOME</code></td>
096 <td>The user's home directory</td>
097 <td><code>user.home</code></td>
098 </tr>
099 <tr>
100 <td><code>$PTII</code></td>
101 <td>The home directory of the Ptolemy II installation</td>
102 <td><code>ptolemy.ptII.dir</code></td>
103 </tr>
104 <tr>
105 <td><code>$TMPDIR</code></td>
106 <td>The temporary directory</td>
107 <td><code>java.io.tmpdir</code></td>
108 </tr>
109 <tr>
110 <td><code>$USERNAME</code></td>
111 <td>The user's account name</td>
112 <td><code>user.name</code></td>
113 </tr>
114 </table>
115 The above properties are normally set when a Ptolemy II application starts.
116 <p>
117 If a file name begins with the reference "$CLASSPATH", then when
118 the file is opened for reading, the openForReading() method
119 will search for the file relative to the classpath (using the
120 getResource() method of the current class loader).  This will only
121 work for a file that exists, and thus the openForWriting() method
122 will not understand the "$CLASSPATH" string; this makes sense
123 since the classpath typically has several directories, and it
124 would not be obvious where to create the file.  The asURL()
125 method also recognizes the "$CLASSPATH" string, but not the asFile()
126 method (which is typically used when accessing a file for writing).
127 NOTE: If the container of this parameter also contains a variable
128 named CLASSPATH, then the value of that variable is used instead
129 of the Java classpath.
130 <p>
131 This parameter has two values,
132 which may not be equal, a <i>current value</i> and a <i>persistent value</i>.
133 The persistent value is returned by
134 getExpression() and is set by any of three different mechanisms:
135 <ul>
136 <li> calling setExpression();
137 <li> calling setToken(); and
138 <li> specifying a value as a constructor argument.
139 </ul>
140 All three of these will also set the current value, which is then
141 equal to the persistent value.
142 The current value is returned by get getToken()
143 and is set by any of three different mechanisms:
144 <ul>
145 <li> calling setCurrentValue();
146 <li> calling update() sets the current value if there is an associated
147 port, and that port has a token to consume; and
148 </ul>
149 These three techniques do not change the persistent value, so after
150 these are used, the persistent value and current value may be different.
151 <p>
152 When using this parameter in an actor, care must be exercised
153 to call update() exactly once per firing prior to calling getToken().
154 Each time update() is called, a new token will be consumed from
155 the associated port (if the port is connected and has a token).
156 If this is called multiple times in an iteration, it may result in
157 consuming tokens that were intended for subsequent iterations.
158 Thus, for example, update() should not be called in fire() and then
159 again in postfire().  Moreover, in some domains (such as DE),
160 it is essential that if a token is provided on a port, that it
161 is consumed.  In DE, the actor will be repeatedly fired until
162 the token is consumed.  Thus, it is an error to not call update()
163 once per iteration.  For an example of an actor that uses this
164 mechanism, see Ramp.
165 <p>
166 If this actor is placed in a container that does not implement
167 the TypedActor interface, then no associated port is created,
168 and it functions as an ordinary file parameter.  This is useful,
169 for example, if this is put in a library, where one would not
170 want the associated port to appear.
171
172 <p>There are a few situations where FilePortParameter might not do what
173 you expect:
174
175 <ol>
176 <li> If it is used in a transparent composite actor, then a token provided
177 to a FilePortParameter will never be read.  A transparent composite actor
178 is one without a director.
179
180 <br>Workaround: Put a director in the composite.
181
182 <li> Certain actors read parameter
183 values only during initialization.  During initialization, a
184 FilePortParameter can only have a value set via the parameter (it
185 can't have yet received a token).  So if the initial value
186 is set to the value of the FilePortParameter, then it will
187 see only the parameter value, never the value provided via the
188 port.
189
190 <br>Workaround: Use a RunCompositeActor to contain the model.
191
192 </ol>
193
194 @see ptolemy.data.expr.FileParameter
195 @see ParameterPort
196 @author Edward A. Lee
197 @version $Id$
198 @since Ptolemy II 4.1
199 @Pt.ProposedRating Yellow (eal)
200 @Pt.AcceptedRating Red (cxh)
201 */
202public class FilePortParameter extends PortParameter
203        implements FileOrURLAccessor {
204    /** Construct a parameter with the given name contained by the specified
205     *  entity. The container argument must not be null, or a
206     *  NullPointerException will be thrown.  This parameter will create
207     *  an associated port in the same container.
208     *  @param container The container.
209     *  @param name The name of the parameter.
210     *  @exception IllegalActionException If the parameter is not of an
211     *   acceptable class for the container.
212     *  @exception NameDuplicationException If the name coincides with
213     *   a parameter already in the container.
214     */
215    public FilePortParameter(NamedObj container, String name)
216            throws IllegalActionException, NameDuplicationException {
217        super(container, name);
218        setStringMode(true);
219        setTypeEquals(BaseType.STRING);
220    }
221
222    /** Construct a Parameter with the given container, name, and Token.
223     *  The token defines the initial persistent and current values.
224     *  The container argument must not be null, or a
225     *  NullPointerException will be thrown.  This parameter will use the
226     *  workspace of the container for synchronization and version counts.
227     *  If the name argument is null, then the name is set to the empty string.
228     *  The object is not added to the list of objects in the workspace
229     *  unless the container is null.
230     *  Increment the version of the workspace.
231     *  If the name argument is null, then the name is set to the empty
232     *  string.
233     *  @param container The container.
234     *  @param name The name.
235     *  @param token The Token contained by this Parameter.
236     *  @exception IllegalActionException If the parameter is not of an
237     *   acceptable class for the container.
238     *  @exception NameDuplicationException If the name coincides with
239     *   an parameter already in the container.
240     */
241    public FilePortParameter(NamedObj container, String name,
242            ptolemy.data.Token token)
243            throws IllegalActionException, NameDuplicationException {
244        super(container, name, token);
245        setStringMode(true);
246        setTypeEquals(BaseType.STRING);
247    }
248
249    ///////////////////////////////////////////////////////////////////
250    ////                         public methods                    ////
251    // NOTE: This code is duplicated from FileParameter, but without
252    // multiple inheritance, I see no way around this.
253
254    /** Return the file as a File object.  This method first attempts
255     *  to directly use the file name to construct the File. If the
256     *  resulting File is not absolute, then it attempts to resolve it
257     *  relative to the base directory returned by getBaseDirectory().
258     *  If there is no such base URI, then it simply returns the
259     *  relative File object.
260     *  <p>
261     *  The file need not exist for this method to succeed.  Thus,
262     *  this method can be used to determine whether a file with a given
263     *  name exists, prior to calling openForWriting().
264     *  A typical usage looks like this:
265     *  <pre>
266     *      FileParameter fileParameter;
267     *      ...
268     *      File file = fileParameter.asFile();
269     *      if (file.exists()) {
270     *         ... Ask the user if it's OK to overwrite...
271     *         ... Throw an exception if not...
272     *      }
273     *      // The following will overwrite an existing file.
274     *      Writer writer = new PrintWriter(fileParameter.openForWriting());
275     *  </pre>
276     *  @return A File, or null if no file name has been specified.
277     *  @see #getBaseDirectory()
278     *  @exception IllegalActionException If a parse error occurs
279     *   reading the file name.
280     */
281    @Override
282    public File asFile() throws IllegalActionException {
283        String name = stringValue();
284
285        try {
286            File file = FileUtilities.nameToFile(name, getBaseDirectory());
287            if (file != null) {
288                if (file.toString().indexOf("!/") != -1
289                        || file.toString().indexOf("!\\") != -1) {
290                    // We have a jar url, try dereferencing it.
291                    // ModelReference.xml needed this under Webstart.
292                    try {
293                        URL possibleJarURL = ClassUtilities
294                                .jarURLEntryResource(name);
295
296                        if (possibleJarURL != null) {
297                            file = new File(possibleJarURL.getFile());
298                        }
299                    } catch (Throwable throwable) {
300                        //Ignored, our attempt failed
301                    }
302                }
303            }
304            return file;
305        } catch (IllegalArgumentException ex) {
306            // Java 1.4.2 some times reports:
307            //  java.lang.IllegalArgumentException: URI is not absolute
308            throw new IllegalActionException(this, ex,
309                    "Failed to create a file with name '" + name + "'.");
310        }
311    }
312
313    /** Return the file as a URL.  If the file name is relative, then
314     *  it is interpreted as being relative to the directory returned
315     *  by getBaseDirectory(). If the name begins with "$CLASSPATH",
316     *  then search for the file relative to the classpath.
317     *  If no file is found, then it throws an exception.
318     *  @return A URL, or null if no file name or URL has been specified.
319     *  @exception IllegalActionException If the file cannot be read, or
320     *   if the file cannot be represented as a URL (e.g. System.in), or
321     *   the name specification cannot be parsed.
322     */
323    @Override
324    public URL asURL() throws IllegalActionException {
325        String name = stringValue();
326
327        try {
328            return FileUtilities.nameToURL(name, getBaseDirectory(),
329                    getClass().getClassLoader());
330        } catch (IOException ex) {
331            throw new IllegalActionException(this, ex,
332                    "Cannot read file '" + name + "'");
333        }
334    }
335
336    /** Clone the attribute into the specified workspace.  The resulting
337     *  object has no base directory name nor any reference to any open stream.
338     *  @param workspace The workspace for the cloned object.
339     *  @return A new attribute.
340     *  @exception CloneNotSupportedException If a derived class contains
341     *   an attribute that cannot be cloned.
342     */
343    @Override
344    public Object clone(Workspace workspace) throws CloneNotSupportedException {
345        FilePortParameter newObject = (FilePortParameter) super.clone(
346                workspace);
347        newObject._baseDirectory = null;
348        newObject._reader = null;
349        newObject._writer = null;
350        return newObject;
351    }
352
353    /** Close the file. If it has not been opened using openForReading()
354     *  or openForWriting(), then do nothing.  Also, if the file is
355     *  System.in or System.out, then do not close it (it does not make
356     *  sense to close these files).
357     *  @exception IllegalActionException If the file or URL cannot be
358     *   closed.
359     */
360    @Override
361    public void close() throws IllegalActionException {
362        if (_reader != null) {
363            if (_reader != FileUtilities.STD_IN) {
364                try {
365                    _reader.close();
366                } catch (IOException ex) {
367                    // This is typically caused by the stream being
368                    // already closed, so we ignore.
369                }
370            }
371        }
372
373        if (_writer != null) {
374            try {
375                _writer.flush();
376
377                if (_writer != FileUtilities.STD_OUT) {
378                    _writer.close();
379                }
380            } catch (IOException ex) {
381                // This is typically caused by the stream being
382                // already closed, so we ignore.
383            }
384        }
385    }
386
387    /** Return the directory to use as the base for relative file or URL names.
388     *  If setBaseDirectory() has been called, then that directory is
389     *  returned.  Otherwise, the directory containing the file returned
390     *  by URIAttribute.getModelURI() is returned, which is the URI
391     *  of the first container above this attribute in the hierarchy that
392     *  has a URIAttribute, or null if there none.
393     *  @return A directory name, or null if there is none.
394     *  @see URIAttribute#getModelURI(NamedObj)
395     *  @see #setBaseDirectory(URI)
396     */
397    @Override
398    public URI getBaseDirectory() {
399        if (_baseDirectory != null) {
400            return _baseDirectory;
401        } else {
402            return URIAttribute.getModelURI(this);
403        }
404    }
405
406    /** Open the file or URL for reading. If the name begins with
407     *  "$CLASSPATH", then search for the file relative to the classpath.
408     *  If the name is relative, then it is relative to the directory
409     *  returned by getBaseDirectory().
410     *  @return A buffered reader.
411     *  @see #getBaseDirectory()
412     *  @exception IllegalActionException If the file or URL cannot be
413     *   opened.
414     */
415    @Override
416    public BufferedReader openForReading() throws IllegalActionException {
417        try {
418            _reader = FileUtilities.openForReading(stringValue(),
419                    getBaseDirectory(), getClass().getClassLoader());
420            return _reader;
421        } catch (IOException ex) {
422            throw new IllegalActionException(this, ex,
423                    "Cannot open file or URL");
424        }
425    }
426
427    /** Open the file for writing.  If the file does not exist, then
428     *  create it.  If the file name is not absolute, the it is assumed
429     *  to be relative to the base directory returned by getBaseDirectory().
430     *  If permitted, this method will return a Writer that will simply
431     *  overwrite the contents of the file. It is up to the user of this
432     *  method to check whether this is OK (by first calling asFile()
433     *  and calling exists() on the returned value).
434     *  @see #getBaseDirectory()
435     *  @see #asFile()
436     *  @return A writer, or null if no file name has been specified.
437     *  @exception IllegalActionException If the file cannot be opened
438     *   or created.
439     */
440    @Override
441    public Writer openForWriting() throws IllegalActionException {
442        return openForWriting(false);
443    }
444
445    /** Open the file for writing or appending.
446     *  If the file does not exist, then
447     *  create it.  If the file name is not absolute, the it is assumed
448     *  to be relative to the base directory returned by getBaseDirectory().
449     *  If permitted, this method will return a Writer that will simply
450     *  overwrite the contents of the file. It is up to the user of this
451     *  method to check whether this is OK (by first calling asFile()
452     *  and calling exists() on the returned value).
453     *  @see #getBaseDirectory()
454     *  @see #asFile()
455     *  @param append If true, then append to the file rather than
456     *   overwriting.
457     *  @return A writer, or null if no file name has been specified.
458     *  @exception IllegalActionException If the file cannot be opened
459     *   or created.
460     */
461    @Override
462    public Writer openForWriting(boolean append) throws IllegalActionException {
463        try {
464            _writer = FileUtilities.openForWriting(stringValue(),
465                    getBaseDirectory(), append);
466            return _writer;
467        } catch (IOException ex) {
468            throw new IllegalActionException(this, ex,
469                    "Cannot open file for writing");
470        }
471    }
472
473    /** Set the directory to use as the base for relative file or URL names.
474     *  If this is not called, then the default is the directory
475     *  containing the file returned by URIAttribute.getModelURI().
476     *  @param directory The base directory.
477     *  @see URIAttribute#getModelURI(NamedObj)
478     *  @see #getBaseDirectory()
479     */
480    @Override
481    public void setBaseDirectory(URI directory) {
482        _baseDirectory = directory;
483    }
484
485    /** Return the string value of this parameter.  This is
486     *  equivalent to
487     *  <pre>
488     *     ((StringToken)this.getToken()).stringValue()
489     *  </pre>
490     *  @return The string value of this parameter.
491     *  @exception IllegalActionException If the expression cannot
492     *   be parsed or cannot be evaluated, or if the result of evaluation
493     *   violates type constraints, or if the result of evaluation is null
494     *   and there are variables that depend on this one.
495     */
496    public String stringValue() throws IllegalActionException {
497        return ((StringToken) getToken()).stringValue();
498    }
499
500    ///////////////////////////////////////////////////////////////////
501    ////                         private members                   ////
502
503    /** The base directory to use for relative file names. */
504    private URI _baseDirectory;
505
506    /** The current reader for the input file. */
507    private BufferedReader _reader;
508
509    /** The current writer for the output file. */
510    private Writer _writer;
511}