1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.forgerock.i18n.maven;
18
19 import com.google.common.xml.XmlEscapers;
20 import java.io.BufferedReader;
21 import java.io.File;
22 import java.io.FileInputStream;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.io.InputStreamReader;
26 import java.io.PrintWriter;
27 import java.nio.charset.Charset;
28 import java.util.ArrayList;
29 import java.util.Arrays;
30 import java.util.Calendar;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.Properties;
34 import java.util.TreeMap;
35 import java.util.UnknownFormatConversionException;
36 import java.util.regex.Matcher;
37 import java.util.regex.Pattern;
38 import org.apache.maven.plugin.AbstractMojo;
39 import org.apache.maven.plugin.MojoExecutionException;
40 import org.apache.maven.plugins.annotations.Parameter;
41 import org.apache.maven.project.MavenProject;
42
43
44
45
46
47 abstract class AbstractGenerateMessagesMojo extends AbstractMojo {
48
49
50
51 static final class MessageFile {
52
53
54
55 private final String name;
56
57
58
59
60
61
62
63
64 MessageFile(final String name) {
65 this.name = name;
66 }
67
68
69
70
71 @Override
72 public String toString() {
73 return name;
74 }
75
76
77
78
79
80
81
82
83 String getClassName() {
84 final StringBuilder builder = new StringBuilder();
85 final String shortName = getShortName();
86 boolean upperCaseNextChar = true;
87 for (final char c : shortName.toCharArray()) {
88 if (c == '_' || c == '-') {
89 upperCaseNextChar = true;
90 continue;
91 }
92
93 if (upperCaseNextChar) {
94 builder.append(Character.toUpperCase(c));
95 upperCaseNextChar = false;
96 } else {
97 builder.append(c);
98 }
99 }
100 builder.append("Messages");
101 return builder.toString();
102 }
103
104
105
106
107
108
109
110 String getName() {
111 return name;
112 }
113
114
115
116
117
118
119
120
121
122
123 File getOutputFile(final File outputDirectory) {
124 final int lastSlash = name.lastIndexOf('/');
125 final String parentPath = name.substring(0, lastSlash);
126 final String path = parentPath.replace('/', File.separatorChar)
127 + File.separator + getClassName() + ".java";
128 return new File(outputDirectory, path);
129 }
130
131
132
133
134
135
136 String getPackageName() {
137 final int lastSlash = name.lastIndexOf('/');
138 final String parentPath = name.substring(0, lastSlash);
139 return parentPath.replace('/', '.');
140 }
141
142
143
144
145
146
147 String getResourceBundleName() {
148 return getPackageName() + "." + getShortName();
149 }
150
151
152
153
154
155
156
157
158
159
160 File getResourceFile(final File resourceDirectory) {
161 final String path = name.replace('/', File.separatorChar);
162 return new File(resourceDirectory, path);
163 }
164
165
166
167
168
169
170
171
172 String getShortName() {
173 final int lastSlash = name.lastIndexOf('/');
174 final String fileName = name.substring(lastSlash + 1);
175 final int lastDot = fileName.lastIndexOf('.');
176 return fileName.substring(0, lastDot);
177 }
178
179 }
180
181
182
183
184 private static final class FormatSpecifier {
185
186 private final String[] sa;
187
188
189
190
191
192
193
194 FormatSpecifier(final String[] sa) {
195 this.sa = sa;
196 }
197
198
199
200
201
202
203
204
205 Class<?> getSimpleConversionClass() {
206 Class<?> c = null;
207 final String sa4 = sa[4] != null ? sa[4].toLowerCase() : null;
208 final String sa5 = sa[5] != null ? sa[5].toLowerCase() : null;
209 if ("t".equals(sa4)) {
210 c = Calendar.class;
211 } else if ("b".equals(sa5)) {
212 c = Boolean.class;
213 } else if ("h".equals(sa5) ) {
214 c = Object.class;
215 } else if ("s".equals(sa5)) {
216 c = Object.class;
217 } else if ("c".equals(sa5)) {
218 c = Character.class;
219 } else if ("d".equals(sa5) || "o".equals(sa5) || "x".equals(sa5)
220 || "e".equals(sa5) || "f".equals(sa5) || "g".equals(sa5)
221 || "a".equals(sa5)) {
222 c = Number.class;
223 } else if ("n".equals(sa5) || "%".equals(sa5)) {
224
225 }
226 return c;
227 }
228
229
230
231
232
233
234
235 boolean specifiesArgumentIndex() {
236 return this.sa[0] != null;
237 }
238
239 }
240
241
242
243
244 private static final class MessageDescriptorDeclaration {
245
246 private final MessagePropertyKey key;
247 private final String formatString;
248 private final List<FormatSpecifier> specifiers;
249 private final List<Class<?>> classTypes;
250 private String[] constructorArgs;
251
252
253
254
255
256
257
258
259
260 MessageDescriptorDeclaration(final MessagePropertyKey key,
261 final String formatString) {
262 this.key = key;
263 this.formatString = formatString;
264 this.specifiers = parse(formatString);
265 this.classTypes = new ArrayList<Class<?>>();
266 for (final FormatSpecifier f : specifiers) {
267 final Class<?> c = f.getSimpleConversionClass();
268 if (c != null) {
269 classTypes.add(c);
270 }
271 }
272 }
273
274
275
276
277 @Override
278 public String toString() {
279 final StringBuilder sb = new StringBuilder();
280 sb.append(getComment());
281 sb.append(indent(1));
282 sb.append("public static final ");
283 sb.append(getDescriptorClassDeclaration());
284 sb.append(" ");
285 sb.append(key.getName());
286 sb.append(" =");
287 sb.append(EOL);
288 sb.append(indent(5));
289 sb.append("new ");
290 sb.append(getDescriptorClassDeclaration());
291 sb.append("(");
292 if (constructorArgs != null) {
293 for (int i = 0; i < constructorArgs.length; i++) {
294 sb.append(constructorArgs[i]);
295 if (i < constructorArgs.length - 1) {
296 sb.append(", ");
297 }
298 }
299 }
300 sb.append(");");
301 return sb.toString();
302 }
303
304
305
306
307
308
309
310
311 String getClassTypeVariables() {
312 final StringBuilder sb = new StringBuilder();
313 if (classTypes.size() > 0) {
314 sb.append("<");
315 for (int i = 0; i < classTypes.size(); i++) {
316 final Class<?> c = classTypes.get(i);
317 if (c != null) {
318 sb.append(getShortClassName(c));
319 if (i < classTypes.size() - 1) {
320 sb.append(", ");
321 }
322 }
323 }
324 sb.append(">");
325 }
326 return sb.toString();
327 }
328
329
330
331
332
333
334
335
336 String getComment() {
337 final StringBuilder sb = new StringBuilder();
338 sb.append(indent(1)).append("/**").append(EOL);
339 sb.append(indent(1)).append(" * ").append("<code>").append(EOL);
340
341
342
343 final String ws = XmlEscapers.xmlContentEscaper().escape(formatString);
344
345 final String[] sa = ws.split(EOL);
346 for (final String s : sa) {
347 sb.append(indent(1)).append(" * ").append(s).append(EOL);
348 }
349
350 sb.append(indent(1)).append(" * ").append("</code>").append(EOL);
351 sb.append(indent(1)).append(" */").append(EOL);
352 return sb.toString();
353 }
354
355
356
357
358
359
360
361
362 String getDescriptorClassDeclaration() {
363 final StringBuilder sb = new StringBuilder();
364 if (useGenericMessageTypeClass()) {
365 sb.append("LocalizableMessageDescriptor");
366 sb.append(".");
367 sb.append(DESCRIPTOR_CLASS_BASE_NAME);
368 sb.append("N");
369 } else {
370 sb.append("LocalizableMessageDescriptor");
371 sb.append(".");
372 sb.append(DESCRIPTOR_CLASS_BASE_NAME);
373 sb.append(classTypes.size());
374 sb.append(getClassTypeVariables());
375 }
376 return sb.toString();
377 }
378
379
380
381
382
383
384
385
386
387 void setConstructorArguments(final String... s) {
388 this.constructorArgs = s;
389 }
390
391 private void checkText(final String s) {
392 int idx = s.indexOf('%');
393
394
395 if (idx != -1) {
396 final char c = (idx > s.length() - 2 ? '%' : s.charAt(idx + 1));
397 throw new UnknownFormatConversionException(String.valueOf(c));
398 }
399 }
400
401 private String getShortClassName(final Class<?> c) {
402 String name;
403 final String fqName = c.getName();
404 final int i = fqName.lastIndexOf('.');
405 if (i > 0) {
406 name = fqName.substring(i + 1);
407 } else {
408 name = fqName;
409 }
410 return name;
411 }
412
413 private String indent(final int indent) {
414 final char[] blankArray = new char[4 * indent];
415 Arrays.fill(blankArray, ' ');
416 return new String(blankArray);
417 }
418
419
420
421
422
423
424
425
426
427 private List<FormatSpecifier> parse(final String s) {
428 final List<FormatSpecifier> sl = new ArrayList<FormatSpecifier>();
429 final Matcher m = SPECIFIER_PATTERN.matcher(s);
430 int i = 0;
431 while (i < s.length()) {
432 if (m.find(i)) {
433
434
435
436 if (m.start() != i) {
437
438
439 checkText(s.substring(i, m.start()));
440
441
442
443 }
444
445
446 final String[] sa = new String[6];
447 for (int j = 0; j < m.groupCount(); j++) {
448 sa[j] = m.group(j + 1);
449 }
450 sl.add(new FormatSpecifier(sa));
451 i = m.end();
452 } else {
453
454
455 checkText(s.substring(i));
456
457
458
459 break;
460 }
461 }
462 return sl;
463 }
464
465
466
467
468
469
470
471
472
473
474
475 private boolean useGenericMessageTypeClass() {
476 if (specifiers.size() > DESCRIPTOR_MAX_ARG_HANDLER) {
477 return true;
478 } else {
479 for (final FormatSpecifier s : specifiers) {
480 if (s.specifiesArgumentIndex()) {
481 return true;
482 }
483 }
484 }
485 return false;
486 }
487
488 }
489
490
491
492
493
494 @Parameter(defaultValue="false", required=true)
495 private boolean force;
496
497
498
499
500
501 @Parameter(required=true)
502 private String[] messageFiles;
503
504
505
506
507 @Parameter(defaultValue="${project}", readonly=true, required=true)
508 private MavenProject project;
509
510
511
512
513
514
515
516 private static final String DESCRIPTOR_CLASS_BASE_NAME = "Arg";
517
518
519
520
521
522
523 private static final int DESCRIPTOR_MAX_ARG_HANDLER = 11;
524
525 private static final String SPECIFIER_REGEX =
526 "%(\\d+\\$)?([-#+ 0,(\\<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])";
527
528 private static final Pattern SPECIFIER_PATTERN = Pattern
529 .compile(SPECIFIER_REGEX);
530
531
532
533
534 private static final String EOL = System.getProperty("line.separator");
535
536
537
538
539 private static final Charset UTF_8 = Charset.forName("UTF-8");
540
541
542
543
544 @Override
545 public final void execute() throws MojoExecutionException {
546 final File resourceDirectory = getResourceDirectory();
547
548 if (!resourceDirectory.exists()) {
549 throw new MojoExecutionException("Source directory "
550 + resourceDirectory.getPath() + " does not exist");
551 } else if (!resourceDirectory.isDirectory()) {
552 throw new MojoExecutionException("Source directory "
553 + resourceDirectory.getPath() + " is not a directory");
554 }
555
556 final File targetDirectory = getTargetDirectory();
557
558 if (!targetDirectory.exists()) {
559 if (targetDirectory.mkdirs()) {
560 getLog().info(
561 "Created message output directory: "
562 + targetDirectory.getPath());
563 } else {
564 throw new MojoExecutionException(
565 "Unable to create message output directory: "
566 + targetDirectory.getPath());
567 }
568 } else if (!targetDirectory.isDirectory()) {
569 throw new MojoExecutionException("Output directory "
570 + targetDirectory.getPath() + " is not a directory");
571 }
572
573 if (project != null) {
574 getLog().info(
575 "Adding source directory: " + targetDirectory.getPath());
576 addNewSourceDirectory(targetDirectory);
577 }
578
579 for (final String messageFile : messageFiles) {
580 processMessageFile(new MessageFile(messageFile));
581 }
582 }
583
584
585
586
587
588
589
590 abstract void addNewSourceDirectory(final File targetDirectory);
591
592
593
594
595
596
597 abstract File getResourceDirectory();
598
599
600
601
602
603
604
605
606 abstract File getTargetDirectory();
607
608
609
610
611 private InputStream getStubFile() throws MojoExecutionException {
612 return getClass().getResourceAsStream("Messages.java.stub");
613 }
614
615 private void processMessageFile(final MessageFile messageFile) throws MojoExecutionException {
616 final File resourceDirectory = getResourceDirectory();
617 final File targetDirectory = getTargetDirectory();
618
619 final File sourceFile = messageFile.getResourceFile(resourceDirectory);
620 final File outputFile = messageFile.getOutputFile(targetDirectory);
621
622
623
624 if (!sourceFile.exists()) {
625 throw new MojoExecutionException("Message file "
626 + messageFile.getName() + " does not exist");
627 }
628
629 if (outputFile.exists()) {
630 if (force || sourceFile.lastModified() > outputFile.lastModified()) {
631 if (!outputFile.delete()) {
632 throw new MojoExecutionException(
633 "Unable to continue because the old message file "
634 + messageFile.getName()
635 + " could not be deleted");
636 }
637
638 getLog().info(
639 "Regenerating " + outputFile.getName() + " from "
640 + sourceFile.getName());
641 } else {
642 getLog().info(outputFile.getName() + " is up to date");
643 return;
644 }
645 } else {
646 final File packageDirectory = outputFile.getParentFile();
647 if (!packageDirectory.exists()) {
648 if (!packageDirectory.mkdirs()) {
649 throw new MojoExecutionException(
650 "Unable to create message output directory: "
651 + packageDirectory.getPath());
652 }
653 }
654 getLog().info(
655 "Generating " + outputFile.getName() + " from "
656 + sourceFile.getName());
657 }
658
659 BufferedReader stubReader = null;
660 PrintWriter outputWriter = null;
661
662 try {
663 stubReader = new BufferedReader(new InputStreamReader(
664 getStubFile(), UTF_8));
665 outputWriter = new PrintWriter(outputFile, "UTF-8");
666
667 final Properties properties = new Properties();
668 final FileInputStream propertiesFile = new FileInputStream(
669 sourceFile);
670 try {
671 properties.load(propertiesFile);
672 } finally {
673 try {
674 propertiesFile.close();
675 } catch (Exception ignored) {
676
677 }
678 }
679
680 for (String stubLine = stubReader.readLine(); stubLine != null; stubLine = stubReader
681 .readLine()) {
682 if (stubLine.contains("${MESSAGES}")) {
683 final Map<MessagePropertyKey, String> propertyMap =
684 new TreeMap<MessagePropertyKey, String>();
685
686 for (final Map.Entry<Object, Object> property : properties
687 .entrySet()) {
688 final String propKey = property.getKey().toString();
689 final MessagePropertyKey key = MessagePropertyKey
690 .valueOf(propKey);
691 propertyMap.put(key, property.getValue().toString());
692 }
693
694 int usesOfGenericDescriptor = 0;
695
696 for (final Map.Entry<MessagePropertyKey, String> property : propertyMap
697 .entrySet()) {
698 final MessageDescriptorDeclaration message =
699 new MessageDescriptorDeclaration(property.getKey(),
700 property.getValue());
701
702 message.setConstructorArguments(
703 messageFile.getClassName() + ".class",
704 "RESOURCE",
705 quote(property.getKey().toString()),
706 String.valueOf(property.getKey().getOrdinal()));
707 outputWriter.println(message.toString());
708 outputWriter.println();
709
710
711
712 if (message.useGenericMessageTypeClass()) {
713 usesOfGenericDescriptor++;
714 }
715 }
716
717 getLog().debug(
718 " Generated " + propertyMap.size()
719 + " LocalizableMessage");
720 getLog().debug(
721 " Number of LocalizableMessageDescriptor.ArgN: "
722 + usesOfGenericDescriptor);
723 } else {
724 stubLine = stubLine.replace("${PACKAGE}",
725 messageFile.getPackageName());
726 stubLine = stubLine.replace("${CLASS_NAME}",
727 messageFile.getClassName());
728 stubLine = stubLine.replace("${FILE_NAME}",
729 messageFile.getName());
730 stubLine = stubLine.replace("${RESOURCE_BUNDLE_NAME}",
731 messageFile.getResourceBundleName());
732 outputWriter.println(stubLine);
733 }
734 }
735 } catch (final IOException e) {
736
737
738 if (outputFile.exists()) {
739 outputFile.deleteOnExit();
740 }
741 throw new MojoExecutionException(
742 "An IO error occurred while generating the message file: "
743 + e);
744 } finally {
745 if (stubReader != null) {
746 try {
747 stubReader.close();
748 } catch (final Exception e) {
749
750 }
751 }
752
753 if (outputWriter != null) {
754 try {
755 outputWriter.close();
756 } catch (final Exception e) {
757
758 }
759 }
760 }
761
762 }
763
764 private String quote(final String s) {
765 return new StringBuilder().append("\"").append(s).append("\"")
766 .toString();
767 }
768
769
770
771
772
773
774 protected MavenProject getMavenProject() {
775 return project;
776 }
777 }