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