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.validation.JsonSchemaContentValidator;
16  import gov.nist.secauto.metaschema.core.model.validation.XmlSchemaContentValidator;
17  import gov.nist.secauto.metaschema.core.model.xml.ModuleLoader;
18  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
19  import gov.nist.secauto.metaschema.databind.DefaultBindingContext;
20  import gov.nist.secauto.metaschema.databind.IBindingContext;
21  import gov.nist.secauto.metaschema.databind.io.DeserializationFeature;
22  import gov.nist.secauto.metaschema.databind.io.Format;
23  import gov.nist.secauto.metaschema.databind.model.metaschema.BindingModuleLoader;
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.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 Function<Path, XmlSchemaContentValidator> xmlContentValidatorProvider = path -> {
117       try {
118         URL schemaResource = path.toUri().toURL();
119         @SuppressWarnings("resource") StreamSource source
120             = new StreamSource(schemaResource.openStream(), schemaResource.toString());
121         List<? extends Source> schemaSources = Collections.singletonList(source);
122         return new XmlSchemaContentValidator(schemaSources);
123       } catch (IOException | SAXException ex) {
124         throw new IllegalStateException(ex);
125       }
126     };
127     XML_CONTENT_VALIDATOR_PROVIDER = xmlContentValidatorProvider;
128 
129     @NonNull Function<Path, JsonSchemaContentValidator> jsonContentValidatorProvider = path -> {
130       try (InputStream is = Files.newInputStream(path, StandardOpenOption.READ)) {
131         assert is != null;
132         return new JsonSchemaContentValidator(is);
133       } catch (IOException ex) {
134         throw new JUnitException("Failed to create content validator for schema: " + path.toString(), ex);
135       }
136     };
137     JSON_CONTENT_VALIDATOR_PROVIDER = jsonContentValidatorProvider;
138   }
139 
140   @Override
141   protected URI getTestSuiteURI() {
142     return ObjectUtils
143         .notNull(Paths.get(UNIT_TEST_CONFIG).toUri());
144   }
145 
146   @Override
147   protected Path getGenerationPath() {
148     return ObjectUtils.notNull(Paths.get("target/test-schemagen"));
149   }
150 
151   protected Path produceXmlSchema(@NonNull IModule module, @NonNull Path schemaPath) throws IOException {
152     generateSchema(module, schemaPath, XML_SCHEMA_PROVIDER);
153     return schemaPath;
154   }
155 
156   protected Path produceJsonSchema(@NonNull IModule module, @NonNull Path schemaPath)
157       throws IOException {
158     generateSchema(module, schemaPath, JSON_SCHEMA_PROVIDER);
159     return schemaPath;
160   }
161 
162   @SuppressWarnings("null")
163   protected void doTest(
164       @NonNull String collectionName,
165       @NonNull String metaschemaName,
166       @NonNull String generatedSchemaName,
167       @NonNull ContentCase... contentCases) throws IOException, MetaschemaException {
168     Path generationDir = getGenerationPath();
169 
170     Path testSuite = Paths.get("../core/metaschema/test-suite/schema-generation/");
171     Path collectionPath = testSuite.resolve(collectionName);
172 
173     IBindingContext context = new DefaultBindingContext();
174 
175     // load the metaschema module
176     BindingModuleLoader loader = new BindingModuleLoader(context);
177     loader.enableFeature(DeserializationFeature.DESERIALIZE_XML_ALLOW_ENTITY_RESOLUTION);
178     loader.allowEntityResolution();
179     Path modulePath = collectionPath.resolve(metaschemaName);
180     IModule module = loader.load(modulePath);
181 
182     // generate the schema
183     Path schemaPath;
184     Format requiredContentFormat = getRequiredContentFormat();
185     switch (requiredContentFormat) {
186     case JSON:
187     case YAML:
188       Path jsonSchema = produceJsonSchema(module, generationDir.resolve(generatedSchemaName + ".json"));
189       assertEquals(true, validateWithSchema(JSON_SCHEMA_VALIDATOR, jsonSchema),
190           String.format("JSON schema '%s' was invalid", jsonSchema.toString()));
191       schemaPath = jsonSchema;
192       break;
193     case XML:
194       schemaPath = produceXmlSchema(module, generationDir.resolve(generatedSchemaName + ".xsd"));
195       break;
196     default:
197       throw new IllegalStateException();
198     }
199 
200     // setup object binding
201     context.registerModule(module, generationDir);
202 
203     // create content test cases
204     for (ContentCase contentCase : contentCases) {
205       Path contentPath = collectionPath.resolve(contentCase.getName());
206 
207       if (!requiredContentFormat.equals(contentCase.getActualFormat())) {
208         contentPath = convertContent(contentPath.toUri(), generationDir, context);
209       }
210 
211       assertEquals(contentCase.isValid(),
212           validateWithSchema(getContentValidatorSupplier().apply(schemaPath), contentPath),
213           String.format("validation of '%s' did not match expectation", contentPath));
214     }
215   }
216 
217   @NonNull
218   protected ContentCase contentCase(
219       @NonNull Format actualFormat,
220       @NonNull String contentName,
221       boolean valid) {
222     return new ContentCase(contentName, actualFormat, valid);
223   }
224 
225   protected static class ContentCase {
226     @NonNull
227     private final String name;
228     @NonNull
229     private final Format actualFormat;
230     private final boolean valid;
231 
232     public ContentCase(@NonNull String name, @NonNull Format actualFormat, boolean valid) {
233       this.name = name;
234       this.actualFormat = actualFormat;
235       this.valid = valid;
236     }
237 
238     @NonNull
239     public String getName() {
240       return name;
241     }
242 
243     @NonNull
244     public Format getActualFormat() {
245       return actualFormat;
246     }
247 
248     public boolean isValid() {
249       return valid;
250     }
251   }
252 }