001/* 002 * SPDX-FileCopyrightText: none 003 * SPDX-License-Identifier: CC0-1.0 004 */ 005 006package dev.metaschema.databind.model.info; 007 008import java.io.IOException; 009import java.lang.reflect.Field; 010import java.lang.reflect.ParameterizedType; 011import java.lang.reflect.Type; 012import java.util.Collection; 013import java.util.List; 014import java.util.Map; 015 016import dev.metaschema.core.model.IBoundObject; 017import dev.metaschema.core.model.JsonGroupAsBehavior; 018import dev.metaschema.databind.io.BindingException; 019import dev.metaschema.databind.model.IBoundInstanceModel; 020import edu.umd.cs.findbugs.annotations.NonNull; 021import edu.umd.cs.findbugs.annotations.Nullable; 022 023// REFACTOR: parameterize the item type? 024/** 025 * Provides information about the collection type for a model instance. 026 * <p> 027 * This interface abstracts the differences between singleton, list, and map 028 * collection types for model instances. 029 * 030 * @param <ITEM> 031 * the Java type of items in the collection 032 */ 033public interface IModelInstanceCollectionInfo<ITEM> { 034 035 /** 036 * Create a new collection info instance for the provided model instance. 037 * <p> 038 * The appropriate collection info type is determined based on the instance's 039 * maximum occurrence and JSON group-as behavior. 040 * 041 * @param <T> 042 * the Java type of items in the collection 043 * @param instance 044 * the model instance to create collection info for 045 * @return the new collection info instance 046 */ 047 @NonNull 048 static <T> IModelInstanceCollectionInfo<T> of( 049 @NonNull IBoundInstanceModel<T> instance) { 050 051 // create the collection info 052 Type type = instance.getType(); 053 Field field = instance.getField(); 054 055 IModelInstanceCollectionInfo<T> retval; 056 if (instance.getMaxOccurs() == -1 || instance.getMaxOccurs() > 1) { 057 // collection case 058 JsonGroupAsBehavior jsonGroupAs = instance.getJsonGroupAsBehavior(); 059 060 // expect a ParameterizedType 061 if (!(type instanceof ParameterizedType)) { 062 switch (jsonGroupAs) { 063 case KEYED: 064 throw new IllegalStateException( 065 String.format("The field '%s' on class '%s' has data type of '%s'," + " but should have a type of '%s'.", 066 field.getName(), 067 field.getDeclaringClass().getName(), 068 field.getType().getName(), Map.class.getName())); 069 case LIST: 070 case SINGLETON_OR_LIST: 071 throw new IllegalStateException( 072 String.format("The field '%s' on class '%s' has data type of '%s'," + " but should have a type of '%s'.", 073 field.getName(), 074 field.getDeclaringClass().getName(), 075 field.getType().getName(), List.class.getName())); 076 default: 077 // this should not occur 078 throw new IllegalStateException(jsonGroupAs.name()); 079 } 080 } 081 082 Class<?> rawType = (Class<?>) ((ParameterizedType) type).getRawType(); 083 if (JsonGroupAsBehavior.KEYED.equals(jsonGroupAs)) { 084 if (!Map.class.isAssignableFrom(rawType)) { 085 throw new IllegalArgumentException(String.format( 086 "The field '%s' on class '%s' has data type '%s', which is not the expected '%s' derived data type.", 087 field.getName(), 088 field.getDeclaringClass().getName(), 089 field.getType().getName(), 090 Map.class.getName())); 091 } 092 retval = new MapCollectionInfo<>(instance); 093 } else { 094 if (!List.class.isAssignableFrom(rawType)) { 095 throw new IllegalArgumentException(String.format( 096 "The field '%s' on class '%s' has data type '%s', which is not the expected '%s' derived data type.", 097 field.getName(), 098 field.getDeclaringClass().getName(), 099 field.getType().getName(), 100 List.class.getName())); 101 } 102 retval = new ListCollectionInfo<>(instance); 103 } 104 } else { 105 // single value case 106 if (type instanceof ParameterizedType) { 107 throw new IllegalStateException(String.format( 108 "The field '%s' on class '%s' has a data parmeterized type of '%s'," 109 + " but the occurance is not multi-valued.", 110 field.getName(), 111 field.getDeclaringClass().getName(), 112 field.getType().getName())); 113 } 114 retval = new SingletonCollectionInfo<>(instance); 115 } 116 return retval; 117 } 118 119 /** 120 * Get the associated instance binding for which this info is for. 121 * 122 * @return the instance binding 123 */ 124 @NonNull 125 IBoundInstanceModel<ITEM> getInstance(); 126 127 /** 128 * Get the number of items associated with the value. 129 * 130 * @param value 131 * the value to identify items for 132 * @return the number of items, which will be {@code 0} if value is {@code null} 133 */ 134 int size(@Nullable Object value); 135 136 /** 137 * Determine if the value is empty. 138 * 139 * @param value 140 * the value representing a collection 141 * @return {@code true} if the value represents a collection with no items or 142 * {@code false} otherwise 143 */ 144 boolean isEmpty(@Nullable Object value); 145 146 /** 147 * Get the type of the bound object. 148 * 149 * @return the raw type of the bound object 150 */ 151 @NonNull 152 Class<? extends ITEM> getItemType(); 153 154 /** 155 * Get the items from a parent instance's property value. 156 * 157 * @param parentInstance 158 * the parent instance to get items from 159 * @return a collection of items, which may be empty but never {@code null} 160 */ 161 @NonNull 162 default Collection<? extends ITEM> getItemsFromParentInstance(@NonNull Object parentInstance) { 163 Object value = getInstance().getValue(parentInstance); 164 return getItemsFromValue(value); 165 } 166 167 /** 168 * Get the items from a raw value object. 169 * 170 * @param value 171 * the value object to extract items from 172 * @return a collection of items, which may be empty but never {@code null} 173 */ 174 @NonNull 175 Collection<? extends ITEM> getItemsFromValue(Object value); 176 177 /** 178 * Get an empty value appropriate for this collection type. 179 * 180 * @return an empty collection value, or {@code null} for singleton types 181 */ 182 Object emptyValue(); 183 184 /** 185 * Create a deep copy of items from one object to another. 186 * 187 * @param fromObject 188 * the source object to copy items from 189 * @param toObject 190 * the target object to copy items to 191 * @return the copied value 192 * @throws BindingException 193 * if an error occurs during the deep copy 194 */ 195 Object deepCopyItems(@NonNull IBoundObject fromObject, @NonNull IBoundObject toObject) throws BindingException; 196 197 /** 198 * Read the value data for the model instance. 199 * <p> 200 * This method will return a value based on the instance's value type. 201 * 202 * @param handler 203 * the item parsing handler 204 * @return the item collection object or {@code null} if the instance is not 205 * defined 206 * @throws IOException 207 * if there was an error when reading the data 208 */ 209 @Nullable 210 Object readItems(@NonNull IModelInstanceReadHandler<ITEM> handler) throws IOException; 211 212 /** 213 * Write the items represented by the given value. 214 * 215 * @param handler 216 * the item writing handler 217 * @param value 218 * the value containing items to write 219 * @throws IOException 220 * if there was an error when writing the data 221 */ 222 void writeItems( 223 @NonNull IModelInstanceWriteHandler<ITEM> handler, 224 @NonNull Object value) throws IOException; 225}