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.marshall.parser;
018
019import java.util.*;
020
021import org.apache.juneau.commons.bean.*;
022
023/**
024 * Thrown when parser input (or the source map of a {@code Map}-to-bean conversion) does not contain one or more
025 * properties marked {@link BeanProp#required() @BeanProp(required=true)}.
026 *
027 * <p>
028 * The check runs once per bean, after all input properties have been read and before the bean is constructed, so
029 * records and constructor-argument beans get this exception instead of a constructor failure.  An explicit
030 * <jk>null</jk> value counts as present.
031 *
032 * <p>
033 * This is a {@link ParseException}, so REST {@code @Content} parameters map it to <c>400 Bad Request</c>.
034 *
035 * <h5 class='section'>Example:</h5>
036 * <p class='bjava'>
037 *    <jk>public record</jk> Person(<ja>@BeanProp</ja>(required=<jk>true</jk>) String <jv>name</jv>, Integer <jv>age</jv>) {}
038 *
039 *    <jk>try</jk> {
040 *       JsonParser.<jsf>DEFAULT</jsf>.read(<js>"{\"age\":30}"</js>, Person.<jk>class</jk>);
041 *    } <jk>catch</jk> (MissingRequiredPropertyException <jv>e</jv>) {
042 *       <jv>e</jv>.getBeanClass();       <jc>// Person.class</jc>
043 *       <jv>e</jv>.getPropertyNames();   <jc>// [name]</jc>
044 *    }
045 * </p>
046 *
047 * <h5 class='section'>See Also:</h5><ul>
048 *    <li class='ja'>{@link BeanProp#required()}
049 * </ul>
050 *
051 * @serial exclude
052 */
053@SuppressWarnings({
054   "java:S110", // Inherited hierarchy depth from ParseException; acceptable for a specific parse failure.
055   "java:S2166" // ParseException extends BasicRuntimeException, so the class is a runtime exception and the name is accurate.
056})
057public class MissingRequiredPropertyException extends ParseException {
058
059   private static final long serialVersionUID = 1L;
060
061   /**
062    * Throws this exception if the bean map is missing required properties.
063    *
064    * <p>
065    * Parser sessions call this through {@code ParserSession.checkRequired(BeanMap)}.  Conversion code that has no
066    * parser session passes <jk>null</jk>, which omits the input position from the message.
067    *
068    * @param <T> The bean type.
069    * @param session The current parser session, or <jk>null</jk> if not parsing.
070    * @param m The bean map to check.  Can be <jk>null</jk>.
071    * @return The same bean map, for chaining.
072    * @throws MissingRequiredPropertyException If a required property was never written.
073    */
074   @SuppressWarnings({
075      "java:S1168" // Pass-through for chaining: returns the argument itself, so null in gives null out
076   })
077   public static <T> BeanMap<T> check(ParserSession session, BeanMap<T> m) {
078      if (m == null)
079         return null;
080      var missing = m.getMissingRequiredProperties();
081      if (! missing.isEmpty())
082         throw new MissingRequiredPropertyException(session, m.getMeta().getClassInfo().inner(), missing);
083      return m;
084   }
085
086   /**
087    * Finds a {@link MissingRequiredPropertyException} in the cause chain of a throwable.
088    *
089    * <p>
090    * Bean property setters wrap failures from nested conversions, so conversion code uses this to re-surface the
091    * original exception (which is a {@link ParseException} and maps to HTTP 400).
092    *
093    * @param t The throwable to inspect.  Can be <jk>null</jk>.
094    * @return The first exception of this type in the chain (including <c>t</c> itself), or <jk>null</jk> if none.
095    */
096   public static MissingRequiredPropertyException find(Throwable t) {
097      for (var i = 0; t != null && i < 32; i++, t = t.getCause())
098         if (t instanceof MissingRequiredPropertyException e)
099            return e;
100      return null;
101   }
102
103   private final Class<?> beanClass;
104   private final String[] propertyNames;  // Array (not List) so the exception stays serializable.
105
106   /**
107    * Constructor.
108    *
109    * @param session The parser session, or <jk>null</jk>.  Used to add the input position to the message.
110    * @param beanClass The bean class.
111    * @param propertyNames The missing property names.
112    */
113   public MissingRequiredPropertyException(ParserSession session, Class<?> beanClass, List<String> propertyNames) {
114      super(session, "Missing required properties on bean class '%s': %s", beanClass.getSimpleName(), propertyNames);
115      this.beanClass = beanClass;
116      this.propertyNames = propertyNames.toArray(String[]::new);
117   }
118
119   /**
120    * Returns the bean class whose required properties were missing.
121    *
122    * @return The bean class.  Never <jk>null</jk>.
123    */
124   public Class<?> getBeanClass() { return beanClass; }
125
126   /**
127    * Returns the names of the missing required properties, in bean property order.
128    *
129    * @return An unmodifiable list.  Never <jk>null</jk> or empty.
130    */
131   public List<String> getPropertyNames() { return List.of(propertyNames); }
132}