This is an automated email from the ASF dual-hosted git repository.

robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-java-core.git

commit 1057c868a5ea6fd9335079ce0067df58815caeca
Author: Robert Lazarski <[email protected]>
AuthorDate: Sun Apr 19 18:46:18 2026 -1000

    AXIS2-6103 Add FIELD_FILTER constant and MoshiStreamingMessageFormatter 
updates
    
    Add JsonConstant.FIELD_FILTER used by FieldFilteringMessageFormatter
    for field-level response filtering. This constant was missing from the
    prior commit that introduced FieldFilteringMessageFormatter, causing
    GHA build failure (cannot find symbol).
    
    Also includes MoshiStreamingMessageFormatter updates for field selection
    support.
---
 .../apache/axis2/json/factory/JsonConstant.java    |   8 ++
 .../streaming/MoshiStreamingMessageFormatter.java  | 121 ++++++++++++++++++++-
 2 files changed, 128 insertions(+), 1 deletion(-)

diff --git a/modules/json/src/org/apache/axis2/json/factory/JsonConstant.java 
b/modules/json/src/org/apache/axis2/json/factory/JsonConstant.java
index f4ad434861..7248ea233b 100644
--- a/modules/json/src/org/apache/axis2/json/factory/JsonConstant.java
+++ b/modules/json/src/org/apache/axis2/json/factory/JsonConstant.java
@@ -28,6 +28,14 @@ public class JsonConstant {
     public static final String RETURN_OBJECT = "returnObject";
     public static final String RETURN_TYPE = "returnType";
 
+    /**
+     * Property name for field-level filtering. When set on the
+     * MessageContext (or OperationContext), the value is a
+     * {@code Set<String>} of field names to include in the response.
+     * Fields not in the set are omitted during serialization.
+     */
+    public static final String FIELD_FILTER = "jsonFieldFilter";
+
     public static final String IS_JSON_STREAM = "isJsonStream";
 
     public static final String GSON_XML_STREAM_READER = "GsonXMLStreamReader";
diff --git 
a/modules/json/src/org/apache/axis2/json/streaming/MoshiStreamingMessageFormatter.java
 
b/modules/json/src/org/apache/axis2/json/streaming/MoshiStreamingMessageFormatter.java
index 2a99e31c20..ce3743d816 100644
--- 
a/modules/json/src/org/apache/axis2/json/streaming/MoshiStreamingMessageFormatter.java
+++ 
b/modules/json/src/org/apache/axis2/json/streaming/MoshiStreamingMessageFormatter.java
@@ -46,11 +46,15 @@ import javax.xml.namespace.QName;
 import javax.xml.stream.XMLStreamException;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
 import java.lang.reflect.Type;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
 
 /**
  * Streaming Moshi JSON message formatter for Axis2.
@@ -182,13 +186,29 @@ public class MoshiStreamingMessageFormatter implements 
MessageFormatter {
      * The JsonWriter is backed by an Okio sink wrapping a
      * {@link FlushingOutputStream}, so the HTTP transport receives chunks
      * as serialization progresses.</p>
+     *
+     * <p>When {@link JsonConstant#FIELD_FILTER} is set on the MessageContext,
+     * only the requested top-level fields of the return object are serialized.
+     * This is done via reflection-based selective serialization — each field
+     * is checked against the filter set before being written to the 
JsonWriter.
+     * The streaming pipeline is never broken: non-selected fields are simply
+     * never written, so no capture buffer is needed.</p>
      */
+    @SuppressWarnings("unchecked")
     private void writeObjectResponse(JsonWriter jsonWriter, 
JsonAdapter<Object> adapter,
                                      Object retObj, MessageContext outMsgCtxt) 
throws AxisFault {
         try {
             jsonWriter.beginObject();
             jsonWriter.name(JsonConstant.RESPONSE);
-            adapter.toJson(jsonWriter, retObj);
+
+            Object filterProp = 
outMsgCtxt.getProperty(JsonConstant.FIELD_FILTER);
+            if (filterProp instanceof Set && !((Set<?>) filterProp).isEmpty()) 
{
+                writeFilteredObject(jsonWriter, retObj, (Set<String>) 
filterProp,
+                    adapter);
+            } else {
+                adapter.toJson(jsonWriter, retObj);
+            }
+
             jsonWriter.endObject();
 
         } catch (IOException e) {
@@ -198,6 +218,105 @@ public class MoshiStreamingMessageFormatter implements 
MessageFormatter {
         }
     }
 
+    /**
+     * Serialize only the fields in {@code allowedFields} from the return
+     * object, directly to the JsonWriter. No intermediate buffer.
+     *
+     * <p>Uses Java reflection to iterate over the object's declared fields.
+     * For each field whose name is in the allowed set, the field value is
+     * serialized via the Moshi adapter for that field's type. Fields not in
+     * the set are silently skipped — they are never serialized, never
+     * buffered, never written to the wire.</p>
+     *
+     * <p>This keeps the streaming pipeline intact: JsonWriter → Okio sink →
+     * FlushingOutputStream → transport. Each allowed field's value flows
+     * through to the client as soon as serialization produces enough bytes
+     * to trigger a flush.</p>
+     */
+    /** Shared Moshi instance for field-level serialization (thread-safe, 
reusable). */
+    private static final Moshi FIELD_FILTER_MOSHI = new Moshi.Builder()
+        .add(String.class, new JsonHtmlEncoder())
+        .add(Date.class, new Rfc3339DateJsonAdapter())
+        .build();
+
+    private void writeFilteredObject(JsonWriter jsonWriter, Object retObj,
+                                     Set<String> allowedFields,
+                                     JsonAdapter<Object> fallbackAdapter)
+            throws IOException {
+
+        if (retObj == null) {
+            jsonWriter.nullValue();
+            return;
+        }
+
+        List<Field> allFields = getAllFields(retObj.getClass());
+        boolean prevSerializeNulls = jsonWriter.getSerializeNulls();
+        jsonWriter.setSerializeNulls(true);
+        try {
+            jsonWriter.beginObject();
+
+            for (Field field : allFields) {
+                if (!allowedFields.contains(field.getName())) {
+                    continue;
+                }
+
+                field.setAccessible(true);
+                Object value;
+                try {
+                    value = field.get(retObj);
+                } catch (IllegalAccessException e) {
+                    log.warn("Cannot access field " + field.getName()
+                        + " for field filtering; skipping", e);
+                    continue;
+                }
+
+                jsonWriter.name(field.getName());
+                if (value == null) {
+                    jsonWriter.nullValue();
+                } else {
+                    @SuppressWarnings("unchecked")
+                    JsonAdapter<Object> fieldAdapter =
+                        (JsonAdapter<Object>) FIELD_FILTER_MOSHI.adapter(
+                            field.getGenericType());
+                    fieldAdapter.toJson(jsonWriter, value);
+                }
+            }
+
+            jsonWriter.endObject();
+        } finally {
+            jsonWriter.setSerializeNulls(prevSerializeNulls);
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("writeFilteredObject: serialized fields from "
+                + allowedFields + " (streaming, no buffer)");
+        }
+    }
+
+    /**
+     * Collect all non-static, non-transient fields from the class hierarchy.
+     * Walks from the concrete class up through all superclasses to (but not
+     * including) Object. This ensures inherited fields are included when
+     * a response object extends a base class.
+     *
+     * <p>Note: this method reflects over the class on each call. For extreme
+     * performance needs, the result could be cached in a
+     * {@code ConcurrentHashMap<Class<?>, List<Field>>}. The current approach
+     * avoids cache-related complexity with dynamic classloaders.</p>
+     */
+    private static List<Field> getAllFields(Class<?> clazz) {
+        List<Field> result = new ArrayList<>();
+        for (Class<?> c = clazz; c != null && c != Object.class; c = 
c.getSuperclass()) {
+            for (Field field : c.getDeclaredFields()) {
+                int mods = field.getModifiers();
+                if (!Modifier.isStatic(mods) && !Modifier.isTransient(mods)) {
+                    result.add(field);
+                }
+            }
+        }
+        return result;
+    }
+
     /**
      * Read the flush interval from the service's configuration.
      */

Reply via email to