feat(gateway-servlet): Support multiple identical keys in HTTP headers in Servlet gateways.
Some HTTP header keys can validly be repeated with different values. This behaviour was not exposed
on the Servlet platform.
DELETE
gateway/engine/beans/src/main/java/io/apiman/gateway/engine/beans/HeaderHashMap.java
-/*
- * Copyright 2014 JBoss Inc
- *
- * Licensed 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 io.apiman.gateway.engine.beans;
-
-import java.util.HashMap;
-
-/**
- * Extends the basic {@link HashMap} class in order to provide case insensitive
- * lookups.
- *
- * @author eric.wittmann@redhat.com
- */
-public class HeaderHashMap extends HashMap<String, String> {
-
- private static final long serialVersionUID = -8627183971399152775L;
-
- private HashMap<String, String> caseInsensitiveIndex = new HashMap<>();
-
- /**
- * Constructor.
- */
- public HeaderHashMap() {
- }
-
- /**
- * @see java.util.HashMap#clear()
- */
- @Override
- public void clear() {
- super.clear();
- caseInsensitiveIndex.clear();
- }
-
- /**
- * @see java.util.HashMap#get(java.lang.Object)
- */
- @Override
- public String get(Object key) {
- String rval = super.get(key);
- if (rval == null) {
- String trimmedKey = trim((String)key);
- rval = caseInsensitiveIndex.get(trimmedKey.toLowerCase());
- }
- return rval;
- }
-
- /**
- * @see java.util.HashMap#containsKey(java.lang.Object)
- */
- @Override
- public boolean containsKey(Object key) {
- boolean rval = super.containsKey(key);
- if (!rval) {
- String trimmedKey = trim((String)key);
- rval = caseInsensitiveIndex.containsKey(trimmedKey.toLowerCase());
- }
- return rval;
- }
-
- /**
- * @see java.util.HashMap#put(java.lang.Object, java.lang.Object)
- */
- @Override
- public String put(String key, String value) {
- String trimmedKey = trim(key);
- String trimmedValue = trim(value);
- String rval = super.put(trimmedKey, trimmedValue);
- caseInsensitiveIndex.put(trimmedKey.toLowerCase(), trimmedValue);
- return rval;
- }
-
- /**
- * @see java.util.HashMap#remove(java.lang.Object)
- */
- @Override
- public String remove(Object key) {
- String trimmedKey = trim((String)key);
- caseInsensitiveIndex.remove(trimmedKey.toLowerCase());
- return super.remove(trimmedKey);
- }
-
- /**
- * Trim string of whitespace.
- *
- * @param string string to trim
- * @return trimmed string, or null if null was provided.
- */
- private static String trim(String string) {
- return string == null ? null : string.trim();
- }
-
-}

