From 35f17c6a9f640ade5f2ef32ba59d95f493f2ed42 Mon Sep 17 00:00:00 2001 From: paytoncain Date: Fri, 26 Apr 2024 17:39:10 -0600 Subject: [PATCH] Flatten json dataset schema with packing job schema --- .../SoundPropagationModelsPackingJob.java | 25 +++++++ .../cires/pace/data/object/AudioDataset.java | 4 +- .../pace/data/object/AudioPackingJob.java | 23 ++++++ .../pace/data/object/CPODPackingJob.java | 23 ++++++ .../cires/pace/data/object/CPodDataset.java | 4 +- .../pace/data/object/DetectionsDataset.java | 4 +- .../data/object/DetectionsPackingJob.java | 23 ++++++ .../cires/pace/data/object/PackingJob.java | 52 +++++++++----- .../pace/data/object/SoundClipsDataset.java | 4 +- .../data/object/SoundClipsPackingJob.java | 23 ++++++ .../object/SoundLevelMetricsPackingJob.java | 23 ++++++ .../object/SoundPropagationModelsDataset.java | 4 +- .../cires/pace/packaging/FileUtils.java | 3 +- .../pace/packaging/PackageProcessor.java | 3 +- .../PackageInstructionFactoryTest.java | 5 +- .../pace/packaging/PackagerProcessorTest.java | 71 +++++++++---------- 16 files changed, 222 insertions(+), 72 deletions(-) create mode 100644 pace-data/src/main/java/edu/colorado/cires/pace/data/SoundPropagationModelsPackingJob.java create mode 100644 pace-data/src/main/java/edu/colorado/cires/pace/data/object/AudioPackingJob.java create mode 100644 pace-data/src/main/java/edu/colorado/cires/pace/data/object/CPODPackingJob.java create mode 100644 pace-data/src/main/java/edu/colorado/cires/pace/data/object/DetectionsPackingJob.java create mode 100644 pace-data/src/main/java/edu/colorado/cires/pace/data/object/SoundClipsPackingJob.java create mode 100644 pace-data/src/main/java/edu/colorado/cires/pace/data/object/SoundLevelMetricsPackingJob.java diff --git a/pace-data/src/main/java/edu/colorado/cires/pace/data/SoundPropagationModelsPackingJob.java b/pace-data/src/main/java/edu/colorado/cires/pace/data/SoundPropagationModelsPackingJob.java new file mode 100644 index 00000000..10a370cb --- /dev/null +++ b/pace-data/src/main/java/edu/colorado/cires/pace/data/SoundPropagationModelsPackingJob.java @@ -0,0 +1,25 @@ +package edu.colorado.cires.pace.data; + +import edu.colorado.cires.pace.data.object.PackingJob; +import edu.colorado.cires.pace.data.object.SoundPropagationModelsDataset; +import java.nio.file.Path; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +@EqualsAndHashCode(callSuper = true) +@Data +@SuperBuilder +@Jacksonized +public class SoundPropagationModelsPackingJob extends SoundPropagationModelsDataset implements PackingJob { + + private final Path temperaturePath; + private final Path biologicalPath; + private final Path otherPath; + private final Path documentsPath; + private final Path calibrationDocumentsPath; + private final Path navigationPath; + private final Path sourcePath; + +} diff --git a/pace-data/src/main/java/edu/colorado/cires/pace/data/object/AudioDataset.java b/pace-data/src/main/java/edu/colorado/cires/pace/data/object/AudioDataset.java index f932bd2b..e4990069 100644 --- a/pace-data/src/main/java/edu/colorado/cires/pace/data/object/AudioDataset.java +++ b/pace-data/src/main/java/edu/colorado/cires/pace/data/object/AudioDataset.java @@ -3,12 +3,12 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; -import lombok.Builder; import lombok.Data; +import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; @Data -@Builder +@SuperBuilder @Jacksonized public class AudioDataset implements Dataset, AudioDatasetDetail, AudioCalibrationDetail { diff --git a/pace-data/src/main/java/edu/colorado/cires/pace/data/object/AudioPackingJob.java b/pace-data/src/main/java/edu/colorado/cires/pace/data/object/AudioPackingJob.java new file mode 100644 index 00000000..92a1631a --- /dev/null +++ b/pace-data/src/main/java/edu/colorado/cires/pace/data/object/AudioPackingJob.java @@ -0,0 +1,23 @@ +package edu.colorado.cires.pace.data.object; + +import java.nio.file.Path; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +@EqualsAndHashCode(callSuper = true) +@Data +@SuperBuilder +@Jacksonized +public class AudioPackingJob extends AudioDataset implements PackingJob { + + private final Path temperaturePath; + private final Path biologicalPath; + private final Path otherPath; + private final Path documentsPath; + private final Path calibrationDocumentsPath; + private final Path navigationPath; + private final Path sourcePath; + +} diff --git a/pace-data/src/main/java/edu/colorado/cires/pace/data/object/CPODPackingJob.java b/pace-data/src/main/java/edu/colorado/cires/pace/data/object/CPODPackingJob.java new file mode 100644 index 00000000..a85d3b05 --- /dev/null +++ b/pace-data/src/main/java/edu/colorado/cires/pace/data/object/CPODPackingJob.java @@ -0,0 +1,23 @@ +package edu.colorado.cires.pace.data.object; + +import java.nio.file.Path; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +@EqualsAndHashCode(callSuper = true) +@Data +@SuperBuilder +@Jacksonized +public class CPODPackingJob extends CPodDataset implements PackingJob { + + private final Path temperaturePath; + private final Path biologicalPath; + private final Path otherPath; + private final Path documentsPath; + private final Path calibrationDocumentsPath; + private final Path navigationPath; + private final Path sourcePath; + +} diff --git a/pace-data/src/main/java/edu/colorado/cires/pace/data/object/CPodDataset.java b/pace-data/src/main/java/edu/colorado/cires/pace/data/object/CPodDataset.java index 3116df29..d073facf 100644 --- a/pace-data/src/main/java/edu/colorado/cires/pace/data/object/CPodDataset.java +++ b/pace-data/src/main/java/edu/colorado/cires/pace/data/object/CPodDataset.java @@ -3,12 +3,12 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; -import lombok.Builder; import lombok.Data; +import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; @Data -@Builder +@SuperBuilder @Jacksonized public class CPodDataset implements Dataset, AudioDatasetDetail, AudioCalibrationDetail { diff --git a/pace-data/src/main/java/edu/colorado/cires/pace/data/object/DetectionsDataset.java b/pace-data/src/main/java/edu/colorado/cires/pace/data/object/DetectionsDataset.java index 36fd768d..0d84df6f 100644 --- a/pace-data/src/main/java/edu/colorado/cires/pace/data/object/DetectionsDataset.java +++ b/pace-data/src/main/java/edu/colorado/cires/pace/data/object/DetectionsDataset.java @@ -3,12 +3,12 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; -import lombok.Builder; import lombok.Data; +import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; @Data -@Builder +@SuperBuilder @Jacksonized public class DetectionsDataset implements Dataset, DetectionsDatasetDetail { diff --git a/pace-data/src/main/java/edu/colorado/cires/pace/data/object/DetectionsPackingJob.java b/pace-data/src/main/java/edu/colorado/cires/pace/data/object/DetectionsPackingJob.java new file mode 100644 index 00000000..3e660f14 --- /dev/null +++ b/pace-data/src/main/java/edu/colorado/cires/pace/data/object/DetectionsPackingJob.java @@ -0,0 +1,23 @@ +package edu.colorado.cires.pace.data.object; + +import java.nio.file.Path; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +@EqualsAndHashCode(callSuper = true) +@Data +@SuperBuilder +@Jacksonized +public class DetectionsPackingJob extends DetectionsDataset implements PackingJob { + + private final Path temperaturePath; + private final Path biologicalPath; + private final Path otherPath; + private final Path documentsPath; + private final Path calibrationDocumentsPath; + private final Path navigationPath; + private final Path sourcePath; + +} diff --git a/pace-data/src/main/java/edu/colorado/cires/pace/data/object/PackingJob.java b/pace-data/src/main/java/edu/colorado/cires/pace/data/object/PackingJob.java index b91d5473..8e5fb4b5 100644 --- a/pace-data/src/main/java/edu/colorado/cires/pace/data/object/PackingJob.java +++ b/pace-data/src/main/java/edu/colorado/cires/pace/data/object/PackingJob.java @@ -1,26 +1,40 @@ package edu.colorado.cires.pace.data.object; -import jakarta.validation.Valid; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeInfo.As; +import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; +import com.fasterxml.jackson.annotation.JsonView; +import edu.colorado.cires.pace.data.SoundPropagationModelsPackingJob; import jakarta.validation.constraints.NotNull; import java.nio.file.Path; -import lombok.Builder; -import lombok.Data; -import lombok.extern.jackson.Jacksonized; -@Data -@Builder -@Jacksonized -public class PackingJob { - - private final Path temperaturePath; - private final Path biologicalPath; - private final Path otherPath; - private final Path documentsPath; - private final Path calibrationDocumentsPath; - private final Path navigationPath; - @NotNull - private final Path sourcePath; - @NotNull @Valid - private final Dataset dataset; +@JsonTypeInfo(use = Id.NAME, include = As.EXTERNAL_PROPERTY, property = "datasetType") +@JsonSubTypes({ + @Type(value = AudioPackingJob.class, name = "audio"), + @Type(value = CPODPackingJob.class, name = "CPOD"), + @Type(value = DetectionsPackingJob.class, name = "detections"), + @Type(value = SoundClipsPackingJob.class, name = "sound clips"), + @Type(value = SoundLevelMetricsPackingJob.class, name = "sound level metrics"), + @Type(value = SoundPropagationModelsPackingJob.class, name = "sound propagation models"), +}) +public interface PackingJob { + @JsonView(PackingJob.class) + Path getTemperaturePath(); + @JsonView(PackingJob.class) + Path getBiologicalPath(); + @JsonView(PackingJob.class) + Path getOtherPath(); + @JsonView(PackingJob.class) + Path getDocumentsPath(); + @JsonView(PackingJob.class) + Path getCalibrationDocumentsPath(); + @JsonView(PackingJob.class) + Path getNavigationPath(); + @JsonView(PackingJob.class) + @NotNull + Path getSourcePath(); + } diff --git a/pace-data/src/main/java/edu/colorado/cires/pace/data/object/SoundClipsDataset.java b/pace-data/src/main/java/edu/colorado/cires/pace/data/object/SoundClipsDataset.java index 37fdd164..d9d57870 100644 --- a/pace-data/src/main/java/edu/colorado/cires/pace/data/object/SoundClipsDataset.java +++ b/pace-data/src/main/java/edu/colorado/cires/pace/data/object/SoundClipsDataset.java @@ -3,12 +3,12 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; -import lombok.Builder; import lombok.Data; +import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; @Data -@Builder +@SuperBuilder @Jacksonized public class SoundClipsDataset implements Dataset, SoundClipsDatasetDetail { diff --git a/pace-data/src/main/java/edu/colorado/cires/pace/data/object/SoundClipsPackingJob.java b/pace-data/src/main/java/edu/colorado/cires/pace/data/object/SoundClipsPackingJob.java new file mode 100644 index 00000000..f61217db --- /dev/null +++ b/pace-data/src/main/java/edu/colorado/cires/pace/data/object/SoundClipsPackingJob.java @@ -0,0 +1,23 @@ +package edu.colorado.cires.pace.data.object; + +import java.nio.file.Path; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +@EqualsAndHashCode(callSuper = true) +@Data +@SuperBuilder +@Jacksonized +public class SoundClipsPackingJob extends SoundClipsDataset implements PackingJob { + + private final Path temperaturePath; + private final Path biologicalPath; + private final Path otherPath; + private final Path documentsPath; + private final Path calibrationDocumentsPath; + private final Path navigationPath; + private final Path sourcePath; + +} diff --git a/pace-data/src/main/java/edu/colorado/cires/pace/data/object/SoundLevelMetricsPackingJob.java b/pace-data/src/main/java/edu/colorado/cires/pace/data/object/SoundLevelMetricsPackingJob.java new file mode 100644 index 00000000..53402882 --- /dev/null +++ b/pace-data/src/main/java/edu/colorado/cires/pace/data/object/SoundLevelMetricsPackingJob.java @@ -0,0 +1,23 @@ +package edu.colorado.cires.pace.data.object; + +import java.nio.file.Path; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +@EqualsAndHashCode(callSuper = true) +@Data +@SuperBuilder +@Jacksonized +public class SoundLevelMetricsPackingJob extends SoundClipsDataset implements PackingJob { + + private final Path temperaturePath; + private final Path biologicalPath; + private final Path otherPath; + private final Path documentsPath; + private final Path calibrationDocumentsPath; + private final Path navigationPath; + private final Path sourcePath; + +} diff --git a/pace-data/src/main/java/edu/colorado/cires/pace/data/object/SoundPropagationModelsDataset.java b/pace-data/src/main/java/edu/colorado/cires/pace/data/object/SoundPropagationModelsDataset.java index 8af89f76..e6148bdc 100644 --- a/pace-data/src/main/java/edu/colorado/cires/pace/data/object/SoundPropagationModelsDataset.java +++ b/pace-data/src/main/java/edu/colorado/cires/pace/data/object/SoundPropagationModelsDataset.java @@ -3,12 +3,12 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; -import lombok.Builder; import lombok.Data; +import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; @Data -@Builder +@SuperBuilder @Jacksonized public class SoundPropagationModelsDataset implements Dataset, SoundPropagationModelsDatasetDetail { diff --git a/pace-packaging/src/main/java/edu/colorado/cires/pace/packaging/FileUtils.java b/pace-packaging/src/main/java/edu/colorado/cires/pace/packaging/FileUtils.java index 8e7b04bc..1e23d5f1 100644 --- a/pace-packaging/src/main/java/edu/colorado/cires/pace/packaging/FileUtils.java +++ b/pace-packaging/src/main/java/edu/colorado/cires/pace/packaging/FileUtils.java @@ -1,6 +1,5 @@ package edu.colorado.cires.pace.packaging; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import edu.colorado.cires.pace.data.object.Dataset; import java.io.FileInputStream; @@ -61,7 +60,7 @@ public static boolean filterTimeSize(Path source, Path target) throws IOExceptio } public static Path writeMetadata(Dataset dataset, ObjectMapper objectMapper, Path targetDirectory) throws IOException { - String metadata = objectMapper.writeValueAsString(dataset); + String metadata = objectMapper.writerWithView(Dataset.class).writeValueAsString(dataset); mkdir(targetDirectory); diff --git a/pace-packaging/src/main/java/edu/colorado/cires/pace/packaging/PackageProcessor.java b/pace-packaging/src/main/java/edu/colorado/cires/pace/packaging/PackageProcessor.java index 23614754..951b709d 100644 --- a/pace-packaging/src/main/java/edu/colorado/cires/pace/packaging/PackageProcessor.java +++ b/pace-packaging/src/main/java/edu/colorado/cires/pace/packaging/PackageProcessor.java @@ -1,6 +1,7 @@ package edu.colorado.cires.pace.packaging; import com.fasterxml.jackson.databind.ObjectMapper; +import edu.colorado.cires.pace.data.object.Dataset; import edu.colorado.cires.pace.data.object.PackingJob; import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; @@ -29,7 +30,7 @@ public void process(PackingJob packingJob, Path outputDir, boolean sourceDataCon Packager.run( PackageInstructionFactory.getPackageInstructions( packingJob, - FileUtils.writeMetadata(packingJob.getDataset(), objectMapper, outputDir.resolve("data")), + FileUtils.writeMetadata((Dataset) packingJob, objectMapper, outputDir.resolve("data")), outputDir, sourceDataContainsAudioFiles ), diff --git a/pace-packaging/src/test/java/edu/colorado/cires/pace/packaging/PackageInstructionFactoryTest.java b/pace-packaging/src/test/java/edu/colorado/cires/pace/packaging/PackageInstructionFactoryTest.java index da5727d5..503332ec 100644 --- a/pace-packaging/src/test/java/edu/colorado/cires/pace/packaging/PackageInstructionFactoryTest.java +++ b/pace-packaging/src/test/java/edu/colorado/cires/pace/packaging/PackageInstructionFactoryTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import edu.colorado.cires.pace.data.object.AudioPackingJob; import edu.colorado.cires.pace.data.object.PackingJob; import java.io.FileWriter; import java.io.IOException; @@ -66,7 +67,7 @@ void testGetPackageInstructions( Path dp = documentsPath == null ? null : Path.of(documentsPath).toAbsolutePath(); Path cdp = calibrationDocumentsPath == null ? null : Path.of(calibrationDocumentsPath).toAbsolutePath(); Path bp = biologicalPath == null ? null : Path.of(biologicalPath).toAbsolutePath(); - PackingJob packingJob = PackingJob.builder() + PackingJob packingJob = AudioPackingJob.builder() .temperaturePath(tp) .biologicalPath(bp) .otherPath(op) @@ -111,7 +112,7 @@ void testGetPackageInstructions( @Test void testPathDoesNotExist() throws IOException { Path sourcePath = Path.of("target/source/source-files").toAbsolutePath(); - PackingJob packingJob = PackingJob.builder() + PackingJob packingJob = AudioPackingJob.builder() .sourcePath(sourcePath) .build(); writeFiles(sourcePath); diff --git a/pace-packaging/src/test/java/edu/colorado/cires/pace/packaging/PackagerProcessorTest.java b/pace-packaging/src/test/java/edu/colorado/cires/pace/packaging/PackagerProcessorTest.java index 296d6cde..15dd40c8 100644 --- a/pace-packaging/src/test/java/edu/colorado/cires/pace/packaging/PackagerProcessorTest.java +++ b/pace-packaging/src/test/java/edu/colorado/cires/pace/packaging/PackagerProcessorTest.java @@ -2,12 +2,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import edu.colorado.cires.pace.data.object.AudioDataset; +import edu.colorado.cires.pace.data.object.AudioPackingJob; import edu.colorado.cires.pace.data.object.AudioSensor; import edu.colorado.cires.pace.data.object.Channel; import edu.colorado.cires.pace.data.object.DataQualityEntry; @@ -28,8 +28,6 @@ import edu.colorado.cires.pace.data.object.SampleRate; import edu.colorado.cires.pace.data.object.Sea; import edu.colorado.cires.pace.data.object.StationaryMarineLocation; -import jakarta.validation.ConstraintViolation; -import jakarta.validation.ConstraintViolationException; import java.io.FileWriter; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -45,7 +43,6 @@ import org.apache.commons.io.FileUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -68,23 +65,6 @@ void afterEach() { FileUtils.deleteQuietly(testOutputPath.toFile()); FileUtils.deleteQuietly(testSourcePath.toFile()); } - - @Test - void testProcessNoMetadata() { - ConstraintViolationException exception = assertThrows(ConstraintViolationException.class, () -> processor.process( - PackingJob.builder() - .sourcePath(Path.of("TEST")) - .build(), - testOutputPath, - true - )); - assertEquals("PackingJob validation failed", exception.getMessage()); - assertEquals(1, exception.getConstraintViolations().size()); - - ConstraintViolation constraintViolation = exception.getConstraintViolations().iterator().next(); - assertEquals("dataset", constraintViolation.getPropertyPath().toString()); - assertEquals("must not be null", constraintViolation.getMessage()); - } @ParameterizedTest @CsvSource(value = { @@ -115,7 +95,15 @@ void testProcess( Path cdp = calibrationDocumentsPath == null ? null : Path.of(calibrationDocumentsPath).toAbsolutePath(); Path bp = biologicalPath == null ? null : Path.of(biologicalPath).toAbsolutePath(); - Dataset dataset = createDataset(); + PackingJob packingJob = createPackingJob( + sp, + tp, + op, + np, + dp, + cdp, + bp + ); writeFiles(bp); writeFiles(cdp); @@ -124,23 +112,12 @@ void testProcess( writeFiles(op); writeFiles(tp); writeFiles(sp); - - PackingJob packingJob = PackingJob.builder() - .temperaturePath(tp) - .biologicalPath(bp) - .otherPath(op) - .documentsPath(dp) - .calibrationDocumentsPath(cdp) - .navigationPath(np) - .sourcePath(sp) - .dataset(dataset) - .build(); packingJob = objectMapper.readValue( objectMapper.writeValueAsString(packingJob), PackingJob.class ); - String expectedMetadata = objectMapper.writeValueAsString(dataset); + String expectedMetadata = objectMapper.writerWithView(Dataset.class).writeValueAsString(packingJob); processor.process(packingJob, testOutputPath, sourceContainsAudioData); @@ -157,14 +134,32 @@ void testProcess( )); String actualMetadata = FileUtils.readFileToString(testOutputPath.resolve("data").resolve(String.format( - "%s.json", dataset.getPackageId() + "%s.json", ((Dataset) packingJob).getPackageId() )).toFile(), StandardCharsets.UTF_8); assertEquals(expectedMetadata, actualMetadata); + + Dataset dataset = objectMapper.readValue(actualMetadata, Dataset.class); + assertInstanceOf(AudioDataset.class, dataset); } - private Dataset createDataset() { - return AudioDataset.builder() + private PackingJob createPackingJob( + Path sourcePath, + Path temperaturePath, + Path otherPath, + Path navigationPath, + Path documentsPath, + Path calibrationDocumentsPath, + Path biologicalPath + ) { + return AudioPackingJob.builder() + .sourcePath(sourcePath) + .temperaturePath(temperaturePath) + .otherPath(otherPath) + .navigationPath(navigationPath) + .documentsPath(documentsPath) + .calibrationDocumentsPath(calibrationDocumentsPath) + .biologicalPath(biologicalPath) .siteOrCruiseName("siteOrCruiseName") .deploymentId("deploymentId") .datasetPackager(Person.builder()