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 + +