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