xref: /aosp_15_r20/external/icu/tools/cldr/cldr-to-icu/src/main/java/org/unicode/icu/tool/cldrtoicu/DebugWriter.java (revision 0e209d3975ff4a8c132096b14b0e9364a753506e)
1 package org.unicode.icu.tool.cldrtoicu;
2 
3 import static com.google.common.base.Preconditions.checkArgument;
4 import static com.google.common.base.Preconditions.checkState;
5 import static java.nio.charset.StandardCharsets.UTF_8;
6 
7 import java.io.ByteArrayInputStream;
8 import java.io.IOException;
9 import java.io.InputStream;
10 import java.io.InputStreamReader;
11 import java.nio.file.Files;
12 import java.nio.file.Path;
13 import java.nio.file.Paths;
14 import java.util.Collection;
15 import java.util.function.Function;
16 import java.util.logging.Formatter;
17 import java.util.logging.LogManager;
18 import java.util.logging.LogRecord;
19 
20 import org.unicode.cldr.api.CldrDataSupplier;
21 
22 import com.google.common.base.CharMatcher;
23 import com.google.common.base.Joiner;
24 import com.google.common.collect.ImmutableList;
25 import com.google.common.io.CharStreams;
26 
27 /**
28  * Utility to allow any "mapper" class to trivially support a main method and useful
29  * logging behaviour to help avoid the need for ad-hoc logging via {@code System.out}.
30  *
31  * <p>In most cases a mapping class can just have a {@code main} method like:
32  * <pre>{@code
33  *   // Arguments: <output-dir> [<log-level>]
34  *   public static void main(String[] args) throws IOException {
35  *       DebugWriter.writeForDebugging(args, MapperClass::process);
36  *   }
37  * }</pre>
38  *
39  * <p>Note however that running this still requires that the {@code CLDR_DIR} system
40  * property is set.
41  */
42 public final class DebugWriter {
43     private static final String PACKAGE_ROOT = "org.unicode.icu.tool.cldrtoicu";
44     private static final String PACKAGE_PREFIX = PACKAGE_ROOT + ".";
45 
46     /**
47      * Writes the IcuData generated by the given function using the default {@code CLDR_DIR}
48      * system property.
49      *
50      * <p>This is a helper method to make it easy for each mapper to have its own main
51      * method for debugging, and it should not be used directly by {@code LdmlConverter}.
52      */
writeForDebugging(String[] args, Function<CldrDataSupplier, IcuData> fn)53     public static void writeForDebugging(String[] args, Function<CldrDataSupplier, IcuData> fn)
54             throws IOException {
55         writeMultipleForDebugging(args, src -> ImmutableList.of(fn.apply(src)));
56     }
57 
58     /**
59      * Writes the IcuData generated by the given function using the default {@code CLDR_DIR}
60      * system property.
61      *
62      * <p>This is a helper method to make it easy for each mapper to have its own main
63      * method for debugging, and it should not be used directly by {@code LdmlConverter}.
64      */
writeMultipleForDebugging( String[] args, Function<CldrDataSupplier, Collection<IcuData>> fn)65     public static void writeMultipleForDebugging(
66             String[] args, Function<CldrDataSupplier, Collection<IcuData>> fn)
67             throws IOException {
68         String cldrPath = System.getProperty("CLDR_DIR", System.getenv("CLDR_DIR"));
69         checkState(cldrPath != null,
70                 "required 'CLDR_DIR' system property or environment variable not set");
71         checkArgument(args.length >= 1, "expected output directory");
72         Path outDir = Paths.get(args[0]);
73         String logLevel = (args.length == 2) ? args[1] : "OFF";
74 
75         String loggerConfig = Joiner.on("\n").join(
76                 "handlers = java.util.logging.ConsoleHandler",
77                 "java.util.logging.ConsoleHandler.level     = ALL",
78                 "java.util.logging.ConsoleHandler.encoding  = UTF-8",
79                 "java.util.logging.ConsoleHandler.formatter = " + LogFormatter.class.getName(),
80                 "",
81                 PACKAGE_ROOT + ".level  = " + logLevel);
82         LogManager.getLogManager()
83                 .readConfiguration(new ByteArrayInputStream(loggerConfig.getBytes(UTF_8)));
84 
85         Files.createDirectories(outDir);
86         CldrDataSupplier src = CldrDataSupplier.forCldrFilesIn(Paths.get(cldrPath));
87         ImmutableList<String> header = readLinesFromResource("/ldml2icu_header.txt");
88         for (IcuData icuData : fn.apply(src)) {
89             IcuTextWriter.writeToFile(icuData, outDir, header, true);
90         }
91     }
92 
readLinesFromResource(String name)93     private static ImmutableList<String> readLinesFromResource(String name) {
94         try (InputStream in = DebugWriter.class.getResourceAsStream(name)) {
95             return ImmutableList.copyOf(CharStreams.readLines(new InputStreamReader(in, UTF_8)));
96         } catch (IOException e) {
97             throw new RuntimeException("cannot read resource: " + name, e);
98         }
99     }
100 
101     // Format is "<localClass>#<plainMethod>: <message>" since this is a fairly
102     // small code base and keeping logs concise is helpful.
103     // This is only public because it has to be reflectively instantiated.
104     public static final class LogFormatter extends Formatter {
105         private static final CharMatcher SEPARATORS = CharMatcher.anyOf("$#");
106 
107         @Override
format(LogRecord logRecord)108         public String format(LogRecord logRecord) {
109             String message = String.format("%s#%s: %s\n",
110                     localClassName(logRecord.getSourceClassName()),
111                     plainMethodName(logRecord.getSourceMethodName()),
112                     logRecord.getMessage());
113             if (logRecord.getThrown() != null) {
114                 message += logRecord.getThrown() + "\n";
115             }
116             return message;
117         }
118 
119         // Since everything is in the same base package, elide that (if present).
localClassName(String className)120         private String localClassName(String className) {
121             return className.startsWith(PACKAGE_PREFIX)
122                     ? className.substring(className.lastIndexOf(".") + 1)
123                     : className;
124         }
125 
126         // Trim method names to remove things like lambda prefixes and anonymous
127         // class suffixes (these add noise to every log and aren't that useful).
plainMethodName(String methodName)128         private String plainMethodName(String methodName) {
129             if (methodName.startsWith("lambda$")) {
130                 methodName = methodName.substring("lambda$".length());
131             }
132             if (SEPARATORS.matchesAnyOf(methodName)) {
133                 methodName = methodName.substring(0, SEPARATORS.indexIn(methodName));
134             }
135             return methodName;
136         }
137     }
138 }
139