001/*
002 * Copyright (c) 2012, 2022, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.  Oracle designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Oracle in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
022 * or visit www.oracle.com if you need additional information or have any
023 * questions.
024 */
025
026package org.jdrupes.mdoclet.internal.doclint;
027
028import java.io.PrintWriter;
029import java.text.MessageFormat;
030import java.util.Comparator;
031import java.util.HashMap;
032import java.util.Locale;
033import java.util.Map;
034import java.util.MissingResourceException;
035import java.util.ResourceBundle;
036import java.util.Set;
037import java.util.TreeMap;
038import java.util.TreeSet;
039
040import javax.tools.Diagnostic;
041
042import org.jdrupes.mdoclet.internal.doclint.Env.AccessKind;
043
044import com.sun.source.doctree.DocTree;
045import com.sun.source.tree.Tree;
046import com.sun.tools.javac.util.StringUtils;
047
048/**
049 * Message reporting for DocLint.
050 *
051 * Options are used to filter out messages based on group and access level.
052 * Support can be enabled for accumulating statistics of different kinds of
053 * messages.
054 */
055public class Messages {
056    /**
057     * Groups used to categorize messages, so that messages in each group
058     * can be enabled or disabled via options.
059     */
060    public enum Group {
061        ACCESSIBILITY,
062        HTML,
063        MISSING,
064        SYNTAX,
065        REFERENCE;
066
067        String optName() {
068            return StringUtils.toLowerCase(name());
069        }
070
071        String notOptName() {
072            return "-" + optName();
073        }
074
075        static boolean accepts(String opt) {
076            for (Group g : values())
077                if (opt.equals(g.optName()))
078                    return true;
079            return false;
080        }
081    }
082
083    private final Options options;
084    private final Stats stats;
085
086    ResourceBundle bundle;
087    Env env;
088
089    Messages(Env env) {
090        this.env = env;
091        String name = getClass().getPackage().getName() + ".resources.doclint";
092        bundle = ResourceBundle.getBundle(name, Locale.ENGLISH);
093
094        stats = new Stats(bundle);
095        options = new Options(stats);
096    }
097
098    void error(Group group, DocTree tree, String code, Object... args) {
099        report(group, Diagnostic.Kind.ERROR, tree, code, args);
100    }
101
102    void warning(Group group, DocTree tree, String code, Object... args) {
103        report(group, Diagnostic.Kind.WARNING, tree, code, args);
104    }
105
106    void setOptions(String opts) {
107        options.setOptions(opts);
108    }
109
110    void setStatsEnabled(boolean b) {
111        stats.setEnabled(b);
112    }
113
114    boolean isEnabled(Group group, Env.AccessKind ak) {
115        return options.isEnabled(group, ak);
116    }
117
118    void reportStats(PrintWriter out) {
119        stats.report(out);
120    }
121
122    protected void report(Group group, Diagnostic.Kind dkind, DocTree tree,
123            String code, Object... args) {
124        if (options.isEnabled(group, env.currAccess)) {
125            if (dkind == Diagnostic.Kind.WARNING
126                && env.suppressWarnings(group)) {
127                return;
128            }
129            String msg
130                = (code == null) ? (String) args[0] : localize(code, args);
131            env.trees.printMessage(dkind, msg, tree,
132                env.currDocComment, env.currPath.getCompilationUnit());
133
134            stats.record(group, dkind, code);
135        }
136    }
137
138    protected void report(Group group, Diagnostic.Kind dkind, Tree tree,
139            String code, Object... args) {
140        if (options.isEnabled(group, env.currAccess)) {
141            if (dkind == Diagnostic.Kind.WARNING
142                && env.suppressWarnings(group)) {
143                return;
144            }
145            String msg = localize(code, args);
146            env.trees.printMessage(dkind, msg, tree,
147                env.currPath.getCompilationUnit());
148
149            stats.record(group, dkind, code);
150        }
151    }
152
153    String localize(String code, Object... args) {
154        String msg = bundle.getString(code);
155        if (msg == null) {
156            StringBuilder sb = new StringBuilder();
157            sb.append("message file broken: code=").append(code);
158            if (args.length > 0) {
159                sb.append(" arguments={0}");
160                for (int i = 1; i < args.length; i++) {
161                    sb.append(", {").append(i).append("}");
162                }
163            }
164            msg = sb.toString();
165        }
166        return MessageFormat.format(msg, args);
167    }
168
169    // <editor-fold defaultstate="collapsed" desc="Options">
170
171    /**
172     * Handler for (sub)options specific to message handling.
173     */
174    static class Options {
175        Map<String, Env.AccessKind> map = new HashMap<>();
176        private final Stats stats;
177
178        static boolean isValidOptions(String opts) {
179            for (String opt : opts.split(",")) {
180                if (!isValidOption(StringUtils.toLowerCase(opt.trim())))
181                    return false;
182            }
183            return true;
184        }
185
186        private static boolean isValidOption(String opt) {
187            if (opt.equals("none") || opt.equals(Stats.OPT))
188                return true;
189
190            int begin = opt.startsWith("-") ? 1 : 0;
191            int sep = opt.indexOf("/");
192            String grp = opt.substring(begin, (sep != -1) ? sep : opt.length());
193            return ((begin == 0 && grp.equals("all")) || Group.accepts(grp))
194                && ((sep == -1) || AccessKind.accepts(opt.substring(sep + 1)));
195        }
196
197        Options(Stats stats) {
198            this.stats = stats;
199        }
200
201        /** Determine if a message group is enabled for a particular access level. */
202        boolean isEnabled(Group g, Env.AccessKind access) {
203            if (map.isEmpty())
204                map.put("all", Env.AccessKind.PROTECTED);
205
206            Env.AccessKind ak = map.get(g.optName());
207            if (ak != null && access.compareTo(ak) >= 0)
208                return true;
209
210            ak = map.get(ALL);
211            if (ak != null && access.compareTo(ak) >= 0) {
212                ak = map.get(g.notOptName());
213                if (ak == null || access.compareTo(ak) > 0) // note >, not >=
214                    return true;
215            }
216
217            return false;
218        }
219
220        void setOptions(String opts) {
221            if (opts == null)
222                setOption(ALL, Env.AccessKind.PRIVATE);
223            else {
224                for (String opt : opts.split(","))
225                    setOption(StringUtils.toLowerCase(opt.trim()));
226            }
227        }
228
229        private void setOption(String arg) throws IllegalArgumentException {
230            if (arg.equals(Stats.OPT)) {
231                stats.setEnabled(true);
232                return;
233            }
234
235            int sep = arg.indexOf("/");
236            if (sep > 0) {
237                Env.AccessKind ak = Env.AccessKind
238                    .valueOf(StringUtils.toUpperCase(arg.substring(sep + 1)));
239                setOption(arg.substring(0, sep), ak);
240            } else {
241                setOption(arg, null);
242            }
243        }
244
245        private void setOption(String opt, Env.AccessKind ak) {
246            map.put(opt, (ak != null) ? ak
247                : opt.startsWith("-") ? Env.AccessKind.PUBLIC
248                    : Env.AccessKind.PRIVATE);
249        }
250
251        private static final String ALL = "all";
252    }
253
254    // </editor-fold>
255
256    // <editor-fold defaultstate="collapsed" desc="Statistics">
257
258    /**
259     * Optionally record statistics of different kinds of message.
260     */
261    static class Stats {
262        public static final String OPT = "stats";
263        public static final String NO_CODE = "";
264        final ResourceBundle bundle;
265
266        // tables only initialized if enabled
267        int[] groupCounts;
268        int[] dkindCounts;
269        Map<String, Integer> codeCounts;
270
271        Stats(ResourceBundle bundle) {
272            this.bundle = bundle;
273        }
274
275        void setEnabled(boolean b) {
276            if (b) {
277                groupCounts = new int[Messages.Group.values().length];
278                dkindCounts = new int[Diagnostic.Kind.values().length];
279                codeCounts = new HashMap<>();
280            } else {
281                groupCounts = null;
282                dkindCounts = null;
283                codeCounts = null;
284            }
285        }
286
287        void record(Messages.Group g, Diagnostic.Kind dkind, String code) {
288            if (codeCounts == null) {
289                return;
290            }
291            groupCounts[g.ordinal()]++;
292            dkindCounts[dkind.ordinal()]++;
293            if (code == null) {
294                code = NO_CODE;
295            }
296            Integer i = codeCounts.get(code);
297            codeCounts.put(code, (i == null) ? 1 : i + 1);
298        }
299
300        void report(PrintWriter out) {
301            if (codeCounts == null) {
302                return;
303            }
304            out.println("By group...");
305            Table groupTable = new Table();
306            for (Messages.Group g : Messages.Group.values()) {
307                groupTable.put(g.optName(), groupCounts[g.ordinal()]);
308            }
309            groupTable.print(out);
310            out.println();
311            out.println("By diagnostic kind...");
312            Table dkindTable = new Table();
313            for (Diagnostic.Kind k : Diagnostic.Kind.values()) {
314                dkindTable.put(StringUtils.toLowerCase(k.toString()),
315                    dkindCounts[k.ordinal()]);
316            }
317            dkindTable.print(out);
318            out.println();
319            out.println("By message kind...");
320            Table codeTable = new Table();
321            for (Map.Entry<String, Integer> e : codeCounts.entrySet()) {
322                String code = e.getKey();
323                String msg;
324                try {
325                    msg = code.equals(NO_CODE) ? "OTHER"
326                        : bundle.getString(code);
327                } catch (MissingResourceException ex) {
328                    msg = code;
329                }
330                codeTable.put(msg, e.getValue());
331            }
332            codeTable.print(out);
333        }
334
335        /**
336         * A table of (int, String) sorted by decreasing int.
337         */
338        private static class Table {
339
340            private static final Comparator<Integer> DECREASING
341                = Comparator.reverseOrder();
342            private final TreeMap<Integer, Set<String>> map
343                = new TreeMap<>(DECREASING);
344
345            void put(String label, int n) {
346                if (n == 0) {
347                    return;
348                }
349                map.computeIfAbsent(n, k -> new TreeSet<>()).add(label);
350            }
351
352            void print(PrintWriter out) {
353                for (Map.Entry<Integer, Set<String>> e : map.entrySet()) {
354                    int count = e.getKey();
355                    Set<String> labels = e.getValue();
356                    for (String label : labels) {
357                        out.println(String.format("%6d: %s", count, label));
358                    }
359                }
360            }
361        }
362    }
363    // </editor-fold>
364}