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

io.helidon.build.services.ServicesMojo Maven / Gradle / Ivy

/*
 * Copyright (c) 2021 Oracle and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.helidon.build.services;

import java.io.File;
import java.io.IOException;
import java.lang.module.ModuleDescriptor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import io.helidon.build.util.Log;
import io.helidon.build.util.MavenLogWriter;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
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.project.MavenProject;

/**
 * Service provider plugin.
 * This plugin generates {@code META-INF/services} records based on
 * {@code module-info.java} files.
 */
@Mojo(name = "services",
      defaultPhase = LifecyclePhase.COMPILE,
      threadSafe = true)
public class ServicesMojo extends AbstractMojo {
    /**
     * Skip execution of this plugin.
     */
    @Parameter(defaultValue = "false", property = "helidon.services.skip")
    private boolean skip;

    /**
     * Source directory.
     */
    @Parameter(defaultValue = "${project.build.sourceDirectory}")
    private File javaDirectory;

    /**
     * Directory where the {@code classes} files reside.
     */
    @Parameter(defaultValue = "${project.build.outputDirectory}")
    private File targetDirectory;

    /**
     * Source directory for resources.
     */
    @Parameter(defaultValue = "${project.basedir}")
    private File baseDir;

    /**
     * Source directory for resources.
     */
    @Parameter(defaultValue = "${project.basedir}/src/main/resources")
    private File resourceDirectory;

    /**
     * Whether to fail if there is no module-info.java present.
     */
    @Parameter(property = "failOnMissingModuleInfo", defaultValue = "true")
    private boolean failOnMissingModuleInfo;

