diff --git a/devenv.lock b/devenv.lock index 6a4523d..fdfb3f3 100644 --- a/devenv.lock +++ b/devenv.lock @@ -3,10 +3,11 @@ "devenv": { "locked": { "dir": "src/modules", - "lastModified": 1761922975, + "lastModified": 1781800860, + "narHash": "sha256-LrEo0eC5ckMvjpBRCuk5q5/vjItKlxnb4n/clHNRZlk=", "owner": "cachix", "repo": "devenv", - "rev": "c9f0b47815a4895fadac87812de8a4de27e0ace1", + "rev": "d59d872d80876d9eeb3e214d3b088bc4a14a9c4f", "type": "github" }, "original": { @@ -16,88 +17,49 @@ "type": "github" } }, - "flake-compat": { - "flake": false, - "locked": { - "lastModified": 1761588595, - "owner": "edolstra", - "repo": "flake-compat", - "rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, - "git-hooks": { + "nixpkgs": { "inputs": { - "flake-compat": "flake-compat", - "gitignore": "gitignore", - "nixpkgs": [ - "nixpkgs" - ] + "nixpkgs-src": "nixpkgs-src" }, "locked": { - "lastModified": 1760663237, + "lastModified": 1781620901, + "narHash": "sha256-UF6scQlG+6lRkZBUpn/3KNavhOo5G8kDWhjVHcno8uc=", "owner": "cachix", - "repo": "git-hooks.nix", - "rev": "ca5b894d3e3e151ffc1db040b6ce4dcc75d31c37", + "repo": "devenv-nixpkgs", + "rev": "2df109b343d3c68efd752e32a444a1d9b9f89afa", "type": "github" }, "original": { "owner": "cachix", - "repo": "git-hooks.nix", - "type": "github" - } - }, - "gitignore": { - "inputs": { - "nixpkgs": [ - "git-hooks", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1709087332, - "owner": "hercules-ci", - "repo": "gitignore.nix", - "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "gitignore.nix", + "ref": "rolling", + "repo": "devenv-nixpkgs", "type": "github" } }, - "nixpkgs": { + "nixpkgs-src": { + "flake": false, "locked": { - "lastModified": 1761313199, - "owner": "cachix", - "repo": "devenv-nixpkgs", - "rev": "d1c30452ebecfc55185ae6d1c983c09da0c274ff", + "lastModified": 1781454065, + "narHash": "sha256-d2xfDjnfRuf/xYGdu9VVRHiav/2w5hDL/5cw2TuVAXw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9eac87a12312b8f60dd52e1c6e1a265f6fc7f5fc", "type": "github" }, "original": { - "owner": "cachix", - "ref": "rolling", - "repo": "devenv-nixpkgs", + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { "devenv": "devenv", - "git-hooks": "git-hooks", - "nixpkgs": "nixpkgs", - "pre-commit-hooks": [ - "git-hooks" - ] + "nixpkgs": "nixpkgs" } } }, "root": "root", "version": 7 -} +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index d69a0e5..0e8a1a6 100644 --- a/pom.xml +++ b/pom.xml @@ -108,7 +108,7 @@ org.immutables value - 2.12.1 + 2.12.2 provided diff --git a/src/main/java/pl/poznan/put/rnapdbee/engine/shared/basepair/boundary/BasePairAnalyzer.java b/src/main/java/pl/poznan/put/rnapdbee/engine/shared/basepair/boundary/BasePairAnalyzer.java index f18ec80..89f8626 100644 --- a/src/main/java/pl/poznan/put/rnapdbee/engine/shared/basepair/boundary/BasePairAnalyzer.java +++ b/src/main/java/pl/poznan/put/rnapdbee/engine/shared/basepair/boundary/BasePairAnalyzer.java @@ -8,6 +8,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -41,6 +42,7 @@ import pl.poznan.put.rnapdbee.engine.shared.multiplet.MultipletSet; import pl.poznan.put.structure.AnalyzedBasePair; import pl.poznan.put.structure.ImmutableAnalyzedBasePair; +import pl.poznan.put.structure.ImmutableBasePair; // TODO: WebFlux would be really efficient with the 3D->multi 2D analysis as we there perform multiple calls to the // adapters, it could be done in parallel and then joined up after each call is performed. We would save a tone of @@ -124,7 +126,12 @@ protected BasePairAnalysis performPostAnalysisOnResponseFromAdapter( .filter(pair -> pair.isCanonical(structureModel)) .map(pair -> BasePairDTO.ofBasePairDTOWithNameFromMap(pair, pairIdentifiersWithTheirShortNames)) - .map(basePair -> ImmutableAnalyzedBasePair.of(basePair.toBasePair(structureModel)) + .map(basePair -> { + ImmutableBasePair bp = basePair.toBasePair(structureModel); + if (bp == null) { + return null; + } + return ImmutableAnalyzedBasePair.of(bp) .withInteractionType(InteractionType.BASE_BASE) .withSaenger(basePair.getSaengerType() != null ? SaengerType.mapToBioCommonsForm( @@ -136,13 +143,20 @@ protected BasePairAnalysis performPostAnalysisOnResponseFromAdapter( : LeontisWesthof.UNKNOWN) .withBph(BPh.UNKNOWN) .withBr(BR.UNKNOWN) - .withStackingTopology(StackingTopology.UNKNOWN)) + .withStackingTopology(StackingTopology.UNKNOWN); + }) + .filter(Objects::nonNull) .collect(Collectors.toList()); List nonCanonical = responseFromAdapter.getBasePairs().stream() .filter(pair -> !pair.isCanonical(structureModel)) .map(pair -> BasePairDTO.ofBasePairDTOWithNameFromMap(pair, pairIdentifiersWithTheirShortNames)) - .map(basePair -> ImmutableAnalyzedBasePair.of(basePair.toBasePair(structureModel)) + .map(basePair -> { + ImmutableBasePair bp = basePair.toBasePair(structureModel); + if (bp == null) { + return null; + } + return ImmutableAnalyzedBasePair.of(bp) .withInteractionType(InteractionType.BASE_BASE) .withSaenger(basePair.getSaengerType() != null ? SaengerType.mapToBioCommonsForm( @@ -154,56 +168,91 @@ protected BasePairAnalysis performPostAnalysisOnResponseFromAdapter( : LeontisWesthof.UNKNOWN) .withBph(BPh.UNKNOWN) .withBr(BR.UNKNOWN) - .withStackingTopology(StackingTopology.UNKNOWN)) + .withStackingTopology(StackingTopology.UNKNOWN); + }) + .filter(Objects::nonNull) .collect(Collectors.toList()); List stackings = responseFromAdapter.getStackings().stream() .map(pair -> BasePairDTO.ofBasePairDTOWithNameFromMap(pair, pairIdentifiersWithTheirShortNames)) - .map(basePair -> ImmutableAnalyzedBasePair.of(basePair.toBasePair(structureModel)) + .map(basePair -> { + ImmutableBasePair bp = basePair.toBasePair(structureModel); + if (bp == null) { + return null; + } + return ImmutableAnalyzedBasePair.of(bp) .withInteractionType(InteractionType.STACKING) .withSaenger(Saenger.UNKNOWN) .withLeontisWesthof(LeontisWesthof.UNKNOWN) .withBph(BPh.UNKNOWN) .withBr(BR.UNKNOWN) - .withStackingTopology(mapToBioCommonsForm(basePair.getTopology()))) + .withStackingTopology(mapToBioCommonsForm(basePair.getTopology())); + }) + .filter(Objects::nonNull) .collect(Collectors.toList()); List basePhosphate = responseFromAdapter.getBasePhosphateInteractions().stream() .map(pair -> BasePairDTO.ofBasePairDTOWithNameFromMap(pair, pairIdentifiersWithTheirShortNames)) - .map(basePair -> ImmutableAnalyzedBasePair.of(basePair.toBasePair(structureModel)) + .map(basePair -> { + ImmutableBasePair bp = basePair.toBasePair(structureModel); + if (bp == null) { + return null; + } + return ImmutableAnalyzedBasePair.of(bp) .withInteractionType(InteractionType.BASE_PHOSPHATE) .withSaenger(Saenger.UNKNOWN) .withLeontisWesthof(LeontisWesthof.UNKNOWN) .withBph(BasePhosphateType.mapToBioCommonsForm(basePair.getBph())) .withBr(BR.UNKNOWN) - .withStackingTopology(StackingTopology.UNKNOWN)) + .withStackingTopology(StackingTopology.UNKNOWN); + }) + .filter(Objects::nonNull) .collect(Collectors.toList()); List baseRibose = responseFromAdapter.getBaseRiboseInteractions().stream() .map(pair -> BasePairDTO.ofBasePairDTOWithNameFromMap(pair, pairIdentifiersWithTheirShortNames)) - .map(basePair -> ImmutableAnalyzedBasePair.of(basePair.toBasePair(structureModel)) + .map(basePair -> { + ImmutableBasePair bp = basePair.toBasePair(structureModel); + if (bp == null) { + return null; + } + return ImmutableAnalyzedBasePair.of(bp) .withInteractionType(InteractionType.BASE_RIBOSE) .withSaenger(Saenger.UNKNOWN) .withLeontisWesthof(LeontisWesthof.UNKNOWN) .withBph(BPh.UNKNOWN) .withBr(BaseRiboseType.mapToBioCommonsForm(basePair.getBr())) - .withStackingTopology(StackingTopology.UNKNOWN)) + .withStackingTopology(StackingTopology.UNKNOWN); + }) + .filter(Objects::nonNull) .collect(Collectors.toList()); List otherInteractions = responseFromAdapter.getOther().stream() .map(pair -> BasePairDTO.ofBasePairDTOWithNameFromMap(pair, pairIdentifiersWithTheirShortNames)) - .map(basePair -> ImmutableAnalyzedBasePair.of(basePair.toBasePair(structureModel)) + .map(basePair -> { + ImmutableBasePair bp = basePair.toBasePair(structureModel); + if (bp == null) { + return null; + } + return ImmutableAnalyzedBasePair.of(bp) .withInteractionType(InteractionType.OTHER) .withSaenger(Saenger.UNKNOWN) .withLeontisWesthof(LeontisWesthof.UNKNOWN) .withBph(BPh.UNKNOWN) .withBr(BR.UNKNOWN) - .withStackingTopology(StackingTopology.UNKNOWN)) + .withStackingTopology(StackingTopology.UNKNOWN); + }) + .filter(Objects::nonNull) .collect(Collectors.toList()); List interStrand = responseFromAdapter.getBasePairs().stream() .map(pair -> BasePairDTO.ofBasePairDTOWithNameFromMap(pair, pairIdentifiersWithTheirShortNames)) - .map(basePair -> ImmutableAnalyzedBasePair.of(basePair.toBasePair(structureModel)) + .map(basePair -> { + ImmutableBasePair bp = basePair.toBasePair(structureModel); + if (bp == null) { + return null; + } + return ImmutableAnalyzedBasePair.of(bp) .withInteractionType(InteractionType.BASE_BASE) .withSaenger(basePair.getSaengerType() != null ? SaengerType.mapToBioCommonsForm( @@ -215,7 +264,9 @@ protected BasePairAnalysis performPostAnalysisOnResponseFromAdapter( : LeontisWesthof.UNKNOWN) .withBph(BPh.UNKNOWN) .withBr(BR.UNKNOWN) - .withStackingTopology(StackingTopology.UNKNOWN)) + .withStackingTopology(StackingTopology.UNKNOWN); + }) + .filter(Objects::nonNull) .filter(basePair -> !basePair.basePair() .left() .chainIdentifier() diff --git a/src/main/java/pl/poznan/put/rnapdbee/engine/shared/basepair/domain/BasePairDTO.java b/src/main/java/pl/poznan/put/rnapdbee/engine/shared/basepair/domain/BasePairDTO.java index 37f9f6b..d68f0c5 100644 --- a/src/main/java/pl/poznan/put/rnapdbee/engine/shared/basepair/domain/BasePairDTO.java +++ b/src/main/java/pl/poznan/put/rnapdbee/engine/shared/basepair/domain/BasePairDTO.java @@ -1,6 +1,8 @@ package pl.poznan.put.rnapdbee.engine.shared.basepair.domain; import com.fasterxml.jackson.annotation.JsonProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import pl.poznan.put.pdb.PdbNamedResidueIdentifier; import pl.poznan.put.pdb.analysis.PdbModel; import pl.poznan.put.rnapdbee.engine.shared.basepair.boundary.ChainNumberKey; @@ -19,6 +21,7 @@ */ public class BasePairDTO { + private static final Logger LOGGER = LoggerFactory.getLogger(BasePairDTO.class); private static final Set CANONICAL_ONE_LETTER_NAME_SORTED_PAIRS = new HashSet<>(Arrays.asList("AU", "GU", "CG")); @JsonProperty("nt1") @@ -122,6 +125,9 @@ public static BasePairDTO ofBasePairDTOWithNameFromMap(BasePairDTO basePairDTO, public ImmutableBasePair toBasePair(PdbModel pdbModel) { PdbNamedResidueIdentifier left = mapResidueToPdbNamedResidueIdentifier(nt1, pdbModel); PdbNamedResidueIdentifier right = mapResidueToPdbNamedResidueIdentifier(nt2, pdbModel); + if (left == null || right == null) { + return null; + } return ImmutableBasePair.of(left, right); } @@ -130,14 +136,15 @@ private PdbNamedResidueIdentifier mapResidueToPdbNamedResidueIdentifier(Residue return pdbModel.findResidue(residue).namedResidueIdentifier(); } - throw new IllegalStateException(String.format( - "Residue not found in model: chain='%s', number=%d, icode=%s, name='%s'. " + LOGGER.warn( + "Residue not found in model: chain='{}', number={}, icode={}, name='{}'. " + "This may indicate a mismatch between adapter response and parsed structure " + "(e.g., chain identifier normalization issue).", residue.chainIdentifier(), residue.residueNumber(), residue.insertionCode().orElse("(none)"), - residue.getAuth().getName())); + residue.getAuth().getName()); + return null; } public boolean isCanonical(PdbModel pdbModel) { @@ -147,6 +154,9 @@ public boolean isCanonical(PdbModel pdbModel) { if (leontisWesthofType == LeontisWesthofType.CWW) { PdbNamedResidueIdentifier left = mapResidueToPdbNamedResidueIdentifier(nt1, pdbModel); PdbNamedResidueIdentifier right = mapResidueToPdbNamedResidueIdentifier(nt2, pdbModel); + if (left == null || right == null) { + return false; + } String sequence = Stream.of(left.oneLetterName(), right.oneLetterName()) .map(c -> Character.toString(c)) .map(String::toUpperCase) diff --git a/src/main/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/DrawerManager.java b/src/main/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/DrawerManager.java index f83711f..2761e68 100644 --- a/src/main/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/DrawerManager.java +++ b/src/main/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/DrawerManager.java @@ -126,54 +126,47 @@ public ImageInformationOutput drawCanonicalAndNonCanonical( LOGGER.info(String.format( "Drawing non-canonical image started with drawer: %s, structure: %s, sequence: %s", visualizationTool, dotBracket.structure(), dotBracket.sequence())); - final boolean onlyDotsMinuses = StringUtils.containsOnly(dotBracket.structure(), ".-"); - final boolean isMainToolVarna = visualizationTool == VisualizationTool.VARNA; final SecondaryStructureDrawer mainDrawer = drawerFactory.loadDrawer(visualizationTool); - if (!onlyDotsMinuses || isMainToolVarna) { - try { - final SVGDocument svgDocument = - mainDrawer.drawSecondaryStructure(dotBracket, structureModel, nonCanonicalBasePairs, stacking); - final byte[] svgDocumentAsByteArray = SVGHelper.export(svgDocument, Format.SVG); - return new ImageInformationOutput() - .withSuccessfulDrawer(visualizationTool) - .withFailedDrawer(VisualizationTool.NONE) - .withDrawingResult(DrawingResult.DONE_BY_MAIN_DRAWER) - .withSvgFile(svgDocumentAsByteArray); - } catch (final VisualizationException | IOException e) { - LOGGER.error( - String.format( - "Failed drawing non-canonical image with drawer: %s, structure: %s, sequence: %s", - visualizationTool, dotBracket.structure(), dotBracket.sequence()), - e); - } + try { + final SVGDocument svgDocument = + mainDrawer.drawSecondaryStructure(dotBracket, structureModel, nonCanonicalBasePairs, stacking); + final byte[] svgDocumentAsByteArray = SVGHelper.export(svgDocument, Format.SVG); + return new ImageInformationOutput() + .withSuccessfulDrawer(visualizationTool) + .withFailedDrawer(VisualizationTool.NONE) + .withDrawingResult(DrawingResult.DONE_BY_MAIN_DRAWER) + .withSvgFile(svgDocumentAsByteArray); + } catch (final VisualizationException | IOException e) { + LOGGER.error( + String.format( + "Failed drawing non-canonical image with drawer: %s, structure: %s, sequence: %s", + visualizationTool, dotBracket.structure(), dotBracket.sequence()), + e); } - final boolean isBackupToolVarna = visualizationTool.getBackupVisualizationTool() == VisualizationTool.VARNA; final VisualizationTool backupVisualizationTool = visualizationTool.getBackupVisualizationTool(); final SecondaryStructureDrawer backupDrawer = drawerFactory.loadDrawer(visualizationTool.getBackupVisualizationTool()); - if (!onlyDotsMinuses || isBackupToolVarna) { - try { - LOGGER.info(String.format( - "Drawing non-canonical image started with backup drawer: %s, structure: %s, sequence: %s", - backupVisualizationTool, dotBracket.structure(), dotBracket.sequence())); - final SVGDocument svgDocument = backupDrawer.drawSecondaryStructure( - dotBracket, structureModel, nonCanonicalBasePairs, stacking); - final byte[] svgDocumentAsByteArray = SVGHelper.export(svgDocument, Format.SVG); - return new ImageInformationOutput() - .withSuccessfulDrawer(backupVisualizationTool) - .withFailedDrawer(visualizationTool) - .withDrawingResult(DrawingResult.DONE_BY_BACKUP_DRAWER) - .withSvgFile(svgDocumentAsByteArray); - } catch (final VisualizationException | IOException e) { - LOGGER.error( - String.format( - "Drawing non-canonical image with drawer: %s, structure: %s, sequence: %s", - visualizationTool, dotBracket.structure(), dotBracket.sequence()), - e); - } + try { + LOGGER.info(String.format( + "Drawing non-canonical image started with backup drawer: %s, structure: %s, sequence: %s", + backupVisualizationTool, dotBracket.structure(), dotBracket.sequence())); + final SVGDocument svgDocument = backupDrawer.drawSecondaryStructure( + dotBracket, structureModel, nonCanonicalBasePairs, stacking); + final byte[] svgDocumentAsByteArray = SVGHelper.export(svgDocument, Format.SVG); + return new ImageInformationOutput() + .withSuccessfulDrawer(backupVisualizationTool) + .withFailedDrawer(visualizationTool) + .withDrawingResult(DrawingResult.DONE_BY_BACKUP_DRAWER) + .withSvgFile(svgDocumentAsByteArray); + } catch (final VisualizationException | IOException e) { + LOGGER.error( + String.format( + "Drawing non-canonical image with drawer: %s, structure: %s, sequence: %s", + visualizationTool, dotBracket.structure(), dotBracket.sequence()), + e); } return ImageInformationOutput.FAILED_INSTANCE; diff --git a/src/main/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/drawer/ExternalDrawerVarna.java b/src/main/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/drawer/ExternalDrawerVarna.java index fe38842..d09682b 100644 --- a/src/main/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/drawer/ExternalDrawerVarna.java +++ b/src/main/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/drawer/ExternalDrawerVarna.java @@ -14,14 +14,18 @@ import pl.poznan.put.rnapdbee.engine.shared.image.logic.drawer.model.Stacking; import pl.poznan.put.rnapdbee.engine.shared.image.logic.drawer.model.StructureData; import pl.poznan.put.structure.ClassifiedBasePair; +import pl.poznan.put.structure.DotBracketSymbol; import pl.poznan.put.structure.formats.DotBracket; import pl.poznan.put.structure.formats.DotBracketFromPdb; +import pl.poznan.put.structure.formats.Strand; import pl.poznan.put.utility.svg.SVGHelper; import java.awt.*; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; /** @@ -74,6 +78,17 @@ private static StructureData createStructureData( List stackingInteractions) { var nucleotides = new ArrayList(); var symbols = combinedStrand.symbols(); + + Map symbolToStrandPosition = new HashMap<>(); + if (!(combinedStrand instanceof DotBracketFromPdb)) { + for (Strand strand : combinedStrand.strands()) { + int position = 1; + for (DotBracketSymbol strandSymbol : strand.symbols()) { + symbolToStrandPosition.put(strandSymbol, position++); + } + } + } + for (int i = 0; i < symbols.size(); i++) { var symbol = symbols.get(i); var nucleotide = new Nucleotide(); @@ -82,7 +97,7 @@ private static StructureData createStructureData( nucleotide.number = ((DotBracketFromPdb) combinedStrand).identifier(symbol).residueNumber(); } else { - nucleotide.number = i + 1; + nucleotide.number = symbolToStrandPosition.get(symbol); } nucleotide.character = String.valueOf(symbol.sequence()); if (symbol.isMissing()) { @@ -155,12 +170,27 @@ private static StructureData createStructureData( var structureData = new StructureData(); structureData.nucleotides = nucleotides; structureData.basePairs = basePairs; + structureData.strandBreaks = computeStrandBreaks(combinedStrand); // TODO: disable stacking visualization for now // structureData.stackings = stackings; structureData.drawingAlgorithm = "NAVIEW"; return structureData; } + private static List computeStrandBreaks(DotBracket combinedStrand) { + var strands = combinedStrand.strands(); + if (strands.size() < 2) { + return Collections.emptyList(); + } + var breaks = new ArrayList(); + int offset = 0; + for (int s = 0; s < strands.size() - 1; s++) { + offset += strands.get(s).symbols().size(); + breaks.add(offset - 1); + } + return breaks; + } + private static ModeleBP.Edge map(NucleobaseEdge edge) { switch (edge) { case WATSON_CRICK: diff --git a/src/main/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/drawer/model/StructureData.java b/src/main/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/drawer/model/StructureData.java index 7f31e2a..6e93e87 100644 --- a/src/main/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/drawer/model/StructureData.java +++ b/src/main/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/drawer/model/StructureData.java @@ -20,6 +20,9 @@ public class StructureData { @JsonProperty("drawingAlgorithm") public String drawingAlgorithm; + @JsonProperty("strandBreaks") + public List strandBreaks; + @Override public String toString() { return "StructureData{" @@ -30,6 +33,8 @@ public String toString() { + " items, drawingAlgorithm='" + drawingAlgorithm + '\'' + + ", strandBreaks=" + + (strandBreaks != null ? strandBreaks : "[]") + ", stackings=" + (stackings != null ? stackings.size() : 0) + " items" diff --git a/src/main/java/pl/poznan/put/rnapdbee/engine/shared/parser/SecondaryFileParser.java b/src/main/java/pl/poznan/put/rnapdbee/engine/shared/parser/SecondaryFileParser.java index 2a1ce4b..ffeca6c 100644 --- a/src/main/java/pl/poznan/put/rnapdbee/engine/shared/parser/SecondaryFileParser.java +++ b/src/main/java/pl/poznan/put/rnapdbee/engine/shared/parser/SecondaryFileParser.java @@ -13,10 +13,15 @@ import pl.poznan.put.structure.formats.DefaultDotBracket; import pl.poznan.put.structure.formats.DotBracket; +import java.util.Arrays; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + @Component public class SecondaryFileParser { private static final Logger LOGGER = LoggerFactory.getLogger(SecondaryFileParser.class); + private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+"); private final Converter converter; public DotBracket parseSecondaryFile(String content, InputType inputType, boolean removeIsolated) { @@ -38,9 +43,10 @@ public DotBracket parseSecondaryFile(String content, InputType inputType, boolea private DotBracket convertBpSeqIntoDotBracket(String content, boolean removeIsolated) { try { + String normalizedContent = normalizeSecondaryFileContent(content); BpSeq bpSeq = removeIsolated - ? BpSeq.fromString(content).withoutIsolatedPairs() - : BpSeq.fromString(content); + ? BpSeq.fromString(normalizedContent).withoutIsolatedPairs() + : BpSeq.fromString(normalizedContent); Ct ct = Ct.fromBpSeq(bpSeq); return DefaultDotBracket.copyWithStrands(converter.convert(bpSeq), ct); } catch (IllegalArgumentException exception) { @@ -52,9 +58,10 @@ private DotBracket convertBpSeqIntoDotBracket(String content, boolean removeIsol private DotBracket convertCtIntoDotBracket(String content, boolean removeIsolated) { try { + String normalizedContent = normalizeSecondaryFileContent(content); Ct ct = removeIsolated - ? Ct.fromString(content).withoutIsolatedPairs() - : Ct.fromString(content); + ? Ct.fromString(normalizedContent).withoutIsolatedPairs() + : Ct.fromString(normalizedContent); BpSeq bpSeq = BpSeq.fromCt(ct); return DefaultDotBracket.copyWithStrands(converter.convert(bpSeq), ct); } catch (IllegalArgumentException exception) { @@ -79,6 +86,14 @@ private DotBracket readDotBracketContent(String content, boolean removeIsolated) } } + private String normalizeSecondaryFileContent(String content) { + return Arrays.stream(content.split("\\R")) + .map(String::trim) + .filter(line -> !line.isEmpty()) + .map(line -> WHITESPACE_PATTERN.matcher(line).replaceAll(" ")) + .collect(Collectors.joining("\n")); + } + @Autowired public SecondaryFileParser(Converter converter) { this.converter = converter; diff --git a/src/test/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/drawer/ExternalDrawerVarnaTest.java b/src/test/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/drawer/ExternalDrawerVarnaTest.java new file mode 100644 index 0000000..dad3fca --- /dev/null +++ b/src/test/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/drawer/ExternalDrawerVarnaTest.java @@ -0,0 +1,83 @@ +package pl.poznan.put.rnapdbee.engine.shared.image.logic.drawer; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.StringReader; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.batik.anim.dom.SAXSVGDocumentFactory; +import org.apache.batik.util.XMLResourceDescriptor; +import org.junit.jupiter.api.Test; +import org.w3c.dom.svg.SVGDocument; +import pl.poznan.put.rnapdbee.engine.shared.image.logic.drawer.model.StructureData; +import pl.poznan.put.structure.formats.DefaultDotBracket; +import pl.poznan.put.structure.formats.DotBracket; + +class ExternalDrawerVarnaTest { + + private static SVGDocument emptySvg() throws Exception { + String svg = ""; + SAXSVGDocumentFactory factory = + new SAXSVGDocumentFactory(XMLResourceDescriptor.getXMLParserClassName()); + return factory.createSVGDocument(null, new StringReader(svg)); + } + + private static class FakeVarnaTzClient extends VarnaTzClient { + private StructureData lastStructureData; + + FakeVarnaTzClient() { + super("http://localhost"); + } + + @Override + public SVGDocument draw(StructureData structureData) { + this.lastStructureData = structureData; + try { + return emptySvg(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + StructureData getLastStructureData() { + return lastStructureData; + } + } + + @Test + void shouldRestartNumberingForEachStrand() throws Exception { + FakeVarnaTzClient client = new FakeVarnaTzClient(); + ExternalDrawerVarna drawer = new ExternalDrawerVarna(client); + DotBracket dotBracket = DefaultDotBracket.fromString( + ">strand_A\nAAAGGGAAA\n...(((...\n>strand_B\nAAACCCAAA\n...)))...\n"); + + drawer.drawSecondaryStructure(dotBracket, Collections.emptyList()); + + StructureData structureData = client.getLastStructureData(); + List numbers = structureData.nucleotides.stream() + .map(nucleotide -> nucleotide.number) + .collect(Collectors.toList()); + + assertEquals(18, numbers.size()); + assertEquals(List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 9), numbers); + assertEquals(List.of(8), structureData.strandBreaks); + } + + @Test + void shouldUseContinuousNumberingForSingleStrand() throws Exception { + FakeVarnaTzClient client = new FakeVarnaTzClient(); + ExternalDrawerVarna drawer = new ExternalDrawerVarna(client); + DotBracket dotBracket = DefaultDotBracket.fromString(">strand\nAAAGGGUUU\n...((()))\n"); + + drawer.drawSecondaryStructure(dotBracket, Collections.emptyList()); + + StructureData structureData = client.getLastStructureData(); + List numbers = structureData.nucleotides.stream() + .map(nucleotide -> nucleotide.number) + .collect(Collectors.toList()); + + assertEquals(List.of(1, 2, 3, 4, 5, 6, 7, 8, 9), numbers); + assertEquals(Collections.emptyList(), structureData.strandBreaks); + } +} diff --git a/src/test/java/pl/poznan/put/rnapdbee/engine/shared/parser/SecondaryFileParserTest.java b/src/test/java/pl/poznan/put/rnapdbee/engine/shared/parser/SecondaryFileParserTest.java new file mode 100644 index 0000000..045afff --- /dev/null +++ b/src/test/java/pl/poznan/put/rnapdbee/engine/shared/parser/SecondaryFileParserTest.java @@ -0,0 +1,56 @@ +package pl.poznan.put.rnapdbee.engine.shared.parser; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import pl.poznan.put.rnapdbee.engine.shared.domain.InputType; +import pl.poznan.put.structure.formats.BpSeq; +import pl.poznan.put.structure.formats.Converter; +import pl.poznan.put.structure.formats.DefaultDotBracket; +import pl.poznan.put.structure.formats.DotBracket; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class SecondaryFileParserTest { + + @Mock + private Converter converter; + + @InjectMocks + private SecondaryFileParser secondaryFileParser; + + @Test + void shouldParseBpSeqWithRedundantSpacesAndEmptyLines() { + String content = "1 A 4\n2 U 3\n\n 3 G 2\n4 C 1\n"; + DotBracket conversionResult = mock(DotBracket.class); + when(conversionResult.sequence()).thenReturn("AAAA"); + when(conversionResult.structure()).thenReturn("()()"); + when(converter.convert(any(BpSeq.class))).thenReturn(conversionResult); + + DotBracket result = secondaryFileParser.parseSecondaryFile(content, InputType.BPSEQ, false); + + assertThat(result).isNotNull(); + verify(converter).convert(any(BpSeq.class)); + } + + @Test + void shouldParseCtWithRedundantSpacesAndEmptyLines() { + String content = " 4 some header\n1 A 0 2 4 1\n 2 U 1 3 3 2\n\n3 G 2 4 2 3\n4 C 3 0 1 4\n"; + DotBracket conversionResult = mock(DotBracket.class); + when(conversionResult.sequence()).thenReturn("AAAA"); + when(conversionResult.structure()).thenReturn("()()"); + when(converter.convert(any(BpSeq.class))).thenReturn(conversionResult); + + DotBracket result = secondaryFileParser.parseSecondaryFile(content, InputType.CT, false); + + assertThat(result).isNotNull(); + verify(converter).convert(any(BpSeq.class)); + } +}