1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.schemagen;
7   
8   import static org.junit.jupiter.api.Assertions.assertEquals;
9   
10  import gov.nist.secauto.metaschema.core.configuration.DefaultConfiguration;
11  import gov.nist.secauto.metaschema.core.configuration.IConfiguration;
12  import gov.nist.secauto.metaschema.core.configuration.IMutableConfiguration;
13  import gov.nist.secauto.metaschema.core.model.IModule;
14  import gov.nist.secauto.metaschema.core.model.MetaschemaException;
15  import gov.nist.secauto.metaschema.core.model.constraint.IConstraintSet;
16  import gov.nist.secauto.metaschema.core.model.validation.JsonSchemaContentValidator;
17  import gov.nist.secauto.metaschema.core.model.validation.XmlSchemaContentValidator;
18  import gov.nist.secauto.metaschema.core.model.xml.ModuleLoader;
19  import gov.nist.secauto.metaschema.core.util.CollectionUtil;
20  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
21  import gov.nist.secauto.metaschema.databind.IBindingContext;
22  import gov.nist.secauto.metaschema.databind.io.Format;
23  import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingModuleLoader;
24  import gov.nist.secauto.metaschema.model.testing.AbstractTestSuite;
25  import gov.nist.secauto.metaschema.schemagen.json.JsonSchemaGenerator;
26  import gov.nist.secauto.metaschema.schemagen.xml.XmlSchemaGenerator;
27  
28  import org.junit.platform.commons.JUnitException;
29  import org.xml.sax.SAXException;
30  
31  import java.io.IOException;
32  import java.io.InputStream;
33  import java.io.Writer;
34  import java.net.URI;
35  import java.net.URL;
36  import java.nio.file.Files;
37  import java.nio.file.Path;
38  import java.nio.file.Paths;
39  import java.nio.file.StandardOpenOption;
40  import java.util.Collection;
41  import java.util.Collections;
42  import java.util.List;
43  import java.util.function.BiFunction;
44  import java.util.function.Function;
45  
46  import javax.xml.transform.Source;
47  import javax.xml.transform.stream.StreamSource;
48  
49  import edu.umd.cs.findbugs.annotations.NonNull;
50  
51  public abstract class AbstractSchemaGeneratorTestSuite
52      extends AbstractTestSuite {
53    @NonNull
54    protected static final ISchemaGenerator XML_SCHEMA_GENERATOR = new XmlSchemaGenerator();
55    @NonNull
56    protected static final ISchemaGenerator JSON_SCHEMA_GENERATOR = new JsonSchemaGenerator();
57    @NonNull
58    protected static final IConfiguration<SchemaGenerationFeature<?>> SCHEMA_GENERATION_CONFIG;
59    @NonNull
60    protected static final BiFunction<IModule, Writer, Void> XML_SCHEMA_PROVIDER;
61    @NonNull
62    protected static final BiFunction<IModule, Writer, Void> JSON_SCHEMA_PROVIDER;
63    @NonNull
64    protected static final JsonSchemaContentValidator JSON_SCHEMA_VALIDATOR;
65    @NonNull
66    protected static final Function<Path, JsonSchemaContentValidator> JSON_CONTENT_VALIDATOR_PROVIDER;
67    @NonNull
68    protected static final Function<Path, XmlSchemaContentValidator> XML_CONTENT_VALIDATOR_PROVIDER;
69  
70    private static final String UNIT_TEST_CONFIG
71        = "../core/metaschema/test-suite/schema-generation/unit-tests.xml";
72  
73    static {
74      IMutableConfiguration<SchemaGenerationFeature<?>> features = new DefaultConfiguration<>();
75      // features.enableFeature(SchemaGenerationFeature.INLINE_DEFINITIONS);
76      features.disableFeature(SchemaGenerationFeature.INLINE_DEFINITIONS);
77      SCHEMA_GENERATION_CONFIG = features;
78  
79      BiFunction<IModule, Writer, Void> xmlProvider = (module, writer) -> {
80        assert module != null;
81        assert writer != null;
82        try {
83          XML_SCHEMA_GENERATOR.generateFromModule(module, writer, SCHEMA_GENERATION_CONFIG);
84        } catch (SchemaGenerationException ex) {
85          throw new JUnitException("IO error", ex);
86        }
87        return null;
88      };
89      XML_SCHEMA_PROVIDER = xmlProvider;
90  
91      BiFunction<IModule, Writer, Void> jsonProvider = (module, writer) -> {
92        assert module != null;
93        assert writer != null;
94        try {
95          JSON_SCHEMA_GENERATOR.generateFromModule(module, writer, SCHEMA_GENERATION_CONFIG);
96        } catch (SchemaGenerationException ex) {
97          throw new JUnitException("IO error", ex);
98        }
99        return null;
100     };
101     JSON_SCHEMA_PROVIDER = jsonProvider;
102     // Module module = ModuleLayer.boot()
103     // .findModule("gov.nist.secauto.metaschema.core")
104     // .orElseThrow();
105     //
106     // try (InputStream is
107     // = module.getResourceAsStream("schema.json/json-schema.json")) {
108     try (InputStream is = ModuleLoader.class.getResourceAsStream("/schema/json/json-schema.json")) {
109       assert is != null : "unable to get JSON schema resource";
110       JsonSchemaContentValidator schemaValidator = new JsonSchemaContentValidator(is);
111       JSON_SCHEMA_VALIDATOR = schemaValidator;
112     } catch (IOException ex) {
113       throw new IllegalStateException(ex);
114     }
115 
116     @SuppressWarnings("null")
117     @NonNull
118     Function<Path, XmlSchemaContentValidator> xmlContentValidatorProvider = path -> {
119       try {
120         URL schemaResource = path.toUri().toURL();
121         @SuppressWarnings("resource")
122         StreamSource source
123             = new StreamSource(schemaResource.openStream(), schemaResource.toString());
124         List<? extends Source> schemaSources = Collections.singletonList(source);
125         return new XmlSchemaContentValidator(schemaSources);
126       } catch (IOException | SAXException ex) {
127         throw new IllegalStateException(ex);
128       }
129     };
130     XML_CONTENT_VALIDATOR_PROVIDER = xmlContentValidatorProvider;
131 
132     @NonNull
133     Function<Path, JsonSchemaContentValidator> jsonContentValidatorProvider = path -> {
134       try (InputStream is = Files.newInputStream(path, StandardOpenOption.READ)) {
135         assert is != null;
136         return new JsonSchemaContentValidator(is);
137       } catch (IOException ex) {
138         throw new JUnitException("Failed to create content validator for schema: " + path.toString(), ex);
139       }
140     };
141     JSON_CONTENT_VALIDATOR_PROVIDER = jsonContentValidatorProvider;
142   }
143 
144   @NonNull
145   protected static IBindingContext newBindingContext() throws IOException {
146     return newBindingContext(CollectionUtil.emptyList());
147   }
148 
149   @NonNull
150   protected static IBindingContext newBindingContext(@NonNull Collection<IConstraintSet> constraints)
151       throws IOException {
152     Path generationDir = Paths.get("target/generated-modules");
153     Files.createDirectories(generationDir);
154 
155     return IBindingContext.builder()
156         .compilePath(ObjectUtils.notNull(Files.createTempDirectory(generationDir, "modules-")))
157         .constraintSet(constraints)
158         .build();
159   }
160 
161   @Override
162   protected URI getTestSuiteURI() {
163     return ObjectUtils
164         .notNull(Paths.get(UNIT_TEST_CONFIG).toUri());
165   }
166 
167   @Override
168   protected Path getGenerationPath() {
169     return ObjectUtils.notNull(Paths.get("target/test-schemagen"));
170   }
171 
172   protected Path produceXmlSchema(@NonNull IModule module, @NonNull Path schemaPath) throws IOException {
173     generateSchema(module, schemaPath, XML_SCHEMA_PROVIDER);
174     return schemaPath;
175   }
176 
177   protected Path produceJsonSchema(@NonNull IModule module, @NonNull Path schemaPath)
178       throws IOException {
179     generateSchema(module, schemaPath, JSON_SCHEMA_PROVIDER);
180     return schemaPath;
181   }
182 
183   @SuppressWarnings("null")
184   protected void doTest(
185       @NonNull String collectionName,
186       @NonNull String metaschemaName,
187       @NonNull String generatedSchemaName,
188       @NonNull ContentCase... contentCases) throws IOException, MetaschemaException {
189     Path generationDir = getGenerationPath();
190 
191     Path testSuite = Paths.get("../core/metaschema/test-suite/schema-generation/");
192     Path collectionPath = testSuite.resolve(collectionName);
193 
194     IBindingContext bindingContext = newBindingContext();
195 
196     // load the metaschema module
197     IBindingModuleLoader loader = bindingContext.newModuleLoader();
198     loader.allowEntityResolution();
199     Path modulePath = collectionPath.resolve(metaschemaName);
200     IModule module = loader.load(modulePath);
201 
202     // generate the schema
203     Path schemaPath;
204     Format requiredContentFormat = getRequiredContentFormat();
205     switch (requiredContentFormat) {
206     case JSON:
207     case YAML:
208       Path jsonSchema = produceJsonSchema(module, generationDir.resolve(generatedSchemaName + ".json"));
209       assertEquals(true, validateWithSchema(JSON_SCHEMA_VALIDATOR, jsonSchema),
210           String.format("JSON schema '%s' was invalid", jsonSchema.toString()));
211       schemaPath = jsonSchema;
212       break;
213     case XML:
214       schemaPath = produceXmlSchema(module, generationDir.resolve(generatedSchemaName + ".xsd"));
215       break;
216     default:
217       throw new IllegalStateException();
218     }
219 
220     // create content test cases
221     for (ContentCase contentCase : contentCases) {
222       Path contentPath = collectionPath.resolve(contentCase.getName());
223 
224       if (!requiredContentFormat.equals(contentCase.getActualFormat())) {
225         contentPath = convertContent(contentPath.toUri(), generationDir, bindingContext);
226       }
227 
228       assertEquals(contentCase.isValid(),
229           validateWithSchema(getContentValidatorSupplier().apply(schemaPath), contentPath),
230           String.format("validation of '%s' did not match expectation", contentPath));
231     }
232   }
233 
234   @NonNull
235   protected ContentCase contentCase(
236       @NonNull Format actualFormat,
237       @NonNull String contentName,
238       boolean valid) {
239     return new ContentCase(contentName, actualFormat, valid);
240   }
241 
242   protected static class ContentCase {
243     @NonNull
244     private final String name;
245     @NonNull
246     private final Format actualFormat;
247     private final boolean valid;
248 
249     public ContentCase(@NonNull String name, @NonNull Format actualFormat, boolean valid) {
250       this.name = name;
251       this.actualFormat = actualFormat;
252       this.valid = valid;
253     }
254 
255     @NonNull
256     public String getName() {
257       return name;
258     }
259 
260     @NonNull
261     public Format getActualFormat() {
262       return actualFormat;
263     }
264 
265     public boolean isValid() {
266       return valid;
267     }
268   }
269 }