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.server.arg; 018 019import java.io.*; 020 021import org.apache.juneau.commons.reflect.*; 022import org.apache.juneau.http.response.*; 023import org.apache.juneau.marshall.serializer.*; 024import org.apache.juneau.marshall.stream.*; 025import org.apache.juneau.rest.server.*; 026 027/** 028 * Resolves method parameters of type {@link RecordWriter} (or any subtype) on 029 * {@link RestOp}-annotated Java methods. 030 * 031 * <p> 032 * The parameter value is resolved by: 033 * <ol> 034 * <li>Negotiating the response's {@code Accept} to a matched {@link Serializer}. 035 * <li>Building a request-configured {@link SerializerSession SerializerSession} 036 * and calling {@link RecordWritable#writeRecords(Object) writeRecords(output)} on it. 037 * <li>Verifying the returned cursor is assignable to the declared parameter type. 038 * </ol> 039 * 040 * <p> 041 * Declaring this parameter flips the response into "user owns the body" mode — the 042 * framework will not later serialize a POJO return value over the same stream. Same contract 043 * as declaring an {@link OutputStream OutputStream} or {@link Writer Writer} 044 * parameter today. 045 * 046 * <p> 047 * Rejection paths (all surfaced as HTTP 415 / 406): 048 * <ul> 049 * <li><b>No serializer matched the {@code Accept}</b> — resolution returns {@code null} and 050 * the framework rejects the request (415/406 via the empty match). 051 * <li><b>Matched serializer does not implement {@link RecordWritable}</b> — throws 052 * {@link UnsupportedMediaType} (415). 053 * <li><b>Matched serializer produces a cursor not assignable to the declared parameter type</b> 054 * — throws {@link UnsupportedMediaType} (415). 055 * </ul> 056 * 057 * <h5 class='section'>See Also:</h5><ul> 058 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JavaMethodParameters">Java Method Parameters</a> 059 * </ul> 060 */ 061@SuppressWarnings({ 062 "resource" // Cursor methods return Closeables owned by the caller; Eclipse JDT @Owning warning is by design. 063}) 064public class RecordWriterArg extends SimpleRestOperationArg { 065 066 /** 067 * Static creator. 068 * 069 * @param paramInfo The Java method parameter being resolved. Must not be <jk>null</jk>. 070 * @return A new {@link RecordWriterArg}, or <jk>null</jk> if the parameter type is not 071 * {@link RecordWriter} or a subtype (other than {@link TokenWriter} — that case is 072 * handled by {@link TokenWriterArg}). 073 */ 074 public static RecordWriterArg create(ParameterInfo paramInfo) { 075 var pt = paramInfo.getParameterType(); 076 if (!pt.isAssignableTo(RecordWriter.class)) 077 return null; 078 if (pt.isAssignableTo(TokenWriter.class)) 079 return null; 080 return new RecordWriterArg(pt.inner()); 081 } 082 083 private final Class<?> declaredType; 084 085 /** Constructor. */ 086 protected RecordWriterArg(Class<?> declaredType) { 087 super(opSession -> resolve(opSession, declaredType)); 088 this.declaredType = declaredType; 089 } 090 091 private static Object resolve(RestOpSession opSession, Class<?> declaredType) throws IOException { 092 var req = opSession.getRequest(); 093 var res = opSession.getResponse(); 094 var match = res.getSerializerMatch(); 095 if (match.isEmpty()) 096 return null; 097 var serializer = match.get().getSerializer(); 098 if (!(serializer instanceof RecordWritable)) 099 throw new UnsupportedMediaType( 100 "Serializer '%s' (matched on Accept) does not support the record-writer surface.", 101 serializer.getClass().getName()); 102 var mediaType = res.getMediaType(); 103 if (mediaType == null) 104 mediaType = match.get().getMediaType(); 105 // Build a request-configured session so the cursor honors the same config as a serialized 106 // response (locale / timezone / schema / charset), then open the cursor on the session. 107 var session = serializer 108 .createSession() 109 .properties(req.getSerializerSessionPropertyMap()) 110 .javaMethod(req.getOpContext().getJavaMethod()) 111 .locale(req.getLocale()) 112 .timeZone(req.getTimeZone().orElse(null)) 113 .mediaType(mediaType) 114 .apply(WriterSerializerSession.Builder.class, x -> x.streamCharset(res.getCharset())) 115 .debug(req.isDebugMarshalling() ? true : null) 116 .build(); 117 Object output = session.isWriterSerializer() 118 ? res.getNegotiatedWriter() 119 : res.getNegotiatedOutputStream(); 120 var cursor = ((RecordWritable) session).writeRecords(output); 121 if (!declaredType.isInstance(cursor)) 122 throw new UnsupportedMediaType( 123 "Serializer '%s' produced cursor type '%s' which is not assignable to the declared parameter type '%s'.", 124 serializer.getClass().getName(), 125 cursor == null ? "null" : cursor.getClass().getName(), 126 declaredType.getName()); 127 return cursor; 128 } 129 130 /** Returns the declared parameter type. */ 131 public Class<?> getDeclaredType() { 132 return declaredType; 133 } 134}