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 &mdash; 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 &mdash; 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> &mdash; 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) &mdash; throws {@link UnsupportedMediaType} (415).
058 *   <li><b>Matched parser produces a cursor not assignable to the declared parameter type</b>
059 *       &mdash; 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}