From 84cb1d48866f64b3ae9ac1d5958546ea0bf414b3 Mon Sep 17 00:00:00 2001
From: Ruslan Forostianov <ruslan@se4.bio>
Date: Sat, 14 Dec 2024 14:05:17 +0100
Subject: [PATCH] Simple entry point. Fetch MAF records and write them to zip
 file

---
 .../file/export/MafRecordFetcher.java         |  78 ++++++++++
 .../org/cbioportal/file/model/MafRecord.java  |   8 +-
 .../java/org/cbioportal/model/Mutation.java   | 143 ++++++++++++++++++
 .../service/impl/ExportService.java           | 116 +++++++-------
 .../org/cbioportal/web/ExportController.java  |  42 +++++
 .../persistence/mybatis/MutationMapper.xml    |  29 +++-
 .../file/export/MafRecordWriterTest.java      |   4 +-
 .../ExportStudyDataIntegrationTest.java       |  77 ++++++++++
 8 files changed, 427 insertions(+), 70 deletions(-)
 create mode 100644 src/main/java/org/cbioportal/file/export/MafRecordFetcher.java
 create mode 100644 src/main/java/org/cbioportal/web/ExportController.java
 create mode 100644 src/test/java/org/cbioportal/test/integration/export/ExportStudyDataIntegrationTest.java

