From 3572850586456b4fd96ed87f74cf06fb9d3ae99a Mon Sep 17 00:00:00 2001 From: Vhati Date: Tue, 3 Sep 2013 02:10:11 -0400 Subject: [PATCH] Incorporated strict-then-sloppy XML parsing into the patch process --- skel_common/readme_changelog.txt | 3 +- skel_common/readme_modders.txt | 93 ++++++++++++++ .../vhati/modmanager/core/ModPatchThread.java | 14 ++- .../vhati/modmanager/core/ModUtilities.java | 114 ++++++++++++++++-- .../net/vhati/modmanager/ui/ManagerFrame.java | 10 +- .../vhati/modmanager/ui/ModXMLSandbox.java | 4 + 6 files changed, 213 insertions(+), 25 deletions(-) diff --git a/skel_common/readme_changelog.txt b/skel_common/readme_changelog.txt index c449e04..dd58813 100644 --- a/skel_common/readme_changelog.txt +++ b/skel_common/readme_changelog.txt @@ -2,7 +2,8 @@ Changelog ???: - Added a commandline interface -- Added XML sandbox for syntax tinkering +- Incorporated strict-then-sloppy XML parsing into the patch process +- Added XML Sandbox for syntax tinkering - Added scrollbars to progress popups to show long error messages 1.1: diff --git a/skel_common/readme_modders.txt b/skel_common/readme_modders.txt index bfaf5ce..5a7f065 100644 --- a/skel_common/readme_modders.txt +++ b/skel_common/readme_modders.txt @@ -97,6 +97,99 @@ Encoding!? UTF-16 or something else. +Advanced XML + + Since v1.2, Slipstream supports special tags to not only append, but + insert and edit existing XML. You can practice using them with the + "XML Sandbox", under the File menu. + + They take the form: + + + + + + + + Some identify existing tags, using each result as context for commands. + + Unless stated otherwise, these all accept optional reverse, start, + and limit args: defaulting to search forward, skip 0 matched + candidates, and return up to an unlimited number of results. + Sometimes the may have an auxiliary tag just to hold more + args. + + + + + Searches for tags of a given type with the given name attribute. + The type arg is optional. + Its unusual defaults are: reverse="true", start="0", limit="1". + It finds the first match from the end. + + + abc + + + Searches for tags of a given type, with all of the given attributes + and the given value. All of these find arguments are optional. To + omit the value, leave it blank, or make self-closing. + If no value or attributes are given, is unnecessary. + + + abc + + + As , except it searches for tags of a given type, that + contain certain children with the attributes and value. All args are + optional here as well. Note: The children are only search criteria, + not results themselves. + + + + + + + + + + Collates results from several criteria, or even multiple + nested entheses. The combines results using "OR" (union) + or "AND" (intersection) logic. Any commands within those + tags will be ignored. + + + The following commands that can occur inside a . + + + Searches the context tag's children and acts on them with its own + nested commands. + + abc + Sets a text value for the context tag. + + + Sets/adds one or more attributes on the context tag. + + + Removes the context tag entirely. + + + + Appends a new child to the context tag. Aside from the prefix, + the tag's type and content will appear as-is. It can be self-closing. + + + + If possible, the first child under the context tag will be + removed, and this will be inserted in its place. Otherwise, + this has the same effect as . + + Special tags and normal append content are processed in the order they + occur in your mod. And when patching several mods at once, later mods + edit in the wake of earlier ones. + + Pitfalls FTL Bug (fixed in 1.03.3): If a ship is modded to have level 5 shields, diff --git a/src/main/java/net/vhati/modmanager/core/ModPatchThread.java b/src/main/java/net/vhati/modmanager/core/ModPatchThread.java index 93fc99a..6ed17ab 100644 --- a/src/main/java/net/vhati/modmanager/core/ModPatchThread.java +++ b/src/main/java/net/vhati/modmanager/core/ModPatchThread.java @@ -19,6 +19,8 @@ import net.vhati.ftldat.FTLDat.FTLPack; import net.vhati.modmanager.core.ModPatchObserver; import net.vhati.modmanager.core.ModUtilities; +import org.jdom2.JDOMException; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -94,7 +96,7 @@ public class ModPatchThread extends Thread { } - private boolean patch() throws IOException { + private boolean patch() throws IOException, JDOMException { observer.patchingProgress( 0, progMax ); @@ -218,16 +220,16 @@ public class ModPatchThread extends Thread { log.warn( String.format( "Non-existent innerPath wasn't appended: %s", innerPath ) ); } else { - InputStream dstStream = null; + InputStream mainStream = null; try { - dstStream = ftlP.getInputStream(innerPath); - InputStream mergedStream = ModUtilities.appendXMLFile( zis, dstStream, ftlP.getName()+":"+innerPath, modFile.getName()+":"+parentPath+fileName ); - dstStream.close(); + mainStream = ftlP.getInputStream(innerPath); + InputStream mergedStream = ModUtilities.patchXMLFile( mainStream, zis, ftlP.getName()+":"+innerPath, modFile.getName()+":"+parentPath+fileName ); + mainStream.close(); ftlP.remove( innerPath ); ftlP.add( innerPath, mergedStream ); } finally { - try {if ( dstStream != null ) dstStream.close();} + try {if ( mainStream != null ) mainStream.close();} catch ( IOException e ) {} } diff --git a/src/main/java/net/vhati/modmanager/core/ModUtilities.java b/src/main/java/net/vhati/modmanager/core/ModUtilities.java index 0906e2a..52f2875 100644 --- a/src/main/java/net/vhati/modmanager/core/ModUtilities.java +++ b/src/main/java/net/vhati/modmanager/core/ModUtilities.java @@ -10,6 +10,7 @@ import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.StringReader; +import java.io.StringWriter; import java.nio.ByteBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; @@ -33,6 +34,7 @@ import net.vhati.modmanager.core.SloppyXMLParser; import ar.com.hjg.pngj.PngReader; import org.jdom2.Document; +import org.jdom2.JDOMException; import org.jdom2.input.JDOMParseException; import org.jdom2.input.SAXBuilder; @@ -44,6 +46,27 @@ public class ModUtilities { private static final Logger log = LogManager.getLogger(ModUtilities.class); + + /** + * Encodes a string (throwing an exception on bad chars) to bytes in a stream. + * Line endings will not be normalized. + * + * @param text a String to encode + * @param encoding the name of a Charset + * @param description how error messages should refer to the string, or null + */ + public static InputStream encodeText( String text, String encoding, String description ) throws IOException { + CharsetEncoder encoder = Charset.forName( encoding ).newEncoder(); + + ByteArrayOutputStream tmpData = new ByteArrayOutputStream(); + BufferedWriter bw = new BufferedWriter( new OutputStreamWriter( tmpData, encoder ) ); + bw.write( text ); + bw.flush(); + + InputStream result = new ByteArrayInputStream( tmpData.toByteArray() ); + return result; + } + /** * Determines text encoding for an InputStream and decodes its bytes as a string. * @@ -131,6 +154,7 @@ public class ModUtilities { /** * Semi-intelligently appends XML from one file (src) onto another (dst). + * Note: This is how patching used to work prior to SMM 1.2. * * The two InputStreams are read, and the combined result * is returned as a new third InputStream. @@ -143,7 +167,7 @@ public class ModUtilities { * The description arguments identify the streams for log messages. */ public static InputStream appendXMLFile( InputStream srcStream, InputStream dstStream, String srcDescription, String dstDescription ) throws IOException { - Pattern xmlDeclPtn = Pattern.compile( "<[?]xml version=\"1.0\" encoding=\"[^\"]+?\"[?]>\n*" ); + Pattern xmlDeclPtn = Pattern.compile( "<[?]xml [^>]*?[?]>\n*" ); String srcText = decodeText( srcStream, srcDescription ).text; srcText = xmlDeclPtn.matcher(srcText).replaceFirst( "" ); @@ -160,16 +184,85 @@ public class ModUtilities { String mergedString = Pattern.compile("\n").matcher( buf ).replaceAll("\r\n"); - ByteArrayOutputStream tmpData = new ByteArrayOutputStream(); - BufferedWriter bw = new BufferedWriter( new OutputStreamWriter( tmpData, "UTF-8" ) ); - bw.write( mergedString ); - bw.flush(); - - InputStream result = new ByteArrayInputStream( tmpData.toByteArray() ); + InputStream result = encodeText( mergedString, "UTF-8", srcDescription+"+"+dstDescription ); return result; } + /** + * Appends and modifies mainStream, using content from appendStream. + * + * The two InputStreams are read, and the combined result + * is returned as a new third InputStream. + * + * The returned stream is a ByteArrayInputStream + * which doesn't need closing. + * + * The result will be UTF-8 with CR-LF line endings. + * + * The description arguments identify the streams for log messages. + * + * @see net.vhati.modmanager.core.XMLPatcher + * @see net.vhati.modmanager.core.SloppyXMLOutputProcessor + */ + public static InputStream patchXMLFile( InputStream mainStream, InputStream appendStream, String mainDescription, String appendDescription ) throws IOException, JDOMException { + Pattern xmlDeclPtn = Pattern.compile( "<[?]xml [^>]*?[?]>\n*" ); + + String mainText = decodeText( mainStream, mainDescription ).text; + mainText = xmlDeclPtn.matcher(mainText).replaceFirst( "" ); + mainText = ""+ mainText +""; + Document mainDoc = parseStrictOrSloppyXML( mainText, mainDescription+" (wrapped)" ); + + String appendText = decodeText( appendStream, appendDescription ).text; + appendText = xmlDeclPtn.matcher(appendText).replaceFirst( "" ); + appendText = ""+ appendText +""; + Document appendDoc = parseStrictOrSloppyXML( appendText, appendDescription+" (wrapped)" ); + + XMLPatcher patcher = new XMLPatcher(); + Document mergedDoc = patcher.patch( mainDoc, appendDoc ); + + StringWriter writer = new StringWriter(); + SloppyXMLOutputProcessor.sloppyPrint( mergedDoc, writer, null ); + String mergedString = writer.toString(); + + InputStream result = encodeText( mergedString, "UTF-8", mainDescription+"+"+appendDescription ); + return result; + } + + + /** + * Returns an XML Document, parsed strictly if possible, or sloppily. + * Exceptions during strict parsing will be ignored. + * + * This method does NOT strip the XML declaration and add a wrapper + * tag with namespaces. That must be done beforehand. + * + * @see net.vhati.modmanager.core.EmptyAwareSAXHandlerFactory + * @see net.vhati.modmanager.core.SloppyXMLParser + */ + public static Document parseStrictOrSloppyXML( CharSequence srcSeq, String srcDescription ) throws IOException, JDOMException { + Document doc = null; + + try { + SAXBuilder strictParser = new SAXBuilder(); + strictParser.setSAXHandlerFactory( new EmptyAwareSAXHandlerFactory() ); + doc = strictParser.build( new StringReader( srcSeq.toString() ) ); + } + catch ( JDOMParseException e ) { + // Ignore the error, and do a sloppy parse instead. + + try { + SloppyXMLParser sloppyParser = new SloppyXMLParser(); + doc = sloppyParser.build( srcSeq ); + } + catch ( JDOMParseException f ) { + throw new JDOMException( String.format( "While processing \"%s\", strict parsing failed, then sloppy parsing failed: %s", srcDescription, f.getMessage() ), f ); + } + } + + return doc; + } + /** * Calls decodeText() on a stream, replaces line endings, and re-encodes. * @@ -185,12 +278,7 @@ public class ModUtilities { String srcText = decodeText( srcStream, srcDescription ).text; String fixedText = Pattern.compile("\n").matcher( srcText ).replaceAll( Matcher.quoteReplacement(eol) ); - ByteArrayOutputStream tmpData = new ByteArrayOutputStream(); - BufferedWriter bw = new BufferedWriter( new OutputStreamWriter( tmpData, "UTF-8" ) ); - bw.write( fixedText ); - bw.flush(); - - InputStream result = new ByteArrayInputStream( tmpData.toByteArray() ); + InputStream result = encodeText( fixedText, "UTF-8", srcDescription+" (with new EOL)" ); return result; } diff --git a/src/main/java/net/vhati/modmanager/ui/ManagerFrame.java b/src/main/java/net/vhati/modmanager/ui/ManagerFrame.java index 115f863..23d30da 100644 --- a/src/main/java/net/vhati/modmanager/ui/ManagerFrame.java +++ b/src/main/java/net/vhati/modmanager/ui/ManagerFrame.java @@ -636,11 +636,11 @@ public class ManagerFrame extends JFrame implements ActionListener, HashObserver resultBuf.append( "But malformed XML may break tools that do proper parsing, " ); resultBuf.append( "and it hinders the development of new tools.\n" ); resultBuf.append( "\n" ); - resultBuf.append( "In future releases, Slipstream will try to parse XML while " ); - resultBuf.append( "patching: first strictly, then failing over to a sloppy " ); - resultBuf.append( "parser. The sloppy parser will tolerate similar errors, " ); - resultBuf.append( "at the risk of unforseen behavior, so satisfying the " ); - resultBuf.append( "strict parser is advised.\n" ); + resultBuf.append( "Since v1.2, Slipstream will try to parse XML while patching: " ); + resultBuf.append( "first strictly, then failing over to a sloppy parser. " ); + resultBuf.append( "The sloppy parser will tolerate similar errors, at the risk " ); + resultBuf.append( "of unforseen behavior, so satisfying the strict parser " ); + resultBuf.append( "is advised.\n" ); } infoArea.setDescription( "Results", resultBuf.toString() ); } diff --git a/src/main/java/net/vhati/modmanager/ui/ModXMLSandbox.java b/src/main/java/net/vhati/modmanager/ui/ModXMLSandbox.java index b0528a1..29877d0 100644 --- a/src/main/java/net/vhati/modmanager/ui/ModXMLSandbox.java +++ b/src/main/java/net/vhati/modmanager/ui/ModXMLSandbox.java @@ -298,6 +298,8 @@ public class ModXMLSandbox extends JDialog implements ActionListener { private void open() { + messageArea.setText( "" ); + FTLDat.FTLPack dataP = null; InputStream is = null; try { @@ -345,6 +347,8 @@ public class ModXMLSandbox extends JDialog implements ActionListener { private void patch() { if ( mainDoc == null ) return; + messageArea.setText( "" ); + try { String appendText = appendArea.getText(); appendText = appendText.replaceFirst( "<[?]xml [^>]*?[?]>", "" );