1 /* 2 * $Id$ 3 * 4 * Copyright (c) 1996, 2009, Oracle and/or its affiliates. All rights reserved. 5 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 6 * 7 * This code is free software; you can redistribute it and/or modify it 8 * under the terms of the GNU General Public License version 2 only, as 9 * published by the Free Software Foundation. Oracle designates this 10 * particular file as subject to the "Classpath" exception as provided 11 * by Oracle in the LICENSE file that accompanied this code. 12 * 13 * This code is distributed in the hope that it will be useful, but WITHOUT 14 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 15 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 16 * version 2 for more details (a copy is included in the LICENSE file that 17 * accompanied this code). 18 * 19 * You should have received a copy of the GNU General Public License version 20 * 2 along with this work; if not, write to the Free Software Foundation, 21 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 22 * 23 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 24 * or visit www.oracle.com if you need additional information or have any 25 * questions. 26 */ 27 package com.sun.javatest; 28 29 import java.io.File; 30 import java.util.Collection; 31 import java.util.HashMap; 32 import java.util.Iterator; 33 import java.util.Map; 34 import java.util.Set; 35 import java.util.Vector; 36 37 import com.sun.javatest.util.DynamicArray; 38 import com.sun.javatest.util.I18NResourceBundle; 39 import com.sun.javatest.util.Properties; 40 import com.sun.javatest.util.StringArray; 41 42 /** 43 * This class provides "environments", as embodied by groups of related properties. 44 * Environments have a name, and consist of those properties provided whose names 45 * do not begin with "<code>env.</code>", and in addition, those 46 * properties provided whose names begin "<code>env.</code><i>env-name</i><code>.</code>". 47 * In addition, an environment may inherit the properties of another environment 48 * by defining a property <code>env.</code><i>env-name</i><code>inherits=</code><i>inherited-env-name</i> 49 * The values of the environment's properties are split into words and various 50 * substitutions are performed. 51 * 52 * <p>The preferred way to make an environment is via a configuration interview, 53 * avoiding the use of the <code>env.</code><i>env-name</i> prefix, which is 54 * retained for backwards compatibility with older test suites that read environments 55 * from environment (.jte) files. 56 */ 57 public class TestEnvironment 58 { 59 /** 60 * This exception is used to report resolving values in an environment. 61 */ 62 public static class Fault extends Exception 63 { 64 Fault(I18NResourceBundle i18n, String s) { 65 super(i18n.getString(s)); 66 } 67 68 Fault(I18NResourceBundle i18n, String s, Object o) { 69 super(i18n.getString(s, o)); 70 } 71 72 Fault(I18NResourceBundle i18n, String s, Object[] o) { 73 super(i18n.getString(s, o)); 74 } 75 } 76 77 /** 78 * Add a default set of properties to be included when environments are 79 * created. Properties are passed in as a {@code Map<String, String>} instance. 80 * @param name a name for this collection or properties, so that the 81 * source of the properties can be identified when browing an environment 82 * @param propTable a table of properties to be included when environments 83 * are created 84 * @see #clearDefaultPropTables 85 * @throws NullPointerException if either name or propTable is null. 86 */ 87 public static synchronized void addDefaultPropTable(String name, Map<String, String> propTable) { 88 if (name == null || propTable == null) 89 throw new NullPointerException(); 90 91 //System.err.println("TEC: add default propTable " + name); 92 defaultPropTableNames = DynamicArray.append(defaultPropTableNames, name); 93 defaultPropTables = DynamicArray.append(defaultPropTables, propTable); 94 } 95 96 /** 97 * Add a default set of properties to be included when environments are 98 * created. Properties are passed in as a {@code java.util.Properties} instance. 99 * @param name a name for this collection or properties, so that the 100 * source of the properties can be identified when browing an environment 101 * @param propTable a table of properties to be included when environments 102 * are created 103 * @see #clearDefaultPropTables 104 * @throws NullPointerException if either name or propTable is null. 105 */ 106 public static synchronized void addDefaultPropTable(String name, java.util.Properties propTable) { 107 addDefaultPropTable(name, Properties.convertToStringProps(propTable)); 108 } 109 110 /** 111 * Remove all previously registered default property tables. 112 * @see #addDefaultPropTable 113 */ 114 public static synchronized void clearDefaultPropTables() { 115 defaultPropTableNames = new String[0]; 116 defaultPropTables = new Map[0]; 117 } 118 119 static String[] defaultPropTableNames = { }; 120 static Map<String, String>[] defaultPropTables = new Map[0]; 121 122 /** 123 * Construct an environment for a named group of properties. 124 * @param name The name by which to identify the group of properties 125 * for this environment 126 * @param propTable Dictionaries containing (but not limited to) the 127 * properties for this environment. 128 * @param propTableName 129 * The name of the property table, for use in diagnostics etc 130 * @throws TestEnvironment.Fault if there is an error in the table 131 * 132 */ 133 public TestEnvironment(String name, Map<String, String> propTable, String propTableName) 134 throws Fault { 135 this(name, (new Map[] {propTable}), (new String[] {propTableName})); 136 } 137 138 /** 139 * Construct an environment for a named group of properties. 140 * @param name The name by which to identify the group of properties 141 * for this environment 142 * @param propTables Dictionaries containing (but not limited to) the 143 * properties for this environment. They should be ordered 144 * so that values specified in later tables override those 145 * specified in subsequent tables. 146 * @param propTableNames 147 * The names of the property tables, for use in diagnostics etc 148 * @throws TestEnvironment.Fault if there is an error in the given tables 149 * 150 */ 151 public TestEnvironment(String name, Map[] propTables, String[] propTableNames) 152 throws Fault 153 { 154 this.name = name; 155 if (defaultPropTables != null && defaultPropTables.length > 0) { 156 propTables = DynamicArray.join(defaultPropTables, propTables); 157 propTableNames = DynamicArray.join(defaultPropTableNames, propTableNames); 158 } 159 160 // First, figure out the inheritance chain 161 Vector<String> v = new Vector<>(); 162 for (String n = name, inherit = null; n != null && n.length() > 0; n = inherit, inherit = null) { 163 if (v.contains(n)) 164 throw new Fault(i18n, "env.loop", name); 165 166 v.addElement(n); 167 String prefix = "env." + n + "."; 168 for (int i = propTables.length - 1; i >= 0 && inherit == null; i--) { 169 inherit = (String)(propTables[i].get("env." + n + ".inherits")); 170 } 171 } 172 inherits = new String[v.size()]; 173 v.copyInto(inherits); 174 175 // for this environment, and its inherited environments, scan for 176 // properties of the form env.NAME.KEY=value and add KEY=value into the 177 // environment's table 178 for (int inheritIndex = 0; inheritIndex < inherits.length; inheritIndex++) { 179 String prefix = "env." + inherits[inheritIndex] + "."; 180 for (int propIndex = propTables.length - 1; propIndex >= 0; propIndex--) { 181 Map<String, String> propTable = propTables[propIndex]; 182 for (Iterator<String> i = propTable.keySet().iterator(); i.hasNext(); ) { 183 String prop = (i.next()); 184 if (prop.startsWith(prefix)) { 185 String key = prop.substring(prefix.length()); 186 if (!table.containsKey(key)) { 187 Element elem = new Element(key, 188 (propTable.get(prop)), 189 inherits[inheritIndex], 190 propTableNames[propIndex]); 191 table.put(key, elem); 192 } 193 } 194 } 195 } 196 } 197 198 // finally, add in any top-level names (not beginning with env.) 199 for (int propIndex = propTables.length - 1; propIndex >= 0; propIndex--) { 200 Map<String, String> propTable = propTables[propIndex]; 201 for (Iterator<String> i = propTable.keySet().iterator(); i.hasNext(); ) { 202 String key = (i.next()); 203 if (!key.startsWith("env.")) { 204 if (!table.containsKey(key)) { 205 Element elem = new Element(key, 206 (propTable.get(key)), 207 null, 208 propTableNames[propIndex]); 209 table.put(key, elem); 210 } 211 } 212 } 213 } 214 } 215 216 217 /** 218 * Create a copy of the current environment. 219 * @return a copy of the current environment 220 */ 221 public TestEnvironment copy() { 222 return new TestEnvironment(this); 223 } 224 225 /** 226 * Get the distinguishing name for the properties of this environment. 227 * @return The name used to distinguish the properties of this environment 228 */ 229 public String getName() { 230 return name; 231 } 232 233 /** 234 * Get the description of this environment, as given by the "description" entry. 235 * @return the description of this environment, or null if not given 236 */ 237 public String getDescription() { 238 if (table == null || ! table.containsKey("description")) { 239 return null; 240 } 241 242 return table.get("description").getValue(); 243 } 244 245 /** 246 * Get the list of names of inherited environments, including this environment, 247 * in reverse order or inheritance (ie this one, parent, grandparent etc). 248 * @return an array containing the names of inherited environments 249 */ 250 public String[] getInherits() { 251 return inherits; 252 } 253 254 /** 255 * A backdoor method to add global properties to the environment. The value is 256 * not subject to any substitutions. 257 * @param name The name of the property to be written 258 * @param value The value of the property to be written 259 */ 260 public void put(String name, String value) { 261 // used to save values without subjecting them to any $ or # processing 262 // Note further that the main props table is considered IMMUTABLE, 263 // because it is shared amongst the clones. 264 String[] v = {value}; 265 extras.put(name, v); 266 } 267 268 /** 269 * A backdoor method to add global properties to the environment. The value is 270 * not subject to any substitutions. 271 * @param name The name of the property to be written 272 * @param value The value of the property to be written 273 */ 274 public void put(String name, String[] value) { 275 // used to save values without subjecting them to any $ or # processing 276 // Note further that the main props table is considered IMMUTABLE, 277 // because it is shared amongst the clones. 278 extras.put(name, value); 279 } 280 281 /** 282 * A backdoor method to add global properties to the environment that have a 283 * value that might be desired as both a file and a URL. The URL form is 284 * installed as a property with "URL" appended to the given property name. 285 * The values are not subject to any substitutions. 286 * URL result constructed using the following expression - 287 * f.toURI().toASCIIString(); 288 * @param name The name of the property to be written 289 * @param f The file indicating the value to be stored. 290 */ 291 public void putUrlAndFile(String name, File f) { 292 String filePath = f.getPath(); 293 294 if (filePath.endsWith(File.separator)) 295 filePath = filePath.substring(0, filePath.length() - File.separator.length()); 296 297 String url = f.toURI().toASCIIString(); 298 299 put(name, filePath); 300 301 // should upgrade to re-encode using UTF-8 perhaps 302 put(name + "URL", url); 303 } 304 305 /** 306 * 307 * @return all external global properties. 308 */ 309 public Map<String, String[]> getExtraValues() { 310 return extras; 311 } 312 313 /** 314 * Lookup a named property in the environment. 315 * @param key The name of the property to look up 316 * @return The resolved value of the property 317 * @throws TestEnvironment.Fault is thrown if there is a problem resolving the value 318 * of the property 319 * @see #resolve 320 */ 321 public String[] lookup(String key) throws Fault { 322 return lookup(key, null); 323 } 324 325 private String[] lookup(String key, Vector<String> activeKeys) throws Fault { 326 String[] v = extras.get(key); 327 if (v != null) 328 return v; 329 330 331 Element elem = table.get(key); 332 if (elem != null) { 333 cache.put(key, elem); 334 if (activeKeys == null) 335 activeKeys = new Vector<>(); 336 else if (activeKeys.contains(key)) 337 throw new Fault(i18n, "env.recursive", 338 new Object[] {key, elem.getDefinedInFile()}); 339 340 activeKeys.addElement(key); 341 try { 342 return resolve(elem.getValue(), activeKeys); 343 } 344 catch (Fault e) { 345 throw new Fault(i18n, "env.badName", 346 new Object[] {key, elem.getDefinedInFile(), e.getMessage()}); 347 } 348 finally { 349 activeKeys.removeElement(key); 350 } 351 } 352 353 return EMPTY_STRING_ARRAY; 354 } 355 356 /** 357 * Resolve a value in the environment by splitting it into words and performing 358 * various substitutions on it. White-space separates words except inside 359 * quoted strings. 360 * `<code>$<em>name</em></code>' and `<code>${<em>name</em>}</code>' are 361 * replaced by the result of calling `lookup(<em>name</em>)'. 362 * `<code>$/</code>' is replaced by the platform-specific file separator; 363 * `<code>$:</code>' is replaced by the platform-specific path separator; and 364 * `<code>$$</code>' is replaced by a single `$'. 365 * No substitutions are performed inside single-quoted strings; $ substitutions 366 * are performed in double-quoted strings. 367 * 368 * @param s The string to be resolved 369 * @return An array of strings containing the words of the argument, after 370 * substitutions have been performed. 371 * @throws TestEnvironment.Fault 372 * This is thrown if there is a problem resolving the value 373 * of the argument. 374 */ 375 public String[] resolve(String s) throws Fault { 376 return resolve(s, null); 377 } 378 379 private String[] resolve(String s, Vector<String> activeKeys) throws Fault { 380 Vector<String> v = new Vector<>(); 381 StringBuffer current = new StringBuffer(64); 382 char term = 0; 383 384 loop: 385 for (int i = 0; i < s.length(); i++) { 386 char c = s.charAt(i); 387 switch (c) { 388 case '#': 389 // # at top level introduces comment to end of line and terminates 390 //command (if found); otherwise, it goes into the current word 391 if ((!isInlineCommentsDisabled() || (i == 0 || s.charAt(i - 1) == ' ' || s.charAt(i - 1) == '\t')) && (term == 0 || term == ' ')) 392 break loop; 393 else 394 current.append(c); 395 break; 396 397 case '\'': 398 case '\"': 399 // string quotes at top level begin/end a matched pair; otherwise they 400 // are part of it 401 if (term == 0 || term == ' ') { 402 term = c; // start matched pair 403 } else if (term == c) 404 term = ' '; // end matched pair 405 else 406 current.append(c); // put character in string 407 break; 408 409 case '$': 410 // dollar introduces a name to be substituted, provided it does not 411 // appear in single quotes. Special values: $/ is File.separatorChar, 412 // $: is File.pathSeparatorChar, and $$ is $ 413 if (term != '\'') { 414 StringBuffer buf = new StringBuffer(); 415 String name = null; 416 String[] nameArgs = null; 417 try { 418 c = s.charAt(++i); 419 switch (c) { 420 case '/': 421 current.append(File.separatorChar); 422 continue loop; 423 424 case ':': 425 current.append(File.pathSeparatorChar); 426 continue loop; 427 428 case '$': 429 current.append('$'); 430 continue loop; 431 432 case '{': 433 c = s.charAt(++i); 434 while (c != ':' && c != '}') { 435 buf.append(c); 436 c = s.charAt(++i); 437 } 438 name=convertToName(resolve(buf.toString())); 439 440 // pick up optional nameArgs after embedded ':' 441 if (c == ':') { 442 buf = new StringBuffer(); 443 c = s.charAt(++i); 444 while (c != '}') { 445 buf.append(c); 446 c = s.charAt(++i); 447 } 448 nameArgs = StringArray.split(buf.toString()); 449 } 450 451 break; 452 453 default: 454 if (isNameChar(c)) { 455 while (i < s.length() && isNameChar(s.charAt(i))) { 456 buf.append(s.charAt(i++)); 457 } 458 i--; 459 } else 460 throw new Fault(i18n, "env.badExprChar", new Character(c)); 461 name = buf.toString(); 462 } 463 464 String[] val = lookup(name, activeKeys); 465 466 // apply nameArgs, if any 467 if (nameArgs != null) { 468 for (int argi = 0; argi < nameArgs.length; argi++) { 469 String arg = nameArgs[argi]; 470 if (arg.startsWith("FS=") && arg.length() == 4) 471 substituteChar(val, File.separatorChar, arg.charAt(3)); 472 else if (arg.startsWith("PS=") && arg.length() == 4) 473 substituteChar(val, File.pathSeparatorChar, arg.charAt(3)); 474 else if (arg.startsWith("MAP=")) 475 substituteMap(val, lookup("map."+arg.substring(4), activeKeys)); 476 else if (arg.equals("MAP")) 477 substituteMap(val, lookup("map", activeKeys)); 478 else 479 throw new Fault(i18n, "env.badOption", arg); 480 } 481 } 482 483 if (val != null && val.length > 0) { 484 // only start a new word if there is something to substitute 485 if (term == 0) 486 term = ' '; 487 for (int vi = 0; vi < val.length; vi++) { 488 if (vi == 0) 489 current.append(val[vi]); 490 else if (term == '"') { 491 current.append(' '); 492 current.append(val[vi]); 493 } 494 else { 495 v.addElement(current.toString()); 496 current.setLength(0); 497 current.append(val[vi]); 498 } 499 } 500 } 501 } 502 catch (IndexOutOfBoundsException e) { 503 throw new Fault(i18n, "env.badExpr"); 504 } 505 } else 506 current.append(c); 507 break; 508 509 case ' ': 510 case '\t': 511 // space or tab are skipped if not in a word; if in a word and 512 // term is space, they terminate it; otherwise they go into the 513 // current word 514 if (term != 0) { 515 if (term == ' ') { 516 v.addElement(current.toString()); 517 current.setLength(0); 518 term = 0; 519 } else 520 current.append(c); 521 } 522 break; 523 524 525 default: 526 // other characters start a word if needed, then go into the word 527 if (term == 0) 528 term = ' '; 529 current.append(c); 530 break; 531 } 532 } 533 534 // we've reached the end; if a word has been started, finish it 535 if (term != 0) 536 v.addElement(current.toString()); 537 538 String[] result = new String[v.size()]; 539 v.copyInto(result); 540 return result; 541 } 542 543 /** 544 * This is the name of system property to turn off the bugfix for inline 545 * comments. You should specify "true" value for this property to enable 546 * the bugfix, disabling the inline comments. 547 */ 548 static String DISABLE_INLINE_COMMENTS_PROPERTY = "com.sun.javatest.InlineEnvComments"; 549 550 static boolean isInlineCommentsDisabled() { 551 return Boolean.parseBoolean(System.getProperty(DISABLE_INLINE_COMMENTS_PROPERTY, "false")); 552 } 553 554 /** 555 * Check if the environment has any undefined values. These are entries containing 556 * the text VALUE_NOT_DEFINED. 557 * @return true if and only if there are any entries containing the text 558 * VALUE_NOT_DEFINED. 559 */ 560 public boolean hasUndefinedValues() { 561 for (Iterator<Element> i = elements().iterator(); i.hasNext(); ) { 562 Element entry = i.next(); 563 if (entry.value.indexOf("VALUE_NOT_DEFINED") >= 0) 564 return true; 565 } 566 return false; 567 } 568 569 private void substituteChar(String[] v, char from, char to) { 570 for (int i = 0; i < v.length; i++) 571 v[i] = v[i].replace(from, to); 572 } 573 574 private void substituteMap(String[] v, String[] map) { 575 if (map == null) 576 return; 577 578 // this algorithm is directly based on the "map" algorithm in 579 // Slave.Map, which it supercedes 580 for (int i = 0; i < v.length; i++) { 581 String word = v[i]; 582 for (int j = 0; j+1 < map.length; j+=2) { 583 String f = map[j]; 584 String t = map[j+1]; 585 for (int index = word.indexOf(f); 586 index != -1; 587 index = word.indexOf(f, index + t.length())) { 588 word = word.substring(0, index) + t + word.substring(index + f.length()); 589 } 590 } 591 v[i] = word; 592 } 593 } 594 595 private String convertToName(String[] v) { 596 String s = ""; 597 for (int i = 0; i < v.length; i++) { 598 if (i > 0) 599 s += '_'; 600 for (int j = 0; j < v[i].length(); j++) { 601 char c = v[i].charAt(j); 602 s += (isNameChar(c) ? c : '_'); 603 } 604 } 605 return s; 606 } 607 608 /** 609 * Identifies the characters recognized for $ names 610 */ 611 private static boolean isNameChar(char c) { 612 return (Character.isUpperCase(c) 613 || Character.isLowerCase(c) 614 || Character.isDigit(c) 615 || (c == '_') 616 || (c == '.')); 617 } 618 619 /** 620 * Enumerate the keys for this environment, including any inherited keys. 621 * Use `lookup' to find the values of the individual keys. 622 * 623 * @return An enumeration that yields the various keys, explicit or inherited, 624 * that are available in this environment. The keys do <em>not</em> 625 * include the `env.<em>environment-name</em>.' prefix of the corresponding 626 * property names. 627 */ 628 public Set<String> keys() { 629 return table.keySet(); 630 } 631 632 /** 633 * Get a collection containing those entries in this environment that have been 634 * referenced, either directly via lookup, or indirectly via the $ syntax in 635 * other entries. 636 * @return a collection of those entries in this environment that have been 637 * referenced. 638 * @see #resetElementsUsed 639 */ 640 public Collection<Element> elementsUsed() { 641 return cache.values(); 642 } 643 644 /** 645 * Reset the record of entries in this environment that have been referenced. 646 * @see #elementsUsed 647 */ 648 public void resetElementsUsed() { 649 cache.clear(); 650 } 651 652 /** 653 * Enumerate the elements for this environment, including any inherited elements. 654 * 655 * @return An enumeration that yields the various elements, explicit or inherited, 656 * that are available in this environment. 657 */ 658 public Collection<Element> elements() { 659 return table.values(); 660 } 661 662 663 protected TestEnvironment(TestEnvironment o) { 664 name = o.name; 665 inherits = o.inherits; 666 table = o.table; 667 extras = new HashMap<>(o.extras); 668 } 669 670 /** 671 * A class representing an entry in a test environment. 672 */ 673 public class Element { 674 /** 675 * Create an entry for a test environment. 676 * @param key The name of the entry 677 * @param value The unresolved value of the entry 678 * @param definedInEnv The name of the environment that defines this entry 679 * @param definedInFile The name of the file (or table) that defines this entry 680 */ 681 Element(String key, String value, String definedInEnv, String definedInFile) { 682 this.key = key; 683 this.value = value; 684 this.definedInEnv = definedInEnv; 685 this.definedInFile = definedInFile; 686 } 687 688 /** 689 * Get the name of this entry. 690 * @return the name of this entry 691 */ 692 public String getKey() { return key; } 693 694 /** 695 * Get the (unresolved) value of this entry. 696 * @return the (unresolved) value of this entry 697 */ 698 public String getValue() { return value; } 699 700 /** 701 * Get the name of the environment that defines this entry. 702 * @return the name of the environment that defines this entry 703 */ 704 public String getDefinedInEnv() { return definedInEnv; } 705 706 /** 707 * Get the name of the file (or table) that defines this entry. 708 * @return the name of the file (or table) that defines this entry 709 */ 710 public String getDefinedInFile() { return definedInFile; } 711 712 String key; 713 String value; 714 String definedInEnv; 715 String definedInFile; 716 } 717 718 private String name; 719 private String[] inherits; 720 private Map<String, Element> table = new HashMap<>(); 721 private Map<String, String[]> extras = new HashMap<>(); 722 private Map<String, Element> cache = new HashMap<>(); 723 724 private static final String[] EMPTY_STRING_ARRAY = {}; 725 private static I18NResourceBundle i18n = I18NResourceBundle.getBundleForClass(TestEnvironment.class); 726 }