diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 4000f8c..4c09c8a 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -7,6 +7,6 @@ jobs: update_release_draft: runs-on: ubuntu-latest steps: - - uses: release-drafter/release-drafter@v5 + - uses: release-drafter/release-drafter@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 6af0dde..1789d9f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ Plexus Build API -======================= +================ [![Apache License, Version 2.0, January 2004](https://img.shields.io/github/license/codehaus-plexus/plexus-classworlds.svg?label=License)](http://www.apache.org/licenses/) [![Maven Central](https://img.shields.io/maven-central/v/org.codehaus.plexus/plexus-build-api.svg?label=Maven%20Central)](https://search.maven.org/artifact/org.codehaus.plexus/plexus-build-api) @@ -13,9 +13,8 @@ It supports - fine-grained error/info markers (referring to specific files in particular line numbers) - notifications about updated files - Current Implementations ------ +----------------------- ### Default Implementation @@ -27,6 +26,13 @@ The default implementation shipping with this artifact is supposed to impose min Currently only versions up to 0.0.7 (with old Maven coordinates `org.sonatype.plexus:plexus-build-api`) are supported, this limitation is tracked in [Issue 944](https://github.com/eclipse-m2e/m2e-core/issues/944). History ------ +------- The project was relocated from . Also its Maven coordinates changed from `org.sonatype.plexus:plexus-build-api` to `org.codehaus.plexus:plexus-build-api`, the API is still the same, though. + +## Provided APIs + +### IDE connection to maven process + +This API is usually not used by mojos but for IDE integration, if enabled as a maven-core extension plexus-build-api supply a way to communicate with the running maven build and get events. +The default implementation open a tcp connections to a port specified by the system property `plexus.build.ipc.port` using key/value encoded message format. If no such value is given all messages are silently discarded. diff --git a/pom.xml b/pom.xml index 34360b9..d6fad88 100644 --- a/pom.xml +++ b/pom.xml @@ -17,16 +17,16 @@ See the Apache License Version 2.0 for the specific language governing permissio org.codehaus.plexus plexus - 13 + 22 plexus-build-api - 1.2.0 + 1.2.1-SNAPSHOT scm:git:https://github.com/codehaus-plexus/plexus-build-api.git ${project.scm.connection} - plexus-build-api-1.2.0 + master https://github.com/codehaus-plexus/plexus-build-api/tree/master @@ -34,7 +34,7 @@ See the Apache License Version 2.0 for the specific language governing permissio org.codehaus.plexus plexus-utils - 4.0.0 + 4.0.2 javax.inject @@ -46,7 +46,7 @@ See the Apache License Version 2.0 for the specific language governing permissio slf4j-api 1.7.36 - + org.sonatype.plexus plexus-build-api @@ -61,7 +61,7 @@ See the Apache License Version 2.0 for the specific language governing permissio org.eclipse.sisu org.eclipse.sisu.plexus - 0.9.0.M2 + 0.9.0.M4 * @@ -69,6 +69,12 @@ See the Apache License Version 2.0 for the specific language governing permissio + + org.apache.maven + maven-core + 3.9.10 + provided + @@ -83,29 +89,10 @@ See the Apache License Version 2.0 for the specific language governing permissio org.eclipse.sisu sisu-maven-plugin - 0.9.0.M2 - - - index-project - - main-index - test-index - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 1.8 - 1.8 - org.apache.maven.plugins maven-surefire-plugin - 3.1.0 true diff --git a/src/main/java/org/codehaus/plexus/build/DefaultBuildContext.java b/src/main/java/org/codehaus/plexus/build/DefaultBuildContext.java index 7fb0da6..2b34e71 100644 --- a/src/main/java/org/codehaus/plexus/build/DefaultBuildContext.java +++ b/src/main/java/org/codehaus/plexus/build/DefaultBuildContext.java @@ -24,6 +24,9 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.apache.maven.plugin.LegacySupport; +import org.codehaus.plexus.build.connect.BuildConnection; +import org.codehaus.plexus.build.connect.messages.RefreshMessage; import org.codehaus.plexus.logging.AbstractLogEnabled; import org.codehaus.plexus.util.Scanner; import org.codehaus.plexus.util.io.CachingOutputStream; @@ -56,15 +59,24 @@ public class DefaultBuildContext implements BuildContext { private final Map contextMap = new ConcurrentHashMap<>(); private org.sonatype.plexus.build.incremental.BuildContext legacy; + private BuildConnection connection; + private LegacySupport legacySupport; /** - * @param legacy the legacy API we delegate to by default, this allow us to - * support "older" plugins and implementors of the API while still - * having a way to move forward! + * @param legacy the legacy API we delegate to by default, this allow us + * to support "older" plugins and implementors of the API + * while still having a way to move forward! + * @param connection the connection we use to forward refresh events + * @param legacySupport legacy support to get the current session */ @Inject - public DefaultBuildContext(org.sonatype.plexus.build.incremental.BuildContext legacy) { + public DefaultBuildContext( + org.sonatype.plexus.build.incremental.BuildContext legacy, + BuildConnection connection, + LegacySupport legacySupport) { this.legacy = legacy; + this.connection = connection; + this.legacySupport = legacySupport; } /** {@inheritDoc} */ @@ -117,6 +129,7 @@ public Scanner newScanner(File basedir) { /** {@inheritDoc} */ public void refresh(File file) { legacy.refresh(file); + connection.send(new RefreshMessage(file.toPath()), legacySupport.getSession()); } /** {@inheritDoc} */ diff --git a/src/main/java/org/codehaus/plexus/build/connect/BuildConnection.java b/src/main/java/org/codehaus/plexus/build/connect/BuildConnection.java new file mode 100644 index 0000000..921115e --- /dev/null +++ b/src/main/java/org/codehaus/plexus/build/connect/BuildConnection.java @@ -0,0 +1,45 @@ +/* +Copyright (c) 2025 Christoph Läubrich All rights reserved. + +This program is licensed to you under the Apache License Version 2.0, +and you may not use this file except in compliance with the Apache License Version 2.0. +You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + +Unless required by applicable law or agreed to in writing, +software distributed under the Apache License Version 2.0 is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. +*/ +package org.codehaus.plexus.build.connect; + +import org.apache.maven.execution.MavenSession; +import org.codehaus.plexus.build.connect.messages.Message; + +/** + * A {@link BuildConnection} allow communication between a an IDE and a maven + * build to observe the state of the build and act on certain events. This is + * usually not used directly by mojos but invoked internally by other APIs. + */ +public interface BuildConnection { + + /** + * Send a message and returns the reply from the other endpoint, should only be + * called from a maven thread! + * + * @param message the message to send + * @param mavenSession the maven session to reference + * @return the reply message or null if this connection is not + * enabled and the message was discarded. + */ + Message send(Message message, MavenSession mavenSession); + + /** + * This method allows code to perform an eager check if a buildconnection is + * present to send messages. This can be used to guard operations to prevent + * allocate resources or objects if the message will be dropped. + * + * @return true if the connection can be used to send messages or + * if they will be discarded + */ + boolean isEnabled(); +} diff --git a/src/main/java/org/codehaus/plexus/build/connect/Configuration.java b/src/main/java/org/codehaus/plexus/build/connect/Configuration.java new file mode 100644 index 0000000..2444145 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/build/connect/Configuration.java @@ -0,0 +1,48 @@ +/* +Copyright (c) 2025 Christoph Läubrich All rights reserved. + +This program is licensed to you under the Apache License Version 2.0, +and you may not use this file except in compliance with the Apache License Version 2.0. +You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + +Unless required by applicable law or agreed to in writing, +software distributed under the Apache License Version 2.0 is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. +*/ +package org.codehaus.plexus.build.connect; + +import org.codehaus.plexus.build.connect.messages.Message; + +/** + * Provides access to the configuration provided by the server + */ +public interface Configuration { + + /** + * If this property is set to true in reply to a InitMessage + */ + public static final String CONFIG_SEND_PROJECTS = "sendProjectInfos"; + + /** + * @return true if {@link #CONFIG_SEND_PROJECTS} is + * provided + */ + public boolean isSendProjects(); + + /** + * Creates a Configuration from a message + * + * @param message + * @return the configuration backed by the message payload + */ + public static Configuration of(Message message) { + return new Configuration() { + + @Override + public boolean isSendProjects() { + return message.getBooleanProperty(CONFIG_SEND_PROJECTS, false); + } + }; + } +} diff --git a/src/main/java/org/codehaus/plexus/build/connect/EventListener.java b/src/main/java/org/codehaus/plexus/build/connect/EventListener.java new file mode 100644 index 0000000..1eb5e8d --- /dev/null +++ b/src/main/java/org/codehaus/plexus/build/connect/EventListener.java @@ -0,0 +1,100 @@ +/* +Copyright (c) 2025 Christoph Läubrich All rights reserved. + +This program is licensed to you under the Apache License Version 2.0, +and you may not use this file except in compliance with the Apache License Version 2.0. +You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + +Unless required by applicable law or agreed to in writing, +software distributed under the Apache License Version 2.0 is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. +*/ +package org.codehaus.plexus.build.connect; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import org.apache.maven.eventspy.EventSpy; +import org.apache.maven.execution.ExecutionEvent; +import org.apache.maven.execution.ExecutionEvent.Type; +import org.apache.maven.execution.MavenSession; +import org.codehaus.plexus.build.connect.messages.InitMessage; +import org.codehaus.plexus.build.connect.messages.Message; +import org.codehaus.plexus.build.connect.messages.MojoMessage; +import org.codehaus.plexus.build.connect.messages.ProjectMessage; +import org.codehaus.plexus.build.connect.messages.ProjectsMessage; +import org.codehaus.plexus.build.connect.messages.SessionMessage; + +/** + * Listen to all maven events and forward them to the endpoint + */ +@Named +@Singleton +public class EventListener implements EventSpy { + + private BuildConnection connection; + private Configuration configuration; + + /** + * Creates endpoint for the given connection + * + * @param connection injected + */ + @Inject + public EventListener(BuildConnection connection) { + this.connection = connection; + } + + @Override + public void init(Context context) throws Exception { + Message message = connection.send(new InitMessage(context), null); + if (message != null) { + configuration = Configuration.of(message); + } + } + + @Override + public void onEvent(Object event) throws Exception { + if (configuration == null) { + return; + } + if (event instanceof ExecutionEvent) { + handleExecutionEvent((ExecutionEvent) event); + } + } + + private void handleExecutionEvent(ExecutionEvent event) { + MavenSession session = event.getSession(); + Type type = event.getType(); + switch (type) { + case SessionStarted: + connection.send(new SessionMessage(session, true), session); + if (configuration.isSendProjects()) { + connection.send(new ProjectsMessage(session.getProjects()), session); + } + break; + case SessionEnded: + connection.send(new SessionMessage(session, false), session); + break; + case ProjectStarted: + case ProjectFailed: + case ProjectSkipped: + case ProjectSucceeded: + connection.send(new ProjectMessage(event.getProject(), type), session); + break; + case MojoStarted: + case MojoFailed: + case MojoSkipped: + case MojoSucceeded: + connection.send(new MojoMessage(event.getMojoExecution(), type), session); + break; + default: + break; + } + } + + @Override + public void close() throws Exception {} +} diff --git a/src/main/java/org/codehaus/plexus/build/connect/TcpBuildConnection.java b/src/main/java/org/codehaus/plexus/build/connect/TcpBuildConnection.java new file mode 100644 index 0000000..339487e --- /dev/null +++ b/src/main/java/org/codehaus/plexus/build/connect/TcpBuildConnection.java @@ -0,0 +1,253 @@ +/* +Copyright (c) 2025 Christoph Läubrich All rights reserved. + +This program is licensed to you under the Apache License Version 2.0, +and you may not use this file except in compliance with the Apache License Version 2.0. +You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + +Unless required by applicable law or agreed to in writing, +software distributed under the Apache License Version 2.0 is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. +*/ +package org.codehaus.plexus.build.connect; + +import javax.inject.Named; +import javax.inject.Singleton; + +import java.io.Closeable; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.WeakHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import org.apache.maven.execution.MavenSession; +import org.codehaus.plexus.build.connect.messages.Message; + +/** + * Default implementation using the system property + * plexus.build.ipc.port to communicate with an endpoint to + * exchange messages + */ +@Named("default") +@Singleton +public class TcpBuildConnection implements BuildConnection { + private static final String PLEXUS_BUILD_IPC_PORT = "plexus.build.ipc.port"; + + private static final int PORT = Integer.getInteger(PLEXUS_BUILD_IPC_PORT, 0); + + private final Map sessionMap = new WeakHashMap<>(); + + private final ThreadLocal connections = + ThreadLocal.withInitial(() -> new TcpClientConnection()); + + @Override + public boolean isEnabled() { + return PORT > 0; + } + + @Override + public Message send(Message message, MavenSession mavenSession) { + if (isEnabled()) { + String sessionId = getId(mavenSession); + byte[] messageBytes = message.serialize(sessionId); + byte[] replyBytes = connections.get().send(messageBytes); + if (replyBytes.length > 0) { + return Message.decode(replyBytes); + } + } + return null; + } + + private synchronized String getId(MavenSession session) { + if (session == null) { + return Thread.currentThread().getName(); + } + return sessionMap.computeIfAbsent(session, x -> UUID.randomUUID().toString()); + } + + /** + * Creates a new server that will receive messages from a remote endpoint and + * inform the consumer + * + * @param consumer the consumer of messages, might be called by different + * threads, if the consumer throws an exception while handling a + * message it will maybe no longer receive some messages. The + * returned map is used as a payload for the reply to the + * server, if null is returned a simple + * acknowledgement without any payload will be send to the + * endpoint. If the consumer performs blocking operations the + * further execution of the maven process might be halted + * depending on the message type, if that is not desired work + * should be offloaded by the consumer to a different thread. + * @return a {@link ServerConnection} that can be used to shutdown the server + * and get properties that needs to be passed to the maven process + * @throws IOException if no local socket can be opened + */ + public static ServerConnection createServer(Function> consumer) throws IOException { + return new ServerConnection(new ServerSocket(0), consumer); + } + + /** + * Represents a server connection that must be created to communicate with the + * maven process using the {@link TcpBuildConnection} + */ + public static final class ServerConnection implements AutoCloseable { + + private ServerSocket socket; + private ExecutorService executor = Executors.newCachedThreadPool(); + private List connections = new ArrayList<>(); + + ServerConnection(ServerSocket socket, Function> consumer) { + this.socket = socket; + executor.execute(() -> { + while (!Thread.currentThread().isInterrupted()) { + try { + TcpServerConnection connection = new TcpServerConnection(socket.accept(), consumer); + connections.add(connection); + executor.execute(connection); + } catch (IOException e) { + return; + } + } + }); + } + + @Override + public void close() { + executor.shutdownNow(); + for (TcpServerConnection connection : connections) { + connection.close(); + } + try { + socket.close(); + } catch (IOException e) { + } + } + + /** + * Given a consumer publishes required properties for a process to launch + * + * @param consumer the consumer for system properties + */ + public void setupProcess(BiConsumer consumer) { + // currently only one but might become more later (e.g. timeout, reconnects, + // ...) + consumer.accept(PLEXUS_BUILD_IPC_PORT, Integer.toString(socket.getLocalPort())); + } + } + + private static final class TcpServerConnection implements Runnable, Closeable { + + private Socket socket; + private Function> consumer; + private DataInputStream in; + private DataOutputStream out; + private AtomicBoolean closed = new AtomicBoolean(); + + public TcpServerConnection(Socket socket, Function> consumer) throws IOException { + this.socket = socket; + this.consumer = consumer; + in = new DataInputStream(socket.getInputStream()); + out = new DataOutputStream(socket.getOutputStream()); + } + + @Override + public void run() { + try { + while (!closed.get() && !Thread.currentThread().isInterrupted()) { + try { + int length = in.readInt(); + if (length == 0) { + return; + } + byte[] bytes = new byte[length]; + in.readFully(bytes); + Message message = Message.decode(bytes); + Map payload = consumer.apply(message); + Message reply = Message.replyTo(message, payload); + byte[] responseBytes = reply.serialize(); + synchronized (out) { + out.writeInt(responseBytes.length); + out.write(responseBytes); + out.flush(); + } + } catch (Exception e) { + return; + } + } + } finally { + close(); + } + } + + @Override + public void close() { + if (closed.compareAndSet(false, true)) { + try { + synchronized (out) { + out.writeInt(0); + out.flush(); + } + } catch (IOException e) { + } + try { + socket.close(); + } catch (IOException e) { + } + } + } + } + + private static final class TcpClientConnection { + + private Socket socket; + private boolean closed; + private DataInputStream in; + private DataOutputStream out; + + public byte[] send(byte[] messageBytes) { + if (!closed) { + try { + if (socket == null) { + socket = new Socket("localhost", PORT); + in = new DataInputStream(socket.getInputStream()); + out = new DataOutputStream(socket.getOutputStream()); + } + out.writeInt(messageBytes.length); + out.write(messageBytes); + out.flush(); + int length = in.readInt(); + if (length == 0) { + socket.close(); + closed = true; + } else { + byte[] bytes = new byte[length]; + in.readFully(bytes); + return bytes; + } + } catch (IOException e) { + closed = true; + if (socket != null) { + try { + socket.close(); + } catch (IOException e1) { + } + } + } + } + return new byte[0]; + } + } +} diff --git a/src/main/java/org/codehaus/plexus/build/connect/messages/InitMessage.java b/src/main/java/org/codehaus/plexus/build/connect/messages/InitMessage.java new file mode 100644 index 0000000..e8cabcb --- /dev/null +++ b/src/main/java/org/codehaus/plexus/build/connect/messages/InitMessage.java @@ -0,0 +1,54 @@ +/* +Copyright (c) 2025 Christoph Läubrich All rights reserved. + +This program is licensed to you under the Apache License Version 2.0, +and you may not use this file except in compliance with the Apache License Version 2.0. +You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + +Unless required by applicable law or agreed to in writing, +software distributed under the Apache License Version 2.0 is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. +*/ +package org.codehaus.plexus.build.connect.messages; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Properties; + +import org.apache.maven.eventspy.EventSpy.Context; + +/** + * Message send to init the inital communication with the endpoints + */ +public class InitMessage extends Message { + + /** + * Creates a message with inital information about the running maven system + * + * @param context the context settings to send to the endpoint + */ + public InitMessage(Context context) { + super(toMap(context)); + } + + private static Map toMap(Context context) { + Map data = new LinkedHashMap<>(); + context.getData().forEach((k, v) -> { + if (v instanceof String) { + data.put(k, (String) v); + } + if (v instanceof Properties) { + Properties properties = (Properties) v; + for (String p : properties.stringPropertyNames()) { + data.put(k + "." + p, properties.getProperty(p)); + } + } + }); + return data; + } + + InitMessage(String sessionId, long threadId, Map payload) { + super(sessionId, threadId, payload); + } +} diff --git a/src/main/java/org/codehaus/plexus/build/connect/messages/Message.java b/src/main/java/org/codehaus/plexus/build/connect/messages/Message.java new file mode 100644 index 0000000..ddc1430 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/build/connect/messages/Message.java @@ -0,0 +1,265 @@ +/* +Copyright (c) 2025 Christoph Läubrich All rights reserved. + +This program is licensed to you under the Apache License Version 2.0, +and you may not use this file except in compliance with the Apache License Version 2.0. +You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + +Unless required by applicable law or agreed to in writing, +software distributed under the Apache License Version 2.0 is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. +*/ +package org.codehaus.plexus.build.connect.messages; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Stream; + +/** + * A message exchanged between two endpoints, usually an IDE and a maven build + */ +public class Message { + private static final ThreadLocal ID = new ThreadLocal() { + private final AtomicLong generator = new AtomicLong(); + + @Override + protected Long initialValue() { + return generator.getAndIncrement(); + } + }; + private final long threadId; + private final Map properties; + private final String sessionId; + + Message(Map payload) { + this(null, ID.get(), payload); + } + + Message(String sessionId, long threadId, Map payload) { + this.sessionId = sessionId; + this.properties = Objects.requireNonNull(payload); + this.threadId = threadId; + } + + /** + * @return the keys stored in this message + */ + public Stream keys() { + return properties.keySet().stream(); + } + + /** + * Get a String property from the payload + * + * @param key the key to fetch + * @return the value + */ + public String getProperty(String key) { + return properties.get(key); + } + + /** + * Get a String property from the payload + * + * @param key the key to fetch + * @param defaultValue default value to use when no value is present + * @return the value + */ + public String getProperty(String key, String defaultValue) { + return properties.getOrDefault(key, defaultValue); + } + + /** + * Get a boolean property from the payload + * + * @param key the key to fetch + * @return the value + */ + public boolean getBooleanProperty(String key) { + return Boolean.parseBoolean(properties.get(key)); + } + + /** + * Get a boolean property from the payload + * + * @param key the key to fetch + * @param defaultValue the value to use if not value is present + * @return the value + */ + public boolean getBooleanProperty(String key, boolean defaultValue) { + String property = getProperty(key); + if (property == null) { + return defaultValue; + } + return Boolean.parseBoolean(property); + } + + /** + * @return the remote session id for this message, only valid for messages not + * created locally + */ + public String getSessionId() { + if (sessionId == null) { + throw new IllegalStateException("can not be called on a local message!"); + } + return sessionId; + } + + /** + * @return the bytes using the message session id + */ + public byte[] serialize() { + return serialize(getSessionId()); + } + + @Override + public String toString() { + return getClass().getSimpleName() + " [" + sessionId + "][" + threadId + "] " + properties; + } + + /** + * Creates bytes for this message using the session id + * + * @param sessionId + * @return the bytes using the supplied message id + */ + public byte[] serialize(String sessionId) { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + DataOutputStream out = new DataOutputStream(stream); + try { + writeString(sessionId, out); + out.writeLong(threadId); + writeString(getClass().getSimpleName(), out); + if (properties.isEmpty()) { + out.writeInt(0); + } else { + Set> set = properties.entrySet(); + out.writeInt(set.size()); + for (Entry entry : set) { + writeString(entry.getKey(), out); + writeString(entry.getValue(), out); + } + } + } catch (IOException e) { + // should never happen, but if it happens something is wrong! + throw new RuntimeException("Internal Error: Write data failed", e); + } + return stream.toByteArray(); + } + + /** + * Creates a reply to a message using the thread id and session id from the + * original but with the provided payload + * + * @param message the reply message to inherit from + * @param payload the new payload + * @return the message + */ + public static Message replyTo(Message message, Map payload) { + if (payload == null) { + payload = Collections.emptyMap(); + } + return new Message(message.sessionId, message.threadId, payload); + } + + /** + * Decodes a message from its bytes + * + * @param bytes the bytes to decode + * @return the message or null if decoding failed + */ + public static Message decode(byte[] bytes) { + ByteArrayInputStream stream = new ByteArrayInputStream(bytes); + DataInputStream in = new DataInputStream(stream); + try { + String sessionId = readString(in); + long threadId = in.readLong(); + String messageType = readString(in); + int size = in.readInt(); + Map payload = new LinkedHashMap<>(size); + for (int i = 0; i < size; i++) { + payload.put(readString(in), readString(in)); + } + if ("SessionMessage".equals(messageType)) { + return new SessionMessage(sessionId, threadId, payload); + } + if ("ProjectsMessage".equals(messageType)) { + return new ProjectsMessage(sessionId, threadId, payload); + } + if ("RefreshMessage".equals(messageType)) { + return new RefreshMessage(sessionId, threadId, payload); + } + if ("InitMessage".equals(messageType)) { + return new InitMessage(sessionId, threadId, payload); + } + if ("ProjectMessage".equals(messageType)) { + return new ProjectMessage(sessionId, threadId, payload); + } + if ("MojoMessage".equals(messageType)) { + return new MojoMessage(sessionId, threadId, payload); + } + return new Message(sessionId, threadId, payload); + } catch (IOException e) { + // should never happen, but if it happens something is wrong! + System.err.println("Internal Error: Message decoding failed: " + e); + } + return null; + } + + private static String readString(DataInputStream in) throws IOException { + int length = in.readInt(); + if (length < 0) { + return null; + } + if (length == 0) { + return ""; + } + byte[] bs = new byte[length]; + in.readFully(bs); + return new String(bs, StandardCharsets.UTF_8); + } + + private static void writeString(String string, DataOutputStream stream) throws IOException { + if (string == null) { + stream.writeInt(-1); + } else { + byte[] bytes = string.getBytes(StandardCharsets.UTF_8); + stream.writeInt(bytes.length); + stream.write(bytes); + } + } + + @Override + public int hashCode() { + return Objects.hash(properties, sessionId, threadId); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Message other = (Message) obj; + return Objects.equals(properties, other.properties) + && Objects.equals(sessionId, other.sessionId) + && threadId == other.threadId; + } +} diff --git a/src/main/java/org/codehaus/plexus/build/connect/messages/MojoMessage.java b/src/main/java/org/codehaus/plexus/build/connect/messages/MojoMessage.java new file mode 100644 index 0000000..e1beb34 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/build/connect/messages/MojoMessage.java @@ -0,0 +1,138 @@ +/* +Copyright (c) 2025 Christoph Läubrich All rights reserved. + +This program is licensed to you under the Apache License Version 2.0, +and you may not use this file except in compliance with the Apache License Version 2.0. +You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + +Unless required by applicable law or agreed to in writing, +software distributed under the Apache License Version 2.0 is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. +*/ +package org.codehaus.plexus.build.connect.messages; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.maven.execution.ExecutionEvent.Type; +import org.apache.maven.plugin.MojoExecution; + +/** + * Mesaage generated when a mojo is executed + */ +public class MojoMessage extends Message { + + private static final String GOAL = "goal"; + private static final String LIFECYCLE_PHASE = "lifecyclePhase"; + private static final String EXECUTION_ID = "executionId"; + private static final String VERSION = "version"; + private static final String ARTIFACT_ID = "artifactId"; + private static final String GROUP_ID = "groupId"; + private static final String EVENT_TYPE = "eventType"; + + MojoMessage(String sessionId, long threadId, Map payload) { + super(sessionId, threadId, payload); + } + + /** + * Creates a Mojo message from execution and type + * + * @param mojoExecution + * @param type + */ + public MojoMessage(MojoExecution mojoExecution, Type type) { + super(toMap(mojoExecution, type)); + } + + /** + * @return the group id + */ + public String getGroupId() { + return getProperty(GROUP_ID); + } + + /** + * @return the artifact id + */ + public String getArtifactId() { + return getProperty(ARTIFACT_ID); + } + + /** + * @return the version + */ + public String getVersion() { + return getProperty(VERSION); + } + + /** + * @return the execution id + */ + public String getExecutionId() { + return getProperty(EXECUTION_ID); + } + + /** + * @return the lifecycle phase + */ + public String getLifecyclePhase() { + return getProperty(LIFECYCLE_PHASE); + } + + /** + * @return the lifecycle phase + */ + public String getGoal() { + return getProperty(GOAL); + } + + /** + * @return the type of event + */ + public EventType getType() { + try { + return EventType.valueOf(getProperty(EVENT_TYPE)); + } catch (RuntimeException e) { + return EventType.Unknown; + } + } + + private static Map toMap(MojoExecution mojoExecution, Type type) { + Map map = new HashMap<>(); + map.put(EVENT_TYPE, type.name()); + map.put(GROUP_ID, mojoExecution.getGroupId()); + map.put(ARTIFACT_ID, mojoExecution.getArtifactId()); + map.put(VERSION, mojoExecution.getVersion()); + map.put(EXECUTION_ID, mojoExecution.getExecutionId()); + map.put(LIFECYCLE_PHASE, mojoExecution.getLifecyclePhase()); + map.put(GOAL, mojoExecution.getGoal()); + return map; + } + + /** + * create the event type + */ + public static enum EventType { + /** + * the mojo was started + */ + MojoStarted, + /** + * The mojo failed + */ + MojoFailed, + /** + * The mojo was skipped + */ + MojoSkipped, + /** + * The mojo succeed + */ + MojoSucceeded, + /** + * the type is unknown + */ + Unknown; + } +} diff --git a/src/main/java/org/codehaus/plexus/build/connect/messages/ProjectMessage.java b/src/main/java/org/codehaus/plexus/build/connect/messages/ProjectMessage.java new file mode 100644 index 0000000..0ec2ef1 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/build/connect/messages/ProjectMessage.java @@ -0,0 +1,122 @@ +/* +Copyright (c) 2025 Christoph Läubrich All rights reserved. + +This program is licensed to you under the Apache License Version 2.0, +and you may not use this file except in compliance with the Apache License Version 2.0. +You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + +Unless required by applicable law or agreed to in writing, +software distributed under the Apache License Version 2.0 is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. +*/ +package org.codehaus.plexus.build.connect.messages; + +import java.io.File; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +import org.apache.maven.execution.ExecutionEvent.Type; +import org.apache.maven.project.MavenProject; + +/** + * Send to inform about project changes + */ +public class ProjectMessage extends Message { + + private static final String BASE_DIR = "baseDir"; + private static final String VERSION = "version"; + private static final String ARTIFACT_ID = "artifactId"; + private static final String GROUP_ID = "groupId"; + private static final String EVENT_TYPE = "eventType"; + + ProjectMessage(String sessionId, long threadId, Map payload) { + super(sessionId, threadId, payload); + } + + /** + * Constructs a new event based on project and type + * + * @param mavenProject + * @param eventtype + */ + public ProjectMessage(MavenProject mavenProject, Type eventtype) { + super(toMap(mavenProject, eventtype)); + } + + /** + * @return the group id of the project + */ + public String getGroupId() { + return getProperty(GROUP_ID); + } + + /** + * @return the artifact id of the project + */ + public String getArtifactId() { + return getProperty(ARTIFACT_ID); + } + + /** + * @return the version of the project + */ + public String getVersion() { + return getProperty(VERSION); + } + + /** + * @return the basedir of the project + */ + public Path getBaseDir() { + return new File(getProperty(BASE_DIR)).toPath(); + } + + /** + * @return the type of the event + */ + public EventType getType() { + try { + return EventType.valueOf(getProperty(EVENT_TYPE)); + } catch (RuntimeException e) { + return EventType.Unknown; + } + } + + private static Map toMap(MavenProject mavenProject, Type eventtype) { + Map map = new HashMap<>(); + map.put(EVENT_TYPE, eventtype.name()); + map.put(GROUP_ID, mavenProject.getGroupId()); + map.put(ARTIFACT_ID, mavenProject.getArtifactId()); + map.put(VERSION, mavenProject.getVersion()); + map.put(BASE_DIR, mavenProject.getBasedir().getAbsolutePath()); + return map; + } + + /** + * Describe the type of the event + */ + public static enum EventType { + /** + * The project was started + */ + ProjectStarted, + /** + * The project failed + */ + ProjectFailed, + /** + * The project was skipped + */ + ProjectSkipped, + /** + * the project succeed + */ + ProjectSucceeded, + /** + * the type of event is unknown + */ + Unknown; + } +} diff --git a/src/main/java/org/codehaus/plexus/build/connect/messages/ProjectsMessage.java b/src/main/java/org/codehaus/plexus/build/connect/messages/ProjectsMessage.java new file mode 100644 index 0000000..93bfa5a --- /dev/null +++ b/src/main/java/org/codehaus/plexus/build/connect/messages/ProjectsMessage.java @@ -0,0 +1,143 @@ +/* +Copyright (c) 2025 Christoph Läubrich All rights reserved. + +This program is licensed to you under the Apache License Version 2.0, +and you may not use this file except in compliance with the Apache License Version 2.0. +You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + +Unless required by applicable law or agreed to in writing, +software distributed under the Apache License Version 2.0 is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. +*/ +package org.codehaus.plexus.build.connect.messages; + +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.file.Path; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; + +import org.apache.maven.model.Model; +import org.apache.maven.model.io.DefaultModelWriter; +import org.apache.maven.project.MavenProject; + +/** + * Message send to inform about reactor project in the build and their effective + * model + */ +public class ProjectsMessage extends Message { + + private static final DefaultModelWriter MODEL_WRITER = new DefaultModelWriter(); + + ProjectsMessage(String sessionId, long threadId, Map payload) { + super(sessionId, threadId, payload); + } + + /** + * @param projects the projects to send + */ + public ProjectsMessage(Collection projects) { + super(buildMap(projects)); + } + + /** + * @return a stream of project infos + */ + public Stream projects() { + return keys().map(key -> { + String[] gav = key.split("\t"); + if (gav.length == 5 && "p".equals(gav[0])) { + ProjectInfo pi = new ProjectInfo() { + + @Override + public String getGroupId() { + return gav[1]; + } + + @Override + public String getArtifactId() { + return gav[2]; + } + + @Override + public String getVersion() { + return gav[3]; + } + + @Override + public String getModel() { + return getProperty(key); + } + + @Override + public Path getBaseDir() { + return new File(gav[4]).toPath(); + } + }; + return pi; + } + return null; + }) + .filter(Objects::nonNull); + } + + private static Map buildMap(Collection projects) { + Map map = new HashMap<>(); + for (MavenProject project : projects) { + String key = String.format( + "p\t%s\t%s\t%s\t%s", + project.getGroupId(), + project.getArtifactId(), + project.getVersion(), + project.getBasedir().getAbsolutePath()); + map.put(key, getEffectiveModel(project)); + } + return map; + } + + private static String getEffectiveModel(MavenProject project) { + Model model = project.getModel(); + StringWriter writer = new StringWriter(); + try { + MODEL_WRITER.write(writer, null, model); + } catch (IOException e) { + } + String string = writer.toString(); + return string; + } + + /** + * Holds basic project info + */ + public static interface ProjectInfo { + /** + * @return the group id of the reactor project + */ + String getGroupId(); + + /** + * @return the artifact id of the reactor project + */ + String getArtifactId(); + + /** + * @return the version of the reactor project + */ + String getVersion(); + + /** + * @return the effective model of the project + */ + String getModel(); + + /** + * @return the basedir of the project + */ + Path getBaseDir(); + } +} diff --git a/src/main/java/org/codehaus/plexus/build/connect/messages/RefreshMessage.java b/src/main/java/org/codehaus/plexus/build/connect/messages/RefreshMessage.java new file mode 100644 index 0000000..a473a02 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/build/connect/messages/RefreshMessage.java @@ -0,0 +1,47 @@ +/* +Copyright (c) 2025 Christoph Läubrich All rights reserved. + +This program is licensed to you under the Apache License Version 2.0, +and you may not use this file except in compliance with the Apache License Version 2.0. +You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + +Unless required by applicable law or agreed to in writing, +software distributed under the Apache License Version 2.0 is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. +*/ +package org.codehaus.plexus.build.connect.messages; + +import java.io.File; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Map; + +/** + * A message that indicates a path should be refreshed (e.g. because new files + * are placed in a generated folder) + */ +public class RefreshMessage extends Message { + + private static final String PATH_KEY = "path"; + + /** + * Create a new message to refresh a path + * + * @param path the path to refresh + */ + public RefreshMessage(Path path) { + super(Collections.singletonMap(PATH_KEY, path.toFile().getAbsolutePath())); + } + + /** + * @return the path to refresh + */ + public Path getPath() { + return new File(getProperty(PATH_KEY)).toPath(); + } + + RefreshMessage(String sessionId, long threadId, Map payload) { + super(sessionId, threadId, payload); + } +} diff --git a/src/main/java/org/codehaus/plexus/build/connect/messages/SessionMessage.java b/src/main/java/org/codehaus/plexus/build/connect/messages/SessionMessage.java new file mode 100644 index 0000000..2f9e0a6 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/build/connect/messages/SessionMessage.java @@ -0,0 +1,63 @@ +/* +Copyright (c) 2025 Christoph Läubrich All rights reserved. + +This program is licensed to you under the Apache License Version 2.0, +and you may not use this file except in compliance with the Apache License Version 2.0. +You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + +Unless required by applicable law or agreed to in writing, +software distributed under the Apache License Version 2.0 is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. +*/ +package org.codehaus.plexus.build.connect.messages; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.maven.execution.MavenSession; + +/** + * Event that is received / send when a session starts/end + */ +public class SessionMessage extends Message { + + private static final String SESSION_EXECUTION_ROOT_DIRECTORY = "sessionExecutionRootDirectory"; + private static final String SESSION_START = "sessionStart"; + + /** + * Creates a new session message + * + * @param session the session to use + * @param start true if it is a start of the session or + * false if it is the end of a session + */ + public SessionMessage(MavenSession session, boolean start) { + super(buildMap(session, start)); + } + + SessionMessage(String sessionId, long threadId, Map payload) { + super(sessionId, threadId, payload); + } + + /** + * @return true if this is a session start event + */ + public boolean isSessionStart() { + return getBooleanProperty(SESSION_START); + } + + /** + * @return the value of the ExecutionRootDirectory of this session + */ + public String getExecutionRootDirectory() { + return getProperty(SESSION_EXECUTION_ROOT_DIRECTORY); + } + + private static Map buildMap(MavenSession session, boolean start) { + Map map = new HashMap<>(2); + map.put(SESSION_START, Boolean.toString(start)); + map.put(SESSION_EXECUTION_ROOT_DIRECTORY, session.getExecutionRootDirectory()); + return map; + } +} diff --git a/src/main/resources/META-INF/maven/extension.xml b/src/main/resources/META-INF/maven/extension.xml new file mode 100644 index 0000000..181a211 --- /dev/null +++ b/src/main/resources/META-INF/maven/extension.xml @@ -0,0 +1,11 @@ + + + + + org.codehaus.plexus.build + + + + org.codehaus.plexus:plexus-build-api + +