From 5b93e75ec08162ec101bbb0a0ff17c2a506c76db Mon Sep 17 00:00:00 2001 From: goutamadwant Date: Tue, 1 Sep 2026 22:24:05 -0700 Subject: [PATCH 1/8] [MNG-8547] Expose repository events through Maven API Add Maven API repository event and listener types and bridge all Resolver repository callbacks without exposing Resolver types. Keep registration scoped to the underlying Resolver session and cover event mapping, listener isolation, and derived-session behavior. Signed-off-by: goutamadwant --- .../org/apache/maven/api/RepositoryEvent.java | 93 +++++ .../apache/maven/api/RepositoryEventType.java | 49 +++ .../apache/maven/api/RepositoryListener.java | 73 ++++ .../apache/maven/api/RepositoryMetadata.java | 64 ++++ .../java/org/apache/maven/api/Session.java | 28 ++ ...DefaultRepositorySystemSessionFactory.java | 4 +- ...ultRepositorySystemSessionFactoryTest.java | 41 +++ .../apache/maven/impl/AbstractSession.java | 26 ++ .../maven/impl/DefaultRepositoryEvent.java | 161 ++++++++ .../maven/impl/MavenRepositoryListener.java | 153 ++++++++ .../maven/impl/standalone/ApiRunner.java | 2 + .../impl/MavenRepositoryListenerTest.java | 347 ++++++++++++++++++ .../impl/standalone/RequestTraceTest.java | 32 +- .../testing/plugin/stubs/SessionStub.java | 12 + 14 files changed, 1067 insertions(+), 18 deletions(-) create mode 100644 api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryEvent.java create mode 100644 api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryEventType.java create mode 100644 api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryListener.java create mode 100644 api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryMetadata.java create mode 100644 impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultRepositoryEvent.java create mode 100644 impl/maven-impl/src/main/java/org/apache/maven/impl/MavenRepositoryListener.java create mode 100644 impl/maven-impl/src/test/java/org/apache/maven/impl/MavenRepositoryListenerTest.java diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryEvent.java b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryEvent.java new file mode 100644 index 000000000000..67fdfeb4ab9f --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryEvent.java @@ -0,0 +1,93 @@ +/* + * 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; + +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; + +import org.apache.maven.api.annotations.Experimental; +import org.apache.maven.api.annotations.Immutable; +import org.apache.maven.api.annotations.Nonnull; +import org.apache.maven.api.services.RequestTrace; + +/** + * Describes an artifact or metadata operation performed against a repository. + * + * @since 4.1.0 + */ +@Experimental +@Immutable +public interface RepositoryEvent { + + /** + * Returns the kind of repository operation represented by this event. + */ + @Nonnull + RepositoryEventType getType(); + + /** + * Returns the Maven session associated with the underlying repository system session. + * Sessions derived from it share the same repository event and listener scope. + */ + @Nonnull + Session getSession(); + + /** + * Returns the artifact involved in the event, if any. + */ + @Nonnull + Optional getArtifact(); + + /** + * Returns the metadata involved in the event, if any. + */ + @Nonnull + Optional getMetadata(); + + /** + * Returns the local path involved in the event, if any. + */ + @Nonnull + Optional getPath(); + + /** + * Returns the repository involved in the event, if any. + */ + @Nonnull + Optional getRepository(); + + /** + * Returns the primary failure associated with the event, if any. + */ + @Nonnull + Optional getException(); + + /** + * Returns all failures associated with the event. + */ + @Nonnull + List getExceptions(); + + /** + * Returns the request trace associated with the event, if any. + */ + @Nonnull + Optional getTrace(); +} diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryEventType.java b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryEventType.java new file mode 100644 index 000000000000..e271f9751a3b --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryEventType.java @@ -0,0 +1,49 @@ +/* + * 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; + +import org.apache.maven.api.annotations.Experimental; + +/** + * The possible types of repository events. + * + * @since 4.1.0 + */ +@Experimental +public enum RepositoryEventType { + ARTIFACT_DESCRIPTOR_INVALID, + ARTIFACT_DESCRIPTOR_MISSING, + METADATA_INVALID, + ARTIFACT_RESOLVING, + ARTIFACT_RESOLVED, + METADATA_RESOLVING, + METADATA_RESOLVED, + ARTIFACT_DOWNLOADING, + ARTIFACT_DOWNLOADED, + METADATA_DOWNLOADING, + METADATA_DOWNLOADED, + ARTIFACT_INSTALLING, + ARTIFACT_INSTALLED, + METADATA_INSTALLING, + METADATA_INSTALLED, + ARTIFACT_DEPLOYING, + ARTIFACT_DEPLOYED, + METADATA_DEPLOYING, + METADATA_DEPLOYED, +} diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryListener.java b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryListener.java new file mode 100644 index 000000000000..c8770ca8b3ff --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryListener.java @@ -0,0 +1,73 @@ +/* + * 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; + +import org.apache.maven.api.annotations.Consumer; +import org.apache.maven.api.annotations.Experimental; +import org.apache.maven.api.annotations.Nonnull; + +/** + * Receives repository events emitted while resolving, installing, and deploying artifacts and metadata. + * Implementations must be thread-safe because callbacks may occur concurrently. + * Runtime exceptions thrown by a listener do not stop repository processing or notification of other listeners. + * + * @since 4.1.0 + */ +@Experimental +@Consumer +public interface RepositoryListener { + + default void artifactDescriptorInvalid(@Nonnull RepositoryEvent event) {} + + default void artifactDescriptorMissing(@Nonnull RepositoryEvent event) {} + + default void metadataInvalid(@Nonnull RepositoryEvent event) {} + + default void artifactResolving(@Nonnull RepositoryEvent event) {} + + default void artifactResolved(@Nonnull RepositoryEvent event) {} + + default void metadataResolving(@Nonnull RepositoryEvent event) {} + + default void metadataResolved(@Nonnull RepositoryEvent event) {} + + default void artifactDownloading(@Nonnull RepositoryEvent event) {} + + default void artifactDownloaded(@Nonnull RepositoryEvent event) {} + + default void metadataDownloading(@Nonnull RepositoryEvent event) {} + + default void metadataDownloaded(@Nonnull RepositoryEvent event) {} + + default void artifactInstalling(@Nonnull RepositoryEvent event) {} + + default void artifactInstalled(@Nonnull RepositoryEvent event) {} + + default void metadataInstalling(@Nonnull RepositoryEvent event) {} + + default void metadataInstalled(@Nonnull RepositoryEvent event) {} + + default void artifactDeploying(@Nonnull RepositoryEvent event) {} + + default void artifactDeployed(@Nonnull RepositoryEvent event) {} + + default void metadataDeploying(@Nonnull RepositoryEvent event) {} + + default void metadataDeployed(@Nonnull RepositoryEvent event) {} +} diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryMetadata.java b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryMetadata.java new file mode 100644 index 000000000000..5ad861a1832b --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryMetadata.java @@ -0,0 +1,64 @@ +/* + * 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; + +import java.nio.file.Path; +import java.util.Map; +import java.util.Optional; + +import org.apache.maven.api.annotations.Experimental; +import org.apache.maven.api.annotations.Immutable; +import org.apache.maven.api.annotations.Nonnull; + +/** + * Metadata involved in a repository event. + * + * @since 4.1.0 + */ +@Experimental +@Immutable +public interface RepositoryMetadata { + + enum Nature { + RELEASE, + SNAPSHOT, + RELEASE_OR_SNAPSHOT, + } + + @Nonnull + String getGroupId(); + + @Nonnull + String getArtifactId(); + + @Nonnull + String getVersion(); + + @Nonnull + String getType(); + + @Nonnull + Nature getNature(); + + @Nonnull + Optional getPath(); + + @Nonnull + Map getProperties(); +} diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/Session.java b/api/maven-api-core/src/main/java/org/apache/maven/api/Session.java index 8ef3802062ea..f9fb130183a0 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/Session.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/Session.java @@ -203,6 +203,34 @@ default Map getEffectiveProperties() { @Nonnull Collection getListeners(); + /** + * Registers a listener for repository events. + * Repository listener registration is scoped to the underlying repository system session and is therefore + * shared with sessions derived from this session. + * + * @param listener the listener to register + * @throws NullPointerException if {@code listener} is null + */ + void registerListener(@Nonnull RepositoryListener listener); + + /** + * Unregisters a previously registered repository listener. + * The listener is removed from the scope shared by sessions derived from the same repository system session. + * + * @param listener the listener to unregister + * @throws NullPointerException if {@code listener} is null + */ + void unregisterListener(@Nonnull RepositoryListener listener); + + /** + * Returns the registered repository listeners. + * The returned listeners belong to the scope shared by sessions derived from the same repository system session. + * + * @return an immutable collection of listeners, never {@code null} + */ + @Nonnull + Collection getRepositoryListeners(); + /** * Shortcut for {@code getService(RepositoryFactory.class).createLocal(...)}. * diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java b/impl/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java index aff1423b9c42..d724b9c647fd 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java +++ b/impl/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java @@ -38,6 +38,7 @@ import org.apache.maven.api.xml.XmlNode; import org.apache.maven.eventspy.internal.EventSpyDispatcher; import org.apache.maven.execution.MavenExecutionRequest; +import org.apache.maven.impl.MavenRepositoryListener; import org.apache.maven.impl.resolver.MavenSessionBuilderSupplier; import org.apache.maven.impl.resolver.type.TypeRegistryAdapter; import org.apache.maven.internal.xml.XmlPlexusConfiguration; @@ -342,7 +343,8 @@ public SessionBuilder newRepositorySessionBuilder(MavenExecutionRequest request) sessionBuilder.setTransferListener(request.getTransferListener()); - RepositoryListener repositoryListener = eventSpyDispatcher.chainListener(new LoggingRepositoryListener(logger)); + RepositoryListener repositoryListener = eventSpyDispatcher.chainListener( + new ChainedRepositoryListener(new LoggingRepositoryListener(logger), new MavenRepositoryListener())); boolean recordReverseTree = Boolean.parseBoolean( mergedProps.getOrDefault(Constants.MAVEN_REPO_LOCAL_RECORD_REVERSE_TREE, Boolean.FALSE.toString())); diff --git a/impl/maven-core/src/test/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactoryTest.java b/impl/maven-core/src/test/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactoryTest.java index 111fb3548b56..0547142fca3c 100644 --- a/impl/maven-core/src/test/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactoryTest.java +++ b/impl/maven-core/src/test/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactoryTest.java @@ -26,13 +26,18 @@ import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.maven.api.RepositoryEvent; +import org.apache.maven.api.RepositoryEventType; +import org.apache.maven.api.RepositoryListener; import org.apache.maven.artifact.InvalidRepositoryException; import org.apache.maven.artifact.repository.ArtifactRepository; import org.apache.maven.bridge.MavenRepositorySystem; import org.apache.maven.eventspy.internal.EventSpyDispatcher; import org.apache.maven.execution.DefaultMavenExecutionRequest; import org.apache.maven.execution.MavenExecutionRequest; +import org.apache.maven.impl.InternalSession; import org.apache.maven.internal.impl.DefaultTypeRegistry; import org.apache.maven.rtinfo.RuntimeInformation; import org.apache.maven.settings.Server; @@ -50,6 +55,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrowsExactly; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * UT for {@link DefaultRepositorySystemSessionFactory}. @@ -77,6 +84,40 @@ public class DefaultRepositorySystemSessionFactoryTest { @Inject protected VersionFilterBuilder versionFilterBuilder; + @Test + void exposesRepositoryEventsThroughMavenApi() throws InvalidRepositoryException { + DefaultRepositorySystemSessionFactory systemSessionFactory = new DefaultRepositorySystemSessionFactory( + aetherRepositorySystem, + eventSpyDispatcher, + information, + defaultTypeRegistry, + versionScheme, + Collections.emptyMap(), + versionFilterBuilder); + MavenExecutionRequest request = new DefaultMavenExecutionRequest(); + request.setLocalRepository(getLocalRepository()); + org.eclipse.aether.RepositorySystemSession resolverSession = systemSessionFactory.newRepositorySession(request); + InternalSession session = mock(InternalSession.class); + AtomicReference received = new AtomicReference<>(); + RepositoryListener listener = new RepositoryListener() { + @Override + public void artifactResolved(RepositoryEvent event) { + received.set(event); + } + }; + when(session.getRepositoryListeners()).thenReturn(List.of(listener)); + InternalSession.associate(resolverSession, session); + + resolverSession + .getRepositoryListener() + .artifactResolved(new org.eclipse.aether.RepositoryEvent.Builder( + resolverSession, org.eclipse.aether.RepositoryEvent.EventType.ARTIFACT_RESOLVED) + .build()); + + assertNotNull(received.get()); + assertEquals(RepositoryEventType.ARTIFACT_RESOLVED, received.get().getType()); + } + @Test void isNoSnapshotUpdatesTest() throws InvalidRepositoryException { DefaultRepositorySystemSessionFactory systemSessionFactory = new DefaultRepositorySystemSessionFactory( diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/AbstractSession.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/AbstractSession.java index 60c8198bcb59..9347815a6893 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/AbstractSession.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/AbstractSession.java @@ -56,6 +56,7 @@ import org.apache.maven.api.Project; import org.apache.maven.api.ProjectScope; import org.apache.maven.api.RemoteRepository; +import org.apache.maven.api.RepositoryListener; import org.apache.maven.api.Service; import org.apache.maven.api.Session; import org.apache.maven.api.SessionData; @@ -123,6 +124,7 @@ public abstract class AbstractSession implements InternalSession { protected final Injector injector; private final Map, Service> services = new ConcurrentHashMap<>(); private final List listeners = new CopyOnWriteArrayList<>(); + private final List repositoryListeners; private final Cache allNodes = Cache.newCache(Cache.ReferenceType.WEAK, "AbstractSession-Nodes"); private final Map, Cache> allArtifacts = @@ -148,6 +150,14 @@ public AbstractSession( this.repositories = getRepositories(repositories, resolverRepositories); this.lookup = lookup; this.injector = lookup != null ? lookup.lookupOptional(Injector.class).orElse(null) : null; + this.repositoryListeners = getRepositoryListeners(session); + } + + @SuppressWarnings("unchecked") + private static List getRepositoryListeners(RepositorySystemSession session) { + // Resolver events are scoped to RepositorySystemSession, so derived Maven sessions share this listener set. + return (List) + session.getData().computeIfAbsent(RepositoryListener.class, CopyOnWriteArrayList::new); } @SuppressWarnings("unchecked") @@ -531,6 +541,22 @@ public Collection getListeners() { return Collections.unmodifiableCollection(listeners); } + @Override + public void registerListener(@Nonnull RepositoryListener listener) { + repositoryListeners.add(requireNonNull(listener)); + } + + @Override + public void unregisterListener(@Nonnull RepositoryListener listener) { + repositoryListeners.remove(requireNonNull(listener)); + } + + @Nonnull + @Override + public Collection getRepositoryListeners() { + return Collections.unmodifiableCollection(repositoryListeners); + } + // // Shortcut implementations // diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultRepositoryEvent.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultRepositoryEvent.java new file mode 100644 index 000000000000..9de5d4c3e8b4 --- /dev/null +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultRepositoryEvent.java @@ -0,0 +1,161 @@ +/* + * 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.impl; + +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.apache.maven.api.Artifact; +import org.apache.maven.api.Repository; +import org.apache.maven.api.RepositoryEvent; +import org.apache.maven.api.RepositoryEventType; +import org.apache.maven.api.RepositoryMetadata; +import org.apache.maven.api.Session; +import org.apache.maven.api.services.RequestTrace; + +final class DefaultRepositoryEvent implements RepositoryEvent { + + private final RepositoryEventType type; + private final Session session; + private final Artifact artifact; + private final RepositoryMetadata metadata; + private final Path path; + private final Repository repository; + private final Exception exception; + private final List exceptions; + private final RequestTrace trace; + + DefaultRepositoryEvent(InternalSession session, org.eclipse.aether.RepositoryEvent event) { + this.type = RepositoryEventType.valueOf(event.getType().name()); + this.session = session; + this.artifact = event.getArtifact() != null ? session.getArtifact(event.getArtifact()) : null; + this.metadata = event.getMetadata() != null ? new DefaultRepositoryMetadata(event.getMetadata()) : null; + this.path = event.getPath(); + this.repository = event.getRepository() != null + ? session.getRepository(event.getRepository()).orElse(null) + : null; + this.exception = event.getException(); + this.exceptions = event.getExceptions() != null ? List.copyOf(event.getExceptions()) : List.of(); + RequestTrace currentTrace = session.getCurrentTrace(); + this.trace = RequestTraceHelper.toMaven(currentTrace != null ? currentTrace.context() : null, event.getTrace()); + } + + @Override + public RepositoryEventType getType() { + return type; + } + + @Override + public Session getSession() { + return session; + } + + @Override + public Optional getArtifact() { + return Optional.ofNullable(artifact); + } + + @Override + public Optional getMetadata() { + return Optional.ofNullable(metadata); + } + + @Override + public Optional getPath() { + return Optional.ofNullable(path); + } + + @Override + public Optional getRepository() { + return Optional.ofNullable(repository); + } + + @Override + public Optional getException() { + return Optional.ofNullable(exception); + } + + @Override + public List getExceptions() { + return exceptions; + } + + @Override + public Optional getTrace() { + return Optional.ofNullable(trace); + } + + private static final class DefaultRepositoryMetadata implements RepositoryMetadata { + + private final String groupId; + private final String artifactId; + private final String version; + private final String type; + private final Nature nature; + private final Path path; + private final Map properties; + + private DefaultRepositoryMetadata(org.eclipse.aether.metadata.Metadata metadata) { + this.groupId = metadata.getGroupId(); + this.artifactId = metadata.getArtifactId(); + this.version = metadata.getVersion(); + this.type = metadata.getType(); + this.nature = Nature.valueOf(metadata.getNature().name()); + this.path = metadata.getPath(); + this.properties = Map.copyOf(metadata.getProperties()); + } + + @Override + public String getGroupId() { + return groupId; + } + + @Override + public String getArtifactId() { + return artifactId; + } + + @Override + public String getVersion() { + return version; + } + + @Override + public String getType() { + return type; + } + + @Override + public Nature getNature() { + return nature; + } + + @Override + public Optional getPath() { + return Optional.ofNullable(path); + } + + @Override + public Map getProperties() { + return properties; + } + } +} diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/MavenRepositoryListener.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/MavenRepositoryListener.java new file mode 100644 index 000000000000..20f8c94f13ab --- /dev/null +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/MavenRepositoryListener.java @@ -0,0 +1,153 @@ +/* + * 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.impl; + +import java.util.Collection; +import java.util.function.BiConsumer; + +import org.apache.maven.api.RepositoryEvent; +import org.apache.maven.api.RepositoryListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Bridges Maven Resolver repository events to the Maven public API. + */ +public final class MavenRepositoryListener extends org.eclipse.aether.AbstractRepositoryListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(MavenRepositoryListener.class); + + private void dispatch( + org.eclipse.aether.RepositoryEvent event, BiConsumer consumer) { + Object associatedSession = event.getSession().getData().get(InternalSession.class); + if (!(associatedSession instanceof InternalSession session)) { + return; + } + Collection listeners = session.getRepositoryListeners(); + if (!listeners.isEmpty()) { + RepositoryEvent repositoryEvent = new DefaultRepositoryEvent(session, event); + for (RepositoryListener listener : listeners) { + try { + consumer.accept(listener, repositoryEvent); + } catch (RuntimeException e) { + LOGGER.warn( + "Failed to notify repository listener {} about {}", + listener.getClass().getName(), + event.getType(), + e); + } + } + } + } + + @Override + public void artifactDescriptorInvalid(org.eclipse.aether.RepositoryEvent event) { + dispatch(event, RepositoryListener::artifactDescriptorInvalid); + } + + @Override + public void artifactDescriptorMissing(org.eclipse.aether.RepositoryEvent event) { + dispatch(event, RepositoryListener::artifactDescriptorMissing); + } + + @Override + public void metadataInvalid(org.eclipse.aether.RepositoryEvent event) { + dispatch(event, RepositoryListener::metadataInvalid); + } + + @Override + public void artifactResolving(org.eclipse.aether.RepositoryEvent event) { + dispatch(event, RepositoryListener::artifactResolving); + } + + @Override + public void artifactResolved(org.eclipse.aether.RepositoryEvent event) { + dispatch(event, RepositoryListener::artifactResolved); + } + + @Override + public void metadataResolving(org.eclipse.aether.RepositoryEvent event) { + dispatch(event, RepositoryListener::metadataResolving); + } + + @Override + public void metadataResolved(org.eclipse.aether.RepositoryEvent event) { + dispatch(event, RepositoryListener::metadataResolved); + } + + @Override + public void artifactDownloading(org.eclipse.aether.RepositoryEvent event) { + dispatch(event, RepositoryListener::artifactDownloading); + } + + @Override + public void artifactDownloaded(org.eclipse.aether.RepositoryEvent event) { + dispatch(event, RepositoryListener::artifactDownloaded); + } + + @Override + public void metadataDownloading(org.eclipse.aether.RepositoryEvent event) { + dispatch(event, RepositoryListener::metadataDownloading); + } + + @Override + public void metadataDownloaded(org.eclipse.aether.RepositoryEvent event) { + dispatch(event, RepositoryListener::metadataDownloaded); + } + + @Override + public void artifactInstalling(org.eclipse.aether.RepositoryEvent event) { + dispatch(event, RepositoryListener::artifactInstalling); + } + + @Override + public void artifactInstalled(org.eclipse.aether.RepositoryEvent event) { + dispatch(event, RepositoryListener::artifactInstalled); + } + + @Override + public void metadataInstalling(org.eclipse.aether.RepositoryEvent event) { + dispatch(event, RepositoryListener::metadataInstalling); + } + + @Override + public void metadataInstalled(org.eclipse.aether.RepositoryEvent event) { + dispatch(event, RepositoryListener::metadataInstalled); + } + + @Override + public void artifactDeploying(org.eclipse.aether.RepositoryEvent event) { + dispatch(event, RepositoryListener::artifactDeploying); + } + + @Override + public void artifactDeployed(org.eclipse.aether.RepositoryEvent event) { + dispatch(event, RepositoryListener::artifactDeployed); + } + + @Override + public void metadataDeploying(org.eclipse.aether.RepositoryEvent event) { + dispatch(event, RepositoryListener::metadataDeploying); + } + + @Override + public void metadataDeployed(org.eclipse.aether.RepositoryEvent event) { + dispatch(event, RepositoryListener::metadataDeployed); + } +} diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/standalone/ApiRunner.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/standalone/ApiRunner.java index 81ac6ec0c2fc..1250d9df25e1 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/standalone/ApiRunner.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/standalone/ApiRunner.java @@ -65,6 +65,7 @@ import org.apache.maven.di.impl.DIException; import org.apache.maven.impl.AbstractSession; import org.apache.maven.impl.InternalSession; +import org.apache.maven.impl.MavenRepositoryListener; import org.apache.maven.impl.di.SessionScope; import org.apache.maven.impl.resolver.scopes.Maven4ScopeManagerConfiguration; import org.eclipse.aether.DefaultRepositorySystemSession; @@ -383,6 +384,7 @@ static Session newSession(RepositorySystem system, Lookup lookup, @Nullable Loca : properties.containsKey("env.MAVEN_HOME") ? Paths.get(properties.get("env.MAVEN_HOME")) : null; DefaultRepositorySystemSession rsession = new DefaultRepositorySystemSession(h -> false); + rsession.setRepositoryListener(new MavenRepositoryListener()); rsession.setScopeManager(new ScopeManagerImpl(Maven4ScopeManagerConfiguration.INSTANCE)); rsession.setSystemProperties(properties); rsession.setConfigProperties(properties); diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/MavenRepositoryListenerTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/MavenRepositoryListenerTest.java new file mode 100644 index 000000000000..487d53528c89 --- /dev/null +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/MavenRepositoryListenerTest.java @@ -0,0 +1,347 @@ +/* + * 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.impl; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; + +import org.apache.maven.api.Artifact; +import org.apache.maven.api.Listener; +import org.apache.maven.api.RemoteRepository; +import org.apache.maven.api.RepositoryEvent; +import org.apache.maven.api.RepositoryEventType; +import org.apache.maven.api.RepositoryListener; +import org.apache.maven.api.RepositoryMetadata; +import org.apache.maven.api.Session; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.metadata.DefaultMetadata; +import org.eclipse.aether.metadata.Metadata; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class MavenRepositoryListenerTest { + + @Test + void repositoryListenerOverloadDoesNotMakeBuildListenerLambdaAmbiguous() { + Session session = mock(Session.class); + + session.registerListener(event -> {}); + + verify(session).registerListener(org.mockito.ArgumentMatchers.any(Listener.class)); + } + + @Test + void ignoresEventsBeforeMavenSessionAssociation() { + DefaultRepositorySystemSession resolverSession = new DefaultRepositorySystemSession(h -> false); + MavenRepositoryListener bridge = new MavenRepositoryListener(); + + bridge.artifactResolving(new org.eclipse.aether.RepositoryEvent.Builder( + resolverSession, org.eclipse.aether.RepositoryEvent.EventType.ARTIFACT_RESOLVING) + .build()); + } + + @Test + void dispatchesAllRepositoryEventTypes() { + TestContext context = new TestContext(); + RecordingListener listener = new RecordingListener(); + when(context.session.getRepositoryListeners()).thenReturn(List.of(listener)); + + for (org.eclipse.aether.RepositoryEvent.EventType type : + org.eclipse.aether.RepositoryEvent.EventType.values()) { + dispatch( + context.bridge, + new org.eclipse.aether.RepositoryEvent.Builder(context.resolverSession, type).build()); + } + + assertEquals(List.of(RepositoryEventType.values()), listener.types); + } + + @Test + void convertsArtifactRepositoryFailurePathAndTrace() { + TestContext context = new TestContext(); + RecordingListener listener = new RecordingListener(); + when(context.session.getRepositoryListeners()).thenReturn(List.of(listener)); + + org.eclipse.aether.artifact.Artifact resolverArtifact = + new org.eclipse.aether.artifact.DefaultArtifact("org.example:demo:jar:1.0"); + Artifact artifact = mock(Artifact.class); + when(context.session.getArtifact(resolverArtifact)).thenReturn(artifact); + org.eclipse.aether.repository.RemoteRepository resolverRepository = + new org.eclipse.aether.repository.RemoteRepository.Builder("central", "default", "https://repo.example") + .build(); + RemoteRepository repository = mock(RemoteRepository.class); + when(context.session.getRepository(resolverRepository)).thenReturn(Optional.of(repository)); + Exception failure = new Exception("resolution failed"); + Path path = Path.of("target", "demo.jar"); + + context.bridge.artifactResolved(new org.eclipse.aether.RepositoryEvent.Builder( + context.resolverSession, org.eclipse.aether.RepositoryEvent.EventType.ARTIFACT_RESOLVED) + .setArtifact(resolverArtifact) + .setRepository(resolverRepository) + .setPath(path) + .setException(failure) + .setTrace(new org.eclipse.aether.RequestTrace("request")) + .build()); + + RepositoryEvent event = listener.events.get(0); + assertEquals(RepositoryEventType.ARTIFACT_RESOLVED, event.getType()); + assertSame(context.session, event.getSession()); + assertSame(artifact, event.getArtifact().orElseThrow()); + assertSame(repository, event.getRepository().orElseThrow()); + assertEquals(path, event.getPath().orElseThrow()); + assertSame(failure, event.getException().orElseThrow()); + assertEquals(List.of(failure), event.getExceptions()); + assertEquals("request", event.getTrace().orElseThrow().data()); + assertTrue(event.getMetadata().isEmpty()); + assertThrows( + UnsupportedOperationException.class, () -> event.getExceptions().add(new Exception())); + } + + @Test + void convertsMetadataWithoutExposingResolverMetadata() { + TestContext context = new TestContext(); + RecordingListener listener = new RecordingListener(); + when(context.session.getRepositoryListeners()).thenReturn(List.of(listener)); + Path path = Path.of("target", "maven-metadata.xml"); + Metadata metadata = new DefaultMetadata( + "org.example", + "demo", + "1.0-SNAPSHOT", + "maven-metadata.xml", + Metadata.Nature.SNAPSHOT, + Map.of("source", "test"), + path); + + context.bridge.metadataResolved(new org.eclipse.aether.RepositoryEvent.Builder( + context.resolverSession, org.eclipse.aether.RepositoryEvent.EventType.METADATA_RESOLVED) + .setMetadata(metadata) + .build()); + + RepositoryMetadata converted = listener.events.get(0).getMetadata().orElseThrow(); + assertEquals("org.example", converted.getGroupId()); + assertEquals("demo", converted.getArtifactId()); + assertEquals("1.0-SNAPSHOT", converted.getVersion()); + assertEquals("maven-metadata.xml", converted.getType()); + assertEquals(RepositoryMetadata.Nature.SNAPSHOT, converted.getNature()); + assertEquals(path, converted.getPath().orElseThrow()); + assertEquals(Map.of("source", "test"), converted.getProperties()); + assertThrows( + UnsupportedOperationException.class, + () -> converted.getProperties().put("key", "value")); + } + + @Test + void isolatesListenerFailures() { + TestContext context = new TestContext(); + RepositoryListener failing = new RepositoryListener() { + @Override + public void artifactResolving(RepositoryEvent event) { + throw new IllegalStateException("listener failure"); + } + }; + AtomicInteger notifications = new AtomicInteger(); + RepositoryListener succeeding = new RepositoryListener() { + @Override + public void artifactResolving(RepositoryEvent event) { + notifications.incrementAndGet(); + } + }; + when(context.session.getRepositoryListeners()).thenReturn(List.of(failing, succeeding)); + + context.bridge.artifactResolving(new org.eclipse.aether.RepositoryEvent.Builder( + context.resolverSession, org.eclipse.aether.RepositoryEvent.EventType.ARTIFACT_RESOLVING) + .build()); + + assertEquals(1, notifications.get()); + } + + @Test + void supportsConcurrentDispatch() { + TestContext context = new TestContext(); + AtomicInteger notifications = new AtomicInteger(); + RepositoryListener listener = new RepositoryListener() { + @Override + public void artifactResolving(RepositoryEvent event) { + notifications.incrementAndGet(); + } + }; + when(context.session.getRepositoryListeners()).thenReturn(List.of(listener)); + + IntStream.range(0, 100) + .parallel() + .forEach(i -> context.bridge.artifactResolving(new org.eclipse.aether.RepositoryEvent.Builder( + context.resolverSession, + org.eclipse.aether.RepositoryEvent.EventType.ARTIFACT_RESOLVING) + .build())); + + assertEquals(100, notifications.get()); + } + + private static void dispatch(MavenRepositoryListener listener, org.eclipse.aether.RepositoryEvent event) { + switch (event.getType()) { + case ARTIFACT_DESCRIPTOR_INVALID -> listener.artifactDescriptorInvalid(event); + case ARTIFACT_DESCRIPTOR_MISSING -> listener.artifactDescriptorMissing(event); + case METADATA_INVALID -> listener.metadataInvalid(event); + case ARTIFACT_RESOLVING -> listener.artifactResolving(event); + case ARTIFACT_RESOLVED -> listener.artifactResolved(event); + case METADATA_RESOLVING -> listener.metadataResolving(event); + case METADATA_RESOLVED -> listener.metadataResolved(event); + case ARTIFACT_DOWNLOADING -> listener.artifactDownloading(event); + case ARTIFACT_DOWNLOADED -> listener.artifactDownloaded(event); + case METADATA_DOWNLOADING -> listener.metadataDownloading(event); + case METADATA_DOWNLOADED -> listener.metadataDownloaded(event); + case ARTIFACT_INSTALLING -> listener.artifactInstalling(event); + case ARTIFACT_INSTALLED -> listener.artifactInstalled(event); + case METADATA_INSTALLING -> listener.metadataInstalling(event); + case METADATA_INSTALLED -> listener.metadataInstalled(event); + case ARTIFACT_DEPLOYING -> listener.artifactDeploying(event); + case ARTIFACT_DEPLOYED -> listener.artifactDeployed(event); + case METADATA_DEPLOYING -> listener.metadataDeploying(event); + case METADATA_DEPLOYED -> listener.metadataDeployed(event); + default -> throw new IllegalArgumentException("Unknown repository event type: " + event.getType()); + } + } + + private static final class TestContext { + private final DefaultRepositorySystemSession resolverSession = new DefaultRepositorySystemSession(h -> false); + private final InternalSession session = mock(InternalSession.class); + private final MavenRepositoryListener bridge = new MavenRepositoryListener(); + + private TestContext() { + InternalSession.associate(resolverSession, session); + } + } + + private static final class RecordingListener implements RepositoryListener { + private final List types = new ArrayList<>(); + private final List events = new ArrayList<>(); + + private void record(RepositoryEvent event) { + types.add(event.getType()); + events.add(event); + } + + @Override + public void artifactDescriptorInvalid(RepositoryEvent event) { + record(event); + } + + @Override + public void artifactDescriptorMissing(RepositoryEvent event) { + record(event); + } + + @Override + public void metadataInvalid(RepositoryEvent event) { + record(event); + } + + @Override + public void artifactResolving(RepositoryEvent event) { + record(event); + } + + @Override + public void artifactResolved(RepositoryEvent event) { + record(event); + } + + @Override + public void metadataResolving(RepositoryEvent event) { + record(event); + } + + @Override + public void metadataResolved(RepositoryEvent event) { + record(event); + } + + @Override + public void artifactDownloading(RepositoryEvent event) { + record(event); + } + + @Override + public void artifactDownloaded(RepositoryEvent event) { + record(event); + } + + @Override + public void metadataDownloading(RepositoryEvent event) { + record(event); + } + + @Override + public void metadataDownloaded(RepositoryEvent event) { + record(event); + } + + @Override + public void artifactInstalling(RepositoryEvent event) { + record(event); + } + + @Override + public void artifactInstalled(RepositoryEvent event) { + record(event); + } + + @Override + public void metadataInstalling(RepositoryEvent event) { + record(event); + } + + @Override + public void metadataInstalled(RepositoryEvent event) { + record(event); + } + + @Override + public void artifactDeploying(RepositoryEvent event) { + record(event); + } + + @Override + public void artifactDeployed(RepositoryEvent event) { + record(event); + } + + @Override + public void metadataDeploying(RepositoryEvent event) { + record(event); + } + + @Override + public void metadataDeployed(RepositoryEvent event) { + record(event); + } + } +} diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/standalone/RequestTraceTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/standalone/RequestTraceTest.java index adef91ddb497..a4bd045d5572 100644 --- a/impl/maven-impl/src/test/java/org/apache/maven/impl/standalone/RequestTraceTest.java +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/standalone/RequestTraceTest.java @@ -28,6 +28,8 @@ import org.apache.maven.api.DownloadedArtifact; import org.apache.maven.api.Node; import org.apache.maven.api.PathScope; +import org.apache.maven.api.RepositoryEvent; +import org.apache.maven.api.RepositoryListener; import org.apache.maven.api.Session; import org.apache.maven.api.di.Named; import org.apache.maven.api.di.Provides; @@ -38,11 +40,6 @@ import org.apache.maven.api.services.ModelBuilderResult; import org.apache.maven.api.services.RequestTrace; import org.apache.maven.api.services.Sources; -import org.apache.maven.impl.InternalSession; -import org.apache.maven.impl.RequestTraceHelper; -import org.eclipse.aether.AbstractRepositoryListener; -import org.eclipse.aether.DefaultRepositorySystemSession; -import org.eclipse.aether.RepositoryEvent; import org.eclipse.aether.spi.connector.transport.http.ChecksumExtractor; import org.eclipse.aether.spi.io.PathProcessor; import org.eclipse.aether.transport.apache.ApacheTransporterFactory; @@ -76,13 +73,13 @@ void testTraces() { assertNotNull(result.getEffectiveModel()); List events = new CopyOnWriteArrayList<>(); - ((DefaultRepositorySystemSession) InternalSession.from(session).getSession()) - .setRepositoryListener(new AbstractRepositoryListener() { - @Override - public void artifactResolved(RepositoryEvent event) { - events.add(event); - } - }); + RepositoryListener listener = new RepositoryListener() { + @Override + public void artifactResolved(RepositoryEvent event) { + events.add(event); + } + }; + session.registerListener(listener); ArtifactCoordinates coords = session.createArtifactCoordinates("org.apache.maven:maven-api-core:4.0.0-alpha-13"); @@ -105,14 +102,15 @@ public void artifactResolved(RepositoryEvent event) { .next(); for (RepositoryEvent event : events) { - org.eclipse.aether.RequestTrace trace = event.getTrace(); - assertNotNull(trace); - - RequestTrace rTrace = RequestTraceHelper.toMaven("collect", trace); - assertNotNull(rTrace); + RequestTrace rTrace = event.getTrace().orElseThrow(); assertNotNull(rTrace.parent()); } + Session derived = session.withRemoteRepositories(session.getRemoteRepositories()); + assertTrue(derived.getRepositoryListeners().contains(listener)); + derived.unregisterListener(listener); + assertTrue(session.getRepositoryListeners().isEmpty()); + assertTrue(derived.getRepositoryListeners().isEmpty()); assertNotNull(node); assertEquals(6, node.getChildren().size()); } diff --git a/impl/maven-testing/src/main/java/org/apache/maven/testing/plugin/stubs/SessionStub.java b/impl/maven-testing/src/main/java/org/apache/maven/testing/plugin/stubs/SessionStub.java index 51ca49b6e7c4..aa56de289cda 100644 --- a/impl/maven-testing/src/main/java/org/apache/maven/testing/plugin/stubs/SessionStub.java +++ b/impl/maven-testing/src/main/java/org/apache/maven/testing/plugin/stubs/SessionStub.java @@ -43,6 +43,7 @@ import org.apache.maven.api.Project; import org.apache.maven.api.ProjectScope; import org.apache.maven.api.RemoteRepository; +import org.apache.maven.api.RepositoryListener; import org.apache.maven.api.Service; import org.apache.maven.api.Session; import org.apache.maven.api.SessionData; @@ -203,6 +204,17 @@ public Collection getListeners() { return null; } + @Override + public void registerListener(RepositoryListener listener) {} + + @Override + public void unregisterListener(RepositoryListener listener) {} + + @Override + public Collection getRepositoryListeners() { + return null; + } + @Override public LocalRepository createLocalRepository(Path path) { return null; From 4e560dbc7f2de228e87f3a99f48df444d9914745 Mon Sep 17 00:00:00 2001 From: goutamadwant Date: Wed, 2 Sep 2026 18:54:07 -0700 Subject: [PATCH 2/8] [MNG-8547] Honor repository listener collection contract Return an empty immutable collection from SessionStub instead of null, matching the non-null Session API contract. Signed-off-by: goutamadwant --- .../java/org/apache/maven/testing/plugin/stubs/SessionStub.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impl/maven-testing/src/main/java/org/apache/maven/testing/plugin/stubs/SessionStub.java b/impl/maven-testing/src/main/java/org/apache/maven/testing/plugin/stubs/SessionStub.java index aa56de289cda..4bac5a5e696b 100644 --- a/impl/maven-testing/src/main/java/org/apache/maven/testing/plugin/stubs/SessionStub.java +++ b/impl/maven-testing/src/main/java/org/apache/maven/testing/plugin/stubs/SessionStub.java @@ -212,7 +212,7 @@ public void unregisterListener(RepositoryListener listener) {} @Override public Collection getRepositoryListeners() { - return null; + return List.of(); } @Override From cac2b6f1f5c4affbf9e71c7d13209d6d13150b54 Mon Sep 17 00:00:00 2001 From: goutamadwant Date: Fri, 4 Sep 2026 23:40:47 -0700 Subject: [PATCH 3/8] [MNG-8547] Document repository event APIs Describe when repository listener callbacks fire and clarify the values exposed by repository metadata. --- .../apache/maven/api/RepositoryListener.java | 95 +++++++++++++++++++ .../apache/maven/api/RepositoryMetadata.java | 29 ++++++ 2 files changed, 124 insertions(+) diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryListener.java b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryListener.java index c8770ca8b3ff..9b8a2dc87b90 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryListener.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryListener.java @@ -33,41 +33,136 @@ @Consumer public interface RepositoryListener { + /** + * Called when an artifact descriptor could not be parsed. + * + * @param event the event containing the artifact and parse failure + */ default void artifactDescriptorInvalid(@Nonnull RepositoryEvent event) {} + /** + * Called when an artifact descriptor could not be found. + * + * @param event the event containing the artifact whose descriptor is missing + */ default void artifactDescriptorMissing(@Nonnull RepositoryEvent event) {} + /** + * Called when repository metadata could not be parsed. + * + * @param event the event containing the metadata and parse failure + */ default void metadataInvalid(@Nonnull RepositoryEvent event) {} + /** + * Called before an artifact is resolved. + * + * @param event the event containing the artifact and, when applicable, its repository + */ default void artifactResolving(@Nonnull RepositoryEvent event) {} + /** + * Called after an artifact resolution attempt finishes. + * + * @param event the event containing the artifact and the resulting path or failure + */ default void artifactResolved(@Nonnull RepositoryEvent event) {} + /** + * Called before repository metadata is resolved. + * + * @param event the event containing the metadata and, when applicable, its repository + */ default void metadataResolving(@Nonnull RepositoryEvent event) {} + /** + * Called after a repository metadata resolution attempt finishes. + * + * @param event the event containing the metadata and the resulting path or failure + */ default void metadataResolved(@Nonnull RepositoryEvent event) {} + /** + * Called before an artifact is downloaded from a remote repository. + * + * @param event the event containing the artifact and remote repository + */ default void artifactDownloading(@Nonnull RepositoryEvent event) {} + /** + * Called after an artifact download attempt finishes. + * + * @param event the event containing the artifact, remote repository, and resulting path or failure + */ default void artifactDownloaded(@Nonnull RepositoryEvent event) {} + /** + * Called before repository metadata is downloaded from a remote repository. + * + * @param event the event containing the metadata and remote repository + */ default void metadataDownloading(@Nonnull RepositoryEvent event) {} + /** + * Called after a repository metadata download attempt finishes. + * + * @param event the event containing the metadata, remote repository, and resulting path or failure + */ default void metadataDownloaded(@Nonnull RepositoryEvent event) {} + /** + * Called before an artifact is installed into the local repository. + * + * @param event the event containing the artifact and source path + */ default void artifactInstalling(@Nonnull RepositoryEvent event) {} + /** + * Called after an artifact installation attempt finishes. + * + * @param event the event containing the artifact, local repository path, and any failure + */ default void artifactInstalled(@Nonnull RepositoryEvent event) {} + /** + * Called before repository metadata is installed into the local repository. + * + * @param event the event containing the metadata and source path + */ default void metadataInstalling(@Nonnull RepositoryEvent event) {} + /** + * Called after a repository metadata installation attempt finishes. + * + * @param event the event containing the metadata, local repository path, and any failure + */ default void metadataInstalled(@Nonnull RepositoryEvent event) {} + /** + * Called before an artifact is deployed to a remote repository. + * + * @param event the event containing the artifact, remote repository, and source path + */ default void artifactDeploying(@Nonnull RepositoryEvent event) {} + /** + * Called after an artifact deployment attempt finishes. + * + * @param event the event containing the artifact, remote repository, and any failure + */ default void artifactDeployed(@Nonnull RepositoryEvent event) {} + /** + * Called before repository metadata is deployed to a remote repository. + * + * @param event the event containing the metadata, remote repository, and source path + */ default void metadataDeploying(@Nonnull RepositoryEvent event) {} + /** + * Called after a repository metadata deployment attempt finishes. + * + * @param event the event containing the metadata, remote repository, and any failure + */ default void metadataDeployed(@Nonnull RepositoryEvent event) {} } diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryMetadata.java b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryMetadata.java index 5ad861a1832b..7081ff8e1e4c 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryMetadata.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryMetadata.java @@ -35,30 +35,59 @@ @Immutable public interface RepositoryMetadata { + /** + * Describes which artifact versions a metadata item applies to. + */ enum Nature { + /** Metadata that applies only to release versions. */ RELEASE, + + /** Metadata that applies only to snapshot versions. */ SNAPSHOT, + + /** Metadata that applies to both release and snapshot versions. */ RELEASE_OR_SNAPSHOT, } + /** + * {@return the group identifier, or an empty string if the metadata applies to the entire repository} + */ @Nonnull String getGroupId(); + /** + * {@return the artifact identifier, or an empty string if the metadata applies at group level} + */ @Nonnull String getArtifactId(); + /** + * {@return the version, or an empty string if the metadata applies at artifact level} + */ @Nonnull String getVersion(); + /** + * {@return the metadata filename, such as {@code maven-metadata.xml}} + */ @Nonnull String getType(); + /** + * {@return the artifact version nature to which the metadata applies} + */ @Nonnull Nature getNature(); + /** + * {@return the local path of the metadata file if it has been resolved} + */ @Nonnull Optional getPath(); + /** + * {@return the read-only properties associated with the metadata} + */ @Nonnull Map getProperties(); } From 95b9d846f084175fc67ec712e0ffc334693e5449 Mon Sep 17 00:00:00 2001 From: goutamadwant Date: Sun, 6 Sep 2026 22:24:54 -0700 Subject: [PATCH 4/8] [MNG-8547] Unify typed event listener registration Route execution and repository events through one shared listener registry. Add typed execution callbacks and noun-style event accessors while preserving existing execution Event getters and Listener lambdas. Cover all event callbacks, combined listeners, derived-session registration, concurrent updates, and failure isolation. --- .../main/java/org/apache/maven/api/Event.java | 7 +- .../org/apache/maven/api/ExecutionEvent.java | 80 +++++ .../apache/maven/api/ExecutionEventType.java | 47 +++ .../apache/maven/api/ExecutionListener.java | 152 ++++++++ .../java/org/apache/maven/api/Listener.java | 8 +- .../org/apache/maven/api/RepositoryEvent.java | 20 +- .../apache/maven/api/RepositoryListener.java | 2 +- .../java/org/apache/maven/api/Session.java | 32 +- .../org/apache/maven/api/SessionEvent.java | 37 ++ .../org/apache/maven/api/TypedListener.java | 42 +++ .../maven/internal/impl/DefaultEvent.java | 32 +- .../maven/internal/impl/EventSpyImpl.java | 54 ++- ...ultRepositorySystemSessionFactoryTest.java | 4 +- .../maven/internal/impl/EventSpyImplTest.java | 335 ++++++++++++++++++ .../apache/maven/impl/AbstractSession.java | 29 +- .../maven/impl/DefaultRepositoryEvent.java | 18 +- .../maven/impl/MavenRepositoryListener.java | 13 +- .../impl/MavenRepositoryListenerTest.java | 36 +- .../impl/standalone/RequestTraceTest.java | 8 +- .../testing/plugin/stubs/SessionStub.java | 12 - 20 files changed, 837 insertions(+), 131 deletions(-) create mode 100644 api/maven-api-core/src/main/java/org/apache/maven/api/ExecutionEvent.java create mode 100644 api/maven-api-core/src/main/java/org/apache/maven/api/ExecutionEventType.java create mode 100644 api/maven-api-core/src/main/java/org/apache/maven/api/ExecutionListener.java create mode 100644 api/maven-api-core/src/main/java/org/apache/maven/api/SessionEvent.java create mode 100644 api/maven-api-core/src/main/java/org/apache/maven/api/TypedListener.java create mode 100644 impl/maven-core/src/test/java/org/apache/maven/internal/impl/EventSpyImplTest.java diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/Event.java b/api/maven-api-core/src/main/java/org/apache/maven/api/Event.java index 0323abd43579..2c2f7cde5827 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/Event.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/Event.java @@ -31,7 +31,12 @@ * @since 4.0.0 */ @Experimental -public interface Event { +public interface Event extends SessionEvent { + + @Override + default Session session() { + return getSession(); + } /** * Gets the type of the event. diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/ExecutionEvent.java b/api/maven-api-core/src/main/java/org/apache/maven/api/ExecutionEvent.java new file mode 100644 index 000000000000..003a64ab0857 --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/ExecutionEvent.java @@ -0,0 +1,80 @@ +/* + * 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; + +import java.util.Optional; + +import org.apache.maven.api.annotations.Experimental; +import org.apache.maven.api.annotations.Immutable; +import org.apache.maven.api.annotations.Nonnull; + +/** + * A build execution event with noun-style accessors. + * Extends {@link Event} so existing execution listeners can consume the same event. + * + * @since 4.1.0 + */ +@Experimental +@Immutable +public interface ExecutionEvent extends Event { + /** {@return the kind of execution operation} */ + @Nonnull + ExecutionEventType type(); + + /** {@return the current project, if applicable} */ + @Nonnull + Optional project(); + + /** {@return the current mojo execution, if applicable} */ + @Nonnull + Optional mojoExecution(); + + /** {@return the failure associated with this event, if any} */ + @Nonnull + Optional exception(); + + @Override + @Nonnull + Session session(); + + @Override + default EventType getType() { + return EventType.valueOf(type().name()); + } + + @Override + default Session getSession() { + return session(); + } + + @Override + default Optional getProject() { + return project(); + } + + @Override + default Optional getMojoExecution() { + return mojoExecution(); + } + + @Override + default Optional getException() { + return exception(); + } +} diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/ExecutionEventType.java b/api/maven-api-core/src/main/java/org/apache/maven/api/ExecutionEventType.java new file mode 100644 index 000000000000..b5727733f390 --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/ExecutionEventType.java @@ -0,0 +1,47 @@ +/* + * 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; + +import org.apache.maven.api.annotations.Experimental; + +/** + * The possible types of build execution events. + * + * @since 4.1.0 + */ +@Experimental +public enum ExecutionEventType { + PROJECT_DISCOVERY_STARTED, + SESSION_STARTED, + SESSION_ENDED, + PROJECT_SKIPPED, + PROJECT_STARTED, + PROJECT_SUCCEEDED, + PROJECT_FAILED, + MOJO_SKIPPED, + MOJO_STARTED, + MOJO_SUCCEEDED, + MOJO_FAILED, + FORK_STARTED, + FORK_SUCCEEDED, + FORK_FAILED, + FORKED_PROJECT_STARTED, + FORKED_PROJECT_SUCCEEDED, + FORKED_PROJECT_FAILED, +} diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/ExecutionListener.java b/api/maven-api-core/src/main/java/org/apache/maven/api/ExecutionListener.java new file mode 100644 index 000000000000..634dc1f6dbe4 --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/ExecutionListener.java @@ -0,0 +1,152 @@ +/* + * 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; + +import org.apache.maven.api.annotations.Consumer; +import org.apache.maven.api.annotations.Experimental; +import org.apache.maven.api.annotations.Nonnull; + +/** + * Receives build execution events through typed callbacks. + * Implementations must support concurrent notification during parallel builds. + * + * @since 4.1.0 + */ +@Experimental +@Consumer +public interface ExecutionListener extends TypedListener { + /** + * Called when project discovery has started. + * + * @param event the execution details, including the session and any project, mojo, or failure + */ + default void projectDiscoveryStarted(@Nonnull ExecutionEvent event) {} + + /** + * Called when the build session starts. + * + * @param event the execution details, including the session and any project, mojo, or failure + */ + default void sessionStarted(@Nonnull ExecutionEvent event) {} + + /** + * Called when the build session ends. + * + * @param event the execution details, including the session and any project, mojo, or failure + */ + default void sessionEnded(@Nonnull ExecutionEvent event) {} + + /** + * Called when a project is skipped. + * + * @param event the execution details, including the session and any project, mojo, or failure + */ + default void projectSkipped(@Nonnull ExecutionEvent event) {} + + /** + * Called when a project starts. + * + * @param event the execution details, including the session and any project, mojo, or failure + */ + default void projectStarted(@Nonnull ExecutionEvent event) {} + + /** + * Called when a project completes successfully. + * + * @param event the execution details, including the session and any project, mojo, or failure + */ + default void projectSucceeded(@Nonnull ExecutionEvent event) {} + + /** + * Called when a project fails. + * + * @param event the execution details, including the session and any project, mojo, or failure + */ + default void projectFailed(@Nonnull ExecutionEvent event) {} + + /** + * Called when a mojo is skipped. + * + * @param event the execution details, including the session and any project, mojo, or failure + */ + default void mojoSkipped(@Nonnull ExecutionEvent event) {} + + /** + * Called when a mojo starts. + * + * @param event the execution details, including the session and any project, mojo, or failure + */ + default void mojoStarted(@Nonnull ExecutionEvent event) {} + + /** + * Called when a mojo completes successfully. + * + * @param event the execution details, including the session and any project, mojo, or failure + */ + default void mojoSucceeded(@Nonnull ExecutionEvent event) {} + + /** + * Called when a mojo fails. + * + * @param event the execution details, including the session and any project, mojo, or failure + */ + default void mojoFailed(@Nonnull ExecutionEvent event) {} + + /** + * Called when a forked execution starts. + * + * @param event the execution details, including the session and any project, mojo, or failure + */ + default void forkStarted(@Nonnull ExecutionEvent event) {} + + /** + * Called when a forked execution completes successfully. + * + * @param event the execution details, including the session and any project, mojo, or failure + */ + default void forkSucceeded(@Nonnull ExecutionEvent event) {} + + /** + * Called when a forked execution fails. + * + * @param event the execution details, including the session and any project, mojo, or failure + */ + default void forkFailed(@Nonnull ExecutionEvent event) {} + + /** + * Called when a forked project starts. + * + * @param event the execution details, including the session and any project, mojo, or failure + */ + default void forkedProjectStarted(@Nonnull ExecutionEvent event) {} + + /** + * Called when a forked project completes successfully. + * + * @param event the execution details, including the session and any project, mojo, or failure + */ + default void forkedProjectSucceeded(@Nonnull ExecutionEvent event) {} + + /** + * Called when a forked project fails. + * + * @param event the execution details, including the session and any project, mojo, or failure + */ + default void forkedProjectFailed(@Nonnull ExecutionEvent event) {} +} diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/Listener.java b/api/maven-api-core/src/main/java/org/apache/maven/api/Listener.java index dda744f7375a..c04d013f7457 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/Listener.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/Listener.java @@ -24,7 +24,8 @@ /** * A listener for session events. - * TODO: open this to other events like similar to {@code org.apache.maven.eventspy.EventSpy} + * Existing implementations and lambdas receive execution events through {@link #onEvent(Event)}. + * Implement {@link ExecutionListener} or {@link RepositoryListener} for typed callbacks. * * @since 4.0.0 */ @@ -32,5 +33,10 @@ @FunctionalInterface @Consumer public interface Listener { + /** + * Receives a build execution event. + * + * @param event the execution event + */ void onEvent(@Nonnull Event event); } diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryEvent.java b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryEvent.java index 67fdfeb4ab9f..f0cb42543f4f 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryEvent.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryEvent.java @@ -34,60 +34,60 @@ */ @Experimental @Immutable -public interface RepositoryEvent { +public interface RepositoryEvent extends SessionEvent { /** * Returns the kind of repository operation represented by this event. */ @Nonnull - RepositoryEventType getType(); + RepositoryEventType type(); /** * Returns the Maven session associated with the underlying repository system session. * Sessions derived from it share the same repository event and listener scope. */ @Nonnull - Session getSession(); + Session session(); /** * Returns the artifact involved in the event, if any. */ @Nonnull - Optional getArtifact(); + Optional artifact(); /** * Returns the metadata involved in the event, if any. */ @Nonnull - Optional getMetadata(); + Optional metadata(); /** * Returns the local path involved in the event, if any. */ @Nonnull - Optional getPath(); + Optional path(); /** * Returns the repository involved in the event, if any. */ @Nonnull - Optional getRepository(); + Optional repository(); /** * Returns the primary failure associated with the event, if any. */ @Nonnull - Optional getException(); + Optional exception(); /** * Returns all failures associated with the event. */ @Nonnull - List getExceptions(); + List exceptions(); /** * Returns the request trace associated with the event, if any. */ @Nonnull - Optional getTrace(); + Optional trace(); } diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryListener.java b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryListener.java index 9b8a2dc87b90..c0259a2a2398 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryListener.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryListener.java @@ -31,7 +31,7 @@ */ @Experimental @Consumer -public interface RepositoryListener { +public interface RepositoryListener extends TypedListener { /** * Called when an artifact descriptor could not be parsed. diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/Session.java b/api/maven-api-core/src/main/java/org/apache/maven/api/Session.java index f9fb130183a0..0900f2c5eaf8 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/Session.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/Session.java @@ -180,7 +180,9 @@ default Map getEffectiveProperties() { Session withRemoteRepositories(@Nonnull List repositories); /** - * Register the given listener which will receive all events. + * Registers a listener for execution events, repository events, or both. + * Typed listeners receive only their event-specific callbacks. Legacy listeners receive execution events. + * Registration is shared by Maven sessions using the same underlying repository system session. * * @param listener the listener to register * @throws NullPointerException if {@code listener} is null @@ -203,34 +205,6 @@ default Map getEffectiveProperties() { @Nonnull Collection getListeners(); - /** - * Registers a listener for repository events. - * Repository listener registration is scoped to the underlying repository system session and is therefore - * shared with sessions derived from this session. - * - * @param listener the listener to register - * @throws NullPointerException if {@code listener} is null - */ - void registerListener(@Nonnull RepositoryListener listener); - - /** - * Unregisters a previously registered repository listener. - * The listener is removed from the scope shared by sessions derived from the same repository system session. - * - * @param listener the listener to unregister - * @throws NullPointerException if {@code listener} is null - */ - void unregisterListener(@Nonnull RepositoryListener listener); - - /** - * Returns the registered repository listeners. - * The returned listeners belong to the scope shared by sessions derived from the same repository system session. - * - * @return an immutable collection of listeners, never {@code null} - */ - @Nonnull - Collection getRepositoryListeners(); - /** * Shortcut for {@code getService(RepositoryFactory.class).createLocal(...)}. * diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/SessionEvent.java b/api/maven-api-core/src/main/java/org/apache/maven/api/SessionEvent.java new file mode 100644 index 000000000000..822f0d38eb49 --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/SessionEvent.java @@ -0,0 +1,37 @@ +/* + * 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; + +import org.apache.maven.api.annotations.Experimental; +import org.apache.maven.api.annotations.Nonnull; + +/** + * An event associated with a Maven session. + * Execution and repository events expose their own operation-specific details. + * + * @since 4.1.0 + */ +@Experimental +public interface SessionEvent { + /** + * {@return the session associated with this event} + */ + @Nonnull + Session session(); +} diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/TypedListener.java b/api/maven-api-core/src/main/java/org/apache/maven/api/TypedListener.java new file mode 100644 index 000000000000..760991584e11 --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/TypedListener.java @@ -0,0 +1,42 @@ +/* + * 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; + +import org.apache.maven.api.annotations.Consumer; +import org.apache.maven.api.annotations.Experimental; +import org.apache.maven.api.annotations.Nonnull; + +/** + * Base for listeners notified through event-specific callbacks. + * Implement {@link ExecutionListener}, {@link RepositoryListener}, or both. + * The shared default implementation allows both interfaces to be implemented without conflicting defaults. + * + * @since 4.1.0 + */ +@Experimental +@Consumer +public interface TypedListener extends Listener { + /** + * Does nothing. Typed listeners are notified through their event-specific callbacks. + * + * @param event the legacy execution event + */ + @Override + default void onEvent(@Nonnull Event event) {} +} diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultEvent.java b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultEvent.java index 7003824de62f..abffffc64d89 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultEvent.java +++ b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultEvent.java @@ -20,46 +20,52 @@ import java.util.Optional; -import org.apache.maven.api.Event; import org.apache.maven.api.EventType; +import org.apache.maven.api.ExecutionEventType; import org.apache.maven.api.MojoExecution; import org.apache.maven.api.Project; import org.apache.maven.api.Session; import org.apache.maven.execution.ExecutionEvent; -public class DefaultEvent implements Event { +public class DefaultEvent implements org.apache.maven.api.ExecutionEvent { private final InternalMavenSession session; - private final ExecutionEvent delegate; + private final Project project; + private final MojoExecution mojoExecution; + private final Exception exception; private final EventType eventType; public DefaultEvent(InternalMavenSession session, ExecutionEvent delegate, EventType eventType) { this.session = session; - this.delegate = delegate; + this.project = session.getProject(delegate.getProject()); + this.mojoExecution = delegate.getMojoExecution() != null + ? new DefaultMojoExecution(session, delegate.getMojoExecution()) + : null; + this.exception = delegate.getException(); this.eventType = eventType; } @Override - public EventType getType() { - return eventType; + public ExecutionEventType type() { + return ExecutionEventType.valueOf(eventType.name()); } @Override - public Session getSession() { + public Session session() { return session; } @Override - public Optional getProject() { - return Optional.ofNullable(session.getProject(delegate.getProject())); + public Optional project() { + return Optional.ofNullable(project); } @Override - public Optional getMojoExecution() { - return Optional.ofNullable(delegate.getMojoExecution()).map(me -> new DefaultMojoExecution(session, me)); + public Optional mojoExecution() { + return Optional.ofNullable(mojoExecution); } @Override - public Optional getException() { - return Optional.ofNullable(delegate.getException()); + public Optional exception() { + return Optional.ofNullable(exception); } } diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/EventSpyImpl.java b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/EventSpyImpl.java index 757b08a7eedb..0fdfaa4f48f4 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/EventSpyImpl.java +++ b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/EventSpyImpl.java @@ -23,11 +23,14 @@ import java.util.Collection; -import org.apache.maven.api.Event; import org.apache.maven.api.EventType; +import org.apache.maven.api.ExecutionListener; import org.apache.maven.api.Listener; +import org.apache.maven.api.TypedListener; import org.apache.maven.eventspy.EventSpy; import org.apache.maven.execution.ExecutionEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Bridges between Maven3 events and Maven4 events. @@ -35,6 +38,8 @@ @Named @Singleton public class EventSpyImpl implements EventSpy { + private static final Logger LOGGER = LoggerFactory.getLogger(EventSpyImpl.class); + @Override public void init(Context context) throws Exception {} @@ -46,16 +51,57 @@ public void onEvent(Object arg) throws Exception { EventType eventType = convert(ee.getType()); Collection listeners = session.getListeners(); if (!listeners.isEmpty()) { - Event event = new DefaultEvent(session, ee, eventType); + org.apache.maven.api.ExecutionEvent event = null; for (Listener listener : listeners) { - listener.onEvent(event); + if (listener instanceof TypedListener && !(listener instanceof ExecutionListener)) { + continue; + } + if (event == null) { + event = new DefaultEvent(session, ee, eventType); + } + try { + if (listener instanceof ExecutionListener executionListener) { + dispatch(executionListener, event); + } else { + listener.onEvent(event); + } + } catch (RuntimeException e) { + LOGGER.warn( + "Failed to notify execution listener {} about {}", + listener.getClass().getName(), + event.type(), + e); + } } } } } + private static void dispatch(ExecutionListener listener, org.apache.maven.api.ExecutionEvent event) { + switch (event.type()) { + case PROJECT_DISCOVERY_STARTED -> listener.projectDiscoveryStarted(event); + case SESSION_STARTED -> listener.sessionStarted(event); + case SESSION_ENDED -> listener.sessionEnded(event); + case PROJECT_SKIPPED -> listener.projectSkipped(event); + case PROJECT_STARTED -> listener.projectStarted(event); + case PROJECT_SUCCEEDED -> listener.projectSucceeded(event); + case PROJECT_FAILED -> listener.projectFailed(event); + case MOJO_SKIPPED -> listener.mojoSkipped(event); + case MOJO_STARTED -> listener.mojoStarted(event); + case MOJO_SUCCEEDED -> listener.mojoSucceeded(event); + case MOJO_FAILED -> listener.mojoFailed(event); + case FORK_STARTED -> listener.forkStarted(event); + case FORK_SUCCEEDED -> listener.forkSucceeded(event); + case FORK_FAILED -> listener.forkFailed(event); + case FORKED_PROJECT_STARTED -> listener.forkedProjectStarted(event); + case FORKED_PROJECT_SUCCEEDED -> listener.forkedProjectSucceeded(event); + case FORKED_PROJECT_FAILED -> listener.forkedProjectFailed(event); + default -> throw new IllegalArgumentException("Unsupported execution event: " + event.type()); + } + } + /** - * Simple "conversion" from Maven3 event type enum to Maven4 enum. + * Converts the Maven 3 execution event type to its Maven API counterpart. */ protected EventType convert(ExecutionEvent.Type type) { return EventType.values()[type.ordinal()]; diff --git a/impl/maven-core/src/test/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactoryTest.java b/impl/maven-core/src/test/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactoryTest.java index 0547142fca3c..c6d30a13e562 100644 --- a/impl/maven-core/src/test/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactoryTest.java +++ b/impl/maven-core/src/test/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactoryTest.java @@ -105,7 +105,7 @@ public void artifactResolved(RepositoryEvent event) { received.set(event); } }; - when(session.getRepositoryListeners()).thenReturn(List.of(listener)); + when(session.getListeners()).thenReturn(List.of(listener)); InternalSession.associate(resolverSession, session); resolverSession @@ -115,7 +115,7 @@ public void artifactResolved(RepositoryEvent event) { .build()); assertNotNull(received.get()); - assertEquals(RepositoryEventType.ARTIFACT_RESOLVED, received.get().getType()); + assertEquals(RepositoryEventType.ARTIFACT_RESOLVED, received.get().type()); } @Test diff --git a/impl/maven-core/src/test/java/org/apache/maven/internal/impl/EventSpyImplTest.java b/impl/maven-core/src/test/java/org/apache/maven/internal/impl/EventSpyImplTest.java new file mode 100644 index 000000000000..6b8acd431dd6 --- /dev/null +++ b/impl/maven-core/src/test/java/org/apache/maven/internal/impl/EventSpyImplTest.java @@ -0,0 +1,335 @@ +/* + * 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.internal.impl; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.maven.api.Event; +import org.apache.maven.api.EventType; +import org.apache.maven.api.ExecutionEvent; +import org.apache.maven.api.ExecutionEventType; +import org.apache.maven.api.ExecutionListener; +import org.apache.maven.api.Listener; +import org.apache.maven.api.RepositoryEvent; +import org.apache.maven.api.RepositoryListener; +import org.apache.maven.api.Session; +import org.apache.maven.execution.DefaultMavenExecutionRequest; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.impl.InternalSession; +import org.apache.maven.impl.MavenRepositoryListener; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.RepositorySystem; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertIterableEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class EventSpyImplTest { + @Test + void dispatchesAllExecutionCallbacksAndPreservesLegacyEvents() throws Exception { + TestContext context = new TestContext(); + RecordingListener typed = new RecordingListener(); + List legacy = new ArrayList<>(); + context.session.registerListener(typed); + context.session.registerListener(legacy::add); + + for (org.apache.maven.execution.ExecutionEvent.Type type : + org.apache.maven.execution.ExecutionEvent.Type.values()) { + context.execution(type); + } + + assertEquals(List.of(ExecutionEventType.values()), typed.executions); + assertEquals( + List.of(EventType.values()), legacy.stream().map(Event::getType).toList()); + for (Event event : legacy) { + ExecutionEvent typedEvent = (ExecutionEvent) event; + assertSame(context.session, event.getSession()); + assertSame(event.getSession(), typedEvent.session()); + assertEquals(event.getProject(), typedEvent.project()); + assertEquals(event.getMojoExecution(), typedEvent.mojoExecution()); + assertEquals(event.getException(), typedEvent.exception()); + } + } + + @Test + void sharesOneRegistrationForBothEventFamiliesAcrossDerivedSessions() throws Exception { + TestContext context = new TestContext(); + RecordingListener combined = new RecordingListener(); + AtomicInteger legacyCalls = new AtomicInteger(); + Listener legacy = event -> legacyCalls.incrementAndGet(); + AtomicInteger repositoryOnlyCalls = new AtomicInteger(); + RepositoryListener repositoryOnly = new RepositoryListener() { + @Override + public void artifactResolving(RepositoryEvent event) { + repositoryOnlyCalls.incrementAndGet(); + } + + @Override + public void onEvent(Event event) { + throw new AssertionError("Repository-only listener received an execution event"); + } + }; + context.session.registerListener(combined); + context.session.registerListener(legacy); + context.session.registerListener(repositoryOnly); + Session derived = context.session.withRemoteRepositories(List.of()); + assertIterableEquals(context.session.getListeners(), derived.getListeners()); + assertThrows( + UnsupportedOperationException.class, + () -> derived.getListeners().clear()); + + context.execution(org.apache.maven.execution.ExecutionEvent.Type.SessionStarted); + context.repository(); + assertEquals(List.of(ExecutionEventType.SESSION_STARTED), combined.executions); + assertEquals(1, combined.repositoryCalls); + assertEquals(1, repositoryOnlyCalls.get()); + assertEquals(1, legacyCalls.get()); + + derived.unregisterListener(combined); + derived.unregisterListener(legacy); + derived.unregisterListener(repositoryOnly); + context.execution(org.apache.maven.execution.ExecutionEvent.Type.SessionStarted); + context.repository(); + assertTrue(context.session.getListeners().isEmpty()); + assertEquals(1, combined.repositoryCalls); + assertEquals(1, legacyCalls.get()); + } + + @Test + void isolatesExecutionListenerFailures() throws Exception { + TestContext context = new TestContext(); + context.session.registerListener(new ExecutionListener() { + @Override + public void sessionStarted(ExecutionEvent event) { + throw new IllegalStateException("listener failure"); + } + }); + AtomicInteger calls = new AtomicInteger(); + context.session.registerListener(event -> calls.incrementAndGet()); + context.execution(org.apache.maven.execution.ExecutionEvent.Type.SessionStarted); + assertEquals(1, calls.get()); + } + + @Test + void rejectsNullRegistrationAndRemoval() { + TestContext context = new TestContext(); + assertThrows(NullPointerException.class, () -> context.session.registerListener(null)); + assertThrows(NullPointerException.class, () -> context.session.unregisterListener(null)); + } + + @Test + void supportsCombinedListenerWithoutLegacyOverride() throws Exception { + TestContext context = new TestContext(); + class CombinedListener implements ExecutionListener, RepositoryListener { + int executionCalls; + int repositoryCalls; + + @Override + public void sessionStarted(ExecutionEvent event) { + executionCalls++; + } + + @Override + public void artifactResolving(RepositoryEvent event) { + repositoryCalls++; + } + } + CombinedListener listener = new CombinedListener(); + context.session.registerListener(listener); + context.execution(org.apache.maven.execution.ExecutionEvent.Type.SessionStarted); + context.repository(); + assertEquals(1, listener.executionCalls); + assertEquals(1, listener.repositoryCalls); + } + + @Test + void supportsConcurrentRegistrationAndRemovalThroughDerivedSession() { + TestContext context = new TestContext(); + Session derived = context.session.withRemoteRepositories(List.of()); + List listeners = java.util.stream.IntStream.range(0, 100) + .mapToObj(i -> (Listener) new ExecutionListener() {}) + .toList(); + listeners.parallelStream().forEach(context.session::registerListener); + assertEquals(100, derived.getListeners().size()); + listeners.parallelStream().forEach(derived::unregisterListener); + assertTrue(context.session.getListeners().isEmpty()); + } + + @Test + void capturesExecutionDetailsWhenEventIsCreated() { + TestContext context = new TestContext(); + org.apache.maven.execution.ExecutionEvent source = mock(org.apache.maven.execution.ExecutionEvent.class); + Exception failure = new Exception("original failure"); + when(source.getException()).thenReturn(failure); + ExecutionEvent event = new DefaultEvent(context.session, source, EventType.PROJECT_FAILED); + when(source.getException()).thenReturn(new Exception("changed failure")); + assertSame(failure, event.exception().orElseThrow()); + assertSame(failure, event.getException().orElseThrow()); + } + + private static class TestContext { + final DefaultRepositorySystemSession resolver = new DefaultRepositorySystemSession(h -> false); + final MavenSession maven = new MavenSession(null, resolver, new DefaultMavenExecutionRequest(), null); + final DefaultSession session = + new DefaultSession(maven, mock(RepositorySystem.class), List.of(), null, null, null); + + TestContext() { + maven.setSession(session); + resolver.getData().set(InternalSession.class, session); + } + + void execution(org.apache.maven.execution.ExecutionEvent.Type type) throws Exception { + org.apache.maven.execution.ExecutionEvent event = mock(org.apache.maven.execution.ExecutionEvent.class); + when(event.getSession()).thenReturn(maven); + when(event.getType()).thenReturn(type); + new EventSpyImpl().onEvent(event); + } + + void repository() { + new MavenRepositoryListener() + .artifactResolving(new org.eclipse.aether.RepositoryEvent.Builder( + resolver, org.eclipse.aether.RepositoryEvent.EventType.ARTIFACT_RESOLVING) + .build()); + } + } + + private static class RecordingListener implements ExecutionListener, RepositoryListener { + final List executions = new ArrayList<>(); + int repositoryCalls; + + @Override + public void onEvent(Event event) { + throw new AssertionError("Typed listener received a duplicate legacy callback"); + } + + @Override + public void artifactResolving(RepositoryEvent event) { + repositoryCalls++; + } + + @Override + public void projectDiscoveryStarted(ExecutionEvent event) { + assertEquals(ExecutionEventType.PROJECT_DISCOVERY_STARTED, event.type()); + executions.add(ExecutionEventType.PROJECT_DISCOVERY_STARTED); + } + + @Override + public void sessionStarted(ExecutionEvent event) { + assertEquals(ExecutionEventType.SESSION_STARTED, event.type()); + executions.add(ExecutionEventType.SESSION_STARTED); + } + + @Override + public void sessionEnded(ExecutionEvent event) { + assertEquals(ExecutionEventType.SESSION_ENDED, event.type()); + executions.add(ExecutionEventType.SESSION_ENDED); + } + + @Override + public void projectSkipped(ExecutionEvent event) { + assertEquals(ExecutionEventType.PROJECT_SKIPPED, event.type()); + executions.add(ExecutionEventType.PROJECT_SKIPPED); + } + + @Override + public void projectStarted(ExecutionEvent event) { + assertEquals(ExecutionEventType.PROJECT_STARTED, event.type()); + executions.add(ExecutionEventType.PROJECT_STARTED); + } + + @Override + public void projectSucceeded(ExecutionEvent event) { + assertEquals(ExecutionEventType.PROJECT_SUCCEEDED, event.type()); + executions.add(ExecutionEventType.PROJECT_SUCCEEDED); + } + + @Override + public void projectFailed(ExecutionEvent event) { + assertEquals(ExecutionEventType.PROJECT_FAILED, event.type()); + executions.add(ExecutionEventType.PROJECT_FAILED); + } + + @Override + public void mojoSkipped(ExecutionEvent event) { + assertEquals(ExecutionEventType.MOJO_SKIPPED, event.type()); + executions.add(ExecutionEventType.MOJO_SKIPPED); + } + + @Override + public void mojoStarted(ExecutionEvent event) { + assertEquals(ExecutionEventType.MOJO_STARTED, event.type()); + executions.add(ExecutionEventType.MOJO_STARTED); + } + + @Override + public void mojoSucceeded(ExecutionEvent event) { + assertEquals(ExecutionEventType.MOJO_SUCCEEDED, event.type()); + executions.add(ExecutionEventType.MOJO_SUCCEEDED); + } + + @Override + public void mojoFailed(ExecutionEvent event) { + assertEquals(ExecutionEventType.MOJO_FAILED, event.type()); + executions.add(ExecutionEventType.MOJO_FAILED); + } + + @Override + public void forkStarted(ExecutionEvent event) { + assertEquals(ExecutionEventType.FORK_STARTED, event.type()); + executions.add(ExecutionEventType.FORK_STARTED); + } + + @Override + public void forkSucceeded(ExecutionEvent event) { + assertEquals(ExecutionEventType.FORK_SUCCEEDED, event.type()); + executions.add(ExecutionEventType.FORK_SUCCEEDED); + } + + @Override + public void forkFailed(ExecutionEvent event) { + assertEquals(ExecutionEventType.FORK_FAILED, event.type()); + executions.add(ExecutionEventType.FORK_FAILED); + } + + @Override + public void forkedProjectStarted(ExecutionEvent event) { + assertEquals(ExecutionEventType.FORKED_PROJECT_STARTED, event.type()); + executions.add(ExecutionEventType.FORKED_PROJECT_STARTED); + } + + @Override + public void forkedProjectSucceeded(ExecutionEvent event) { + assertEquals(ExecutionEventType.FORKED_PROJECT_SUCCEEDED, event.type()); + executions.add(ExecutionEventType.FORKED_PROJECT_SUCCEEDED); + } + + @Override + public void forkedProjectFailed(ExecutionEvent event) { + assertEquals(ExecutionEventType.FORKED_PROJECT_FAILED, event.type()); + executions.add(ExecutionEventType.FORKED_PROJECT_FAILED); + } + } +} diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/AbstractSession.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/AbstractSession.java index 9347815a6893..ee7e3c227f8f 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/AbstractSession.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/AbstractSession.java @@ -56,7 +56,6 @@ import org.apache.maven.api.Project; import org.apache.maven.api.ProjectScope; import org.apache.maven.api.RemoteRepository; -import org.apache.maven.api.RepositoryListener; import org.apache.maven.api.Service; import org.apache.maven.api.Session; import org.apache.maven.api.SessionData; @@ -123,8 +122,7 @@ public abstract class AbstractSession implements InternalSession { protected final Lookup lookup; protected final Injector injector; private final Map, Service> services = new ConcurrentHashMap<>(); - private final List listeners = new CopyOnWriteArrayList<>(); - private final List repositoryListeners; + private final List listeners; private final Cache allNodes = Cache.newCache(Cache.ReferenceType.WEAK, "AbstractSession-Nodes"); private final Map, Cache> allArtifacts = @@ -150,14 +148,13 @@ public AbstractSession( this.repositories = getRepositories(repositories, resolverRepositories); this.lookup = lookup; this.injector = lookup != null ? lookup.lookupOptional(Injector.class).orElse(null) : null; - this.repositoryListeners = getRepositoryListeners(session); + this.listeners = getListeners(session); } @SuppressWarnings("unchecked") - private static List getRepositoryListeners(RepositorySystemSession session) { - // Resolver events are scoped to RepositorySystemSession, so derived Maven sessions share this listener set. - return (List) - session.getData().computeIfAbsent(RepositoryListener.class, CopyOnWriteArrayList::new); + private static List getListeners(RepositorySystemSession session) { + // Both event families share the registration scope of the underlying repository system session. + return (List) session.getData().computeIfAbsent(Listener.class, CopyOnWriteArrayList::new); } @SuppressWarnings("unchecked") @@ -541,22 +538,6 @@ public Collection getListeners() { return Collections.unmodifiableCollection(listeners); } - @Override - public void registerListener(@Nonnull RepositoryListener listener) { - repositoryListeners.add(requireNonNull(listener)); - } - - @Override - public void unregisterListener(@Nonnull RepositoryListener listener) { - repositoryListeners.remove(requireNonNull(listener)); - } - - @Nonnull - @Override - public Collection getRepositoryListeners() { - return Collections.unmodifiableCollection(repositoryListeners); - } - // // Shortcut implementations // diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultRepositoryEvent.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultRepositoryEvent.java index 9de5d4c3e8b4..fbf836f0d808 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultRepositoryEvent.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultRepositoryEvent.java @@ -59,47 +59,47 @@ final class DefaultRepositoryEvent implements RepositoryEvent { } @Override - public RepositoryEventType getType() { + public RepositoryEventType type() { return type; } @Override - public Session getSession() { + public Session session() { return session; } @Override - public Optional getArtifact() { + public Optional artifact() { return Optional.ofNullable(artifact); } @Override - public Optional getMetadata() { + public Optional metadata() { return Optional.ofNullable(metadata); } @Override - public Optional getPath() { + public Optional path() { return Optional.ofNullable(path); } @Override - public Optional getRepository() { + public Optional repository() { return Optional.ofNullable(repository); } @Override - public Optional getException() { + public Optional exception() { return Optional.ofNullable(exception); } @Override - public List getExceptions() { + public List exceptions() { return exceptions; } @Override - public Optional getTrace() { + public Optional trace() { return Optional.ofNullable(trace); } diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/MavenRepositoryListener.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/MavenRepositoryListener.java index 20f8c94f13ab..c68c80eb4142 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/MavenRepositoryListener.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/MavenRepositoryListener.java @@ -21,6 +21,7 @@ import java.util.Collection; import java.util.function.BiConsumer; +import org.apache.maven.api.Listener; import org.apache.maven.api.RepositoryEvent; import org.apache.maven.api.RepositoryListener; import org.slf4j.Logger; @@ -39,10 +40,16 @@ private void dispatch( if (!(associatedSession instanceof InternalSession session)) { return; } - Collection listeners = session.getRepositoryListeners(); + Collection listeners = session.getListeners(); if (!listeners.isEmpty()) { - RepositoryEvent repositoryEvent = new DefaultRepositoryEvent(session, event); - for (RepositoryListener listener : listeners) { + RepositoryEvent repositoryEvent = null; + for (Listener registered : listeners) { + if (!(registered instanceof RepositoryListener listener)) { + continue; + } + if (repositoryEvent == null) { + repositoryEvent = new DefaultRepositoryEvent(session, event); + } try { consumer.accept(listener, repositoryEvent); } catch (RuntimeException e) { diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/MavenRepositoryListenerTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/MavenRepositoryListenerTest.java index 487d53528c89..1ff61c2c7292 100644 --- a/impl/maven-impl/src/test/java/org/apache/maven/impl/MavenRepositoryListenerTest.java +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/MavenRepositoryListenerTest.java @@ -50,7 +50,7 @@ class MavenRepositoryListenerTest { @Test - void repositoryListenerOverloadDoesNotMakeBuildListenerLambdaAmbiguous() { + void unifiedRegistrationPreservesBuildListenerLambdas() { Session session = mock(Session.class); session.registerListener(event -> {}); @@ -72,7 +72,7 @@ void ignoresEventsBeforeMavenSessionAssociation() { void dispatchesAllRepositoryEventTypes() { TestContext context = new TestContext(); RecordingListener listener = new RecordingListener(); - when(context.session.getRepositoryListeners()).thenReturn(List.of(listener)); + when(context.session.getListeners()).thenReturn(List.of(listener)); for (org.eclipse.aether.RepositoryEvent.EventType type : org.eclipse.aether.RepositoryEvent.EventType.values()) { @@ -88,7 +88,7 @@ void dispatchesAllRepositoryEventTypes() { void convertsArtifactRepositoryFailurePathAndTrace() { TestContext context = new TestContext(); RecordingListener listener = new RecordingListener(); - when(context.session.getRepositoryListeners()).thenReturn(List.of(listener)); + when(context.session.getListeners()).thenReturn(List.of(listener)); org.eclipse.aether.artifact.Artifact resolverArtifact = new org.eclipse.aether.artifact.DefaultArtifact("org.example:demo:jar:1.0"); @@ -112,24 +112,24 @@ void convertsArtifactRepositoryFailurePathAndTrace() { .build()); RepositoryEvent event = listener.events.get(0); - assertEquals(RepositoryEventType.ARTIFACT_RESOLVED, event.getType()); - assertSame(context.session, event.getSession()); - assertSame(artifact, event.getArtifact().orElseThrow()); - assertSame(repository, event.getRepository().orElseThrow()); - assertEquals(path, event.getPath().orElseThrow()); - assertSame(failure, event.getException().orElseThrow()); - assertEquals(List.of(failure), event.getExceptions()); - assertEquals("request", event.getTrace().orElseThrow().data()); - assertTrue(event.getMetadata().isEmpty()); + assertEquals(RepositoryEventType.ARTIFACT_RESOLVED, event.type()); + assertSame(context.session, event.session()); + assertSame(artifact, event.artifact().orElseThrow()); + assertSame(repository, event.repository().orElseThrow()); + assertEquals(path, event.path().orElseThrow()); + assertSame(failure, event.exception().orElseThrow()); + assertEquals(List.of(failure), event.exceptions()); + assertEquals("request", event.trace().orElseThrow().data()); + assertTrue(event.metadata().isEmpty()); assertThrows( - UnsupportedOperationException.class, () -> event.getExceptions().add(new Exception())); + UnsupportedOperationException.class, () -> event.exceptions().add(new Exception())); } @Test void convertsMetadataWithoutExposingResolverMetadata() { TestContext context = new TestContext(); RecordingListener listener = new RecordingListener(); - when(context.session.getRepositoryListeners()).thenReturn(List.of(listener)); + when(context.session.getListeners()).thenReturn(List.of(listener)); Path path = Path.of("target", "maven-metadata.xml"); Metadata metadata = new DefaultMetadata( "org.example", @@ -145,7 +145,7 @@ void convertsMetadataWithoutExposingResolverMetadata() { .setMetadata(metadata) .build()); - RepositoryMetadata converted = listener.events.get(0).getMetadata().orElseThrow(); + RepositoryMetadata converted = listener.events.get(0).metadata().orElseThrow(); assertEquals("org.example", converted.getGroupId()); assertEquals("demo", converted.getArtifactId()); assertEquals("1.0-SNAPSHOT", converted.getVersion()); @@ -174,7 +174,7 @@ public void artifactResolving(RepositoryEvent event) { notifications.incrementAndGet(); } }; - when(context.session.getRepositoryListeners()).thenReturn(List.of(failing, succeeding)); + when(context.session.getListeners()).thenReturn(List.of(failing, succeeding)); context.bridge.artifactResolving(new org.eclipse.aether.RepositoryEvent.Builder( context.resolverSession, org.eclipse.aether.RepositoryEvent.EventType.ARTIFACT_RESOLVING) @@ -193,7 +193,7 @@ public void artifactResolving(RepositoryEvent event) { notifications.incrementAndGet(); } }; - when(context.session.getRepositoryListeners()).thenReturn(List.of(listener)); + when(context.session.getListeners()).thenReturn(List.of(listener)); IntStream.range(0, 100) .parallel() @@ -245,7 +245,7 @@ private static final class RecordingListener implements RepositoryListener { private final List events = new ArrayList<>(); private void record(RepositoryEvent event) { - types.add(event.getType()); + types.add(event.type()); events.add(event); } diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/standalone/RequestTraceTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/standalone/RequestTraceTest.java index a4bd045d5572..05ec85e270b4 100644 --- a/impl/maven-impl/src/test/java/org/apache/maven/impl/standalone/RequestTraceTest.java +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/standalone/RequestTraceTest.java @@ -102,15 +102,15 @@ public void artifactResolved(RepositoryEvent event) { .next(); for (RepositoryEvent event : events) { - RequestTrace rTrace = event.getTrace().orElseThrow(); + RequestTrace rTrace = event.trace().orElseThrow(); assertNotNull(rTrace.parent()); } Session derived = session.withRemoteRepositories(session.getRemoteRepositories()); - assertTrue(derived.getRepositoryListeners().contains(listener)); + assertTrue(derived.getListeners().contains(listener)); derived.unregisterListener(listener); - assertTrue(session.getRepositoryListeners().isEmpty()); - assertTrue(derived.getRepositoryListeners().isEmpty()); + assertTrue(session.getListeners().isEmpty()); + assertTrue(derived.getListeners().isEmpty()); assertNotNull(node); assertEquals(6, node.getChildren().size()); } diff --git a/impl/maven-testing/src/main/java/org/apache/maven/testing/plugin/stubs/SessionStub.java b/impl/maven-testing/src/main/java/org/apache/maven/testing/plugin/stubs/SessionStub.java index 4bac5a5e696b..178265d00b8e 100644 --- a/impl/maven-testing/src/main/java/org/apache/maven/testing/plugin/stubs/SessionStub.java +++ b/impl/maven-testing/src/main/java/org/apache/maven/testing/plugin/stubs/SessionStub.java @@ -43,7 +43,6 @@ import org.apache.maven.api.Project; import org.apache.maven.api.ProjectScope; import org.apache.maven.api.RemoteRepository; -import org.apache.maven.api.RepositoryListener; import org.apache.maven.api.Service; import org.apache.maven.api.Session; import org.apache.maven.api.SessionData; @@ -201,17 +200,6 @@ public void unregisterListener(Listener listener) {} @Override public Collection getListeners() { - return null; - } - - @Override - public void registerListener(RepositoryListener listener) {} - - @Override - public void unregisterListener(RepositoryListener listener) {} - - @Override - public Collection getRepositoryListeners() { return List.of(); } From ceeae2808dbd9ba5dd96032ba8b7301475f7ff98 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 7 Sep 2026 11:45:46 +0000 Subject: [PATCH 5/8] [MNG-8547] Simplify event hierarchy: drop SessionEvent and TypedListener Make Event the base interface for both ExecutionEvent and RepositoryEvent, removing the SessionEvent intermediate. Drop TypedListener by moving the dispatch logic into ExecutionListener.onEvent() and RepositoryListener.onEvent() as default methods. This makes the dispatch self-contained in the API interfaces and simplifies EventSpyImpl to a single listener.onEvent(event) call. Changes: - Event: base interface with session() + deprecated getX() compat defaults - ExecutionEvent: extends Event (unchanged noun-style accessors) - RepositoryEvent: extends Event (was SessionEvent) - ExecutionListener: extends Listener, default onEvent() dispatches to typed callbacks - RepositoryListener: extends Listener, default onEvent() dispatches to typed callbacks - SessionEvent: deleted (redundant, Event is the base) - TypedListener: deleted (each sub-interface provides its own default onEvent()) - EventSpyImpl: simplified to just call listener.onEvent(event) - Listener: unchanged (@FunctionalInterface, abstract onEvent(Event)) --- .../main/java/org/apache/maven/api/Event.java | 72 ++++++++++++++----- .../org/apache/maven/api/ExecutionEvent.java | 32 +-------- .../apache/maven/api/ExecutionListener.java | 42 ++++++++++- .../org/apache/maven/api/RepositoryEvent.java | 2 +- .../apache/maven/api/RepositoryListener.java | 43 ++++++++++- .../org/apache/maven/api/SessionEvent.java | 37 ---------- .../org/apache/maven/api/TypedListener.java | 42 ----------- .../maven/internal/impl/EventSpyImpl.java | 46 ++---------- .../maven/internal/impl/EventSpyImplTest.java | 14 ++-- 9 files changed, 158 insertions(+), 172 deletions(-) delete mode 100644 api/maven-api-core/src/main/java/org/apache/maven/api/SessionEvent.java delete mode 100644 api/maven-api-core/src/main/java/org/apache/maven/api/TypedListener.java diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/Event.java b/api/maven-api-core/src/main/java/org/apache/maven/api/Event.java index 2c2f7cde5827..25307c01ae57 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/Event.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/Event.java @@ -21,59 +21,99 @@ import java.util.Optional; import org.apache.maven.api.annotations.Experimental; +import org.apache.maven.api.annotations.Immutable; import org.apache.maven.api.annotations.Nonnull; /** - * Event sent by maven during various phases of the build process. - * Such events can be listened to using {@link Listener}s objects - * registered in the {@link Session}. + * Base interface for all Maven events. + * Specific event families extend this interface to provide typed event data. + * Events can be listened to using {@link Listener} objects registered in the {@link Session}. * + * @see ExecutionEvent + * @see RepositoryEvent + * @see Listener * @since 4.0.0 */ @Experimental -public interface Event extends SessionEvent { - - @Override - default Session session() { - return getSession(); - } +@Immutable +public interface Event { /** - * Gets the type of the event. + * Returns the session from which this event originates. * - * @return the type of the event, never {@code null} + * @return the current session, never {@code null} */ @Nonnull - EventType getType(); + Session session(); /** * Gets the session from which this event originates. * * @return the current session, never {@code null} + * @deprecated Use {@link #session()} instead. */ + @Deprecated(since = "4.1.0", forRemoval = true) @Nonnull - Session getSession(); + default Session getSession() { + return session(); + } + + /** + * Gets the type of the event. + * + * @return the type of the event, never {@code null} + * @deprecated Use {@link ExecutionEvent#type()} instead. + */ + @Deprecated(since = "4.1.0", forRemoval = true) + @Nonnull + default EventType getType() { + if (this instanceof ExecutionEvent ee) { + return EventType.valueOf(ee.type().name()); + } + throw new UnsupportedOperationException("getType() is only supported on ExecutionEvent instances"); + } /** * Gets the current project (if any). * * @return the current project or {@code empty()} if not applicable + * @deprecated Use {@link ExecutionEvent#project()} instead. */ + @Deprecated(since = "4.1.0", forRemoval = true) @Nonnull - Optional getProject(); + default Optional getProject() { + if (this instanceof ExecutionEvent ee) { + return ee.project(); + } + return Optional.empty(); + } /** * Gets the current mojo execution (if any). * * @return the current mojo execution or {@code empty()} if not applicable + * @deprecated Use {@link ExecutionEvent#mojoExecution()} instead. */ + @Deprecated(since = "4.1.0", forRemoval = true) @Nonnull - Optional getMojoExecution(); + default Optional getMojoExecution() { + if (this instanceof ExecutionEvent ee) { + return ee.mojoExecution(); + } + return Optional.empty(); + } /** * Gets the exception that caused the event (if any). * * @return the exception or {@code empty()} if none + * @deprecated Use {@link ExecutionEvent#exception()} instead. */ - Optional getException(); + @Deprecated(since = "4.1.0", forRemoval = true) + default Optional getException() { + if (this instanceof ExecutionEvent ee) { + return ee.exception(); + } + return Optional.empty(); + } } diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/ExecutionEvent.java b/api/maven-api-core/src/main/java/org/apache/maven/api/ExecutionEvent.java index 003a64ab0857..8e06b9c70955 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/ExecutionEvent.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/ExecutionEvent.java @@ -28,11 +28,14 @@ * A build execution event with noun-style accessors. * Extends {@link Event} so existing execution listeners can consume the same event. * + * @see ExecutionListener + * @see ExecutionEventType * @since 4.1.0 */ @Experimental @Immutable public interface ExecutionEvent extends Event { + /** {@return the kind of execution operation} */ @Nonnull ExecutionEventType type(); @@ -48,33 +51,4 @@ public interface ExecutionEvent extends Event { /** {@return the failure associated with this event, if any} */ @Nonnull Optional exception(); - - @Override - @Nonnull - Session session(); - - @Override - default EventType getType() { - return EventType.valueOf(type().name()); - } - - @Override - default Session getSession() { - return session(); - } - - @Override - default Optional getProject() { - return project(); - } - - @Override - default Optional getMojoExecution() { - return mojoExecution(); - } - - @Override - default Optional getException() { - return exception(); - } } diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/ExecutionListener.java b/api/maven-api-core/src/main/java/org/apache/maven/api/ExecutionListener.java index 634dc1f6dbe4..cd5797963d88 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/ExecutionListener.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/ExecutionListener.java @@ -24,13 +24,53 @@ /** * Receives build execution events through typed callbacks. + * Each method corresponds to a specific {@link ExecutionEventType} and has a no-op default implementation, + * so implementations only need to override the methods they care about. + *

+ * Register an {@code ExecutionListener} via {@link Session#registerListener(Listener)}. * Implementations must support concurrent notification during parallel builds. * + * @see ExecutionEvent + * @see ExecutionEventType * @since 4.1.0 */ @Experimental @Consumer -public interface ExecutionListener extends TypedListener { +public interface ExecutionListener extends Listener { + + /** + * Dispatches the given event to the appropriate typed callback. + * This default implementation routes {@link ExecutionEvent}s to the matching method + * and ignores other event types. + * + * @param event the event to dispatch + */ + @Override + default void onEvent(@Nonnull Event event) { + if (event instanceof ExecutionEvent ee) { + switch (ee.type()) { + case PROJECT_DISCOVERY_STARTED -> projectDiscoveryStarted(ee); + case SESSION_STARTED -> sessionStarted(ee); + case SESSION_ENDED -> sessionEnded(ee); + case PROJECT_SKIPPED -> projectSkipped(ee); + case PROJECT_STARTED -> projectStarted(ee); + case PROJECT_SUCCEEDED -> projectSucceeded(ee); + case PROJECT_FAILED -> projectFailed(ee); + case MOJO_SKIPPED -> mojoSkipped(ee); + case MOJO_STARTED -> mojoStarted(ee); + case MOJO_SUCCEEDED -> mojoSucceeded(ee); + case MOJO_FAILED -> mojoFailed(ee); + case FORK_STARTED -> forkStarted(ee); + case FORK_SUCCEEDED -> forkSucceeded(ee); + case FORK_FAILED -> forkFailed(ee); + case FORKED_PROJECT_STARTED -> forkedProjectStarted(ee); + case FORKED_PROJECT_SUCCEEDED -> forkedProjectSucceeded(ee); + case FORKED_PROJECT_FAILED -> forkedProjectFailed(ee); + default -> {} + } + } + } + /** * Called when project discovery has started. * diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryEvent.java b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryEvent.java index f0cb42543f4f..49ab3a15d00b 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryEvent.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryEvent.java @@ -34,7 +34,7 @@ */ @Experimental @Immutable -public interface RepositoryEvent extends SessionEvent { +public interface RepositoryEvent extends Event { /** * Returns the kind of repository operation represented by this event. diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryListener.java b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryListener.java index c0259a2a2398..920fb301ad8b 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryListener.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryListener.java @@ -24,14 +24,55 @@ /** * Receives repository events emitted while resolving, installing, and deploying artifacts and metadata. + * Each method corresponds to a specific {@link RepositoryEventType} and has a no-op default implementation, + * so implementations only need to override the methods they care about. + *

+ * Register a {@code RepositoryListener} via {@link Session#registerListener(Listener)}. * Implementations must be thread-safe because callbacks may occur concurrently. * Runtime exceptions thrown by a listener do not stop repository processing or notification of other listeners. * + * @see RepositoryEvent + * @see RepositoryEventType * @since 4.1.0 */ @Experimental @Consumer -public interface RepositoryListener extends TypedListener { +public interface RepositoryListener extends Listener { + + /** + * Dispatches the given event to the appropriate typed callback. + * This default implementation routes {@link RepositoryEvent}s to the matching method + * and ignores other event types. + * + * @param event the event to dispatch + */ + @Override + default void onEvent(@Nonnull Event event) { + if (event instanceof RepositoryEvent re) { + switch (re.type()) { + case ARTIFACT_DESCRIPTOR_INVALID -> artifactDescriptorInvalid(re); + case ARTIFACT_DESCRIPTOR_MISSING -> artifactDescriptorMissing(re); + case METADATA_INVALID -> metadataInvalid(re); + case ARTIFACT_RESOLVING -> artifactResolving(re); + case ARTIFACT_RESOLVED -> artifactResolved(re); + case METADATA_RESOLVING -> metadataResolving(re); + case METADATA_RESOLVED -> metadataResolved(re); + case ARTIFACT_DOWNLOADING -> artifactDownloading(re); + case ARTIFACT_DOWNLOADED -> artifactDownloaded(re); + case METADATA_DOWNLOADING -> metadataDownloading(re); + case METADATA_DOWNLOADED -> metadataDownloaded(re); + case ARTIFACT_INSTALLING -> artifactInstalling(re); + case ARTIFACT_INSTALLED -> artifactInstalled(re); + case METADATA_INSTALLING -> metadataInstalling(re); + case METADATA_INSTALLED -> metadataInstalled(re); + case ARTIFACT_DEPLOYING -> artifactDeploying(re); + case ARTIFACT_DEPLOYED -> artifactDeployed(re); + case METADATA_DEPLOYING -> metadataDeploying(re); + case METADATA_DEPLOYED -> metadataDeployed(re); + default -> {} + } + } + } /** * Called when an artifact descriptor could not be parsed. diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/SessionEvent.java b/api/maven-api-core/src/main/java/org/apache/maven/api/SessionEvent.java deleted file mode 100644 index 822f0d38eb49..000000000000 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/SessionEvent.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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; - -import org.apache.maven.api.annotations.Experimental; -import org.apache.maven.api.annotations.Nonnull; - -/** - * An event associated with a Maven session. - * Execution and repository events expose their own operation-specific details. - * - * @since 4.1.0 - */ -@Experimental -public interface SessionEvent { - /** - * {@return the session associated with this event} - */ - @Nonnull - Session session(); -} diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/TypedListener.java b/api/maven-api-core/src/main/java/org/apache/maven/api/TypedListener.java deleted file mode 100644 index 760991584e11..000000000000 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/TypedListener.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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; - -import org.apache.maven.api.annotations.Consumer; -import org.apache.maven.api.annotations.Experimental; -import org.apache.maven.api.annotations.Nonnull; - -/** - * Base for listeners notified through event-specific callbacks. - * Implement {@link ExecutionListener}, {@link RepositoryListener}, or both. - * The shared default implementation allows both interfaces to be implemented without conflicting defaults. - * - * @since 4.1.0 - */ -@Experimental -@Consumer -public interface TypedListener extends Listener { - /** - * Does nothing. Typed listeners are notified through their event-specific callbacks. - * - * @param event the legacy execution event - */ - @Override - default void onEvent(@Nonnull Event event) {} -} diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/EventSpyImpl.java b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/EventSpyImpl.java index 0fdfaa4f48f4..382a4ec100a8 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/EventSpyImpl.java +++ b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/EventSpyImpl.java @@ -24,9 +24,7 @@ import java.util.Collection; import org.apache.maven.api.EventType; -import org.apache.maven.api.ExecutionListener; import org.apache.maven.api.Listener; -import org.apache.maven.api.TypedListener; import org.apache.maven.eventspy.EventSpy; import org.apache.maven.execution.ExecutionEvent; import org.slf4j.Logger; @@ -34,6 +32,9 @@ /** * Bridges between Maven3 events and Maven4 events. + * Each listener's {@link Listener#onEvent(org.apache.maven.api.Event)} method handles its own dispatch: + * legacy listeners receive the event directly, while typed listeners (such as + * {@link org.apache.maven.api.ExecutionListener}) route it to their specific callbacks. */ @Named @Singleton @@ -51,25 +52,15 @@ public void onEvent(Object arg) throws Exception { EventType eventType = convert(ee.getType()); Collection listeners = session.getListeners(); if (!listeners.isEmpty()) { - org.apache.maven.api.ExecutionEvent event = null; + org.apache.maven.api.ExecutionEvent event = new DefaultEvent(session, ee, eventType); for (Listener listener : listeners) { - if (listener instanceof TypedListener && !(listener instanceof ExecutionListener)) { - continue; - } - if (event == null) { - event = new DefaultEvent(session, ee, eventType); - } try { - if (listener instanceof ExecutionListener executionListener) { - dispatch(executionListener, event); - } else { - listener.onEvent(event); - } + listener.onEvent(event); } catch (RuntimeException e) { LOGGER.warn( - "Failed to notify execution listener {} about {}", + "Failed to notify listener {} about {}", listener.getClass().getName(), - event.type(), + eventType, e); } } @@ -77,29 +68,6 @@ public void onEvent(Object arg) throws Exception { } } - private static void dispatch(ExecutionListener listener, org.apache.maven.api.ExecutionEvent event) { - switch (event.type()) { - case PROJECT_DISCOVERY_STARTED -> listener.projectDiscoveryStarted(event); - case SESSION_STARTED -> listener.sessionStarted(event); - case SESSION_ENDED -> listener.sessionEnded(event); - case PROJECT_SKIPPED -> listener.projectSkipped(event); - case PROJECT_STARTED -> listener.projectStarted(event); - case PROJECT_SUCCEEDED -> listener.projectSucceeded(event); - case PROJECT_FAILED -> listener.projectFailed(event); - case MOJO_SKIPPED -> listener.mojoSkipped(event); - case MOJO_STARTED -> listener.mojoStarted(event); - case MOJO_SUCCEEDED -> listener.mojoSucceeded(event); - case MOJO_FAILED -> listener.mojoFailed(event); - case FORK_STARTED -> listener.forkStarted(event); - case FORK_SUCCEEDED -> listener.forkSucceeded(event); - case FORK_FAILED -> listener.forkFailed(event); - case FORKED_PROJECT_STARTED -> listener.forkedProjectStarted(event); - case FORKED_PROJECT_SUCCEEDED -> listener.forkedProjectSucceeded(event); - case FORKED_PROJECT_FAILED -> listener.forkedProjectFailed(event); - default -> throw new IllegalArgumentException("Unsupported execution event: " + event.type()); - } - } - /** * Converts the Maven 3 execution event type to its Maven API counterpart. */ diff --git a/impl/maven-core/src/test/java/org/apache/maven/internal/impl/EventSpyImplTest.java b/impl/maven-core/src/test/java/org/apache/maven/internal/impl/EventSpyImplTest.java index 6b8acd431dd6..0ba976c85184 100644 --- a/impl/maven-core/src/test/java/org/apache/maven/internal/impl/EventSpyImplTest.java +++ b/impl/maven-core/src/test/java/org/apache/maven/internal/impl/EventSpyImplTest.java @@ -86,11 +86,6 @@ void sharesOneRegistrationForBothEventFamiliesAcrossDerivedSessions() throws Exc public void artifactResolving(RepositoryEvent event) { repositoryOnlyCalls.incrementAndGet(); } - - @Override - public void onEvent(Event event) { - throw new AssertionError("Repository-only listener received an execution event"); - } }; context.session.registerListener(combined); context.session.registerListener(legacy); @@ -147,6 +142,12 @@ class CombinedListener implements ExecutionListener, RepositoryListener { int executionCalls; int repositoryCalls; + @Override + public void onEvent(Event event) { + ExecutionListener.super.onEvent(event); + RepositoryListener.super.onEvent(event); + } + @Override public void sessionStarted(ExecutionEvent event) { executionCalls++; @@ -222,7 +223,8 @@ private static class RecordingListener implements ExecutionListener, RepositoryL @Override public void onEvent(Event event) { - throw new AssertionError("Typed listener received a duplicate legacy callback"); + ExecutionListener.super.onEvent(event); + RepositoryListener.super.onEvent(event); } @Override From 740c915e0433fefa0db7fbdf51a729e6c92a41e5 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 7 Sep 2026 18:41:34 +0000 Subject: [PATCH 6/8] [MNG-8547] Apply review suggestions: fix getType() Javadoc and remove redundant session() in RepositoryEvent --- .../src/main/java/org/apache/maven/api/Event.java | 1 + .../main/java/org/apache/maven/api/RepositoryEvent.java | 7 ------- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/Event.java b/api/maven-api-core/src/main/java/org/apache/maven/api/Event.java index 25307c01ae57..1cead84c232b 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/Event.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/Event.java @@ -62,6 +62,7 @@ default Session getSession() { * Gets the type of the event. * * @return the type of the event, never {@code null} + * @throws UnsupportedOperationException if this event is not an {@link ExecutionEvent} * @deprecated Use {@link ExecutionEvent#type()} instead. */ @Deprecated(since = "4.1.0", forRemoval = true) diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryEvent.java b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryEvent.java index 49ab3a15d00b..bf3dc5037098 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryEvent.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryEvent.java @@ -42,13 +42,6 @@ public interface RepositoryEvent extends Event { @Nonnull RepositoryEventType type(); - /** - * Returns the Maven session associated with the underlying repository system session. - * Sessions derived from it share the same repository event and listener scope. - */ - @Nonnull - Session session(); - /** * Returns the artifact involved in the event, if any. */ From b963221926371fd949897cd4eef77306549c587c Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 7 Sep 2026 19:28:00 +0000 Subject: [PATCH 7/8] [MNG-8547] Use noun-style accessors on @Immutable RepositoryMetadata --- .../org/apache/maven/api/RepositoryMetadata.java | 14 +++++++------- .../maven/impl/DefaultRepositoryEvent.java | 14 +++++++------- .../maven/impl/MavenRepositoryListenerTest.java | 16 ++++++++-------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryMetadata.java b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryMetadata.java index 7081ff8e1e4c..d5be0ed74571 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryMetadata.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryMetadata.java @@ -53,41 +53,41 @@ enum Nature { * {@return the group identifier, or an empty string if the metadata applies to the entire repository} */ @Nonnull - String getGroupId(); + String groupId(); /** * {@return the artifact identifier, or an empty string if the metadata applies at group level} */ @Nonnull - String getArtifactId(); + String artifactId(); /** * {@return the version, or an empty string if the metadata applies at artifact level} */ @Nonnull - String getVersion(); + String version(); /** * {@return the metadata filename, such as {@code maven-metadata.xml}} */ @Nonnull - String getType(); + String type(); /** * {@return the artifact version nature to which the metadata applies} */ @Nonnull - Nature getNature(); + Nature nature(); /** * {@return the local path of the metadata file if it has been resolved} */ @Nonnull - Optional getPath(); + Optional path(); /** * {@return the read-only properties associated with the metadata} */ @Nonnull - Map getProperties(); + Map properties(); } diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultRepositoryEvent.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultRepositoryEvent.java index fbf836f0d808..325906a852de 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultRepositoryEvent.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultRepositoryEvent.java @@ -124,37 +124,37 @@ private DefaultRepositoryMetadata(org.eclipse.aether.metadata.Metadata metadata) } @Override - public String getGroupId() { + public String groupId() { return groupId; } @Override - public String getArtifactId() { + public String artifactId() { return artifactId; } @Override - public String getVersion() { + public String version() { return version; } @Override - public String getType() { + public String type() { return type; } @Override - public Nature getNature() { + public Nature nature() { return nature; } @Override - public Optional getPath() { + public Optional path() { return Optional.ofNullable(path); } @Override - public Map getProperties() { + public Map properties() { return properties; } } diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/MavenRepositoryListenerTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/MavenRepositoryListenerTest.java index 1ff61c2c7292..1dd69aed188b 100644 --- a/impl/maven-impl/src/test/java/org/apache/maven/impl/MavenRepositoryListenerTest.java +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/MavenRepositoryListenerTest.java @@ -146,16 +146,16 @@ void convertsMetadataWithoutExposingResolverMetadata() { .build()); RepositoryMetadata converted = listener.events.get(0).metadata().orElseThrow(); - assertEquals("org.example", converted.getGroupId()); - assertEquals("demo", converted.getArtifactId()); - assertEquals("1.0-SNAPSHOT", converted.getVersion()); - assertEquals("maven-metadata.xml", converted.getType()); - assertEquals(RepositoryMetadata.Nature.SNAPSHOT, converted.getNature()); - assertEquals(path, converted.getPath().orElseThrow()); - assertEquals(Map.of("source", "test"), converted.getProperties()); + assertEquals("org.example", converted.groupId()); + assertEquals("demo", converted.artifactId()); + assertEquals("1.0-SNAPSHOT", converted.version()); + assertEquals("maven-metadata.xml", converted.type()); + assertEquals(RepositoryMetadata.Nature.SNAPSHOT, converted.nature()); + assertEquals(path, converted.path().orElseThrow()); + assertEquals(Map.of("source", "test"), converted.properties()); assertThrows( UnsupportedOperationException.class, - () -> converted.getProperties().put("key", "value")); + () -> converted.properties().put("key", "value")); } @Test From f77c6dbeb9bad13d4a19abc7c30574a1e7652fb7 Mon Sep 17 00:00:00 2001 From: Goutam Adwant Date: Wed, 9 Sep 2026 23:02:40 -0700 Subject: [PATCH 8/8] [MNG-8547] Document combined listener dispatch Explain the onEvent override required when a listener implements both ExecutionListener and RepositoryListener, with an example delegating to both default methods. --- .../main/java/org/apache/maven/api/Listener.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/Listener.java b/api/maven-api-core/src/main/java/org/apache/maven/api/Listener.java index c04d013f7457..ab11b458d590 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/Listener.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/Listener.java @@ -26,6 +26,19 @@ * A listener for session events. * Existing implementations and lambdas receive execution events through {@link #onEvent(Event)}. * Implement {@link ExecutionListener} or {@link RepositoryListener} for typed callbacks. + *

+ * A listener implementing both interfaces must override {@link #onEvent(Event)} to resolve + * their conflicting default methods. Delegate to both defaults to receive typed callbacks + * for both event families; each default ignores events from the other family: + *

{@code
+ * class CombinedListener implements ExecutionListener, RepositoryListener {
+ *     @Override
+ *     public void onEvent(Event event) {
+ *         ExecutionListener.super.onEvent(event);
+ *         RepositoryListener.super.onEvent(event);
+ *     }
+ * }
+ * }
* * @since 4.0.0 */