4444import java .io .OutputStream ;
4545import java .io .OutputStreamWriter ;
4646import java .io .UnsupportedEncodingException ;
47+ import java .lang .reflect .Field ;
48+ import java .lang .reflect .Modifier ;
4749import java .lang .reflect .Type ;
4850import java .net .URL ;
4951import java .util .ArrayList ;
5052import java .util .Iterator ;
53+ import java .util .LinkedHashSet ;
54+ import java .util .List ;
55+ import java .util .Set ;
5156
5257/**
5358 * Streaming JSON message formatter for Axis2.
@@ -210,7 +215,13 @@ private void writeElementResponse(MessageContext outMsgCtxt, JsonWriter jsonWrit
210215 * Because the JsonWriter is backed by a {@link FlushingOutputStream},
211216 * the HTTP transport receives chunks as serialization progresses —
212217 * the full response is never buffered in a single String or byte[].</p>
218+ *
219+ * <p>When {@link JsonConstant#FIELD_FILTER} is set on the MessageContext,
220+ * only the requested fields are serialized via reflection. Supports
221+ * dot-notation for one level of nesting ({@code container.field}).
222+ * This matches the Moshi formatter's filtering behavior.</p>
213223 */
224+ @ SuppressWarnings ("unchecked" )
214225 private void writeObjectResponse (MessageContext outMsgCtxt , JsonWriter jsonWriter ,
215226 Object retObj ) throws AxisFault {
216227 try {
@@ -220,8 +231,15 @@ private void writeObjectResponse(MessageContext outMsgCtxt, JsonWriter jsonWrite
220231
221232 jsonWriter .beginObject ();
222233 jsonWriter .name (JsonConstant .RESPONSE );
223- Type returnType = (Type ) outMsgCtxt .getProperty (JsonConstant .RETURN_TYPE );
224- gson .toJson (retObj , returnType , jsonWriter );
234+
235+ Object filterProp = outMsgCtxt .getProperty (JsonConstant .FIELD_FILTER );
236+ if (filterProp instanceof Set && !((Set <?>) filterProp ).isEmpty ()) {
237+ writeFilteredObjectGson (jsonWriter , retObj , (Set <String >) filterProp , gson );
238+ } else {
239+ Type returnType = (Type ) outMsgCtxt .getProperty (JsonConstant .RETURN_TYPE );
240+ gson .toJson (retObj , returnType , jsonWriter );
241+ }
242+
225243 jsonWriter .endObject ();
226244
227245 } catch (IOException e ) {
@@ -231,6 +249,176 @@ private void writeObjectResponse(MessageContext outMsgCtxt, JsonWriter jsonWrite
231249 }
232250 }
233251
252+ /**
253+ * Serialize only the requested fields from the return object using GSON.
254+ * Supports dot-notation for one level of nesting ({@code container.field}).
255+ *
256+ * <p>Behavioral parity with
257+ * {@link MoshiStreamingMessageFormatter#writeFilteredObject} — same
258+ * two-phase approach (top-level prune, then nested prune), same
259+ * request-scoped field cache, same edge case handling.</p>
260+ */
261+ private void writeFilteredObjectGson (JsonWriter jsonWriter , Object retObj ,
262+ Set <String > allowedFields , Gson gson )
263+ throws IOException {
264+
265+ if (retObj == null ) {
266+ jsonWriter .nullValue ();
267+ return ;
268+ }
269+
270+ // Parse dot-notation into top-level keeps + nested specs
271+ Set <String > topLevel = new LinkedHashSet <>();
272+ java .util .Map <String , Set <String >> nestedSpecs = new java .util .LinkedHashMap <>();
273+
274+ for (String fieldSpec : allowedFields ) {
275+ int dot = fieldSpec .indexOf ('.' );
276+ if (dot > 0 && dot < fieldSpec .length () - 1 ) {
277+ String container = fieldSpec .substring (0 , dot );
278+ String subField = fieldSpec .substring (dot + 1 );
279+ topLevel .add (container );
280+ nestedSpecs .computeIfAbsent (container , k -> new LinkedHashSet <>())
281+ .add (subField );
282+ } else {
283+ topLevel .add (fieldSpec );
284+ }
285+ }
286+
287+ // Request-scoped cache for reflected field lists
288+ java .util .Map <Class <?>, List <Field >> fieldCache = new java .util .HashMap <>();
289+
290+ List <Field > allFields = fieldCache .computeIfAbsent (
291+ retObj .getClass (), JSONStreamingMessageFormatter ::getAllFields );
292+ boolean prevSerializeNulls = jsonWriter .getSerializeNulls ();
293+ jsonWriter .setSerializeNulls (true );
294+ try {
295+ jsonWriter .beginObject ();
296+
297+ for (Field field : allFields ) {
298+ if (!topLevel .contains (field .getName ())) {
299+ continue ;
300+ }
301+
302+ Object value ;
303+ try {
304+ field .setAccessible (true );
305+ value = field .get (retObj );
306+ } catch (IllegalAccessException | SecurityException e ) {
307+ log .warn ("Cannot access field "
308+ + field .getName ().replaceAll ("[\r \n ]" , "_" )
309+ + " for field filtering; skipping" , e );
310+ continue ;
311+ }
312+
313+ jsonWriter .name (field .getName ());
314+
315+ Set <String > subFields = nestedSpecs .get (field .getName ());
316+ if (subFields != null && value != null ) {
317+ writeFilteredNestedGson (jsonWriter , value , subFields , gson , fieldCache );
318+ } else if (value == null ) {
319+ jsonWriter .nullValue ();
320+ } else {
321+ gson .toJson (value , field .getGenericType (), jsonWriter );
322+ }
323+ }
324+
325+ jsonWriter .endObject ();
326+ } finally {
327+ jsonWriter .setSerializeNulls (prevSerializeNulls );
328+ }
329+ }
330+
331+ /**
332+ * Serialize a nested field (Collection, Map, or single POJO) with only
333+ * the specified sub-fields. GSON equivalent of the Moshi
334+ * {@code writeFilteredNested} method.
335+ */
336+ private void writeFilteredNestedGson (JsonWriter jsonWriter , Object value ,
337+ Set <String > subFields , Gson gson ,
338+ java .util .Map <Class <?>, List <Field >> fieldCache )
339+ throws IOException {
340+
341+ if (value instanceof java .util .Collection ) {
342+ jsonWriter .beginArray ();
343+ for (Object element : (java .util .Collection <?>) value ) {
344+ if (element == null ) {
345+ jsonWriter .nullValue ();
346+ } else {
347+ writeFilteredSingleObjectGson (jsonWriter , element , subFields ,
348+ gson , fieldCache );
349+ }
350+ }
351+ jsonWriter .endArray ();
352+ } else if (value instanceof java .util .Map ) {
353+ jsonWriter .beginObject ();
354+ for (java .util .Map .Entry <?, ?> entry : ((java .util .Map <?, ?>) value ).entrySet ()) {
355+ String key = String .valueOf (entry .getKey ());
356+ if (subFields .contains (key )) {
357+ jsonWriter .name (key );
358+ gson .toJson (entry .getValue (), Object .class , jsonWriter );
359+ }
360+ }
361+ jsonWriter .endObject ();
362+ } else if (value .getClass ().getName ().startsWith ("java.lang." )) {
363+ gson .toJson (value , value .getClass (), jsonWriter );
364+ } else {
365+ writeFilteredSingleObjectGson (jsonWriter , value , subFields , gson , fieldCache );
366+ }
367+ }
368+
369+ /**
370+ * Serialize a single object with only the specified fields using GSON.
371+ * Inner loop of nested filtering — called once per collection element.
372+ */
373+ private void writeFilteredSingleObjectGson (JsonWriter jsonWriter , Object obj ,
374+ Set <String > allowedFields , Gson gson ,
375+ java .util .Map <Class <?>, List <Field >> fieldCache )
376+ throws IOException {
377+
378+ List <Field > fields = fieldCache .computeIfAbsent (
379+ obj .getClass (), JSONStreamingMessageFormatter ::getAllFields );
380+ jsonWriter .beginObject ();
381+ for (Field field : fields ) {
382+ if (!allowedFields .contains (field .getName ())) {
383+ continue ;
384+ }
385+ Object value ;
386+ try {
387+ field .setAccessible (true );
388+ value = field .get (obj );
389+ } catch (IllegalAccessException | SecurityException e ) {
390+ log .warn ("Cannot access field "
391+ + field .getDeclaringClass ().getName ().replaceAll ("[\r \n ]" , "_" )
392+ + "." + field .getName ().replaceAll ("[\r \n ]" , "_" )
393+ + " for nested field filtering; skipping" , e );
394+ continue ;
395+ }
396+ jsonWriter .name (field .getName ());
397+ if (value == null ) {
398+ jsonWriter .nullValue ();
399+ } else {
400+ gson .toJson (value , field .getGenericType (), jsonWriter );
401+ }
402+ }
403+ jsonWriter .endObject ();
404+ }
405+
406+ /**
407+ * Collect all non-static, non-transient fields from the class hierarchy.
408+ */
409+ private static List <Field > getAllFields (Class <?> clazz ) {
410+ List <Field > result = new ArrayList <>();
411+ for (Class <?> c = clazz ; c != null && c != Object .class ; c = c .getSuperclass ()) {
412+ for (Field field : c .getDeclaredFields ()) {
413+ int mods = field .getModifiers ();
414+ if (!Modifier .isStatic (mods ) && !Modifier .isTransient (mods )) {
415+ result .add (field );
416+ }
417+ }
418+ }
419+ return result ;
420+ }
421+
234422 /**
235423 * Read the flush interval from the service's configuration.
236424 * Falls back to {@link FlushingOutputStream#DEFAULT_FLUSH_INTERVAL}.
0 commit comments