From 2b8a7df25878f546ace25373f44baeb026cf6d2b Mon Sep 17 00:00:00 2001
From: Christof Rabensteiner <christof.rabensteiner@iaik.tugraz.at>
Date: Tue, 20 Aug 2019 11:13:28 +0200
Subject: Add Custom Http Headers to HTTP Clients

- Change app2mzs schema: Add Custom Headers to ClientType Element.
- Set custom headers in ClientFactory.
- Parse custom headers from spring environment.
- Merge custom headers from two different profiles.
- Add example to application.yaml.
- Test conversion of custom headers from spring profile
- Test merging and overriding custom headers.
---
 .../at/gv/egiz/moazs/client/ClientFactory.java     | 10 ++-
 .../moazs/preprocess/ConfigProfileGenerator.java   |  3 +-
 .../at/gv/egiz/moazs/preprocess/ConfigUtil.java    | 62 +++++++++++++++++
 src/main/resources/application.yaml                | 10 ++-
 src/main/resources/mzs/app2mzs.xsd                 |  9 +++
 src/test/java/at/gv/egiz/moazs/ConfigUtilTest.java | 80 ++++++++++++++++++++++
 6 files changed, 169 insertions(+), 5 deletions(-)
 create mode 100644 src/test/java/at/gv/egiz/moazs/ConfigUtilTest.java

(limited to 'src')

diff --git a/src/main/java/at/gv/egiz/moazs/client/ClientFactory.java b/src/main/java/at/gv/egiz/moazs/client/ClientFactory.java
index d0a445b..94eb712 100644
--- a/src/main/java/at/gv/egiz/moazs/client/ClientFactory.java
+++ b/src/main/java/at/gv/egiz/moazs/client/ClientFactory.java
@@ -1,5 +1,6 @@
 package at.gv.egiz.moazs.client;
 
+import at.gv.egiz.moazs.preprocess.ConfigUtil;
 import at.gv.egiz.moazs.util.FileUtils;
 import at.gv.egiz.moazs.util.StoreSOAPBodyBinaryInRepositoryInterceptor;
 import at.gv.zustellung.app2mzs.xsd.ClientType;
@@ -10,6 +11,7 @@ import org.apache.cxf.endpoint.Client;
 import org.apache.cxf.frontend.ClientProxy;
 import org.apache.cxf.jaxws.JaxWsClientFactoryBean;
 import org.apache.cxf.jaxws.JaxWsProxyFactoryBean;
+import org.apache.cxf.message.Message;
 import org.apache.cxf.transport.http.HTTPConduit;
 import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
 import org.slf4j.Logger;
@@ -23,6 +25,7 @@ import javax.xml.ws.BindingProvider;
 import javax.xml.ws.soap.SOAPBinding;
 
 import static at.gv.zustellung.app2mzs.xsd.KeyStoreType.keyStoreTypeBuilder;
