Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
package com.netgrif.application.engine.configuration.web.config;

import com.netgrif.application.engine.configuration.web.filter.TrailingSlashRedirectFilter;
import com.netgrif.application.engine.serialization.FieldSelectorInterceptor;
import jakarta.servlet.Filter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig {
public class WebConfig implements WebMvcConfigurer {

private final FieldSelectorInterceptor interceptor;

public WebConfig(FieldSelectorInterceptor interceptor) {
this.interceptor = interceptor;
}

@Bean
public Filter trailingSlashRedirectFilter() {
Expand All @@ -21,4 +30,9 @@ public FilterRegistrationBean<Filter> trailingSlashFilter() {
registrationBean.addUrlPatterns("/*");
return registrationBean;
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.netgrif.application.engine.serialization;

import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
Comment on lines +3 to +4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

🧩 Analysis chain

🌐 Web query:

tools.jackson.databind SerializationContext introspectBeanDescription Jackson 3 API

💡 Result:

In the Jackson 3 API, the introspectBeanDescription method is defined within the SerializationContext class [1]. However, it is explicitly intended for internal framework use rather than as a public API for general application code [2]. Jackson maintainers have clarified that the introspection mechanism used by internal serializers is not designed to be exposed for external use [2]. While the method may appear in the SerializationContext documentation, it is typically considered restricted to testing or internal processing [2]. For tasks requiring external introspection of types or bean properties, the recommended public API approach is to use ObjectMapper.acceptJsonFormatVisitor() [2]. This method is the officially supported way to inspect or interact with the serialization handling logic of a type [2]. For more advanced requirements, SerializationContextExt is available as an extension of SerializationContext that exposes additional methods needed specifically by ObjectMapper and ObjectWriter, which are further decoupled from the standard SerializationContext API to prevent accidental misuse [3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="application-engine/src/main/java/com/netgrif/application/engine/serialization/DynamicFieldSerializer.java"

echo "== file exists =="
test -f "$file" && echo yes

echo
echo "== line-numbered excerpt =="
cat -n "$file" | sed -n '1,220p'

echo
echo "== dependency/version hints =="
rg -n --hidden --glob '!**/target/**' --glob '!**/build/**' \
  'tools\.jackson|com\.fasterxml\.jackson|jackson-databind|jackson-core' \
  pom.xml **/pom.xml build.gradle* settings.gradle* gradle.properties 2>/dev/null || true

Repository: netgrif/application-engine

Length of output: 7136


Update the Javadocs to Jackson 3. JsonSerializer/SerializerProvider are only referenced in doc comments, and serializeWithSelector(...) still documents the old SerializerProvider signature. Switch those references to ValueSerializer/SerializationContext and drop the Jackson 2 imports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@application-engine/src/main/java/com/netgrif/application/engine/serialization/DynamicFieldSerializer.java`
around lines 3 - 4, The Javadocs in DynamicFieldSerializer are still using
Jackson 2 terminology, and the Jackson 2 imports are no longer needed since
those types are only mentioned in comments. Update the doc references in
serializeWithSelector(...) and related Javadocs to use ValueSerializer and
SerializationContext instead of JsonSerializer and SerializerProvider, and
remove the unused Jackson 2 imports from the class.

import tools.jackson.core.JsonGenerator;
import tools.jackson.databind.BeanDescription;
import tools.jackson.databind.JavaType;
import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.ValueSerializer;
import tools.jackson.databind.introspect.BeanPropertyDefinition;

import java.io.IOException;

/**
* Custom JSON serializer that provides selective field serialization based on a {@link FieldSelector} configuration.
* <p>
* This serializer extends {@link JsonSerializer} to enable dynamic control over which fields of an object
* are included in the JSON output. It uses a {@link FieldSelectorHolder} to access the field selector
* configuration and applies it during serialization to filter object properties.
* </p>
* <p>
* The serializer supports:
* <ul>
* <li>Selective field inclusion based on field names</li>
* <li>Nested field selection for complex object hierarchies</li>
* <li>Fallback to default serialization when all fields are included</li>
* <li>Null-safe serialization</li>
* </ul>
* </p>
* <p>
* Example usage:
* <pre>
* FieldSelector selector = new FieldSelector();
* selector.include("name");
* selector.include("address.city");
*
* FieldSelectorHolder holder = new FieldSelectorHolder(selector);
* DynamicFieldSerializer serializer = new DynamicFieldSerializer(holder);
* </pre>
* </p>
*
* @see JsonSerializer
* @see FieldSelector
* @see FieldSelectorHolder
*/
public class DynamicFieldSerializer extends ValueSerializer<Object> {

/**
* Holder containing the field selector used to determine which fields should be serialized.
*/
private final FieldSelectorHolder holder;

/**
* Constructs a new DynamicFieldSerializer with the specified field selector holder.
*
* @param holder the field selector holder that provides the selector for filtering fields during serialization
*/
public DynamicFieldSerializer(FieldSelectorHolder holder) {
this.holder = holder;
}

/**
* Serializes the given object value to JSON using the field selector from the holder.
* This method delegates to {@link #serializeWithSelector(Object, JsonGenerator, SerializerProvider, FieldSelector)}
* with the selector obtained from the holder.
*
* @param value the object to serialize
* @param gen the JSON generator used for writing JSON content
* @param provider the serializer provider for accessing serializers
* @throws IOException if an I/O error occurs during serialization
*/
@Override
public void serialize(Object value, JsonGenerator gen, SerializationContext context) {
serializeWithSelector(value, gen, context, holder.getSelector());
}

/**
* Serializes the given object value to JSON based on the provided field selector.
* <p>
* This method performs selective serialization by including only the fields specified in the selector.
* If the selector includes all fields, the default serialization is performed. Otherwise, it introspects
* the object's bean properties and serializes only those included in the selector. Nested field selectors
* are applied recursively for complex object structures.
* </p>
*
* @param value the object to serialize, it may be null
* @param gen the JSON generator used for writing JSON content
* @param provider the serializer provider for accessing serializers and configuration
* @param selector the field selector that determines which fields to include in serialization
* @throws IOException if an I/O error occurs during serialization
*/
public void serializeWithSelector(Object value, JsonGenerator gen, SerializationContext context, FieldSelector selector) {

if (value == null) {
gen.writeNull();
return;
}

if (selector.includeAll()) {
context.findValueSerializer(value.getClass()).serialize(value, gen, context);
return;
}

JavaType type = context.constructType(value.getClass());
BeanDescription desc = context.introspectBeanDescription(type);

gen.writeStartObject();
for (BeanPropertyDefinition prop : desc.findProperties()) {

String name = prop.getName();
if (!selector.includes(name)) {
continue;
}

Object propValue = prop.getAccessor().getValue(value);
gen.writeName(name);

FieldSelector nested = selector.nested(name);
if (nested.includeAll()) {
context.findValueSerializer(propValue.getClass()).serialize(propValue, gen, context);
} else {
serializeWithSelector(propValue, gen, context, nested);
}
}
Comment on lines +108 to +124

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

NPE when an included property value is null.

propValue can be null. In the nested.includeAll() branch (Line 120) you call propValue.getClass(), which throws a NullPointerException. This is reachable for any included leaf field whose value is null (the common case, since leaf fields have no nested selector and thus take the includeAll() path). The recursive else branch handles null via the top-of-method guard, but the includeAll branch does not.

🐛 Proposed fix to handle null property values
             Object propValue = prop.getAccessor().getValue(value);
             gen.writeName(name);
 
+            if (propValue == null) {
+                gen.writeNull();
+                continue;
+            }
+
             FieldSelector nested = selector.nested(name);
             if (nested.includeAll()) {
                 context.findValueSerializer(propValue.getClass()).serialize(propValue, gen, context);
             } else {
                 serializeWithSelector(propValue, gen, context, nested);
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (BeanPropertyDefinition prop : desc.findProperties()) {
String name = prop.getName();
if (!selector.includes(name)) {
continue;
}
Object propValue = prop.getAccessor().getValue(value);
gen.writeName(name);
FieldSelector nested = selector.nested(name);
if (nested.includeAll()) {
context.findValueSerializer(propValue.getClass()).serialize(propValue, gen, context);
} else {
serializeWithSelector(propValue, gen, context, nested);
}
}
for (BeanPropertyDefinition prop : desc.findProperties()) {
String name = prop.getName();
if (!selector.includes(name)) {
continue;
}
Object propValue = prop.getAccessor().getValue(value);
gen.writeName(name);
if (propValue == null) {
gen.writeNull();
continue;
}
FieldSelector nested = selector.nested(name);
if (nested.includeAll()) {
context.findValueSerializer(propValue.getClass()).serialize(propValue, gen, context);
} else {
serializeWithSelector(propValue, gen, context, nested);
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@application-engine/src/main/java/com/netgrif/application/engine/serialization/DynamicFieldSerializer.java`
around lines 108 - 124, The DynamicFieldSerializer#serializeWithSelector loop
can throw a NullPointerException when an included property value is null because
the nested.includeAll() branch calls propValue.getClass() without a null check.
Update the handling around prop.getAccessor().getValue(value) so the includeAll
path serializes null values safely, reusing the method’s existing null handling
logic before calling context.findValueSerializer, and keep the fix localized to
serializeWithSelector and its nested.includeAll() branch.

gen.writeEndObject();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package com.netgrif.application.engine.serialization;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
* A utility class for parsing and managing field selection specifications in JSON serialization.
* <p>
* This class enables selective field inclusion during serialization by parsing a field specification
* string that supports both simple field names and nested field selections using parentheses notation.
* </p>
* <p>
* Field specification format:
* <ul>
* <li>Simple fields: {@code "field1,field2,field3"}</li>
* <li>Nested fields: {@code "field1,field2(nestedField1,nestedField2),field3"}</li>
* <li>Deep nesting: {@code "field1(nested1(deepNested1,deepNested2))"}</li>
* </ul>
* </p>
* <p>
* When no specification is provided (null or blank), the selector allows all fields (unrestricted mode).
* </p>
*
* @see LocalizedEventOutcomeSerializer
* @see DynamicFieldSerializer
* @see FieldSelectorHolder
*/

public final class FieldSelector {
/**
* Set of field names to include at the current level of serialization.
* When null, all fields are included (unrestricted mode).
*/
private final Set<String> fields;

/**
* Map of field names to their corresponding nested {@link FieldSelector} instances.
* Enables hierarchical field selection for complex object structures.
* When null, no nested field restrictions are applied.
*/
private final Map<String, FieldSelector> nested;

/**
* Creates a new FieldSelector in unrestricted mode.
* <p>
* This constructor creates a selector that allows all fields to be included
* during serialization without any restrictions.
* </p>
*/
private FieldSelector() {
this(null, null);
}

/**
* Creates a new FieldSelector with specified field selections and nested selectors.
*
* @param fields the set of field names to include at this level, or null for unrestricted mode
* @param nested the map of field names to their nested selectors, or null if no nested selections
*/
private FieldSelector(Set<String> fields, Map<String, FieldSelector> nested) {
this.fields = fields;
this.nested = nested;
}

/**
* Parses a field specification string and creates a corresponding FieldSelector.
* <p>
* The parser supports:
* <ul>
* <li>Comma-separated field names: {@code "field1,field2,field3"}</li>
* <li>Nested field selections using parentheses: {@code "field1(nested1,nested2)"}</li>
* <li>Multiple levels of nesting: {@code "field1(nested1(deepNested1))"}</li>
* </ul>
* </p>
* <p>
* If the specification is null or blank, an unrestricted selector is returned that allows all fields.
* </p>
*
* @param spec the field specification string to parse, may be null or blank
* @return a FieldSelector instance representing the parsed specification
* @throws IllegalArgumentException if the specification contains unmatched parentheses
*/
public static FieldSelector parse(String spec) {
if (spec == null || spec.isBlank()) {
return new FieldSelector(null, null);
}

Set<String> fields = new HashSet<>();
Map<String, FieldSelector> nested = new HashMap<>();

int i = 0;
while (i < spec.length()) {
int commaIdx = nextCommaOrEnd(spec, i);
int parenthesisIdx = spec.indexOf('(', i);

if (parenthesisIdx != -1 && parenthesisIdx < commaIdx) {
String name = spec.substring(i, parenthesisIdx).trim();
int closeParenthesisIdx = findMatchingParen(spec, parenthesisIdx);
String inner = spec.substring(parenthesisIdx + 1, closeParenthesisIdx);
fields.add(name);
nested.put(name, parse(inner));
i = closeParenthesisIdx + 1;
if (i < spec.length() && spec.charAt(i) == ',') i++;
} else {
String name = spec.substring(i, commaIdx).trim();
if (!name.isEmpty()) fields.add(name);
i = commaIdx + 1;
}
}
return new FieldSelector(fields, nested);
}
Comment on lines +85 to +113

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Unbounded recursion in parse() allows StackOverflowError from user-controlled input.

The fields query parameter is user-controlled and parse() recurses without a depth limit. A crafted specification with thousands of nesting levels (e.g., a(a(a(a(...))))) can exhaust the call stack and crash the request-handling thread. With typical Tomcat URL limits (~8KB) and Java's default stack size (~512KB), this is achievable in a single GET request.

🔒 Proposed fix: add a depth guard
 public static FieldSelector parse(String spec) {
+    return parse(spec, 0);
+}
+
+private static final int MAX_DEPTH = 32;
+
+private static FieldSelector parse(String spec, int depth) {
+    if (depth >= MAX_DEPTH) {
+        throw new IllegalArgumentException("Field selector nesting exceeds maximum depth of " + MAX_DEPTH);
+    }
     if (spec == null || spec.isBlank()) {
         return new FieldSelector(null, null);
     }
 
     Set<String> fields = new HashSet<>();
     Map<String, FieldSelector> nested = new HashMap<>();
 
     int i = 0;
     while (i < spec.length()) {
         int commaIdx = nextCommaOrEnd(spec, i);
         int parenthesisIdx = spec.indexOf('(', i);
 
         if (parenthesisIdx != -1 && parenthesisIdx < commaIdx) {
             String name = spec.substring(i, parenthesisIdx).trim();
             int closeParenthesisIdx = findMatchingParen(spec, parenthesisIdx);
             String inner = spec.substring(parenthesisIdx + 1, closeParenthesisIdx);
             fields.add(name);
-            nested.put(name, parse(inner));
+            nested.put(name, parse(inner, depth + 1));
             i = closeParenthesisIdx + 1;
             if (i < spec.length() && spec.charAt(i) == ',') i++;
         } else {
             String name = spec.substring(i, commaIdx).trim();
             if (!name.isEmpty()) fields.add(name);
             i = commaIdx + 1;
         }
     }
     return new FieldSelector(fields, nested);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static FieldSelector parse(String spec) {
if (spec == null || spec.isBlank()) {
return new FieldSelector(null, null);
}
Set<String> fields = new HashSet<>();
Map<String, FieldSelector> nested = new HashMap<>();
int i = 0;
while (i < spec.length()) {
int commaIdx = nextCommaOrEnd(spec, i);
int parenthesisIdx = spec.indexOf('(', i);
if (parenthesisIdx != -1 && parenthesisIdx < commaIdx) {
String name = spec.substring(i, parenthesisIdx).trim();
int closeParenthesisIdx = findMatchingParen(spec, parenthesisIdx);
String inner = spec.substring(parenthesisIdx + 1, closeParenthesisIdx);
fields.add(name);
nested.put(name, parse(inner));
i = closeParenthesisIdx + 1;
if (i < spec.length() && spec.charAt(i) == ',') i++;
} else {
String name = spec.substring(i, commaIdx).trim();
if (!name.isEmpty()) fields.add(name);
i = commaIdx + 1;
}
}
return new FieldSelector(fields, nested);
}
public static FieldSelector parse(String spec) {
return parse(spec, 0);
}
private static final int MAX_DEPTH = 32;
private static FieldSelector parse(String spec, int depth) {
if (depth >= MAX_DEPTH) {
throw new IllegalArgumentException("Field selector nesting exceeds maximum depth of " + MAX_DEPTH);
}
if (spec == null || spec.isBlank()) {
return new FieldSelector(null, null);
}
Set<String> fields = new HashSet<>();
Map<String, FieldSelector> nested = new HashMap<>();
int i = 0;
while (i < spec.length()) {
int commaIdx = nextCommaOrEnd(spec, i);
int parenthesisIdx = spec.indexOf('(', i);
if (parenthesisIdx != -1 && parenthesisIdx < commaIdx) {
String name = spec.substring(i, parenthesisIdx).trim();
int closeParenthesisIdx = findMatchingParen(spec, parenthesisIdx);
String inner = spec.substring(parenthesisIdx + 1, closeParenthesisIdx);
fields.add(name);
nested.put(name, parse(inner, depth + 1));
i = closeParenthesisIdx + 1;
if (i < spec.length() && spec.charAt(i) == ',') i++;
} else {
String name = spec.substring(i, commaIdx).trim();
if (!name.isEmpty()) fields.add(name);
i = commaIdx + 1;
}
}
return new FieldSelector(fields, nested);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@application-engine/src/main/java/com/netgrif/application/engine/serialization/FieldSelector.java`
around lines 85 - 113, `FieldSelector.parse()` is recursively parsing nested
field specs without any depth limit, so a crafted `fields` value can trigger
`StackOverflowError`. Add a recursion-depth guard to `parse()` (and its nested
`parse(inner)` call path), using a bounded counter or maximum nesting constant,
and reject overly deep specs before recursing further. Use the existing `parse`,
`findMatchingParen`, and `nextCommaOrEnd` flow to keep the change localized.


/**
* Checks whether the specified field should be included in serialization.
* <p>
* A field is included if:
* <ul>
* <li>The selector is in unrestricted mode (fields is null), or</li>
* <li>The field is explicitly listed in the fields set, or</li>
* <li>The field has a nested selector defined</li>
* </ul>
* </p>
*
* @param field the name of the field to check
* @return true if the field should be included, false otherwise
*/
public boolean includes(String field) {
if (fields == null) return true;
if (fields.contains(field)) return true;
return nested != null && nested.containsKey(field);
}

/**
* Checks whether this selector is in unrestricted mode, allowing all fields.
* <p>
* Unrestricted mode occurs when no field specification was provided during parsing
* (i.e., the specification was null or blank).
* </p>
*
* @return true if all fields should be included without restrictions, false otherwise
*/
public boolean includeAll() {
return fields == null;
}

/**
* Retrieves the nested FieldSelector for the specified field.
* <p>
* If no nested selector is defined for the field, returns an unrestricted selector
* that allows all fields at the nested level.
* </p>
*
* @param field the name of the field whose nested selector to retrieve
* @return the nested FieldSelector for the field, or an unrestricted selector if none exists
*/
public FieldSelector nested(String field) {
if (nested == null || !nested.containsKey(field)) {
return new FieldSelector(null, null); // no restriction on nested level either
}
return nested.get(field);
}

/**
* Finds the index of the next comma at the current nesting depth, or the end of the string.
* <p>
* This method tracks parenthesis depth to avoid splitting on commas that are inside
* nested field specifications. Only commas at depth 0 (not inside any parentheses)
* are considered as field separators.
* </p>
*
* @param spec the specification string to search
* @param from the starting index for the search
* @return the index of the next comma at depth 0, or the length of the string if none found
*/
private static int nextCommaOrEnd(String spec, int from) {
int depth = 0;
for (int i = from; i < spec.length(); i++) {
char c = spec.charAt(i);
if (c == '(') depth++;
else if (c == ')') depth--;
else if (c == ',' && depth == 0) return i;
}
return spec.length();
}

/**
* Finds the index of the closing parenthesis that matches the opening parenthesis at the specified position.
* <p>
* This method correctly handles nested parentheses by tracking the depth level and returning
* the index of the closing parenthesis that brings the depth back to 0.
* </p>
*
* @param spec the specification string to search
* @param openIdx the index of the opening parenthesis
* @return the index of the matching closing parenthesis
* @throws IllegalArgumentException if no matching closing parenthesis is found
*/
private static int findMatchingParen(String spec, int openIdx) {
int depth = 0;
for (int i = openIdx; i < spec.length(); i++) {
char c = spec.charAt(i);
if (c == '(') depth++;
else if (c == ')') {
depth--;
if (depth == 0) return i;
}
}
throw new IllegalArgumentException("Unmatched '(' in fields selector: " + spec);
}
}
Loading
Loading