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