+import static java.util.stream.Collectors.toMap;
 
 @Component
 public class ClientFactory {
@@ -32,14 +35,16 @@ public class ClientFactory {
     private final StoreSOAPBodyBinaryInRepositoryInterceptor storeResponseInterceptor;
     private final SSLContextCreator sslContextCreator;
     private final FileUtils fileUtils;
+    private final ConfigUtil configUtil;
 
     @Autowired
     public ClientFactory(StoreSOAPBodyBinaryInRepositoryInterceptor storeResponseInterceptor,
                          SSLContextCreator creator,
-                         FileUtils fileUtils) {
+                         FileUtils fileUtils, ConfigUtil configUtil) {
         this.storeResponseInterceptor = storeResponseInterceptor;
         this.sslContextCreator = creator;
         this.fileUtils = fileUtils;
+        this.configUtil = configUtil;
     }
 
     /**
@@ -68,6 +73,9 @@ public class ClientFactory {
         httpClientPolicy.setReceiveTimeout(params.getReceiveTimeout().longValueExact());
         http.setClient(httpClientPolicy);
 
+        var customHeaders = configUtil.convertHeadersToMap(params.getCustomHTTPHeader());
+        client.getRequestContext().put(Message.PROTOCOL_HEADERS, customHeaders);
+
         if (params.getURL().startsWith("https")) {
             TLSClientParameters tlsParams = setupTLSParams(params.getSSL());
             http.setTlsClientParameters(tlsParams);
diff --git a/src/main/java/at/gv/egiz/moazs/preprocess/ConfigProfileGenerator.java b/src/main/java/at/gv/egiz/moazs/preprocess/ConfigProfileGenerator.java
index 0637f98..ed67154 100644
--- a/src/main/java/at/gv/egiz/moazs/preprocess/ConfigProfileGenerator.java
+++ b/src/main/java/at/gv/egiz/moazs/preprocess/ConfigProfileGenerator.java
@@ -43,7 +43,8 @@ public class ConfigProfileGenerator {
     private ConfigProfileGenerator(
             SpringPropertiesFacade properties,
             ConfigUtil util,
-            MzsDeliveryRequestValidator validator, boolean verifyCompletenessOfDefaultConfiguration,
+            MzsDeliveryRequestValidator validator,
+            boolean verifyCompletenessOfDefaultConfiguration,
             String profilePrefix,
             String defaultConfigKey) {
         this.util = util;
diff --git a/src/main/java/at/gv/egiz/moazs/preprocess/ConfigUtil.java b/src/main/java/at/gv/egiz/moazs/preprocess/ConfigUtil.java
index f49132f..ec7b7c8 100644
--- a/src/main/java/at/gv/egiz/moazs/preprocess/ConfigUtil.java
+++ b/src/main/java/at/gv/egiz/moazs/preprocess/ConfigUtil.java
@@ -5,15 +5,19 @@ import at.gv.zustellung.app2mzs.xsd.*;
 import org.springframework.stereotype.Component;
 
 import java.math.BigInteger;
+import java.util.List;
 import java.util.Map;
+import java.util.stream.Stream;
 
 import static at.gv.zustellung.app2mzs.xsd.ClientType.clientTypeBuilder;
 import static at.gv.zustellung.app2mzs.xsd.ConfigType.configTypeBuilder;
+import static at.gv.zustellung.app2mzs.xsd.CustomHTTPHeaderType.*;
 import static at.gv.zustellung.app2mzs.xsd.ForwardResponseToServiceType.forwardResponseToServiceTypeBuilder;
 import static at.gv.zustellung.app2mzs.xsd.KeyStoreType.keyStoreTypeBuilder;
 import static at.gv.zustellung.app2mzs.xsd.MsgResponseSinksType.msgResponseSinksTypeBuilder;
 import static at.gv.zustellung.app2mzs.xsd.SSLType.SSLTypeBuilder;
 import static at.gv.zustellung.app2mzs.xsd.SaveResponseToFileType.saveResponseToFileTypeBuilder;
+import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toMap;
 
 @Component
@@ -41,6 +45,7 @@ public class ConfigUtil {
     public static final String FORWARD_RESPONSE_TO_SERVICE_KEY = "forward-response-to-service";
     public static final String MZS_CLIENT_KEY = "mzs-client";
     public static final String SERVICE_TIMEOUT_KEY = "service-timeout";
+    public static final String CUSTOM_HTTP_HEADERS_KEY = "custom-http-headers";
 
 
     /**
@@ -95,11 +100,16 @@ public class ConfigUtil {
         SSLType ssl = sslParams.isEmpty()
                 ? null : buildSSL(sslParams);
 
+        var customHeaderParams = filterMapByPrefix(clientParams, CUSTOM_HTTP_HEADERS_KEY);
+        var customHeaders = customHeaderParams.isEmpty()
+                ? null : convertMapToHeaders(customHeaderParams);
+
         return clientTypeBuilder()
                 .withURL(url)
                 .withSSL(ssl)
                 .withConnectionTimeout(connectionTimeout)
                 .withReceiveTimeout(receiveTimeout)
+                .withCustomHTTPHeader(customHeaders)
                 .build();
 
     }
@@ -243,6 +253,10 @@ public class ConfigUtil {
             builder.withReceiveTimeout(primary.getReceiveTimeout());
         }
 
+        if (primary.getCustomHTTPHeader() != null) {
+            builder.withCustomHTTPHeader(merge(primary.getCustomHTTPHeader(), fallback.getCustomHTTPHeader()));
+        }
+
         return builder.build();
     }
 
@@ -282,6 +296,26 @@ public class ConfigUtil {
 
     }
 
+    private List<CustomHTTPHeaderType> merge(List<CustomHTTPHeaderType> primary, List<CustomHTTPHeaderType> fallback) {
+
+        if (fallback == null) return primary;
+
+        var primaryMap = convertHeadersToMap(primary);
+        var fallbackMap = convertHeadersToMap(fallback);
+
+        Map<String, String> mergedMap = Stream.of(fallbackMap, primaryMap)
+                .flatMap(map -> map.entrySet().stream())
+                .collect(toMap(
+                        Map.Entry::getKey,
+                        entry -> entry.getValue().get(0),
+                        (v1, v2) -> v2
+                ));
+
+        return convertMapToHeaders(mergedMap);
+
+    }
+
+
     private MsgResponseSinksType merge(MsgResponseSinksType primary, MsgResponseSinksType fallback) {
 
         if (fallback == null) {
@@ -340,4 +374,32 @@ public class ConfigUtil {
 
     }
 
+    /**
+     * Convert a Map that is indexed by header names into a List of CustomHTTPHeaders.
+     * @param customHeaderMap map of headers, indexed by header names.
+     * @return List of CustomHTTPHeaders
+     */
+    public List<CustomHTTPHeaderType> convertMapToHeaders(Map<String, String> customHeaderMap) {
+        return customHeaderMap.entrySet().stream()
+                .map(e -> customHTTPHeaderTypeBuilder()
+                        .withName(e.getKey())
+                        .withValue(e.getValue())
+                        .build())
+                .collect(toList());
+    }
+
+
+    /**
+     * Convert a List of CustomHTTPHeaders into a Map that is indexed by the header names. Values are wrapped in Lists.
+     * @param customHeaders List of CustomHTTPHeaders
+     * @return Map of headers, indexed by header names.
+     */
+    public Map<String, List<String>> convertHeadersToMap(List<CustomHTTPHeaderType> customHeaders) {
+        return customHeaders
+                .stream()
+                .collect(toMap(
+                        h -> h.getName(),
+                        h -> List.of(h.getValue())));
+    }
+
 }
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index ce4d892..0380cf5 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -61,11 +61,15 @@ delivery-request-configuration-profiles:
 
       # Mandatory
       url: http://localhost:8082/tnvz/
+      connection-timeout: 0
+      receive-timeout: 0
 
       # Optional
-      # ssl: ...
-      # connection-timeout: ...
-      # receive-timeout: ...
+      #ssl: ...
+      #custom-http-headers:
+      #  X-PVP-NAME-1: VALUE-X
+      #  X-PVP-NAME-2: VALUE-Y
+      #  ...
 
     # Mandatory
     # Parameters for the connection to msg.
diff --git a/src/main/resources/mzs/app2mzs.xsd b/src/main/resources/mzs/app2mzs.xsd
index d19ff3b..7ae243b 100644
--- a/src/main/resources/mzs/app2mzs.xsd
+++ b/src/main/resources/mzs/app2mzs.xsd
@@ -3,6 +3,7 @@
 	<xs:import namespace="http://reference.e-government.gv.at/namespace/zustellung/mzs/persondata#" schemaLocation="mzs_mypersondata_en.xsd"/>
 	<xs:import namespace="http://reference.e-government.gv.at/namespace/zustellung/msg/phase2/20181206#" schemaLocation="../zusemsg/zuse_p2.xsd"/>
 	<xs:import namespace="http://reference.e-government.gv.at/namespace/zustellung/tnvz/phase2/20181206#" schemaLocation="../zusetnvz/zusetnvz_p2.xsd"/>
+
 	<xs:element name="DeliveryRequest" type="DeliveryRequestType"/>
 	<xs:complexType name="DeliveryRequestType">
 		<xs:sequence>
@@ -98,6 +99,7 @@
 			<xs:element ref="SSL" minOccurs="0"/>
 			<xs:element name="ConnectionTimeout" minOccurs="0" type="xs:nonNegativeInteger" />
 			<xs:element name="ReceiveTimeout" minOccurs="0" type="xs:nonNegativeInteger" />
+			<xs:element ref="CustomHTTPHeader" minOccurs="0" maxOccurs="unbounded"/>
 		</xs:sequence>
 	</xs:complexType>
 	<xs:element name="SSL" type="SSLType" />
@@ -118,6 +120,13 @@
 			<xs:element name="FileType" type="xs:string" minOccurs="0"/>
 		</xs:sequence>
 	</xs:complexType>
+	<xs:element name="CustomHTTPHeader" type="CustomHTTPHeaderType"/>
+	<xs:complexType name="CustomHTTPHeaderType">
+		<xs:sequence>
+			<xs:element name="Name" type="xs:string" />
+			<xs:element name="Value" type="xs:string" />
+		</xs:sequence>
+	</xs:complexType>
 	<xs:element name="MsgResponseSinks" type="MsgResponseSinksType"/>
 	<xs:complexType name="MsgResponseSinksType">
 		<xs:sequence>
diff --git a/src/test/java/at/gv/egiz/moazs/ConfigUtilTest.java b/src/test/java/at/gv/egiz/moazs/ConfigUtilTest.java
new file mode 100644
index 0000000..d7ac0a1
--- /dev/null
+++ b/src/test/java/at/gv/egiz/moazs/ConfigUtilTest.java
@@ -0,0 +1,80 @@
+package at.gv.egiz.moazs;
+
+import at.gv.egiz.moazs.preprocess.ConfigUtil;
+import at.gv.zustellung.app2mzs.xsd.ConfigType;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static at.gv.zustellung.app2mzs.xsd.ClientType.clientTypeBuilder;
+import static at.gv.zustellung.app2mzs.xsd.ConfigType.configTypeBuilder;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ConfigUtilTest {
+
+    private ConfigUtil configUtil;
+
+    @Before
+    public void setup() {
+        configUtil = new ConfigUtil();
+    }
+
+    @Test
+    public void convertTNVZClientsCustomHttpHeaders() {
+        var prefix = ConfigUtil.TNVZ_CLIENT_KEY + "." + ConfigUtil.CUSTOM_HTTP_HEADERS_KEY + ".";
+        var map = Map.of(
+                prefix + "X-PVP-VERSION", "2.1",
+                prefix + "X-PVP-BINDING", "http",
+                prefix + "X-PVP-PARTICIPANT_ID", "AT:L9:MA2412"
+        );
+
+        ConfigType config = configUtil.convert(map);
+        var customHeaderMap = configUtil.convertHeadersToMap(config.getTNVZClient().getCustomHTTPHeader());
+
+        assertThat(customHeaderMap).containsEntry("X-PVP-VERSION", List.of("2.1"));
+        assertThat(customHeaderMap).containsEntry("X-PVP-BINDING", List.of("http"));
+        assertThat(customHeaderMap).containsEntry("X-PVP-PARTICIPANT_ID", List.of("AT:L9:MA2412"));
+        assertThat(customHeaderMap).hasSize(3);
+    }
+
+    @Test
+    public void overrideCustomHeaders() {
+
+        var fallbackConfig = createConfig(Map.of(
+                "X-header-1", "fallback-value-a",
+                "X-header-2", "fallback-value-b"
+        ));
+
+        var primaryConfig = createConfig(Map.of(
+                "X-header-2", "primary-value-c",
+                "X-header-3", "primary-value-d"
+        ));
+
+        ConfigType mergedConfig = configUtil.merge(primaryConfig, fallbackConfig);
+        var mergedHeaderMap = configUtil.convertHeadersToMap(mergedConfig.getTNVZClient().getCustomHTTPHeader());
+
+        assertThat(mergedHeaderMap).containsEntry("X-header-1", List.of("fallback-value-a"));
+        assertThat(mergedHeaderMap).containsEntry("X-header-2", List.of("primary-value-c"));
+        assertThat(mergedHeaderMap).containsEntry("X-header-3", List.of("primary-value-d"));
+        assertThat(mergedHeaderMap).hasSize(3);
+
+    }
+
+    private ConfigType createConfig(Map<String, String> headerMap) {
+
+        var headers = configUtil.convertMapToHeaders(headerMap);
+
+        var client = clientTypeBuilder()
+                .withCustomHTTPHeader(headers)
+                .build();
+
+        return configTypeBuilder()
+                .withTNVZClient(client)
+                .build();
+    }
+
+
+
+}
-- 
cgit v1.2.3