JavahTask.java revision 2734:b96d74fa60aa
1/*
2 * Copyright (c) 2002, 2014, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package com.sun.tools.javah;
27
28import java.io.File;
29import java.io.FileNotFoundException;
30import java.io.IOException;
31import java.io.OutputStream;
32import java.io.PrintWriter;
33import java.io.Writer;
34import java.nio.file.NoSuchFileException;
35import java.text.MessageFormat;
36import java.util.ArrayList;
37import java.util.Arrays;
38import java.util.Collections;
39import java.util.HashMap;
40import java.util.Iterator;
41import java.util.LinkedHashSet;
42import java.util.List;
43import java.util.Locale;
44import java.util.Map;
45import java.util.MissingResourceException;
46import java.util.ResourceBundle;
47import java.util.Set;
48
49import javax.annotation.processing.AbstractProcessor;
50import javax.annotation.processing.Messager;
51import javax.annotation.processing.ProcessingEnvironment;
52import javax.annotation.processing.RoundEnvironment;
53import javax.annotation.processing.SupportedAnnotationTypes;
54import javax.lang.model.SourceVersion;
55import javax.lang.model.element.ExecutableElement;
56import javax.lang.model.element.TypeElement;
57import javax.lang.model.element.VariableElement;
58import javax.lang.model.type.ArrayType;
59import javax.lang.model.type.DeclaredType;
60import javax.lang.model.type.TypeMirror;
61import javax.lang.model.type.TypeVisitor;
62import javax.lang.model.util.ElementFilter;
63import javax.lang.model.util.SimpleTypeVisitor9;
64import javax.lang.model.util.Types;
65import javax.tools.Diagnostic;
66import javax.tools.DiagnosticListener;
67import javax.tools.JavaCompiler;
68import javax.tools.JavaCompiler.CompilationTask;
69import javax.tools.JavaFileManager;
70import javax.tools.JavaFileObject;
71import javax.tools.StandardJavaFileManager;
72import javax.tools.StandardLocation;
73import javax.tools.ToolProvider;
74
75import com.sun.tools.javac.code.Symbol.CompletionFailure;
76import com.sun.tools.javac.main.CommandLine;
77import com.sun.tools.javac.util.DefinedBy;
78import com.sun.tools.javac.util.DefinedBy.Api;
79
80import static javax.tools.Diagnostic.Kind.*;
81
82
83/**
84 * Javah generates support files for native methods.
85 * Parse commandline options and invokes javadoc to execute those commands.
86 *
87 * <p><b>This is NOT part of any supported API.
88 * If you write code that depends on this, you do so at your own
89 * risk.  This code and its internal interfaces are subject to change
90 * or deletion without notice.</b></p>
91 *
92 * @author Sucheta Dambalkar
93 * @author Jonathan Gibbons
94 */
95public class JavahTask implements NativeHeaderTool.NativeHeaderTask {
96    public class BadArgs extends Exception {
97        private static final long serialVersionUID = 1479361270874789045L;
98        BadArgs(String key, Object... args) {
99            super(JavahTask.this.getMessage(key, args));
100            this.key = key;
101            this.args = args;
102        }
103
104        BadArgs showUsage(boolean b) {
105            showUsage = b;
106            return this;
107        }
108
109        final String key;
110        final Object[] args;
111        boolean showUsage;
112    }
113
114    static abstract class Option {
115        Option(boolean hasArg, String... aliases) {
116            this.hasArg = hasArg;
117            this.aliases = aliases;
118        }
119
120        boolean isHidden() {
121            return false;
122        }
123
124        boolean matches(String opt) {
125            for (String a: aliases) {
126                if (a.equals(opt))
127                    return true;
128            }
129            return false;
130        }
131
132        boolean ignoreRest() {
133            return false;
134        }
135
136        abstract void process(JavahTask task, String opt, String arg) throws BadArgs;
137
138        final boolean hasArg;
139        final String[] aliases;
140    }
141
142    static abstract class HiddenOption extends Option {
143        HiddenOption(boolean hasArg, String... aliases) {
144            super(hasArg, aliases);
145        }
146
147        @Override
148        boolean isHidden() {
149            return true;
150        }
151    }
152
153    static final Option[] recognizedOptions = {
154        new Option(true, "-o") {
155            void process(JavahTask task, String opt, String arg) {
156                task.ofile = new File(arg);
157            }
158        },
159
160        new Option(true, "-d") {
161            void process(JavahTask task, String opt, String arg) {
162                task.odir = new File(arg);
163            }
164        },
165
166        new HiddenOption(true, "-td") {
167            void process(JavahTask task, String opt, String arg) {
168                 // ignored; for backwards compatibility
169            }
170        },
171
172        new HiddenOption(false, "-stubs") {
173            void process(JavahTask task, String opt, String arg) {
174                 // ignored; for backwards compatibility
175            }
176        },
177
178        new Option(false, "-v", "-verbose") {
179            void process(JavahTask task, String opt, String arg) {
180                task.verbose = true;
181            }
182        },
183
184        new Option(false, "-h", "-help", "--help", "-?") {
185            void process(JavahTask task, String opt, String arg) {
186                task.help = true;
187            }
188        },
189
190        new HiddenOption(false, "-trace") {
191            void process(JavahTask task, String opt, String arg) {
192                task.trace = true;
193            }
194        },
195
196        new Option(false, "-version") {
197            void process(JavahTask task, String opt, String arg) {
198                task.version = true;
199            }
200        },
201
202        new HiddenOption(false, "-fullversion") {
203            void process(JavahTask task, String opt, String arg) {
204                task.fullVersion = true;
205            }
206        },
207
208        new Option(false, "-jni") {
209            void process(JavahTask task, String opt, String arg) {
210                task.jni = true;
211            }
212        },
213
214        new Option(false, "-force") {
215            void process(JavahTask task, String opt, String arg) {
216                task.force = true;
217            }
218        },
219
220        new HiddenOption(false, "-Xnew") {
221            void process(JavahTask task, String opt, String arg) {
222                // we're already using the new javah
223            }
224        },
225
226        new HiddenOption(false, "-llni", "-Xllni") {
227            void process(JavahTask task, String opt, String arg) {
228                task.llni = true;
229            }
230        },
231
232        new HiddenOption(false, "-llnidouble") {
233            void process(JavahTask task, String opt, String arg) {
234                task.llni = true;
235                task.doubleAlign = true;
236            }
237        },
238
239        new HiddenOption(false) {
240            boolean matches(String opt) {
241                return opt.startsWith("-XD");
242            }
243            void process(JavahTask task, String opt, String arg) {
244                task.javac_extras.add(opt);
245            }
246        },
247    };
248
249    JavahTask() {
250    }
251
252    JavahTask(Writer out,
253            JavaFileManager fileManager,
254            DiagnosticListener<? super JavaFileObject> diagnosticListener,
255            Iterable<String> options,
256            Iterable<String> classes) {
257        this();
258        this.log = getPrintWriterForWriter(out);
259        this.fileManager = fileManager;
260        this.diagnosticListener = diagnosticListener;
261
262        try {
263            handleOptions(options, false);
264        } catch (BadArgs e) {
265            throw new IllegalArgumentException(e.getMessage());
266        }
267
268        this.classes = new ArrayList<>();
269        if (classes != null) {
270            for (String classname: classes) {
271                classname.getClass(); // null-check
272                this.classes.add(classname);
273            }
274        }
275    }
276
277    public void setLocale(Locale locale) {
278        if (locale == null)
279            locale = Locale.getDefault();
280        task_locale = locale;
281    }
282
283    public void setLog(PrintWriter log) {
284        this.log = log;
285    }
286
287    public void setLog(OutputStream s) {
288        setLog(getPrintWriterForStream(s));
289    }
290
291    static PrintWriter getPrintWriterForStream(OutputStream s) {
292        return new PrintWriter(s, true);
293    }
294
295    static PrintWriter getPrintWriterForWriter(Writer w) {
296        if (w == null)
297            return getPrintWriterForStream(null);
298        else if (w instanceof PrintWriter)
299            return (PrintWriter) w;
300        else
301            return new PrintWriter(w, true);
302    }
303
304    public void setDiagnosticListener(DiagnosticListener<? super JavaFileObject> dl) {
305        diagnosticListener = dl;
306    }
307
308    public void setDiagnosticListener(OutputStream s) {
309        setDiagnosticListener(getDiagnosticListenerForStream(s));
310    }
311
312    private DiagnosticListener<JavaFileObject> getDiagnosticListenerForStream(OutputStream s) {
313        return getDiagnosticListenerForWriter(getPrintWriterForStream(s));
314    }
315
316    private DiagnosticListener<JavaFileObject> getDiagnosticListenerForWriter(Writer w) {
317        final PrintWriter pw = getPrintWriterForWriter(w);
318        return new DiagnosticListener<JavaFileObject> () {
319            @DefinedBy(Api.COMPILER)
320            public void report(Diagnostic<? extends JavaFileObject> diagnostic) {
321                if (diagnostic.getKind() == Diagnostic.Kind.ERROR) {
322                    pw.print(getMessage("err.prefix"));
323                    pw.print(" ");
324                }
325                pw.println(diagnostic.getMessage(null));
326            }
327        };
328    }
329
330    int run(String[] args) {
331        try {
332            handleOptions(args);
333            boolean ok = run();
334            return ok ? 0 : 1;
335        } catch (BadArgs e) {
336            diagnosticListener.report(createDiagnostic(e.key, e.args));
337            return 1;
338        } catch (InternalError e) {
339            diagnosticListener.report(createDiagnostic("err.internal.error", e.getMessage()));
340            return 1;
341        } catch (Util.Exit e) {
342            return e.exitValue;
343        } finally {
344            log.flush();
345        }
346    }
347
348    public void handleOptions(String[] args) throws BadArgs {
349        handleOptions(Arrays.asList(args), true);
350    }
351
352    private void handleOptions(Iterable<String> args, boolean allowClasses) throws BadArgs {
353        if (log == null) {
354            log = getPrintWriterForStream(System.out);
355            if (diagnosticListener == null)
356              diagnosticListener = getDiagnosticListenerForStream(System.err);
357        } else {
358            if (diagnosticListener == null)
359              diagnosticListener = getDiagnosticListenerForWriter(log);
360        }
361
362        if (fileManager == null)
363            fileManager = getDefaultFileManager(diagnosticListener, log);
364
365        Iterator<String> iter = expandAtArgs(args).iterator();
366        noArgs = !iter.hasNext();
367
368        while (iter.hasNext()) {
369            String arg = iter.next();
370            if (arg.startsWith("-"))
371                handleOption(arg, iter);
372            else if (allowClasses) {
373                if (classes == null)
374                    classes = new ArrayList<>();
375                classes.add(arg);
376                while (iter.hasNext())
377                    classes.add(iter.next());
378            } else
379                throw new BadArgs("err.unknown.option", arg).showUsage(true);
380        }
381
382        if ((classes == null || classes.size() == 0) &&
383                !(noArgs || help || version || fullVersion)) {
384            throw new BadArgs("err.no.classes.specified");
385        }
386
387        if (jni && llni)
388            throw new BadArgs("jni.llni.mixed");
389
390        if (odir != null && ofile != null)
391            throw new BadArgs("dir.file.mixed");
392    }
393
394    private void handleOption(String name, Iterator<String> rest) throws BadArgs {
395        for (Option o: recognizedOptions) {
396            if (o.matches(name)) {
397                if (o.hasArg) {
398                    if (rest.hasNext())
399                        o.process(this, name, rest.next());
400                    else
401                        throw new BadArgs("err.missing.arg", name).showUsage(true);
402                } else
403                    o.process(this, name, null);
404
405                if (o.ignoreRest()) {
406                    while (rest.hasNext())
407                        rest.next();
408                }
409                return;
410            }
411        }
412
413        if (fileManager.handleOption(name, rest))
414            return;
415
416        throw new BadArgs("err.unknown.option", name).showUsage(true);
417    }
418
419    private Iterable<String> expandAtArgs(Iterable<String> args) throws BadArgs {
420        try {
421            List<String> l = new ArrayList<>();
422            for (String arg: args) l.add(arg);
423            return Arrays.asList(CommandLine.parse(l.toArray(new String[l.size()])));
424        } catch (FileNotFoundException | NoSuchFileException e) {
425            throw new BadArgs("at.args.file.not.found", e.getLocalizedMessage());
426        } catch (IOException e) {
427            throw new BadArgs("at.args.io.exception", e.getLocalizedMessage());
428        }
429    }
430
431    public Boolean call() {
432        return run();
433    }
434
435    public boolean run() throws Util.Exit {
436
437        Util util = new Util(log, diagnosticListener);
438
439        if (noArgs || help) {
440            showHelp();
441            return help; // treat noArgs as an error for purposes of exit code
442        }
443
444        if (version || fullVersion) {
445            showVersion(fullVersion);
446            return true;
447        }
448
449        util.verbose = verbose;
450
451        Gen g;
452
453        if (llni)
454            g = new LLNI(doubleAlign, util);
455        else {
456//            if (stubs)
457//                throw new BadArgs("jni.no.stubs");
458            g = new JNI(util);
459        }
460
461        if (ofile != null) {
462            if (!(fileManager instanceof StandardJavaFileManager)) {
463                diagnosticListener.report(createDiagnostic("err.cant.use.option.for.fm", "-o"));
464                return false;
465            }
466            Iterable<? extends JavaFileObject> iter =
467                    ((StandardJavaFileManager) fileManager).getJavaFileObjectsFromFiles(Collections.singleton(ofile));
468            JavaFileObject fo = iter.iterator().next();
469            g.setOutFile(fo);
470        } else {
471            if (odir != null) {
472                if (!(fileManager instanceof StandardJavaFileManager)) {
473                    diagnosticListener.report(createDiagnostic("err.cant.use.option.for.fm", "-d"));
474                    return false;
475                }
476
477                if (!odir.exists())
478                    if (!odir.mkdirs())
479                        util.error("cant.create.dir", odir.toString());
480                try {
481                    ((StandardJavaFileManager) fileManager).setLocation(StandardLocation.CLASS_OUTPUT, Collections.singleton(odir));
482                } catch (IOException e) {
483                    Object msg = e.getLocalizedMessage();
484                    if (msg == null) {
485                        msg = e;
486                    }
487                    diagnosticListener.report(createDiagnostic("err.ioerror", odir, msg));
488                    return false;
489                }
490            }
491            g.setFileManager(fileManager);
492        }
493
494        /*
495         * Force set to false will turn off smarts about checking file
496         * content before writing.
497         */
498        g.setForce(force);
499
500        if (fileManager instanceof JavahFileManager)
501            ((JavahFileManager) fileManager).setSymbolFileEnabled(false);
502
503        JavaCompiler c = ToolProvider.getSystemJavaCompiler();
504        List<String> opts = new ArrayList<>();
505        opts.add("-proc:only");
506        opts.addAll(javac_extras);
507        CompilationTask t = c.getTask(log, fileManager, diagnosticListener, opts, classes, null);
508        JavahProcessor p = new JavahProcessor(g);
509        t.setProcessors(Collections.singleton(p));
510
511        boolean ok = t.call();
512        if (p.exit != null)
513            throw new Util.Exit(p.exit);
514        return ok;
515    }
516
517    private List<File> pathToFiles(String path) {
518        List<File> files = new ArrayList<>();
519        for (String f: path.split(File.pathSeparator)) {
520            if (f.length() > 0)
521                files.add(new File(f));
522        }
523        return files;
524    }
525
526    static StandardJavaFileManager getDefaultFileManager(final DiagnosticListener<? super JavaFileObject> dl, PrintWriter log) {
527        return JavahFileManager.create(dl, log);
528    }
529
530    private void showHelp() {
531        log.println(getMessage("main.usage", progname));
532        for (Option o: recognizedOptions) {
533            if (o.isHidden())
534                continue;
535            String name = o.aliases[0].substring(1); // there must always be at least one name
536            log.println(getMessage("main.opt." + name));
537        }
538        String[] fmOptions = { "-classpath", "-cp", "-bootclasspath" };
539        for (String o: fmOptions) {
540            if (fileManager.isSupportedOption(o) == -1)
541                continue;
542            String name = o.substring(1);
543            log.println(getMessage("main.opt." + name));
544        }
545        log.println(getMessage("main.usage.foot"));
546    }
547
548    private void showVersion(boolean full) {
549        log.println(version(full));
550    }
551
552    private static final String versionRBName = "com.sun.tools.javah.resources.version";
553    private static ResourceBundle versionRB;
554
555    private String version(boolean full) {
556        String msgKey = (full ? "javah.fullVersion" : "javah.version");
557        String versionKey = (full ? "full" : "release");
558        // versionKey=product:  mm.nn.oo[-milestone]
559        // versionKey=full:     mm.mm.oo[-milestone]-build
560        if (versionRB == null) {
561            try {
562                versionRB = ResourceBundle.getBundle(versionRBName);
563            } catch (MissingResourceException e) {
564                return getMessage("version.resource.missing", System.getProperty("java.version"));
565            }
566        }
567        try {
568            return getMessage(msgKey, "javah", versionRB.getString(versionKey));
569        }
570        catch (MissingResourceException e) {
571            return getMessage("version.unknown", System.getProperty("java.version"));
572        }
573    }
574
575    private Diagnostic<JavaFileObject> createDiagnostic(final String key, final Object... args) {
576        return new Diagnostic<JavaFileObject>() {
577            @DefinedBy(Api.COMPILER)
578            public Kind getKind() {
579                return Diagnostic.Kind.ERROR;
580            }
581
582            @DefinedBy(Api.COMPILER)
583            public JavaFileObject getSource() {
584                return null;
585            }
586
587            @DefinedBy(Api.COMPILER)
588            public long getPosition() {
589                return Diagnostic.NOPOS;
590            }
591
592            @DefinedBy(Api.COMPILER)
593            public long getStartPosition() {
594                return Diagnostic.NOPOS;
595            }
596
597            @DefinedBy(Api.COMPILER)
598            public long getEndPosition() {
599                return Diagnostic.NOPOS;
600            }
601
602            @DefinedBy(Api.COMPILER)
603            public long getLineNumber() {
604                return Diagnostic.NOPOS;
605            }
606
607            @DefinedBy(Api.COMPILER)
608            public long getColumnNumber() {
609                return Diagnostic.NOPOS;
610            }
611
612            @DefinedBy(Api.COMPILER)
613            public String getCode() {
614                return key;
615            }
616
617            @DefinedBy(Api.COMPILER)
618            public String getMessage(Locale locale) {
619                return JavahTask.this.getMessage(locale, key, args);
620            }
621
622        };
623    }
624
625    private String getMessage(String key, Object... args) {
626        return getMessage(task_locale, key, args);
627    }
628
629    private String getMessage(Locale locale, String key, Object... args) {
630        if (bundles == null) {
631            // could make this a HashMap<Locale,SoftReference<ResourceBundle>>
632            // and for efficiency, keep a hard reference to the bundle for the task
633            // locale
634            bundles = new HashMap<>();
635        }
636
637        if (locale == null)
638            locale = Locale.getDefault();
639
640        ResourceBundle b = bundles.get(locale);
641        if (b == null) {
642            try {
643                b = ResourceBundle.getBundle("com.sun.tools.javah.resources.l10n", locale);
644                bundles.put(locale, b);
645            } catch (MissingResourceException e) {
646                throw new InternalError("Cannot find javah resource bundle for locale " + locale, e);
647            }
648        }
649
650        try {
651            return MessageFormat.format(b.getString(key), args);
652        } catch (MissingResourceException e) {
653            return key;
654            //throw new InternalError(e, key);
655        }
656    }
657
658    File ofile;
659    File odir;
660    String bootcp;
661    String usercp;
662    List<String> classes;
663    boolean verbose;
664    boolean noArgs;
665    boolean help;
666    boolean trace;
667    boolean version;
668    boolean fullVersion;
669    boolean jni;
670    boolean llni;
671    boolean doubleAlign;
672    boolean force;
673    Set<String> javac_extras = new LinkedHashSet<>();
674
675    PrintWriter log;
676    JavaFileManager fileManager;
677    DiagnosticListener<? super JavaFileObject> diagnosticListener;
678    Locale task_locale;
679    Map<Locale, ResourceBundle> bundles;
680
681    private static final String progname = "javah";
682
683    @SupportedAnnotationTypes("*")
684    class JavahProcessor extends AbstractProcessor {
685        private Messager messager;
686
687        JavahProcessor(Gen g) {
688            this.g = g;
689        }
690
691        @Override @DefinedBy(Api.ANNOTATION_PROCESSING)
692        public SourceVersion getSupportedSourceVersion() {
693            // since this is co-bundled with javac, we can assume it supports
694            // the latest source version
695            return SourceVersion.latest();
696        }
697
698        @Override @DefinedBy(Api.ANNOTATION_PROCESSING)
699        public void init(ProcessingEnvironment pEnv) {
700            super.init(pEnv);
701            messager  = processingEnv.getMessager();
702        }
703
704        @DefinedBy(Api.ANNOTATION_PROCESSING)
705        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
706            try {
707                Set<TypeElement> classes = getAllClasses(ElementFilter.typesIn(roundEnv.getRootElements()));
708                if (classes.size() > 0) {
709                    checkMethodParameters(classes);
710                    g.setProcessingEnvironment(processingEnv);
711                    g.setClasses(classes);
712                    g.run();
713                }
714            } catch (CompletionFailure cf) {
715                messager.printMessage(ERROR, getMessage("class.not.found", cf.sym.getQualifiedName().toString()));
716            } catch (ClassNotFoundException cnfe) {
717                messager.printMessage(ERROR, getMessage("class.not.found", cnfe.getMessage()));
718            } catch (IOException ioe) {
719                messager.printMessage(ERROR, getMessage("io.exception", ioe.getMessage()));
720            } catch (Util.Exit e) {
721                exit = e;
722            }
723
724            return true;
725        }
726
727        private Set<TypeElement> getAllClasses(Set<? extends TypeElement> classes) {
728            Set<TypeElement> allClasses = new LinkedHashSet<>();
729            getAllClasses0(classes, allClasses);
730            return allClasses;
731        }
732
733        private void getAllClasses0(Iterable<? extends TypeElement> classes, Set<TypeElement> allClasses) {
734            for (TypeElement c: classes) {
735                allClasses.add(c);
736                getAllClasses0(ElementFilter.typesIn(c.getEnclosedElements()), allClasses);
737            }
738        }
739
740        // 4942232:
741        // check that classes exist for all the parameters of native methods
742        private void checkMethodParameters(Set<TypeElement> classes) {
743            Types types = processingEnv.getTypeUtils();
744            for (TypeElement te: classes) {
745                for (ExecutableElement ee: ElementFilter.methodsIn(te.getEnclosedElements())) {
746                    for (VariableElement ve: ee.getParameters()) {
747                        TypeMirror tm = ve.asType();
748                        checkMethodParametersVisitor.visit(tm, types);
749                    }
750                }
751            }
752        }
753
754        private TypeVisitor<Void,Types> checkMethodParametersVisitor =
755                new SimpleTypeVisitor9<Void,Types>() {
756            @Override @DefinedBy(Api.LANGUAGE_MODEL)
757            public Void visitArray(ArrayType t, Types types) {
758                visit(t.getComponentType(), types);
759                return null;
760            }
761            @Override @DefinedBy(Api.LANGUAGE_MODEL)
762            public Void visitDeclared(DeclaredType t, Types types) {
763                t.asElement().getKind(); // ensure class exists
764                for (TypeMirror st: types.directSupertypes(t))
765                    visit(st, types);
766                return null;
767            }
768        };
769
770        private Gen g;
771        private Util.Exit exit;
772    }
773}
774