@@ -114,6 +114,12 @@ public class MessageSerializerGenerator {
114114 /** The marshallable message type. */
115115 private final TypeMirror marshallableMsgType ;
116116
117+ /** Collection of lines for the {@code prepareMarshalCacheObjects} method. Empty when the message has no such fields. */
118+ private final List <String > prepareCacheObjects = new ArrayList <>();
119+
120+ /** Collection of lines for the {@code finishUnmarshalCacheObjects} method. Empty when the message has no such fields. */
121+ private final List <String > finishCacheObjects = new ArrayList <>();
122+
117123 /** */
118124 private int indent ;
119125
@@ -189,6 +195,20 @@ private String generateSerializerCode(String serClsName) throws IOException {
189195
190196 writer .write (TAB + "}" + NL );
191197
198+ // Write #prepareMarshalCacheObjects / #finishUnmarshalCacheObjects overrides if the message has any
199+ // cache-object-bearing @Order fields (IGNITE-28520).
200+ if (!prepareCacheObjects .isEmpty ()) {
201+ writer .write (NL );
202+
203+ for (String p : prepareCacheObjects )
204+ writer .write (p + NL );
205+
206+ writer .write (NL );
207+
208+ for (String f : finishCacheObjects )
209+ writer .write (f + NL );
210+ }
211+
192212 writer .write ("}" );
193213
194214 writer .write (NL );
@@ -242,6 +262,178 @@ private void generateMethods(List<VariableElement> fields) throws Exception {
242262
243263 finish (write , false , false );
244264 finish (read , true , marshallableMessage ());
265+
266+ generateCacheObjectMethods (fields );
267+ }
268+
269+ /**
270+ * Generates bodies of {@code prepareMarshalCacheObjects} / {@code finishUnmarshalCacheObjects} overrides when the
271+ * message carries at least one {@code @Order}-annotated field whose declared type is {@code CacheObject},
272+ * {@code KeyCacheObject}, a {@code Collection} of them, or an array of them. Traversal is single-level: it does
273+ * not recurse into nested messages or enter map values. See IGNITE-28520.
274+ */
275+ private void generateCacheObjectMethods (List <VariableElement > fields ) throws Exception {
276+ List <VariableElement > cacheObjFields = new ArrayList <>();
277+
278+ for (VariableElement field : fields ) {
279+ if (isCacheObjectBearingField (field .asType ()))
280+ cacheObjFields .add (field );
281+ }
282+
283+ if (cacheObjFields .isEmpty ())
284+ return ;
285+
286+ imports .add ("org.apache.ignite.IgniteCheckedException" );
287+ imports .add ("org.apache.ignite.internal.processors.cache.CacheObjectValueContext" );
288+
289+ startCacheObjectMethod (prepareCacheObjects , true );
290+ startCacheObjectMethod (finishCacheObjects , false );
291+
292+ indent ++;
293+
294+ for (VariableElement field : cacheObjFields ) {
295+ emitCacheObjectCall (prepareCacheObjects , field , true );
296+ emitCacheObjectCall (finishCacheObjects , field , false );
297+ }
298+
299+ indent --;
300+
301+ prepareCacheObjects .add (identedLine ("}" ));
302+ finishCacheObjects .add (identedLine ("}" ));
303+ }
304+
305+ /**
306+ * @return {@code true} if the given field type is a {@code CacheObject}/{@code KeyCacheObject}, a collection of
307+ * them, or an array of them. Maps and nested messages are intentionally excluded — see IGNITE-28520 scope.
308+ */
309+ private boolean isCacheObjectBearingField (TypeMirror type ) {
310+ if (type .getKind () == TypeKind .ARRAY ) {
311+ TypeMirror comp = ((ArrayType )type ).getComponentType ();
312+
313+ return comp .getKind () == TypeKind .DECLARED && isCacheObjectType (comp );
314+ }
315+
316+ if (type .getKind () != TypeKind .DECLARED )
317+ return false ;
318+
319+ if (isCacheObjectType (type ))
320+ return true ;
321+
322+ // Map is intentionally skipped even though Map is Collection-unrelated — exclude it explicitly for clarity.
323+ if (assignableFrom (erasedType (type ), type (Map .class .getName ())))
324+ return false ;
325+
326+ if (assignableFrom (erasedType (type ), type (Collection .class .getName ()))) {
327+ List <? extends TypeMirror > typeArgs = ((DeclaredType )type ).getTypeArguments ();
328+
329+ if (typeArgs .size () == 1 ) {
330+ TypeMirror arg = typeArgs .get (0 );
331+
332+ return arg .getKind () == TypeKind .DECLARED && isCacheObjectType (arg );
333+ }
334+ }
335+
336+ return false ;
337+ }
338+
339+ /** @return {@code true} if {@code type} is assignable to {@code CacheObject} (this also covers {@code KeyCacheObject}). */
340+ private boolean isCacheObjectType (TypeMirror type ) {
341+ return assignableFrom (type , type ("org.apache.ignite.internal.processors.cache.CacheObject" ));
342+ }
343+
344+ /** Emits method signature and opening brace for cache-object marshalling methods. */
345+ private void startCacheObjectMethod (List <String > code , boolean prepare ) {
346+ indent = 1 ;
347+
348+ code .add (identedLine (METHOD_JAVADOC ));
349+
350+ if (prepare ) {
351+ code .add (identedLine (
352+ "@Override public void prepareMarshalCacheObjects(" + type .getSimpleName () +
353+ " msg, CacheObjectValueContext ctx) throws IgniteCheckedException {" ));
354+ }
355+ else {
356+ code .add (identedLine (
357+ "@Override public void finishUnmarshalCacheObjects(" + type .getSimpleName () +
358+ " msg, CacheObjectValueContext ctx, ClassLoader ldr) throws IgniteCheckedException {" ));
359+ }
360+ }
361+
362+ /**
363+ * Emits a single field traversal statement — a guarded {@code prepareMarshal} / {@code finishUnmarshal} call
364+ * for a direct {@code CacheObject} field, or a null-safe loop for a collection / array of them.
365+ */
366+ private void emitCacheObjectCall (List <String > code , VariableElement field , boolean prepare ) {
367+ String mtd = prepare ? "prepareMarshal(ctx)" : "finishUnmarshal(ctx, ldr)" ;
368+
369+ String accessor = fieldAccessor (field );
370+
371+ TypeMirror type = field .asType ();
372+
373+ if (type .getKind () == TypeKind .DECLARED && isCacheObjectType (type )) {
374+ code .add (identedLine ("if (%s != null)" , accessor ));
375+
376+ indent ++;
377+
378+ code .add (identedLine ("%s.%s;" , accessor , mtd ));
379+
380+ indent --;
381+ }
382+ else {
383+ // Collection or array of CacheObject — iterate with a null-check on both the container and the element.
384+ code .add (identedLine ("if (%s != null) {" , accessor ));
385+
386+ indent ++;
387+
388+ String elementType = "org.apache.ignite.internal.processors.cache.KeyCacheObject" ;
389+
390+ if (type .getKind () == TypeKind .ARRAY ) {
391+ TypeMirror comp = ((ArrayType )type ).getComponentType ();
392+
393+ if (!assignableFrom (comp , type (elementType )))
394+ elementType = "org.apache.ignite.internal.processors.cache.CacheObject" ;
395+ }
396+ else {
397+ TypeMirror arg = ((DeclaredType )type ).getTypeArguments ().get (0 );
398+
399+ if (!assignableFrom (arg , type (elementType )))
400+ elementType = "org.apache.ignite.internal.processors.cache.CacheObject" ;
401+ }
402+
403+ imports .add (elementType );
404+
405+ String simpleElement = elementType .substring (elementType .lastIndexOf ('.' ) + 1 );
406+
407+ code .add (identedLine ("for (%s obj : %s) {" , simpleElement , accessor ));
408+
409+ indent ++;
410+
411+ code .add (identedLine ("if (obj != null)" ));
412+
413+ indent ++;
414+
415+ code .add (identedLine ("obj.%s;" , mtd ));
416+
417+ indent --;
418+
419+ indent --;
420+
421+ code .add (identedLine ("}" ));
422+
423+ indent --;
424+
425+ code .add (identedLine ("}" ));
426+ }
427+ }
428+
429+ /** Returns the field access expression, taking superclass-field access into account. */
430+ private String fieldAccessor (VariableElement field ) {
431+ String name = field .getSimpleName ().toString ();
432+
433+ if (type .equals (field .getEnclosingElement ()))
434+ return "msg." + name ;
435+
436+ return "((" + field .getEnclosingElement ().getSimpleName () + ")msg)." + name ;
245437 }
246438
247439 /**
0 commit comments