1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.schemagen.json.impl;
7   
8   import com.fasterxml.jackson.core.JsonGenerator;
9   import com.fasterxml.jackson.databind.node.JsonNodeFactory;
10  import com.fasterxml.jackson.databind.node.ObjectNode;
11  
12  import java.io.IOException;
13  import java.util.LinkedHashMap;
14  import java.util.List;
15  import java.util.Map;
16  import java.util.Objects;
17  import java.util.concurrent.ConcurrentHashMap;
18  import java.util.function.Supplier;
19  
20  import dev.metaschema.core.configuration.IConfiguration;
21  import dev.metaschema.core.datatype.IDataTypeAdapter;
22  import dev.metaschema.core.model.IAssemblyDefinition;
23  import dev.metaschema.core.model.IAssemblyInstanceAbsolute;
24  import dev.metaschema.core.model.IAssemblyInstanceGrouped;
25  import dev.metaschema.core.model.IChoiceGroupInstance;
26  import dev.metaschema.core.model.IDefinition;
27  import dev.metaschema.core.model.IFieldDefinition;
28  import dev.metaschema.core.model.IFieldInstanceAbsolute;
29  import dev.metaschema.core.model.IFieldInstanceGrouped;
30  import dev.metaschema.core.model.IFlagDefinition;
31  import dev.metaschema.core.model.IFlagInstance;
32  import dev.metaschema.core.model.IModelDefinition;
33  import dev.metaschema.core.model.IModelInstanceAbsolute;
34  import dev.metaschema.core.model.IModule;
35  import dev.metaschema.core.model.INamedModelInstanceGrouped;
36  import dev.metaschema.core.model.IValuedDefinition;
37  import dev.metaschema.core.model.constraint.IAllowedValue;
38  import dev.metaschema.core.qname.IEnhancedQName;
39  import dev.metaschema.core.util.ObjectUtils;
40  import dev.metaschema.schemagen.AbstractGenerationState;
41  import dev.metaschema.schemagen.IGenerationState;
42  import dev.metaschema.schemagen.SchemaGenerationFeature;
43  import edu.umd.cs.findbugs.annotations.NonNull;
44  import edu.umd.cs.findbugs.annotations.Nullable;
45  
46  /**
47   * Maintains state during JSON Schema generation from a Metaschema module.
48   * <p>
49   * This class manages caches for data type schemas, definition schemas, and
50   * provides methods for generating JSON schema definitions and writing the
51   * output.
52   */
53  public class JsonGenerationState
54      extends AbstractGenerationState<JsonGenerator, JsonDatatypeManager>
55      implements IJsonGenerationState {
56    @NonNull
57    private final JsonNodeFactory jsonNodeFactory = new JsonNodeFactory(true);
58    @NonNull
59    private final Map<IValuedDefinition, IDataTypeJsonSchema> definitionValueToDataTypeSchemaMap
60        = new ConcurrentHashMap<>();
61    @NonNull
62    private final Map<IDataTypeAdapter<?>, IDataTypeJsonSchema> dataTypeToSchemaMap = new ConcurrentHashMap<>();
63  
64    @NonNull
65    private final Map<String, IJsonSchemaDefinable> definitionNameToJsonSchemaMap = new ConcurrentHashMap<>();
66  
67    @NonNull
68    private final Map<IDefinition, Map<IEnhancedQName, IJsonSchemaDefinition>> definitionToJsonKeyToJsonSchemaMap
69        = new ConcurrentHashMap<>();
70  
71    @NonNull
72    private final Map<GroupedDefinition,
73        Map<IEnhancedQName, IJsonSchemaPropertyGrouped>> groupedInstanceToJsonKeyToJsonSchemaMap
74            = new ConcurrentHashMap<>();
75  
76    /**
77     * Constructs a new JSON generation state for the specified module.
78     *
79     * @param module
80     *          the Metaschema module to generate a schema for
81     * @param writer
82     *          the JSON generator for writing the schema output
83     * @param configuration
84     *          the schema generation configuration settings
85     */
86    public JsonGenerationState(
87        @NonNull IModule module,
88        @NonNull JsonGenerator writer,
89        @NonNull IConfiguration<SchemaGenerationFeature<?>> configuration) {
90      super(module, writer, configuration, new JsonDatatypeManager());
91    }
92  
93    @NonNull
94    private <T extends IJsonSchemaDefinition> T addToCache(
95        @NonNull IDefinition definition,
96        @Nullable IEnhancedQName jsonKeyName,
97        @NonNull Supplier<T> supplier) {
98      // add to definition to JSON key to JsonSchema map
99      Map<IEnhancedQName, IJsonSchemaDefinition> jsonKeyMap
100         = definitionToJsonKeyToJsonSchemaMap.computeIfAbsent(definition, (key) -> new LinkedHashMap<>());
101 
102     @SuppressWarnings("unchecked")
103     T retval = (T) jsonKeyMap.computeIfAbsent(jsonKeyName, (key) -> supplier.get());
104 
105     assert definition.equals(retval.getDefinition());
106 
107     if (!isInline(definition)) {
108       // add to definition to JSON definition name to definition map
109       IJsonSchemaDefinable newSchema
110           = definitionNameToJsonSchemaMap.computeIfAbsent(retval.getDefinitionName(), (key) -> retval);
111 
112       assert newSchema.equals(retval) : "Duplicate JSON definition name: "
113           + retval.getDefinitionName();
114     }
115 
116     return retval;
117   }
118 
119   @NonNull
120   private <T extends IJsonSchemaPropertyGrouped> T addToCache(
121       @NonNull INamedModelInstanceGrouped instance,
122       @Nullable IEnhancedQName jsonKeyName,
123       @NonNull Supplier<T> supplier) {
124     GroupedDefinition grouped = new GroupedDefinition(instance);
125 
126     // add to definition to JSON key to JsonSchema map
127     Map<IEnhancedQName, IJsonSchemaPropertyGrouped> jsonKeyMap
128         = groupedInstanceToJsonKeyToJsonSchemaMap.computeIfAbsent(grouped, (key) -> new LinkedHashMap<>());
129 
130     @SuppressWarnings("unchecked")
131     T retval = (T) jsonKeyMap.computeIfAbsent(jsonKeyName, (key) -> supplier.get());
132 
133     assert grouped.equals(new GroupedDefinition(retval.getInstance()));
134 
135     if (!isInline(instance.getDefinition())) {
136       // add to definition to JSON definition name to definition map
137       IJsonSchemaDefinable newSchema
138           = definitionNameToJsonSchemaMap.computeIfAbsent(retval.getDefinitionName(), (key) -> retval);
139 
140       assert newSchema.equals(retval) : "Duplicate JSON definition name: "
141           + retval.getDefinitionName();
142     }
143     return retval;
144   }
145 
146   private static class GroupedDefinition {
147     private final IModelDefinition definition;
148     private final String disciminatorProperty;
149     private final String disciminatorValue;
150 
151     public GroupedDefinition(@NonNull INamedModelInstanceGrouped instance) {
152       this.definition = instance.getDefinition();
153       this.disciminatorProperty = instance.getParentContainer().getJsonDiscriminatorProperty();
154       this.disciminatorValue = instance.getEffectiveDisciminatorValue();
155     }
156 
157     @Override
158     public int hashCode() {
159       return Objects.hash(definition, disciminatorProperty, disciminatorValue);
160     }
161 
162     @Override
163     public boolean equals(Object obj) {
164       if (this == obj) {
165         return true;
166       }
167       if (obj == null) {
168         return false;
169       }
170       if (getClass() != obj.getClass()) {
171         return false;
172       }
173       GroupedDefinition other = (GroupedDefinition) obj;
174       return Objects.equals(definition, other.definition)
175           && Objects.equals(disciminatorProperty, other.disciminatorProperty)
176           && Objects.equals(disciminatorValue, other.disciminatorValue);
177     }
178   }
179 
180   @Override
181   public IJsonSchemaDefinitionAssembly getAssemblyDefinition(
182       IAssemblyDefinition definition,
183       IEnhancedQName jsonKeyName) {
184     return addToCache(definition, jsonKeyName, () -> new JsonSchemaDefinitionAssembly(definition, jsonKeyName, this));
185   }
186 
187   @Override
188   public IJsonSchemaDefinitionField getFieldDefinition(IFieldDefinition definition, IEnhancedQName jsonKeyName) {
189     return addToCache(definition, jsonKeyName, () -> new JsonSchemaDefinitionField(definition, jsonKeyName, this));
190   }
191 
192   @Override
193   public IJsonSchemaDefinition getFlagDefinition(IFlagDefinition definition) {
194     return addToCache(definition, null, () -> new JsonSchemaDefinitionFlag(definition, this));
195   }
196 
197   @Override
198   public IJsonSchemaPropertyFlag getJsonSchemaPropertyFlag(IFlagInstance instance) {
199     return new JsonSchemaPropertyFlag(instance, this);
200   }
201 
202   @Override
203   public IJsonSchemaPropertyNamed getJsonSchemaPropertyModel(@NonNull IModelInstanceAbsolute instance) {
204     IJsonSchemaPropertyNamed retval;
205     if (instance instanceof IAssemblyInstanceAbsolute) {
206       retval = new JsonSchemaPropertyAssembly((IAssemblyInstanceAbsolute) instance, this);
207     } else if (instance instanceof IFieldInstanceAbsolute) {
208       retval = new JsonSchemaPropertyField((IFieldInstanceAbsolute) instance, this);
209     } else if (instance instanceof IChoiceGroupInstance) {
210       retval = new JsonSchemaPropertyChoiceGroup((IChoiceGroupInstance) instance, this);
211     } else {
212       throw new UnsupportedOperationException("Unsupported property type: " + instance.getClass());
213     }
214     return retval;
215   }
216 
217   @Override
218   public IJsonSchemaPropertyGrouped getJsonSchemaPropertyGrouped(INamedModelInstanceGrouped instance) {
219     return addToCache(instance, null, () -> newJsonSchemaPropertyGrouped(instance));
220   }
221 
222   private IJsonSchemaPropertyGrouped newJsonSchemaPropertyGrouped(INamedModelInstanceGrouped instance) {
223     IJsonSchemaPropertyGrouped retval;
224     if (instance instanceof IAssemblyInstanceGrouped) {
225       retval = new JsonSchemaPropertyGroupedAssembly((IAssemblyInstanceGrouped) instance, this);
226     } else if (instance instanceof IFieldInstanceGrouped) {
227       retval = new JsonSchemaPropertyGroupedField((IFieldInstanceGrouped) instance, this);
228     } else {
229       throw new UnsupportedOperationException("Unsupported property type: " + instance.getClass());
230     }
231     return retval;
232   }
233 
234   @Override
235   @NonNull
236   public IDataTypeJsonSchema getSchema(@NonNull IDataTypeAdapter<?> datatype) {
237     IDataTypeJsonSchema retval = dataTypeToSchemaMap.get(datatype);
238     if (retval == null) {
239       retval = new DataTypeJsonSchema(
240           getDatatypeManager().getTypeNameForDatatype(datatype),
241           datatype);
242       dataTypeToSchemaMap.put(datatype, retval);
243     }
244     return retval;
245   }
246 
247   @Override
248   public void generateDataTypeDefinitions(@NonNull ObjectNode definitionsNode) {
249     getDatatypeManager().generateDatatypeDefinitions(definitionsNode);
250   }
251 
252   @Override
253   public JsonNodeFactory getJsonNodeFactory() {
254     return jsonNodeFactory;
255   }
256 
257   @Override
258   @NonNull
259   public IDataTypeJsonSchema getDataTypeSchemaForDefinition(@NonNull IValuedDefinition definition) {
260     IDataTypeJsonSchema retval = definitionValueToDataTypeSchemaMap.get(definition);
261     if (retval == null) {
262       AllowedValueCollection allowedValuesCollection = getContextIndependentEnumeratedValues(definition);
263       List<IAllowedValue> allowedValues = allowedValuesCollection.getValues();
264 
265       IDataTypeAdapter<?> dataTypeAdapter = definition.getJavaTypeAdapter();
266 
267       // register data type use
268       retval = getSchema(dataTypeAdapter);
269       if (!allowedValues.isEmpty()) {
270         // create restriction
271         retval = new DataTypeRestrictionDefinitionJsonSchema(definition, allowedValuesCollection, this);
272       }
273       definitionValueToDataTypeSchemaMap.put(definition, retval);
274     }
275     return retval;
276   }
277 
278   @Override
279   @SuppressWarnings("PMD.UseObjectForClearerAPI")
280   public String generateJsonSchemaDefinitionName(
281       @NonNull IDefinition definition,
282       @Nullable String jsonKeyFlagName,
283       @Nullable String discriminatorProperty,
284       @Nullable String discriminatorValue,
285       @Nullable String suffix) {
286     StringBuilder builder = new StringBuilder();
287     if (jsonKeyFlagName != null) {
288       builder
289           .append(IGenerationState.toCamelCase(jsonKeyFlagName))
290           .append("JsonKey");
291     }
292 
293     if (discriminatorProperty != null || discriminatorValue != null) {
294       builder
295           .append(IGenerationState.toCamelCase(ObjectUtils.requireNonNull(discriminatorProperty)))
296           .append(IGenerationState.toCamelCase(ObjectUtils.requireNonNull(discriminatorValue)))
297           .append("Choice");
298     }
299 
300     if (suffix != null) {
301       builder.append(suffix);
302     }
303     return getTypeNameForDefinition(definition, builder.toString());
304   }
305 
306   /**
307    * Writes an object node to the JSON output.
308    *
309    * @param schemaNode
310    *          the object node to write
311    * @throws IOException
312    *           if an I/O error occurs during writing
313    */
314   public void writeObject(ObjectNode schemaNode) throws IOException {
315     getWriter().writeObject(schemaNode);
316   }
317 
318   /**
319    * Writes the start of a JSON object to the output.
320    *
321    * @throws IOException
322    *           if an I/O error occurs during writing
323    */
324   public void writeStartObject() throws IOException {
325     getWriter().writeStartObject();
326   }
327 
328   /**
329    * Writes the end of a JSON object to the output.
330    *
331    * @throws IOException
332    *           if an I/O error occurs during writing
333    */
334   public void writeEndObject() throws IOException {
335     getWriter().writeEndObject();
336   }
337 
338   /**
339    * Writes a field with a string value to the JSON output.
340    *
341    * @param fieldName
342    *          the name of the field to write
343    * @param value
344    *          the string value of the field
345    * @throws IOException
346    *           if an I/O error occurs during writing
347    */
348   public void writeField(String fieldName, String value) throws IOException {
349     getWriter().writeStringField(fieldName, value);
350 
351   }
352 
353   /**
354    * Writes a field with an object node value to the JSON output.
355    *
356    * @param fieldName
357    *          the name of the field to write
358    * @param obj
359    *          the object node value of the field
360    * @throws IOException
361    *           if an I/O error occurs during writing
362    */
363   public void writeField(String fieldName, ObjectNode obj) throws IOException {
364     JsonGenerator writer = getWriter(); // NOPMD not closable here
365 
366     writer.writeFieldName(fieldName);
367     writer.writeTree(obj);
368   }
369 
370   @Override
371   public void flushWriter() throws IOException {
372     getWriter().flush();
373   }
374 }