001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package gov.nist.secauto.metaschema.databind.model.annotations;
007
008import gov.nist.secauto.metaschema.core.datatype.IDataTypeAdapter;
009import gov.nist.secauto.metaschema.core.datatype.adapter.MetaschemaDataTypeProvider;
010import gov.nist.secauto.metaschema.core.datatype.markup.MarkupLine;
011import gov.nist.secauto.metaschema.core.datatype.markup.MarkupMultiline;
012import gov.nist.secauto.metaschema.core.model.IBoundObject;
013import gov.nist.secauto.metaschema.core.model.IMetaschemaData;
014import gov.nist.secauto.metaschema.core.model.IModule;
015import gov.nist.secauto.metaschema.core.util.ObjectUtils;
016import gov.nist.secauto.metaschema.databind.IBindingContext;
017import gov.nist.secauto.metaschema.databind.model.IGroupAs;
018import gov.nist.secauto.metaschema.databind.model.impl.DefaultGroupAs;
019
020import java.lang.annotation.Annotation;
021import java.lang.reflect.Field;
022import java.net.URI;
023
024import edu.umd.cs.findbugs.annotations.NonNull;
025import edu.umd.cs.findbugs.annotations.Nullable;
026
027public final class ModelUtil {
028  // TODO: replace NO_STRING_VALUE with NULL_VALUE where possible. URIs will not
029  // allow NULL_VALUE.
030  public static final String NO_STRING_VALUE = "##none";
031  public static final String DEFAULT_STRING_VALUE = "##default";
032  /**
033   * A placeholder for a {@code null} value for use in annotations, which cannot
034   * be null by default.
035   * <p>
036   * Use of {@code "\u0000"} simple substitute for {@code null} to allow
037   * implementations to recognize the "no default value" state.
038   */
039  public static final String NULL_VALUE = "\u0000";
040
041  private ModelUtil() {
042    // disable construction
043  }
044
045  /**
046   * Get the requested annotation from the provided Java class.
047   *
048   * @param <A>
049   *          the annotation Java type
050   * @param clazz
051   *          the Java class to get the annotation from
052   * @param annotationClass
053   *          the annotation class instance
054   * @return the annotation
055   * @throws IllegalArgumentException
056   *           if the annotation was not present on the class
057   */
058  @NonNull
059  public static <A extends Annotation> A getAnnotation(
060      @NonNull Class<?> clazz,
061      Class<A> annotationClass) {
062    A annotation = clazz.getAnnotation(annotationClass);
063    if (annotation == null) {
064      throw new IllegalArgumentException(
065          String.format("Class '%s' is missing the '%s' annotation.",
066              clazz.getName(),
067              annotationClass.getName()));
068    }
069    return annotation;
070  }
071
072  /**
073   * Get the requested annotation from the provided Java field.
074   *
075   * @param <A>
076   *          the annotation Java type
077   * @param javaField
078   *          the Java field to get the annotation from
079   * @param annotationClass
080   *          the annotation class instance
081   * @return the annotation
082   * @throws IllegalArgumentException
083   *           if the annotation was not present on the field
084   */
085  @NonNull
086  public static <A extends Annotation> A getAnnotation(
087      @NonNull Field javaField,
088      Class<A> annotationClass) {
089    A annotation = javaField.getAnnotation(annotationClass);
090    if (annotation == null) {
091      throw new IllegalArgumentException(
092          String.format("Field '%s' is missing the '%s' annotation.",
093              javaField.toGenericString(),
094              annotationClass.getName()));
095    }
096    return annotation;
097  }
098
099  /**
100   * Resolves a string value. If the value is {@code null} or "##default", then
101   * the provided default value will be used instead. If the value is "##none",
102   * then the value will be {@code null}. Otherwise, the value is returned.
103   *
104   * @param value
105   *          the requested value
106   * @param defaultValue
107   *          the default value
108   * @return the resolved value or {@code null}
109   */
110  @Nullable
111  public static String resolveNoneOrDefault(@Nullable String value, @Nullable String defaultValue) {
112    String retval;
113    if (value == null || DEFAULT_STRING_VALUE.equals(value)) {
114      retval = defaultValue;
115    } else if (NO_STRING_VALUE.equals(value)) {
116      retval = null; // NOPMD - intentional
117    } else {
118      retval = value;
119    }
120    return retval;
121  }
122
123  /**
124   * Get the processed value of a string. If the value is "##none", then the value
125   * will be {@code null}. Otherwise the value is returned.
126   *
127   * @param value
128   *          text or {@code "##none"} if no text is provided
129   * @return the resolved value or {@code null}
130   */
131  @Nullable
132  public static String resolveNoneOrValue(@NonNull String value) {
133    return NO_STRING_VALUE.equals(value) ? null : value;
134  }
135
136  /**
137   * Get the markup value of a markdown string.
138   *
139   * @param value
140   *          markdown text or {@code "##none"} if no text is provided
141   * @return the markup line content or {@code null} if no markup content was
142   *         provided
143   */
144  @Nullable
145  public static MarkupLine resolveToMarkupLine(@NonNull String value) {
146    return resolveNoneOrValue(value) == null ? null : MarkupLine.fromMarkdown(value);
147  }
148
149  /**
150   * Get the markup value of a markdown string.
151   *
152   * @param value
153   *          markdown text or {@code "##none"} if no text is provided
154   * @return the markup line content or {@code null} if no markup content was
155   *         provided
156   */
157  @Nullable
158  public static MarkupMultiline resolveToMarkupMultiline(@NonNull String value) {
159    return resolveNoneOrValue(value) == null ? null : MarkupMultiline.fromMarkdown(value);
160  }
161
162  /**
163   * Get the data type adapter instance of the provided adapter class.
164   * <p>
165   * If the provided adapter Java class is the {@link NullJavaTypeAdapter} class,
166   * then the default data type adapter will be returned.
167   *
168   * @param adapterClass
169   *          the data type adapter class to get the data type adapter instance
170   *          for
171   * @param bindingContext
172   *          the Metaschema binding context used to lookup the data type adapter
173   * @return the data type adapter
174   */
175  @NonNull
176  public static IDataTypeAdapter<?> getDataTypeAdapter(
177      @NonNull Class<? extends IDataTypeAdapter<?>> adapterClass,
178      @NonNull IBindingContext bindingContext) {
179    IDataTypeAdapter<?> retval;
180    if (NullJavaTypeAdapter.class.equals(adapterClass)) {
181      retval = MetaschemaDataTypeProvider.DEFAULT_DATA_TYPE;
182    } else {
183      retval = ObjectUtils.requireNonNull(bindingContext.getJavaTypeAdapterInstance(adapterClass));
184    }
185    return retval;
186  }
187
188  /**
189   * Given a provided default value string, get the data type specific default
190   * value using the provided data type adapter.
191   * <p>
192   * If the provided default value is {@link ModelUtil#NULL_VALUE}, then this
193   * method will return a {@code null} value.
194   *
195   * @param defaultValue
196   *          the string representation of the default value
197   * @param adapter
198   *          the data type adapter instance used to cast the default string value
199   *          to a data type specific object
200   * @return the data type specific object or {@code null} if the provided default
201   *         value was {@link ModelUtil#NULL_VALUE}
202   */
203  @Nullable
204  public static Object resolveDefaultValue(@NonNull String defaultValue, IDataTypeAdapter<?> adapter) {
205    Object retval = null;
206    if (!NULL_VALUE.equals(defaultValue)) {
207      retval = adapter.parse(defaultValue);
208    }
209    return retval;
210  }
211
212  /**
213   * Resolves an integer value by determining if an actual value is provided or
214   * -2^31, which indicates that no actual value was provided.
215   * <p>
216   * The integer value -2^31 cannot be used, since this indicates no value.
217   *
218   * @param value
219   *          the integer value to resolve
220   * @return the integer value or {@code null} if the provided value was -2^31
221   */
222  public static Integer resolveDefaultInteger(int value) {
223    return value == Integer.MIN_VALUE ? null : value;
224  }
225
226  /**
227   * Resolves a {@link GroupAs} annotation determining if an actual value is
228   * provided or if the value is the default, which indicates that no actual
229   * GroupAs was provided.
230   *
231   * @param groupAs
232   *          the GroupAs value to resolve
233   * @param module
234   *          the containing module instance
235   * @return a new {@link IGroupAs} instance or a singleton group as if the
236   *         provided value was the default value
237   */
238  @NonNull
239  public static IGroupAs resolveDefaultGroupAs(
240      @NonNull GroupAs groupAs,
241      @NonNull IModule module) {
242    return NULL_VALUE.equals(groupAs.name())
243        ? IGroupAs.SINGLETON_GROUP_AS
244        : new DefaultGroupAs(groupAs, module);
245  }
246
247  public static String toLocation(@NonNull IBoundObject obj) {
248    IMetaschemaData data = obj.getMetaschemaData();
249
250    String retval = "";
251    if (data != null) {
252      int line = data.getLine();
253      if (line > -1) {
254        retval = line + ":" + data.getColumn();
255      }
256    }
257    return retval;
258  }
259
260  public static String toLocation(@NonNull IBoundObject obj, @Nullable URI uri) {
261    String retval = uri == null ? "" : uri.toASCIIString();
262
263    String location = toLocation(obj);
264    if (!location.isEmpty()) {
265      retval = retval.isEmpty() ? location : retval + "@" + location;
266    }
267    return retval;
268  }
269}