4040import org .xml .sax .SAXException ;
4141
4242import javax .xml .XMLConstants ;
43+ import javax .xml .stream .XMLInputFactory ;
44+ import javax .xml .stream .XMLStreamConstants ;
4345import javax .xml .stream .XMLStreamException ;
46+ import javax .xml .stream .XMLStreamReader ;
4447import javax .xml .transform .Source ;
4548import javax .xml .transform .stream .StreamSource ;
4649import javax .xml .validation .Schema ;
@@ -179,7 +182,7 @@ private static void validate(InputStream is, IidmVersion version, ExtensionsSupp
179182 }
180183
181184 private static Map <IidmVersion , Schema > createDefaultSchemas (ExtensionsSupplier extensionsSupplier ) {
182- Map <IidmVersion , Schema > schemasByIIdmVersion = new HashMap <>();
185+ Map <IidmVersion , Schema > schemasByIIdmVersion = new EnumMap <>(IidmVersion . class );
183186 for (IidmVersion version : IidmVersion .values ()) {
184187 Schema schema = createSchema (extensionsSupplier , version );
185188 schemasByIIdmVersion .put (version , schema );
@@ -196,35 +199,15 @@ private static Schema createSchema(ExtensionsSupplier extensionsSupplier, IidmVe
196199 factory .setProperty (XMLConstants .ACCESS_EXTERNAL_SCHEMA , "" );
197200 factory .setProperty (XMLConstants .ACCESS_EXTERNAL_DTD , "" );
198201 List <Source > sources = new ArrayList <>();
199- // iidm: validation
202+ // iidm: source
200203 sources .add (new StreamSource (NetworkSerDe .class .getResourceAsStream ("/xsd/" + version .getXsd ())));
201- // equipement: validation
204+ // equipment: source
202205 if (version .supportEquipmentValidationLevel ()) {
203206 sources .add (new StreamSource (NetworkSerDe .class .getResourceAsStream ("/xsd/" + version .getXsd (false ))));
204207 }
205- // extension: validation
206- /**
207- * TODO docs to complete
208- * In some xsd extension there is the mention of other iidm version,
209- * try to resolve those iidm version first then resolve extension xsd,
210- *
211- * Extension XSD look like (the extension v 1_0 needs iidm v 1_10)
212- * <xs:schema version="1.0"
213- * xmlns:xs="http://www.w3.org/2001/XMLSchema"
214- * xmlns:iidm="http://www.powsybl.org/schema/iidm/1_10"
215- * targetNamespace="http://www.powsybl.org/schema/iidm/ext/remote_reactive_power_control/1_0"
216- * elementFormDefault="qualified">
217- * <xs:import namespace="http://www.powsybl.org/schema/iidm/1_10" schemaLocation="iidm_V1_10.xsd"/>
218- * ...
219- */
220- List <Source > extensionsSchemas = new ArrayList <>();
221- for (ExtensionSerDe <?, ?> extension : getExtensionsByVersion (extensionsSupplier .get ().getProviders (), version )) {
222- byte [] extensionXsd = extension .getXsdAsStream ().readAllBytes ();
223- extractReferencedIidmVersions (extensionXsd )
224- .forEach (requiredIidmVersion -> sources .add (new StreamSource (NetworkSerDe .class .getResourceAsStream ("/xsd/" + requiredIidmVersion .getXsd ()))));
225- extensionsSchemas .add (new StreamSource (new ByteArrayInputStream (extensionXsd )));
226- }
227- sources .addAll (extensionsSchemas );
208+ // extension: source
209+ sources .addAll (getExtensionSources (extensionsSupplier , version ));
210+
228211 return factory .newSchema (sources .toArray (Source []::new ));
229212 } catch (SAXException e ) {
230213 throw new UncheckedSaxException (e );
@@ -233,30 +216,82 @@ private static Schema createSchema(ExtensionsSupplier extensionsSupplier, IidmVe
233216 }
234217 }
235218
236- private static List <ExtensionSerDe <?, ?>> getExtensionsByVersion (Collection <ExtensionSerDe > extensionList , IidmVersion version ) {
219+ /**
220+ * Build the list of XSD required to validate extensions for a given IIDM version.
221+ *
222+ * <p>Some extension XSDs import an IIDM schema through {@code xs:import/@schemaLocation}</p>
223+ * This method parses each supported extension XSD, extracts such schema locations,
224+ * and adds the corresponding IIDM XSD resources from {@code /xsd/...} to the returned sources.
225+ *
226+ * <p>Extension snippet:</p>
227+ * <pre>{@code
228+ * ...
229+ * targetNamespace="http://www.powsybl.org/schema/iidm/ext/extension-name/1_0"
230+ * xmlns:iidm="http://www.powsybl.org/schema/iidm/1_10">
231+ * <xs:import namespace="http://www.powsybl.org/schema/iidm/1_10" schemaLocation="iidm_V1_10.xsd"/>
232+ * </xs:schema>
233+ * }</pre>
234+ *
235+ * <p>Only extensions supported by the provided IIDM version are considered.</p>
236+ *
237+ * @param extensionsSupplier extension provider used to discover available extension serializers
238+ * @param version IIDM version used to filter compatible extensions
239+ * @return list of additional schema sources required by extension
240+ */
241+ private static List <Source > getExtensionSources (ExtensionsSupplier extensionsSupplier , IidmVersion version ) throws IOException {
242+ List <Source > sources = new ArrayList <>();
243+ for (ExtensionSerDe <?, ?> extension : getSupportedExtensionsByIIdmVersion (extensionsSupplier .get ().getProviders (), version )) {
244+ byte [] extensionXsd = extension .getXsdAsStream ().readAllBytes ();
245+ extractSchemaLocations (extensionXsd )
246+ .forEach (schemaLocation -> sources .add (new StreamSource (NetworkSerDe .class .getResourceAsStream ("/xsd/" + schemaLocation ))));
247+ sources .add (new StreamSource (new ByteArrayInputStream (extensionXsd )));
248+ }
249+ return sources ;
250+ }
251+
252+ private static List <ExtensionSerDe <?, ?>> getSupportedExtensionsByIIdmVersion (Collection <ExtensionSerDe > extensionSerDes , IidmVersion version ) {
237253 List <ExtensionSerDe <?, ?>> extensions = new ArrayList <>();
238- for (ExtensionSerDe <?, ?> extension : extensionList ) {
239- if (extension instanceof AbstractVersionableNetworkExtensionSerDe <?, ?, ?> ab ) {
254+ for (ExtensionSerDe <?, ?> extensionSerDe : extensionSerDes ) {
255+ if (extensionSerDe instanceof AbstractVersionableNetworkExtensionSerDe <?, ?, ?> ab ) {
240256 if (ab .versionExists (version )) {
241- extensions .add (extension );
257+ extensions .add (extensionSerDe );
242258 }
243259 } else {
244- // keep non versionable extensions
245- extensions .add (extension );
260+ // no versionable extensions
261+ extensions .add (extensionSerDe );
246262 }
247263 }
248264 return extensions ;
249265 }
250266
251- private static Set <IidmVersion > extractReferencedIidmVersions (byte [] xsdBytes ) {
252- String xsdContent = new String (xsdBytes , StandardCharsets .UTF_8 );
253- Set <IidmVersion > versions = new HashSet <>();
254- for (IidmVersion v : IidmVersion .values ()) {
255- if (xsdContent .contains (v .getXsd ()) || v .supportEquipmentValidationLevel () && xsdContent .contains (v .getXsd (false ))) {
256- versions .add (v );
267+ /**
268+ * Extract {@code xs:import/@schemaLocation} in an XSD document
269+ * @param xsdBytes XSD content as bytes
270+ * @return schema locations found in {@code xs:import}
271+ */
272+ private static List <String > extractSchemaLocations (byte [] xsdBytes ) {
273+ List <String > locations = new ArrayList <>();
274+ XMLInputFactory xif = XMLInputFactory .newFactory ();
275+ xif .setProperty (XMLInputFactory .SUPPORT_DTD , false );
276+ xif .setProperty (XMLInputFactory .IS_SUPPORTING_EXTERNAL_ENTITIES , false );
277+ try {
278+ XMLStreamReader reader = xif .createXMLStreamReader (new ByteArrayInputStream (xsdBytes ));
279+ while (reader .hasNext ()) {
280+ int event = reader .next ();
281+ if (event == XMLStreamConstants .START_ELEMENT
282+ && XMLConstants .W3C_XML_SCHEMA_NS_URI .equals (reader .getNamespaceURI ())
283+ && ("import" .equals (reader .getLocalName ()))) {
284+ String schemaLocation = reader .getAttributeValue (null , "schemaLocation" );
285+ if (schemaLocation != null && !schemaLocation .isBlank ()) {
286+ locations .add (schemaLocation );
287+ }
288+ }
257289 }
290+ reader .close ();
291+ } catch (XMLStreamException e ) {
292+ throw new RuntimeException (e );
258293 }
259- return versions ;
294+ return locations ;
260295 }
261296
262297 private static void throwExceptionIfOption (AbstractOptions <?> options , String message ) {
0 commit comments