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[] defaultPropTables = { };
 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 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 propTable = propTables[propIndex];
 182                 for (Iterator i = propTable.keySet().iterator(); i.hasNext(); ) {
 183                     String prop = (String) (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                                                        (String)(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 propTable = propTables[propIndex];
 201             for (Iterator i = propTable.keySet().iterator(); i.hasNext(); ) {
 202                 String key = (String) (i.next());
 203                 if (!key.startsWith("env.")) {
 204                     if (!table.containsKey(key)) {
 205                         Element elem = new Element(key,
 206                                                    (String)(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 i = elements().iterator(); i.hasNext(); ) {
 562             TestEnvironment.Element entry = (TestEnvironment.Element) (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 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 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 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 }