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> — 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> — throws 046 * {@link UnsupportedMediaType} (415). 047 * <li><b>Matched serializer produces a cursor not assignable to the declared parameter type</b> 048 * — 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}