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.server.arg;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.lang.reflect.*;
022import java.util.*;
023
024import org.apache.juneau.commons.httppart.*;
025import org.apache.juneau.httppart.bean.*;
026
027/**
028 * Resolves and caches the public no-arg constructor and per-getter setter {@link Method}s for a concrete
029 * (non-interface) {@link org.apache.juneau.http.Request @Request} bean class.
030 *
031 * <p>
032 * {@link RequestBeanArg} resolves one of these once, when the {@code @RestOp} is registered (servlet init), instead of the historical per-request {@code Class.getMethod} lookup in
033 * {@code RestRequest.newRequestBean}. A getter with no matching setter, or a bean with no public no-arg constructor,
034 * now fails registration with an {@link IllegalArgumentException} naming the bean, the getter and the expected setter
035 * &mdash; not only the requests that happen to supply that parameter.
036 *
037 * <h5 class='section'>Example:</h5>
038 * <p class='bjava'>
039 *    <jc>// A concrete request bean: public no-arg constructor plus a setter for every annotated getter.</jc>
040 *    <ja>@Request</ja>
041 *    <jk>public class</jk> MyQuery {
042 *       <jk>private</jk> String <jf>foo</jf>;
043 *       <ja>@Query</ja> <jk>public</jk> String getFoo() { <jk>return</jk> <jf>foo</jf>; }
044 *       <jk>public void</jk> setFoo(String <jv>value</jv>) { <jf>foo</jf> = <jv>value</jv>; }
045 *    }
046 *
047 *    RequestBeanMeta <jv>meta</jv> = RequestBeanMeta.<jsm>create</jsm>(MyQuery.<jk>class</jk>, <jv>annotations</jv>);
048 *    RequestBeanSetters <jv>setters</jv> = RequestBeanSetters.<jsm>resolve</jsm>(MyQuery.<jk>class</jk>, <jv>meta</jv>);
049 *    MyQuery <jv>bean</jv> = <jv>setters</jv>.newInstance();
050 *    <jv>setters</jv>.set(<jv>bean</jv>, <jv>meta</jv>.getProperty(<js>"getFoo"</js>).getGetter(), <js>"value"</js>);
051 * </p>
052 *
053 * @since 10.0.0
054 */
055public final class RequestBeanSetters {
056
057   private static final Map<HttpPartType,String> ANNOTATION_NAMES = Map.of(
058      HttpPartType.HEADER, "@Header",
059      HttpPartType.QUERY, "@Query",
060      HttpPartType.FORMDATA, "@FormData",
061      HttpPartType.PATH, "@Path",
062      HttpPartType.BODY, "@Content"
063   );
064
065   /**
066    * Resolves the constructor and setters for a concrete {@link org.apache.juneau.http.Request @Request} bean class.
067    *
068    * @param beanClass The bean class. Must not be an interface.
069    * @param meta The metadata gathered from the class's annotated getters. Must not be <jk>null</jk>.
070    * @return A new {@link RequestBeanSetters}.
071    * @throws IllegalArgumentException If {@code beanClass} has no public no-arg constructor, or if any annotated
072    *    getter has no matching setter.
073    */
074   public static RequestBeanSetters resolve(Class<?> beanClass, RequestBeanMeta meta) {
075      Constructor<?> ctor;
076      try {
077         ctor = beanClass.getConstructor();
078      } catch (NoSuchMethodException e) {
079         throw new IllegalArgumentException(
080            f("Request bean %s must have a public no-arg constructor.", beanClass.getName()));
081      }
082      var setters = new LinkedHashMap<String,Method>();
083      for (var pm : meta.getProperties()) {
084         var getter = pm.getGetter();
085         var setterName = "set" + getter.getName().substring(getter.getName().startsWith("is") ? 2 : 3);
086         try {
087            setters.put(getter.getName(), beanClass.getMethod(setterName, getter.getReturnType()));
088         } catch (NoSuchMethodException e) {
089            throw new IllegalArgumentException(
090               f("Request bean %s has %s getter %s() with no matching setter %s(%s).",
091                  beanClass.getName(), ANNOTATION_NAMES.getOrDefault(pm.getPartType(), "@" + pm.getPartType()), getter.getName(), setterName,
092                  getter.getReturnType().getSimpleName()));
093         }
094      }
095      return new RequestBeanSetters(ctor, setters);
096   }
097
098   private final Constructor<?> ctor;
099   private final Map<String,Method> setters;
100
101   private RequestBeanSetters(Constructor<?> ctor, Map<String,Method> setters) {
102      this.ctor = ctor;
103      this.setters = setters;
104   }
105
106   /**
107    * Instantiates the bean via its public no-arg constructor.
108    *
109    * @param <T> The bean type.
110    * @return A new, empty bean instance.
111    * @throws ReflectiveOperationException If instantiation fails.
112    */
113   @SuppressWarnings({
114      "unchecked" // newInstance() casts the constructed bean to the caller's T.
115   })
116   public <T> T newInstance() throws ReflectiveOperationException {
117      return (T)ctor.newInstance();
118   }
119
120   /**
121    * Invokes the setter matching the specified getter on the specified bean.
122    *
123    * @param bean The bean to populate. Must not be <jk>null</jk>.
124    * @param getter The getter identifying the property, as returned by {@link RequestBeanPropertyMeta#getGetter()}.
125    * @param value The value to set. Callers skip absent (<jk>null</jk>) values rather than passing them here.
126    * @throws ReflectiveOperationException If the setter invocation fails.
127    */
128   public void set(Object bean, Method getter, Object value) throws ReflectiveOperationException {
129      setters.get(getter.getName()).invoke(bean, value);
130   }
131}