/*
 * 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.MultipartConfigElement;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.Part;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandles;
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.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.tracing.GlobalTracer;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.server.MultiParts;
import org.eclipse.jetty.server.Request;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.apache.solr.common.params.CommonParams.PATH;


public class SolrRequestParsers {

  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  // 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();

  /**
   * 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.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());
    }
  }

  /**
   * 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 MultipartConfigElement multipartConfigElement;

    public MultipartRequestParser(int uploadLimitKB) {
      multipartConfigElement = new MultipartConfigElement(
          null, // temp dir (null=default)
          -1, // maxFileSize  (-1=none)
          uploadLimitKB * 1024, // maxRequestSize
          100 * 1024 ); // fileSizeThreshold after which will go to disk
    }
    
    @Override
    public SolrParams parseParamsAndFillStreams(
        final HttpServletRequest req, ArrayList<ContentStream> streams) throws Exception {
      if (!isMultipart(req)) {
        throw new SolrException( ErrorCode.BAD_REQUEST, "Not multipart content! "+req.getContentType() );
      }
      // Magic way to tell Jetty dynamically we want multi-part processing.  "Request" here is a Jetty class
      req.setAttribute(Request.MULTIPART_CONFIG_ELEMENT, multipartConfigElement);

      MultiMapSolrParams params = parseQueryString( req.getQueryString() );

      // IMPORTANT: the Parts will all have the delete() method called by cleanupMultipartFiles()

      for (Part part : req.getParts()) {
        if (part.getSubmittedFileName() == null) { // thus a form field and not file upload
          // If it's a form field, put it in our parameter map
          String partAsString = org.apache.commons.io.IOUtils.toString(new PartContentStream(part).getReader());
          MultiMapSolrParams.addParam(
              part.getName().trim(),
              partAsString, params.getMap() );
        } else { // file upload
          streams.add(new PartContentStream(part));
        }
      }
      return params;
    }

    boolean isMultipart(HttpServletRequest req) {
      // Jetty utilities
      return MimeTypes.Type.MULTIPART_FORM_DATA.is(HttpFields.valueParameters(req.getContentType(), null));
    }

    /** Wrap a MultiPart-{@link Part} as a {@link ContentStream} */
    static class PartContentStream extends ContentStreamBase {
      private final Part part;

      public PartContentStream(Part part ) {
        this.part = part;
        contentType = part.getContentType();
        name = part.getName();
        sourceInfo = part.getSubmittedFileName();
        size = part.getSize();
      }

      @Override
      public InputStream getStream() throws IOException {
        return part.getInputStream();
      }
    }
  }


  /** Clean up any tmp files created by MultiPartInputStream. */
  static void cleanupMultipartFiles(HttpServletRequest request) {
    // See Jetty MultiPartCleanerListener from which we drew inspiration
    MultiParts multiParts = (MultiParts) request.getAttribute(Request.MULTIPARTS);
    if (multiParts == null || multiParts.getContext() != request.getServletContext()) {
      return;
    }

    log.debug("Deleting multipart files");

    Collection<Part> parts;
    try {
      parts = multiParts.getParts();
    } catch (IOException e) {
      log.warn("Errors deleting multipart tmp files", e);
      return;
    }

    for (Part part : parts) {
      try {
        part.delete();
      } catch (IOException e) {
        log.warn("Errors deleting multipart tmp files", e);
      }
    }
  }

  /**
   * 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 (multipart.isMultipart(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;
    }
  }



}