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}