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/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b0dfc833..6ede3405 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -32,11 +32,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v3.0.2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -47,7 +47,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -61,4 +61,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 10cbc78e..776d2439 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -21,35 +21,5 @@ on: [push, pull_request] jobs: build: - - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macOS-latest] - java: [8, 11, 16, 17-ea] - jdk: [temurin, zulu, adopt-openj9] - fail-fast: false - - runs-on: ${{ matrix.os }} - - steps: - - name: Checkout - uses: actions/checkout@v2.3.4 - - - name: Set up cache for ~./m2/repository - uses: actions/cache@v2.1.6 - with: - path: ~/.m2/repository - key: maven-${{ matrix.os }}-java${{ matrix.java }}-${{ hashFiles('**/pom.xml') }} - restore-keys: | - maven-${{ matrix.os }}-java${{ matrix.java }}- - maven-${{ matrix.os }}- - - - name: Set up JDK - uses: actions/setup-java@v2.3.0 - with: - cache: 'maven' - distribution: ${{ matrix.jdk }} - java-version: ${{ matrix.java }} - - - name: Build with Maven - run: mvn verify javadoc:javadoc -e -B -V + name: Build it + uses: codehaus-plexus/.github/.github/workflows/maven.yml@v0.0.1 diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 4e2af995..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.15.0 + - uses: release-drafter/release-drafter@v5.19.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 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. diff --git a/pom.xml b/pom.xml index 164b7b15..f9342b1f 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ limitations under the License. plexus-utils - 3.4.1 + 3.4.2 Plexus Common Utilities A collection of various utility classes to ease working with strings, files, command lines, XML and @@ -37,7 +37,7 @@ limitations under the License. scm:git:git@github.com:codehaus-plexus/plexus-utils.git scm:git:git@github.com:codehaus-plexus/plexus-utils.git http://github.com/codehaus-plexus/plexus-utils - plexus-utils-3.4.1 + plexus-utils-3.4.2 github @@ -51,7 +51,7 @@ limitations under the License. - 2021-08-27T00:06:38Z + 2022-05-17T20:30:09Z @@ -64,13 +64,13 @@ limitations under the License. org.openjdk.jmh jmh-core - 1.33 + 1.35 test org.openjdk.jmh jmh-generator-annprocess - 1.33 + 1.35 test diff --git a/src/main/java/org/codehaus/plexus/util/AbstractScanner.java b/src/main/java/org/codehaus/plexus/util/AbstractScanner.java index af3fbc4a..16d64ef9 100644 --- a/src/main/java/org/codehaus/plexus/util/AbstractScanner.java +++ b/src/main/java/org/codehaus/plexus/util/AbstractScanner.java @@ -42,8 +42,8 @@ public abstract class AbstractScanner *
  • SurroundSCM: **/.MySCMServerInfo
  • *
  • Mac: **/.DS_Store
  • *
  • Serena Dimension: **/.metadata, **/.metadata/**
  • - *
  • Mercurial: **/.hg, **/.hg/**, **/.hgignore
  • - *
  • GIT: **/.git, **/.gitignore, **/.gitattributes, **/.git/**
  • + *
  • Mercurial: **/.hg, **/.hg/**
  • + *
  • Git: **/.git, **/.git/**
  • *
  • Bitkeeper: **/BitKeeper, **/BitKeeper/**, **/ChangeSet, * **/ChangeSet/**
  • *
  • Darcs: **/_darcs, **/_darcs/**, **/.darcsrepo, @@ -90,10 +90,10 @@ public abstract class AbstractScanner "**/.metadata", "**/.metadata/**", // Mercurial - "**/.hg", "**/.hgignore", "**/.hg/**", + "**/.hg", "**/.hg/**", // git - "**/.git", "**/.gitignore", "**/.gitattributes", "**/.git/**", + "**/.git", "**/.git/**", // BitKeeper "**/BitKeeper", "**/BitKeeper/**", "**/ChangeSet", "**/ChangeSet/**", @@ -124,7 +124,7 @@ public abstract class AbstractScanner * @since 3.3.0 */ protected Comparator filenameComparator; - + /** * Sets whether or not the file system should be regarded as case sensitive. * @@ -137,7 +137,7 @@ public void setCaseSensitive( boolean isCaseSensitive ) /** *

    Tests whether or not a given path matches the start of a given pattern up to the first "**".

    - * + * *

    This is not a general purpose test and should only be used if you can live with false positives. For example, * pattern=**\a and str=b will yield true.

    * @@ -152,7 +152,7 @@ protected static boolean matchPatternStart( String pattern, String str ) /** *

    Tests whether or not a given path matches the start of a given pattern up to the first "**".

    - * + * *

    This is not a general purpose test and should only be used if you can live with false positives. For example, * pattern=**\a and str=b will yield true.

    * @@ -223,7 +223,7 @@ protected static boolean match( String pattern, String str, boolean isCaseSensit /** *

    Sets the list of include patterns to use. All '/' and '\' characters are replaced by * File.separatorChar, so the separator used need not match File.separatorChar.

    - * + * *

    When a pattern ends with a '/' or '\', "**" is appended.

    * * @param includes A list of include patterns. May be null, indicating that all files should be @@ -253,7 +253,7 @@ public void setIncludes( String[] includes ) /** *

    Sets the list of exclude patterns to use. All '/' and '\' characters are replaced by * File.separatorChar, so the separator used need not match File.separatorChar.

    - * + * *

    When a pattern ends with a '/' or '\', "**" is appended.

    * * @param excludes A list of exclude patterns. May be null, indicating that no files should be 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 ) { diff --git a/src/main/java/org/codehaus/plexus/util/SelectorUtils.java b/src/main/java/org/codehaus/plexus/util/SelectorUtils.java index de39f4fe..234a92c5 100644 --- a/src/main/java/org/codehaus/plexus/util/SelectorUtils.java +++ b/src/main/java/org/codehaus/plexus/util/SelectorUtils.java @@ -253,21 +253,32 @@ public static boolean matchPath( String pattern, String str, String separator, b { if ( isRegexPrefixedPattern( pattern ) ) { - pattern = + String localPattern = pattern.substring( REGEX_HANDLER_PREFIX.length(), pattern.length() - PATTERN_HANDLER_SUFFIX.length() ); - return str.matches( pattern ); + return str.matches( localPattern ); } else { - if ( isAntPrefixedPattern( pattern ) ) - { - pattern = pattern.substring( ANT_HANDLER_PREFIX.length(), - pattern.length() - PATTERN_HANDLER_SUFFIX.length() ); - } + String localPattern = isAntPrefixedPattern( pattern ) + ? pattern.substring( ANT_HANDLER_PREFIX.length(), pattern.length() - PATTERN_HANDLER_SUFFIX.length() ) + : pattern; + final String osRelatedPath = toOSRelatedPath( str, separator ); + final String osRelatedPattern = toOSRelatedPath( localPattern, separator ); + return matchAntPathPattern( osRelatedPattern, osRelatedPath, separator, isCaseSensitive ); + } + } - return matchAntPathPattern( pattern, str, separator, isCaseSensitive ); + private static String toOSRelatedPath( String pattern, String separator ) + { + if ( "/".equals( separator ) ) + { + return pattern.replace( "\\", separator ); + } + if ( "\\".equals( separator ) ) { + return pattern.replace( "/", separator ); } + return pattern; } static boolean isRegexPrefixedPattern( String pattern ) 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..744e6f06 --- /dev/null +++ b/src/main/java/org/codehaus/plexus/util/io/CachingOutputStream.java @@ -0,0 +1,173 @@ +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 + { + if ( channel.isOpen() ) + { + flush(); + long position = channel.position(); + if ( position != channel.size() ) + { + 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/main/java/org/codehaus/plexus/util/xml/pull/MXParser.java b/src/main/java/org/codehaus/plexus/util/xml/pull/MXParser.java index 3874f572..d44c9a7f 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 @@ -464,7 +464,7 @@ private void reset() // System.out.println("reset() called"); location = null; lineNumber = 1; - columnNumber = 0; + columnNumber = 1; seenRoot = false; reachedEnd = false; eventType = START_DOCUMENT; @@ -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; } /** - * Check if the provided parameter is a valid Char, according to: {@link https://www.w3.org/TR/REC-xml/#NT-Char} + * 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 + * 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() @@ -2935,7 +2988,7 @@ private void parseComment() posStart = pos; final int curLine = lineNumber; - final int curColumn = columnNumber; + final int curColumn = columnNumber - 4; try { final boolean normalizeIgnorableWS = tokenize && !roundtripSupported; @@ -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 ) { @@ -3056,7 +3109,7 @@ private boolean parsePI() if ( tokenize ) posStart = pos; final int curLine = lineNumber; - final int curColumn = columnNumber; + final int curColumn = columnNumber - 2; int piTargetStart = pos; int piTargetEnd = -1; final boolean normalizeIgnorableWS = tokenize && !roundtripSupported; @@ -3105,6 +3158,10 @@ else if ( !seenInnerTag ) throw new XmlPullParserException( "processing instruction started on line " + curLine + " and column " + curColumn + " was not closed", this, null ); } + else + { + seenInnerTag = false; + } } else if ( ch == '<' ) { @@ -3484,7 +3541,8 @@ else if ( ch == '>' && bracketLevel == 0 ) break; else if ( ch == '&' ) { - extractEntityRef(); + extractEntityRefInDocDecl(); + continue; } if ( normalizeIgnorableWS ) { @@ -3536,6 +3594,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 +3630,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 +3642,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 ) { @@ -3893,7 +3964,7 @@ private char more() fillBuf(); // this return value should be ignored as it is used in epilog parsing ... if ( reachedEnd ) - return (char) -1; + throw new EOFException( "no more data available" + getPositionDescription() ); } final char ch = buf[pos++]; // line/columnNumber 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 { diff --git a/src/test/java/org/codehaus/plexus/util/SelectorUtilsTest.java b/src/test/java/org/codehaus/plexus/util/SelectorUtilsTest.java index ebc2dca0..160aa16d 100644 --- a/src/test/java/org/codehaus/plexus/util/SelectorUtilsTest.java +++ b/src/test/java/org/codehaus/plexus/util/SelectorUtilsTest.java @@ -27,7 +27,6 @@ *

    SelectorUtilsTest class.

    * * @author herve - * @version $Id: $Id * @since 3.4.0 */ public class SelectorUtilsTest @@ -92,4 +91,55 @@ public void testMatchPath_WindowsFileSeparator() // Pattern and target don't start with file separator assertTrue( SelectorUtils.matchPath( "*" + separator + "a.txt", "b" + separator + "a.txt", separator, false ) ); } + + @Test + public void testPatternMatchSingleWildcardPosix() + { + assertFalse(SelectorUtils.matchPath( + "/com/test/*", + "/com/test/test/hallo")); + } + + + @Test + public void testPatternMatchDoubleWildcardCaseInsensitivePosix() + { + assertTrue(SelectorUtils.matchPath( + "/com/test/**", + "/com/test/test/hallo")); + } + + + @Test + public void testPatternMatchDoubleWildcardPosix() + { + assertTrue(SelectorUtils.matchPath( + "/com/test/**", + "/com/test/test/hallo")); + } + + + @Test + public void testPatternMatchSingleWildcardWindows() + { + assertFalse(SelectorUtils.matchPath( + "D:\\com\\test\\*", + "D:\\com\\test\\test\\hallo")); + + assertFalse(SelectorUtils.matchPath( + "D:/com/test/*", + "D:/com/test/test/hallo")); + } + + @Test + public void testPatternMatchDoubleWildcardWindows() + { + assertTrue(SelectorUtils.matchPath( + "D:\\com\\test\\**", + "D:\\com\\test\\test\\hallo")); + + assertTrue(SelectorUtils.matchPath( + "D:\\com\\test\\**", + "D:/com/test/test/hallo")); + } } 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..e7888ad4 --- /dev/null +++ b/src/test/java/org/codehaus/plexus/util/io/CachingOutputStreamTest.java @@ -0,0 +1,142 @@ +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 CachingOutputStreamTest +{ + + Path tempDir; + Path checkLastModified; + + @Before + public void setup() throws IOException + { + Path dir = Paths.get( "target/io" ); + Files.createDirectories( dir ); + tempDir = Files.createTempDirectory( dir, "temp-" ); + checkLastModified = tempDir.resolve( ".check" ); + } + + 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 ) ) + { + 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..a4ffec91 --- /dev/null +++ b/src/test/java/org/codehaus/plexus/util/io/CachingWriterTest.java @@ -0,0 +1,140 @@ +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.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; + + @Before + public void setup() throws IOException + { + Path dir = Paths.get( "target/io" ); + Files.createDirectories( dir ); + tempDir = Files.createTempDirectory( dir, "temp-" ); + checkLastModified = tempDir.resolve( ".check" ); + } + + 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 ) ) + { + 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; + } +} 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..e5e04708 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; @@ -351,6 +353,35 @@ public void testValidCharacterReferenceDecimal() * * @throws java.lang.Exception if any. */ + @Test + public void testParserPosition() + throws Exception + { + String input = " \n \tnnn\n"; + + MXParser parser = new MXParser(); + parser.setInput( new StringReader( input ) ); + + assertEquals( XmlPullParser.PROCESSING_INSTRUCTION, parser.nextToken() ); + assertPosition( 1, 39, parser ); + assertEquals( XmlPullParser.COMMENT, parser.nextToken() ); + assertPosition( 1, 49, parser ); + assertEquals( XmlPullParser.IGNORABLE_WHITESPACE, parser.nextToken() ); + assertPosition( 2, 3, parser ); // end when next token starts + assertEquals( XmlPullParser.COMMENT, parser.nextToken() ); + assertPosition( 2, 12, parser ); + assertEquals( XmlPullParser.START_TAG, parser.nextToken() ); + assertPosition( 2, 18, parser ); + assertEquals( XmlPullParser.TEXT, parser.nextToken() ); + assertPosition( 2, 23, parser ); // end when next token starts + assertEquals( XmlPullParser.END_TAG, parser.nextToken() ); + assertPosition( 2, 29, parser ); + assertEquals( XmlPullParser.IGNORABLE_WHITESPACE, parser.nextToken() ); + assertPosition( 3, 2, parser ); // end when next token starts + assertEquals( XmlPullParser.COMMENT, parser.nextToken() ); + assertPosition( 4, 6, parser ); + } + @Test public void testProcessingInstruction() throws Exception @@ -401,6 +432,37 @@ public void testProcessingInstructionsContainingXml() * * @throws java.lang.Exception if any. */ + @Test + public void testMalformedProcessingInstructionsContainingXmlNoClosingQuestionMark() + throws Exception + { + StringBuffer sb = new StringBuffer(); + sb.append( "\n" ); + sb.append( "\n" ); + sb.append( "\n" ); + sb.append( " >\n" ); + + MXParser parser = new MXParser(); + parser.setInput( new StringReader( sb.toString() ) ); + + try + { + assertEquals( XmlPullParser.PROCESSING_INSTRUCTION, parser.nextToken() ); + assertEquals( XmlPullParser.IGNORABLE_WHITESPACE, parser.nextToken() ); + assertEquals( XmlPullParser.START_TAG, parser.nextToken() ); + assertEquals( XmlPullParser.END_TAG, parser.nextToken() ); + assertEquals( XmlPullParser.IGNORABLE_WHITESPACE, parser.nextToken() ); + assertEquals( XmlPullParser.PROCESSING_INSTRUCTION, parser.nextToken() ); + + fail( "Should fail since it has invalid PI" ); + } + catch ( XmlPullParserException ex ) + { + assertTrue( ex.getMessage().contains( "processing instruction started on line 3 and column 1 was not closed" ) ); + } + } + @Test public void testSubsequentProcessingInstructionShort() throws Exception @@ -622,7 +684,7 @@ public void testMalformedProcessingInstructionNoClosingQuestionMark() } catch ( XmlPullParserException ex ) { - assertTrue( ex.getMessage().contains( "processing instruction started on line 1 and column 2 was not closed" ) ); + assertTrue( ex.getMessage().contains( "processing instruction started on line 1 and column 1 was not closed" ) ); } } @@ -655,7 +717,7 @@ public void testSubsequentMalformedProcessingInstructionNoClosingQuestionMark() } catch ( XmlPullParserException ex ) { - assertTrue( ex.getMessage().contains( "processing instruction started on line 1 and column 13 was not closed" ) ); + assertTrue( ex.getMessage().contains( "processing instruction started on line 1 and column 12 was not closed" ) ); } } @@ -664,6 +726,58 @@ public void testSubsequentMalformedProcessingInstructionNoClosingQuestionMark() * * @throws java.lang.Exception if any. */ + @Test + public void testSubsequentAbortedProcessingInstruction() + throws Exception + { + MXParser parser = new MXParser(); + StringBuilder sb = new StringBuilder(); + sb.append( "" ); + sb.append( "" ); + sb.append( "\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-UNIX.xml b/src/test/resources/xml/test-entities-UNIX.xml new file mode 100644 index 00000000..e1d6d17a --- /dev/null +++ b/src/test/resources/xml/test-entities-UNIX.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-UNIX.xml b/src/test/resources/xml/test-entities-in-attr-UNIX.xml new file mode 100644 index 00000000..a423c995 --- /dev/null +++ b/src/test/resources/xml/test-entities-in-attr-UNIX.xml @@ -0,0 +1,9 @@ + + + + + + +]> +&myCustomEntity; \ No newline at end of file