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