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}