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

lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git


The following commit(s) were added to refs/heads/main by this push:
     new 690c4c273 WW-5626 cleanup follow-ups for @StrutsParameter JSON/REST 
enforcement (#1673)
690c4c273 is described below

commit 690c4c27371aca74abbc0c824d40764a237c7bef
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sat May 9 12:09:03 2026 +0200

    WW-5626 cleanup follow-ups for @StrutsParameter JSON/REST enforcement 
(#1673)
    
    * WW-5626 add ParameterAuthorizer#resolveTarget for centralized ModelDriven 
resolution
    
    Move the ValueStack peek logic that derives the target object from 
action+ModelDriven
    state out of ParametersInterceptor and into ParameterAuthorizer. Callers 
that need
    both authorization and the resolved target (for downstream OGNL 
allowlisting) can
    now call resolveTarget once and reuse the result.
    
    * WW-5626 delegate ModelDriven target resolution to ParameterAuthorizer
    
    Replace the inline ValueStack peek in 
ParametersInterceptor#isParameterAnnotatedAndAllowlist
    with a call to ParameterAuthorizer#resolveTarget. The ModelDriven import is 
no longer
    needed in this class.
    
    * WW-5626 defensively skip non-String JSON keys in authorization filter
    
    The (String) cast in filterUnauthorizedKeysRecursive threw 
ClassCastException
    for any custom JSONReader producing non-String keys. Replace with an 
instanceof
    pattern that debug-logs and skips entries whose key cannot be converted to a
    parameter path.
    
    * WW-5626 add real JacksonJsonHandler integration tests for 
@StrutsParameter filtering
    
    The existing ContentTypeInterceptorTest uses mock ContentTypeHandlers, so 
its
    requireAnnotations=true tests verify only that intercept() returns SUCCESS 
— they
    assert nothing about which properties were actually filtered. These 
integration
    tests use a real JacksonJsonHandler + a real StrutsParameterAuthorizer to 
verify
    end-to-end property-level filtering for top-level annotated/unannotated 
properties
    and nested properties at varying authorized depths.
    
    The SecureRestAction fixture documents a semantic divergence: REST's 
recursive
    copy authorizes each path level independently, so depth-0 authorization on 
the
    top-level property requires @StrutsParameter on the setter even when nested
    field access is the actual goal. ParametersInterceptor only requires the 
getter
    annotation. This divergence is tracked for the Approach C refactor.
    
    * WW-5626 make ParameterAuthorizer#resolveTarget a default method to 
preserve SAM
    
    Making resolveTarget abstract broke ParameterAuthorizer as a functional 
interface,
    which the existing JSON and REST plugin tests rely on for lambda-based 
stubs:
      interceptor.setParameterAuthorizer((parameterName, target, action) -> 
true);
    
    The default returns the action unchanged — adequate for lambda test stubs 
whose
    authorization decisions don't depend on the resolved target. The production
    implementation (StrutsParameterAuthorizer) overrides this with the proper
    ModelDriven value-stack peek.
---
 .../interceptor/parameter/ParameterAuthorizer.java |  20 +++
 .../parameter/ParametersInterceptor.java           |  10 +-
 .../parameter/StrutsParameterAuthorizer.java       |  12 ++
 .../parameter/ParameterAuthorizerTest.java         |  39 ++++++
 .../org/apache/struts2/json/JSONInterceptor.java   |   9 +-
 .../apache/struts2/json/JSONInterceptorTest.java   |  26 ++++
 .../ContentTypeInterceptorIntegrationTest.java     | 134 +++++++++++++++++++++
 .../org/apache/struts2/rest/SecureRestAction.java  |  70 +++++++++++
 8 files changed, 310 insertions(+), 10 deletions(-)

diff --git 
a/core/src/main/java/org/apache/struts2/interceptor/parameter/ParameterAuthorizer.java
 
b/core/src/main/java/org/apache/struts2/interceptor/parameter/ParameterAuthorizer.java
index d064fbf68..6c5848bd1 100644
--- 
a/core/src/main/java/org/apache/struts2/interceptor/parameter/ParameterAuthorizer.java
+++ 
b/core/src/main/java/org/apache/struts2/interceptor/parameter/ParameterAuthorizer.java
@@ -45,4 +45,24 @@ public interface ParameterAuthorizer {
      * @return {@code true} if the parameter is authorized for injection, 
{@code false} otherwise
      */
     boolean isAuthorized(String parameterName, Object target, Object action);
+
+    /**
+     * Resolves the target object whose annotations should be checked for 
authorization.
+     * For {@link org.apache.struts2.ModelDriven} actions, the default 
implementation returns the action itself;
+     * the production implementation ({@link StrutsParameterAuthorizer}) 
overrides this to return the model from
+     * the value stack.
+     *
+     * <p>Callers that need both authorization checks AND the resolved target 
(e.g. for downstream OGNL allowlisting)
+     * should call this once and reuse the result.</p>
+     *
+     * <p>This is a {@code default} method to preserve the interface as a 
functional interface (SAM) for
+     * lambda-based test stubs.</p>
+     *
+     * @param action the action instance
+     * @return the resolved target — either the action or its model
+     * @since 7.2.0
+     */
+    default Object resolveTarget(Object action) {
+        return action;
+    }
 }
diff --git 
a/core/src/main/java/org/apache/struts2/interceptor/parameter/ParametersInterceptor.java
 
b/core/src/main/java/org/apache/struts2/interceptor/parameter/ParametersInterceptor.java
index 6173a85c6..5491b585f 100644
--- 
a/core/src/main/java/org/apache/struts2/interceptor/parameter/ParametersInterceptor.java
+++ 
b/core/src/main/java/org/apache/struts2/interceptor/parameter/ParametersInterceptor.java
@@ -23,7 +23,6 @@ import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.struts2.ActionContext;
 import org.apache.struts2.ActionInvocation;
-import org.apache.struts2.ModelDriven;
 import org.apache.struts2.StrutsConstants;
 import org.apache.struts2.action.NoParameters;
 import org.apache.struts2.action.ParameterNameAware;
@@ -369,14 +368,7 @@ public class ParametersInterceptor extends 
MethodFilterInterceptor {
             return true;
         }
 
-        // Resolve target for ModelDriven: if the ValueStack peek is different 
from the action, it's the model
-        Object target = action;
-        if (action instanceof ModelDriven<?>) {
-            Object stackTop = 
ActionContext.getContext().getValueStack().peek();
-            if (!stackTop.equals(action)) {
-                target = stackTop;
-            }
-        }
+        Object target = parameterAuthorizer.resolveTarget(action);
 
         // Delegate authorization check to shared ParameterAuthorizer (no OGNL 
side effects)
         if (!parameterAuthorizer.isAuthorized(name, target, action)) {
diff --git 
a/core/src/main/java/org/apache/struts2/interceptor/parameter/StrutsParameterAuthorizer.java
 
b/core/src/main/java/org/apache/struts2/interceptor/parameter/StrutsParameterAuthorizer.java
index 82e47717e..03cc22c0e 100644
--- 
a/core/src/main/java/org/apache/struts2/interceptor/parameter/StrutsParameterAuthorizer.java
+++ 
b/core/src/main/java/org/apache/struts2/interceptor/parameter/StrutsParameterAuthorizer.java
@@ -21,6 +21,7 @@ package org.apache.struts2.interceptor.parameter;
 import org.apache.commons.lang3.BooleanUtils;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
+import org.apache.struts2.ActionContext;
 import org.apache.struts2.ModelDriven;
 import org.apache.struts2.StrutsConstants;
 import org.apache.struts2.inject.Inject;
@@ -91,6 +92,17 @@ public class StrutsParameterAuthorizer implements 
ParameterAuthorizer {
         this.requireAnnotationsTransitionMode = 
BooleanUtils.toBoolean(transitionMode);
     }
 
+    @Override
+    public Object resolveTarget(Object action) {
+        if (action instanceof ModelDriven<?>) {
+            Object stackTop = 
ActionContext.getContext().getValueStack().peek();
+            if (!stackTop.equals(action)) {
+                return stackTop;
+            }
+        }
+        return action;
+    }
+
     @Override
     public boolean isAuthorized(String parameterName, Object target, Object 
action) {
         if (parameterName == null || parameterName.isEmpty()) {
diff --git 
a/core/src/test/java/org/apache/struts2/interceptor/parameter/ParameterAuthorizerTest.java
 
b/core/src/test/java/org/apache/struts2/interceptor/parameter/ParameterAuthorizerTest.java
index f7c16fbb9..43eb57bcc 100644
--- 
a/core/src/test/java/org/apache/struts2/interceptor/parameter/ParameterAuthorizerTest.java
+++ 
b/core/src/test/java/org/apache/struts2/interceptor/parameter/ParameterAuthorizerTest.java
@@ -18,13 +18,16 @@
  */
 package org.apache.struts2.interceptor.parameter;
 
+import org.apache.struts2.ActionContext;
 import org.apache.struts2.ModelDriven;
+import org.apache.struts2.StubValueStack;
 import org.apache.struts2.ognl.DefaultOgnlBeanInfoCacheFactory;
 import org.apache.struts2.ognl.DefaultOgnlExpressionCacheFactory;
 import org.apache.struts2.ognl.OgnlUtil;
 import org.apache.struts2.ognl.StrutsOgnlGuard;
 import org.apache.struts2.ognl.StrutsProxyCacheFactory;
 import org.apache.struts2.util.StrutsProxyService;
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -56,6 +59,11 @@ public class ParameterAuthorizerTest {
         authorizer.setProxyService(proxyService);
     }
 
+    @After
+    public void tearDown() {
+        ActionContext.clear();
+    }
+
     // --- requireAnnotations=false (backward compat) ---
 
     @Test
@@ -182,6 +190,37 @@ public class ParameterAuthorizerTest {
         assertThat(authorizer.isAuthorized(null, action, action)).isFalse();
     }
 
+    // --- resolveTarget ---
+
+    @Test
+    public void resolveTarget_nonModelDriven_returnsAction() {
+        var action = new SecureAction();
+        assertThat(authorizer.resolveTarget(action)).isSameAs(action);
+    }
+
+    @Test
+    public void resolveTarget_modelDriven_returnsModelFromValueStack() {
+        var action = new ModelAction();
+        var model = action.getModel();
+        var valueStack = new StubValueStack();
+        valueStack.push(model);
+        ActionContext.of().withValueStack(valueStack).bind();
+
+        assertThat(authorizer.resolveTarget(action)).isSameAs(model);
+    }
+
+    @Test
+    public void resolveTarget_modelDriven_stackTopEqualsAction_returnsAction() 
{
+        // Edge case: ModelDriven action where stack top equals the action 
itself.
+        // No exemption applies — target stays as action.
+        var action = new ModelAction();
+        var valueStack = new StubValueStack();
+        valueStack.push(action);
+        ActionContext.of().withValueStack(valueStack).bind();
+
+        assertThat(authorizer.resolveTarget(action)).isSameAs(action);
+    }
+
     // --- Inner test classes ---
 
     public static class SecureAction {
diff --git 
a/plugins/json/src/main/java/org/apache/struts2/json/JSONInterceptor.java 
b/plugins/json/src/main/java/org/apache/struts2/json/JSONInterceptor.java
index a0279f2f0..d0acfd4ce 100644
--- a/plugins/json/src/main/java/org/apache/struts2/json/JSONInterceptor.java
+++ b/plugins/json/src/main/java/org/apache/struts2/json/JSONInterceptor.java
@@ -215,7 +215,14 @@ public class JSONInterceptor extends AbstractInterceptor {
         Iterator<Map.Entry> it = json.entrySet().iterator();
         while (it.hasNext()) {
             Map.Entry entry = it.next();
-            String key = (String) entry.getKey();
+            if (!(entry.getKey() instanceof String key)) {
+                // Defensive: a custom JSONReader could produce non-String 
keys. Skip — we cannot
+                // construct a parameter path for authorization, and 
JSONPopulator wouldn't bind
+                // these to bean properties anyway.
+                LOG.debug("Skipping JSON entry with non-String key [{}] of 
type [{}] under prefix [{}]",
+                        entry.getKey(), entry.getKey() == null ? "null" : 
entry.getKey().getClass().getName(), prefix);
+                continue;
+            }
             String fullPath = prefix.isEmpty() ? key : prefix + "." + key;
 
             if (!parameterAuthorizer.isAuthorized(fullPath, target, action)) {
diff --git 
a/plugins/json/src/test/java/org/apache/struts2/json/JSONInterceptorTest.java 
b/plugins/json/src/test/java/org/apache/struts2/json/JSONInterceptorTest.java
index 2203f6f29..ece2f1272 100644
--- 
a/plugins/json/src/test/java/org/apache/struts2/json/JSONInterceptorTest.java
+++ 
b/plugins/json/src/test/java/org/apache/struts2/json/JSONInterceptorTest.java
@@ -583,6 +583,32 @@ public class JSONInterceptorTest extends StrutsTestCase {
         assertNull(action.getBar());
     }
 
+    public void testNonStringKeysAreSkippedByAuthorizationFilter() throws 
Exception {
+        // Simulate a custom JSON reader producing a Map with a non-String key.
+        // The authorizer should skip the entry rather than throw 
ClassCastException.
+        JSONInterceptor interceptor = new JSONInterceptor();
+        JSONUtil jsonUtil = new JSONUtil();
+        jsonUtil.setReader(new StrutsJSONReader());
+        jsonUtil.setWriter(new StrutsJSONWriter());
+        interceptor.setJsonUtil(jsonUtil);
+        interceptor.setParameterAuthorizer((parameterName, target, action) -> 
true);
+
+        java.util.Map<Object, Object> mixedKeyMap = new 
java.util.LinkedHashMap<>();
+        mixedKeyMap.put("validKey", "ok");
+        mixedKeyMap.put(42, "shouldBeSkipped"); // Integer key, not String
+
+        java.lang.reflect.Method method = 
JSONInterceptor.class.getDeclaredMethod(
+                "filterUnauthorizedKeys", java.util.Map.class, Object.class, 
Object.class);
+        method.setAccessible(true);
+
+        // Should not throw ClassCastException
+        method.invoke(interceptor, mixedKeyMap, new TestAction(), new 
TestAction());
+
+        // The non-String key entry should still be present (skipped, not 
removed)
+        assertTrue("non-String-key entry should remain (skipped, not 
removed)", mixedKeyMap.containsKey(42));
+        assertTrue("String-key entry should remain", 
mixedKeyMap.containsKey("validKey"));
+    }
+
     public void testParameterAuthorizerAllowsAllWhenPermissive() throws 
Exception {
         // Same JSON body, but authorizer allows all
         this.request.setContent("{\"foo\":\"value1\", 
\"bar\":\"value2\"}".getBytes());
diff --git 
a/plugins/rest/src/test/java/org/apache/struts2/rest/ContentTypeInterceptorIntegrationTest.java
 
b/plugins/rest/src/test/java/org/apache/struts2/rest/ContentTypeInterceptorIntegrationTest.java
new file mode 100644
index 000000000..dc11c6fdf
--- /dev/null
+++ 
b/plugins/rest/src/test/java/org/apache/struts2/rest/ContentTypeInterceptorIntegrationTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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;
+
+import com.mockobjects.dynamic.AnyConstraintMatcher;
+import com.mockobjects.dynamic.Mock;
+import junit.framework.TestCase;
+import org.apache.struts2.ActionContext;
+import org.apache.struts2.ActionInvocation;
+import org.apache.struts2.action.Action;
+import org.apache.struts2.dispatcher.mapper.ActionMapping;
+import org.apache.struts2.interceptor.parameter.StrutsParameterAuthorizer;
+import org.apache.struts2.ognl.DefaultOgnlBeanInfoCacheFactory;
+import org.apache.struts2.ognl.DefaultOgnlExpressionCacheFactory;
+import org.apache.struts2.ognl.OgnlUtil;
+import org.apache.struts2.ognl.StrutsOgnlGuard;
+import org.apache.struts2.ognl.StrutsProxyCacheFactory;
+import org.apache.struts2.rest.handler.JacksonJsonHandler;
+import org.apache.struts2.util.StrutsProxyService;
+import org.springframework.mock.web.MockHttpServletRequest;
+
+import static org.apache.struts2.ognl.OgnlCacheFactory.CacheType.LRU;
+
+/**
+ * Integration tests for ContentTypeInterceptor that use a real {@link 
JacksonJsonHandler}
+ * and a real {@link StrutsParameterAuthorizer}, end-to-end. Verifies that 
property
+ * filtering actually occurs on the deserialized object — not merely that the 
wiring runs.
+ */
+public class ContentTypeInterceptorIntegrationTest extends TestCase {
+
+    private ContentTypeInterceptor interceptor;
+    private SecureRestAction action;
+    private Mock mockActionInvocation;
+    private Mock mockSelector;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        action = new SecureRestAction();
+
+        var ognlUtil = new OgnlUtil(
+                new DefaultOgnlExpressionCacheFactory<>("1000", 
LRU.toString()),
+                new DefaultOgnlBeanInfoCacheFactory<>("1000", LRU.toString()),
+                new StrutsOgnlGuard());
+        var proxyService = new StrutsProxyService(new 
StrutsProxyCacheFactory<>("1000", "basic"));
+
+        StrutsParameterAuthorizer authorizer = new StrutsParameterAuthorizer();
+        authorizer.setOgnlUtil(ognlUtil);
+        authorizer.setProxyService(proxyService);
+        authorizer.setRequireAnnotations(Boolean.TRUE.toString());
+
+        interceptor = new ContentTypeInterceptor();
+        interceptor.setParameterAuthorizer(authorizer);
+        interceptor.setRequireAnnotations(Boolean.TRUE.toString());
+
+        mockActionInvocation = new Mock(ActionInvocation.class);
+        mockSelector = new Mock(ContentTypeHandlerManager.class);
+        // ContentTypeInterceptor calls getAction() twice when 
requireAnnotations=true
+        mockActionInvocation.expectAndReturn("getAction", action);
+        mockActionInvocation.expectAndReturn("getAction", action);
+        mockActionInvocation.expectAndReturn("invoke", Action.SUCCESS);
+        mockSelector.expectAndReturn("getHandlerForRequest", new 
AnyConstraintMatcher() {
+            public boolean matches(Object[] args) { return true; }
+        }, new JacksonJsonHandler());
+        interceptor.setContentTypeHandlerSelector((ContentTypeHandlerManager) 
mockSelector.proxy());
+    }
+
+    private void runWithBody(String body) throws Exception {
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        request.setContent(body.getBytes());
+        request.setContentType("application/json");
+
+        ActionContext.of()
+                .withActionMapping(new ActionMapping())
+                .withServletRequest(request)
+                .bind();
+
+        interceptor.intercept((ActionInvocation) mockActionInvocation.proxy());
+        mockSelector.verify();
+        mockActionInvocation.verify();
+    }
+
+    public void testAnnotatedTopLevelPropertyIsApplied() throws Exception {
+        runWithBody("{\"name\":\"alice\"}");
+        assertEquals("alice", action.getName());
+    }
+
+    public void testUnannotatedTopLevelPropertyIsRejected() throws Exception {
+        runWithBody("{\"role\":\"admin\"}");
+        assertNull("unannotated 'role' must not be set", action.getRole());
+    }
+
+    public void testMixedPropertiesFilteredCorrectly() throws Exception {
+        runWithBody("{\"name\":\"alice\",\"role\":\"admin\"}");
+        assertEquals("alice", action.getName());
+        assertNull(action.getRole());
+    }
+
+    public void testNestedPropertyAuthorizedWhenDepthAllows() throws Exception 
{
+        runWithBody("{\"address\":{\"city\":\"Warsaw\",\"zip\":\"00-001\"}}");
+        assertNotNull("address should be set", action.getAddress());
+        assertEquals("Warsaw", action.getAddress().getCity());
+        assertEquals("00-001", action.getAddress().getZip());
+    }
+
+    public void testNestedPropertyRejectedWhenDepthInsufficient() throws 
Exception {
+        // shallowAddress has @StrutsParameter (depth=0) — its nested fields 
should NOT be set
+        
runWithBody("{\"shallowAddress\":{\"city\":\"Warsaw\",\"zip\":\"00-001\"}}");
+        // The top-level shallowAddress reference itself may be created 
(Jackson behavior)
+        // but its nested city/zip must remain null because depth-1 isn't 
allowed.
+        if (action.getShallowAddress() != null) {
+            assertNull("nested city should be rejected (depth insufficient)",
+                    action.getShallowAddress().getCity());
+            assertNull("nested zip should be rejected (depth insufficient)",
+                    action.getShallowAddress().getZip());
+        }
+    }
+}
diff --git 
a/plugins/rest/src/test/java/org/apache/struts2/rest/SecureRestAction.java 
b/plugins/rest/src/test/java/org/apache/struts2/rest/SecureRestAction.java
new file mode 100644
index 000000000..16966dec1
--- /dev/null
+++ b/plugins/rest/src/test/java/org/apache/struts2/rest/SecureRestAction.java
@@ -0,0 +1,70 @@
+/*
+ * 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;
+
+import org.apache.struts2.ActionSupport;
+import org.apache.struts2.interceptor.parameter.StrutsParameter;
+
+/**
+ * Test fixture for ContentTypeInterceptor integration tests.
+ * Has annotated and unannotated properties to exercise authorization 
filtering.
+ */
+public class SecureRestAction extends ActionSupport {
+
+    private String name;
+    private String role;
+    private Address address;
+    private Address shallowAddress;
+
+    public String getName() { return name; }
+
+    @StrutsParameter
+    public void setName(String name) { this.name = name; }
+
+    public String getRole() { return role; }
+    public void setRole(String role) { this.role = role; }
+
+    // Both annotations needed for REST: setter authorizes the top-level 
"address" (depth 0),
+    // getter(depth=1) authorizes nested "address.city" (depth 1). Note: 
ParametersInterceptor
+    // only requires the getter annotation — REST's recursive copy authorizes 
each path level
+    // independently. This divergence is tracked for the Approach C refactor.
+    @StrutsParameter(depth = 1)
+    public Address getAddress() { return address; }
+
+    @StrutsParameter
+    public void setAddress(Address address) { this.address = address; }
+
+    // shallowAddress: depth-0 authorized (setter annotated), but nested 
fields rejected
+    // because the getter has no depth>=1 annotation.
+    public Address getShallowAddress() { return shallowAddress; }
+
+    @StrutsParameter
+    public void setShallowAddress(Address shallowAddress) { 
this.shallowAddress = shallowAddress; }
+
+    public static class Address {
+        private String city;
+        private String zip;
+
+        public String getCity() { return city; }
+        public void setCity(String city) { this.city = city; }
+
+        public String getZip() { return zip; }
+        public void setZip(String zip) { this.zip = zip; }
+    }
+}

Reply via email to