SpiWorkspaceReaderAdapter.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.maven.resolver;

import java.io.File;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;

import org.apache.maven.api.ArtifactCoordinates;
import org.apache.maven.api.Version;
import org.apache.maven.api.annotations.Nonnull;
import org.apache.maven.api.model.Model;
import org.apache.maven.impl.resolver.MavenWorkspaceReader;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.repository.WorkspaceRepository;

/**
 * Bridge adapter that wraps an {@link org.apache.maven.api.spi.WorkspaceReader SPI WorkspaceReader}
 * into a {@link MavenWorkspaceReader}.
 *
 * <p>This adapter translates between resolver {@link Artifact} and Maven API
 * {@link org.apache.maven.api.Artifact} types, allowing SPI implementations to work
 * with Maven 4 API types exclusively while being integrated into the resolver's
 * workspace reader chain. By implementing {@link MavenWorkspaceReader}, the adapter
 * also participates in model resolution: {@link #findModel(Artifact)} delegates directly
 * to {@link org.apache.maven.api.spi.WorkspaceReader#findModel(org.apache.maven.api.Artifact)},
 * which returns a pre-built {@link Model} (bypassing {@code ModelProcessor} entirely).
 *
 * @since 4.2.0
 */
public class SpiWorkspaceReaderAdapter implements MavenWorkspaceReader {

    private final org.apache.maven.api.spi.WorkspaceReader delegate;
    private final WorkspaceRepository repository;

    public SpiWorkspaceReaderAdapter(org.apache.maven.api.spi.WorkspaceReader delegate) {
        this.delegate = delegate;
        this.repository = new WorkspaceRepository("spi-" + delegate.getClass().getSimpleName());
    }

    @Override
    public WorkspaceRepository getRepository() {
        return repository;
    }

    @Override
    public File findArtifact(Artifact artifact) {
        Optional<Path> result = delegate.findArtifact(toApiArtifact(artifact));
        return result.map(Path::toFile).orElse(null);
    }

    @Override
    public List<String> findVersions(Artifact artifact) {
        return delegate.findVersions(toApiArtifact(artifact));
    }

    @Override
    public Model findModel(Artifact artifact) {
        return delegate.findModel(toApiArtifact(artifact)).orElse(null);
    }

    /**
     * Returns the underlying SPI workspace reader.
     */
    public org.apache.maven.api.spi.WorkspaceReader getDelegate() {
        return delegate;
    }

    /**
     * Creates a lightweight Maven API {@link org.apache.maven.api.Artifact} from a resolver artifact
     * without requiring an active session.
     */
    private static org.apache.maven.api.Artifact toApiArtifact(Artifact artifact) {
        return new LightweightApiArtifact(artifact);
    }

    /**
     * A lightweight implementation of {@link org.apache.maven.api.Artifact} that wraps a resolver artifact
     * for the purpose of passing artifact coordinates to SPI workspace readers.
     */
    private static class LightweightApiArtifact implements org.apache.maven.api.Artifact {
        private final Artifact artifact;
        private final String key;

        LightweightApiArtifact(Artifact artifact) {
            this.artifact = artifact;
            this.key = artifact.getGroupId()
                    + ':'
                    + artifact.getArtifactId()
                    + ':'
                    + artifact.getExtension()
                    + (artifact.getClassifier().isEmpty() ? "" : ":" + artifact.getClassifier())
                    + ':'
                    + artifact.getVersion();
        }

        @Override
        public String key() {
            return key;
        }

        @Nonnull
        @Override
        public String groupId() {
            return artifact.getGroupId();
        }

        @Nonnull
        @Override
        public String artifactId() {
            return artifact.getArtifactId();
        }

        @Nonnull
        @Override
        public Version version() {
            return new StringVersion(artifact.getVersion());
        }

        @Nonnull
        @Override
        public Version baseVersion() {
            return new StringVersion(artifact.getBaseVersion());
        }

        @Nonnull
        @Override
        public String extension() {
            return artifact.getExtension();
        }

        @Nonnull
        @Override
        public String classifier() {
            return artifact.getClassifier();
        }

        @Override
        public boolean isSnapshot() {
            return artifact.isSnapshot();
        }

        @Nonnull
        @Override
        public ArtifactCoordinates toCoordinates() {
            throw new UnsupportedOperationException("Lightweight artifact wrapper does not support toCoordinates(); "
                    + "use Session.createArtifactCoordinates() instead");
        }

        @Override
        public boolean equals(Object o) {
            return o instanceof org.apache.maven.api.Artifact a && key.equals(a.key());
        }

        @Override
        public int hashCode() {
            return key.hashCode();
        }

        @Override
        public String toString() {
            return key;
        }
    }

    /**
     * Simple {@link Version} implementation that wraps a version string.
     *
     * <p><b>Note on ordering:</b> {@link #compareTo(Version)} uses lexicographic string
     * comparison, which does <em>not</em> give correct semantic version ordering for
     * multi-digit components (e.g. {@code "10"} sorts before {@code "9"}).  This wrapper
     * is used solely to pass artifact coordinates to SPI workspace readers; callers should
     * not rely on {@code compareTo} for correct Maven version ordering.  Use a proper
     * {@link org.apache.maven.api.services.VersionParser} when version ordering matters.</p>
     */
    private record StringVersion(String version) implements Version {
        @Override
        public int compareTo(Version o) {
            return version.compareTo(o.toString());
        }

        @Override
        public String toString() {
            return version;
        }
    }
}