You are viewing a plain text version of this content. The canonical link for it is here.
Posted to java-commits@axis.apache.org by ro...@apache.org on 2026/04/20 04:47:54 UTC
(axis-axis2-java-core) 01/02: AXIS2-6103 Add FIELD_FILTER constant and MoshiStreamingMessageFormatter updates
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 <ro...@gmail.com>
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.
*/