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)} — 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> -> { 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}