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