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 }