1/*
2 * Copyright (c) 2015, 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.
8 *
9 * This code is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 * version 2 for more details (a copy is included in the LICENSE file that
13 * accompanied this code).
14 *
15 * You should have received a copy of the GNU General Public License version
16 * 2 along with this work; if not, write to the Free Software Foundation,
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20 * or visit www.oracle.com if you need additional information or have any
21 * questions.
22 */
23
24package propertiesparser.gen;
25
26import propertiesparser.parser.Message;
27import propertiesparser.parser.MessageFile;
28import propertiesparser.parser.MessageInfo;
29import propertiesparser.parser.MessageLine;
30import propertiesparser.parser.MessageType;
31import propertiesparser.parser.MessageType.CompoundType;
32import propertiesparser.parser.MessageType.CustomType;
33import propertiesparser.parser.MessageType.SimpleType;
34import propertiesparser.parser.MessageType.UnionType;
35import propertiesparser.parser.MessageType.Visitor;
36
37import java.io.File;
38import java.io.FileWriter;
39import java.io.IOException;
40import java.io.InputStream;
41import java.text.MessageFormat;
42import java.util.ArrayList;
43import java.util.Arrays;
44import java.util.Collections;
45import java.util.TreeSet;
46import java.util.List;
47import java.util.Map;
48import java.util.Set;
49import java.util.Properties;
50import java.util.stream.Collectors;
51import java.util.stream.Stream;
52
53public class ClassGenerator {
54
55    /** Empty string - used to generate indentation padding. */
56    private final static String INDENT_STRING = "                                                                   ";
57
58    /** Default indentation step. */
59    private final static int INDENT_WIDTH = 4;
60
61    /** File-backed property file containing basic code stubs. */
62    static Properties stubs;
63
64    static {
65        //init properties from file
66        stubs = new Properties();
67        String resourcePath = "/propertiesparser/resources/templates.properties";
68        try (InputStream in = ClassGenerator.class.getResourceAsStream(resourcePath)) {
69            stubs.load(in);
70        } catch (IOException ex) {
71            throw new AssertionError(ex);
72        }
73    }
74
75    /**
76     * Supported stubs in the property file.
77     */
78    enum StubKind {
79        TOPLEVEL("toplevel.decl"),
80        FACTORY_CLASS("nested.decl"),
81        IMPORT("import.decl"),
82        FACTORY_METHOD_DECL("factory.decl.method"),
83        FACTORY_METHOD_ARG("factory.decl.method.arg"),
84        FACTORY_METHOD_BODY("factory.decl.method.body"),
85        FACTORY_FIELD("factory.decl.field"),
86        WILDCARDS_EXTENDS("wildcards.extends"),
87        SUPPRESS_WARNINGS("suppress.warnings");
88
89        /** stub key (as it appears in the property file) */
90        String key;
91
92        StubKind(String key) {
93            this.key = key;
94        }
95
96        /**
97         * Subst a list of arguments into a given stub.
98         */
99        String format(Object... args) {
100            return MessageFormat.format((String)stubs.get(key), args);
101        }
102    }
103
104    /**
105     * Nested factory class kind. There are multiple sub-factories, one for each kind of commonly used
106     * diagnostics (i.e. error, warnings, note, fragment). An additional category is defined for
107     * those resource keys whose prefix doesn't match any predefined category.
108     */
109    enum FactoryKind {
110        ERR("err", "Error", "Errors"),
111        WARN("warn", "Warning", "Warnings"),
112        NOTE("note", "Note", "Notes"),
113        MISC("misc", "Fragment", "Fragments"),
114        OTHER(null, null, null);
115
116        /** The prefix for this factory kind (i.e. 'err'). */
117        String prefix;
118
119        /** The type of the factory method/fields in this class. */
120        String keyClazz;
121
122        /** The class name to be used for this factory. */
123        String factoryClazz;
124
125        FactoryKind(String prefix, String keyClazz, String factoryClazz) {
126            this.prefix = prefix;
127            this.keyClazz = keyClazz;
128            this.factoryClazz = factoryClazz;
129        }
130
131        /**
132         * Utility method for parsing a factory kind from a resource key prefix.
133         */
134        static FactoryKind parseFrom(String prefix) {
135            for (FactoryKind k : FactoryKind.values()) {
136                if (k.prefix == null || k.prefix.equals(prefix)) {
137                    return k;
138                }
139            }
140            return null;
141        }
142    }
143
144    /**
145     * Main entry-point: generate a Java enum-like set of nested factory classes into given output
146     * folder. The factories are populated as mandated by the comments in the input resource file.
147     */
148    public void generateFactory(MessageFile messageFile, File outDir) {
149        Map<FactoryKind, List<Map.Entry<String, Message>>> groupedEntries =
150                messageFile.messages.entrySet().stream()
151                        .collect(Collectors.groupingBy(e -> FactoryKind.parseFrom(e.getKey().split("\\.")[1])));
152        //generate nested classes
153        List<String> nestedDecls = new ArrayList<>();
154        Set<String> importedTypes = new TreeSet<>();
155        for (Map.Entry<FactoryKind, List<Map.Entry<String, Message>>> entry : groupedEntries.entrySet()) {
156            if (entry.getKey() == FactoryKind.OTHER) continue;
157            //emit members
158            String members = entry.getValue().stream()
159                    .flatMap(e -> generateFactoryMethodsAndFields(e.getKey(), e.getValue()).stream())
160                    .collect(Collectors.joining("\n\n"));
161            //emit nested class
162            String factoryDecl =
163                    StubKind.FACTORY_CLASS.format(entry.getKey().factoryClazz, indent(members, 1));
164            nestedDecls.add(indent(factoryDecl, 1));
165            //add imports
166            entry.getValue().stream().forEach(e ->
167                    importedTypes.addAll(importedTypes(e.getValue().getMessageInfo().getTypes())));
168        }
169        String clazz = StubKind.TOPLEVEL.format(
170                packageName(messageFile.file),
171                String.join("\n", generateImports(importedTypes)),
172                toplevelName(messageFile.file),
173                String.join("\n", nestedDecls));
174        try (FileWriter fw = new FileWriter(new File(outDir, toplevelName(messageFile.file) + ".java"))) {
175            fw.append(clazz);
176        } catch (Throwable ex) {
177            throw new AssertionError(ex);
178        }
179    }
180
181    /**
182     * Indent a string to a given level.
183     */
184    String indent(String s, int level) {
185        return Stream.of(s.split("\n"))
186                .map(sub -> INDENT_STRING.substring(0, level * INDENT_WIDTH) + sub)
187                .collect(Collectors.joining("\n"));
188    }
189
190    /**
191     * Retrieve package part of given file object.
192     */
193    String packageName(File file) {
194        String path = file.getAbsolutePath();
195        int begin = path.lastIndexOf(File.separatorChar + "com" + File.separatorChar);
196        String packagePath = path.substring(begin + 1, path.lastIndexOf(File.separatorChar));
197        String packageName =  packagePath.replace(File.separatorChar, '.');
198        return packageName;
199    }
200
201    /**
202     * Form the name of the toplevel factory class.
203     */
204    public static String toplevelName(File file) {
205        return Stream.of(file.getName().split("\\."))
206                .map(s -> Character.toUpperCase(s.charAt(0)) + s.substring(1))
207                .collect(Collectors.joining(""));
208    }
209
210    /**
211     * Generate a list of import declarations given a set of imported types.
212     */
213    List<String> generateImports(Set<String> importedTypes) {
214        List<String> importDecls = new ArrayList<>();
215        for (String it : importedTypes) {
216            importDecls.add(StubKind.IMPORT.format(it));
217        }
218        return importDecls;
219    }
220
221    /**
222     * Generate a list of factory methods/fields to be added to a given factory nested class.
223     */
224    List<String> generateFactoryMethodsAndFields(String key, Message msg) {
225        MessageInfo msgInfo = msg.getMessageInfo();
226        List<MessageLine> lines = msg.getLines(false);
227        String javadoc = lines.stream()
228                .filter(ml -> !ml.isInfo() && !ml.isEmptyOrComment())
229                .map(ml -> ml.text)
230                .collect(Collectors.joining("\n *"));
231        String[] keyParts = key.split("\\.");
232        FactoryKind k = FactoryKind.parseFrom(keyParts[1]);
233        String factoryName = factoryName(key);
234        if (msgInfo.getTypes().isEmpty()) {
235            //generate field
236            String factoryField = StubKind.FACTORY_FIELD.format(k.keyClazz, factoryName,
237                    "\"" + keyParts[0] + "\"",
238                    "\"" + Stream.of(keyParts).skip(2).collect(Collectors.joining(".")) + "\"",
239                    javadoc);
240            return Collections.singletonList(factoryField);
241        } else {
242            //generate method
243            List<String> factoryMethods = new ArrayList<>();
244            for (List<MessageType> msgTypes : normalizeTypes(0, msgInfo.getTypes())) {
245                List<String> types = generateTypes(msgTypes);
246                List<String> argNames = argNames(types.size());
247                String suppressionString = needsSuppressWarnings(msgTypes) ?
248                        StubKind.SUPPRESS_WARNINGS.format() : "";
249                String factoryMethod = StubKind.FACTORY_METHOD_DECL.format(suppressionString, k.keyClazz,
250                        factoryName, argDecls(types, argNames).stream().collect(Collectors.joining(", ")),
251                        indent(StubKind.FACTORY_METHOD_BODY.format(k.keyClazz,
252                                "\"" + keyParts[0] + "\"",
253                                "\"" + Stream.of(keyParts).skip(2).collect(Collectors.joining(".")) + "\"",
254                                argNames.stream().collect(Collectors.joining(", "))), 1),
255                        javadoc);
256                factoryMethods.add(factoryMethod);
257            }
258            return factoryMethods;
259        }
260    }
261
262    /**
263     * Form the name of a factory method/field given a resource key.
264     */
265    String factoryName(String key) {
266        return Stream.of(key.split("[\\.-]"))
267                .skip(2)
268                .map(s -> Character.toUpperCase(s.charAt(0)) + s.substring(1))
269                .collect(Collectors.joining(""));
270    }
271
272    /**
273     * Generate a formal parameter list given a list of types and names.
274     */
275    List<String> argDecls(List<String> types, List<String> args) {
276        List<String> argNames = new ArrayList<>();
277        for (int i = 0 ; i < types.size() ; i++) {
278            argNames.add(types.get(i) + " " + args.get(i));
279        }
280        return argNames;
281    }
282
283    /**
284     * Generate a list of formal parameter names given a size.
285     */
286    List<String> argNames(int size) {
287        List<String> argNames = new ArrayList<>();
288        for (int i = 0 ; i < size ; i++) {
289            argNames.add(StubKind.FACTORY_METHOD_ARG.format(i));
290        }
291        return argNames;
292    }
293
294    /**
295     * Convert a (normalized) parsed type into a string-based representation of some Java type.
296     */
297    List<String> generateTypes(List<MessageType> msgTypes) {
298        return msgTypes.stream().map(t -> t.accept(stringVisitor, null)).collect(Collectors.toList());
299    }
300    //where
301        Visitor<String, Void> stringVisitor = new Visitor<String, Void>() {
302            @Override
303            public String visitCustomType(CustomType t, Void aVoid) {
304                String customType = t.typeString;
305                return customType.substring(customType.lastIndexOf('.') + 1);
306            }
307
308            @Override
309            public String visitSimpleType(SimpleType t, Void aVoid) {
310                return t.clazz;
311            }
312
313            @Override
314            public String visitCompoundType(CompoundType t, Void aVoid) {
315                return StubKind.WILDCARDS_EXTENDS.format(t.kind.clazz.clazz,
316                        t.elemtype.accept(this, null));
317            }
318
319            @Override
320            public String visitUnionType(UnionType t, Void aVoid) {
321                throw new AssertionError("Union types should have been denormalized!");
322            }
323        };
324
325    /**
326     * See if any of the parsed types in the given list needs warning suppression.
327     */
328    boolean needsSuppressWarnings(List<MessageType> msgTypes) {
329        return msgTypes.stream().anyMatch(t -> t.accept(suppressWarningsVisitor, null));
330    }
331    //where
332    Visitor<Boolean, Void> suppressWarningsVisitor = new Visitor<Boolean, Void>() {
333        @Override
334        public Boolean visitCustomType(CustomType t, Void aVoid) {
335            //play safe
336            return true;
337        }
338        @Override
339        public Boolean visitSimpleType(SimpleType t, Void aVoid) {
340            switch (t) {
341                case LIST:
342                case SET:
343                    return true;
344                default:
345                    return false;
346            }
347        }
348
349        @Override
350        public Boolean visitCompoundType(CompoundType t, Void aVoid) {
351            return t.elemtype.accept(this, null);
352        }
353
354        @Override
355        public Boolean visitUnionType(UnionType t, Void aVoid) {
356            return needsSuppressWarnings(Arrays.asList(t.choices));
357        }
358    };
359
360    /**
361     * Retrieve a list of types that need to be imported, so that the factory body can refer
362     * to the types in the given list using simple names.
363     */
364    Set<String> importedTypes(List<MessageType> msgTypes) {
365        Set<String> imports = new TreeSet<>();
366        msgTypes.forEach(t -> t.accept(importVisitor, imports));
367        return imports;
368    }
369    //where
370    Visitor<Void, Set<String>> importVisitor = new Visitor<Void, Set<String>>() {
371        @Override
372        public Void visitCustomType(CustomType t, Set<String> imports) {
373            imports.add(t.typeString);
374            return null;
375        }
376
377        @Override
378        public Void visitSimpleType(SimpleType t, Set<String> imports) {
379            if (t.qualifier != null) {
380                imports.add(t.qualifier + "." + t.clazz);
381            }
382            return null;
383        }
384
385        @Override
386        public Void visitCompoundType(CompoundType t, Set<String> imports) {
387            visitSimpleType(t.kind.clazz, imports);
388            t.elemtype.accept(this, imports);
389            return null;
390        }
391
392        @Override
393        public Void visitUnionType(UnionType t, Set<String> imports) {
394            Stream.of(t.choices).forEach(c -> c.accept(this, imports));
395            return null;
396        }
397    };
398
399    /**
400     * Normalize parsed types in a comment line. If one or more types in the line contains alternatives,
401     * this routine generate a list of 'overloaded' normalized signatures.
402     */
403    List<List<MessageType>> normalizeTypes(int idx, List<MessageType> msgTypes) {
404        if (msgTypes.size() == idx) return Collections.singletonList(Collections.emptyList());
405        MessageType head = msgTypes.get(idx);
406        List<List<MessageType>> buf = new ArrayList<>();
407        for (MessageType alternative : head.accept(normalizeVisitor, null)) {
408            for (List<MessageType> rest : normalizeTypes(idx + 1, msgTypes)) {
409                List<MessageType> temp = new ArrayList<>(rest);
410                temp.add(0, alternative);
411                buf.add(temp);
412            }
413        }
414        return buf;
415    }
416    //where
417    Visitor<List<MessageType>, Void> normalizeVisitor = new Visitor<List<MessageType>, Void>() {
418        @Override
419        public List<MessageType> visitCustomType(CustomType t, Void aVoid) {
420            return Collections.singletonList(t);
421        }
422
423        @Override
424        public List<MessageType> visitSimpleType(SimpleType t, Void aVoid) {
425            return Collections.singletonList(t);
426        }
427
428        @Override
429        public List<MessageType> visitCompoundType(CompoundType t, Void aVoid) {
430            return t.elemtype.accept(this, null).stream()
431                    .map(nt -> new CompoundType(t.kind, nt))
432                    .collect(Collectors.toList());
433        }
434
435        @Override
436        public List<MessageType> visitUnionType(UnionType t, Void aVoid) {
437            return Stream.of(t.choices)
438                    .flatMap(t2 -> t2.accept(this, null).stream())
439                    .collect(Collectors.toList());
440        }
441    };
442}
443