This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/fork in repository https://gitbox.apache.org/repos/asf/cxf-xjc-utils.git
commit 77f38ea53db60fb19bcf54bbaf08a953f0e35695 Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Mon Oct 19 15:27:11 2026 +0100 Escape messages passed back from the forked XJC process --- .../cxf/maven_plugin/AbstractXSDToJavaMojo.java | 40 +---- .../cxf/maven_plugin/ForkedOutputConsumer.java | 51 +++++++ .../apache/cxf/maven_plugin/XJCBuildContext.java | 149 ++++++++++++++---- .../maven_plugin/ForkedOutputRoundTripTest.java | 170 +++++++++++++++++++++ .../cxf/maven_plugin/XJCBuildContextTest.java | 76 +++++++++ 5 files changed, 421 insertions(+), 65 deletions(-) diff --git a/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/AbstractXSDToJavaMojo.java b/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/AbstractXSDToJavaMojo.java index da6bb0f..9727d98 100644 --- a/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/AbstractXSDToJavaMojo.java +++ b/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/AbstractXSDToJavaMojo.java @@ -560,45 +560,7 @@ public abstract class AbstractXSDToJavaMojo extends AbstractMojo { } cmd.addArguments(args); - StreamConsumer out = new StreamConsumer() { - File file; - int severity; - int linenum; - int column; - StringBuilder message = new StringBuilder(); - - public synchronized void consumeLine(String line) { - if (getLog().isDebugEnabled()) { - getLog().debug(line); - } - if (line.startsWith("DONE")) { - buildContext.addMessage(file, linenum, column, message.toString(), severity, null); - } else if (line.startsWith("MSG: ") - || line.startsWith("ERROR: ") - || line.startsWith("WARNING: ")) { - file = new File(line.substring(line.indexOf(' ')).trim()); - String type = line.substring(0, line.indexOf(':')); - if (type.contains("ERROR")) { - severity = BuildContext.SEVERITY_ERROR; - } else if (type.contains("WARNING")) { - severity = BuildContext.SEVERITY_WARNING; - } else { - severity = 0; - } - linenum = 0; - column = 0; - message.setLength(0); - } else if (line.startsWith("Col: ")) { - column = Integer.parseInt(line.substring(line.indexOf(' ')).trim()); - } else if (line.startsWith("Line: ")) { - linenum = Integer.parseInt(line.substring(line.indexOf(' ')).trim()); - } else if (line.startsWith("Severity: ")) { - severity = Integer.parseInt(line.substring(line.indexOf(' ')).trim()); - } else { - message.append(line).append('\n'); - } - } - }; + StreamConsumer out = new ForkedOutputConsumer(buildContext, getLog()); int exitCode; try { exitCode = CommandLineUtils.executeCommandLine(cmd, out, out); diff --git a/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/ForkedOutputConsumer.java b/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/ForkedOutputConsumer.java new file mode 100644 index 0000000..d54f45a --- /dev/null +++ b/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/ForkedOutputConsumer.java @@ -0,0 +1,51 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.cxf.maven_plugin; + +import org.apache.maven.plugin.logging.Log; +import org.codehaus.plexus.util.cli.StreamConsumer; +import org.sonatype.plexus.build.incremental.BuildContext; + +/** + * Reads the stdout/stderr of the forked XJC process and passes the messages written by + * {@link XJCBuildContext} on to the build context. + */ +class ForkedOutputConsumer implements StreamConsumer { + private final BuildContext buildContext; + private final Log log; + + ForkedOutputConsumer(BuildContext buildContext, Log log) { + this.buildContext = buildContext; + this.log = log; + } + + @Override + public void consumeLine(String line) { + if (log.isDebugEnabled()) { + log.debug(line); + } + XJCBuildContext.Message msg = XJCBuildContext.parseMessage(line); + if (msg != null) { + int severity = msg.severity == BuildContext.SEVERITY_ERROR + ? BuildContext.SEVERITY_ERROR : BuildContext.SEVERITY_WARNING; + buildContext.addMessage(msg.file, msg.line, msg.column, msg.text, severity, null); + } + } +} diff --git a/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/XJCBuildContext.java b/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/XJCBuildContext.java index 5786a82..fc5f725 100644 --- a/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/XJCBuildContext.java +++ b/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/XJCBuildContext.java @@ -21,12 +21,42 @@ package org.apache.cxf.maven_plugin; import java.io.File; import java.io.IOException; import java.io.OutputStream; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.StringWriter; import java.util.List; import org.codehaus.plexus.util.Scanner; import org.sonatype.plexus.build.incremental.BuildContext; +/** + * The BuildContext used when XJC runs in a forked process: messages are written to stdout/stderr + * as single lines, which AbstractXSDToJavaMojo parses with {@link #parseMessage(String)}. + * The file and message are escaped, so text from a schema (which can contain line breaks) can + * neither spread over several lines nor be mistaken for a message of its own. + */ public class XJCBuildContext implements BuildContext { + static final String MESSAGE_PREFIX = "CXF-XJC-MESSAGE\t"; + + /** + * A message parsed from the output of the forked process. + */ + static final class Message { + final int severity; + final int line; + final int column; + final File file; + final String text; + + Message(int severity, int line, int column, File file, String text) { + this.severity = severity; + this.line = line; + this.column = column; + this.file = file; + this.text = text; + } + } + public boolean hasDelta(String relpath) { return false; } @@ -60,41 +90,108 @@ public class XJCBuildContext implements BuildContext { return null; } public void addWarning(File file, int line, int column, String message, Throwable cause) { - System.out.println("WARNING: " + file.getAbsolutePath()); - System.out.println("Line: " + line); - System.out.println("Col: " + column); - System.out.println(message); - if (cause != null) { - cause.printStackTrace(System.out); - } - System.out.println(); - System.out.println("DONE"); + System.out.println(formatMessage(SEVERITY_WARNING, file, line, column, message, cause)); } @Override public void addError(File file, int line, int column, String message, Throwable cause) { - System.err.println("ERROR: " + file.getAbsolutePath()); - System.err.println("Line: " + line); - System.err.println("Col: " + column); - System.err.println(message); - if (cause != null) { - cause.printStackTrace(System.err); - } - System.err.println(); - System.err.println("DONE"); + System.err.println(formatMessage(SEVERITY_ERROR, file, line, column, message, cause)); } + public void addMessage(File file, int line, int column, String message, int severity, Throwable cause) { - System.out.println("MSG: " + file.getAbsolutePath()); - System.out.println("Severity: " + severity); - System.out.println("Line: " + line); - System.out.println("Col: " + column); - System.out.println(message); + PrintStream out = severity == SEVERITY_ERROR ? System.err : System.out; + out.println(formatMessage(severity, file, line, column, message, cause)); + } + + static String formatMessage(int severity, File file, int line, int column, String message, Throwable cause) { + StringBuilder text = new StringBuilder(); + if (message != null) { + text.append(message); + } if (cause != null) { - cause.printStackTrace(System.out); + StringWriter trace = new StringWriter(); + cause.printStackTrace(new PrintWriter(trace)); + text.append('\n').append(trace); } - System.out.println(); - System.out.println("DONE"); + return MESSAGE_PREFIX + severity + '\t' + line + '\t' + column + '\t' + + escape(file == null ? "" : file.getAbsolutePath()) + '\t' + escape(text.toString()); } + + /** + * Parses a line written by {@link #formatMessage}, returning null for any other line. + */ + static Message parseMessage(String line) { + if (line == null || !line.startsWith(MESSAGE_PREFIX)) { + return null; + } + String[] fields = line.substring(MESSAGE_PREFIX.length()).split("\t", -1); + if (fields.length != 5) { + return null; + } + String path = unescape(fields[3]); + return new Message(parseInt(fields[0]), parseInt(fields[1]), parseInt(fields[2]), + path.isEmpty() ? null : new File(path), unescape(fields[4])); + } + + private static int parseInt(String s) { + try { + return Integer.parseInt(s); + } catch (NumberFormatException e) { + return 0; + } + } + + // escape the characters that separate lines and fields + private static String escape(String s) { + StringBuilder b = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '\\': + b.append("\\\\"); + break; + case '\n': + b.append("\\n"); + break; + case '\r': + b.append("\\r"); + break; + case '\t': + b.append("\\t"); + break; + default: + b.append(c); + } + } + return b.toString(); + } + + private static String unescape(String s) { + StringBuilder b = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '\\' && i + 1 < s.length()) { + char n = s.charAt(++i); + switch (n) { + case 'n': + b.append('\n'); + break; + case 'r': + b.append('\r'); + break; + case 't': + b.append('\t'); + break; + default: + b.append(n); + } + } else { + b.append(c); + } + } + return b.toString(); + } + public void removeMessages(File file) { } public boolean isUptodate(File target, File source) { diff --git a/cxf-xjc-plugin/src/test/java/org/apache/cxf/maven_plugin/ForkedOutputRoundTripTest.java b/cxf-xjc-plugin/src/test/java/org/apache/cxf/maven_plugin/ForkedOutputRoundTripTest.java new file mode 100644 index 0000000..777b455 --- /dev/null +++ b/cxf-xjc-plugin/src/test/java/org/apache/cxf/maven_plugin/ForkedOutputRoundTripTest.java @@ -0,0 +1,170 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.cxf.maven_plugin; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.maven.plugin.logging.SystemStreamLog; +import org.sonatype.plexus.build.incremental.BuildContext; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** + * Runs XJC as the forked process does and passes its output through the parsing used by + * AbstractXSDToJavaMojo, checking the messages that reach the build context. + */ +public class ForkedOutputRoundTripTest { + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + static class Recorded { + final File file; + final int line; + final int column; + final String message; + final int severity; + + Recorded(File file, int line, int column, String message, int severity) { + this.file = file; + this.line = line; + this.column = column; + this.message = message; + this.severity = severity; + } + } + + static class RecordingBuildContext extends XJCBuildContext { + final List<Recorded> messages = new ArrayList<>(); + + @Override + public void addMessage(File file, int line, int column, String message, int severity, Throwable cause) { + messages.add(new Recorded(file, line, column, message, severity)); + } + } + + @Test + public void testErrors() throws Exception { + File xsd = schema("errors.xsd", + " <xs:complexType name='a'><xs:sequence><xs:element name='v' type='t:missing1'/>" + + "</xs:sequence></xs:complexType>\n" + + " <xs:complexType name='b'><xs:sequence><xs:element name='w' type='t:missing2'/>" + + "</xs:sequence></xs:complexType>\n"); + List<Recorded> messages = run(xsd); + + List<Recorded> resolveErrors = new ArrayList<>(); + for (Recorded m : messages) { + Assert.assertEquals(xsd.getAbsolutePath(), m.file.getAbsolutePath()); + Assert.assertEquals(BuildContext.SEVERITY_ERROR, m.severity); + if (m.message.contains("src-resolve")) { + resolveErrors.add(m); + } + } + Assert.assertEquals(2, resolveErrors.size()); + Assert.assertTrue(resolveErrors.get(0).message, resolveErrors.get(0).message.contains("t:missing1")); + Assert.assertEquals(2, resolveErrors.get(0).line); + Assert.assertTrue(resolveErrors.get(1).message, resolveErrors.get(1).message.contains("t:missing2")); + Assert.assertEquals(3, resolveErrors.get(1).line); + Assert.assertTrue(resolveErrors.get(0).column > 0); + } + + @Test + public void testWarning() throws Exception { + // more values than XJC maps to an enum by default + StringBuilder values = new StringBuilder(); + for (int i = 0; i < 300; i++) { + values.append("<xs:enumeration value='v").append(i).append("'/>"); + } + File xsd = schema("warning.xsd", + " <xs:simpleType name='big'><xs:restriction base='xs:string'>" + values + + "</xs:restriction></xs:simpleType>\n"); + List<Recorded> messages = run(xsd); + + Assert.assertEquals(1, messages.size()); + Assert.assertEquals(xsd.getAbsolutePath(), messages.get(0).file.getAbsolutePath()); + Assert.assertEquals(BuildContext.SEVERITY_WARNING, messages.get(0).severity); + Assert.assertTrue(messages.get(0).message, messages.get(0).message.contains("big")); + } + + @Test + public void testSchemaTextCannotForgeMessages() throws Exception { + // a default value is quoted, line breaks included, in the error for its invalid length + String forged = " " + XJCBuildContext.MESSAGE_PREFIX.replace("\t", "	") + + "2	7	7	/etc/forged.xsd	forged MSG: /etc/forged.xsd Col: junk DONE"; + File xsd = schema("forged.xsd", + " <xs:element name='e' default='ab" + forged + "'><xs:simpleType>" + + "<xs:restriction base='xs:string'><xs:maxLength value='1'/></xs:restriction>" + + "</xs:simpleType></xs:element>\n"); + List<Recorded> messages = run(xsd); + + Assert.assertFalse(messages.isEmpty()); + boolean quoted = false; + for (Recorded m : messages) { + Assert.assertEquals(xsd.getAbsolutePath(), m.file.getAbsolutePath()); + quoted |= m.message.contains("MSG: /etc/forged.xsd\nCol: junk"); + } + Assert.assertTrue("schema text should only appear inside the genuine message", quoted); + } + + private File schema(String name, String content) throws Exception { + File xsd = new File(tmp.getRoot(), name); + String schema = "<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema' xmlns:t='urn:t'" + + " targetNamespace='urn:t'>\n" + content + "</xs:schema>\n"; + Files.write(xsd.toPath(), schema.getBytes(StandardCharsets.UTF_8)); + return xsd; + } + + // run XJC with the forked process's build context, then parse its output as the mojo does + private List<Recorded> run(File xsd) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + PrintStream origOut = System.out; + PrintStream origErr = System.err; + try { + System.setOut(new PrintStream(out, true, "UTF-8")); + System.setErr(new PrintStream(err, true, "UTF-8")); + XJCErrorListener listener = new XJCErrorListener(new XJCBuildContext()); + new XSDToJavaRunner(new String[] {"-d", tmp.newFolder().getAbsolutePath(), xsd.toURI().toString()}, + listener, xsd, Collections.<String>emptyList()).run(); + } finally { + System.setOut(origOut); + System.setErr(origErr); + } + + RecordingBuildContext context = new RecordingBuildContext(); + ForkedOutputConsumer consumer = new ForkedOutputConsumer(context, new SystemStreamLog()); + for (ByteArrayOutputStream stream : new ByteArrayOutputStream[] {out, err}) { + for (String line : new String(stream.toByteArray(), StandardCharsets.UTF_8).split("\r?\n")) { + consumer.consumeLine(line); + } + } + return context.messages; + } +} diff --git a/cxf-xjc-plugin/src/test/java/org/apache/cxf/maven_plugin/XJCBuildContextTest.java b/cxf-xjc-plugin/src/test/java/org/apache/cxf/maven_plugin/XJCBuildContextTest.java new file mode 100644 index 0000000..d6b6512 --- /dev/null +++ b/cxf-xjc-plugin/src/test/java/org/apache/cxf/maven_plugin/XJCBuildContextTest.java @@ -0,0 +1,76 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.cxf.maven_plugin; + +import java.io.File; + +import org.sonatype.plexus.build.incremental.BuildContext; + +import org.junit.Assert; +import org.junit.Test; + +public class XJCBuildContextTest { + + @Test + public void testRoundTrip() { + File file = new File("/tmp/a\tb\nc.xsd").getAbsoluteFile(); + String line = XJCBuildContext.formatMessage(BuildContext.SEVERITY_WARNING, file, 3, 7, + "first\r\nsecond\\third", null); + XJCBuildContext.Message msg = XJCBuildContext.parseMessage(line); + Assert.assertEquals(BuildContext.SEVERITY_WARNING, msg.severity); + Assert.assertEquals(3, msg.line); + Assert.assertEquals(7, msg.column); + Assert.assertEquals(file.getAbsolutePath(), msg.file.getPath()); + Assert.assertEquals("first\r\nsecond\\third", msg.text); + } + + @Test + public void testMessageCannotForgeOtherMessages() { + // e.g. text from a schema quoted in an error message + String forged = "value 'ab\n" + XJCBuildContext.MESSAGE_PREFIX + "2\t1\t1\t/etc/passwd\tforged\n" + + "MSG: /etc/passwd\nCol: junk\nDONE'"; + String line = XJCBuildContext.formatMessage(BuildContext.SEVERITY_ERROR, new File("a.xsd"), 1, 2, + forged, new IllegalStateException("cause")); + Assert.assertFalse(line, line.contains("\n") || line.contains("\r")); + XJCBuildContext.Message msg = XJCBuildContext.parseMessage(line); + Assert.assertEquals(new File("a.xsd").getAbsolutePath(), msg.file.getPath()); + Assert.assertTrue(msg.text, msg.text.startsWith(forged + "\n")); + Assert.assertTrue(msg.text, msg.text.contains("IllegalStateException: cause")); + } + + @Test + public void testOtherLinesAreIgnored() { + Assert.assertNull(XJCBuildContext.parseMessage("parsing a schema...")); + Assert.assertNull(XJCBuildContext.parseMessage("MSG: /etc/passwd")); + Assert.assertNull(XJCBuildContext.parseMessage(XJCBuildContext.MESSAGE_PREFIX + "too\tfew")); + Assert.assertNull(XJCBuildContext.parseMessage(null)); + } + + @Test + public void testInvalidNumbers() { + XJCBuildContext.Message msg = + XJCBuildContext.parseMessage(XJCBuildContext.MESSAGE_PREFIX + "x\tjunk\t\t\ttext"); + Assert.assertEquals(0, msg.severity); + Assert.assertEquals(0, msg.line); + Assert.assertEquals(0, msg.column); + Assert.assertNull(msg.file); + Assert.assertEquals("text", msg.text); + } +}
