1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.databind.model.annotations;
7   
8   import gov.nist.secauto.metaschema.core.datatype.IDataTypeAdapter;
9   import gov.nist.secauto.metaschema.core.datatype.adapter.MetaschemaDataTypeProvider;
10  import gov.nist.secauto.metaschema.core.datatype.markup.MarkupLine;
11  import gov.nist.secauto.metaschema.core.datatype.markup.MarkupMultiline;
12  import gov.nist.secauto.metaschema.core.model.IAttributable;
13  import gov.nist.secauto.metaschema.core.model.IBoundObject;
14  import gov.nist.secauto.metaschema.core.model.IMetaschemaData;
15  import gov.nist.secauto.metaschema.core.model.IModule;
16  import gov.nist.secauto.metaschema.core.util.CollectionUtil;
17  import gov.nist.secauto.metaschema.databind.IBindingContext;
18  import gov.nist.secauto.metaschema.databind.model.IGroupAs;
19  import gov.nist.secauto.metaschema.databind.model.impl.DefaultGroupAs;
20  
21  import java.lang.annotation.Annotation;
22  import java.lang.reflect.Field;
23  import java.net.URI;
24  import java.util.Arrays;
25  import java.util.LinkedHashSet;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Set;
29  
30  import edu.umd.cs.findbugs.annotations.NonNull;
31  import edu.umd.cs.findbugs.annotations.Nullable;
32  
33  public final class ModelUtil {
34    // TODO: replace NO_STRING_VALUE with NULL_VALUE where possible. URIs will not
35    // allow NULL_VALUE.
36    public static final String NO_STRING_VALUE = "##none";
37    public static final String DEFAULT_STRING_VALUE = "##default";
38    /**
39     * A placeholder for a {@code null} value for use in annotations, which cannot
40     * be null by default.
41     * <p>
42     * Use of {@code "\u0000"} simple substitute for {@code null} to allow
43     * implementations to recognize the "no default value" state.
44     */
45    public static final String NULL_VALUE = "\u0000";
46  
47    private ModelUtil() {
48      // disable construction
49    }
50  
51    /**
52     * Get the requested annotation from the provided Java class.
53     *
54     * @param <A>
55     *          the annotation Java type
56     * @param clazz
57     *          the Java class to get the annotation from
58     * @param annotationClass
59     *          the annotation class instance
60     * @return the annotation
61     * @throws IllegalArgumentException
62     *           if the annotation was not present on the class
63     */
64    @NonNull
65    public static <A extends Annotation> A getAnnotation(
66        @NonNull Class<?> clazz,
67        Class<A> annotationClass) {
68      A annotation = clazz.getAnnotation(annotationClass);
69      if (annotation == null) {
70        throw new IllegalArgumentException(
71            String.format("Class '%s' is missing the '%s' annotation.",
72                clazz.getName(),
73                annotationClass.getName()));
74      }
75      return annotation;
76    }
77  
78    /**
79     * Get the requested annotation from the provided Java field.
80     *
81     * @param <A>
82     *          the annotation Java type
83     * @param javaField
84     *          the Java field to get the annotation from
85     * @param annotationClass
86     *          the annotation class instance
87     * @return the annotation
88     * @throws IllegalArgumentException
89     *           if the annotation was not present on the field
90     */
91    @NonNull
92    public static <A extends Annotation> A getAnnotation(
93        @NonNull Field javaField,
94        Class<A> annotationClass) {
95      A annotation = javaField.getAnnotation(annotationClass);
96      if (annotation == null) {
97        throw new IllegalArgumentException(
98            String.format("Field '%s' is missing the '%s' annotation.",
99                javaField.toGenericString(),
100               annotationClass.getName()));
101     }
102     return annotation;
103   }
104 
105   /**
106    * Resolves a string value. If the value is {@code null} or "##default", then
107    * the provided default value will be used instead. If the value is "##none",
108    * then the value will be {@code null}. Otherwise, the value is returned.
109    *
110    * @param value
111    *          the requested value
112    * @param defaultValue
113    *          the default value
114    * @return the resolved value or {@code null}
115    */
116   @Nullable
117   public static String resolveNoneOrDefault(@Nullable String value, @Nullable String defaultValue) {
118     String retval;
119     if (value == null || DEFAULT_STRING_VALUE.equals(value)) {
120       retval = defaultValue;
121     } else if (NO_STRING_VALUE.equals(value)) {
122       retval = null; // NOPMD - intentional
123     } else {
124       retval = value;
125     }
126     return retval;
127   }
128 
129   /**
130    * Get the processed value of a string. If the value is "##none", then the value
131    * will be {@code null}. Otherwise the value is returned.
132    *
133    * @param value
134    *          text or {@code "##none"} if no text is provided
135    * @return the resolved value or {@code null}
136    */
137   @Nullable
138   public static String resolveNoneOrValue(@NonNull String value) {
139     return NO_STRING_VALUE.equals(value) ? null : value;
140   }
141 
142   /**
143    * Get the markup value of a markdown string.
144    *
145    * @param value
146    *          markdown text or {@code "##none"} if no text is provided
147    * @return the markup line content or {@code null} if no markup content was
148    *         provided
149    */
150   @Nullable
151   public static MarkupLine resolveToMarkupLine(@NonNull String value) {
152     return resolveNoneOrValue(value) == null ? null : MarkupLine.fromMarkdown(value);
153   }
154 
155   /**
156    * Get the markup value of a markdown string.
157    *
158    * @param value
159    *          markdown text or {@code "##none"} if no text is provided
160    * @return the markup line content or {@code null} if no markup content was
161    *         provided
162    */
163   @Nullable
164   public static MarkupMultiline resolveToMarkupMultiline(@NonNull String value) {
165     return resolveNoneOrValue(value) == null ? null : MarkupMultiline.fromMarkdown(value);
166   }
167 
168   /**
169    * Get the data type adapter instance of the provided adapter class.
170    * <p>
171    * If the provided adapter Java class is the {@link NullJavaTypeAdapter} class,
172    * then the default data type adapter will be returned.
173    *
174    * @param adapterClass
175    *          the data type adapter class to get the data type adapter instance
176    *          for
177    * @param bindingContext
178    *          the Metaschema binding context used to lookup the data type adapter
179    * @return the data type adapter
180    * @throws IllegalArgumentException
181    *           if the provided adapter is not registered with the binding context
182    */
183   @NonNull
184   public static IDataTypeAdapter<?> getDataTypeAdapter(
185       @NonNull Class<? extends IDataTypeAdapter<?>> adapterClass,
186       @NonNull IBindingContext bindingContext) {
187     IDataTypeAdapter<?> retval;
188     if (NullJavaTypeAdapter.class.equals(adapterClass)) {
189       retval = MetaschemaDataTypeProvider.DEFAULT_DATA_TYPE;
190     } else {
191       retval = bindingContext.getJavaTypeAdapterInstance(adapterClass);
192       if (retval == null) {
193         throw new IllegalArgumentException("Unable to get type adapter instance for class: " + adapterClass.getName());
194       }
195     }
196     return retval;
197   }
198 
199   /**
200    * Given a provided default value string, get the data type specific default
201    * value using the provided data type adapter.
202    * <p>
203    * If the provided default value is {@link ModelUtil#NULL_VALUE}, then this
204    * method will return a {@code null} value.
205    *
206    * @param defaultValue
207    *          the string representation of the default value
208    * @param adapter
209    *          the data type adapter instance used to cast the default string value
210    *          to a data type specific object
211    * @return the data type specific object or {@code null} if the provided default
212    *         value was {@link ModelUtil#NULL_VALUE}
213    */
214   @Nullable
215   public static Object resolveDefaultValue(@NonNull String defaultValue, IDataTypeAdapter<?> adapter) {
216     Object retval = null;
217     if (!NULL_VALUE.equals(defaultValue)) {
218       retval = adapter.parse(defaultValue);
219     }
220     return retval;
221   }
222 
223   /**
224    * Resolves an integer value by determining if an actual value is provided or
225    * -2^31, which indicates that no actual value was provided.
226    * <p>
227    * The integer value -2^31 cannot be used, since this indicates no value.
228    *
229    * @param value
230    *          the integer value to resolve
231    * @return the integer value or {@code null} if the provided value was -2^31
232    */
233   public static Integer resolveDefaultInteger(int value) {
234     return value == Integer.MIN_VALUE ? null : value;
235   }
236 
237   /**
238    * Resolves a {@link GroupAs} annotation determining if an actual value is
239    * provided or if the value is the default, which indicates that no actual
240    * GroupAs was provided.
241    *
242    * @param groupAs
243    *          the GroupAs value to resolve
244    * @param module
245    *          the containing module instance
246    * @return a new {@link IGroupAs} instance or a singleton group as if the
247    *         provided value was the default value
248    */
249   @NonNull
250   public static IGroupAs resolveDefaultGroupAs(
251       @NonNull GroupAs groupAs,
252       @NonNull IModule module) {
253     return NULL_VALUE.equals(groupAs.name())
254         ? IGroupAs.SINGLETON_GROUP_AS
255         : new DefaultGroupAs(groupAs, module);
256   }
257 
258   public static String toLocation(@NonNull IBoundObject obj) {
259     IMetaschemaData data = obj.getMetaschemaData();
260 
261     String retval = "";
262     if (data != null) {
263       int line = data.getLine();
264       if (line > -1) {
265         retval = line + ":" + data.getColumn();
266       }
267     }
268     return retval;
269   }
270 
271   public static String toLocation(@NonNull IBoundObject obj, @Nullable URI uri) {
272     String retval = uri == null ? "" : uri.toASCIIString();
273 
274     String location = toLocation(obj);
275     if (!location.isEmpty()) {
276       retval = retval.isEmpty() ? location : retval + "@" + location;
277     }
278     return retval;
279   }
280 
281   public static Map.Entry<IAttributable.Key, Set<String>> toPropertyEntry(@NonNull Property property) {
282     String name = property.name();
283     String namespace = property.namespace();
284     @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") // ok
285     IAttributable.Key key = IAttributable.key(namespace, name);
286 
287     String[] values = property.values();
288     List<String> valueList = Arrays.asList(values);
289     @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") // ok
290     Set<String> valueSet = new LinkedHashSet<>(valueList);
291 
292     return Map.entry(key, CollectionUtil.unmodifiableSet(valueSet));
293   }
294 }