View Javadoc
1   /*
2   Copyright (c) 2025 Christoph Läubrich All rights reserved.
3   
4   This program is licensed to you under the Apache License Version 2.0,
5   and you may not use this file except in compliance with the Apache License Version 2.0.
6   You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
7   
8   Unless required by applicable law or agreed to in writing,
9   software distributed under the Apache License Version 2.0 is distributed on an
10  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11  See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
12  */
13  package org.codehaus.plexus.build.connect.messages;
14  
15  import java.io.ByteArrayInputStream;
16  import java.io.ByteArrayOutputStream;
17  import java.io.DataInputStream;
18  import java.io.DataOutputStream;
19  import java.io.IOException;
20  import java.nio.charset.StandardCharsets;
21  import java.util.Collections;
22  import java.util.LinkedHashMap;
23  import java.util.Map;
24  import java.util.Map.Entry;
25  import java.util.Objects;
26  import java.util.Set;
27  import java.util.concurrent.atomic.AtomicLong;
28  import java.util.stream.Stream;
29  
30  /**
31   * A message exchanged between two endpoints, usually an IDE and a maven build
32   */
33  public class Message {
34      private static final ThreadLocal<Long> ID = new ThreadLocal<Long>() {
35          private final AtomicLong generator = new AtomicLong();
36  
37          @Override
38          protected Long initialValue() {
39              return generator.getAndIncrement();
40          }
41      };
42      private final long threadId;
43      private final Map<String, String> properties;
44      private final String sessionId;
45  
46      Message(Map<String, String> payload) {
47          this(null, ID.get(), payload);
48      }
49  
50      Message(String sessionId, long threadId, Map<String, String> payload) {
51          this.sessionId = sessionId;
52          this.properties = Objects.requireNonNull(payload);
53          this.threadId = threadId;
54      }
55  
56      /**
57       * @return the keys stored in this message
58       */
59      public Stream<String> keys() {
60          return properties.keySet().stream();
61      }
62  
63      /**
64       * Get a String property from the payload
65       *
66       * @param key the key to fetch
67       * @return the value
68       */
69      public String getProperty(String key) {
70          return properties.get(key);
71      }
72  
73      /**
74       * Get a String property from the payload
75       *
76       * @param key          the key to fetch
77       * @param defaultValue default value to use when no value is present
78       * @return the value
79       */
80      public String getProperty(String key, String defaultValue) {
81          return properties.getOrDefault(key, defaultValue);
82      }
83  
84      /**
85       * Get a boolean property from the payload
86       *
87       * @param key the key to fetch
88       * @return the value
89       */
90      public boolean getBooleanProperty(String key) {
91          return Boolean.parseBoolean(properties.get(key));
92      }
93  
94      /**
95       * Get a boolean property from the payload
96       *
97       * @param key          the key to fetch
98       * @param defaultValue the value to use if not value is present
99       * @return the value
100      */
101     public boolean getBooleanProperty(String key, boolean defaultValue) {
102         String property = getProperty(key);
103         if (property == null) {
104             return defaultValue;
105         }
106         return Boolean.parseBoolean(property);
107     }
108 
109     /**
110      * @return the remote session id for this message, only valid for messages not
111      *         created locally
112      */
113     public String getSessionId() {
114         if (sessionId == null) {
115             throw new IllegalStateException("can not be called on a local message!");
116         }
117         return sessionId;
118     }
119 
120     /**
121      * @return the bytes using the message session id
122      */
123     public byte[] serialize() {
124         return serialize(getSessionId());
125     }
126 
127     @Override
128     public String toString() {
129         return getClass().getSimpleName() + " [" + sessionId + "][" + threadId + "] " + properties;
130     }
131 
132     /**
133      * Creates bytes for this message using the session id
134      *
135      * @param sessionId
136      * @return the bytes using the supplied message id
137      */
138     public byte[] serialize(String sessionId) {
139         ByteArrayOutputStream stream = new ByteArrayOutputStream();
140         DataOutputStream out = new DataOutputStream(stream);
141         try {
142             writeString(sessionId, out);
143             out.writeLong(threadId);
144             writeString(getClass().getSimpleName(), out);
145             if (properties.isEmpty()) {
146                 out.writeInt(0);
147             } else {
148                 Set<Entry<String, String>> set = properties.entrySet();
149                 out.writeInt(set.size());
150                 for (Entry<String, String> entry : set) {
151                     writeString(entry.getKey(), out);
152                     writeString(entry.getValue(), out);
153                 }
154             }
155         } catch (IOException e) {
156             // should never happen, but if it happens something is wrong!
157             throw new RuntimeException("Internal Error: Write data failed", e);
158         }
159         return stream.toByteArray();
160     }
161 
162     /**
163      * Creates a reply to a message using the thread id and session id from the
164      * original but with the provided payload
165      *
166      * @param message the reply message to inherit from
167      * @param payload the new payload
168      * @return the message
169      */
170     public static Message replyTo(Message message, Map<String, String> payload) {
171         if (payload == null) {
172             payload = Collections.emptyMap();
173         }
174         return new Message(message.sessionId, message.threadId, payload);
175     }
176 
177     /**
178      * Decodes a message from its bytes
179      *
180      * @param bytes the bytes to decode
181      * @return the message or <code>null</code> if decoding failed
182      */
183     public static Message decode(byte[] bytes) {
184         ByteArrayInputStream stream = new ByteArrayInputStream(bytes);
185         DataInputStream in = new DataInputStream(stream);
186         try {
187             String sessionId = readString(in);
188             long threadId = in.readLong();
189             String messageType = readString(in);
190             int size = in.readInt();
191             Map<String, String> payload = new LinkedHashMap<>(size);
192             for (int i = 0; i < size; i++) {
193                 payload.put(readString(in), readString(in));
194             }
195             if ("SessionMessage".equals(messageType)) {
196                 return new SessionMessage(sessionId, threadId, payload);
197             }
198             if ("ProjectsMessage".equals(messageType)) {
199                 return new ProjectsMessage(sessionId, threadId, payload);
200             }
201             if ("RefreshMessage".equals(messageType)) {
202                 return new RefreshMessage(sessionId, threadId, payload);
203             }
204             if ("InitMessage".equals(messageType)) {
205                 return new InitMessage(sessionId, threadId, payload);
206             }
207             if ("ProjectMessage".equals(messageType)) {
208                 return new ProjectMessage(sessionId, threadId, payload);
209             }
210             if ("MojoMessage".equals(messageType)) {
211                 return new MojoMessage(sessionId, threadId, payload);
212             }
213             return new Message(sessionId, threadId, payload);
214         } catch (IOException e) {
215             // should never happen, but if it happens something is wrong!
216             System.err.println("Internal Error: Message decoding failed: " + e);
217         }
218         return null;
219     }
220 
221     private static String readString(DataInputStream in) throws IOException {
222         int length = in.readInt();
223         if (length < 0) {
224             return null;
225         }
226         if (length == 0) {
227             return "";
228         }
229         byte[] bs = new byte[length];
230         in.readFully(bs);
231         return new String(bs, StandardCharsets.UTF_8);
232     }
233 
234     private static void writeString(String string, DataOutputStream stream) throws IOException {
235         if (string == null) {
236             stream.writeInt(-1);
237         } else {
238             byte[] bytes = string.getBytes(StandardCharsets.UTF_8);
239             stream.writeInt(bytes.length);
240             stream.write(bytes);
241         }
242     }
243 
244     @Override
245     public int hashCode() {
246         return Objects.hash(properties, sessionId, threadId);
247     }
248 
249     @Override
250     public boolean equals(Object obj) {
251         if (this == obj) {
252             return true;
253         }
254         if (obj == null) {
255             return false;
256         }
257         if (getClass() != obj.getClass()) {
258             return false;
259         }
260         Message other = (Message) obj;
261         return Objects.equals(properties, other.properties)
262                 && Objects.equals(sessionId, other.sessionId)
263                 && threadId == other.threadId;
264     }
265 }