
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.i18n.plugin.AbstractI18nMojo;
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.bundleOutputDir", defaultValue = "${basedir}/target/generated-sources/resources", required = true)
private File bundleOutputDir;
/**
* 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(bundleOutputDir, "**/*.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 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", bundleOutputDir));
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