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