1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.databind.model.impl;
7   
8   import java.lang.reflect.Field;
9   import java.util.Arrays;
10  import java.util.LinkedHashMap;
11  import java.util.Map;
12  import java.util.Set;
13  import java.util.function.Predicate;
14  import java.util.stream.Collectors;
15  
16  import dev.metaschema.core.datatype.markup.MarkupLine;
17  import dev.metaschema.core.datatype.markup.MarkupMultiline;
18  import dev.metaschema.core.model.AbstractAssemblyInstance;
19  import dev.metaschema.core.model.IAttributable;
20  import dev.metaschema.core.model.IBoundObject;
21  import dev.metaschema.core.util.CollectionUtil;
22  import dev.metaschema.core.util.ObjectUtils;
23  import dev.metaschema.databind.model.IBoundDefinitionModelAssembly;
24  import dev.metaschema.databind.model.IBoundInstanceFlag;
25  import dev.metaschema.databind.model.IBoundInstanceModelAssembly;
26  import dev.metaschema.databind.model.IBoundModule;
27  import dev.metaschema.databind.model.IBoundProperty;
28  import dev.metaschema.databind.model.IGroupAs;
29  import dev.metaschema.databind.model.annotations.BoundAssembly;
30  import dev.metaschema.databind.model.annotations.GroupAs;
31  import dev.metaschema.databind.model.annotations.ModelUtil;
32  import dev.metaschema.databind.model.info.IModelInstanceCollectionInfo;
33  import edu.umd.cs.findbugs.annotations.NonNull;
34  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
35  import nl.talsmasoftware.lazy4j.Lazy;
36  
37  /**
38   * Implements a Metaschema module assembly instance bound to a Java field,
39   * supported by a bound definition class.
40   */
41  /**
42   * Implementation of an assembly instance bound to a Java field.
43   * <p>
44   * This class handles the binding of a field referencing an assembly definition
45   * to the containing assembly's model.
46   */
47  public final class InstanceModelAssemblyComplex
48      extends AbstractAssemblyInstance<
49          IBoundDefinitionModelAssembly,
50          IBoundDefinitionModelAssembly,
51          IBoundInstanceModelAssembly,
52          IBoundDefinitionModelAssembly>
53      implements IBoundInstanceModelAssembly, IFeatureInstanceModelGroupAs {
54    @NonNull
55    private final Field javaField;
56    @NonNull
57    private final BoundAssembly annotation;
58    @NonNull
59    private final Lazy<IModelInstanceCollectionInfo<IBoundObject>> collectionInfo;
60    @NonNull
61    private final IBoundDefinitionModelAssembly definition;
62    @NonNull
63    private final IGroupAs groupAs;
64    @NonNull
65    private final Lazy<Map<String, IBoundProperty<?>>> jsonProperties;
66    @NonNull
67    private final Lazy<Map<IAttributable.Key, Set<String>>> properties;
68  
69    /**
70     * Construct a new field instance bound to a Java field, supported by a bound
71     * definition class.
72     *
73     * @param javaField
74     *          the Java field bound to this instance
75     * @param definition
76     *          the assembly definition this instance is bound to
77     * @param containingDefinition
78     *          the definition containing this instance
79     * @return the instance
80     */
81    @NonNull
82    public static InstanceModelAssemblyComplex newInstance(
83        @NonNull Field javaField,
84        @NonNull IBoundDefinitionModelAssembly definition,
85        @NonNull IBoundDefinitionModelAssembly containingDefinition) {
86      BoundAssembly annotation = ModelUtil.getAnnotation(javaField, BoundAssembly.class);
87      IGroupAs groupAs = ModelUtil.resolveDefaultGroupAs(
88          annotation.groupAs(),
89          containingDefinition.getContainingModule());
90      if (annotation.maxOccurs() == -1 || annotation.maxOccurs() > 1) {
91        if (IGroupAs.SINGLETON_GROUP_AS.equals(groupAs)) {
92          throw new IllegalStateException(String.format("Field '%s' on class '%s' is missing the '%s' annotation.",
93              javaField.getName(),
94              containingDefinition.getBoundClass().getName(),
95              GroupAs.class.getName()));
96        }
97      } else if (!IGroupAs.SINGLETON_GROUP_AS.equals(groupAs)) {
98        // max is 1 and a groupAs is set
99        throw new IllegalStateException(
100           String.format(
101               "Field '%s' on class '%s' has the '%s' annotation, but maxOccurs=1. A groupAs must not be specfied.",
102               javaField.getName(),
103               containingDefinition.getBoundClass().getName(),
104               GroupAs.class.getName()));
105     }
106     return new InstanceModelAssemblyComplex(javaField, annotation, groupAs, definition, containingDefinition);
107   }
108 
109   @SuppressFBWarnings(value = "CT_CONSTRUCTOR_THROW", justification = "Use of final fields")
110   private InstanceModelAssemblyComplex(
111       @NonNull Field javaField,
112       @NonNull BoundAssembly annotation,
113       @NonNull IGroupAs groupAs,
114       @NonNull IBoundDefinitionModelAssembly definition,
115       @NonNull IBoundDefinitionModelAssembly containingDefinition) {
116     super(containingDefinition);
117     FieldSupport.bindField(javaField);
118     this.javaField = javaField;
119     this.annotation = annotation;
120     this.groupAs = groupAs;
121     this.collectionInfo = ObjectUtils.notNull(Lazy.of(() -> IModelInstanceCollectionInfo.of(this)));
122     this.definition = definition;
123     this.jsonProperties = ObjectUtils.notNull(Lazy.of(() -> {
124       IBoundInstanceFlag jsonKey = getEffectiveJsonKey();
125       Predicate<IBoundInstanceFlag> flagFilter = jsonKey == null ? null : flag -> !jsonKey.equals(flag);
126       return getDefinition().getJsonProperties(flagFilter);
127     }));
128     this.properties = ObjectUtils.notNull(
129         Lazy.of(() -> CollectionUtil.unmodifiableMap(ObjectUtils.notNull(
130             Arrays.stream(annotation.properties())
131                 .map(ModelUtil::toPropertyEntry)
132                 .collect(
133                     Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (v1, v2) -> v2, LinkedHashMap::new))))));
134   }
135 
136   // ------------------------------------------
137   // - Start annotation driven code - CPD-OFF -
138   // ------------------------------------------
139 
140   @Override
141   public Field getField() {
142     return javaField;
143   }
144 
145   /**
146    * Get the binding Java annotation.
147    *
148    * @return the binding Java annotation
149    */
150   @NonNull
151   public BoundAssembly getAnnotation() {
152     return annotation;
153   }
154 
155   @SuppressWarnings("null")
156   @Override
157   public IModelInstanceCollectionInfo<IBoundObject> getCollectionInfo() {
158     return collectionInfo.get();
159   }
160 
161   @Override
162   public Map<String, IBoundProperty<?>> getJsonProperties() {
163     return ObjectUtils.notNull(jsonProperties.get());
164   }
165 
166   @Override
167   public IBoundDefinitionModelAssembly getDefinition() {
168     return definition;
169   }
170 
171   @Override
172   public IBoundModule getContainingModule() {
173     return getContainingDefinition().getContainingModule();
174   }
175 
176   @Override
177   public IGroupAs getGroupAs() {
178     return groupAs;
179   }
180 
181   @Override
182   public String getFormalName() {
183     return ModelUtil.resolveNoneOrValue(getAnnotation().formalName());
184   }
185 
186   @Override
187   public MarkupLine getDescription() {
188     return ModelUtil.resolveToMarkupLine(getAnnotation().description());
189   }
190 
191   @Override
192   public String getUseName() {
193     return ModelUtil.resolveNoneOrValue(getAnnotation().useName());
194   }
195 
196   @Override
197   public Integer getUseIndex() {
198     int value = getAnnotation().useIndex();
199     return value == Integer.MIN_VALUE ? null : value;
200   }
201 
202   @Override
203   public int getMinOccurs() {
204     return getAnnotation().minOccurs();
205   }
206 
207   @Override
208   public int getMaxOccurs() {
209     return getAnnotation().maxOccurs();
210   }
211 
212   @Override
213   public Map<Key, Set<String>> getProperties() {
214     return ObjectUtils.notNull(properties.get());
215   }
216 
217   @Override
218   public MarkupMultiline getRemarks() {
219     return ModelUtil.resolveToMarkupMultiline(getAnnotation().remarks());
220   }
221 }