MZTabColumnFactory.java

/* 
 * Copyright 2018 Leibniz-Institut für Analytische Wissenschaften – ISAS – e.V..
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package uk.ac.ebi.pride.jmztab2.model;

import de.isas.mztab2.model.Assay;
import de.isas.mztab2.model.IndexedElement;
import de.isas.mztab2.model.Parameter;
import de.isas.mztab2.model.StudyVariable;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * This is a static factory class which used to generate a couple of MZTabColumn
 * objects, and organizes them into "logicalPosition, MZTabColumn" pairs.
 * Currently, mzTab table including three kinds of columns:
 * <ol>
 * <li>
 * Stable column with stable order: header name, data type, logical position and
 * order are stable in these columns. All of them are defined in
 * {@link uk.ac.ebi.pride.jmztab2.model.SmallMoleculeColumn}, {@link uk.ac.ebi.pride.jmztab2.model.SmallMoleculeFeatureColumn},
 * and {@link uk.ac.ebi.pride.jmztab2.model.SmallMoleculeEvidenceColumn}.
 * </li>
 * <li>
 * Optional column with stable order: column name, data type and order are
 * defined in the {@link uk.ac.ebi.pride.jmztab2.model.SmallMoleculeColumn},
 * {@link uk.ac.ebi.pride.jmztab2.model.SmallMoleculeFeatureColumn}, and
 * {@link uk.ac.ebi.pride.jmztab2.model.SmallMoleculeEvidenceColumn}. But header
 * name, logical position dynamically depend on {@link IndexedElement}.
 * </li>
 * <li>
 * Optional columns which are placed at the end of a table-based section. There
 * are three types of optional column:
 * {@link uk.ac.ebi.pride.jmztab2.model.AbundanceColumn}, {@link uk.ac.ebi.pride.jmztab2.model.OptionColumn}
 * and {@link uk.ac.ebi.pride.jmztab2.model.ParameterOptionColumn}, which always
 * are added at the end of the table. These optional columns have no stable
 * column name, data type or order. In this factory, we use
 * {@link #addOptionalColumn(String, Class)} to create
 * {@link uk.ac.ebi.pride.jmztab2.model.OptionColumn}; and
 * {@link #addOptionalColumn(de.isas.mztab2.model.IndexedElement, java.lang.String, java.lang.Class)}
 * or {@link #addOptionalColumn(IndexedElement, Parameter, Class)} to create
 * {@link uk.ac.ebi.pride.jmztab2.model.ParameterOptionColumn}.
 * </li>
 * </ol>
 *
 * @author qingwei
 * @author nilshoffmann
 * @since 23/05/13
 *
 */
public class MZTabColumnFactory {

    private final SortedMap<String, IMZTabColumn> stableColumnMapping = new TreeMap<>();
    private final SortedMap<String, IMZTabColumn> optionalColumnMapping = new TreeMap<>();
    private final SortedMap<String, IMZTabColumn> abundanceColumnMapping = new TreeMap<>();
    private final SortedMap<String, IMZTabColumn> columnMapping = new TreeMap<>();

    private Section section;

    private MZTabColumnFactory() {
    }

    /**
     * Retrieves the MZTabColumnFactory accordingly to the {@link #section}
     *
     * @param section SHOULD be
     * {@link uk.ac.ebi.pride.jmztab2.model.Section#Protein_Header}, {@link uk.ac.ebi.pride.jmztab2.model.Section#Peptide_Header} {@link uk.ac.ebi.pride.jmztab2.model.Section#PSM_Header}
     * or {@link uk.ac.ebi.pride.jmztab2.model.Section#Small_Molecule_Header}.
     * @return a {@link uk.ac.ebi.pride.jmztab2.model.MZTabColumnFactory}
     * object.
     */
    public static MZTabColumnFactory getInstance(Section section) {
        section = Section.toHeaderSection(section);

        if (section == null) {
            throw new IllegalArgumentException(
                "Section should use Protein_Header, Peptide_Header, PSM_Header, Small_Molecule_Header, Small_Molecule_Feature_Header, or Small_Molecule_Evidence_Header.");
        }

        MZTabColumnFactory factory = new MZTabColumnFactory();
        factory.section = section;

        return factory;
    }

    /**
     * Get stable columns mapping. Key is logical position, and value is
     * MZTabColumn object. Stable column with stable order: header name, data
     * type, logical position and order are stable in these columns. All of them
     * have been defined in null     {@link uk.ac.ebi.pride.jmztab2.model.SmallMoleculeColumn}, {@link uk.ac.ebi.pride.jmztab2.model.SmallMoleculeFeatureColumn},
     * {@link uk.ac.ebi.pride.jmztab2.model.SmallMoleculeEvidenceColumn}.
     *
     * @return a {@link java.util.SortedMap} object.
     */
    public SortedMap<String, IMZTabColumn> getStableColumnMapping() {
        return stableColumnMapping;
    }

