001/* An attribute that specifies a file or URL.
002
003 Copyright (c) 2001-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.kernel.attributes;
029
030import java.io.BufferedReader;
031import java.io.File;
032import java.io.IOException;
033import java.io.Writer;
034import java.net.MalformedURLException;
035import java.net.URI;
036import java.net.URL;
037
038import ptolemy.kernel.util.IllegalActionException;
039import ptolemy.kernel.util.NameDuplicationException;
040import ptolemy.kernel.util.NamedObj;
041import ptolemy.kernel.util.StringAttribute;
042import ptolemy.kernel.util.Workspace;
043import ptolemy.util.FileUtilities;
044import ptolemy.util.StringUtilities;
045
046///////////////////////////////////////////////////////////////////
047//// FileAttribute
048
049/**
050 This is an attribute that specifies a file or URL.  The value of this
051 attribute, accessed by getExpression(), is a string that names a file
052 or URL. If the model containing this attribute has been saved to a
053 MoML file, then the file name can be given relative to the directory
054 containing that MoML file.  If the model has not been saved to a file,
055 then the classpath is used for identifying relative file names.
056 <p>
057 Files can be given relative to a <i>base</i>, where the base is
058 the URI of the first container above this one that has a URIAttribute.
059 Normally, this URI specifies the file or URL containing the model
060 definition. Thus, files that are referred to here can be kept in the
061 same directory as the model, or in a related directory, and can
062 moved together with the model.
063 <p>
064 The following special file names are understood:
065 <ul>
066 <li> System.in: Standard input.
067 <li> System.out: Standard output.
068 </ul>
069 Note, however, that these file names cannot be converted to URLs
070 using the asURL() method.
071 <p>
072 A file name can also contain the following strings that start
073 with "$", which get substituted
074 with the appropriate values</p>
075 <table>
076  <caption>Variables that are substituted.</caption>
077  <tr>
078   <th>String</th>
079   <th>Description</th>
080   <th>Property</th>
081  </tr>
082  <tr>
083   <td><code>$CWD</code></td>
084   <td>The current working directory</td>
085   <td><code>user.dir</code></td>
086  </tr>
087  <tr>
088   <td><code>$HOME</code></td>
089   <td>The user's home directory</td>
090   <td><code>user.home</code></td>
091  </tr>
092  <tr>
093   <td><code>$PTII</code></td>
094   <td>The home directory of the Ptolemy II installation</td>
095   <td><code>ptolemy.ptII.dir</code></td>
096  </tr>
097  <tr>
098   <td><code>$TMPDIR</code></td>
099   <td>The temporary directory</td>
100   <td><code>java.io.tmpdir</code></td>
101  </tr>
102 </table>
103 The above properties are normally set when a Ptolemy II application starts.
104 <p>
105 If a file name begins with the string "$CLASSPATH", followed by either
106 "/" or "\", then when the file
107 is opened for reading, the openForReading() method
108 will search for the file relative to the classpath (using the
109 getResource() method of the current class loader).  This will only
110 work for a file that exists, and thus the openForWriting() method
111 will not understand the "$CLASSPATH" string; this makes sense
112 since the classpath typically has several directories, and it
113 would not be obvious where to create the file.  The asURL()
114 method also recognizes the "$CLASSPATH" string, but not the asFile()
115 method (which is typically used when accessing a file for writing).
116 <p>
117 @author Edward A. Lee
118 @version $Id$
119 @see URIAttribute
120 @since Ptolemy II 3.0
121 @deprecated Use {@link ptolemy.data.expr.FileParameter} instead.
122 @Pt.ProposedRating Green (eal)
123 @Pt.AcceptedRating Yellow (cxh)
124 */
125@Deprecated
126public class FileAttribute extends StringAttribute
127        implements FileOrURLAccessor {
128    /** Construct an attribute with the given name contained by the
129     *  specified container. The container argument must not be null, or a
130     *  NullPointerException will be thrown.  This attribute will use the
131     *  workspace of the container for synchronization and version counts.
132     *  If the name argument is null, then the name is set to the empty
133     *  string. Increment the version of the workspace.
134     *  @param container The container.
135     *  @param name The name of this attribute.
136     *  @exception IllegalActionException If the attribute is not of an
137     *   acceptable class for the container, or if the name contains a period.
138     *  @exception NameDuplicationException If the name coincides with
139     *   an attribute already in the container.
140     */
141    public FileAttribute(NamedObj container, String name)
142            throws IllegalActionException, NameDuplicationException {
143        super(container, name);
144    }
145
146    ///////////////////////////////////////////////////////////////////
147    ////                         public methods                    ////
148
149    /** Return the file as a File object.  This method first attempts
150     *  to directly use the file name to construct the File. If the
151     *  resulting File is not absolute, then it attempts to resolve it
152     *  relative to the base directory returned by getBaseDirectory().
153     *  If there is no such base URI, then it simply returns the
154     *  relative File object.
155     *  <p>
156     *  The file need not exist for this method to succeed.  Thus,
157     *  this method can be used to determine whether a file with a given
158     *  name exists, prior to calling openForWriting().
159     *  A typical usage looks like this:
160     *  <pre>
161     *      FileAttribute fileAttribute;
162     *      ...
163     *      File file = fileAttribute.asFile();
164     *      if (file.exists()) {
165     *         ... Ask the user if it's OK to overwrite...
166     *         ... Throw an exception if not...
167     *      }
168     *      // The following will overwrite an existing file.
169     *      Writer writer = new PrintWriter(fileAttribute.openForWriting());
170     *  </pre>
171     *  @return A File, or null if no file name has been specified.
172     *  @see #getBaseDirectory()
173     *  @exception IllegalActionException If a parse error occurs
174     *   reading the file name.
175     */
176    @Override
177    public File asFile() throws IllegalActionException {
178        String name = _substituteSpecialStrings(getExpression());
179
180        try {
181            return FileUtilities.nameToFile(name, getBaseDirectory());
182        } catch (IllegalArgumentException ex) {
183            // Java 1.4.2 some times reports:
184            //  java.lang.IllegalArgumentException: URI is not absolute
185            throw new IllegalActionException(this, ex,
186                    "Failed to create a file with name '" + name + "'.");
187        }
188    }
189
190    /** Return the file as a URL.  If the file name is relative, then
191     *  it is interpreted as being relative to the directory returned
192     *  by getBaseDirectory(). If the name begins with "$CLASSPATH",
193     *  then search for the file relative to the classpath.
194     *  If no file is found, then it throws an exception.
195     *  @return A URL, or null if no file name or URL has been specified.
196     *  @exception IllegalActionException If the file cannot be read, or
197     *   if the file cannot be represented as a URL (e.g. System.in).
198     */
199    @Override
200    public URL asURL() throws IllegalActionException {
201        String name = _substituteSpecialStrings(getExpression());
202
203        if (name == null || name.trim().equals("")) {
204            return null;
205        }
206
207        // If the name begins with "$CLASSPATH", then attempt to
208        // open the file relative to the classpath.
209        if (name.startsWith("$CLASSPATH")) {
210            // Try relative to classpath.
211            String trimmedName = name.substring(11);
212            URL result = getClass().getClassLoader().getResource(trimmedName);
213
214            if (result == null) {
215                throw new IllegalActionException(this,
216                        "Cannot find file in classpath: " + name);
217            }
218
219            return result;
220        }
221
222        File file = new File(name);
223
224        if (file.isAbsolute()) {
225            if (!file.canRead()) {
226                throw new IllegalActionException(this,
227                        "Cannot read file: " + name);
228            }
229
230            try {
231                return file.toURL();
232            } catch (MalformedURLException ex) {
233                throw new IllegalActionException(this,
234                        "Cannot open file: " + ex.toString());
235            }
236        } else {
237            // Try relative to the base directory.
238            URI modelURI = getBaseDirectory();
239
240            if (modelURI != null) {
241                try {
242                    // Try to resolve the URI.
243                    URI newURI = modelURI.resolve(name);
244                    return newURI.toURL();
245                } catch (MalformedURLException e) {
246                    throw new IllegalActionException(this,
247                            "Unable to open as a file or URL: " + name);
248                }
249            }
250
251            // As a last resort, try an absolute URL.
252            try {
253                // Try an absolute URL
254                return new URL(name);
255            } catch (MalformedURLException e) {
256                throw new IllegalActionException(this,
257                        "Unable to open as a file or URL: " + name);
258            }
259        }
260    }
261
262    /** Clone the attribute into the specified workspace.  The resulting
263     *  object has no base directory name nor any reference to any open stream.
264     *  @param workspace The workspace for the cloned object.
265     *  @return A new attribute.
266     *  @exception CloneNotSupportedException If a derived class contains
267     *   an attribute that cannot be cloned.
268     */
269    @Override
270    public Object clone(Workspace workspace) throws CloneNotSupportedException {
271        FileAttribute newObject = (FileAttribute) super.clone(workspace);
272        newObject._baseDirectory = null;
273        newObject._reader = null;
274        newObject._writer = null;
275        return newObject;
276    }
277
278    /** Close the file. If it has not been opened using openForReading()
279     *  or openForWriting(), then do nothing.  Also, if the file is
280     *  System.in or System.out, then do not close it (it does not make
281     *  sense to close these files).
282     *  @exception IllegalActionException If the file or URL cannot be
283     *   closed.
284     */
285    @Override
286    public void close() throws IllegalActionException {
287        if (_reader != null) {
288            if (_reader != FileUtilities.STD_IN) {
289                try {
290                    _reader.close();
291                } catch (IOException ex) {
292                    // This is typically caused by the stream being
293                    // already closed, so we ignore.
294                }
295            }
296        }
297
298        if (_writer != null) {
299            try {
300                _writer.flush();
301
302                if (_writer != FileUtilities.STD_OUT) {
303                    _writer.close();
304                }
305            } catch (IOException ex) {
306                // This is typically caused by the stream being
307                // already closed, so we ignore.
308            }
309        }
310    }
311
312    /** Return the directory to use as the base for relative file or URL names.
313     *  If setBaseDirectory() has been called, then that directory is
314     *  returned.  Otherwise, the directory containing the file returned
315     *  by URIAttribute.getModelURI() is returned, which is the URI
316     *  of the first container above this attribute in the hierarchy that
317     *  has a URIAttribute, or null if there none.
318     *  @return A directory name, or null if there is none.
319     *  @see #setBaseDirectory(URI)
320     *  @see URIAttribute#getModelURI(NamedObj)
321     */
322    @Override
323    public URI getBaseDirectory() {
324        if (_baseDirectory != null) {
325            return _baseDirectory;
326        } else {
327            return URIAttribute.getModelURI(this);
328        }
329    }
330
331    /** Open the file or URL for reading. If the name begins with
332     *  "$CLASSPATH", then search for the file relative to the classpath.
333     *  If the name is relative, then it is relative to the directory
334     *  returned by getBaseDirectory().
335     *  @return A buffered reader.
336     *  @see #getBaseDirectory()
337     *  @exception IllegalActionException If the file or URL cannot be
338     *   opened.
339     */
340    @Override
341    public BufferedReader openForReading() throws IllegalActionException {
342        try {
343            _reader = FileUtilities.openForReading(getExpression(),
344                    getBaseDirectory(), getClass().getClassLoader());
345            return _reader;
346        } catch (IOException ex) {
347            throw new IllegalActionException(this, ex,
348                    "Cannot open file or URL");
349        }
350    }
351
352    /** Open the file for writing.  If the file does not exist, then
353     *  create it.  If the file name is not absolute, the it is assumed
354     *  to be relative to the base directory returned by getBaseDirectory().
355     *  If permitted, this method will return a Writer that will simply
356     *  overwrite the contents of the file. It is up to the user of this
357     *  method to check whether this is OK (by first calling asFile()
358     *  and calling exists() on the returned value).
359     *  @see #getBaseDirectory()
360     *  @see #asFile()
361     *  @return A writer, or null if no file name has been specified.
362     *  @exception IllegalActionException If the file cannot be opened
363     *   or created.
364     */
365    @Override
366    public Writer openForWriting() throws IllegalActionException {
367        return openForWriting(false);
368    }
369
370    /** Open the file for writing or appending.
371     *  If the file does not exist, then
372     *  create it.  If the file name is not absolute, the it is assumed
373     *  to be relative to the base directory returned by getBaseDirectory().
374     *  If permitted, this method will return a Writer that will simply
375     *  overwrite the contents of the file. It is up to the user of this
376     *  method to check whether this is OK (by first calling asFile()
377     *  and calling exists() on the returned value).
378     *  @see #getBaseDirectory()
379     *  @see #asFile()
380     *  @param append If true, then append to the file rather than
381     *   overwriting.
382     *  @return A writer, or null if no file name has been specified.
383     *  @exception IllegalActionException If the file cannot be opened
384     *   or created.
385     */
386    @Override
387    public Writer openForWriting(boolean append) throws IllegalActionException {
388        try {
389            _writer = FileUtilities.openForWriting(getExpression(),
390                    getBaseDirectory(), append);
391            return _writer;
392        } catch (IOException ex) {
393            throw new IllegalActionException(this, ex,
394                    "Cannot open file for writing");
395        }
396    }
397
398    /** Set the directory to use as the base for relative file or URL names.
399     *  If this is not called, then the default is the directory
400     *  containing the file returned by URIAttribute.getModelURI().
401     *  @param directory The base directory.
402     *  @see #getBaseDirectory()
403     *  @see URIAttribute#getModelURI(NamedObj)
404     */
405    @Override
406    public void setBaseDirectory(URI directory) {
407        _baseDirectory = directory;
408    }
409
410    ///////////////////////////////////////////////////////////////////
411    ////                         private methods                   ////
412
413    /** Return a string that is the current value of this attribute
414     *  with the strings "$CWD, "$HOME", "$PTII" and "$TMPDIR"
415     *  replaced by their respective values.
416     *  @param string The string in which to do the substitution.
417     *  @return A new string.
418     */
419    private static String _substituteSpecialStrings(String string) {
420        String result = string;
421
422        // Keep these alphabetized.
423        if (result.indexOf("$CWD") >= 0) {
424            result = StringUtilities.substitute(result, "$CWD",
425                    StringUtilities.getProperty("user.dir"));
426        }
427
428        if (result.indexOf("$HOME") >= 0) {
429            result = StringUtilities.substitute(result, "$HOME",
430                    StringUtilities.getProperty("user.home"));
431        }
432
433        if (result.indexOf("$PTII") >= 0) {
434            result = StringUtilities.substitute(result, "$PTII",
435                    StringUtilities.getProperty("ptolemy.ptII.dir"));
436        }
437
438        if (result.indexOf("$TMPDIR") >= 0) {
439            result = StringUtilities.substitute(result, "$TMPDIR",
440                    StringUtilities.getProperty("java.io.tmpdir"));
441        }
442
443        return result;
444    }
445
446    ///////////////////////////////////////////////////////////////////
447    ////                         private members                   ////
448
449    /** The base directory to use for relative file names. */
450    private URI _baseDirectory;
451
452    /** The current reader for the input file. */
453    private BufferedReader _reader;
454
455    /** The current writer for the output file. */
456    private Writer _writer;
457}