MODIFY
gateway/engine/beans/src/main/java/io/apiman/gateway/engine/beans/util/CaseInsensitiveStringMultiMap.java
gateway/engine/beans/src/main/java/io/apiman/gateway/engine/beans/util/CaseInsensitiveStringMultiMap.java
@@ -125,6 +125,11 @@ public class CaseInsensitiveStringMultiMap implements IStringMultiMap, Serializa
return this;
}
+ public IStringMultiMap add(String key, List<String> values) {
+ values.forEach((value) -> add(key, value));
+ return this;
+ }
+
private Element getElement(String key) {
long hash = getHash(key);
Element head = hashArray[getIndex(hash)];

MODIFY
gateway/platforms/servlet/src/main/java/io/apiman/gateway/platforms/servlet/GatewayServlet.java
gateway/platforms/servlet/src/main/java/io/apiman/gateway/platforms/servlet/GatewayServlet.java
@@ -41,6 +41,7 @@ import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Enumeration;
+import java.util.Map.Entry;
import java.util.concurrent.CountDownLatch;
import javax.servlet.ServletException;
@@ -264,7 +265,11 @@ public abstract class GatewayServlet extends HttpServlet {
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String hname = headerNames.nextElement();
- String hval = request.getHeader(hname);
+ Enumeration<String> headerValues = request.getHeaders(hname);
+
+ while (headerValues.hasMoreElements()) {
+ String hval = headerValues.nextElement();
+
if (hname != null && hname.equalsIgnoreCase("accept") && hval != null && hval.startsWith("application/apiman.")) { //$NON-NLS-1$ //$NON-NLS-2$
if (hval.contains("+json")) { //$NON-NLS-1$
hval = "application/json"; //$NON-NLS-1$
@@ -278,6 +283,7 @@ public abstract class GatewayServlet extends HttpServlet {
srequest.getHeaders().add(hname, hval);
}
}
+ }
/**
* Writes the API response to the HTTP servlet response object.
@@ -286,11 +292,8 @@ public abstract class GatewayServlet extends HttpServlet {
*/
protected void writeResponse(HttpServletResponse response, ApiResponse sresponse) {
response.setStatus(sresponse.getCode());
- HeaderMap headers = sresponse.getHeaders();
- for (String hkey : headers.keySet()) {
- for (String hval : headers.getAll(hkey)) {
- response.addHeader(hkey, hval);
- }
+ for (Entry<String, String> entry : sresponse.getHeaders()) {
+ response.addHeader(entry.getKey(), entry.getValue());
}
}

MODIFY
gateway/platforms/servlet/src/main/java/io/apiman/gateway/platforms/servlet/components/HttpClientRequestImpl.java
gateway/platforms/servlet/src/main/java/io/apiman/gateway/platforms/servlet/components/HttpClientRequestImpl.java
@@ -17,6 +17,7 @@ package io.apiman.gateway.platforms.servlet.components;
import io.apiman.gateway.engine.async.AsyncResultImpl;
import io.apiman.gateway.engine.async.IAsyncResultHandler;
+import io.apiman.gateway.engine.beans.util.HeaderMap;
import io.apiman.gateway.engine.components.http.HttpMethod;
import io.apiman.gateway.engine.components.http.IHttpClientRequest;
import io.apiman.gateway.engine.components.http.IHttpClientResponse;
@@ -26,8 +27,6 @@ import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
-import java.util.HashMap;
-import java.util.Map;
import org.apache.commons.io.IOUtils;
@@ -40,7 +39,7 @@ public class HttpClientRequestImpl implements IHttpClientRequest {
private String endpoint;
private HttpMethod method;
- private Map<String, String> headers = new HashMap<>();
+ private HeaderMap headers = new HeaderMap();
private IAsyncResultHandler<IHttpClientResponse> handler;
private HttpURLConnection connection;
@@ -82,7 +81,7 @@ public class HttpClientRequestImpl implements IHttpClientRequest {
*/
@Override
public void addHeader(String headerName, String headerValue) {
- headers.put(headerName, headerValue);
+ headers.add(headerName, headerValue);
}
/**

MODIFY
gateway/platforms/servlet/src/main/java/io/apiman/gateway/platforms/servlet/connectors/HttpApiConnection.java
gateway/platforms/servlet/src/main/java/io/apiman/gateway/platforms/servlet/connectors/HttpApiConnection.java
@@ -210,10 +210,10 @@ public class HttpApiConnection implements IApiConnection, IApiConnectionResponse
// Set the request headers
for (Entry<String, String> entry : request.getHeaders()) {
- String hname = entry.getKey();
+ String hkey = entry.getKey();
String hval = entry.getValue();
- if (!suppressedHeaders.contains(hname)) {
- connection.setRequestProperty(hname, hval);
+ if (!suppressedHeaders.contains(hkey)) {
+ connection.addRequestProperty(hkey, hval);
}
}
@@ -369,12 +369,17 @@ public class HttpApiConnection implements IApiConnection, IApiConnectionResponse
outputStream = null;
// Process the response, convert to an ApiResponse object, and return it
response = GatewayThreadContext.getApiResponse();
- Map<String, List<String>> headerFields = connection.getHeaderFields();
- for (String headerName : headerFields.keySet()) {
+
+ // Add headers
+ for (Entry<String, List<String>> headerEntry : connection.getHeaderFields().entrySet()) {
+ String headerName = headerEntry.getKey();
+ List<String> headerValues = headerEntry.getValue();
+
if (headerName != null && !SUPPRESSED_RESPONSE_HEADERS.contains(headerName)) {
- response.getHeaders().add(headerName, connection.getHeaderField(headerName));
+ response.getHeaders().add(headerName, headerValues);
}
}
+
response.setCode(connection.getResponseCode());
response.setMessage(connection.getResponseMessage());
responseHandler.handle(AsyncResultImpl.<IApiConnectionResponse> create(this));

MODIFY
test/common/src/main/java/io/apiman/test/common/echo/EchoServerVertx.java
test/common/src/main/java/io/apiman/test/common/echo/EchoServerVertx.java
@@ -18,6 +18,7 @@ package io.apiman.test.common.echo;
import io.apiman.common.util.SimpleStringUtils;
import io.apiman.gateway.engine.beans.EngineErrorResponse;
+import io.apiman.gateway.engine.beans.util.CaseInsensitiveStringMultiMap;
import io.apiman.test.common.mock.EchoResponse;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.AsyncResult;
@@ -44,8 +45,6 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;
-import java.util.LinkedHashMap;
-import java.util.Map;
import java.util.Optional;
import javax.xml.bind.JAXBContext;
@@ -264,11 +263,10 @@ public class EchoServerVertx extends AbstractVerticle {
}
}
- // IMPORTANT: This is lossy
- private Map<String, String> multimapToMap(MultiMap headers) {
- LinkedHashMap<String, String> out = new LinkedHashMap<>();
- headers.forEach(pair -> out.put(pair.getKey(), pair.getValue()));
- return out;
+ private CaseInsensitiveStringMultiMap multimapToMap(MultiMap headers) {
+ CaseInsensitiveStringMultiMap map = new CaseInsensitiveStringMultiMap();
+ headers.forEach(pair -> map.add(pair.getKey(), pair.getValue()));
+ return map;
}
private String normaliseResource(HttpServerRequest req) {

feat(gateway-servlet): Support multiple identical keys in HTTP headers in Servlet gateways.

Some HTTP header keys can validly be repeated with different values. This behaviour was not exposed
on the Servlet platform.

Signed-off-by: Marc Savy <marc@rhymewithgravy.com>