diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index 71bcf3045e7..85870317092 100644 --- a/solr/CHANGES.txt +++ b/solr/CHANGES.txt @@ -145,6 +145,11 @@ Bug Fixes * SOLR-6039: fixed debug output when no results in response (Tomás Fernández Löbbe, hossman) +New Features +---------------------- + +* SOLR-6043: Add ability to set http headers in solr response + (Tomás Fernández Löbbe via Ryan Ernst) Other Changes --------------------- diff --git a/solr/core/src/java/org/apache/solr/response/SolrQueryResponse.java b/solr/core/src/java/org/apache/solr/response/SolrQueryResponse.java index 5d51ebdf7b7..a1cf0c4fc69 100644 --- a/solr/core/src/java/org/apache/solr/response/SolrQueryResponse.java +++ b/solr/core/src/java/org/apache/solr/response/SolrQueryResponse.java @@ -17,13 +17,19 @@ package org.apache.solr.response; +import java.util.Collection; +import java.util.Date; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; + +import javax.servlet.http.HttpServletResponse; + import org.apache.solr.common.util.NamedList; import org.apache.solr.common.util.SimpleOrderedMap; import org.apache.solr.search.ReturnFields; import org.apache.solr.search.SolrReturnFields; -import java.util.*; - /** * SolrQueryResponse is used by a query handler to return * the response to a query request. @@ -77,6 +83,15 @@ public class SolrQueryResponse { protected NamedList toLog = new SimpleOrderedMap<>(); protected ReturnFields returnFields; + + /** + * Container for storing HTTP headers. Internal Solr components can add headers to + * this SolrQueryResponse through the methods: {@link #addHttpHeader(String, String)} + * and {@link #setHttpHeader(String, String)}, or remove existing ones through + * {@link #removeHttpHeader(String)} and {@link #removeHttpHeaders(String)}. + * All these headers are going to be added to the HTTP response. + */ + private final NamedList headers = new SimpleOrderedMap<>(); // error if this is set... protected Exception err; @@ -245,4 +260,110 @@ public class SolrQueryResponse { public boolean isHttpCaching() { return this.httpCaching; } + + /** + * + * Sets a response header with the given name and value. This header + * will be included in the HTTP response + * If the header had already been set, the new value overwrites the + * previous ones (all of them if there are multiple for the same name). + * + * @param name the name of the header + * @param value the header value If it contains octet string, + * it should be encoded according to RFC 2047 + * (http://www.ietf.org/rfc/rfc2047.txt) + * + * @see #addHttpHeader + * @see HttpServletResponse#setHeader + */ + public void setHttpHeader(String name, String value) { + headers.removeAll(name); + headers.add(name, value); + } + + /** + * Adds a response header with the given name and value. This header + * will be included in the HTTP response + * This method allows response headers to have multiple values. + * + * @param name the name of the header + * @param value the additional header value If it contains + * octet string, it should be encoded + * according to RFC 2047 + * (http://www.ietf.org/rfc/rfc2047.txt) + * + * @see #setHttpHeader + * @see HttpServletResponse#addHeader + */ + public void addHttpHeader(String name, String value) { + headers.add(name, value); + } + + /** + * Gets the value of the response header with the given name. + * + *

If a response header with the given name exists and contains + * multiple values, the value that was added first will be returned.

+ * + *

NOTE: this runs in linear time (it scans starting at the + * beginning of the list until it finds the first pair with + * the specified name).

