KNOX-1728 - Allow custom parameters to be passed to dispatches
authorKevin Risden <krisden@apache.org>
Mon, 19 Nov 2018 21:33:27 +0000 (16:33 -0500)
committerKevin Risden <krisden@apache.org>
Tue, 8 Jan 2019 14:32:27 +0000 (09:32 -0500)
Signed-off-by: Kevin Risden <krisden@apache.org>
gateway-server/src/main/java/org/apache/knox/gateway/deploy/impl/ServiceDefinitionDeploymentContributor.java
gateway-server/src/main/java/org/apache/knox/gateway/topology/xml/KnoxFormatXmlTopologyRules.java
gateway-server/src/test/java/org/apache/knox/gateway/deploy/impl/ServiceDefinitionDeploymentContributorTest.java
gateway-server/src/test/java/org/apache/knox/gateway/topology/xml/TopologyRulesModuleTest.java
gateway-server/src/test/resources/org/apache/knox/gateway/topology/xml/TopologyRulesModuleTest/topology-with-dispatch-parameters.xml [new file with mode: 0644]
gateway-service-definitions/src/main/java/org/apache/knox/gateway/service/definition/CustomDispatch.java
gateway-service-definitions/src/main/java/org/apache/knox/gateway/service/definition/DispatchParam.java [new file with mode: 0644]
gateway-spi/src/main/java/org/apache/knox/gateway/deploy/ServiceDeploymentContributorBase.java
gateway-test-utils/src/main/java/org/apache/knox/test/mock/MockHttpServletResponse.java

index 63b22a5..b192ee0 100644 (file)
@@ -38,6 +38,7 @@ import org.apache.knox.gateway.topology.Version;
 
 import java.net.URISyntaxException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
@@ -200,25 +201,26 @@ public class ServiceDefinitionDeploymentContributor extends ServiceDeploymentCon
       String haClassName = customDispatch.getHaClassName();
       String httpClientFactory = customDispatch.getHttpClientFactory();
       boolean useTwoWaySsl = customDispatch.getUseTwoWaySsl();
+      Map<String, String> dispatchParams = customDispatch.getParams();
       if ( isHaEnabled) {
         if (haContributorName != null) {
-          addDispatchFilter(context, service, resource, DISPATCH_ROLE, haContributorName);
+          addDispatchFilter(context, service, resource, DISPATCH_ROLE, haContributorName, dispatchParams);
         } else if (haClassName != null) {
-          addDispatchFilterForClass(context, service, resource, haClassName, httpClientFactory, useTwoWaySsl);
+          addDispatchFilterForClass(context, service, resource, haClassName, httpClientFactory, useTwoWaySsl, dispatchParams);
         } else {
-          addDefaultHaDispatchFilter(context, service, resource);
+          addDefaultHaDispatchFilter(context, service, resource, dispatchParams);
         }
       } else {
         String contributorName = customDispatch.getContributorName();
         if ( contributorName != null ) {
-          addDispatchFilter(context, service, resource, DISPATCH_ROLE, contributorName);
+          addDispatchFilter(context, service, resource, DISPATCH_ROLE, contributorName, dispatchParams);
         } else {
           String className = customDispatch.getClassName();
           if ( className != null ) {
-            addDispatchFilterForClass(context, service, resource, className, httpClientFactory, useTwoWaySsl);
+            addDispatchFilterForClass(context, service, resource, className, httpClientFactory, useTwoWaySsl, dispatchParams);
           } else {
             //final fallback to the default dispatch
-            addDispatchFilter(context, service, resource, DISPATCH_ROLE, "http-client");
+            addDispatchFilter(context, service, resource, DISPATCH_ROLE, "http-client", dispatchParams);
           }
         }
       }
