Skip to content

Commit c9249ff

Browse files
committed
[MSHARED-1067] - Improve the Reproducible Builds methods
Signed-off-by: Jorge Solórzano <jorsol@gmail.com>
1 parent 2a1d52e commit c9249ff

File tree

3 files changed

+129
-85
lines changed

3 files changed

+129
-85
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@
8484
<dependency>
8585
<groupId>org.codehaus.plexus</groupId>
8686
<artifactId>plexus-io</artifactId>
87-
<version>3.3.0</version>
87+
<version>3.3.1</version>
8888
</dependency>
8989
<dependency>
9090
<groupId>org.codehaus.plexus</groupId>

src/main/java/org/apache/maven/archiver/MavenArchiver.java

Lines changed: 67 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,17 @@
2424
import java.io.File;
2525
import java.io.IOException;
2626
import java.io.InputStream;
27-
import java.text.DateFormat;
28-
import java.text.ParseException;
29-
import java.text.SimpleDateFormat;
27+
import java.nio.file.attribute.FileTime;
28+
import java.time.Instant;
29+
import java.time.format.DateTimeFormatter;
30+
import java.time.format.DateTimeParseException;
31+
import java.time.temporal.ChronoUnit;
3032
import java.util.ArrayList;
3133
import java.util.Collections;
3234
import java.util.Date;
3335
import java.util.List;
3436
import java.util.Map;
37+
import java.util.Optional;
3538
import java.util.Properties;
3639
import java.util.Set;
3740
import java.util.jar.Attributes;
@@ -812,28 +815,73 @@ public void setBuildJdkSpecDefaultEntry( boolean buildJdkSpecDefaultEntry )
812815
* @return the parsed timestamp, may be <code>null</code> if <code>null</code> input or input contains only 1
813816
* character
814817
* @since 3.5.0
815-
* @throws java.lang.IllegalArgumentException if the outputTimestamp is neither ISO 8601 nor an integer
818+
* @throws IllegalArgumentException if the outputTimestamp is neither ISO 8601 nor an integer
819+
* @deprecated Use {@link #parseBuildOutputTimestamp(String)} instead.
816820
*/
821+
@Deprecated
817822
public Date parseOutputTimestamp( String outputTimestamp )
818823
{
824+
return parseBuildOutputTimestamp( outputTimestamp ).map( Date::from ).orElse( null );
825+
}
826+
827+
/**
828+
* Configure Reproducible Builds archive creation if a timestamp is provided.
829+
*
830+
* @param outputTimestamp the value of {@code ${project.build.outputTimestamp}} (may be {@code null})
831+
* @return the parsed timestamp as {@link java.util.Date}
832+
* @since 3.5.0
833+
* @see #parseOutputTimestamp
834+
* @deprecated Use {@link #configureReproducibleBuild(String)} instead.
835+
*/
836+
@Deprecated
837+
public Date configureReproducible( String outputTimestamp )
838+
{
839+
configureReproducibleBuild( outputTimestamp );
840+
return parseOutputTimestamp( outputTimestamp );
841+
}
842+
843+
/**
844+
* Parse output timestamp configured for Reproducible Builds' archive entries.
845+
*
846+
* <p>Either as {@link java.time.format.DateTimeFormatter#ISO_OFFSET_DATE_TIME} or as a number representing seconds
847+
* since the epoch (like <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
848+
*
849+
* @param outputTimestamp the value of {@code ${project.build.outputTimestamp}} (may be {@code null})
850+
* @return the parsed timestamp as an {@code Optional<Instant>}, {@code empty} if input is {@code null} or input
851+
* contains only 1 character (not a number)
852+
* @since 3.6.0
853+
* @throws IllegalArgumentException if the outputTimestamp is neither ISO 8601 nor an integer
854+
*/
855+
public static Optional<Instant> parseBuildOutputTimestamp( String outputTimestamp )
856+
{
857+
// Fail-fast on nulls
858+
if ( outputTimestamp == null )
859+
{
860+
return Optional.empty();
861+
}
862+
863+
// Number representing seconds since the epoch
819864
if ( StringUtils.isNumeric( outputTimestamp ) && StringUtils.isNotEmpty( outputTimestamp ) )
820865
{
821-
return new Date( Long.parseLong( outputTimestamp ) * 1000 );
866+
return Optional.of( Instant.ofEpochSecond( Long.parseLong( outputTimestamp ) ) );
822867
}
823868

824-
if ( outputTimestamp == null || outputTimestamp.length() < 2 )
869+
// no timestamp configured (1 character configuration is useful to override a full value during pom
870+
// inheritance)
871+
if ( outputTimestamp.length() < 2 )
825872
{
826-
// no timestamp configured (1 character configuration is useful to override a full value during pom
827-
// inheritance)
828-
return null;
873+
return Optional.empty();
829874
}
830875

831-
DateFormat df = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ssXXX" );
832876
try
833877
{
834-
return df.parse( outputTimestamp );
878+
// Parse the date in UTC such as '2011-12-03T10:15:30Z' or with an offset '2019-10-05T20:37:42+02:00'.
879+
// ISO_INSTANT doesn't handle offsets in Java < 12 (JDK-8166138)
880+
Instant parsedInstant = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse( outputTimestamp, Instant::from )
881+
.truncatedTo( ChronoUnit.SECONDS );
882+
return Optional.of( parsedInstant );
835883
}
836-
catch ( ParseException pe )
884+
catch ( DateTimeParseException pe )
837885
{
838886
throw new IllegalArgumentException( "Invalid project.build.outputTimestamp value '" + outputTimestamp + "'",
839887
pe );
@@ -843,18 +891,14 @@ public Date parseOutputTimestamp( String outputTimestamp )
843891
/**
844892
* Configure Reproducible Builds archive creation if a timestamp is provided.
845893
*
846-
* @param outputTimestamp the value of <code>${project.build.outputTimestamp}</code> (may be <code>null</code>)
847-
* @return the parsed timestamp
848-
* @since 3.5.0
849-
* @see #parseOutputTimestamp
894+
* @param outputTimestamp the value of {@code project.build.outputTimestamp} (may be {@code null})
895+
* @since 3.6.0
896+
* @see #parseBuildOutputTimestamp(String)
850897
*/
851-
public Date configureReproducible( String outputTimestamp )
898+
public void configureReproducibleBuild( String outputTimestamp )
852899
{
853-
Date outputDate = parseOutputTimestamp( outputTimestamp );
854-
if ( outputDate != null )
855-
{
856-
getArchiver().configureReproducible( outputDate );
857-
}
858-
return outputDate;
900+
parseBuildOutputTimestamp( outputTimestamp )
901+
.map( FileTime::from )
902+
.ifPresent( modifiedTime -> getArchiver().configureReproducibleBuild( modifiedTime ) );
859903
}
860904
}

src/test/java/org/apache/maven/archiver/MavenArchiverTest.java

Lines changed: 61 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,19 @@
3939
import org.eclipse.aether.DefaultRepositorySystemSession;
4040
import org.eclipse.aether.RepositorySystemSession;
4141
import org.junit.jupiter.api.Test;
42+
import org.junit.jupiter.params.ParameterizedTest;
43+
import org.junit.jupiter.params.provider.CsvSource;
44+
import org.junit.jupiter.params.provider.EmptySource;
45+
import org.junit.jupiter.params.provider.NullAndEmptySource;
46+
import org.junit.jupiter.params.provider.ValueSource;
4247

4348
import java.io.File;
4449
import java.io.IOException;
4550
import java.io.InputStream;
4651
import java.net.URI;
4752
import java.net.URL;
53+
import java.time.Instant;
54+
import java.time.format.DateTimeParseException;
4855
import java.util.ArrayList;
4956
import java.util.Collections;
5057
import java.util.Comparator;
@@ -61,7 +68,7 @@
6168
import java.util.zip.ZipEntry;
6269

6370
import static org.assertj.core.api.Assertions.assertThat;
64-
import static org.junit.jupiter.api.Assertions.fail;
71+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
6572

6673
class MavenArchiverTest
6774
{
@@ -79,28 +86,21 @@ public boolean equals( Object o )
7986
}
8087
}
8188

82-
@Test
83-
void testInvalidModuleNames()
89+
@ParameterizedTest
90+
@EmptySource
91+
@ValueSource( strings = { ".", "dash-is-invalid", "plus+is+invalid", "colon:is:invalid", "new.class",
92+
"123.at.start.is.invalid", "digit.at.123start.is.invalid" } )
93+
void testInvalidModuleNames( String value )
8494
{
85-
assertThat( MavenArchiver.isValidModuleName( "" ) ).isFalse();
86-
assertThat( MavenArchiver.isValidModuleName( "." ) ).isFalse();
87-
assertThat( MavenArchiver.isValidModuleName( "dash-is-invalid" ) ).isFalse();
88-
assertThat( MavenArchiver.isValidModuleName( "plus+is+invalid" ) ).isFalse();
89-
assertThat( MavenArchiver.isValidModuleName( "colon:is:invalid" ) ).isFalse();
90-
assertThat( MavenArchiver.isValidModuleName( "new.class" ) ).isFalse();
91-
assertThat( MavenArchiver.isValidModuleName( "123.at.start.is.invalid" ) ).isFalse();
92-
assertThat( MavenArchiver.isValidModuleName( "digit.at.123start.is.invalid" ) ).isFalse();
95+
assertThat( MavenArchiver.isValidModuleName( value ) ).isFalse();
9396
}
9497

95-
@Test
96-
void testValidModuleNames()
98+
@ParameterizedTest
99+
@ValueSource( strings = { "a", "a.b", "a_b", "trailing0.digits123.are456.ok789", "UTF8.chars.are.okay.äëïöüẍ",
100+
"ℤ€ℕ" } )
101+
void testValidModuleNames( String value )
97102
{
98-
assertThat( MavenArchiver.isValidModuleName( "a" ) ).isTrue();
99-
assertThat( MavenArchiver.isValidModuleName( "a.b" ) ).isTrue();
100-
assertThat( MavenArchiver.isValidModuleName( "a_b" ) ).isTrue();
101-
assertThat( MavenArchiver.isValidModuleName( "trailing0.digits123.are456.ok789" ) ).isTrue();
102-
assertThat( MavenArchiver.isValidModuleName( "UTF8.chars.are.okay.äëïöüẍ" ) ).isTrue();
103-
assertThat( MavenArchiver.isValidModuleName( "ℤ€ℕ" ) ).isTrue();
103+
assertThat( MavenArchiver.isValidModuleName( value ) ).isTrue();
104104
}
105105

106106
@Test
@@ -1366,7 +1366,8 @@ private File getClasspathFile( String file )
13661366
URL resource = Thread.currentThread().getContextClassLoader().getResource( file );
13671367
if ( resource == null )
13681368
{
1369-
fail( "Cannot retrieve java.net.URL for file: " + file + " on the current test classpath." );
1369+
throw new IllegalStateException( "Cannot retrieve java.net.URL for file: " + file
1370+
+ " on the current test classpath." );
13701371
}
13711372

13721373
URI uri = new File( resource.getPath() ).toURI().normalize();
@@ -1444,54 +1445,53 @@ public void testParseOutputTimestamp()
14441445
assertThat( archiver.parseOutputTimestamp( "*" ) ).isNull();
14451446

14461447
assertThat( archiver.parseOutputTimestamp( "1570300662" ).getTime() ).isEqualTo( 1570300662000L );
1447-
assertThat( archiver.parseOutputTimestamp( "0" ).getTime() ).isEqualTo( 0L );
1448+
assertThat( archiver.parseOutputTimestamp( "0" ).getTime() ).isZero();
14481449
assertThat( archiver.parseOutputTimestamp( "1" ).getTime() ).isEqualTo( 1000L );
14491450

1450-
assertThat( archiver.parseOutputTimestamp( "2019-10-05T18:37:42Z" ).getTime() ).isEqualTo( 1570300662000L );
1451-
assertThat( archiver.parseOutputTimestamp( "2019-10-05T20:37:42+02:00" ).getTime() ).isEqualTo(
1452-
1570300662000L );
1453-
assertThat( archiver.parseOutputTimestamp( "2019-10-05T16:37:42-02:00" ).getTime() ).isEqualTo(
1454-
1570300662000L );
1451+
assertThat( archiver.parseOutputTimestamp( "2019-10-05T18:37:42Z" ).getTime() )
1452+
.isEqualTo( 1570300662000L );
1453+
assertThat( archiver.parseOutputTimestamp( "2019-10-05T20:37:42+02:00" ).getTime() )
1454+
.isEqualTo( 1570300662000L );
1455+
assertThat( archiver.parseOutputTimestamp( "2019-10-05T16:37:42-02:00" ).getTime() )
1456+
.isEqualTo( 1570300662000L );
14551457

14561458
// These must result in IAE because we expect extended ISO format only (ie with - separator for date and
14571459
// : separator for timezone), hence the XXX SimpleDateFormat for tz offset
14581460
// X SimpleDateFormat accepts timezone without separator while date has separator, which is a mix between
14591461
// basic (no separators, both for date and timezone) and extended (separator for both)
1460-
try
1461-
{
1462-
archiver.parseOutputTimestamp( "2019-10-05T20:37:42+0200" );
1463-
fail();
1464-
}
1465-
catch ( IllegalArgumentException ignored )
1466-
{
1467-
}
1468-
try
1469-
{
1470-
archiver.parseOutputTimestamp( "2019-10-05T20:37:42-0200" );
1471-
fail();
1472-
}
1473-
catch ( IllegalArgumentException ignored )
1474-
{
1475-
}
1462+
assertThatExceptionOfType( IllegalArgumentException.class )
1463+
.isThrownBy( () -> archiver.parseOutputTimestamp( "2019-10-05T20:37:42+0200" ) );
1464+
assertThatExceptionOfType( IllegalArgumentException.class )
1465+
.isThrownBy( () -> archiver.parseOutputTimestamp( "2019-10-05T20:37:42-0200" ) );
1466+
}
14761467

