From 61180289e8c79b6712bf824e7ebdd59876b75e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Sat, 22 Feb 2025 05:39:53 +0100 Subject: [PATCH] Add a connect API for inter-process-communication between maven and IDE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently an IDE launching an external maven process is rather blind, it can only see if the process exit with success and maybe read the standard stream. A much more feature rich experience could been offered if the launching process can gather some information about the run, e.g. what process is currently executed, what mojo and if it failed. Also currently features of the build context can only be implemented if running inside the IDE process what has several implications. This now adds a connect API that allows an IDE to supply an extension to the maven process (e.g. via maven.ext.class.path) and set a system property (plexus.build.ipc.port) to communicate with the running maven process. Signed-off-by: Christoph Läubrich --- README.md | 14 +- pom.xml | 6 + .../plexus/build/DefaultBuildContext.java | 14 +- .../plexus/build/connect/BuildConnection.java | 50 ++++ .../plexus/build/connect/Configuration.java | 51 ++++ .../plexus/build/connect/SessionListener.java | 65 ++++ .../build/connect/TcpBuildConnection.java | 281 ++++++++++++++++++ .../build/connect/messages/Message.java | 226 ++++++++++++++ .../connect/messages/ProjectsReadMessage.java | 63 ++++ .../connect/messages/RefreshMessage.java | 47 +++ .../connect/messages/SessionMessage.java | 90 ++++++ .../resources/META-INF/maven/extension.xml | 11 + 12 files changed, 910 insertions(+), 8 deletions(-) create mode 100644 src/main/java/org/codehaus/plexus/build/connect/BuildConnection.java create mode 100644 src/main/java/org/codehaus/plexus/build/connect/Configuration.java create mode 100644 src/main/java/org/codehaus/plexus/build/connect/SessionListener.java create mode 100644 src/main/java/org/codehaus/plexus/build/connect/TcpBuildConnection.java create mode 100644 src/main/java/org/codehaus/plexus/build/connect/messages/Message.java create mode 100644 src/main/java/org/codehaus/plexus/build/connect/messages/ProjectsReadMessage.java create mode 100644 src/main/java/org/codehaus/plexus/build/connect/messages/RefreshMessage.java create mode 100644 src/main/java/org/codehaus/plexus/build/connect/messages/SessionMessage.java create mode 100644 src/main/resources/META-INF/maven/extension.xml 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 f332246..15a4f4c 100644 --- a/pom.xml +++ b/pom.xml @@ -69,6 +69,12 @@ See the Apache License Version 2.0 for the specific language governing permissio + + org.apache.maven + maven-core + 3.9.9 + provided + diff --git a/src/main/java/org/codehaus/plexus/build/DefaultBuildContext.java b/src/main/java/org/codehaus/plexus/build/DefaultBuildContext.java index 7fb0da6..797079e 100644 --- a/src/main/java/org/codehaus/plexus/build/DefaultBuildContext.java +++ b/src/main/java/org/codehaus/plexus/build/DefaultBuildContext.java @@ -24,6 +24,8 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +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 +58,18 @@ public class DefaultBuildContext implements BuildContext { private final Map contextMap = new ConcurrentHashMap<>(); private org.sonatype.plexus.build.incremental.BuildContext legacy; + private BuildConnection connection; /** - * @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 */ @Inject - public DefaultBuildContext(org.sonatype.plexus.build.incremental.BuildContext legacy) { + public DefaultBuildContext(org.sonatype.plexus.build.incremental.BuildContext legacy, BuildConnection connection) { this.legacy = legacy; + this.connection = connection; } /** {@inheritDoc} */ @@ -117,6 +122,7 @@ public Scanner newScanner(File basedir) { /** {@inheritDoc} */ public void refresh(File file) { legacy.refresh(file); + connection.send(new RefreshMessage(file.toPath())); } /** {@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..f2b83df --- /dev/null +++ b/src/main/java/org/codehaus/plexus/build/connect/BuildConnection.java @@ -0,0 +1,50 @@ +/* +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; + +/** + * 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 + * @return the reply message or null if this connection is not + * enabled and the message was discarded. + */ + Message send(Message message); + + /** + * 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(); + + /** + * Obtains the current configuration, can only be called from a maven thread + * + * @return the active configuration + */ + Configuration getConfiguration(); +} 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..c467b36 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/build/connect/Configuration.java @@ -0,0 +1,51 @@ +/* +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; +import org.codehaus.plexus.build.connect.messages.ProjectsReadMessage; + +/** + * Provides access to the configuration provided by the server + */ +public interface Configuration { + + /** + * If this property is set to true in reply to a session start, a + * {@link ProjectsReadMessage} will be send to the endpoint containing all + * projects with their effective model + */ + public static final String CONFIG_SEND_AFTER_PROJECTS_READ = "afterProjectsRead"; + + /** + * @return true if {@link #CONFIG_SEND_AFTER_PROJECTS_READ} 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_AFTER_PROJECTS_READ, false); + } + }; + } +} diff --git a/src/main/java/org/codehaus/plexus/build/connect/SessionListener.java b/src/main/java/org/codehaus/plexus/build/connect/SessionListener.java new file mode 100644 index 0000000..c8cea4c --- /dev/null +++ b/src/main/java/org/codehaus/plexus/build/connect/SessionListener.java @@ -0,0 +1,65 @@ +/* +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.AbstractMavenLifecycleParticipant; +import org.apache.maven.MavenExecutionException; +import org.apache.maven.execution.MavenSession; +import org.codehaus.plexus.build.connect.messages.Message; +import org.codehaus.plexus.build.connect.messages.ProjectsReadMessage; +import org.codehaus.plexus.build.connect.messages.SessionMessage; + +/** + * Listen to session events and send them to the connection + */ +@Named +@Singleton +public class SessionListener extends AbstractMavenLifecycleParticipant { + + @Inject + private BuildConnection connection; + + private boolean sendProjects; + private boolean started; + + @Override + public void afterSessionStart(MavenSession session) throws MavenExecutionException { + started = true; + Message reply = connection.send(new SessionMessage(session, true)); + if (reply != null) { + sendProjects = Configuration.of(reply).isSendProjects(); + } + } + + @Override + public void afterProjectsRead(MavenSession session) throws MavenExecutionException { + if (connection.isEnabled()) { + if (!started) { + afterSessionStart(session); + } + if (sendProjects) { + connection.send(new ProjectsReadMessage(session.getAllProjects())); + } + } + } + + @Override + public void afterSessionEnd(MavenSession session) throws MavenExecutionException { + connection.send(new SessionMessage(session, false)); + started = false; + } +} 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..b9b1a95 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/build/connect/TcpBuildConnection.java @@ -0,0 +1,281 @@ +/* +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 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.concurrent.ConcurrentHashMap; +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.plugin.LegacySupport; +import org.codehaus.plexus.build.connect.messages.Message; +import org.codehaus.plexus.build.connect.messages.SessionMessage; + +/** + * 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); + + @Inject + private LegacySupport support; + + private Map configMap = new ConcurrentHashMap<>(); + + private final ThreadLocal connections = + ThreadLocal.withInitial(() -> new TcpClientConnection()); + + @Override + public boolean isEnabled() { + return PORT > 0; + } + + @Override + public Message send(Message message) { + if (isEnabled()) { + String sessionId; + boolean sessionStart; + if (message instanceof SessionMessage) { + sessionId = message.getSessionId(); + sessionStart = ((SessionMessage) message).isSessionStart(); + } else { + sessionId = getThreadSessionId(); + sessionStart = false; + } + byte[] messageBytes = message.serialize(sessionId); + byte[] replyBytes = connections.get().send(messageBytes); + if (replyBytes.length > 0) { + Message reply = Message.decode(replyBytes); + if (reply != null && sessionStart) { + configMap.put(sessionId, Configuration.of(reply)); + } + return reply; + } + } + return null; + } + + private String getThreadSessionId() { + // We must use LegacySupport here to get the currents threads session (what + // might be cloned) + return SessionMessage.getId(support.getSession()); + } + + @Override + public Configuration getConfiguration() { + String id = getThreadSessionId(); + if (id == null) { + throw new IllegalStateException("No session attached to current thread!"); + } + Configuration configuration = configMap.get(id); + if (configuration == null) { + throw new IllegalStateException("No configuration active for session " + id + "!"); + } + return configuration; + } + + /** + * 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/Message.java b/src/main/java/org/codehaus/plexus/build/connect/messages/Message.java new file mode 100644 index 0000000..af47e5d --- /dev/null +++ b/src/main/java/org/codehaus/plexus/build/connect/messages/Message.java @@ -0,0 +1,226 @@ +/* +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; + +/** + * 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; + } + + /** + * 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 ("ProjectsReadMessage".equals(messageType)) { + return new ProjectsReadMessage(sessionId, threadId, payload); + } + if ("RefreshMessage".equals(messageType)) { + return new RefreshMessage(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); + } + } +} diff --git a/src/main/java/org/codehaus/plexus/build/connect/messages/ProjectsReadMessage.java b/src/main/java/org/codehaus/plexus/build/connect/messages/ProjectsReadMessage.java new file mode 100644 index 0000000..91825ef --- /dev/null +++ b/src/main/java/org/codehaus/plexus/build/connect/messages/ProjectsReadMessage.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.io.IOException; +import java.io.StringWriter; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +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 ProjectsReadMessage extends Message { + + private static final DefaultModelWriter MODEL_WRITER = new DefaultModelWriter(); + + ProjectsReadMessage(String sessionId, long threadId, Map payload) { + super(sessionId, threadId, payload); + } + + /** + * @param projects the projects to send + */ + public ProjectsReadMessage(Collection projects) { + super(buildMap(projects)); + } + + private static Map buildMap(Collection projects) { + Map map = new HashMap<>(); + for (MavenProject project : projects) { + String key = project.getGroupId() + ":" + project.getArtifactId() + ":" + project.getVersion(); + 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; + } +} 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..f88d906 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/build/connect/messages/SessionMessage.java @@ -0,0 +1,90 @@ +/* +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 java.util.UUID; +import java.util.WeakHashMap; + +import org.apache.maven.execution.MavenExecutionRequest; +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"; + private static final String SESSION_ID = "sessionId"; + private static final Map ID_MAP = new WeakHashMap<>(); + + /** + * 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); + } + + public String getSessionId() { + return getProperty(SESSION_ID); + } + + /** + * @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); + } + + /** + * Returns the unique ID for a session + * + * @param session the session to get an Id for + * @return the id of the session or the name of the current thread if the + * session is null + */ + public static synchronized String getId(MavenSession session) { + if (session == null) { + return Thread.currentThread().getName(); + } + // we can't use the session itself as a key, because sessions might be cloned, + // but the execution request should (hopefully) stay constant... + return ID_MAP.computeIfAbsent( + session.getRequest(), x -> UUID.randomUUID().toString()); + } + + private static Map buildMap(MavenSession session, boolean start) { + Map map = new HashMap<>(2); + map.put(SESSION_ID, getId(session)); + 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 + +