Example.java revision 3875:f94e974fe589
1219820Sjeff/*
2219820Sjeff * Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved.
3219820Sjeff * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4219820Sjeff *
5328653Shselasky * This code is free software; you can redistribute it and/or modify it
6219820Sjeff * under the terms of the GNU General Public License version 2 only, as
7219820Sjeff * published by the Free Software Foundation.
8219820Sjeff *
9219820Sjeff * This code is distributed in the hope that it will be useful, but WITHOUT
10219820Sjeff * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11219820Sjeff * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12219820Sjeff * version 2 for more details (a copy is included in the LICENSE file that
13219820Sjeff * accompanied this code).
14219820Sjeff *
15219820Sjeff * You should have received a copy of the GNU General Public License version
16219820Sjeff * 2 along with this work; if not, write to the Free Software Foundation,
17219820Sjeff * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18219820Sjeff *
19219820Sjeff * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20219820Sjeff * or visit www.oracle.com if you need additional information or have any
21219820Sjeff * questions.
22219820Sjeff */
23219820Sjeff
24219820Sjeffimport java.io.*;
25219820Sjeffimport java.net.URL;
26219820Sjeffimport java.net.URLClassLoader;
27219820Sjeffimport java.util.*;
28289644Shselaskyimport java.util.regex.*;
29289644Shselaskyimport javax.annotation.processing.Processor;
30219820Sjeffimport javax.tools.Diagnostic;
31219820Sjeffimport javax.tools.DiagnosticCollector;
32219820Sjeffimport javax.tools.JavaCompiler;
33219820Sjeffimport javax.tools.JavaCompiler.CompilationTask;
34219820Sjeffimport javax.tools.JavaFileManager;
35219820Sjeffimport javax.tools.JavaFileObject;
36270710Shselaskyimport javax.tools.StandardJavaFileManager;
37270710Shselaskyimport javax.tools.ToolProvider;
38219820Sjeff
39219820Sjeff// The following two classes are both used, but cannot be imported directly
40219820Sjeff// import com.sun.tools.javac.Main
41270710Shselasky// import com.sun.tools.javac.main.Main
42270710Shselasky
43270710Shselaskyimport com.sun.tools.javac.api.ClientCodeWrapper;
44270710Shselaskyimport com.sun.tools.javac.file.JavacFileManager;
45270710Shselaskyimport com.sun.tools.javac.main.Main;
46228443Smdfimport com.sun.tools.javac.util.Context;
47270710Shselaskyimport com.sun.tools.javac.util.JavacMessages;
48219820Sjeffimport com.sun.tools.javac.util.JCDiagnostic;
49270710Shselasky
50270710Shselasky/**
51270710Shselasky * Class to handle example code designed to illustrate javac diagnostic messages.
52270710Shselasky */
53270710Shselaskyclass Example implements Comparable<Example> {
54270710Shselasky    /* Create an Example from the files found at path.
55255932Salfred     * The head of the file, up to the first Java code, is scanned
56219820Sjeff     * for information about the test, such as what resource keys it
57219820Sjeff     * generates when run, what options are required to run it, and so on.
58219820Sjeff     */
59219820Sjeff    Example(File file) {
60328653Shselasky        this.file = file;
61328653Shselasky        declaredKeys = new TreeSet<String>();
62219820Sjeff        srcFiles = new ArrayList<File>();
63270710Shselasky        procFiles = new ArrayList<File>();
64270710Shselasky        srcPathFiles = new ArrayList<File>();
65335434Shselasky        moduleSourcePathFiles = new ArrayList<File>();
66335434Shselasky        modulePathFiles = new ArrayList<File>();
67219820Sjeff        classPathFiles = new ArrayList<File>();
68219820Sjeff        additionalFiles = new ArrayList<File>();
69219820Sjeff
70328653Shselasky        findFiles(file, srcFiles);
71328653Shselasky        for (File f: srcFiles) {
72328653Shselasky            parse(f);
73328653Shselasky        }
74328653Shselasky
75328653Shselasky        if (infoFile == null)
76328653Shselasky            throw new Error("Example " + file + " has no info file");
77328653Shselasky    }
78328653Shselasky
79328653Shselasky    private void findFiles(File f, List<File> files) {
80219820Sjeff        if (f.isDirectory()) {
81            for (File c: f.listFiles()) {
82                if (files == srcFiles && c.getName().equals("processors"))
83                    findFiles(c, procFiles);
84                else if (files == srcFiles && c.getName().equals("sourcepath")) {
85                    srcPathDir = c;
86                    findFiles(c, srcPathFiles);
87                } else if (files == srcFiles && c.getName().equals("modulesourcepath")) {
88                    moduleSourcePathDir = c;
89                    findFiles(c, moduleSourcePathFiles);
90                } else if (files == srcFiles && c.getName().equals("additional")) {
91                    additionalFilesDir = c;
92                    findFiles(c, additionalFiles);
93                } else if (files == srcFiles && c.getName().equals("modulepath")) {
94                    findFiles(c, modulePathFiles);
95                } else if (files == srcFiles && c.getName().equals("classpath")) {
96                    findFiles(c, classPathFiles);
97                } else {
98                    findFiles(c, files);
99                }
100            }
101        } else if (f.isFile()) {
102                if (f.getName().endsWith(".java")) {
103                    files.add(f);
104                } else if (f.getName().equals("modulesourcepath")) {
105                    moduleSourcePathDir = f;
106                }
107        }
108    }
109
110    private void parse(File f) {
111        Pattern keyPat = Pattern.compile(" *// *key: *([^ ]+) *");
112        Pattern optPat = Pattern.compile(" *// *options: *(.*)");
113        Pattern runPat = Pattern.compile(" *// *run: *(.*)");
114        Pattern javaPat = Pattern.compile(" *@?[A-Za-z].*");
115        try {
116            String[] lines = read(f).split("[\r\n]+");
117            for (String line: lines) {
118                Matcher keyMatch = keyPat.matcher(line);
119                if (keyMatch.matches()) {
120                    foundInfo(f);
121                    declaredKeys.add(keyMatch.group(1));
122                    continue;
123                }
124                Matcher optMatch = optPat.matcher(line);
125                if (optMatch.matches()) {
126                    foundInfo(f);
127                    options = Arrays.asList(optMatch.group(1).trim().split(" +"));
128                    continue;
129                }
130                Matcher runMatch = runPat.matcher(line);
131                if (runMatch.matches()) {
132                    foundInfo(f);
133                    runOpts = Arrays.asList(runMatch.group(1).trim().split(" +"));
134                }
135                if (javaPat.matcher(line).matches())
136                    break;
137            }
138        } catch (IOException e) {
139            throw new Error(e);
140        }
141    }
142
143    private void foundInfo(File file) {
144        if (infoFile != null && !infoFile.equals(file))
145            throw new Error("multiple info files found: " + infoFile + ", " + file);
146        infoFile = file;
147    }
148
149    String getName() {
150        return file.getName();
151    }
152
153    /**
154     * Get the set of resource keys that this test declares it will generate
155     * when it is run.
156     */
157    Set<String> getDeclaredKeys() {
158        return declaredKeys;
159    }
160
161    /**
162     * Get the set of resource keys that this test generates when it is run.
163     * The test will be run if it has not already been run.
164     */
165    Set<String> getActualKeys() {
166        if (actualKeys == null)
167            actualKeys = run(false);
168        return actualKeys;
169    }
170
171    /**
172     * Run the test.  Information in the test header is used to determine
173     * how to run the test.
174     */
175    void run(PrintWriter out, boolean raw, boolean verbose) {
176        if (out == null)
177            throw new NullPointerException();
178        try {
179            run(out, null, raw, verbose);
180        } catch (IOException e) {
181            e.printStackTrace(out);
182        }
183    }
184
185    Set<String> run(boolean verbose) {
186        Set<String> keys = new TreeSet<String>();
187        try {
188            run(null, keys, true, verbose);
189        } catch (IOException e) {
190            e.printStackTrace(System.err);
191        }
192        return keys;
193    }
194
195    /**
196     * Run the test.  Information in the test header is used to determine
197     * how to run the test.
198     */
199    private void run(PrintWriter out, Set<String> keys, boolean raw, boolean verbose)
200            throws IOException {
201        List<String> opts = new ArrayList<String>();
202        if (!modulePathFiles.isEmpty()) {
203            File modulepathDir = new File(tempDir, "modulepath");
204            modulepathDir.mkdirs();
205            clean(modulepathDir);
206            List<String> sOpts = Arrays.asList("-d", modulepathDir.getPath(),
207                                               "--module-source-path", new File(file, "modulepath").getAbsolutePath());
208            new Jsr199Compiler(verbose).run(null, null, false, sOpts, modulePathFiles);
209            opts.add("--module-path");
210            opts.add(modulepathDir.getAbsolutePath());
211        }
212
213        if (!classPathFiles.isEmpty()) {
214            File classpathDir = new File(tempDir, "classpath");
215            classpathDir.mkdirs();
216            clean(classpathDir);
217            List<String> sOpts = Arrays.asList("-d", classpathDir.getPath());
218            new Jsr199Compiler(verbose).run(null, null, false, sOpts, classPathFiles);
219            opts.add("--class-path");
220            opts.add(classpathDir.getAbsolutePath());
221        }
222
223        File classesDir = new File(tempDir, "classes");
224        classesDir.mkdirs();
225        clean(classesDir);
226
227        opts.add("-d");
228        opts.add(classesDir.getPath());
229        if (options != null)
230            opts.addAll(options);
231
232        if (procFiles.size() > 0) {
233            List<String> pOpts = new ArrayList<>(Arrays.asList("-d", classesDir.getPath()));
234
235            // hack to automatically add exports; a better solution would be to grep the
236            // source for import statements or a magic comment
237            for (File pf: procFiles) {
238                if (pf.getName().equals("CreateBadClassFile.java")) {
239                    pOpts.add("--add-exports=jdk.jdeps/com.sun.tools.classfile=ALL-UNNAMED");
240                }
241            }
242
243            new Jsr199Compiler(verbose).run(null, null, false, pOpts, procFiles);
244            opts.add("-classpath"); // avoid using -processorpath for now
245            opts.add(classesDir.getPath());
246            createAnnotationServicesFile(classesDir, procFiles);
247        } else if (options != null) {
248            int i = options.indexOf("-processor");
249            // check for built-in anno-processor(s)
250            if (i != -1 && options.get(i + 1).equals("DocCommentProcessor")) {
251                opts.add("-classpath");
252                opts.add(System.getProperty("test.classes"));
253            }
254        }
255
256        List<File> files = srcFiles;
257
258        if (srcPathDir != null) {
259            opts.add("-sourcepath");
260            opts.add(srcPathDir.getPath());
261        }
262
263        if (moduleSourcePathDir != null) {
264            opts.add("--module-source-path");
265            opts.add(moduleSourcePathDir.getPath());
266            files = moduleSourcePathFiles;
267        }
268
269        if (additionalFiles.size() > 0) {
270            List<String> sOpts = Arrays.asList("-d", classesDir.getPath());
271            new Jsr199Compiler(verbose).run(null, null, false, sOpts, additionalFiles);
272        }
273
274        try {
275            Compiler c = Compiler.getCompiler(runOpts, verbose);
276            c.run(out, keys, raw, opts, files);
277        } catch (IllegalArgumentException e) {
278            if (out != null) {
279                out.println("Invalid value for run tag: " + runOpts);
280            }
281        }
282    }
283
284    void createAnnotationServicesFile(File dir, List<File> procFiles) throws IOException {
285        File servicesDir = new File(new File(dir, "META-INF"), "services");
286        servicesDir.mkdirs();
287        File annoServices = new File(servicesDir, Processor.class.getName());
288        Writer out = new FileWriter(annoServices);
289        try {
290            for (File f: procFiles) {
291                out.write(f.getName().toString().replace(".java", ""));
292            }
293        } finally {
294            out.close();
295        }
296    }
297
298    @Override
299    public int compareTo(Example e) {
300        return file.compareTo(e.file);
301    }
302
303    @Override
304    public String toString() {
305        return file.getPath();
306    }
307
308    /**
309     * Read the contents of a file.
310     */
311    private String read(File f) throws IOException {
312        byte[] bytes = new byte[(int) f.length()];
313        DataInputStream in = new DataInputStream(new FileInputStream(f));
314        try {
315            in.readFully(bytes);
316        } finally {
317            in.close();
318        }
319        return new String(bytes);
320    }
321
322    /**
323     * Clean the contents of a directory.
324     */
325    boolean clean(File dir) {
326        boolean ok = true;
327        for (File f: dir.listFiles()) {
328            if (f.isDirectory())
329                ok &= clean(f);
330            ok &= f.delete();
331        }
332        return ok;
333    }
334
335    File file;
336    List<File> srcFiles;
337    List<File> procFiles;
338    File srcPathDir;
339    File moduleSourcePathDir;
340    File additionalFilesDir;
341    List<File> srcPathFiles;
342    List<File> moduleSourcePathFiles;
343    List<File> modulePathFiles;
344    List<File> classPathFiles;
345    List<File> additionalFiles;
346    File infoFile;
347    private List<String> runOpts;
348    private List<String> options;
349    private Set<String> actualKeys;
350    private Set<String> declaredKeys;
351
352    static File tempDir = (System.getProperty("test.src") != null) ?
353            new File(System.getProperty("user.dir")):
354            new File(System.getProperty("java.io.tmpdir"));
355
356    static void setTempDir(File tempDir) {
357        Example.tempDir = tempDir;
358    }
359
360    abstract static class Compiler {
361        interface Factory {
362            Compiler getCompiler(List<String> opts, boolean verbose);
363        }
364
365        static class DefaultFactory implements Factory {
366            public Compiler getCompiler(List<String> opts, boolean verbose) {
367                String first;
368                String[] rest;
369                    if (opts == null || opts.isEmpty()) {
370                    first = null;
371                    rest = new String[0];
372                } else {
373                    first = opts.get(0);
374                    rest = opts.subList(1, opts.size()).toArray(new String[opts.size() - 1]);
375                }
376                // For more details on the different compilers,
377                // see their respective class doc comments.
378                // See also README.examples.txt in this directory.
379                if (first == null || first.equals("jsr199"))
380                    return new Jsr199Compiler(verbose, rest);
381                else if (first.equals("simple"))
382                    return new SimpleCompiler(verbose);
383                else if (first.equals("backdoor"))
384                    return new BackdoorCompiler(verbose);
385                else if (first.equals("exec"))
386                    return new ExecCompiler(verbose, rest);
387                else
388                    throw new IllegalArgumentException(first);
389            }
390        }
391
392        static Factory factory;
393
394        static Compiler getCompiler(List<String> opts, boolean verbose) {
395            if (factory == null)
396                factory = new DefaultFactory();
397
398            return factory.getCompiler(opts, verbose);
399        }
400
401        protected Compiler(boolean verbose) {
402            this.verbose = verbose;
403        }
404
405        abstract boolean run(PrintWriter out, Set<String> keys, boolean raw,
406                List<String> opts,  List<File> files);
407
408        void setSupportClassLoader(ClassLoader cl) {
409            loader = cl;
410        }
411
412        protected void close(JavaFileManager fm) {
413            try {
414                fm.close();
415            } catch (IOException e) {
416                throw new Error(e);
417            }
418        }
419
420        protected ClassLoader loader;
421        protected boolean verbose;
422    }
423
424    /**
425     * Compile using the JSR 199 API.  The diagnostics generated are
426     * scanned for resource keys.   Not all diagnostic keys are generated
427     * via the JSR 199 API -- for example, rich diagnostics are not directly
428     * accessible, and some diagnostics generated by the file manager may
429     * not be generated (for example, the JSR 199 file manager does not see
430     * -Xlint:path).
431     */
432    static class Jsr199Compiler extends Compiler {
433        List<String> fmOpts;
434
435        Jsr199Compiler(boolean verbose, String... args) {
436            super(verbose);
437            for (int i = 0; i < args.length; i++) {
438                String arg = args[i];
439                if (arg.equals("-filemanager") && (i + 1 < args.length)) {
440                    fmOpts = Arrays.asList(args[++i].split(","));
441                } else
442                    throw new IllegalArgumentException(arg);
443            }
444        }
445
446        @Override
447        boolean run(PrintWriter out, Set<String> keys, boolean raw, List<String> opts, List<File> files) {
448            if (out != null && keys != null)
449                throw new IllegalArgumentException();
450
451            if (verbose)
452                System.err.println("run_jsr199: " + opts + " " + files);
453
454            DiagnosticCollector<JavaFileObject> dc = null;
455            if (keys != null)
456                dc = new DiagnosticCollector<JavaFileObject>();
457
458            if (raw) {
459                List<String> newOpts = new ArrayList<String>();
460                newOpts.add("-XDrawDiagnostics");
461                newOpts.addAll(opts);
462                opts = newOpts;
463            }
464
465            JavaCompiler c = ToolProvider.getSystemJavaCompiler();
466
467            StandardJavaFileManager fm = c.getStandardFileManager(dc, null, null);
468            try {
469                if (fmOpts != null)
470                    fm = new FileManager(fm, fmOpts);
471
472                Iterable<? extends JavaFileObject> fos = fm.getJavaFileObjectsFromFiles(files);
473
474                CompilationTask t = c.getTask(out, fm, dc, opts, null, fos);
475                Boolean ok = t.call();
476
477                if (keys != null) {
478                    for (Diagnostic<? extends JavaFileObject> d: dc.getDiagnostics()) {
479                        scanForKeys(unwrap(d), keys);
480                    }
481                }
482
483                return ok;
484            } finally {
485                close(fm);
486            }
487        }
488
489        /**
490         * Scan a diagnostic for resource keys.  This will not detect additional
491         * sub diagnostics that might be generated by a rich diagnostic formatter.
492         */
493        private static void scanForKeys(JCDiagnostic d, Set<String> keys) {
494            keys.add(d.getCode());
495            for (Object o: d.getArgs()) {
496                if (o instanceof JCDiagnostic) {
497                    scanForKeys((JCDiagnostic) o, keys);
498                }
499            }
500            for (JCDiagnostic sd: d.getSubdiagnostics())
501                scanForKeys(sd, keys);
502        }
503
504        private JCDiagnostic unwrap(Diagnostic<? extends JavaFileObject> diagnostic) {
505            if (diagnostic instanceof JCDiagnostic)
506                return (JCDiagnostic) diagnostic;
507            if (diagnostic instanceof ClientCodeWrapper.DiagnosticSourceUnwrapper)
508                return ((ClientCodeWrapper.DiagnosticSourceUnwrapper)diagnostic).d;
509            throw new IllegalArgumentException();
510        }
511    }
512
513    /**
514     * Run the test using the standard simple entry point.
515     */
516    static class SimpleCompiler extends Compiler {
517        SimpleCompiler(boolean verbose) {
518            super(verbose);
519        }
520
521        @Override
522        boolean run(PrintWriter out, Set<String> keys, boolean raw, List<String> opts, List<File> files) {
523            if (out != null && keys != null)
524                throw new IllegalArgumentException();
525
526            if (verbose)
527                System.err.println("run_simple: " + opts + " " + files);
528
529            List<String> args = new ArrayList<String>();
530
531            if (keys != null || raw)
532                args.add("-XDrawDiagnostics");
533
534            args.addAll(opts);
535            for (File f: files)
536                args.add(f.getPath());
537
538            StringWriter sw = null;
539            PrintWriter pw;
540            if (keys != null) {
541                sw = new StringWriter();
542                pw = new PrintWriter(sw);
543            } else
544                pw = out;
545
546            int rc = com.sun.tools.javac.Main.compile(args.toArray(new String[args.size()]), pw);
547
548            if (keys != null) {
549                pw.close();
550                scanForKeys(sw.toString(), keys);
551            }
552
553            return (rc == 0);
554        }
555
556        private static void scanForKeys(String text, Set<String> keys) {
557            StringTokenizer st = new StringTokenizer(text, " ,\r\n():");
558            while (st.hasMoreElements()) {
559                String t = st.nextToken();
560                if (t.startsWith("compiler."))
561                    keys.add(t);
562            }
563        }
564    }
565
566    /**
567     * Run the test in a separate process.
568     */
569    static class ExecCompiler extends Compiler {
570        List<String> vmOpts;
571
572        ExecCompiler(boolean verbose, String... args) {
573            super(verbose);
574            vmOpts = Arrays.asList(args);
575        }
576
577        @Override
578        boolean run(PrintWriter out, Set<String> keys, boolean raw, List<String> opts, List<File> files) {
579            if (out != null && keys != null)
580                throw new IllegalArgumentException();
581
582            if (verbose)
583                System.err.println("run_exec: " + vmOpts + " " + opts + " " + files);
584
585            List<String> args = new ArrayList<String>();
586
587            File javaHome = new File(System.getProperty("java.home"));
588            if (javaHome.getName().equals("jre"))
589                javaHome = javaHome.getParentFile();
590            File javaExe = new File(new File(javaHome, "bin"), "java");
591            args.add(javaExe.getPath());
592
593            File toolsJar = new File(new File(javaHome, "lib"), "tools.jar");
594            if (toolsJar.exists()) {
595                args.add("-classpath");
596                args.add(toolsJar.getPath());
597            }
598
599            args.addAll(vmOpts);
600            addOpts(args, "test.vm.opts");
601            addOpts(args, "test.java.opts");
602            args.add(com.sun.tools.javac.Main.class.getName());
603
604            if (keys != null || raw)
605                args.add("-XDrawDiagnostics");
606
607            args.addAll(opts);
608            for (File f: files)
609                args.add(f.getPath());
610
611            try {
612                ProcessBuilder pb = new ProcessBuilder(args);
613                pb.redirectErrorStream(true);
614                Process p = pb.start();
615                BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
616                String line;
617                while ((line = in.readLine()) != null) {
618                    if (keys != null)
619                        scanForKeys(line, keys);
620                }
621                int rc = p.waitFor();
622
623                return (rc == 0);
624            } catch (IOException | InterruptedException e) {
625                System.err.println("Exception execing javac" + e);
626                System.err.println("Command line: " + opts);
627                return false;
628            }
629        }
630
631        private static void scanForKeys(String text, Set<String> keys) {
632            StringTokenizer st = new StringTokenizer(text, " ,\r\n():");
633            while (st.hasMoreElements()) {
634                String t = st.nextToken();
635                if (t.startsWith("compiler."))
636                    keys.add(t);
637            }
638        }
639
640        private static void addOpts(List<String> args, String propName) {
641            String propValue = System.getProperty(propName);
642            if (propValue == null || propValue.isEmpty())
643                return;
644            args.addAll(Arrays.asList(propValue.split(" +", 0)));
645        }
646    }
647
648    static class BackdoorCompiler extends Compiler {
649        BackdoorCompiler(boolean verbose) {
650            super(verbose);
651        }
652
653        @Override
654        boolean run(PrintWriter out, Set<String> keys, boolean raw, List<String> opts, List<File> files) {
655            if (out != null && keys != null)
656                throw new IllegalArgumentException();
657
658            if (verbose)
659                System.err.println("run_simple: " + opts + " " + files);
660
661            List<String> args = new ArrayList<String>();
662
663            if (out != null && raw)
664                args.add("-XDrawDiagnostics");
665
666            args.addAll(opts);
667            for (File f: files)
668                args.add(f.getPath());
669
670            StringWriter sw = null;
671            PrintWriter pw;
672            if (keys != null) {
673                sw = new StringWriter();
674                pw = new PrintWriter(sw);
675            } else
676                pw = out;
677
678            Context c = new Context();
679            JavacFileManager.preRegister(c); // can't create it until Log has been set up
680            MessageTracker.preRegister(c, keys);
681
682            try {
683                Main m = new Main("javac", pw);
684                Main.Result rc = m.compile(args.toArray(new String[args.size()]), c);
685
686                if (keys != null) {
687                    pw.close();
688                }
689
690                return rc.isOK();
691            } finally {
692                close(c.get(JavaFileManager.class));
693            }
694        }
695
696        static class MessageTracker extends JavacMessages {
697
698            MessageTracker(Context context) {
699                super(context);
700            }
701
702            static void preRegister(Context c, final Set<String> keys) {
703                if (keys != null) {
704                    c.put(JavacMessages.messagesKey, new Context.Factory<JavacMessages>() {
705                        public JavacMessages make(Context c) {
706                            return new MessageTracker(c) {
707                                @Override
708                                public String getLocalizedString(Locale l, String key, Object... args) {
709                                    keys.add(key);
710                                    return super.getLocalizedString(l, key, args);
711                                }
712                            };
713                        }
714                    });
715                }
716            }
717        }
718
719    }
720}
721