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}