View Javadoc
1   /*
2    * The contents of this file are subject to the terms of the Common Development and
3    * Distribution License (the License). You may not use this file except in compliance with the
4    * License.
5    *
6    * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7    * specific language governing permission and limitations under the License.
8    *
9    * When distributing Covered Software, include this CDDL Header Notice in each file and include
10   * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11   * Header, with the fields enclosed by brackets [] replaced by your own identifying
12   * information: "Portions Copyrighted [year] [name of copyright owner]".
13   *
14   * Copyright 2011 ForgeRock AS
15   * Portions Copyright 2022 Wren Security.
16   */
17  package org.forgerock.i18n.maven;
18  
19  import java.io.File;
20  import java.io.FileInputStream;
21  import java.io.FileOutputStream;
22  import java.io.IOException;
23  import java.io.InputStreamReader;
24  import java.io.LineNumberReader;
25  import java.io.OutputStreamWriter;
26  import java.util.ArrayList;
27  import java.util.Iterator;
28  import java.util.LinkedList;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.Properties;
32  import java.util.concurrent.ConcurrentHashMap;
33  import java.util.concurrent.ExecutorService;
34  import java.util.concurrent.Executors;
35  import java.util.concurrent.TimeUnit;
36  import java.util.concurrent.atomic.AtomicInteger;
37  
38  import org.apache.maven.plugin.AbstractMojo;
39  import org.apache.maven.plugin.MojoExecutionException;
40  import org.apache.maven.plugins.annotations.Mojo;
41  import org.apache.maven.plugins.annotations.Parameter;
42  
43  /**
44   * Goal which cleans unused messages files from a property file.
45   */
46  @Mojo(name="clean-messages", threadSafe=true)
47  @SuppressWarnings("resource")
48  public final class CleanMessagesMojo extends AbstractMojo {
49  
50      /**
51       * A task which searches a single source file for any messages.
52       */
53      private final class SourceFileTask implements Runnable {
54  
55          private final File sourceFile;
56  
57          private SourceFileTask(final File sourceFile) {
58              this.sourceFile = sourceFile;
59          }
60  
61          /**
62           * {@inheritDoc}
63           */
64          public void run() {
65              // Cache the keys that we want to check so that we avoid excessive
66              // contention on the CHM.
67              final List<MessagePropertyKey> keys = new LinkedList<MessagePropertyKey>(
68                      unreferencedProperties.keySet());
69  
70              try {
71                  final FileInputStream s = new FileInputStream(sourceFile);
72                  try {
73                      final LineNumberReader reader = new LineNumberReader(
74                              new InputStreamReader(s, encoding));
75  
76                      String line;
77                      while ((line = reader.readLine()) != null) {
78                          final Iterator<MessagePropertyKey> i = keys.iterator();
79                          while (i.hasNext()) {
80                              final MessagePropertyKey key = i.next();
81                              if (key.isPresent(line)) {
82                                  i.remove();
83                                  unreferencedProperties.remove(key);
84                                  referencedProperties.put(key, ""); // Dummy
85                                                                     // value.
86                              }
87                          }
88                      }
89                  } finally {
90                      try {
91                          s.close();
92                      } catch (final Exception ignored) {
93                          // Ignore.
94                      }
95                  }
96  
97                  fileCount.incrementAndGet();
98              } catch (final IOException e) {
99                  getLog().error(
100                         "An error occurred while reading source file "
101                                 + sourceFile.getName(), e);
102             }
103         }
104 
105     }
106 
107     /**
108      * The target directory in which the source files should be generated.
109      */
110     @Parameter(defaultValue="${project.build.sourceDirectory}", required=true)
111     private File sourceDirectory;
112 
113     /**
114      * The message message file to be cleaned.
115      */
116     @Parameter(required=true)
117     private File messageFile;
118 
119     /**
120      * The encoding argument used by Java source files.
121      */
122     @Parameter(defaultValue="${project.build.sourceEncoding}", required=true)
123     private String encoding;
124 
125     private final Map<MessagePropertyKey, String> unreferencedProperties =
126             new ConcurrentHashMap<MessagePropertyKey, String>();
127     private final Map<MessagePropertyKey, String> referencedProperties =
128             new ConcurrentHashMap<MessagePropertyKey, String>();
129     private final AtomicInteger fileCount = new AtomicInteger();
130 
131     /**
132      * {@inheritDoc}
133      */
134     // @Checkstyle:ignore
135     public void execute() throws MojoExecutionException {
136         if (!sourceDirectory.exists()) {
137             throw new MojoExecutionException("Source directory "
138                     + sourceDirectory.getPath() + " does not exist");
139         } else if (!sourceDirectory.isDirectory()) {
140             throw new MojoExecutionException("Source directory "
141                     + sourceDirectory.getPath() + " is not a directory");
142         }
143 
144         if (!messageFile.exists()) {
145             throw new MojoExecutionException("Message file "
146                     + messageFile.getPath() + " does not exist");
147         } else if (!messageFile.isFile()) {
148             throw new MojoExecutionException("Message file "
149                     + messageFile.getPath() + " is not a file");
150         }
151 
152         final Properties properties = new Properties();
153 
154         try {
155             final FileInputStream propertiesFile = new FileInputStream(
156                     messageFile);
157             try {
158                 properties.load(propertiesFile);
159             } finally {
160                 try {
161                     propertiesFile.close();
162                 } catch (final Exception ignored) {
163                     // Ignore.
164                 }
165             }
166         } catch (final IOException e) {
167             throw new MojoExecutionException(
168                     "An IO error occurred while reading the message property file: "
169                             + e);
170         }
171 
172         // Initially all properties are deemed to have no references.
173         for (final Map.Entry<Object, Object> property : properties.entrySet()) {
174             final String propKey = property.getKey().toString();
175             final MessagePropertyKey key = MessagePropertyKey.valueOf(propKey);
176             unreferencedProperties.put(key, property.getValue().toString());
177         }
178 
179         final int messageCount = unreferencedProperties.size();
180 
181         // Process source files in parallel.
182         final ExecutorService executor = Executors.newFixedThreadPool(Runtime
183                 .getRuntime().availableProcessors() * 2);
184 
185         // Recursively add source files to task queue.
186         processSourceDirectory(executor, sourceDirectory);
187 
188         executor.shutdown();
189         try {
190             executor.awaitTermination(1, TimeUnit.DAYS);
191         } catch (final InterruptedException e) {
192             Thread.currentThread().interrupt();
193             throw new MojoExecutionException(
194                     "Interrupted while processing source files");
195         }
196 
197         if ((unreferencedProperties.size() + referencedProperties.size()) != messageCount) {
198             throw new IllegalStateException("Message table sizes are invalid");
199         }
200 
201         getLog().info("Processed " + fileCount.get() + " source files");
202         getLog().info(
203                 "Found " + unreferencedProperties.size() + " / " + messageCount
204                         + " unreferenced properties");
205 
206         // All source files processed and each message is known to be either
207         // referenced or not. Re-write the property file including only the
208         // referenced properties.
209         int cleanedMessageCount = 0;
210         int savedMessageCount = 0;
211 
212         final List<String> lines = new ArrayList<String>(10000);
213         try {
214             final FileInputStream propertiesFile = new FileInputStream(
215                     messageFile);
216             try {
217                 final LineNumberReader reader = new LineNumberReader(
218                         new InputStreamReader(propertiesFile, "ISO-8859-1"));
219 
220                 String line;
221                 boolean inValue = false;
222                 boolean lineNeedsRemoving = false;
223                 boolean foundErrors = false;
224                 while ((line = reader.readLine()) != null) {
225                     if (!inValue) {
226                         // This could be a comment or the start of a new
227                         // property.
228                         final String trimmedLine = line.trim();
229                         if (trimmedLine.length() == 0) {
230                             lines.add(line);
231                         } else if (trimmedLine.startsWith("#")) {
232                             lines.add(line);
233                         } else {
234                             final String key;
235                             final int separator = trimmedLine.indexOf('=');
236                             if (separator < 0) {
237                                 key = trimmedLine;
238                             } else {
239                                 key = trimmedLine.substring(0, separator)
240                                         .trim();
241                             }
242 
243                             MessagePropertyKey mpk;
244                             try {
245                                 mpk = MessagePropertyKey.valueOf(key);
246                             } catch (IllegalArgumentException e) {
247                                 getLog().error(
248                                         "Unable to decode line "
249                                                 + reader.getLineNumber() + ": "
250                                                 + line, e);
251                                 lines.add(line);
252                                 lineNeedsRemoving = false;
253                                 inValue = isContinuedOnNextLine(line);
254                                 foundErrors = true;
255                                 continue;
256                             }
257 
258                             if (referencedProperties.containsKey(mpk)) {
259                                 savedMessageCount++;
260                                 lines.add(line);
261                                 lineNeedsRemoving = false;
262                             } else {
263                                 if (!unreferencedProperties.containsKey(mpk)) {
264                                     throw new IllegalStateException(
265                                             "Unregistered message key");
266                                 }
267                                 cleanedMessageCount++;
268                                 lineNeedsRemoving = true;
269                             }
270                             inValue = isContinuedOnNextLine(line);
271                         }
272                     } else {
273                         // This is a continuation line.
274                         if (!lineNeedsRemoving) {
275                             lines.add(line);
276                         }
277 
278                         inValue = isContinuedOnNextLine(line);
279                     }
280                 }
281 
282                 if (foundErrors) {
283                     throw new MojoExecutionException(
284                             "Aborting because the message file could not be parsed");
285                 }
286             } finally {
287                 try {
288                     propertiesFile.close();
289                 } catch (final Exception ignored) {
290                     // Ignore.
291                 }
292             }
293         } catch (final IOException e) {
294             throw new MojoExecutionException(
295                     "An IO error occurred while reading the message property file: "
296                             + e);
297         }
298 
299         // Now write out the cleaned message file.
300         if (cleanedMessageCount == 0) {
301             // Nothing to do.
302             getLog().info(
303                     "Message file " + messageFile.getName()
304                             + " unchanged: no messages were cleaned");
305         } else {
306             try {
307                 final FileOutputStream propertiesFile = new FileOutputStream(
308                         messageFile);
309                 try {
310                     final OutputStreamWriter writer = new OutputStreamWriter(
311                             propertiesFile, "ISO-8859-1");
312                     final String eol = System.getProperty("line.separator");
313                     for (final String line : lines) {
314                         writer.write(line);
315                         writer.write(eol);
316                     }
317 
318                     writer.close();
319                 } finally {
320                     try {
321                         propertiesFile.close();
322                     } catch (final Exception ignored) {
323                         // Ignore.
324                     }
325                 }
326             } catch (final IOException e) {
327                 throw new MojoExecutionException(
328                         "An IO error occurred while writing the message property file: "
329                                 + e);
330             }
331 
332             getLog().info(
333                     "Message file " + messageFile.getName() + " cleaned: "
334                             + cleanedMessageCount + " messages removed and "
335                             + savedMessageCount + " kept");
336         }
337     }
338 
339     // Check to see if this line ends with a continuation character (odd
340     // number of consecutive back-slash).
341     boolean isContinuedOnNextLine(final String line) {
342         int bsCount = 0;
343         for (int i = line.length() - 1; i >= 0 && line.charAt(i) == '\\'; i--) {
344             bsCount++;
345         }
346 
347         return ((bsCount % 2) == 1);
348     }
349 
350     // Recursively create tasks for each Java source file in the provided source
351     // directory and sub-directories.
352     private void processSourceDirectory(final ExecutorService executor,
353             final File s) {
354         for (final File f : s.listFiles()) {
355             if (f.isDirectory()) {
356                 processSourceDirectory(executor, f);
357             } else if (f.isFile()) {
358                 if (f.getName().endsWith(".java")) {
359                     // Add a task for processing this source file.
360                     executor.execute(new SourceFileTask(f));
361                 }
362             }
363         }
364     }
365 }