From 3896620b4fbf031b1b02b4d46b328c5839a231da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20=C5=BBygie=C5=82o?= Date: Thu, 24 Feb 2022 12:28:56 +0100 Subject: [PATCH 01/12] Use (already) precalculated value --- .../org/codehaus/plexus/util/xml/pull/MXSerializer.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/codehaus/plexus/util/xml/pull/MXSerializer.java b/src/main/java/org/codehaus/plexus/util/xml/pull/MXSerializer.java index cd1edc59..c69db3f4 100644 --- a/src/main/java/org/codehaus/plexus/util/xml/pull/MXSerializer.java +++ b/src/main/java/org/codehaus/plexus/util/xml/pull/MXSerializer.java @@ -460,15 +460,15 @@ public void startDocument( String encoding, Boolean standalone ) if ( encoding != null ) { out.write( " encoding=" ); - out.write( attributeUseApostrophe ? '\'' : '"' ); + out.write( apos ); out.write( encoding ); - out.write( attributeUseApostrophe ? '\'' : '"' ); + out.write( apos ); // out.write('\''); } if ( standalone != null ) { out.write( " standalone=" ); - out.write( attributeUseApostrophe ? '\'' : '"' ); + out.write( apos ); if ( standalone ) { out.write( "yes" ); @@ -477,7 +477,7 @@ public void startDocument( String encoding, Boolean standalone ) { out.write( "no" ); } - out.write( attributeUseApostrophe ? '\'' : '"' ); + out.write( apos ); // if(standalone.booleanValue()) { // out.write(" standalone='yes'"); // } else { From b99f7c0e46d8b08d63afa4c2c7d274ebf5ebcc94 Mon Sep 17 00:00:00 2001 From: Gabriel Belingueres Date: Sat, 2 Apr 2022 12:31:39 -0300 Subject: [PATCH 02/12] Fixed regressions: * #163 - new case: Don't assume UTF8 as default, to allow parsing from String. * #194 - Incorrect getText() after parsing the DOCDECL section. * Added tests exercising other regressions exposed while fixing this issues. --- .../plexus/util/xml/pull/MXParser.java | 157 ++++-- .../plexus/util/xml/pull/MXParserTest.java | 495 ++++++++++++++++++ src/test/resources/xml/test-entities-dos.xml | 6 + .../xml/test-entities-in-attr-dos.xml | 9 + .../resources/xml/test-entities-in-attr.xml | 9 + src/test/resources/xml/test-entities.xml | 6 + 6 files changed, 637 insertions(+), 45 deletions(-) create mode 100644 src/test/resources/xml/test-entities-dos.xml create mode 100644 src/test/resources/xml/test-entities-in-attr-dos.xml create mode 100644 src/test/resources/xml/test-entities-in-attr.xml create mode 100644 src/test/resources/xml/test-entities.xml diff --git a/src/main/java/org/codehaus/plexus/util/xml/pull/MXParser.java b/src/main/java/org/codehaus/plexus/util/xml/pull/MXParser.java index 3874f572..e21b66cb 100644 --- a/src/main/java/org/codehaus/plexus/util/xml/pull/MXParser.java +++ b/src/main/java/org/codehaus/plexus/util/xml/pull/MXParser.java @@ -124,7 +124,7 @@ private String newStringIntern( char[] cbuf, int off, int len ) // private String elValue[]; private int elNamespaceCount[]; - private String fileEncoding = "UTF8"; + private String fileEncoding = null; /** * Make sure that we have enough space to keep element stack if passed size. It will always create one additional @@ -587,8 +587,8 @@ else if ( FEATURE_XML_ROUNDTRIP.equals( name ) ) } } - /** - * Unknown properties are always returned as false + /** + * Unknown properties are always returned as false */ @Override public boolean getFeature( String name ) @@ -1596,11 +1596,11 @@ else if ( ch == '&' ) } final int oldStart = posStart + bufAbsoluteStart; final int oldEnd = posEnd + bufAbsoluteStart; - final char[] resolvedEntity = parseEntityRef(); + parseEntityRef(); if ( tokenize ) return eventType = ENTITY_REF; // check if replacement text can be resolved !!! - if ( resolvedEntity == null ) + if ( resolvedEntityRefCharBuf == BUF_NOT_RESOLVED ) { if ( entityRefName == null ) { @@ -1628,7 +1628,7 @@ else if ( ch == '&' ) } // assert usePC == true; // write into PC replacement text - do merge for replacement text!!!! - for ( char aResolvedEntity : resolvedEntity ) + for ( char aResolvedEntity : resolvedEntityRefCharBuf ) { if ( pcEnd >= pc.length ) { @@ -2675,9 +2675,28 @@ else if ( ch == '\t' || ch == '\n' || ch == '\r' ) return ch; } - private char[] charRefOneCharBuf = new char[1]; + // state representing that no entity ref have been resolved + private static final char[] BUF_NOT_RESOLVED = new char[0]; + + // predefined entity refs + private static final char[] BUF_LT = new char[] { '<' }; + private static final char[] BUF_AMP = new char[] { '&' }; + private static final char[] BUF_GT = new char[] { '>' }; + private static final char[] BUF_APO = new char[] { '\'' }; + private static final char[] BUF_QUOT = new char[] { '"' }; - private char[] parseEntityRef() + private char[] resolvedEntityRefCharBuf = BUF_NOT_RESOLVED; + + /** + * parse Entity Ref, either a character entity or one of the predefined name entities. + * + * @return the length of the valid found character reference, which may be one of the predefined character reference + * names (resolvedEntityRefCharBuf contains the replaced chars). Returns the length of the not found entity + * name, otherwise. + * @throws XmlPullParserException if invalid XML is detected. + * @throws IOException if an I/O error is found. + */ + private int parseCharOrPredefinedEntityRef() throws XmlPullParserException, IOException { // entity reference http://www.w3.org/TR/2000/REC-xml-20001006#NT-Reference @@ -2686,6 +2705,8 @@ private char[] parseEntityRef() // ASSUMPTION just after & entityRefName = null; posStart = pos; + int len = 0; + resolvedEntityRefCharBuf = BUF_NOT_RESOLVED; char ch = more(); if ( ch == '#' ) { @@ -2750,7 +2771,6 @@ else if ( ch >= 'A' && ch <= 'F' ) ch = more(); } } - posEnd = pos - 1; boolean isValidCodePoint = true; try @@ -2759,7 +2779,7 @@ else if ( ch >= 'A' && ch <= 'F' ) isValidCodePoint = isValidCodePoint( codePoint ); if ( isValidCodePoint ) { - charRefOneCharBuf = Character.toChars( codePoint ); + resolvedEntityRefCharBuf = Character.toChars( codePoint ); } } catch ( IllegalArgumentException e ) @@ -2775,14 +2795,14 @@ else if ( ch >= 'A' && ch <= 'F' ) if ( tokenize ) { - text = newString( charRefOneCharBuf, 0, charRefOneCharBuf.length ); + text = newString( resolvedEntityRefCharBuf, 0, resolvedEntityRefCharBuf.length ); } - return charRefOneCharBuf; + len = resolvedEntityRefCharBuf.length; } else { // [68] EntityRef ::= '&' Name ';' - // scan anem until ; + // scan name until ; if ( !isNameStartChar( ch ) ) { throw new XmlPullParserException( "entity reference names can not start with character '" @@ -2801,17 +2821,15 @@ else if ( ch >= 'A' && ch <= 'F' ) + printable( ch ) + "'", this, null ); } } - posEnd = pos - 1; // determine what name maps to - final int len = posEnd - posStart; + len = ( pos - 1 ) - posStart; if ( len == 2 && buf[posStart] == 'l' && buf[posStart + 1] == 't' ) { if ( tokenize ) { text = "<"; } - charRefOneCharBuf[0] = '<'; - return charRefOneCharBuf; + resolvedEntityRefCharBuf = BUF_LT; // if(paramPC || isParserTokenizing) { // if(pcEnd >= pc.length) ensurePC(); // pc[pcEnd++] = '<'; @@ -2823,8 +2841,7 @@ else if ( len == 3 && buf[posStart] == 'a' && buf[posStart + 1] == 'm' && buf[po { text = "&"; } - charRefOneCharBuf[0] = '&'; - return charRefOneCharBuf; + resolvedEntityRefCharBuf = BUF_AMP; } else if ( len == 2 && buf[posStart] == 'g' && buf[posStart + 1] == 't' ) { @@ -2832,8 +2849,7 @@ else if ( len == 2 && buf[posStart] == 'g' && buf[posStart + 1] == 't' ) { text = ">"; } - charRefOneCharBuf[0] = '>'; - return charRefOneCharBuf; + resolvedEntityRefCharBuf = BUF_GT; } else if ( len == 4 && buf[posStart] == 'a' && buf[posStart + 1] == 'p' && buf[posStart + 2] == 'o' && buf[posStart + 3] == 's' ) @@ -2842,8 +2858,7 @@ else if ( len == 4 && buf[posStart] == 'a' && buf[posStart + 1] == 'p' && buf[po { text = "'"; } - charRefOneCharBuf[0] = '\''; - return charRefOneCharBuf; + resolvedEntityRefCharBuf = BUF_APO; } else if ( len == 4 && buf[posStart] == 'q' && buf[posStart + 1] == 'u' && buf[posStart + 2] == 'o' && buf[posStart + 3] == 't' ) @@ -2852,25 +2867,65 @@ else if ( len == 4 && buf[posStart] == 'q' && buf[posStart + 1] == 'u' && buf[po { text = "\""; } - charRefOneCharBuf[0] = '"'; - return charRefOneCharBuf; - } - else - { - final char[] result = lookuEntityReplacement( len ); - if ( result != null ) - { - return result; - } + resolvedEntityRefCharBuf = BUF_QUOT; } - if ( tokenize ) - text = null; - return null; } + + posEnd = pos; + + return len; + } + + /** + * Parse an entity reference inside the DOCDECL section. + * + * @throws XmlPullParserException if invalid XML is detected. + * @throws IOException if an I/O error is found. + */ + private void parseEntityRefInDocDecl() + throws XmlPullParserException, IOException + { + parseCharOrPredefinedEntityRef(); + if (usePC) { + posStart--; // include in PC the starting '&' of the entity + joinPC(); + } + + if ( resolvedEntityRefCharBuf != BUF_NOT_RESOLVED ) + return; + if ( tokenize ) + text = null; + } + + /** + * Parse an entity reference inside a tag or attribute. + * + * @throws XmlPullParserException if invalid XML is detected. + * @throws IOException if an I/O error is found. + */ + private void parseEntityRef() + throws XmlPullParserException, IOException + { + final int len = parseCharOrPredefinedEntityRef(); + + posEnd--; // don't involve the final ';' from the entity in the search + + if ( resolvedEntityRefCharBuf != BUF_NOT_RESOLVED ) { + return; + } + + resolvedEntityRefCharBuf = lookuEntityReplacement( len ); + if ( resolvedEntityRefCharBuf != BUF_NOT_RESOLVED ) + { + return; + } + if ( tokenize ) + text = null; } /** - * Check if the provided parameter is a valid Char, according to: {@link https://www.w3.org/TR/REC-xml/#NT-Char} + * Check if the provided parameter is a valid Char. According to + * https://www.w3.org/TR/REC-xml/#NT-Char * * @param codePoint the numeric value to check * @return true if it is a valid numeric character reference. False otherwise. @@ -2883,8 +2938,6 @@ private static boolean isValidCodePoint( int codePoint ) } private char[] lookuEntityReplacement( int entityNameLen ) - throws XmlPullParserException, IOException - { if ( !allStringsInterned ) { @@ -2919,7 +2972,7 @@ private char[] lookuEntityReplacement( int entityNameLen ) } } } - return null; + return BUF_NOT_RESOLVED; } private void parseComment() @@ -2977,7 +3030,7 @@ else if (isValidCodePoint( ch )) } else { - throw new XmlPullParserException( "Illegal character 0x" + Integer.toHexString(((int) ch)) + " found in comment", this, null ); + throw new XmlPullParserException( "Illegal character 0x" + Integer.toHexString(ch) + " found in comment", this, null ); } if ( normalizeIgnorableWS ) { @@ -3484,7 +3537,8 @@ else if ( ch == '>' && bracketLevel == 0 ) break; else if ( ch == '&' ) { - extractEntityRef(); + extractEntityRefInDocDecl(); + continue; } if ( normalizeIgnorableWS ) { @@ -3536,6 +3590,19 @@ else if ( ch == '\n' ) } posEnd = pos - 1; + text = null; + } + + private void extractEntityRefInDocDecl() + throws XmlPullParserException, IOException + { + // extractEntityRef + posEnd = pos - 1; + + int prevPosStart = posStart; + parseEntityRefInDocDecl(); + + posStart = prevPosStart; } private void extractEntityRef() @@ -3559,9 +3626,9 @@ private void extractEntityRef() } // assert usePC == true; - final char[] resolvedEntity = parseEntityRef(); + parseEntityRef(); // check if replacement text can be resolved !!! - if ( resolvedEntity == null ) + if ( resolvedEntityRefCharBuf == BUF_NOT_RESOLVED ) { if ( entityRefName == null ) { @@ -3571,7 +3638,7 @@ private void extractEntityRef() + "'", this, null ); } // write into PC replacement text - do merge for replacement text!!!! - for ( char aResolvedEntity : resolvedEntity ) + for ( char aResolvedEntity : resolvedEntityRefCharBuf ) { if ( pcEnd >= pc.length ) { diff --git a/src/test/java/org/codehaus/plexus/util/xml/pull/MXParserTest.java b/src/test/java/org/codehaus/plexus/util/xml/pull/MXParserTest.java index e0be666a..6fc6e9c6 100644 --- a/src/test/java/org/codehaus/plexus/util/xml/pull/MXParserTest.java +++ b/src/test/java/org/codehaus/plexus/util/xml/pull/MXParserTest.java @@ -17,6 +17,7 @@ */ import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -29,6 +30,7 @@ import java.nio.file.Files; import java.nio.file.Paths; +import org.codehaus.plexus.util.IOUtil; import org.codehaus.plexus.util.ReaderFactory; import org.junit.Test; @@ -898,4 +900,497 @@ public void testEncodingISO_8859_1_setInputStream() } } + /** + * Issue 163: https://github.com/codehaus-plexus/plexus-utils/issues/163 + * + * Another case of bug #163: File encoding information is lost after the input file is copied to a String. + * + * @throws IOException if IO error. + * + * @since 3.4.2 + */ + @Test + public void testEncodingISO_8859_1setStringReader() + throws IOException + { + try ( Reader reader = + ReaderFactory.newXmlReader( new File( "src/test/resources/xml", "test-encoding-ISO-8859-1.xml" ) ) ) + { + MXParser parser = new MXParser(); + String xmlFileContents = IOUtil.toString( reader ); + parser.setInput( new StringReader( xmlFileContents ) ); + while ( parser.nextToken() != XmlPullParser.END_DOCUMENT ) + ; + assertTrue( true ); + } + catch ( XmlPullParserException e ) + { + fail( "should not raise exception: " + e ); + } + } + + /** + *

+ * Test custom Entity not found. + *

+ * + * Regression test: assure same behavior of MXParser from plexus-utils 3.3.0. + * + * @throws java.lang.Exception if any. + * + * @since 3.4.2 + */ + @Test + public void testCustomEntityNotFoundInText() + throws Exception + { + MXParser parser = new MXParser(); + + String input = "&otherentity;"; + parser.setInput( new StringReader( input ) ); + parser.defineEntityReplacementText( "myentity", "replacement" ); + + try + { + assertEquals( XmlPullParser.START_TAG, parser.next() ); + assertEquals( XmlPullParser.TEXT, parser.next() ); + fail( "should raise exception" ); + } + catch ( XmlPullParserException e ) + { + assertTrue( e.getMessage().contains( "could not resolve entity named 'otherentity' (position: START_TAG seen &otherentity;... @1:19)" ) ); + assertEquals( XmlPullParser.START_TAG, parser.getEventType() ); // not an ENTITY_REF + assertEquals( "otherentity", parser.getText() ); + } + } + + /** + *

+ * Test custom Entity not found, with tokenize. + *

+ * + * Regression test: assure same behavior of MXParser from plexus-utils 3.3.0. + * + * @throws java.lang.Exception if any. + * + * @since 3.4.2 + */ + @Test + public void testCustomEntityNotFoundInTextTokenize() + throws Exception + { + MXParser parser = new MXParser(); + + String input = "&otherentity;"; + parser.setInput( new StringReader( input ) ); + parser.defineEntityReplacementText( "myentity", "replacement" ); + + try + { + assertEquals( XmlPullParser.START_TAG, parser.nextToken() ); + assertEquals( XmlPullParser.ENTITY_REF, parser.nextToken() ); + assertNull( parser.getText() ); + } + catch ( XmlPullParserException e ) + { + fail( "should not throw exception if tokenize" ); + } + } + + /** + *

+ * Test custom Entity not found in attribute. + *

+ * + * Regression test: assure same behavior of MXParser from plexus-utils 3.3.0. + * + * @throws java.lang.Exception if any. + * + * @since 3.4.2 + */ + @Test + public void testCustomEntityNotFoundInAttr() + throws Exception + { + MXParser parser = new MXParser(); + + String input = "sometext"; + parser.setInput( new StringReader( input ) ); + parser.defineEntityReplacementText( "myentity", "replacement" ); + + try + { + assertEquals( XmlPullParser.START_TAG, parser.next() ); + fail( "should raise exception" ); + } + catch ( XmlPullParserException e ) + { + assertTrue( e.getMessage().contains( "could not resolve entity named 'otherentity' (position: START_DOCUMENT seen + * Test custom Entity not found in attribute, with tokenize. + *

+ * + * Regression test: assure same behavior of MXParser from plexus-utils 3.3.0. + * @throws XmlPullParserException + * + * @throws Exception if any. + * + * @since 3.4.2 + */ + @Test + public void testCustomEntityNotFoundInAttrTokenize() throws Exception + { + MXParser parser = new MXParser(); + + String input = "sometext"; + + try + { + parser.setInput( new StringReader( input ) ); + parser.defineEntityReplacementText( "myentity", "replacement" ); + + assertEquals( XmlPullParser.START_TAG, parser.nextToken() ); + fail( "should raise exception" ); + } + catch ( XmlPullParserException e ) + { + assertTrue( e.getMessage().contains( "could not resolve entity named 'otherentity' (position: START_DOCUMENT seen Issue #194: Incorrect getText() after parsing the DOCDECL section + * + *

test DOCDECL text with myCustomEntity that cannot be resolved, Unix line separator.

+ * + * Regression test: assure same behavior of MXParser from plexus-utils 3.3.0. + * + * @throws IOException if any. + * + * @since 3.4.2 + */ + @Test + public void testDocdeclTextWithEntitiesUnix() + throws IOException + { + testDocdeclTextWithEntities( "test-entities.xml" ); + } + + /** + *

Issue #194: Incorrect getText() after parsing the DOCDECL section + * + *

test DOCDECL text with myCustomEntity that cannot be resolved, DOS line separator.

+ * + * Regression test: assure same behavior of MXParser from plexus-utils 3.3.0. + * + * @throws IOException if any. + * + * @since 3.4.2 + */ + @Test + public void testDocdeclTextWithEntitiesDOS() + throws IOException + { + testDocdeclTextWithEntities( "test-entities-dos.xml" ); + } + + private void testDocdeclTextWithEntities( String filename ) + throws IOException + { + try ( Reader reader = ReaderFactory.newXmlReader( new File( "src/test/resources/xml", filename ) ) ) + { + MXParser parser = new MXParser(); + parser.setInput( reader ); + assertEquals( XmlPullParser.PROCESSING_INSTRUCTION, parser.nextToken() ); + assertEquals( XmlPullParser.IGNORABLE_WHITESPACE, parser.nextToken() ); + assertEquals( XmlPullParser.DOCDECL, parser.nextToken() ); + assertEquals( " document [\n" + + "\n" + + "\n" + + "]", parser.getText() ); + assertEquals( XmlPullParser.IGNORABLE_WHITESPACE, parser.nextToken() ); + assertEquals( XmlPullParser.START_TAG, parser.nextToken() ); + assertEquals( "document", parser.getName() ); + assertEquals( XmlPullParser.TEXT, parser.next() ); + + fail( "should fail to resolve 'myCustomEntity' entity"); + } + catch ( XmlPullParserException e ) + { + assertTrue( e.getMessage().contains( "could not resolve entity named 'myCustomEntity'" )); + } + } + + /** + *

Issue #194: Incorrect getText() after parsing the DOCDECL section + * + *

test DOCDECL text with entities appearing in attributes, Unix line separator.

+ * + * Regression test: assure same behavior of MXParser from plexus-utils 3.3.0. + * + * @throws IOException if any. + * + * @since 3.4.2 + */ + @Test + public void testDocdeclTextWithEntitiesInAttributesUnix() + throws IOException + { + testDocdeclTextWithEntitiesInAttributes( "test-entities-in-attr.xml" ); + } + + /** + *

Issue #194: Incorrect getText() after parsing the DOCDECL section + * + *

test DOCDECL text with entities appearing in attributes, DOS line separator.

+ * + * Regression test: assure same behavior of MXParser from plexus-utils 3.3.0. + * + * @throws IOException if any. + * + * @since 3.4.2 + */ + @Test + public void testDocdeclTextWithEntitiesInAttributesDOS() + throws IOException + { + testDocdeclTextWithEntitiesInAttributes( "test-entities-in-attr-dos.xml" ); + } + + private void testDocdeclTextWithEntitiesInAttributes( String filename ) + throws IOException + { + try ( Reader reader = ReaderFactory.newXmlReader( new File( "src/test/resources/xml", filename ) ) ) + { + MXParser parser = new MXParser(); + parser.setInput( reader ); + parser.defineEntityReplacementText( "nbsp", " " ); + parser.defineEntityReplacementText( "Alpha", "Α" ); + parser.defineEntityReplacementText( "tritPos", "𝟭" ); + parser.defineEntityReplacementText( "flo", "ř" ); + parser.defineEntityReplacementText( "myCustomEntity", "&flo;" ); + assertEquals( XmlPullParser.PROCESSING_INSTRUCTION, parser.nextToken() ); + assertEquals( XmlPullParser.IGNORABLE_WHITESPACE, parser.nextToken() ); + assertEquals( XmlPullParser.DOCDECL, parser.nextToken() ); + assertEquals( " document [\n" + + " \n" + + " \n" + + " \n" + + "\n" + + "\n" + + "]", parser.getText() ); + assertEquals( XmlPullParser.IGNORABLE_WHITESPACE, parser.nextToken() ); + assertEquals( XmlPullParser.START_TAG, parser.nextToken() ); + assertEquals( "document", parser.getName() ); + assertEquals( 1, parser.getAttributeCount() ); + assertEquals( "name", parser.getAttributeName( 0 ) ); + assertEquals( "section name with entities: '&' 'Α' '<' ' ' '>' '𝟭' ''' 'ř' '\"'", + parser.getAttributeValue( 0 ) ); + + assertEquals( XmlPullParser.ENTITY_REF, parser.nextToken() ); + assertEquals( "myCustomEntity", parser.getName() ); + assertEquals( "ř", parser.getText() ); + + assertEquals( XmlPullParser.END_TAG, parser.nextToken() ); + assertEquals( XmlPullParser.END_DOCUMENT, parser.nextToken() ); + } + catch ( XmlPullParserException e ) + { + fail( "should not raise exception: " + e ); + } + } + + /** + *

test entity ref with entities appearing in tags, Unix line separator.

+ * + * Regression test: assure same behavior of MXParser from plexus-utils 3.3.0. + * + * @throws IOException if any. + * + * @since 3.4.2 + */ + @Test + public void testEntityRefTextUnix() + throws IOException + { + testEntityRefText( "\n" ); + } + + /** + *

test entity ref with entities appearing in tags, DOS line separator.

+ * + * Regression test: assure same behavior of MXParser from plexus-utils 3.3.0. + * + * @throws IOException if any. + * + * @since 3.4.2 + */ + @Test + public void testEntityRefTextDOS() + throws IOException + { + testEntityRefText( "\r\n" ); + } + + private void testEntityRefText( String newLine ) + throws IOException + { + StringBuilder sb = new StringBuilder(); + sb.append( "" ).append( newLine ); + sb.append( "" ).append( newLine ); + sb.append( "" ).append( newLine ); + sb.append( "" ).append( newLine ); + sb.append( "]>" ).append( newLine ); + sb.append( "&foo;&foo1;&foo2;&tritPos;" ); + + try + { + MXParser parser = new MXParser(); + parser.setInput( new StringReader( sb.toString() ) ); + parser.defineEntityReplacementText( "foo", "ř" ); + parser.defineEntityReplacementText( "nbsp", " " ); + parser.defineEntityReplacementText( "foo1", " " ); + parser.defineEntityReplacementText( "foo2", "š" ); + parser.defineEntityReplacementText( "tritPos", "𝟭" ); + + assertEquals( XmlPullParser.DOCDECL, parser.nextToken() ); + assertEquals( " test [\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "]", parser.getText() ); + assertEquals( XmlPullParser.IGNORABLE_WHITESPACE, parser.nextToken() ); + assertEquals( XmlPullParser.START_TAG, parser.nextToken() ); + assertEquals( "b", parser.getName() ); + assertEquals( XmlPullParser.ENTITY_REF, parser.nextToken() ); + assertEquals( "ř", parser.getText() ); + assertEquals( "foo", parser.getName() ); + assertEquals( XmlPullParser.ENTITY_REF, parser.nextToken() ); + assertEquals( " ", parser.getText() ); + assertEquals( "foo1", parser.getName() ); + assertEquals( XmlPullParser.ENTITY_REF, parser.nextToken() ); + assertEquals( "š", parser.getText() ); + assertEquals( "foo2", parser.getName() ); + assertEquals( XmlPullParser.ENTITY_REF, parser.nextToken() ); + assertEquals( "𝟭", parser.getText() ); + assertEquals( "tritPos", parser.getName() ); + assertEquals( XmlPullParser.END_TAG, parser.nextToken() ); + assertEquals( "b", parser.getName() ); + assertEquals( XmlPullParser.END_DOCUMENT, parser.nextToken() ); + } + catch ( XmlPullParserException e ) + { + fail( "should not raise exception: " + e ); + } + } + + /** + * Ensures that entity ref getText() and getName() return the correct value. + * + * Regression test: assure same behavior of MXParser from plexus-utils 3.3.0. + * + * @throws IOException if any. + * + * @since 3.4.2 + */ + @Test + public void testEntityReplacement() throws IOException { + String input = "

  

"; + + try + { + MXParser parser = new MXParser(); + parser.setInput( new StringReader( input ) ); + parser.defineEntityReplacementText( "nbsp", " " ); + + assertEquals( XmlPullParser.START_TAG, parser.nextToken() ); + assertEquals( "p", parser.getName() ); + assertEquals( XmlPullParser.COMMENT, parser.nextToken() ); + assertEquals( " a pagebreak: ", parser.getText() ); + assertEquals( XmlPullParser.COMMENT, parser.nextToken() ); + assertEquals( " PB ", parser.getText() ); + assertEquals( XmlPullParser.ENTITY_REF, parser.nextToken() ); + assertEquals( "\u00A0", parser.getText() ); + assertEquals( "#160", parser.getName() ); + assertEquals( XmlPullParser.ENTITY_REF, parser.nextToken() ); + assertEquals( " ", parser.getText() ); + assertEquals( "nbsp", parser.getName() ); + assertEquals( XmlPullParser.START_TAG, parser.nextToken() ); + assertEquals( "unknown", parser.getName() ); + assertEquals( XmlPullParser.END_TAG, parser.nextToken() ); + assertEquals( "unknown", parser.getName() ); + assertEquals( XmlPullParser.END_TAG, parser.nextToken() ); + assertEquals( "p", parser.getName() ); + assertEquals( XmlPullParser.END_DOCUMENT, parser.nextToken() ); + } + catch ( XmlPullParserException e ) + { + fail( "should not raise exception: " + e ); + } + } + + /** + * Ensures correct replacements inside the internal PC array when the new copied array size is shorter than + * previous ones. + * + * Regression test: assure same behavior of MXParser from plexus-utils 3.3.0. + * + * @throws IOException if any. + * + * @since 3.4.2 + */ + @Test + public void testReplacementInPCArrayWithShorterCharArray() + throws IOException + { + String input = "]>" + + "

&&foo;&tritPos;

"; + + try + { + MXParser parser = new MXParser(); + parser.setInput( new StringReader( new String(input.getBytes(), "ISO-8859-1" ) ) ); + parser.defineEntityReplacementText( "foo", "ř" ); + parser.defineEntityReplacementText( "tritPos", "𝟭" ); + + assertEquals( XmlPullParser.DOCDECL, parser.nextToken() ); + assertEquals( " test []", parser.getText() ); + assertEquals( XmlPullParser.START_TAG, parser.nextToken() ); + assertEquals( "section", parser.getName() ); + assertEquals( 1, parser.getAttributeCount() ); + assertEquals( "name" , parser.getAttributeName( 0 ) ); + assertEquals( "&ř𝟭" , parser.getAttributeValue( 0 ) ); + assertEquals( XmlPullParser.START_TAG, parser.nextToken() ); + assertEquals( "

", parser.getText() ); + assertEquals( "p", parser.getName() ); + assertEquals( XmlPullParser.ENTITY_REF, parser.nextToken() ); + assertEquals( "&", parser.getText() ); + assertEquals( "amp", parser.getName() ); + assertEquals( XmlPullParser.ENTITY_REF, parser.nextToken() ); + assertEquals( "ř", parser.getText() ); + assertEquals( "foo", parser.getName() ); + assertEquals( XmlPullParser.ENTITY_REF, parser.nextToken() ); + assertEquals( "𝟭", parser.getText() ); + assertEquals( "tritPos", parser.getName() ); + assertEquals( XmlPullParser.END_TAG, parser.nextToken() ); + assertEquals( "p", parser.getName() ); + assertEquals( XmlPullParser.END_TAG, parser.nextToken() ); + assertEquals( "section", parser.getName() ); + assertEquals( XmlPullParser.END_DOCUMENT, parser.nextToken() ); + } + catch ( XmlPullParserException e ) + { + fail( "should not raise exception: " + e ); + } + } } diff --git a/src/test/resources/xml/test-entities-dos.xml b/src/test/resources/xml/test-entities-dos.xml new file mode 100644 index 00000000..e1d6d17a --- /dev/null +++ b/src/test/resources/xml/test-entities-dos.xml @@ -0,0 +1,6 @@ + + + +]> +&myCustomEntity; \ No newline at end of file diff --git a/src/test/resources/xml/test-entities-in-attr-dos.xml b/src/test/resources/xml/test-entities-in-attr-dos.xml new file mode 100644 index 00000000..a423c995 --- /dev/null +++ b/src/test/resources/xml/test-entities-in-attr-dos.xml @@ -0,0 +1,9 @@ + + + + + + +]> +&myCustomEntity; \ No newline at end of file diff --git a/src/test/resources/xml/test-entities-in-attr.xml b/src/test/resources/xml/test-entities-in-attr.xml new file mode 100644 index 00000000..a423c995 --- /dev/null +++ b/src/test/resources/xml/test-entities-in-attr.xml @@ -0,0 +1,9 @@ + + + + + + +]> +&myCustomEntity; \ No newline at end of file diff --git a/src/test/resources/xml/test-entities.xml b/src/test/resources/xml/test-entities.xml new file mode 100644 index 00000000..e1d6d17a --- /dev/null +++ b/src/test/resources/xml/test-entities.xml @@ -0,0 +1,6 @@ + + + +]> +&myCustomEntity; \ No newline at end of file From 298fd8d626a7dd1271dcc4ae8e2029a2eba2c1ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Apr 2022 02:03:30 +0000 Subject: [PATCH 03/12] Bump actions/checkout from 2.4.0 to 3.0.1 Bumps [actions/checkout](https://github.com/actions/checkout) from 2.4.0 to 3.0.1. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2.4.0...v3.0.1) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 3f39b7e2..9935c621 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL From 52d64eadb7d4f8a543d43cc49194cdf74b93f944 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Mar 2022 02:02:37 +0000 Subject: [PATCH 04/12] Bump jmh-core from 1.34 to 1.35 Bumps jmh-core from 1.34 to 1.35. --- updated-dependencies: - dependency-name: org.openjdk.jmh:jmh-core dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a758c676..8a9018e9 100644 --- a/pom.xml +++ b/pom.xml @@ -64,7 +64,7 @@ limitations under the License. org.openjdk.jmh jmh-core - 1.34 + 1.35 test From 11be3b5bf2a8b9853ab2e7f2e1245233220ca46a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Mar 2022 02:02:35 +0000 Subject: [PATCH 05/12] Bump jmh-generator-annprocess from 1.34 to 1.35 Bumps jmh-generator-annprocess from 1.34 to 1.35. --- updated-dependencies: - dependency-name: org.openjdk.jmh:jmh-generator-annprocess dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8a9018e9..beda14b7 100644 --- a/pom.xml +++ b/pom.xml @@ -70,7 +70,7 @@ limitations under the License. org.openjdk.jmh jmh-generator-annprocess - 1.34 + 1.35 test From 10f729542509b039a748fc7085826e43137bba35 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Mar 2022 02:02:38 +0000 Subject: [PATCH 06/12] Bump release-drafter/release-drafter from 5.18.1 to 5.19.0 Bumps [release-drafter/release-drafter](https://github.com/release-drafter/release-drafter) from 5.18.1 to 5.19.0. - [Release notes](https://github.com/release-drafter/release-drafter/releases) - [Commits](https://github.com/release-drafter/release-drafter/compare/v5.18.1...v5.19.0) --- updated-dependencies: - dependency-name: release-drafter/release-drafter dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/release-drafter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 4ece1c1a..96f54b5d 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -7,6 +7,6 @@ jobs: update_release_draft: runs-on: ubuntu-latest steps: - - uses: release-drafter/release-drafter@v5.18.1 + - uses: release-drafter/release-drafter@v5.19.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 0d384e75ba5f950d389281226e56515be195b604 Mon Sep 17 00:00:00 2001 From: Gabriel Belingueres Date: Sun, 17 Apr 2022 04:01:59 -0300 Subject: [PATCH 07/12] Fix some testing XML files checkout with incorrect eol --- .gitattributes | 4 ++++ .../org/codehaus/plexus/util/xml/pull/MXParserTest.java | 8 ++++---- .../xml/{test-entities-dos.xml => test-entities-DOS.xml} | 0 .../xml/{test-entities.xml => test-entities-UNIX.xml} | 0 ...ties-in-attr-dos.xml => test-entities-in-attr-DOS.xml} | 0 ...ntities-in-attr.xml => test-entities-in-attr-UNIX.xml} | 0 6 files changed, 8 insertions(+), 4 deletions(-) rename src/test/resources/xml/{test-entities-dos.xml => test-entities-DOS.xml} (100%) rename src/test/resources/xml/{test-entities.xml => test-entities-UNIX.xml} (100%) rename src/test/resources/xml/{test-entities-in-attr-dos.xml => test-entities-in-attr-DOS.xml} (100%) rename src/test/resources/xml/{test-entities-in-attr.xml => test-entities-in-attr-UNIX.xml} (100%) diff --git a/.gitattributes b/.gitattributes index 3bb3b5ea..ccd9f864 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,3 +6,7 @@ *.css text *.js text *.sql text + +# some files require the correct eol for proper testing +*-DOS.xml text eol=crlf +*-UNIX.xml text eol=lf diff --git a/src/test/java/org/codehaus/plexus/util/xml/pull/MXParserTest.java b/src/test/java/org/codehaus/plexus/util/xml/pull/MXParserTest.java index 6fc6e9c6..27ba5f8c 100644 --- a/src/test/java/org/codehaus/plexus/util/xml/pull/MXParserTest.java +++ b/src/test/java/org/codehaus/plexus/util/xml/pull/MXParserTest.java @@ -1081,7 +1081,7 @@ public void testCustomEntityNotFoundInAttrTokenize() throws Exception public void testDocdeclTextWithEntitiesUnix() throws IOException { - testDocdeclTextWithEntities( "test-entities.xml" ); + testDocdeclTextWithEntities( "test-entities-UNIX.xml" ); } /** @@ -1099,7 +1099,7 @@ public void testDocdeclTextWithEntitiesUnix() public void testDocdeclTextWithEntitiesDOS() throws IOException { - testDocdeclTextWithEntities( "test-entities-dos.xml" ); + testDocdeclTextWithEntities( "test-entities-DOS.xml" ); } private void testDocdeclTextWithEntities( String filename ) @@ -1144,7 +1144,7 @@ private void testDocdeclTextWithEntities( String filename ) public void testDocdeclTextWithEntitiesInAttributesUnix() throws IOException { - testDocdeclTextWithEntitiesInAttributes( "test-entities-in-attr.xml" ); + testDocdeclTextWithEntitiesInAttributes( "test-entities-in-attr-UNIX.xml" ); } /** @@ -1162,7 +1162,7 @@ public void testDocdeclTextWithEntitiesInAttributesUnix() public void testDocdeclTextWithEntitiesInAttributesDOS() throws IOException { - testDocdeclTextWithEntitiesInAttributes( "test-entities-in-attr-dos.xml" ); + testDocdeclTextWithEntitiesInAttributes( "test-entities-in-attr-DOS.xml" ); } private void testDocdeclTextWithEntitiesInAttributes( String filename ) diff --git a/src/test/resources/xml/test-entities-dos.xml b/src/test/resources/xml/test-entities-DOS.xml similarity index 100% rename from src/test/resources/xml/test-entities-dos.xml rename to src/test/resources/xml/test-entities-DOS.xml diff --git a/src/test/resources/xml/test-entities.xml b/src/test/resources/xml/test-entities-UNIX.xml similarity index 100% rename from src/test/resources/xml/test-entities.xml rename to src/test/resources/xml/test-entities-UNIX.xml diff --git a/src/test/resources/xml/test-entities-in-attr-dos.xml b/src/test/resources/xml/test-entities-in-attr-DOS.xml similarity index 100% rename from src/test/resources/xml/test-entities-in-attr-dos.xml rename to src/test/resources/xml/test-entities-in-attr-DOS.xml diff --git a/src/test/resources/xml/test-entities-in-attr.xml b/src/test/resources/xml/test-entities-in-attr-UNIX.xml similarity index 100% rename from src/test/resources/xml/test-entities-in-attr.xml rename to src/test/resources/xml/test-entities-in-attr-UNIX.xml From d3823dc54686cbd268b0cd8f7c6ef6058d163817 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 Apr 2022 02:02:31 +0000 Subject: [PATCH 08/12] Bump actions/checkout from 3.0.1 to 3.0.2 Bumps [actions/checkout](https://github.com/actions/checkout) from 3.0.1 to 3.0.2. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3.0.1...v3.0.2) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9935c621..6240fce9 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3.0.1 + uses: actions/checkout@v3.0.2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL From 2f68e930054aae96ca8f694774b07fd63741301c Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Sat, 23 Apr 2022 19:43:51 +0200 Subject: [PATCH 09/12] Fix regression and deprecate: FileUtils.fileAppend should create file if not exist. --- src/main/java/org/codehaus/plexus/util/FileUtils.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/codehaus/plexus/util/FileUtils.java b/src/main/java/org/codehaus/plexus/util/FileUtils.java index cac82ba3..4dca8b3c 100644 --- a/src/main/java/org/codehaus/plexus/util/FileUtils.java +++ b/src/main/java/org/codehaus/plexus/util/FileUtils.java @@ -389,6 +389,8 @@ private static InputStreamReader getInputStreamReader( File file, String encodin * @param fileName The path of the file to write. * @param data The content to write to the file. * @throws IOException if any + * @deprecated use {@code java.nio.files.Files.write(filename, data.getBytes(encoding), + * StandardOpenOption.APPEND, StandardOpenOption.CREATE)} */ public static void fileAppend( String fileName, String data ) throws IOException @@ -403,11 +405,14 @@ public static void fileAppend( String fileName, String data ) * @param encoding The encoding of the file. * @param data The content to write to the file. * @throws IOException if any + * @deprecated use {@code java.nio.files.Files.write(filename, data.getBytes(encoding), + * StandardOpenOption.APPEND, StandardOpenOption.CREATE)} */ public static void fileAppend( String fileName, String encoding, String data ) throws IOException { - try ( OutputStream out = Files.newOutputStream( Paths.get(fileName), StandardOpenOption.APPEND ) ) + try ( OutputStream out = Files.newOutputStream( Paths.get(fileName), + StandardOpenOption.APPEND, StandardOpenOption.CREATE ) ) { if ( encoding != null ) { From 0808fe36dde238e6824043d5ca597b7cec1bd5c7 Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Sat, 23 Apr 2022 22:33:48 +0200 Subject: [PATCH 10/12] Added Licence file Closes #78 --- LICENSE.txt | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 LICENSE.txt diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. From 49773f19e43af7d39f441aac06adbc6a4052a063 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Tue, 26 Apr 2022 15:42:15 +0200 Subject: [PATCH 11/12] Provides a CachingOuptutStream and a CachingWriter (#184) Provides a CachingOuptutStream and a CachingWriter --- .../plexus/util/io/CachingOutputStream.java | 175 ++++++++++++++++++ .../plexus/util/io/CachingWriter.java | 62 +++++++ .../util/io/CachingOutputStreamTest.java | 145 +++++++++++++++ .../plexus/util/io/CachingWriterTest.java | 143 ++++++++++++++ 4 files changed, 525 insertions(+) create mode 100644 src/main/java/org/codehaus/plexus/util/io/CachingOutputStream.java create mode 100644 src/main/java/org/codehaus/plexus/util/io/CachingWriter.java create mode 100644 src/test/java/org/codehaus/plexus/util/io/CachingOutputStreamTest.java create mode 100644 src/test/java/org/codehaus/plexus/util/io/CachingWriterTest.java diff --git a/src/main/java/org/codehaus/plexus/util/io/CachingOutputStream.java b/src/main/java/org/codehaus/plexus/util/io/CachingOutputStream.java new file mode 100644 index 00000000..521d5373 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/util/io/CachingOutputStream.java @@ -0,0 +1,175 @@ +package org.codehaus.plexus.util.io; + +/* + * Copyright The Codehaus Foundation. + * + * 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. + */ + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.util.Objects; + +/** + * Caching OutputStream to avoid overwriting a file with + * the same content. + */ +public class CachingOutputStream extends OutputStream +{ + private final Path path; + private FileChannel channel; + private ByteBuffer readBuffer; + private ByteBuffer writeBuffer; + private boolean modified; + + public CachingOutputStream( File path ) throws IOException + { + this( Objects.requireNonNull( path ).toPath() ); + } + + public CachingOutputStream( Path path ) throws IOException + { + this( path, 32 * 1024 ); + } + + public CachingOutputStream( Path path, int bufferSize ) throws IOException + { + this.path = Objects.requireNonNull( path ); + this.channel = FileChannel.open( path, + StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE ); + this.readBuffer = ByteBuffer.allocate( bufferSize ); + this.writeBuffer = ByteBuffer.allocate( bufferSize ); + } + + @Override + public void write( int b ) throws IOException + { + if ( writeBuffer.remaining() < 1 ) + { + ( ( Buffer ) writeBuffer ).flip(); + flushBuffer( writeBuffer ); + ( ( Buffer ) writeBuffer ).clear(); + } + writeBuffer.put( ( byte ) b ); + } + + @Override + public void write( byte[] b ) throws IOException + { + write( b, 0, b.length ); + } + + @Override + public void write( byte[] b, int off, int len ) throws IOException + { + if ( writeBuffer.remaining() < len ) + { + ( ( Buffer ) writeBuffer ).flip(); + flushBuffer( writeBuffer ); + ( ( Buffer ) writeBuffer ).clear(); + } + int capacity = writeBuffer.capacity(); + while ( len >= capacity ) + { + flushBuffer( ByteBuffer.wrap( b, off, capacity ) ); + off += capacity; + len -= capacity; + } + if ( len > 0 ) + { + writeBuffer.put( b, off, len ); + } + } + + @Override + public void flush() throws IOException + { + ( ( Buffer ) writeBuffer ).flip(); + flushBuffer( writeBuffer ); + ( ( Buffer ) writeBuffer ).clear(); + super.flush(); + } + + private void flushBuffer( ByteBuffer writeBuffer ) throws IOException + { + if ( modified ) + { + channel.write( writeBuffer ); + } + else + { + int len = writeBuffer.remaining(); + ByteBuffer readBuffer; + if ( this.readBuffer.capacity() >= len ) + { + readBuffer = this.readBuffer; + ( ( Buffer ) readBuffer ).clear(); + } + else + { + readBuffer = ByteBuffer.allocate( len ); + } + while ( len > 0 ) + { + int read = channel.read( readBuffer ); + if ( read <= 0 ) + { + modified = true; + channel.position( channel.position() - readBuffer.position() ); + channel.write( writeBuffer ); + return; + } + len -= read; + } + ( ( Buffer ) readBuffer ).flip(); + if ( readBuffer.compareTo( writeBuffer ) != 0 ) + { + modified = true; + channel.position( channel.position() - readBuffer.remaining() ); + channel.write( writeBuffer ); + } + } + } + + @Override + public void close() throws IOException + { + flush(); + long position = channel.position(); + if ( position != channel.size() ) + { + if ( !modified ) + { + FileTime now = FileTime.from( Instant.now() ); + Files.setLastModifiedTime( path, now ); + modified = true; + } + channel.truncate( position ); + } + channel.close(); + } + + public boolean isModified() + { + return modified; + } +} diff --git a/src/main/java/org/codehaus/plexus/util/io/CachingWriter.java b/src/main/java/org/codehaus/plexus/util/io/CachingWriter.java new file mode 100644 index 00000000..23cc4411 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/util/io/CachingWriter.java @@ -0,0 +1,62 @@ +package org.codehaus.plexus.util.io; + +/* + * Copyright The Codehaus Foundation. + * + * 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. + */ + +import java.io.File; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.StringWriter; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Objects; + +/** + * Caching Writer to avoid overwriting a file with + * the same content. + */ +public class CachingWriter extends OutputStreamWriter +{ + private final CachingOutputStream cos; + + public CachingWriter( File path, Charset charset ) throws IOException + { + this( Objects.requireNonNull( path ).toPath(), charset ); + } + + public CachingWriter( Path path, Charset charset ) throws IOException + { + this( path, charset, 32 * 1024 ); + } + + public CachingWriter( Path path, Charset charset, int bufferSize ) throws IOException + { + this( new CachingOutputStream( path, bufferSize ), charset ); + } + + private CachingWriter( CachingOutputStream outputStream, Charset charset ) throws IOException + { + super( outputStream, charset ); + this.cos = outputStream; + } + + public boolean isModified() + { + return cos.isModified(); + } +} diff --git a/src/test/java/org/codehaus/plexus/util/io/CachingOutputStreamTest.java b/src/test/java/org/codehaus/plexus/util/io/CachingOutputStreamTest.java new file mode 100644 index 00000000..3c329ea9 --- /dev/null +++ b/src/test/java/org/codehaus/plexus/util/io/CachingOutputStreamTest.java @@ -0,0 +1,145 @@ +package org.codehaus.plexus.util.io; + +/* + * Copyright The Codehaus Foundation. + * + * 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. + */ + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.FileTime; +import java.util.Objects; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +public class CachingOutputStreamTest +{ + + Path tempDir; + Path checkLastModified; + FileTime lm; + + @Before + public void setup() throws IOException + { + Path dir = Paths.get( "target/io" ); + Files.createDirectories( dir ); + tempDir = Files.createTempDirectory( dir, "temp-" ); + checkLastModified = tempDir.resolve( ".check" ); + Files.newOutputStream( checkLastModified ).close(); + lm = Files.getLastModifiedTime( checkLastModified ); + } + + private void waitLastModified() throws IOException, InterruptedException + { + while ( true ) + { + Files.newOutputStream( checkLastModified ).close(); + FileTime nlm = Files.getLastModifiedTime( checkLastModified ); + if ( !Objects.equals( nlm, lm ) ) + { + lm = nlm; + break; + } + Thread.sleep( 10 ); + } + } + + @Test + public void testWriteNoExistingFile() throws IOException, InterruptedException + { + byte[] data = "Hello world!".getBytes( StandardCharsets.UTF_8 ); + Path path = tempDir.resolve( "file.txt" ); + assertFalse( Files.exists( path ) ); + + try ( CachingOutputStream cos = new CachingOutputStream( path, 4 ) ) + { + cos.write( data ); + } + assertTrue( Files.exists( path ) ); + byte[] read = Files.readAllBytes( path ); + assertArrayEquals( data, read ); + FileTime modified = Files.getLastModifiedTime( path ); + + waitLastModified(); + + try ( CachingOutputStream cos = new CachingOutputStream( path, 4 ) ) + { + cos.write( data ); + } + assertTrue( Files.exists( path ) ); + read = Files.readAllBytes( path ); + assertArrayEquals( data, read ); + FileTime newModified = Files.getLastModifiedTime( path ); + assertEquals( modified, newModified ); + modified = newModified; + + waitLastModified(); + + // write longer data + data = "Good morning!".getBytes( StandardCharsets.UTF_8 ); + try ( CachingOutputStream cos = new CachingOutputStream( path, 4 ) ) + { + cos.write( data ); + } + assertTrue( Files.exists( path ) ); + read = Files.readAllBytes( path ); + assertArrayEquals( data, read ); + newModified = Files.getLastModifiedTime( path ); + assertNotEquals( modified, newModified ); + modified = newModified; + + waitLastModified(); + + // different data same size + data = "Good mornong!".getBytes( StandardCharsets.UTF_8 ); + try ( CachingOutputStream cos = new CachingOutputStream( path, 4 ) ) + { + cos.write( data ); + } + assertTrue( Files.exists( path ) ); + read = Files.readAllBytes( path ); + assertArrayEquals( data, read ); + newModified = Files.getLastModifiedTime( path ); + assertNotEquals( modified, newModified ); + modified = newModified; + + waitLastModified(); + + // same data but shorter + data = "Good mornon".getBytes( StandardCharsets.UTF_8 ); + try ( CachingOutputStream cos = new CachingOutputStream( path, 4 ) ) + { + cos.write( data ); + } + assertTrue( Files.exists( path ) ); + read = Files.readAllBytes( path ); + assertArrayEquals( data, read ); + newModified = Files.getLastModifiedTime( path ); + assertNotEquals( modified, newModified ); + modified = newModified; + } + +} diff --git a/src/test/java/org/codehaus/plexus/util/io/CachingWriterTest.java b/src/test/java/org/codehaus/plexus/util/io/CachingWriterTest.java new file mode 100644 index 00000000..f5b95903 --- /dev/null +++ b/src/test/java/org/codehaus/plexus/util/io/CachingWriterTest.java @@ -0,0 +1,143 @@ +package org.codehaus.plexus.util.io; + +/* + * Copyright The Codehaus Foundation. + * + * 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. + */ + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.FileTime; +import java.util.Objects; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +public class CachingWriterTest +{ + + Path tempDir; + Path checkLastModified; + FileTime lm; + + @Before + public void setup() throws IOException + { + Path dir = Paths.get( "target/io" ); + Files.createDirectories( dir ); + tempDir = Files.createTempDirectory( dir, "temp-" ); + checkLastModified = tempDir.resolve( ".check" ); + Files.newOutputStream( checkLastModified ).close(); + lm = Files.getLastModifiedTime( checkLastModified ); + } + + private void waitLastModified() throws IOException, InterruptedException + { + while ( true ) + { + Files.newOutputStream( checkLastModified ).close(); + FileTime nlm = Files.getLastModifiedTime( checkLastModified ); + if ( !Objects.equals( nlm, lm ) ) + { + lm = nlm; + break; + } + Thread.sleep( 10 ); + } + } + + @Test + public void testWriteNoExistingFile() throws IOException, InterruptedException + { + String data = "Hello world!"; + Path path = tempDir.resolve( "file.txt" ); + assertFalse( Files.exists( path ) ); + + try ( CachingWriter cos = new CachingWriter( path, StandardCharsets.UTF_8, 4 ) ) + { + cos.write( data ); + } + assertTrue( Files.exists( path ) ); + String read = new String( Files.readAllBytes( path ), StandardCharsets.UTF_8 ); + assertEquals( data, read ); + FileTime modified = Files.getLastModifiedTime( path ); + + waitLastModified(); + + try ( CachingWriter cos = new CachingWriter( path, StandardCharsets.UTF_8, 4 ) ) + { + cos.write( data ); + } + assertTrue( Files.exists( path ) ); + read = new String( Files.readAllBytes( path ), StandardCharsets.UTF_8 ); + assertEquals( data, read ); + FileTime newModified = Files.getLastModifiedTime( path ); + assertEquals( modified, newModified ); + modified = newModified; + + waitLastModified(); + + // write longer data + data = "Good morning!"; + try ( CachingWriter cos = new CachingWriter( path, StandardCharsets.UTF_8, 4 ) ) + { + cos.write( data ); + } + assertTrue( Files.exists( path ) ); + read = new String( Files.readAllBytes( path ), StandardCharsets.UTF_8 ); + assertEquals( data, read ); + newModified = Files.getLastModifiedTime( path ); + assertNotEquals( modified, newModified ); + modified = newModified; + + waitLastModified(); + + // different data same size + data = "Good mornong!"; + try ( CachingWriter cos = new CachingWriter( path, StandardCharsets.UTF_8, 4 ) ) + { + cos.write( data ); + } + assertTrue( Files.exists( path ) ); + read = new String( Files.readAllBytes( path ), StandardCharsets.UTF_8 ); + assertEquals( data, read ); + newModified = Files.getLastModifiedTime( path ); + assertNotEquals( modified, newModified ); + modified = newModified; + + waitLastModified(); + + // same data but shorter + data = "Good mornon"; + try ( CachingWriter cos = new CachingWriter( path, StandardCharsets.UTF_8, 4 ) ) + { + cos.write( data ); + } + assertTrue( Files.exists( path ) ); + read = new String( Files.readAllBytes( path ), StandardCharsets.UTF_8 ); + assertEquals( data, read ); + newModified = Files.getLastModifiedTime( path ); + assertNotEquals( modified, newModified ); + modified = newModified; + } +} From ee5fe41b032f95f9b39f6ef21256800a6608dfd6 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Tue, 26 Apr 2022 17:44:51 +0200 Subject: [PATCH 12/12] Fix last modified time not being updated on linux (#203) Fix test, clean things --- .../plexus/util/io/CachingOutputStream.java | 14 ++++++-------- .../plexus/util/io/CachingOutputStreamTest.java | 7 ++----- .../codehaus/plexus/util/io/CachingWriterTest.java | 7 ++----- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/codehaus/plexus/util/io/CachingOutputStream.java b/src/main/java/org/codehaus/plexus/util/io/CachingOutputStream.java index 521d5373..744e6f06 100644 --- a/src/main/java/org/codehaus/plexus/util/io/CachingOutputStream.java +++ b/src/main/java/org/codehaus/plexus/util/io/CachingOutputStream.java @@ -153,19 +153,17 @@ private void flushBuffer( ByteBuffer writeBuffer ) throws IOException @Override public void close() throws IOException { - flush(); - long position = channel.position(); - if ( position != channel.size() ) + if ( channel.isOpen() ) { - if ( !modified ) + flush(); + long position = channel.position(); + if ( position != channel.size() ) { - FileTime now = FileTime.from( Instant.now() ); - Files.setLastModifiedTime( path, now ); modified = true; + channel.truncate( position ); } - channel.truncate( position ); + channel.close(); } - channel.close(); } public boolean isModified() diff --git a/src/test/java/org/codehaus/plexus/util/io/CachingOutputStreamTest.java b/src/test/java/org/codehaus/plexus/util/io/CachingOutputStreamTest.java index 3c329ea9..e7888ad4 100644 --- a/src/test/java/org/codehaus/plexus/util/io/CachingOutputStreamTest.java +++ b/src/test/java/org/codehaus/plexus/util/io/CachingOutputStreamTest.java @@ -17,7 +17,6 @@ */ import java.io.IOException; -import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -39,7 +38,6 @@ public class CachingOutputStreamTest Path tempDir; Path checkLastModified; - FileTime lm; @Before public void setup() throws IOException @@ -48,19 +46,18 @@ public void setup() throws IOException Files.createDirectories( dir ); tempDir = Files.createTempDirectory( dir, "temp-" ); checkLastModified = tempDir.resolve( ".check" ); - Files.newOutputStream( checkLastModified ).close(); - lm = Files.getLastModifiedTime( checkLastModified ); } private void waitLastModified() throws IOException, InterruptedException { + Files.newOutputStream( checkLastModified ).close(); + FileTime lm = Files.getLastModifiedTime( checkLastModified ); while ( true ) { Files.newOutputStream( checkLastModified ).close(); FileTime nlm = Files.getLastModifiedTime( checkLastModified ); if ( !Objects.equals( nlm, lm ) ) { - lm = nlm; break; } Thread.sleep( 10 ); diff --git a/src/test/java/org/codehaus/plexus/util/io/CachingWriterTest.java b/src/test/java/org/codehaus/plexus/util/io/CachingWriterTest.java index f5b95903..a4ffec91 100644 --- a/src/test/java/org/codehaus/plexus/util/io/CachingWriterTest.java +++ b/src/test/java/org/codehaus/plexus/util/io/CachingWriterTest.java @@ -27,7 +27,6 @@ import org.junit.Before; import org.junit.Test; -import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; @@ -38,7 +37,6 @@ public class CachingWriterTest Path tempDir; Path checkLastModified; - FileTime lm; @Before public void setup() throws IOException @@ -47,19 +45,18 @@ public void setup() throws IOException Files.createDirectories( dir ); tempDir = Files.createTempDirectory( dir, "temp-" ); checkLastModified = tempDir.resolve( ".check" ); - Files.newOutputStream( checkLastModified ).close(); - lm = Files.getLastModifiedTime( checkLastModified ); } private void waitLastModified() throws IOException, InterruptedException { + Files.newOutputStream( checkLastModified ).close(); + FileTime lm = Files.getLastModifiedTime( checkLastModified ); while ( true ) { Files.newOutputStream( checkLastModified ).close(); FileTime nlm = Files.getLastModifiedTime( checkLastModified ); if ( !Objects.equals( nlm, lm ) ) { - lm = nlm; break; } Thread.sleep( 10 );