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