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.parser.*; 024import org.apache.juneau.marshall.stream.*; 025import org.apache.juneau.rest.server.*; 026 027/** 028 * Resolves method parameters of type {@link RecordReader} (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 request's {@code Content-Type} to a matched {@link Parser}. 035 * <li>Building a request-configured {@link ParserSession ParserSession} 036 * and calling {@link RecordReadable#readRecords(Object) readRecords(input)} on it where 037 * {@code input} is the request reader (text formats) or input stream (binary formats). 038 * <li>Verifying the returned cursor's runtime type is assignable to the declared parameter 039 * type — this enforces the two-layer resolution for concrete cursor subtypes 040 * (e.g. {@link org.apache.juneau.marshall.json.JsonTokenReader JsonTokenReader}). 041 * </ol> 042 * 043 * <p> 044 * Rejection paths (all surfaced as HTTP 415 / 406): 045 * <ul> 046 * <li><b>No parser matched the {@code Content-Type}</b> — resolution returns {@code null} and 047 * the framework rejects the request (415/406 via the empty match). 048 * <li><b>Matched parser does not implement {@link RecordReadable}</b> — throws 049 * {@link UnsupportedMediaType} (415). 050 * <li><b>Matched parser produces a cursor not assignable to the declared parameter type</b> 051 * — throws {@link UnsupportedMediaType} (415). 052 * </ul> 053 * 054 * <h5 class='section'>See Also:</h5><ul> 055 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JavaMethodParameters">Java Method Parameters</a> 056 * </ul> 057 */ 058@SuppressWarnings({ 059 "resource" // Cursor methods return Closeables owned by the caller; Eclipse JDT @Owning warning is by design. 060}) 061public class RecordReaderArg extends SimpleRestOperationArg { 062 063 /** 064 * Static creator. 065 * 066 * @param paramInfo The Java method parameter being resolved. Must not be <jk>null</jk>. 067 * @return A new {@link RecordReaderArg}, or <jk>null</jk> if the parameter type is not 068 * {@link RecordReader} or a subtype (other than {@link TokenReader} — that case is 069 * handled by {@link TokenReaderArg}). 070 */ 071 public static RecordReaderArg create(ParameterInfo paramInfo) { 072 var pt = paramInfo.getParameterType(); 073 // Only handle RecordReader and direct RecordReader-but-not-TokenReader subtypes. 074 // TokenReader-and-subtypes go through TokenReaderArg. 075 if (!pt.isAssignableTo(RecordReader.class)) 076 return null; 077 if (pt.isAssignableTo(TokenReader.class)) 078 return null; 079 return new RecordReaderArg(pt.inner()); 080 } 081 082 private final Class<?> declaredType; 083 084 /** 085 * Constructor. 086 * 087 * @param declaredType The declared parameter type (used to verify the matched parser's 088 * cursor type is assignable to the parameter). 089 */ 090 protected RecordReaderArg(Class<?> declaredType) { 091 super(opSession -> resolve(opSession, declaredType)); 092 this.declaredType = declaredType; 093 } 094 095 private static Object resolve(RestOpSession opSession, Class<?> declaredType) throws IOException, ParseException { 096 var req = opSession.getRequest(); 097 var content = req.getContent(); 098 var match = content.getParserMatch(); 099 if (match.isEmpty()) 100 return null; 101 var parser = match.get().getParser(); 102 if (!(parser instanceof RecordReadable)) 103 throw new UnsupportedMediaType( 104 "Parser '%s' (matched on Content-Type) does not support the record-reader surface.", 105 parser.getClass().getName()); 106 // Build a request-configured session so the cursor honors the same config as a parsed body 107 // (locale / timezone / schema / charset), then open the cursor on the session. 108 var session = parser 109 .createSession() 110 .properties(req.getParserSessionPropertyMap()) 111 .javaMethod(req.getOpContext().getJavaMethod()) 112 .locale(req.getLocale()) 113 .timeZone(req.getTimeZone().orElse(null)) 114 .mediaType(match.get().getMediaType()) 115 .apply(ReaderParser.Builder.class, x -> x.streamCharset(req.getCharset())) 116 .debug(req.isDebugMarshalling() ? true : null) 117 .build(); 118 var input = session.isReaderParser() ? content.getReader() : content.getInputStream(); 119 var cursor = ((RecordReadable) session).readRecords(input); 120 if (!declaredType.isInstance(cursor)) 121 throw new UnsupportedMediaType( 122 "Parser '%s' produced cursor type '%s' which is not assignable to the declared parameter type '%s'.", 123 parser.getClass().getName(), 124 cursor == null ? "null" : cursor.getClass().getName(), 125 declaredType.getName()); 126 return cursor; 127 } 128 129 /** Returns the declared parameter type for diagnostic / introspection use. */ 130 public Class<?> getDeclaredType() { 131 return declaredType; 132 } 133}