    /**
     * Get all optional columns, including option column with stable order and
     * name, abundance columns, optional columns and cv param optional columns.
     * Key is logical position, and value is MZTabColumn object.
     *
     * @see AbundanceColumn
     * @see OptionColumn
     * @see ParameterOptionColumn
     * @return a {@link java.util.SortedMap} object.
     */
    public SortedMap<String, IMZTabColumn> getOptionalColumnMapping() {
        return optionalColumnMapping;
    }

    /**
     * Get all columns in the factory. In this class, we maintain the following
     * constraint at any time:
     *
     * @return a {@link java.util.SortedMap} object.
     */
    public SortedMap<String, IMZTabColumn> getColumnMapping() {
        return columnMapping;
    }

    /**
     * Extract the order from logical position. Normally, the order is coming
     * from top two characters of logical position. For example, logical
     * position is 092, then the order number is 9.
     */
    private String getColumnOrder(String position) {
        return position.substring(0, 2);
    }
    
    private void checkOptionalColumn(IMZTabColumn column) throws IllegalArgumentException {
        if(optionalColumnMapping.containsKey(column.getLogicPosition())) {
            throw new IllegalArgumentException("Key " + column.getLogicPosition() + " for column " + column.getName() + " is already assigned to: " + optionalColumnMapping.get(column.getLogicPosition()).getName());
        }
        optionalColumnMapping.put(column.getLogicPosition(), column);
        if(columnMapping.containsKey(column.getLogicPosition())) {
            throw new IllegalArgumentException("Key " + column.getLogicPosition() + " for column " + column.getName() + " is already assigned to: " + columnMapping.get(column.getLogicPosition()).getName());
        }
        columnMapping.put(column.getLogicPosition(), column);
    }
    
    private void checkAbundanceOptionalColumn(IMZTabColumn column) throws IllegalArgumentException {
        if(abundanceColumnMapping.containsKey(column.getLogicPosition())) {
            throw new IllegalArgumentException("Key " + column.getLogicPosition() + " for column " + column.getName() + " is already assigned to: " + abundanceColumnMapping.get(column.getLogicPosition()).getName());
        }
        abundanceColumnMapping.put(column.getLogicPosition(), column);
    }

    private String addOptionColumn(IMZTabColumn column) {

        checkOptionalColumn(column);

        return column.getLogicPosition();
    }

    private String addOptionColumn(IMZTabColumn column, String order) {

        column.setOrder(order);
        checkOptionalColumn(column);

        return column.getLogicPosition();
    }

    /**
     * Add global {@link uk.ac.ebi.pride.jmztab2.model.OptionColumn} into
     * {@link #optionalColumnMapping} and {@link #columnMapping}. The header
     * like: opt_global_{name}
     *
     * @param name SHOULD NOT be empty.
     * @param columnType SHOULD NOT be empty.
     * @return the column's logic position.
     */
    public String addOptionalColumn(String name, Class columnType) {
        IMZTabColumn column = new OptionColumn(null, name, columnType,
            Integer.parseInt(getColumnOrder(columnMapping.lastKey())));
        return addOptionColumn(column);
    }

    /**
     * Add {@link uk.ac.ebi.pride.jmztab2.model.OptionColumn} followed by an
     * indexed element (study variable, assay, ms run) into
     * {@link #optionalColumnMapping} and {@link #columnMapping}. The header
     * will look like: opt_study_variable[1]_{name} for a study variable
     *
     * @param <T> the type of the columnEntity.
     * @param columnEntity SHOULD NOT be empty.
     * @param name SHOULD NOT be empty.
     * @param columnType SHOULD NOT be empty.
     * @return the column's logic position.
     */
    public <T extends IndexedElement> String addOptionalColumn(T columnEntity,
        String name, Class columnType) {
        IMZTabColumn column = new OptionColumn(columnEntity, name, columnType,
            Integer.parseInt(getColumnOrder(columnMapping.lastKey())));
        return addOptionColumn(column);
    }

    /**
     * Add global {@link uk.ac.ebi.pride.jmztab2.model.ParameterOptionColumn}
     * into {@link #optionalColumnMapping} and {@link #columnMapping}. The
     * header like: opt_global_cv_{accession}_{parameter name}
     *
     * @param param SHOULD NOT empty.
     * @param columnType SHOULD NOT empty.
     * @return the column's logic position.
     */
    public String addOptionalColumn(Parameter param, Class columnType) {
        IMZTabColumn column = new ParameterOptionColumn(null, param, columnType,
            Integer.parseInt(getColumnOrder(columnMapping.lastKey())));
        return addOptionColumn(column);
    }

    /**
     * Add {@link uk.ac.ebi.pride.jmztab2.model.ParameterOptionColumn} followed
     * by an indexed element (study variable, assay, ms run) into
     * {@link #optionalColumnMapping} and {@link #columnMapping}. The header
     * will look like: opt_assay[1]_cv_{accession}_{parameter name} for an
     * assay.
     *
     * @param <T> the type of the columnEntity.
     * @param columnEntity SHOULD NOT empty.
     * @param param SHOULD NOT empty.
     * @param columnType SHOULD NOT empty.
     * @return the column's logic position.
     */
    public <T extends IndexedElement> String addOptionalColumn(T columnEntity,
        Parameter param, Class columnType) {
        IMZTabColumn column = new ParameterOptionColumn(columnEntity, param,
            columnType, Integer.parseInt(getColumnOrder(columnMapping.lastKey())));
        return addOptionColumn(column);
    }

