001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.rest.client;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.io.*;
022import java.util.function.*;
023
024import org.apache.juneau.http.*;
025import org.apache.juneau.marshall.json.*;
026import org.apache.juneau.marshall.serializer.*;
027import org.apache.juneau.marshall.stream.*;
028
029/**
030 * A streaming HTTP request body that emits its content through a token/record-streaming cursor.
031 *
032 * <p>
033 * Lets a caller pass a {@code Consumer<RecordWriter>} or {@code Consumer<TokenWriter>} as the body of a request.
034 * The consumer is invoked lazily during {@link #writeTo(OutputStream)} &mdash; the cursor writes directly to the
035 * live transport output stream, so arbitrarily large payloads are streamed to the wire without being buffered in
036 * memory first.
037 *
038 * <p>
039 * While the next-generation client negotiates a parser for inbound/response bodies (from the response
040 * {@code Content-Type}), the request-body OUTPUT format here is an explicit caller choice: the {@link Serializer}
041 * that backs the cursor is supplied directly (defaulting to {@link JsonSerializer#DEFAULT}).  The body's
042 * {@link #getContentType()} reflects that serializer's media type.
043 *
044 * <h5 class='section'>Repeatability:</h5>
045 * <p>
046 * Streaming bodies are <b>non-repeatable</b> by default ({@link #isRepeatable()} returns {@code false}) because the
047 * caller's consumer may be backed by a one-shot source.  A non-repeatable body fails fast (throws {@link IOException})
048 * if a resend is required (i.e. {@link #writeTo(OutputStream)} is invoked more than once).  Callers whose consumer can
049 * be replayed safely may opt in via {@link #repeatable()}.
050 *
051 * <h5 class='section'>Usage:</h5>
052 * <p class='bjava'>
053 *    <ja>@Remote</ja>
054 *    <jk>public interface</jk> MyApi {
055 *
056 *       <ja>@RemotePost</ja>(<js>"/bulk-upload"</js>)
057 *       <jk>void</jk> upload(<ja>@Content</ja> RecordStreamBody <jv>body</jv>);
058 *    }
059 *
060 *    <jc>// Caller</jc>
061 *    <jv>api</jv>.upload(RecordStreamBody.<jsm>records</jsm>(<jv>w</jv> -&gt; {
062 *       <jk>for</jk> (Bean <jv>b</jv> : <jv>source</jv>())
063 *          <jv>w</jv>.write(<jv>b</jv>);
064 *    }));
065 * </p>
066 *
067 * <h5 class='section'>See Also:</h5><ul>
068 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/NextGenRestClient">Next-Generation REST Client</a>
069 * </ul>
070 *
071 * @since 9.2.1
072 */
073public final class RecordStreamBody implements HttpBody {
074
075   /**
076    * Creates a body that streams via a {@link RecordWriter} (whole-value record cursor) using the default JSON serializer.
077    *
078    * @param consumer The callback that emits records. Must not be <jk>null</jk>.
079    * @return A new body. Never <jk>null</jk>.
080    */
081   public static RecordStreamBody records(Consumer<RecordWriter> consumer) {
082      return records(JsonSerializer.DEFAULT, consumer);
083   }
084
085   /**
086    * Creates a body that streams via a {@link RecordWriter} (whole-value record cursor) using the given serializer.
087    *
088    * @param serializer The serializer that opens the record cursor. Must not be <jk>null</jk> and must implement
089    *    {@link RecordWritable}.
090    * @param consumer The callback that emits records. Must not be <jk>null</jk>.
091    * @return A new body. Never <jk>null</jk>.
092    * @throws IllegalArgumentException If the serializer does not support the record-writer surface.
093    */
094   public static RecordStreamBody records(Serializer serializer, Consumer<RecordWriter> consumer) {
095      reqnn("serializer", serializer);
096      reqnn("consumer", consumer);
097      if (! (serializer instanceof RecordWritable))
098         throw iaex("Serializer '%s' does not support the record-writer surface.", serializer.getClass().getName());
099      return new RecordStreamBody(serializer, consumer, RecordWriter.class, false);
100   }
101
102   /**
103    * Creates a body that streams via a {@link TokenWriter} (fine-grained structural cursor) using the default JSON serializer.
104    *
105    * @param consumer The callback that emits structural events. Must not be <jk>null</jk>.
106    * @return A new body. Never <jk>null</jk>.
107    */
108   public static RecordStreamBody token(Consumer<TokenWriter> consumer) {
109      return token(JsonSerializer.DEFAULT, consumer);
110   }
111
112   /**
113    * Creates a body that streams via a {@link TokenWriter} (fine-grained structural cursor) using the given serializer.
114    *
115    * @param serializer The serializer that opens the token cursor. Must not be <jk>null</jk> and must implement
116    *    {@link TokenWritable}.
117    * @param consumer The callback that emits structural events. Must not be <jk>null</jk>.
118    * @return A new body. Never <jk>null</jk>.
119    * @throws IllegalArgumentException If the serializer does not support the token-writer surface.
120    */
121   public static RecordStreamBody token(Serializer serializer, Consumer<TokenWriter> consumer) {
122      reqnn("serializer", serializer);
123      reqnn("consumer", consumer);
124      if (! (serializer instanceof TokenWritable))
125         throw iaex("Serializer '%s' does not support the token-writer surface.", serializer.getClass().getName());
126      return new RecordStreamBody(serializer, consumer, TokenWriter.class, false);
127   }
128
129   private final Serializer serializer;
130   private final Consumer<?> consumer;
131   private final Class<?> writerKind;
132   private final boolean repeatable;
133   private boolean written;
134
135   private RecordStreamBody(Serializer serializer, Consumer<?> consumer, Class<?> writerKind, boolean repeatable) {
136      this.serializer = serializer;
137      this.consumer = consumer;
138      this.writerKind = writerKind;
139      this.repeatable = repeatable;
140   }
141
142   /**
143    * Returns a copy of this body flagged as repeatable.
144    *
145    * <p>
146    * Only opt in when the caller's consumer can be safely replayed (e.g. it iterates a re-readable source).
147    *
148    * @return A new repeatable body. Never <jk>null</jk>.
149    */
150   public RecordStreamBody repeatable() {
151      return new RecordStreamBody(serializer, consumer, writerKind, true);
152   }
153
154   @Override /* HttpBody */
155   public String getContentType() {
156      var mt = serializer.getResponseContentType();
157      return mt == null ? null : mt.toString();
158   }
159
160   @Override /* HttpBody */
161   public long getContentLength() {
162      return -1;
163   }
164
165   @Override /* HttpBody */
166   public boolean isRepeatable() {
167      return repeatable;
168   }
169
170   @Override /* HttpBody */
171   @SuppressWarnings({
172      "resource", // The cursor wraps the caller-owned 'out'; its close() flushes but does not close 'out'.
173      "unchecked" // Consumer is paired with writerKind at construction; the cast matches the opened cursor type.
174   })
175   public void writeTo(OutputStream out) throws IOException {
176      reqnn("out", out);
177      if (written && ! repeatable)
178         throw new IOException("Non-repeatable streaming body cannot be resent.  Mark the body repeatable() if its producer can be replayed.");
179      written = true;
180      try (var w = (writerKind == TokenWriter.class)
181            ? ((TokenWritable) serializer).writeTokens(out)
182            : ((RecordWritable) serializer).writeRecords(out)) {
183         ((Consumer<RecordWriter>) consumer).accept(w);
184         w.flush();
185      }
186   }
187}