From 2ef2a3a5db911bbbf3c2f8b5efbcf494ea74f416 Mon Sep 17 00:00:00 2001 From: Clemens Wrzodek Date: Thu, 8 Nov 2012 14:24:16 +0000 Subject: [PATCH] (hopefully final commit for KEGGtranslator version 2.2.0) - Added separate format options for SBML_L2V4 and SBML_L3V1 - Massively changed how relations are translated to BioPAX, especially in Level 3: o Added support for controller/control combinations o Added support for modified proteins with a modificaiton feature, that point to the same protein instance o Added complexAssembly and biochemicalReaction for some relations o ... - Various other improvements to 2BioPAX translations - Added PSI-MI and PSI-MOD ontologies - Improved command-line usage --- src/de/zbit/kegg/Translator.java | 17 + .../zbit/kegg/gui/TranslatorPanelTools.java | 4 +- src/de/zbit/kegg/gui/TranslatorUI.java | 39 +- .../zbit/kegg/io/AbstractKEGGtranslator.java | 6 +- src/de/zbit/kegg/io/BatchKEGGtranslator.java | 8 + src/de/zbit/kegg/io/KEGG2BioPAX.java | 270 ++++++-- src/de/zbit/kegg/io/KEGG2BioPAX_level2.java | 27 +- src/de/zbit/kegg/io/KEGG2BioPAX_level3.java | 631 ++++++++++++++++-- src/de/zbit/kegg/io/KEGG2SBMLqual.java | 7 + src/de/zbit/kegg/io/KEGG2jSBML.java | 60 ++ .../zbit/kegg/io/KEGGtranslatorIOOptions.java | 14 +- src/de/zbit/kegg/io/SBOMapping.java | 46 +- 12 files changed, 1015 insertions(+), 114 deletions(-) diff --git a/src/de/zbit/kegg/Translator.java b/src/de/zbit/kegg/Translator.java index 65d0618..64ce18f 100644 --- a/src/de/zbit/kegg/Translator.java +++ b/src/de/zbit/kegg/Translator.java @@ -536,5 +536,22 @@ protected boolean addCopyrightToSplashScreen() { public Window initGUI(AppConf appConf) { return new TranslatorUI(appConf); } + + /** + * Decide whether to show the gui or not. + */ + @Override + public boolean showsGUI() { + SBProperties props = getCommandLineArgs(); + boolean showGUI = (props.size() < 1) || props.getBooleanProperty(GUIOptions.GUI); + if (!showGUI) { + // Check if an input file is given. This is required for and will trigger the command-line mode. + String inputFile = props.getProperty(KEGGtranslatorIOOptions.INPUT); + if (inputFile==null || inputFile.length()<1) { + showGUI = true; + } + } + return showGUI; + } } diff --git a/src/de/zbit/kegg/gui/TranslatorPanelTools.java b/src/de/zbit/kegg/gui/TranslatorPanelTools.java index 5a1f4ac..08fb580 100644 --- a/src/de/zbit/kegg/gui/TranslatorPanelTools.java +++ b/src/de/zbit/kegg/gui/TranslatorPanelTools.java @@ -50,7 +50,7 @@ public static TranslatorPanel createPanel(final File inputFile, final Format TranslatorPanel panel = null; switch (outputFormat) { - case SBML: case SBML_QUAL: case SBML_CORE_AND_QUAL: /*case LaTeX: */ + case SBML: case SBML_QUAL: case SBML_CORE_AND_QUAL: case SBML_L2V4: case SBML_L3V1: /*case LaTeX: */ panel = new TranslatorSBMLPanel(inputFile, outputFormat, translationResult); break; @@ -80,7 +80,7 @@ public static TranslatorPanel createPanel(final String pathwayID, final Forma TranslatorPanel panel = null; switch (outputFormat) { - case SBML: case SBML_QUAL: case SBML_CORE_AND_QUAL: /*case LaTeX: */ + case SBML: case SBML_QUAL: case SBML_CORE_AND_QUAL: case SBML_L2V4: case SBML_L3V1: /*case LaTeX: */ panel = new TranslatorSBMLPanel(pathwayID, outputFormat, translationResult); break; diff --git a/src/de/zbit/kegg/gui/TranslatorUI.java b/src/de/zbit/kegg/gui/TranslatorUI.java index 36f3a92..187ea94 100644 --- a/src/de/zbit/kegg/gui/TranslatorUI.java +++ b/src/de/zbit/kegg/gui/TranslatorUI.java @@ -66,6 +66,7 @@ import de.zbit.gui.prefs.FileSelector; import de.zbit.gui.prefs.PreferencesPanel; import de.zbit.io.filefilter.SBFileFilter; +import de.zbit.kegg.KEGGtranslatorOptions; import de.zbit.kegg.Translator; import de.zbit.kegg.ext.KEGGTranslatorPanelOptions; import de.zbit.kegg.io.KEGG2jSBML; @@ -275,6 +276,11 @@ public void actionPerformed(ActionEvent e) { // Get selected file and format File inFile = getInputFile(r); String format = getOutputFileFormat(r); + + // Check if it is conform with current settings + if (!checkSettingsAndIssueWarning(format)) { + return; + } // Translate createNewTab(inFile, format); @@ -290,8 +296,38 @@ public void actionPerformed(ActionEvent e) { } /** + * Checks if the current application preferences are conform + * with the currently selected format and eventually + * issues a warning. + * @param format + * @return FALSE if the translation should be + * stopped. + */ + protected boolean checkSettingsAndIssueWarning(String format) { + + Format f = Format.valueOf(format); + if (f==null) { + GUITools.showErrorMessage(this, "Unknown output format: " + format); + return false; + } else if (f == Format.SBML_L2V4) { + // Check if Level 2 and extensions are selected. + SBPreferences prefs = SBPreferences.getPreferencesFor(KEGGtranslatorOptions.class); + if (KEGGtranslatorOptions.ADD_LAYOUT_EXTENSION.getValue(prefs) || + KEGGtranslatorOptions.USE_GROUPS_EXTENSION.getValue(prefs)) { + String message = "SBML supports extensions since Level 3. You've chosen to translate a document to Level 2 including the layout or groups extension, what is not possible.\nDo you want to deactivate the extensions for this translation?"; + int ret = GUITools.showQuestionMessage(this, message, "Conflict between selected Level and extension support", JOptionPane.OK_CANCEL_OPTION); + if (ret == JOptionPane.CANCEL_OPTION) { + return false; + } + } + } + + return true; + } + + /** * Searches for any JComponent with - * "TranslatorOptions.FORMAT.getOptionName()" on it and returns the selected + * {@link KEGGtranslatorIOOptions#FORMAT}.getOptionName() on it and returns the selected * format. Use it e.g. with {@link #translateToolBar}. * * @param r @@ -774,7 +810,6 @@ public URL getURLOnlineHelp() { /* (non-Javadoc) * @see java.beans.PropertyChangeListener#propertyChange(java.beans.PropertyChangeEvent) */ - @Override public void propertyChange(PropertyChangeEvent evt) { logger.fine(evt.toString()); } diff --git a/src/de/zbit/kegg/io/AbstractKEGGtranslator.java b/src/de/zbit/kegg/io/AbstractKEGGtranslator.java index 4fafdb8..b47f480 100644 --- a/src/de/zbit/kegg/io/AbstractKEGGtranslator.java +++ b/src/de/zbit/kegg/io/AbstractKEGGtranslator.java @@ -802,8 +802,8 @@ protected static String firstName(String name) { */ protected String NameToSId(String name) { /* - * letter ::= �a�..�z�,�A�..�Z� digit ::= �0�..�9� idChar ::= letter | - * digit | �_� SId ::= ( letter | �_� ) idChar* + * letter = a-z,A-Z; digit = 0-9; idChar = (letter | digit | _ ); + * SId = ( letter | _ ) idChar* */ String ret; if (name == null || name.trim().length() == 0) { @@ -848,7 +848,7 @@ protected String NameToSId(String name) { * @return */ private static boolean isLetter(char c) { - // Unfortunately Character.isLetter also acceps ß, but SBML doesn't. + // Unfortunately Character.isLetter also accepts ß, but SBML doesn't. // a-z or A-Z return (c>=97 && c<=122) || (c>=65 && c<=90); } diff --git a/src/de/zbit/kegg/io/BatchKEGGtranslator.java b/src/de/zbit/kegg/io/BatchKEGGtranslator.java index 7565ab9..8bf7962 100644 --- a/src/de/zbit/kegg/io/BatchKEGGtranslator.java +++ b/src/de/zbit/kegg/io/BatchKEGGtranslator.java @@ -294,6 +294,8 @@ private boolean writeAsJPG(Object translatedDoc, Pathway originalPW, String outF break; case SBML: + case SBML_L2V4: + case SBML_L3V1: myGraph = new SBML2GraphML().createGraph((org.sbml.jsbml.SBMLDocument) translatedDoc); break; @@ -331,6 +333,12 @@ public static KEGGtranslator getTranslator(Format outFormat, KeggInfoManageme case SBML: translator = new KEGG2jSBML(manager); break; + case SBML_L2V4: + translator = new KEGG2jSBML(manager, 2, 4); + break; + case SBML_L3V1: + translator = new KEGG2jSBML(manager, 3, 1); + break; case SBML_QUAL: translator = new KEGG2SBMLqual(manager); break; diff --git a/src/de/zbit/kegg/io/KEGG2BioPAX.java b/src/de/zbit/kegg/io/KEGG2BioPAX.java index 43946f5..93ad387 100644 --- a/src/de/zbit/kegg/io/KEGG2BioPAX.java +++ b/src/de/zbit/kegg/io/KEGG2BioPAX.java @@ -31,6 +31,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; @@ -60,12 +61,15 @@ import org.biopax.paxtools.model.level2.xref; import org.biopax.paxtools.model.level3.BioSource; import org.biopax.paxtools.model.level3.BiochemicalReaction; +import org.biopax.paxtools.model.level3.EntityReference; import org.biopax.paxtools.model.level3.InteractionVocabulary; import org.biopax.paxtools.model.level3.Level3Element; import org.biopax.paxtools.model.level3.Named; import org.biopax.paxtools.model.level3.Provenance; import org.biopax.paxtools.model.level3.PublicationXref; import org.biopax.paxtools.model.level3.RelationshipXref; +import org.biopax.paxtools.model.level3.SequenceModificationVocabulary; +import org.biopax.paxtools.model.level3.SimplePhysicalEntity; import org.biopax.paxtools.model.level3.SmallMolecule; import org.biopax.paxtools.model.level3.SmallMoleculeReference; import org.biopax.paxtools.model.level3.UnificationXref; @@ -90,6 +94,7 @@ import de.zbit.util.DatabaseIdentifiers.IdentifierDatabases; import de.zbit.util.EscapeChars; import de.zbit.util.Species; +import de.zbit.util.StringUtil; import de.zbit.util.Utils; import de.zbit.util.objectwrapper.ValuePair; @@ -154,12 +159,16 @@ protected Model translateWithoutPreprocessing(Pathway p) { initProgressBar(p,false,false); // The order of the following processes is important! + log.fine("Creating the BioPAX pathway instance."); pathway = createPathwayInstance(p); + log.fine("Creating the BioPAX entities."); createPhysicalEntities(p); if(considerReactions()){ + log.fine("Creating the BioPAX biochemical reactions."); createReactions(p); } if (considerRelations()) { + log.fine("Creating the BioPAX relations/interactions."); createRelations(p); } // TODO: (eventuell) ??? @@ -253,6 +262,9 @@ public BioPAXElement createXRef(IdentifierDatabases db, String id) { */ public BioPAXElement createXRef(IdentifierDatabases db, String id, int type) { if (id==null) return null; + if (type<1 || type>3) { + type = 2; // Default to relationship. + } String formattedID = DatabaseIdentifiers.getFormattedID(db, id); if (!DatabaseIdentifiers.checkID(db, formattedID)) { log.warning("Skipping invalid database entry " + id); @@ -260,11 +272,14 @@ public BioPAXElement createXRef(IdentifierDatabases db, String id, int type) { } if (formattedID==null || formattedID.length()<1) formattedID = id; - //String uri = '#'+NameToSId(db.toString() + '_' + formattedID); - String uri = DatabaseIdentifiers.getMiriamURI(db, formattedID); - if (uri==null || uri.length()<1) { - uri = '#'+NameToSId(db.toString() + '_' + formattedID); - } + // Igor R. told me not to use the identifiers.org URL as URI + // String uri = DatabaseIdentifiers.getMiriamURI(db, formattedID); + + // We cannot use nameToSId here, because it makes the ID unique, what is undesired. + String uri = formattedID.startsWith(db.toString().toUpperCase()) ? + formattedID : StringUtil.toWord(db.toString() + '_' + formattedID); + uri += "_" + type; // We need to create a different XRef for unifications or relationships! + // Avoid creating duplicates. if (model.getByID(uri)!=null) return model.getByID(uri); @@ -367,7 +382,8 @@ public BioPAXElement createBioSource(Pathway p) { } else if (model.getLevel()==BioPAXLevel.L3) { BioSource bioSource = model.addNew(BioSource.class, NameToSId(speciesString)); - bioSource.setName(Collections.singleton(speciesString)); + //bioSource.setName(Collections.singleton(speciesString)); + bioSource.setDisplayName(createDisplayName(speciesString)); if (taxonID!=null && taxonID.length()>0) { UnificationXref uxr = (UnificationXref) createXRef(IdentifierDatabases.NCBI_Taxonomy, taxonID, 1); if (uxr!=null) { @@ -445,7 +461,7 @@ public Collection createDataSources(Pathway p) { Provenance ds = model.addNew(Provenance.class, NameToSId(System.getProperty("app.name"))+"_DataSource"); pathwayComponentCreated(ds); // ds.setName(Collections.singleton(System.getProperty("app.name"))); - ds.setStandardName(System.getProperty("app.name")); + ds.setDisplayName(System.getProperty("app.name")); ds.addComment("http://www.cogsys.cs.uni-tuebingen.de/software/KEGGtranslator/"); ds.addXref((Xref) getPublicationXref()); ret.add(ds); @@ -453,7 +469,7 @@ public Collection createDataSources(Pathway p) { ds = model.addNew(Provenance.class, "KEGG_DataSource"); pathwayComponentCreated(ds); // ds.setName(Collections.singleton("KEGG Data")); - ds.setStandardName("KEGG database"); + //ds.setStandardName("KEGG database"); // No need to duplicate information ds.setDisplayName("KEGG database"); ds.addComment("http://www.genome.jp/kegg/"); ret.add(ds); @@ -508,7 +524,7 @@ public void addAnnotations(Reaction r, BioPAXElement reaction) { } if (infos.getDefinition() != null) { - String def = String.format("Definition of %s: %s", ko_id.toUpperCase(), formatTextForHTMLnotes(infos.getDefinition())); + String def = String.format("Definition of %s: %s", ko_id.toUpperCase(), (infos.getDefinition())); // Paxtools escapes HTML-chars automatically if (reaction instanceof Level2Element) { ((Level2Element) reaction).addCOMMENT(def); } else if (reaction instanceof Level3Element) { @@ -598,9 +614,9 @@ public void addAnnotations(Entry entry, BioPAXElement element) { } if (infos.getDefinition()!=null) { if (element instanceof Level2Element) { - ((Level2Element) element).addCOMMENT(formatTextForHTMLnotes(infos.getDefinition())); + ((Level2Element) element).addCOMMENT((infos.getDefinition())); // Paxtools escapes the chars. } else if (element instanceof Level3Element) { - ((Level3Element) element).addComment(formatTextForHTMLnotes(infos.getDefinition())); + ((Level3Element) element).addComment((infos.getDefinition())); } } @@ -616,23 +632,29 @@ public void addAnnotations(Entry entry, BioPAXElement element) { } } else if (element instanceof SmallMolecule) { if (infos.getFormulaDirectOrFromSynonym(manager) != null || infos.getMass() != null) { - SmallMoleculeReference ref = (SmallMoleculeReference) model.getByID(element.getRDFId() + "_reference"); - if (ref==null) { - ref = model.addNew(SmallMoleculeReference.class, element.getRDFId() + "_reference"); - pathwayComponentCreated(ref); - ((SmallMolecule) element).setEntityReference(ref); - } - - // Add some Xrefs to the SmallMoleculeReference - addSmallMoleculeXRefs(ref, ids); - - if (infos.getFormulaDirectOrFromSynonym(manager) != null) { - ref.setChemicalFormula(infos.getFormulaDirectOrFromSynonym(manager)); - } - if (infos.getMolecularWeight() != null) { - ref.setMolecularWeight((float) getNumber(infos.getMolecularWeight())); - } else if (infos.getMass() != null) { - ref.setMolecularWeight((float) getNumber(infos.getMass())); + BioPAXElement refNative = getEntityReference(element); + if (refNative==null || refNative instanceof SmallMoleculeReference) { + // should always be true + SmallMoleculeReference ref = (SmallMoleculeReference) refNative; + if (ref==null) { + ref = model.addNew(SmallMoleculeReference.class, + ensureUniqueRDFId(element.getRDFId() + KEGG2BioPAX_level3.EntityReferenceSuffix)); + pathwayComponentCreated(ref); + ((SmallMolecule) element).setEntityReference(ref); + } + + // Add some Xrefs to the SmallMoleculeReference + // This is now done later in the specific level 3 class! + //addSmallMoleculeXRefs(ref, ids); + + if (infos.getFormulaDirectOrFromSynonym(manager) != null) { + ref.setChemicalFormula(infos.getFormulaDirectOrFromSynonym(manager)); + } + if (infos.getMolecularWeight() != null) { + ref.setMolecularWeight((float) getNumber(infos.getMolecularWeight())); + } else if (infos.getMass() != null) { + ref.setMolecularWeight((float) getNumber(infos.getMass())); + } } } } @@ -642,14 +664,24 @@ public void addAnnotations(Entry entry, BioPAXElement element) { // Add X-REFs String pointOfView = entry.getRealType(); if (pointOfView == null) pointOfView = "protein"; - if (pointOfView.equals("complex")) pointOfView = "protein"; // complex are multple proteins. + if (pointOfView.equals("complex")) pointOfView = "protein"; // complex are multple proteins. + boolean hadAlreadyOneUnification = false; for (IdentifierDatabases db: ids.keySet()) { Collection id = ids.get(db); if (id==null) continue; for (Object i: id) { + // We should only add EXACTLY ONE unification XRef + int type = infereType(db, pointOfView, i.toString()); + if (type==1 && hadAlreadyOneUnification) { + type = 2; // Switch to relationship + } // Unfortunately, biopax does not allow grouping multiple ids in one xref bag for one db... - BioPAXElement xref = createXRef(db, i.toString(), infereType(db, pointOfView, i.toString())); + BioPAXElement xref = createXRef(db, i.toString(), type); if (xref!=null) { + if (UnificationXref.class.isAssignableFrom(xref.getModelInterface()) || + unificationXref.class.isAssignableFrom(xref.getModelInterface())) { + hadAlreadyOneUnification = true; + } if (element instanceof XReferrable) { ((XReferrable) element).addXREF((org.biopax.paxtools.model.level2.xref) xref); } else if (element instanceof org.biopax.paxtools.model.level3.XReferrable) { @@ -840,10 +872,44 @@ private void setReactionToReactionEntry(Pathway p, Reaction r, BioPAXElement rea public void createRelations(Pathway p) { Set processedRelations = new HashSet(); - // All species added. Parse reactions and relations. + + + // Resort the list: + // Try to keep the current order, but move the "block" of all phosphorylations (and similar) + // to top of list, append the reverse reactions (DEPHOSPHORYLATION) and then append the rest. + List sorted = new ArrayList(p.getRelations().size()); + int reverseReactionsPosition = 0; + int addedReverseReactions = 0; + Set avoidDuplicates = new HashSet(); for (Relation r : p.getRelations()) { + Collection subtypes = r.getSubtypesNames(); + + // Avoid duplicates + Entry eOne = p.getEntryForId(r.getEntry1()); + Entry eTwo = p.getEntryForId(r.getEntry2()); + String uniqueString = eOne.getName() + "|" + eTwo.getName() + "|" + ArrayUtils.implode(subtypes, "|", true); + if (!avoidDuplicates.add(uniqueString)) { + continue; // Duplicate realtion + } + // Insert into sorted list + if (subtypes.contains(SubType.PHOSPHORYLATION) || subtypes.contains(SubType.METHYLATION) || + subtypes.contains(SubType.UBIQUITINATION) || subtypes.contains(SubType.GLYCOSYLATION)) { + sorted.add(reverseReactionsPosition, r); // append to top of list + reverseReactionsPosition++; + } else if (subtypes.contains(SubType.DEPHOSPHORYLATION)) { + sorted.add(reverseReactionsPosition+addedReverseReactions, r); // append below phosphorylations + addedReverseReactions++; + } else { + sorted.add(r); // add to end of list + } + } + + + // Appropriately add all relations to the model + for (Relation r : sorted) { if (processedRelations.add(r.toString())) { + log.finer("Processing " + r.toString()); addKGMLRelation(r,p); } } @@ -905,7 +971,8 @@ public void createPhysicalEntities(Pathway p) { protected BioPAXElement getInteractionVocuabulary(SubType st) { String formattedName = st.getName().trim().replace(' ', '_').replace("/", "_or_"); - String rfid = "#relation_subtype_" + formattedName; + //String rfid = "#relation_subtype_" + formattedName; + String rfid = getVocabularyID(st, false); BioPAXElement voc=null; if (level == BioPAXLevel.L3) { voc = (InteractionVocabulary) model.getByID(rfid); @@ -937,25 +1004,88 @@ protected BioPAXElement getInteractionVocuabulary(SubType st) { // Add additional XRefs to MI, SBO and GO + boolean addedOneUnificationXRef = false; if (miTerm!=null && miTerm.getB()!=null && miTerm.getB()>0) { BioPAXElement xr = createXRef(IdentifierDatabases.MI, Integer.toString(miTerm.getB()), 1); addOntologyXRef(voc, xr, miTerm.getA()); + addedOneUnificationXRef = true; } - int sbo = SBOMapping.getSBOTerm(st.getName()); - if (sbo>0) { - BioPAXElement xr = createXRef(IdentifierDatabases.SBO, Integer.toString(sbo), 1); - addOntologyXRef(voc, xr, formattedName); + if (!addedOneUnificationXRef) { // Unfortunately, the spec does not allow relationship xrefs... + int sbo = SBOMapping.getSBOTerm(st.getName()); + if (sbo>0) { + BioPAXElement xr = createXRef(IdentifierDatabases.SBO, Integer.toString(sbo), addedOneUnificationXRef?2:1); + addOntologyXRef(voc, xr, formattedName); + addedOneUnificationXRef = true; + } } + if (!addedOneUnificationXRef) { // Unfortunately, the spec does not allow relationship xrefs... + int go = SBOMapping.getGOTerm(st.getName()); + if (go>0) { + BioPAXElement xr = createXRef(IdentifierDatabases.GeneOntology, Integer.toString(go), addedOneUnificationXRef?2:1); + addOntologyXRef(voc, xr, formattedName); + addedOneUnificationXRef = true; + } + } + } + + return voc; + } + + /** + * Get the RDF-ID (URI) that is used for a controlled vocabulary. + * This is to date either a {@link SequenceModificationVocabulary} or + * a {@link InteractionVocabulary}. + *

+ * This method tries to denote the vocabulary with an identifiers.org URI. + * This has been suggested by Igor R. + * @param st the corresponding subtype + * @param proteinModification {@code TRUE} ONLY for modification + * vocabularies, such as {@link SequenceModificationVocabulary}. + * @return an RDF-ID of the vocabulary element. + */ + protected String getVocabularyID(SubType st, boolean proteinModification) { + + // Use UNIQUE database for distinction between interactions and modifications + if (proteinModification) { + ValuePair MODterm = SBOMapping.getMODTerm(st.getName()); + if (MODterm!=null && MODterm.getB()!=null && MODterm.getB()>0) { + return DatabaseIdentifiers.getMiriamURI(IdentifierDatabases.MOD, Integer.toString(MODterm.getB())); + } + } else { + ValuePair miTerm = SBOMapping.getMITerm(st.getName()); + if (miTerm!=null && miTerm.getB()!=null && miTerm.getB()>0) { + return DatabaseIdentifiers.getMiriamURI(IdentifierDatabases.MI, Integer.toString(miTerm.getB())); + } + } + + String uri = null; + int sbo = SBOMapping.getSBOTerm(st.getName()); + if (sbo>0) { + uri = DatabaseIdentifiers.getMiriamURI(IdentifierDatabases.SBO, Integer.toString(sbo)); + } + + if (uri==null) { int go = SBOMapping.getGOTerm(st.getName()); if (go>0) { - BioPAXElement xr = createXRef(IdentifierDatabases.GeneOntology, Integer.toString(go), 1); - addOntologyXRef(voc, xr, formattedName); + uri = DatabaseIdentifiers.getMiriamURI(IdentifierDatabases.GeneOntology, Integer.toString(go)); } } - return voc; + // Should actually never occur... + if (uri==null) { + String formattedName = st.getName().trim().replace(' ', '_').replace("/", "_or_"); + uri = "#voc_subtype_" + formattedName; + } + + // We NEED a distinction between URIs for modifications (SequenceModificationVocabulary classes) + // and interactions (InteractionVocabulary classes) + if (proteinModification) { + uri += "_mod"; + } + + return uri; } @@ -964,7 +1094,7 @@ protected BioPAXElement getInteractionVocuabulary(SubType st) { * @param xRef * @param formattedName */ - private void addOntologyXRef(BioPAXElement xReferrableBPelement, + protected void addOntologyXRef(BioPAXElement xReferrableBPelement, BioPAXElement xRef, String formattedName) { if (xRef!=null) { if (level == BioPAXLevel.L3) { @@ -1026,4 +1156,64 @@ protected void pathwayComponentCreated(BioPAXElement element) { } } + /** + * Ensures a displayName that has at most 24 characters. + * @param displayName any (potentially long) name + * @return a string that is no longer than 24 characters. + */ + public static String createDisplayName(String displayName) { + if (displayName.length()>24) { + // Try to cut the string at a good position + int max = displayName.lastIndexOf(' ', 21); + max = Math.max(max, displayName.lastIndexOf('\t', 21)); + max = Math.max(max, displayName.lastIndexOf('\n', 21)); + max = Math.max(max, displayName.lastIndexOf(',', 21)); + max = Math.max(max, displayName.lastIndexOf(';', 21)); + + if (max>=10) { + displayName = displayName.substring(0, max)+"..."; + } else { + displayName = displayName.substring(0, 20)+"..."; + } + } + + return displayName; + } + + /** + * Ensures the given {@code id} is unique in the current + * {@link #model}. + *

If possible, use {@link #NameToSId(String)} instead of this method!!!

+ * @param id + * @return + */ + protected String ensureUniqueRDFId(String id) { + String originalID = id; + if (model.containsID(id)) { + int i=2; + id = originalID.concat(Integer.toString(i)); + while (model.containsID(id)) { + i++; + id = originalID.concat(Integer.toString(i)); + } + } + return id; + } + + /** + * Only for Level3. + * Gets the common reference to the given element. + * Onls for {@link SimplePhysicalEntity}s. + *

Does NOT create one if there is none (returns {@code null}). + * @param element + * @return + * @return + */ + protected EntityReference getEntityReference(BioPAXElement element) { + if (element instanceof SimplePhysicalEntity) { + return ((SimplePhysicalEntity) element).getEntityReference(); + } + return (EntityReference) model.getByID(element.getRDFId() + KEGG2BioPAX_level3.EntityReferenceSuffix); + } + } diff --git a/src/de/zbit/kegg/io/KEGG2BioPAX_level2.java b/src/de/zbit/kegg/io/KEGG2BioPAX_level2.java index f067d4a..7746086 100644 --- a/src/de/zbit/kegg/io/KEGG2BioPAX_level2.java +++ b/src/de/zbit/kegg/io/KEGG2BioPAX_level2.java @@ -36,6 +36,7 @@ import org.biopax.paxtools.model.level2.biochemicalReaction; import org.biopax.paxtools.model.level2.catalysis; import org.biopax.paxtools.model.level2.complex; +import org.biopax.paxtools.model.level2.complexAssembly; import org.biopax.paxtools.model.level2.conversion; import org.biopax.paxtools.model.level2.dataSource; import org.biopax.paxtools.model.level2.dna; @@ -108,7 +109,7 @@ public KEGG2BioPAX_level2(KeggInfoManagement manager) { protected BioPAXElement createPathwayInstance(Pathway p) { pathway = model.addNew(pathway.class, p.getName()); pathway.setAVAILABILITY(Collections.singleton(String.format("This file has been generated by %s version %s", System.getProperty("app.name"), System.getProperty("app.version")))); - pathway.setNAME(formatTextForHTMLnotes(p.getTitle())); + pathway.setNAME((p.getTitle())); // Paxtools escapes chars for HTML automatically // Parse Kegg Pathway information boolean isKEGGPathway = DatabaseIdentifiers.checkID(DatabaseIdentifiers.IdentifierDatabases.KEGG_Pathway, p.getNameForMIRIAM()); @@ -125,7 +126,7 @@ protected BioPAXElement createPathwayInstance(Pathway p) { // Get PW infos from KEGG Api for Description and GO ids. KeggInfos pwInfos = KeggInfos.get(p.getName(), manager); // NAME, DESCRIPTION, DBLINKS verwertbar if (pwInfos.queryWasSuccessfull()) { - pathway.addCOMMENT(formatTextForHTMLnotes(pwInfos.getDescription())); + pathway.addCOMMENT((pwInfos.getDescription())); // GO IDs if (pwInfos.getGo_id() != null) { @@ -291,7 +292,7 @@ public BioPAXElement addEntry(Entry entry, Pathway p) { } // Create the actual element - BioPAXElement element = model.addNew(instantiate, '#'+NameToSId(entry.getName())); + BioPAXElement element = model.addNew(instantiate, '#'+NameToSId(entry.getName().length()>45?entry.getName().substring(0, 45):entry.getName())); pathwayComponentCreated(element); // NOTE: we can cast to entity, as all used classes are derived from entity @@ -501,12 +502,26 @@ public BioPAXElement addKGMLRelation(Relation r, Pathway p) { } } - // "binding/assoc.", "dissociation", "missing interaction" and in doubt to PhysicalphysicalInteraction + + // "binding/assoc.", "dissociation", "missing interaction" and in doubt to PhysicalInteraction if ((subtype.contains(SubType.ASSOCIATION) || subtype.contains(SubType.BINDING) || subtype.contains(SubType.BINDING_ASSOCIATION)) || (subtype.contains(SubType.DISSOCIATION)) || subtype.contains(SubType.MISSING_INTERACTION) || subtype.size()<1) { + // This property may get overwritten later on! instantiate = physicalInteraction.class; // Same as Interaction.class in L3 } + // Check if "binding/assoc." describes the formation of a complex. + if ((eTwo.getType().equals(EntryType.group) || eTwo.getType().equals(EntryType.genes)) && + (subtype.contains(SubType.ASSOCIATION) || subtype.contains(SubType.BINDING) || subtype.contains(SubType.BINDING_ASSOCIATION))) { + instantiate = complexAssembly.class; + } + + // Check if "DISSOCIATION" describes the DISASSEMBLY of a complex. + if ((eOne.getType().equals(EntryType.group) || eOne.getType().equals(EntryType.genes)) && + (subtype.contains(SubType.DISSOCIATION))) { + instantiate = complexAssembly.class; // this is also used for DISASSEMBLY. + } + // Make a final check, if we are able to create a conversion if ((!(qOne instanceof physicalEntity) || !(qTwo instanceof physicalEntity)) && (instantiate == conversion.class)) { @@ -521,7 +536,9 @@ public BioPAXElement addKGMLRelation(Relation r, Pathway p) { // Add Annotations bpe.setDATA_SOURCE(pathway.getDATA_SOURCE()); if (subtype.size()>0) { - bpe.addCOMMENT("LINE-TYPE: " + r.getSubtypes().iterator().next().getValue()); + if (!subtype.contains(SubType.COMPOUND)) { + bpe.addCOMMENT("LINE-TYPE: " + r.getSubtypes().iterator().next().getValue()); + } bpe.setNAME(ArrayUtils.implode(subtype, ", ")); for (SubType st: r.getSubtypes()) { diff --git a/src/de/zbit/kegg/io/KEGG2BioPAX_level3.java b/src/de/zbit/kegg/io/KEGG2BioPAX_level3.java index 9d87419..9bbcbb7 100644 --- a/src/de/zbit/kegg/io/KEGG2BioPAX_level3.java +++ b/src/de/zbit/kegg/io/KEGG2BioPAX_level3.java @@ -20,9 +20,13 @@ */ package de.zbit.kegg.io; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; import java.util.Set; +import java.util.logging.Level; import org.biopax.paxtools.model.BioPAXElement; import org.biopax.paxtools.model.BioPAXLevel; @@ -30,6 +34,9 @@ import org.biopax.paxtools.model.level3.BiochemicalReaction; import org.biopax.paxtools.model.level3.Catalysis; import org.biopax.paxtools.model.level3.Complex; +import org.biopax.paxtools.model.level3.ComplexAssembly; +import org.biopax.paxtools.model.level3.Control; +import org.biopax.paxtools.model.level3.ControlType; import org.biopax.paxtools.model.level3.Controller; import org.biopax.paxtools.model.level3.Conversion; import org.biopax.paxtools.model.level3.ConversionDirectionType; @@ -38,11 +45,16 @@ import org.biopax.paxtools.model.level3.DnaRegion; import org.biopax.paxtools.model.level3.DnaRegionReference; import org.biopax.paxtools.model.level3.Entity; +import org.biopax.paxtools.model.level3.EntityFeature; import org.biopax.paxtools.model.level3.EntityReference; import org.biopax.paxtools.model.level3.Gene; import org.biopax.paxtools.model.level3.Interaction; import org.biopax.paxtools.model.level3.InteractionVocabulary; +import org.biopax.paxtools.model.level3.Level3Element; +import org.biopax.paxtools.model.level3.ModificationFeature; import org.biopax.paxtools.model.level3.MolecularInteraction; +import org.biopax.paxtools.model.level3.Named; +import org.biopax.paxtools.model.level3.NucleicAcid; import org.biopax.paxtools.model.level3.PhysicalEntity; import org.biopax.paxtools.model.level3.Protein; import org.biopax.paxtools.model.level3.ProteinReference; @@ -51,10 +63,16 @@ import org.biopax.paxtools.model.level3.RnaReference; import org.biopax.paxtools.model.level3.RnaRegion; import org.biopax.paxtools.model.level3.RnaRegionReference; +import org.biopax.paxtools.model.level3.SequenceEntityReference; +import org.biopax.paxtools.model.level3.SequenceModificationVocabulary; import org.biopax.paxtools.model.level3.SimplePhysicalEntity; import org.biopax.paxtools.model.level3.SmallMolecule; import org.biopax.paxtools.model.level3.SmallMoleculeReference; import org.biopax.paxtools.model.level3.Stoichiometry; +import org.biopax.paxtools.model.level3.TemplateDirectionType; +import org.biopax.paxtools.model.level3.TemplateReaction; +import org.biopax.paxtools.model.level3.TemplateReactionRegulation; +import org.biopax.paxtools.model.level3.UnificationXref; import org.biopax.paxtools.model.level3.XReferrable; import org.biopax.paxtools.model.level3.Xref; @@ -74,6 +92,8 @@ import de.zbit.util.ArrayUtils; import de.zbit.util.DatabaseIdentifiers; import de.zbit.util.DatabaseIdentifiers.IdentifierDatabases; +import de.zbit.util.StringUtil; +import de.zbit.util.objectwrapper.ValuePair; /** * KEGG2BioPAX level 3 converter (also called KGML2BioPAX). @@ -93,7 +113,17 @@ public class KEGG2BioPAX_level3 extends KEGG2BioPAX { * {@link BioSource} for the organism */ private BioSource organism = null; + + /** + * A common suffix for all {@link EntityReference}s. + */ + public final static String EntityReferenceSuffix = ".eref"; + /** + * A common suffix for all entities that are being duplicated as + * result of a modification (may end with "_mod", BUT ALSO "_mod2",...). + */ + public final static String ENTITY_MODIFICATION_SUFFIX = "_mod"; /** * Initialize a new {@link KEGG2BioPAX} object, using a new Cache and a new KeggAdaptor. */ @@ -171,7 +201,7 @@ public BioPAXElement addEntry(Entry entry, Pathway p) { } // Create the actual element - String eId = '#'+NameToSId(entry.getName()); + String eId = '#'+NameToSId(entry.getName().length()>45?entry.getName().substring(0, 45):entry.getName()); BioPAXElement element = model.addNew(instantiate, eId); pathwayComponentCreated(element); @@ -187,10 +217,7 @@ public BioPAXElement addEntry(Entry entry, Pathway p) { if (fullName!=null) { ((Entity)element).setStandardName(fullName); // Graphics name } - String displayName = name; - if (displayName.length()>20) { - displayName = displayName.substring(0, 16)+"..."; - } + String displayName = createDisplayName(name); ((Entity)element).setDisplayName(displayName); // Intelligent name // --- addDataSources(element); @@ -209,12 +236,11 @@ public BioPAXElement addEntry(Entry entry, Pathway p) { } if (ceb==null || !(ceb instanceof PhysicalEntity)) continue; - ((Complex)element).addComponent((PhysicalEntity) ceb); + ((Complex) element).addComponent((PhysicalEntity) ceb); } } } - // XXX: Possible to set ORGANISM on COMPLEX & sequenceEntity (& Gene in L3) // TODO: CellularLocation from EntryExtended in L3 // Add various annotations and xrefs @@ -225,19 +251,7 @@ public BioPAXElement addEntry(Entry entry, Pathway p) { // error if no entityReferences are set. if ((element instanceof SimplePhysicalEntity) && !(element instanceof Complex)) { - - EntityReference er = createEntityReference(element); - if (er!=null) { - ((SimplePhysicalEntity)element).setEntityReference(er); - - // We need at least 1 xref on each element to avoid errors in the validator. - if (((XReferrable)element).getXref()!=null) { - for (Xref xr : ((XReferrable)element).getXref()) { - er.addXref(xr); - } - } - //--- - } + setupEntityReference(element); } @@ -245,13 +259,59 @@ public BioPAXElement addEntry(Entry entry, Pathway p) { return element; } + /** + * @param element + */ + private void setupEntityReference(BioPAXElement element) { + EntityReference er = getEntityReference(element); + if (er==null) { + er = createEntityReference(element); + } + + if (er!=null) { + ((SimplePhysicalEntity) element).setEntityReference(er); + + // Actually we could also MOVE all XRefs to the reference! + if (((XReferrable)element).getXref()!=null) { + List unifications = new LinkedList(); + for (Xref xr : ((XReferrable)element).getXref()) { + er.addXref(xr); + if (xr.getModelInterface().equals(UnificationXref.class)) { + unifications.add(xr); + } + } + // Unifications should relly only be used once (and this should be on the reference) + for (Xref xr : unifications) { + ((XReferrable)element).removeXref(xr); + } + } + //--- + + // Further set Organism and names (do not set organism on h2o and similar). + if (er instanceof SequenceEntityReference && + !er.getModelInterface().equals(SmallMoleculeReference.class)) { + ((SequenceEntityReference) er).setOrganism(organism); + } + if (er instanceof Named && element instanceof Named) { + ((Named) er).setStandardName(((Named) element).getStandardName()); + ((Named) er).setDisplayName(((Named) element).getDisplayName()); + ((Named) er).setName(((Named) element).getName()); + } + //--- + } + } + /** * Create an {@link EntityReference} for any {@link BioPAXElement}. + * + *

Please setup organism, names and XRefs on this element. + * * @param element * @return corresponding {@link EntityReference} or {@code null}. */ private EntityReference createEntityReference(BioPAXElement element) { - String id = element.getRDFId() + ".eref"; + String id = element.getRDFId() + EntityReferenceSuffix; + id = ensureUniqueRDFId(id); // we cannot use nameToSID because it would remove, e.g., the starting dash #. EntityReference bpEr = null; if (element instanceof SmallMolecule){ @@ -261,7 +321,6 @@ private EntityReference createEntityReference(BioPAXElement element) { } else if (element instanceof Protein) { bpEr = model.addNew(ProteinReference.class, id); - if (organism != null) ((ProteinReference)bpEr).setOrganism(organism); } else if (element instanceof Rna) { bpEr = model.addNew(RnaReference.class, id); @@ -280,6 +339,14 @@ private EntityReference createEntityReference(BioPAXElement element) { // or unknown or unspecified elements } + + // Adjust organism, names and xrefs (ATP has no organism...) + if (bpEr instanceof SequenceEntityReference && + !bpEr.getModelInterface().equals(SmallMoleculeReference.class)) { + ((SequenceEntityReference)bpEr).setOrganism(organism); + } + + if (bpEr!=null) { pathwayComponentCreated(bpEr); } @@ -291,8 +358,10 @@ private EntityReference createEntityReference(BioPAXElement element) { * @param element */ private void addDataSources(BioPAXElement element) { - for (Provenance ds : pathway.getDataSource()) { - ((Entity)element).addDataSource(ds); + if (element instanceof Entity) { + for (Provenance ds : pathway.getDataSource()) { + ((Entity)element).addDataSource(ds); + } } } @@ -303,12 +372,9 @@ private void addDataSources(BioPAXElement element) { protected BioPAXElement createPathwayInstance(Pathway p) { pathway = model.addNew(org.biopax.paxtools.model.level3.Pathway.class, p.getName()); pathway.addAvailability(String.format("This file has been generated by %s version %s", System.getProperty("app.name"), System.getProperty("app.version"))); - String htmlName = formatTextForHTMLnotes(p.getTitle()); + String htmlName = (p.getTitle()); // Escaping is done automatically in Paxtools! pathway.addName(htmlName); - String displayName = htmlName; - if (displayName.length()>20) { - displayName = displayName.substring(0, 16)+"..."; - } + String displayName = createDisplayName(htmlName); pathway.setDisplayName(displayName); pathway.setStandardName(htmlName); @@ -322,13 +388,13 @@ protected BioPAXElement createPathwayInstance(Pathway p) { } // Retrieve further information via Kegg Adaptor - organism = (BioSource) createBioSource(p); + organism = (BioSource) createBioSource(p); pathway.setOrganism(organism); // Get PW infos from KEGG Api for Description and GO ids. KeggInfos pwInfos = KeggInfos.get(p.getName(), manager); // NAME, DESCRIPTION, DBLINKS verwertbar if (pwInfos.queryWasSuccessfull()) { - pathway.addComment(formatTextForHTMLnotes(pwInfos.getDescription())); + pathway.addComment((pwInfos.getDescription())); // GO IDs if (pwInfos.getGo_id() != null) { @@ -403,10 +469,7 @@ public BioPAXElement addKGMLReaction(Reaction r, Pathway p) { reaction.addName(r.getName()); - String displayName = r.getName(); - if (displayName.length()>20) { - displayName = displayName.substring(0, 16)+"..."; - } + String displayName = createDisplayName(r.getName()); reaction.setDisplayName(displayName); addDataSources(reaction); @@ -446,7 +509,7 @@ private boolean configureReactionComponent(Pathway p, BiochemicalReaction reacti // Set the stoichiometry Integer stoich = rc.getStoichiometry(); - Stoichiometry s = model.addNew(Stoichiometry.class, '#'+NameToSId(ce.getName()+"_"+reaction.getDisplayName()+"_stoich")); + Stoichiometry s = model.addNew(Stoichiometry.class, '#'+NameToSId(ce.getName()+"_"+getNameForElement(reaction)+"_stoich")); pathwayComponentCreated(s); s.setPhysicalEntity((PhysicalEntity) ceb); s.setStoichiometricCoefficient(stoich==null?1f:(float)stoich); @@ -486,18 +549,10 @@ public BioPAXElement addKGMLRelation(Relation r, Pathway p) { return null; } -// // Relations should prefarably translated, using the EntityReferences -// if (qOne instanceof SimplePhysicalEntity && -// ((SimplePhysicalEntity)qOne).getEntityReference()!=null ) { -// qOne = ((SimplePhysicalEntity)qOne).getEntityReference(); -// } -// if (qTwo instanceof SimplePhysicalEntity && -// ((SimplePhysicalEntity)qTwo).getEntityReference()!=null ) { -// qTwo = ((SimplePhysicalEntity)qTwo).getEntityReference(); -// } // Most relations have a left and right side => conversion as default Class instantiate = Conversion.class; + boolean createConversionAndControl = false; // Compound (only PPREL) to Conversion, SKIP ALL OTHERS [IF CONSIDERREACTIONS()] @@ -513,34 +568,87 @@ public BioPAXElement addKGMLRelation(Relation r, Pathway p) { } } + // Simple A -> B + if (subtype.contains(SubType.STATE_CHANGE) || subtype.contains(SubType.INDIRECT_EFFECT)) { + createConversionAndControl = false; + instantiate = Conversion.class; + } + + // Create a controlled "B -> B' (activated)" conversion + if (subtype.contains(SubType.ACTIVATION) || subtype.contains(SubType.INHIBITION) || + subtype.contains(SubType.EXPRESSION) || subtype.contains(SubType.REPRESSION)) { + createConversionAndControl = true; + instantiate = Conversion.class; + if (subtype.contains(SubType.EXPRESSION) || subtype.contains(SubType.REPRESSION)) { + if (qTwo instanceof PhysicalEntity) { + instantiate = TemplateReaction.class; // Create a Regulated template reaction + } + } + } + // "binding/assoc.", "dissociation", "missing interaction" and in doubt to PhysicalInteraction if ((subtype.contains(SubType.ASSOCIATION) || subtype.contains(SubType.BINDING) || subtype.contains(SubType.BINDING_ASSOCIATION)) || (subtype.contains(SubType.DISSOCIATION)) || subtype.contains(SubType.MISSING_INTERACTION) || subtype.size()<1) { + // This property may get overwritten later on! instantiate = MolecularInteraction.class; // Interaction is same as physicalInteraction.class in L2 } + // Check if "binding/assoc." describes the formation of a complex. + if ((eTwo.getType().equals(EntryType.group) || eTwo.getType().equals(EntryType.genes)) && + (subtype.contains(SubType.ASSOCIATION) || subtype.contains(SubType.BINDING) || subtype.contains(SubType.BINDING_ASSOCIATION))) { + instantiate = ComplexAssembly.class; + } + // Check if "DISSOCIATION" describes the DISASSEMBLY of a complex. + if ((eOne.getType().equals(EntryType.group) || eOne.getType().equals(EntryType.genes)) && + (subtype.contains(SubType.DISSOCIATION))) { + instantiate = ComplexAssembly.class; // this is also used for DISASSEMBLY. + } + + // These types are controlleds relations in which A Phosphorylates B. + if (subtype.contains(SubType.PHOSPHORYLATION) || subtype.contains(SubType.DEPHOSPHORYLATION) || + subtype.contains(SubType.GLYCOSYLATION) || subtype.contains(SubType.UBIQUITINATION) || + subtype.contains(SubType.METHYLATION)) { + createConversionAndControl = true; + instantiate = BiochemicalReaction.class; + } // Make a final check, if we are able to create a the desired class (e.g., a conversion) - if (!(qOne instanceof PhysicalEntity) || !(qTwo instanceof PhysicalEntity)) { - if (instantiate == Conversion.class) { + if ((!createConversionAndControl && !(qOne instanceof PhysicalEntity)) || !(qTwo instanceof PhysicalEntity)) { + // Explanation: if createConversionAndControl then qOne is the controller and not involved in the conversion. + // else, it is translated to qOne->qTwo and it is involved in the conversion. + if (Conversion.class.isAssignableFrom(instantiate)) { log.fine("Changing from Conversion to MolecularInteraction, because Conversion requires physical entities as participants " + r); instantiate = MolecularInteraction.class; } - if ((instantiate == MolecularInteraction.class) && - (!(qOne instanceof PhysicalEntity) && !(qTwo instanceof PhysicalEntity))) { + + if ((MolecularInteraction.class.isAssignableFrom(instantiate)) && + ((!createConversionAndControl && !(qOne instanceof PhysicalEntity)) && !(qTwo instanceof PhysicalEntity))) { // MolecularInteraction requires at least one PhysicalEntity (only by definition). log.fine("Changing from MolecularInteraction to Interaction, because MolecularInteraction requires at least one physical entity as participant " + r); instantiate = Interaction.class; } } + // If we do NOT create a controller/Control thing and just a simple A -> B + // then try to "keep reaction chains", e.g., "A -> A' -> B". + // Thus, look for a modified qOne (=A) here. + if (!createConversionAndControl) { + BioPAXElement qOneMod = getModifiedEntity(qOne, null); + if (qOneMod!=null) { + qOne = qOneMod; + } + } + // Create the relation Interaction bpe = (Interaction) model.addNew(instantiate, '#'+NameToSId("KEGGrelation")); pathwayComponentCreated(bpe); + bpe.setDisplayName(createDisplayName(ArrayUtils.implode(subtype, ", ") + " of " + getNameForElement(qTwo))); // Add Annotations addDataSources(bpe); if (subtype.size()>0) { - bpe.addComment("LINE-TYPE: " + r.getSubtypes().iterator().next().getValue()); + if (!subtype.contains(SubType.COMPOUND)) { + bpe.addComment("LINE-TYPE: " + r.getSubtypes().iterator().next().getValue()); + } bpe.addName(ArrayUtils.implode(subtype, ", ")); for (SubType st: r.getSubtypes()) { @@ -550,8 +658,18 @@ public BioPAXElement addKGMLRelation(Relation r, Pathway p) { // Add participants if (bpe instanceof Conversion) { - ((Conversion) bpe).addLeft((PhysicalEntity) qOne); - ((Conversion) bpe).addRight((PhysicalEntity) qTwo); + // if qTwo is no SimplePhysicalEntity, we cannot add any mofification feature. Hence, + // it does not make sense to crate a controller/controlled thing. + if (createConversionAndControl && (qTwo instanceof SimplePhysicalEntity)) { + setupControllerControlled(r, bpe, qOne, qTwo); + + } else { + + // A "default arrow" from ony -> two. + ((Conversion) bpe).addLeft((PhysicalEntity) qOne); + ((Conversion) bpe).addRight((PhysicalEntity) qTwo); + } + } else { bpe.addParticipant((Entity) qOne); bpe.addParticipant((Entity) qTwo); @@ -560,4 +678,413 @@ public BioPAXElement addKGMLRelation(Relation r, Pathway p) { return bpe; } + /** + * Get the best possible name for a {@link BioPAXElement}. + * @param qTwo + * @return + */ + private String getNameForElement(BioPAXElement qTwo) { + String name = null; + if (qTwo instanceof Named) { + name = ((Named) qTwo).getDisplayName(); + if (name==null || name.length()<1) { + name = ((Named) qTwo).getStandardName(); + } + if ((name==null || name.length()<1) && ((Named) qTwo).getName()!=null) { + name = ArrayUtils.implode(((Named) qTwo).getName(), ", "); + } + } + + if (name==null || name.length()<1) { + name = qTwo.getRDFId(); + } + + return name; + } + + /** + * @param r + * @param subtype + * @param bpe + * @param qOne + * @param qTwo + */ + private void setupControllerControlled(Relation r, Interaction bpe, BioPAXElement qOne, BioPAXElement qTwo) { + Collection subtype = r.getSubtypesNames(); + + // Determine the type of controller that should be created + Class instantiate = Control.class; + + if (bpe instanceof TemplateReaction) { + instantiate = TemplateReactionRegulation.class; +// } else if (bpe instanceof BiochemicalReaction) { +// instantiate = Catalysis.class; + } + + // Create the controller + Control controller = (Control) model.addNew(instantiate, '#'+NameToSId("KEGGrelationController")); + pathwayComponentCreated(controller); + addDataSources(controller); + String name = getNameForElement(qOne); + name = ArrayUtils.implode(subtype, ", ") + " by " + name; + controller.addName(name); + controller.setDisplayName(createDisplayName(name)); + controller.addControlled(bpe); + try { + controller.addController((Controller) qOne); + controller.addParticipant((Entity) qOne); + } catch (Exception e) { + //should actually never happen + log.log(Level.WARNING, "Catched an unexpected exception.", e); + } + for (SubType st: r.getSubtypes()) { + // Same InteractionTypes as bpe has. + controller.addInteractionType((InteractionVocabulary) getInteractionVocuabulary(st)); + } + + // Setup the controlType + if (subtype.contains(SubType.ACTIVATION) || subtype.contains(SubType.EXPRESSION)) { + controller.setControlType(ControlType.ACTIVATION); + } else if (subtype.contains(SubType.INHIBITION) || subtype.contains(SubType.REPRESSION)) { + controller.setControlType(ControlType.INHIBITION); + } + + // Maybe we need to setup a reverse reaction (B' -> B) instead of normally B -> B' (B' is e.g. a phosphorylated entitity). + boolean modelReversely = (subtype.contains(SubType.DEPHOSPHORYLATION)); + + + // Get or create the modified qTwo protein + BioPAXElement qThree = null; + if (qTwo instanceof SimplePhysicalEntity && modelReversely) { + // If a dephosphorylation occurs, we maybe already have a phosphorylation feature! + // Search for an already phosphorylated entity + BioPAXElement phosphoQTwo = getModifiedEntity(qTwo, SubType.PHOSPHORYLATION); + if (phosphoQTwo!=null) { + // Use the phosphorylated thing as source for the dephosphorylation. + qThree = qTwo; + qTwo = phosphoQTwo; + } else { + modelReversely = false; + } + } else { + modelReversely = false; + } + + // Create a third protein + if (qThree==null) { + if (!(bpe instanceof TemplateReaction) || !(qTwo instanceof PhysicalEntity)) { + // the normal case + qThree = createCopy(qTwo); + } else { + // we need to create some nucleicAcid + BioPAXElement nAcid = createCopy(qTwo, NucleicAcid.class); + qThree = qTwo; + qTwo = nAcid; + } + } + + // Setup the Features + for (SubType st: r.getSubtypes()) { + boolean isDePhospho = (st.getName().equals(SubType.DEPHOSPHORYLATION)); + + String modifiedName = st.getName(); + if (isDePhospho && modelReversely) { + st = new SubType(SubType.PHOSPHORYLATION); + } + if (modifiedName.endsWith("ion")) { + modifiedName = modifiedName.substring(0, modifiedName.length()-3)+"ed"; + } + + // Modification types are UNIQUE for a certain [combination of] subtypes. + String modID = '#'+modifiedName.trim().replace(' ', '_').replace("/", "_or_"); + ModificationFeature mod = (ModificationFeature) model.getByID(modID); + boolean modificationDidAlreadyExist = mod!=null; + if (mod==null) { + mod = (ModificationFeature) model.addNew(ModificationFeature.class, modID); + pathwayComponentCreated(mod); + addDataSources(mod); + } + + + // Add the modification to both proteins and the reference + if (qThree instanceof PhysicalEntity) { + if (modelReversely && isDePhospho) { + // This is modeled reversely by +p -> -p + ((PhysicalEntity) qThree).addNotFeature(mod); + ((PhysicalEntity) qTwo).addFeature(mod); + } else { + ((PhysicalEntity) qThree).addFeature(mod); + ((PhysicalEntity) qTwo).addNotFeature(mod); + } + if (qTwo instanceof SimplePhysicalEntity) { + EntityReference eRef = ((SimplePhysicalEntity) qTwo).getEntityReference(); + if (eRef!=null) { + eRef.addEntityFeature(mod); + } + } + removeContradictingFeatures((PhysicalEntity) qTwo); + removeContradictingFeatures((PhysicalEntity) qThree); + } + + // Annotate the kind of modification + SequenceModificationVocabulary mVoc; + boolean addCommentToSubstrate = false; + mVoc = getSequenceModificationVocabulary(st); + if (isDePhospho) { + addCommentToSubstrate = true; // must not be equal to isReversePhospho ! + controller.addComment("Dephosphorylation"); + } else { + String comment = ArrayUtils.implode(mVoc.getComment(), ", ").replace("ed_", "ion_"); + if (comment.endsWith("ed")) comment = comment.substring(0, comment.length()-2)+"ion"; + controller.addComment(comment); // E.g. "methylation_at_unknown_residue" + } + if (!modificationDidAlreadyExist) { + mod.setModificationType(mVoc); + } + + // FACT: if (isReversePhospho) then qThree gets NOT feature. + // FACT: if (isReversePhospho && DEPHOSPHORYLATION) than type is now PHOSPHORYLATION. + //String comment = (isReversePhospho?"NOT [":"")+ArrayUtils.implode(mVoc.getComment(), ", ")+(isReversePhospho?"]":""); // E.g. "methylated_at_unknown_residue" + + if (addCommentToSubstrate) { + ((Level3Element) qTwo).addComment(ArrayUtils.implode(mVoc.getComment(), ", ")); // E.g. "methylated_at_unknown_residue" + } else { // usual case, except for dephosphorylation, what is changed to phosphorylation of the substrate. + ((Level3Element) qThree).addComment(ArrayUtils.implode(mVoc.getComment(), ", ")); // E.g. "methylated_at_unknown_residue" + } + } + + // Avoid duplicate entries, search if exactly this one has already been creted once + List objects = new ArrayList(model.getObjects()); + boolean checkTwo = true, checkThree = true; + for (BioPAXElement e : objects) { + if (e == qTwo || e == qThree) { + // The same pointer, not only equal! + continue; + } else if (checkTwo && e.isEquivalent(qTwo)) { + model.remove(qTwo); + qTwo = e; + checkTwo = false; + } else if (checkThree && e.isEquivalent(qThree)) { + model.remove(qThree); + qThree = e; + checkThree = false; + } + if (!checkTwo && !checkThree) { + break; + } + } + + + // Configure the actual conversion + ((Conversion) bpe).addLeft((PhysicalEntity) qTwo); + ((Conversion) bpe).addRight((PhysicalEntity) qThree); + controller.addParticipant((Entity) qTwo); + controller.addParticipant((Entity) qThree); + ((Conversion) bpe).setConversionDirection(ConversionDirectionType.LEFT_TO_RIGHT); + + if (bpe instanceof TemplateReaction && qTwo instanceof NucleicAcid) { + ((TemplateReaction) bpe).setTemplate((NucleicAcid) qTwo); + ((TemplateReaction) bpe).addProduct((PhysicalEntity) qThree); + ((TemplateReaction) bpe).setTemplateDirection(TemplateDirectionType.FORWARD); + } + } + + /** + * Removes features that occur as notFeatures and features. + * @param qTwo + */ + private void removeContradictingFeatures(PhysicalEntity qTwo) { + List features = new ArrayList(qTwo.getFeature()); + features.retainAll(qTwo.getNotFeature()); + for (EntityFeature ft : features) { + qTwo.removeFeature(ft); + qTwo.removeNotFeature(ft); + } + } + + /** + * Search an instance of {@code entity} that has a feature that has been + * created, based on a modification from a {@code subtype}. + * @param entity the BASIC, unmodified entity (e.g., does NOT end with {@link #ENTITY_MODIFICATION_SUFFIX}). + * @param subtype (name of modification process). If {@code null}, any modified {@code entity} will be returned. + * @return the already existing {@link BioPAXElement} which corresponds to {@code entity} with the given modification {@code subtype}. + * Or {@code null} if such an element is not yet available. + */ + private BioPAXElement getModifiedEntity(BioPAXElement entity, String subtype) { + if (subtype!=null) { + subtype = subtype.trim().replace(' ', '_').replace("/", "_or_"); + } + // They end with "_mod", "_mod2",... look if they share the same + // ent.Reference and maybe contain a phosphorylation feature. + + if (!(entity instanceof SimplePhysicalEntity)) { + return null; + } + + EntityReference eRef = ((SimplePhysicalEntity) entity).getEntityReference(); + BioPAXElement modEntity = model.getByID(entity.getRDFId() + ENTITY_MODIFICATION_SUFFIX); + int i = 1; + while (modEntity!=null) { + // Are both derived from the same thing? + if (modEntity instanceof SimplePhysicalEntity && + ((SimplePhysicalEntity)modEntity).getEntityReference().equals(eRef)) { + // Does it contain the specified subtype? + Set features = ((PhysicalEntity) modEntity).getFeature(); + if (features!=null) { + if (subtype==null) return modEntity; + for (EntityFeature f : features) { + if (StringUtil.containsWord(f.getRDFId(), subtype)) { + return modEntity; + } + } + } + + } + i++; + modEntity = model.getByID(entity.getRDFId() + ENTITY_MODIFICATION_SUFFIX + i); + } + + return null; + } + + + /** + * ONLY FOR LEVEL 3
+ * Gets or creates a {@link SequenceModificationVocabulary} corresponding to the given {@link SubType}. + * @return {@link SequenceModificationVocabulary} for level 3. + */ + protected SequenceModificationVocabulary getSequenceModificationVocabulary(SubType st) { + String formattedName = st.getName().trim().replace(' ', '_').replace("/", "_or_"); + //String rfid = "#modification_type_" + formattedName; + String rfid = getVocabularyID(st, true); + SequenceModificationVocabulary voc = (SequenceModificationVocabulary) model.getByID(rfid); + + // Term is not yet available => create it. + if (voc==null) { + // Create the object + voc = model.addNew(SequenceModificationVocabulary.class, rfid); + pathwayComponentCreated(voc); + + // For methylation, phosphorylation, etc. we have MOD terms + ValuePair MODterm = SBOMapping.getMODTerm(st.getName()); + + String termName; + if (MODterm!=null && MODterm.getA()!=null && MODterm.getA().length()>0) { + termName = MODterm.getA(); + + // The term MUST be a string from MOD-ontology! Else, it is a BioPAX ERROR! + ((SequenceModificationVocabulary)voc).addTerm(termName); + } else { + termName = formattedName; + if (termName.endsWith("ion")) { + termName = termName.replace("ion", "ed"); // methylation -> methylated + } + } + + ((SequenceModificationVocabulary)voc).addComment(termName); // + "_at_unknown_residue" + + + boolean addedAUnification = false; + // Add additional XRefs to MI, SBO and GO + if (MODterm!=null && MODterm.getB()>0) { + // It MUST BE any children of MOD:01157 or MOD:01156. + BioPAXElement xr = createXRef(IdentifierDatabases.MOD, Integer.toString(MODterm.getB()), 1); + addOntologyXRef(voc, xr, MODterm.getA()); + addedAUnification = true; + } + + /* + * It would be nice to include SBO and GO here with RELATIONSHIP xrefs (type=2). + * However, BioPAX only allows unification xrefs, what is critically here, because + * a modification is no interaction... + * Therefore, I changed it now to create unifications (type=1) and exactly one xref! + */ + + // I know, SBO and GO are actually for interactions and not for states. But there is no other possibility to non-textually encode + // e.g., "protein that is methylated at any residue". + if (!addedAUnification) { + int sbo = SBOMapping.getSBOTerm(st.getName()); + if (sbo>0) { + BioPAXElement xr = createXRef(IdentifierDatabases.SBO, Integer.toString(sbo), 1); + addOntologyXRef(voc, xr, formattedName); + addedAUnification = true; + } + } + + if (!addedAUnification) { + int go = SBOMapping.getGOTerm(st.getName()); + if (go>0) { + BioPAXElement xr = createXRef(IdentifierDatabases.GeneOntology, Integer.toString(go), 1); + addOntologyXRef(voc, xr, formattedName); + addedAUnification = true; + } + } + } + + return voc; + } + + /** + * Creates a copy of an {@link BioPAXElement} that has been created with + * {@link #addEntry(Entry, Pathway)}. This is very useful, e.g., to + * create a second instance of the same protein with a different features + * (e.g., a phosphorylation). + * + * @param element + * @return copy of {@code element} with the same properties and only different RFId. + */ + public BioPAXElement createCopy(BioPAXElement element) { + return createCopy(element, element.getModelInterface()); + } + public BioPAXElement createCopy(BioPAXElement element, Class typeOfCopy) { + String eId = ensureUniqueRDFId(element.getRDFId() + ENTITY_MODIFICATION_SUFFIX); // Make unique + BioPAXElement newElement = model.addNew(typeOfCopy, eId); + pathwayComponentCreated(newElement); + + // Names + if (element instanceof Named && newElement instanceof Named ) { + ((Named) newElement).setStandardName(((Named) element).getStandardName()); + ((Named) newElement).setDisplayName(((Named) element).getDisplayName()); + ((Named) newElement).setName(((Named) element).getName()); + } + // --- + addDataSources(newElement); + + // Complex components + if (element instanceof Complex && newElement instanceof Complex) { + for (PhysicalEntity pe : ((Complex) newElement).getComponent()) { + ((Complex) newElement).addComponent(pe); + } + } + + + // Add all potential annotations that come from addAnnotations(); + if (element instanceof XReferrable && newElement instanceof XReferrable) { + for (Xref xr : ((XReferrable) element).getXref()) { + ((XReferrable) newElement).addXref(xr); + } + } + if (element instanceof BiochemicalReaction && newElement instanceof BiochemicalReaction) { + for (String ec : ((BiochemicalReaction) element).getECNumber()) { + ((BiochemicalReaction) newElement).addECNumber(ec); + } + } + if (element instanceof Level3Element && newElement instanceof Level3Element) { + for (String c : ((Level3Element) element).getComment()) { + ((Level3Element) newElement).addComment(c); + } + } + + // TODO: So far not copied (but not important in KEGGtranslator): CellularLocations, participantOf, Features, NotFeatures + + + // Now comes the important part, we need to set a reference to the same entity as + // the oritinal biopax element + if (element instanceof SimplePhysicalEntity && newElement instanceof SimplePhysicalEntity) { + ((SimplePhysicalEntity) newElement).setEntityReference(((SimplePhysicalEntity) element).getEntityReference()); + } + + return newElement; + } + } diff --git a/src/de/zbit/kegg/io/KEGG2SBMLqual.java b/src/de/zbit/kegg/io/KEGG2SBMLqual.java index 054f4ff..b418b95 100644 --- a/src/de/zbit/kegg/io/KEGG2SBMLqual.java +++ b/src/de/zbit/kegg/io/KEGG2SBMLqual.java @@ -304,6 +304,13 @@ public Transition addKGMLRelation(Relation r, Pathway p, QualitativeModel qualMo cv.addResource(go); } } + de.zbit.util.objectwrapper.ValuePair subMI = SBOMapping.getMITerm(subType); + if (subMI!=null && subMI.getB()!=null && subMI.getB()>0) { + String mi = DatabaseIdentifiers.getMiriamURN(IdentifierDatabases.GeneOntology, Integer.toString(subMI.getB())); + if (mi!=null) { + cv.addResource(mi); + } + } } diff --git a/src/de/zbit/kegg/io/KEGG2jSBML.java b/src/de/zbit/kegg/io/KEGG2jSBML.java index d90ff62..8c636a6 100644 --- a/src/de/zbit/kegg/io/KEGG2jSBML.java +++ b/src/de/zbit/kegg/io/KEGG2jSBML.java @@ -116,6 +116,19 @@ public class KEGG2jSBML extends AbstractKEGGtranslator { */ private double speciesDefaultInitialAmount=1d; + /** + * The SBML level to be initialized. NULL results in + * initializing it automatically (adjusts the level as needed (e.g. + * extensions require L3)). + */ + protected Integer level = null; + + /** + * The SBML version to be initialized. NULL results in + * initializing it automatically (adjusts it as needed). + */ + protected Integer version = null; + /*=========================== * PUBLIC, STATIC VARIABLES @@ -154,6 +167,25 @@ public KEGG2jSBML(KeggInfoManagement manager) { * Getters and Setters * ===========================*/ + /** + * @param manager + * @param level + * @param version + */ + public KEGG2jSBML(KeggInfoManagement manager, int level, int version) { + this(manager); + setLevelAndVersion(new ValuePair(level, version)); + } + + /** + * Set the SBML level and version that should be created. + * @param level + */ + private void setLevelAndVersion(ValuePair level) { + this.level = level.getL(); + this.version = level.getV(); + } + /** * See {@link #addCellDesignerAnnots} * @return @@ -658,10 +690,38 @@ private List getEntriesWithGroupsAsLast(Pathway p) { } /** + * Automatically returns an appropriate level and version. If previously + * a level has been set manually with {@link #setLevelAndVersion(ValuePair)}, + * this is returned and the configuration of this class might be change + * to be conform with the preset level. + * * @return the level and version of the SBML core (2,4) if no extension * should be used. Else: 3,1. */ protected ValuePair getLevelAndVersion() { + + // If a level is manually set, check if it supports extensions + if (level!=null) { + if (level<3) { + if (addLayoutExtension) { + log.warning("SBML supports extensions since Level 3. You've chosen to translate a document to Level 2 including the layout extension, what is not possible.\nDeavtivating the layout extension for this translation."); + setAddLayoutExtension(false); + } + if (useGroupsExtension) { + log.warning("SBML supports extensions since Level 3. You've chosen to translate a document to Level 2 including the groups extension, what is not possible.\nDeavtivating the groups extension for this translation."); + setUseGroupsExtension(false); + } + } + if (version==null) { + // Actually, they should only be set in combination... + if (level==2) version = 4; + else version = 1; + } + return new ValuePair(level, version); + } + + // Auto-adjust level as needed + // Layout extension requires Level 3 if (!addLayoutExtension && !useGroupsExtension) { return new ValuePair(Integer.valueOf(2), Integer.valueOf(4)); diff --git a/src/de/zbit/kegg/io/KEGGtranslatorIOOptions.java b/src/de/zbit/kegg/io/KEGGtranslatorIOOptions.java index 3d4664a..b97cb83 100644 --- a/src/de/zbit/kegg/io/KEGGtranslatorIOOptions.java +++ b/src/de/zbit/kegg/io/KEGGtranslatorIOOptions.java @@ -48,9 +48,17 @@ public interface KEGGtranslatorIOOptions extends KeyProvider { */ public enum Format { /** - * + * Automatically adjusts the level as needed (e.g., extensions require L3). */ SBML, + /** + * + */ + SBML_L2V4, + /** + * + */ + SBML_L3V1, /** * */ @@ -116,6 +124,10 @@ public enum Format { */ //SVG ; + + public boolean isSBML() { + return toString().contains("SBML"); + } } /* diff --git a/src/de/zbit/kegg/io/SBOMapping.java b/src/de/zbit/kegg/io/SBOMapping.java index 382d4d0..9c2d09f 100644 --- a/src/de/zbit/kegg/io/SBOMapping.java +++ b/src/de/zbit/kegg/io/SBOMapping.java @@ -268,7 +268,8 @@ public static int getGOTerm(String subtype) { } /** - * Convert to a MI-term that is a child of 'MI:0190' (Molecular Interaction (PSI-MI)). + * Convert subtype to a MI-term that is a child of 'MI:0190' (Molecular Interaction (PSI-MI)). + * The terms are for relations/interactions. * @param subtype * @return {@link ValuePair} with the term name and integer id. Or NULL if * no MI term is available that matches the given input {@link SubType}. @@ -276,6 +277,11 @@ public static int getGOTerm(String subtype) { public static ValuePair getMITerm(String subtype) { if (subtype.equals(SubType.ASSOCIATION)) { return new ValuePair("association", 914); //MI:0914 + } else if (subtype.equals(SubType.BINDING)) { + return new ValuePair("covalent binding", 195); + } else if (subtype.equals(SubType.BINDING_ASSOCIATION)) { + return new ValuePair("association", 914); + } else if (subtype.equals(SubType.PHOSPHORYLATION)) { return new ValuePair("phosphorylation reaction", 217); } else if (subtype.equals(SubType.DEPHOSPHORYLATION)) { @@ -287,16 +293,13 @@ public static ValuePair getMITerm(String subtype) { return new ValuePair("glycosylation reaction", 559); } else if (subtype.equals(SubType.METHYLATION)) { return new ValuePair("methylation reaction", 213); - } else if (subtype.equals(SubType.BINDING)) { - return new ValuePair("covalent binding", 195); - } else if (subtype.equals(SubType.BINDING_ASSOCIATION)) { - return new ValuePair("association", 914); + } else if (subtype.equals(SubType.COMPOUND) || subtype.equals(SubType.HIDDEN_COMPOUND)) { return new ValuePair("direct interaction", 407); -// } else if (subtype.equals(SubType.DEPHOSPHORYLATION)) { -// return new ValuePair("dephosphorylation reaction", 203); +// } else if (subtype.equals(SubType.REPRESSION)) { +// return new ValuePair("suppression", 796); // } else if (subtype.equals(SubType.DEPHOSPHORYLATION)) { // return new ValuePair("dephosphorylation reaction", 203); // } else if (subtype.equals(SubType.DEPHOSPHORYLATION)) { @@ -321,9 +324,34 @@ public static ValuePair getMITerm(String subtype) { return null; } - - + /** + * Convert subtype to a MOD-term (Protein Modification Ontology (PSI-MOD)) + * that is either a child of 'MOD:01157' or 'MOD:01156'. + *

The terms are for entries that are modified as result of an relation/interaction. + * @param subtype + * @return {@link ValuePair} with the term name and integer id. Or NULL if + * no MI term is available that matches the given input {@link SubType}. + */ + public static ValuePair getMODTerm(String subtype) { + + if (subtype.equals(SubType.PHOSPHORYLATION)) { + return new ValuePair("phosphorylated residue", 696); +// } else if (subtype.equals(SubType.DEPHOSPHORYLATION)) { +// return new ValuePair("5'-dephospho", 948); + // This term is not valid (no child of 'MOD:01157' or 'MOD:01156'). + } else if (subtype.equals(SubType.UBIQUITINATION) || + subtype.equalsIgnoreCase("ubiquination")) { + return new ValuePair("ubiquitinylation residue", 492); + } else if (subtype.equals(SubType.GLYCOSYLATION)) { + return new ValuePair("glycosylated residue", 693); + } else if (subtype.equals(SubType.METHYLATION)) { + return new ValuePair("methylated residue", 427); + } + + return null; + } + /** * Formats an SBO term. E.g. "177" to "SBO:0000177". * @param sbo