1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.databind.model.info;
7   
8   import java.io.IOException;
9   import java.lang.reflect.Field;
10  import java.lang.reflect.ParameterizedType;
11  import java.lang.reflect.Type;
12  import java.util.Collection;
13  import java.util.List;
14  import java.util.Map;
15  
16  import dev.metaschema.core.model.IBoundObject;
17  import dev.metaschema.core.model.JsonGroupAsBehavior;
18  import dev.metaschema.databind.io.BindingException;
19  import dev.metaschema.databind.model.IBoundInstanceModel;
20  import edu.umd.cs.findbugs.annotations.NonNull;
21  import edu.umd.cs.findbugs.annotations.Nullable;
22  
23  // REFACTOR: parameterize the item type?
24  /**
25   * Provides information about the collection type for a model instance.
26   * <p>
27   * This interface abstracts the differences between singleton, list, and map
28   * collection types for model instances.
29   *
30   * @param <ITEM>
31   *          the Java type of items in the collection
32   */
33  public interface IModelInstanceCollectionInfo<ITEM> {
34  
35    /**
36     * Create a new collection info instance for the provided model instance.
37     * <p>
38     * The appropriate collection info type is determined based on the instance's
39     * maximum occurrence and JSON group-as behavior.
40     *
41     * @param <T>
42     *          the Java type of items in the collection
43     * @param instance
44     *          the model instance to create collection info for
45     * @return the new collection info instance
46     */
47    @NonNull
48    static <T> IModelInstanceCollectionInfo<T> of(
49        @NonNull IBoundInstanceModel<T> instance) {
50  
51      // create the collection info
52      Type type = instance.getType();
53      Field field = instance.getField();
54  
55      IModelInstanceCollectionInfo<T> retval;
56      if (instance.getMaxOccurs() == -1 || instance.getMaxOccurs() > 1) {
57        // collection case
58        JsonGroupAsBehavior jsonGroupAs = instance.getJsonGroupAsBehavior();
59  
60        // expect a ParameterizedType
61        if (!(type instanceof ParameterizedType)) {
62          switch (jsonGroupAs) {
63          case KEYED:
64            throw new IllegalStateException(
65                String.format("The field '%s' on class '%s' has data type of '%s'," + " but should have a type of '%s'.",
66                    field.getName(),
67                    field.getDeclaringClass().getName(),
68                    field.getType().getName(), Map.class.getName()));
69          case LIST:
70          case SINGLETON_OR_LIST:
71            throw new IllegalStateException(
72                String.format("The field '%s' on class '%s' has data type of '%s'," + " but should have a type of '%s'.",
73                    field.getName(),
74                    field.getDeclaringClass().getName(),
75                    field.getType().getName(), List.class.getName()));
76          default:
77            // this should not occur
78            throw new IllegalStateException(jsonGroupAs.name());
79          }
80        }
81  
82        Class<?> rawType = (Class<?>) ((ParameterizedType) type).getRawType();
83        if (JsonGroupAsBehavior.KEYED.equals(jsonGroupAs)) {
84          if (!Map.class.isAssignableFrom(rawType)) {
85            throw new IllegalArgumentException(String.format(
86                "The field '%s' on class '%s' has data type '%s', which is not the expected '%s' derived data type.",
87                field.getName(),
88                field.getDeclaringClass().getName(),
89                field.getType().getName(),
90                Map.class.getName()));
91          }
92          retval = new MapCollectionInfo<>(instance);
93        } else {
94          if (!List.class.isAssignableFrom(rawType)) {
95            throw new IllegalArgumentException(String.format(
96                "The field '%s' on class '%s' has data type '%s', which is not the expected '%s' derived data type.",
97                field.getName(),
98                field.getDeclaringClass().getName(),
99                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 }