@@ -230,16 +232,31 @@ public class ServiceDefinitionDeploymentContributor extends ServiceDeploymentCon
   }
 
   private void addDefaultHaDispatchFilter(DeploymentContext context, Service service, ResourceDescriptor resource) {
-    FilterDescriptor filter = addDispatchFilterForClass(context, service, resource, DEFAULT_HA_DISPATCH_CLASS, null);
+    addDefaultHaDispatchFilter(context, service, resource, Collections.emptyMap());
+  }
+
+  private void addDefaultHaDispatchFilter(DeploymentContext context, Service service, ResourceDescriptor resource,
+                                          Map<String, String> dispatchParams) {
+    FilterDescriptor filter = addDispatchFilterForClass(context, service, resource, DEFAULT_HA_DISPATCH_CLASS, null, dispatchParams);
     filter.param().name(SERVICE_ROLE_PARAM).value(service.getRole());
   }
 
-  private FilterDescriptor addDispatchFilterForClass(DeploymentContext context, Service service, ResourceDescriptor resource, String dispatchClass, String httpClientFactory, boolean useTwoWaySsl) {
+  private FilterDescriptor addDispatchFilterForClass(DeploymentContext context, Service service,
+                                                     ResourceDescriptor resource, String dispatchClass,
+                                                     String httpClientFactory, boolean useTwoWaySsl,
+                                                     Map<String, String> dispatchParams) {
     FilterDescriptor filter = resource.addFilter().name(getName()).role(DISPATCH_ROLE).impl(GatewayDispatchFilter.class);
     filter.param().name(DISPATCH_IMPL_PARAM).value(dispatchClass);
     if (httpClientFactory != null) {
       filter.param().name(HTTP_CLIENT_FACTORY_PARAM).value(httpClientFactory);
     }
+
+    if(dispatchParams != null) {
+      for ( Map.Entry<String, String> dispatchParam : dispatchParams.entrySet() ) {
+        filter.param().name(dispatchParam.getKey()).value(dispatchParam.getValue());
+      }
+    }
+
     // let's take the value of useTwoWaySsl which is derived from the service definition
     // then allow it to be overridden by service params from the topology
     filter.param().name("useTwoWaySsl").value(Boolean.toString(useTwoWaySsl));
@@ -256,8 +273,10 @@ public class ServiceDefinitionDeploymentContributor extends ServiceDeploymentCon
     return filter;
   }
 
-  private FilterDescriptor addDispatchFilterForClass(DeploymentContext context, Service service, ResourceDescriptor resource, String dispatchClass, String httpClientFactory) {
-    return addDispatchFilterForClass(context, service, resource, dispatchClass, httpClientFactory, false);
+  private FilterDescriptor addDispatchFilterForClass(DeploymentContext context, Service service,
+                                                     ResourceDescriptor resource, String dispatchClass,
+                                                     String httpClientFactory, Map<String, String> dispatchParams) {
+    return addDispatchFilterForClass(context, service, resource, dispatchClass, httpClientFactory, false, dispatchParams);
   }
 
   private boolean isHaEnabled(DeploymentContext context) {
index 82d9806..7da6955 100644 (file)
@@ -20,6 +20,7 @@ package org.apache.knox.gateway.topology.xml;
 import org.apache.commons.digester3.Rule;
 import org.apache.commons.digester3.binder.AbstractRulesModule;
 import org.apache.knox.gateway.service.definition.CustomDispatch;
+import org.apache.knox.gateway.service.definition.DispatchParam;
 import org.apache.knox.gateway.topology.Application;
 import org.apache.knox.gateway.topology.Param;
 import org.apache.knox.gateway.topology.Provider;
@@ -54,6 +55,7 @@ public class KnoxFormatXmlTopologyRules extends AbstractRulesModule {
   private static final String USE_TWO_WAY_SSL = "use-two-way-ssl";
 
   private static final Rule paramRule = new ParamRule();
+  private static final Rule dispatchParamRule = new DispatchParamRule();
 
   @Override
   protected void configure() {
@@ -88,6 +90,9 @@ public class KnoxFormatXmlTopologyRules extends AbstractRulesModule {
     forPattern( ROOT_TAG + "/" + SERVICE_TAG + "/" + DISPATCH_TAG + "/" + HA_CLASSNAME ).callMethod( "setHaClassName" ).usingElementBodyAsArgument();
     forPattern( ROOT_TAG + "/" + SERVICE_TAG + "/" + DISPATCH_TAG + "/" + HTTP_CLIENT_FACTORY ).callMethod( "setHttpClientFactory" ).usingElementBodyAsArgument();
     forPattern( ROOT_TAG + "/" + SERVICE_TAG + "/" + DISPATCH_TAG + "/" + USE_TWO_WAY_SSL ).callMethod( "setUseTwoWaySsl" ).usingElementBodyAsArgument();
+    forPattern( ROOT_TAG + "/" + SERVICE_TAG + "/" + DISPATCH_TAG + "/" + PARAM_TAG ).createObject().ofType( DispatchParam.class ).then().addRule( dispatchParamRule ).then().setNext( "addParam" );
+    forPattern( ROOT_TAG + "/" + SERVICE_TAG + "/" + DISPATCH_TAG + "/" + PARAM_TAG + "/" + NAME_TAG ).setBeanProperty();
+    forPattern( ROOT_TAG + "/" + SERVICE_TAG + "/" + DISPATCH_TAG + "/" + PARAM_TAG + "/" + VALUE_TAG ).setBeanProperty();
 
     forPattern( ROOT_TAG + "/" + PROVIDER_TAG ).createObject().ofType( Provider.class ).then().setNext( "addProvider" );
     forPattern( ROOT_TAG + "/" + PROVIDER_TAG + "/" + ROLE_TAG ).setBeanProperty();
@@ -100,7 +105,6 @@ public class KnoxFormatXmlTopologyRules extends AbstractRulesModule {
   }
 
   private static class ParamRule extends Rule {
-
     @Override
     public void begin( String namespace, String name, Attributes attributes ) {
       Param param = getDigester().peek();
@@ -110,7 +114,17 @@ public class KnoxFormatXmlTopologyRules extends AbstractRulesModule {
         param.setValue( attributes.getValue( "value" ) );
       }
     }
-
   }
 
+  private static class DispatchParamRule extends Rule {
+    @Override
+    public void begin( String namespace, String name, Attributes attributes ) {
+      DispatchParam param = getDigester().peek();
+      String paramName = attributes.getValue( "name" );
+      if( paramName != null ) {
+        param.setName( paramName );
+        param.setValue( attributes.getValue( "value" ) );
+      }
+    }
+  }
 }
index 10c635f..c8e7c60 100644 (file)
@@ -278,6 +278,94 @@ public class ServiceDefinitionDeploymentContributorTest {
 
   }
 
+  @Test
+  public void testServiceAttributeParameters() throws Exception {
+    final String TEST_SERVICE_ROLE     = "Test";
+
+    UrlRewriteRulesDescriptor clusterRules = EasyMock.createNiceMock(UrlRewriteRulesDescriptor.class);
+    EasyMock.replay(clusterRules);
+
+    UrlRewriteRulesDescriptor svcRules = EasyMock.createNiceMock(UrlRewriteRulesDescriptor.class);
+    EasyMock.replay(svcRules);
+
+    ServiceDefinition svcDef = EasyMock.createNiceMock(ServiceDefinition.class);
+    EasyMock.expect(svcDef.getRole()).andReturn(TEST_SERVICE_ROLE).anyTimes();
+    List<Route> svcRoutes = new ArrayList<>();
+    Route route = EasyMock.createNiceMock(Route.class);
+    List<Rewrite> filters = new ArrayList<>();
+    EasyMock.expect(route.getRewrites()).andReturn(filters).anyTimes();
+    svcRoutes.add(route);
+    EasyMock.replay(route);
+    EasyMock.expect(svcDef.getRoutes()).andReturn(svcRoutes).anyTimes();
+    CustomDispatch cd = EasyMock.createNiceMock(CustomDispatch.class);
+    EasyMock.expect(cd.getClassName()).andReturn("TestDispatch").anyTimes();
+    EasyMock.expect(cd.getHaClassName()).andReturn("TestHADispatch").anyTimes();
+    EasyMock.expect(cd.getHaContributorName()).andReturn(null).anyTimes();
+
+    EasyMock.replay(cd);
+    EasyMock.expect(svcDef.getDispatch()).andReturn(cd).anyTimes();
+    EasyMock.replay(svcDef);
+
+    ServiceDefinitionDeploymentContributor sddc = new ServiceDefinitionDeploymentContributor(svcDef, svcRules);
+
+    DeploymentContext context = EasyMock.createNiceMock(DeploymentContext.class);
+    EasyMock.expect(context.getDescriptor("rewrite")).andReturn(clusterRules).anyTimes();
+    GatewayConfig gc = EasyMock.createNiceMock(GatewayConfig.class);
+    EasyMock.expect(gc.isXForwardedEnabled()).andReturn(false).anyTimes();
+    EasyMock.expect(gc.isCookieScopingToPathEnabled()).andReturn(false).anyTimes();
+    EasyMock.replay(gc);
+    EasyMock.expect(context.getGatewayConfig()).andReturn(gc).anyTimes();
+
+    // Configure the HaProvider
+    Topology topology = EasyMock.createNiceMock(Topology.class);
+    List<Provider> providers = new ArrayList<>();
+    Provider haProvider = EasyMock.createNiceMock(Provider.class);
+    EasyMock.expect(haProvider.getRole()).andReturn("ha").anyTimes();
+    EasyMock.expect(haProvider.isEnabled()).andReturn(true).anyTimes();
+    Map<String, String> providerParams = new HashMap<>();
+    providerParams.put(TEST_SERVICE_ROLE, "whatever");
+    EasyMock.expect(haProvider.getParams()).andReturn(providerParams).anyTimes();
+
+    EasyMock.replay(haProvider);
+    providers.add(haProvider);
+    EasyMock.expect(topology.getProviders()).andReturn(providers).anyTimes();
+    EasyMock.replay(topology);
+    EasyMock.expect(context.getTopology()).andReturn(topology).anyTimes();
+
+    TestGatewayDescriptor gd = new TestGatewayDescriptor();
+    EasyMock.expect(context.getGatewayDescriptor()).andReturn(gd).anyTimes();
+    EasyMock.replay(context);
+
+    // Configure the service with the useTwoWaySsl param to OVERRIDE the value in the service definition
+    Service service = EasyMock.createNiceMock(Service.class);
+    Map<String, String> svcParams = new HashMap<>();
+    svcParams.put("test1", "test1abc");
+    svcParams.put("test2", "test2def");
+    EasyMock.expect(service.getParams()).andReturn(svcParams).anyTimes();
+    EasyMock.replay(service);
+
+    sddc.contributeService(context, service);
+
+    assertEquals(1, gd.resources().size());
+    ResourceDescriptor res = gd.resources().get(0);
+    assertNotNull(res);
+    List<FilterDescriptor> filterList = res.filters();
+    assertEquals(1, filterList.size());
+    FilterDescriptor f = filterList.get(0);
+    assertNotNull(f);
+    assertEquals("dispatch", f.role());
+    List<FilterParamDescriptor> fParams = f.params();
+    assertNotNull(fParams);
+
+    Map<String, String> fparamKeyVal = new HashMap<>();
+    for(FilterParamDescriptor fparam : fParams) {
+      fparamKeyVal.put(fparam.name(), fparam.value());
+    }
+
+    assertEquals("test1abc", fparamKeyVal.get("test1"));
+    assertEquals("test2def", fparamKeyVal.get("test2"));
+  }
+
   private static class TestGatewayDescriptor extends GatewayDescriptorImpl {
   }
 
index c6447e5..8e5e1e8 100644 (file)
@@ -277,5 +277,33 @@ public class TopologyRulesModuleTest {
     assertThat( dispatch.getHaClassName(), is("testHAClassName") );
     assertThat( dispatch.getHttpClientFactory(), is("testHttpClientFactory") );
     assertThat( dispatch.getUseTwoWaySsl(), is(true) );
+    assertThat( dispatch.getParams().size(), is(0) );
+  }
+
+  @Test
+  public void testParseTopologyWithDispatchParameters() throws IOException, SAXException {
+    final Digester digester = loader.newDigester();
+    final String name = "topology-with-dispatch-parameters.xml";
+    final URL url = TestUtils.getResourceUrl( TopologyRulesModuleTest.class, name );
+    assertThat( "Failed to find URL for resource " + name, url, notNullValue() );
+    final File file = new File( url.getFile() );
+    final TopologyBuilder topologyBuilder = digester.parse( url );
+    final Topology topology = topologyBuilder.build();
+    assertThat( "Failed to parse resource " + name, topology, notNullValue() );
+    topology.setTimestamp( file.lastModified() );
+
+    final Collection<Service> services =  topology.getServices();
+    final CustomDispatch dispatch = services.iterator().next().getDispatch();
+
+    assertThat( "Failed to find dispatch", dispatch, notNullValue() );
+    assertThat( dispatch.getContributorName(), is("testContributor") );
+    assertThat( dispatch.getHaContributorName(), is("testHAContributor") );
+    assertThat( dispatch.getClassName(), is("org.apache.hadoop.gateway.hbase.HBaseDispatch") );
+    assertThat( dispatch.getHaClassName(), is("testHAClassName") );
+    assertThat( dispatch.getHttpClientFactory(), is("testHttpClientFactory") );
+    assertThat( dispatch.getUseTwoWaySsl(), is(true) );
+    assertThat( dispatch.getParams().size(), is(2) );
+    assertThat( dispatch.getParams().get("abc"), is("def") );
+    assertThat( dispatch.getParams().get("ghi"), is("123") );
   }
 }
diff --git a/gateway-server/src/test/resources/org/apache/knox/gateway/topology/xml/TopologyRulesModuleTest/topology-with-dispatch-parameters.xml b/gateway-server/src/test/resources/org/apache/knox/gateway/topology/xml/TopologyRulesModuleTest/topology-with-dispatch-parameters.xml
new file mode 100644 (file)
index 0000000..d0dc78d
--- /dev/null
@@ -0,0 +1,39 @@
+<!--
+  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.
+-->
+<topology>
+    <name>test-topology-name</name>
+    <service>
+        <role>TestRole</role>
+        <url>http://localhost:8081</url>
+        <dispatch>
+            <contributor-name>testContributor</contributor-name>
+            <ha-contributor-name>testHAContributor</ha-contributor-name>
+            <classname>org.apache.hadoop.gateway.hbase.HBaseDispatch</classname>
+            <ha-classname>testHAClassName</ha-classname>
+            <http-client-factory>testHttpClientFactory</http-client-factory>
+            <use-two-way-ssl>true</use-two-way-ssl>
+            <param>
+                <name>abc</name>
+                <value>def</value>
+            </param>
+            <param>
+                <name>ghi</name>
+                <value>123</value>
+            </param>
+        </dispatch>
+    </service>
+</topology>
index 35ed198..bad7879 100644 (file)
  */
 package org.apache.knox.gateway.service.definition;
 
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
 import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlType;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
 
 @XmlType(name = "dispatch")
 public class CustomDispatch {
@@ -35,6 +42,9 @@ public class CustomDispatch {
 
   private boolean useTwoWaySsl;
 
+  @XmlElement(name = "param")
+  private List<XMLParam> params = new ArrayList<>();
+
   @XmlAttribute(name = "contributor-name")
   public String getContributorName() {
     return contributorName;
@@ -93,4 +103,43 @@ public class CustomDispatch {
   public void setUseTwoWaySsl(String useTwoWaySsl) {
     this.useTwoWaySsl = Boolean.parseBoolean(useTwoWaySsl);
   }
+
+  public void addParam( DispatchParam param ) {
+    params.add(new XMLParam(param.getName(), param.getValue()));
+  }
+
+  public Map<String, String> getParams() {
+    Map<String, String> result = new LinkedHashMap<>();
+    if( params != null ) {
+      for (XMLParam p : params) {
+        result.put(p.name, p.value);
+      }
+      }
+    return result;
+  }
+
+  @XmlAccessorType(XmlAccessType.NONE)
+  private static class XMLParam {
+    XMLParam() {
+
+    }
+
+    XMLParam(String name, String value) {
+      this.name = name;
+      this.value = value;
+    }
+    @XmlElement
+    private String name;
+
+    @XmlElement
+    private String value;
+
+    String getName() {
+      return name;
+    }
+
+    String getValue() {
+      return value;
+    }
+  }
 }
diff --git a/gateway-service-definitions/src/main/java/org/apache/knox/gateway/service/definition/DispatchParam.java b/gateway-service-definitions/src/main/java/org/apache/knox/gateway/service/definition/DispatchParam.java
new file mode 100644 (file)
index 0000000..2562d8d
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * 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.knox.gateway.service.definition;
+
+public class DispatchParam {
+
+  private String name;
+  private String value;
+
+  public DispatchParam() {
+  }
+
+  public DispatchParam(String name, String value) {
+    this.name = name;
+    this.value = value;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public String getValue() {
+    return value;
+  }
+
+  public void setValue(String value) {
+    this.value = value;
+  }
+}
index 9f3b7a8..e803112 100644 (file)
@@ -24,8 +24,11 @@ import org.apache.knox.gateway.topology.Service;
 import org.apache.knox.gateway.topology.Version;
 
 import java.net.URISyntaxException;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 
 public abstract class ServiceDeploymentContributorBase extends DeploymentContributorBase implements ServiceDeploymentContributor {
 
@@ -107,10 +110,22 @@ public abstract class ServiceDeploymentContributorBase extends DeploymentContrib
   }
 
   protected void addDispatchFilter(DeploymentContext context, Service service, ResourceDescriptor resource, String role, String name ) {
+    addDispatchFilter(context, service, resource, role, name, Collections.emptyMap());
+  }
+
+  protected void addDispatchFilter(DeploymentContext context, Service service, ResourceDescriptor resource, String role,
+                                   String name, Map<String, String> dispatchParams) {
+    List<FilterParamDescriptor> params = new ArrayList<>();
+    if(dispatchParams != null) {
+      for ( Map.Entry<String, String> dispatchParam : dispatchParams.entrySet() ) {
+        params.add(resource.createFilterParam().name(dispatchParam.getKey()).value(dispatchParam.getValue()));
+      }
+    }
+
     if (name == null) {
       name = "http-client";
     }
-    context.contributeFilter( service, resource, role, name, null );
+    context.contributeFilter( service, resource, role, name, params );
   }
 
 }
index efb2ec3..423449d 100644 (file)
@@ -23,17 +23,22 @@ import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.Locale;
+import java.util.Map;
 
 public class MockHttpServletResponse implements HttpServletResponse {
 
+  private Map<String, String> headers = new HashMap<>();
+
   @Override
   public void addCookie( Cookie cookie ) {
   }
 
   @Override
   public boolean containsHeader( String s ) {
-    return false;
+    return headers.containsKey(s);
   }
 
   @Override
@@ -78,11 +83,13 @@ public class MockHttpServletResponse implements HttpServletResponse {
   }
 
   @Override
-  public void setHeader( String s, String s1 ) {
+  public void setHeader( String name, String value ) {
+    headers.put(name, value);
   }
 
   @Override
-  public void addHeader( String s, String s1 ) {
+  public void addHeader( String name, String value ) {
+    headers.put(name, value);
   }
 
   @Override
@@ -109,17 +116,17 @@ public class MockHttpServletResponse implements HttpServletResponse {
 
   @Override
   public String getHeader( String s ) {
-    return null;
+    return headers.get(s);
   }
 
   @Override
   public Collection<String> getHeaders( String s ) {
-    return null;
+    return Collections.singletonList(headers.get(s));
   }
 
   @Override
   public Collection<String> getHeaderNames() {
-    return null;
+    return headers.keySet();
   }
 
   @Override