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}