001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyrighted [year] [name of copyright owner]". 013 * 014 * Copyright 2011 ForgeRock AS 015 * Portions Copyright 2022 Wren Security. 016 */ 017package org.forgerock.i18n.maven; 018 019import java.io.File; 020import java.io.FileInputStream; 021import java.io.FileOutputStream; 022import java.io.IOException; 023import java.io.InputStreamReader; 024import java.io.LineNumberReader; 025import java.io.OutputStreamWriter; 026import java.util.ArrayList; 027import java.util.Iterator; 028import java.util.LinkedList; 029import java.util.List; 030import java.util.Map; 031import java.util.Properties; 032import java.util.concurrent.ConcurrentHashMap; 033import java.util.concurrent.ExecutorService; 034import java.util.concurrent.Executors; 035import java.util.concurrent.TimeUnit; 036import java.util.concurrent.atomic.AtomicInteger; 037 038import org.apache.maven.plugin.AbstractMojo; 039import org.apache.maven.plugin.MojoExecutionException; 040import org.apache.maven.plugins.annotations.Mojo; 041import org.apache.maven.plugins.annotations.Parameter; 042 043/** 044 * Goal which cleans unused messages files from a property file. 045 */ 046@Mojo(name="clean-messages", threadSafe=true) 047@SuppressWarnings("resource") 048public final class CleanMessagesMojo extends AbstractMojo { 049 050 /** 051 * A task which searches a single source file for any messages. 052 */ 053 private final class SourceFileTask implements Runnable { 054 055 private final File sourceFile; 056 057 private SourceFileTask(final File sourceFile) { 058 this.sourceFile = sourceFile; 059 } 060 061 /** 062 * {@inheritDoc} 063 */ 064 public void run() { 065 // Cache the keys that we want to check so that we avoid excessive 066 // contention on the CHM. 067 final List<MessagePropertyKey> keys = new LinkedList<MessagePropertyKey>( 068 unreferencedProperties.keySet()); 069 070 try { 071 final FileInputStream s = new FileInputStream(sourceFile); 072 try { 073 final LineNumberReader reader = new LineNumberReader( 074 new InputStreamReader(s, encoding)); 075 076 String line; 077 while ((line = reader.readLine()) != null) { 078 final Iterator<MessagePropertyKey> i = keys.iterator(); 079 while (i.hasNext()) { 080 final MessagePropertyKey key = i.next(); 081 if (key.isPresent(line)) { 082 i.remove(); 083 unreferencedProperties.remove(key); 084 referencedProperties.put(key, ""); // Dummy 085 // value. 086 } 087 } 088 } 089 } finally { 090 try { 091 s.close(); 092 } catch (final Exception ignored) { 093 // Ignore. 094 } 095 } 096 097 fileCount.incrementAndGet(); 098 } catch (final IOException e) { 099 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}