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

lukaszlenart pushed a commit to branch WW-5626-approach-c
in repository https://gitbox.apache.org/repos/asf/struts.git

commit 0166c6a0b5356f9c5ded8a985d5dd75a7fc29018
Author: Lukasz Lenart <[email protected]>
AuthorDate: Mon May 4 16:06:28 2026 +0200

    WW-5626 remove Jackson auth spike; replaced by production tests
---
 .../struts2/rest/spike/JacksonAuthSpikeTest.java   | 257 ---------------------
 1 file changed, 257 deletions(-)

diff --git 
a/plugins/rest/src/test/java/org/apache/struts2/rest/spike/JacksonAuthSpikeTest.java
 
b/plugins/rest/src/test/java/org/apache/struts2/rest/spike/JacksonAuthSpikeTest.java
deleted file mode 100644
index 204f87bbb..000000000
--- 
a/plugins/rest/src/test/java/org/apache/struts2/rest/spike/JacksonAuthSpikeTest.java
+++ /dev/null
@@ -1,257 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *  http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.struts2.rest.spike;
-
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.databind.BeanDescription;
-import com.fasterxml.jackson.databind.DeserializationConfig;
-import com.fasterxml.jackson.databind.DeserializationContext;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder;
-import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
-import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
-import com.fasterxml.jackson.databind.module.SimpleModule;
-import junit.framework.TestCase;
-
-import java.util.ArrayDeque;
-import java.util.Deque;
-import java.util.Iterator;
-import java.util.function.BiPredicate;
-
-/**
- * SPIKE — NOT PRODUCTION CODE.
- *
- * Validates that Jackson's BeanDeserializerModifier + SettableBeanProperty 
wrapping can
- * intercept per-property deserialization for Approach C (handler-level 
@StrutsParameter
- * authorization). Path tracking uses a ThreadLocal Deque pushed/popped around 
each
- * authorized property's deserialization.
- *
- * Three claims under test:
- *   (1) updateBuilder() can replace SettableBeanProperty instances on the 
builder
- *   (2) A wrapping property can call parser.skipChildren() to discard 
unauthorized values
- *   (3) Path tracking via ThreadLocal Deque produces dot/bracket paths 
matching
- *       ParametersInterceptor depth semantics for nested objects
- *
- * If green, this approach is viable for production.
- */
-public class JacksonAuthSpikeTest extends TestCase {
-
-    // --- ThreadLocal path tracking ---
-
-    private static final ThreadLocal<Deque<String>> PATH_STACK = 
ThreadLocal.withInitial(ArrayDeque::new);
-
-    private static String currentPath(String propertyName) {
-        Deque<String> stack = PATH_STACK.get();
-        if (stack.isEmpty()) {
-            return propertyName;
-        }
-        // Build path: stack contains parent prefix(es) bottom-to-top
-        StringBuilder sb = new StringBuilder();
-        Iterator<String> it = stack.descendingIterator();
-        while (it.hasNext()) {
-            sb.append(it.next());
-            sb.append('.');
-        }
-        sb.append(propertyName);
-        return sb.toString();
-    }
-
-    // --- Wrapping SettableBeanProperty ---
-
-    static class AuthorizingSettableBeanProperty extends 
SettableBeanProperty.Delegating {
-        private final BiPredicate<String, Object> authorizer;
-
-        AuthorizingSettableBeanProperty(SettableBeanProperty delegate, 
BiPredicate<String, Object> authorizer) {
-            super(delegate);
-            this.authorizer = authorizer;
-        }
-
-        @Override
-        protected SettableBeanProperty withDelegate(SettableBeanProperty d) {
-            return new AuthorizingSettableBeanProperty(d, authorizer);
-        }
-
-        /**
-         * For Collection/Map/Array properties, the path to push for nested 
element members must include
-         * the indexed bracket suffix so children build paths like 
"items[0].field" — matching
-         * ParametersInterceptor depth semantics and the existing 
JSONInterceptor recursive filter.
-         * For scalar/bean properties, push the path unchanged.
-         */
-        private String prefixForNested(String pathOfThisProperty) {
-            com.fasterxml.jackson.databind.JavaType type = getType();
-            if (type != null && (type.isCollectionLikeType() || 
type.isMapLikeType() || type.isArrayType())) {
-                return pathOfThisProperty + "[0]";
-            }
-            return pathOfThisProperty;
-        }
-
-        @Override
-        public void deserializeAndSet(JsonParser p, DeserializationContext 
ctxt, Object instance) throws java.io.IOException {
-            String path = currentPath(getName());
-            if (!authorizer.test(path, instance)) {
-                p.skipChildren(); // discard the JSON value for this property
-                return;
-            }
-            PATH_STACK.get().push(prefixForNested(path));
-            try {
-                delegate.deserializeAndSet(p, ctxt, instance);
-            } finally {
-                PATH_STACK.get().pop();
-            }
-        }
-
-        @Override
-        public Object deserializeSetAndReturn(JsonParser p, 
DeserializationContext ctxt, Object instance) throws java.io.IOException {
-            String path = currentPath(getName());
-            if (!authorizer.test(path, instance)) {
-                p.skipChildren();
-                return instance;
-            }
-            PATH_STACK.get().push(prefixForNested(path));
-            try {
-                return delegate.deserializeSetAndReturn(p, ctxt, instance);
-            } finally {
-                PATH_STACK.get().pop();
-            }
-        }
-    }
-
-    // --- Module that registers the modifier ---
-
-    static ObjectMapper buildAuthorizingMapper(BiPredicate<String, Object> 
authorizer) {
-        SimpleModule module = new SimpleModule();
-        module.setDeserializerModifier(new BeanDeserializerModifier() {
-            @Override
-            public BeanDeserializerBuilder updateBuilder(DeserializationConfig 
config,
-                                                         BeanDescription 
beanDesc,
-                                                         
BeanDeserializerBuilder builder) {
-                Iterator<SettableBeanProperty> it = builder.getProperties();
-                while (it.hasNext()) {
-                    SettableBeanProperty original = it.next();
-                    builder.addOrReplaceProperty(new 
AuthorizingSettableBeanProperty(original, authorizer), true);
-                }
-                return builder;
-            }
-        });
-        return new ObjectMapper().registerModule(module);
-    }
-
-    // --- Test fixtures ---
-
-    public static class Person {
-        public String name;
-        public String role;
-        public Address address;
-        public java.util.List<Address> addresses;
-        public Address[] addressArray;
-        public java.util.Map<String, Address> addressMap;
-    }
-
-    public static class Address {
-        public String city;
-        public String zip;
-    }
-
-    // --- Tests ---
-
-    @Override
-    protected void setUp() {
-        PATH_STACK.remove();
-    }
-
-    public void testTopLevelAuthorizedPropertyIsApplied() throws Exception {
-        ObjectMapper mapper = buildAuthorizingMapper((path, instance) -> 
"name".equals(path));
-        Person p = mapper.readValue("{\"name\":\"alice\",\"role\":\"admin\"}", 
Person.class);
-        assertEquals("alice", p.name);
-        assertNull("role must be skipped", p.role);
-    }
-
-    public void testNestedPropertyAuthorizedByFullPath() throws Exception {
-        // Authorize address (depth 0) and address.city (depth 1), but NOT 
address.zip
-        ObjectMapper mapper = buildAuthorizingMapper((path, instance) ->
-                "address".equals(path) || "address.city".equals(path));
-        Person p = 
mapper.readValue("{\"address\":{\"city\":\"Warsaw\",\"zip\":\"00-001\"}}", 
Person.class);
-        assertNotNull("address should be set", p.address);
-        assertEquals("Warsaw", p.address.city);
-        assertNull("zip must be skipped because address.zip not authorized", 
p.address.zip);
-    }
-
-    public void testNestedRejectedAtParent() throws Exception {
-        // Reject "address" entirely at depth 0; nested fields should not be 
visited
-        ObjectMapper mapper = buildAuthorizingMapper((path, instance) -> 
"name".equals(path));
-        Person p = 
mapper.readValue("{\"name\":\"alice\",\"address\":{\"city\":\"Warsaw\"}}", 
Person.class);
-        assertEquals("alice", p.name);
-        assertNull("address must be rejected at the parent, no nested visit", 
p.address);
-    }
-
-    // --- Collection / Array / Map indexed-path tests ---
-
-    public void testListOfBeansUsesIndexedPath() throws Exception {
-        // Authorize "addresses" (depth 0) AND "addresses[0].city" (depth 2) 
but NOT "addresses[0].zip"
-        ObjectMapper mapper = buildAuthorizingMapper((path, instance) ->
-                "addresses".equals(path) || "addresses[0].city".equals(path));
-        Person p = mapper.readValue(
-                
"{\"addresses\":[{\"city\":\"Warsaw\",\"zip\":\"00-001\"},{\"city\":\"Krakow\",\"zip\":\"30-001\"}]}",
-                Person.class);
-        assertNotNull(p.addresses);
-        assertEquals(2, p.addresses.size());
-        assertEquals("Warsaw", p.addresses.get(0).city);
-        assertNull("addresses[0].zip must be skipped on element 0", 
p.addresses.get(0).zip);
-        assertEquals("Krakow", p.addresses.get(1).city);
-        assertNull("addresses[0].zip must be skipped on element 1 too (same 
path token)", p.addresses.get(1).zip);
-    }
-
-    public void testListRejectedAtParentSkipsAllElements() throws Exception {
-        ObjectMapper mapper = buildAuthorizingMapper((path, instance) -> 
"name".equals(path));
-        Person p = mapper.readValue(
-                "{\"name\":\"alice\",\"addresses\":[{\"city\":\"Warsaw\"}]}", 
Person.class);
-        assertEquals("alice", p.name);
-        assertNull("addresses must be rejected at parent — Jackson never 
visits elements", p.addresses);
-    }
-
-    public void testArrayOfBeansUsesIndexedPath() throws Exception {
-        ObjectMapper mapper = buildAuthorizingMapper((path, instance) ->
-                "addressArray".equals(path) || 
"addressArray[0].city".equals(path));
-        Person p = mapper.readValue(
-                
"{\"addressArray\":[{\"city\":\"Warsaw\",\"zip\":\"00-001\"}]}", Person.class);
-        assertNotNull(p.addressArray);
-        assertEquals(1, p.addressArray.length);
-        assertEquals("Warsaw", p.addressArray[0].city);
-        assertNull("addressArray[0].zip must be skipped", 
p.addressArray[0].zip);
-    }
-
-    public void testMapOfBeansUsesIndexedPath() throws Exception {
-        // Map values use [0] suffix (matching ParametersInterceptor bracket 
semantics + JSONInterceptor)
-        ObjectMapper mapper = buildAuthorizingMapper((path, instance) ->
-                "addressMap".equals(path) || 
"addressMap[0].city".equals(path));
-        Person p = mapper.readValue(
-                
"{\"addressMap\":{\"home\":{\"city\":\"Warsaw\",\"zip\":\"00-001\"}}}", 
Person.class);
-        assertNotNull(p.addressMap);
-        assertEquals(1, p.addressMap.size());
-        assertNotNull(p.addressMap.get("home"));
-        assertEquals("Warsaw", p.addressMap.get("home").city);
-        assertNull("addressMap[0].zip must be skipped", 
p.addressMap.get("home").zip);
-    }
-
-    public void testPathStackIsCleanAfterDeserialization() throws Exception {
-        ObjectMapper mapper = buildAuthorizingMapper((path, instance) -> true);
-        
mapper.readValue("{\"name\":\"alice\",\"address\":{\"city\":\"Warsaw\"}}", 
Person.class);
-        assertTrue("ThreadLocal stack must be empty after deserialization", 
PATH_STACK.get().isEmpty());
-    }
-}

Reply via email to