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.part;
018
019import static org.apache.juneau.commons.utils.ThrowableUtils.*;
020import static org.apache.juneau.commons.utils.Utils.*;
021
022import java.lang.reflect.*;
023import java.util.concurrent.*;
024
025import org.apache.juneau.commons.reflect.*;
026import org.apache.juneau.http.annotation.*;
027import org.apache.juneau.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/JuneauRestCommonBasics">juneau-rest-common Basics</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.
077    * @return The metadata, or <jk>null</jk> if a valid constructor could not be found.
078    */
079   @SuppressWarnings("unchecked")
080   public static <T> PartBeanMeta<T> of(Class<T> type) {
081      PartBeanMeta<?> m = CACHE.get(type);
082      if (m == null) {
083         m = new PartBeanMeta<>(type);
084         CACHE.put(type, m);
085      }
086      return (PartBeanMeta<T>)m;
087   }
088
089   private final Class<T> type;
090   private final Constructor<T> constructor;
091
092   private final HttpPartSchema schema;
093
094   private PartBeanMeta(Class<T> type) {
095      this.type = type;
096
097      var ci = ClassInfo.of(type);
098
099      // @formatter:off
100      constructor = ci.getPublicConstructor(x -> x.hasParameterTypes(String.class))
101         .or(() -> ci.getPublicConstructor(x -> x.hasParameterTypes(Object.class)))
102         .or(() -> ci.getPublicConstructor(x -> x.hasParameterTypes(String.class, String.class)))
103         .or(() -> ci.getPublicConstructor(x -> x.hasParameterTypes(String.class, Object.class)))
104         .map(x -> x.<T>inner())
105         .orElse(null);
106      // @formatter:on
107
108      if (ci.hasAnnotation(Query.class))
109         this.schema = HttpPartSchema.create(Query.class, type);
110      else if (ci.hasAnnotation(FormData.class))
111         this.schema = HttpPartSchema.create(FormData.class, type);
112      else if (ci.hasAnnotation(Path.class))
113         this.schema = HttpPartSchema.create(Path.class, type);
114      else
115         this.schema = HttpPartSchema.create(org.apache.juneau.http.annotation.Header.class, type);
116   }
117
118   /**
119    * Constructs a part bean with the specified name or value.
120    *
121    * <p>
122    * Can only be used on beans where the part name is known.
123    *
124    * @param value
125    *    The part value.
126    * @return A newly constructed bean.
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 org.apache.juneau.http.annotation.Header#name() @Header(name)} or
138    *    {@link org.apache.juneau.http.annotation.Header#value() @Header(value)} annotations.
139    * @param value
140    *    The part value.
141    * @return A newly constructed bean.
142    * @throws UnsupportedOperationException If bean could not be constructed (e.g. couldn't find a constructor).
143    */
144   public T construct(String name, Object value) {
145
146      if (constructor == null)
147         throw unsupportedOp("Constructor for type {0} could not be found.", cn(type));
148
149      if (name == null)
150         name = schema.getName();
151
152      var pt = constructor.getParameterTypes();
153      var args = new Object[pt.length];
154      if (pt.length == 1) {
155         args[0] = pt[0] == String.class ? s(value) : value;
156      } else {
157         if (name == null)
158            throw unsupportedOp("Constructor for type {0} requires a name as the first argument.", cn(type));
159         args[0] = name;
160         args[1] = pt[1] == String.class ? s(value) : value;
161      }
162
163      try {
164         return constructor.newInstance(args);
165      } catch (Exception e) {
166         throw toRex(e);
167      }
168   }
169
170   /**
171    * Returns schema information about this part.
172    *
173    * <p>
174    * This is information pulled from {@link Query @Query}, {@link FormData @FormData}, or {@link Path @Path} annotations
175    * on the class.
176    *
177    * @return The schema information.
178    */
179   public HttpPartSchema getSchema() { return schema; }
180}