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.http.classic.header;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020import static org.apache.juneau.commons.utils.ThrowableUtils.*;
021
022import java.lang.reflect.*;
023import java.util.*;
024import java.util.concurrent.*;
025
026import org.apache.juneau.commons.reflect.*;
027import org.apache.juneau.marshall.httppart.*;
028
029/**
030 * Holds metadata about header beans (POJOs that get serialized as HTTP headers).
031 *
032 * <p>
033 * Header beans are typically annotated with {@link org.apache.juneau.http.Header @Header} although it's not an
034 * absolute requirement.
035 *
036 * <p>
037 * Header beans must have one of the following public constructors:
038 * <ul>
039 *    <li><c><jk>public</jk> X(String <jv>headerValue</jv>)</c>
040 *    <li><c><jk>public</jk> X(Object <jv>headerValue</jv>)</c>
041 *    <li><c><jk>public</jk> X(String <jv>headerName</jv>, String <jv>headerValue</jv>)</c>
042 *    <li><c><jk>public</jk> X(String <jv>headerName</jv>, Object <jv>headerValue</jv>)</c>
043 * </ul>
044 *
045 * <p>
046 * <h5 class='figure'>Example</h5>
047 * <p class='bjava'>
048 *    <jc>// Our header bean.</jc>
049 *    <ja>@Header</ja>(<js>"Foo"</js>)
050 *    <jk>public class</jk> FooHeader <jk>extends</jk> BasicStringHeader {
051 *
052 *       <jk>public</jk> FooHeader(String <jv>headerValue</jv>) {
053 *          <jk>super</jk>(<js>"Foo"</js>, <jv>headerValue</jv>);
054 *       }
055 *  }
056 *
057 *  <jc>// Code to retrieve a header bean from a header list in a request.</jc>
058 *  HeaderList <jv>headers</jv> = <jv>httpRequest</jv>.getHeaders();
059 *  FooHeader <jv>foo</jv> = <jv>headers</jv>.get(FooHeader.<jk>class</jk>);
060 * </p>
061 *
062 * <h5 class='section'>See Also:</h5><ul>
063 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauRestCommon">juneau-rest-common</a>
064 * </ul>
065 *
066 * @param <T> The header bean type.
067 */
068public class HeaderBeanMeta<T> {
069
070   private static final ConcurrentHashMap<Class<?>,HeaderBeanMeta<?>> CACHE = new ConcurrentHashMap<>();
071
072   /**
073    * Finds the header bean meta for the specified type.
074    *
075    * @param <T> The header bean type.
076    * @param type The header bean type.  Must not be <jk>null</jk> or a {@link NullPointerException} is thrown.
077    * @return The metadata.  Never <jk>null</jk>.
078    */
079   @SuppressWarnings({
080      "unchecked" // Type erasure requires cast for HeaderBeanMeta creation
081   })
082   public static <T> HeaderBeanMeta<T> of(Class<T> type) {
083      HeaderBeanMeta<?> m = CACHE.computeIfAbsent(type, HeaderBeanMeta::new);
084      return (HeaderBeanMeta<T>)m;
085   }
086
087   private final Class<T> type;
088   private final Constructor<T> constructor;
089
090   private final HttpPartSchema schema;
091
092   private HeaderBeanMeta(Class<T> type) {
093      this.type = type;
094
095      var ci = ClassInfo.of(type);
096
097      // @formatter:off
098      constructor = ci.getPublicConstructor(x -> x.hasParameterTypes(String.class))
099         .or(() -> ci.getPublicConstructor(x -> x.hasParameterTypes(Object.class)))
100         .or(() -> ci.getPublicConstructor(x -> x.hasParameterTypes(String.class, String.class)))
101         .or(() -> ci.getPublicConstructor(x -> x.hasParameterTypes(String.class, Object.class)))
102         .map(x -> x.<T>inner())
103         .orElse(null);
104      // @formatter:on
105
106      this.schema = HttpPartSchema.create(org.apache.juneau.http.Header.class, type);
107   }
108
109   /**
110    * Constructs a header bean with the specified name or value.
111    *
112    * <p>
113    * Can only be used on beans where the header name is known.
114    *
115    * @param value
116    *    The header value.
117    *    <br>Can be <jk>null</jk>, in which case it is passed as-is (or stringified to <jk>null</jk>) to the bean's constructor.
118    * @return A newly constructed bean.
119    * @throws UnsupportedOperationException If bean could not be constructed (e.g. couldn't find a constructor).
120    */
121   public T construct(Object value) {
122      return construct(null, value);
123   }
124
125   /**
126    * Constructs a header bean with the specified name or value.
127    *
128    * @param name
129    *    The header name.
130    *    <br>If <jk>null</jk>, uses the value pulled from the {@link org.apache.juneau.http.Header#name() @Header(name)} or
131    *    {@link org.apache.juneau.http.Header#value() @Header(value)} annotations.
132    * @param value
133    *    The header value.
134    *    <br>Can be <jk>null</jk>, in which case it is passed as-is (or stringified to <jk>null</jk>) to the bean's constructor.
135    * @return A newly constructed bean.
136    * @throws UnsupportedOperationException If bean could not be constructed (e.g. couldn't find a constructor).
137    */
138   public T construct(String name, Object value) {
139
140      if (constructor == null)
141         throw uoex("Constructor for type %s could not be found.", cn(type));
142
143      if (name == null)
144         name = schema.getName();
145
146      var pt = constructor.getParameterTypes();
147      var args = new Object[pt.length];
148      if (pt.length == 1) {
149         args[0] = pt[0] == String.class ? s(value) : value;
150      } else {
151         if (name == null)
152            throw uoex("Constructor for type %s requires a name as the first argument.", cn(type));
153         args[0] = name;
154         args[1] = pt[1] == String.class ? s(value) : value;
155      }
156
157      try {
158         return constructor.newInstance(args);
159      } catch (Exception e) {
160         throw toRex(e);
161      }
162   }
163
164   @Override
165   public boolean equals(Object o) {
166      return o instanceof HeaderBeanMeta<?> other && eq(this, other, (x, y) -> x.type == y.type);
167   }
168
169   @Override
170   public int hashCode() {
171      return Objects.hashCode(type);
172   }
173
174   /**
175    * Returns schema information about this header.
176    *
177    * <p>
178    * This is information pulled from {@link org.apache.juneau.http.Header @Header} annotation on the class.
179    *
180    * @return The schema information.
181    */
182   public HttpPartSchema getSchema() { return schema; }
183}