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 java.io.BufferedReader;
20 import java.io.File;
21 import java.io.FileInputStream;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.InputStreamReader;
25 import java.io.PrintWriter;
26 import java.nio.charset.Charset;
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.Calendar;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Properties;
33 import java.util.TreeMap;
34 import java.util.UnknownFormatConversionException;
35 import java.util.regex.Matcher;
36 import java.util.regex.Pattern;
37
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 = 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("}").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 public final void execute() throws MojoExecutionException {
545 final File resourceDirectory = getResourceDirectory();
546
547 if (!resourceDirectory.exists()) {
548 throw new MojoExecutionException("Source directory "
549 + resourceDirectory.getPath() + " does not exist");
550 } else if (!resourceDirectory.isDirectory()) {
551 throw new MojoExecutionException("Source directory "
552 + resourceDirectory.getPath() + " is not a directory");
553 }
554
555 final File targetDirectory = getTargetDirectory();
556
557 if (!targetDirectory.exists()) {
558 if (targetDirectory.mkdirs()) {
559 getLog().info(
560 "Created message output directory: "
561 + targetDirectory.getPath());
562 } else {
563 throw new MojoExecutionException(
564 "Unable to create message output directory: "
565 + targetDirectory.getPath());
566 }
567 } else if (!targetDirectory.isDirectory()) {
568 throw new MojoExecutionException("Output directory "
569 + targetDirectory.getPath() + " is not a directory");
570 }
571
572 if (project != null) {
573 getLog().info(
574 "Adding source directory: " + targetDirectory.getPath());
575 addNewSourceDirectory(targetDirectory);
576 }
577
578 for (final String messageFile : messageFiles) {
579 processMessageFile(new MessageFile(messageFile));
580 }
581 }
582
583
584
585
586
587
588
589 abstract void addNewSourceDirectory(final File targetDirectory);
590
591
592
593
594
595
596 abstract File getResourceDirectory();
597
598
599
600
601
602
603
604
605 abstract File getTargetDirectory();
606
607
608
609
610 private InputStream getStubFile() throws MojoExecutionException {
611 return getClass().getResourceAsStream("Messages.java.stub");
612 }
613
614 private void processMessageFile(final MessageFile messageFile) throws MojoExecutionException {
615 final File resourceDirectory = getResourceDirectory();
616 final File targetDirectory = getTargetDirectory();
617
618 final File sourceFile = messageFile.getResourceFile(resourceDirectory);
619 final File outputFile = messageFile.getOutputFile(targetDirectory);
620
621
622
623 if (!sourceFile.exists()) {
624 throw new MojoExecutionException("Message file "
625 + messageFile.getName() + " does not exist");
626 }
627
628 if (outputFile.exists()) {
629 if (force || sourceFile.lastModified() > outputFile.lastModified()) {
630 if (!outputFile.delete()) {
631 throw new MojoExecutionException(
632 "Unable to continue because the old message file "
633 + messageFile.getName()
634 + " could not be deleted");
635 }
636
637 getLog().info(
638 "Regenerating " + outputFile.getName() + " from "
639 + sourceFile.getName());
640 } else {
641 getLog().info(outputFile.getName() + " is up to date");
642 return;
643 }
644 } else {
645 final File packageDirectory = outputFile.getParentFile();
646 if (!packageDirectory.exists()) {
647 if (!packageDirectory.mkdirs()) {
648 throw new MojoExecutionException(
649 "Unable to create message output directory: "
650 + packageDirectory.getPath());
651 }
652 }
653 getLog().info(
654 "Generating " + outputFile.getName() + " from "
655 + sourceFile.getName());
656 }
657
658 BufferedReader stubReader = null;
659 PrintWriter outputWriter = null;
660
661 try {
662 stubReader = new BufferedReader(new InputStreamReader(
663 getStubFile(), UTF_8));
664 outputWriter = new PrintWriter(outputFile, "UTF-8");
665
666 final Properties properties = new Properties();
667 final FileInputStream propertiesFile = new FileInputStream(
668 sourceFile);
669 try {
670 properties.load(propertiesFile);
671 } finally {
672 try {
673 propertiesFile.close();
674 } catch (Exception ignored) {
675
676 }
677 }
678
679 for (String stubLine = stubReader.readLine(); stubLine != null; stubLine = stubReader
680 .readLine()) {
681 if (stubLine.contains("${MESSAGES}")) {
682 final Map<MessagePropertyKey, String> propertyMap =
683 new TreeMap<MessagePropertyKey, String>();
684
685 for (final Map.Entry<Object, Object> property : properties
686 .entrySet()) {
687 final String propKey = property.getKey().toString();
688 final MessagePropertyKey key = MessagePropertyKey
689 .valueOf(propKey);
690 propertyMap.put(key, property.getValue().toString());
691 }
692
693 int usesOfGenericDescriptor = 0;
694
695 for (final Map.Entry<MessagePropertyKey, String> property : propertyMap
696 .entrySet()) {
697 final MessageDescriptorDeclaration message =
698 new MessageDescriptorDeclaration(property.getKey(),
699 property.getValue());
700
701 message.setConstructorArguments(
702 messageFile.getClassName() + ".class",
703 "RESOURCE",
704 quote(property.getKey().toString()),
705 String.valueOf(property.getKey().getOrdinal()));
706 outputWriter.println(message.toString());
707 outputWriter.println();
708
709
710
711 if (message.useGenericMessageTypeClass()) {
712 usesOfGenericDescriptor++;
713 }
714 }
715
716 getLog().debug(
717 " Generated " + propertyMap.size()
718 + " LocalizableMessage");
719 getLog().debug(
720 " Number of LocalizableMessageDescriptor.ArgN: "
721 + usesOfGenericDescriptor);
722 } else {
723 stubLine = stubLine.replace("${PACKAGE}",
724 messageFile.getPackageName());
725 stubLine = stubLine.replace("${CLASS_NAME}",
726 messageFile.getClassName());
727 stubLine = stubLine.replace("${FILE_NAME}",
728 messageFile.getName());
729 stubLine = stubLine.replace("${RESOURCE_BUNDLE_NAME}",
730 messageFile.getResourceBundleName());
731 outputWriter.println(stubLine);
732 }
733 }
734 } catch (final IOException e) {
735
736
737 if (outputFile.exists()) {
738 outputFile.deleteOnExit();
739 }
740 throw new MojoExecutionException(
741 "An IO error occurred while generating the message file: "
742 + e);
743 } finally {
744 if (stubReader != null) {
745 try {
746 stubReader.close();
747 } catch (final Exception e) {
748
749 }
750 }
751
752 if (outputWriter != null) {
753 try {
754 outputWriter.close();
755 } catch (final Exception e) {
756
757 }
758 }
759 }
760
761 }
762
763 private String quote(final String s) {
764 return new StringBuilder().append("\"").append(s).append("\"")
765 .toString();
766 }
767
768
769
770
771
772
773 protected MavenProject getMavenProject() {
774 return project;
775 }
776 }