001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.databind.model.annotations;
007
008import java.lang.annotation.Annotation;
009import java.lang.reflect.Field;
010import java.net.URI;
011import java.util.Arrays;
012import java.util.LinkedHashSet;
013import java.util.List;
014import java.util.Map;
015import java.util.Set;
016
017import dev.metaschema.core.datatype.IDataTypeAdapter;
018import dev.metaschema.core.datatype.adapter.MetaschemaDataTypeProvider;
019import dev.metaschema.core.datatype.markup.MarkupLine;
020import dev.metaschema.core.datatype.markup.MarkupMultiline;
021import dev.metaschema.core.model.IAttributable;
022import dev.metaschema.core.model.IBoundObject;
023import dev.metaschema.core.model.IMetaschemaData;
024import dev.metaschema.core.model.IModule;
025import dev.metaschema.core.util.CollectionUtil;
026import dev.metaschema.databind.IBindingContext;
027import dev.metaschema.databind.model.IGroupAs;
028import dev.metaschema.databind.model.impl.DefaultGroupAs;
029import edu.umd.cs.findbugs.annotations.NonNull;
030import edu.umd.cs.findbugs.annotations.Nullable;
031
032/**
033 * Utility methods for processing Metaschema binding annotations.
034 * <p>
035 * This class provides helper methods for extracting and interpreting annotation
036 * values from Java classes and fields.
037 */
038public final class ModelUtil {
039  // TODO: replace NO_STRING_VALUE with NULL_VALUE where possible. URIs will not
040  // allow NULL_VALUE.
041  /**
042   * A sentinel value indicating that no string value was provided in an
043   * annotation.
044   */
045  public static final String NO_STRING_VALUE = "##none";
046  /**
047   * A sentinel value indicating that the default string value should be used.
048   */
049  public static final String DEFAULT_STRING_VALUE = "##default";
050  /**
051   * A placeholder for a {@code null} value for use in annotations, which cannot
052   * be null by default.
053   * <p>
054   * Use of {@code "\u0000"} simple substitute for {@code null} to allow
055   * implementations to recognize the "no default value" state.
056   */
057  public static final String NULL_VALUE = "\u0000";
058
059  private ModelUtil() {
060    // disable construction
061  }
062
063  /**
064   * Get the requested annotation from the provided Java class.
065   *
066   * @param <A>
067   *          the annotation Java type
068   * @param clazz
069   *          the Java class to get the annotation from
070   * @param annotationClass
071   *          the annotation class instance
072   * @return the annotation
073   * @throws IllegalArgumentException
074   *           if the annotation was not present on the class
075   */
076  @NonNull
077  public static <A extends Annotation> A getAnnotation(
078      @NonNull Class<?> clazz,
079      Class<A> annotationClass) {
080    A annotation = clazz.getAnnotation(annotationClass);
081    if (annotation == null) {
082      throw new IllegalArgumentException(
083          String.format("Class '%s' is missing the '%s' annotation.",
084              clazz.getName(),
085              annotationClass.getName()));
086    }
087    return annotation;
088  }
089
090  /**
091   * Get the requested annotation from the provided Java field.
092   *
093   * @param <A>
094   *          the annotation Java type
095   * @param javaField
096   *          the Java field to get the annotation from
097   * @param annotationClass
098   *          the annotation class instance
099   * @return the annotation
100   * @throws IllegalArgumentException
101   *           if the annotation was not present on the field
102   */
103  @NonNull
104  public static <A extends Annotation> A getAnnotation(
105      @NonNull Field javaField,
106      Class<A> annotationClass) {
107    A annotation = javaField.getAnnotation(annotationClass);
108    if (annotation == null) {
109      throw new IllegalArgumentException(
110          String.format("Field '%s' is missing the '%s' annotation.",
111              javaField.toGenericString(),
112              annotationClass.getName()));
113    }
114    return annotation;
115  }
116
117  /**
118   * Resolves a string value. If the value is {@code null} or "##default", then
119   * the provided default value will be used instead. If the value is "##none",
120   * then the value will be {@code null}. Otherwise, the value is returned.
121   *
122   * @param value
123   *          the requested value
124   * @param defaultValue
125   *          the default value
126   * @return the resolved value or {@code null}
127   */
128  @Nullable
129  public static String resolveNoneOrDefault(@Nullable String value, @Nullable String defaultValue) {
130    String retval;
131    if (value == null || DEFAULT_STRING_VALUE.equals(value)) {
132      retval = defaultValue;
133    } else if (NO_STRING_VALUE.equals(value)) {
134      retval = null; // NOPMD - intentional
135    } else {
136      retval = value;
137    }
138    return retval;
139  }
140
141  /**
142   * Get the processed value of a string. If the value is "##none", then the value
143   * will be {@code null}. Otherwise the value is returned.
144   *
145   * @param value
146   *          text or {@code "##none"} if no text is provided
147   * @return the resolved value or {@code null}
148   */
149  @Nullable
150  public static String resolveNoneOrValue(@NonNull String value) {
151    return NO_STRING_VALUE.equals(value) ? null : value;
152  }
153
154  /**
155   * Get the markup value of a markdown string.
156   *
157   * @param value
158   *          markdown text or {@code "##none"} if no text is provided
159   * @return the markup line content or {@code null} if no markup content was
160   *         provided
161   */
162  @Nullable
163  public static MarkupLine resolveToMarkupLine(@NonNull String value) {
164    return resolveNoneOrValue(value) == null ? null : MarkupLine.fromMarkdown(value);
165  }
166
167  /**
168   * Get the markup value of a markdown string.
169   *
170   * @param value
171   *          markdown text or {@code "##none"} if no text is provided
172   * @return the markup line content or {@code null} if no markup content was
173   *         provided
174   */
175  @Nullable
176  public static MarkupMultiline resolveToMarkupMultiline(@NonNull String value) {
177    return resolveNoneOrValue(value) == null ? null : MarkupMultiline.fromMarkdown(value);
178  }
179
180  /**
181   * Get the data type adapter instance of the provided adapter class.
182   * <p>
183   * If the provided adapter Java class is the {@link NullJavaTypeAdapter} class,
184   * then the default data type adapter will be returned.
185   *
186   * @param adapterClass
187   *          the data type adapter class to get the data type adapter instance
188   *          for
189   * @param bindingContext
190   *          the Metaschema binding context used to lookup the data type adapter
191   * @return the data type adapter
192   * @throws IllegalArgumentException
193   *           if the provided adapter is not registered with the binding context
194   */
195  @NonNull
196  public static IDataTypeAdapter<?> getDataTypeAdapter(
197      @NonNull Class<? extends IDataTypeAdapter<?>> adapterClass,
198      @NonNull IBindingContext bindingContext) {
199    IDataTypeAdapter<?> retval;
200    if (NullJavaTypeAdapter.class.equals(adapterClass)) {
201      retval = MetaschemaDataTypeProvider.DEFAULT_DATA_TYPE;
202    } else {
203      retval = bindingContext.getDataTypeAdapterInstance(adapterClass);
204      if (retval == null) {
205        throw new IllegalArgumentException("Unable to get type adapter instance for class: " + adapterClass.getName());
206      }
207    }
208    return retval;
209  }
210
211  /**
212   * Given a provided default value string, get the data type specific default
213   * value using the provided data type adapter.
214   * <p>
215   * If the provided default value is {@link ModelUtil#NULL_VALUE}, then this
216   * method will return a {@code null} value.
217   *
218   * @param defaultValue
219   *          the string representation of the default value
220   * @param adapter
221   *          the data type adapter instance used to cast the default string value
222   *          to a data type specific object
223   * @return the data type specific object or {@code null} if the provided default
224   *         value was {@link ModelUtil#NULL_VALUE}
225   */
226  @Nullable
227  public static Object resolveDefaultValue(@NonNull String defaultValue, IDataTypeAdapter<?> adapter) {
228    Object retval = null;
229    if (!NULL_VALUE.equals(defaultValue)) {
230      retval = adapter.parse(defaultValue);
231    }
232    return retval;
233  }
234
235  /**
236   * Resolves an integer value by determining if an actual value is provided or
237   * -2^31, which indicates that no actual value was provided.
238   * <p>
239   * The integer value -2^31 cannot be used, since this indicates no value.
240   *
241   * @param value
242   *          the integer value to resolve
243   * @return the integer value or {@code null} if the provided value was -2^31
244   */
245  public static Integer resolveDefaultInteger(int value) {
246    return value == Integer.MIN_VALUE ? null : value;
247  }
248
249  /**
250   * Resolves a {@link GroupAs} annotation determining if an actual value is
251   * provided or if the value is the default, which indicates that no actual
252   * GroupAs was provided.
253   *
254   * @param groupAs
255   *          the GroupAs value to resolve
256   * @param module
257   *          the containing module instance
258   * @return a new {@link IGroupAs} instance or a singleton group as if the
259   *         provided value was the default value
260   */
261  @NonNull
262  public static IGroupAs resolveDefaultGroupAs(
263      @NonNull GroupAs groupAs,
264      @NonNull IModule module) {
265    return NULL_VALUE.equals(groupAs.name())
266        ? IGroupAs.SINGLETON_GROUP_AS
267        : new DefaultGroupAs(groupAs, module);
268  }
269
270  /**
271   * Get a location string for the given bound object based on its metaschema
272   * data.
273   *
274   * @param obj
275   *          the bound object to get the location for
276   * @return a location string in the format "line:column", or an empty string if
277   *         location information is not available
278   */
279  public static String toLocation(@NonNull IBoundObject obj) {
280    IMetaschemaData data = obj.getMetaschemaData();
281
282    String retval = "";
283    if (data != null) {
284      int line = data.getLine();
285      if (line > -1) {
286        retval = line + ":" + data.getColumn();
287      }
288    }
289    return retval;
290  }
291
292  /**
293   * Get a location string for the given bound object, optionally including a URI.
294   *
295   * @param obj
296   *          the bound object to get the location for
297   * @param uri
298   *          the URI of the document containing the object, or {@code null} if
299   *          not available
300   * @return a location string in the format "uri@line:column", or just the URI or
301   *         location portion if only one is available, or an empty string if
302   *         neither is available
303   */
304  public static String toLocation(@NonNull IBoundObject obj, @Nullable URI uri) {
305    String retval = uri == null ? "" : uri.toASCIIString();
306
307    String location = toLocation(obj);
308    if (!location.isEmpty()) {
309      retval = retval.isEmpty() ? location : retval + "@" + location;
310    }
311    return retval;
312  }
313
314  /**
315   * Convert a {@link Property} annotation to a map entry suitable for use in an
316   * {@link IAttributable} properties map.
317   *
318   * @param property
319   *          the property annotation to convert
320   * @return a map entry containing the property key and its set of values
321   */
322  public static Map.Entry<IAttributable.Key, Set<String>> toPropertyEntry(@NonNull Property property) {
323    String name = property.name();
324    String namespace = property.namespace();
325    IAttributable.Key key = IAttributable.key(namespace, name);
326
327    String[] values = property.values();
328    List<String> valueList = Arrays.asList(values);
329    Set<String> valueSet = new LinkedHashSet<>(valueList);
330
331    return Map.entry(key, CollectionUtil.unmodifiableSet(valueSet));
332  }
333}