Java tutorial
/* * To change this license header, choose License Headers in Project Properties. * To change this template file, choose Tools | Templates * and open the template in the editor. */ package com.compomics.colims.core.io.mztab; import com.compomics.colims.core.service.FastaDbService; import com.compomics.colims.core.service.PeptideService; import com.compomics.colims.core.service.ProteinGroupQuantLabeledService; import com.compomics.colims.core.service.ProteinGroupQuantService; import com.compomics.colims.core.service.ProteinGroupService; import com.compomics.colims.core.service.SearchAndValidationSettingsService; import com.compomics.colims.core.service.UniProtService; import com.compomics.colims.core.util.AccessionConverter; import com.compomics.colims.core.util.ProteinCoverage; import com.compomics.colims.model.AnalyticalRun; import com.compomics.colims.model.FastaDb; import com.compomics.colims.model.Peptide; import com.compomics.colims.model.PeptideHasModification; import com.compomics.colims.model.PeptideHasProteinGroup; import com.compomics.colims.model.Protein; import com.compomics.colims.model.ProteinGroupHasProtein; import com.compomics.colims.model.ProteinGroupQuantLabeled; import com.compomics.colims.model.QuantificationMethodHasReagent; import com.compomics.colims.model.QuantificationSettings; import com.compomics.colims.model.SearchAndValidationSettings; import com.compomics.colims.model.SearchParametersHasModification; import com.compomics.colims.model.Spectrum; import com.compomics.colims.model.enums.FastaDbType; import com.compomics.colims.model.enums.ModificationType; import com.compomics.colims.repository.hibernate.ProteinGroupDTO; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.log4j.Logger; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.io.*; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import org.apache.commons.lang3.StringUtils; /** * @author Niels Hulstaert */ @Component("mzTabExporter") public class MzTabExporter { /** * Logger instance. */ private static final Logger LOGGER = Logger.getLogger(MzTabExporter.class); private static final String JSON_VALUES = "values"; private static final String JSON_NAME = "user_friendly_name"; private static final String MZTAB_EXTENSION = ".mzTab"; private static final String COLUMN_DELIMITER = "\t"; private static final String COMMENT_PREFIX = "COM"; private static final String METADATA_PREFIX = "MTD"; private static final String PROTEINS_HEADER_PREFIX = "PRH"; private static final String PROTEINS_PREFIX = "PRT"; private static final String PSM_HEADER_PREFIX = "PSH"; private static final String PSM_PREFIX = "PSM"; private static final String OPEN_BRACKET = "["; private static final String CLOSE_BRACKET = "]"; private static final String COMMA_SEPARATOR = ", "; private static final String VERTICAL_BAR = "|"; private static final String UNLABELED_SAMPLE = "unlabeled sample"; /** * Metadata section. */ private static final String MZTAB_VERSION = "mzTab_version"; private static final String VERSION = "1.0.0"; private static final String MZTAB_MODE = "mzTab_mode"; private static final String MODE_SUMMARY = "Summary"; private static final String MZTAB_COMPLETE = "Complete"; private static final String MZTAB_TYPE = "mzTab_type"; private static final String MZTAB_ID = "mzTab_ID"; private static final String DESCRIPTION = "description"; private static final String RUN_LOCATION = "ms_run[%d]-location"; private static final String FIXED_MOD = "fixed_mod[%d]"; private static final String VARIABLE_MOD = "variable_mod[%d]"; private static final String STUDY_VARIABLE_ASSAY_REFS = "study_variable[%d]-assay_refs"; private static final String STUDY_VARIABLE_DESCRIPTION = "study_variable[%d]-description"; private static final String SOFTWARE = "software[%d]"; private static final String ASSAY_RUN_REF = "assay[%d]-ms_run_ref"; private static final String MS_RUN_REF = "ms_run[%d]"; private static final String ASSAY = "assay[%d]"; private static final String ASSAY_QUANTIFICATION_REAGENT = "assay[%d]-quantification_reagent"; /** * Search engine metadata. */ private static final String PROTEIN_SEARCH_ENGINE_SCORE = "protein_search_engine_score[%d]"; private static final String PEPTIDE_SEARCH_ENGINE_SCORE = "peptide_search_engine_score[%d]"; private static final String PSM_SEARCH_ENGINE_SCORE = "psm_search_engine_score[%d]"; private static final String SMALL_MOLECULE_SEARCH_ENGINE_SCORE = "smallmolecule_search_engine_score[%d]"; /** * Quantification metadata. */ private static final String QUANTIFICATION_METHOD = "quantification_method"; private static final String PROTEIN_QUANTIFICATION_UNIT = "protein-quantification-unit"; private static final String PEPTIDE_QUANTIFICATION_UNIT = "peptide-quantification-unit"; private static final String SMALL_MOLECULE_QUANTIFICATION_UNIT = "smallmolecule-quantification-unit"; /** * Instrument metadata. */ private static final String INSTRUMENT_NAME = "instrument[%d]-name"; private static final String INSTRUMENT_SOURCE = "instrument[%d]-source"; private static final String INSTRUMENT_ANALYZER = "instrument[%d]-analyzer[%d]"; private static final String INSTRUMENT_DETECTOR = "instrument[%d]-detector"; private static final String CONTACT_NAME = "contact[%d]-name"; private static final String CONTACT_AFFILIATION = "contact[%d]-affiliation"; private static final String CONTACT_EMAIL = "contact[%d]-email"; /** * mztab Json mapping */ private static final int PROTEIN_SEARCH_ENGINE_SCORE_ALIGNMENT = 0; private static final int PEPTIDE_SEARCH_ENGINE_SCORE_ALIGNMENT = 1; private static final int PSM_SEARCH_ENGINE_SCORE_ALIGNMENT = 2; private static final int SOFTWARE_ALIGNMENT = 4; /** * Common headers for all sections */ private static final String ACCESSION = "accession"; private static final String DATABASE = "database"; private static final String DATABASE_VERSION = "database_version"; private static final String SEARCH_ENGINE = "search_engine"; private static final String MODIFICATIONS = "modifications"; /** * protein section */ private static final String TAXID = "taxid"; private static final String SPECIES = "species"; private static final String BEST_SEARCH_ENGINE_SCORE = "best_search_engine_score[%d]"; private static final String AMBIGUITY_MEMBERS = "ambiguity_members"; private static final String PROTEIN_COVERAGE = "protein_coverage"; private static final String PROTEIN_ABUNDANCE_STUDY_VARIABLE = "protein_abundance_study_variable[%d]"; private static final String PROTEIN_ABUNDANCE_STDEV_STUDY_VARIABLE = "protein_abundance_stdev_study_variable[%d]"; private static final String PROTEIN_ABUNDANCE_STD_ERROR_STUDY_VARIABLE = "protein_abundance_std_error_study_variable[%d]"; private static final String SEARCH_ENGINE_SCORE_MS_RUN = "search_engine_score[%d]_ms_run[%d]"; private static final String NUM_PSMS_MS_RUN = "num_psms_ms_run[%d]"; private static final String NUM_PEPTIDES_DISTINCT_MS_RUN = "num_peptides_distinct_ms_run[%d]"; private static final String NUM_PEPTIDE_UNIQUE_MS_RUN = "num_peptide_unique_ms_run[%d]"; private static final String PROTEIN_ABUNDANCE_ASSAY = "protein_abundance_assay[%d]"; /** * psm section */ private static final String SEQUENCE = "sequence"; private static final String PSM_ID = "PSM_ID"; private static final String UNIQUE = "unique"; private static final String SEARCH_ENGINE_SCORE = "search_engine_score[%d]"; private static final String RETENTION_TIME = "retention_time"; private static final String CHARGE = "charge"; private static final String EXPERIMENTAL_MASS_TO_CHARGE = "exp_mass_to_charge"; private static final String CALCULATED_MASS_TO_CHARGE = "calc_mass_to_charge"; private static final String SPECTRA_REFERENCE = "spectra_ref"; private static final String PRE = "pre"; private static final String POST = "post"; private static final String START = "start"; private static final String END = "end"; private final ObjectMapper mapper = new ObjectMapper(); private List<MzTabParam> mzTabParams = new ArrayList<>(); /** * assayReagentRef (key : assay, value : quantification reagent) */ private final Map<String, String> assayReagentRef = new HashMap<>(); /** * assayAnalyticalRunRef (key : assay, value : analytical run) */ private final Map<String, AnalyticalRun> assayAnalyticalRunRef = new HashMap<>(); /** * analyticalRunIndexRef (key : analyticalRunId, value : index) */ private final Map<Long, Integer> analyticalRunIndexRef = new HashMap<>(); /** * Software name */ private String software; private final ProteinGroupService proteinGroupService; private final PeptideService peptideService; private final ProteinGroupQuantLabeledService proteinGroupQuantLabeledService; private final ProteinGroupQuantService proteinGroupQuantService; private final SearchAndValidationSettingsService searchAndValidationSettingsService; private final FastaDbService fastaDbService; private final UniProtService uniProtService; /** * The MzTabExport instance. */ private MzTabExport mzTabExport; public MzTabExporter(ProteinGroupService proteinGroupService, SearchAndValidationSettingsService searchAndValidationSettingsService, FastaDbService fastaDbService, UniProtService uniProtService, ProteinGroupQuantLabeledService proteinGroupQuantLabeledService, ProteinGroupQuantService proteinGroupQuantService, PeptideService peptideService) { this.proteinGroupService = proteinGroupService; this.searchAndValidationSettingsService = searchAndValidationSettingsService; this.fastaDbService = fastaDbService; this.uniProtService = uniProtService; this.proteinGroupQuantLabeledService = proteinGroupQuantLabeledService; this.proteinGroupQuantService = proteinGroupQuantService; this.peptideService = peptideService; } /** * Inits the exporter; parses the mzTab JSON file into java objects. * * @throws IOException IOException thrown in case of an I/O related problem */ @PostConstruct public void init() throws IOException { Resource mzTabJson = new ClassPathResource("config/mztab.json"); JsonNode mzTabParamsNode = mapper.readTree(mzTabJson.getInputStream()); //parse JSON node to a list of MzTabParam instances mzTabParams = parseJsonNode(mzTabParamsNode); } /** * Export the mzTabExport input to a mzTab file. * * @param mzTabExport the MzTabExport instance * @throws java.io.IOException */ public void export(MzTabExport mzTabExport) throws IOException { this.mzTabExport = mzTabExport; try (FileOutputStream fos = new FileOutputStream( new File(mzTabExport.getExportDirectory(), mzTabExport.getFileName() + MZTAB_EXTENSION)); OutputStreamWriter osw = new OutputStreamWriter(fos, Charset.forName("UTF-8").newEncoder()); BufferedWriter bw = new BufferedWriter(osw); PrintWriter pw = new PrintWriter(bw)) { switch (mzTabExport.getMzTabType()) { case QUANTIFICATION: switch (mzTabExport.getMzTabMode()) { case SUMMARY: constructMetadata(1, pw); break; case COMPLETE: constructMetadata(2, pw); break; default: break; } break; case IDENTIFICATION: switch (mzTabExport.getMzTabMode()) { case SUMMARY: constructMetadata(3, pw); break; case COMPLETE: constructMetadata(4, pw); break; default: break; } break; default: break; } pw.flush(); } catch (IOException e) { LOGGER.error(e.getMessage(), e); throw new IOException(e.getMessage()); } } private void constructMetadata(int type, PrintWriter pw) throws IOException { //version, type, mode and description pw.println(new StringBuilder().append(METADATA_PREFIX).append(COLUMN_DELIMITER).append(MZTAB_VERSION) .append(COLUMN_DELIMITER).append(VERSION)); pw.println(new StringBuilder().append(METADATA_PREFIX).append(COLUMN_DELIMITER).append(MZTAB_MODE) .append(COLUMN_DELIMITER).append(mzTabExport.getMzTabMode().mzTabName())); pw.println(new StringBuilder().append(METADATA_PREFIX).append(COLUMN_DELIMITER).append(MZTAB_TYPE) .append(COLUMN_DELIMITER).append(mzTabExport.getMzTabType().mzTabName())); pw.println(new StringBuilder().append(METADATA_PREFIX).append(COLUMN_DELIMITER).append(DESCRIPTION) .append(COLUMN_DELIMITER).append(mzTabExport.getDescription())); //run locations for (int i = 0; i < mzTabExport.getRuns().size(); i++) { analyticalRunIndexRef.put(mzTabExport.getRuns().get(i).getId(), i + 1); pw.println(new StringBuilder().append(METADATA_PREFIX).append(COLUMN_DELIMITER) .append(String.format(RUN_LOCATION, i + 1)).append(COLUMN_DELIMITER) .append(mzTabExport.getRuns().get(i).getStorageLocation()).append("\\") .append(mzTabExport.getRuns().get(i).getName())); } if (type == 1 || type == 2) { //protein quantification unit (relative quantification unit from mztab.json) pw.println(new StringBuilder().append(METADATA_PREFIX).append(COLUMN_DELIMITER) .append(PROTEIN_QUANTIFICATION_UNIT).append(COLUMN_DELIMITER) .append(createOntology(mzTabParams.get(3).getMzTabParamOptions().get(1).getOntology(), mzTabParams.get(3).getMzTabParamOptions().get(1).getAccession(), mzTabParams.get(3).getMzTabParamOptions().get(1).getName()))); //peptide quantification unit (relative quantification unit from mztab.json)(same with protein quant unit) pw.println(new StringBuilder().append(METADATA_PREFIX).append(COLUMN_DELIMITER) .append(PEPTIDE_QUANTIFICATION_UNIT).append(COLUMN_DELIMITER) .append(createOntology(mzTabParams.get(3).getMzTabParamOptions().get(1).getOntology(), mzTabParams.get(3).getMzTabParamOptions().get(1).getAccession(), mzTabParams.get(3).getMzTabParamOptions().get(1).getName()))); } //set softwares setSoftware(); //software if (type == 2 || type == 4) { pw.println(new StringBuilder().append(METADATA_PREFIX).append(COLUMN_DELIMITER) .append(String.format(SOFTWARE, 1)).append(COLUMN_DELIMITER).append(getSearchEngine()) .append(COLUMN_DELIMITER)); } // protein search engine score pw.println(new StringBuilder() .append(getEngineScore(PROTEIN_SEARCH_ENGINE_SCORE, PROTEIN_SEARCH_ENGINE_SCORE_ALIGNMENT))); // peptide search engine score pw.println(new StringBuilder() .append(getEngineScore(PEPTIDE_SEARCH_ENGINE_SCORE, PEPTIDE_SEARCH_ENGINE_SCORE_ALIGNMENT))); // psm search engine score pw.println(new StringBuilder() .append(getEngineScore(PSM_SEARCH_ENGINE_SCORE, PSM_SEARCH_ENGINE_SCORE_ALIGNMENT))); // fixed modifications int counter = 1; List<String> modifications = new ArrayList<>(); for (int i = 0; i < mzTabExport.getRuns().size(); i++) { for (SearchParametersHasModification searchParametersHasModification : getSearchAndValidationSettings( mzTabExport.getRuns().get(i)).getSearchParameters().getSearchParametersHasModifications()) { if (searchParametersHasModification.getModificationType().equals(ModificationType.FIXED) && searchParametersHasModification.getSearchModification().getAccession() != null && !modifications .contains(searchParametersHasModification.getSearchModification().getName())) { modifications.add(searchParametersHasModification.getSearchModification().getName()); pw.println(new StringBuilder().append(METADATA_PREFIX).append(COLUMN_DELIMITER) .append(String.format(FIXED_MOD, counter)).append(COLUMN_DELIMITER) .append(createOntology(StringUtils.substringBefore( searchParametersHasModification.getSearchModification().getAccession(), ":"), searchParametersHasModification.getSearchModification().getAccession(), searchParametersHasModification.getSearchModification().getName()))); counter++; } } } // variable modifications counter = 1; modifications = new ArrayList<>(); for (int i = 0; i < mzTabExport.getRuns().size(); i++) { for (SearchParametersHasModification searchParametersHasModification : getSearchAndValidationSettings( mzTabExport.getRuns().get(i)).getSearchParameters().getSearchParametersHasModifications()) { if (searchParametersHasModification.getModificationType().equals(ModificationType.VARIABLE) && searchParametersHasModification.getSearchModification().getAccession() != null && !modifications .contains(searchParametersHasModification.getSearchModification().getName())) { modifications.add(searchParametersHasModification.getSearchModification().getName()); pw.println(new StringBuilder().append(METADATA_PREFIX).append(COLUMN_DELIMITER) .append(String.format(VARIABLE_MOD, counter)).append(COLUMN_DELIMITER) .append(createOntology(StringUtils.substringBefore( searchParametersHasModification.getSearchModification().getAccession(), ":"), searchParametersHasModification.getSearchModification().getAccession(), searchParametersHasModification.getSearchModification().getName()))); counter++; } } } // quantification method if (type == 2) { pw.println(new StringBuilder().append(METADATA_PREFIX).append(COLUMN_DELIMITER) .append(QUANTIFICATION_METHOD).append(COLUMN_DELIMITER) .append(createOntology( StringUtils.substringBefore(getQuantificationSettings(mzTabExport.getRuns().get(0)) .getQuantificationMethod().getAccession(), ":"), getQuantificationSettings(mzTabExport.getRuns().get(0)).getQuantificationMethod() .getAccession(), getQuantificationSettings(mzTabExport.getRuns().get(0)).getQuantificationMethod() .getName()))); } //assay quantification reagents if (type == 1 || type == 2) { for (int i = 0; i < mzTabExport.getRuns().size(); i++) { counter = 0; // label free if (getQuantificationSettings(mzTabExport.getRuns().get(i)).getQuantificationMethod() .getQuantificationMethodHasReagents().isEmpty()) { int assayNumber = mzTabExport.getAnalyticalRunsAssaysRefs() .get(mzTabExport.getRuns().get(i))[counter]; pw.println(new StringBuilder().append(METADATA_PREFIX).append(COLUMN_DELIMITER) .append(String.format(ASSAY_QUANTIFICATION_REAGENT, assayNumber)) .append(COLUMN_DELIMITER) .append(createOntology(mzTabParams.get(5).getMzTabParamOptions().get(2).getOntology(), mzTabParams.get(5).getMzTabParamOptions().get(2).getAccession(), mzTabParams.get(5).getMzTabParamOptions().get(2).getName()))); assayReagentRef.put(String.format(ASSAY, assayNumber), mzTabParams.get(5).getMzTabParamOptions().get(2).getName()); } // labeled for (QuantificationMethodHasReagent quantificationMethodHasReagent : getQuantificationSettings( mzTabExport.getRuns().get(i)).getQuantificationMethod() .getQuantificationMethodHasReagents()) { int assayNumber = mzTabExport.getAnalyticalRunsAssaysRefs() .get(mzTabExport.getRuns().get(i))[counter]; pw.println(new StringBuilder().append(METADATA_PREFIX).append(COLUMN_DELIMITER) .append(String.format(ASSAY_QUANTIFICATION_REAGENT, assayNumber)) .append(COLUMN_DELIMITER) .append(createOntology( StringUtils.substringBefore(quantificationMethodHasReagent .getQuantificationReagent().getAccession(), ":"), quantificationMethodHasReagent.getQuantificationReagent().getAccession(), quantificationMethodHasReagent.getQuantificationReagent().getName()))); assayReagentRef.put(String.format(ASSAY, assayNumber), quantificationMethodHasReagent.getQuantificationReagent().getName()); counter++; } mzTabExport.getAnalyticalRunsAssaysRefs().get(mzTabExport.getRuns().get(i)); } } // assay ms run reference for (int i = 0; i < mzTabExport.getRuns().size(); i++) { // label free if (getQuantificationSettings(mzTabExport.getRuns().get(i)).getQuantificationMethod() .getQuantificationMethodHasReagents().isEmpty()) { int assayNumber = mzTabExport.getAnalyticalRunsAssaysRefs().get(mzTabExport.getRuns().get(i))[0]; pw.println(new StringBuilder().append(METADATA_PREFIX).append(COLUMN_DELIMITER) .append(String.format(ASSAY_RUN_REF, assayNumber)).append(COLUMN_DELIMITER) .append(String.format(MS_RUN_REF, i + 1))); assayAnalyticalRunRef.put(String.format(ASSAY, assayNumber), mzTabExport.getRuns().get(i)); } // labeled for (int j = 0; j < getQuantificationSettings(mzTabExport.getRuns().get(i)).getQuantificationMethod() .getQuantificationMethodHasReagents().size(); j++) { int assayNumber = mzTabExport.getAnalyticalRunsAssaysRefs().get(mzTabExport.getRuns().get(i))[j]; pw.println(new StringBuilder().append(METADATA_PREFIX).append(COLUMN_DELIMITER) .append(String.format(ASSAY_RUN_REF, assayNumber)).append(COLUMN_DELIMITER) .append(String.format(MS_RUN_REF, i + 1))); assayAnalyticalRunRef.put(String.format(ASSAY, assayNumber), mzTabExport.getRuns().get(i)); } } // study variable assay references if (type == 1 || type == 2) { counter = 1; for (Map.Entry<String, int[]> studyVariablesAssaysRefs : mzTabExport.getStudyVariablesAssaysRefs() .entrySet()) { String[] assay = new String[studyVariablesAssaysRefs.getValue().length]; for (int i = 0; i < studyVariablesAssaysRefs.getValue().length; i++) { assay[i] = String.format(ASSAY, studyVariablesAssaysRefs.getValue()[i]); } String assays = StringUtils.join(assay, ','); pw.println(new StringBuilder().append(METADATA_PREFIX).append(COLUMN_DELIMITER) .append(String.format(STUDY_VARIABLE_ASSAY_REFS, counter)).append(COLUMN_DELIMITER) .append(assays)); counter++; } } // study variable description counter = 1; for (Map.Entry<String, int[]> studyVariablesAssaysRefs : mzTabExport.getStudyVariablesAssaysRefs() .entrySet()) { pw.println(new StringBuilder().append(METADATA_PREFIX).append(COLUMN_DELIMITER) .append(String.format(STUDY_VARIABLE_DESCRIPTION, counter)).append(COLUMN_DELIMITER) .append(studyVariablesAssaysRefs.getKey())); counter++; } constructProteins(type, pw); constructPSM(type, pw); } private void constructProteins(int type, PrintWriter pw) throws IOException { StringBuilder proteins = new StringBuilder(); // protein headers proteins.append(PROTEINS_HEADER_PREFIX).append(COLUMN_DELIMITER).append(ACCESSION).append(COLUMN_DELIMITER) .append(DESCRIPTION).append(COLUMN_DELIMITER).append(TAXID).append(COLUMN_DELIMITER).append(SPECIES) .append(COLUMN_DELIMITER).append(DATABASE).append(COLUMN_DELIMITER).append(DATABASE_VERSION) .append(COLUMN_DELIMITER).append(SEARCH_ENGINE).append(COLUMN_DELIMITER); // create best search engine score headers. Check if there is different search engine proteins.append(String.format(BEST_SEARCH_ENGINE_SCORE, 1)).append(COLUMN_DELIMITER); proteins.append(AMBIGUITY_MEMBERS).append(COLUMN_DELIMITER).append(MODIFICATIONS).append(COLUMN_DELIMITER); if (type == 2 || type == 4) { proteins.append(PROTEIN_COVERAGE).append(COLUMN_DELIMITER); } if (type == 1 || type == 2) { for (int i = 0; i < mzTabExport.getStudyVariablesAssaysRefs().size(); i++) { proteins.append(String.format(PROTEIN_ABUNDANCE_STUDY_VARIABLE, i + 1)).append(COLUMN_DELIMITER); proteins.append(String.format(PROTEIN_ABUNDANCE_STDEV_STUDY_VARIABLE, i + 1)) .append(COLUMN_DELIMITER); proteins.append(String.format(PROTEIN_ABUNDANCE_STD_ERROR_STUDY_VARIABLE, i + 1)) .append(COLUMN_DELIMITER); } } // add search engine score per run if (type == 2 || type == 4) { for (int run = 0; run < mzTabExport.getRuns().size(); run++) { proteins.append(String.format(SEARCH_ENGINE_SCORE_MS_RUN, 1, run + 1)).append(COLUMN_DELIMITER); } } if (type == 4) { for (int i = 0; i < mzTabExport.getRuns().size(); i++) { proteins.append(String.format(NUM_PSMS_MS_RUN, i + 1)).append(COLUMN_DELIMITER); proteins.append(String.format(NUM_PEPTIDES_DISTINCT_MS_RUN, i + 1)).append(COLUMN_DELIMITER); proteins.append(String.format(NUM_PEPTIDE_UNIQUE_MS_RUN, i + 1)).append(COLUMN_DELIMITER); } } if (type == 1 || type == 2) { for (int i = 0; i < mzTabExport.getRuns().size(); i++) { for (int j = 0; j < mzTabExport.getAnalyticalRunsAssaysRefs() .get(mzTabExport.getRuns().get(i)).length; j++) { proteins.append(String.format(PROTEIN_ABUNDANCE_ASSAY, mzTabExport.getAnalyticalRunsAssaysRefs().get(mzTabExport.getRuns().get(i))[j])) .append(COLUMN_DELIMITER); } } } pw.println(proteins.toString()); addProteinData(type, pw); } /** * Fill protein section by adding data. * * @return proteins */ private void addProteinData(int type, PrintWriter pw) throws IOException { List<Long> analyticalRunIds = new ArrayList<>(); mzTabExport.getRuns().forEach(analyticalRun -> { analyticalRunIds.add(analyticalRun.getId()); }); List<ProteinGroupDTO> proteinList = getProteinGroupsForAnalyticalRuns(analyticalRunIds); SearchAndValidationSettings searchAndValidationSettings = searchAndValidationSettingsService .getByAnalyticalRun(mzTabExport.getRuns().get(0)); Map<FastaDb, FastaDbType> fastaDbs = fastaDbService .findBySearchAndValidationSettings(searchAndValidationSettings); // check if db is uniprot! for (int i = 0; i < proteinList.size(); i++) { StringBuilder proteins = new StringBuilder(); // set prefix and accession proteins.append(PROTEINS_PREFIX).append(COLUMN_DELIMITER).append(proteinList.get(i).getMainAccession()) .append(COLUMN_DELIMITER); // define uniprot map and uniprot accession list Map<String, String> uniProtMap = new HashMap<>(); List<String> uniprotAccessions = new ArrayList<>(); // we do not know the database and version. String database = "N/A"; String databaseVersion = "N/A"; for (FastaDb fastaDb : fastaDbs.keySet()) { if (fastaDbs.get(fastaDb).equals(FastaDbType.PRIMARY)) { database = fastaDb.getDatabaseName(); databaseVersion = fastaDb.getVersion(); switch (fastaDb.getDatabaseName()) { case "UNIPROT": uniprotAccessions.add(proteinList.get(i).getMainAccession()); uniProtMap = uniProtService.getUniProtByAccession(uniprotAccessions.get(0)); break; case "Not in the EMBL-EBI list": break; default: uniprotAccessions = AccessionConverter .convertToUniProt(proteinList.get(i).getMainAccession(), fastaDb.getDatabaseName()); uniProtMap = uniProtService.getUniProtByAccession(uniprotAccessions.get(0)); break; } } } // if uniprot map has values if (!uniProtMap.isEmpty()) { // set description, taxid, species, database, database version proteins.append(uniProtMap.get("description")).append(COLUMN_DELIMITER) .append(uniProtMap.get("taxid")).append(COLUMN_DELIMITER).append(uniProtMap.get("species")) .append(COLUMN_DELIMITER).append(database).append(COLUMN_DELIMITER).append(databaseVersion) .append(COLUMN_DELIMITER); } else { for (FastaDb fastaDb : fastaDbs.keySet()) { if (fastaDbs.get(fastaDb).equals(FastaDbType.ADDITIONAL)) { database = fastaDb.getDatabaseName(); databaseVersion = fastaDb.getVersion(); switch (fastaDb.getDatabaseName()) { case "UNIPROT": uniprotAccessions.add(proteinList.get(i).getMainAccession()); uniProtMap = uniProtService.getUniProtByAccession(uniprotAccessions.get(0)); break; case "Not in the EMBL-EBI list": break; default: uniprotAccessions = AccessionConverter.convertToUniProt( proteinList.get(i).getMainAccession(), fastaDb.getDatabaseName()); uniProtMap = uniProtService.getUniProtByAccession(uniprotAccessions.get(0)); break; } if (!uniProtMap.isEmpty()) { // set description, taxid, species, database, database version proteins.append(uniProtMap.get("description")).append(COLUMN_DELIMITER) .append(uniProtMap.get("taxid")).append(COLUMN_DELIMITER) .append(uniProtMap.get("species")).append(COLUMN_DELIMITER) .append(COLUMN_DELIMITER).append(database).append(COLUMN_DELIMITER) .append(databaseVersion).append(COLUMN_DELIMITER); break; } } } } if (uniProtMap.isEmpty()) { // set description, taxid, species, database, database version proteins.append("N/A").append(COLUMN_DELIMITER).append("N/A").append(COLUMN_DELIMITER).append("N/A") .append(COLUMN_DELIMITER).append("N/A").append(COLUMN_DELIMITER).append("N/A") .append(COLUMN_DELIMITER); } // set search engine proteins.append(getSearchEngine()).append(COLUMN_DELIMITER); // set best search engine score for protein. proteins.append(proteinList.get(i).getProteinPostErrorProbability()).append(COLUMN_DELIMITER); // set ambiguity members proteins.append(getAmbiguityMembers(proteinList.get(i).getId())).append(COLUMN_DELIMITER); // set modification (null, beacuse protein level modification is not reported) proteins.append("null").append(COLUMN_DELIMITER); // set protein coverage if (type == 2 || type == 4) { List<String> peptideSequences = new ArrayList<>(); for (PeptideHasProteinGroup peptideHasProteinGroup : proteinGroupService .findById(proteinList.get(i).getId()).getPeptideHasProteinGroups()) { peptideSequences.add(peptideHasProteinGroup.getPeptide().getSequence()); } proteins.append(ProteinCoverage.calculateProteinCoverage(proteinList.get(i).getMainSequence(), peptideSequences)).append(COLUMN_DELIMITER); } //set protein abundance study variable, protein abundance standard deviation study variable, protein abundance standard error study variable if (type == 1 || type == 2) { for (Map.Entry<String, int[]> studyVariablesAssaysRefs : mzTabExport.getStudyVariablesAssaysRefs() .entrySet()) { double abundanceSum = 0.0; double[] abundanceArray = new double[studyVariablesAssaysRefs.getValue().length]; double abundanceMean; double stdDev; for (int j = 0; j < studyVariablesAssaysRefs.getValue().length; j++) { abundanceArray[j] = getProteinAbundanceForAssay(proteinList.get(i), String.format(ASSAY, studyVariablesAssaysRefs.getValue()[j])); abundanceSum += abundanceArray[j]; } if (studyVariablesAssaysRefs.getValue().length != 0) { abundanceMean = abundanceSum / studyVariablesAssaysRefs.getValue().length; // set protein abundance study variable proteins.append(abundanceMean).append(COLUMN_DELIMITER); abundanceSum = 0.0; //find the square of each abundance's distance to the mean. for (int arr = 0; arr < abundanceArray.length; arr++) { abundanceArray[arr] = Math.pow((abundanceArray[arr] - abundanceMean), 2); abundanceSum += abundanceArray[arr]; } // calculate new abundance mean abundanceMean = abundanceSum / abundanceArray.length; stdDev = Math.sqrt(abundanceMean); // set protein abundance standard deviation study variable proteins.append(stdDev).append(COLUMN_DELIMITER); // set protein abundance standard error study variable proteins.append(stdDev / Math.sqrt(abundanceArray.length)).append(COLUMN_DELIMITER); } else { proteins.append("null").append(COLUMN_DELIMITER).append("null").append(COLUMN_DELIMITER) .append("null").append(COLUMN_DELIMITER); } } } // add search engine score per run (both maxquant and peptideshaker do not provide search engine score per run) if (type == 2 || type == 4) { mzTabExport.getRuns().stream().forEach(run -> { proteins.append("null").append(COLUMN_DELIMITER); }); } // set psms count, number of distinct peptide and unique peptide per run if (type == 4) { for (int j = 0; j < mzTabExport.getRuns().size(); j++) { List<Long> analyticalRunIdList = new ArrayList<>(1); analyticalRunIdList.add(mzTabExport.getRuns().get(j).getId()); proteins.append( peptideService.getPeptideDTO(proteinList.get(i).getId(), analyticalRunIdList).size()) .append(COLUMN_DELIMITER); proteins.append(peptideService .getDistinctPeptideSequence(proteinList.get(i).getId(), analyticalRunIdList).size()) .append(COLUMN_DELIMITER); proteins.append(peptideService .getUniquePeptides(proteinList.get(i).getId(), analyticalRunIdList).size()) .append(COLUMN_DELIMITER); } } // set protein abundance for every assay if (type == 1 || type == 2) { for (int r = 0; r < mzTabExport.getRuns().size(); r++) { for (int j = 0; j < mzTabExport.getAnalyticalRunsAssaysRefs() .get(mzTabExport.getRuns().get(r)).length; j++) { proteins.append(getProteinAbundanceForAssay(proteinList.get(i), String.format(ASSAY, mzTabExport.getAnalyticalRunsAssaysRefs().get(mzTabExport.getRuns().get(r))[j]))) .append(COLUMN_DELIMITER); } } } pw.println(proteins); } } private void constructPSM(int type, PrintWriter pw) throws IOException { StringBuilder psms = new StringBuilder(); psms.append(PSM_HEADER_PREFIX).append(COLUMN_DELIMITER).append(SEQUENCE).append(COLUMN_DELIMITER) .append(PSM_ID).append(COLUMN_DELIMITER).append(ACCESSION).append(COLUMN_DELIMITER).append(UNIQUE) .append(COLUMN_DELIMITER).append(DATABASE).append(COLUMN_DELIMITER).append(DATABASE_VERSION) .append(COLUMN_DELIMITER).append(SEARCH_ENGINE).append(COLUMN_DELIMITER) .append(String.format(SEARCH_ENGINE_SCORE, 1)).append(COLUMN_DELIMITER).append(MODIFICATIONS) .append(COLUMN_DELIMITER).append(RETENTION_TIME).append(COLUMN_DELIMITER).append(CHARGE) .append(COLUMN_DELIMITER).append(EXPERIMENTAL_MASS_TO_CHARGE).append(COLUMN_DELIMITER) .append(CALCULATED_MASS_TO_CHARGE).append(COLUMN_DELIMITER).append(SPECTRA_REFERENCE) .append(COLUMN_DELIMITER).append(PRE).append(COLUMN_DELIMITER).append(POST).append(COLUMN_DELIMITER) .append(START).append(COLUMN_DELIMITER).append(END); pw.println(psms); addPSMData(type, pw); } /** * Fill PSM section by adding data. * * @return proteins */ private void addPSMData(int type, PrintWriter pw) throws IOException { List<Long> peptideIds = new ArrayList<>(); List<Long> analyticalRunIds = new ArrayList<>(); mzTabExport.getRuns().forEach(analyticalRun -> { analyticalRunIds.add(analyticalRun.getId()); }); Map<PeptideHasProteinGroup, AnalyticalRun> peptideHasProteinGroups = getPeptideHasProteinGroupForAnalyticalRuns( analyticalRunIds); for (Map.Entry<PeptideHasProteinGroup, AnalyticalRun> peptideHasProteinGroup : peptideHasProteinGroups .entrySet()) { StringBuilder psms = new StringBuilder(); Map<ProteinGroupHasProtein, Protein> proteinGroupHasProtein = proteinGroupService .getProteinGroupHasProteinbyProteinGroupId( peptideHasProteinGroup.getKey().getProteinGroup().getId()); psms.append(PSM_PREFIX).append(COLUMN_DELIMITER) .append(peptideHasProteinGroup.getKey().getPeptide().getSequence()).append(COLUMN_DELIMITER) .append(peptideHasProteinGroup.getKey().getPeptide().getId()).append(COLUMN_DELIMITER) .append(proteinGroupHasProtein.entrySet().stream().findFirst().get().getKey() .getProteinAccession()) .append(COLUMN_DELIMITER); // if peptide was seen more than once in peptideHasProteinGroups list, it is not unique if (isPeptideUnique(peptideHasProteinGroups, peptideHasProteinGroup.getKey().getPeptide())) { psms.append("1").append(COLUMN_DELIMITER); } else { psms.append("0").append(COLUMN_DELIMITER); } // database and version psms.append(findDatabaseAndVersion().get(0)).append(COLUMN_DELIMITER) .append(findDatabaseAndVersion().get(1)).append(COLUMN_DELIMITER); peptideIds.add(peptideHasProteinGroup.getKey().getPeptide().getId()); // search engine psms.append(getSearchEngine()).append(COLUMN_DELIMITER); // search engine score psms.append(peptideHasProteinGroup.getKey().getPeptide().getPsmProbability()).append(COLUMN_DELIMITER); // modifications psms.append(getModifications(peptideHasProteinGroup.getKey().getPeptide())).append(COLUMN_DELIMITER); //retention time psms.append(peptideHasProteinGroup.getKey().getPeptide().getSpectrum().getRetentionTime()) .append(COLUMN_DELIMITER); //charge psms.append(peptideHasProteinGroup.getKey().getPeptide().getCharge()).append(COLUMN_DELIMITER); // experimental mass to charge psms.append(peptideHasProteinGroup.getKey().getPeptide().getSpectrum().getMzRatio()) .append(COLUMN_DELIMITER); // calculated mass to charge psms.append(peptideHasProteinGroup.getKey().getPeptide().getTheoreticalMass()).append(COLUMN_DELIMITER); // spectra reference psms.append(createSpectraRef(peptideHasProteinGroup.getKey().getPeptide().getSpectrum(), peptideHasProteinGroup.getValue())).append(COLUMN_DELIMITER); // preceding amino acid (pre) psms.append(ProteinCoverage.findAminoAcidPrecedingPeptide( proteinGroupHasProtein.entrySet().stream().findFirst().get().getValue().getSequence(), peptideHasProteinGroup.getKey().getPeptide().getSequence())).append(COLUMN_DELIMITER); // following amino acid (post) psms.append(ProteinCoverage.findAminoAcidFollowingPeptide( proteinGroupHasProtein.entrySet().stream().findFirst().get().getValue().getSequence(), peptideHasProteinGroup.getKey().getPeptide().getSequence())).append(COLUMN_DELIMITER); // start position of the peptide psms.append(ProteinCoverage.findStartPositionOfPeptide( proteinGroupHasProtein.entrySet().stream().findFirst().get().getValue().getSequence(), peptideHasProteinGroup.getKey().getPeptide().getSequence())).append(COLUMN_DELIMITER); // end position of the peptide psms.append(ProteinCoverage.findEndPositionOfPeptide( proteinGroupHasProtein.entrySet().stream().findFirst().get().getValue().getSequence(), peptideHasProteinGroup.getKey().getPeptide().getSequence())).append(COLUMN_DELIMITER); pw.println(psms); } } /** * This method parses the JSON root node and returns a list of MzTabParam * instances. * * @param jsonNode the root JsonNode * @return the list of MzTabParam instances * @throws IOException thrown in case of an I/O related problem */ private List<MzTabParam> parseJsonNode(JsonNode jsonNode) throws IOException { List<MzTabParam> mzTabParamList = new ArrayList<>(); Iterator<Map.Entry<String, JsonNode>> fields = jsonNode.fields(); while (fields.hasNext()) { Map.Entry<String, JsonNode> entry = fields.next(); MzTabParam mzTabParam = new MzTabParam(entry.getKey()); JsonNode nameNode = entry.getValue().get("name"); mzTabParam.setUserFriendlyName(nameNode.get(JSON_NAME).asText()); Iterator<JsonNode> optionElements = entry.getValue().get("values").elements(); while (optionElements.hasNext()) { MzTabParamOption mzTabParamOption = mapper.treeToValue(optionElements.next(), MzTabParamOption.class); mzTabParam.addOption(mzTabParamOption); } mzTabParamList.add(mzTabParam); } return mzTabParamList; } /** * Get engine scores of the meta data section. * * @param field * @param alignment * @return */ private String getEngineScore(String field, int alignment) { // TO DO working properly for search engine score?? StringBuilder metadata = new StringBuilder(); int counter = 1; if (software.equals("PeptideShaker")) { metadata.append(METADATA_PREFIX).append(COLUMN_DELIMITER).append(String.format(field, counter)) .append(COLUMN_DELIMITER) .append(createOntology(mzTabParams.get(alignment).getMzTabParamOptions().get(2).getOntology(), mzTabParams.get(alignment).getMzTabParamOptions().get(2).getAccession(), mzTabParams.get(alignment).getMzTabParamOptions().get(2).getName())); } else if (software.equals("MaxQuant")) { metadata.append(METADATA_PREFIX).append(COLUMN_DELIMITER).append(String.format(field, counter)) .append(COLUMN_DELIMITER) .append(createOntology(mzTabParams.get(alignment).getMzTabParamOptions().get(1).getOntology(), mzTabParams.get(alignment).getMzTabParamOptions().get(1).getAccession(), mzTabParams.get(alignment).getMzTabParamOptions().get(1).getName())); } return metadata.toString(); } /** * Get search engine. * * @return searchEngine */ private String getSearchEngine() { StringBuilder searchEngine = new StringBuilder(); if (software.equals("PeptideShaker")) { searchEngine .append(createOntology( mzTabParams.get(SOFTWARE_ALIGNMENT).getMzTabParamOptions().get(1).getOntology(), mzTabParams.get(SOFTWARE_ALIGNMENT).getMzTabParamOptions().get(1).getAccession(), mzTabParams.get(SOFTWARE_ALIGNMENT).getMzTabParamOptions().get(1).getName())) .append(VERTICAL_BAR); } else if (software.equals("MaxQuant")) { searchEngine .append(createOntology( mzTabParams.get(SOFTWARE_ALIGNMENT).getMzTabParamOptions().get(0).getOntology(), mzTabParams.get(SOFTWARE_ALIGNMENT).getMzTabParamOptions().get(0).getAccession(), mzTabParams.get(SOFTWARE_ALIGNMENT).getMzTabParamOptions().get(0).getName())) .append(VERTICAL_BAR); } searchEngine = searchEngine.deleteCharAt(searchEngine.length() - 1); return searchEngine.toString(); } /** * Only one software can be used. (MaxQuant or PeptideShaker) */ private void setSoftware() { software = getSearchAndValidationSettings(mzTabExport.getRuns().get(0)).getSearchEngine().getName(); } /** * Create ontology list with the given variables. * * @param ontology * @param accession * @param name * @return ontology list string */ private String createOntology(String ontology, String accession, String name) { StringBuilder ontologyBuilder = new StringBuilder(); return ontologyBuilder.append(OPEN_BRACKET).append(ontology).append(COMMA_SEPARATOR).append(accession) .append(COMMA_SEPARATOR).append(name).append(COMMA_SEPARATOR).append(CLOSE_BRACKET).toString(); } /** * Get the quantification settings * * @param analyticalRuns * @return quantificationSettingsMap */ private QuantificationSettings getQuantificationSettings(AnalyticalRun analyticalRun) { return analyticalRun.getQuantificationSettings(); } /** * Get the search and validation settings. * * @param analyticalRun the {@link AnalyticalRun} instance * @return quantificationSettingsMap */ private SearchAndValidationSettings getSearchAndValidationSettings(AnalyticalRun analyticalRun) { return analyticalRun.getSearchAndValidationSettings(); } /** * Get protein groups for given list of analyticalRuns. * * @param analyticalRunIds * @return list of ProteinGroupDTO object */ private List<ProteinGroupDTO> getProteinGroupsForAnalyticalRuns(List<Long> analyticalRunIds) { return proteinGroupService.getProteinGroupsForRuns(analyticalRunIds); } /** * Get PeptideHasProteinGroup for the given list of analytical runs * * @param analyticalRunIds * @return list of PeptideHasProteinGroup */ private Map<PeptideHasProteinGroup, AnalyticalRun> getPeptideHasProteinGroupForAnalyticalRuns( List<Long> analyticalRunIds) { return peptideService.getPeptideHasProteinGroupByAnalyticalRunId(analyticalRunIds); } /** * Get ambiguity members for given proteinGroupID * * @param proteinGroupId * @return ambiguity members as string */ private String getAmbiguityMembers(Long proteinGroupId) { List<ProteinGroupHasProtein> proteinGroupHasProteins = proteinGroupService .getAmbiguityMembers(proteinGroupId); StringBuilder ambiguityMembers = new StringBuilder(""); proteinGroupHasProteins.stream().forEach((proteinGroupHasProtein) -> { ambiguityMembers.append(proteinGroupHasProtein.getProteinAccession()).append(","); }); if (ambiguityMembers.toString().equals("")) { return ambiguityMembers.toString(); } else { return ambiguityMembers.deleteCharAt(ambiguityMembers.length() - 1).toString(); } } /** * Get protein abundance for given assay, protein group and analyticalRun * Use assay to get the reagent. * * @param analyticalRun * @param proteinGroup * @param assay * @return protein abundance */ private double getProteinAbundanceForAssay(ProteinGroupDTO proteinGroup, String assay) { double proteinAbundance = 0.0; AnalyticalRun analyticalRun = assayAnalyticalRunRef.get(assay); if (assayReagentRef.get(assay).equals(UNLABELED_SAMPLE)) { proteinAbundance = proteinGroupQuantService .getProteinGroupQuantForRunAndProteinGroup(analyticalRun.getId(), proteinGroup.getId()) .getIntensity(); } else { // get label from user interface. String label = mzTabExport.getQuantificationReagentLabelMatch().get(assayReagentRef.get(assay)); List<ProteinGroupQuantLabeled> proteinGroupQuantLabeleds = proteinGroupQuantLabeledService .getProteinGroupQuantLabeledForRunAndProteinGroup(analyticalRun.getId(), proteinGroup.getId()); for (ProteinGroupQuantLabeled proteinGroupQuantLabeled : proteinGroupQuantLabeleds) { if (proteinGroupQuantLabeled.getLabel().equals(label)) { proteinAbundance = proteinGroupQuantLabeled.getLabelValue(); } } } return proteinAbundance; } /** * find database and version only first runs primary FASTA file. * * @return map (key:index ; value:database and version) */ private Map<Integer, String> findDatabaseAndVersion() { // key:database ; value:version Map<Integer, String> databaseVersion = new HashMap<>(); SearchAndValidationSettings searchAndValidationSettings = searchAndValidationSettingsService .getByAnalyticalRun(mzTabExport.getRuns().get(0)); Map<FastaDb, FastaDbType> fastaDbs = fastaDbService .findBySearchAndValidationSettings(searchAndValidationSettings); for (FastaDb fastaDb : fastaDbs.keySet()) { if (fastaDbs.get(fastaDb).equals(FastaDbType.PRIMARY)) { databaseVersion.put(0, fastaDb.getDatabaseName()); databaseVersion.put(1, fastaDb.getVersion()); } } return databaseVersion; } /** * Create spectra reference for given spectrum * * @param spectrum * @param analyticalRun * @return */ private String createSpectraRef(Spectrum spectrum, AnalyticalRun analyticalRun) { return String.format(MS_RUN_REF, analyticalRunIndexRef.get(analyticalRun.getId())) + ":index=" + spectrum.getScanNumber(); } /** * Check if given peptide is unique * * @param peptideHasProteinGroups * @param peptide * @return true or false */ private boolean isPeptideUnique(Map<PeptideHasProteinGroup, AnalyticalRun> peptideHasProteinGroups, Peptide peptide) { List<Peptide> peptides = new ArrayList<>(); peptideHasProteinGroups.keySet().stream() .filter(p -> Objects.equals(p.getPeptide().getId(), peptide.getId())) .forEach(peptideHasProteinGroup -> { peptides.add(peptideHasProteinGroup.getPeptide()); }); return peptides.size() == 1; } /** * Get modifications for given PSM * @param peptide * @return modifications */ private String getModifications(Peptide peptide) { StringBuilder modifications = new StringBuilder(); peptideService.fetchPeptideHasModifications(peptide); for (PeptideHasModification peptideHasModification : peptide.getPeptideHasModifications()) { modifications.append(peptideHasModification.getLocation()); if (software.equals("PeptideShaker")) { modifications.append(createOntology(mzTabParams.get(5).getMzTabParamOptions().get(1).getOntology(), mzTabParams.get(5).getMzTabParamOptions().get(1).getAccession(), mzTabParams.get(5).getMzTabParamOptions().get(1).getName())); } else if (software.equals("MaxQuant")) { modifications.append(createOntology(mzTabParams.get(5).getMzTabParamOptions().get(0).getOntology(), mzTabParams.get(5).getMzTabParamOptions().get(0).getAccession(), mzTabParams.get(5).getMzTabParamOptions().get(0).getName())); } modifications = modifications.deleteCharAt(modifications.length() - 1); modifications.append(peptideHasModification.getProbabilityScore()).append(CLOSE_BRACKET).append("-"); modifications.append(peptideHasModification.getModification().getAccession()).append(VERTICAL_BAR); } if (modifications.length() > 0) { return modifications.deleteCharAt(modifications.length() - 1).toString(); } else { return null; } } }