| /* |
| * 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.solr.client.solrj.impl; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.Closeable; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.UnsupportedEncodingException; |
| import java.lang.invoke.MethodHandles; |
| import java.net.ConnectException; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.nio.charset.Charset; |
| import java.nio.charset.StandardCharsets; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.ExecutorService; |
| import java.util.concurrent.Phaser; |
| import java.util.concurrent.Semaphore; |
| import java.util.concurrent.ThreadPoolExecutor; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.TimeoutException; |
| |
| import org.apache.commons.io.IOUtils; |
| import org.apache.http.HttpStatus; |
| import org.apache.http.entity.ContentType; |
| import org.apache.solr.client.solrj.ResponseParser; |
| import org.apache.solr.client.solrj.SolrClient; |
| import org.apache.solr.client.solrj.SolrRequest; |
| import org.apache.solr.client.solrj.SolrServerException; |
| import org.apache.solr.client.solrj.V2RequestSupport; |
| import org.apache.solr.client.solrj.embedded.SSLConfig; |
| import org.apache.solr.client.solrj.request.RequestWriter; |
| import org.apache.solr.client.solrj.request.UpdateRequest; |
| import org.apache.solr.client.solrj.request.V2Request; |
| import org.apache.solr.client.solrj.util.ClientUtils; |
| import org.apache.solr.client.solrj.util.Constants; |
| import org.apache.solr.common.SolrException; |
| import org.apache.solr.common.StringUtils; |
| import org.apache.solr.common.params.CommonParams; |
| import org.apache.solr.common.params.ModifiableSolrParams; |
| import org.apache.solr.common.params.SolrParams; |
| import org.apache.solr.common.params.UpdateParams; |
| import org.apache.solr.common.util.Base64; |
| import org.apache.solr.common.util.ContentStream; |
| import org.apache.solr.common.util.ExecutorUtil; |
| import org.apache.solr.common.util.NamedList; |
| import org.apache.solr.common.util.ObjectReleaseTracker; |
| import org.apache.solr.common.util.SolrNamedThreadFactory; |
| import org.apache.solr.common.util.Utils; |
| import org.eclipse.jetty.client.HttpClient; |
| import org.eclipse.jetty.client.HttpClientTransport; |
| import org.eclipse.jetty.client.ProtocolHandlers; |
| import org.eclipse.jetty.client.api.Request; |
| import org.eclipse.jetty.client.api.Response; |
| import org.eclipse.jetty.client.api.Result; |
| import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; |
| import org.eclipse.jetty.client.util.BufferingResponseListener; |
| import org.eclipse.jetty.client.util.BytesContentProvider; |
| import org.eclipse.jetty.client.util.FormContentProvider; |
| import org.eclipse.jetty.client.util.InputStreamContentProvider; |
| import org.eclipse.jetty.client.util.InputStreamResponseListener; |
| import org.eclipse.jetty.client.util.MultiPartContentProvider; |
| import org.eclipse.jetty.client.util.OutputStreamContentProvider; |
| import org.eclipse.jetty.client.util.StringContentProvider; |
| import org.eclipse.jetty.http.HttpField; |
| import org.eclipse.jetty.http.HttpFields; |
| import org.eclipse.jetty.http.HttpHeader; |
| import org.eclipse.jetty.http.HttpMethod; |
| import org.eclipse.jetty.http2.client.HTTP2Client; |
| import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2; |
| import org.eclipse.jetty.util.BlockingArrayQueue; |
| import org.eclipse.jetty.util.Fields; |
| import org.eclipse.jetty.util.ssl.SslContextFactory; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import static org.apache.solr.client.solrj.impl.BaseHttpSolrClient.RemoteExecutionException; |
| import static org.apache.solr.client.solrj.impl.BaseHttpSolrClient.RemoteSolrException; |
| import static org.apache.solr.common.util.Utils.getObjectByPath; |
| |
| /** |
| * Difference between this {@link Http2SolrClient} and {@link HttpSolrClient}: |
| * <ul> |
| * <li>{@link Http2SolrClient} sends requests in HTTP/2</li> |
| * <li>{@link Http2SolrClient} can point to multiple urls</li> |
| * <li>{@link Http2SolrClient} does not expose its internal httpClient like {@link HttpSolrClient#getHttpClient()}, |
| * sharing connection pools should be done by {@link Http2SolrClient.Builder#withHttpClient(Http2SolrClient)} </li> |
| * </ul> |
| * @lucene.experimental |
| */ |
| public class Http2SolrClient extends SolrClient { |
| public static final String REQ_PRINCIPAL_KEY = "solr-req-principal"; |
| |
| private static volatile SSLConfig defaultSSLConfig; |
| |
| private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); |
| private static final String AGENT = "Solr[" + Http2SolrClient.class.getName() + "] 2.0"; |
| private static final Charset FALLBACK_CHARSET = StandardCharsets.UTF_8; |
| private static final String DEFAULT_PATH = "/select"; |
| private static final List<String> errPath = Arrays.asList("metadata", "error-class"); |
| |
| private HttpClient httpClient; |
| private volatile Set<String> queryParams = Collections.emptySet(); |
| private int idleTimeout; |
| |
| private ResponseParser parser = new BinaryResponseParser(); |
| private volatile RequestWriter requestWriter = new BinaryRequestWriter(); |
| private List<HttpListenerFactory> listenerFactory = new LinkedList<>(); |
| private AsyncTracker asyncTracker = new AsyncTracker(); |
| /** |
| * The URL of the Solr server. |
| */ |
| private String serverBaseUrl; |
| private boolean closeClient; |
| |
| protected Http2SolrClient(String serverBaseUrl, Builder builder) { |
| if (serverBaseUrl != null) { |
| if (!serverBaseUrl.equals("/") && serverBaseUrl.endsWith("/")) { |
| serverBaseUrl = serverBaseUrl.substring(0, serverBaseUrl.length() - 1); |
| } |
| |
| if (serverBaseUrl.startsWith("//")) { |
| serverBaseUrl = serverBaseUrl.substring(1, serverBaseUrl.length()); |
| } |
| this.serverBaseUrl = serverBaseUrl; |
| } |
| |
| if (builder.idleTimeout != null && builder.idleTimeout > 0) idleTimeout = builder.idleTimeout; |
| else idleTimeout = HttpClientUtil.DEFAULT_SO_TIMEOUT; |
| |
| if (builder.http2SolrClient == null) { |
| httpClient = createHttpClient(builder); |
| closeClient = true; |
| } else { |
| httpClient = builder.http2SolrClient.httpClient; |
| } |
| assert ObjectReleaseTracker.track(this); |
| } |
| |
| public void addListenerFactory(HttpListenerFactory factory) { |
| this.listenerFactory.add(factory); |
| } |
| |
| // internal usage only |
| HttpClient getHttpClient() { |
| return httpClient; |
| } |
| |
| // internal usage only |
| ProtocolHandlers getProtocolHandlers() { |
| return httpClient.getProtocolHandlers(); |
| } |
| |
| private HttpClient createHttpClient(Builder builder) { |
| HttpClient httpClient; |
| |
| BlockingArrayQueue<Runnable> queue = new BlockingArrayQueue<>(256, 256); |
| ThreadPoolExecutor httpClientExecutor = new ExecutorUtil.MDCAwareThreadPoolExecutor(32, |
| 256, 60, TimeUnit.SECONDS, queue, new SolrNamedThreadFactory("h2sc")); |
| |
| SslContextFactory.Client sslContextFactory; |
| boolean ssl; |
| if (builder.sslConfig == null) { |
| sslContextFactory = getDefaultSslContextFactory(); |
| ssl = sslContextFactory.getTrustStore() != null || sslContextFactory.getTrustStorePath() != null; |
| } else { |
| sslContextFactory = builder.sslConfig.createClientContextFactory(); |
| ssl = true; |
| } |
| |
| boolean sslOnJava8OrLower = ssl && !Constants.JRE_IS_MINIMUM_JAVA9; |
| HttpClientTransport transport; |
| if (builder.useHttp1_1 || sslOnJava8OrLower) { |
| if (sslOnJava8OrLower && !builder.useHttp1_1) { |
| log.warn("Create Http2SolrClient with HTTP/1.1 transport since Java 8 or lower versions does not support SSL + HTTP/2"); |
| } else { |
| log.debug("Create Http2SolrClient with HTTP/1.1 transport"); |
| } |
| transport = new HttpClientTransportOverHTTP(2); |
| httpClient = new HttpClient(transport, sslContextFactory); |
| if (builder.maxConnectionsPerHost != null) httpClient.setMaxConnectionsPerDestination(builder.maxConnectionsPerHost); |
| } else { |
| log.debug("Create Http2SolrClient with HTTP/2 transport"); |
| HTTP2Client http2client = new HTTP2Client(); |
| transport = new HttpClientTransportOverHTTP2(http2client); |
| httpClient = new HttpClient(transport, sslContextFactory); |
| httpClient.setMaxConnectionsPerDestination(4); |
| } |
| |
| httpClient.setExecutor(httpClientExecutor); |
| httpClient.setStrictEventOrdering(false); |
| httpClient.setConnectBlocking(true); |
| httpClient.setFollowRedirects(false); |
| httpClient.setMaxRequestsQueuedPerDestination(asyncTracker.getMaxRequestsQueuedPerDestination()); |
| httpClient.setUserAgentField(new HttpField(HttpHeader.USER_AGENT, AGENT)); |
| |
| httpClient.setIdleTimeout(idleTimeout); |
| if (builder.connectionTimeout != null) httpClient.setConnectTimeout(builder.connectionTimeout); |
| try { |
| httpClient.start(); |
| } catch (Exception e) { |
| throw new RuntimeException(e); |
| } |
| return httpClient; |
| } |
| |
| public void close() { |
| // we wait for async requests, so far devs don't want to give sugar for this |
| asyncTracker.waitForComplete(); |
| if (closeClient) { |
| try { |
| ExecutorService executor = (ExecutorService) httpClient.getExecutor(); |
| httpClient.setStopTimeout(1000); |
| httpClient.stop(); |
| ExecutorUtil.shutdownAndAwaitTermination(executor); |
| } catch (Exception e) { |
| throw new RuntimeException("Exception on closing client", e); |
| } |
| } |
| |
| assert ObjectReleaseTracker.release(this); |
| } |
| |
| public boolean isV2ApiRequest(@SuppressWarnings({"rawtypes"})final SolrRequest request) { |
| return request instanceof V2Request || request.getPath().contains("/____v2"); |
| } |
| |
| public long getIdleTimeout() { |
| return idleTimeout; |
| } |
| |
| public static class OutStream implements Closeable{ |
| private final String origCollection; |
| private final ModifiableSolrParams origParams; |
| private final OutputStreamContentProvider outProvider; |
| private final InputStreamResponseListener responseListener; |
| private final boolean isXml; |
| |
| public OutStream(String origCollection, ModifiableSolrParams origParams, |
| OutputStreamContentProvider outProvider, InputStreamResponseListener responseListener, boolean isXml) { |
| this.origCollection = origCollection; |
| this.origParams = origParams; |
| this.outProvider = outProvider; |
| this.responseListener = responseListener; |
| this.isXml = isXml; |
| } |
| |
| boolean belongToThisStream(@SuppressWarnings({"rawtypes"})SolrRequest solrRequest, String collection) { |
| ModifiableSolrParams solrParams = new ModifiableSolrParams(solrRequest.getParams()); |
| if (!origParams.toNamedList().equals(solrParams.toNamedList()) || !StringUtils.equals(origCollection, collection)) { |
| return false; |
| } |
| return true; |
| } |
| |
| public void write(byte b[]) throws IOException { |
| this.outProvider.getOutputStream().write(b); |
| } |
| |
| public void flush() throws IOException { |
| this.outProvider.getOutputStream().flush(); |
| } |
| |
| @Override |
| public void close() throws IOException { |
| if (isXml) { |
| write("</stream>".getBytes(FALLBACK_CHARSET)); |
| } |
| this.outProvider.getOutputStream().close(); |
| } |
| |
| //TODO this class should be hidden |
| public InputStreamResponseListener getResponseListener() { |
| return responseListener; |
| } |
| } |
| |
| public OutStream initOutStream(String baseUrl, |
| UpdateRequest updateRequest, |
| String collection) throws IOException { |
| String contentType = requestWriter.getUpdateContentType(); |
| final ModifiableSolrParams origParams = new ModifiableSolrParams(updateRequest.getParams()); |
| |
| // The parser 'wt=' and 'version=' params are used instead of the |
| // original params |
| ModifiableSolrParams requestParams = new ModifiableSolrParams(origParams); |
| requestParams.set(CommonParams.WT, parser.getWriterType()); |
| requestParams.set(CommonParams.VERSION, parser.getVersion()); |
| |
| String basePath = baseUrl; |
| if (collection != null) |
| basePath += "/" + collection; |
| if (!basePath.endsWith("/")) |
| basePath += "/"; |
| |
| OutputStreamContentProvider provider = new OutputStreamContentProvider(); |
| Request postRequest = httpClient |
| .newRequest(basePath + "update" |
| + requestParams.toQueryString()) |
| .method(HttpMethod.POST) |
| .header(HttpHeader.CONTENT_TYPE, contentType) |
| .content(provider); |
| decorateRequest(postRequest, updateRequest); |
| InputStreamResponseListener responseListener = new InputStreamResponseListener(); |
| postRequest.send(responseListener); |
| |
| boolean isXml = ClientUtils.TEXT_XML.equals(requestWriter.getUpdateContentType()); |
| OutStream outStream = new OutStream(collection, origParams, provider, responseListener, |
| isXml); |
| if (isXml) { |
| outStream.write("<stream>".getBytes(FALLBACK_CHARSET)); |
| } |
| return outStream; |
| } |
| |
| public void send(OutStream outStream, @SuppressWarnings({"rawtypes"})SolrRequest req, String collection) throws IOException { |
| assert outStream.belongToThisStream(req, collection); |
| this.requestWriter.write(req, outStream.outProvider.getOutputStream()); |
| if (outStream.isXml) { |
| // check for commit or optimize |
| SolrParams params = req.getParams(); |
| if (params != null) { |
| String fmt = null; |
| if (params.getBool(UpdateParams.OPTIMIZE, false)) { |
| fmt = "<optimize waitSearcher=\"%s\" />"; |
| } else if (params.getBool(UpdateParams.COMMIT, false)) { |
| fmt = "<commit waitSearcher=\"%s\" />"; |
| } |
| if (fmt != null) { |
| byte[] content = String.format(Locale.ROOT, |
| fmt, params.getBool(UpdateParams.WAIT_SEARCHER, false) |
| + "") |
| .getBytes(FALLBACK_CHARSET); |
| outStream.write(content); |
| } |
| } |
| } |
| outStream.flush(); |
| } |
| |
| public NamedList<Object> request(@SuppressWarnings({"rawtypes"})SolrRequest solrRequest, |
| String collection, |
| OnComplete onComplete) throws IOException, SolrServerException { |
| Request req = makeRequest(solrRequest, collection); |
| final ResponseParser parser = solrRequest.getResponseParser() == null |
| ? this.parser: solrRequest.getResponseParser(); |
| |
| if (onComplete != null) { |
| // This async call only suitable for indexing since the response size is limited by 5MB |
| req.onRequestQueued(asyncTracker.queuedListener) |
| .onComplete(asyncTracker.completeListener).send(new BufferingResponseListener(5 * 1024 * 1024) { |
| |
| @Override |
| public void onComplete(Result result) { |
| if (result.isFailed()) { |
| onComplete.onFailure(result.getFailure()); |
| return; |
| } |
| |
| NamedList<Object> rsp; |
| try { |
| InputStream is = getContentAsInputStream(); |
| assert ObjectReleaseTracker.track(is); |
| rsp = processErrorsAndResponse(result.getResponse(), |
| parser, is, getMediaType(), getEncoding(), isV2ApiRequest(solrRequest)); |
| onComplete.onSuccess(rsp); |
| } catch (Exception e) { |
| onComplete.onFailure(e); |
| } |
| } |
| }); |
| return null; |
| } else { |
| try { |
| InputStreamResponseListener listener = new InputStreamResponseListener(); |
| req.send(listener); |
| Response response = listener.get(idleTimeout, TimeUnit.MILLISECONDS); |
| InputStream is = listener.getInputStream(); |
| assert ObjectReleaseTracker.track(is); |
| |
| ContentType contentType = getContentType(response); |
| String mimeType = null; |
| String encoding = null; |
| if (contentType != null) { |
| mimeType = contentType.getMimeType(); |
| encoding = contentType.getCharset() != null? contentType.getCharset().name() : null; |
| } |
| return processErrorsAndResponse(response, parser, is, mimeType, encoding, isV2ApiRequest(solrRequest)); |
| } catch (InterruptedException e) { |
| Thread.currentThread().interrupt(); |
| throw new RuntimeException(e); |
| } catch (TimeoutException e) { |
| throw new SolrServerException( |
| "Timeout occured while waiting response from server at: " + req.getURI(), e); |
| } catch (ExecutionException e) { |
| Throwable cause = e.getCause(); |
| if (cause instanceof ConnectException) { |
| throw new SolrServerException("Server refused connection at: " + req.getURI(), cause); |
| } |
| if (cause instanceof SolrServerException) { |
| throw (SolrServerException) cause; |
| } else if (cause instanceof IOException) { |
| throw new SolrServerException( |
| "IOException occured when talking to server at: " + getBaseURL(), cause); |
| } |
| throw new SolrServerException(cause.getMessage(), cause); |
| } |
| } |
| } |
| |
| private ContentType getContentType(Response response) { |
| String contentType = response.getHeaders().get(HttpHeader.CONTENT_TYPE); |
| return StringUtils.isEmpty(contentType)? null : ContentType.parse(contentType); |
| } |
| |
| private void setBasicAuthHeader(@SuppressWarnings({"rawtypes"})SolrRequest solrRequest, Request req) { |
| if (solrRequest.getBasicAuthUser() != null && solrRequest.getBasicAuthPassword() != null) { |
| String userPass = solrRequest.getBasicAuthUser() + ":" + solrRequest.getBasicAuthPassword(); |
| String encoded = Base64.byteArrayToBase64(userPass.getBytes(FALLBACK_CHARSET)); |
| req.header("Authorization", "Basic " + encoded); |
| } |
| } |
| |
| private Request makeRequest(@SuppressWarnings({"rawtypes"})SolrRequest solrRequest, String collection) |
| throws SolrServerException, IOException { |
| Request req = createRequest(solrRequest, collection); |
| decorateRequest(req, solrRequest); |
| return req; |
| } |
| |
| private void decorateRequest(Request req, @SuppressWarnings({"rawtypes"})SolrRequest solrRequest) { |
| req.header(HttpHeader.ACCEPT_ENCODING, null); |
| if (solrRequest.getUserPrincipal() != null) { |
| req.attribute(REQ_PRINCIPAL_KEY, solrRequest.getUserPrincipal()); |
| } |
| |
| setBasicAuthHeader(solrRequest, req); |
| for (HttpListenerFactory factory : listenerFactory) { |
| HttpListenerFactory.RequestResponseListener listener = factory.get(); |
| listener.onQueued(req); |
| req.onRequestBegin(listener); |
| req.onComplete(listener); |
| } |
| |
| @SuppressWarnings({"unchecked"}) |
| Map<String, String> headers = solrRequest.getHeaders(); |
| if (headers != null) { |
| for (Map.Entry<String, String> entry : headers.entrySet()) { |
| req.header(entry.getKey(), entry.getValue()); |
| } |
| } |
| } |
| |
| private String changeV2RequestEndpoint(String basePath) throws MalformedURLException { |
| URL oldURL = new URL(basePath); |
| String newPath = oldURL.getPath().replaceFirst("/solr", "/api"); |
| return new URL(oldURL.getProtocol(), oldURL.getHost(), oldURL.getPort(), newPath).toString(); |
| } |
| |
| @SuppressWarnings({"unchecked"}) |
| private Request createRequest(@SuppressWarnings({"rawtypes"})SolrRequest solrRequest, String collection) throws IOException, SolrServerException { |
| if (solrRequest.getBasePath() == null && serverBaseUrl == null) |
| throw new IllegalArgumentException("Destination node is not provided!"); |
| |
| if (solrRequest instanceof V2RequestSupport) { |
| solrRequest = ((V2RequestSupport) solrRequest).getV2Request(); |
| } |
| SolrParams params = solrRequest.getParams(); |
| RequestWriter.ContentWriter contentWriter = requestWriter.getContentWriter(solrRequest); |
| Collection<ContentStream> streams = contentWriter == null ? requestWriter.getContentStreams(solrRequest) : null; |
| String path = requestWriter.getPath(solrRequest); |
| if (path == null || !path.startsWith("/")) { |
| path = DEFAULT_PATH; |
| } |
| |
| ResponseParser parser = solrRequest.getResponseParser(); |
| if (parser == null) { |
| parser = this.parser; |
| } |
| |
| // The parser 'wt=' and 'version=' params are used instead of the original |
| // params |
| ModifiableSolrParams wparams = new ModifiableSolrParams(params); |
| if (parser != null) { |
| wparams.set(CommonParams.WT, parser.getWriterType()); |
| wparams.set(CommonParams.VERSION, parser.getVersion()); |
| } |
| |
| //TODO add invariantParams support |
| |
| String basePath = solrRequest.getBasePath() == null ? serverBaseUrl : solrRequest.getBasePath(); |
| if (collection != null) |
| basePath += "/" + collection; |
| |
| if (solrRequest instanceof V2Request) { |
| if (System.getProperty("solr.v2RealPath") == null) { |
| basePath = changeV2RequestEndpoint(basePath); |
| } else { |
| basePath = serverBaseUrl + "/____v2"; |
| } |
| } |
| |
| if (SolrRequest.METHOD.GET == solrRequest.getMethod()) { |
| if (streams != null || contentWriter != null) { |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "GET can't send streams!"); |
| } |
| |
| return httpClient.newRequest(basePath + path + wparams.toQueryString()).method(HttpMethod.GET); |
| } |
| |
| if (SolrRequest.METHOD.DELETE == solrRequest.getMethod()) { |
| return httpClient.newRequest(basePath + path + wparams.toQueryString()).method(HttpMethod.DELETE); |
| } |
| |
| if (SolrRequest.METHOD.POST == solrRequest.getMethod() || SolrRequest.METHOD.PUT == solrRequest.getMethod()) { |
| |
| String url = basePath + path; |
| boolean hasNullStreamName = false; |
| if (streams != null) { |
| hasNullStreamName = streams.stream().anyMatch(cs -> cs.getName() == null); |
| } |
| |
| boolean isMultipart = streams != null && streams.size() > 1 && !hasNullStreamName; |
| |
| HttpMethod method = SolrRequest.METHOD.POST == solrRequest.getMethod() ? HttpMethod.POST : HttpMethod.PUT; |
| |
| if (contentWriter != null) { |
| Request req = httpClient |
| .newRequest(url + wparams.toQueryString()) |
| .method(method); |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| contentWriter.write(baos); |
| |
| //TODO reduce memory usage |
| return req.content(new BytesContentProvider(contentWriter.getContentType(), baos.toByteArray())); |
| } else if (streams == null || isMultipart) { |
| // send server list and request list as query string params |
| ModifiableSolrParams queryParams = calculateQueryParams(this.queryParams, wparams); |
| queryParams.add(calculateQueryParams(solrRequest.getQueryParams(), wparams)); |
| Request req = httpClient |
| .newRequest(url + queryParams.toQueryString()) |
| .method(method); |
| return fillContentStream(req, streams, wparams, isMultipart); |
| } else { |
| // It is has one stream, it is the post body, put the params in the URL |
| ContentStream contentStream = streams.iterator().next(); |
| return httpClient |
| .newRequest(url + wparams.toQueryString()) |
| .method(method) |
| .content(new InputStreamContentProvider(contentStream.getStream()), contentStream.getContentType()); |
| } |
| } |
| |
| throw new SolrServerException("Unsupported method: " + solrRequest.getMethod()); |
| } |
| |
| private Request fillContentStream(Request req, Collection<ContentStream> streams, |
| ModifiableSolrParams wparams, |
| boolean isMultipart) throws IOException { |
| if (isMultipart) { |
| // multipart/form-data |
| MultiPartContentProvider content = new MultiPartContentProvider(); |
| Iterator<String> iter = wparams.getParameterNamesIterator(); |
| while (iter.hasNext()) { |
| String key = iter.next(); |
| String[] vals = wparams.getParams(key); |
| if (vals != null) { |
| for (String val : vals) { |
| content.addFieldPart(key, new StringContentProvider(val), null); |
| } |
| } |
| } |
| if (streams != null) { |
| for (ContentStream contentStream : streams) { |
| String contentType = contentStream.getContentType(); |
| if (contentType == null) { |
| contentType = BinaryResponseParser.BINARY_CONTENT_TYPE; // default |
| } |
| String name = contentStream.getName(); |
| if (name == null) { |
| name = ""; |
| } |
| HttpFields fields = new HttpFields(); |
| fields.add(HttpHeader.CONTENT_TYPE, contentType); |
| content.addFilePart(name, contentStream.getName(), new InputStreamContentProvider(contentStream.getStream()), fields); |
| } |
| } |
| req.content(content); |
| } else { |
| // application/x-www-form-urlencoded |
| Fields fields = new Fields(); |
| Iterator<String> iter = wparams.getParameterNamesIterator(); |
| while (iter.hasNext()) { |
| String key = iter.next(); |
| String[] vals = wparams.getParams(key); |
| if (vals != null) { |
| for (String val : vals) { |
| fields.add(key, val); |
| } |
| } |
| } |
| req.content(new FormContentProvider(fields, FALLBACK_CHARSET)); |
| } |
| |
| return req; |
| } |
| |
| private boolean wantStream(final ResponseParser processor) { |
| return processor == null || processor instanceof InputStreamResponseParser; |
| } |
| |
| @SuppressWarnings({"unchecked", "rawtypes"}) |
| private NamedList<Object> processErrorsAndResponse(Response response, |
| final ResponseParser processor, |
| InputStream is, |
| String mimeType, |
| String encoding, |
| final boolean isV2Api) |
| throws SolrServerException { |
| boolean shouldClose = true; |
| try { |
| // handle some http level checks before trying to parse the response |
| int httpStatus = response.getStatus(); |
| |
| switch (httpStatus) { |
| case HttpStatus.SC_OK: |
| case HttpStatus.SC_BAD_REQUEST: |
| case HttpStatus.SC_CONFLICT: // 409 |
| break; |
| case HttpStatus.SC_MOVED_PERMANENTLY: |
| case HttpStatus.SC_MOVED_TEMPORARILY: |
| if (!httpClient.isFollowRedirects()) { |
| throw new SolrServerException("Server at " + getBaseURL() |
| + " sent back a redirect (" + httpStatus + ")."); |
| } |
| break; |
| default: |
| if (processor == null || mimeType == null) { |
| throw new RemoteSolrException(serverBaseUrl, httpStatus, "non ok status: " + httpStatus |
| + ", message:" + response.getReason(), |
| null); |
| } |
| } |
| |
| if (wantStream(parser)) { |
| // no processor specified, return raw stream |
| NamedList<Object> rsp = new NamedList<>(); |
| rsp.add("stream", is); |
| // Only case where stream should not be closed |
| shouldClose = false; |
| return rsp; |
| } |
| |
| String procCt = processor.getContentType(); |
| if (procCt != null) { |
| String procMimeType = ContentType.parse(procCt).getMimeType().trim().toLowerCase(Locale.ROOT); |
| if (!procMimeType.equals(mimeType)) { |
| // unexpected mime type |
| String msg = "Expected mime type " + procMimeType + " but got " + mimeType + "."; |
| String exceptionEncoding = encoding != null? encoding : FALLBACK_CHARSET.name(); |
| try { |
| msg = msg + " " + IOUtils.toString(is, exceptionEncoding); |
| } catch (IOException e) { |
| throw new RemoteSolrException(serverBaseUrl, httpStatus, "Could not parse response with encoding " + exceptionEncoding, e); |
| } |
| throw new RemoteSolrException(serverBaseUrl, httpStatus, msg, null); |
| } |
| } |
| |
| NamedList<Object> rsp; |
| try { |
| rsp = processor.processResponse(is, encoding); |
| } catch (Exception e) { |
| throw new RemoteSolrException(serverBaseUrl, httpStatus, e.getMessage(), e); |
| } |
| |
| Object error = rsp == null ? null : rsp.get("error"); |
| if (error != null && (String.valueOf(getObjectByPath(error, true, errPath)).endsWith("ExceptionWithErrObject"))) { |
| throw RemoteExecutionException.create(serverBaseUrl, rsp); |
| } |
| if (httpStatus != HttpStatus.SC_OK && !isV2Api) { |
| NamedList<String> metadata = null; |
| String reason = null; |
| try { |
| if (error != null) { |
| reason = (String) Utils.getObjectByPath(error, false, Collections.singletonList("msg")); |
| if(reason == null) { |
| reason = (String) Utils.getObjectByPath(error, false, Collections.singletonList("trace")); |
| } |
| Object metadataObj = Utils.getObjectByPath(error, false, Collections.singletonList("metadata")); |
| if (metadataObj instanceof NamedList) { |
| metadata = (NamedList<String>) metadataObj; |
| } else if (metadataObj instanceof List) { |
| // NamedList parsed as List convert to NamedList again |
| List<Object> list = (List<Object>) metadataObj; |
| metadata = new NamedList<>(list.size()/2); |
| for (int i = 0; i < list.size(); i+=2) { |
| metadata.add((String)list.get(i), (String) list.get(i+1)); |
| } |
| } else if (metadataObj instanceof Map) { |
| metadata = new NamedList((Map) metadataObj); |
| } |
| } |
| } catch (Exception ex) {} |
| if (reason == null) { |
| |
| StringBuilder msg = new StringBuilder(); |
| msg.append(response.getReason()) |
| .append("\n\n") |
| .append("request: ") |
| .append(response.getRequest().getMethod()); |
| try { |
| reason = java.net.URLDecoder.decode(msg.toString(), FALLBACK_CHARSET.name()); |
| } catch (UnsupportedEncodingException e) { |
| // This shouldn't occur because FALLBACK_CHARSET will always be supported. |
| } |
| } |
| RemoteSolrException rss = new RemoteSolrException(serverBaseUrl, httpStatus, reason, null); |
| if (metadata != null) rss.setMetadata(metadata); |
| throw rss; |
| } |
| return rsp; |
| } finally { |
| if (shouldClose) { |
| try { |
| is.close(); |
| assert ObjectReleaseTracker.release(is); |
| } catch (IOException e) { |
| // quitely |
| } |
| } |
| } |
| } |
| |
| @Override |
| public NamedList<Object> request(@SuppressWarnings({"rawtypes"})SolrRequest request, String collection) throws SolrServerException, IOException { |
| return request(request, collection, null); |
| } |
| |
| public void setRequestWriter(RequestWriter requestWriter) { |
| this.requestWriter = requestWriter; |
| } |
| |
| public interface OnComplete { |
| void onSuccess(NamedList<Object> result); |
| |
| void onFailure(Throwable e); |
| } |
| |
| public void setFollowRedirects(boolean follow) { |
| httpClient.setFollowRedirects(follow); |
| } |
| |
| public String getBaseURL() { |
| return serverBaseUrl; |
| } |
| |
| private static class AsyncTracker { |
| private static final int MAX_OUTSTANDING_REQUESTS = 1000; |
| |
| // wait for async requests |
| private final Phaser phaser; |
| // maximum outstanding requests left |
| private final Semaphore available; |
| private final Request.QueuedListener queuedListener; |
| private final Response.CompleteListener completeListener; |
| |
| AsyncTracker() { |
| // TODO: what about shared instances? |
| phaser = new Phaser(1); |
| available = new Semaphore(MAX_OUTSTANDING_REQUESTS, false); |
| queuedListener = request -> { |
| phaser.register(); |
| try { |
| available.acquire(); |
| } catch (InterruptedException ignored) { |
| |
| } |
| }; |
| completeListener = result -> { |
| phaser.arriveAndDeregister(); |
| available.release(); |
| }; |
| } |
| |
| int getMaxRequestsQueuedPerDestination() { |
| // comfortably above max outstanding requests |
| return MAX_OUTSTANDING_REQUESTS * 3; |
| } |
| |
| public void waitForComplete() { |
| phaser.arriveAndAwaitAdvance(); |
| phaser.arriveAndDeregister(); |
| } |
| } |
| |
| public static class Builder { |
| |
| private Http2SolrClient http2SolrClient; |
| private SSLConfig sslConfig = defaultSSLConfig; |
| private Integer idleTimeout; |
| private Integer connectionTimeout; |
| private Integer maxConnectionsPerHost; |
| private boolean useHttp1_1 = Boolean.getBoolean("solr.http1"); |
| protected String baseSolrUrl; |
| |
| public Builder() { |
| |
| } |
| |
| public Builder(String baseSolrUrl) { |
| this.baseSolrUrl = baseSolrUrl; |
| } |
| |
| public Http2SolrClient build() { |
| return new Http2SolrClient(baseSolrUrl, this); |
| } |
| |
| /** |
| * Reuse {@code httpClient} connections pool |
| */ |
| public Builder withHttpClient(Http2SolrClient httpClient) { |
| this.http2SolrClient = httpClient; |
| return this; |
| } |
| |
| public Builder withSSLConfig(SSLConfig sslConfig) { |
| this.sslConfig = sslConfig; |
| return this; |
| } |
| |
| /** |
| * Set maxConnectionsPerHost for http1 connections, maximum number http2 connections is limited by 4 |
| */ |
| public Builder maxConnectionsPerHost(int max) { |
| this.maxConnectionsPerHost = max; |
| return this; |
| } |
| |
| public Builder idleTimeout(int idleConnectionTimeout) { |
| this.idleTimeout = idleConnectionTimeout; |
| return this; |
| } |
| |
| public Builder useHttp1_1(boolean useHttp1_1) { |
| this.useHttp1_1 = useHttp1_1; |
| return this; |
| } |
| |
| public Builder connectionTimeout(int connectionTimeOut) { |
| this.connectionTimeout = connectionTimeOut; |
| return this; |
| } |
| } |
| |
| public Set<String> getQueryParams() { |
| return queryParams; |
| } |
| |
| /** |
| * Expert Method |
| * |
| * @param queryParams set of param keys to only send via the query string |
| * Note that the param will be sent as a query string if the key is part |
| * of this Set or the SolrRequest's query params. |
| * @see org.apache.solr.client.solrj.SolrRequest#getQueryParams |
| */ |
| public void setQueryParams(Set<String> queryParams) { |
| this.queryParams = queryParams; |
| } |
| |
| private ModifiableSolrParams calculateQueryParams(Set<String> queryParamNames, |
| ModifiableSolrParams wparams) { |
| ModifiableSolrParams queryModParams = new ModifiableSolrParams(); |
| if (queryParamNames != null) { |
| for (String param : queryParamNames) { |
| String[] value = wparams.getParams(param); |
| if (value != null) { |
| for (String v : value) { |
| queryModParams.add(param, v); |
| } |
| wparams.remove(param); |
| } |
| } |
| } |
| return queryModParams; |
| } |
| |
| public ResponseParser getParser() { |
| return parser; |
| } |
| |
| public void setParser(ResponseParser processor) { |
| parser = processor; |
| } |
| |
| public static void setDefaultSSLConfig(SSLConfig sslConfig) { |
| Http2SolrClient.defaultSSLConfig = sslConfig; |
| } |
| |
| // public for testing, only used by tests |
| public static void resetSslContextFactory() { |
| Http2SolrClient.defaultSSLConfig = null; |
| } |
| |
| /* package-private for testing */ |
| static SslContextFactory.Client getDefaultSslContextFactory() { |
| String checkPeerNameStr = System.getProperty(HttpClientUtil.SYS_PROP_CHECK_PEER_NAME); |
| boolean sslCheckPeerName = true; |
| if (checkPeerNameStr == null || "false".equalsIgnoreCase(checkPeerNameStr)) { |
| sslCheckPeerName = false; |
| } |
| |
| SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(!sslCheckPeerName); |
| |
| if (null != System.getProperty("javax.net.ssl.keyStore")) { |
| sslContextFactory.setKeyStorePath |
| (System.getProperty("javax.net.ssl.keyStore")); |
| } |
| if (null != System.getProperty("javax.net.ssl.keyStorePassword")) { |
| sslContextFactory.setKeyStorePassword |
| (System.getProperty("javax.net.ssl.keyStorePassword")); |
| } |
| if (null != System.getProperty("javax.net.ssl.trustStore")) { |
| sslContextFactory.setTrustStorePath |
| (System.getProperty("javax.net.ssl.trustStore")); |
| } |
| if (null != System.getProperty("javax.net.ssl.trustStorePassword")) { |
| sslContextFactory.setTrustStorePassword |
| (System.getProperty("javax.net.ssl.trustStorePassword")); |
| } |
| |
| sslContextFactory.setEndpointIdentificationAlgorithm(System.getProperty("solr.jetty.ssl.verifyClientHostName")); |
| |
| return sslContextFactory; |
| } |
| } |