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.databind.node.ArrayNode;
9   import com.fasterxml.jackson.databind.node.JsonNodeFactory;
10  import com.fasterxml.jackson.databind.node.ObjectNode;
11  
12  import gov.nist.secauto.metaschema.core.model.IAssemblyDefinition;
13  import gov.nist.secauto.metaschema.core.model.IChoiceGroupInstance;
14  import gov.nist.secauto.metaschema.core.model.IChoiceInstance;
15  import gov.nist.secauto.metaschema.core.model.INamedModelInstanceAbsolute;
16  import gov.nist.secauto.metaschema.core.model.ModelType;
17  import gov.nist.secauto.metaschema.core.util.CollectionUtil;
18  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
19  import gov.nist.secauto.metaschema.schemagen.json.IDefinitionJsonSchema;
20  import gov.nist.secauto.metaschema.schemagen.json.IJsonGenerationState;
21  import gov.nist.secauto.metaschema.schemagen.json.impl.IJsonProperty.PropertyCollection;
22  
23  import java.io.IOException;
24  import java.util.Collection;
25  import java.util.LinkedList;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.function.Function;
29  import java.util.stream.Collectors;
30  import java.util.stream.Stream;
31  
32  import edu.umd.cs.findbugs.annotations.NonNull;
33  import edu.umd.cs.findbugs.annotations.Nullable;
34  import nl.talsmasoftware.lazy4j.Lazy;
35  
36  public class AssemblyDefinitionJsonSchema
37      extends AbstractModelDefinitionJsonSchema<IAssemblyDefinition> {
38  
39    private final Lazy<List<IGroupableModelInstanceJsonProperty<?>>> groupableModelInstances;
40  
41    private final Map<INamedModelInstanceAbsolute, IGroupableModelInstanceJsonProperty<?>> choiceInstances;
42  
43    public AssemblyDefinitionJsonSchema(
44        @NonNull IAssemblyDefinition definition,
45        @Nullable String jsonKeyFlagName,
46        @Nullable String discriminatorProperty,
47        @Nullable String discriminatorValue,
48        @NonNull IJsonGenerationState state) {
49      super(definition, jsonKeyFlagName, discriminatorProperty, discriminatorValue);
50      this.groupableModelInstances = Lazy.lazy(() -> ObjectUtils.notNull(definition.getModelInstances().stream()
51          .filter(instance -> !(instance instanceof IChoiceInstance))
52          .map(instance -> {
53            IGroupableModelInstanceJsonProperty<?> property;
54            if (instance instanceof INamedModelInstanceAbsolute) {
55              INamedModelInstanceAbsolute named = (INamedModelInstanceAbsolute) instance;
56              property = new NamedModelInstanceJsonProperty(named, state);
57            } else if (instance instanceof IChoiceGroupInstance) {
58              IChoiceGroupInstance choice = (IChoiceGroupInstance) instance;
59              property = new ChoiceGroupInstanceJsonProperty(choice, state);
60            } else {
61              throw new UnsupportedOperationException(
62                  "model instance class not supported: " + instance.getClass().getName());
63            }
64            return property;
65          })
66          .collect(Collectors.toUnmodifiableList())));
67  
68      this.choiceInstances = definition.getChoiceInstances().stream()
69          .flatMap(choice -> explodeChoice(ObjectUtils.requireNonNull(choice)))
70          .collect(Collectors.toUnmodifiableMap(
71              Function.identity(),
72              instance -> new NamedModelInstanceJsonProperty(ObjectUtils.requireNonNull(instance), state)));
73    }
74  
75    private static Stream<? extends INamedModelInstanceAbsolute> explodeChoice(@NonNull IChoiceInstance choice) {
76      return choice.getNamedModelInstances().stream();
77    }
78  
79    @NonNull
80    protected List<IGroupableModelInstanceJsonProperty<?>> getGroupableModelInstances() {
81      return groupableModelInstances.get();
82    }
83  
84    @Override
85    public void gatherDefinitions(
86        @NonNull Map<IKey, IDefinitionJsonSchema<?>> gatheredDefinitions,
87        @NonNull IJsonGenerationState state) {
88      // avoid recursion
89      if (!gatheredDefinitions.containsKey(getKey())) {
90        super.gatherDefinitions(gatheredDefinitions, state);
91  
92        // if (isInline(state)) {
93        for (IGroupableModelInstanceJsonProperty<?> property : getGroupableModelInstances()) {
94          property.gatherDefinitions(gatheredDefinitions, state);
95        }
96  
97        // handle choices
98        this.choiceInstances.values().forEach(property -> {
99          property.gatherDefinitions(gatheredDefinitions, state);
100       });
101     }
102   }
103 
104   @Override
105   protected void generateBody(
106       IJsonGenerationState state,
107       ObjectNode obj) throws IOException {
108     obj.put("type", "object");
109 
110     PropertyCollection properties = new PropertyCollection();
111 
112     // handle possible discriminator
113     String discriminatorProperty = getDiscriminatorProperty();
114     if (discriminatorProperty != null) {
115       ObjectNode discriminatorObj = state.getJsonNodeFactory().objectNode();
116       discriminatorObj.put("const", getDiscriminatorValue());
117       properties.addProperty(discriminatorProperty, discriminatorObj);
118     }
119 
120     // generate flag properties
121     for (FlagInstanceJsonProperty flag : getFlagProperties()) {
122       assert flag != null;
123       flag.generateProperty(properties, state);
124     }
125 
126     // generate model properties
127     for (IGroupableModelInstanceJsonProperty<?> property : getGroupableModelInstances()) {
128       assert property != null;
129       property.generateProperty(properties, state);
130     }
131 
132     Collection<? extends IChoiceInstance> choices = getDefinition().getChoiceInstances();
133     if (choices.isEmpty()) {
134       properties.generate(obj);
135       obj.put("additionalProperties", false);
136     } else {
137       List<PropertyCollection> propertyChoices = CollectionUtil.singletonList(properties);
138       propertyChoices = explodeChoices(choices, propertyChoices, state);
139 
140       if (propertyChoices.size() == 1) {
141         propertyChoices.iterator().next().generate(obj);
142       } else if (propertyChoices.size() > 1) {
143         generateChoices(propertyChoices, obj, state);
144       }
145     }
146   }
147 
148   protected void generateChoices(
149       List<PropertyCollection> propertyChoices,
150       @NonNull ObjectNode definitionNode,
151       @NonNull IJsonGenerationState state) {
152     ArrayNode anyOfdNode = ObjectUtils.notNull(JsonNodeFactory.instance.arrayNode());
153     for (PropertyCollection propertyChoice : propertyChoices) {
154       ObjectNode choiceDefinitionNode = ObjectUtils.notNull(JsonNodeFactory.instance.objectNode());
155       propertyChoice.generate(choiceDefinitionNode);
156       choiceDefinitionNode.put("additionalProperties", false);
157       anyOfdNode.add(choiceDefinitionNode);
158     }
159     definitionNode.set("anyOf", anyOfdNode);
160   }
161 
162   protected List<PropertyCollection> explodeChoices(
163       @NonNull Collection<? extends IChoiceInstance> choices,
164       @NonNull List<PropertyCollection> propertyChoices,
165       @NonNull IJsonGenerationState state) throws IOException {
166 
167     List<PropertyCollection> retval = propertyChoices;
168 
169     for (IChoiceInstance choice : choices) {
170       List<PropertyCollection> newRetval = new LinkedList<>(); // NOPMD - intentional
171       for (INamedModelInstanceAbsolute optionInstance : choice.getNamedModelInstances()) {
172         if (ModelType.CHOICE.equals(optionInstance.getModelType())) {
173           // recurse
174           newRetval.addAll(explodeChoices(
175               CollectionUtil.singleton((IChoiceInstance) optionInstance),
176               retval,
177               state));
178         } else {
179           // iterate over the old array of choices and append new choice
180           for (PropertyCollection oldInstanceProperties : retval) {
181             @SuppressWarnings("null")
182             @NonNull PropertyCollection newInstanceProperties = oldInstanceProperties.copy();
183 
184             // add the choice
185             choiceInstances.get(optionInstance)
186                 .generateProperty(newInstanceProperties, state);
187 
188             newRetval.add(newInstanceProperties);
189           }
190         }
191       }
192       retval = newRetval;
193     }
194     return retval;
195   }
196 }