001/* A scope extending attribute that reads multiple values from a file.
002
003 Copyright (c) 2006-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 @ProposedRating Red (liuxj)
028 @AcceptedRating Red (liuxj)
029
030 */
031package ptolemy.actor.parameters;
032
033import java.io.IOException;
034import java.io.InputStream;
035import java.net.URL;
036import java.net.URLConnection;
037import java.util.Iterator;
038import java.util.LinkedList;
039import java.util.List;
040import java.util.Map;
041import java.util.Properties;
042
043import ptolemy.actor.CompositeActor;
044import ptolemy.actor.Executable;
045import ptolemy.actor.Initializable;
046import ptolemy.data.BooleanToken;
047import ptolemy.data.expr.FileParameter;
048import ptolemy.data.expr.Parameter;
049import ptolemy.data.expr.ScopeExtendingAttribute;
050import ptolemy.data.expr.StringParameter;
051import ptolemy.data.expr.Variable;
052import ptolemy.data.type.BaseType;
053import ptolemy.kernel.util.Attribute;
054import ptolemy.kernel.util.IllegalActionException;
055import ptolemy.kernel.util.NameDuplicationException;
056import ptolemy.kernel.util.NamedObj;
057import ptolemy.kernel.util.Settable;
058
059///////////////////////////////////////////////////////////////////
060//// ParameterSet
061
062/**
063 An attribute that reads multiple values from a file and sets
064 corresponding parameters in the container.
065 The values are in the form:
066 <pre>
067 <i>attributeName</i> = <i>value</i>
068 </pre>
069 where <code><i>variableName</i></code> is the name of the attribute
070 in a format suitable for {@link ptolemy.kernel.util.NamedObj#setName(String)}
071 (i.e., does not contain periods) and  <code><i>value</i></code> is
072 the expression in the Ptolemy expression language.
073 Comments are lines that begin with the <code>#</code> character.
074 Each line in the file is interpreted as a separate assignment.
075
076 <p>The attributes that are created will have the same
077 visibility as parameters of the container of the attribute.
078 They are shadowed, however, by parameters of the container.
079 That is, if the container has a parameter with the same name
080 as one in the parameter set, the one in the container provides
081 the value to any observer.
082
083 <p>If the file is modified during execution of a model, by default
084 this will not be noticed until the next run. If you set the
085 <i>checkForFileUpdates</i> parameter to <i>true</i>, then
086 on each prefiring of the enclosing opaque composite actor,
087 this parameter will check for updates of the file. Otherwise,
088 it will only check between runs of the model or when the file
089 name or URL gets changed.
090
091 <p>Note that the order the parameters are created is arbitrary,
092 this is because we read the file in using java.util.Properties.load(),
093 which uses a HashMap to store the properties.  We use a Properties.load()
094 because it provides a nice parser for the files and can read and write
095 values in both text and XML.
096
097 @author Christopher Brooks, contributor: Edward A. Lee
098 @version $Id$
099 @since Ptolemy II 5.2
100 @see ptolemy.data.expr.Variable
101 */
102public class ParameterSet extends ScopeExtendingAttribute
103        implements Executable {
104    /** Construct an attribute with the given name contained by the specified
105     *  entity. The container argument must not be null, or a
106     *  NullPointerException will be thrown.  This attribute will use the
107     *  workspace of the container for synchronization and version counts.
108     *  If the name argument is null, then the name is set to the empty string.
109     *  Increment the version of the workspace.
110     *  @param container The container.
111     *  @param name The name of this attribute.
112     *  @exception IllegalActionException If the attribute is not of an
113     *   acceptable class for the container, or if the name contains a period.
114     *  @exception NameDuplicationException If the name coincides with
115     *   an attribute already in the container.
116     */
117    public ParameterSet(NamedObj container, String name)
118            throws IllegalActionException, NameDuplicationException {
119        super(container, name);
120        fileOrURL = new FileParameter(this, "fileOrURL");
121        fileOrURL.setExpression("");
122
123        checkForFileUpdates = new Parameter(this, "checkForFileUpdates");
124        checkForFileUpdates.setExpression("false");
125        checkForFileUpdates.setTypeEquals(BaseType.BOOLEAN);
126
127        StringParameter initialDefaultContents = new StringParameter(this,
128                "initialDefaultContents");
129        initialDefaultContents.setExpression(
130                "# This file defines parameters in the current container.\n# Each non-comment line in the file is interpreted as a separate assignment.\n# The lines are of the form:\n# attributeName = value\n# where variableName is the name of the attribute\n# in a format suitable for ptolemy.kernel.util.NamedObj.setName()\n# (i.e., does not contain periods) and value is\n# the expression in the Ptolemy expression language.\n# Comments are lines that begin with the # character.\n# FIXME: After saving, you need to update the fileOrURLParameter by hand.\n# Sample line (remove the leading #):\n# foo = \"bar\"\n");
131        initialDefaultContents.setPersistent(false);
132        initialDefaultContents.setVisibility(Settable.EXPERT);
133    }
134
135    ///////////////////////////////////////////////////////////////////
136    ////                         parameters                        ////
137
138    /** If this parameter is set to true, then the specified file or
139     *  URL will be checked for updates on every prefiring of the
140     *  enclosing opaque composite actor. Otherwise, it will check
141     *  for updates only between runs. This is a boolean that
142     *  defaults to false.
143     */
144    public Parameter checkForFileUpdates;
145
146    /** A parameter naming the file or URL to be read that contains
147     *  attribute names and values.  The file should be in a format
148     *  suitable for java.util.Properties.load(), see the class
149     *  comment of this class for details.
150     *  This initial default value is the empty string "",
151     *  which means that no file will be read and no parameter
152     *  values will be defined.
153     */
154    public FileParameter fileOrURL;
155
156    ///////////////////////////////////////////////////////////////////
157    ////                         public methods                    ////
158
159    /** Add the specified object to the list of objects whose
160     *  preinitialize(), initialize(), and wrapup()
161     *  methods should be invoked upon invocation of the corresponding
162     *  methods of this object.
163     *  @param initializable The object whose methods should be invoked.
164     *  @see #removeInitializable(Initializable)
165     *  @see ptolemy.actor.CompositeActor#addPiggyback(Executable)
166     */
167    @Override
168    public void addInitializable(Initializable initializable) {
169        if (_initializables == null) {
170            _initializables = new LinkedList<Initializable>();
171        }
172        _initializables.add(initializable);
173    }
174
175    /** If the parameter is <i>fileOrURL</i>, and the specified file
176     *  name is not null, then open and read the file.
177     *  @param attribute The attribute that changed.
178     *  @exception IllegalActionException If the superclass throws it, or
179     *   if the file cannot be read, or if the file parameters cannot
180     *   be evaluated.
181     */
182    @Override
183    public void attributeChanged(Attribute attribute)
184            throws IllegalActionException {
185        if (attribute == fileOrURL) {
186            // Do not read the file if the name is the same as
187            // what was previously read. EAL 9/8/06
188            if (!fileOrURL.getExpression().equals(_fileName)) {
189                try {
190                    read();
191                    validate();
192                } catch (Throwable throwable) {
193                    throw new IllegalActionException(this, throwable,
194                            "Failed to read file: "
195                                    + fileOrURL.getExpression());
196                }
197            }
198        } else {
199            super.attributeChanged(attribute);
200        }
201    }
202
203    /** Expand the scope of the container by creating any required attributes.
204     *  This method reads the specified file if it has not already been read
205     *  or if has changed since it was last read.
206     *  @exception IllegalActionException If any required attribute cannot be
207     *   created.
208     */
209    @Override
210    public void expand() throws IllegalActionException {
211        _reReadIfNeeded();
212        // Do not call validate.
213    }
214
215    /** Do nothing.
216     */
217    @Override
218    public void fire() throws IllegalActionException {
219    }
220
221    /** Do nothing except invoke the initialize methods
222     *  of objects that have been added using addInitializable().
223     *  @exception IllegalActionException If one of the added objects
224     *   throws it.
225     */
226    @Override
227    public void initialize() throws IllegalActionException {
228        // Invoke initializable methods.
229        if (_initializables != null) {
230            for (Initializable initializable : _initializables) {
231                initializable.initialize();
232            }
233        }
234    }
235
236    /** Return true.
237     *  @return True.
238     */
239    @Override
240    public boolean isFireFunctional() {
241        return true;
242    }
243
244    /** Return false.
245     *  @return False.
246     */
247    @Override
248    public boolean isStrict() {
249        return false;
250    }
251
252    /** Check to see whether the specified file has changed, and if so,
253     *  re-read it.
254     *  @param count The number of iterations to perform, ignored by this
255     *  method.
256     *  @exception IllegalActionException If re-reading the file fails.
257     *  @return Executable.COMPLETED.
258     */
259    @Override
260    public int iterate(int count) throws IllegalActionException {
261        if (((BooleanToken) checkForFileUpdates.getToken()).booleanValue()) {
262            if (_reReadIfNeeded()) {
263                validate();
264            }
265        }
266        return Executable.COMPLETED;
267    }
268
269    /** Do nothing.
270     *  @return True.
271     */
272    @Override
273    public boolean postfire() {
274        return true;
275    }
276
277    /** Check to see whether the specified file has changed, and if so,
278     *  re-read it.
279     *  @return True.
280     *  @exception IllegalActionException If re-reading the file fails.
281     */
282    @Override
283    public boolean prefire() throws IllegalActionException {
284        if (((BooleanToken) checkForFileUpdates.getToken()).booleanValue()) {
285            if (_reReadIfNeeded()) {
286                validate();
287            }
288        }
289        return true;
290    }
291
292    /** Check to see whether the specified file has changed, and if so,
293     *  re-read it, and invoke the preinitialize() methods
294     *  of objects that have been added using addInitializable().
295     *  @exception IllegalActionException If one of the added objects
296     *   throws it, or if re-reading the file fails.
297     */
298    @Override
299    public void preinitialize() throws IllegalActionException {
300        // Invoke initializable methods.
301        if (_initializables != null) {
302            for (Initializable initializable : _initializables) {
303                initializable.preinitialize();
304            }
305        }
306        if (_reReadIfNeeded()) {
307            validate();
308        }
309    }
310
311    /** Read the contents of the file named by this parameter and create
312     *  attributes in the current scope.
313     *  @exception IOException If there is a problem reading the file.
314     *  @exception IllegalActionException If there is a problem
315     *  reading the previous attribute or  validating the settables
316     *  @exception NameDuplicationException If there is a problem removing
317     *  a previous attribute or creating a new variable.
318     */
319    public void read() throws IllegalActionException, NameDuplicationException,
320            IOException {
321
322        _fileName = fileOrURL.getExpression();
323
324        if (_fileName == null || _fileName.trim().equals("")) {
325            // Delete all previously defined attributes.
326            if (_properties != null) {
327                Iterator attributeNames = _properties.keySet().iterator();
328                while (attributeNames.hasNext()) {
329                    String attributeName = (String) attributeNames.next();
330                    getAttribute(attributeName).setContainer(null);
331                }
332                _properties = null;
333            }
334            return;
335        }
336
337        URL url = fileOrURL.asURL();
338
339        if (url == null) {
340            throw new IOException("Could not convert \""
341                    + fileOrURL.getExpression() + "\" with base \""
342                    + fileOrURL.getBaseDirectory() + "\" to a URL.");
343        }
344        // NOTE: Properties are unordered, which is not
345        // strictly right in Ptolemy II semantics.  However,
346        // we wait until all are loaded before validating them,
347        // so it should be OK.
348        Properties properties = new Properties();
349        InputStream inputStream = null;
350        try {
351            URLConnection connection = url.openConnection();
352            inputStream = connection.getInputStream();
353            properties.load(url.openStream());
354            _date = connection.getDate();
355        } finally {
356            if (inputStream != null) {
357                try {
358                    inputStream.close();
359                } catch (Throwable throwable) {
360                    // Ignore.
361                }
362            }
363        }
364
365        if (_properties != null) {
366            // Remove previous parameters that are not defined
367            // in the new set.
368            Iterator attributeNames = _properties.keySet().iterator();
369            while (attributeNames.hasNext()) {
370                String attributeName = (String) attributeNames.next();
371                if (!properties.containsKey(attributeName)) {
372                    getAttribute(attributeName).setContainer(null);
373                }
374            }
375        }
376
377        _properties = properties;
378
379        // Iterate through all the properties and either create new parameters
380        // or set current parameters.
381        // Use entrySet for performance reasons.
382        Iterator attributeMapEntries = properties.entrySet().iterator();
383        while (attributeMapEntries.hasNext()) {
384            Map.Entry attributeNames = (Map.Entry) attributeMapEntries.next();
385            String attributeName = (String) attributeNames.getKey();
386            String attributeValue = (String) attributeNames.getValue();
387            Variable variable = (Variable) getAttribute(attributeName);
388            if (variable == null) {
389                variable = new Variable(this, attributeName);
390            }
391            variable.setExpression(attributeValue);
392        }
393    }
394
395    /** Remove the specified object from the list of objects whose
396     *  preinitialize(), initialize(), and wrapup()
397     *  methods should be invoked upon invocation of the corresponding
398     *  methods of this object. If the specified object is not
399     *  on the list, do nothing.
400     *  @param initializable The object whose methods should no longer be invoked.
401     *  @see #addInitializable(Initializable)
402     *  @see ptolemy.actor.CompositeActor#removePiggyback(Executable)
403     */
404    @Override
405    public void removeInitializable(Initializable initializable) {
406        if (_initializables != null) {
407            _initializables.remove(initializable);
408            if (_initializables.size() == 0) {
409                _initializables = null;
410            }
411        }
412    }
413
414    /** Override the base class to register as a piggyback with the nearest opaque
415     *  composite actor above in the hierarchy.
416     *  @param container The proposed container.
417     *  @exception IllegalActionException If the action would result in a
418     *   recursive containment structure, or if
419     *   this entity and container are not in the same workspace.
420     *  @exception NameDuplicationException If the container already has
421     *   an entity with the name of this entity.
422     */
423    @Override
424    public void setContainer(NamedObj container)
425            throws IllegalActionException, NameDuplicationException {
426        if (container != getContainer()) {
427            // May need to unregister as a piggyback with the previous container.
428            NamedObj previousContainer = getContainer();
429            if (previousContainer instanceof CompositeActor) {
430                ((CompositeActor) previousContainer).removePiggyback(this);
431            }
432        }
433        super.setContainer(container);
434        if (container instanceof CompositeActor) {
435            ((CompositeActor) container).addPiggyback(this);
436        }
437    }
438
439    /** Do nothing.
440     */
441    @Override
442    public void stop() {
443    }
444
445    /** Do nothing.
446     */
447    @Override
448    public void stopFire() {
449    }
450
451    /** Do nothing.
452     */
453    @Override
454    public void terminate() {
455    }
456
457    /** Check to see whether the specified file has changed, and if so,
458     *  re-read it, and invoke the wrapup() methods
459     *  of objects that have been added using addInitializable().
460     *  @exception IllegalActionException If one of the added objects
461     *   throws it, or if re-reading the file fails.
462     */
463    @Override
464    public void wrapup() throws IllegalActionException {
465        // Invoke initializable methods.
466        if (_initializables != null) {
467            for (Initializable initializable : _initializables) {
468                initializable.wrapup();
469            }
470        }
471        if (_reReadIfNeeded()) {
472            validate();
473        }
474    }
475
476    ///////////////////////////////////////////////////////////////////
477    ////                         private methods                   ////
478
479    /** If either the file name or the date on the file have changed
480     *  since the last reading, then re-read the file.
481     *  @return True if re-reading was done.
482     *  @exception IllegalActionException If re-reading the file fails.
483     */
484    private boolean _reReadIfNeeded() throws IllegalActionException {
485        try {
486            String currentFileName = fileOrURL.getExpression();
487            if (!currentFileName.equals(_fileName)) {
488                // File name has changed. Must re-read.
489                read();
490                return true;
491            }
492            URL url = fileOrURL.asURL();
493            if (url == null) {
494                throw new IOException("Could not convert \""
495                        + fileOrURL.getExpression() + "\" with base \""
496                        + fileOrURL.getBaseDirectory() + "\" to a URL.");
497            }
498            long date = url.openConnection().getDate();
499            if (date == 0L || date != _date) {
500                read();
501                return true;
502            }
503            return false;
504        } catch (NameDuplicationException ex) {
505            // Two separate exceptions to get FindBugs to shut up.
506            throw new IllegalActionException(this, ex,
507                    "Failed to re-read parameter set, problem with dupliate names.");
508        } catch (IOException ex2) {
509            throw new IllegalActionException(this, ex2,
510                    "Failed to re-read parameter set.");
511        }
512    }
513
514    ///////////////////////////////////////////////////////////////////
515    ////                         private fields                    ////
516
517    /** Date of the file when last read. */
518    private long _date = 0L;
519
520    /** The previously read file name. */
521    private String _fileName;
522
523    /** List of objects whose (pre)initialize() and wrapup() methods
524     *  should be slaved to these.
525     */
526    private transient List<Initializable> _initializables;
527
528    /** Cached copy of the last hashset of properties, used to remove old
529     *  properties.
530     */
531    private Properties _properties;
532}