4040import java .util .LinkedHashMap ;
4141import java .util .List ;
4242import java .util .Map ;
43- import java .util .function .BiConsumer ;
43+ import java .util .concurrent .ConcurrentHashMap ;
44+ import java .util .function .Function ;
4445
4546import static org .apposed .appose .NDArray .Shape .Order .C_ORDER ;
4647
@@ -65,6 +66,80 @@ private Messages() {
6566 // Counter for auto-generated proxy variable names.
6667 private static int proxyCounter = 0 ;
6768
69+ // Registry of class -> (appose_type, encoder) for encoding custom types.
70+ private static final Map <Class <?>, String > ENCODER_TYPES = new ConcurrentHashMap <>();
71+ private static final Map <Class <?>, Function <Object , Object >> ENCODER_FNS = new ConcurrentHashMap <>();
72+
73+ // Registry of appose_type -> factory for decoding custom types.
74+ private static final Map <String , Function <Map <String , Object >, Object >> DECODERS =
75+ new ConcurrentHashMap <>();
76+
77+ /**
78+ * Registers encoder and decoder functions for a custom Appose type.
79+ * <p>
80+ * When encoding, if an object is an instance of {@code objType}, {@code encoder}
81+ * is called and its return value is wrapped as
82+ * {@code {"appose_type": apposeType, "data": <encoded>}}.
83+ * </p>
84+ * <p>
85+ * When decoding, if a JSON object has the given {@code apposeType}, {@code decoder}
86+ * is called with the {@code "data"} field value and should return the
87+ * reconstructed object.
88+ * </p>
89+ *
90+ * @param <T> The type being registered.
91+ * @param objType The class of objects to encode.
92+ * @param apposeType The {@code appose_type} string used on the wire.
93+ * @param encoder Function from object to JSON-compatible value (without appose_type).
94+ * @param decoder Function from decoded data map to reconstructed object.
95+ */
96+ @ SuppressWarnings ("unchecked" )
97+ public static <T > void register (
98+ Class <T > objType ,
99+ String apposeType ,
100+ Function <T , Object > encoder ,
101+ Function <Map <String , Object >, Object > decoder
102+ ) {
103+ ENCODER_TYPES .put (objType , apposeType );
104+ ENCODER_FNS .put (objType , (Function <Object , Object >) (Function <?, ?>) encoder );
105+ DECODERS .put (apposeType , decoder );
106+ }
107+
108+ static {
109+ // NB: Built-in type registrations live here rather than in their respective
110+ // classes (SharedMemory, NDArray) because Java static initializers only run
111+ // when a class is first loaded. If Messages.decode() were called before
112+ // SharedMemory or NDArray had been referenced, their decoders would not yet
113+ // be registered. Keeping registrations here ensures they are always in place
114+ // as soon as the Messages class itself is loaded.
115+ register (SharedMemory .class , "shm" ,
116+ shm -> {
117+ Map <String , Object > payload = new LinkedHashMap <>();
118+ payload .put ("name" , shm .name ());
119+ payload .put ("rsize" , shm .rsize ());
120+ return payload ;
121+ },
122+ map -> SharedMemory .attach (
123+ (String ) map .get ("name" ),
124+ ((Number ) map .get ("rsize" )).longValue ()
125+ )
126+ );
127+ register (NDArray .class , "ndarray" ,
128+ nda -> {
129+ Map <String , Object > payload = new LinkedHashMap <>();
130+ payload .put ("dtype" , nda .dType ().label ());
131+ payload .put ("shape" , nda .shape ().toIntArray (C_ORDER ));
132+ payload .put ("shm" , nda .shm ());
133+ return payload ;
134+ },
135+ map -> new NDArray (
136+ toDType ((String ) map .get ("dtype" )),
137+ toShape ((List <Integer >) map .get ("shape" )),
138+ (SharedMemory ) map .get ("shm" )
139+ )
140+ );
141+ }
142+
68143 /**
69144 * Converts a Map into a JSON string.
70145 * @param data
@@ -137,13 +212,12 @@ public static String stackTrace(Throwable t) {
137212 * Map, List, String (and other CharSequences), Number, Boolean, and primitives.
138213 * </p>
139214 * <p>
140- * Other types either have custom Appose converters (SharedMemory, NDArray) or
141- * will be auto-proxied as worker_object references when in worker mode.
215+ * Other types either implement {@link ForJson} (and are handled by the ForJson
216+ * converter) or will be auto-proxied as worker_object references when in worker mode.
142217 * </p>
143218 * <p>
144- * Note: This list should remain stable. When adding new Appose-specific converters
145- * (e.g., for custom types), add them to the GENERATOR converter chain before the
146- * catch-all converter - you do NOT need to modify this method.
219+ * Note: This list should remain stable. Types implementing {@link ForJson} are
220+ * handled before the catch-all and do not require changes here.
147221 * </p>
148222 *
149223 * @param type The class to check
@@ -158,50 +232,28 @@ private static boolean isNativelyJsonSerializable(Class<?> type) {
158232 || type .isPrimitive ();
159233 }
160234
161- /**
162- * Helper to create a {@link JsonGenerator.Converter}.
163- * <p>
164- * The converter encodes objects of a specified class {@code clz} into a
165- * {@code Map<String, Object>}. The given {@code appose_type} string is put
166- * into the map with key {@code "appose_type"}. The map and value to be
167- * converted are passed to the given {@code BiConsumer} which should
168- * serialize the value into the map somehow.
169- * </p>
170- *
171- * @param clz the converter will handle objects of this class (or sub-classes)
172- * @param appose_type the value for key "appose_type" in the returned Map
173- * @param converter accepts a map and a value, and serializes the value into the map somehow.
174- * @return a new converter
175- * @param <T> object type handled by this converter
176- */
177- private static <T > JsonGenerator .Converter convert (Class <T > clz , String appose_type , BiConsumer <Map <String , Object >, T > converter ) {
178- return new JsonGenerator .Converter () {
179-
180- @ Override
181- public boolean handles (Class <?> type ) {
182- return clz .isAssignableFrom (type );
183- }
184-
185- @ SuppressWarnings ("unchecked" )
186- @ Override
187- public Object convert (Object value , String key ) {
188- Map <String , Object > map = new LinkedHashMap <>();
189- map .put ("appose_type" , appose_type );
190- converter .accept (map , (T ) value );
191- return map ;
192- }
193- };
194- }
195-
196235 static final JsonGenerator GENERATOR = new JsonGenerator .Options () //
197- .addConverter (convert (SharedMemory .class , "shm" , (map , shm ) -> {
198- map .put ("name" , shm .name ());
199- map .put ("rsize" , shm .rsize ());
200- })).addConverter (convert (NDArray .class , "ndarray" , (map , ndArray ) -> {
201- map .put ("dtype" , ndArray .dType ().label ());
202- map .put ("shape" , ndArray .shape ().toIntArray (C_ORDER ));
203- map .put ("shm" , ndArray .shm ());
204- })).addConverter (new JsonGenerator .Converter () {
236+ .addConverter (new JsonGenerator .Converter () {
237+ @ Override
238+ public boolean handles (Class <?> type ) {
239+ return ENCODER_TYPES .keySet ().stream ().anyMatch (c -> c .isAssignableFrom (type ));
240+ }
241+
242+ @ Override
243+ public Object convert (Object value , String key ) {
244+ for (Map .Entry <Class <?>, Function <Object , Object >> entry : ENCODER_FNS .entrySet ()) {
245+ if (entry .getKey ().isAssignableFrom (value .getClass ())) {
246+ Map <String , Object > map = new LinkedHashMap <>();
247+ map .put ("appose_type" , ENCODER_TYPES .get (entry .getKey ()));
248+ @ SuppressWarnings ("unchecked" )
249+ Map <String , Object > payload = (Map <String , Object >) entry .getValue ().apply (value );
250+ map .putAll (payload );
251+ return map ;
252+ }
253+ }
254+ throw new IllegalStateException ("No encoder for " + value .getClass ());
255+ }
256+ }).addConverter (new JsonGenerator .Converter () {
205257 // Catch-all converter for non-serializable objects in worker mode.
206258 // This should be the LAST converter in the chain, so it only handles
207259 // objects that no other converter claimed.
@@ -211,8 +263,7 @@ public boolean handles(Class<?> type) {
211263 if (!workerMode ) return false ;
212264
213265 // Don't auto-proxy types that Groovy's JSON encoder handles natively.
214- // Custom converters (SharedMemory, NDArray, etc.) are earlier in the
215- // chain and will have already claimed their types.
266+ // Registered types are earlier in the chain and will have already claimed theirs.
216267 return !isNativelyJsonSerializable (type );
217268 }
218269
@@ -250,23 +301,14 @@ private static Object processValue(Object value) {
250301 Object v = map .get ("appose_type" );
251302 if (v instanceof String ) {
252303 String appose_type = (String ) v ;
253- switch (appose_type ) {
254- case "shm" :
255- String name = (String ) map .get ("name" );
256- long rsize = ((Number ) map .get ("rsize" )).longValue ();
257- return SharedMemory .attach (name , rsize );
258- case "ndarray" :
259- NDArray .DType dType = toDType ((String ) map .get ("dtype" ));
260- NDArray .Shape shape = toShape ((List <Integer >) map .get ("shape" ));
261- SharedMemory shm = (SharedMemory ) map .get ("shm" );
262- return new NDArray (dType , shape , shm );
263- case "worker_object" :
264- // Return map as-is; will be converted to WorkerObject
265- // by Proxies.proxifyWorkerObjects() in Service.Task.handle().
266- return map ;
267- default :
268- System .err .println ("unknown appose_type \" " + appose_type + "\" " );
304+ if ("worker_object" .equals (appose_type )) {
305+ // Return map as-is; will be converted to WorkerObject
306+ // by Proxies.proxifyWorkerObjects() in Service.Task.handle().
307+ return map ;
269308 }
309+ Function <Map <String , Object >, Object > factory = DECODERS .get (appose_type );
310+ if (factory != null ) return factory .apply (map );
311+ System .err .println ("unknown appose_type \" " + appose_type + "\" " );
270312 }
271313 return map ;
272314 } else {
0 commit comments