Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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.api.spi;

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

import org.apache.maven.api.Artifact;
import org.apache.maven.api.annotations.Consumer;
import org.apache.maven.api.annotations.Experimental;
import org.apache.maven.api.di.Named;

/**
* SPI for IDE and tool integrators to provide workspace artifact resolution.
*
* <p>Implementations are discovered via DI and are automatically bridged into the
* resolver's workspace reader chain, replacing the need for the internal
* {@code @Named("ide")} resolver {@code WorkspaceReader} mechanism.
*
* <p>Implementations should be annotated with {@link Named} and registered as
* Maven core extensions (via {@code .mvn/extensions.xml}).
*
* <p>Unlike the legacy {@code org.eclipse.aether.repository.WorkspaceReader}, this SPI
* uses Maven 4 API types only (no maven-resolver-api dependency required).
*
* @since 4.1.0
*/
@Experimental
@Consumer
@Named
public interface WorkspaceReader extends SpiService {

/**
* Finds the path on disk for the given artifact in the workspace, if present.
*
* @param artifact the artifact to look up, never {@code null}
* @return the path to the artifact file, or empty if not found in workspace
*/
Optional<Path> findArtifact(Artifact artifact);

/**
* Returns the list of available versions for the given artifact in the workspace.
*
* @param artifact the artifact to look up (version is ignored), never {@code null}
* @return list of available versions, may be empty
*/
List<String> findVersions(Artifact artifact);
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
import org.apache.maven.project.MavenProject;
import org.apache.maven.resolver.MavenChainedWorkspaceReader;
import org.apache.maven.resolver.RepositorySystemSessionFactory;
import org.apache.maven.resolver.SpiWorkspaceReaderAdapter;
import org.apache.maven.session.scope.internal.SessionScope;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.RepositorySystemSession.CloseableSession;
Expand Down Expand Up @@ -213,6 +214,14 @@ private MavenExecutionResult doExecute(MavenExecutionRequest request) {
try {
MavenChainedWorkspaceReader chainedWorkspaceReader =
new MavenChainedWorkspaceReader(request.getWorkspaceReader(), ideWorkspaceReader);
// Add SPI workspace readers to the chain — looked up dynamically so that
// implementations discovered from core extensions are included (extensions
// are loaded after the container is bootstrapped, so constructor injection
// would miss them).
for (org.apache.maven.api.spi.WorkspaceReader spiReader :
lookup.lookupList(org.apache.maven.api.spi.WorkspaceReader.class)) {
chainedWorkspaceReader.addReader(new SpiWorkspaceReaderAdapter(spiReader));
}
try (CloseableSession closeableSession = newCloseableSession(request, chainedWorkspaceReader)) {
MavenSession session = new MavenSession(closeableSession, request, result);
session.setSession(defaultSessionFactory.newSession(session));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,25 @@ public void flush() {
cache.clear();
}

@Override
public void invalidate(org.apache.maven.api.Artifact artifact) {
cache.entrySet().removeIf(entry -> {
boolean matches = entry.getValue().getArtifacts().stream()
.anyMatch(a -> a.getGroupId().equals(artifact.getGroupId())
&& a.getArtifactId().equals(artifact.getArtifactId())
&& a.getVersion().equals(artifact.getVersion().toString()));
if (matches) {
ClassRealm realm = entry.getValue().getRealm();
try {
realm.getWorld().disposeRealm(realm.getId());
} catch (NoSuchRealmException e) {
// ignore
}
}
return matches;
});
}

protected static int pluginHashCode(Plugin plugin) {
return CacheUtils.pluginHashCode(plugin);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,26 @@ default CacheRecord get(Key key, PluginRealmSupplier supplier)

void flush();

/**
* Invalidates all cache entries whose resolved artifacts include the given artifact.
*
* <p>IDE integrators and other workspace-aware tools can call this method when a workspace
* artifact changes on disk, so that subsequent builds will re-resolve the affected plugin
* realms from the updated sources rather than using a stale cached classloader.
*
* <p>Implementations may choose to match on {@code groupId:artifactId:version} only, ignoring
* classifier and extension, to maximize the chance of invalidating related entries.
*
* <p>The default implementation is a no-op (safe for existing implementations that do not
* track artifact-to-entry mappings).
*
* @param artifact the workspace artifact that has changed, never {@code null}
* @since 4.1.0
*/
default void invalidate(org.apache.maven.api.Artifact artifact) {
// no-op by default
}

/**
* Registers the specified cache record for usage with the given project. Integrators can use the information
* collected from this method in combination with a custom cache implementation to dispose unused records from the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* 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.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.repository.WorkspaceReader;
import org.eclipse.aether.repository.WorkspaceRepository;

/**
* Bridge adapter that wraps an {@link org.apache.maven.api.spi.WorkspaceReader SPI WorkspaceReader}
* into a resolver {@link WorkspaceReader}.
*
* <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.
*
* @since 4.1.0
*/
public class SpiWorkspaceReaderAdapter implements WorkspaceReader {

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));
}

/**
* 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 = getGroupId()
+ ':'
+ getArtifactId()
+ ':'
+ getExtension()
+ (getClassifier().isEmpty() ? "" : ":" + getClassifier())
+ ':'
+ artifact.getVersion();
}

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

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

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

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

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

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

@Nonnull
@Override
public String getClassifier() {
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.
*/
private record StringVersion(String version) implements Version {
@Override
public int compareTo(Version o) {
return version.compareTo(o.toString());
}

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