From 9cc93bcfae87d99768abfd31f51c43b1f704b1be Mon Sep 17 00:00:00 2001 From: Burak KALAYCI Date: Fri, 21 Aug 2026 18:17:16 +0300 Subject: [PATCH] Encode BPMN XSD URL so schema validation works on non-ASCII paths Fixes #4214 Xerces cannot resolve xsd:include/import when ClassLoader.getResource returns a URL whose path contains non-ASCII characters. Encode the schema URL with URI.toASCIIString() before SchemaFactory.newSchema. --- .../bpmn/converter/BpmnXMLConverter.java | 24 +++-- ...NonAsciiClasspathSchemaValidationTest.java | 102 ++++++++++++++++++ 2 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 modules/flowable-bpmn-converter/src/test/java/org/flowable/editor/language/xml/NonAsciiClasspathSchemaValidationTest.java diff --git a/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/BpmnXMLConverter.java b/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/BpmnXMLConverter.java index 1a62f38830b..ba1c80b8569 100644 --- a/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/BpmnXMLConverter.java +++ b/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/BpmnXMLConverter.java @@ -17,6 +17,10 @@ import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -248,19 +252,23 @@ public void validateModel(XMLStreamReader xmlStreamReader) throws Exception { protected Schema createSchema() throws SAXException { SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); - Schema schema = null; + URL xsdUrl = null; if (classloader != null) { - schema = factory.newSchema(classloader.getResource(BPMN_XSD)); + xsdUrl = classloader.getResource(BPMN_XSD); } - - if (schema == null) { - schema = factory.newSchema(BpmnXMLConverter.class.getClassLoader().getResource(BPMN_XSD)); + if (xsdUrl == null) { + xsdUrl = BpmnXMLConverter.class.getClassLoader().getResource(BPMN_XSD); } - - if (schema == null) { + if (xsdUrl == null) { throw new XMLException("BPMN XSD could not be found"); } - return schema; + try { + // Xerces cannot resolve xsd:include/import against a schema URL whose path contains + // non-ASCII characters unless the URL is ASCII-encoded. + return factory.newSchema(URI.create(xsdUrl.toURI().toASCIIString()).toURL()); + } catch (MalformedURLException | URISyntaxException e) { + return factory.newSchema(xsdUrl); + } } public BpmnModel convertToBpmnModel(InputStreamProvider inputStreamProvider, boolean validateSchema, boolean enableSafeBpmnXml) { diff --git a/modules/flowable-bpmn-converter/src/test/java/org/flowable/editor/language/xml/NonAsciiClasspathSchemaValidationTest.java b/modules/flowable-bpmn-converter/src/test/java/org/flowable/editor/language/xml/NonAsciiClasspathSchemaValidationTest.java new file mode 100644 index 00000000000..87335547954 --- /dev/null +++ b/modules/flowable-bpmn-converter/src/test/java/org/flowable/editor/language/xml/NonAsciiClasspathSchemaValidationTest.java @@ -0,0 +1,102 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.editor.language.xml; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; + +import org.flowable.bpmn.converter.BpmnXMLConverter; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.common.engine.impl.util.io.InputStreamSource; +import org.junit.jupiter.api.Test; + +class NonAsciiClasspathSchemaValidationTest { + + private static final String XSD_PACKAGE = "org/flowable/impl/bpmn/parser/"; + private static final List SCHEMA_FILES = List.of( + "BPMN20.xsd", "BPMNDI.xsd", "Semantic.xsd", "DC.xsd", "DI.xsd"); + + private static final String MINIMAL_BPMN = """ + + + + + + + """; + + @Test + void shouldValidateSchemaWhenXsdIsLoadedFromNonAsciiPath() throws Exception { + Path tempDir = Files.createTempDirectory("flowable-流程-"); + try { + Path parserDir = tempDir.resolve(XSD_PACKAGE); + Files.createDirectories(parserDir); + ClassLoader runtimeLoader = BpmnXMLConverter.class.getClassLoader(); + for (String name : SCHEMA_FILES) { + try (InputStream in = runtimeLoader.getResourceAsStream(XSD_PACKAGE + name)) { + assertThat(in).as(name).isNotNull(); + Files.copy(in, parserDir.resolve(name)); + } + } + + URL rawFileUrl = new URL("file:" + parserDir.resolve("BPMN20.xsd").toAbsolutePath()); + ClassLoader classLoader = new ClassLoader(runtimeLoader) { + @Override + public URL getResource(String name) { + if ((XSD_PACKAGE + "BPMN20.xsd").equals(name)) { + return rawFileUrl; + } + return super.getResource(name); + } + }; + + BpmnXMLConverter converter = new BpmnXMLConverter(); + converter.setClassloader(classLoader); + + BpmnModel model = converter.convertToBpmnModel( + new InputStreamSource(new ByteArrayInputStream(MINIMAL_BPMN.getBytes(StandardCharsets.UTF_8))), + true, false, "UTF-8"); + + assertThat(model).isNotNull(); + assertThat(model.getMainProcess().getId()).isEqualTo("process1"); + } finally { + deleteRecursively(tempDir); + } + } + + private static void deleteRecursively(Path root) throws IOException { + if (root == null || !Files.exists(root)) { + return; + } + try (var paths = Files.walk(root)) { + paths.sorted(Comparator.reverseOrder()).forEach(path -> { + try { + Files.deleteIfExists(path); + } catch (IOException ignored) { + // best-effort cleanup of the temp schema copy + } + }); + } + } +}