View Javadoc
1   /*
2    * #%L
3    * wcm.io
4    * %%
5    * Copyright (C) 2014 wcm.io
6    * %%
7    * Licensed under the Apache License, Version 2.0 (the "License");
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   *
11   *      http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   * #L%
19   */
20  package io.wcm.maven.plugins.i18n;
21  
22  import java.io.File;
23  import java.io.IOException;
24  import java.nio.charset.StandardCharsets;
25  import java.util.Collections;
26  import java.util.List;
27  
28  import javax.inject.Inject;
29  
30  import org.apache.commons.lang3.StringUtils;
31  import org.apache.commons.lang3.Strings;
32  import org.apache.maven.model.Build;
33  import org.apache.maven.model.Resource;
34  import org.apache.maven.plugin.AbstractMojo;
35  import org.apache.maven.plugin.MojoExecutionException;
36  import org.apache.maven.plugin.MojoFailureException;
37  import org.apache.maven.plugins.annotations.LifecyclePhase;
38  import org.apache.maven.plugins.annotations.Mojo;
39  import org.apache.maven.plugins.annotations.Parameter;
40  import org.apache.maven.project.MavenProject;
41  import org.codehaus.plexus.util.FileUtils;
42  import org.codehaus.plexus.util.Scanner;
43  import org.sonatype.plexus.build.incremental.BuildContext;
44  
45  import io.wcm.maven.plugins.i18n.readers.I18nReader;
46  import io.wcm.maven.plugins.i18n.readers.JsonI18nReader;
47  import io.wcm.maven.plugins.i18n.readers.PropertiesI18nReader;
48  import io.wcm.maven.plugins.i18n.readers.XmlI18nReader;
49  
50  /**
51   * Transform i18n resources in Java Properties, JSON or XML file format to Sling i18n Messages JSON or XML format.
52   */
53  @Mojo(name = "transform", defaultPhase = LifecyclePhase.GENERATE_RESOURCES, requiresProject = true, threadSafe = true)
54  @SuppressWarnings("java:S6813") // allow field injection
55  public class TransformMojo extends AbstractMojo {
56  
57    // file extensions
58    private static final String FILE_EXTENSION_JSON = "json";
59    private static final String FILE_EXTENSION_XML = "xml";
60    private static final String FILE_EXTENSION_PROPERTIES = "properties";
61  
62    private static final String ALL_FILES = "**/*.";
63    private static final String[] SOURCE_FILES_INCLUDES = new String[] {
64        ALL_FILES + FILE_EXTENSION_PROPERTIES,
65        ALL_FILES + FILE_EXTENSION_XML,
66        ALL_FILES + FILE_EXTENSION_JSON
67    };
68  
69    /**
70     * Source path containing the i18n source .properties or .xml files.
71     */
72    @Parameter(defaultValue = "${basedir}/src/main/resources/i18n")
73    private String source;
74  
75    /**
76     * Relative target path for the generated resources.
77     */
78    @Parameter(defaultValue = "SLING-INF/app-root/i18n")
79    private String target;
80  
81    /**
82     * Output format. Possible values:
83     * <ul>
84     * <li><code>JSON</code>: Sling Message format serialized as JSON.</li>
85     * <li><code>JSON_PROPERTIES</code>: Flat list of key/value pairs in JSON format.</li>
86     * <li><code>XML</code>: Sling Message format serialized as JCR XML.</li>
87     * <li><code>PROPERTIES</code>: Flat list of key/value pairs in Java Properties format.</li>
88     * </ul>
89     */
90    @Parameter(defaultValue = "JSON")
91    private String outputFormat;
92  
93    @Parameter(defaultValue = "generated-i18n-resources")
94    private String generatedResourcesFolderPath;
95  
96    @Parameter(property = "project", required = true, readonly = true)
97    private MavenProject project;
98  
99    @Inject
100   private BuildContext buildContext;
101 
102   private File generatedResourcesFolder;
103   private List<File> i18nSourceFiles;
104 
105   @Override
106   public void execute() throws MojoExecutionException, MojoFailureException {
107     OutputFormat selectedOutputFormat = OutputFormat.valueOf(StringUtils.upperCase(outputFormat));
108     try {
109       File sourceDirectory = getSourceDirectory();
110       intialize(sourceDirectory);
111 
112       // skip incremental build if no i18n source file was changed
113       if (buildContext.isIncremental() && !isI18nSourceFileChanged(sourceDirectory)) {
114         return;
115       }
116 
117       List<File> sourceFiles = getI18nSourceFiles(sourceDirectory);
118       for (File file : sourceFiles) {
119         transformFile(file, selectedOutputFormat);
120       }
121     }
122     catch (IOException ex) {
123       throw new MojoFailureException("Failure to transform i18n resources", ex);
124     }
125   }
126 
127   private void transformFile(File file, OutputFormat selectedOutputFormat) throws MojoFailureException {
128     try {
129       // transform i18n files
130       String languageKey = FileUtils.removeExtension(file.getName());
131       I18nReader reader = getI18nReader(file);
132       SlingI18nMap i18nMap = new SlingI18nMap(languageKey, reader.read(file));
133 
134       // write mappings to target file
135       File targetFile = getTargetFile(file, selectedOutputFormat);
136       writeTargetI18nFile(i18nMap, targetFile, selectedOutputFormat);
137 
138       getLog().info("Transformed " + file.getPath() + " to  " + targetFile.getPath());
139     }
140     catch (IOException ex) {
141       throw new MojoFailureException("Unable to transform i18n resource: " + file.getPath(), ex);
142     }
143   }
144 
145   /**
146    * Checks if and i18n source file was changes in incremental build.
147    * @param sourceDirectory Source directory
148    * @return true if changes detected
149    */
150   private boolean isI18nSourceFileChanged(File sourceDirectory) {
151     Scanner scanner = buildContext.newScanner(sourceDirectory);
152     Scanner deleteScanner = buildContext.newDeleteScanner(sourceDirectory);
153     return isI18nSourceFileChanged(scanner) || isI18nSourceFileChanged(deleteScanner);
154   }
155 
156   private boolean isI18nSourceFileChanged(Scanner scanner) {
157     scanner.setIncludes(SOURCE_FILES_INCLUDES);
158     scanner.addDefaultExcludes();
159     scanner.scan();
160     return scanner.getIncludedFiles().length > 0;
161   }
162 
163   /**
164    * Initialize parameters, which cannot get defaults from annotations. Currently only the root nodes.
165    * @throws IOException I/O exception
166    */
167   private void intialize(File sourceDirectory) throws IOException {
168     getLog().debug("Initializing i18n plugin...");
169 
170     // resource
171     if (!getI18nSourceFiles(sourceDirectory).isEmpty()) {
172       File myGeneratedResourcesFolder = getGeneratedResourcesFolder();
173       addResource(myGeneratedResourcesFolder.getPath(), target);
174     }
175 
176   }
177 
178   private void addResource(String generatedResourcesDirectory, String targetPath) {
179 
180     // construct resource
181     Resource resource = new Resource();
182     resource.setDirectory(generatedResourcesDirectory);
183     resource.setTargetPath(targetPath);
184 
185     // add to build
186     Build build = this.project.getBuild();
187     build.addResource(resource);
188     getLog().debug("Added resource: " + resource.getDirectory() + " -> " + resource.getTargetPath());
189   }
190 
191   /**
192    * Fetches i18n source files from source directory.
193    * @param sourceDirectory Source directory
194    * @return a list of XML files
195    */
196   private List<File> getI18nSourceFiles(File sourceDirectory) throws IOException {
197 
198     if (i18nSourceFiles == null) {
199       if (!sourceDirectory.isDirectory()) {
200         i18nSourceFiles = Collections.emptyList();
201       }
202       else {
203         // get list of source files
204         String includes = StringUtils.join(SOURCE_FILES_INCLUDES, ",");
205         String excludes = FileUtils.getDefaultExcludesAsString();
206 
207         i18nSourceFiles = FileUtils.getFiles(sourceDirectory, includes, excludes);
208       }
209     }
210 
211     return i18nSourceFiles;
212   }
213 
214   /**
215    * Get directory containing source i18n files.
216    * @return directory containing source i18n files.
217    */
218   private File getSourceDirectory() throws IOException {
219     File file = new File(source);
220     if (!file.isDirectory()) {
221       getLog().debug("Could not find directory at '" + source + "'");
222     }
223     return file.getCanonicalFile();
224   }
225 
226   /**
227    * Writes mappings to file in Sling compatible JSON format.
228    * @param i18nMap mappings
229    * @param targetfile target file
230    * @param selectedOutputFormat Output format
231    */
232   private void writeTargetI18nFile(SlingI18nMap i18nMap, File targetfile, OutputFormat selectedOutputFormat) throws IOException {
233     switch (selectedOutputFormat) {
234       case XML:
235         FileUtils.fileWrite(targetfile, StandardCharsets.UTF_8.name(), i18nMap.getI18nXmlString());
236         break;
237       case PROPERTIES:
238         FileUtils.fileWrite(targetfile, StandardCharsets.ISO_8859_1.name(), i18nMap.getI18nPropertiesString());
239         break;
240       case JSON:
241         FileUtils.fileWrite(targetfile, StandardCharsets.UTF_8.name(), i18nMap.getI18nJsonString());
242         break;
243       case JSON_PROPERTIES:
244         FileUtils.fileWrite(targetfile, StandardCharsets.UTF_8.name(), i18nMap.getI18nJsonPropertiesString());
245         break;
246       default:
247         throw new IllegalArgumentException("Unsupported ouptut format: " + selectedOutputFormat);
248 
249     }
250     buildContext.refresh(targetfile);
251   }
252 
253   /**
254    * Get the JSON file for source file.
255    * @param sourceFile the source file
256    * @param selectedOutputFormat Output format
257    * @return File with name and path based on file parameter
258    */
259   private File getTargetFile(File sourceFile, OutputFormat selectedOutputFormat) throws IOException {
260 
261     File sourceDirectory = getSourceDirectory();
262     String relativePath = StringUtils.substringAfter(sourceFile.getAbsolutePath(), sourceDirectory.getAbsolutePath());
263     String relativeTargetPath = FileUtils.removeExtension(relativePath) + "." + selectedOutputFormat.getFileExtension();
264 
265     File jsonFile = new File(getGeneratedResourcesFolder().getPath() + relativeTargetPath);
266 
267     jsonFile = jsonFile.getCanonicalFile();
268 
269     File parentDirectory = jsonFile.getParentFile();
270     if (!parentDirectory.exists()) {
271       if (!parentDirectory.mkdirs()) {
272         throw new IOException("Unable to create directory: " + parentDirectory.getPath());
273       }
274       buildContext.refresh(parentDirectory);
275     }
276 
277     return jsonFile;
278   }
279 
280   private File getGeneratedResourcesFolder() throws IOException {
281     if (generatedResourcesFolder == null) {
282       generatedResourcesFolder = new File(this.project.getBuild().getDirectory(), generatedResourcesFolderPath);
283       if (!generatedResourcesFolder.exists()) {
284         if (!generatedResourcesFolder.mkdirs()) {
285           throw new IOException("Unable to create directory: " + generatedResourcesFolder.getPath());
286         }
287         buildContext.refresh(generatedResourcesFolder);
288       }
289     }
290     return generatedResourcesFolder;
291   }
292 
293   /**
294    * Get i18n reader for source file.
295    * @param sourceFile Source file
296    * @return I18n reader
297    */
298   private I18nReader getI18nReader(File sourceFile) throws MojoFailureException {
299     String extension = FileUtils.getExtension(sourceFile.getName());
300     if (Strings.CI.equals(extension, FILE_EXTENSION_PROPERTIES)) {
301       return new PropertiesI18nReader();
302     }
303     if (Strings.CI.equals(extension, FILE_EXTENSION_XML)) {
304       return new XmlI18nReader();
305     }
306     if (Strings.CI.equals(extension, FILE_EXTENSION_JSON)) {
307       return new JsonI18nReader();
308     }
309     throw new MojoFailureException("Unsupported file extension '" + extension + "': " + sourceFile.getAbsolutePath());
310   }
311 
312 }