+ * + * @param name the name of the response header whose value to return + * @return the value of the response header with the given name, + * or null if no header with the given name has been set + * on this response + */ + public String getHttpHeader(String name) { + return headers.get(name); + } + + /** + * Gets the values of the response header with the given name. + * + * @param name the name of the response header whose values to return + * + * @return a (possibly empty) Collection of the values + * of the response header with the given name + * + */ + public Collection getHttpHeaders(String name) { + return headers.getAll(name); + } + + /** + * Removes a previously added header with the given name (only + * the first one if multiple are present for the same name) + * + *

NOTE: this runs in linear time (it scans starting at the + * beginning of the list until it finds the first pair with + * the specified name).

+ * + * @param name the name of the response header to remove + * @return the value of the removed entry or null if no + * value is found for the given header name + */ + public String removeHttpHeader(String name) { + return headers.remove(name); + } + + /** + * Removes all previously added headers with the given name. + * + * @param name the name of the response headers to remove + * @return a Collection with all the values + * of the removed entries. It returns null if no + * entries are found for the given name + */ + public Collection removeHttpHeaders(String name) { + return headers.removeAll(name); + } + + /** + * Returns a new iterator of response headers + * @return a new Iterator instance for the response headers + */ + public Iterator> httpHeaders() { + return headers.iterator(); + } } diff --git a/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java b/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java index da4f9f0cb3f..c61cecc5149 100644 --- a/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java +++ b/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java @@ -419,16 +419,11 @@ public class SolrDispatchFilter extends BaseSolrFilter { SolrRequestInfo.setRequestInfo(new SolrRequestInfo(solrReq, solrRsp)); this.execute( req, handler, solrReq, solrRsp ); HttpCacheHeaderUtil.checkHttpCachingVeto(solrRsp, resp, reqMethod); - // add info to http headers - //TODO: See SOLR-232 and SOLR-267. - /*try { - NamedList solrRspHeader = solrRsp.getResponseHeader(); - for (int i=0; i> headers = solrRsp.httpHeaders(); + while (headers.hasNext()) { + Entry entry = headers.next(); + resp.addHeader(entry.getKey(), entry.getValue()); + } QueryResponseWriter responseWriter = core.getQueryResponseWriter(solrReq); writeResponse(solrRsp, response, responseWriter, solrReq, reqMethod); } diff --git a/solr/core/src/test-files/solr/collection1/conf/solrconfig-headers.xml b/solr/core/src/test-files/solr/collection1/conf/solrconfig-headers.xml new file mode 100644 index 00000000000..e19ecfa70ba --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/solrconfig-headers.xml @@ -0,0 +1,32 @@ + + + + + + ${tests.luceneMatchVersion:LUCENE_CURRENT} + ${solr.data.dir:} + + + + + + componentThatAddsHeader + + + + diff --git a/solr/core/src/test/org/apache/solr/servlet/ResponseHeaderTest.java b/solr/core/src/test/org/apache/solr/servlet/ResponseHeaderTest.java new file mode 100644 index 00000000000..1f4689e1cd4 --- /dev/null +++ b/solr/core/src/test/org/apache/solr/servlet/ResponseHeaderTest.java @@ -0,0 +1,231 @@ +package org.apache.solr.servlet; + +/* + * 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. + */ + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Map.Entry; + +import org.apache.commons.io.FileUtils; +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.solr.SolrJettyTestBase; +import org.apache.solr.SolrTestCaseJ4; +import org.apache.solr.client.solrj.SolrServerException; +import org.apache.solr.client.solrj.impl.HttpSolrServer; +import org.apache.solr.handler.component.ResponseBuilder; +import org.apache.solr.handler.component.SearchComponent; +import org.apache.solr.response.SolrQueryResponse; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + + +public class ResponseHeaderTest extends SolrJettyTestBase { + + private static File solrHomeDirectory; + + @BeforeClass + public static void beforeTest() throws Exception { + solrHomeDirectory = createTempDir(); + setupJettyTestHome(solrHomeDirectory, "collection1"); + String top = SolrTestCaseJ4.TEST_HOME() + "/collection1/conf"; + FileUtils.copyFile(new File(top, "solrconfig-headers.xml"), new File(solrHomeDirectory + "/collection1/conf", "solrconfig.xml")); + createJetty(solrHomeDirectory.getAbsolutePath(), null, null); + } + + @AfterClass + public static void afterTest() throws Exception { + cleanUpJettyHome(solrHomeDirectory); + } + + @Test + public void testHttpResponse() throws SolrServerException, IOException { + HttpSolrServer client = (HttpSolrServer)getSolrServer(); + HttpClient httpClient = client.getHttpClient(); + URI uri = URI.create(client.getBaseURL() + "/withHeaders?q=*:*"); + HttpGet httpGet = new HttpGet(uri); + HttpResponse response = httpClient.execute(httpGet); + Header[] headers = response.getAllHeaders(); + boolean containsWarningHeader = false; + for (Header header:headers) { + if ("Warning".equals(header.getName())) { + containsWarningHeader = true; + assertEquals("This is a test warning", header.getValue()); + break; + } + } + assertTrue("Expected header not found", containsWarningHeader); + } + + @Test + public void testAddHttpHeader() { + SolrQueryResponse response = new SolrQueryResponse(); + Iterator> it = response.httpHeaders(); + assertFalse(it.hasNext()); + + response.addHttpHeader("key1", "value1"); + it = response.httpHeaders(); + assertTrue(it.hasNext()); + Entry entry = it.next(); + assertEquals("key1", entry.getKey()); + assertEquals("value1", entry.getValue()); + assertFalse(it.hasNext()); + + response.addHttpHeader("key1", "value2"); + it = response.httpHeaders(); + assertTrue(it.hasNext()); + entry = it.next(); + assertEquals("key1", entry.getKey()); + assertEquals("value1", entry.getValue()); + assertTrue(it.hasNext()); + entry = it.next(); + assertEquals("key1", entry.getKey()); + assertEquals("value2", entry.getValue()); + assertFalse(it.hasNext()); + + response.addHttpHeader("key2", "value2"); + it = response.httpHeaders(); + assertTrue(it.hasNext()); + entry = it.next(); + assertEquals("key1", entry.getKey()); + assertEquals("value1", entry.getValue()); + assertTrue(it.hasNext()); + entry = it.next(); + assertEquals("key1", entry.getKey()); + assertEquals("value2", entry.getValue()); + assertTrue(it.hasNext()); + entry = it.next(); + assertEquals("key2", entry.getKey()); + assertEquals("value2", entry.getValue()); + assertFalse(it.hasNext()); + } + + @Test + public void testSetHttpHeader() { + SolrQueryResponse response = new SolrQueryResponse(); + Iterator> it = response.httpHeaders(); + assertFalse(it.hasNext()); + + response.setHttpHeader("key1", "value1"); + it = response.httpHeaders(); + assertTrue(it.hasNext()); + Entry entry = it.next(); + assertEquals("key1", entry.getKey()); + assertEquals("value1", entry.getValue()); + assertFalse(it.hasNext()); + + response.setHttpHeader("key1", "value2"); + it = response.httpHeaders(); + assertTrue(it.hasNext()); + entry = it.next(); + assertEquals("key1", entry.getKey()); + assertEquals("value2", entry.getValue()); + assertFalse(it.hasNext()); + + response.addHttpHeader("key1", "value3"); + response.setHttpHeader("key1", "value4"); + it = response.httpHeaders(); + assertTrue(it.hasNext()); + entry = it.next(); + assertEquals("key1", entry.getKey()); + assertEquals("value4", entry.getValue()); + assertFalse(it.hasNext()); + + response.setHttpHeader("key2", "value5"); + it = response.httpHeaders(); + assertTrue(it.hasNext()); + entry = it.next(); + assertEquals("key1", entry.getKey()); + assertEquals("value4", entry.getValue()); + assertTrue(it.hasNext()); + entry = it.next(); + assertEquals("key2", entry.getKey()); + assertEquals("value5", entry.getValue()); + assertFalse(it.hasNext()); + } + + @Test + public void testRemoveHttpHeader() { + SolrQueryResponse response = new SolrQueryResponse(); + Iterator> it = response.httpHeaders(); + assertFalse(it.hasNext()); + response.addHttpHeader("key1", "value1"); + assertTrue(response.httpHeaders().hasNext()); + assertEquals("value1", response.removeHttpHeader("key1")); + assertFalse(response.httpHeaders().hasNext()); + + response.addHttpHeader("key1", "value2"); + response.addHttpHeader("key1", "value3"); + response.addHttpHeader("key2", "value4"); + assertTrue(response.httpHeaders().hasNext()); + assertEquals("value2", response.removeHttpHeader("key1")); + assertEquals("value3", response.httpHeaders().next().getValue()); + assertEquals("value3", response.removeHttpHeader("key1")); + assertNull(response.removeHttpHeader("key1")); + assertEquals("key2", response.httpHeaders().next().getKey()); + + } + + @Test + public void testRemoveHttpHeaders() { + SolrQueryResponse response = new SolrQueryResponse(); + Iterator> it = response.httpHeaders(); + assertFalse(it.hasNext()); + response.addHttpHeader("key1", "value1"); + assertTrue(response.httpHeaders().hasNext()); + assertEquals(Arrays.asList("value1"), response.removeHttpHeaders("key1")); + assertFalse(response.httpHeaders().hasNext()); + + response.addHttpHeader("key1", "value2"); + response.addHttpHeader("key1", "value3"); + response.addHttpHeader("key2", "value4"); + assertTrue(response.httpHeaders().hasNext()); + assertEquals(Arrays.asList(new String[]{"value2", "value3"}), response.removeHttpHeaders("key1")); + assertNull(response.removeHttpHeaders("key1")); + assertEquals("key2", response.httpHeaders().next().getKey()); + } + + public static class ComponentThatAddsHeader extends SearchComponent { + + @Override + public void prepare(ResponseBuilder rb) throws IOException { + rb.rsp.addHttpHeader("Warning", "This is a test warning"); + } + + @Override + public void process(ResponseBuilder rb) throws IOException {} + + @Override + public String getDescription() { + return null; + } + + @Override + public String getSource() { + return null; + } + + } + +}