1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.databind.model.annotations;
7   
8   import java.lang.annotation.Annotation;
9   import java.lang.reflect.Field;
10  import java.net.URI;
11  import java.util.Arrays;
12  import java.util.LinkedHashSet;
13  import java.util.List;
14  import java.util.Map;
15  import java.util.Set;
16  
17  import dev.metaschema.core.datatype.IDataTypeAdapter;
18  import dev.metaschema.core.datatype.adapter.MetaschemaDataTypeProvider;
19  import dev.metaschema.core.datatype.markup.MarkupLine;
20  import dev.metaschema.core.datatype.markup.MarkupMultiline;
21  import dev.metaschema.core.model.IAttributable;
22  import dev.metaschema.core.model.IBoundObject;
23  import dev.metaschema.core.model.IMetaschemaData;
24  import dev.metaschema.core.model.IModule;
25  import dev.metaschema.core.util.CollectionUtil;
26  import dev.metaschema.databind.IBindingContext;
27  import dev.metaschema.databind.model.IGroupAs;
28  import dev.metaschema.databind.model.impl.DefaultGroupAs;
29  import edu.umd.cs.findbugs.annotations.NonNull;
30  import edu.umd.cs.findbugs.annotations.Nullable;
31  
32  /**
33   * Utility methods for processing Metaschema binding annotations.
34   * <p>
35   * This class provides helper methods for extracting and interpreting annotation
36   * values from Java classes and fields.
37   */
38  public final class ModelUtil {
39    // TODO: replace NO_STRING_VALUE with NULL_VALUE where possible. URIs will not
40    // allow NULL_VALUE.
41    /**
42     * A sentinel value indicating that no string value was provided in an
43     * annotation.
44     */
45    public static final String NO_STRING_VALUE = "##none";
46    /**
47     * A sentinel value indicating that the default string value should be used.
48     */
49    public static final String DEFAULT_STRING_VALUE = "##default";
50    /**
51     * A placeholder for a {@code null} value for use in annotations, which cannot
52     * be null by default.
53     * <p>
54     * Use of {@code "\u0000"} simple substitute for {@code null} to allow
55     * implementations to recognize the "no default value" state.
56     */
57    public static final String NULL_VALUE = "\u0000";
58  
59    private ModelUtil() {
60      // disable construction
61    }
62  
63    /**
64     * Get the requested annotation from the provided Java class.
65     *
66     * @param <A>
67     *          the annotation Java type
68     * @param clazz
69     *          the Java class to get the annotation from
70     * @param annotationClass
71     *          the annotation class instance
72     * @return the annotation
73     * @throws IllegalArgumentException
74     *           if the annotation was not present on the class
75     */
76    @NonNull
77    public static <A extends Annotation> A getAnnotation(
78        @NonNull Class<?> clazz,
79        Class<A> annotationClass) {
80      A annotation = clazz.getAnnotation(annotationClass);
81      if (annotation == null) {
82        throw new IllegalArgumentException(
83            String.format("Class '%s' is missing the '%s' annotation.",
84                clazz.getName(),
85                annotationClass.getName()));
86      }
87      return annotation;
88    }
89  
90    /**
91     * Get the requested annotation from the provided Java field.
92     *
93     * @param <A>
94     *          the annotation Java type
95     * @param javaField
96     *          the Java field to get the annotation from
97     * @param annotationClass
98     *          the annotation class instance
99     * @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 }