diff --git a/src/main/java/org/cbioportal/file/export/MafRecordFetcher.java b/src/main/java/org/cbioportal/file/export/MafRecordFetcher.java
new file mode 100644
index 00000000000..86dc2027c59
--- /dev/null
+++ b/src/main/java/org/cbioportal/file/export/MafRecordFetcher.java
@@ -0,0 +1,78 @@
+package org.cbioportal.file.export;
+
+import org.cbioportal.file.model.MafRecord;
+import org.cbioportal.model.MolecularProfile;
+import org.cbioportal.model.Mutation;
+import org.cbioportal.service.MolecularProfileService;
+import org.cbioportal.service.MutationService;
+import org.springframework.stereotype.Component;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@Component
+public class MafRecordFetcher {
+
+    private final MolecularProfileService molecularProfileService;
+    private final MutationService mutationService;
+
+    public MafRecordFetcher(MolecularProfileService molecularProfileService, MutationService mutationService) {
+        this.molecularProfileService = molecularProfileService;
+        this.mutationService = mutationService;
+    }
+
+    public Iterator<MafRecord> fetch(Map<String, Set<String>> sampleIdsByStudyId) {
+        List<String> studyIds = List.copyOf(sampleIdsByStudyId.keySet());
+        List<String> molecularProfileStableIds = this.molecularProfileService.getMolecularProfilesInStudies(studyIds, "ID").stream()
+            .map(MolecularProfile::getStableId).toList();
+        List<String> sampleIds = List.copyOf(sampleIdsByStudyId.values().stream().flatMap(Set::stream).toList());
+        List<Integer> entrezGeneIds = List.of();
+        List<Mutation> mutationList = mutationService.getMutationsInMultipleMolecularProfiles(
+            molecularProfileStableIds, sampleIds, entrezGeneIds, "EXPORT", null, null, null, null);
+        return mutationList.stream()
+            .filter(mutation -> sampleIdsByStudyId.get(mutation.getStudyId()).contains(mutation.getSampleId()))
+            .map(mutation -> new MafRecord(
+                mutation.getGene().getHugoGeneSymbol(),
+                mutation.getGene().getEntrezGeneId().toString(),
+                mutation.getCenter(),
+                mutation.getNcbiBuild(),
+                mutation.getChr(),
+                mutation.getStartPosition(),
+                mutation.getEndPosition(),
+                "+",
+                mutation.getMutationType(),
+                mutation.getVariantType(),
+                mutation.getReferenceAllele(),
+                mutation.getTumorSeqAllele(),
+                //TODO check if this is correct
+                mutation.getTumorSeqAllele(),
+                mutation.getDbSnpRs(),
+                mutation.getDbSnpValStatus(),
+                mutation.getSampleId(),
+                mutation.getMatchedNormSampleBarcode(),
+                mutation.getMatchNormSeqAllele1(),
+                mutation.getMatchNormSeqAllele2(),
+                mutation.getTumorValidationAllele1(),
+                mutation.getTumorValidationAllele2(),
+                mutation.getMatchNormValidationAllele1(),
+                mutation.getMatchNormValidationAllele2(),
+                mutation.getVerificationStatus(),
+                mutation.getValidationStatus(),
+                mutation.getMutationStatus(),
+                mutation.getSequencingPhase(),
+                mutation.getSequenceSource(),
+                mutation.getValidationMethod(),
+                mutation.getScore() == null ? null : mutation.getScore().toString(),
+                mutation.getBamFile(),
+                mutation.getSequencer(),
+                //TODO how to calculate HgvpShort?
+                "",
+                mutation.getTumorAltCount(),
+                mutation.getTumorRefCount(),
+                mutation.getNormalAltCount(),
+                mutation.getNormalRefCount()
+            )).iterator();
+    }
+}
diff --git a/src/main/java/org/cbioportal/file/model/MafRecord.java b/src/main/java/org/cbioportal/file/model/MafRecord.java
index 9dbda61785a..1a3bfe85825 100644
--- a/src/main/java/org/cbioportal/file/model/MafRecord.java
+++ b/src/main/java/org/cbioportal/file/model/MafRecord.java
@@ -32,12 +32,12 @@ public record MafRecord(
     /**
      * Start position of event.
      */
-    Integer startPosition,
+    Long startPosition,
 
     /**
      * End position of event.
      */
-    Integer endPosition,
+    Long endPosition,
 
     /**
      * We assume that the mutation is reported for the + strand.
@@ -150,12 +150,12 @@ public record MafRecord(
     String validationMethod,
 
     /**
-     * Not used.
+     * Score
      */
     String score,
 
     /**
-     * Not used.
+     * The BAM file used to call the variant.
      */
     String bamFile,
 
diff --git a/src/main/java/org/cbioportal/model/Mutation.java b/src/main/java/org/cbioportal/model/Mutation.java
index 3ddae915587..6f5079352d5 100644
--- a/src/main/java/org/cbioportal/model/Mutation.java
+++ b/src/main/java/org/cbioportal/model/Mutation.java
@@ -33,6 +33,22 @@ public class Mutation extends Alteration implements Serializable {
     @JsonRawValue
     @Schema(type = "java.util.Map")
     private Object annotationJSON;
+    private String dbSnpRs;
+    private String dbSnpValStatus;
+    private String matchedNormSampleBarcode;
+    private String matchNormSeqAllele1;
+    private String matchNormSeqAllele2;
+    private String tumorValidationAllele1;
+    private String tumorValidationAllele2;
+    private String matchNormValidationAllele1;
+    private String matchNormValidationAllele2;
+    private String verificationStatus;
+    private String sequencingPhase;
+    private String sequenceSource;
+    private String validationMethod;
+    private BigDecimal score;
+    private String bamFile;
+    private String sequencer;
     
     public String getCenter() {
         return center;
@@ -213,4 +229,131 @@ public void setAnnotationJSON(String annotationJSON) {
         this.annotationJSON = annotationJSON;
     }
 
+    public String getDbSnpRs() {
+        return dbSnpRs;
+    }
+
+    public void setDbSnpRs(String dbSnpRs) {
+        this.dbSnpRs = dbSnpRs;
+    }
+
+    public String getDbSnpValStatus() {
+        return dbSnpValStatus;
+    }
+
+    public void setDbSnpValStatus(String dbSnpValStatus) {
+        this.dbSnpValStatus = dbSnpValStatus;
+    }
+
+    public String getMatchedNormSampleBarcode() {
+        return matchedNormSampleBarcode;
+    }
+
+    public void setMatchedNormSampleBarcode(String matchedNormSampleBarcode) {
+        this.matchedNormSampleBarcode = matchedNormSampleBarcode;
+    }
+
+    public String getMatchNormSeqAllele1() {
+        return matchNormSeqAllele1;
+    }
+
+    public void setMatchNormSeqAllele1(String matchNormSeqAllele1) {
+        this.matchNormSeqAllele1 = matchNormSeqAllele1;
+    }
+
+    public String getMatchNormSeqAllele2() {
+        return matchNormSeqAllele2;
+    }
+
+    public void setMatchNormSeqAllele2(String matchNormSeqAllele2) {
+        this.matchNormSeqAllele2 = matchNormSeqAllele2;
+    }
+
+    public String getTumorValidationAllele1() {
+        return tumorValidationAllele1;
+    }
+
+    public void setTumorValidationAllele1(String tumorValidationAllele1) {
+        this.tumorValidationAllele1 = tumorValidationAllele1;
+    }
+
+    public String getTumorValidationAllele2() {
+        return tumorValidationAllele2;
+    }
+
+    public void setTumorValidationAllele2(String tumorValidationAllele2) {
+        this.tumorValidationAllele2 = tumorValidationAllele2;
+    }
+
+    public String getMatchNormValidationAllele1() {
+        return matchNormValidationAllele1;
+    }
+
+    public void setMatchNormValidationAllele1(String matchNormValidationAllele1) {
+        this.matchNormValidationAllele1 = matchNormValidationAllele1;
+    }
+
+    public String getMatchNormValidationAllele2() {
+        return matchNormValidationAllele2;
+    }
+
+    public void setMatchNormValidationAllele2(String matchNormValidationAllele2) {
+        this.matchNormValidationAllele2 = matchNormValidationAllele2;
+    }
+
+    public String getVerificationStatus() {
+        return verificationStatus;
+    }
+
+    public void setVerificationStatus(String verificationStatus) {
+        this.verificationStatus = verificationStatus;
+    }
+
+    public String getSequencingPhase() {
+        return sequencingPhase;
+    }
+
+    public void setSequencingPhase(String sequencingPhase) {
+        this.sequencingPhase = sequencingPhase;
+    }
+
+    public String getSequenceSource() {
+        return sequenceSource;
+    }
+
+    public void setSequenceSource(String sequenceSource) {
+        this.sequenceSource = sequenceSource;
+    }
+
+    public String getValidationMethod() {
+        return validationMethod;
+    }
+
+    public void setValidationMethod(String validationMethod) {
+        this.validationMethod = validationMethod;
+    }
+
+    public BigDecimal getScore() {
+        return score;
+    }
+
+    public void setScore(BigDecimal score) {
+        this.score = score;
+    }
+
+    public String getBamFile() {
+        return bamFile;
+    }
+
+    public void setBamFile(String bamFile) {
+        this.bamFile = bamFile;
+    }
+
+    public String getSequencer() {
+        return sequencer;
+    }
+
+    public void setSequencer(String sequencer) {
+        this.sequencer = sequencer;
+    }
 }
diff --git a/src/main/java/org/cbioportal/service/impl/ExportService.java b/src/main/java/org/cbioportal/service/impl/ExportService.java
index f72c98bdbd4..c1cac6120d2 100644
--- a/src/main/java/org/cbioportal/service/impl/ExportService.java
+++ b/src/main/java/org/cbioportal/service/impl/ExportService.java
@@ -1,76 +1,74 @@
 package org.cbioportal.service.impl;
 
-import org.apache.commons.lang3.tuple.Pair;
-import org.cbioportal.model.*;
+import org.cbioportal.file.export.MafRecordFetcher;
+import org.cbioportal.file.export.MafRecordWriter;
+import org.cbioportal.model.CancerStudy;
+import org.cbioportal.model.Sample;
 import org.cbioportal.service.*;
-import org.cbioportal.service.exception.MolecularProfileNotFoundException;
-import org.cbioportal.service.exception.PatientNotFoundException;
-import org.cbioportal.service.exception.SampleNotFoundException;
-import org.cbioportal.service.exception.StudyNotFoundException;
-import org.springframework.beans.factory.annotation.Autowired;
+import org.cbioportal.service.util.SessionServiceRequestHandler;
+import org.cbioportal.web.parameter.VirtualStudy;
+import org.cbioportal.web.parameter.VirtualStudySamples;
 import org.springframework.stereotype.Service;
 
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
 
 @Service
 public class ExportService {
 
-    @Autowired
-    PatientService patientService;
-    
-    @Autowired
-    SampleService sampleService;
-    
-    @Autowired
-    ClinicalDataService clinicalDataService;
-    
-    @Autowired
-    MutationService mutationService;
-    
-    @Autowired
-    MolecularProfileService molecularProfileService; 
+    private final StudyService studyService;
+    private final SessionServiceRequestHandler sessionServiceRequestHandler;
+    private final SampleService sampleService;
+    private final MafRecordFetcher mafRecordFetcher;
+
+    public ExportService(StudyService studyService,
+                         SessionServiceRequestHandler sessionServiceRequestHandler,
+                         SampleService sampleService,
+                         MafRecordFetcher mafRecordFetcher) {
+        this.studyService = studyService;
+        this.sessionServiceRequestHandler = sessionServiceRequestHandler;
+        this.sampleService = sampleService;
+        this.mafRecordFetcher = mafRecordFetcher;
+    }
     
-    public void exportData(Map<String, List<String>> samples) throws SampleNotFoundException, StudyNotFoundException, PatientNotFoundException, MolecularProfileNotFoundException {
-        if (samples.size() > 1) {
-            //virtual study
+    public ByteArrayOutputStream exportStudyDataToZip(String studyId) throws IOException {
+        List<CancerStudy> studies = studyService.fetchStudies(List.of(studyId), "DETAILED");
+        Map<String, Set<String>> studyToSampleMap = new HashMap<>();
+        if (studies.isEmpty()) {
+           VirtualStudy virtualStudy = sessionServiceRequestHandler.getVirtualStudyById(studyId);
+           studyToSampleMap.putAll(
+               virtualStudy.getData().getStudies().stream().collect(Collectors.toMap(VirtualStudySamples::getId, VirtualStudySamples::getSamples)));
+        } else {
+           List<Sample> samples = sampleService.getAllSamplesInStudies(List.of(studyId), "ID", null, null, null, null);
+           studyToSampleMap.put(studyId, samples.stream().map(Sample::getStableId).collect(Collectors.toSet()));
         }
-        for (Map.Entry<String, List<String>> studySamples: samples.entrySet()) {
-            String studyId = studySamples.getKey();
-            for (String sampleId: studySamples.getValue()) {
-                Sample sample = sampleService.getSampleInStudy(studyId, sampleId);
-                String patientStableId = sample.getPatientStableId();
-                String sampleStableId = sample.getStableId();
-                List<ClinicalData> sampleClinicalData = clinicalDataService.getAllClinicalDataOfSampleInStudy(studyId, sampleStableId, null, null, null, null, null, null);
-                for (ClinicalData sampleClinicalDatum : sampleClinicalData) {
-                   ClinicalAttribute clinicalAttribute = sampleClinicalDatum.getClinicalAttribute();
-                   sampleClinicalDatum.getAttrId();
-                   sampleClinicalDatum.getAttrValue();
-                }
-                Patient patient = patientService.getPatientInStudy(studyId, patientStableId);
-                List<ClinicalData> patientClinicalData = clinicalDataService.getAllClinicalDataOfPatientInStudy(studyId, patientStableId, null, null, null, null, null, null);
-                for (ClinicalData patientClinicalDataItem : patientClinicalData) {
-                    ClinicalAttribute clinicalAttribute = patientClinicalDataItem.getClinicalAttribute();
-                    patientClinicalDataItem.getAttrId();
-                    patientClinicalDataItem.getAttrValue();
-                }
-            }
-            List<MolecularProfileCaseIdentifier> molecularProfileCaseIdentifiers = molecularProfileService.getMolecularProfileCaseIdentifiers(List.of(studyId), studySamples.getValue());
-            for (MolecularProfileCaseIdentifier molecularProfileCaseIdentifier : molecularProfileCaseIdentifiers) {
-               MolecularProfile molecularProfile = molecularProfileService.getMolecularProfile(molecularProfileCaseIdentifier.getMolecularProfileId());
-               MolecularProfile.MolecularAlterationType molecularAlterationType = molecularProfile.getMolecularAlterationType();
-               molecularProfile.getDatatype();
-               molecularProfile.getName();
-               switch (molecularAlterationType) {
-                   case MUTATION_EXTENDED -> {
-                       List<Mutation> mutationList = mutationService.getMutationsInMultipleMolecularProfilesByGeneQueries(List.of(molecularProfileCaseIdentifier.getMolecularProfileId()), studySamples.getValue(), List.of(), "DETAILED", 10000, 1, null, null);
-                       for (Mutation mutation : mutationList) {
-                           mutation.getChr();
-                       }
-                   }
-               }
-            }
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        try (ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream)) {
+            // Add files to the ZIP
+            StringWriter mafRecordsStringWriter = new StringWriter();
+            MafRecordWriter mafRecordWriter = new MafRecordWriter(mafRecordsStringWriter);
+            //TODO do not produce the file if no data has been retrieved
+            mafRecordWriter.write(mafRecordFetcher.fetch(studyToSampleMap));
+            addFileToZip(zipOutputStream, "data_mutation.txt", mafRecordsStringWriter.toString().getBytes());
         }
+        return byteArrayOutputStream;
+    }
+
+    private void addFileToZip(ZipOutputStream zipOutputStream, String fileName, byte[] fileContent) throws IOException {
+        // Create a new ZIP entry for the file
+        ZipEntry zipEntry = new ZipEntry(fileName);
+        zipOutputStream.putNextEntry(zipEntry);
+
+        // Write file content
+        zipOutputStream.write(fileContent);
+        zipOutputStream.closeEntry();
     }
 }
diff --git a/src/main/java/org/cbioportal/web/ExportController.java b/src/main/java/org/cbioportal/web/ExportController.java
new file mode 100644
index 00000000000..73735b5e2cd
--- /dev/null
+++ b/src/main/java/org/cbioportal/web/ExportController.java
@@ -0,0 +1,42 @@
+package org.cbioportal.web;
+
+import org.cbioportal.service.impl.ExportService;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+@RestController
+public class ExportController {
+
+    private final ExportService exportService;
+
+    public ExportController(ExportService exportService) {
+        this.exportService = exportService;
+    }
+
+    //TODO make it work for virtual studies as well
+    //@PreAuthorize("hasPermission(#studyId, 'CancerStudyId', T(org.cbioportal.utils.security.AccessLevel).READ)")
+    @GetMapping("/export/study/{studyId}.zip")
+    public ResponseEntity<byte[]> downloadStudyData(@PathVariable String studyId) throws IOException {
+        ByteArrayOutputStream byteArrayOutputStream = exportService.exportStudyDataToZip(studyId);
+
+        // Build the response
+        byte[] zipBytes = byteArrayOutputStream.toByteArray();
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(new MediaType("application", "zip"));
+        headers.setContentDispositionFormData("attachment", studyId + ".zip");
+
+        return ResponseEntity.ok()
+            .headers(headers)
+            .body(zipBytes);
+    }
+}
diff --git a/src/main/resources/org/cbioportal/persistence/mybatis/MutationMapper.xml b/src/main/resources/org/cbioportal/persistence/mybatis/MutationMapper.xml
index de6a18dc104..2219580b315 100644
--- a/src/main/resources/org/cbioportal/persistence/mybatis/MutationMapper.xml
+++ b/src/main/resources/org/cbioportal/persistence/mybatis/MutationMapper.xml
@@ -9,7 +9,7 @@
         patient.STABLE_ID AS "patientId",
         mutation.ENTREZ_GENE_ID AS "entrezGeneId",
         cancer_study.CANCER_STUDY_IDENTIFIER AS "studyId"
-        <if test="projection == 'SUMMARY' || projection == 'DETAILED'">
+        <if test="projection == 'SUMMARY' || projection == 'DETAILED' || projection == 'EXPORT'">
             ,
             mutation.CENTER AS "center",
             mutation.MUTATION_STATUS AS "mutationStatus",
@@ -38,7 +38,7 @@
             alteration_driver_annotation.DRIVER_TIERS_FILTER AS "driverTiersFilter",
             alteration_driver_annotation.DRIVER_TIERS_FILTER_ANNOTATION as "driverTiersFilterAnnotation"
         </if>
-        <if test="projection == 'DETAILED'">
+        <if test="projection == 'DETAILED' || projection == 'EXPORT'">
             ,
             <include refid="org.cbioportal.persistence.mybatis.GeneMapper.select">
                 <property name="prefix" value="gene."/>
@@ -48,6 +48,25 @@
                 <property name="prefix" value="alleleSpecificCopyNumber."/>
             </include>
         </if>
+        <if test="projection == 'EXPORT'">
+            ,
+            mutation.MATCHED_NORM_SAMPLE_BARCODE as "matchedNormSampleBarcode",
+            mutation.MATCH_NORM_SEQ_ALLELE1 as "matchNormSeqAllele1",
+            mutation.MATCH_NORM_SEQ_ALLELE2 as "matchNormSeqAllele2",
+            mutation.TUMOR_VALIDATION_ALLELE1 as "tumorValidationAllele1",
+            mutation.TUMOR_VALIDATION_ALLELE2 as "tumorValidationAllele2",
+            mutation.MATCH_NORM_VALIDATION_ALLELE1 as "matchNormValidationAllele1",
+            mutation.MATCH_NORM_VALIDATION_ALLELE2 as "matchNormValidationAllele2",
+            mutation.VERIFICATION_STATUS as "verificationStatus",
+            mutation.SEQUENCING_PHASE as "sequencingPhase",
+            mutation.SEQUENCE_SOURCE as "sequenceSource",
+            mutation.VALIDATION_METHOD as "validationMethod",
+            mutation.SCORE as "score",
+            mutation.BAM_FILE as "bamFile",
+            mutation.SEQUENCER as "sequencer",
+            mutation_event.DB_SNP_RS AS "dbSnpRs",
+            mutation_event.DB_SNP_VAL_STATUS AS "dbSnpValStatus"
+        </if>
     </sql>
     
     <sql id="projectionAndLimitFilter">
@@ -294,7 +313,7 @@
         <include refid="select"/>
         <include refid="from"/>
         INNER JOIN mutation_event ON mutation.MUTATION_EVENT_ID = mutation_event.MUTATION_EVENT_ID
-        <if test="projection == 'DETAILED'">
+        <if test="projection == 'DETAILED' || projection == 'EXPORT'">
             INNER JOIN gene ON mutation.ENTREZ_GENE_ID = gene.ENTREZ_GENE_ID
             <include refid="includeAlleleSpecificCopyNumber"/>
         </if>
@@ -324,7 +343,7 @@
         <include refid="select"/>
         <include refid="from"/>
         INNER JOIN mutation_event ON mutation.MUTATION_EVENT_ID = mutation_event.MUTATION_EVENT_ID
-        <if test="projection == 'DETAILED'">
+        <if test="projection == 'DETAILED' || projection == 'EXPORT'">
             INNER JOIN gene ON mutation.ENTREZ_GENE_ID = gene.ENTREZ_GENE_ID
             <include refid="includeAlleleSpecificCopyNumber"/>
         </if>
@@ -337,7 +356,7 @@
         <include refid="select"/>
         <include refid="from"/>
         INNER JOIN mutation_event ON mutation.MUTATION_EVENT_ID = mutation_event.MUTATION_EVENT_ID
-        <if test="projection == 'DETAILED'">
+        <if test="projection == 'DETAILED' || projection == 'EXPORT'">
             INNER JOIN gene ON mutation.ENTREZ_GENE_ID = gene.ENTREZ_GENE_ID
             <include refid="includeAlleleSpecificCopyNumber"/>
         </if>
diff --git a/src/test/java/org/cbioportal/file/export/MafRecordWriterTest.java b/src/test/java/org/cbioportal/file/export/MafRecordWriterTest.java
index 990f1efec33..cdc2ddfa882 100644
--- a/src/test/java/org/cbioportal/file/export/MafRecordWriterTest.java
+++ b/src/test/java/org/cbioportal/file/export/MafRecordWriterTest.java
@@ -22,8 +22,8 @@ public void testMafRecordWriter() {
                 "center1", 
                 "hg38", 
                 "X", 
-                1000000, 
-                1000100, 
+                1000000L,
+                1000100L,
                 "+",
                 "Missense_Mutation",
                 "SNP",
diff --git a/src/test/java/org/cbioportal/test/integration/export/ExportStudyDataIntegrationTest.java b/src/test/java/org/cbioportal/test/integration/export/ExportStudyDataIntegrationTest.java
new file mode 100644
index 00000000000..978fad32829
--- /dev/null
+++ b/src/test/java/org/cbioportal/test/integration/export/ExportStudyDataIntegrationTest.java
@@ -0,0 +1,77 @@
+package org.cbioportal.test.integration.export;
+
+import org.cbioportal.test.integration.security.ContainerConfig;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.MethodSorters;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.core.io.Resource;
+import org.springframework.http.*;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.cbioportal.test.integration.security.ContainerConfig.*;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(
+    webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT
+)
+@TestPropertySource(
+    properties = {
+        "authenticate=false",
+        "session.endpoint.publisher-api-key=this-is-a-secret",
+        "session.service.url=http://localhost:" + SESSION_SERVICE_PORT + "/api/sessions/public_portal/",
+        // DB settings (also see MysqlInitializer)
+        "spring.datasource.driverClassName=com.mysql.jdbc.Driver",
+        "spring.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect",
+    }
+)
+@ContextConfiguration(initializers = {
+    MyMysqlInitializer.class,
+    PortInitializer.class
+})
+@DirtiesContext
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class ExportStudyDataIntegrationTest extends ContainerConfig {
+
+    static final String CBIO_URL = String.format("http://localhost:%d", CBIO_PORT);
+
+    @Autowired
+    private TestRestTemplate restTemplate;
+
+    @Test
+    public void test1NoPublicVirtualStudiesAtTheBeginning() throws IOException {
+        ResponseEntity<Resource> response1 = restTemplate.getForEntity(
+            CBIO_URL + "/export/study/study_tcga_pub.zip", Resource.class);
+
+        assertThat(response1.getStatusCode().is2xxSuccessful()).isTrue();
+        HttpHeaders headers = response1.getHeaders();
+        assertThat(headers.getContentType()).isNotNull();
+        assertThat(headers.getContentType().toString()).isEqualTo("application/zip");
+
+        // Verify Content-Disposition header for file name
+        String contentDisposition = headers.getFirst(HttpHeaders.CONTENT_DISPOSITION);
+        assertThat(contentDisposition).isNotNull();
+        assertThat(contentDisposition).contains("attachment");
+        assertThat(contentDisposition).contains("study_tcga_pub.zip");
+        // Ensure the ZIP file is not empty
+        try (InputStream zipInputStream = response1.getBody().getInputStream();
+             ZipInputStream zis = new ZipInputStream(zipInputStream)) {
+
+            // Ensure there's at least one entry in the ZIP
+            ZipEntry entry = zis.getNextEntry();
+            assertThat(entry).isNotNull(); // Assert that the ZIP contains at least one file
+        }
+    }
+}
\ No newline at end of file