michael-o commented on code in PR #22: URL: https://github.com/apache/maven-archiver/pull/22#discussion_r902241569
########## src/main/java/org/apache/maven/archiver/MavenArchiver.java: ########## @@ -97,6 +101,10 @@ "${artifact.groupIdPath}/${artifact.artifactId}/" + "${artifact.baseVersion}/${artifact.artifactId}-" + "${artifact.baseVersion}${dashClassifier?}.${artifact.extension}"; + private static final ZonedDateTime DATE_MIN = ZonedDateTime.parse( "1980-01-01T00:00:02Z" ); + + private static final ZonedDateTime DATE_MAX = ZonedDateTime.parse( "2099-12-31T23:59:59Z" ); Review Comment: Isn't this for Zip files only? ########## src/main/java/org/apache/maven/archiver/MavenArchiver.java: ########## @@ -812,28 +820,78 @@ public void setBuildJdkSpecDefaultEntry( boolean buildJdkSpecDefaultEntry ) * @return the parsed timestamp, may be <code>null</code> if <code>null</code> input or input contains only 1 * character * @since 3.5.0 - * @throws java.lang.IllegalArgumentException if the outputTimestamp is neither ISO 8601 nor an integer + * @throws IllegalArgumentException if the outputTimestamp is neither ISO 8601 nor an integer + * @deprecated Use {@link #parseBuildOutputTimestamp(String)} instead. */ + @Deprecated public Date parseOutputTimestamp( String outputTimestamp ) { - if ( StringUtils.isNumeric( outputTimestamp ) && StringUtils.isNotEmpty( outputTimestamp ) ) + return parseBuildOutputTimestamp( outputTimestamp ).map( Date::from ).orElse( null ); + } + + /** + * Configure Reproducible Builds archive creation if a timestamp is provided. + * + * @param outputTimestamp the value of {@code ${project.build.outputTimestamp}} (may be {@code null}) + * @return the parsed timestamp as {@link java.util.Date} + * @since 3.5.0 + * @see #parseOutputTimestamp + * @deprecated Use {@link #configureReproducibleBuild(String)} instead. + */ + @Deprecated + public Date configureReproducible( String outputTimestamp ) + { + configureReproducibleBuild( outputTimestamp ); + return parseOutputTimestamp( outputTimestamp ); + } + + /** + * Parse output timestamp configured for Reproducible Builds' archive entries. + * + * <p>Either as {@link java.time.format.DateTimeFormatter#ISO_ZONED_DATE_TIME} or as a number representing seconds + * since the epoch (like <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>). + * + * @param outputTimestamp the value of {@code ${project.build.outputTimestamp}} (may be {@code null}) + * @return the parsed timestamp as an {@code Optional<Instant>}, {@code empty} if input is {@code null} or input + * contains only 1 character (not a number) + * @since 3.6.0 + * @throws IllegalArgumentException if the outputTimestamp is neither ISO 8601 nor an integer + */ + public static Optional<Instant> parseBuildOutputTimestamp( String outputTimestamp ) + { + // Fail-fast on nulls + if ( outputTimestamp == null ) + { + return Optional.empty(); + } + + // Number representing seconds since the epoch + if ( StringUtils.isNotEmpty( outputTimestamp ) && StringUtils.isNumeric( outputTimestamp ) ) { - return new Date( Long.parseLong( outputTimestamp ) * 1000 ); + return Optional.of( Instant.ofEpochSecond( Long.parseLong( outputTimestamp ) ) ); } - if ( outputTimestamp == null || outputTimestamp.length() < 2 ) + // no timestamp configured (1 character configuration is useful to override a full value during pom + // inheritance) + if ( outputTimestamp.length() < 2 ) { - // no timestamp configured (1 character configuration is useful to override a full value during pom - // inheritance) - return null; + return Optional.empty(); } - DateFormat df = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ssXXX" ); try { - return df.parse( outputTimestamp ); + // Parse the date in UTC such as '2011-12-03T10:15:30Z' or with an offset '2019-10-05T20:37:42+06:00'. + final ZonedDateTime date = ZonedDateTime.parse( outputTimestamp ) + .withZoneSameInstant( ZoneOffset.UTC ).truncatedTo( ChronoUnit.SECONDS ); Review Comment: That reads beautifully... ########## src/test/java/org/apache/maven/archiver/MavenArchiverTest.java: ########## @@ -1444,54 +1447,76 @@ public void testParseOutputTimestamp() assertThat( archiver.parseOutputTimestamp( "*" ) ).isNull(); assertThat( archiver.parseOutputTimestamp( "1570300662" ).getTime() ).isEqualTo( 1570300662000L ); - assertThat( archiver.parseOutputTimestamp( "0" ).getTime() ).isEqualTo( 0L ); + assertThat( archiver.parseOutputTimestamp( "0" ).getTime() ).isZero(); assertThat( archiver.parseOutputTimestamp( "1" ).getTime() ).isEqualTo( 1000L ); - assertThat( archiver.parseOutputTimestamp( "2019-10-05T18:37:42Z" ).getTime() ).isEqualTo( 1570300662000L ); - assertThat( archiver.parseOutputTimestamp( "2019-10-05T20:37:42+02:00" ).getTime() ).isEqualTo( - 1570300662000L ); - assertThat( archiver.parseOutputTimestamp( "2019-10-05T16:37:42-02:00" ).getTime() ).isEqualTo( - 1570300662000L ); + assertThat( archiver.parseOutputTimestamp( "2019-10-05T18:37:42Z" ).getTime() ) + .isEqualTo( 1570300662000L ); + assertThat( archiver.parseOutputTimestamp( "2019-10-05T20:37:42+02:00" ).getTime() ) + .isEqualTo( 1570300662000L ); + assertThat( archiver.parseOutputTimestamp( "2019-10-05T16:37:42-02:00" ).getTime() ) + .isEqualTo( 1570300662000L ); // These must result in IAE because we expect extended ISO format only (ie with - separator for date and // : separator for timezone), hence the XXX SimpleDateFormat for tz offset // X SimpleDateFormat accepts timezone without separator while date has separator, which is a mix between // basic (no separators, both for date and timezone) and extended (separator for both) - try - { - archiver.parseOutputTimestamp( "2019-10-05T20:37:42+0200" ); - fail(); - } - catch ( IllegalArgumentException ignored ) - { - } - try - { - archiver.parseOutputTimestamp( "2019-10-05T20:37:42-0200" ); - fail(); - } - catch ( IllegalArgumentException ignored ) - { - } + assertThatExceptionOfType( IllegalArgumentException.class ) + .isThrownBy( () -> archiver.parseOutputTimestamp( "2019-10-05T20:37:42+0200" ) ); + assertThatExceptionOfType( IllegalArgumentException.class ) + .isThrownBy( () -> archiver.parseOutputTimestamp( "2019-10-05T20:37:42-0200" ) ); + } - // These unfortunately fail although the input is valid according to ISO 8601 - // SDF does not allow strict telescoping parsing w/o permitting invalid input as depicted above. - // One has to use the new Java Time API for this. - try - { - archiver.parseOutputTimestamp( "2019-10-05T20:37:42+02" ); - fail(); - } - catch ( IllegalArgumentException ignored ) - { - } - try - { - archiver.parseOutputTimestamp( "2019-10-05T20:37:42-02" ); - fail(); - } - catch ( IllegalArgumentException ignored ) - { - } + @ParameterizedTest + @NullAndEmptySource + @ValueSource( strings = { ".", " ", "_", "-", "T", "/", "!", "!", "*", "ñ" } ) + public void testEmptyParseOutputTimestampInstant( String value ) + { + // Empty optional if null or 1 char + assertThat( MavenArchiver.parseBuildOutputTimestamp( value ) ).isEmpty(); + } + + @ParameterizedTest + @CsvSource( { "0,0", "1,1", "9,9", "1570300662,1570300662", "2147483648,2147483648", + "2019-10-05T18:37:42Z,1570300662", "2019-10-05T20:37:42+02:00,1570300662", + "2019-10-05T16:37:42-02:00,1570300662", "1988-02-22T15:23:47.76598Z,572541827", + "2011-12-03T10:15:30+01:00[Europe/Paris],1322903730", + "1980-01-01T00:00:02Z,315532802", "2099-12-31T23:59:59Z,4102444799" } ) + public void testParseOutputTimestampInstant( String value, long expected ) + { + assertThat( MavenArchiver.parseBuildOutputTimestamp( value ) ) + .contains( Instant.ofEpochSecond( expected ) ); + } + + @ParameterizedTest + @ValueSource( strings = { "2019-10-05T20:37:42+0200", "2019-10-05T20:37:42-0200", "2019-10-05T25:00:00Z", + "2019-10-05", "XYZ", "Tue, 3 Jun 2008 11:05:30 GMT" } ) + public void testThrownParseOutputTimestampInstant( String outputTimestamp ) + { + // Invalid parsing + assertThatExceptionOfType( IllegalArgumentException.class ) + .isThrownBy( () -> MavenArchiver.parseBuildOutputTimestamp( outputTimestamp ) ) + .withCauseInstanceOf( DateTimeParseException.class ); + } + + @ParameterizedTest + @ValueSource( strings = { "1980-01-01T00:00:01Z", "2100-01-01T00:00Z", "2100-02-28T23:59:59Z", + "2099-12-31T23:59:59-01:00", "1980-01-01T00:15:35+01:00[Europe/Madrid]" } ) + public void testThrownParseOutputTimestampValidRange( String outputTimestamp ) + { + // date is not within the valid range 1980-01-01T00:00:02Z to 2099-12-31T23:59:59Z + assertThatExceptionOfType( IllegalArgumentException.class ) + .isThrownBy( () -> MavenArchiver.parseBuildOutputTimestamp( outputTimestamp ) ) + .withMessageContaining("is not within the valid range 1980-01-01T00:00:02Z to 2099-12-31T23:59:59Z"); + } + + @ParameterizedTest + @CsvSource( { "2011-12-03T10:15:30+01[Europe/Paris],1322903730", "2019-10-05T20:37:42+02,1570300662", + "2011-12-03T10:15:30+06[America/Managua],1322885730", "1988-02-22T20:37:42+06,572539062" } ) Review Comment: Here too. ########## src/test/java/org/apache/maven/archiver/MavenArchiverTest.java: ########## @@ -1444,54 +1447,76 @@ public void testParseOutputTimestamp() assertThat( archiver.parseOutputTimestamp( "*" ) ).isNull(); assertThat( archiver.parseOutputTimestamp( "1570300662" ).getTime() ).isEqualTo( 1570300662000L ); - assertThat( archiver.parseOutputTimestamp( "0" ).getTime() ).isEqualTo( 0L ); + assertThat( archiver.parseOutputTimestamp( "0" ).getTime() ).isZero(); assertThat( archiver.parseOutputTimestamp( "1" ).getTime() ).isEqualTo( 1000L ); - assertThat( archiver.parseOutputTimestamp( "2019-10-05T18:37:42Z" ).getTime() ).isEqualTo( 1570300662000L ); - assertThat( archiver.parseOutputTimestamp( "2019-10-05T20:37:42+02:00" ).getTime() ).isEqualTo( - 1570300662000L ); - assertThat( archiver.parseOutputTimestamp( "2019-10-05T16:37:42-02:00" ).getTime() ).isEqualTo( - 1570300662000L ); + assertThat( archiver.parseOutputTimestamp( "2019-10-05T18:37:42Z" ).getTime() ) + .isEqualTo( 1570300662000L ); + assertThat( archiver.parseOutputTimestamp( "2019-10-05T20:37:42+02:00" ).getTime() ) + .isEqualTo( 1570300662000L ); + assertThat( archiver.parseOutputTimestamp( "2019-10-05T16:37:42-02:00" ).getTime() ) + .isEqualTo( 1570300662000L ); // These must result in IAE because we expect extended ISO format only (ie with - separator for date and // : separator for timezone), hence the XXX SimpleDateFormat for tz offset // X SimpleDateFormat accepts timezone without separator while date has separator, which is a mix between // basic (no separators, both for date and timezone) and extended (separator for both) - try - { - archiver.parseOutputTimestamp( "2019-10-05T20:37:42+0200" ); - fail(); - } - catch ( IllegalArgumentException ignored ) - { - } - try - { - archiver.parseOutputTimestamp( "2019-10-05T20:37:42-0200" ); - fail(); - } - catch ( IllegalArgumentException ignored ) - { - } + assertThatExceptionOfType( IllegalArgumentException.class ) + .isThrownBy( () -> archiver.parseOutputTimestamp( "2019-10-05T20:37:42+0200" ) ); + assertThatExceptionOfType( IllegalArgumentException.class ) + .isThrownBy( () -> archiver.parseOutputTimestamp( "2019-10-05T20:37:42-0200" ) ); + } - // These unfortunately fail although the input is valid according to ISO 8601 - // SDF does not allow strict telescoping parsing w/o permitting invalid input as depicted above. - // One has to use the new Java Time API for this. - try - { - archiver.parseOutputTimestamp( "2019-10-05T20:37:42+02" ); - fail(); - } - catch ( IllegalArgumentException ignored ) - { - } - try - { - archiver.parseOutputTimestamp( "2019-10-05T20:37:42-02" ); - fail(); - } - catch ( IllegalArgumentException ignored ) - { - } + @ParameterizedTest + @NullAndEmptySource + @ValueSource( strings = { ".", " ", "_", "-", "T", "/", "!", "!", "*", "ñ" } ) + public void testEmptyParseOutputTimestampInstant( String value ) + { + // Empty optional if null or 1 char + assertThat( MavenArchiver.parseBuildOutputTimestamp( value ) ).isEmpty(); + } + + @ParameterizedTest + @CsvSource( { "0,0", "1,1", "9,9", "1570300662,1570300662", "2147483648,2147483648", + "2019-10-05T18:37:42Z,1570300662", "2019-10-05T20:37:42+02:00,1570300662", + "2019-10-05T16:37:42-02:00,1570300662", "1988-02-22T15:23:47.76598Z,572541827", + "2011-12-03T10:15:30+01:00[Europe/Paris],1322903730", Review Comment: `2011-12-03T10:15:30+01:00[Europe/Paris]` is invalid. Should not pass. ########## src/test/java/org/apache/maven/archiver/MavenArchiverTest.java: ########## @@ -1444,54 +1447,76 @@ public void testParseOutputTimestamp() assertThat( archiver.parseOutputTimestamp( "*" ) ).isNull(); assertThat( archiver.parseOutputTimestamp( "1570300662" ).getTime() ).isEqualTo( 1570300662000L ); - assertThat( archiver.parseOutputTimestamp( "0" ).getTime() ).isEqualTo( 0L ); + assertThat( archiver.parseOutputTimestamp( "0" ).getTime() ).isZero(); assertThat( archiver.parseOutputTimestamp( "1" ).getTime() ).isEqualTo( 1000L ); - assertThat( archiver.parseOutputTimestamp( "2019-10-05T18:37:42Z" ).getTime() ).isEqualTo( 1570300662000L ); - assertThat( archiver.parseOutputTimestamp( "2019-10-05T20:37:42+02:00" ).getTime() ).isEqualTo( - 1570300662000L ); - assertThat( archiver.parseOutputTimestamp( "2019-10-05T16:37:42-02:00" ).getTime() ).isEqualTo( - 1570300662000L ); + assertThat( archiver.parseOutputTimestamp( "2019-10-05T18:37:42Z" ).getTime() ) + .isEqualTo( 1570300662000L ); + assertThat( archiver.parseOutputTimestamp( "2019-10-05T20:37:42+02:00" ).getTime() ) + .isEqualTo( 1570300662000L ); + assertThat( archiver.parseOutputTimestamp( "2019-10-05T16:37:42-02:00" ).getTime() ) + .isEqualTo( 1570300662000L ); // These must result in IAE because we expect extended ISO format only (ie with - separator for date and // : separator for timezone), hence the XXX SimpleDateFormat for tz offset // X SimpleDateFormat accepts timezone without separator while date has separator, which is a mix between // basic (no separators, both for date and timezone) and extended (separator for both) - try - { - archiver.parseOutputTimestamp( "2019-10-05T20:37:42+0200" ); - fail(); - } - catch ( IllegalArgumentException ignored ) - { - } - try - { - archiver.parseOutputTimestamp( "2019-10-05T20:37:42-0200" ); - fail(); - } - catch ( IllegalArgumentException ignored ) - { - } + assertThatExceptionOfType( IllegalArgumentException.class ) + .isThrownBy( () -> archiver.parseOutputTimestamp( "2019-10-05T20:37:42+0200" ) ); + assertThatExceptionOfType( IllegalArgumentException.class ) + .isThrownBy( () -> archiver.parseOutputTimestamp( "2019-10-05T20:37:42-0200" ) ); + } - // These unfortunately fail although the input is valid according to ISO 8601 - // SDF does not allow strict telescoping parsing w/o permitting invalid input as depicted above. - // One has to use the new Java Time API for this. - try - { - archiver.parseOutputTimestamp( "2019-10-05T20:37:42+02" ); - fail(); - } - catch ( IllegalArgumentException ignored ) - { - } - try - { - archiver.parseOutputTimestamp( "2019-10-05T20:37:42-02" ); - fail(); - } - catch ( IllegalArgumentException ignored ) - { - } + @ParameterizedTest + @NullAndEmptySource + @ValueSource( strings = { ".", " ", "_", "-", "T", "/", "!", "!", "*", "ñ" } ) + public void testEmptyParseOutputTimestampInstant( String value ) + { + // Empty optional if null or 1 char + assertThat( MavenArchiver.parseBuildOutputTimestamp( value ) ).isEmpty(); + } + + @ParameterizedTest + @CsvSource( { "0,0", "1,1", "9,9", "1570300662,1570300662", "2147483648,2147483648", + "2019-10-05T18:37:42Z,1570300662", "2019-10-05T20:37:42+02:00,1570300662", + "2019-10-05T16:37:42-02:00,1570300662", "1988-02-22T15:23:47.76598Z,572541827", + "2011-12-03T10:15:30+01:00[Europe/Paris],1322903730", + "1980-01-01T00:00:02Z,315532802", "2099-12-31T23:59:59Z,4102444799" } ) + public void testParseOutputTimestampInstant( String value, long expected ) + { + assertThat( MavenArchiver.parseBuildOutputTimestamp( value ) ) + .contains( Instant.ofEpochSecond( expected ) ); + } + + @ParameterizedTest + @ValueSource( strings = { "2019-10-05T20:37:42+0200", "2019-10-05T20:37:42-0200", "2019-10-05T25:00:00Z", + "2019-10-05", "XYZ", "Tue, 3 Jun 2008 11:05:30 GMT" } ) + public void testThrownParseOutputTimestampInstant( String outputTimestamp ) + { + // Invalid parsing + assertThatExceptionOfType( IllegalArgumentException.class ) + .isThrownBy( () -> MavenArchiver.parseBuildOutputTimestamp( outputTimestamp ) ) + .withCauseInstanceOf( DateTimeParseException.class ); + } + + @ParameterizedTest + @ValueSource( strings = { "1980-01-01T00:00:01Z", "2100-01-01T00:00Z", "2100-02-28T23:59:59Z", + "2099-12-31T23:59:59-01:00", "1980-01-01T00:15:35+01:00[Europe/Madrid]" } ) Review Comment: Same for the last. ########## src/main/java/org/apache/maven/archiver/MavenArchiver.java: ########## @@ -812,28 +820,78 @@ public void setBuildJdkSpecDefaultEntry( boolean buildJdkSpecDefaultEntry ) * @return the parsed timestamp, may be <code>null</code> if <code>null</code> input or input contains only 1 * character * @since 3.5.0 - * @throws java.lang.IllegalArgumentException if the outputTimestamp is neither ISO 8601 nor an integer + * @throws IllegalArgumentException if the outputTimestamp is neither ISO 8601 nor an integer + * @deprecated Use {@link #parseBuildOutputTimestamp(String)} instead. */ + @Deprecated public Date parseOutputTimestamp( String outputTimestamp ) { - if ( StringUtils.isNumeric( outputTimestamp ) && StringUtils.isNotEmpty( outputTimestamp ) ) + return parseBuildOutputTimestamp( outputTimestamp ).map( Date::from ).orElse( null ); + } + + /** + * Configure Reproducible Builds archive creation if a timestamp is provided. + * + * @param outputTimestamp the value of {@code ${project.build.outputTimestamp}} (may be {@code null}) + * @return the parsed timestamp as {@link java.util.Date} + * @since 3.5.0 + * @see #parseOutputTimestamp + * @deprecated Use {@link #configureReproducibleBuild(String)} instead. + */ + @Deprecated + public Date configureReproducible( String outputTimestamp ) + { + configureReproducibleBuild( outputTimestamp ); + return parseOutputTimestamp( outputTimestamp ); + } + + /** + * Parse output timestamp configured for Reproducible Builds' archive entries. + * + * <p>Either as {@link java.time.format.DateTimeFormatter#ISO_ZONED_DATE_TIME} or as a number representing seconds + * since the epoch (like <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>). + * + * @param outputTimestamp the value of {@code ${project.build.outputTimestamp}} (may be {@code null}) + * @return the parsed timestamp as an {@code Optional<Instant>}, {@code empty} if input is {@code null} or input + * contains only 1 character (not a number) + * @since 3.6.0 + * @throws IllegalArgumentException if the outputTimestamp is neither ISO 8601 nor an integer + */ + public static Optional<Instant> parseBuildOutputTimestamp( String outputTimestamp ) + { Review Comment: I have the feeling that such parsing is done over and over again in many components. We need to consider to have ths at as central place. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: issues-unsubscr...@maven.apache.org For queries about this service, please contact Infrastructure at: us...@infra.apache.org