/*
* Copyright (c) 2003-2012 Fred Hutchinson Cancer Research Center
*
* 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 org.fhcrc.cpl.toolbox.proteomics.feature.extraInfo;
import org.apache.log4j.Logger;
import org.fhcrc.cpl.toolbox.TextProvider;
import org.fhcrc.cpl.toolbox.proteomics.feature.Feature;
import org.fhcrc.cpl.toolbox.proteomics.feature.FeatureSet;
import org.fhcrc.cpl.toolbox.proteomics.feature.extraInfo.AmtExtraInfoDef;
import javax.swing.*;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Constructor;
/**
* Feature has a HashMap<String, Object> of properties. Anything that isn't defined
* in a field of Feature goes there.
*
* Also, Properties at the FeatureSet level can be managed by FeatureExtraInformationDefs.
* For instance, only MS2 FeatureSets care about the "modifications" and "search_database_path"
* Properties, so those properties are handled explicitly by MS2ExtraInfoDef. That handling
* is somewhat less rigid than the handling for Feature-level properties, in that you don't
* need to declare the Class of the property ahead of time, just ways to convert the property
* to and from String form.
*
* Groups of properties are tied together in instances of the FeatureExtraInformationDef
* class. Not the actual values, but the definitions of them: for each property, a
* column name and datatype (Class).
*
* When FeatureSet loads a file, if it finds any
* column from a given ExtraInformationDef, it considers itself to possess that set of
* data -- i.e., all of those columns will be written out when the FeatureSet is
* saved to a file. If two column names from different ExtraInformationDefs are the same,
* an exception will be thrown, since that would cause a collision in the hashtable.
*
* When reading data from and writing to files, FeatureExtraInformationDef uses a
* single-String constructor to instantiate an object of the appropriate type and the
* toString() method to write it out.
*
* The vanilla way to reference these data is with Feature.getProperty() and
* Feature.setProperty(). But that's dangerous, because there's no compile-time validation
* that you got the column name right, or the class of the property. So
* FeatureExtraInformationDef can be subclassed to provide static methods to do this
* validation. For instance: MS2ExtraInfoDef.getPeptideProphet(feature)
*
* Subclasses can also do their own thing to read in and write out data, instead of using
* single-String constructors and toString(), in case that ever becomes necessary.
*
* FeatureExtraInformationDefs may also provide popup menus that appear when a feature
* containing this extra information type is right-clicked from the main image pane
* (see createPopupMenuItems(Feature feature))
*/
public class FeatureExtraInformationDef
{
protected static final String MULTI_VALUE_LIST_SEPARATOR = ";";
static Logger _log = Logger.getLogger(FeatureExtraInformationDef.class);
//static initialization stuff
public static final FeatureExtraInformationDef intensityWindowsInformationDef =
new FeatureExtraInformationDef(
"INTENSITY_WINDOWS",
new String[]{"window"},
new Class[]{String.class}
);
public static final FeatureExtraInformationDef cidExtraInfoDef =
new FeatureExtraInformationDef(
"CID",
new String[]{"cidscan"},
new Class[]{Integer.class}
);
//all known extra info types. In theory this could contain custom info types
//not envisioned at development time
protected static List<FeatureExtraInformationDef> _knownExtraInfoTypes;
//Initialize the standard info types. In a static block for clarity
//TODO: find a good way to initialize custom info types
static
{
_standardExtraInformationTypes =
new FeatureExtraInformationDef[]{
IsotopicLabelExtraInfoDef.getSingletonInstance(),
MS2ExtraInfoDef.getSingletonInstance(),
TimeExtraInfoDef.getSingletonInstance(),
AmtExtraInfoDef.getSingletonInstance(),
intensityWindowsInformationDef,
cidExtraInfoDef
};
for (FeatureExtraInformationDef def : getStandardExtraInformationTypes())
{
addKnownExtraInfoType(def);
}
}
//instance variables
//each one of these needs a name, primarily so that it can appear in an error
//string if anything goes wrong
protected String textCode;
protected Map<String, Class<?>> columnNameDatatypeMap;
//unfortunately we have to store the column names in an array, too, to preserve ordering
protected String[] columnNames;
//feature set properties
protected String[] featureSetPropertyNames = null;
public FeatureExtraInformationDef()
{
init();
}
public FeatureExtraInformationDef(String name, String[] columnNames, Class<?>[] dataTypes)
{
this();
setTextCode(name);
if (columnNames.length != dataTypes.length)
throw new RuntimeException("FeatureSetExtraInformation: not all column datatypes defined");
for (int i = 0; i < columnNames.length; i++)
columnNameDatatypeMap.put(columnNames[i], dataTypes[i]);
this.columnNames = columnNames;
}
public FeatureExtraInformationDef(String name, String[] columnNames, Class[] dataTypes,
String[] featureSetPropertyNames)
{
this(name, columnNames, dataTypes);
this.featureSetPropertyNames = featureSetPropertyNames;
}
public String getTextCode()
{
return textCode;
}
public void setTextCode(String textCode)
{
this.textCode = textCode;
}
protected Object getFeatureSetProperty(FeatureSet featureSet, String propertyName)
{
return featureSet.getProperty(createFeatureSetPropertyName(propertyName));
}
protected void setFeatureSetProperty(FeatureSet featureSet, String propertyName,
Object propertyValue)
{
_log.debug("Setting featureset property " + propertyName + " to " + propertyValue);
featureSet.setProperty(createFeatureSetPropertyName(propertyName), propertyValue);
}
public String createFeatureSetPropertyName(String propertyName)
{
return textCode + ":" + propertyName;
}
public String stripPrefixFromFeatureSetPropertyName(String propertyName)
{
return propertyName.substring(propertyName.indexOf(":") + 1);
}
public boolean isThisTypeOfFeatureSetProperty(String propertyName)
{
return propertyName.startsWith(textCode + ":");
}
/**
* Doing the call to TextProvider here so that custom subclasses can
* do whatever they want instead
* @return
*/
public String getTranslatedText()
{
return TextProvider.getText(getTextCode());
}
protected void init()
{
columnNameDatatypeMap = new HashMap<String, Class<?>>();
}
/**
* Return the array that preserves original column order
* @return
*/
public String[] getColumnNames()
{
return columnNames;
}
public Class<?> getDatatypeForColumnName(String columnName)
{
return columnNameDatatypeMap.get(columnName);
}
public String toString()
{
StringBuffer resultBuf = new StringBuffer("FeatureExtraInformation, columns: ");
for (String columnName : getColumnNames())
resultBuf.append(columnName + " (" +
getDatatypeForColumnName(columnName).getName() + "), ");
return resultBuf.toString();
}
/**
* Convert a String value to the appropriate datatype for the column.
* In this implementation, simply calls a one-arg constructor.
* This can be overridden in a subclass to do something more interesting,
* if necessary.
*
* Many things can conceivably go wrong here. The column might not be
* defined in the hashmap (NullPointerException). The class might not
* have a one-String constructor. That constructor might not be public.
* The constructor might fail on the provided String. Any of these
* things will throw an exception
*
* @param columnName
* @param value
* @return
* @throws InstantiationException
* @throws IllegalAccessException
* @throws InvocationTargetException
* @throws NoSuchMethodException
*/
public Object convertStringValue(String columnName, String value)
throws InstantiationException, IllegalAccessException,
InvocationTargetException, NoSuchMethodException
{
Class dataTypeClass = getDatatypeForColumnName(columnName);
//if we don't need to convert, don't convert
if (String.class.isAssignableFrom(dataTypeClass))
return value;
Constructor<String> constructor =
dataTypeClass.getConstructor(String.class);
return constructor.newInstance(value);
}
/**
* For output to feature file. This is used so that the Class containing
* the value doesn't necessarily have to implement a toString() method that does
* what we want it to.
*
* In the default implementation, however, that's what happens.
* @param columnName
* @param value
* @return
*/
public String convertToString(String columnName, Object value)
{
if (value == null)
return null;
return value.toString();
}
/**
* Same as convertToString, but for feature set properties
* @param propertyName
* @param value
* @return
*/
public String convertFeatureSetPropertyToString(String propertyName, Object value)
{
if (value == null)
return null;
return value.toString();
}
/**
* Save as convertStringValue, but for feature set properties
* @param propertyName
* @param value
* @return
*/
public Object convertFeatureSetPropertyStringValue(String propertyName, String value)
{
return value;
}
protected static FeatureExtraInformationDef[] _standardExtraInformationTypes;
protected static Map<String, Class> _knownColumnDataClassMap;
public static Map<String, Class> getKnownColumnDataClassMap()
{
if (_knownColumnDataClassMap == null)
_knownColumnDataClassMap = new HashMap<String, Class>();
return _knownColumnDataClassMap;
}
public static Class getKnownDataClassForColumn(String columnName)
{
return getKnownColumnDataClassMap().get(columnName);
}
public static List<FeatureExtraInformationDef> getKnownExtraInfoTypes()
{
if (_knownExtraInfoTypes == null)
{
_knownExtraInfoTypes =
new ArrayList<FeatureExtraInformationDef>(_standardExtraInformationTypes.length);
}
return _knownExtraInfoTypes;
}
protected static Map<String, FeatureExtraInformationDef> _columnInfoTypeMap;
public static Map<String, FeatureExtraInformationDef> getColumnInfoTypeMap()
{
if (_columnInfoTypeMap == null)
{
_columnInfoTypeMap =
new HashMap<String, FeatureExtraInformationDef>();
}
return _columnInfoTypeMap;
}
/**
* This method is how you make the system aware of custom information types
* @param infoType
*/
public static void addKnownExtraInfoType(FeatureExtraInformationDef infoType)
{
getKnownExtraInfoTypes().add(infoType);
for (String columnName : infoType.getColumnNames())
{
if (getColumnInfoTypeMap().containsKey(columnName))
_log.info("WARNING!! double-defining extra info type with column " +
columnName + "; new info type is " + infoType.getTextCode() +
", will override older one");
getColumnInfoTypeMap().put(columnName, infoType);
getKnownColumnDataClassMap().put(columnName, infoType.getDatatypeForColumnName(columnName));
}
}
public static FeatureExtraInformationDef getInfoTypeForColumn(String columnName)
{
return getColumnInfoTypeMap().get(columnName);
}
public static FeatureExtraInformationDef[] getStandardExtraInformationTypes()
{
return _standardExtraInformationTypes;
}
/**
* This method should, in the subclasses, add popup menu items that should be
* shown when the feature is right-clicked from the main image pane
* @param feature
* @return
*/
public List<JMenuItem> createPopupMenuItems(Feature feature)
{
return new ArrayList<JMenuItem>();
}
public static String convertStringArrayToString(String[] stringArray)
{
return convertStringArrayToString(stringArray, MULTI_VALUE_LIST_SEPARATOR);
}
public static String convertStringArrayToString(String[] stringArray, String separatorString)
{
if (stringArray == null || stringArray.length == 0)
return "";
//check to see if every value is null. If so, return empty string.
//This is for situations where we've added a bunch of peptides
//without adding proteins. We keep adding items to the list, to preserve
//association between proteins and peptides, but there's no need to write
//out the separators
boolean hasNonNullValue = false;
for (String value : stringArray)
{
if (value != null) hasNonNullValue = true;
}
if (!hasNonNullValue)
return "";
StringBuffer result = new StringBuffer(stringArray[0]);
for (int i = 1; i < stringArray.length; i++)
result.append(separatorString + stringArray[i]);
return result.toString();
}
public static String convertIntListToString(List<Integer> intList)
{
if (intList == null)
return "";
List<String> intListAsString = new ArrayList<String>(intList.size());
for (Integer val : intList)
intListAsString.add("" + val);
return convertStringListToString(intListAsString);
}
public static String convertStringListToString(List<String> stringList)
{
return convertStringListToString(stringList, MULTI_VALUE_LIST_SEPARATOR);
}
/**
* Convert a list of peptides or proteins into a String for storage in a feature file
*
* @param stringList
* @return
*/
public static String convertStringListToString(List<String> stringList, String listSeparator)
{
if (stringList == null)
return "";
return convertStringArrayToString(stringList.toArray(new String[stringList.size()]),
listSeparator);
}
public static List<String>
parseFormulaListString(String stringListString)
{
return parseStringListString(stringListString, MULTI_VALUE_LIST_SEPARATOR);
}
public static List<String>
parseStringListString(String stringListString)
{
return parseStringListString(stringListString, MULTI_VALUE_LIST_SEPARATOR);
}
public static List<Integer> parseIntListString(String intListString)
{
if (intListString == null)
return new ArrayList<Integer>();
List<String> stringList = parseStringListString(intListString);
List<Integer> intList = new ArrayList<Integer>(stringList.size());
for (String string : stringList)
intList.add(Integer.parseInt(string));
return intList;
}
/**
* Parse the value of a peptide or protein list column in a feature file
* @param stringListString
* @return
*/
public static List<String>
parseStringListString(String stringListString, String separatorString)
{
List<String> result = new ArrayList<String>();
String[] stringArray =
stringListString.split(separatorString);
for (String string : stringArray)
{
result.add(string);
}
return result;
}
}