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.IAttributable;
013import gov.nist.secauto.metaschema.core.model.IBoundObject;
014import gov.nist.secauto.metaschema.core.model.IMetaschemaData;
015import gov.nist.secauto.metaschema.core.model.IModule;
016import gov.nist.secauto.metaschema.core.util.CollectionUtil;
017import gov.nist.secauto.metaschema.databind.IBindingContext;
018import gov.nist.secauto.metaschema.databind.model.IGroupAs;
019import gov.nist.secauto.metaschema.databind.model.impl.DefaultGroupAs;
020
021import java.lang.annotation.Annotation;
022import java.lang.reflect.Field;
023import java.net.URI;
024import java.util.Arrays;
025import java.util.LinkedHashSet;
026import java.util.List;
027import java.util.Map;
028import java.util.Set;
029
030import edu.umd.cs.findbugs.annotations.NonNull;
031import edu.umd.cs.findbugs.annotations.Nullable;
032
033public final class ModelUtil {
034  // TODO: replace NO_STRING_VALUE with NULL_VALUE where possible. URIs will not
035  // allow NULL_VALUE.
036  public static final String NO_STRING_VALUE = "##none";
037  public static final String DEFAULT_STRING_VALUE = "##default";
038  /**
039   * A placeholder for a {@code null} value for use in annotations, which cannot
040   * be null by default.
041   * <p>
042   * Use of {@code "\u0000"} simple substitute for {@code null} to allow
043   * implementations to recognize the "no default value" state.
044   */
045  public static final String NULL_VALUE = "\u0000";
046
047  private ModelUtil() {
048    // disable construction
049  }
050
051  /**
052   * Get the requested annotation from the provided Java class.
053   *
054   * @param <A>
055   *          the annotation Java type
056   * @param clazz
057   *          the Java class to get the annotation from
058   * @param annotationClass
059   *          the annotation class instance
060   * @return the annotation
061   * @throws IllegalArgumentException
062   *           if the annotation was not present on the class
063   */
064  @NonNull
065  public static <A extends Annotation> A getAnnotation(
066      @NonNull Class<?> clazz,
067      Class<A> annotationClass) {
068    A annotation = clazz.getAnnotation(annotationClass);
069    if (annotation == null) {
070      throw new IllegalArgumentException(
071          String.format("Class '%s' is missing the '%s' annotation.",
072              clazz.getName(),
073              annotationClass.getName()));
074    }
075    return annotation;
076  }
077
078  /**
079   * Get the requested annotation from the provided Java field.
080   *
081   * @param <A>
082   *          the annotation Java type
083   * @param javaField
084   *          the Java field to get the annotation from
085   * @param annotationClass
086   *          the annotation class instance
087   * @return the annotation
088   * @throws IllegalArgumentException
089   *           if the annotation was not present on the field
090   */
091  @NonNull
092  public static <A extends Annotation> A getAnnotation(
093      @NonNull Field javaField,
094      Class<A> annotationClass) {
095    A annotation = javaField.getAnnotation(annotationClass);
096    if (annotation == null) {
097      throw new IllegalArgumentException(
098          String.format("Field '%s' is missing the '%s' annotation.",
099              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}