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