001/* 
002 * Copyright 2018 Leibniz-Institut für Analytische Wissenschaften – ISAS – e.V..
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package uk.ac.ebi.pride.jmztab2.model;
017
018import de.isas.mztab2.model.Assay;
019import de.isas.mztab2.model.IndexedElement;
020import de.isas.mztab2.model.Parameter;
021import de.isas.mztab2.model.StudyVariable;
022import java.util.SortedMap;
023import java.util.TreeMap;
024
025/**
026 * This is a static factory class which used to generate a couple of MZTabColumn
027 * objects, and organizes them into "logicalPosition, MZTabColumn" pairs.
028 * Currently, mzTab table including three kinds of columns:
029 * <ol>
030 * <li>
031 * Stable column with stable order: header name, data type, logical position and
032 * order are stable in these columns. All of them are defined in
033 * {@link uk.ac.ebi.pride.jmztab2.model.SmallMoleculeColumn}, {@link uk.ac.ebi.pride.jmztab2.model.SmallMoleculeFeatureColumn},
034 * and {@link uk.ac.ebi.pride.jmztab2.model.SmallMoleculeEvidenceColumn}.
035 * </li>
036 * <li>
037 * Optional column with stable order: column name, data type and order are
038 * defined in the {@link uk.ac.ebi.pride.jmztab2.model.SmallMoleculeColumn},
039 * {@link uk.ac.ebi.pride.jmztab2.model.SmallMoleculeFeatureColumn}, and
040 * {@link uk.ac.ebi.pride.jmztab2.model.SmallMoleculeEvidenceColumn}. But header
041 * name, logical position dynamically depend on {@link IndexedElement}.
042 * </li>
043 * <li>
044 * Optional columns which are placed at the end of a table-based section. There
045 * are three types of optional column:
046 * {@link uk.ac.ebi.pride.jmztab2.model.AbundanceColumn}, {@link uk.ac.ebi.pride.jmztab2.model.OptionColumn}
047 * and {@link uk.ac.ebi.pride.jmztab2.model.ParameterOptionColumn}, which always
048 * are added at the end of the table. These optional columns have no stable
049 * column name, data type or order. In this factory, we use
050 * {@link #addOptionalColumn(String, Class)} to create
051 * {@link uk.ac.ebi.pride.jmztab2.model.OptionColumn}; and
052 * {@link #addOptionalColumn(de.isas.mztab2.model.IndexedElement, java.lang.String, java.lang.Class)}
053 * or {@link #addOptionalColumn(IndexedElement, Parameter, Class)} to create
054 * {@link uk.ac.ebi.pride.jmztab2.model.ParameterOptionColumn}.
055 * </li>
056 * </ol>
057 *
058 * @author qingwei
059 * @author nilshoffmann
060 * @since 23/05/13
061 *
062 */
063public class MZTabColumnFactory {
064
065    private final SortedMap<String, IMZTabColumn> stableColumnMapping = new TreeMap<>();
066    private final SortedMap<String, IMZTabColumn> optionalColumnMapping = new TreeMap<>();
067    private final SortedMap<String, IMZTabColumn> abundanceColumnMapping = new TreeMap<>();
068    private final SortedMap<String, IMZTabColumn> columnMapping = new TreeMap<>();
069
070    private Section section;
071
072    private MZTabColumnFactory() {
073    }
074
075    /**
076     * Retrieves the MZTabColumnFactory accordingly to the {@link #section}
077     *
078     * @param section SHOULD be
079     * {@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}
080     * or {@link uk.ac.ebi.pride.jmztab2.model.Section#Small_Molecule_Header}.
081     * @return a {@link uk.ac.ebi.pride.jmztab2.model.MZTabColumnFactory}
082     * object.
083     */
084    public static MZTabColumnFactory getInstance(Section section) {
085        section = Section.toHeaderSection(section);
086
087        if (section == null) {
088            throw new IllegalArgumentException(
089                "Section should use Protein_Header, Peptide_Header, PSM_Header, Small_Molecule_Header, Small_Molecule_Feature_Header, or Small_Molecule_Evidence_Header.");
090        }
091
092        MZTabColumnFactory factory = new MZTabColumnFactory();
093        factory.section = section;
094
095        return factory;
096    }
097
098    /**
099     * Get stable columns mapping. Key is logical position, and value is
100     * MZTabColumn object. Stable column with stable order: header name, data
101     * type, logical position and order are stable in these columns. All of them
102     * have been defined in null     {@link uk.ac.ebi.pride.jmztab2.model.SmallMoleculeColumn}, {@link uk.ac.ebi.pride.jmztab2.model.SmallMoleculeFeatureColumn},
103     * {@link uk.ac.ebi.pride.jmztab2.model.SmallMoleculeEvidenceColumn}.
104     *
105     * @return a {@link java.util.SortedMap} object.
106     */
107    public SortedMap<String, IMZTabColumn> getStableColumnMapping() {
108        return stableColumnMapping;
109    }
110
111    /**
112     * Get all optional columns, including option column with stable order and
113     * name, abundance columns, optional columns and cv param optional columns.
114     * Key is logical position, and value is MZTabColumn object.
115     *
116     * @see AbundanceColumn
117     * @see OptionColumn
118     * @see ParameterOptionColumn
119     * @return a {@link java.util.SortedMap} object.
120     */
121    public SortedMap<String, IMZTabColumn> getOptionalColumnMapping() {
122        return optionalColumnMapping;
123    }
124
125    /**
126     * Get all columns in the factory. In this class, we maintain the following
127     * constraint at any time:
128     *
129     * @return a {@link java.util.SortedMap} object.
130     */
131    public SortedMap<String, IMZTabColumn> getColumnMapping() {
132        return columnMapping;
133    }
134
135    /**
136     * Extract the order from logical position. Normally, the order is coming
137     * from top two characters of logical position. For example, logical
138     * position is 092, then the order number is 9.
139     */
140    private String getColumnOrder(String position) {
141        return position.substring(0, 2);
142    }
143    
144    private void checkOptionalColumn(IMZTabColumn column) throws IllegalArgumentException {
145        if(optionalColumnMapping.containsKey(column.getLogicPosition())) {
146            throw new IllegalArgumentException("Key " + column.getLogicPosition() + " for column " + column.getName() + " is already assigned to: " + optionalColumnMapping.get(column.getLogicPosition()).getName());
147        }
148        optionalColumnMapping.put(column.getLogicPosition(), column);
149        if(columnMapping.containsKey(column.getLogicPosition())) {
150            throw new IllegalArgumentException("Key " + column.getLogicPosition() + " for column " + column.getName() + " is already assigned to: " + columnMapping.get(column.getLogicPosition()).getName());
151        }
152        columnMapping.put(column.getLogicPosition(), column);
153    }
154    
155    private void checkAbundanceOptionalColumn(IMZTabColumn column) throws IllegalArgumentException {
156        if(abundanceColumnMapping.containsKey(column.getLogicPosition())) {
157            throw new IllegalArgumentException("Key " + column.getLogicPosition() + " for column " + column.getName() + " is already assigned to: " + abundanceColumnMapping.get(column.getLogicPosition()).getName());
158        }
159        abundanceColumnMapping.put(column.getLogicPosition(), column);
160    }
161
162    private String addOptionColumn(IMZTabColumn column) {
163
164        checkOptionalColumn(column);
165
166        return column.getLogicPosition();
167    }
168
169    private String addOptionColumn(IMZTabColumn column, String order) {
170
171        column.setOrder(order);
172        checkOptionalColumn(column);
173
174        return column.getLogicPosition();
175    }
176
177    /**
178     * Add global {@link uk.ac.ebi.pride.jmztab2.model.OptionColumn} into
179     * {@link #optionalColumnMapping} and {@link #columnMapping}. The header
180     * like: opt_global_{name}
181     *
182     * @param name SHOULD NOT be empty.
183     * @param columnType SHOULD NOT be empty.
184     * @return the column's logic position.
185     */
186    public String addOptionalColumn(String name, Class columnType) {
187        IMZTabColumn column = new OptionColumn(null, name, columnType,
188            Integer.parseInt(getColumnOrder(columnMapping.lastKey())));
189        return addOptionColumn(column);
190    }
191
192    /**
193     * Add {@link uk.ac.ebi.pride.jmztab2.model.OptionColumn} followed by an
194     * indexed element (study variable, assay, ms run) into
195     * {@link #optionalColumnMapping} and {@link #columnMapping}. The header
196     * will look like: opt_study_variable[1]_{name} for a study variable
197     *
198     * @param <T> the type of the columnEntity.
199     * @param columnEntity SHOULD NOT be empty.
200     * @param name SHOULD NOT be empty.
201     * @param columnType SHOULD NOT be empty.
202     * @return the column's logic position.
203     */
204    public <T extends IndexedElement> String addOptionalColumn(T columnEntity,
205        String name, Class columnType) {
206        IMZTabColumn column = new OptionColumn(columnEntity, name, columnType,
207            Integer.parseInt(getColumnOrder(columnMapping.lastKey())));
208        return addOptionColumn(column);
209    }
210
211    /**
212     * Add global {@link uk.ac.ebi.pride.jmztab2.model.ParameterOptionColumn}
213     * into {@link #optionalColumnMapping} and {@link #columnMapping}. The
214     * header like: opt_global_cv_{accession}_{parameter name}
215     *
216     * @param param SHOULD NOT empty.
217     * @param columnType SHOULD NOT empty.
218     * @return the column's logic position.
219     */
220    public String addOptionalColumn(Parameter param, Class columnType) {
221        IMZTabColumn column = new ParameterOptionColumn(null, param, columnType,
222            Integer.parseInt(getColumnOrder(columnMapping.lastKey())));
223        return addOptionColumn(column);
224    }
225
226    /**
227     * Add {@link uk.ac.ebi.pride.jmztab2.model.ParameterOptionColumn} followed
228     * by an indexed element (study variable, assay, ms run) into
229     * {@link #optionalColumnMapping} and {@link #columnMapping}. The header
230     * will look like: opt_assay[1]_cv_{accession}_{parameter name} for an
231     * assay.
232     *
233     * @param <T> the type of the columnEntity.
234     * @param columnEntity SHOULD NOT empty.
235     * @param param SHOULD NOT empty.
236     * @param columnType SHOULD NOT empty.
237     * @return the column's logic position.
238     */
239    public <T extends IndexedElement> String addOptionalColumn(T columnEntity,
240        Parameter param, Class columnType) {
241        IMZTabColumn column = new ParameterOptionColumn(columnEntity, param,
242            columnType, Integer.parseInt(getColumnOrder(columnMapping.lastKey())));
243        return addOptionColumn(column);
244    }
245
246    /**
247     * <p>
248     * addAbundanceOptionalColumn.</p>
249     *
250     * @param assay a {@link de.isas.mztab2.model.Assay} object.
251     * @param order the order string for this column.
252     * @return the column's logic position.
253     */
254    public String addAbundanceOptionalColumn(Assay assay, String order) {
255        IMZTabColumn column = AbundanceColumn.createOptionalColumn(section,
256            assay, Integer.parseInt(order));
257        checkAbundanceOptionalColumn(column);
258        return addOptionColumn(column, order);
259    }
260
261    /**
262     * Add an {@link uk.ac.ebi.pride.jmztab2.model.AbundanceColumn} into
263     * {@link uk.ac.ebi.pride.jmztab2.model.AbundanceColumn}, {@link #optionalColumnMapping}
264     * and {@link #columnMapping}. The header can be one of
265     * abundance_study_variable[1], abundance_coeffvar_study_variable[1].
266     *
267     * @see
268     * AbundanceColumn#createOptionalColumns(uk.ac.ebi.pride.jmztab2.model.Section,
269     * de.isas.mztab2.model.StudyVariable, java.lang.String, java.lang.String)
270     * @param studyVariable SHOULD NOT empty.
271     * @param columnHeader the column header without the 'abundance_' prefix.
272     * @param order the order string for this column.
273     * @return the column's logic position.
274     */
275    public String addAbundanceOptionalColumn(StudyVariable studyVariable,
276        String columnHeader, String order) {
277        SortedMap<String, MZTabColumn> columns = AbundanceColumn.
278            createOptionalColumns(section, studyVariable, columnHeader, order);
279        for(IMZTabColumn col:columns.values()) {
280            checkAbundanceOptionalColumn(col);
281            checkOptionalColumn(col);
282        }
283        return columns.lastKey();
284    }
285
286    /**
287     * <p>
288     * addIdConfidenceMeasureColumn.</p>
289     *
290     * @param parameter a {@link de.isas.mztab2.model.Parameter} object.
291     * @param index a {@link java.lang.Integer} object.
292     * @param columnType the class of values in this column.
293     * @return the column's logic position.
294     */
295    public String addIdConfidenceMeasureColumn(Parameter parameter,
296        Integer index, Class columnType) {
297        if (section != Section.Small_Molecule_Evidence_Header && section != Section.Small_Molecule_Evidence) {
298            throw new IllegalArgumentException(
299                "Section should be SmallMoleculeEvidence, but is " + section.
300                    getName());
301        }
302        if (parameter == null) {
303            throw new NullPointerException("Parameter should not be null!");
304        }
305
306        SortedMap<String, MZTabColumn> columns = new TreeMap<>();
307
308        MZTabColumn column = new MZTabColumn("id_confidence_measure", columnType,
309            false, Integer.parseInt(getColumnOrder(columnMapping.lastKey())) + "",
310            index);
311
312        columns.put(column.getLogicPosition(), column);
313        for(IMZTabColumn col : columns.values()) {
314            checkOptionalColumn(col);
315        }
316        return columns.lastKey();
317    }
318
319    /**
320     * The offset record the position of MZTabColumn in header line. For
321     * example, protein header line, the relationships between Logical Position,
322     * MZTabColumn, offset and order are like following structure: Logical
323     * Position MZTabColumn offset order "01" accession 1 01 "02" description 2
324     * 02 ...... "08" best_search_engine_score 8 08 "091"
325     * search_engine_score_ms_run[1] 9 09 "092" search_engine_score_ms_run[2] 10
326     * 09 "10" reliability 11 10 "111" num_psms_ms_run[1] 12 11 "112"
327     * num_psms_ms_run[2] 13 11
328     *
329     * @return a {@link java.util.SortedMap} object with the offsets for each
330     * column.
331     */
332    public SortedMap<Integer, IMZTabColumn> getOffsetColumnsMap() {
333        SortedMap<Integer, IMZTabColumn> map = new TreeMap<>();
334
335        int offset = 1;
336        for (IMZTabColumn column : columnMapping.values()) {
337            map.put(offset++, column);
338        }
339
340        return map;
341    }
342
343    /**
344     * Query the MZTabColumn in factory, based on column header with
345     * case-insensitive. Notice: for optional columns, header name maybe
346     * flexible. For example, num_psms_ms_run[1]. At this time, user SHOULD BE
347     * provide the full header name to query MZTabColumn. If just provide
348     * num_psms_ms_run, return null.
349     *
350     * @param header the column header to use as the search key.
351     * @return a {@link uk.ac.ebi.pride.jmztab2.model.IMZTabColumn} object or
352     * null.
353     */
354    public IMZTabColumn findColumnByHeader(String header) {
355        header = header.trim();
356
357        for (IMZTabColumn column : columnMapping.values()) {
358            if (header.equalsIgnoreCase(column.getHeader())) {
359                return column;
360            }
361        }
362
363        return null;
364    }
365}