This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 741f75431a99 CAMEL-22849: Fix AS2 server wildcard pattern matching for
requestUriPattern (#21089)
741f75431a99 is described below
commit 741f75431a99999deea5a5437b3ac179165162ac
Author: Guillaume Nodet <[email protected]>
AuthorDate: Tue Jan 27 16:30:39 2026 +0100
CAMEL-22849: Fix AS2 server wildcard pattern matching for requestUriPattern
(#21089)
The AS2 server/listen functionality was not properly resolving
requestUriPattern wildcards when selecting the appropriate consumer
configuration. The getConfigurationForPath method in AS2ServerConnection was
doing a simple Map.get() lookup which only supported exact matches.
---
.../component/as2/api/AS2ServerConnection.java | 105 +++++++-
.../AS2ServerConnectionPatternMatchingTest.java | 87 +++++++
.../component/as2/AS2ServerWildcardPatternIT.java | 277 +++++++++++++++++++++
3 files changed, 465 insertions(+), 4 deletions(-)
diff --git
a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ServerConnection.java
b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ServerConnection.java
index a27c86f6778f..cf4b5c79ebc5 100644
---
a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ServerConnection.java
+++
b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ServerConnection.java
@@ -24,11 +24,14 @@ import java.net.SocketException;
import java.net.URI;
import java.security.PrivateKey;
import java.security.cert.Certificate;
+import java.util.Collections;
+import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
+import java.util.regex.Pattern;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocketFactory;
@@ -89,9 +92,18 @@ public class AS2ServerConnection {
private final String accessToken;
/**
- * Stores the configuration for each consumer endpoint path (e.g.,
"/consumerA")
+ * Stores the configuration for each consumer endpoint path (e.g.,
"/consumerA"). Uses LinkedHashMap to preserve
+ * insertion order, ensuring that when multiple patterns match a request,
the first registered pattern (in route
+ * definition order) takes precedence.
*/
- private final Map<String, AS2ConsumerConfiguration> consumerConfigurations
= new ConcurrentHashMap<>();
+ private final Map<String, AS2ConsumerConfiguration> consumerConfigurations
+ = Collections.synchronizedMap(new LinkedHashMap<>());
+
+ /**
+ * Cache of compiled regex patterns for wildcard matching. Key is the
pattern string, value is the compiled Pattern
+ * object.
+ */
+ private final Map<String, Pattern> compiledPatterns = new
ConcurrentHashMap<>();
/**
* Simple wrapper class to associate the AS2ConsumerConfiguration with the
specific request URI path that was
@@ -160,13 +172,98 @@ public class AS2ServerConnection {
}
/**
- * Retrieves the specific AS2 consumer configuration associated with the
given request path.
+ * Retrieves the specific AS2 consumer configuration associated with the
given request path. Supports wildcard
+ * patterns (e.g., "/consumer/*") in addition to exact matches.
+ *
+ * <p>
+ * Pattern matching examples:
+ * <ul>
+ * <li>Pattern "/consumer/*" matches "/consumer/orders",
"/consumer/invoices", "/consumer/a/b/c"</li>
+ * <li>Pattern "/api/*/orders" matches "/api/v1/orders",
"/api/v2/orders"</li>
+ * <li>Pattern "/*" matches any path</li>
+ * </ul>
+ *
+ * <p>
+ * When multiple patterns match, the first registered pattern (in route
definition order) takes precedence. Exact
+ * matches always take precedence over wildcard matches.
*
* @param path The canonical request URI path (e.g., "/consumerA").
* @return An Optional containing the configuration if a match is
found, otherwise empty.
*/
public Optional<AS2ConsumerConfiguration> getConfigurationForPath(String
path) {
- return Optional.ofNullable(consumerConfigurations.get(path));
+ // First try exact match for performance - this is the most common case
+ AS2ConsumerConfiguration exactMatch = consumerConfigurations.get(path);
+ if (exactMatch != null) {
+ LOG.debug("Found exact match for path: {}", path);
+ return Optional.of(exactMatch);
+ }
+
+ LOG.debug("No exact match for path: {}, trying pattern matching",
path);
+
+ // Then try pattern matching for wildcards - first match wins (in
insertion order)
+ for (String pattern : consumerConfigurations.keySet()) {
+ if (matchesPattern(path, pattern)) {
+ LOG.debug("Path {} matched pattern: {}", path, pattern);
+ return
Optional.ofNullable(consumerConfigurations.get(pattern));
+ }
+ }
+
+ LOG.debug("No pattern matched for path: {}", path);
+ return Optional.empty();
+ }
+
+ /**
+ * Checks if a request path matches a pattern that may contain wildcards.
Supports wildcard '*' which matches any
+ * sequence of characters.
+ *
+ * <p>
+ * This method uses compiled regex patterns with caching for performance.
All regex special characters in the
+ * pattern are properly escaped using Pattern.quote(), ensuring that only
the wildcard '*' has special meaning.
+ *
+ * @param requestPath the incoming request path
+ * @param pattern the pattern to match against (may contain wildcards)
+ * @return true if the path matches the pattern, false
otherwise
+ */
+ private boolean matchesPattern(String requestPath, String pattern) {
+ // Exact match - fast path
+ if (requestPath.equals(pattern)) {
+ return true;
+ }
+
+ // No wildcard in pattern, and not exact match
+ if (!pattern.contains("*")) {
+ return false;
+ }
+
+ // Get or compile the pattern
+ Pattern compiledPattern = getCompiledPattern(pattern);
+ return compiledPattern.matcher(requestPath).matches();
+ }
+
+ /**
+ * Gets a compiled regex Pattern for the given wildcard pattern string.
Patterns are cached for performance.
+ *
+ * <p>
+ * This method splits the pattern by '*' and uses Pattern.quote() to
properly escape all regex special characters in
+ * each segment, then joins them with '.*' to create the final regex
pattern.
+ *
+ * @param pattern the wildcard pattern string (e.g., "/consumer/*")
+ * @return the compiled Pattern object
+ */
+ private Pattern getCompiledPattern(String pattern) {
+ return compiledPatterns.computeIfAbsent(pattern, p -> {
+ // Split by * and quote each segment, then join with .*
+ String[] segments = p.split("\\*", -1);
+ StringBuilder regex = new StringBuilder();
+ for (int i = 0; i < segments.length; i++) {
+ // Pattern.quote() properly escapes all regex special
characters
+ regex.append(Pattern.quote(segments[i]));
+ if (i < segments.length - 1) {
+ regex.append(".*");
+ }
+ }
+ return Pattern.compile(regex.toString());
+ });
}
/**
diff --git
a/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2ServerConnectionPatternMatchingTest.java
b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2ServerConnectionPatternMatchingTest.java
new file mode 100644
index 000000000000..eb4468a52c75
--- /dev/null
+++
b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2ServerConnectionPatternMatchingTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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.camel.component.as2.api;
+
+import java.util.regex.Pattern;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit test for AS2ServerConnection pattern matching logic
+ */
+public class AS2ServerConnectionPatternMatchingTest {
+
+ /**
+ * Simulates the getCompiledPattern logic from AS2ServerConnection
+ */
+ private Pattern getCompiledPattern(String pattern) {
+ String[] segments = pattern.split("\\*", -1);
+ StringBuilder regex = new StringBuilder();
+ for (int i = 0; i < segments.length; i++) {
+ regex.append(Pattern.quote(segments[i]));
+ if (i < segments.length - 1) {
+ regex.append(".*");
+ }
+ }
+ return Pattern.compile(regex.toString());
+ }
+
+ @Test
+ public void testWildcardPatternMatching() {
+ Pattern pattern = getCompiledPattern("/consumer/*");
+
+ assertTrue(pattern.matcher("/consumer/orders").matches());
+ assertTrue(pattern.matcher("/consumer/invoices").matches());
+ assertFalse(pattern.matcher("/admin/orders").matches());
+ }
+
+ @Test
+ public void testRegexSpecialCharactersWithWildcard() {
+ Pattern pattern = getCompiledPattern("/api/v2(3)/*");
+
+ // Should match - parentheses are literal
+ assertTrue(pattern.matcher("/api/v2(3)/orders").matches());
+ assertTrue(pattern.matcher("/api/v2(3)/invoices").matches());
+
+ // Should NOT match - parentheses are literal, not regex grouping
+ assertFalse(pattern.matcher("/api/v23/orders").matches());
+ assertFalse(pattern.matcher("/api/v2/orders").matches());
+ }
+
+ @Test
+ public void testRegexSpecialCharactersExactMatch() {
+ Pattern pattern = getCompiledPattern("/api/v1.2/endpoint");
+
+ // Should match - dot is literal
+ assertTrue(pattern.matcher("/api/v1.2/endpoint").matches());
+
+ // Should NOT match - dot is literal, not regex "any character"
+ assertFalse(pattern.matcher("/api/v1X2/endpoint").matches());
+ }
+
+ @Test
+ public void testMultipleWildcards() {
+ Pattern pattern = getCompiledPattern("/api/*/v2(3)/*");
+
+ assertTrue(pattern.matcher("/api/v1/v2(3)/orders").matches());
+ assertTrue(pattern.matcher("/api/test/v2(3)/invoices").matches());
+ assertFalse(pattern.matcher("/api/v1/v23/orders").matches());
+ }
+}
diff --git
a/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2ServerWildcardPatternIT.java
b/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2ServerWildcardPatternIT.java
new file mode 100644
index 000000000000..811007d5ab82
--- /dev/null
+++
b/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2ServerWildcardPatternIT.java
@@ -0,0 +1,277 @@
+/*
+ * 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.camel.component.as2;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.as2.api.*;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.hc.core5.http.protocol.HttpCoreContext;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * Tests AS2 server wildcard pattern matching for requestUriPattern
(CAMEL-22849). This test verifies that wildcard
+ * patterns like "/consumer/*" correctly match incoming requests to
"/consumer/orders", "/consumer/invoices", etc.
+ *
+ * When multiple patterns match a request, the first registered pattern (in
route definition order) takes precedence.
+ * This means more specific patterns should be defined before more general
patterns.
+ */
+public class AS2ServerWildcardPatternIT extends AS2ServerSecTestBase {
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ public void configure() {
+ // Routes are registered in order - first matching pattern wins
+ // More specific patterns should be defined first
+
+ // Consumer with exact match - takes precedence over all
wildcards
+ from("as2://server/listen?requestUriPattern=/consumer/orders")
+ .to("mock:exactConsumer");
+
+ // Consumer with more specific wildcard pattern - should be
before less specific patterns
+
from("as2://server/listen?requestUriPattern=/consumer/orders/*")
+ .to("mock:specificWildcardConsumer");
+
+ // Consumer with general wildcard pattern - matches remaining
/consumer/* paths
+ from("as2://server/listen?requestUriPattern=/consumer/*")
+ .to("mock:wildcardConsumer");
+
+ // Consumer with different path - should not match /consumer/*
requests
+ from("as2://server/listen?requestUriPattern=/admin/*")
+ .to("mock:adminConsumer");
+
+ // Consumer with regex special characters in pattern - should
be treated as literals
+
from("as2://server/listen?requestUriPattern=/api/v1.2/endpoint")
+ .to("mock:regexSpecialCharsConsumer");
+
+ // Consumer with wildcard and special characters (using
parentheses which are regex special chars)
+ from("as2://server/listen?requestUriPattern=/api/v2(3)/*")
+ .to("mock:regexSpecialCharsWildcardConsumer");
+ }
+ };
+ }
+
+ @Test
+ public void testWildcardPatternMatchesOrders() throws Exception {
+ MockEndpoint mockEndpoint = getMockEndpoint("mock:wildcardConsumer");
+ mockEndpoint.expectedMessageCount(1);
+
+ // Send to /consumer/products - should match the /consumer/* pattern
+ HttpCoreContext context = sendToPath("/consumer/products",
AS2MessageStructure.PLAIN);
+
+ verifyOkResponse(context);
+ mockEndpoint.assertIsSatisfied();
+
+ // Verify the message was received
+ Exchange exchange = mockEndpoint.getReceivedExchanges().get(0);
+ assertNotNull(exchange);
+ assertEquals(EDI_MESSAGE, exchange.getIn().getBody(String.class));
+ }
+
+ @Test
+ public void testWildcardPatternMatchesInvoices() throws Exception {
+ MockEndpoint mockEndpoint = getMockEndpoint("mock:wildcardConsumer");
+ mockEndpoint.expectedMessageCount(1);
+
+ // Send to /consumer/invoices - should also match the /consumer/*
pattern
+ HttpCoreContext context = sendToPath("/consumer/invoices",
AS2MessageStructure.PLAIN);
+
+ verifyOkResponse(context);
+ mockEndpoint.assertIsSatisfied();
+
+ // Verify the message was received
+ Exchange exchange = mockEndpoint.getReceivedExchanges().get(0);
+ assertNotNull(exchange);
+ assertEquals(EDI_MESSAGE, exchange.getIn().getBody(String.class));
+ }
+
+ @Test
+ public void testWildcardPatternMatchesNestedPath() throws Exception {
+ MockEndpoint mockEndpoint =
getMockEndpoint("mock:specificWildcardConsumer");
+ mockEndpoint.expectedMessageCount(1);
+
+ // Send to /consumer/orders/123 - should match the more specific
/consumer/orders/* pattern
+ HttpCoreContext context = sendToPath("/consumer/orders/123",
AS2MessageStructure.PLAIN);
+
+ verifyOkResponse(context);
+ mockEndpoint.assertIsSatisfied();
+
+ // Verify the message was received
+ Exchange exchange = mockEndpoint.getReceivedExchanges().get(0);
+ assertNotNull(exchange);
+ assertEquals(EDI_MESSAGE, exchange.getIn().getBody(String.class));
+ }
+
+ @Test
+ public void testExactMatchTakesPrecedence() throws Exception {
+ MockEndpoint exactEndpoint = getMockEndpoint("mock:exactConsumer");
+ MockEndpoint wildcardEndpoint =
getMockEndpoint("mock:wildcardConsumer");
+ exactEndpoint.expectedMessageCount(1);
+ wildcardEndpoint.expectedMessageCount(0);
+
+ // Send to /consumer/orders - should match exact pattern, not wildcard
+ HttpCoreContext context = sendToPath("/consumer/orders",
AS2MessageStructure.PLAIN);
+
+ verifyOkResponse(context);
+ exactEndpoint.assertIsSatisfied();
+ wildcardEndpoint.assertIsSatisfied();
+
+ // Verify the message was received by exact consumer
+ Exchange exchange = exactEndpoint.getReceivedExchanges().get(0);
+ assertNotNull(exchange);
+ assertEquals(EDI_MESSAGE, exchange.getIn().getBody(String.class));
+ }
+
+ @Test
+ public void testFirstMatchingPatternWins() throws Exception {
+ MockEndpoint specificEndpoint =
getMockEndpoint("mock:specificWildcardConsumer");
+ MockEndpoint generalEndpoint =
getMockEndpoint("mock:wildcardConsumer");
+ specificEndpoint.expectedMessageCount(1);
+ generalEndpoint.expectedMessageCount(0);
+
+ // Send to /consumer/orders/456 - should match /consumer/orders/*
(first matching pattern)
+ // not /consumer/* (which also matches but is registered later)
+ HttpCoreContext context = sendToPath("/consumer/orders/456",
AS2MessageStructure.PLAIN);
+
+ verifyOkResponse(context);
+ specificEndpoint.assertIsSatisfied();
+ generalEndpoint.assertIsSatisfied();
+
+ // Verify the message was received by specific consumer
+ Exchange exchange = specificEndpoint.getReceivedExchanges().get(0);
+ assertNotNull(exchange);
+ assertEquals(EDI_MESSAGE, exchange.getIn().getBody(String.class));
+ }
+
+ @Test
+ public void testNoPatternMatch() throws Exception {
+ MockEndpoint wildcardEndpoint =
getMockEndpoint("mock:wildcardConsumer");
+ MockEndpoint adminEndpoint = getMockEndpoint("mock:adminConsumer");
+ wildcardEndpoint.expectedMessageCount(0);
+ adminEndpoint.expectedMessageCount(1);
+
+ // Send to /admin/test - should match /admin/*, not /consumer/*
+ HttpCoreContext context = sendToPath("/admin/test",
AS2MessageStructure.PLAIN);
+
+ verifyOkResponse(context);
+ wildcardEndpoint.assertIsSatisfied();
+ adminEndpoint.assertIsSatisfied();
+
+ // Verify the message was received by admin consumer
+ Exchange exchange = adminEndpoint.getReceivedExchanges().get(0);
+ assertNotNull(exchange);
+ assertEquals(EDI_MESSAGE, exchange.getIn().getBody(String.class));
+ }
+
+ @Test
+ public void testRegexSpecialCharactersTreatedAsLiterals() throws Exception
{
+ MockEndpoint regexEndpoint =
getMockEndpoint("mock:regexSpecialCharsConsumer");
+ regexEndpoint.expectedMessageCount(1);
+
+ // Send to /api/v1.2/endpoint - the dot should be treated as a literal
dot, not regex "any character"
+ HttpCoreContext context = sendToPath("/api/v1.2/endpoint",
AS2MessageStructure.PLAIN);
+
+ verifyOkResponse(context);
+ regexEndpoint.assertIsSatisfied();
+
+ // Verify the message was received
+ Exchange exchange = regexEndpoint.getReceivedExchanges().get(0);
+ assertNotNull(exchange);
+ assertEquals(EDI_MESSAGE, exchange.getIn().getBody(String.class));
+ }
+
+ @Test
+ public void testRegexSpecialCharactersNotMatchedAsRegex() throws Exception
{
+ MockEndpoint regexEndpoint =
getMockEndpoint("mock:regexSpecialCharsConsumer");
+ regexEndpoint.expectedMessageCount(0);
+
+ // Send to /api/v1X2/endpoint - should NOT match because dot is
literal, not regex "any character"
+ // If Pattern.quote() wasn't working, this would incorrectly match
+ HttpCoreContext context = sendToPath("/api/v1X2/endpoint",
AS2MessageStructure.PLAIN);
+
+ // This should fail to find a matching consumer, but we're just
verifying it doesn't match the wrong one
+ regexEndpoint.assertIsSatisfied();
+ }
+
+ @Test
+ public void testRegexSpecialCharactersWithWildcard() throws Exception {
+ MockEndpoint regexWildcardEndpoint =
getMockEndpoint("mock:regexSpecialCharsWildcardConsumer");
+ regexWildcardEndpoint.expectedMessageCount(1);
+
+ // Send to /api/v2(3)/orders - the parentheses should be treated as
literal chars, not regex grouping
+ HttpCoreContext context = sendToPath("/api/v2(3)/orders",
AS2MessageStructure.PLAIN);
+
+ verifyOkResponse(context);
+ regexWildcardEndpoint.assertIsSatisfied();
+
+ // Verify the message was received
+ Exchange exchange =
regexWildcardEndpoint.getReceivedExchanges().get(0);
+ assertNotNull(exchange);
+ assertEquals(EDI_MESSAGE, exchange.getIn().getBody(String.class));
+ }
+
+ @Test
+ public void testRegexSpecialCharactersWithWildcardNotMatchedAsRegex()
throws Exception {
+ MockEndpoint regexWildcardEndpoint =
getMockEndpoint("mock:regexSpecialCharsWildcardConsumer");
+ regexWildcardEndpoint.expectedMessageCount(0);
+
+ // Send to /api/v23/orders - should NOT match because parentheses are
literal, not regex grouping
+ // If Pattern.quote() wasn't working, /api/v2(3)/* would match
/api/v23/orders as a regex
+ HttpCoreContext context = sendToPath("/api/v23/orders",
AS2MessageStructure.PLAIN);
+
+ // This should fail to find a matching consumer, but we're just
verifying it doesn't match the wrong one
+ regexWildcardEndpoint.assertIsSatisfied();
+ }
+
+ /**
+ * Helper method to send a message to a specific path
+ */
+ protected HttpCoreContext sendToPath(String requestUri,
AS2MessageStructure structure) throws Exception {
+ AS2SignatureAlgorithm signingAlgorithm = structure.isSigned() ?
AS2SignatureAlgorithm.SHA256WITHRSA : null;
+ AS2EncryptionAlgorithm encryptionAlgorithm = structure.isEncrypted() ?
AS2EncryptionAlgorithm.AES128_CBC : null;
+ AS2CompressionAlgorithm compressionAlgorithm =
structure.isCompressed() ? AS2CompressionAlgorithm.ZLIB : null;
+
+ return clientConnection().send(
+ EDI_MESSAGE,
+ requestUri, // Use the provided requestUri instead of
REQUEST_URI constant
+ SUBJECT,
+ FROM,
+ AS2_NAME,
+ AS2_NAME,
+ structure,
+ AS2MediaType.APPLICATION_EDIFACT,
+ null,
+ null,
+ signingAlgorithm,
+ structure.isSigned() ? new java.security.cert.Certificate[] {
signingCert } : null,
+ structure.isSigned() ? signingKP.getPrivate() : null,
+ compressionAlgorithm,
+ DISPOSITION_NOTIFICATION_TO,
+ SIGNED_RECEIPT_MIC_ALGORITHMS,
+ encryptionAlgorithm,
+ structure.isEncrypted() ? new java.security.cert.Certificate[]
{ signingCert } : null,
+ null,
+ null,
+ null,
+ null,
+ null);
+ }
+}