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}