    /**
     * Merge mode.
     * Possible options:
     * 
    *
  • overwrite - use only values from module-info.java, delete any existing files in output
  • *
  • ignore - use module-info.java, unless there are existing META-INF/services, in that case ignore module-info.java
  • *
  • fail - fail if there is any existing META-INF/services record (default)
  • *
  • clean - delete existing META-INF/services in source directory if they are in module-info, fail otherwise * only works with the default resource directory in {@code src/main/resources}
  • *
  • validate - validate that module-info.java and META-INF/services contain the same records
  • *
*/ @Parameter(property = "mode", defaultValue = "fail") private String mode; /** * The Maven project this mojo executes on. */ @Parameter(defaultValue = "${project}", readonly = true, required = true) private MavenProject project; /** * The comment used in the generated files. */ @Parameter(property = "comment", defaultValue = "# This file was generated by Helidon services Maven plugin.") private String comment; /** * Whether to add comment to the generated files. */ @Parameter(property = "addComment", defaultValue = "true") private boolean addComment; @Override public void execute() throws MojoExecutionException, MojoFailureException { MavenLogWriter.install(getLog()); if (skip) { Log.info("Skipping execution."); return; } if ("pom".equals(project.getPackaging())) { Log.info("Skipping POM packaging project."); return; } Path sourcePath = javaDirectory.toPath(); if (Files.notExists(sourcePath)) { Log.info("$(YELLOW Skipping project with no sources): " + sourcePath); return; } Path targetPath = targetDirectory.toPath(); Path moduleInfo = targetPath.resolve("module-info.class"); if (Files.notExists(moduleInfo)) { if (failOnMissingModuleInfo) { throw new MojoExecutionException("Project does not contain module-info.class: " + moduleInfo); } Log.info("$(YELLOW Skipping project with no module-info.)"); return; } // now we have module info, let's validate existing files Path metaInfServices = targetPath.resolve("META-INF/services"); if ("validate".equals(mode)) { validate(moduleInfo, existing(metaInfServices)); return; } if (Files.exists(metaInfServices)) { if ("overwrite".equals(mode)) { deleteExisting(metaInfServices); } else { List existing = existing(metaInfServices); if ("ignore".equals(mode) && !existing.isEmpty()) { Log.info("Ignoring module-info.java, as there are existing META-INF/services"); Log.verbose("Existing files: " + existing); return; } else if ("clean".equals(mode) && !existing.isEmpty()) { clean(metaInfServices, moduleInfo, existing); } else if (!existing.isEmpty()) { throw new MojoExecutionException("There are existing META-INF/services files: " + existing); } } } try { createServices(metaInfServices, moduleInfo); } catch (IOException e) { throw new MojoFailureException("Failed to create META-INF/services records for " + moduleInfo, e); } } private void validate(Path moduleInfo, List existing) throws MojoFailureException, MojoExecutionException { Map fromModuleInfo = loadModuleInfo(moduleInfo); Map fromMetaInf = loadProviders(existing); if (fromModuleInfo.isEmpty() && fromMetaInf.isEmpty()) { Log.info("There are no services defined in module."); return; } List problems = new LinkedList<>(); // first let's find all missing from module-info.java fromMetaInf.forEach((service, provider) -> { if (!fromModuleInfo.containsKey(service)) { problems.add("Service " + service + " missing from module-info.java, providers: " + provider.providers()); } else { Provider moduleInfoProvider = fromModuleInfo.get(service); if (!moduleInfoProvider.providers().equals(provider.providers)) { Set missing = new LinkedHashSet<>(provider.providers()); moduleInfoProvider.providers().forEach(missing::remove); if (!missing.isEmpty()) { problems.add("Service " + service + " is missing the following providers in module-info.java: " + missing); } } } }); // now let's find all missing from META-INF fromModuleInfo.forEach((service, provider) -> { if (!fromMetaInf.containsKey(service)) { problems.add("Service " + service + " missing from META-INF/services, providers: " + provider.providers()); } else { Provider metaInfProvider = fromMetaInf.get(service); if (!metaInfProvider.providers().equals(provider.providers)) { Set missing = new LinkedHashSet<>(provider.providers()); metaInfProvider.providers().forEach(missing::remove); if (!missing.isEmpty()) { problems.add("Service " + service + " is missing the following providers in META-INF/services: " + missing); } } } }); if (!problems.isEmpty()) { throw new MojoExecutionException("Mismatch between module-info.java and META-INF/services:\n" + String.join("\n", problems)); } Log.info("Services are valid."); } private void clean(Path metaInfServices, Path moduleInfo, List existing) throws MojoFailureException, MojoExecutionException { // existing is not empty Map fromModuleInfo = loadModuleInfo(moduleInfo); if (fromModuleInfo.isEmpty()) { throw new MojoExecutionException("module-info.java does not define any service providers, yet the following providers" + " are defined in META-INF/services: " + relativize(metaInfServices, existing)); } Map fromMetaInf = loadProviders(existing); List problems = new LinkedList<>(); // I just care about stuff that is in meta inf, but missing in module info. Inverse is OK. fromMetaInf.forEach((service, provider) -> { if (!fromModuleInfo.containsKey(service)) { problems.add("Service " + service + " missing from module-info.java, providers: " + provider.providers()); } else { Provider moduleInfoProvider = fromModuleInfo.get(service); if (!moduleInfoProvider.providers().equals(provider.providers)) { Set missing = new LinkedHashSet<>(provider.providers()); moduleInfoProvider.providers().forEach(missing::remove); if (!missing.isEmpty()) { problems.add("Service " + service + " is missing the following providers in module-info.java: " + missing); } } } }); if (!problems.isEmpty()) { throw new MojoExecutionException("Mismatch between module-info.java and META-INF/services:\n" + String.join("\n", problems)); } // if we got here, the information in module-info.java is a superset of META-INF/services, we can safely delete all // META-INF/services in sources Path resourcePath = resourceDirectory.toPath(); Path srcMetaInfServices = resourcePath.resolve("META-INF/services"); if (!Files.exists(srcMetaInfServices)) { throw new MojoExecutionException("Cannot clean META-INF/services in sources, as path does not exist: " + srcMetaInfServices); } Path basePath = baseDir.toPath(); for (Path path : existing) { Path source = srcMetaInfServices.resolve(path.getFileName()); if (!Files.exists(source)) { throw new MojoExecutionException("Cannot clean META-INF/services, file " + path + " exists in output," + " but file " + source + " does not exist in sources"); } Log.info("Deleting source file " + basePath.relativize(source)); try { Files.delete(source); } catch (IOException e) { throw new MojoFailureException("Failed to delete file " + source, e); } } try { Files.delete(srcMetaInfServices); } catch (IOException e) { throw new MojoFailureException("Failed to delete directory " + srcMetaInfServices, e); } } private List relativize(Path metaInfServices, List existing) { List relative = new ArrayList<>(existing.size()); for (Path path : existing) { relative.add(metaInfServices.relativize(path).toString()); } return relative; } private Map loadProviders(List existing) throws MojoFailureException { Map providerMap = new HashMap<>(); for (Path path : existing) { String service = path.getFileName().toString(); try { List lines = Files.readAllLines(path); List providers = new ArrayList<>(lines.size()); for (String line : lines) { if (line.startsWith("#")) { continue; } if (line.isBlank()) { continue; } providers.add(line); } providerMap.put(service, Provider.create(service, providers)); } catch (IOException e) { throw new MojoFailureException("Failed to read service provider definition: " + path); } } return providerMap; } private void createServices(Path metaInfServices, Path moduleInfo) throws IOException, MojoFailureException { // make sure the directories exist metaInfServices = Files.createDirectories(metaInfServices); // now we can safely create the new META-INF/services Map provides = loadModuleInfo(moduleInfo); if (provides.isEmpty()) { Log.info("There are no services provided by this module."); return; } for (Provider provide : provides.values()) { Path serviceFile = metaInfServices.resolve(provide.service()); List providers = new ArrayList<>(); if (addComment) { providers.add(comment); } providers.addAll(provide.providers()); Log.verbose("Creating service file " + serviceFile); Log.debug("File lines: \n" + String.join("\n", providers)); Files.write(serviceFile, providers, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); } Log.info("Created $(CYAN " + provides.size() + ") META-INF/services files"); } private Map loadModuleInfo(Path moduleInfo) throws MojoFailureException { try { ModuleDescriptor module = ModuleDescriptor.read(Files.newInputStream(moduleInfo)); return module.provides() .stream() .map(Provider::create) .collect(Collectors.toMap(Provider::service, Function.identity())); } catch (IOException e) { throw new MojoFailureException("Failed to load module descriptor " + moduleInfo, e); } } private List existing(Path metaInfServices) throws MojoFailureException { if (!Files.exists(metaInfServices)) { return List.of(); } try { return Files.list(metaInfServices) .collect(Collectors.toList()); } catch (IOException e) { throw new MojoFailureException("Failed to list META-INF/services directory " + metaInfServices, e); } } private void deleteExisting(Path metaInfServices) throws MojoFailureException { try { for (Path path : existing(metaInfServices)) { Files.delete(path); } } catch (IOException e) { throw new MojoFailureException("Failed to delete existing META-INF/services records", e); } } static final class Provider { private final String service; private final Set providers; private Provider(String service, List providers) { this.service = service; this.providers = new LinkedHashSet<>(providers); } static Provider create(ModuleDescriptor.Provides provides) { String service = provides.service(); List providers = provides.providers(); return new Provider(service, providers); } public static Provider create(String service, List providers) { return new Provider(service, providers); } String service() { return service; } Set providers() { return providers; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy