jjg@946: /* jjg@946: * Copyright (c) 2011 Oracle and/or its affiliates. All rights reserved. jjg@946: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. jjg@946: * jjg@946: * This code is free software; you can redistribute it and/or modify it jjg@946: * under the terms of the GNU General Public License version 2 only, as jjg@946: * published by the Free Software Foundation. jjg@946: * jjg@946: * This code is distributed in the hope that it will be useful, but WITHOUT jjg@946: * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or jjg@946: * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License jjg@946: * version 2 for more details (a copy is included in the LICENSE file that jjg@946: * accompanied this code). jjg@946: * jjg@946: * You should have received a copy of the GNU General Public License version jjg@946: * 2 along with this work; if not, write to the Free Software Foundation, jjg@946: * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. jjg@946: * jjg@946: * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA jjg@946: * or visit www.oracle.com if you need additional information or have any jjg@946: * questions. jjg@946: */ jjg@946: jjg@946: /* jjg@946: * @test jjg@946: * @bug 6437138 6482554 jjg@946: * @summary JSR 199: Compiler doesn't diagnose crash in user code jjg@946: * @library ../lib jjg@946: * @build JavacTestingAbstractProcessor TestClientCodeWrapper jjg@946: * @run main TestClientCodeWrapper jjg@946: */ jjg@946: jjg@946: import java.io.*; jjg@946: import java.lang.reflect.Method; jjg@946: import java.net.URI; jjg@946: import java.util.*; jjg@946: import javax.annotation.processing.*; jjg@946: import javax.lang.model.*; jjg@946: import javax.lang.model.element.*; jjg@946: import javax.tools.*; jjg@946: import com.sun.source.util.*; jjg@946: import com.sun.tools.javac.api.*; jjg@946: import javax.tools.JavaFileObject.Kind; jjg@946: jjg@946: public class TestClientCodeWrapper extends JavacTestingAbstractProcessor { jjg@946: public static void main(String... args) throws Exception { jjg@946: new TestClientCodeWrapper().run(); jjg@946: } jjg@946: jjg@946: /** jjg@946: * Run a series of compilations, each with a different user-provided object jjg@946: * configured to throw an exception when a specific method is invoked. jjg@946: * Then, verify the exception is thrown as expected. jjg@946: * jjg@946: * Some methods are not invoked from the compiler, and are excluded from the test. jjg@946: */ jjg@946: void run() throws Exception { jjg@946: JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); jjg@946: defaultFileManager = compiler.getStandardFileManager(null, null, null); jjg@946: jjg@946: for (Method m: getMethodsExcept(JavaFileManager.class, "close", "getJavaFileForInput")) { jjg@946: test(m); jjg@946: } jjg@946: jjg@946: for (Method m: getMethodsExcept(FileObject.class, "delete")) { jjg@946: test(m); jjg@946: } jjg@946: jjg@946: for (Method m: getMethods(JavaFileObject.class)) { jjg@946: test(m); jjg@946: } jjg@946: jjg@946: for (Method m: getMethodsExcept(Processor.class, "getCompletions")) { jjg@946: test(m); jjg@946: } jjg@946: jjg@946: for (Method m: DiagnosticListener.class.getDeclaredMethods()) { jjg@946: test(m); jjg@946: } jjg@946: jjg@946: for (Method m: TaskListener.class.getDeclaredMethods()) { jjg@946: test(m); jjg@946: } jjg@946: jjg@946: if (errors > 0) jjg@946: throw new Exception(errors + " errors occurred"); jjg@946: } jjg@946: jjg@946: /** Get a sorted set of the methods declared on a class. */ jjg@946: Set getMethods(Class clazz) { jjg@946: return getMethodsExcept(clazz, new String[0]); jjg@946: } jjg@946: jjg@946: /** Get a sorted set of the methods declared on a class, excluding jjg@946: * specified methods by name. */ jjg@946: Set getMethodsExcept(Class clazz, String... exclude) { jjg@946: Set methods = new TreeSet(new Comparator() { jjg@946: public int compare(Method m1, Method m2) { jjg@946: return m1.toString().compareTo(m2.toString()); jjg@946: } jjg@946: }); jjg@946: Set e = new HashSet(Arrays.asList(exclude)); jjg@946: for (Method m: clazz.getDeclaredMethods()) { jjg@946: if (!e.contains(m.getName())) jjg@946: methods.add(m); jjg@946: } jjg@946: return methods; jjg@946: } jjg@946: jjg@946: /** jjg@946: * Test a method in a user supplied component, to verify javac's handling jjg@946: * of any exceptions thrown by that method. jjg@946: */ jjg@946: void test(Method m) throws Exception { jjg@946: testNum++; jjg@946: jjg@946: File extDirs = new File("empty-extdirs"); jjg@946: extDirs.mkdirs(); jjg@946: jjg@946: File testClasses = new File("test" + testNum); jjg@946: testClasses.mkdirs(); jjg@946: defaultFileManager.setLocation(StandardLocation.CLASS_OUTPUT, Arrays.asList(testClasses)); jjg@946: jjg@946: System.err.println("test " + testNum + ": " jjg@946: + m.getDeclaringClass().getSimpleName() + "." + m.getName()); jjg@946: jjg@946: StringWriter sw = new StringWriter(); jjg@946: PrintWriter pw = new PrintWriter(sw); jjg@946: jjg@946: List javacOptions = Arrays.asList( jjg@946: "-extdirs", extDirs.getPath(), // for use by filemanager handleOption jjg@946: "-processor", TestClientCodeWrapper.class.getName() jjg@946: ); jjg@946: jjg@946: List classes = Collections.emptyList(); jjg@946: jjg@946: JavacTool tool = JavacTool.create(); jjg@946: try { jjg@946: JavacTask task = tool.getTask(pw, jjg@946: getFileManager(m, defaultFileManager), jjg@946: getDiagnosticListener(m, pw), jjg@946: javacOptions, jjg@946: classes, jjg@946: getCompilationUnits(m)); jjg@946: jjg@946: if (isDeclaredIn(m, Processor.class)) jjg@946: task.setProcessors(getProcessors(m)); jjg@946: jjg@946: if (isDeclaredIn(m, TaskListener.class)) jjg@946: task.setTaskListener(getTaskListener(m, pw)); jjg@946: jjg@946: boolean ok = task.call(); jjg@946: error("compilation " + (ok ? "succeeded" : "failed") + " unexpectedly"); jjg@946: } catch (RuntimeException e) { jjg@946: System.err.println("caught " + e); jjg@946: if (e.getClass() == RuntimeException.class) { jjg@946: Throwable cause = e.getCause(); jjg@946: if (cause instanceof UserError) { jjg@946: String expect = m.getName(); jjg@946: String found = cause.getMessage(); jjg@946: checkEqual("exception messaqe", expect, found); jjg@946: } else { jjg@946: cause.printStackTrace(System.err); jjg@946: error("Unexpected exception: " + cause); jjg@946: } jjg@946: } else { jjg@946: e.printStackTrace(System.err); jjg@946: error("Unexpected exception: " + e); jjg@946: } jjg@946: } jjg@946: jjg@946: pw.close(); jjg@946: String out = sw.toString(); jjg@946: System.err.println(out); jjg@946: } jjg@946: jjg@946: /** Get a file manager to use for the test compilation. */ jjg@946: JavaFileManager getFileManager(Method m, JavaFileManager defaultFileManager) { jjg@946: return isDeclaredIn(m, JavaFileManager.class, FileObject.class, JavaFileObject.class) jjg@946: ? new UserFileManager(m, defaultFileManager) jjg@946: : defaultFileManager; jjg@946: } jjg@946: jjg@946: /** Get a diagnostic listener to use for the test compilation. */ jjg@946: DiagnosticListener getDiagnosticListener(Method m, PrintWriter out) { jjg@946: return isDeclaredIn(m, DiagnosticListener.class) jjg@946: ? new UserDiagnosticListener(m, out) jjg@946: : null; jjg@946: } jjg@946: jjg@946: /** Get a set of file objects to use for the test compilation. */ jjg@946: Iterable getCompilationUnits(Method m) { jjg@946: File testSrc = new File(System.getProperty("test.src")); jjg@946: File thisSrc = new File(testSrc, TestClientCodeWrapper.class.getName() + ".java"); jjg@946: Iterable files = defaultFileManager.getJavaFileObjects(thisSrc); jjg@946: if (isDeclaredIn(m, FileObject.class, JavaFileObject.class)) jjg@946: return Arrays.asList(new UserFileObject(m, files.iterator().next())); jjg@946: else jjg@946: return files; jjg@946: } jjg@946: jjg@946: /** Get a set of annotation processors to use for the test compilation. */ jjg@946: Iterable getProcessors(Method m) { jjg@946: return Arrays.asList(new UserProcessor(m)); jjg@946: } jjg@946: jjg@946: /** Get a task listener to use for the test compilation. */ jjg@946: TaskListener getTaskListener(Method m, PrintWriter out) { jjg@946: return new UserTaskListener(m, out); jjg@946: } jjg@946: jjg@946: /** Check if two values are .equal, and report an error if not. */ jjg@946: void checkEqual(String label, T expect, T found) { jjg@946: if (!expect.equals(found)) jjg@946: error("Unexpected value for " + label + ": " + found + "; expected: " + expect); jjg@946: } jjg@946: jjg@946: /** Report an error. */ jjg@946: void error(String msg) { jjg@946: System.err.println("Error: " + msg); jjg@946: errors++; jjg@946: } jjg@946: jjg@946: /** Check if a method is declared in any of a set of classes */ jjg@946: static boolean isDeclaredIn(Method m, Class... classes) { jjg@946: Class dc = m.getDeclaringClass(); jjg@946: for (Class c: classes) { jjg@946: if (c == dc) return true; jjg@946: } jjg@946: return false; jjg@946: } jjg@946: jjg@946: /** Throw an intentional error if the method has a given name. */ jjg@946: static void throwUserExceptionIfNeeded(Method m, String name) { jjg@946: if (m != null && m.getName().equals(name)) jjg@946: throw new UserError(name); jjg@946: } jjg@946: jjg@946: StandardJavaFileManager defaultFileManager; jjg@946: int testNum; jjg@946: int errors; jjg@946: jjg@946: //-------------------------------------------------------------------------- jjg@946: jjg@946: /** jjg@946: * Processor used to trigger use of methods not normally used by javac. jjg@946: */ jjg@946: @Override jjg@946: public boolean process(Set annotations, RoundEnvironment roundEnv) { jjg@946: boolean firstRound = false; jjg@946: for (Element e: roundEnv.getRootElements()) { jjg@946: if (e.getSimpleName().contentEquals(TestClientCodeWrapper.class.getSimpleName())) jjg@946: firstRound = true; jjg@946: } jjg@946: if (firstRound) { jjg@946: try { jjg@946: FileObject f1 = filer.getResource(StandardLocation.CLASS_PATH, "", jjg@946: TestClientCodeWrapper.class.getName() + ".java"); jjg@946: f1.openInputStream().close(); jjg@946: f1.openReader(false).close(); jjg@946: jjg@946: FileObject f2 = filer.createResource( jjg@946: StandardLocation.CLASS_OUTPUT, "", "f2.txt", (Element[]) null); jjg@946: f2.openOutputStream().close(); jjg@946: jjg@946: FileObject f3 = filer.createResource( jjg@946: StandardLocation.CLASS_OUTPUT, "", "f3.txt", (Element[]) null); jjg@946: f3.openWriter().close(); jjg@946: jjg@946: JavaFileObject f4 = filer.createSourceFile("f4", (Element[]) null); jjg@946: f4.openWriter().close(); jjg@946: f4.getNestingKind(); jjg@946: f4.getAccessLevel(); jjg@946: jjg@946: messager.printMessage(Diagnostic.Kind.NOTE, "informational note", jjg@946: roundEnv.getRootElements().iterator().next()); jjg@946: jjg@946: } catch (IOException e) { jjg@946: throw new UserError(e); jjg@946: } jjg@946: } jjg@946: return true; jjg@946: } jjg@946: jjg@946: //-------------------------------------------------------------------------- jjg@946: jjg@946: // jjg@946: jjg@946: static class UserError extends Error { jjg@946: private static final long serialVersionUID = 1L; jjg@946: UserError(String msg) { jjg@946: super(msg); jjg@946: } jjg@946: UserError(Throwable t) { jjg@946: super(t); jjg@946: } jjg@946: } jjg@946: jjg@946: static class UserFileManager extends ForwardingJavaFileManager { jjg@946: Method fileManagerMethod; jjg@946: Method fileObjectMethod; jjg@946: jjg@946: UserFileManager(Method m, JavaFileManager delegate) { jjg@946: super(delegate); jjg@946: if (isDeclaredIn(m, JavaFileManager.class)) { jjg@946: fileManagerMethod = m; jjg@946: } else if (isDeclaredIn(m, FileObject.class, JavaFileObject.class)) { jjg@946: fileObjectMethod = m; jjg@946: } else jjg@946: assert false; jjg@946: } jjg@946: jjg@946: @Override jjg@946: public ClassLoader getClassLoader(Location location) { jjg@946: throwUserExceptionIfNeeded(fileManagerMethod, "getClassLoader"); jjg@946: return super.getClassLoader(location); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public Iterable list(Location location, String packageName, Set kinds, boolean recurse) throws IOException { jjg@946: throwUserExceptionIfNeeded(fileManagerMethod, "list"); jjg@946: return wrap(super.list(location, packageName, kinds, recurse)); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public String inferBinaryName(Location location, JavaFileObject file) { jjg@946: throwUserExceptionIfNeeded(fileManagerMethod, "inferBinaryName"); jjg@946: return super.inferBinaryName(location, unwrap(file)); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public boolean isSameFile(FileObject a, FileObject b) { jjg@946: throwUserExceptionIfNeeded(fileManagerMethod, "isSameFile"); jjg@946: return super.isSameFile(unwrap(a), unwrap(b)); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public boolean handleOption(String current, Iterator remaining) { jjg@946: throwUserExceptionIfNeeded(fileManagerMethod, "handleOption"); jjg@946: return super.handleOption(current, remaining); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public boolean hasLocation(Location location) { jjg@946: throwUserExceptionIfNeeded(fileManagerMethod, "hasLocation"); jjg@946: return super.hasLocation(location); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public JavaFileObject getJavaFileForInput(Location location, String className, Kind kind) throws IOException { jjg@946: throwUserExceptionIfNeeded(fileManagerMethod, "getJavaFileForInput"); jjg@946: return wrap(super.getJavaFileForInput(location, className, kind)); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind, FileObject sibling) throws IOException { jjg@946: throwUserExceptionIfNeeded(fileManagerMethod, "getJavaFileForOutput"); jjg@946: return wrap(super.getJavaFileForOutput(location, className, kind, sibling)); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException { jjg@946: throwUserExceptionIfNeeded(fileManagerMethod, "getFileForInput"); jjg@946: return wrap(super.getFileForInput(location, packageName, relativeName)); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public FileObject getFileForOutput(Location location, String packageName, String relativeName, FileObject sibling) throws IOException { jjg@946: throwUserExceptionIfNeeded(fileManagerMethod, "getFileForOutput"); jjg@946: return wrap(super.getFileForOutput(location, packageName, relativeName, sibling)); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public void flush() throws IOException { jjg@946: throwUserExceptionIfNeeded(fileManagerMethod, "flush"); jjg@946: super.flush(); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public void close() throws IOException { jjg@946: throwUserExceptionIfNeeded(fileManagerMethod, "close"); jjg@946: super.close(); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public int isSupportedOption(String option) { jjg@946: throwUserExceptionIfNeeded(fileManagerMethod, "isSupportedOption"); jjg@946: return super.isSupportedOption(option); jjg@946: } jjg@946: jjg@946: public FileObject wrap(FileObject fo) { jjg@946: if (fileObjectMethod == null) jjg@946: return fo; jjg@946: return new UserFileObject(fileObjectMethod, (JavaFileObject)fo); jjg@946: } jjg@946: jjg@946: FileObject unwrap(FileObject fo) { jjg@946: if (fo instanceof UserFileObject) jjg@946: return ((UserFileObject) fo).unwrap(); jjg@946: else jjg@946: return fo; jjg@946: } jjg@946: jjg@946: public JavaFileObject wrap(JavaFileObject fo) { jjg@946: if (fileObjectMethod == null) jjg@946: return fo; jjg@946: return new UserFileObject(fileObjectMethod, fo); jjg@946: } jjg@946: jjg@946: public Iterable wrap(Iterable list) { jjg@946: List wrapped = new ArrayList(); jjg@946: for (JavaFileObject fo : list) jjg@946: wrapped.add(wrap(fo)); jjg@946: return Collections.unmodifiableList(wrapped); jjg@946: } jjg@946: jjg@946: JavaFileObject unwrap(JavaFileObject fo) { jjg@946: if (fo instanceof UserFileObject) jjg@946: return ((UserFileObject) fo).unwrap(); jjg@946: else jjg@946: return fo; jjg@946: } jjg@946: } jjg@946: jjg@946: static class UserFileObject extends ForwardingJavaFileObject { jjg@946: Method method; jjg@946: jjg@946: UserFileObject(Method m, JavaFileObject delegate) { jjg@946: super(delegate); jjg@946: assert isDeclaredIn(m, FileObject.class, JavaFileObject.class); jjg@946: this.method = m; jjg@946: } jjg@946: jjg@946: JavaFileObject unwrap() { jjg@946: return fileObject; jjg@946: } jjg@946: jjg@946: @Override jjg@946: public Kind getKind() { jjg@946: throwUserExceptionIfNeeded(method, "getKind"); jjg@946: return super.getKind(); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public boolean isNameCompatible(String simpleName, Kind kind) { jjg@946: throwUserExceptionIfNeeded(method, "isNameCompatible"); jjg@946: return super.isNameCompatible(simpleName, kind); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public NestingKind getNestingKind() { jjg@946: throwUserExceptionIfNeeded(method, "getNestingKind"); jjg@946: return super.getNestingKind(); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public Modifier getAccessLevel() { jjg@946: throwUserExceptionIfNeeded(method, "getAccessLevel"); jjg@946: return super.getAccessLevel(); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public URI toUri() { jjg@946: throwUserExceptionIfNeeded(method, "toUri"); jjg@946: return super.toUri(); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public String getName() { jjg@946: throwUserExceptionIfNeeded(method, "getName"); jjg@946: return super.getName(); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public InputStream openInputStream() throws IOException { jjg@946: throwUserExceptionIfNeeded(method, "openInputStream"); jjg@946: return super.openInputStream(); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public OutputStream openOutputStream() throws IOException { jjg@946: throwUserExceptionIfNeeded(method, "openOutputStream"); jjg@946: return super.openOutputStream(); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public Reader openReader(boolean ignoreEncodingErrors) throws IOException { jjg@946: throwUserExceptionIfNeeded(method, "openReader"); jjg@946: return super.openReader(ignoreEncodingErrors); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { jjg@946: throwUserExceptionIfNeeded(method, "getCharContent"); jjg@946: return super.getCharContent(ignoreEncodingErrors); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public Writer openWriter() throws IOException { jjg@946: throwUserExceptionIfNeeded(method, "openWriter"); jjg@946: return super.openWriter(); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public long getLastModified() { jjg@946: throwUserExceptionIfNeeded(method, "getLastModified"); jjg@946: return super.getLastModified(); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public boolean delete() { jjg@946: throwUserExceptionIfNeeded(method, "delete"); jjg@946: return super.delete(); jjg@946: } jjg@946: jjg@946: } jjg@946: jjg@946: static class UserProcessor extends JavacTestingAbstractProcessor { jjg@946: Method method; jjg@946: jjg@946: UserProcessor(Method m) { jjg@946: assert isDeclaredIn(m, Processor.class); jjg@946: method = m; jjg@946: } jjg@946: jjg@946: @Override jjg@946: public Set getSupportedOptions() { jjg@946: throwUserExceptionIfNeeded(method, "getSupportedOptions"); jjg@946: return super.getSupportedOptions(); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public Set getSupportedAnnotationTypes() { jjg@946: throwUserExceptionIfNeeded(method, "getSupportedAnnotationTypes"); jjg@946: return super.getSupportedAnnotationTypes(); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public SourceVersion getSupportedSourceVersion() { jjg@946: throwUserExceptionIfNeeded(method, "getSupportedSourceVersion"); jjg@946: return super.getSupportedSourceVersion(); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public void init(ProcessingEnvironment processingEnv) { jjg@946: throwUserExceptionIfNeeded(method, "init"); jjg@946: super.init(processingEnv); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public boolean process(Set annotations, RoundEnvironment roundEnv) { jjg@946: throwUserExceptionIfNeeded(method, "process"); jjg@946: return true; jjg@946: } jjg@946: jjg@946: @Override jjg@946: public Iterable getCompletions(Element element, AnnotationMirror annotation, ExecutableElement member, String userText) { jjg@946: throwUserExceptionIfNeeded(method, "getCompletions"); jjg@946: return super.getCompletions(element, annotation, member, userText); jjg@946: } jjg@946: } jjg@946: jjg@946: static class UserDiagnosticListener implements DiagnosticListener { jjg@946: Method method; jjg@946: PrintWriter out; jjg@946: jjg@946: UserDiagnosticListener(Method m, PrintWriter out) { jjg@946: assert isDeclaredIn(m, DiagnosticListener.class); jjg@946: this.method = m; jjg@946: this.out = out; jjg@946: } jjg@946: jjg@946: @Override jjg@946: public void report(Diagnostic diagnostic) { jjg@946: throwUserExceptionIfNeeded(method, "report"); jjg@946: out.println("report: " + diagnostic); jjg@946: } jjg@946: } jjg@946: jjg@946: static class UserTaskListener implements TaskListener { jjg@946: Method method; jjg@946: PrintWriter out; jjg@946: jjg@946: UserTaskListener(Method m, PrintWriter out) { jjg@946: assert isDeclaredIn(m, TaskListener.class); jjg@946: this.method = m; jjg@946: this.out = out; jjg@946: } jjg@946: jjg@946: @Override jjg@946: public void started(TaskEvent e) { jjg@946: throwUserExceptionIfNeeded(method, "started"); jjg@946: out.println("started: " + e); jjg@946: } jjg@946: jjg@946: @Override jjg@946: public void finished(TaskEvent e) { jjg@946: throwUserExceptionIfNeeded(method, "finished"); jjg@946: out.println("finished: " + e); jjg@946: } jjg@946: } jjg@946: jjg@946: // jjg@946: }