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