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.arg; 018 019import static org.apache.juneau.commons.utils.CollectionUtils.*; 020import static org.apache.juneau.commons.utils.StringUtils.*; 021import static org.apache.juneau.commons.utils.Utils.*; 022import static org.apache.juneau.http.annotation.HeaderAnnotation.*; 023 024import java.util.*; 025 026import org.apache.juneau.*; 027import org.apache.juneau.annotation.*; 028import org.apache.juneau.collections.*; 029import org.apache.juneau.http.annotation.*; 030import org.apache.juneau.http.header.*; 031import org.apache.juneau.httppart.*; 032import org.apache.juneau.commons.lang.*; 033import org.apache.juneau.commons.reflect.*; 034import org.apache.juneau.rest.*; 035import org.apache.juneau.rest.annotation.*; 036 037/** 038 * Resolves method parameters and parameter types annotated with {@link Header} on {@link RestOp}-annotated Java methods. 039 * 040 * <p> 041 * This includes any of the following predefined request header types: 042 * 043 * <ul class='javatree condensed'> 044 * <li class='jc'>{@link Accept} 045 * <li class='jc'>{@link AcceptCharset} 046 * <li class='jc'>{@link AcceptEncoding} 047 * <li class='jc'>{@link AcceptLanguage} 048 * <li class='jc'>{@link AcceptRanges} 049 * <li class='jc'>{@link Authorization} 050 * <li class='jc'>{@link CacheControl} 051 * <li class='jc'>{@link ClientVersion} 052 * <li class='jc'>{@link Connection} 053 * <li class='jc'>{@link ContentDisposition} 054 * <li class='jc'>{@link ContentEncoding} 055 * <li class='jc'>{@link ContentLength} 056 * <li class='jc'>{@link ContentType} 057 * <li class='jc'>{@link org.apache.juneau.http.header.Date} 058 * <li class='jc'>{@link Debug} 059 * <li class='jc'>{@link Expect} 060 * <li class='jc'>{@link Forwarded} 061 * <li class='jc'>{@link From} 062 * <li class='jc'>{@link Host} 063 * <li class='jc'>{@link IfMatch} 064 * <li class='jc'>{@link IfModifiedSince} 065 * <li class='jc'>{@link IfNoneMatch} 066 * <li class='jc'>{@link IfRange} 067 * <li class='jc'>{@link IfUnmodifiedSince} 068 * <li class='jc'>{@link MaxForwards} 069 * <li class='jc'>{@link NoTrace} 070 * <li class='jc'>{@link Origin} 071 * <li class='jc'>{@link Pragma} 072 * <li class='jc'>{@link ProxyAuthorization} 073 * <li class='jc'>{@link Range} 074 * <li class='jc'>{@link Referer} 075 * <li class='jc'>{@link TE} 076 * <li class='jc'>{@link Thrown} 077 * <li class='jc'>{@link Upgrade} 078 * <li class='jc'>{@link UserAgent} 079 * <li class='jc'>{@link Warning} 080 * </ul> 081 * 082 * <p> 083 * The parameter value is resolved using: 084 * <p class='bjava'> 085 * <jv>opSession</jv> 086 * .{@link RestOpSession#getRequest() getRequest}() 087 * .{@link RestRequest#getHeaders() getHeaders}(); 088 * </p> 089 * 090 * <p> 091 * {@link HttpPartSchema schema} is derived from the {@link Header} annotation. 092 * 093 * <p> 094 * If the {@link Schema#collectionFormat()} value is {@link HttpPartCollectionFormat#MULTI}, then the data type can be a {@link Collection} or array. 095 * 096 * <h5 class='section'>See Also:</h5><ul> 097 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JavaMethodParameters">Java Method Parameters</a> 098 * </ul> 099 */ 100public class HeaderArg implements RestOpArg { 101 102 private static final AnnotationProvider AP = AnnotationProvider.INSTANCE; 103 104 /** 105 * Static creator. 106 * 107 * @param paramInfo The Java method parameter being resolved. 108 * @param annotations The annotations to apply to any new part parsers. 109 * @return A new {@link HeaderArg}, or <jk>null</jk> if the parameter is not annotated with {@link Header}. 110 */ 111 public static HeaderArg create(ParameterInfo paramInfo, AnnotationWorkList annotations) { 112 if ((! paramInfo.getParameterType().is(Value.class)) && AP.has(Header.class, paramInfo)) 113 return new HeaderArg(paramInfo, annotations); 114 return null; 115 } 116 117 /** 118 * Gets the merged @Header annotation combining class-level and parameter-level values. 119 * 120 * @param pi The parameter info. 121 * @param paramName The header name. 122 * @return Merged annotation, or null if no class-level defaults exist. 123 */ 124 private static Header getMergedHeader(ParameterInfo pi, String paramName) { 125 // Get the declaring class 126 var declaringClass = pi.getMethod().getDeclaringClass(); 127 if (declaringClass == null) 128 return null; 129 130 // Find @Rest annotation on the class 131 var restAnnotation = declaringClass.getAnnotations(Rest.class).findFirst().map(AnnotationInfo::inner).orElse(null); 132 if (restAnnotation == null) 133 return null; 134 135 // Find matching @Header from class-level headerParams array 136 var classLevelHeader = (Header)null; 137 for (var h : restAnnotation.headerParams()) { 138 var hName = firstNonEmpty(h.name(), h.value()); 139 if (paramName.equals(hName)) { 140 classLevelHeader = h; 141 break; 142 } 143 } 144 145 if (classLevelHeader == null) 146 return null; 147 148 // Get parameter-level @Header 149 var paramHeader = AP.find(Header.class, pi).stream().findFirst().map(AnnotationInfo::inner).orElse(null); 150 151 if (paramHeader == null) { 152 // No parameter-level @Header, use class-level as-is 153 return classLevelHeader; 154 } 155 156 // Merge the two annotations: parameter-level takes precedence 157 return mergeAnnotations(classLevelHeader, paramHeader); 158 } 159 160 /** 161 * Merges two @Header annotations, with param-level taking precedence over class-level. 162 * 163 * @param classLevel The class-level default. 164 * @param paramLevel The parameter-level override. 165 * @return Merged annotation. 166 */ 167 private static Header mergeAnnotations(Header classLevel, Header paramLevel) { 168 // @formatter:off 169 return HeaderAnnotation.create() 170 .name(firstNonEmpty(paramLevel.name(), paramLevel.value(), classLevel.name(), classLevel.value())) 171 .value(firstNonEmpty(paramLevel.value(), paramLevel.name(), classLevel.value(), classLevel.name())) 172 .def(firstNonEmpty(paramLevel.def(), classLevel.def())) 173 .description(paramLevel.description().length > 0 ? paramLevel.description() : classLevel.description()) 174 .parser(paramLevel.parser() != HttpPartParser.Void.class ? paramLevel.parser() : classLevel.parser()) 175 .serializer(paramLevel.serializer() != HttpPartSerializer.Void.class ? paramLevel.serializer() : classLevel.serializer()) 176 .schema(mergeSchemas(classLevel.schema(), paramLevel.schema())) 177 .build(); 178 // @formatter:on 179 } 180 181 /** 182 * Merges two @Schema annotations, with param-level taking precedence. 183 * 184 * @param classLevel The class-level default. 185 * @param paramLevel The parameter-level override. 186 * @return Merged annotation. 187 */ 188 private static Schema mergeSchemas(Schema classLevel, Schema paramLevel) { 189 // If parameter has a non-default schema, use it; otherwise use class-level 190 if (! SchemaAnnotation.empty(paramLevel)) 191 return paramLevel; 192 return classLevel; 193 } 194 195 private final HttpPartParser partParser; 196 197 private final HttpPartSchema schema; 198 199 private final boolean multi; 200 201 private final String name, def; 202 203 private final ClassInfo type; 204 205 /** 206 * Constructor. 207 * 208 * @param pi The Java method parameter being resolved. 209 * @param annotations The annotations to apply to any new part parsers. 210 */ 211 protected HeaderArg(ParameterInfo pi, AnnotationWorkList annotations) { 212 // Get the header name from the parameter 213 this.name = findName(pi).orElseThrow(() -> new ArgException(pi, "@Header used without name or value")); 214 215 // Check for class-level defaults and merge if found 216 var mergedHeader = getMergedHeader(pi, name); 217 218 // Use merged header annotation for all lookups 219 this.def = nn(mergedHeader) && ! mergedHeader.def().isEmpty() ? mergedHeader.def() : findDef(pi).orElse(null); 220 this.type = pi.getParameterType(); 221 this.schema = nn(mergedHeader) ? HttpPartSchema.create(mergedHeader) : HttpPartSchema.create(Header.class, pi); 222 var pp = schema.getParser(); 223 this.partParser = nn(pp) ? HttpPartParser.creator().type(pp).apply(annotations).create() : null; 224 this.multi = schema.getCollectionFormat() == HttpPartCollectionFormat.MULTI; 225 226 if (multi && ! type.isCollectionOrArray()) 227 throw new ArgException(pi, "Use of multipart flag on @Header parameter that is not an array or Collection"); 228 } 229 230 @SuppressWarnings({ "rawtypes", "unchecked" }) 231 @Override /* Overridden from RestOpArg */ 232 public Object resolve(RestOpSession opSession) throws Exception { 233 var req = opSession.getRequest(); 234 var ps = partParser == null ? req.getPartParserSession() : partParser.getPartSession(); 235 var rh = req.getHeaders(); 236 var bs = req.getBeanSession(); 237 var cm = bs.getClassMeta(type.innerType()); 238 239 if (multi) { 240 var c = cm.isArray() ? list() : (Collection)(cm.canCreateNewInstance() ? cm.newInstance() : new JsonList()); 241 rh.stream(name).map(x -> x.parser(ps).schema(schema).as(cm.getElementType()).orElse(null)).forEach(x -> c.add(x)); 242 return cm.isArray() ? toArray(c, cm.getElementType().inner()) : c; 243 } 244 245 if (cm.isMapOrBean() && isOneOf(name, "*", "")) { 246 var m = new JsonMap(); 247 rh.forEach(x -> m.put(x.getName(), x.parser(ps).schema(schema == null ? null : schema.getProperty(x.getName())).as(cm.getValueType()).orElse(null))); 248 return req.getBeanSession().convertToType(m, cm); 249 } 250 251 return rh.getLast(name).parser(ps).schema(schema).def(def).as(type.innerType()).orElse(null); 252 } 253}