blob: 56df7cafe61f1421bd0287031c0c1b800cac023d [file] [log] [blame]
/*
* 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 static org.apache.solr.common.util.Utils.getObjectByPath;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
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.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
import java.util.function.Function;
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.request.RequestWriter;
import org.apache.solr.client.solrj.request.V2Request;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.util.ContentStream;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.Utils;
public abstract class HttpSolrClientBase extends SolrClient {
protected static final String DEFAULT_PATH = "/select";
protected static final Charset FALLBACK_CHARSET = StandardCharsets.UTF_8;
private static final List<String> errPath = Arrays.asList("metadata", "error-class");
/** The URL of the Solr server. */
protected final String serverBaseUrl;
protected final long idleTimeoutMillis;
protected final long requestTimeoutMillis;
protected final Set<String> urlParamNames;
protected RequestWriter requestWriter = new BinaryRequestWriter();
// updating parser instance needs to go via the setter to ensure update of defaultParserMimeTypes
protected ResponseParser parser = new BinaryResponseParser();
protected Set<String> defaultParserMimeTypes;
protected final String basicAuthAuthorizationStr;
protected HttpSolrClientBase(String serverBaseUrl, HttpSolrClientBuilderBase<?, ?> 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;
} else {
this.serverBaseUrl = null;
}
if (builder.idleTimeoutMillis != null) {
this.idleTimeoutMillis = builder.idleTimeoutMillis;
} else {
this.idleTimeoutMillis = -1;
}
this.basicAuthAuthorizationStr = builder.basicAuthAuthorizationStr;
if (builder.requestWriter != null) {
this.requestWriter = builder.requestWriter;
}
if (builder.responseParser != null) {
this.parser = builder.responseParser;
}
this.defaultCollection = builder.defaultCollection;
if (builder.requestTimeoutMillis != null) {
this.requestTimeoutMillis = builder.requestTimeoutMillis;
} else {
this.requestTimeoutMillis = -1;
}
if (builder.urlParamNames != null) {
this.urlParamNames = builder.urlParamNames;
} else {
this.urlParamNames = Set.of();
}
}
protected String getRequestPath(SolrRequest<?> solrRequest, String collection)
throws MalformedURLException {
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";
}
}
String path = requestWriter.getPath(solrRequest);
if (path == null || !path.startsWith("/")) {
path = DEFAULT_PATH;
}
return basePath + path;
}
protected 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();
}
protected ResponseParser responseParser(SolrRequest<?> solrRequest) {
// TODO add invariantParams support
return solrRequest.getResponseParser() == null ? this.parser : solrRequest.getResponseParser();
}
protected ModifiableSolrParams initalizeSolrParams(
SolrRequest<?> solrRequest, ResponseParser parserToUse) {
// The parser 'wt=' and 'version=' params are used instead of the original
// params
ModifiableSolrParams wparams = new ModifiableSolrParams(solrRequest.getParams());
wparams.set(CommonParams.WT, parserToUse.getWriterType());
wparams.set(CommonParams.VERSION, parserToUse.getVersion());
return wparams;
}
protected boolean isMultipart(Collection<ContentStream> streams) {
boolean isMultipart = false;
if (streams != null) {
boolean hasNullStreamName = false;
hasNullStreamName = streams.stream().anyMatch(cs -> cs.getName() == null);
isMultipart = !hasNullStreamName && streams.size() > 1;
}
return isMultipart;
}
protected 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;
}
protected void validateGetRequest(SolrRequest<?> solrRequest) throws IOException {
RequestWriter.ContentWriter contentWriter = requestWriter.getContentWriter(solrRequest);
Collection<ContentStream> streams =
contentWriter == null ? requestWriter.getContentStreams(solrRequest) : null;
if (contentWriter != null || streams != null) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "GET can't send streams!");
}
}
protected abstract boolean isFollowRedirects();
@SuppressWarnings({"unchecked", "rawtypes"})
protected NamedList<Object> processErrorsAndResponse(
int httpStatus,
String responseReason,
String responseMethod,
final ResponseParser processor,
InputStream is,
String mimeType,
String encoding,
final boolean isV2Api,
String urlExceptionMessage)
throws SolrServerException {
boolean shouldClose = true;
try {
// handle some http level checks before trying to parse the response
switch (httpStatus) {
case 200: // OK
case 400: // Bad Request
case 409: // Conflict
break;
case 301: // Moved Permanently
case 302: // Moved Temporarily
if (!isFollowRedirects()) {
throw new SolrServerException(
"Server at " + urlExceptionMessage + " sent back a redirect (" + httpStatus + ").");
}
break;
default:
if (processor == null || mimeType == null) {
throw new BaseHttpSolrClient.RemoteSolrException(
urlExceptionMessage,
httpStatus,
"non ok status: " + httpStatus + ", message:" + responseReason,
null);
}
}
if (wantStream(processor)) {
// no processor specified, return raw stream
NamedList<Object> rsp = new NamedList<>();
rsp.add("stream", is);
rsp.add("responseStatus", httpStatus);
// Only case where stream should not be closed
shouldClose = false;
return rsp;
}
checkContentType(processor, is, mimeType, encoding, httpStatus, urlExceptionMessage);
NamedList<Object> rsp;
try {
rsp = processor.processResponse(is, encoding);
} catch (Exception e) {
throw new BaseHttpSolrClient.RemoteSolrException(
urlExceptionMessage, httpStatus, e.getMessage(), e);
}
Object error = rsp == null ? null : rsp.get("error");
if (rsp != null && error == null && processor instanceof NoOpResponseParser) {
error = rsp.get("response");
}
if (error != null
&& (String.valueOf(getObjectByPath(error, true, errPath))
.endsWith("ExceptionWithErrObject"))) {
throw BaseHttpSolrClient.RemoteExecutionException.create(urlExceptionMessage, rsp);
}
if (httpStatus != 200 && !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) {
/* Ignored */
}
if (reason == null) {
StringBuilder msg = new StringBuilder();
msg.append(responseReason).append("\n").append("request: ").append(responseMethod);
if (error != null) {
msg.append("\n\nError returned:\n").append(error);
}
reason = java.net.URLDecoder.decode(msg.toString(), FALLBACK_CHARSET);
}
BaseHttpSolrClient.RemoteSolrException rss =
new BaseHttpSolrClient.RemoteSolrException(
urlExceptionMessage, httpStatus, reason, null);
if (metadata != null) rss.setMetadata(metadata);
throw rss;
}
return rsp;
} finally {
if (shouldClose) {
try {
is.close();
} catch (IOException e) {
// quitely
}
}
}
}
protected boolean wantStream(final ResponseParser processor) {
return processor == null || processor instanceof InputStreamResponseParser;
}
protected abstract boolean processorAcceptsMimeType(
Collection<String> processorSupportedContentTypes, String mimeType);
protected abstract String allProcessorSupportedContentTypesCommaDelimited(
Collection<String> processorSupportedContentTypes);
/**
* Validates that the content type in the response can be processed by the Response Parser. Throws
* a {@code RemoteSolrException} if not.
*/
private void checkContentType(
ResponseParser processor,
InputStream is,
String mimeType,
String encoding,
int httpStatus,
String urlExceptionMessage) {
if (mimeType == null
|| (processor == this.parser && defaultParserMimeTypes.contains(mimeType))) {
// Shortcut the default scenario
return;
}
final Collection<String> processorSupportedContentTypes = processor.getContentTypes();
if (processorSupportedContentTypes != null && !processorSupportedContentTypes.isEmpty()) {
boolean processorAcceptsMimeType =
processorAcceptsMimeType(processorSupportedContentTypes, mimeType);
if (!processorAcceptsMimeType) {
// unexpected mime type
final String allSupportedTypes =
allProcessorSupportedContentTypesCommaDelimited(processorSupportedContentTypes);
String prefix =
"Expected mime type in [" + allSupportedTypes + "] but got " + mimeType + ". ";
String exceptionEncoding = encoding != null ? encoding : FALLBACK_CHARSET.name();
try {
ByteArrayOutputStream body = new ByteArrayOutputStream();
is.transferTo(body);
throw new BaseHttpSolrClient.RemoteSolrException(
urlExceptionMessage, httpStatus, prefix + body.toString(exceptionEncoding), null);
} catch (IOException e) {
throw new BaseHttpSolrClient.RemoteSolrException(
urlExceptionMessage,
httpStatus,
"Could not parse response with encoding " + exceptionEncoding,
e);
}
}
}
}
protected static String basicAuthCredentialsToAuthorizationString(String user, String pass) {
String userPass = user + ":" + pass;
return "Basic " + Base64.getEncoder().encodeToString(userPass.getBytes(FALLBACK_CHARSET));
}
protected void setParser(ResponseParser parser) {
this.parser = parser;
updateDefaultMimeTypeForParser();
}
protected abstract void updateDefaultMimeTypeForParser();
/**
* Execute an asynchronous request against a Solr server for a given collection.
*
* @param request the request to execute
* @param collection the collection to execute the request against
* @return a {@link CompletableFuture} that tracks the progress of the async request. Supports
* cancelling requests via {@link CompletableFuture#cancel(boolean)}, adding callbacks/error
* handling using {@link CompletableFuture#whenComplete(BiConsumer)} and {@link
* CompletableFuture#exceptionally(Function)} methods, and other CompletableFuture
* functionality. Will complete exceptionally in case of either an {@link IOException} or
* {@link SolrServerException} during the request. Once completed, the CompletableFuture will
* contain a {@link NamedList} with the response from the server.
*/
public abstract CompletableFuture<NamedList<Object>> requestAsync(
final SolrRequest<?> request, String collection);
/**
* Execute an asynchronous request against a Solr server using the default collection.
*
* @param request the request to execute
* @return a {@link CompletableFuture} see {@link #requestAsync(SolrRequest, String)}.
*/
public CompletableFuture<NamedList<Object>> requestAsync(final SolrRequest<?> request) {
return requestAsync(request, null);
}
public boolean isV2ApiRequest(final SolrRequest<?> request) {
return request instanceof V2Request || request.getPath().contains("/____v2");
}
public String getBaseURL() {
return serverBaseUrl;
}
public ResponseParser getParser() {
return parser;
}
public long getIdleTimeout() {
return idleTimeoutMillis;
}
public Set<String> getUrlParamNames() {
return urlParamNames;
}
}