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 TokenWriter} (or any subtype) on
029 * {@link RestOp}-annotated Java methods.
030 *
031 * <p>
032 * Implements two-layer resolution: static parameter type filters eligible serializers; response
033 * {@code Accept} negotiates among them.  The cursor is opened on a request-configured
034 * {@link SerializerSession SerializerSession} so it honors the
035 * same locale / timezone / schema / charset as a normal serialized response.
036 *
037 * <p>
038 * Declaring this parameter flips the response into "user owns the body" mode.
039 *
040 * <p>
041 * Rejection paths (all surfaced as HTTP 415 / 406):
042 * <ul>
043 *   <li><b>No serializer matched the {@code Accept}</b> &mdash; resolution returns {@code null} and
044 *       the framework rejects the request (415/406 via the empty match).
045 *   <li><b>Matched serializer does not implement {@link TokenWritable}</b> &mdash; throws
046 *       {@link UnsupportedMediaType} (415).
047 *   <li><b>Matched serializer produces a cursor not assignable to the declared parameter type</b>
048 *       &mdash; throws {@link UnsupportedMediaType} (415).
049 * </ul>
050 *
051 * <h5 class='section'>See Also:</h5><ul>
052 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JavaMethodParameters">Java Method Parameters</a>
053 * </ul>
054 */
055@SuppressWarnings({
056   "resource" // Cursor methods return Closeables owned by the caller; Eclipse JDT @Owning warning is by design.
057})
058public class TokenWriterArg extends SimpleRestOperationArg {
059
060   /**
061    * Static creator.
062    *
063    * @param paramInfo The Java method parameter being resolved. Must not be <jk>null</jk>.
064    * @return A new {@link TokenWriterArg}, or <jk>null</jk> if the parameter type is not
065    *    {@link TokenWriter} or a subtype.
066    */
067   public static TokenWriterArg create(ParameterInfo paramInfo) {
068      var pt = paramInfo.getParameterType();
069      if (!pt.isAssignableTo(TokenWriter.class))
070         return null;
071      return new TokenWriterArg(pt.inner());
072   }
073
074   private final Class<?> declaredType;
075
076   /** Constructor. */
077   protected TokenWriterArg(Class<?> declaredType) {
078      super(opSession -> resolve(opSession, declaredType));
079      this.declaredType = declaredType;
080   }
081
082   private static Object resolve(RestOpSession opSession, Class<?> declaredType) throws IOException {
083      var req = opSession.getRequest();
084      var res = opSession.getResponse();
085      var match = res.getSerializerMatch();
086      if (match.isEmpty())
087         return null;
088      var serializer = match.get().getSerializer();
089      if (!(serializer instanceof TokenWritable))
090         throw new UnsupportedMediaType(
091            "Serializer '%s' (matched on Accept) does not support the token-writer surface.",
092            serializer.getClass().getName());
093      var mediaType = res.getMediaType();
094      if (mediaType == null)
095         mediaType = match.get().getMediaType();
096      // Build a request-configured session so the cursor honors the same config as a serialized
097      // response (locale / timezone / schema / charset), then open the cursor on the session.
098      var session = serializer
099         .createSession()
100         .properties(req.getSerializerSessionPropertyMap())
101         .javaMethod(req.getOpContext().getJavaMethod())
102         .locale(req.getLocale())
103         .timeZone(req.getTimeZone().orElse(null))
104         .mediaType(mediaType)
105         .apply(WriterSerializerSession.Builder.class, x -> x.streamCharset(res.getCharset()))
106         .debug(req.isDebugMarshalling() ? true : null)
107         .build();
108      Object output = session.isWriterSerializer()
109         ? res.getNegotiatedWriter()
110         : res.getNegotiatedOutputStream();
111      var cursor = ((TokenWritable) session).writeTokens(output);
112      if (!declaredType.isInstance(cursor))
113         throw new UnsupportedMediaType(
114            "Serializer '%s' produced cursor type '%s' which is not assignable to the declared parameter type '%s'.",
115            serializer.getClass().getName(),
116            cursor == null ? "null" : cursor.getClass().getName(),
117            declaredType.getName());
118      return cursor;
119   }
120
121   /** Returns the declared parameter type. */
122   public Class<?> getDeclaredType() {
123      return declaredType;
124   }
125}