001/* An attribute whose value can be set via the MoML configure tag.
002
003 Copyright (c) 1998-2015 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.util;
029
030import java.io.BufferedReader;
031import java.io.IOException;
032import java.io.InputStream;
033import java.io.InputStreamReader;
034import java.io.Writer;
035import java.net.URL;
036import java.util.Collection;
037import java.util.HashSet;
038import java.util.Iterator;
039import java.util.LinkedList;
040import java.util.List;
041
042///////////////////////////////////////////////////////////////////
043//// ConfigurableAttribute
044
045/**
046 This class provides a simple way to get a long string into an attribute.
047 It implements Configurable, so its value can be set using a configure MoML
048 element.  For example,
049 <pre>
050 &lt;property name="x" class="ptolemy.moml.ConfigurableAttribute"&gt;
051 &lt;configure source="url"&gt;xxx&lt;/configure&gt;
052 &lt;/property&gt;
053 </pre>
054 The value of this property, obtained via the value() method,
055 will be whatever text is contained by the referenced URL (which
056 is optional), followed by the text "xxx".
057
058 @author Steve Neuendorffer and Edward A. Lee
059 @version $Id$
060 @since Ptolemy II 1.0
061 @Pt.ProposedRating Green (eal)
062 @Pt.AcceptedRating Green (janneck)
063 */
064public class ConfigurableAttribute extends Attribute
065        implements Configurable, Settable {
066    /** Construct a new attribute with no
067     *  container and an empty string as its name. Add the attribute to the
068     *  default workspace directory.
069     *  Increment the version number of the workspace.
070     */
071    public ConfigurableAttribute() {
072        super();
073    }
074
075    /** Construct a new attribute with
076     *  no container and an empty string as a name. You can then change
077     *  the name with setName(). If the workspace argument is null, then
078     *  use the default workspace.
079     *  Add the attribute to the workspace directory.
080     *  Increment the version number of the workspace.
081     *  @param workspace The workspace that will list the attribute.
082     */
083    public ConfigurableAttribute(Workspace workspace) {
084        super(workspace);
085    }
086
087    /** Construct a new attribute with the given container and name.
088     *  @param container The container.
089     *  @param name The name.
090     *  @exception IllegalActionException If the attribute cannot be contained
091     *   by the proposed container.
092     *  @exception NameDuplicationException If the container already has an
093     *   attribute with this name.
094     */
095    public ConfigurableAttribute(NamedObj container, String name)
096            throws NameDuplicationException, IllegalActionException {
097        super(container, name);
098    }
099
100    ///////////////////////////////////////////////////////////////////
101    ////                         public methods                    ////
102
103    /** Add a listener to be notified when the value of this attribute changes.
104     *  If the listener is already on the list of listeners, then do nothing.
105     *  @param listener The listener to add.
106     *  @see #removeValueListener(ValueListener)
107     */
108    @Override
109    public void addValueListener(ValueListener listener) {
110        if (_valueListeners == null) {
111            _valueListeners = new LinkedList();
112        }
113
114        if (!_valueListeners.contains(listener)) {
115            _valueListeners.add(listener);
116        }
117    }
118
119    /** Clone the attribute.  This creates a new attribute with the same value.
120     *  @param workspace The workspace in which to place the cloned variable.
121     *  @exception CloneNotSupportedException Not thrown in this base class.
122     *  @see java.lang.Object#clone()
123     *  @return The cloned attribute.
124     */
125    @Override
126    public Object clone(Workspace workspace) throws CloneNotSupportedException {
127        ConfigurableAttribute newObject = (ConfigurableAttribute) super.clone(
128                workspace);
129
130        newObject._base = null;
131
132        // The clone has new value listeners.
133        newObject._valueListeners = null;
134
135        return newObject;
136    }
137
138    /** Configure the object with data from the specified input source
139     *  (a URL) and/or textual data.  The input source, if any, is assumed
140     *  to contain textual data as well.  Note that the URL is not read
141     *  until the value() or validate() method is called.
142     *  @param base The base relative to which references within the input
143     *   are found, or null if this is not known, or there is none.
144     *   This argument is ignored in this method.
145     *  @param source The input source, which specifies a URL.
146     *  @param text Configuration information given as text.
147     *  @exception Exception Not thrown in this base class.
148     */
149    @Override
150    public void configure(URL base, String source, String text)
151            throws Exception {
152        if (_defaultText == null) {
153            _defaultText = _configureText;
154        }
155
156        _base = base;
157        _configureSource = source;
158        _configureText = text;
159    }
160
161    /** Return the base specified in the most recent call to the
162     *  configure() method, or null if none.
163     *  @return The base with respect to which the relative references
164     *   in the source file should be interpreted.
165     */
166    public URL getBase() {
167        return _base;
168    }
169
170    /** Return the source specified in the most recent call to the
171     *  configure() method, or null if none.
172     *  @return A URL specifying an external source for configure
173     *   information.
174     */
175    @Override
176    public String getConfigureSource() {
177        return _configureSource;
178    }
179
180    /** Return the text specified in the most recent call to the
181     *  configure() method, or null if none.
182     *  @return Text giving configure information.
183     */
184    @Override
185    public String getConfigureText() {
186        return _configureText;
187    }
188
189    /** Return the default value of this Settable,
190     *  if there is one.  If this is a derived object, then the default
191     *  is the value of the object from which this is derived (the
192     *  "prototype").  If this is not a derived object, then the default
193     *  is the first value set using setExpression(), or null if
194     *  setExpression() has not been called.
195     *  @return The default value of this attribute, or null
196     *   if there is none.
197     *  @see #setExpression(String)
198     */
199    @Override
200    public String getDefaultExpression() {
201        try {
202            List prototypeList = getPrototypeList();
203
204            if (prototypeList.size() > 0) {
205                return ((Settable) prototypeList.get(0)).getExpression();
206            }
207        } catch (IllegalActionException e) {
208            // This should not occur.
209            throw new InternalErrorException(e);
210        }
211
212        return _defaultText;
213    }
214
215    /** Return the the result of calling value().
216     *  @return The value, or a description of the exception if one is thrown.
217     *  @see #value()
218     *  @see #setExpression(String)
219     */
220    @Override
221    public String getExpression() {
222        try {
223            return value();
224        } catch (Exception ex) {
225            return ex.toString();
226        }
227    }
228
229    /** Get the value of the attribute, which is the evaluated expression.
230     *  @return The same as getExpression().
231     *  @see #getExpression()
232     */
233    @Override
234    public String getValueAsString() {
235        return getExpression();
236    }
237
238    /** Get the visibility of this attribute, as set by setVisibility().
239     *  The visibility is set by default to NONE.
240     *  @return The visibility of this attribute.
241     *  @see #setVisibility(Settable.Visibility)
242     */
243    @Override
244    public Settable.Visibility getVisibility() {
245        return _visibility;
246    }
247
248    /** Remove a listener from the list of listeners that is
249     *  notified when the value of this attribute changes.  If no such listener
250     *  exists, do nothing.
251     *  @param listener The listener to remove.
252     *  @see #addValueListener(ValueListener)
253     */
254    @Override
255    public void removeValueListener(ValueListener listener) {
256        if (_valueListeners != null) {
257            _valueListeners.remove(listener);
258        }
259    }
260
261    /** Set the value of the string attribute and notify the container
262     *  of the value of this attribute by calling attributeChanged(),
263     *  and notify any listeners that have
264     *  been registered using addValueListener().  This is the same
265     *  as calling configure with a null base and source, passing
266     *  the argument as text.
267     *  @param expression The text to configure the attribute with.
268     *  @exception IllegalActionException If the change is not acceptable
269     *   to the container.
270     *  @see #getExpression()
271     */
272    @Override
273    public void setExpression(String expression) throws IllegalActionException {
274        try {
275            configure(null, null, expression);
276            validate();
277        } catch (IllegalActionException ex) {
278            throw ex;
279        } catch (Exception ex) {
280            throw new InternalErrorException("Unexpected exception: " + ex);
281        }
282    }
283
284    /** Set the visibility of this attribute.  The argument should be one
285     *  of the public static instances in Settable.
286     *  @param visibility The visibility of this attribute.
287     *  @see #getVisibility()
288     */
289    @Override
290    public void setVisibility(Settable.Visibility visibility) {
291        _visibility = visibility;
292    }
293
294    /** Validate this attribute by calling {@link #value()}.
295     *  Notify the container and listeners that the value of this
296     *  attribute has changed.
297     *  @return A list of contained instances of Settable.
298     *  @exception IllegalActionException If the change is not acceptable
299     *   to the container.
300     */
301    @Override
302    public Collection validate() throws IllegalActionException {
303        // Validate by obtaining the value.
304        try {
305            value();
306        } catch (IOException ex) {
307            throw new IllegalActionException(this, ex,
308                    "Failed to read configuration at: " + _configureSource);
309        }
310        // Notify the container that the attribute has changed.
311        NamedObj container = getContainer();
312        if (container != null) {
313            container.attributeChanged(this);
314        }
315        // Notify value listeners.
316        Collection result = new HashSet();
317        if (_valueListeners != null) {
318            Iterator listeners = _valueListeners.iterator();
319
320            while (listeners.hasNext()) {
321                ValueListener listener = (ValueListener) listeners.next();
322                if (listener instanceof Settable) {
323                    Collection validated = ((Settable) listener).validate();
324                    if (validated != null) {
325                        result.addAll(validated);
326                    }
327                    result.add(listener);
328                } else {
329                    listener.valueChanged(this);
330                }
331            }
332        }
333        return result;
334    }
335
336    /** Return the value given by the configure tag.  This is the text
337     *  read from the specified URL (if any), followed by the text
338     *  specified in the body of the configure element.  Note that the
339     *  URL given in the configure() method, if any, is read each time
340     *  this method is called.
341     *  @return The value set in the configure tag.
342     *  @exception IOException If the URL given in the configure method
343     *   (if any) cannot be read.
344     */
345    public String value() throws IOException {
346        StringBuffer value = new StringBuffer();
347
348        // If a source is given, read its data.
349        if (_configureSource != null && !_configureSource.trim().equals("")) {
350            URL textFile = new URL(_configureSource);
351            InputStream stream = textFile.openStream();
352            BufferedReader reader = null;
353
354            try {
355                reader = new BufferedReader(new InputStreamReader(stream,
356                        java.nio.charset.Charset.defaultCharset()));
357
358                String line = reader.readLine();
359
360                while (line != null) {
361                    value.append(line);
362                    value.append("\n");
363                    line = reader.readLine();
364                }
365            } finally {
366                if (reader != null) {
367                    reader.close();
368                }
369            }
370
371            // NOTE: Do we need to close both?  Java docs don't say.
372            stream.close();
373        }
374
375        if (_configureText != null) {
376            value.append(_configureText);
377        }
378
379        return value.toString();
380    }
381
382    ///////////////////////////////////////////////////////////////////
383    ////                         protected methods                 ////
384
385    /** Write a MoML description of the contents of this object.
386     *  This method is called by exportMoML().  Each description
387     *  is indented according to the
388     *  specified depth and terminated with a newline character.
389     *  @param output The output stream to write to.
390     *  @param depth The depth in the hierarchy, to determine indenting.
391     *  @exception IOException If an I/O error occurs.
392     */
393    @Override
394    protected void _exportMoMLContents(Writer output, int depth)
395            throws IOException {
396        super._exportMoMLContents(output, depth);
397
398        String sourceSpec = "";
399
400        if (_configureSource != null && !_configureSource.trim().equals("")) {
401            sourceSpec = " source=\"" + _configureSource + "\"";
402
403            if (_configureText == null) {
404                output.write(_getIndentPrefix(depth) + "<configure" + sourceSpec
405                        + "/>\n");
406            }
407        }
408
409        if (_configureText != null) {
410            output.write(_getIndentPrefix(depth) + "<configure" + sourceSpec
411                    + ">" + _configureText + "</configure>\n");
412        }
413    }
414
415    /** Propagate the value of this object to the
416     *  specified object. The specified object is required
417     *  to be an instance of the same class as this one, or
418     *  a ClassCastException will be thrown.
419     *  @param destination Object to which to propagate the
420     *   value.
421     *  @exception IllegalActionException If the value cannot
422     *   be propagated.
423     */
424    @Override
425    protected void _propagateValue(NamedObj destination)
426            throws IllegalActionException {
427        try {
428            ((Configurable) destination).configure(_base, _configureSource,
429                    _configureText);
430        } catch (Exception ex) {
431            throw new IllegalActionException(this, ex, "Propagation failed.");
432        }
433    }
434
435    ///////////////////////////////////////////////////////////////////
436    ////                         private members                   ////
437    // The base specified in the configure() method.
438    private URL _base;
439
440    // The URL from which configure data is read.
441    private String _configureSource;
442
443    // The text in the body of the configure.
444    private String _configureText;
445
446    // The default text in the body of the configure.
447    private String _defaultText;
448
449    // Listeners for changes in value.
450    private List _valueListeners;
451
452    // The visibility of this attribute, which defaults to NONE;
453    private Settable.Visibility _visibility = Settable.NONE;
454}