Java tutorial
/* * 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.servlet; import javax.servlet.http.HttpServletRequest; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.nio.ByteBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; import java.security.Principal; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.apache.commons.fileupload.FileItem; import org.apache.commons.fileupload.disk.DiskFileItemFactory; import org.apache.commons.fileupload.servlet.ServletFileUpload; import org.apache.commons.io.FileCleaningTracker; import org.apache.commons.io.input.CloseShieldInputStream; import org.apache.lucene.util.IOUtils; import org.apache.solr.api.V2HttpCall; import org.apache.solr.common.SolrException; import org.apache.solr.common.SolrException.ErrorCode; import org.apache.solr.common.params.CommonParams; import org.apache.solr.common.params.MultiMapSolrParams; import org.apache.solr.common.params.SolrParams; import org.apache.solr.common.util.CommandOperation; import org.apache.solr.common.util.ContentStream; import org.apache.solr.common.util.ContentStreamBase; import org.apache.solr.common.util.FastInputStream; import org.apache.solr.core.RequestHandlers; import org.apache.solr.core.SolrConfig; import org.apache.solr.core.SolrCore; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.request.SolrQueryRequestBase; import org.apache.solr.util.RTimerTree; import org.apache.solr.util.SolrFileCleaningTracker; import org.apache.solr.util.tracing.GlobalTracer; import static org.apache.solr.common.params.CommonParams.PATH; public class SolrRequestParsers { // Should these constants be in a more public place? public static final String MULTIPART = "multipart"; public static final String FORMDATA = "formdata"; public static final String RAW = "raw"; public static final String SIMPLE = "simple"; public static final String STANDARD = "standard"; private static final Charset CHARSET_US_ASCII = Charset.forName("US-ASCII"); public static final String INPUT_ENCODING_KEY = "ie"; private static final byte[] INPUT_ENCODING_BYTES = INPUT_ENCODING_KEY.getBytes(CHARSET_US_ASCII); public static final String REQUEST_TIMER_SERVLET_ATTRIBUTE = "org.apache.solr.RequestTimer"; private final HashMap<String, SolrRequestParser> parsers = new HashMap<>(); private final boolean enableRemoteStreams; private final boolean enableStreamBody; private StandardRequestParser standard; private boolean handleSelect = true; private boolean addHttpRequestToContext; /** Default instance for e.g. admin requests. Limits to 2 MB uploads and does not allow remote streams. */ public static final SolrRequestParsers DEFAULT = new SolrRequestParsers(); public static volatile SolrFileCleaningTracker fileCleaningTracker; /** * Pass in an xml configuration. A null configuration will enable * everything with maximum values. */ public SolrRequestParsers(SolrConfig globalConfig) { final int multipartUploadLimitKB, formUploadLimitKB; if (globalConfig == null) { multipartUploadLimitKB = formUploadLimitKB = Integer.MAX_VALUE; enableRemoteStreams = false; enableStreamBody = false; handleSelect = false; addHttpRequestToContext = false; } else { multipartUploadLimitKB = globalConfig.getMultipartUploadLimitKB(); formUploadLimitKB = globalConfig.getFormUploadLimitKB(); enableRemoteStreams = globalConfig.isEnableRemoteStreams(); enableStreamBody = globalConfig.isEnableStreamBody(); // Let this filter take care of /select?xxx format handleSelect = globalConfig.isHandleSelect(); addHttpRequestToContext = globalConfig.isAddHttpRequestToContext(); } init(multipartUploadLimitKB, formUploadLimitKB); } private SolrRequestParsers() { enableRemoteStreams = false; enableStreamBody = false; handleSelect = false; addHttpRequestToContext = false; init(Integer.MAX_VALUE, Integer.MAX_VALUE); } private void init(int multipartUploadLimitKB, int formUploadLimitKB) { MultipartRequestParser multi = new MultipartRequestParser(multipartUploadLimitKB); RawRequestParser raw = new RawRequestParser(); FormDataRequestParser formdata = new FormDataRequestParser(formUploadLimitKB); standard = new StandardRequestParser(multi, raw, formdata); // I don't see a need to have this publicly configured just yet // adding it is trivial parsers.put(MULTIPART, multi); parsers.put(FORMDATA, formdata); parsers.put(RAW, raw); parsers.put(SIMPLE, new SimpleRequestParser()); parsers.put(STANDARD, standard); parsers.put("", standard); } private static RTimerTree getRequestTimer(HttpServletRequest req) { final Object reqTimer = req.getAttribute(REQUEST_TIMER_SERVLET_ATTRIBUTE); if (reqTimer != null && reqTimer instanceof RTimerTree) { return ((RTimerTree) reqTimer); } return new RTimerTree(); } public SolrQueryRequest parse(SolrCore core, String path, HttpServletRequest req) throws Exception { SolrRequestParser parser = standard; // TODO -- in the future, we could pick a different parser based on the request // Pick the parser from the request... ArrayList<ContentStream> streams = new ArrayList<>(1); SolrParams params = parser.parseParamsAndFillStreams(req, streams); if (GlobalTracer.get().tracing()) { GlobalTracer.get().getTracer().activeSpan().setTag("params", params.toString()); } SolrQueryRequest sreq = buildRequestFrom(core, params, streams, getRequestTimer(req), req); // Handlers and login will want to know the path. If it contains a ':' // the handler could use it for RESTful URLs sreq.getContext().put(PATH, RequestHandlers.normalize(path)); sreq.getContext().put("httpMethod", req.getMethod()); if (addHttpRequestToContext) { sreq.getContext().put("httpRequest", req); } return sreq; } public SolrQueryRequest buildRequestFrom(SolrCore core, SolrParams params, Collection<ContentStream> streams) throws Exception { return buildRequestFrom(core, params, streams, new RTimerTree(), null); } private SolrQueryRequest buildRequestFrom(SolrCore core, SolrParams params, Collection<ContentStream> streams, RTimerTree requestTimer, final HttpServletRequest req) throws Exception { // The content type will be applied to all streaming content String contentType = params.get(CommonParams.STREAM_CONTENTTYPE); // Handle anything with a remoteURL String[] strs = params.getParams(CommonParams.STREAM_URL); if (strs != null) { if (!enableRemoteStreams) { throw new SolrException(ErrorCode.BAD_REQUEST, "Remote Streaming is disabled."); } for (final String url : strs) { ContentStreamBase stream = new ContentStreamBase.URLStream(new URL(url)); if (contentType != null) { stream.setContentType(contentType); } streams.add(stream); } } // Handle streaming files strs = params.getParams(CommonParams.STREAM_FILE); if (strs != null) { if (!enableRemoteStreams) { throw new SolrException(ErrorCode.BAD_REQUEST, "Remote Streaming is disabled. See http://lucene.apache.org/solr/guide/requestdispatcher-in-solrconfig.html for help"); } for (final String file : strs) { ContentStreamBase stream = new ContentStreamBase.FileStream(new File(file)); if (contentType != null) { stream.setContentType(contentType); } streams.add(stream); } } // Check for streams in the request parameters strs = params.getParams(CommonParams.STREAM_BODY); if (strs != null) { if (!enableStreamBody) { throw new SolrException(ErrorCode.BAD_REQUEST, "Stream Body is disabled. See http://lucene.apache.org/solr/guide/requestdispatcher-in-solrconfig.html for help"); } for (final String body : strs) { ContentStreamBase stream = new ContentStreamBase.StringStream(body); if (contentType != null) { stream.setContentType(contentType); } streams.add(stream); } } final HttpSolrCall httpSolrCall = req == null ? null : (HttpSolrCall) req.getAttribute(HttpSolrCall.class.getName()); SolrQueryRequestBase q = new SolrQueryRequestBase(core, params, requestTimer) { @Override public Principal getUserPrincipal() { return req == null ? null : req.getUserPrincipal(); } @Override public List<CommandOperation> getCommands(boolean validateInput) { if (httpSolrCall != null) { return httpSolrCall.getCommands(validateInput); } return super.getCommands(validateInput); } @Override public Map<String, String> getPathTemplateValues() { if (httpSolrCall != null && httpSolrCall instanceof V2HttpCall) { return ((V2HttpCall) httpSolrCall).getUrlParts(); } return super.getPathTemplateValues(); } @Override public HttpSolrCall getHttpSolrCall() { return httpSolrCall; } }; if (streams != null && streams.size() > 0) { q.setContentStreams(streams); } return q; } private static HttpSolrCall getHttpSolrCall(HttpServletRequest req) { return req == null ? null : (HttpSolrCall) req.getAttribute(HttpSolrCall.class.getName()); } /** * Given a url-encoded query string (UTF-8), map it into solr params */ public static MultiMapSolrParams parseQueryString(String queryString) { Map<String, String[]> map = new HashMap<>(); parseQueryString(queryString, map); return new MultiMapSolrParams(map); } /** * Given a url-encoded query string (UTF-8), map it into the given map * @param queryString as given from URL * @param map place all parameters in this map */ static void parseQueryString(final String queryString, final Map<String, String[]> map) { if (queryString != null && queryString.length() > 0) { try { final int len = queryString.length(); // this input stream emulates to get the raw bytes from the URL as passed to servlet container, it disallows any byte > 127 and enforces to %-escape them: final InputStream in = new InputStream() { int pos = 0; @Override public int read() { if (pos < len) { final char ch = queryString.charAt(pos); if (ch > 127) { throw new SolrException(ErrorCode.BAD_REQUEST, "URLDecoder: The query string contains a not-%-escaped byte > 127 at position " + pos); } pos++; return ch; } else { return -1; } } }; parseFormDataContent(in, Long.MAX_VALUE, StandardCharsets.UTF_8, map, true); } catch (IOException ioe) { throw new SolrException(ErrorCode.BAD_REQUEST, ioe); } } } /** * Given a url-encoded form from POST content (as InputStream), map it into the given map. * The given InputStream should be buffered! * @param postContent to be parsed * @param charset to be used to decode resulting bytes after %-decoding * @param map place all parameters in this map */ @SuppressWarnings({ "fallthrough", "resource" }) static long parseFormDataContent(final InputStream postContent, final long maxLen, Charset charset, final Map<String, String[]> map, boolean supportCharsetParam) throws IOException { CharsetDecoder charsetDecoder = supportCharsetParam ? null : getCharsetDecoder(charset); final LinkedList<Object> buffer = supportCharsetParam ? new LinkedList<>() : null; long len = 0L, keyPos = 0L, valuePos = 0L; final ByteArrayOutputStream keyStream = new ByteArrayOutputStream(), valueStream = new ByteArrayOutputStream(); ByteArrayOutputStream currentStream = keyStream; for (;;) { int b = postContent.read(); switch (b) { case -1: // end of stream case '&': // separator if (keyStream.size() > 0) { final byte[] keyBytes = keyStream.toByteArray(), valueBytes = valueStream.toByteArray(); if (Arrays.equals(keyBytes, INPUT_ENCODING_BYTES)) { // we found a charset declaration in the raw bytes if (charsetDecoder != null) { throw new SolrException(ErrorCode.BAD_REQUEST, supportCharsetParam ? ("Query string invalid: duplicate '" + INPUT_ENCODING_KEY + "' (input encoding) key.") : ("Key '" + INPUT_ENCODING_KEY + "' (input encoding) cannot " + "be used in POSTed application/x-www-form-urlencoded form data. " + "To set the input encoding of POSTed form data, use the " + "'Content-Type' header and provide a charset!")); } // decode the charset from raw bytes charset = Charset .forName(decodeChars(valueBytes, keyPos, getCharsetDecoder(CHARSET_US_ASCII))); charsetDecoder = getCharsetDecoder(charset); // finally decode all buffered tokens decodeBuffer(buffer, map, charsetDecoder); } else if (charsetDecoder == null) { // we have no charset decoder until now, buffer the keys / values for later processing: buffer.add(keyBytes); buffer.add(Long.valueOf(keyPos)); buffer.add(valueBytes); buffer.add(Long.valueOf(valuePos)); } else { // we already have a charsetDecoder, so we can directly decode without buffering: final String key = decodeChars(keyBytes, keyPos, charsetDecoder), value = decodeChars(valueBytes, valuePos, charsetDecoder); MultiMapSolrParams.addParam(key.trim(), value, map); } } else if (valueStream.size() > 0) { throw new SolrException(ErrorCode.BAD_REQUEST, "application/x-www-form-urlencoded invalid: missing key"); } keyStream.reset(); valueStream.reset(); keyPos = valuePos = len + 1; currentStream = keyStream; break; case '+': // space replacement currentStream.write(' '); break; case '%': // escape final int upper = digit16(b = postContent.read()); len++; final int lower = digit16(b = postContent.read()); len++; currentStream.write(((upper << 4) + lower)); break; case '=': // kv separator if (currentStream == keyStream) { valuePos = len + 1; currentStream = valueStream; break; } // fall-through default: currentStream.write(b); } if (b == -1) { break; } len++; if (len > maxLen) { throw new SolrException(ErrorCode.BAD_REQUEST, "application/x-www-form-urlencoded content exceeds upload limit of " + (maxLen / 1024L) + " KB"); } } // if we have not seen a charset declaration, decode the buffer now using the default one (UTF-8 or given via Content-Type): if (buffer != null && !buffer.isEmpty()) { assert charsetDecoder == null; decodeBuffer(buffer, map, getCharsetDecoder(charset)); } return len; } private static CharsetDecoder getCharsetDecoder(Charset charset) { return charset.newDecoder().onMalformedInput(CodingErrorAction.REPORT) .onUnmappableCharacter(CodingErrorAction.REPORT); } private static String decodeChars(byte[] bytes, long position, CharsetDecoder charsetDecoder) { try { return charsetDecoder.decode(ByteBuffer.wrap(bytes)).toString(); } catch (CharacterCodingException cce) { throw new SolrException(ErrorCode.BAD_REQUEST, "URLDecoder: Invalid character encoding detected after position " + position + " of query string / form data (while parsing as " + charsetDecoder.charset().name() + ")"); } } private static void decodeBuffer(final LinkedList<Object> input, final Map<String, String[]> map, CharsetDecoder charsetDecoder) { for (final Iterator<Object> it = input.iterator(); it.hasNext();) { final byte[] keyBytes = (byte[]) it.next(); it.remove(); final Long keyPos = (Long) it.next(); it.remove(); final byte[] valueBytes = (byte[]) it.next(); it.remove(); final Long valuePos = (Long) it.next(); it.remove(); MultiMapSolrParams.addParam(decodeChars(keyBytes, keyPos.longValue(), charsetDecoder).trim(), decodeChars(valueBytes, valuePos.longValue(), charsetDecoder), map); } } private static int digit16(int b) { if (b == -1) { throw new SolrException(ErrorCode.BAD_REQUEST, "URLDecoder: Incomplete trailing escape (%) pattern"); } if (b >= '0' && b <= '9') { return b - '0'; } if (b >= 'A' && b <= 'F') { return b - ('A' - 10); } if (b >= 'a' && b <= 'f') { return b - ('a' - 10); } throw new SolrException(ErrorCode.BAD_REQUEST, "URLDecoder: Invalid digit (" + ((char) b) + ") in escape (%) pattern"); } public boolean isHandleSelect() { return handleSelect; } public void setHandleSelect(boolean handleSelect) { this.handleSelect = handleSelect; } public boolean isAddRequestHeadersToContext() { return addHttpRequestToContext; } public void setAddRequestHeadersToContext(boolean addRequestHeadersToContext) { this.addHttpRequestToContext = addRequestHeadersToContext; } //----------------------------------------------------------------- //----------------------------------------------------------------- // I guess we don't really even need the interface, but i'll keep it here just for kicks interface SolrRequestParser { public SolrParams parseParamsAndFillStreams(final HttpServletRequest req, ArrayList<ContentStream> streams) throws Exception; } //----------------------------------------------------------------- //----------------------------------------------------------------- /** * The simple parser just uses the params directly, does not support POST URL-encoded forms */ static class SimpleRequestParser implements SolrRequestParser { @Override public SolrParams parseParamsAndFillStreams(final HttpServletRequest req, ArrayList<ContentStream> streams) throws Exception { return parseQueryString(req.getQueryString()); } } /** * Wrap an HttpServletRequest as a ContentStream */ static class HttpRequestContentStream extends ContentStreamBase { private final HttpServletRequest req; public HttpRequestContentStream(HttpServletRequest req) { this.req = req; contentType = req.getContentType(); // name = ??? // sourceInfo = ??? String v = req.getHeader("Content-Length"); if (v != null) { size = Long.valueOf(v); } } @Override public InputStream getStream() throws IOException { // we explicitly protect this servlet stream from being closed // so that it does not trip our test assert in our close shield // in SolrDispatchFilter - we must allow closes from getStream // due to the other impls of ContentStream return new CloseShieldInputStream(req.getInputStream()); } } /** * Wrap a FileItem as a ContentStream */ static class FileItemContentStream extends ContentStreamBase { private final FileItem item; public FileItemContentStream(FileItem f) { item = f; contentType = item.getContentType(); name = item.getName(); sourceInfo = item.getFieldName(); size = item.getSize(); } @Override public InputStream getStream() throws IOException { return item.getInputStream(); } } /** * The raw parser just uses the params directly */ static class RawRequestParser implements SolrRequestParser { @Override public SolrParams parseParamsAndFillStreams(final HttpServletRequest req, ArrayList<ContentStream> streams) throws Exception { streams.add(new HttpRequestContentStream(req)); return parseQueryString(req.getQueryString()); } } /** * Extract Multipart streams */ static class MultipartRequestParser implements SolrRequestParser { private final int uploadLimitKB; private DiskFileItemFactory factory = new DiskFileItemFactory(); public MultipartRequestParser(int limit) { uploadLimitKB = limit; // Set factory constraints FileCleaningTracker fct = fileCleaningTracker; if (fct != null) { factory.setFileCleaningTracker(fileCleaningTracker); } // TODO - configure factory.setSizeThreshold(yourMaxMemorySize); // TODO - configure factory.setRepository(yourTempDirectory); } @Override public SolrParams parseParamsAndFillStreams(final HttpServletRequest req, ArrayList<ContentStream> streams) throws Exception { if (!ServletFileUpload.isMultipartContent(req)) { throw new SolrException(ErrorCode.BAD_REQUEST, "Not multipart content! " + req.getContentType()); } MultiMapSolrParams params = parseQueryString(req.getQueryString()); // Create a new file upload handler ServletFileUpload upload = new ServletFileUpload(factory); upload.setSizeMax(((long) uploadLimitKB) * 1024L); // Parse the request List<FileItem> items = upload.parseRequest(req); for (FileItem item : items) { // If it's a form field, put it in our parameter map if (item.isFormField()) { MultiMapSolrParams.addParam(item.getFieldName().trim(), item.getString(), params.getMap()); } // Add the stream else { streams.add(new FileItemContentStream(item)); } } return params; } } /** * Extract application/x-www-form-urlencoded form data for POST requests */ static class FormDataRequestParser implements SolrRequestParser { private static final long WS_MASK = (1L << ' ') | (1L << '\t') | (1L << '\r') | (1L << '\n') | (1L << '#') | (1L << '/') | (0x01); // set 1 bit so 0xA0 will be flagged as possible whitespace private final int uploadLimitKB; public FormDataRequestParser(int limit) { uploadLimitKB = limit; } public SolrParams parseParamsAndFillStreams(HttpServletRequest req, ArrayList<ContentStream> streams, InputStream in) throws Exception { final Map<String, String[]> map = new HashMap<>(); // also add possible URL parameters and include into the map (parsed using UTF-8): final String qs = req.getQueryString(); if (qs != null) { parseQueryString(qs, map); } // may be -1, so we check again later. But if it's already greater we can stop processing! final long totalLength = req.getContentLength(); final long maxLength = ((long) uploadLimitKB) * 1024L; if (totalLength > maxLength) { throw new SolrException(ErrorCode.BAD_REQUEST, "application/x-www-form-urlencoded content length (" + totalLength + " bytes) exceeds upload limit of " + uploadLimitKB + " KB"); } // get query String from request body, using the charset given in content-type: final String cs = ContentStreamBase.getCharsetFromContentType(req.getContentType()); final Charset charset = (cs == null) ? StandardCharsets.UTF_8 : Charset.forName(cs); try { // Protect container owned streams from being closed by us, see SOLR-8933 in = FastInputStream.wrap(in == null ? new CloseShieldInputStream(req.getInputStream()) : in); final long bytesRead = parseFormDataContent(in, maxLength, charset, map, false); if (bytesRead == 0L && totalLength > 0L) { throw getParameterIncompatibilityException(); } } catch (IOException ioe) { throw new SolrException(ErrorCode.BAD_REQUEST, ioe); } catch (IllegalStateException ise) { throw (SolrException) getParameterIncompatibilityException().initCause(ise); } finally { IOUtils.closeWhileHandlingException(in); } return new MultiMapSolrParams(map); } @Override public SolrParams parseParamsAndFillStreams(HttpServletRequest req, ArrayList<ContentStream> streams) throws Exception { if (!isFormData(req)) { throw new SolrException(ErrorCode.BAD_REQUEST, "Not application/x-www-form-urlencoded content: " + req.getContentType()); } return parseParamsAndFillStreams(req, streams, null); } public static SolrException getParameterIncompatibilityException() { return new SolrException(ErrorCode.SERVER_ERROR, "Solr requires that request parameters sent using application/x-www-form-urlencoded " + "content-type can be read through the request input stream. Unfortunately, the " + "stream was empty / not available. This may be caused by another servlet filter calling " + "ServletRequest.getParameter*() before SolrDispatchFilter, please remove it."); } public boolean isFormData(HttpServletRequest req) { String contentType = req.getContentType(); if (contentType != null) { int idx = contentType.indexOf(';'); if (idx > 0) { // remove the charset definition "; charset=utf-8" contentType = contentType.substring(0, idx); } contentType = contentType.trim(); if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)) { return true; } } return false; } } /** * The default Logic */ static class StandardRequestParser implements SolrRequestParser { MultipartRequestParser multipart; RawRequestParser raw; FormDataRequestParser formdata; StandardRequestParser(MultipartRequestParser multi, RawRequestParser raw, FormDataRequestParser formdata) { this.multipart = multi; this.raw = raw; this.formdata = formdata; } @Override public SolrParams parseParamsAndFillStreams(final HttpServletRequest req, ArrayList<ContentStream> streams) throws Exception { String contentType = req.getContentType(); String method = req.getMethod(); // No need to uppercase... HTTP verbs are case sensitive String uri = req.getRequestURI(); boolean isV2 = getHttpSolrCall(req) instanceof V2HttpCall; boolean isPost = "POST".equals(method); // SOLR-6787 changed the behavior of a POST without content type. Previously it would throw an exception, // but now it will use the raw request parser. /*** if (contentType == null && isPost) { throw new SolrException(ErrorCode.UNSUPPORTED_MEDIA_TYPE, "Must specify a Content-Type header with POST requests"); } ***/ // According to previous StandardRequestParser logic (this is a re-written version), // POST was handled normally, but other methods (PUT/DELETE) // were handled by restlet if the URI contained /schema or /config // "handled by restlet" means that we don't attempt to handle any request body here. if (!isPost) { if (isV2) { return raw.parseParamsAndFillStreams(req, streams); } if (contentType == null) { return parseQueryString(req.getQueryString()); } // OK, we have a BODY at this point boolean restletPath = false; int idx = uri.indexOf("/schema"); if (idx >= 0 && uri.endsWith("/schema") || uri.contains("/schema/")) { restletPath = true; } if (restletPath) { return parseQueryString(req.getQueryString()); } if ("PUT".equals(method) || "DELETE".equals(method)) { throw new SolrException(ErrorCode.BAD_REQUEST, "Unsupported method: " + method + " for request " + req); } } if (formdata.isFormData(req)) { String userAgent = req.getHeader("User-Agent"); boolean isCurl = userAgent != null && userAgent.startsWith("curl/"); FastInputStream input = FastInputStream.wrap(req.getInputStream()); if (isCurl) { SolrParams params = autodetect(req, streams, input); if (params != null) return params; } return formdata.parseParamsAndFillStreams(req, streams, input); } if (ServletFileUpload.isMultipartContent(req)) { return multipart.parseParamsAndFillStreams(req, streams); } // some other content-type (json, XML, csv, etc) return raw.parseParamsAndFillStreams(req, streams); } } private static final long WS_MASK = (1L << ' ') | (1L << '\t') | (1L << '\r') | (1L << '\n') | (1L << '#') | (1L << '/') | (0x01); // set 1 bit so 0xA0 will be flagged as possible whitespace /** Returns the parameter map if a different content type was auto-detected */ private static SolrParams autodetect(HttpServletRequest req, ArrayList<ContentStream> streams, FastInputStream in) throws IOException { String detectedContentType = null; boolean shouldClose = true; try { in.peek(); // should cause some bytes to be read byte[] arr = in.getBuffer(); int pos = in.getPositionInBuffer(); int end = in.getEndInBuffer(); for (int i = pos; i < end - 1; i++) { // we do "end-1" because we check "arr[i+1]" sometimes in the loop body int ch = arr[i]; boolean isWhitespace = ((WS_MASK >> ch) & 0x01) != 0 && (ch <= ' ' || ch == 0xa0); if (!isWhitespace) { // first non-whitespace chars if (ch == '#' // single line comment || (ch == '/' && (arr[i + 1] == '/' || arr[i + 1] == '*')) // single line or multi-line comment || (ch == '{' || ch == '[') // start of JSON object ) { detectedContentType = "application/json"; } if (ch == '<') { detectedContentType = "text/xml"; } break; } } if (detectedContentType == null) { shouldClose = false; return null; } Long size = null; String v = req.getHeader("Content-Length"); if (v != null) { size = Long.valueOf(v); } streams.add(new InputStreamContentStream(in, detectedContentType, size)); final Map<String, String[]> map = new HashMap<>(); // also add possible URL parameters and include into the map (parsed using UTF-8): final String qs = req.getQueryString(); if (qs != null) { parseQueryString(qs, map); } return new MultiMapSolrParams(map); } catch (IOException ioe) { throw new SolrException(ErrorCode.BAD_REQUEST, ioe); } catch (IllegalStateException ise) { throw (SolrException) FormDataRequestParser.getParameterIncompatibilityException().initCause(ise); } finally { if (shouldClose) { IOUtils.closeWhileHandlingException(in); } } } /** * Wrap InputStream as a ContentStream */ static class InputStreamContentStream extends ContentStreamBase { private final InputStream is; public InputStreamContentStream(InputStream is, String detectedContentType, Long size) { this.is = is; this.contentType = detectedContentType; this.size = size; } @Override public InputStream getStream() throws IOException { return is; } } }