test/tools/javac/diags/Example.java

Sat, 18 Sep 2010 09:56:23 -0700

author
mcimadamore
date
Sat, 18 Sep 2010 09:56:23 -0700
changeset 689
77cc34d5e548
parent 636
a31c511db424
child 842
3da26790ccb7
permissions
-rw-r--r--

5088624: cannot find symbol message should be more intelligent
Summary: Resolve.java should keep track of all candidates found during a method resolution sweep to generate more meaningful diagnostics
Reviewed-by: jjg

jjg@610 1 /*
jjg@610 2 * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
jjg@610 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
jjg@610 4 *
jjg@610 5 * This code is free software; you can redistribute it and/or modify it
jjg@610 6 * under the terms of the GNU General Public License version 2 only, as
jjg@610 7 * published by the Free Software Foundation.
jjg@610 8 *
jjg@610 9 * This code is distributed in the hope that it will be useful, but WITHOUT
jjg@610 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
jjg@610 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
jjg@610 12 * version 2 for more details (a copy is included in the LICENSE file that
jjg@610 13 * accompanied this code).
jjg@610 14 *
jjg@610 15 * You should have received a copy of the GNU General Public License version
jjg@610 16 * 2 along with this work; if not, write to the Free Software Foundation,
jjg@610 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
jjg@610 18 *
jjg@610 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
jjg@610 20 * or visit www.oracle.com if you need additional information or have any
jjg@610 21 * questions.
jjg@610 22 */
jjg@610 23
jjg@610 24 import com.sun.tools.javac.file.JavacFileManager;
jjg@610 25 import java.io.*;
jjg@610 26 import java.util.*;
jjg@610 27 import java.util.regex.*;
jjg@610 28 import javax.tools.Diagnostic;
jjg@610 29 import javax.tools.DiagnosticCollector;
jjg@610 30 import javax.tools.JavaCompiler;
jjg@610 31 import javax.tools.JavaCompiler.CompilationTask;
jjg@610 32 import javax.tools.JavaFileObject;
jjg@610 33 import javax.tools.StandardJavaFileManager;
jjg@610 34 import javax.tools.ToolProvider;
jjg@610 35
jjg@610 36 // The following two classes are both used, but cannot be imported directly
jjg@610 37 // import com.sun.tools.javac.Main
jjg@610 38 // import com.sun.tools.javac.main.Main
jjg@610 39
jjg@610 40 import com.sun.tools.javac.util.Context;
jjg@610 41 import com.sun.tools.javac.util.JavacMessages;
jjg@610 42 import com.sun.tools.javac.util.JCDiagnostic;
jjg@610 43 import java.net.URL;
jjg@610 44 import java.net.URLClassLoader;
jjg@610 45 import javax.annotation.processing.Processor;
jjg@610 46
jjg@610 47 /**
jjg@610 48 * Class to handle example code designed to illustrate javac diagnostic messages.
jjg@610 49 */
jjg@610 50 class Example implements Comparable<Example> {
jjg@610 51 /* Create an Example from the files found at path.
jjg@610 52 * The head of the file, up to the first Java code, is scanned
jjg@610 53 * for information about the test, such as what resource keys it
jjg@610 54 * generates when run, what options are required to run it, and so on.
jjg@610 55 */
jjg@610 56 Example(File file) {
jjg@610 57 this.file = file;
jjg@610 58 declaredKeys = new TreeSet<String>();
jjg@610 59 srcFiles = new ArrayList<File>();
jjg@610 60 procFiles = new ArrayList<File>();
jjg@610 61 supportFiles = new ArrayList<File>();
jjg@610 62 srcPathFiles = new ArrayList<File>();
jjg@610 63
jjg@610 64 findFiles(file, srcFiles);
jjg@610 65 for (File f: srcFiles) {
jjg@610 66 parse(f);
jjg@610 67 }
jjg@610 68
jjg@610 69 if (infoFile == null)
jjg@610 70 throw new Error("Example " + file + " has no info file");
jjg@610 71 }
jjg@610 72
jjg@610 73 private void findFiles(File f, List<File> files) {
jjg@610 74 if (f.isDirectory()) {
jjg@610 75 for (File c: f.listFiles()) {
jjg@610 76 if (files == srcFiles && c.getName().equals("processors"))
jjg@610 77 findFiles(c, procFiles);
jjg@610 78 else if (files == srcFiles && c.getName().equals("sourcepath")) {
jjg@610 79 srcPathDir = c;
jjg@610 80 findFiles(c, srcPathFiles);
jjg@610 81 } else if (files == srcFiles && c.getName().equals("support"))
jjg@610 82 findFiles(c, supportFiles);
jjg@610 83 else
jjg@610 84 findFiles(c, files);
jjg@610 85 }
jjg@610 86 } else if (f.isFile() && f.getName().endsWith(".java")) {
jjg@610 87 files.add(f);
jjg@610 88 }
jjg@610 89 }
jjg@610 90
jjg@610 91 private void parse(File f) {
jjg@610 92 Pattern keyPat = Pattern.compile(" *// *key: *([^ ]+) *");
jjg@610 93 Pattern optPat = Pattern.compile(" *// *options: *(.*)");
jjg@610 94 Pattern runPat = Pattern.compile(" *// *run: *(.*)");
jjg@610 95 Pattern javaPat = Pattern.compile(" *@?[A-Za-z].*");
jjg@610 96 try {
jjg@610 97 String[] lines = read(f).split("[\r\n]+");
jjg@610 98 for (String line: lines) {
jjg@610 99 Matcher keyMatch = keyPat.matcher(line);
jjg@610 100 if (keyMatch.matches()) {
jjg@610 101 foundInfo(f);
jjg@610 102 declaredKeys.add(keyMatch.group(1));
jjg@610 103 continue;
jjg@610 104 }
jjg@610 105 Matcher optMatch = optPat.matcher(line);
jjg@610 106 if (optMatch.matches()) {
jjg@610 107 foundInfo(f);
jjg@610 108 options = Arrays.asList(optMatch.group(1).trim().split(" +"));
jjg@610 109 continue;
jjg@610 110 }
jjg@610 111 Matcher runMatch = runPat.matcher(line);
jjg@610 112 if (runMatch.matches()) {
jjg@610 113 foundInfo(f);
jjg@610 114 runOpts = Arrays.asList(runMatch.group(1).trim().split(" +"));
jjg@610 115 }
jjg@610 116 if (javaPat.matcher(line).matches())
jjg@610 117 break;
jjg@610 118 }
jjg@610 119 } catch (IOException e) {
jjg@610 120 throw new Error(e);
jjg@610 121 }
jjg@610 122 }
jjg@610 123
jjg@610 124 private void foundInfo(File file) {
jjg@610 125 if (infoFile != null && !infoFile.equals(file))
jjg@610 126 throw new Error("multiple info files found: " + infoFile + ", " + file);
jjg@610 127 infoFile = file;
jjg@610 128 }
jjg@610 129
jjg@610 130 String getName() {
jjg@610 131 return file.getName();
jjg@610 132 }
jjg@610 133
jjg@610 134 /**
jjg@610 135 * Get the set of resource keys that this test declares it will generate
jjg@610 136 * when it is run.
jjg@610 137 */
jjg@610 138 Set<String> getDeclaredKeys() {
jjg@610 139 return declaredKeys;
jjg@610 140 }
jjg@610 141
jjg@610 142 /**
jjg@610 143 * Get the set of resource keys that this test generates when it is run.
jjg@610 144 * The test will be run if it has not already been run.
jjg@610 145 */
jjg@610 146 Set<String> getActualKeys() {
jjg@610 147 if (actualKeys == null)
jjg@610 148 actualKeys = run(false);
jjg@610 149 return actualKeys;
jjg@610 150 }
jjg@610 151
jjg@610 152 /**
jjg@610 153 * Run the test. Information in the test header is used to determine
jjg@610 154 * how to run the test.
jjg@610 155 */
jjg@610 156 void run(PrintWriter out, boolean raw, boolean verbose) {
jjg@610 157 if (out == null)
jjg@610 158 throw new NullPointerException();
jjg@610 159 try {
jjg@610 160 run(out, null, raw, verbose);
jjg@610 161 } catch (IOException e) {
jjg@610 162 e.printStackTrace(out);
jjg@610 163 }
jjg@610 164 }
jjg@610 165
jjg@610 166 Set<String> run(boolean verbose) {
jjg@610 167 Set<String> keys = new TreeSet<String>();
jjg@610 168 try {
jjg@610 169 run(null, keys, true, verbose);
jjg@610 170 } catch (IOException e) {
jjg@610 171 e.printStackTrace();
jjg@610 172 }
jjg@610 173 return keys;
jjg@610 174 }
jjg@610 175
jjg@610 176 /**
jjg@610 177 * Run the test. Information in the test header is used to determine
jjg@610 178 * how to run the test.
jjg@610 179 */
jjg@610 180 private void run(PrintWriter out, Set<String> keys, boolean raw, boolean verbose)
jjg@610 181 throws IOException {
jjg@610 182 ClassLoader loader = getClass().getClassLoader();
jjg@610 183 if (supportFiles.size() > 0) {
jjg@610 184 File supportDir = new File(tempDir, "support");
jjg@610 185 supportDir.mkdirs();
jjg@610 186 clean(supportDir);
jjg@610 187 List<String> sOpts = Arrays.asList("-d", supportDir.getPath());
jjg@610 188 new Jsr199Compiler(verbose).run(null, null, false, sOpts, procFiles);
jjg@610 189 URLClassLoader ucl =
jjg@610 190 new URLClassLoader(new URL[] { supportDir.toURI().toURL() }, loader);
jjg@610 191 loader = ucl;
jjg@610 192 }
jjg@610 193
jjg@610 194 File classesDir = new File(tempDir, "classes");
jjg@610 195 classesDir.mkdirs();
jjg@610 196 clean(classesDir);
jjg@610 197
jjg@610 198 List<String> opts = new ArrayList<String>();
jjg@610 199 opts.add("-d");
jjg@610 200 opts.add(classesDir.getPath());
jjg@610 201 if (options != null)
jjg@610 202 opts.addAll(options);
jjg@610 203
jjg@610 204 if (procFiles.size() > 0) {
jjg@610 205 List<String> pOpts = Arrays.asList("-d", classesDir.getPath());
jjg@610 206 new Jsr199Compiler(verbose).run(null, null, false, pOpts, procFiles);
jjg@610 207 opts.add("-classpath"); // avoid using -processorpath for now
jjg@610 208 opts.add(classesDir.getPath());
jjg@610 209 createAnnotationServicesFile(classesDir, procFiles);
jjg@610 210 }
jjg@610 211
jjg@610 212 if (srcPathDir != null) {
jjg@610 213 opts.add("-sourcepath");
jjg@610 214 opts.add(srcPathDir.getPath());
jjg@610 215 }
jjg@610 216
jjg@610 217 try {
jjg@610 218 Compiler c = Compiler.getCompiler(runOpts, verbose);
jjg@610 219 c.run(out, keys, raw, opts, srcFiles);
jjg@610 220 } catch (IllegalArgumentException e) {
jjg@610 221 if (out != null) {
jjg@610 222 out.println("Invalid value for run tag: " + runOpts);
jjg@610 223 }
jjg@610 224 }
jjg@610 225 }
jjg@610 226
jjg@610 227 void createAnnotationServicesFile(File dir, List<File> procFiles) throws IOException {
jjg@610 228 File servicesDir = new File(new File(dir, "META-INF"), "services");
jjg@610 229 servicesDir.mkdirs();
jjg@610 230 File annoServices = new File(servicesDir, Processor.class.getName());
jjg@610 231 Writer out = new FileWriter(annoServices);
jjg@610 232 try {
jjg@610 233 for (File f: procFiles) {
jjg@610 234 out.write(f.getName().toString().replace(".java", ""));
jjg@610 235 }
jjg@610 236 } finally {
jjg@610 237 out.close();
jjg@610 238 }
jjg@610 239 }
jjg@610 240
jjg@610 241 @Override
jjg@610 242 public int compareTo(Example e) {
jjg@610 243 return file.compareTo(e.file);
jjg@610 244 }
jjg@610 245
jjg@610 246 @Override
jjg@610 247 public String toString() {
jjg@610 248 return file.getPath();
jjg@610 249 }
jjg@610 250
jjg@610 251 /**
jjg@610 252 * Read the contents of a file.
jjg@610 253 */
jjg@610 254 private String read(File f) throws IOException {
jjg@610 255 byte[] bytes = new byte[(int) f.length()];
jjg@610 256 DataInputStream in = new DataInputStream(new FileInputStream(f));
jjg@610 257 try {
jjg@610 258 in.readFully(bytes);
jjg@610 259 } finally {
jjg@610 260 in.close();
jjg@610 261 }
jjg@610 262 return new String(bytes);
jjg@610 263 }
jjg@610 264
jjg@610 265 /**
jjg@610 266 * Clean the contents of a directory.
jjg@610 267 */
jjg@610 268 boolean clean(File dir) {
jjg@610 269 boolean ok = true;
jjg@610 270 for (File f: dir.listFiles()) {
jjg@610 271 if (f.isDirectory())
jjg@610 272 ok &= clean(f);
jjg@610 273 ok &= f.delete();
jjg@610 274 }
jjg@610 275 return ok;
jjg@610 276 }
jjg@610 277
jjg@610 278 File file;
jjg@610 279 List<File> srcFiles;
jjg@610 280 List<File> procFiles;
jjg@610 281 File srcPathDir;
jjg@610 282 List<File> srcPathFiles;
jjg@610 283 List<File> supportFiles;
jjg@610 284 File infoFile;
jjg@610 285 private List<String> runOpts;
jjg@610 286 private List<String> options;
jjg@610 287 private Set<String> actualKeys;
jjg@610 288 private Set<String> declaredKeys;
jjg@610 289
jjg@610 290 static File tempDir = new File(System.getProperty("java.io.tmpdir"));
jjg@610 291 static void setTempDir(File tempDir) {
jjg@610 292 Example.tempDir = tempDir;
jjg@610 293 }
jjg@610 294
jjg@610 295 abstract static class Compiler {
jjg@610 296 static Compiler getCompiler(List<String> opts, boolean verbose) {
jjg@610 297 String first;
jjg@610 298 String[] rest;
jjg@610 299 if (opts == null || opts.size() == 0) {
jjg@610 300 first = null;
jjg@610 301 rest = new String[0];
jjg@610 302 } else {
jjg@610 303 first = opts.get(0);
jjg@610 304 rest = opts.subList(1, opts.size()).toArray(new String[opts.size() - 1]);
jjg@610 305 }
jjg@610 306 if (first == null || first.equals("jsr199"))
jjg@610 307 return new Jsr199Compiler(verbose, rest);
jjg@610 308 else if (first.equals("simple"))
jjg@610 309 return new SimpleCompiler(verbose);
jjg@610 310 else if (first.equals("backdoor"))
jjg@610 311 return new BackdoorCompiler(verbose);
jjg@610 312 else
jjg@610 313 throw new IllegalArgumentException(first);
jjg@610 314 }
jjg@610 315
jjg@610 316 protected Compiler(boolean verbose) {
jjg@610 317 this.verbose = verbose;
jjg@610 318 }
jjg@610 319
jjg@610 320 abstract boolean run(PrintWriter out, Set<String> keys, boolean raw,
jjg@610 321 List<String> opts, List<File> files);
jjg@610 322
jjg@610 323 void setSupportClassLoader(ClassLoader cl) {
jjg@610 324 loader = cl;
jjg@610 325 }
jjg@610 326
jjg@610 327 protected ClassLoader loader;
jjg@610 328 protected boolean verbose;
jjg@610 329 }
jjg@610 330
jjg@610 331 /**
jjg@610 332 * Compile using the JSR 199 API. The diagnostics generated are
jjg@610 333 * scanned for resource keys. Not all diagnostic keys are generated
jjg@610 334 * via the JSR 199 API -- for example, rich diagnostics are not directly
jjg@610 335 * accessible, and some diagnostics generated by the file manager may
jjg@610 336 * not be generated (for example, the JSR 199 file manager does not see
jjg@610 337 * -Xlint:path).
jjg@610 338 */
jjg@610 339 static class Jsr199Compiler extends Compiler {
jjg@610 340 List<String> fmOpts;
jjg@610 341
jjg@610 342 Jsr199Compiler(boolean verbose, String... args) {
jjg@610 343 super(verbose);
jjg@610 344 for (int i = 0; i < args.length; i++) {
jjg@610 345 String arg = args[i];
jjg@610 346 if (arg.equals("-filemanager") && (i + 1 < args.length)) {
jjg@610 347 fmOpts = Arrays.asList(args[++i].split(","));
jjg@610 348 } else
jjg@610 349 throw new IllegalArgumentException(arg);
jjg@610 350 }
jjg@610 351 }
jjg@610 352
jjg@610 353 @Override
jjg@610 354 boolean run(PrintWriter out, Set<String> keys, boolean raw, List<String> opts, List<File> files) {
jjg@610 355 if (out != null && keys != null)
jjg@610 356 throw new IllegalArgumentException();
jjg@610 357
jjg@610 358 if (verbose)
jjg@610 359 System.err.println("run_jsr199: " + opts + " " + files);
jjg@610 360
jjg@610 361 DiagnosticCollector<JavaFileObject> dc = null;
jjg@610 362 if (keys != null)
jjg@610 363 dc = new DiagnosticCollector<JavaFileObject>();
jjg@610 364
jjg@610 365 if (raw) {
jjg@610 366 List<String> newOpts = new ArrayList<String>();
jjg@610 367 newOpts.add("-XDrawDiagnostics");
jjg@610 368 newOpts.addAll(opts);
jjg@610 369 opts = newOpts;
jjg@610 370 }
jjg@610 371
jjg@610 372 JavaCompiler c = ToolProvider.getSystemJavaCompiler();
jjg@610 373
jjg@610 374 StandardJavaFileManager fm = c.getStandardFileManager(dc, null, null);
jjg@610 375 if (fmOpts != null)
jjg@610 376 fm = new FileManager(fm, fmOpts);
jjg@610 377
jjg@610 378 Iterable<? extends JavaFileObject> fos = fm.getJavaFileObjectsFromFiles(files);
jjg@610 379
jjg@610 380 CompilationTask t = c.getTask(out, fm, dc, opts, null, fos);
jjg@610 381 Boolean ok = t.call();
jjg@610 382
jjg@610 383 if (keys != null) {
jjg@610 384 for (Diagnostic<? extends JavaFileObject> d: dc.getDiagnostics()) {
jjg@610 385 scanForKeys((JCDiagnostic) d, keys);
jjg@610 386 }
jjg@610 387 }
jjg@610 388
jjg@610 389 return ok;
jjg@610 390 }
jjg@610 391
jjg@610 392 /**
jjg@610 393 * Scan a diagnostic for resource keys. This will not detect additional
jjg@610 394 * sub diagnostics that might be generated by a rich diagnostic formatter.
jjg@610 395 */
jjg@610 396 private static void scanForKeys(JCDiagnostic d, Set<String> keys) {
jjg@610 397 keys.add(d.getCode());
jjg@610 398 for (Object o: d.getArgs()) {
jjg@610 399 if (o instanceof JCDiagnostic) {
jjg@610 400 scanForKeys((JCDiagnostic) o, keys);
jjg@610 401 }
jjg@610 402 }
jjg@610 403 for (JCDiagnostic sd: d.getSubdiagnostics())
mcimadamore@689 404 scanForKeys(sd, keys);
jjg@610 405 }
jjg@610 406 }
jjg@610 407
jjg@610 408 /**
jjg@610 409 * Run the test using the standard simple entry point.
jjg@610 410 */
jjg@610 411 static class SimpleCompiler extends Compiler {
jjg@610 412 SimpleCompiler(boolean verbose) {
jjg@610 413 super(verbose);
jjg@610 414 }
jjg@610 415
jjg@610 416 @Override
jjg@610 417 boolean run(PrintWriter out, Set<String> keys, boolean raw, List<String> opts, List<File> files) {
jjg@610 418 if (out != null && keys != null)
jjg@610 419 throw new IllegalArgumentException();
jjg@610 420
jjg@610 421 if (verbose)
jjg@610 422 System.err.println("run_simple: " + opts + " " + files);
jjg@610 423
jjg@636 424 List<String> args = new ArrayList<String>();
jjg@610 425
jjg@610 426 if (keys != null || raw)
jjg@610 427 args.add("-XDrawDiagnostics");
jjg@610 428
jjg@610 429 args.addAll(opts);
jjg@610 430 for (File f: files)
jjg@610 431 args.add(f.getPath());
jjg@610 432
jjg@610 433 StringWriter sw = null;
jjg@610 434 PrintWriter pw;
jjg@610 435 if (keys != null) {
jjg@610 436 sw = new StringWriter();
jjg@610 437 pw = new PrintWriter(sw);
jjg@610 438 } else
jjg@610 439 pw = out;
jjg@610 440
jjg@610 441 int rc = com.sun.tools.javac.Main.compile(args.toArray(new String[args.size()]), pw);
jjg@610 442
jjg@610 443 if (keys != null) {
jjg@610 444 pw.close();
jjg@610 445 scanForKeys(sw.toString(), keys);
jjg@610 446 }
jjg@610 447
jjg@610 448 return (rc == 0);
jjg@610 449 }
jjg@610 450
jjg@610 451 private static void scanForKeys(String text, Set<String> keys) {
jjg@610 452 StringTokenizer st = new StringTokenizer(text, " ,\r\n():");
jjg@610 453 while (st.hasMoreElements()) {
jjg@610 454 String t = st.nextToken();
jjg@610 455 if (t.startsWith("compiler."))
jjg@610 456 keys.add(t);
jjg@610 457 }
jjg@610 458 }
jjg@610 459 }
jjg@610 460
jjg@610 461 static class BackdoorCompiler extends Compiler {
jjg@610 462 BackdoorCompiler(boolean verbose) {
jjg@610 463 super(verbose);
jjg@610 464 }
jjg@610 465
jjg@610 466 @Override
jjg@610 467 boolean run(PrintWriter out, Set<String> keys, boolean raw, List<String> opts, List<File> files) {
jjg@610 468 if (out != null && keys != null)
jjg@610 469 throw new IllegalArgumentException();
jjg@610 470
jjg@610 471 if (verbose)
jjg@610 472 System.err.println("run_simple: " + opts + " " + files);
jjg@610 473
jjg@610 474 List<String> args = new ArrayList<String>(opts);
jjg@610 475
jjg@610 476 if (out != null && raw)
jjg@610 477 args.add("-XDrawDiagnostics");
jjg@610 478
jjg@610 479 args.addAll(opts);
jjg@610 480 for (File f: files)
jjg@610 481 args.add(f.getPath());
jjg@610 482
jjg@610 483 StringWriter sw = null;
jjg@610 484 PrintWriter pw;
jjg@610 485 if (keys != null) {
jjg@610 486 sw = new StringWriter();
jjg@610 487 pw = new PrintWriter(sw);
jjg@610 488 } else
jjg@610 489 pw = out;
jjg@610 490
jjg@610 491 Context c = new Context();
jjg@610 492 JavacFileManager.preRegister(c); // can't create it until Log has been set up
jjg@610 493 MessageTracker.preRegister(c, keys);
jjg@610 494 com.sun.tools.javac.main.Main m = new com.sun.tools.javac.main.Main("javac", pw);
jjg@610 495 int rc = m.compile(args.toArray(new String[args.size()]), c);
jjg@610 496
jjg@610 497 if (keys != null) {
jjg@610 498 pw.close();
jjg@610 499 }
jjg@610 500
jjg@610 501 return (rc == 0);
jjg@610 502 }
jjg@610 503
jjg@610 504 static class MessageTracker extends JavacMessages {
mcimadamore@616 505
mcimadamore@616 506 MessageTracker(Context context) {
mcimadamore@616 507 super(context);
mcimadamore@616 508 }
mcimadamore@616 509
mcimadamore@616 510 static void preRegister(final Context c, final Set<String> keys) {
jjg@610 511 if (keys != null) {
jjg@610 512 c.put(JavacMessages.messagesKey, new Context.Factory<JavacMessages>() {
jjg@610 513 public JavacMessages make() {
mcimadamore@616 514 return new MessageTracker(c) {
jjg@610 515 @Override
jjg@610 516 public String getLocalizedString(Locale l, String key, Object... args) {
jjg@610 517 keys.add(key);
jjg@610 518 return super.getLocalizedString(l, key, args);
jjg@610 519 }
jjg@610 520 };
jjg@610 521 }
jjg@610 522 });
jjg@610 523 }
jjg@610 524 }
jjg@610 525 }
jjg@610 526
jjg@610 527 }
jjg@610 528 }

mercurial