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..22f725e67507 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,54 +21,100 @@ 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 +@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 + default Session getSession() { + return session(); + } + + /** + * Gets the type of the event. + * + * @return the execution event type, never {@code null}; only meaningful when this event is an {@link ExecutionEvent} + * @throws UnsupportedOperationException if this event is not an {@link ExecutionEvent} + * @deprecated Use {@link ExecutionEvent#type()} instead. */ + @Deprecated(since = "4.1.0", forRemoval = true) @Nonnull - Session getSession(); + 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 new file mode 100644 index 000000000000..8e06b9c70955 --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/ExecutionEvent.java @@ -0,0 +1,54 @@ +/* + * 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. + * + * @see ExecutionListener + * @see ExecutionEventType + * @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(); +} 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..cd5797963d88 --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/ExecutionListener.java @@ -0,0 +1,192 @@ +/* + * 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. + * 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 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. + * + * @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..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 @@ -24,7 +24,21 @@ /** * 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. + *

+ * 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 */ @@ -32,5 +46,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 new file mode 100644 index 000000000000..271f9bf5b450 --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryEvent.java @@ -0,0 +1,70 @@ +/* + * 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 extends Event { + + /** {@return the kind of repository operation represented by this event} */ + @Nonnull + RepositoryEventType type(); + + /** {@return the artifact involved in the event, if any} */ + @Nonnull + Optional artifact(); + + /** {@return the metadata involved in the event, if any} */ + @Nonnull + Optional metadata(); + + /** {@return the local path involved in the event, if any} */ + @Nonnull + Optional path(); + + /** {@return the repository involved in the event, if any} */ + @Nonnull + Optional repository(); + + /** {@return the primary failure associated with the event, if any} */ + @Nonnull + Optional exception(); + + /** {@return all failures associated with the event} */ + @Nonnull + List exceptions(); + + /** {@return the request trace associated with the event, if any} */ + @Nonnull + Optional trace(); +} 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..920fb301ad8b --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryListener.java @@ -0,0 +1,209 @@ +/* + * 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. + * 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 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. + * + * @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 new file mode 100644 index 000000000000..d5be0ed74571 --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/RepositoryMetadata.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.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 { + + /** + * 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 groupId(); + + /** + * {@return the artifact identifier, or an empty string if the metadata applies at group level} + */ + @Nonnull + String artifactId(); + + /** + * {@return the version, or an empty string if the metadata applies at artifact level} + */ + @Nonnull + String version(); + + /** + * {@return the metadata filename, such as {@code maven-metadata.xml}} + */ + @Nonnull + String type(); + + /** + * {@return the artifact version nature to which the metadata applies} + */ + @Nonnull + Nature nature(); + + /** + * {@return the local path of the metadata file if it has been resolved} + */ + @Nonnull + Optional path(); + + /** + * {@return the read-only properties associated with the metadata} + */ + @Nonnull + Map properties(); +} 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 38e608021cba..0138f461b1aa 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 @@ -192,7 +192,9 @@ default Map getEffectiveProperties() { Session withContext(@Nonnull RequestTrace trace); /** - * 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 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 b3bd5d95a59e..45ca17114bd3 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 @@ -40,6 +40,7 @@ import org.apache.maven.artifact.repository.ArtifactRepository; 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; @@ -380,7 +381,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/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..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 @@ -23,18 +23,24 @@ import java.util.Collection; -import org.apache.maven.api.Event; import org.apache.maven.api.EventType; import org.apache.maven.api.Listener; 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. + * 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 public class EventSpyImpl implements EventSpy { + private static final Logger LOGGER = LoggerFactory.getLogger(EventSpyImpl.class); + @Override public void init(Context context) throws Exception {} @@ -46,16 +52,24 @@ 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 = new DefaultEvent(session, ee, eventType); for (Listener listener : listeners) { - listener.onEvent(event); + try { + listener.onEvent(event); + } catch (RuntimeException e) { + LOGGER.warn( + "Failed to notify listener {} about {}", + listener.getClass().getName(), + eventType, + e); + } } } } } /** - * 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 111fb3548b56..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 @@ -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.getListeners()).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().type()); + } + @Test void isNoSnapshotUpdatesTest() throws InvalidRepositoryException { DefaultRepositorySystemSessionFactory systemSessionFactory = new DefaultRepositorySystemSessionFactory( 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..0ba976c85184 --- /dev/null +++ b/impl/maven-core/src/test/java/org/apache/maven/internal/impl/EventSpyImplTest.java @@ -0,0 +1,337 @@ +/* + * 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(); + } + }; + 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 onEvent(Event event) { + ExecutionListener.super.onEvent(event); + RepositoryListener.super.onEvent(event); + } + + @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) { + ExecutionListener.super.onEvent(event); + RepositoryListener.super.onEvent(event); + } + + @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 512174a46590..bdce42779a5b 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 @@ -122,7 +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 listeners; private final Cache allNodes = Cache.newCache(Cache.ReferenceType.WEAK, "AbstractSession-Nodes"); private final Map, Cache> allArtifacts = @@ -159,9 +159,16 @@ protected AbstractSession( this.repositories = getRepositories(repositories, resolverRepositories); this.lookup = lookup; this.injector = lookup != null ? lookup.lookupOptional(Injector.class).orElse(null) : null; + this.listeners = getListeners(session); this.context = context; } + @SuppressWarnings("unchecked") + 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") private static Stream> collectServiceInterfaces(Class clazz) { if (clazz == null) { 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..325906a852de --- /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 type() { + return type; + } + + @Override + public Session session() { + return session; + } + + @Override + public Optional artifact() { + return Optional.ofNullable(artifact); + } + + @Override + public Optional metadata() { + return Optional.ofNullable(metadata); + } + + @Override + public Optional path() { + return Optional.ofNullable(path); + } + + @Override + public Optional repository() { + return Optional.ofNullable(repository); + } + + @Override + public Optional exception() { + return Optional.ofNullable(exception); + } + + @Override + public List exceptions() { + return exceptions; + } + + @Override + public Optional trace() { + 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 groupId() { + return groupId; + } + + @Override + public String artifactId() { + return artifactId; + } + + @Override + public String version() { + return version; + } + + @Override + public String type() { + return type; + } + + @Override + public Nature nature() { + return nature; + } + + @Override + public Optional path() { + return Optional.ofNullable(path); + } + + @Override + public Map properties() { + 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..c68c80eb4142 --- /dev/null +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/MavenRepositoryListener.java @@ -0,0 +1,160 @@ +/* + * 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.Listener; +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.getListeners(); + if (!listeners.isEmpty()) { + 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) { + 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 fa3faa2e6656..98ecb75b9c74 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 @@ -78,6 +78,7 @@ import org.apache.maven.di.impl.InjectorImpl; 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.model.DefaultInterpolator; import org.apache.maven.impl.resolver.MavenSessionBuilderSupplier; @@ -555,6 +556,7 @@ static Session newSession(RepositorySystem system, Lookup lookup, @Nullable Loca // Configure the resolver session with dependency resolution machinery MavenSessionBuilderSupplier sessionBuilderSupplier = new MavenSessionBuilderSupplier(system, false); DefaultRepositorySystemSession rsession = new DefaultRepositorySystemSession(h -> false); + rsession.setRepositoryListener(new MavenRepositoryListener()); rsession.setScopeManager(new ScopeManagerImpl(Maven4ScopeManagerConfiguration.INSTANCE)); rsession.setDependencyTraverser(sessionBuilderSupplier.getDependencyTraverser()); rsession.setDependencyManager(sessionBuilderSupplier.getDependencyManager(true)); 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..1dd69aed188b --- /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 unifiedRegistrationPreservesBuildListenerLambdas() { + 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.getListeners()).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.getListeners()).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.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.exceptions().add(new Exception())); + } + + @Test + void convertsMetadataWithoutExposingResolverMetadata() { + TestContext context = new TestContext(); + RecordingListener listener = new RecordingListener(); + when(context.session.getListeners()).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).metadata().orElseThrow(); + 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.properties().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.getListeners()).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.getListeners()).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.type()); + 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..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 @@ -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.trace().orElseThrow(); assertNotNull(rTrace.parent()); } + Session derived = session.withRemoteRepositories(session.getRemoteRepositories()); + assertTrue(derived.getListeners().contains(listener)); + derived.unregisterListener(listener); + 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 41f9394c868b..6299fa8abe95 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 @@ -206,7 +206,7 @@ public void unregisterListener(Listener listener) {} @Override public Collection getListeners() { - return null; + return List.of(); } @Override