
org.nuiton.i18n.plugin.GenerateI18nLabelsMojo Maven / Gradle / Ivy
Show all versions of i18n-maven-plugin Show documentation
package org.nuiton.i18n.plugin;
/*-
* #%L
* I18n :: Maven Plugin
* %%
* Copyright (C) 2007 - 2024 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%
*/
import com.google.common.collect.LinkedHashMultimap;
import io.ultreia.java4all.i18n.spi.bean.RegisterI18nLabel;
import io.ultreia.java4all.i18n.spi.bean.RegisterI18nLabels;
import io.ultreia.java4all.i18n.spi.builder.I18nKeySet;
import io.ultreia.java4all.i18n.spi.builder.I18nModule;
import io.ultreia.java4all.i18n.spi.type.TypeTranslators;
import io.ultreia.java4all.util.SortedProperties;
import org.apache.commons.lang3.tuple.Pair;
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.reflections.Reflections;
import org.reflections.Store;
import org.reflections.util.ClasspathHelper;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* To generate i18n labels mapping from any classes with the {@link RegisterI18nLabel} annotation on it.
*
* Created on 08/08/2021.
*
* @author Tony Chemit - [email protected]
* @since 4.0.0
*/
@Mojo(name = "generate-i18n-labels", threadSafe = true, defaultPhase = LifecyclePhase.GENERATE_RESOURCES, requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME)
public class GenerateI18nLabelsMojo extends I18nMojoSupport {
/**
* Build directory.
*/
@Parameter(property = "i18n.buildOutputDirectory", defaultValue = "${project.build.outputDirectory}")
private File buildOutputDirectory;
/**
* The root directory where to generate java code.
*/
@Parameter(property = "i18n.javaOutputDirectory", defaultValue = "${basedir}/src/main/resources", required = true)
private File resourceDirectory;
/**
* Strict mode to only keep in user i18n detected i18n keys and remove obsolete keys.
*
* Note : By default not active. Use this with care since it can
* delete keys. Moreover if this flag is activated, then all files will be parsed.
*/
@Parameter(property = "i18n.strictMode", defaultValue = "false")
private boolean strictMode;
/**
* I18n prefix to add to any generated i18n keys.
*/
@Parameter(property = "i18n.i18nPrefix", required = true)
private String i18nPrefix;
/**
* I18n modelName used to to add to any generated i18n keys.
*/
@Parameter(property = "i18n.targetFileName", defaultValue = "labels.properties", required = true)
private String targetFileName;
/**
* Package where to seek for i18n definitions.
*/
@Parameter(property = "i18n.packageName", required = true)
private String packageName;
/**
* Package where to seek for i18n definitions.
*/
@Parameter(property = "i18n.getterName", defaultValue = "labels", required = true)
private String getterName;
private I18nKeySet i18nKeysFile;
private I18nModule i18nModule;
@Override
protected void doAction() throws Exception {
if (strictMode) {
getLog().info("Use i18n strict mode.");
}
i18nModule = I18nModule.forGetter(getProject().getProperties());
i18nKeysFile = i18nModule.getModuleKeySet(getterName);
Path targetFile = resourceDirectory.toPath().resolve("META-INF").resolve("i18n").resolve(targetFileName);
SortedProperties originalStore = loadStore(targetFile);
SortedProperties store = new SortedProperties();
if (!strictMode) {
store.putAll(originalStore);
}
getLog().info(String.format("Loaded %d i18n label key(s).", store.size()));
URLClassLoader classLoader = initClassLoader(getProject(), buildOutputDirectory, true, false, true, true, true);
if (isVerbose()) {
getLog().info(String.format("Used classLoader with %d url(s).", classLoader.getURLs().length));
}
Reflections reflections = new Reflections(ClasspathHelper.forPackage(packageName, classLoader));
reflections.getTypesAnnotatedWith(RegisterI18nLabels.class);
reflections.getTypesAnnotatedWith(RegisterI18nLabel.class);
Store reflectionsStore = reflections.getStore();
Set annotations = new LinkedHashSet<>();
Set typesAnnotated = reflectionsStore.get("TypesAnnotated").get(RegisterI18nLabels.class.getName());
for (String s : typesAnnotated) {
Class> aClass = classLoader.loadClass(s);
RegisterI18nLabels annotation = aClass.getAnnotation(RegisterI18nLabels.class);
annotations.addAll(Arrays.asList(annotation.value()));
}
Set types2Annotated = reflectionsStore.get("TypesAnnotated").get(RegisterI18nLabel.class.getName());
for (String s : types2Annotated) {
Class> aClass = classLoader.loadClass(s);
RegisterI18nLabel annotation = aClass.getAnnotation(RegisterI18nLabel.class);
annotations.add(annotation);
}
// Set annotations = reflections.getTypesAnnotatedWith(RegisterI18nLabels.class).stream().map(t -> t.getAnnotation(RegisterI18nLabels.class)).flatMap(l -> Stream.of(l.value())).filter(Objects::nonNull).collect(Collectors.toSet());
// annotations.addAll(reflections.getTypesAnnotatedWith(RegisterI18nLabel.class).stream().map(t -> t.getAnnotation(RegisterI18nLabel.class)).filter(Objects::nonNull).collect(Collectors.toSet()));
getLog().info(String.format("Found %d i18n label(s) to register.", annotations.size()));
// to compute for a type, his dependencies
LinkedHashMultimap, Class>> dependencies = LinkedHashMultimap.create();
annotations.forEach(k -> dependencies.put(k.target(), k.target()));
LinkedHashMultimap, String> propertiesI18nLabels = LinkedHashMultimap.create();
LinkedHashMultimap, String> overridesI18nLabels = LinkedHashMultimap.create();
LinkedHashMultimap, String> offersI18nLabels = LinkedHashMultimap.create();
for (RegisterI18nLabel annotation : annotations) {
Class> aClass = annotation.target();
List properties = Arrays.asList(annotation.properties());
List offers = Arrays.asList(annotation.offers());
List overrides = Arrays.asList(annotation.overrides());
propertiesI18nLabels.putAll(aClass, properties);
overridesI18nLabels.putAll(aClass, overrides);
offersI18nLabels.putAll(aClass, offers);
dependencies.keySet().forEach(k -> {
if (k.isAssignableFrom(aClass)) {
dependencies.put(aClass, k);
}
});
}
List> dependenciesOrder = computeDependenciesOrder(dependencies);
LinkedHashMultimap, Pair> i18nLabels = computeI18nLabels(dependenciesOrder, dependencies, propertiesI18nLabels, offersI18nLabels, overridesI18nLabels);
getLog().info(String.format("Computed %d i18n key(s).", i18nLabels.values().size()));
for (Map.Entry, Collection>> entry : i18nLabels.asMap().entrySet()) {
String typeKey = getI18nTypeKey(entry.getKey());
for (Pair pair : entry.getValue()) {
String property = pair.getKey();
String i18nKey = getI18nPropertyKey(i18nPrefix, typeKey, property);
String realI18nKey = pair.getValue();
i18nKeysFile.addKey(realI18nKey);
if (!Objects.equals(i18nKey, realI18nKey)) {
// only store translations, the getter file has already registered the i18n key to keep
store.put(i18nKey, realI18nKey);
}
}
}
flush(targetFile, originalStore, store);
}
private List> computeDependenciesOrder(LinkedHashMultimap, Class>> dependencies) {
// all types to process
Set> todo = new LinkedHashSet<>(dependencies.keySet());
// remove self dependency
todo.forEach(k -> dependencies.remove(k, k));
List> result = new LinkedList<>();
int round = 0;
while (!todo.isEmpty()) {
getLog().info(String.format("Start round %d with %d type(s).", round, todo.size()));
Iterator> iterator = todo.iterator();
while (iterator.hasNext()) {
Class> k = iterator.next();
Set> classes = dependencies.get(k);
if (classes == null || result.containsAll(classes)) {
result.add(k);
iterator.remove();
}
}
getLog().info(String.format("End round %d with %d type(s).", round, todo.size()));
round++;
}
return result;
}
private LinkedHashMultimap, Pair> computeI18nLabels(List> dependenciesOrder, LinkedHashMultimap, Class>> dependenciesMap, LinkedHashMultimap, String> propertiesI18nLabels, LinkedHashMultimap, String> offersI18nLabels, LinkedHashMultimap, String> overridesI18nLabels) {
LinkedHashMultimap, Pair> result = LinkedHashMultimap.create();
for (Class> aClass : dependenciesOrder) {
String typeKey = getI18nTypeKey(aClass);
Set> dependencies = dependenciesMap.get(aClass);
boolean withDependencies = dependencies != null && !dependencies.isEmpty();
Set properties = propertiesI18nLabels.get(aClass);
// We need to produce exactly one entry for allProperties
Set allProperties = new LinkedHashSet<>(properties);
if (withDependencies) {
allProperties.addAll(propertiesI18nLabels.asMap().entrySet().stream().filter(e -> dependencies.contains(e.getKey())).flatMap(e -> e.getValue().stream()).collect(Collectors.toSet()));
}
Set offers = offersI18nLabels.get(aClass);
// offers is always associated to this type
registerDirectProperties(aClass, typeKey, offers, result);
allProperties.removeAll(offers);
Set overrides = overridesI18nLabels.get(aClass);
// overrides is always associated to this type
registerDirectProperties(aClass, typeKey, overrides, result);
allProperties.removeAll(overrides);
if (withDependencies) {
// register every property offered by dependency
registerDependencies(aClass, allProperties, dependencies, offersI18nLabels, result);
// register every property registered already for a dependency
registerDependencies(aClass, allProperties, dependencies, propertiesI18nLabels, result);
}
// the rest of properties are on this type
registerDirectProperties(aClass, typeKey, allProperties, result);
}
return result;
}
private void registerDirectProperties(Class> key,
String typeKey,
Set properties,
LinkedHashMultimap, Pair> result) {
for (String property : properties) {
String i18nKey = getI18nPropertyKey(i18nPrefix, typeKey, property);
result.put(key, Pair.of(property, i18nKey));
}
}
private void registerDependencies(Class> aClass,
Set properties,
Set> dependencies,
LinkedHashMultimap, String> map,
LinkedHashMultimap, Pair> result) {
map.asMap().entrySet().stream().filter(e -> dependencies.contains(e.getKey())).forEach(e -> {
Class> dependencyOffering = e.getKey();
Collection dependencyProperties = e.getValue();
String dependencyTypeKey = getI18nTypeKey(dependencyOffering);
for (String property : dependencyProperties) {
if (properties.contains(property)) {
result.put(aClass, Pair.of(property, getI18nPropertyKey(i18nPrefix, dependencyTypeKey, property)));
properties.remove(property);
}
}
});
}
public String getI18nTypeKey(Class> type) {
return TypeTranslators.getSimplifiedName(type);
}
public String getI18nPropertyKey(String i18nPrefix, String typeKey, String property) {
return i18nPrefix + typeKey + "." + property;
}
protected SortedProperties loadStore(Path targetFile) {
SortedProperties store = new SortedProperties();
if (Files.exists(targetFile)) {
try (BufferedReader reader = Files.newBufferedReader(targetFile)) {
store.load(reader);
} catch (IOException e) {
throw new IllegalStateException("Can't load properties from " + targetFile, e);
}
}
return store;
}
private void flush(Path targetFile, SortedProperties originalStore, SortedProperties store) throws IOException {
i18nModule.storeModuleKeySet(i18nKeysFile);
if (Files.exists(targetFile) && originalStore.equals(store)) {
getLog().info(String.format("No modification found at %s", targetFile));
return;
}
store(targetFile, store);
}
private void store(Path targetFile, SortedProperties store) {
getLog().info(String.format("will store i18n labels mapping at %s", targetFile));
if (Files.notExists(targetFile.getParent())) {
try {
Files.createDirectories(targetFile.getParent());
} catch (IOException e) {
throw new IllegalStateException("Can't create directories for " + targetFile, e);
}
}
try (BufferedWriter writer = Files.newBufferedWriter(targetFile)) {
store.store(writer, "Generated by " + getClass().getName());
} catch (IOException e) {
throw new IllegalStateException("Can't store i18n labels mapping at " + targetFile, e);
}
}
}