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.part;
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.concurrent.*;
024
025import org.apache.juneau.commons.reflect.*;
026import org.apache.juneau.http.*;
027import org.apache.juneau.marshall.httppart.*;
028
029/**
030 * Holds metadata about http part beans (POJOs that get serialized as HTTP parts such as form data or query parameters).
031 *
032 * <p>
033 * HTTP part beans are typically annotated with {@link Query @Query}, {@link FormData @FormData}, or {@link Path @Path}
034 * although it's not an absolute requirement.
035 *
036 * <p>
037 * HTTP part beans must have one of the following public constructors:
038 * <ul>
039 *    <li><c><jk>public</jk> X(String <jv>partValue</jv>)</c>
040 *    <li><c><jk>public</jk> X(Object <jv>partValue</jv>)</c>
041 *    <li><c><jk>public</jk> X(String <jv>partName</jv>, String <jv>partValue</jv>)</c>
042 *    <li><c><jk>public</jk> X(String <jv>partName</jv>, Object <jv>partValue</jv>)</c>
043 * </ul>
044 *
045 * <p>
046 * <h5 class='figure'>Example</h5>
047 * <p class='bjava'>
048 *    <jc>// Our part bean.</jc>
049 *    <ja>@Query</ja>(<js>"foo"</js>)
050 *    <jk>public class</jk> FooPart <jk>extends</jk> BasicStringPart {
051 *
052 *       <jk>public</jk> FooPart(String <jv>partValue</jv>) {
053 *          <jk>super</jk>(<js>"foo"</js>, <jv>partValue</jv>);
054 *       }
055 *  }
056 *
057 *  <jc>// Code to retrieve a part bean from a part list in a request.</jc>
058 *  PartList <jv>parts</jv> = <jv>httpRequest</jv>.getFormDataList();
059 *  FooPart <jv>foo</jv> = <jv>parts</jv>.get(FooPart.<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 HTTP part bean type.
067 */
068public class PartBeanMeta<T> {
069
070   private static final ConcurrentHashMap<Class<?>,PartBeanMeta<?>> CACHE = new ConcurrentHashMap<>();
071
072   /**
073    * Finds the part bean meta for the specified type.
074    *
075    * @param <T> The part bean type.
076    * @param type The part bean type.  Must not be <jk>null</jk>.
077    * @return The metadata for the specified type.  Never <jk>null</jk> (a bean with no usable constructor causes {@link #construct(String,Object)} to throw instead).
078    */
079   @SuppressWarnings({
080      "unchecked" // Type erasure requires cast for PartBeanMeta creation
081   })
082   public static <T> PartBeanMeta<T> of(Class<T> type) {
083      PartBeanMeta<?> m = CACHE.computeIfAbsent(type, PartBeanMeta::new);
084      return (PartBeanMeta<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 PartBeanMeta(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      if (ci.hasAnnotation(Query.class))
107         this.schema = HttpPartSchema.create(Query.class, type);
108      else if (ci.hasAnnotation(FormData.class))
109         this.schema = HttpPartSchema.create(FormData.class, type);
110      else if (ci.hasAnnotation(Path.class))
111         this.schema = HttpPartSchema.create(Path.class, type);
112      else
113         this.schema = HttpPartSchema.create(Header.class, type);
114   }
115
116   /**
117    * Constructs a part bean with the specified name or value.
118    *
119    * <p>
120    * Can only be used on beans where the part name is known.
121    *
122    * @param value
123    *    The part value.
124    *    <br>Can be <jk>null</jk>.  The effect is implementation-dependent, depending on the target bean type's constructor shape — see {@link #construct(String, Object)}.
125    * @return A newly constructed bean.
126    * @throws UnsupportedOperationException If bean could not be constructed (e.g. couldn't find a constructor).
127    */
128   public T construct(Object value) {
129      return construct(null, value);
130   }
131
132   /**
133    * Constructs a part bean with the specified name or value.
134    *
135    * @param name
136    *    The part name.
137    *    <br>If <jk>null</jk>, uses the value pulled from the {@link Header#name() @Header(name)} or
138    *    {@link Header#value() @Header(value)} annotations.
139    * @param value
140    *    The part value.
141    *    <br>Can be <jk>null</jk>.  The effect is implementation-dependent, depending on the target bean type's constructor shape (e.g. coerced to <c>String</c> for a single-<c>String</c>-arg constructor, passed through unchanged otherwise).
142    * @return A newly constructed bean.
143    * @throws UnsupportedOperationException If bean could not be constructed (e.g. couldn't find a constructor).
144    */
145   public T construct(String name, Object value) {
146
147      if (constructor == null)
148         throw uoex("Constructor for type %s could not be found.", cn(type));
149
150      if (name == null)
151         name = schema.getName();
152
153      var pt = constructor.getParameterTypes();
154      var args = new Object[pt.length];
155      if (pt.length == 1) {
156         args[0] = pt[0] == String.class ? s(value) : value;
157      } else {
158         if (name == null)
159            throw uoex("Constructor for type %s requires a name as the first argument.", cn(type));
160         args[0] = name;
161         args[1] = pt[1] == String.class ? s(value) : value;
162      }
163
164      try {
165         return constructor.newInstance(args);
166      } catch (Exception e) {
167         throw toRex(e);
168      }
169   }
170
171   /**
172    * Returns schema information about this part.
173    *
174    * <p>
175    * This is information pulled from {@link Query @Query}, {@link FormData @FormData}, or {@link Path @Path} annotations
176    * on the class.
177    *
178    * @return The schema information.
179    */
180   public HttpPartSchema getSchema() { return schema; }
181}