All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.nuiton.i18n.plugin.bundle.BundleMojo Maven / Gradle / Ivy

/*
 * #%L
 * I18n :: Maven Plugin
 * %%
 * Copyright (C) 2007 - 2017 Code Lutin, Ultreia.io
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Lesser Public License for more details.
 * 
 * You should have received a copy of the GNU General Lesser Public
 * License along with this program.  If not, see
 * .
 * #L%
 */

package org.nuiton.i18n.plugin.bundle;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.TreeSet;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Execute;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.nuiton.i18n.I18nDefinitionFile;
import org.nuiton.i18n.bundle.I18nBundleEntry;
import org.nuiton.i18n.bundle.I18nBundleUtil;
import org.nuiton.i18n.init.DefaultI18nInitializer;
import org.nuiton.io.SortedProperties;
import org.nuiton.plugin.PluginHelper;
import org.nuiton.version.Versions;

/**
 * Generate an aggregate i18n bundle for all dependencies of the project.
 * 

* The main idea is to have a final unique i18n bundle for an application to * launch, this really improve i18n loading time to have a unique named bundle, * no need to seek in all dependencies... *

* Moreover, this permits also to deal with order of i18n keys, more precisely, * we want to use the higher level i18n key for an application. If the i18n * key is present on a library, we want to be able to override it in * application (or user wants it:)). *

* This goal permits this using the dependencies graph order of artifacts. * * @author Tony Chemit - [email protected] * @since 0.12 */ @Mojo(name = "bundle", defaultPhase = LifecyclePhase.GENERATE_RESOURCES, requiresDependencyResolution = ResolutionScope.RUNTIME) @Execute(goal = "collect-i18n-artifacts") public class BundleMojo extends AbstractI18nBundleMojo { /** * Encoding used to load any i18n property files. *

* If not defined, will use the {@link #encoding} parameter. * * @since 2.4 */ @Parameter(property = "i18n.bundleInputEncoding") private String bundleInputEncoding; /** * Encoding used to write any i18n property files. *

* If not defined, will use the {@link #encoding} parameter. * * @since 2.4 */ @Parameter(property = "i18n.bundleOutputEncoding") private String bundleOutputEncoding; /** * Root directory where to generate aggregated bundles (this directory will * be added as resources of the project). * * @since 1.0.0 */ @Parameter(property = "i18n.bundleOutputDirectory", defaultValue = "${project.build.outputDirectory}", required = true) private File bundleOutputDirectory; /** * Package name of the generate aggregated bundles. *

* Note: By default we use the META-INF package * since it is the favorite package of I18n runtime initializer. *

* The package name is dotted as it will be stored as folder like in Java * language. *

* Example : *

     *     package name : foo.bar
     *     directory    : foo/bar
     * 
* * @since 2.3.2 */ @Parameter(property = "i18n.bundleOutputPackage", defaultValue = "META-INF", required = true) private String bundleOutputPackage; /** * Name of the bundle to generate. * * @since 1.0.2 */ @Parameter(property = "i18n.bundleOutputName", defaultValue = "${project.artifactId}-i18n", required = true) private String bundleOutputName; /** * A flag to generate a bundle with the first locale defined as a default * bundle (with no locale specialization). * * @since 2.1 */ @Parameter(property = "i18n.generateDefaultLocale", defaultValue = "false") private boolean generateDefaultLocale; /** * A flag to check that bundles are complete (no missing i18n translations). *

* Note : This behaviour will be activated is {@link #failsIfWarning} is on. * * @since 1.0.0 */ @Parameter(property = "i18n.checkBundle", defaultValue = "true") private boolean checkBundle; /** * A flag to show missing i18n translation. *

* Note : Need the {@link #checkBundle} to be activated). * * @since 1.0.0 */ @Parameter(property = "i18n.showEmpty", defaultValue = "false") private boolean showEmpty; /** * A flag to make the build fails if there is some warnings while generating * bundle, says when it misses some translations. *

* Note : This parameter should be used in a release profile to * ensure bundles are complete. * * @since 2.0 * @deprecated since 3.6.1, prefer use now {@link #failsIfAnyKeyMissingValue} or {@link #failsIfAnyKeyMissingInBundle}. */ @Parameter(property = "i18n.failsIfWarning", defaultValue = "false") @Deprecated private boolean failsIfWarning; /** * A flag to make the build fails if there is some missing key values. *

* Note : This parameter should be used in a release profile to ensure bundles are complete. * * @since 3.6.1 */ @Parameter(property = "i18n.failsIfAnyKeyMissingValue", defaultValue = "false") private boolean failsIfAnyKeyMissingValue; /** * A flag to make the build fails if there is some missing keys. *

* Note : This parameter should be used in a release profile to ensure bundles are complete. * * @since 3.6.1 */ @Parameter(property = "i18n.failsIfAnyKeyMissingInBundle", defaultValue = "false") private boolean failsIfAnyKeyMissingInBundle; /** * Contains validation result after {@link #checkBundle(Locale, Properties, boolean, BundleValidation)}. *

* May be null if validation is disabled. * * @since 3.5 */ private BundleValidation bundleValidation; /** * The definitive directory where to generate the bundles (includes the * package of bundle). * * @since 2.3.2 */ private File outputFolder; /** * A flag to generate the i18n definition file. *

* This file contains all generated bundles and the paths of all i18n * artifacts used to make it. * * @since 2.0 */ @Parameter(property = "i18n.generateDefinitionFile", defaultValue = "true") private boolean generateDefinitionFile; /** * Converter used to change format of bundles. * * @since 2.4 */ @Parameter(property = "i18n.bundleFormatConverter") private String bundleFormatConverter; /** * Templates to register in the i18n definition file in the template.list property. *

* This is used by the I18n Editor. * * @since 3.6.4 */ @Parameter(property = "i18n.template.list") private List templateList; /** * Templates extension to register in i18n definition defile in the template.extension property. * This is used by the I18n Editor. * * @since 3.6.4 */ @Parameter(property = "i18n.template.extension") private String templateExtension; /** * Map of all availables {@link BundleFormatConverter}. * * @since 2.4 */ @Component(role = BundleFormatConverter.class) private Map bundleFormatConverters; /** Format converter to apply if */ private BundleFormatConverter converter; @Override public void init() throws Exception { super.init(); if (failsIfWarning) { // check bundle if wants to fail on unsafe bundles checkBundle = true; bundleValidation = new BundleValidation(locales); } else { bundleValidation = null; } // get the definitive folder where to generate bundles (including // bundle package) outputFolder = getBundleOutputFolder(); if (isVerbose()) { getLog().info(String.format("Will generates bundles in %s", outputFolder)); } createDirectoryIfNecessary(outputFolder); if (StringUtils.isEmpty(bundleInputEncoding)) { // use the default encoding bundleInputEncoding = getEncoding(); if (getLog().isDebugEnabled()) { getLog().debug(String.format("Use as input encoding the default one : %s", bundleInputEncoding)); } } if (StringUtils.isEmpty(bundleOutputEncoding)) { // use the default encoding bundleOutputEncoding = getEncoding(); if (getLog().isDebugEnabled()) { getLog().debug(String.format("Use as output encoding the default one : %s", bundleOutputEncoding)); } } // add root bundle directory as resources of the project addResourceDir(bundleOutputDirectory, "**/*.properties"); if (StringUtils.isNotEmpty(bundleFormatConverter)) { // get converter from universe converter = bundleFormatConverters.get(bundleFormatConverter); if (converter == null) { // unknown converter throw new MojoExecutionException( String.format("There is no bundleFormatConverter named \"%s\", known ones are %s", bundleFormatConverter, bundleFormatConverters.keySet())); } } } @Override protected boolean checkSkip() { boolean result = super.checkSkip(); if (result && !needInvoke(true, false, getProjectCacheKey())) { getLog().info("Skip - already executed."); result = false; } return result; } @Override protected void doAction() throws Exception { long t00 = System.nanoTime(); String version = getProject().getVersion(); version = PluginHelper.removeSnapshotSuffix(version); String inputEncoding = getBundleInputEncoding(); String outputEncoding = getBundleOutputEncoding(); if (!silent) { getLog().info(String.format("config - resources dir : %s", bundleOutputDirectory)); getLog().info(String.format("config - package name : %s", bundleOutputPackage)); getLog().info(String.format("config - bundle name : %s", bundleOutputName)); getLog().info(String.format("config - input encoding : %s", inputEncoding)); getLog().info(String.format("config - output encoding : %s", outputEncoding)); if (bundleFormatConverter != null) { getLog().info(String.format("config - format converter : %s", bundleFormatConverter)); } getLog().info(String.format("config - locales : %s", Arrays.toString(locales))); getLog().info(String.format("config - version : %s", version)); } Map bundleDico = new LinkedHashMap<>(locales.length); for (Locale locale : locales) { long t0 = System.nanoTime(); File bundleOut = getI18nFile(outputFolder, bundleOutputName, locale, false); SortedProperties propertiesOut = new SortedProperties(outputEncoding, false); StringBuilder buffer = new StringBuilder(); URL[] urls = getCollectI18nResources(locale); if (urls.length == 0) { getLog().warn(String.format("no bundle for locale %s", locale)); continue; } if (!silent) { getLog().info(String.format("generate bundle for locale %s from %d i18n resource(s)", locale, urls.length)); } List bundlesUrls = new ArrayList<>(); Charset loadEncoding = Charset.forName(inputEncoding); for (URL url : urls) { long t000 = System.nanoTime(); I18nBundleEntry bundleEntry = new I18nBundleEntry(url, locale, null); bundleEntry.load(propertiesOut, loadEncoding); String strPath = bundleEntry.getPath().toString(); int index = strPath.indexOf("i18n/"); String str = strPath.substring(index); bundlesUrls.add(str); buffer.append(',').append(str); if (verbose) { getLog().info( String.format("loaded %s in %s", bundleEntry.getPath(), PluginHelper.convertTime(t000, System.nanoTime()))); } } if (!bundlesUrls.isEmpty()) { bundleDico.put(locale, buffer.substring(1)); if (!silent) { if (getLog().isDebugEnabled()) { getLog().debug(String.format("%d i18n resource(s) detected", bundlesUrls.size())); } for (String u : bundlesUrls) { getLog().info(u); } } } // Apply conversion if necessary, depends on input bundleFormatConverter if (converter != null) { applyConversion(propertiesOut); } propertiesOut.store(bundleOut); if (!silent && verbose) { getLog().info(String.format("bundle created in %s (detected sentences : %d)", PluginHelper.convertTime(t0, System.nanoTime()), propertiesOut.size())); } if (checkBundle) { checkBundle(locale, propertiesOut, showEmpty, bundleValidation); } } failsIfAnyKeyMissingValue(failsIfWarning || failsIfAnyKeyMissingValue, bundleValidation); failsIfAnyKeyMissingInBundle(failsIfAnyKeyMissingInBundle, bundleValidation); if (generateDefaultLocale) { generateDefaultBundle(); } if (generateDefinitionFile) { generateDefinitionFile(version, bundleDico); } if (!silent && verbose) { getLog().info("done in " + PluginHelper.convertTime(t00, System.nanoTime())); } } private File getBundleFile(File root, String artifactId, Locale locale, boolean create) throws IOException { return getI18nFile(root, artifactId, locale, create); } private void generateDefinitionFile(String version, Map bundleDico) throws IOException { // ecriture du ficher des definitions i18n (permet de faire une // recherche exacte sur un fichier puis d'en deduire les bundles a // charger String f = String.format(DefaultI18nInitializer.UNIQUE_BUNDLE_DEF, bundleOutputName); File defOut = new File(outputFolder, f); if (!silent) { getLog().info(String.format("prepare i18n definition file in %s", defOut.getAbsolutePath())); } Multimap abundles = ArrayListMultimap.create(); for (Entry e : bundleDico.entrySet()) { String value = e.getValue(); abundles.putAll(e.getKey(), Arrays.asList(value.split(","))); } I18nDefinitionFile definitionFile = new I18nDefinitionFile( bundleOutputName, Charset.forName(encoding), Versions.valueOf(version), new LinkedHashSet<>(Arrays.asList(locales)), templateExtension, templateList == null ? Collections.emptySortedSet() : new TreeSet<>(templateList), abundles ); definitionFile.store(outputFolder); } @Override protected URL[] getCollectI18nResources(Locale locale) throws IOException { File file = getCollectOutputFile(locale, false); if (!file.exists()) { return I18nBundleUtil.EMPTY_URL_ARRAY; } return PluginHelper.getLinesAsURL(file); } /** * Apply conversion over {@code properties} with internal converter. * * @param properties Properties to walk through * @since 2.4 */ private void applyConversion(Properties properties) { for (Entry entry : properties.entrySet()) { String convertedValue = converter.convert((String) entry.getValue()); properties.setProperty((String) entry.getKey(), convertedValue); } } /** * Generates the default bundle, says the bundle with no locale specialized. *

* This bundle is a copy of the bundle of the first locale (which in fact * is considered as the main locale). * * @throws IOException if any IO problem while the copy. * @since 2.1 */ private void generateDefaultBundle() throws IOException { File bundleFirstLocale = getBundleFile(outputFolder, bundleOutputName, locales[0], false); File bundleWithoutLocale = getBundleFile(outputFolder, bundleOutputName, null, false); if (!isSilent()) { getLog().info(String.format("Generate default bundle at %s", bundleWithoutLocale)); } FileUtils.copyFile(bundleFirstLocale, bundleWithoutLocale); } private File getBundleOutputFolder() { File result = bundleOutputDirectory; if (StringUtils.isNotEmpty(bundleOutputPackage)) { String[] paths = bundleOutputPackage.split("\\."); for (String path : paths) { result = new File(result, path); } } return result; } private String getBundleOutputEncoding() { return bundleOutputEncoding; } private String getBundleInputEncoding() { return bundleInputEncoding; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy