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

io.helidon.build.maven.cache.ProjectState Maven / Gradle / Ivy

There is a newer version: 4.0.16
Show newest version
/*
 * Copyright (c) 2021, 2024 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.maven.cache;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import io.helidon.build.common.Lists;
import io.helidon.build.common.Strings;
import io.helidon.build.common.xml.XMLElement;
import io.helidon.build.common.xml.XMLException;
import io.helidon.build.common.xml.XMLWriter;

import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Model;
import org.apache.maven.project.DefaultMavenProjectHelper;
import org.apache.maven.project.MavenProject;

import static java.util.function.Predicate.not;

/**
 * Project state.
 */
final class ProjectState {

    private static final DefaultMavenProjectHelper PROJECT_HELPER = new DefaultMavenProjectHelper();

    private final Properties properties;
    private final ArtifactEntry artifact;
    private final List attachedArtifacts;
    private final List compileSourceRoots;
    private final List testCompileSourceRoots;
    private final ProjectFiles projectFiles;
    private final List executions;
    private final Map> executionMatches;

    ProjectState(Properties properties,
                 ArtifactEntry artifact,
                 List attachedArtifacts,
                 List compileSourceRoots,
                 List testCompileSourceRoots,
                 ProjectFiles projectFiles,
                 List executions) {

        this.properties = Objects.requireNonNull(properties, "properties is null");
        this.artifact = artifact;
        this.attachedArtifacts = attachedArtifacts == null ? List.of() : attachedArtifacts;
        this.compileSourceRoots = compileSourceRoots == null ? List.of() : compileSourceRoots;
        this.testCompileSourceRoots = testCompileSourceRoots == null ? List.of() : testCompileSourceRoots;
        this.projectFiles = Objects.requireNonNull(projectFiles, "projectFiles is null");
        this.executions = executions == null ? List.of() : executions;
        this.executionMatches = new HashMap<>();
    }

    /**
     * Get the properties.
     *
     * @return properties, never {@code null}
     */
    Properties properties() {
        return properties;
    }

    /**
     * Get the artifact.
     *
     * @return ArtifactEntry, may be {@code null}
     */
    ArtifactEntry artifact() {
        return artifact;
    }

    /**
     * Get the attached artifacts.
     *
     * @return list, never {@code null}
     */
    List attachedArtifacts() {
        return attachedArtifacts;
    }

    /**
     * Get the compile source roots.
     *
     * @return list, never {@code null}
     */
    List compileSourceRoots() {
        return compileSourceRoots;
    }

    /**
     * Get the test compile source roots.
     *
     * @return list, never {@code null}
     */
    List testCompileSourceRoots() {
        return testCompileSourceRoots;
    }

    /**
     * Get the project files.
     *
     * @return project files, never {@code null}
     */
    ProjectFiles projectFiles() {
        return projectFiles;
    }

    /**
     * Get the executions.
     *
     * @return list, never {@code null}
     */
    List executions() {
        return executions;
    }

    /**
     * Load the project state from file.
     *
     * @param project       maven project
     * @param stateFileName state file name
     * @return state if state file exists, or {@code null}
     * @throws IOException  if an IO error occurs
     * @throws XMLException if a parsing error occurs
     */
    static ProjectState load(MavenProject project, String stateFileName) throws IOException, XMLException {
        return load(project.getModel().getProjectDirectory().toPath()
                .resolve(project.getModel().getBuild().getDirectory())
                .resolve(stateFileName));
    }

    /**
     * Load the project state from file.
     *
     * @param stateFile state file
     * @return state if state file exists, or {@code null}
     * @throws IOException  if an IO error occurs
     * @throws XMLException if a parsing error occurs
     */
    static ProjectState load(Path stateFile) throws IOException, XMLException {
        if (!Files.exists(stateFile)) {
            return null;
        }
        Properties properties = new Properties();
        XMLElement rootElt = XMLElement.parse(Files.newInputStream(stateFile));
        for (XMLElement elt : rootElt.childrenAt("properties", "property")) {
            String name = elt.attribute("name", null);
            String value = elt.attribute("value", null);
            if (Strings.isValid(name) && value != null) {
                properties.setProperty(name, value);
            }
        }
        ArtifactEntry artifact = rootElt.child("artifact").map(ProjectState::readArtifact).orElse(null);
        List attachedArtifacts = Lists.map(rootElt.childrenAt("attached-artifacts", "artifact"),
                ProjectState::readArtifact);
        List compileSourceRoots = rootElt.childrenAt("compile-source-roots", "path").stream()
                .map(XMLElement::value)
                .filter(Strings::isValid)
                .collect(Collectors.toList());
        List testCompileSourceRoots = rootElt.childrenAt("test-compile-source-roots", "path").stream()
                .map(XMLElement::value)
                .filter(Strings::isValid)
                .collect(Collectors.toList());

        ProjectFiles projectFiles = ProjectFiles.fromXml(rootElt.child("project-files").orElse(null));
        List executions = Lists.map(rootElt.childrenAt("executions", "execution"), e -> {
            Map attributes = e.attributes();
            return new ExecutionEntry(
                    attributes.get("groupId"),
                    attributes.get("artifactId"),
                    attributes.get("version"),
                    attributes.get("goal"),
                    attributes.get("id"),
                    e.child("configuration")
                            .map(XMLElement::detach)
                            .orElseGet(() -> XMLElement.builder().build()));
        });
        return new ProjectState(properties, artifact, attachedArtifacts, compileSourceRoots, testCompileSourceRoots,
                projectFiles, executions);
    }

    /**
     * Save the project state.
     *
     * @param project       Maven project
     * @param stateFileName state file name
     * @throws IOException if an IO error occurs
     */
    void save(MavenProject project, String stateFileName) throws IOException {
        Model model = project.getModel();
        Path buildDir = model.getProjectDirectory().toPath().resolve(model.getBuild().getDirectory());
        if (!Files.exists(buildDir)) {
            Files.createDirectories(buildDir);
        }
        save(buildDir.resolve(stateFileName));
    }

    /**
     * Save the project state.
     *
     * @param stateFile state file
     * @throws IOException if an IO error occurs
     */
    void save(Path stateFile) throws IOException {
        XMLWriter writer = new XMLWriter(Files.newBufferedWriter(stateFile));
        writer.prolog().startElement("project-state");

        writer.startElement("properties");
        properties.forEach((k, v) -> writer
                .startElement("property")
                .attribute("name", k.toString())
                .attribute("value", v.toString())
                .endElement());
        writer.endElement();

        if (artifact != null) {
            writeArtifact(writer, artifact);
        }

        writer.startElement("attached-artifacts");
        for (ArtifactEntry artifact : attachedArtifacts) {
            writeArtifact(writer, artifact);
        }
        writer.endElement();

        writer.startElement("compile-source-roots");
        for (String path : compileSourceRoots) {
            writer.startElement("path").value(path).endElement();
        }
        writer.endElement();

        writer.startElement("test-compile-source-roots");
        for (String path : testCompileSourceRoots) {
            writer.startElement("path").value(path).endElement();
        }
        writer.endElement();

        writer.startElement("project-files");
        writer.attribute("count", projectFiles.filesCount());
        writer.attribute("last-modified", projectFiles.lastModified());
        String checksum = projectFiles.checksum();
        if (checksum != null) {
            writer.attribute("checksum", checksum);
        }
        projectFiles.allChecksums().forEach((k, v) -> writer
                .startElement("file")
                .attribute("checksum", v)
                .value(k)
                .endElement());

        writer.endElement();

        writer.startElement("executions");
        for (ExecutionEntry execution : executions) {
            writer.startElement("execution")
                    .attribute("groupId", execution.groupId())
                    .attribute("artifactId", execution.artifactId())
                    .attribute("version", execution.version())
                    .attribute("goal", execution.goal())
                    .attribute("id", execution.executionId());
            writer.append(execution.config());
            writer.endElement();
        }
        writer.endElement();
        writer.endElement();
        writer.close();
    }

    private static ArtifactEntry readArtifact(XMLElement elt) {
        Map attributes = elt.attributes();
        return new ArtifactEntry(
                attributes.get("file"),
                attributes.get("type"),
                attributes.get("extension"),
                attributes.get("classifier"),
                attributes.get("language"),
                Boolean.parseBoolean(attributes.get("includesDependencies")),
                Boolean.parseBoolean(attributes.get("addedToClasspath")));
    }

    private static void writeArtifact(XMLWriter writer, ArtifactEntry artifact) {
        writer.startElement("artifact").attribute("file", artifact.file());
        writer.attribute("type", artifact.type());
        writer.attribute("extension", artifact.extension());
        String classifier = artifact.classifier();
        if (Strings.isValid(classifier)) {
            writer.attribute("classifier", classifier);
        }
        String language = artifact.language();
        if (Strings.isValid(language)) {
            writer.attribute("language", language);
        }
        writer.attribute("includesDependencies", artifact.includesDependencies());
        writer.attribute("addedToClasspath", artifact.addedToClasspath());
        writer.endElement();
    }

    /**
     * Apply this state to the given project.
     *
     * @param project Maven project
     * @param session Maven session
     */
    void apply(MavenProject project, MavenSession session) {
        Path projectDir = project.getModel().getProjectDirectory().toPath();
        properties.forEach((k, v) -> project.getProperties().put(k, loadPropValue(session, (String) v)));
        Optional.ofNullable(artifact)
                .map(a -> a.toArtifact(project))
                .ifPresent(project::setArtifact);
        compileSourceRoots
                .stream()
                .map(projectDir::resolve)
                .map(Object::toString)
                .filter(not(project.getCompileSourceRoots()::contains))
                .forEach(project::addCompileSourceRoot);
        testCompileSourceRoots
                .stream()
                .map(projectDir::resolve)
                .map(Object::toString)
                .filter(not(project.getTestCompileSourceRoots()::contains))
                .forEach(project::addTestCompileSourceRoot);
        attachedArtifacts
                .stream()
                .map(a -> a.toArtifact(project))
                .forEach(a -> PROJECT_HELPER.attachArtifact(project, a));
    }

    /**
     * Test if the given execution matches any of the ones in this state using
     * {@link ExecutionEntry#matches(ExecutionEntry)}.
     *
     * @param execution execution to match
     * @return {@code true} if found, {@code false} otherwise
     */
    boolean hasMatchingExecution(ExecutionEntry execution) {
        return findMatchingExecution(execution) != null;
    }

    /**
     * Find a recorded execution matching the given one using
     * {@link ExecutionEntry#matches(ExecutionEntry)}.
     *
     * @param execution execution to match
     * @return ExecutionEntry or {@code null} if not found
     */
    ExecutionEntry findMatchingExecution(ExecutionEntry execution) {
        return executionMatches.computeIfAbsent(execution, (key) -> {
            for (ExecutionEntry exec : executions) {
                if (exec.matches(key)) {
                    return Optional.of(exec);
                }
            }
            return Optional.empty();
        }).orElse(null);
    }

    /**
     * Merge two project states.
     *
     * @param state1 state1, must be non {@code null}
     * @param state2 state1, must be non {@code null}
     * @return ProjectState
     */
    static ProjectState merge(ProjectState state1, ProjectState state2) {
        Properties properties = new Properties();
        properties.putAll(state1.properties);
        properties.putAll(state2.properties);
        ProjectFiles projectFiles1 = state1.projectFiles;
        ProjectFiles projectFiles2 = state2.projectFiles;
        return new ProjectState(
                properties,
                Optional.ofNullable(state1.artifact).orElse(state2.artifact),
                Stream.of(state1.attachedArtifacts.stream(), state2.attachedArtifacts.stream())
                        .flatMap(Function.identity())
                        .distinct()
                        .collect(Collectors.toList()),
                Stream.of(state1.compileSourceRoots.stream(), state2.compileSourceRoots.stream())
                        .flatMap(Function.identity())
                        .distinct()
                        .collect(Collectors.toList()),
                Stream.of(state1.testCompileSourceRoots.stream(), state2.testCompileSourceRoots.stream())
                        .flatMap(Function.identity())
                        .distinct()
                        .collect(Collectors.toList()),
                projectFiles1.lastModified() > projectFiles2.lastModified() ? projectFiles1 : projectFiles2,
                Stream.of(state1.executions.stream(), state2.executions.stream())
                        .flatMap(Function.identity())
                        .distinct()
                        .collect(Collectors.toList()));
    }

    /**
     * Create a state for the given project and merge it with the existing state for this project.
     *
     * @param state           existing state, may be {@code null}
     * @param project         Maven project
     * @param session         Maven session
     * @param configManager   cache config manager
     * @param newExecutions   new executions
     * @param newProjectFiles current project files
     * @return ProjectState
     * @throws IOException if an IO error occurs while scanning project files
     */
    static ProjectState merge(ProjectState state,
                              MavenProject project,
                              MavenSession session,
                              CacheConfigManager configManager,
                              List newExecutions,
                              ProjectFiles newProjectFiles)
            throws IOException {

        Path projectDir = project.getModel().getProjectDirectory().toPath();
        List executions;
        Properties properties;
        if (state == null) {
            executions = List.of();
            properties = new Properties();
        } else {
            executions = state.executions;
            properties = state.properties;
        }
        Properties projectProps = new Properties();
        project.getProperties().forEach((k, v) -> projectProps.put(k, savePropValue(session, (String) v)));
        return new ProjectState(
                mergeProperties(properties, projectProps),
                Optional.ofNullable(project.getArtifact())
                        .map(a -> ArtifactEntry.create(a, project))
                        .orElse(null),
                project.getAttachedArtifacts()
                        .stream()
                        .map(a -> ArtifactEntry.create(a, project))
                        .collect(Collectors.toList()),
                project.getCompileSourceRoots()
                        .stream()
                        .map(Paths::get)
                        .map(projectDir::relativize)
                        .map(Path::toString)
                        .collect(Collectors.toList()),
                project.getTestCompileSourceRoots()
                        .stream()
                        .map(Paths::get)
                        .map(projectDir::relativize)
                        .map(Path::toString)
                        .collect(Collectors.toList()),
                newProjectFiles == null ? ProjectFiles.of(project, configManager) : newProjectFiles,
                Stream.concat(executions.stream().filter(exec -> newExecutions.stream().noneMatch(exec::matches)),
                        newExecutions.stream()).collect(Collectors.toList()));
    }

    private static String loadPropValue(MavenSession session, String value) {
        String rootDir = session.getRequest().getMultiModuleProjectDirectory().toPath().toString();
        return value.replace("#{root.dir}", rootDir);
    }

    private static String savePropValue(MavenSession session, String value) {
        String rootDir = session.getRequest().getMultiModuleProjectDirectory().toPath().toString();
        return value.replace(rootDir, "#{root.dir}");
    }

    private static Properties mergeProperties(Properties props1, Properties props2) {
        Properties properties = new Properties();
        properties.putAll(props1);
        properties.putAll(props2);
        return properties;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy