AbstractRestChannel.java 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. /*
  2. * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
  3. * or more contributor license agreements. Licensed under the Elastic License
  4. * 2.0 and the Server Side Public License, v 1; you may not use this file except
  5. * in compliance with, at your election, the Elastic License 2.0 or the Server
  6. * Side Public License, v 1.
  7. */
  8. package org.elasticsearch.rest;
  9. import org.apache.logging.log4j.LogManager;
  10. import org.apache.logging.log4j.Logger;
  11. import org.elasticsearch.common.Strings;
  12. import org.elasticsearch.common.io.stream.BytesStream;
  13. import org.elasticsearch.common.io.stream.BytesStreamOutput;
  14. import org.elasticsearch.core.Nullable;
  15. import org.elasticsearch.xcontent.ParsedMediaType;
  16. import org.elasticsearch.xcontent.XContentBuilder;
  17. import org.elasticsearch.xcontent.XContentFactory;
  18. import org.elasticsearch.xcontent.XContentType;
  19. import java.io.IOException;
  20. import java.io.OutputStream;
  21. import java.io.UncheckedIOException;
  22. import java.util.Collections;
  23. import java.util.Map;
  24. import java.util.Set;
  25. import java.util.function.Predicate;
  26. import static java.util.stream.Collectors.toSet;
  27. public abstract class AbstractRestChannel implements RestChannel {
  28. private static final Logger logger = LogManager.getLogger(AbstractRestChannel.class);
  29. private static final Predicate<String> INCLUDE_FILTER = f -> f.charAt(0) != '-';
  30. private static final Predicate<String> EXCLUDE_FILTER = INCLUDE_FILTER.negate();
  31. protected final RestRequest request;
  32. private final boolean detailedErrorsEnabled;
  33. private final String format;
  34. private final String filterPath;
  35. private final boolean pretty;
  36. private final boolean human;
  37. private final String acceptHeader;
  38. private BytesStream bytesOut;
  39. /**
  40. * Construct a channel for handling the request.
  41. *
  42. * @param request the request
  43. * @param detailedErrorsEnabled if detailed errors should be reported to the channel
  44. * @throws IllegalArgumentException if parsing the pretty or human parameters fails
  45. */
  46. protected AbstractRestChannel(RestRequest request, boolean detailedErrorsEnabled) {
  47. this.request = request;
  48. this.detailedErrorsEnabled = detailedErrorsEnabled;
  49. this.format = request.param("format");
  50. this.acceptHeader = request.header("Accept");
  51. this.filterPath = request.param("filter_path", null);
  52. this.pretty = request.paramAsBoolean("pretty", false);
  53. this.human = request.paramAsBoolean("human", false);
  54. }
  55. @Override
  56. public XContentBuilder newBuilder() throws IOException {
  57. return newBuilder(request.getXContentType(), true);
  58. }
  59. @Override
  60. public XContentBuilder newErrorBuilder() throws IOException {
  61. // release whatever output we already buffered and write error response to fresh buffer
  62. releaseOutputBuffer();
  63. // Disable filtering when building error responses
  64. return newBuilder(request.getXContentType(), false);
  65. }
  66. /**
  67. * Creates a new {@link XContentBuilder} for a response to be sent using this channel. The builder's type is determined by the following
  68. * logic. If the request has a format parameter that will be used to attempt to map to an {@link XContentType}. If there is no format
  69. * parameter, the HTTP Accept header is checked to see if it can be matched to a {@link XContentType}. If this first attempt to map
  70. * fails, the request content type will be used if the value is not {@code null}; if the value is {@code null} the output format falls
  71. * back to JSON.
  72. */
  73. @Override
  74. public XContentBuilder newBuilder(@Nullable XContentType requestContentType, boolean useFiltering) throws IOException {
  75. return newBuilder(requestContentType, null, useFiltering);
  76. }
  77. /**
  78. * Creates a new {@link XContentBuilder} for a response to be sent using this channel. The builder's type can be sent as a parameter,
  79. * through {@code responseContentType} or it can fallback to {@link #newBuilder(XContentType, boolean)} logic if the sent type value
  80. * is {@code null}.
  81. */
  82. @Override
  83. public XContentBuilder newBuilder(
  84. @Nullable XContentType requestContentType,
  85. @Nullable XContentType responseContentType,
  86. boolean useFiltering
  87. ) throws IOException {
  88. return newBuilder(
  89. requestContentType,
  90. responseContentType,
  91. useFiltering,
  92. org.elasticsearch.common.io.Streams.flushOnCloseStream(bytesOutput())
  93. );
  94. }
  95. /**
  96. * Creates a new {@link XContentBuilder} for a response to be sent using this channel. The builder's type can be sent as a parameter,
  97. * through {@code responseContentType} or it can fallback to {@link #newBuilder(XContentType, boolean)} logic if the sent type value
  98. * is {@code null}.
  99. */
  100. @Override
  101. public XContentBuilder newBuilder(
  102. @Nullable XContentType requestContentType,
  103. @Nullable XContentType responseContentType,
  104. boolean useFiltering,
  105. OutputStream outputStream
  106. ) throws IOException {
  107. if (responseContentType == null) {
  108. if (Strings.hasText(format)) {
  109. responseContentType = XContentType.fromFormat(format);
  110. }
  111. if (responseContentType == null && Strings.hasText(acceptHeader)) {
  112. responseContentType = XContentType.fromMediaType(acceptHeader);
  113. }
  114. }
  115. // try to determine the response content type from the media type or the format query string parameter, with the format parameter
  116. // taking precedence over the Accept header
  117. if (responseContentType == null) {
  118. if (requestContentType != null) {
  119. // if there was a parsed content-type for the incoming request use that since no format was specified using the query
  120. // string parameter or the HTTP Accept header
  121. responseContentType = requestContentType;
  122. } else {
  123. // default to JSON output when all else fails
  124. responseContentType = XContentType.JSON;
  125. }
  126. }
  127. Set<String> includes = Collections.emptySet();
  128. Set<String> excludes = Collections.emptySet();
  129. if (useFiltering) {
  130. Set<String> filters = Strings.tokenizeByCommaToSet(filterPath);
  131. includes = filters.stream().filter(INCLUDE_FILTER).collect(toSet());
  132. excludes = filters.stream().filter(EXCLUDE_FILTER).map(f -> f.substring(1)).collect(toSet());
  133. }
  134. Map<String, String> parameters = request.getParsedAccept() != null
  135. ? request.getParsedAccept().getParameters()
  136. : Collections.emptyMap();
  137. ParsedMediaType responseMediaType = ParsedMediaType.parseMediaType(responseContentType, parameters);
  138. XContentBuilder builder = new XContentBuilder(
  139. XContentFactory.xContent(responseContentType),
  140. outputStream,
  141. includes,
  142. excludes,
  143. responseMediaType,
  144. request.getRestApiVersion()
  145. );
  146. if (pretty) {
  147. builder.prettyPrint().lfAtEnd();
  148. }
  149. builder.humanReadable(human);
  150. return builder;
  151. }
  152. /**
  153. * A channel level bytes output that can be reused. The bytes output is lazily instantiated
  154. * by a call to {@link #newBytesOutput()}. This method should only be called once per request.
  155. */
  156. @Override
  157. public final BytesStream bytesOutput() {
  158. if (bytesOut != null) {
  159. // fallback in case of encountering a bug, release the existing buffer if any (to avoid leaking memory) and acquire a new one
  160. // to send out an error response
  161. assert false : "getting here is always a bug";
  162. logger.error("channel handling [{}] reused", request.rawPath());
  163. releaseOutputBuffer();
  164. }
  165. bytesOut = newBytesOutput();
  166. return bytesOut;
  167. }
  168. /**
  169. * Releases the current output buffer for this channel. Must be called after the buffer derived from {@link #bytesOutput} is no longer
  170. * needed.
  171. */
  172. protected final void releaseOutputBuffer() {
  173. if (bytesOut != null) {
  174. try {
  175. bytesOut.close();
  176. } catch (IOException e) {
  177. // should never throw
  178. assert false : e;
  179. throw new UncheckedIOException(e);
  180. }
  181. bytesOut = null;
  182. }
  183. }
  184. protected BytesStream newBytesOutput() {
  185. return new BytesStreamOutput();
  186. }
  187. @Override
  188. public RestRequest request() {
  189. return this.request;
  190. }
  191. @Override
  192. public boolean detailedErrorsEnabled() {
  193. return detailedErrorsEnabled;
  194. }
  195. }