1477-
// These unfortunately fail although the input is valid according to ISO 8601
1478-
// SDF does not allow strict telescoping parsing w/o permitting invalid input as depicted above.
1479-
// One has to use the new Java Time API for this.
1480-
try
1481-
{
1482-
archiver.parseOutputTimestamp( "2019-10-05T20:37:42+02" );
1483-
fail();
1484-
}
1485-
catch ( IllegalArgumentException ignored )
1486-
{
1487-
}
1488-
try
1489-
{
1490-
archiver.parseOutputTimestamp( "2019-10-05T20:37:42-02" );
1491-
fail();
1492-
}
1493-
catch ( IllegalArgumentException ignored )
1494-
{
1495-
}
1468+
@ParameterizedTest
1469+
@NullAndEmptySource
1470+
@ValueSource( strings = { ".", " ", "_", "-", "T", "/", "!", "!", "*", "ñ" } )
1471+
public void testEmptyParseOutputTimestampInstant( String value )
1472+
{
1473+
// Empty optional if null or 1 char
1474+
assertThat( MavenArchiver.parseBuildOutputTimestamp( value ) ).isEmpty();
1475+
}
1476+
1477+
@ParameterizedTest
1478+
@CsvSource( { "0,0", "1,1000", "9,9000", "1570300662,1570300662000", "2147483648,2147483648000",
1479+
"2019-10-05T18:37:42Z,1570300662000", "2019-10-05T20:37:42+02:00,1570300662000",
1480+
"2019-10-05T16:37:42-02:00,1570300662000", "1988-02-22T15:23:47.76598Z,572541827000" } )
1481+
public void testParseOutputTimestampInstant( String value, long expected )
1482+
{
1483+
assertThat( MavenArchiver.parseBuildOutputTimestamp( value ) )
1484+
.contains( Instant.ofEpochMilli( expected ) );
1485+
}
1486+
1487+
@ParameterizedTest
1488+
@ValueSource( strings = { "2019-10-05T20:37:42+0200", "2019-10-05T20:37:42-0200", "2019-10-05T25:00:00Z",
1489+
"2019-10-05", "XYZ", "Tue, 3 Jun 2008 11:05:30 GMT", "2011-12-03T10:15:30+01:00[Europe/Paris]" } )
1490+
public void testThrownParseOutputTimestampInstant( String outputTimestamp )
1491+
{
1492+
// Invalid parsing
1493+
assertThatExceptionOfType( IllegalArgumentException.class )
1494+
.isThrownBy( () -> MavenArchiver.parseBuildOutputTimestamp( outputTimestamp ) )
1495+
.withCauseInstanceOf( DateTimeParseException.class );
14961496
}
14971497
}

0 commit comments

Comments
 (0)