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}