diff --git a/CHANGELOG b/CHANGELOG index fc6e768e723b27707a15e1c32a84ac272b3052e6..64fd46a81741edf0f676cea7c34fc6ea5d89d176 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +minerva (14.0.3) stable; urgency=medium + * Bug fix: some SBGN files uploaded to minerva could not be exported to SBML + due to problems with identifiers used by SBGN (#1006) + + -- Piotr Gawron <piotr.gawron@uni.lu> Wed, 06 Nov 2019 12:00:00 +0200 + minerva (14.0.3) stable; urgency=medium * Bug fix: default zoom level on main map works even when x or y are undefined (#993) diff --git a/converter-sbml/src/main/java/lcsb/mapviewer/converter/model/sbml/SbmlBioEntityExporter.java b/converter-sbml/src/main/java/lcsb/mapviewer/converter/model/sbml/SbmlBioEntityExporter.java index fc092330c465da21d2fbccb554867c0cbb6d2ed0..262fcf0afd949791200a1ba54aadf8ed74b2b0ef 100644 --- a/converter-sbml/src/main/java/lcsb/mapviewer/converter/model/sbml/SbmlBioEntityExporter.java +++ b/converter-sbml/src/main/java/lcsb/mapviewer/converter/model/sbml/SbmlBioEntityExporter.java @@ -1,7 +1,8 @@ package lcsb.mapviewer.converter.model.sbml; -import java.awt.*; +import java.awt.Color; import java.util.*; +import java.util.regex.Pattern; import javax.xml.stream.XMLStreamException; @@ -10,10 +11,8 @@ import org.apache.logging.log4j.Logger; import org.sbml.jsbml.Model; import org.sbml.jsbml.ext.SBasePlugin; import org.sbml.jsbml.ext.layout.*; -import org.sbml.jsbml.ext.layout.Point; import org.sbml.jsbml.ext.multi.MultiModelPlugin; import org.sbml.jsbml.ext.render.*; -import org.sbml.jsbml.ext.render.Polygon; import lcsb.mapviewer.common.XmlParser; import lcsb.mapviewer.common.exception.InvalidStateException; @@ -49,12 +48,26 @@ public abstract class SbmlBioEntityExporter<T extends BioEntity, S extends org.s private boolean provideDefaults; + private Pattern sbmlValidIdMatcher; + + /** + * Map with elementId mapping for identifiers that cannot be used in sbml. + */ + private Map<String, String> correctedElementId = new HashMap<>(); + public SbmlBioEntityExporter(Model sbmlModel, lcsb.mapviewer.model.map.model.Model minervaModel, Collection<SbmlExtension> sbmlExtensions) { this.sbmlModel = sbmlModel; this.layout = getLayout(sbmlModel); this.minervaModel = minervaModel; this.sbmlExtensions.addAll(sbmlExtensions); + + String underscore = "_"; + String letter = "a-zA-Z"; + String digit = "0-9"; + String idChar = '[' + letter + digit + underscore + ']'; + sbmlValidIdMatcher = Pattern.compile("^[" + letter + underscore + "]" + idChar + '*'); + } public void exportElements() throws InconsistentModelException { @@ -63,10 +76,10 @@ public abstract class SbmlBioEntityExporter<T extends BioEntity, S extends org.s try { S sbmlElement = getSbmlElement(bioEntity); - if (sbmlElementByElementId.get(bioEntity.getElementId()) != null) { - throw new InconsistentModelException("More than one species with id: " + bioEntity.getElementId()); + if (sbmlElementByElementId.get(getElementId(bioEntity)) != null) { + throw new InconsistentModelException("More than one species with id: " + getElementId(bioEntity)); } - sbmlElementByElementId.put(bioEntity.getElementId(), sbmlElement); + sbmlElementByElementId.put(getElementId(bioEntity), sbmlElement); } catch (Exception e) { throw new InconsistentModelException(new ElementUtils().getElementTag(bioEntity) + "Problem with exporting bioEntity", e); @@ -76,7 +89,7 @@ public abstract class SbmlBioEntityExporter<T extends BioEntity, S extends org.s for (T bioEntity : speciesList) { try { AbstractReferenceGlyph elementGlyph = createGlyph(bioEntity); - sbmlGlyphByElementId.put(bioEntity.getElementId(), elementGlyph); + sbmlGlyphByElementId.put(getElementId(bioEntity), elementGlyph); } catch (Exception e) { throw new InconsistentModelException(new ElementUtils().getElementTag(bioEntity) + "Problem with exporting bioEntity", e); @@ -127,8 +140,8 @@ public abstract class SbmlBioEntityExporter<T extends BioEntity, S extends org.s protected abstract void assignLayoutToGlyph(T element, AbstractReferenceGlyph compartmentGlyph); protected AbstractReferenceGlyph createGlyph(T element) { - String sbmlElementId = sbmlElementByElementId.get(element.getElementId()).getId(); - String glyphId = element.getElementId(); + String sbmlElementId = sbmlElementByElementId.get(getElementId(element)).getId(); + String glyphId = getElementId(element); AbstractReferenceGlyph elementGlyph = createElementGlyph(sbmlElementId, glyphId); assignLayoutToGlyph(element, elementGlyph); return elementGlyph; @@ -403,4 +416,19 @@ public abstract class SbmlBioEntityExporter<T extends BioEntity, S extends org.s this.provideDefaults = provideDefaults; } + public String getElementId(BioEntity bioEntity) { + String result = bioEntity.getElementId(); + if (!sbmlValidIdMatcher.matcher(result).matches()) { + if (correctedElementId.get(result) == null) { + String newElementId = this.getClass().getSimpleName() + "_" + getNextId(); + logger.warn(new ElementUtils().getElementTag(bioEntity) + " Element id cannot be used in sbml. Changing to: " + + newElementId); + correctedElementId.put(result, newElementId); + } + result = correctedElementId.get(result); + } + + return result; + } + } diff --git a/converter-sbml/src/main/java/lcsb/mapviewer/converter/model/sbml/SbmlExporter.java b/converter-sbml/src/main/java/lcsb/mapviewer/converter/model/sbml/SbmlExporter.java index c86d28e26e932207c290bbd8d111af44ffdd79a5..784fefebd28dce33bfc08452315acb0f5e7317d7 100644 --- a/converter-sbml/src/main/java/lcsb/mapviewer/converter/model/sbml/SbmlExporter.java +++ b/converter-sbml/src/main/java/lcsb/mapviewer/converter/model/sbml/SbmlExporter.java @@ -73,7 +73,12 @@ public class SbmlExporter { */ protected SBMLDocument toSbmlDocument(lcsb.mapviewer.model.map.model.Model model) throws InconsistentModelException { SBMLDocument doc = new SBMLDocument(3, 2); - Model result = doc.createModel(model.getIdModel()); + Model result = doc.createModel(); + try { + result.setId(model.getIdModel()); + } catch (IllegalArgumentException e) { + logger.warn("Invalid model identifier: \"" + model.getIdModel() + "\". Ignoring."); + } result.setName(model.getName()); try { result.setNotes(NotesUtility.prepareEscapedXmlNotes(model.getNotes())); diff --git a/converter-sbml/src/main/java/lcsb/mapviewer/converter/model/sbml/compartment/SbmlCompartmentExporter.java b/converter-sbml/src/main/java/lcsb/mapviewer/converter/model/sbml/compartment/SbmlCompartmentExporter.java index d4d47b95c9de55ae5253051d4b1a90dfc657bcfb..f3a7ac1cd850507da72d95a28ec41e34adc049ea 100644 --- a/converter-sbml/src/main/java/lcsb/mapviewer/converter/model/sbml/compartment/SbmlCompartmentExporter.java +++ b/converter-sbml/src/main/java/lcsb/mapviewer/converter/model/sbml/compartment/SbmlCompartmentExporter.java @@ -34,7 +34,7 @@ public class SbmlCompartmentExporter extends SbmlElementExporter<Compartment, or result.addAll(getMinervaModel().getCompartments()); boolean defaultFound = false; for (Compartment compartment : result) { - if (compartment.getElementId().equals("default")) { + if (getElementId(compartment).equals("default")) { defaultFound = true; } } @@ -48,7 +48,7 @@ public class SbmlCompartmentExporter extends SbmlElementExporter<Compartment, or @Override public org.sbml.jsbml.Compartment createSbmlElement(Compartment element) throws InconsistentModelException { org.sbml.jsbml.Compartment result; - if (element == null || element.getElementId().equals("default")) { + if (element == null || getElementId(element).equals("default")) { result = getSbmlModel().createCompartment("default"); } else { result = getSbmlModel().createCompartment("comp_" + (getNextId())); @@ -77,7 +77,7 @@ public class SbmlCompartmentExporter extends SbmlElementExporter<Compartment, or @Override protected String getSbmlIdKey(Compartment compartment) { - if (compartment == null || compartment.getElementId().equals("default")) { + if (compartment == null || getElementId(compartment).equals("default")) { return "default"; } return compartment.getName(); @@ -131,7 +131,7 @@ public class SbmlCompartmentExporter extends SbmlElementExporter<Compartment, or @Override protected void assignLayoutToGlyph(Compartment element, AbstractReferenceGlyph speciesGlyph) { - if (element.getElementId().equals("default")) { + if (getElementId(element).equals("default")) { BoundingBox boundingBox = new BoundingBox(); boundingBox.setPosition(new Point(element.getX(), element.getY(), 0)); @@ -145,7 +145,7 @@ public class SbmlCompartmentExporter extends SbmlElementExporter<Compartment, or speciesGlyph.setBoundingBox(boundingBox); } else { super.assignLayoutToGlyph(element, speciesGlyph); - TextGlyph textGlyph = getLayout().createTextGlyph("text_" + element.getElementId(), element.getName()); + TextGlyph textGlyph = getLayout().createTextGlyph("text_" + getElementId(element), element.getName()); Point2D point = element.getNamePoint(); double width = element.getWidth() - (point.getX() - element.getX()); double height = element.getHeight() - (point.getY() - element.getY()); diff --git a/converter-sbml/src/main/java/lcsb/mapviewer/converter/model/sbml/reaction/SbmlReactionExporter.java b/converter-sbml/src/main/java/lcsb/mapviewer/converter/model/sbml/reaction/SbmlReactionExporter.java index d4184cb7b4e211f4b6370c32635b14e4976af7a3..ff142e888749871471836450813bd26f3f49a198 100644 --- a/converter-sbml/src/main/java/lcsb/mapviewer/converter/model/sbml/reaction/SbmlReactionExporter.java +++ b/converter-sbml/src/main/java/lcsb/mapviewer/converter/model/sbml/reaction/SbmlReactionExporter.java @@ -98,11 +98,12 @@ public class SbmlReactionExporter extends SbmlBioEntityExporter<Reaction, org.sb } private String getReactionId(Reaction reaction) { - int separatorIndex = reaction.getElementId().indexOf("__"); + String elementId = getElementId(reaction); + int separatorIndex = elementId.indexOf("__"); if (separatorIndex > 0) { - return reaction.getElementId().substring(0, separatorIndex); + return elementId.substring(0, separatorIndex); } - return reaction.getElementId(); + return elementId; } @Override @@ -124,19 +125,22 @@ public class SbmlReactionExporter extends SbmlBioEntityExporter<Reaction, org.sb result.setSBOTerm(sboTerm); } for (Product product : reaction.getProducts()) { - Species sbmlSymbol = speciesExporter.getSbmlElementByElementId(product.getElement().getElementId()); + Species sbmlSymbol = speciesExporter + .getSbmlElementByElementId(speciesExporter.getElementId(product.getElement())); SpeciesReference speciesReference = result.createProduct(sbmlSymbol); speciesReference.setConstant(false); speciesReferenceByReactionNode.put(product, speciesReference); } for (Reactant reactant : reaction.getReactants()) { - Species sbmlSymbol = speciesExporter.getSbmlElementByElementId(reactant.getElement().getElementId()); + Species sbmlSymbol = speciesExporter + .getSbmlElementByElementId(speciesExporter.getElementId(reactant.getElement())); SpeciesReference speciesReference = result.createReactant(sbmlSymbol); speciesReference.setConstant(false); speciesReferenceByReactionNode.put(reactant, speciesReference); } for (Modifier modifier : reaction.getModifiers()) { - Species sbmlSymbol = speciesExporter.getSbmlElementByElementId(modifier.getElement().getElementId()); + Species sbmlSymbol = speciesExporter + .getSbmlElementByElementId(speciesExporter.getElementId(modifier.getElement())); SimpleSpeciesReference speciesReference = result.createModifier(sbmlSymbol); speciesReferenceByReactionNode.put(modifier, speciesReference); String term = SBOTermModifierType.getTermByType(modifier.getClass()); @@ -151,8 +155,8 @@ public class SbmlReactionExporter extends SbmlBioEntityExporter<Reaction, org.sb } @Override - protected String getSbmlIdKey(Reaction element) { - return element.getClass().getSimpleName() + "\n" + element.getElementId(); + protected String getSbmlIdKey(Reaction reaction) { + return reaction.getClass().getSimpleName() + "\n" + getElementId(reaction); } @Override @@ -367,7 +371,8 @@ public class SbmlReactionExporter extends SbmlBioEntityExporter<Reaction, org.sb private SpeciesReferenceGlyph createNodeGlyph(ReactionGlyph reactionGlyph, ReactionNode node) { SpeciesReferenceGlyph reactantGlyph = reactionGlyph.createSpeciesReferenceGlyph("node_" + getNextId()); - reactantGlyph.setSpeciesGlyph(speciesExporter.getSbmlGlyphByElementId(node.getElement().getElementId()).getId()); + reactantGlyph.setSpeciesGlyph( + speciesExporter.getSbmlGlyphByElementId(speciesExporter.getElementId(node.getElement())).getId()); Curve curve = createCurve(node.getLine(), node instanceof Reactant); if (curve.getCurveSegmentCount() == 0) { logger.warn(new ElementUtils().getElementTag(node) + " Problematic line"); diff --git a/converter-sbml/src/main/java/lcsb/mapviewer/converter/model/sbml/species/SbmlSpeciesExporter.java b/converter-sbml/src/main/java/lcsb/mapviewer/converter/model/sbml/species/SbmlSpeciesExporter.java index a7cf1fdfd142f2eb56c6266bbb900b91d4b2b71d..deb5139c335c66672d9d3f2b927692e7ea96cfec 100644 --- a/converter-sbml/src/main/java/lcsb/mapviewer/converter/model/sbml/species/SbmlSpeciesExporter.java +++ b/converter-sbml/src/main/java/lcsb/mapviewer/converter/model/sbml/species/SbmlSpeciesExporter.java @@ -466,7 +466,7 @@ public class SbmlSpeciesExporter extends SbmlElementExporter<Species, org.sbml.j } } } - TextGlyph textGlyph = getLayout().createTextGlyph("text_" + element.getElementId(), element.getName()); + TextGlyph textGlyph = getLayout().createTextGlyph("text_" + getElementId(element), element.getName()); textGlyph .setBoundingBox(createBoundingBox(element.getX(), element.getY(), element.getZ() + 1, element.getWidth(), element.getHeight())); diff --git a/converter-sbml/src/test/java/lcsb/mapviewer/converter/model/sbml/SbmlExporterTest.java b/converter-sbml/src/test/java/lcsb/mapviewer/converter/model/sbml/SbmlExporterTest.java index 145e36e21ba025019f5d521ee7215783aa88f87d..bd052950b39ce7015886d0a9e4b5b243ea133c2b 100644 --- a/converter-sbml/src/test/java/lcsb/mapviewer/converter/model/sbml/SbmlExporterTest.java +++ b/converter-sbml/src/test/java/lcsb/mapviewer/converter/model/sbml/SbmlExporterTest.java @@ -3,7 +3,6 @@ package lcsb.mapviewer.converter.model.sbml; import static org.junit.Assert.*; import java.awt.Color; -import java.awt.geom.Line2D; import java.awt.geom.Point2D; import java.io.File; import java.lang.reflect.Modifier; @@ -17,7 +16,6 @@ import org.sbml.jsbml.SBMLDocument; import org.sbml.jsbml.ext.multi.*; import lcsb.mapviewer.common.Configuration; -import lcsb.mapviewer.common.comparator.LineComparator; import lcsb.mapviewer.common.comparator.ListComparator; import lcsb.mapviewer.converter.ConverterParams; import lcsb.mapviewer.converter.model.celldesigner.CellDesignerXmlParser; @@ -605,4 +603,80 @@ public class SbmlExporterTest extends SbmlTestFunctions { assertEquals(0, comparator.compare(model, originalModel)); } + @Test + public void testInvalidSpeciesId() throws Exception { + Model model = createEmptyModel(); + + GenericProtein p = createProtein(); + p.setElementId("a-b"); + model.addElement(p); + + String xml = exporter.toXml(model); + assertNotNull(xml); + + assertEquals(1, getWarnings().size()); + } + + @Test + public void testInvalidReactionId() throws Exception { + Model model = createEmptyModel(); + + Protein p1 = createProtein(); + Protein p2 = createProtein(); + Reaction p = createReaction(p1, p2); + p.setIdReaction("a-b"); + model.addElement(p1); + model.addElement(p2); + model.addReaction(p); + + String xml = exporter.toXml(model); + assertNotNull(xml); + assertEquals(1, getWarnings().size()); + + } + + @Test + public void testInvalidSpeciesIdInReaction() throws Exception { + Model model = createEmptyModel(); + + Protein p1 = createProtein(); + Protein p2 = createProtein(); + Reaction p = createReaction(p1, p2); + p1.setElementId("a-b"); + model.addElement(p1); + model.addElement(p2); + model.addReaction(p); + + String xml = exporter.toXml(model); + assertNotNull(xml); + assertEquals(1, getWarnings().size()); + + } + + @Test + public void testInvalidCompartmentId() throws Exception { + Model model = createEmptyModel(); + + Compartment p = createCompartment(); + p.setElementId("a-b"); + model.addElement(p); + + String xml = exporter.toXml(model); + assertNotNull(xml); + + assertEquals(1, getWarnings().size()); + } + + @Test + public void testInvalidModelId() throws Exception { + Model model = createEmptyModel(); + model.setIdModel("a-b"); + + String xml = exporter.toXml(model); + assertNotNull(xml); + assertEquals(1, getWarnings().size()); + + } + + } diff --git a/converter-sbml/src/test/java/lcsb/mapviewer/converter/model/sbml/SbmlTestFunctions.java b/converter-sbml/src/test/java/lcsb/mapviewer/converter/model/sbml/SbmlTestFunctions.java index 1be181333c72c3a208210ed372dedfd6e3843a24..c62b98cf280ea802be7e868d8273b1c0f0a9cb03 100644 --- a/converter-sbml/src/test/java/lcsb/mapviewer/converter/model/sbml/SbmlTestFunctions.java +++ b/converter-sbml/src/test/java/lcsb/mapviewer/converter/model/sbml/SbmlTestFunctions.java @@ -18,7 +18,8 @@ import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.LogEvent; import org.junit.*; import org.w3c.dom.Document; @@ -39,14 +40,14 @@ import lcsb.mapviewer.model.map.species.Element; import lcsb.mapviewer.model.map.species.GenericProtein; public class SbmlTestFunctions { + private Logger logger = LogManager.getLogger(); private static int identifierCounter = 0; + @Rule public UnitTestFailedWatcher unitTestFailedWatcher = new UnitTestFailedWatcher(); SbmlParser parser = new SbmlParser(); SbmlExporter exporter; - @SuppressWarnings("unused") - private Logger logger = Logger.getLogger(SbmlTestFunctions.class); private MinervaLoggerAppender appender; public SbmlTestFunctions() { diff --git a/rest-api/src/test/java/lcsb/mapviewer/api/convert/ConvertRestImplTest.java b/rest-api/src/test/java/lcsb/mapviewer/api/convert/ConvertRestImplTest.java index 0c69b7ff2f481c275206d3603374c22d233d1f55..5154d715356ebd0f9cb9e50f4615e7c63c8b78a9 100644 --- a/rest-api/src/test/java/lcsb/mapviewer/api/convert/ConvertRestImplTest.java +++ b/rest-api/src/test/java/lcsb/mapviewer/api/convert/ConvertRestImplTest.java @@ -211,4 +211,10 @@ public class ConvertRestImplTest extends RestTestFunctions { test2All(content, "SBGN-ML"); } + @Test + public void testNewtSbgn2All() throws Exception { + String content = readFile("testFiles/convert/newt.sbgn"); + test2All(content, "SBGN-ML"); + } + } diff --git a/rest-api/testFiles/convert/newt.sbgn b/rest-api/testFiles/convert/newt.sbgn new file mode 100644 index 0000000000000000000000000000000000000000..459ff42ac25c4ff91fd884d547f56c0629dff216 --- /dev/null +++ b/rest-api/testFiles/convert/newt.sbgn @@ -0,0 +1,9 @@ +<?xml version='1.0' encoding='UTF-8' standalone='yes'?> +<sbgn xmlns="http://sbgn.org/libsbgn/0.2"> + <map language="process description"> + <glyph id="nwtN_f034af08-f2b3-4912-aaa8-58b4f55f43b9" class="macromolecule"> + <label text="dffgdf"/> + <bbox x="707" y="292" w="60" h="30"/> + </glyph> + </map> +</sbgn> \ No newline at end of file