001/* A store for key-value pairs.
002
003 Copyright (c) 2013-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 */
028
029package ptolemy.actor.lib;
030
031import java.io.BufferedReader;
032import java.io.File;
033import java.io.IOException;
034import java.util.ArrayList;
035import java.util.HashMap;
036import java.util.HashSet;
037import java.util.Set;
038import java.util.logging.Level;
039
040import ptolemy.actor.TypedAtomicActor;
041import ptolemy.actor.TypedIOPort;
042import ptolemy.data.ArrayToken;
043import ptolemy.data.BooleanToken;
044import ptolemy.data.RecordToken;
045import ptolemy.data.StringToken;
046import ptolemy.data.Token;
047import ptolemy.data.expr.ASTPtRootNode;
048import ptolemy.data.expr.FileParameter;
049import ptolemy.data.expr.ModelScope;
050import ptolemy.data.expr.Parameter;
051import ptolemy.data.expr.ParseTreeEvaluator;
052import ptolemy.data.expr.ParserScope;
053import ptolemy.data.expr.PtParser;
054import ptolemy.data.expr.SingletonParameter;
055import ptolemy.data.type.ArrayType;
056import ptolemy.data.type.BaseType;
057import ptolemy.data.type.Type;
058import ptolemy.kernel.CompositeEntity;
059import ptolemy.kernel.util.IllegalActionException;
060import ptolemy.kernel.util.LoggerListener;
061import ptolemy.kernel.util.NameDuplicationException;
062import ptolemy.kernel.util.StringAttribute;
063import ptolemy.kernel.util.Workspace;
064import ptolemy.util.MessageHandler;
065
066/**
067   A store for key-value pairs.
068   This actor stores key-value pairs and provides an interface for retrieving
069   them one at a time or in groups.
070
071   @author Shuhei Emoto, Edward A. Lee, Kentaro Mizouchi, Tetsuya Suga
072   @version $Id$
073   @since Ptolemy II 10.0
074   @Pt.ProposedRating Yellow (cxh)
075   @Pt.AcceptedRating Red (cxh)
076 */
077public class Dictionary extends TypedAtomicActor {
078
079    /** Construct an actor with the given container and name.
080     *  @param container The container.
081     *  @param name The name of this actor.
082     *  @exception IllegalActionException If the actor cannot be contained
083     *   by the proposed container.
084     *  @exception NameDuplicationException If the container already has an
085     *   actor with this name.
086     */
087    public Dictionary(CompositeEntity container, String name)
088            throws IllegalActionException, NameDuplicationException {
089        super(container, name);
090
091        // Alphabetized by variable name.
092
093        keys = new TypedIOPort(this, "keys", false, true);
094        new SingletonParameter(keys, "_showName").setExpression("true");
095
096        readKey = new TypedIOPort(this, "readKey", true, false);
097        readKey.setTypeEquals(BaseType.STRING);
098        new SingletonParameter(readKey, "_showName").setExpression("true");
099
100        readKeyArray = new TypedIOPort(this, "readKeyArray", true, false);
101        new SingletonParameter(readKeyArray, "_showName").setExpression("true");
102
103        result = new TypedIOPort(this, "result", false, true);
104        new SingletonParameter(result, "_showName").setExpression("true");
105
106        resultArray = new TypedIOPort(this, "resultArray", false, true);
107        new SingletonParameter(resultArray, "_showName").setExpression("true");
108        // FIXME: The length of the output array should match the length of the readKeyArray.
109        // How to do that?
110
111        triggerKeys = new TypedIOPort(this, "triggerKeys", true, false);
112        new SingletonParameter(triggerKeys, "_showName").setExpression("true");
113        new StringAttribute(triggerKeys, "_cardinal").setExpression("SOUTH");
114
115        triggerValues = new TypedIOPort(this, "triggerValues", true, false);
116        new SingletonParameter(triggerValues, "_showName")
117                .setExpression("true");
118        new StringAttribute(triggerValues, "_cardinal").setExpression("SOUTH");
119
120        values = new TypedIOPort(this, "values", false, true);
121        new SingletonParameter(values, "_showName").setExpression("true");
122
123        value = new TypedIOPort(this, "value", true, false);
124        new SingletonParameter(value, "_showName").setExpression("true");
125
126        writeKey = new TypedIOPort(this, "writeKey", true, false);
127        writeKey.setTypeEquals(BaseType.STRING);
128        new SingletonParameter(writeKey, "_showName").setExpression("true");
129
130        notFound = new TypedIOPort(this, "notFound", false, true);
131        new SingletonParameter(notFound, "_showName").setExpression("true");
132
133        // Set the type constraints.
134        keys.setTypeAtLeast(ArrayType.arrayOf(writeKey));
135        readKeyArray.setTypeAtLeast(ArrayType.arrayOf(readKey));
136        result.setTypeSameAs(value);
137        resultArray.setTypeAtLeast(ArrayType.arrayOf(value));
138        values.setTypeAtLeast(ArrayType.arrayOf(value));
139        notFound.setTypeEquals(new ArrayType(BaseType.STRING));
140
141        _store = new HashMap<String, Token>();
142
143        file = new FileParameter(this, "file");
144        updateFile = new Parameter(this, "updateFile");
145        updateFile.setTypeEquals(BaseType.BOOLEAN);
146        updateFile.setExpression("false");
147
148        loggingDirectory = new FileParameter(this, "loggingDirectory");
149    }
150
151    ///////////////////////////////////////////////////////////////////
152    ////                     ports and parameters                  ////
153
154    /** If a file is given here, it will be read upon initialization
155     *  (if it exists and can be parsed as an array of arrays of tokens)
156     *  to initialize the dictionary.
157     */
158    public FileParameter file;
159
160    /** Upon receiving any token at the triggerKeys port, this actor
161     *  will produce on this output an array containing all the keys
162     *  of entries in the dictionary. The order is arbitrary.
163     *  If there are no entries in the dictionary, then send an
164     *  empty array.
165     *  The type is array of string.
166     */
167    public TypedIOPort keys;
168
169    /** If given, a log file will be written to the specified
170     *  directory.
171     *  <p>A file name can also contain the following strings that start
172     *  with "$", which get substituted
173     *  with the appropriate values.</p>
174     *  <table>
175     *  <caption>"Properties that are substituted.</caption>
176     *  <tr>
177     *  <th>String</th>
178     *  <th>Description</th>
179     *  <th>Property</th>
180     *  </tr>
181     *  <tr>
182     *  <td><code>$CWD</code></td>
183     *  <td>The current working directory</td>
184     *  <td><code>user.dir</code></td>
185     *  </tr>
186     *  <tr>
187     *  <td><code>$HOME</code></td>
188     *  <td>The user's home directory</td>
189     *  <td><code>user.home</code></td>
190     *  </tr>
191     *  <tr>
192     *  <td><code>$PTII</code></td>
193     *  <td>The home directory of the Ptolemy II installation</td>
194     *  <td><code>ptolemy.ptII.dir</code></td>
195     *  </tr>
196     *  <tr>
197     *  <td><code>$TMPDIR</code></td>
198     *  <td>The temporary directory</td>
199     *  <td><code>java.io.tmpdir</code></td>
200     *  </tr>
201     *  <tr>
202     *  <td><code>$USERNAME</code></td>
203     *  <td>The user's account name</td>
204     *  <td><code>user.name</code></td>
205     *  </tr>
206     *  </table>
207     */
208    public FileParameter loggingDirectory;
209
210    /** An output listing one or more keys that were
211     *  requested but not found in the dictionary.
212     *  The output is produced only if a key is not
213     *  found. The output type is an array of strings.
214     */
215    public TypedIOPort notFound;
216
217    /** An input that provides a key for a value to be read from the
218     *  dictionary.  If the dictionary does not contain any value
219     *  corresponding to this key, then the output will be a nil
220     *  token. This has type string.
221     */
222    public TypedIOPort readKey;
223
224    /** An input that provides an array of keys to be read
225     *  simultaneously from the dictionary. The output will be an
226     *  array with the same length as this input where each entry in
227     *  the output array is the value corresponding to the
228     *  corresponding key in the input array. For any key that has no
229     *  entry in the dictionary, a nil token will be inserted in the
230     *  output array.
231     *  The type is array of string.
232     */
233    public TypedIOPort readKeyArray;
234
235    /** An output providing the result of a single reading of the
236     *  dictionary via the readKey input port. If the specified key
237     *  is not found, this port will produce a nil token, and an
238     *  array of length one with the key will be produced on the
239     *  {@link #notFound} output port.
240     */
241    public TypedIOPort result;
242
243    /** An output providing the result of a multiple reading of the
244     *  dictionary via the readKeyArray input port. For any of the
245     *  keys in the {@link #readKeyArray} input is not in the dictionary,
246     *  there will be a nil token in the result array in the position
247     *  of the missing key. The missing keys will be produced on the
248     *  notFound output.
249     */
250    public TypedIOPort resultArray;
251
252    /** Upon receiving any token at this port, this actor will produce
253     *  on the keys output an array containing all the keys of entries
254     *  in the dictionary. The order is arbitrary.
255     */
256    public TypedIOPort triggerKeys;
257
258    /** Upon receiving any token at this port, this actor will produce
259     *  on the values output an array containing all the values of entries
260     *  in the dictionary. The order is arbitrary.
261     */
262    public TypedIOPort triggerValues;
263
264    /** Upon receiving any token at the triggerValues port, this actor
265     *  will produce on this output an array containing all the values
266     *  of entries in the dictionary. The order is arbitrary.
267     *  If there are no entries in the dictionary, then send an
268     *  empty array.
269     *  The type is array of token.
270     */
271    public TypedIOPort values;
272
273    /** If set to true, and if a <i>file</i> parameter is given, then
274     *  upon each update to the dictionary, the contents of the dictionary
275     *  will be stored in the file.  This defaults to false.
276     */
277    public Parameter updateFile;
278
279    /** Input port for providing a value to store in the dictionary.
280     *  The value will be stored only if a writeKey input arrives at
281     *  the same time. Otherwise, it will be discarded.
282     */
283    public TypedIOPort value;
284
285    /** An input that provides a key for a key-value pair to be stored
286     *  in the dictionary. If a key arrives on this port, but there is
287     *  no value on the value port or the value is nil, then the
288     *  dictionary entry with the specified key will be
289     *  removed. Otherwise, the value provided on the value port will
290     *  be stored indexed by this key. This has type string.
291     */
292    public TypedIOPort writeKey;
293
294    ///////////////////////////////////////////////////////////////////
295    ////                         public methods                    ////
296
297    /** Clone the actor into the specified workspace.
298     *  @param workspace The workspace for the new object.
299     *  @return A new actor.
300     *  @exception CloneNotSupportedException If a derived class contains
301     *   an attribute that cannot be cloned.
302     */
303    @Override
304    public Object clone(Workspace workspace) throws CloneNotSupportedException {
305        Dictionary newObject = (Dictionary) super.clone(workspace);
306
307        try {
308            // Set the type constraints.
309            newObject.readKeyArray
310                    .setTypeAtLeast(ArrayType.arrayOf(newObject.readKey));
311            newObject.keys
312                    .setTypeAtLeast(ArrayType.arrayOf(newObject.writeKey));
313            newObject.result.setTypeSameAs(newObject.value);
314            newObject.resultArray
315                    .setTypeAtLeast(ArrayType.arrayOf(newObject.value));
316            newObject.values.setTypeAtLeast(ArrayType.arrayOf(newObject.value));
317        } catch (IllegalActionException ex) {
318            CloneNotSupportedException exception = new CloneNotSupportedException(
319                    "Failed to clone " + getFullName());
320            exception.initCause(ex);
321            throw exception;
322        }
323        // Initialize objects.
324        newObject._store = new HashMap<String, Token>();
325
326        return newObject;
327    }
328
329    /** If there is a writeKey input, then update the dictionary;
330     *  specifically, if there is also a value input, then insert into
331     *  the dictionary the key-value pair given by these two inputs.
332     *  Otherwise, or if the value input is a nil token, then delete
333     *  the dictionary entry corresponding to the key.  If there is a
334     *  readKey input, then read the dictionary and produce on the
335     *  result output the entry corresponding to the key, or a nil
336     *  token if there is no such entry.  If there is a readKeyArray
337     *  input, then read the dictionary and produce on the resultArray
338     *  output the entries corresponding to the keys, with nil tokens
339     *  inserted for any missing entry.  If there is a triggerKeys
340     *  input, then produce on the keys output an array containing all
341     *  the keys in the dictionary, in arbitrary order.
342     */
343    @Override
344    public void fire() throws IllegalActionException {
345        super.fire();
346        if (writeKey.getWidth() > 0 && writeKey.hasToken(0)) {
347            StringToken theKey = (StringToken) writeKey.get(0);
348
349            // Get a value if there is one.
350            Token theValue = null;
351            if (value.getWidth() > 0 && value.hasToken(0)) {
352                theValue = value.get(0);
353            }
354            if (theValue == null || theValue.isNil()) {
355                // Remove the entry.
356                Token removed = _store.remove(theKey.stringValue());
357                if (_debugging) {
358                    if (removed == null) {
359                        _debug("Attempted to remove non-existent key: "
360                                + theKey);
361                    } else {
362                        _debug("Removed key: " + theKey);
363                    }
364                }
365            } else {
366                _store.put(theKey.stringValue(), theValue);
367                if (_debugging) {
368                    _debug("Storing key, value: " + theKey + ", " + theValue);
369                }
370            }
371        } else if (value.getWidth() > 0 && value.hasToken(0)) {
372            // Read and discard the input token so that DE doesn't refire me.
373            value.get(0);
374        }
375        if (readKey.getWidth() > 0 && readKey.hasToken(0)) {
376            StringToken theKey = (StringToken) readKey.get(0);
377            Token theResult = _store.get(theKey.stringValue());
378            // NOTE: We choose to output a nil token if the result is not in the store.
379            if (theResult != null) {
380                result.send(0, theResult);
381                if (_debugging) {
382                    _debug("Retrieved key, value: " + theKey + ", "
383                            + theResult);
384                }
385            } else {
386                // Sending nil on the output enables use of this actor in SDF, since
387                // every input will trigger an output.
388                result.send(0, Token.NIL);
389                StringToken[] theKeys = new StringToken[1];
390                theKeys[0] = theKey;
391                notFound.send(0, new ArrayToken(theKeys));
392                if (_debugging) {
393                    _debug("Requested key with no value: " + theKey);
394                }
395            }
396        }
397        if (readKeyArray.getWidth() > 0 && readKeyArray.hasToken(0)) {
398            ArrayToken theKeys = (ArrayToken) readKeyArray.get(0);
399            Token[] theResult = new Token[theKeys.length()];
400            ArrayList<StringToken> keysNotFound = new ArrayList<StringToken>();
401            int i = 0;
402            for (Token theKey : theKeys.arrayValue()) {
403                String theKeyAsString = ((StringToken) theKey).stringValue();
404                theResult[i] = _store.get(theKeyAsString);
405                if (theResult[i] == null) {
406                    theResult[i] = Token.NIL;
407                    keysNotFound.add(new StringToken(theKeyAsString));
408                }
409                i++;
410            }
411            ArrayToken resultToken = new ArrayToken(value.getType(), theResult);
412            if (_debugging) {
413                _debug("Retrieved keys, values: " + theKeys + ", "
414                        + resultToken);
415            }
416            resultArray.send(0, resultToken);
417            if (keysNotFound.size() > 0) {
418                ArrayToken notFoundToken = new ArrayToken(BaseType.STRING,
419                        keysNotFound
420                                .toArray(new StringToken[keysNotFound.size()]));
421                notFound.send(0, notFoundToken);
422                if (_debugging) {
423                    _debug("Keys with no value: " + notFoundToken);
424                }
425            }
426        }
427        if (triggerKeys.getWidth() > 0 && triggerKeys.hasToken(0)) {
428            // Must consume the trigger, or DE will fire me again.
429            triggerKeys.get(0);
430            StringToken[] result = new StringToken[_store.size()];
431            int i = 0;
432            for (String label : _store.keySet()) {
433                result[i] = new StringToken(label);
434                i++;
435            }
436            if (result.length > 0) {
437                keys.send(0, new ArrayToken(result));
438            } else {
439                // Send an empty array.
440                keys.send(0, new ArrayToken(BaseType.STRING));
441            }
442        }
443        if (triggerValues.getWidth() > 0 && triggerValues.hasToken(0)) {
444            // Must consume the trigger, or DE will fire me again.
445            triggerValues.get(0);
446
447            Token[] theResult = new Token[_store.values().size()];
448            int i = 0;
449            for (Token value : _store.values()) {
450                theResult[i] = value;
451                i++;
452            }
453            ArrayToken resultToken = new ArrayToken(value.getType(), theResult);
454            values.send(0, resultToken);
455        }
456    }
457
458    /** Clear the dictionary. If a <i>file</i> is specified,
459     *  attempt to read it to initialize the dictionary.
460     *  If <i>enableLogging</i> is true, then start logging.
461     *  @exception IllegalActionException If the superclass throws it.
462     */
463    @Override
464    public void initialize() throws IllegalActionException {
465        super.initialize();
466
467        File directory = loggingDirectory.asFile();
468        if (directory != null) {
469            // Start logging.
470            // Leave off the leading period on the file so it doen't get hidden.
471            _logger = new LoggerListener(getFullName().substring(1), directory);
472            addDebugListener(_logger);
473        } else {
474            if (_logger != null) {
475                removeDebugListener(_logger);
476                _logger = null;
477            }
478        }
479
480        _store.clear();
481
482        File theFile = file.asFile();
483        if (theFile != null && theFile.canRead()) {
484            BufferedReader reader = file.openForReading();
485            StringBuffer dictionary = new StringBuffer();
486            String line;
487            try {
488                line = reader.readLine();
489                while (line != null) {
490                    dictionary.append(line);
491                    line = reader.readLine();
492                }
493                // FIXME: May want to support JSON formatted input.
494                if (_parser == null) {
495                    _parser = new PtParser();
496                }
497                ASTPtRootNode parseTree = _parser
498                        .generateParseTree(dictionary.toString());
499
500                if (_parseTreeEvaluator == null) {
501                    _parseTreeEvaluator = new ParseTreeEvaluator();
502                }
503
504                if (_scope == null) {
505                    _scope = new EmptyScope();
506                }
507
508                Token parsed = _parseTreeEvaluator.evaluateParseTree(parseTree,
509                        _scope);
510
511                if (!(parsed instanceof RecordToken)) {
512                    _errorMessage(
513                            "Initialization file does not evaluate to a Ptolemy II record: "
514                                    + file.getExpression());
515                }
516
517                for (String key : ((RecordToken) parsed).labelSet()) {
518                    Token value = ((RecordToken) parsed).get(key);
519                    _store.put(key, value);
520                }
521                if (_debugging) {
522                    _debug("Initialized store from file: " + theFile.getPath());
523                }
524            } catch (Exception e) {
525                // Warning only. Continue without the file.
526                _errorMessage("Failed to initialize store from file: "
527                        + theFile.getPath() + " Exception: " + e.toString());
528            } finally {
529                try {
530                    reader.close();
531                } catch (IOException e) {
532                    _errorMessage("Failed to close initialization file: "
533                            + theFile.getPath() + " Exception: "
534                            + e.toString());
535                }
536            }
537        } else {
538            if (_debugging) {
539                _debug("Initialization file does not exist or cannot be read.");
540            }
541        }
542    }
543
544    /** If a <i>file</i> has been specified and <i>updateFile</i> is true, then
545     *  save the current state of the dictionary in the file.
546     *  If the file cannot be written, then dictionary contents will be sent
547     *  to standard out and an exception will be thrown.
548     *  @exception IllegalActionException If the file cannot be written.
549     */
550    @Override
551    public void wrapup() throws IllegalActionException {
552        super.wrapup();
553
554        File theFile = file.asFile();
555        if (theFile != null
556                && ((BooleanToken) updateFile.getToken()).booleanValue()) {
557
558            // Assemble a record from the current state of the store.
559            RecordToken record = new RecordToken(_store);
560            try {
561                java.io.Writer writer = file.openForWriting();
562                writer.write(record.toString());
563                if (_debugging) {
564                    _debug("Key-value store written to file: "
565                            + theFile.getPath());
566                }
567            } catch (Exception e) {
568                _errorMessage("Failed to update file: " + theFile.getPath()
569                        + " Exception: " + e.toString());
570                // Write contents to standard out so it can hopefully be retrieved.
571                System.out.println(record.toString());
572            } finally {
573                file.close();
574            }
575        } else {
576            if (_debugging) {
577                _debug("Dictionary data discarded.");
578            }
579        }
580        if (_logger != null) {
581            _logger.close();
582        }
583    }
584
585    ///////////////////////////////////////////////////////////////////
586    ////                         private methods                   ////
587
588    /** Log an error, or send messages to the default MessageHandler
589     *  and debug listeners, if any.
590     *  @param message The message.
591     */
592    private void _errorMessage(String message) {
593        if (_logger != null) {
594            _logger.log(Level.SEVERE, message);
595        } else {
596            MessageHandler.error(message);
597            if (_debugging) {
598                _debug(message);
599            }
600        }
601    }
602
603    ///////////////////////////////////////////////////////////////////
604    ////                         private variables                 ////
605
606    /** The logger to use, if logging is enabled. */
607    private LoggerListener _logger;
608
609    /** The parser to use. */
610    private PtParser _parser = null;
611
612    /** The parse tree evaluator to use. */
613    private ParseTreeEvaluator _parseTreeEvaluator = null;
614
615    /** The scope for the parser. */
616    private ParserScope _scope = null;
617
618    /** The store. */
619    private HashMap<String, Token> _store;
620
621    ///////////////////////////////////////////////////////////////////
622    ////                         inner classes                     ////
623
624    /** An empty scope to be used when parsing files. */
625    private static class EmptyScope extends ModelScope {
626
627        // FindBugs suggests making this static.
628
629        /** Return null indicating that the attribute does not exist.
630         *  @return Null.
631         */
632        @Override
633        public Token get(String name) throws IllegalActionException {
634            return null;
635        }
636
637        /** Return null indicating that the attribute does not exist.
638         *  @return Null.
639         */
640        @Override
641        public Type getType(String name) throws IllegalActionException {
642            return null;
643        }
644
645        /** Return null indicating that the attribute does not exist.
646         *  @return Null.
647         */
648        @Override
649        public ptolemy.graph.InequalityTerm getTypeTerm(String name)
650                throws IllegalActionException {
651            return null;
652        }
653
654        /** Return the list of identifiers within the scope.
655         *  @return The list of identifiers within the scope.
656         */
657        @Override
658        public Set identifierSet() {
659            return _emptySet;
660        }
661
662        private Set _emptySet = new HashSet();
663    }
664}