    /**
     * <p>
     * addAbundanceOptionalColumn.</p>
     *
     * @param assay a {@link de.isas.mztab2.model.Assay} object.
     * @param order the order string for this column.
     * @return the column's logic position.
     */
    public String addAbundanceOptionalColumn(Assay assay, String order) {
        IMZTabColumn column = AbundanceColumn.createOptionalColumn(section,
            assay, Integer.parseInt(order));
        checkAbundanceOptionalColumn(column);
        return addOptionColumn(column, order);
    }

    /**
     * Add an {@link uk.ac.ebi.pride.jmztab2.model.AbundanceColumn} into
     * {@link uk.ac.ebi.pride.jmztab2.model.AbundanceColumn}, {@link #optionalColumnMapping}
     * and {@link #columnMapping}. The header can be one of
     * abundance_study_variable[1], abundance_coeffvar_study_variable[1].
     *
     * @see
     * AbundanceColumn#createOptionalColumns(uk.ac.ebi.pride.jmztab2.model.Section,
     * de.isas.mztab2.model.StudyVariable, java.lang.String, java.lang.String)
     * @param studyVariable SHOULD NOT empty.
     * @param columnHeader the column header without the 'abundance_' prefix.
     * @param order the order string for this column.
     * @return the column's logic position.
     */
    public String addAbundanceOptionalColumn(StudyVariable studyVariable,
        String columnHeader, String order) {
        SortedMap<String, MZTabColumn> columns = AbundanceColumn.
            createOptionalColumns(section, studyVariable, columnHeader, order);
        for(IMZTabColumn col:columns.values()) {
            checkAbundanceOptionalColumn(col);
            checkOptionalColumn(col);
        }
        return columns.lastKey();
    }

    /**
     * <p>
     * addIdConfidenceMeasureColumn.</p>
     *
     * @param parameter a {@link de.isas.mztab2.model.Parameter} object.
     * @param index a {@link java.lang.Integer} object.
     * @param columnType the class of values in this column.
     * @return the column's logic position.
     */
    public String addIdConfidenceMeasureColumn(Parameter parameter,
        Integer index, Class columnType) {
        if (section != Section.Small_Molecule_Evidence_Header && section != Section.Small_Molecule_Evidence) {
            throw new IllegalArgumentException(
                "Section should be SmallMoleculeEvidence, but is " + section.
                    getName());
        }
        if (parameter == null) {
            throw new NullPointerException("Parameter should not be null!");
        }

        SortedMap<String, MZTabColumn> columns = new TreeMap<>();

        MZTabColumn column = new MZTabColumn("id_confidence_measure", columnType,
            false, Integer.parseInt(getColumnOrder(columnMapping.lastKey())) + "",
            index);

        columns.put(column.getLogicPosition(), column);
        for(IMZTabColumn col : columns.values()) {
            checkOptionalColumn(col);
        }
        return columns.lastKey();
    }

    /**
     * The offset record the position of MZTabColumn in header line. For
     * example, protein header line, the relationships between Logical Position,
     * MZTabColumn, offset and order are like following structure: Logical
     * Position MZTabColumn offset order "01" accession 1 01 "02" description 2
     * 02 ...... "08" best_search_engine_score 8 08 "091"
     * search_engine_score_ms_run[1] 9 09 "092" search_engine_score_ms_run[2] 10
     * 09 "10" reliability 11 10 "111" num_psms_ms_run[1] 12 11 "112"
     * num_psms_ms_run[2] 13 11
     *
     * @return a {@link java.util.SortedMap} object with the offsets for each
     * column.
     */
    public SortedMap<Integer, IMZTabColumn> getOffsetColumnsMap() {
        SortedMap<Integer, IMZTabColumn> map = new TreeMap<>();

        int offset = 1;
        for (IMZTabColumn column : columnMapping.values()) {
            map.put(offset++, column);
        }

        return map;
    }

    /**
     * Query the MZTabColumn in factory, based on column header with
     * case-insensitive. Notice: for optional columns, header name maybe
     * flexible. For example, num_psms_ms_run[1]. At this time, user SHOULD BE
     * provide the full header name to query MZTabColumn. If just provide
     * num_psms_ms_run, return null.
     *
     * @param header the column header to use as the search key.
     * @return a {@link uk.ac.ebi.pride.jmztab2.model.IMZTabColumn} object or
     * null.
     */
    public IMZTabColumn findColumnByHeader(String header) {
        header = header.trim();

        for (IMZTabColumn column : columnMapping.values()) {
            if (header.equalsIgnoreCase(column.getHeader())) {
                return column;
            }
        }

        return null;
    }
}