From 515c623529daf9c7fb643a94fa6e55a374ed7ad4 Mon Sep 17 00:00:00 2001 From: josemduarte Date: Wed, 12 Nov 2025 16:56:00 -0800 Subject: [PATCH 01/67] [maven-release-plugin] prepare for next development iteration --- biojava-aa-prop/pom.xml | 6 +++--- biojava-alignment/pom.xml | 4 ++-- biojava-core/pom.xml | 2 +- biojava-genome/pom.xml | 6 +++--- biojava-integrationtest/pom.xml | 4 ++-- biojava-modfinder/pom.xml | 4 ++-- biojava-ontology/pom.xml | 2 +- biojava-protein-comparison-tool/pom.xml | 10 +++++----- biojava-protein-disorder/pom.xml | 4 ++-- biojava-structure-gui/pom.xml | 6 +++--- biojava-structure/pom.xml | 6 +++--- biojava-survival/pom.xml | 2 +- biojava-ws/pom.xml | 4 ++-- pom.xml | 4 ++-- 14 files changed, 32 insertions(+), 32 deletions(-) diff --git a/biojava-aa-prop/pom.xml b/biojava-aa-prop/pom.xml index 0c1016c3b6..fd7bfcd086 100644 --- a/biojava-aa-prop/pom.xml +++ b/biojava-aa-prop/pom.xml @@ -2,7 +2,7 @@ biojava org.biojava - 7.2.3 + 7.2.4-SNAPSHOT 4.0.0 biojava-aa-prop @@ -70,12 +70,12 @@ org.biojava biojava-core - 7.2.3 + 7.2.4-SNAPSHOT org.biojava biojava-structure - 7.2.3 + 7.2.4-SNAPSHOT diff --git a/biojava-alignment/pom.xml b/biojava-alignment/pom.xml index 7bb6e3b2ee..3886b68bd1 100644 --- a/biojava-alignment/pom.xml +++ b/biojava-alignment/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.3 + 7.2.4-SNAPSHOT biojava-alignment biojava-alignment @@ -47,7 +47,7 @@ org.biojava biojava-core - 7.2.3 + 7.2.4-SNAPSHOT compile diff --git a/biojava-core/pom.xml b/biojava-core/pom.xml index 890ce360bc..6b3eb9d8ae 100644 --- a/biojava-core/pom.xml +++ b/biojava-core/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.3 + 7.2.4-SNAPSHOT 4.0.0 biojava-core diff --git a/biojava-genome/pom.xml b/biojava-genome/pom.xml index 79608afaf4..29479bcfdf 100644 --- a/biojava-genome/pom.xml +++ b/biojava-genome/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.3 + 7.2.4-SNAPSHOT 4.0.0 biojava-genome @@ -70,13 +70,13 @@ org.biojava biojava-core - 7.2.3 + 7.2.4-SNAPSHOT compile org.biojava biojava-alignment - 7.2.3 + 7.2.4-SNAPSHOT compile diff --git a/biojava-integrationtest/pom.xml b/biojava-integrationtest/pom.xml index 41d9be230c..4646e96277 100644 --- a/biojava-integrationtest/pom.xml +++ b/biojava-integrationtest/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.3 + 7.2.4-SNAPSHOT biojava-integrationtest jar @@ -40,7 +40,7 @@ org.biojava biojava-structure - 7.2.3 + 7.2.4-SNAPSHOT diff --git a/biojava-modfinder/pom.xml b/biojava-modfinder/pom.xml index 876203f207..953feeed09 100644 --- a/biojava-modfinder/pom.xml +++ b/biojava-modfinder/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.3 + 7.2.4-SNAPSHOT biojava-modfinder biojava-modfinder @@ -31,7 +31,7 @@ org.biojava biojava-structure - 7.2.3 + 7.2.4-SNAPSHOT jar compile diff --git a/biojava-ontology/pom.xml b/biojava-ontology/pom.xml index 02e4a894d2..a6475ce2d7 100644 --- a/biojava-ontology/pom.xml +++ b/biojava-ontology/pom.xml @@ -4,7 +4,7 @@ org.biojava biojava - 7.2.3 + 7.2.4-SNAPSHOT biojava-ontology diff --git a/biojava-protein-comparison-tool/pom.xml b/biojava-protein-comparison-tool/pom.xml index b4de1b1e8a..9b06a29f3b 100644 --- a/biojava-protein-comparison-tool/pom.xml +++ b/biojava-protein-comparison-tool/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.3 + 7.2.4-SNAPSHOT biojava-protein-comparison-tool @@ -36,23 +36,23 @@ org.biojava biojava-alignment - 7.2.3 + 7.2.4-SNAPSHOT org.biojava biojava-core - 7.2.3 + 7.2.4-SNAPSHOT org.biojava biojava-structure - 7.2.3 + 7.2.4-SNAPSHOT org.biojava biojava-structure-gui - 7.2.3 + 7.2.4-SNAPSHOT net.sourceforge.jmol diff --git a/biojava-protein-disorder/pom.xml b/biojava-protein-disorder/pom.xml index d5e216b60d..eba96d6f87 100644 --- a/biojava-protein-disorder/pom.xml +++ b/biojava-protein-disorder/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.3 + 7.2.4-SNAPSHOT biojava-protein-disorder jar @@ -63,7 +63,7 @@ org.biojava biojava-core - 7.2.3 + 7.2.4-SNAPSHOT diff --git a/biojava-structure-gui/pom.xml b/biojava-structure-gui/pom.xml index 62db686d60..caa5e31782 100644 --- a/biojava-structure-gui/pom.xml +++ b/biojava-structure-gui/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.3 + 7.2.4-SNAPSHOT 4.0.0 biojava-structure-gui @@ -27,13 +27,13 @@ org.biojava biojava-structure - 7.2.3 + 7.2.4-SNAPSHOT compile org.biojava biojava-core - 7.2.3 + 7.2.4-SNAPSHOT compile diff --git a/biojava-structure/pom.xml b/biojava-structure/pom.xml index aeff7465c0..3cc048a81a 100644 --- a/biojava-structure/pom.xml +++ b/biojava-structure/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.3 + 7.2.4-SNAPSHOT biojava-structure biojava-structure @@ -51,13 +51,13 @@ org.biojava biojava-alignment - 7.2.3 + 7.2.4-SNAPSHOT compile org.biojava biojava-core - 7.2.3 + 7.2.4-SNAPSHOT compile diff --git a/biojava-survival/pom.xml b/biojava-survival/pom.xml index a1facab532..e2ca04fe41 100644 --- a/biojava-survival/pom.xml +++ b/biojava-survival/pom.xml @@ -4,7 +4,7 @@ org.biojava biojava - 7.2.3 + 7.2.4-SNAPSHOT biojava-survival diff --git a/biojava-ws/pom.xml b/biojava-ws/pom.xml index 23866ccbb1..532034b29f 100644 --- a/biojava-ws/pom.xml +++ b/biojava-ws/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.3 + 7.2.4-SNAPSHOT biojava-ws biojava-ws @@ -19,7 +19,7 @@ org.biojava biojava-core - 7.2.3 + 7.2.4-SNAPSHOT compile diff --git a/pom.xml b/pom.xml index c4dfdd3b74..faa29d48ad 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ org.biojava biojava pom - 7.2.3 + 7.2.4-SNAPSHOT biojava BioJava is an open-source project dedicated to providing a Java framework for processing biological data. It provides analytical and statistical routines, parsers for common file formats and allows the @@ -51,7 +51,7 @@ scm:git:git@github.com:biojava/biojava.git https://github.com/biojava/biojava - biojava-7.2.3 + HEAD From 0af196ffd522f98aa093dd569659438f1ab81ebf Mon Sep 17 00:00:00 2001 From: JasonKamsu Date: Fri, 21 Nov 2025 08:24:39 +0100 Subject: [PATCH 03/67] Fix RSPEC-1940 boolean checks --- .../nbio/core/sequence/location/InsdcParser.java | 4 +++- .../java/org/biojava/nbio/core/util/Equals.java | 16 ++++++++++++++-- .../nbio/genome/parsers/gff/Location.java | 4 ++-- .../java/org/biojava/nbio/structure/Author.java | 2 +- .../java/org/biojava/nbio/structure/Element.java | 2 +- .../nbio/structure/align/ce/CECalculator.java | 2 +- .../structure/align/ce/CeCalculatorEnhanced.java | 2 +- .../structure/quaternary/BioAssemblyTools.java | 2 +- .../nbio/structure/secstruc/SecStrucTools.java | 2 +- 9 files changed, 25 insertions(+), 11 deletions(-) diff --git a/biojava-core/src/main/java/org/biojava/nbio/core/sequence/location/InsdcParser.java b/biojava-core/src/main/java/org/biojava/nbio/core/sequence/location/InsdcParser.java index e49bd22216..2d43a481bf 100644 --- a/biojava-core/src/main/java/org/biojava/nbio/core/sequence/location/InsdcParser.java +++ b/biojava-core/src/main/java/org/biojava/nbio/core/sequence/location/InsdcParser.java @@ -260,7 +260,9 @@ private List parseLocationString(String string, int versus) { l.setPartialOn3prime(true); } - if (!(accession == null || "".equals(accession))) l.setAccession(new AccessionID(accession)); + if (accession != null && !"".equals(accession)) { + l.setAccession(new AccessionID(accession)); + } boundedLocationsCollection.add(l); diff --git a/biojava-core/src/main/java/org/biojava/nbio/core/util/Equals.java b/biojava-core/src/main/java/org/biojava/nbio/core/util/Equals.java index e8f78243ed..7e2c7128c9 100644 --- a/biojava-core/src/main/java/org/biojava/nbio/core/util/Equals.java +++ b/biojava-core/src/main/java/org/biojava/nbio/core/util/Equals.java @@ -49,7 +49,13 @@ public static boolean equal(boolean one, boolean two) { * @see #classEqual(Object, Object) */ public static boolean equal(Object one, Object two) { - return one == null && two == null || !(one == null || two == null) && (one == two || one.equals(two)); + if (one == two) { + return true; + } + if (one == null || two == null) { + return false; + } + return one.equals(two); } /** @@ -84,6 +90,12 @@ public static boolean equal(Object one, Object two) { * equal at the class level */ public static boolean classEqual(Object one, Object two) { - return one == two || !(one == null || two == null) && one.getClass() == two.getClass(); + if (one == two) { + return true; + } + if (one == null || two == null) { + return false; + } + return one.getClass() == two.getClass(); } } diff --git a/biojava-genome/src/main/java/org/biojava/nbio/genome/parsers/gff/Location.java b/biojava-genome/src/main/java/org/biojava/nbio/genome/parsers/gff/Location.java index 4163be2b56..a28f1019fb 100644 --- a/biojava-genome/src/main/java/org/biojava/nbio/genome/parsers/gff/Location.java +++ b/biojava-genome/src/main/java/org/biojava/nbio/genome/parsers/gff/Location.java @@ -135,7 +135,7 @@ public static Location fromBio( int start, int end, char strand ) int s= start - 1; int e= end; - if( !( strand == '-' || strand == '+' || strand == '.' )) + if( strand != '-' && strand != '+' && strand != '.' ) { throw new IllegalArgumentException( "Strand must be '+', '-', or '.'" ); } @@ -166,7 +166,7 @@ public static Location fromBioExt( int start, int length, char strand, int total int s= start; int e= s + length; - if( !( strand == '-' || strand == '+' || strand == '.' )) + if( strand != '-' && strand != '+' && strand != '.' ) { throw new IllegalArgumentException( "Strand must be '+', '-', or '.'" ); } diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/Author.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/Author.java index b0d7253507..bd5a01b885 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/Author.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/Author.java @@ -62,7 +62,7 @@ public boolean equals(Object obj) { if ((this.surname == null) ? (other.surname != null) : !this.surname.equals(other.surname)) { return false; } - return !((this.initials == null) ? (other.initials != null) : !this.initials.equals(other.initials)); + return (this.initials == null) ? other.initials == null : this.initials.equals(other.initials); } @Override diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/Element.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/Element.java index 2f534b2828..4e2d3e340a 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/Element.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/Element.java @@ -424,7 +424,7 @@ public boolean isHeavyAtom() { * @return true if Element is not Hydrogen and not Carbon. */ public boolean isHeteroAtom() { - return !(this == C || this == H); + return this != C && this != H; } /** diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/align/ce/CECalculator.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/align/ce/CECalculator.java index 6c045ba48e..83f16b7982 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/align/ce/CECalculator.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/align/ce/CECalculator.java @@ -1450,7 +1450,7 @@ private int optimizeSuperposition(AFPChain afpChain, int nse1, int nse2, int str //afpChain.setTotalRmsdOpt(rmsd); //System.out.println("rmsd: " + rmsd); - if(!(nAtom= strLen * 0.95 && !isRmsdLenAssigned) { rmsdLen=rmsd; isRmsdLenAssigned=true; } diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/align/ce/CeCalculatorEnhanced.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/align/ce/CeCalculatorEnhanced.java index 4f57161268..cab98b0113 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/align/ce/CeCalculatorEnhanced.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/align/ce/CeCalculatorEnhanced.java @@ -1455,7 +1455,7 @@ private int optimizeSuperposition(AFPChain afpChain, int nse1, int nse2, int str //afpChain.setTotalRmsdOpt(rmsd); //System.out.println("rmsd: " + rmsd); - if(!(nAtom= strLen * 0.95 && !isRmsdLenAssigned) { rmsdLen=rmsd; isRmsdLenAssigned=true; } diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/quaternary/BioAssemblyTools.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/quaternary/BioAssemblyTools.java index 7c359121de..f65f4617fe 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/quaternary/BioAssemblyTools.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/quaternary/BioAssemblyTools.java @@ -55,7 +55,7 @@ public static boolean isUnaryExpression(String expression) { if (first < 0 || last < 0) { return true; } - return ! (first == 0 && last > first); + return first != 0 || last <= first; } public static List parseUnaryOperatorExpression(String operatorExpression) { diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/secstruc/SecStrucTools.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/secstruc/SecStrucTools.java index 7732c04b80..42191d682d 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/secstruc/SecStrucTools.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/secstruc/SecStrucTools.java @@ -57,7 +57,7 @@ public static List getSecStrucInfo(Structure s) { Group g = iter.next(); if (g.hasAminoAtoms()) { Object p = g.getProperty(Group.SEC_STRUC); - if (!(p == null)) { + if (p != null) { SecStrucInfo ss = (SecStrucInfo) p; listSSI.add(ss); } From 3f039fd3f5128e3f97981873f9855f45949848a0 Mon Sep 17 00:00:00 2001 From: Pasjonat90D <20847880+Pasjonat90D@users.noreply.github.com> Date: Sun, 30 Nov 2025 20:44:39 +0100 Subject: [PATCH 04/67] Fix #1079: Use isEmpty() instead of size() == 0 --- .../nbio/core/alignment/SimpleAlignedSequence.java | 2 +- .../org/biojava/nbio/core/sequence/GeneSequence.java | 2 +- .../sequence/storage/SequenceAsStringHelper.java | 2 +- .../org/biojava/nbio/core/util/PrettyXMLWriter.java | 2 +- .../nbio/structure/gui/util/SequenceScalePanel.java | 2 +- .../nbio/structure/align/ClusterAltAligs.java | 2 +- .../nbio/structure/align/multiple/BlockImpl.java | 2 +- .../nbio/structure/align/multiple/BlockSetImpl.java | 6 +++--- .../align/multiple/MultipleAlignmentImpl.java | 4 ++-- .../align/multiple/mc/MultipleMcOptimizer.java | 2 +- .../structure/align/quaternary/QsAlignResult.java | 2 +- .../biojava/nbio/structure/align/util/AtomCache.java | 12 ++++++------ .../nbio/structure/chem/ZipChemCompProvider.java | 2 +- .../nbio/structure/cluster/SubunitCluster.java | 2 +- .../nbio/structure/cluster/SubunitClusterer.java | 2 +- .../nbio/structure/geometry/MomentsOfInertia.java | 2 +- .../org/biojava/nbio/structure/io/PDBFileParser.java | 4 ++-- .../nbio/structure/io/mmtf/MmtfStructureReader.java | 2 +- .../nbio/structure/quaternary/BioAssemblyTools.java | 2 +- .../quaternary/BiologicalAssemblyBuilder.java | 2 +- .../structure/symmetry/core/HelicalRepeatUnit.java | 2 +- .../nbio/structure/symmetry/core/RotationGroup.java | 2 +- .../nbio/structure/symmetry/core/RotationSolver.java | 2 +- .../structure/symmetry/core/SystematicSolver.java | 2 +- .../structure/symmetry/geometry/DistanceBox.java | 2 +- .../org/biojava/nbio/structure/xtal/SpaceGroup.java | 4 ++-- .../kaplanmeier/figure/NumbersAtRiskPanel.java | 2 +- .../java/org/biojava/nbio/ws/hmmer/HmmerResult.java | 2 +- 28 files changed, 38 insertions(+), 38 deletions(-) diff --git a/biojava-core/src/main/java/org/biojava/nbio/core/alignment/SimpleAlignedSequence.java b/biojava-core/src/main/java/org/biojava/nbio/core/alignment/SimpleAlignedSequence.java index 331480bbef..99b70c036d 100644 --- a/biojava-core/src/main/java/org/biojava/nbio/core/alignment/SimpleAlignedSequence.java +++ b/biojava-core/src/main/java/org/biojava/nbio/core/alignment/SimpleAlignedSequence.java @@ -411,7 +411,7 @@ private void setLocation(List steps) { } // combine sublocations into 1 Location - if (sublocations.size() == 0) { + if (sublocations.isEmpty()) { location = null; } else if (sublocations.size() == 1) { location = sublocations.get(0); diff --git a/biojava-core/src/main/java/org/biojava/nbio/core/sequence/GeneSequence.java b/biojava-core/src/main/java/org/biojava/nbio/core/sequence/GeneSequence.java index f0f2662fea..638e4e68d9 100644 --- a/biojava-core/src/main/java/org/biojava/nbio/core/sequence/GeneSequence.java +++ b/biojava-core/src/main/java/org/biojava/nbio/core/sequence/GeneSequence.java @@ -119,7 +119,7 @@ public void addIntronsUsingExons() throws Exception { if (intronAdded) { //going to assume introns are correct return; } - if (exonSequenceList.size() == 0) { + if (exonSequenceList.isEmpty()) { return; } ExonComparator exonComparator = new ExonComparator(); diff --git a/biojava-core/src/main/java/org/biojava/nbio/core/sequence/storage/SequenceAsStringHelper.java b/biojava-core/src/main/java/org/biojava/nbio/core/sequence/storage/SequenceAsStringHelper.java index c2b02debee..4acd8969f1 100644 --- a/biojava-core/src/main/java/org/biojava/nbio/core/sequence/storage/SequenceAsStringHelper.java +++ b/biojava-core/src/main/java/org/biojava/nbio/core/sequence/storage/SequenceAsStringHelper.java @@ -44,7 +44,7 @@ public class SequenceAsStringHelper { */ public String getSequenceAsString(List parsedCompounds, CompoundSet compoundSet, Integer bioBegin, Integer bioEnd, Strand strand) { // TODO Optimise/cache. - if(parsedCompounds.size() == 0) + if(parsedCompounds.isEmpty()) return ""; StringBuilder builder = new StringBuilder(); if (strand.equals(Strand.NEGATIVE)) { diff --git a/biojava-core/src/main/java/org/biojava/nbio/core/util/PrettyXMLWriter.java b/biojava-core/src/main/java/org/biojava/nbio/core/util/PrettyXMLWriter.java index 437085866f..6e4a7db77c 100644 --- a/biojava-core/src/main/java/org/biojava/nbio/core/util/PrettyXMLWriter.java +++ b/biojava-core/src/main/java/org/biojava/nbio/core/util/PrettyXMLWriter.java @@ -72,7 +72,7 @@ public void declareNamespace(String nsURI, String prefixHint) private void handleDeclaredNamespaces() throws IOException { - if (namespacesDeclared.size() == 0) { + if (namespacesDeclared.isEmpty()) { for (Iterator nsi = namespacesDeclared.iterator(); nsi.hasNext(); ) { String nsURI = nsi.next(); if (!namespacePrefixes.containsKey(nsURI)) { diff --git a/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/gui/util/SequenceScalePanel.java b/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/gui/util/SequenceScalePanel.java index 06542e5271..75da6c8e28 100644 --- a/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/gui/util/SequenceScalePanel.java +++ b/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/gui/util/SequenceScalePanel.java @@ -126,7 +126,7 @@ private void setPrefSize() { public void setAligMap(List apos){ this.apos = apos; - if ( apos.size() == 0) + if (apos.isEmpty()) return; AlignedPosition last = apos.get(apos.size()-1); diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/align/ClusterAltAligs.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/align/ClusterAltAligs.java index 373bcf1611..0933198d7b 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/align/ClusterAltAligs.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/align/ClusterAltAligs.java @@ -102,7 +102,7 @@ public static void cluster(AlternativeAlignment[] aligs, int cutoff){ } clusters.add(currentCluster); - if ( remainList.size() == 0) { + if ( remainList.isEmpty()) { break; } } diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/align/multiple/BlockImpl.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/align/multiple/BlockImpl.java index e0423b6f8f..43da1d7c06 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/align/multiple/BlockImpl.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/align/multiple/BlockImpl.java @@ -127,7 +127,7 @@ public void setAlignRes(List> alignRes) { public int length() { if (alignRes == null) return 0; - if (alignRes.size() == 0) + if (alignRes.isEmpty()) return 0; return alignRes.get(0).size(); } diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/align/multiple/BlockSetImpl.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/align/multiple/BlockSetImpl.java index cbbb3ae895..344ee3c239 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/align/multiple/BlockSetImpl.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/align/multiple/BlockSetImpl.java @@ -179,7 +179,7 @@ public int size() { // Get the size from the variables that can contain the information if (parent != null) return parent.size(); - else if (getBlocks().size() == 0) { + else if (getBlocks().isEmpty()) { throw new IndexOutOfBoundsException( "Empty BlockSet: number of Blocks == 0."); } else @@ -194,7 +194,7 @@ public int getCoreLength() { } protected void updateLength() { - if (getBlocks().size() == 0) { + if (getBlocks().isEmpty()) { throw new IndexOutOfBoundsException( "Empty BlockSet: number of Blocks == 0."); } @@ -207,7 +207,7 @@ protected void updateLength() { } protected void updateCoreLength() { - if (getBlocks().size() == 0) { + if (getBlocks().isEmpty()) { throw new IndexOutOfBoundsException( "Empty BlockSet: number of Blocks == 0."); } diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/align/multiple/MultipleAlignmentImpl.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/align/multiple/MultipleAlignmentImpl.java index 738eee30c5..06c93a4403 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/align/multiple/MultipleAlignmentImpl.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/align/multiple/MultipleAlignmentImpl.java @@ -207,7 +207,7 @@ public int getCoreLength() { * lengths. */ protected void updateLength() { - if (getBlockSets().size() == 0) { + if (getBlockSets().isEmpty()) { throw new IndexOutOfBoundsException( "Empty MultipleAlignment: blockSets size == 0."); } // Otherwise try to calculate it from the BlockSet information @@ -223,7 +223,7 @@ protected void updateLength() { * BlockSet core lengths. */ protected void updateCoreLength() { - if (getBlockSets().size() == 0) { + if (getBlockSets().isEmpty()) { throw new IndexOutOfBoundsException( "Empty MultipleAlignment: blockSets size == 0."); } // Otherwise try to calculate it from the BlockSet information diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/align/multiple/mc/MultipleMcOptimizer.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/align/multiple/mc/MultipleMcOptimizer.java index 052f147fc6..29c7012801 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/align/multiple/mc/MultipleMcOptimizer.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/align/multiple/mc/MultipleMcOptimizer.java @@ -153,7 +153,7 @@ public MultipleMcOptimizer(MultipleAlignment seedAln, for (Block b : toDelete) { for (BlockSet bs : msa.getBlockSets()) { bs.getBlocks().remove(b); - if (bs.getBlocks().size() == 0) + if (bs.getBlocks().isEmpty()) emptyBs.add(bs); } } diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/align/quaternary/QsAlignResult.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/align/quaternary/QsAlignResult.java index 7ac77a602e..fe1c9c411b 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/align/quaternary/QsAlignResult.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/align/quaternary/QsAlignResult.java @@ -117,7 +117,7 @@ public void setSubunitMap(Map subunitMap) { "Subunit Map index higher than Subunit List size."); // Update the relation enum - if (subunitMap.size() == 0) { + if (subunitMap.isEmpty()) { relation = QsRelation.DIFFERENT; } else if (subunitMap.keySet().size() == subunits1.size()) { if (subunitMap.values().size() == subunits2.size()) { diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/align/util/AtomCache.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/align/util/AtomCache.java index 1435191c2c..71b8a3da22 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/align/util/AtomCache.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/align/util/AtomCache.java @@ -228,7 +228,7 @@ public Structure getBiologicalAssembly(String pdbId, int bioAssemblyId, boolean throws StructureException, IOException { return getBiologicalAssembly(new PdbId(pdbId), bioAssemblyId, multiModel); } - + /** * Returns the biological assembly for a given PDB ID and bioAssemblyId, by building the * assembly from the biounit annotations found in {@link Structure#getPDBHeader()} @@ -284,7 +284,7 @@ public Structure getBiologicalAssembly(PdbId pdbId, int bioAssemblyId, boolean m asymUnit.getPDBHeader().getBioAssemblies().get(bioAssemblyId).getTransforms(); - if (transformations == null || transformations.size() == 0) { + if (transformations == null || transformations.isEmpty()) { throw new StructureException("Could not load transformations to recreate biological assembly id " + bioAssemblyId + " of " + pdbId); } @@ -339,7 +339,7 @@ public Structure getBiologicalAssembly(String pdbId, boolean multiModel) throws asymUnit.getPDBHeader().getBioAssemblies().get(bioAssemblyId).getTransforms(); - if (transformations == null || transformations.size() == 0) { + if (transformations == null || transformations.isEmpty()) { throw new StructureException("Could not load transformations to recreate biological assembly id " + bioAssemblyId + " of " + pdbId); } @@ -385,7 +385,7 @@ public List getBiologicalAssemblies(String pdbId, boolean multiModel) List transformations = asymUnit.getPDBHeader().getBioAssemblies().get(bioAssemblyId).getTransforms(); - if (transformations == null || transformations.size() == 0) { + if (transformations == null || transformations.isEmpty()) { logger.info("Could not load transformations to recreate biological assembly id {} of {}. Assembly " + "id will be missing in biological assemblies.", bioAssemblyId, pdbId); continue; @@ -807,7 +807,7 @@ public Structure getStructureForPdbId(String id) throws IOException, StructureEx public Structure getStructureForPdbId(PdbId pdbId) throws IOException { if (pdbId == null) return null; - + while (checkLoading(pdbId)) { // waiting for loading to be finished... try { @@ -833,7 +833,7 @@ public Structure getStructureForPdbId(PdbId pdbId) throws IOException { protected Structure loadStructureFromCifByPdbId(String pdbId) throws IOException { return loadStructureFromCifByPdbId(new PdbId(pdbId)); } - + protected Structure loadStructureFromCifByPdbId(PdbId pdbId) throws IOException { logger.debug("Loading structure {} from mmCIF file {}.", pdbId, path); Structure s; diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/chem/ZipChemCompProvider.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/chem/ZipChemCompProvider.java index 4fe19aca58..a68019efb7 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/chem/ZipChemCompProvider.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/chem/ZipChemCompProvider.java @@ -120,7 +120,7 @@ public ChemComp getChemComp(String recordName) { } // If a null record or an empty chemcomp, return a default ChemComp and blacklist. - if (cc == null || (null == cc.getName() && cc.getAtoms().size() == 0)) { + if (cc == null || (null == cc.getName() && cc.getAtoms().isEmpty())) { s_logger.info("Unable to find or download {} - excluding from future searches.", recordName); unavailable.add(recordName); return getEmptyChemComp(recordName); diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/cluster/SubunitCluster.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/cluster/SubunitCluster.java index 9a87e92f88..b8ef74c8f1 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/cluster/SubunitCluster.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/cluster/SubunitCluster.java @@ -331,7 +331,7 @@ public boolean mergeIdenticalByEntityId(SubunitCluster other) { } } - if (thisAligned.size() == 0 && otherAligned.size() == 0) { + if (thisAligned.isEmpty() && otherAligned.isEmpty()) { logger.warn("No equivalent aligned atoms found between SubunitClusters {}-{} via entity SEQRES alignment. Is FileParsingParameters.setAlignSeqRes() set?", thisName, otherName); } diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/cluster/SubunitClusterer.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/cluster/SubunitClusterer.java index 6295f8fdf0..964a0aaba3 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/cluster/SubunitClusterer.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/cluster/SubunitClusterer.java @@ -58,7 +58,7 @@ public static Stoichiometry cluster(Structure structure, public static Stoichiometry cluster(List subunits, SubunitClustererParameters params) { List clusters = new ArrayList<>(); - if (subunits.size() == 0) + if (subunits.isEmpty()) return new Stoichiometry(clusters); // First generate a new cluster for each Subunit diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/geometry/MomentsOfInertia.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/geometry/MomentsOfInertia.java index 8cfd032daa..5a6e69c4cf 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/geometry/MomentsOfInertia.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/geometry/MomentsOfInertia.java @@ -72,7 +72,7 @@ public void addPoint(Point3d point, double mass) { public Point3d getCenterOfMass() { - if (points.size() == 0) { + if (points.isEmpty()) { throw new IllegalStateException( "MomentsOfInertia: no points defined"); } diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/PDBFileParser.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/PDBFileParser.java index 176459bbf2..58e4ee6625 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/PDBFileParser.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/PDBFileParser.java @@ -1970,7 +1970,7 @@ private Group getCorrectAltLocGroup( Character altLoc, // build it up. if ( groupCode3.equals(currentGroup.getPDBName())) { - if ( currentGroup.getAtoms().size() == 0) { + if ( currentGroup.getAtoms().isEmpty()) { //System.out.println("current group is empty " + current_group + " " + altLoc); return currentGroup; } @@ -2762,7 +2762,7 @@ private void makeCompounds(List compoundList, } // System.out.println("[makeCompounds] adding sources to compounds from sourceLines"); // since we're starting again from the first compound, reset it here - if ( entities.size() == 0){ + if ( entities.isEmpty()){ current_compound = new EntityInfo(); } else { current_compound = entities.get(0); diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/mmtf/MmtfStructureReader.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/mmtf/MmtfStructureReader.java index c2830c1685..21ef8bb64c 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/mmtf/MmtfStructureReader.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/mmtf/MmtfStructureReader.java @@ -396,7 +396,7 @@ private Group getCorrectAltLocGroup(Character altLoc) { } // no matching altLoc group found. // build it up. - if (group.getAtoms().size() == 0) { + if (group.getAtoms().isEmpty()) { return group; } Group altLocG = (Group) group.clone(); diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/quaternary/BioAssemblyTools.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/quaternary/BioAssemblyTools.java index 7c359121de..786ce7646f 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/quaternary/BioAssemblyTools.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/quaternary/BioAssemblyTools.java @@ -279,7 +279,7 @@ public static double[] getBiologicalMoleculeCentroid( final Structure asymUnit, return centroid; } - if ( transformations.size() == 0) { + if ( transformations.isEmpty()) { return Calc.getCentroid(atoms).getCoords(); } diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/quaternary/BiologicalAssemblyBuilder.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/quaternary/BiologicalAssemblyBuilder.java index c6ec6bc8ff..9edb8f404c 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/quaternary/BiologicalAssemblyBuilder.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/quaternary/BiologicalAssemblyBuilder.java @@ -205,7 +205,7 @@ private void addChainMultiModel(Structure s, Chain newChain, String transformId) // multi-model bioassembly - if ( modelIndex.size() == 0) + if (modelIndex.isEmpty()) modelIndex.add("PLACEHOLDER FOR ASYM UNIT"); int modelCount = modelIndex.indexOf(transformId); diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/HelicalRepeatUnit.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/HelicalRepeatUnit.java index ff9c77cf13..cd16aa5f87 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/HelicalRepeatUnit.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/HelicalRepeatUnit.java @@ -64,7 +64,7 @@ public Map getInteractingRepeatUnits() { private void run() { this.repeatUnitCenters = calcRepeatUnitCenters(); - if (this.repeatUnitCenters.size() == 0) { + if (this.repeatUnitCenters.isEmpty()) { return; } this.repeatUnits = calcRepeatUnits(); diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/RotationGroup.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/RotationGroup.java index 70b69afe14..1490d27036 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/RotationGroup.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/RotationGroup.java @@ -98,7 +98,7 @@ public void complete() { public String getPointGroup() { if (modified) { - if (rotations.size() == 0) { + if (rotations.isEmpty()) { return "C1"; } complete(); diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/RotationSolver.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/RotationSolver.java index 37a44be7ac..b1566a6d2f 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/RotationSolver.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/RotationSolver.java @@ -305,7 +305,7 @@ private boolean isSpherical() { * @return null if invalid, or a rotation if valid */ private Rotation isValidPermutation(List permutation) { - if (permutation.size() == 0) { + if (permutation.isEmpty()) { return null; } diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/SystematicSolver.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/SystematicSolver.java index d13fa4db16..a449771b58 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/SystematicSolver.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/SystematicSolver.java @@ -145,7 +145,7 @@ private void completeRotationGroup() { } private boolean isValidPermutation(List permutation) { - if (permutation.size() == 0) { + if (permutation.isEmpty()) { return false; } diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/geometry/DistanceBox.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/geometry/DistanceBox.java index 2d9b1d6dca..25d37693fb 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/geometry/DistanceBox.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/geometry/DistanceBox.java @@ -166,7 +166,7 @@ private List getBoxTwo(long location) { } // ensure that boxTwo has no empty element by copying from tempBox of defined size List boxTwo = null; - if (tempBox.size() == 0) { + if (tempBox.isEmpty()) { boxTwo = Collections.emptyList(); } else if (tempBox.size() == 1) { boxTwo = Collections.singletonList(tempBox.get(0)); diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/xtal/SpaceGroup.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/xtal/SpaceGroup.java index cff84c70f8..852d213bb9 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/xtal/SpaceGroup.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/xtal/SpaceGroup.java @@ -645,10 +645,10 @@ public List getTransfAlgebraic() { public void setTransfAlgebraic(List transfAlgebraic) { //System.out.println("setting transfAlgebraic " + transfAlgebraic); - if ( transformations == null || transformations.size() == 0) + if ( transformations == null || transformations.isEmpty()) transformations = new ArrayList(transfAlgebraic.size()); - if ( this.transfAlgebraic == null || this.transfAlgebraic.size() == 0) + if ( this.transfAlgebraic == null || this.transfAlgebraic.isEmpty()) this.transfAlgebraic = new ArrayList<>(transfAlgebraic.size()); for ( String transf : transfAlgebraic){ diff --git a/biojava-survival/src/main/java/org/biojava/nbio/survival/kaplanmeier/figure/NumbersAtRiskPanel.java b/biojava-survival/src/main/java/org/biojava/nbio/survival/kaplanmeier/figure/NumbersAtRiskPanel.java index c4578f1c8b..144942a809 100644 --- a/biojava-survival/src/main/java/org/biojava/nbio/survival/kaplanmeier/figure/NumbersAtRiskPanel.java +++ b/biojava-survival/src/main/java/org/biojava/nbio/survival/kaplanmeier/figure/NumbersAtRiskPanel.java @@ -76,7 +76,7 @@ private void paintTable(Graphics g) { sfiHashMap = sfi.getStrataInfoHashMap(); } - if(sfiHashMap.size() == 0) + if(sfiHashMap.isEmpty()) return; //int height = this.getHeight(); diff --git a/biojava-ws/src/main/java/org/biojava/nbio/ws/hmmer/HmmerResult.java b/biojava-ws/src/main/java/org/biojava/nbio/ws/hmmer/HmmerResult.java index 3304e78d4f..373b78cd0a 100644 --- a/biojava-ws/src/main/java/org/biojava/nbio/ws/hmmer/HmmerResult.java +++ b/biojava-ws/src/main/java/org/biojava/nbio/ws/hmmer/HmmerResult.java @@ -140,7 +140,7 @@ public int compareTo(HmmerResult o) { return(me.getSqFrom().compareTo(other.getSqFrom())); } private boolean emptyDomains(HmmerResult o) { - if ( o.getDomains() == null || o.getDomains().size() == 0) + if ( o.getDomains() == null || o.getDomains().isEmpty()) return true; return false; } From 423cd86fbc2355f9943a3bbd81597c3d0c231627 Mon Sep 17 00:00:00 2001 From: josemduarte Date: Mon, 8 Dec 2025 15:22:01 -0800 Subject: [PATCH 05/67] Bugfix: edge-case with no equiv residues in alignment wasn't dealt with and led to IllegalArgumentException. Also: improved logging --- .../structure/cluster/SubunitCluster.java | 43 ++++++++----------- .../structure/cluster/SubunitClusterer.java | 11 ++--- 2 files changed, 23 insertions(+), 31 deletions(-) diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/cluster/SubunitCluster.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/cluster/SubunitCluster.java index 9a87e92f88..2bbad10f1f 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/cluster/SubunitCluster.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/cluster/SubunitCluster.java @@ -507,27 +507,24 @@ public boolean mergeStructure(SubunitCluster other, SubunitClustererParameters p } } - AFPChain afp = aligner.align(this.subunits.get(this.representative) - .getRepresentativeAtoms(), - other.subunits.get(other.representative) - .getRepresentativeAtoms()); + AFPChain afp = aligner.align(this.subunits.get(this.representative).getRepresentativeAtoms(), + other.subunits.get(other.representative).getRepresentativeAtoms()); + String pairName = this.subunits.get(this.representative).getName() + "-" + other.subunits.get(other.representative).getName(); if (afp.getOptLength() < 1) { // alignment failed (eg if chains were too short) throw new StructureException( - String.format("Subunits failed to align using %s", params.getSuperpositionAlgorithm())); + String.format("Subunits %s failed to align using %s", pairName, params.getSuperpositionAlgorithm())); } // Convert AFPChain to MultipleAlignment for convenience MultipleAlignment msa = new MultipleAlignmentEnsembleImpl( afp, this.subunits.get(this.representative).getRepresentativeAtoms(), - other.subunits.get(other.representative) - .getRepresentativeAtoms(), false) - .getMultipleAlignment(0); + other.subunits.get(other.representative).getRepresentativeAtoms(), + false).getMultipleAlignment(0); - double structureCoverage = Math.min(msa.getCoverages().get(0), msa - .getCoverages().get(1)); + double structureCoverage = Math.min(msa.getCoverages().get(0), msa.getCoverages().get(1)); if(params.isUseStructureCoverage() && structureCoverage < params.getStructureCoverageThreshold()) { return false; @@ -543,8 +540,7 @@ public boolean mergeStructure(SubunitCluster other, SubunitClustererParameters p return false; } - logger.info(String.format("SubunitClusters are structurally similar with " - + "%.2f RMSD %.2f coverage", rmsd, structureCoverage)); + logger.info("SubunitClusters {} are structurally similar with [ {} ] RMSD and [ {} ] coverage", pairName, String.format("%.2f", rmsd), String.format("%.2f", structureCoverage)); // Merge clusters List> alignedRes = msa.getBlock(0).getAlignRes(); @@ -565,13 +561,18 @@ public boolean mergeStructure(SubunitCluster other, SubunitClustererParameters p // Only consider residues that are part of the SubunitCluster if (this.subunitEQR.get(this.representative).contains(thisIndex) - && other.subunitEQR.get(other.representative).contains( - otherIndex)) { + && other.subunitEQR.get(other.representative).contains(otherIndex)) { thisAligned.add(thisIndex); otherAligned.add(otherIndex); } } + // this can happen in very rare cases, e.g. 9y9z when merging E_1 into the cluster D_1, OM_1, Y_1 + if (thisAligned.isEmpty() && otherAligned.isEmpty()) { + logger.warn("No equivalent aligned atoms found between SubunitClusters {} via structure alignment. Will not merge the second one into the first.", pairName); + return false; + } + updateEquivResidues(other, thisAligned, otherAligned); this.method = SubunitClustererMethod.STRUCTURE; @@ -602,18 +603,12 @@ private void updateEquivResidues(SubunitCluster other, List thisAligned Collections.sort(otherRemove); Collections.reverse(otherRemove); - for (int t = 0; t < thisRemove.size(); t++) { - for (List eqr : this.subunitEQR) { - int column = thisRemove.get(t); - eqr.remove(column); - } + for (int column : thisRemove) { + this.subunitEQR.forEach(eqr -> eqr.remove(column)); } - for (int t = 0; t < otherRemove.size(); t++) { - for (List eqr : other.subunitEQR) { - int column = otherRemove.get(t); - eqr.remove(column); - } + for (int column : otherRemove) { + other.subunitEQR.forEach(eqr -> eqr.remove(column)); } // The representative is the longest sequence diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/cluster/SubunitClusterer.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/cluster/SubunitClusterer.java index 6295f8fdf0..70da0b56db 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/cluster/SubunitClusterer.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/cluster/SubunitClusterer.java @@ -83,8 +83,7 @@ public static Stoichiometry cluster(List subunits, SubunitClustererPara } } catch (CompoundNotFoundException e) { - logger.warn("Could not merge by Sequence. {}", - e.getMessage()); + logger.info("Could not merge by Sequence. {}", e.getMessage()); } } } @@ -100,7 +99,7 @@ public static Stoichiometry cluster(List subunits, SubunitClustererPara clusters.remove(c2); } } catch (StructureException e) { - logger.warn("Could not merge by Structure. {}", e.getMessage()); + logger.info("Could not merge by Structure. {}", e.getMessage()); } } } @@ -112,8 +111,7 @@ public static Stoichiometry cluster(List subunits, SubunitClustererPara try { clusters.get(c).divideInternally(params); } catch (StructureException e) { - logger.warn("Error analyzing internal symmetry. {}", - e.getMessage()); + logger.info("Error analyzing internal symmetry. {}", e.getMessage()); } } @@ -125,8 +123,7 @@ public static Stoichiometry cluster(List subunits, SubunitClustererPara if (clusters.get(c1).mergeStructure(clusters.get(c2), params)) clusters.remove(c2); } catch (StructureException e) { - logger.warn("Could not merge by Structure. {}", - e.getMessage()); + logger.info("Could not merge by Structure. {}", e.getMessage()); } } } From f55940c0612a61e4f347d8cec502797f34cf8c1f Mon Sep 17 00:00:00 2001 From: josemduarte Date: Wed, 10 Dec 2025 13:55:30 -0800 Subject: [PATCH 06/67] [maven-release-plugin] prepare release biojava-7.2.4 --- biojava-aa-prop/pom.xml | 6 +++--- biojava-alignment/pom.xml | 4 ++-- biojava-core/pom.xml | 2 +- biojava-genome/pom.xml | 6 +++--- biojava-integrationtest/pom.xml | 4 ++-- biojava-modfinder/pom.xml | 4 ++-- biojava-ontology/pom.xml | 2 +- biojava-protein-comparison-tool/pom.xml | 10 +++++----- biojava-protein-disorder/pom.xml | 4 ++-- biojava-structure-gui/pom.xml | 6 +++--- biojava-structure/pom.xml | 6 +++--- biojava-survival/pom.xml | 2 +- biojava-ws/pom.xml | 4 ++-- pom.xml | 4 ++-- 14 files changed, 32 insertions(+), 32 deletions(-) diff --git a/biojava-aa-prop/pom.xml b/biojava-aa-prop/pom.xml index fd7bfcd086..b1efc6c661 100644 --- a/biojava-aa-prop/pom.xml +++ b/biojava-aa-prop/pom.xml @@ -2,7 +2,7 @@ biojava org.biojava - 7.2.4-SNAPSHOT + 7.2.4 4.0.0 biojava-aa-prop @@ -70,12 +70,12 @@ org.biojava biojava-core - 7.2.4-SNAPSHOT + 7.2.4 org.biojava biojava-structure - 7.2.4-SNAPSHOT + 7.2.4 diff --git a/biojava-alignment/pom.xml b/biojava-alignment/pom.xml index 3886b68bd1..a03519aeca 100644 --- a/biojava-alignment/pom.xml +++ b/biojava-alignment/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.4-SNAPSHOT + 7.2.4 biojava-alignment biojava-alignment @@ -47,7 +47,7 @@ org.biojava biojava-core - 7.2.4-SNAPSHOT + 7.2.4 compile diff --git a/biojava-core/pom.xml b/biojava-core/pom.xml index 6b3eb9d8ae..e7f36a646d 100644 --- a/biojava-core/pom.xml +++ b/biojava-core/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.4-SNAPSHOT + 7.2.4 4.0.0 biojava-core diff --git a/biojava-genome/pom.xml b/biojava-genome/pom.xml index 29479bcfdf..b4e2a0e791 100644 --- a/biojava-genome/pom.xml +++ b/biojava-genome/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.4-SNAPSHOT + 7.2.4 4.0.0 biojava-genome @@ -70,13 +70,13 @@ org.biojava biojava-core - 7.2.4-SNAPSHOT + 7.2.4 compile org.biojava biojava-alignment - 7.2.4-SNAPSHOT + 7.2.4 compile diff --git a/biojava-integrationtest/pom.xml b/biojava-integrationtest/pom.xml index 4646e96277..a5b307c637 100644 --- a/biojava-integrationtest/pom.xml +++ b/biojava-integrationtest/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.4-SNAPSHOT + 7.2.4 biojava-integrationtest jar @@ -40,7 +40,7 @@ org.biojava biojava-structure - 7.2.4-SNAPSHOT + 7.2.4 diff --git a/biojava-modfinder/pom.xml b/biojava-modfinder/pom.xml index 953feeed09..4545d15f2f 100644 --- a/biojava-modfinder/pom.xml +++ b/biojava-modfinder/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.4-SNAPSHOT + 7.2.4 biojava-modfinder biojava-modfinder @@ -31,7 +31,7 @@ org.biojava biojava-structure - 7.2.4-SNAPSHOT + 7.2.4 jar compile diff --git a/biojava-ontology/pom.xml b/biojava-ontology/pom.xml index a6475ce2d7..82ccc0ed59 100644 --- a/biojava-ontology/pom.xml +++ b/biojava-ontology/pom.xml @@ -4,7 +4,7 @@ org.biojava biojava - 7.2.4-SNAPSHOT + 7.2.4 biojava-ontology diff --git a/biojava-protein-comparison-tool/pom.xml b/biojava-protein-comparison-tool/pom.xml index 9b06a29f3b..3425644447 100644 --- a/biojava-protein-comparison-tool/pom.xml +++ b/biojava-protein-comparison-tool/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.4-SNAPSHOT + 7.2.4 biojava-protein-comparison-tool @@ -36,23 +36,23 @@ org.biojava biojava-alignment - 7.2.4-SNAPSHOT + 7.2.4 org.biojava biojava-core - 7.2.4-SNAPSHOT + 7.2.4 org.biojava biojava-structure - 7.2.4-SNAPSHOT + 7.2.4 org.biojava biojava-structure-gui - 7.2.4-SNAPSHOT + 7.2.4 net.sourceforge.jmol diff --git a/biojava-protein-disorder/pom.xml b/biojava-protein-disorder/pom.xml index eba96d6f87..30c263b398 100644 --- a/biojava-protein-disorder/pom.xml +++ b/biojava-protein-disorder/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.4-SNAPSHOT + 7.2.4 biojava-protein-disorder jar @@ -63,7 +63,7 @@ org.biojava biojava-core - 7.2.4-SNAPSHOT + 7.2.4 diff --git a/biojava-structure-gui/pom.xml b/biojava-structure-gui/pom.xml index caa5e31782..133f64781c 100644 --- a/biojava-structure-gui/pom.xml +++ b/biojava-structure-gui/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.4-SNAPSHOT + 7.2.4 4.0.0 biojava-structure-gui @@ -27,13 +27,13 @@ org.biojava biojava-structure - 7.2.4-SNAPSHOT + 7.2.4 compile org.biojava biojava-core - 7.2.4-SNAPSHOT + 7.2.4 compile diff --git a/biojava-structure/pom.xml b/biojava-structure/pom.xml index 3cc048a81a..b58fda9a96 100644 --- a/biojava-structure/pom.xml +++ b/biojava-structure/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.4-SNAPSHOT + 7.2.4 biojava-structure biojava-structure @@ -51,13 +51,13 @@ org.biojava biojava-alignment - 7.2.4-SNAPSHOT + 7.2.4 compile org.biojava biojava-core - 7.2.4-SNAPSHOT + 7.2.4 compile diff --git a/biojava-survival/pom.xml b/biojava-survival/pom.xml index e2ca04fe41..993b9aa1fe 100644 --- a/biojava-survival/pom.xml +++ b/biojava-survival/pom.xml @@ -4,7 +4,7 @@ org.biojava biojava - 7.2.4-SNAPSHOT + 7.2.4 biojava-survival diff --git a/biojava-ws/pom.xml b/biojava-ws/pom.xml index 532034b29f..32a8af0fb4 100644 --- a/biojava-ws/pom.xml +++ b/biojava-ws/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.4-SNAPSHOT + 7.2.4 biojava-ws biojava-ws @@ -19,7 +19,7 @@ org.biojava biojava-core - 7.2.4-SNAPSHOT + 7.2.4 compile diff --git a/pom.xml b/pom.xml index faa29d48ad..0cd4634a92 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ org.biojava biojava pom - 7.2.4-SNAPSHOT + 7.2.4 biojava BioJava is an open-source project dedicated to providing a Java framework for processing biological data. It provides analytical and statistical routines, parsers for common file formats and allows the @@ -51,7 +51,7 @@ scm:git:git@github.com:biojava/biojava.git https://github.com/biojava/biojava - HEAD + biojava-7.2.4 diff --git a/biojava-alignment/pom.xml b/biojava-alignment/pom.xml index a03519aeca..b1d0bd9097 100644 --- a/biojava-alignment/pom.xml +++ b/biojava-alignment/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.4 + 7.2.5-SNAPSHOT biojava-alignment biojava-alignment @@ -47,7 +47,7 @@ org.biojava biojava-core - 7.2.4 + 7.2.5-SNAPSHOT compile diff --git a/biojava-core/pom.xml b/biojava-core/pom.xml index e7f36a646d..909f46f088 100644 --- a/biojava-core/pom.xml +++ b/biojava-core/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.4 + 7.2.5-SNAPSHOT 4.0.0 biojava-core diff --git a/biojava-genome/pom.xml b/biojava-genome/pom.xml index b4e2a0e791..68575bbb9b 100644 --- a/biojava-genome/pom.xml +++ b/biojava-genome/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.4 + 7.2.5-SNAPSHOT 4.0.0 biojava-genome @@ -70,13 +70,13 @@ org.biojava biojava-core - 7.2.4 + 7.2.5-SNAPSHOT compile org.biojava biojava-alignment - 7.2.4 + 7.2.5-SNAPSHOT compile diff --git a/biojava-integrationtest/pom.xml b/biojava-integrationtest/pom.xml index a5b307c637..1c21373bd1 100644 --- a/biojava-integrationtest/pom.xml +++ b/biojava-integrationtest/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.4 + 7.2.5-SNAPSHOT biojava-integrationtest jar @@ -40,7 +40,7 @@ org.biojava biojava-structure - 7.2.4 + 7.2.5-SNAPSHOT diff --git a/biojava-modfinder/pom.xml b/biojava-modfinder/pom.xml index 4545d15f2f..e8de9e38d9 100644 --- a/biojava-modfinder/pom.xml +++ b/biojava-modfinder/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.4 + 7.2.5-SNAPSHOT biojava-modfinder biojava-modfinder @@ -31,7 +31,7 @@ org.biojava biojava-structure - 7.2.4 + 7.2.5-SNAPSHOT jar compile diff --git a/biojava-ontology/pom.xml b/biojava-ontology/pom.xml index 82ccc0ed59..6a3fd5cbc4 100644 --- a/biojava-ontology/pom.xml +++ b/biojava-ontology/pom.xml @@ -4,7 +4,7 @@ org.biojava biojava - 7.2.4 + 7.2.5-SNAPSHOT biojava-ontology diff --git a/biojava-protein-comparison-tool/pom.xml b/biojava-protein-comparison-tool/pom.xml index 3425644447..cf8cd570a7 100644 --- a/biojava-protein-comparison-tool/pom.xml +++ b/biojava-protein-comparison-tool/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.4 + 7.2.5-SNAPSHOT biojava-protein-comparison-tool @@ -36,23 +36,23 @@ org.biojava biojava-alignment - 7.2.4 + 7.2.5-SNAPSHOT org.biojava biojava-core - 7.2.4 + 7.2.5-SNAPSHOT org.biojava biojava-structure - 7.2.4 + 7.2.5-SNAPSHOT org.biojava biojava-structure-gui - 7.2.4 + 7.2.5-SNAPSHOT net.sourceforge.jmol diff --git a/biojava-protein-disorder/pom.xml b/biojava-protein-disorder/pom.xml index 30c263b398..7273bbff8f 100644 --- a/biojava-protein-disorder/pom.xml +++ b/biojava-protein-disorder/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.4 + 7.2.5-SNAPSHOT biojava-protein-disorder jar @@ -63,7 +63,7 @@ org.biojava biojava-core - 7.2.4 + 7.2.5-SNAPSHOT diff --git a/biojava-structure-gui/pom.xml b/biojava-structure-gui/pom.xml index 133f64781c..581b8a53cc 100644 --- a/biojava-structure-gui/pom.xml +++ b/biojava-structure-gui/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.4 + 7.2.5-SNAPSHOT 4.0.0 biojava-structure-gui @@ -27,13 +27,13 @@ org.biojava biojava-structure - 7.2.4 + 7.2.5-SNAPSHOT compile org.biojava biojava-core - 7.2.4 + 7.2.5-SNAPSHOT compile diff --git a/biojava-structure/pom.xml b/biojava-structure/pom.xml index b58fda9a96..7fbc64a3c1 100644 --- a/biojava-structure/pom.xml +++ b/biojava-structure/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.4 + 7.2.5-SNAPSHOT biojava-structure biojava-structure @@ -51,13 +51,13 @@ org.biojava biojava-alignment - 7.2.4 + 7.2.5-SNAPSHOT compile org.biojava biojava-core - 7.2.4 + 7.2.5-SNAPSHOT compile diff --git a/biojava-survival/pom.xml b/biojava-survival/pom.xml index 993b9aa1fe..88f901cdea 100644 --- a/biojava-survival/pom.xml +++ b/biojava-survival/pom.xml @@ -4,7 +4,7 @@ org.biojava biojava - 7.2.4 + 7.2.5-SNAPSHOT biojava-survival diff --git a/biojava-ws/pom.xml b/biojava-ws/pom.xml index 32a8af0fb4..7e960b4445 100644 --- a/biojava-ws/pom.xml +++ b/biojava-ws/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.4 + 7.2.5-SNAPSHOT biojava-ws biojava-ws @@ -19,7 +19,7 @@ org.biojava biojava-core - 7.2.4 + 7.2.5-SNAPSHOT compile diff --git a/pom.xml b/pom.xml index 0cd4634a92..e9f246202b 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ org.biojava biojava pom - 7.2.4 + 7.2.5-SNAPSHOT biojava BioJava is an open-source project dedicated to providing a Java framework for processing biological data. It provides analytical and statistical routines, parsers for common file formats and allows the @@ -51,7 +51,7 @@ scm:git:git@github.com:biojava/biojava.git https://github.com/biojava/biojava - biojava-7.2.4 + HEAD From c0240dd9d192d2ed3df5bab95830016bc9215133 Mon Sep 17 00:00:00 2001 From: Pasjonat90D <20847880+Pasjonat90D@users.noreply.github.com> Date: Sun, 14 Dec 2025 22:33:43 +0100 Subject: [PATCH 09/67] Refactor: fix SonarQube S1155 issues (#1079) --- .../loader/GenbankProxySequenceReaderTest.java | 2 +- .../org/biojava/nbio/genome/GeneFeatureHelper.java | 14 +++++++------- .../genome/homology/GFF3FromUniprotBlastHits.java | 14 +++++++------- .../nbio/genome/parsers/gff/GFF3Writer.java | 4 ++-- .../nbio/structure/test/io/TestSeqResParsing.java | 2 +- .../protmod/io/ModifiedCompoundXMLConverter.java | 2 +- .../structure/ProteinModificationIdentifier.java | 2 +- .../nbio/protmod/phosphosite/TestAcetylation.java | 7 ++++--- .../nbio/ontology/utils/WeakValueHashMap.java | 4 ++-- .../align/gui/autosuggest/JAutoSuggest.java | 4 ++-- .../gui/autosuggest/SCOPAutoSuggestProvider.java | 6 +++--- .../align/gui/jmol/RasmolCommandListener.java | 2 +- .../main/java/org/biojava/nbio/structure/Site.java | 2 +- .../multiple/util/MultipleAlignmentWriter.java | 2 +- .../nbio/structure/align/util/AlignmentTools.java | 2 +- .../align/xml/MultipleAlignmentXMLParser.java | 2 +- .../nbio/structure/contact/GroupContact.java | 2 +- .../structure/contact/StructureInterfaceList.java | 2 +- .../org/biojava/nbio/structure/io/BondMaker.java | 2 +- .../biojava/nbio/structure/io/PDBFileParser.java | 6 +++--- .../nbio/structure/io/SeqRes2AtomAligner.java | 4 ++-- .../structure/io/cif/CifStructureConsumerImpl.java | 6 +++--- .../structure/io/mmtf/MmtfStructureReader.java | 2 +- .../BiologicalAssemblyTransformation.java | 2 +- .../nbio/structure/scop/ScopInstallation.java | 8 ++++---- .../structure/symmetry/core/PermutationGroup.java | 2 +- .../symmetry/core/QuatSymmetrySubunits.java | 2 +- .../structure/symmetry/core/RotationGroup.java | 4 ++-- .../symmetry/internal/SequenceFunctionRefiner.java | 2 +- .../structure/symmetry/internal/SymmOptimizer.java | 5 +---- .../structure/symmetry/utils/BlastClustReader.java | 2 +- .../nbio/structure/align/util/AtomCacheTest.java | 5 +++-- .../biojava/nbio/structure/io/TestHeaderOnly.java | 3 +-- .../structure/io/TestMMcifOrganismParsing.java | 4 ++-- .../nbio/structure/io/TestSiftsParsing.java | 12 ++++++------ .../structure/io/cif/CifFileConsumerImplTest.java | 4 ++-- .../org/biojava/nbio/survival/cox/CoxInfo.java | 4 ++-- .../biojava/nbio/survival/cox/ResidualsCoxph.java | 4 ++-- .../org/biojava/nbio/survival/data/WorkSheet.java | 4 ++-- 39 files changed, 80 insertions(+), 82 deletions(-) diff --git a/biojava-core/src/test/java/org/biojava/nbio/core/sequence/loader/GenbankProxySequenceReaderTest.java b/biojava-core/src/test/java/org/biojava/nbio/core/sequence/loader/GenbankProxySequenceReaderTest.java index 6883637a49..0e8f7f41f5 100644 --- a/biojava-core/src/test/java/org/biojava/nbio/core/sequence/loader/GenbankProxySequenceReaderTest.java +++ b/biojava-core/src/test/java/org/biojava/nbio/core/sequence/loader/GenbankProxySequenceReaderTest.java @@ -162,7 +162,7 @@ so it should be done here (manualy). logger.info("taxonomy name '{}'", taxonName); Assert.assertNotNull(taxonName); - if (seq.getFeaturesByType("CDS").size() > 0) { + if (!seq.getFeaturesByType("CDS").isEmpty()) { FeatureInterface, AminoAcidCompound> CDS = seq.getFeaturesByType("CDS").get(0); logger.info("CDS: {}", CDS); String codedBy = CDS.getQualifiers().get("coded_by").get(0).getValue(); diff --git a/biojava-genome/src/main/java/org/biojava/nbio/genome/GeneFeatureHelper.java b/biojava-genome/src/main/java/org/biojava/nbio/genome/GeneFeatureHelper.java index c9786b3ad0..b417c4608d 100644 --- a/biojava-genome/src/main/java/org/biojava/nbio/genome/GeneFeatureHelper.java +++ b/biojava-genome/src/main/java/org/biojava/nbio/genome/GeneFeatureHelper.java @@ -36,7 +36,7 @@ /** * - * @author Scooter Willis + * @author Scooter Willis */ public class GeneFeatureHelper { @@ -418,7 +418,7 @@ static public void addGmodGFF3GeneFeatures(Map chrom String startCodonName = ""; String stopCodonName = ""; FeatureList startCodonList = mRNAChildren.selectByType("five_prime_UTR"); - if (startCodonList != null && startCodonList.size() > 0) { + if (startCodonList != null && !startCodonList.isEmpty()) { startCodon = startCodonList.get(0); if (strand == Strand.NEGATIVE) { startCodonBegin = startCodon.location().bioEnd(); @@ -430,7 +430,7 @@ static public void addGmodGFF3GeneFeatures(Map chrom FeatureList stopCodonList = mRNAChildren.selectByType("three_prime_UTR"); - if (stopCodonList != null && stopCodonList.size() > 0) { + if (stopCodonList != null && !stopCodonList.isEmpty()) { stopCodon = stopCodonList.get(0); if (strand == Strand.NEGATIVE) { stopCodonEnd = stopCodon.location().bioStart(); @@ -577,7 +577,7 @@ static public void addGlimmerGFF3GeneFeatures(Map ch String startCodonName = ""; String stopCodonName = ""; FeatureList startCodonList = gene.selectByAttribute("Note", "initial-exon"); - if (startCodonList != null && startCodonList.size() > 0) { + if (startCodonList != null && !startCodonList.isEmpty()) { startCodon = startCodonList.get(0); if (strand == Strand.NEGATIVE) { startCodonBegin = startCodon.location().bioEnd(); @@ -589,7 +589,7 @@ static public void addGlimmerGFF3GeneFeatures(Map ch FeatureList stopCodonList = gene.selectByAttribute("Note", "final-exon"); - if (stopCodonList != null && stopCodonList.size() > 0) { + if (stopCodonList != null && !stopCodonList.isEmpty()) { stopCodon = stopCodonList.get(0); if (strand == Strand.NEGATIVE) { stopCodonEnd = stopCodon.location().bioStart(); @@ -723,7 +723,7 @@ static public void addGeneMarkGTFGeneFeatures(Map ch String startCodonName = ""; String stopCodonName = ""; FeatureList startCodonList = transcriptFeature.selectByType("start_codon"); - if (startCodonList != null && startCodonList.size() > 0) { + if (startCodonList != null && !startCodonList.isEmpty()) { startCodon = startCodonList.get(0); if (strand == Strand.POSITIVE) { startCodonBegin = startCodon.location().bioStart(); @@ -735,7 +735,7 @@ static public void addGeneMarkGTFGeneFeatures(Map ch FeatureList stopCodonList = transcriptFeature.selectByType("stop_codon"); - if (stopCodonList != null && stopCodonList.size() > 0) { + if (stopCodonList != null && !stopCodonList.isEmpty()) { stopCodon = stopCodonList.get(0); if (strand == Strand.POSITIVE) { stopCodonEnd = stopCodon.location().bioEnd(); diff --git a/biojava-genome/src/main/java/org/biojava/nbio/genome/homology/GFF3FromUniprotBlastHits.java b/biojava-genome/src/main/java/org/biojava/nbio/genome/homology/GFF3FromUniprotBlastHits.java index 67e22bd992..1547aba2d9 100644 --- a/biojava-genome/src/main/java/org/biojava/nbio/genome/homology/GFF3FromUniprotBlastHits.java +++ b/biojava-genome/src/main/java/org/biojava/nbio/genome/homology/GFF3FromUniprotBlastHits.java @@ -46,7 +46,7 @@ /** * - * @author Scooter Willis + * @author Scooter Willis * @author Mark Chapman */ public class GFF3FromUniprotBlastHits { @@ -163,7 +163,7 @@ PairwiseSequenceAlignerType.LOCAL, new SimpleGapPenalty(), String notes = ""; if (featureKeyWords != null) { List keyWords = featureKeyWords.getKeyWords(); - if (keyWords.size() > 0) { + if (!keyWords.isEmpty()) { notes = ";Note="; for (String note : keyWords) { if ("Complete proteome".equals(note)) { @@ -187,7 +187,7 @@ PairwiseSequenceAlignerType.LOCAL, new SimpleGapPenalty(), List cazyList = databaseReferenceHashMap.get("CAZy"); List goList = databaseReferenceHashMap.get("GO"); List eccList = databaseReferenceHashMap.get("BRENDA"); - if (pfamList != null && pfamList.size() > 0) { + if (pfamList != null && !pfamList.isEmpty()) { if (notes.length() == 0) { notes = ";Note="; } @@ -197,7 +197,7 @@ PairwiseSequenceAlignerType.LOCAL, new SimpleGapPenalty(), } } - if (cazyList != null && cazyList.size() > 0) { + if (cazyList != null && !cazyList.isEmpty()) { if (notes.length() == 0) { notes = ";Note="; } @@ -208,7 +208,7 @@ PairwiseSequenceAlignerType.LOCAL, new SimpleGapPenalty(), } } - if (eccList != null && eccList.size() > 0) { + if (eccList != null && !eccList.isEmpty()) { if (notes.length() == 0) { notes = ";Note="; } @@ -221,8 +221,8 @@ PairwiseSequenceAlignerType.LOCAL, new SimpleGapPenalty(), } } - if (goList != null && goList.size() > 0) { - if (notes.length() == 0) { + if (goList != null && !goList.isEmpty()) { + if (notes.isEmpty()) { notes = ";Note="; } for (DBReferenceInfo note : goList) { diff --git a/biojava-genome/src/main/java/org/biojava/nbio/genome/parsers/gff/GFF3Writer.java b/biojava-genome/src/main/java/org/biojava/nbio/genome/parsers/gff/GFF3Writer.java index 88af970928..0f8bd97884 100644 --- a/biojava-genome/src/main/java/org/biojava/nbio/genome/parsers/gff/GFF3Writer.java +++ b/biojava-genome/src/main/java/org/biojava/nbio/genome/parsers/gff/GFF3Writer.java @@ -31,7 +31,7 @@ /** * - * @author Scooter Willis + * @author Scooter Willis */ public class GFF3Writer { @@ -122,7 +122,7 @@ public void write(OutputStream outputStream, Map chr private String getGFF3Note(List notesList) { String notes = ""; - if (notesList.size() > 0) { + if (!notesList.isEmpty()) { notes = ";Note="; int noteindex = 1; for (String note : notesList) { diff --git a/biojava-integrationtest/src/test/java/org/biojava/nbio/structure/test/io/TestSeqResParsing.java b/biojava-integrationtest/src/test/java/org/biojava/nbio/structure/test/io/TestSeqResParsing.java index de6c072719..6ea23aef8a 100644 --- a/biojava-integrationtest/src/test/java/org/biojava/nbio/structure/test/io/TestSeqResParsing.java +++ b/biojava-integrationtest/src/test/java/org/biojava/nbio/structure/test/io/TestSeqResParsing.java @@ -53,7 +53,7 @@ public void test11GS() throws IOException, StructureException{ s = StructureIO.getStructure(pdbID); assertNotNull(s); - assertTrue(s.getChains().size() > 0); + assertFalse(s.getChains().isEmpty()); Chain c = s.getChainByIndex(0); assertTrue(c.getSeqResGroups().size() > 2); diff --git a/biojava-modfinder/src/main/java/org/biojava/nbio/protmod/io/ModifiedCompoundXMLConverter.java b/biojava-modfinder/src/main/java/org/biojava/nbio/protmod/io/ModifiedCompoundXMLConverter.java index 187e113924..e038f57cd3 100644 --- a/biojava-modfinder/src/main/java/org/biojava/nbio/protmod/io/ModifiedCompoundXMLConverter.java +++ b/biojava-modfinder/src/main/java/org/biojava/nbio/protmod/io/ModifiedCompoundXMLConverter.java @@ -69,7 +69,7 @@ public static String toXML(ModifiedCompound mc) throws IOException{ Set linkages = mc.getAtomLinkages(); - if ( linkages.size() > 0 ) { + if (!linkages.isEmpty()) { int pos = -1; for ( StructureAtomLinkage link: linkages){ pos ++; diff --git a/biojava-modfinder/src/main/java/org/biojava/nbio/protmod/structure/ProteinModificationIdentifier.java b/biojava-modfinder/src/main/java/org/biojava/nbio/protmod/structure/ProteinModificationIdentifier.java index c9575a5444..0d94d36e6e 100644 --- a/biojava-modfinder/src/main/java/org/biojava/nbio/protmod/structure/ProteinModificationIdentifier.java +++ b/biojava-modfinder/src/main/java/org/biojava/nbio/protmod/structure/ProteinModificationIdentifier.java @@ -285,7 +285,7 @@ public void identify(final List chains, if (residues.isEmpty()) { String pdbId = "?"; - if ( chains.size() > 0) { + if (!chains.isEmpty()) { Structure struc = chains.get(0).getStructure(); if ( struc != null) pdbId = struc.getPDBCode(); diff --git a/biojava-modfinder/src/test/java/org/biojava/nbio/protmod/phosphosite/TestAcetylation.java b/biojava-modfinder/src/test/java/org/biojava/nbio/protmod/phosphosite/TestAcetylation.java index 34376307a0..ba8e6d2a3d 100644 --- a/biojava-modfinder/src/test/java/org/biojava/nbio/protmod/phosphosite/TestAcetylation.java +++ b/biojava-modfinder/src/test/java/org/biojava/nbio/protmod/phosphosite/TestAcetylation.java @@ -32,7 +32,8 @@ import java.net.URL; import java.util.List; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; @@ -100,11 +101,11 @@ public void testAcetylation() throws IOException { List sites = Site.parseSites(localFile); - assertTrue(sites.size() > 0); + assertFalse(sites.isEmpty()); for (Site s : sites) { - assertTrue(s.getResidue() != null); + assertNotNull(s.getResidue()); } diff --git a/biojava-ontology/src/main/java/org/biojava/nbio/ontology/utils/WeakValueHashMap.java b/biojava-ontology/src/main/java/org/biojava/nbio/ontology/utils/WeakValueHashMap.java index 7762642c76..c508cdc206 100644 --- a/biojava-ontology/src/main/java/org/biojava/nbio/ontology/utils/WeakValueHashMap.java +++ b/biojava-ontology/src/main/java/org/biojava/nbio/ontology/utils/WeakValueHashMap.java @@ -58,12 +58,12 @@ public WeakValueHashMap() { private void diddleReferenceQueue() { // Avoid making behind-the-scenes modifications while iterators exist. - if (iteratorRefs.size() > 0) { + if (!iteratorRefs.isEmpty()) { Reference ref; while ((ref = iteratorRefQueue.poll()) != null) { iteratorRefs.remove(ref); } - if (iteratorRefs.size() > 0) { + if (!iteratorRefs.isEmpty()) { return; } } diff --git a/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/autosuggest/JAutoSuggest.java b/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/autosuggest/JAutoSuggest.java index 9e3c825910..c5542b4edc 100644 --- a/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/autosuggest/JAutoSuggest.java +++ b/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/autosuggest/JAutoSuggest.java @@ -229,7 +229,7 @@ public void keyReleased(KeyEvent e) { list.ensureIndexIsVisible(list.getSelectedIndex() - 1); return; } else if (e.getKeyCode() == KeyEvent.VK_ENTER - && list.getSelectedIndex() != -1 && suggestions.size() > 0) { + && list.getSelectedIndex() != -1 && !suggestions.isEmpty()) { setText((String) list.getSelectedValue()); @@ -365,7 +365,7 @@ public String doInBackground() { setFont(regular); - if (suggestions.size() > 0) { + if (!suggestions.isEmpty()) { list.setListData(suggestions); list.setSelectedIndex(0); list.ensureIndexIsVisible(0); diff --git a/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/autosuggest/SCOPAutoSuggestProvider.java b/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/autosuggest/SCOPAutoSuggestProvider.java index 136f184584..8e1a975205 100644 --- a/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/autosuggest/SCOPAutoSuggestProvider.java +++ b/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/autosuggest/SCOPAutoSuggestProvider.java @@ -111,7 +111,7 @@ private List getPossibleScopDomains(String userInput) { if ( stop.get()) return domains; - if ( domains == null || domains.size() < 1){ + if ( domains == null || domains.isEmpty()){ if ( userInput.length() > 5){ // e.g. d4hhba @@ -127,11 +127,11 @@ private List getPossibleScopDomains(String userInput) { if (DEBUG) System.out.println("domains: " + domains); - if ( domains == null || domains.size() < 1) { + if ( domains == null || domains.isEmpty()) { if ( userInput.length() > 0 ){ List descs = scop.filterByClassificationId(userInput); - if ( descs == null || descs.size() < 1){ + if ( descs == null || descs.isEmpty()){ descs = scop.filterByDescription(userInput); } diff --git a/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/jmol/RasmolCommandListener.java b/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/jmol/RasmolCommandListener.java index d1ce5c0e30..feec8b0366 100644 --- a/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/jmol/RasmolCommandListener.java +++ b/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/jmol/RasmolCommandListener.java @@ -74,7 +74,7 @@ public void actionPerformed(ActionEvent event) { // check last command in history // if equivalent, don't add, // otherwise add - if (history.size()>0){ + if (!history.isEmpty()){ String txt=history.get(history.size()-1); if (! txt.equals(cmd)) { history.add(cmd); diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/Site.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/Site.java index 9158906d23..341483f31b 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/Site.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/Site.java @@ -83,7 +83,7 @@ public String toPDB() { @Override public void toPDB(StringBuffer buf) { - if (groups == null || groups.size() < 1) { + if (groups == null || groups.isEmpty()) { return; } diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/align/multiple/util/MultipleAlignmentWriter.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/align/multiple/util/MultipleAlignmentWriter.java index 771b8b5f68..5033576df0 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/align/multiple/util/MultipleAlignmentWriter.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/align/multiple/util/MultipleAlignmentWriter.java @@ -205,7 +205,7 @@ public static String toTransformMatrices(MultipleAlignment alignment) { List btransforms = alignment.getBlockSet(bs) .getTransformations(); - if (btransforms == null || btransforms.size() < 1) + if (btransforms == null || btransforms.isEmpty()) continue; if (alignment.getBlockSets().size() > 1) { diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/align/util/AlignmentTools.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/align/util/AlignmentTools.java index c6791f4ed2..e535a87508 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/align/util/AlignmentTools.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/align/util/AlignmentTools.java @@ -1313,7 +1313,7 @@ public static Group[] prepareGroupsForDisplay(AFPChain afpChain, Atom[] ca1, Ato if ( afpChain.getBlockNum() > 0){ // Superimpose ligands relative to the first block - if( hetatms2.size() > 0 ) { + if(!hetatms2.isEmpty()) { if ( afpChain.getBlockRotationMatrix().length > 0 ) { diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/align/xml/MultipleAlignmentXMLParser.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/align/xml/MultipleAlignmentXMLParser.java index 759ee61931..e8d5434578 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/align/xml/MultipleAlignmentXMLParser.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/align/xml/MultipleAlignmentXMLParser.java @@ -169,7 +169,7 @@ else if ("ScoresCache".equals(child.getNodeName())){ } } //Because if it is 0 means that there were no transformations - if (transforms.size() != 0){ + if (!transforms.isEmpty()){ bs.setTransformations(transforms); } return bs; diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/GroupContact.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/GroupContact.java index 07b163730d..245d4481f6 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/GroupContact.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/GroupContact.java @@ -57,7 +57,7 @@ public void setPair(Pair pair) { } public double getMinDistance() { - if (atomContacts.size()==0) return 0; + if (atomContacts.isEmpty()) return 0; double minDistance = Double.MAX_VALUE; for (AtomContact atomContact:atomContacts) { diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/StructureInterfaceList.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/StructureInterfaceList.java index 60f7c3a91b..00cf7ef65c 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/StructureInterfaceList.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/StructureInterfaceList.java @@ -380,7 +380,7 @@ public List getClusters(double contactOverlapScoreClu clusters = new ArrayList<>(); // nothing to do if we have no interfaces - if (list.size()==0) return clusters; + if (list.isEmpty()) return clusters; logger.debug("Calculating all-vs-all Jaccard scores for {} interfaces", list.size()); double[][] matrix = new double[list.size()][list.size()]; diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/BondMaker.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/BondMaker.java index e6b8548025..e81f7fb867 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/BondMaker.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/BondMaker.java @@ -273,7 +273,7 @@ private void trimBondLists() { for (Chain chain : structure.getChains(modelInd)) { for (Group group : chain.getAtomGroups()) { for (Atom atom : group.getAtoms()) { - if (atom.getBonds()!=null && atom.getBonds().size() > 0) { + if (atom.getBonds()!=null && !atom.getBonds().isEmpty()) { ((ArrayList) atom.getBonds()).trimToSize(); } } diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/PDBFileParser.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/PDBFileParser.java index 58e4ee6625..f5cc851fec 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/PDBFileParser.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/PDBFileParser.java @@ -1943,7 +1943,7 @@ private Group getCorrectAltLocGroup( Character altLoc, // see if we know this altLoc already; List atoms = currentGroup.getAtoms(); - if ( atoms.size() > 0) { + if (!atoms.isEmpty()) { Atom a1 = atoms.get(0); // we are just adding atoms to the current group // probably there is a second group following later... @@ -1956,7 +1956,7 @@ private Group getCorrectAltLocGroup( Character altLoc, List altLocs = currentGroup.getAltLocs(); for ( Group altLocG : altLocs ){ atoms = altLocG.getAtoms(); - if ( atoms.size() > 0) { + if (!atoms.isEmpty()) { for ( Atom a1 : atoms) { if (a1.getAltLoc().equals( altLoc)) { @@ -2921,7 +2921,7 @@ private void triggerEndFileChecks(){ pdbHeader.setBioAssemblies(bioAssemblyParser.getTransformationMap()); } - if (ncsOperators !=null && ncsOperators.size()>0) { + if (ncsOperators !=null && !ncsOperators.isEmpty()) { crystallographicInfo.setNcsOperators( ncsOperators.toArray(new Matrix4d[ncsOperators.size()])); } diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/SeqRes2AtomAligner.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/SeqRes2AtomAligner.java index 7ee21de4b4..0652ad1809 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/SeqRes2AtomAligner.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/SeqRes2AtomAligner.java @@ -198,7 +198,7 @@ public void mapSeqresRecords(Chain atomRes, Chain seqRes) { } } - if ( atomRes.getAtomGroups(GroupType.AMINOACID).size() < 1) { + if (atomRes.getAtomGroups(GroupType.AMINOACID).isEmpty()) { logger.debug("ATOM chain {} does not contain amino acids, ignoring...", atomRes.getId()); return; } @@ -215,7 +215,7 @@ public void mapSeqresRecords(Chain atomRes, Chain seqRes) { private void alignNucleotideChains(Chain seqRes, Chain atomRes) { - if ( atomRes.getAtomGroups(GroupType.NUCLEOTIDE).size() < 1) { + if (atomRes.getAtomGroups(GroupType.NUCLEOTIDE).isEmpty()) { logger.debug("ATOM chain {} does not contain nucleotides, ignoring...", atomRes.getId()); return; diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/CifStructureConsumerImpl.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/CifStructureConsumerImpl.java index 67514edd84..03ebd027c6 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/CifStructureConsumerImpl.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/CifStructureConsumerImpl.java @@ -372,7 +372,7 @@ public void consumeAtomSite(AtomSite atomSite) { private Group getAltLocGroup(String recordName, Character altLoc, Character oneLetterCode, String threeLetterCode, long seqId) { List atoms = currentGroup.getAtoms(); - if (atoms.size() > 0) { + if (!atoms.isEmpty()) { if (atoms.get(0).getAltLoc().equals(altLoc)) { return currentGroup; } @@ -381,7 +381,7 @@ private Group getAltLocGroup(String recordName, Character altLoc, Character oneL List altLocs = currentGroup.getAltLocs(); for (Group altLocGroup : altLocs) { atoms = altLocGroup.getAtoms(); - if (atoms.size() > 0) { + if (!atoms.isEmpty()) { for (Atom a1 : atoms) { if (a1.getAltLoc().equals(altLoc)) { return altLocGroup; @@ -1478,7 +1478,7 @@ private void setStructNcsOps() { } } - if (ncsOperators.size() > 0) { + if (!ncsOperators.isEmpty()) { structure.getCrystallographicInfo() .setNcsOperators(ncsOperators.toArray(new Matrix4d[0])); } diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/mmtf/MmtfStructureReader.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/mmtf/MmtfStructureReader.java index 21ef8bb64c..865c9e0da4 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/mmtf/MmtfStructureReader.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/mmtf/MmtfStructureReader.java @@ -372,7 +372,7 @@ public void setInterGroupBond(int indOne, int indTwo, int bondOrder) { private Group getCorrectAltLocGroup(Character altLoc) { // see if we know this altLoc already; List atoms = group.getAtoms(); - if (atoms.size() > 0) { + if (!atoms.isEmpty()) { Atom a1 = atoms.get(0); // we are just adding atoms to the current group // probably there is a second group following later... diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/quaternary/BiologicalAssemblyTransformation.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/quaternary/BiologicalAssemblyTransformation.java index 36bccd7b39..ff1efd6e32 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/quaternary/BiologicalAssemblyTransformation.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/quaternary/BiologicalAssemblyTransformation.java @@ -226,7 +226,7 @@ public static BiologicalAssemblyTransformation fromXML(String xml) List transformations = fromMultiXML(xml); - if ( transformations.size() > 0) + if (!transformations.isEmpty()) return transformations.get(0); else diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/scop/ScopInstallation.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/scop/ScopInstallation.java index d092d5485e..4ac55cfa08 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/scop/ScopInstallation.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/scop/ScopInstallation.java @@ -655,7 +655,7 @@ private List extractRanges(String range) { } protected void downloadClaFile() throws IOException{ - if(mirrors.size()<1) { + if(mirrors.isEmpty()) { initScopURLs(); } IOException exception = null; @@ -676,7 +676,7 @@ protected void downloadClaFile() throws IOException{ } protected void downloadDesFile() throws IOException{ - if(mirrors.size()<1) { + if(mirrors.isEmpty()) { initScopURLs(); } IOException exception = null; @@ -697,7 +697,7 @@ protected void downloadDesFile() throws IOException{ } protected void downloadHieFile() throws IOException{ - if(mirrors.size()<1) { + if(mirrors.isEmpty()) { initScopURLs(); } IOException exception = null; @@ -719,7 +719,7 @@ protected void downloadHieFile() throws IOException{ } protected void downloadComFile() throws IOException{ - if(mirrors.size()<1) { + if(mirrors.isEmpty()) { initScopURLs(); } IOException exception = null; diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/PermutationGroup.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/PermutationGroup.java index e1f4792410..b3ac53f385 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/PermutationGroup.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/PermutationGroup.java @@ -65,7 +65,7 @@ public void completeGroup() { Set> known = new HashSet<>(permutations); //breadth-first search through the map of all members List> currentLevel = new ArrayList<>(permutations); - while( currentLevel.size() > 0) { + while(!currentLevel.isEmpty()) { List> nextLevel = new ArrayList<>(); for( List p : currentLevel) { for(List gen : gens) { diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/QuatSymmetrySubunits.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/QuatSymmetrySubunits.java index b0bef7f1e6..7f434cabaf 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/QuatSymmetrySubunits.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/QuatSymmetrySubunits.java @@ -211,7 +211,7 @@ public MomentsOfInertia getMomentsOfInertia() { } private void run() { - if (centers.size() > 0) { + if (!centers.isEmpty()) { return; } calcOriginalCenters(); diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/RotationGroup.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/RotationGroup.java index 1490d27036..002d046e52 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/RotationGroup.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/core/RotationGroup.java @@ -83,7 +83,7 @@ public void removeRotation(int index) { public void complete() { if (modified) { - if (rotations.size() > 0) { + if (!rotations.isEmpty()) { findHighestOrderAxis(); setEAxis(); calcAxesDirections(); @@ -344,7 +344,7 @@ private void calcPointGroup() { // when a structure is symmetric, some subunits are below the rmsd threshold, // and some are just above the rmsd threshold int n = 0; - if (rotations.size() > 0) { + if (!rotations.isEmpty()) { n = rotations.get(0).getPermutation().size(); rotations.clear(); } diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/internal/SequenceFunctionRefiner.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/internal/SequenceFunctionRefiner.java index a6ad226698..0d52e92fef 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/internal/SequenceFunctionRefiner.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/internal/SequenceFunctionRefiner.java @@ -79,7 +79,7 @@ public static AFPChain refineSymmetry(AFPChain afpChain, Atom[] ca1, Atom[] ca2, // Refine the alignment Map Map refined = refineSymmetry(alignment, k); - if (refined.size() < 1) + if (refined.isEmpty()) throw new RefinerFailedException("Refiner returned empty alignment"); //Substitute and partition the alignment diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/internal/SymmOptimizer.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/internal/SymmOptimizer.java index d03e90080f..627908ac8b 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/internal/SymmOptimizer.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/internal/SymmOptimizer.java @@ -392,10 +392,7 @@ private boolean checkGaps() { length--; } - if (shrinkColumns.size() != 0) - return true; - else - return false; + return !shrinkColumns.isEmpty(); } /** diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/utils/BlastClustReader.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/utils/BlastClustReader.java index b2b3298157..5d1faa8754 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/utils/BlastClustReader.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/symmetry/utils/BlastClustReader.java @@ -140,7 +140,7 @@ public List> getChainIdsInEntry(String pdbId) { private void loadClusters(int sequenceIdentity) { // load clusters only once - if (clusters.size() > 0) { + if (!clusters.isEmpty()) { return; } diff --git a/biojava-structure/src/test/java/org/biojava/nbio/structure/align/util/AtomCacheTest.java b/biojava-structure/src/test/java/org/biojava/nbio/structure/align/util/AtomCacheTest.java index 073a679dbb..f2f06ed2f5 100644 --- a/biojava-structure/src/test/java/org/biojava/nbio/structure/align/util/AtomCacheTest.java +++ b/biojava-structure/src/test/java/org/biojava/nbio/structure/align/util/AtomCacheTest.java @@ -24,6 +24,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.fail; import java.io.File; @@ -408,7 +409,7 @@ public void testEmptyChemComp() throws IOException, StructureException { // should be unknown ChemComp chem = g.getChemComp(); assertNotNull(chem); - assertTrue(chem.getAtoms().size() > 0); + assertFalse(chem.getAtoms().isEmpty()); assertEquals("NON-POLYMER", chem.getType()); } finally { FileDownloadUtils.deleteDirectory(tmpCache); @@ -471,7 +472,7 @@ public void testEmptyGZChemComp() throws IOException, StructureException { // should be unknown ChemComp chem = g.getChemComp(); assertNotNull(chem); - assertTrue(chem.getAtoms().size() > 0); + assertFalse(chem.getAtoms().isEmpty()); assertEquals("NON-POLYMER", chem.getType()); } finally { FileDownloadUtils.deleteDirectory(tmpCache); diff --git a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/TestHeaderOnly.java b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/TestHeaderOnly.java index d3c9568240..f579752d4c 100644 --- a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/TestHeaderOnly.java +++ b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/TestHeaderOnly.java @@ -205,8 +205,7 @@ public boolean doSeqResHaveAtoms(Structure s) { * @return true if has any Atom(s) */ public boolean hasAtoms(Group g) { - if (g.getAtoms().size() > 0) return true; - return false; + return !g.getAtoms().isEmpty(); } /** diff --git a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/TestMMcifOrganismParsing.java b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/TestMMcifOrganismParsing.java index 8d018f6c0a..2cbdbca679 100644 --- a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/TestMMcifOrganismParsing.java +++ b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/TestMMcifOrganismParsing.java @@ -37,7 +37,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; public class TestMMcifOrganismParsing { @@ -90,7 +90,7 @@ private void checkPDB(String pdbId, String organismTaxId) throws IOException, St Structure s = StructureIO.getStructure(pdbId); assertNotNull(s.getEntityInfos()); - assertTrue(s.getEntityInfos().size() > 0); + assertFalse(s.getEntityInfos().isEmpty()); for ( EntityInfo c : s.getEntityInfos()) { if(EntityType.POLYMER.equals(c.getType())) { diff --git a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/TestSiftsParsing.java b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/TestSiftsParsing.java index 6a9f6ae93c..2b99d660d8 100644 --- a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/TestSiftsParsing.java +++ b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/TestSiftsParsing.java @@ -47,9 +47,9 @@ public void test4DIA() throws Exception { for (SiftsEntity e : entities) { //System.out.println(e.getEntityId() + " " +e.getType()); - Assert.assertTrue(e.getSegments().size() > 0); + Assert.assertFalse(e.getSegments().isEmpty()); for (SiftsSegment seg : e.getSegments()) { - Assert.assertTrue(seg.getResidues().size() > 0); + Assert.assertFalse(seg.getResidues().isEmpty()); for (SiftsResidue res : seg.getResidues()) { @@ -78,9 +78,9 @@ public void test4jn3() throws Exception { for (SiftsEntity e : entities) { //System.out.println(e.getEntityId() + " " +e.getType()); - Assert.assertTrue(e.getSegments().size() > 0); + Assert.assertFalse(e.getSegments().isEmpty()); for (SiftsSegment seg : e.getSegments()) { - Assert.assertTrue(seg.getResidues().size() > 0); + Assert.assertFalse(seg.getResidues().isEmpty()); //System.out.println(seg.getResidues().size()); //System.out.println(" Segment: " + seg.getSegId() + " " + seg.getStart() + " " + seg.getEnd()) ; @@ -125,7 +125,7 @@ public void test4DOU() throws Exception { //assertTrue(seg1.getResidues().size() == 17); for (SiftsSegment seg : e.getSegments()) { - Assert.assertTrue(seg.getResidues().size() > 0); + Assert.assertFalse(seg.getResidues().isEmpty()); //System.out.println(" Segment: " + seg.getSegId() + " " + seg.getStart() + " " + seg.getEnd() + " res. size: " + seg.getResidues().size()) ; @@ -175,7 +175,7 @@ public void test4O6W() throws Exception { //System.out.println(" Segment: " + seg1.getSegId() + " " + seg1.getStart() + " " + seg1.getEnd() + " res. size: " + seg1.getResidues().size()); //assertTrue(seg1.getResidues().size() == 17); - Assert.assertTrue(seg.getResidues().size() > 0); + Assert.assertFalse(seg.getResidues().isEmpty()); for (SiftsResidue res : seg.getResidues()) { diff --git a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/cif/CifFileConsumerImplTest.java b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/cif/CifFileConsumerImplTest.java index a8925afa88..8fbd1060b0 100644 --- a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/cif/CifFileConsumerImplTest.java +++ b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/cif/CifFileConsumerImplTest.java @@ -147,7 +147,7 @@ public void testWaterOnlyChainCif() throws IOException { Chain c = s2.getWaterChainByPDB("F"); assertNotNull("Got null when looking for water-only chain with author id F", c); - assertTrue(c.getAtomGroups().size() > 0); + assertFalse(c.getAtomGroups().isEmpty()); // checking that compounds are linked assertNotNull(c.getEntityInfo()); @@ -157,7 +157,7 @@ public void testWaterOnlyChainCif() throws IOException { Chain cAsymId = s2.getWaterChain("E"); assertNotNull("Got null when looking for water-only chain with asym id E", cAsymId); - assertTrue(cAsymId.getAtomGroups().size() > 0); + assertFalse(cAsymId.getAtomGroups().isEmpty()); assertSame(c, cAsymId); } diff --git a/biojava-survival/src/main/java/org/biojava/nbio/survival/cox/CoxInfo.java b/biojava-survival/src/main/java/org/biojava/nbio/survival/cox/CoxInfo.java index c9f4f18056..eebdaf86ba 100644 --- a/biojava-survival/src/main/java/org/biojava/nbio/survival/cox/CoxInfo.java +++ b/biojava-survival/src/main/java/org/biojava/nbio/survival/cox/CoxInfo.java @@ -32,7 +32,7 @@ /** * Holds the results of a cox analysis where calling dump(), toString() will give an output similar to R - * @author Scooter Willis + * @author Scooter Willis */ public class CoxInfo { @@ -505,7 +505,7 @@ public String toString(String beginLine, String del, String endLine) { o = o + beginLine + endLine; - if (baselineSurvivorFunction.size() > 0) { + if (!baselineSurvivorFunction.isEmpty()) { o = o + beginLine + "Baseline Survivor Function (at predictor means)" + endLine; for (Double time : baselineSurvivorFunction.keySet()) { Double mean = baselineSurvivorFunction.get(time); diff --git a/biojava-survival/src/main/java/org/biojava/nbio/survival/cox/ResidualsCoxph.java b/biojava-survival/src/main/java/org/biojava/nbio/survival/cox/ResidualsCoxph.java index 42b34905cc..955a7c1f6d 100644 --- a/biojava-survival/src/main/java/org/biojava/nbio/survival/cox/ResidualsCoxph.java +++ b/biojava-survival/src/main/java/org/biojava/nbio/survival/cox/ResidualsCoxph.java @@ -29,7 +29,7 @@ /** * - * @author Scooter Willis + * @author Scooter Willis */ public class ResidualsCoxph { @@ -108,7 +108,7 @@ public static double[][] process(CoxInfo ci, Type type, boolean useWeighted, Arr double[] weighted = ci.getWeighted(); rr = Matrix.scale(rr, weighted); } - if (cluster != null && cluster.size() > 0) { + if (cluster != null && !cluster.isEmpty()) { rr = rowsum(rr, cluster); } diff --git a/biojava-survival/src/main/java/org/biojava/nbio/survival/data/WorkSheet.java b/biojava-survival/src/main/java/org/biojava/nbio/survival/data/WorkSheet.java index 542085942e..17ed18419a 100644 --- a/biojava-survival/src/main/java/org/biojava/nbio/survival/data/WorkSheet.java +++ b/biojava-survival/src/main/java/org/biojava/nbio/survival/data/WorkSheet.java @@ -27,7 +27,7 @@ * Need to handle very large spreadsheets of expression data so keep memory * footprint low * - * @author Scooter Willis + * @author Scooter Willis */ public class WorkSheet { @@ -1391,7 +1391,7 @@ static public WorkSheet unionWorkSheetsRowJoin(WorkSheet w1, WorkSheet w2, boole ArrayList joinedColumns = new ArrayList<>(); joinedColumns.addAll(w1DataColumns); joinedColumns.addAll(w2DataColumns); - if (!joinedColumns.contains("META_DATA") && (w1MetaDataColumns.size() > 0 || w2MetaDataColumns.size() > 0)) { + if (!joinedColumns.contains("META_DATA") && (!w1MetaDataColumns.isEmpty() || !w2MetaDataColumns.isEmpty())) { joinedColumns.add("META_DATA"); } for (String column : w1MetaDataColumns) { From ffff7687258311068742a61a57fcd9709204b538 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 23:45:13 +0000 Subject: [PATCH 10/67] Bump org.apache.logging.log4j:log4j-core from 2.23.1 to 2.25.3 Bumps org.apache.logging.log4j:log4j-core from 2.23.1 to 2.25.3. --- updated-dependencies: - dependency-name: org.apache.logging.log4j:log4j-core dependency-version: 2.25.3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e9f246202b..76397ae657 100644 --- a/pom.xml +++ b/pom.xml @@ -41,7 +41,7 @@ 512M 1.0.11 2.0.12 - 2.23.1 + 2.25.3 5.10.1 ciftools-java 7.0.1 From 01669f8a1402249451550c9b30608b592cadfb21 Mon Sep 17 00:00:00 2001 From: Michael L Heuer Date: Mon, 27 Apr 2026 05:44:25 -0500 Subject: [PATCH 11/67] Update guava dependency version to 33.6.0-jre --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e9f246202b..998512c549 100644 --- a/pom.xml +++ b/pom.xml @@ -486,7 +486,7 @@ com.google.guava guava - 33.4.0-jre + 33.6.0-jre From 97db9acd64e4d2f0861ea28c4ac0fab2c021507c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 21:44:09 +0000 Subject: [PATCH 12/67] Bump org.apache.logging.log4j:log4j-core from 2.25.3 to 2.25.4 Bumps org.apache.logging.log4j:log4j-core from 2.25.3 to 2.25.4. --- updated-dependencies: - dependency-name: org.apache.logging.log4j:log4j-core dependency-version: 2.25.4 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 76397ae657..626206a48f 100644 --- a/pom.xml +++ b/pom.xml @@ -41,7 +41,7 @@ 512M 1.0.11 2.0.12 - 2.25.3 + 2.25.4 5.10.1 ciftools-java 7.0.1 From 96de89a50dd14011596ccaf5ee1a39c0a48a3503 Mon Sep 17 00:00:00 2001 From: josemduarte Date: Tue, 5 May 2026 14:58:47 -0700 Subject: [PATCH 13/67] Duplicate plugin --- pom.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pom.xml b/pom.xml index 76397ae657..dea60d1110 100644 --- a/pom.xml +++ b/pom.xml @@ -325,12 +325,6 @@ 3.1.3 - - org.apache.maven.plugins - maven-javadoc-plugin - 3.11.2 - - org.apache.maven.plugins maven-site-plugin From 7c3909fcff05b76440632518d6350474ca4365da Mon Sep 17 00:00:00 2001 From: josemduarte Date: Tue, 5 May 2026 15:04:44 -0700 Subject: [PATCH 14/67] Adding safeguard, bug introduced in #1116 and reported in Biojava-l --- .../biojava/nbio/structure/io/cif/AbstractCifFileSupplier.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/AbstractCifFileSupplier.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/AbstractCifFileSupplier.java index 7e9d8ad7ac..6bf8af90ef 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/AbstractCifFileSupplier.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/AbstractCifFileSupplier.java @@ -309,7 +309,8 @@ public void accept(WrappedAtom wrappedAtom) { } labelEntityId.add(entityId); // see https://github.com/biojava/biojava/issues/1116 - if (chain.getEntityInfo().getType() == EntityType.POLYMER) { + // note the first condition is to safeguard and to have a default that writes labelSeqId if there's no knowledge about what's the entity type + if (chain.getEntityInfo()==null || chain.getEntityInfo().getType() == EntityType.POLYMER) { labelSeqId.add(seqId); } else { labelSeqId.markNextNotPresent(); From 5d047ab428b176a437131d31171c7f779caa239e Mon Sep 17 00:00:00 2001 From: josemduarte Date: Tue, 5 May 2026 15:22:40 -0700 Subject: [PATCH 15/67] [maven-release-plugin] prepare release biojava-7.2.5 --- biojava-aa-prop/pom.xml | 6 +++--- biojava-alignment/pom.xml | 4 ++-- biojava-core/pom.xml | 2 +- biojava-genome/pom.xml | 6 +++--- biojava-integrationtest/pom.xml | 4 ++-- biojava-modfinder/pom.xml | 4 ++-- biojava-ontology/pom.xml | 2 +- biojava-protein-comparison-tool/pom.xml | 10 +++++----- biojava-protein-disorder/pom.xml | 4 ++-- biojava-structure-gui/pom.xml | 6 +++--- biojava-structure/pom.xml | 6 +++--- biojava-survival/pom.xml | 2 +- biojava-ws/pom.xml | 4 ++-- pom.xml | 4 ++-- 14 files changed, 32 insertions(+), 32 deletions(-) diff --git a/biojava-aa-prop/pom.xml b/biojava-aa-prop/pom.xml index cbc82fff7b..ca38a3aacc 100644 --- a/biojava-aa-prop/pom.xml +++ b/biojava-aa-prop/pom.xml @@ -2,7 +2,7 @@ biojava org.biojava - 7.2.5-SNAPSHOT + 7.2.5 4.0.0 biojava-aa-prop @@ -70,12 +70,12 @@ org.biojava biojava-core - 7.2.5-SNAPSHOT + 7.2.5 org.biojava biojava-structure - 7.2.5-SNAPSHOT + 7.2.5 diff --git a/biojava-alignment/pom.xml b/biojava-alignment/pom.xml index b1d0bd9097..f73fa949c0 100644 --- a/biojava-alignment/pom.xml +++ b/biojava-alignment/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.5-SNAPSHOT + 7.2.5 biojava-alignment biojava-alignment @@ -47,7 +47,7 @@ org.biojava biojava-core - 7.2.5-SNAPSHOT + 7.2.5 compile diff --git a/biojava-core/pom.xml b/biojava-core/pom.xml index 909f46f088..cb7f3b5208 100644 --- a/biojava-core/pom.xml +++ b/biojava-core/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.5-SNAPSHOT + 7.2.5 4.0.0 biojava-core diff --git a/biojava-genome/pom.xml b/biojava-genome/pom.xml index 68575bbb9b..aca993304f 100644 --- a/biojava-genome/pom.xml +++ b/biojava-genome/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.5-SNAPSHOT + 7.2.5 4.0.0 biojava-genome @@ -70,13 +70,13 @@ org.biojava biojava-core - 7.2.5-SNAPSHOT + 7.2.5 compile org.biojava biojava-alignment - 7.2.5-SNAPSHOT + 7.2.5 compile diff --git a/biojava-integrationtest/pom.xml b/biojava-integrationtest/pom.xml index 1c21373bd1..e3d4122962 100644 --- a/biojava-integrationtest/pom.xml +++ b/biojava-integrationtest/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.5-SNAPSHOT + 7.2.5 biojava-integrationtest jar @@ -40,7 +40,7 @@ org.biojava biojava-structure - 7.2.5-SNAPSHOT + 7.2.5 diff --git a/biojava-modfinder/pom.xml b/biojava-modfinder/pom.xml index e8de9e38d9..fbc0c89ad6 100644 --- a/biojava-modfinder/pom.xml +++ b/biojava-modfinder/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.5-SNAPSHOT + 7.2.5 biojava-modfinder biojava-modfinder @@ -31,7 +31,7 @@ org.biojava biojava-structure - 7.2.5-SNAPSHOT + 7.2.5 jar compile diff --git a/biojava-ontology/pom.xml b/biojava-ontology/pom.xml index 6a3fd5cbc4..f99f80e60c 100644 --- a/biojava-ontology/pom.xml +++ b/biojava-ontology/pom.xml @@ -4,7 +4,7 @@ org.biojava biojava - 7.2.5-SNAPSHOT + 7.2.5 biojava-ontology diff --git a/biojava-protein-comparison-tool/pom.xml b/biojava-protein-comparison-tool/pom.xml index cf8cd570a7..cf5b3181fc 100644 --- a/biojava-protein-comparison-tool/pom.xml +++ b/biojava-protein-comparison-tool/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.5-SNAPSHOT + 7.2.5 biojava-protein-comparison-tool @@ -36,23 +36,23 @@ org.biojava biojava-alignment - 7.2.5-SNAPSHOT + 7.2.5 org.biojava biojava-core - 7.2.5-SNAPSHOT + 7.2.5 org.biojava biojava-structure - 7.2.5-SNAPSHOT + 7.2.5 org.biojava biojava-structure-gui - 7.2.5-SNAPSHOT + 7.2.5 net.sourceforge.jmol diff --git a/biojava-protein-disorder/pom.xml b/biojava-protein-disorder/pom.xml index 7273bbff8f..5fe23d98ea 100644 --- a/biojava-protein-disorder/pom.xml +++ b/biojava-protein-disorder/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.5-SNAPSHOT + 7.2.5 biojava-protein-disorder jar @@ -63,7 +63,7 @@ org.biojava biojava-core - 7.2.5-SNAPSHOT + 7.2.5 diff --git a/biojava-structure-gui/pom.xml b/biojava-structure-gui/pom.xml index 581b8a53cc..36d676b671 100644 --- a/biojava-structure-gui/pom.xml +++ b/biojava-structure-gui/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.5-SNAPSHOT + 7.2.5 4.0.0 biojava-structure-gui @@ -27,13 +27,13 @@ org.biojava biojava-structure - 7.2.5-SNAPSHOT + 7.2.5 compile org.biojava biojava-core - 7.2.5-SNAPSHOT + 7.2.5 compile diff --git a/biojava-structure/pom.xml b/biojava-structure/pom.xml index 7fbc64a3c1..6c70ab9c58 100644 --- a/biojava-structure/pom.xml +++ b/biojava-structure/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.5-SNAPSHOT + 7.2.5 biojava-structure biojava-structure @@ -51,13 +51,13 @@ org.biojava biojava-alignment - 7.2.5-SNAPSHOT + 7.2.5 compile org.biojava biojava-core - 7.2.5-SNAPSHOT + 7.2.5 compile diff --git a/biojava-survival/pom.xml b/biojava-survival/pom.xml index 88f901cdea..e4ae967a4e 100644 --- a/biojava-survival/pom.xml +++ b/biojava-survival/pom.xml @@ -4,7 +4,7 @@ org.biojava biojava - 7.2.5-SNAPSHOT + 7.2.5 biojava-survival diff --git a/biojava-ws/pom.xml b/biojava-ws/pom.xml index 7e960b4445..920c77414d 100644 --- a/biojava-ws/pom.xml +++ b/biojava-ws/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.5-SNAPSHOT + 7.2.5 biojava-ws biojava-ws @@ -19,7 +19,7 @@ org.biojava biojava-core - 7.2.5-SNAPSHOT + 7.2.5 compile diff --git a/pom.xml b/pom.xml index 581c5423be..94ef570778 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ org.biojava biojava pom - 7.2.5-SNAPSHOT + 7.2.5 biojava BioJava is an open-source project dedicated to providing a Java framework for processing biological data. It provides analytical and statistical routines, parsers for common file formats and allows the @@ -51,7 +51,7 @@ scm:git:git@github.com:biojava/biojava.git https://github.com/biojava/biojava - HEAD + biojava-7.2.5 diff --git a/biojava-alignment/pom.xml b/biojava-alignment/pom.xml index f73fa949c0..6f57e23426 100644 --- a/biojava-alignment/pom.xml +++ b/biojava-alignment/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.5 + 7.2.6-SNAPSHOT biojava-alignment biojava-alignment @@ -47,7 +47,7 @@ org.biojava biojava-core - 7.2.5 + 7.2.6-SNAPSHOT compile diff --git a/biojava-core/pom.xml b/biojava-core/pom.xml index cb7f3b5208..3ad7189c9e 100644 --- a/biojava-core/pom.xml +++ b/biojava-core/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.5 + 7.2.6-SNAPSHOT 4.0.0 biojava-core diff --git a/biojava-genome/pom.xml b/biojava-genome/pom.xml index aca993304f..698fa280c7 100644 --- a/biojava-genome/pom.xml +++ b/biojava-genome/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.5 + 7.2.6-SNAPSHOT 4.0.0 biojava-genome @@ -70,13 +70,13 @@ org.biojava biojava-core - 7.2.5 + 7.2.6-SNAPSHOT compile org.biojava biojava-alignment - 7.2.5 + 7.2.6-SNAPSHOT compile diff --git a/biojava-integrationtest/pom.xml b/biojava-integrationtest/pom.xml index e3d4122962..349393cfa6 100644 --- a/biojava-integrationtest/pom.xml +++ b/biojava-integrationtest/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.5 + 7.2.6-SNAPSHOT biojava-integrationtest jar @@ -40,7 +40,7 @@ org.biojava biojava-structure - 7.2.5 + 7.2.6-SNAPSHOT diff --git a/biojava-modfinder/pom.xml b/biojava-modfinder/pom.xml index fbc0c89ad6..cec4445407 100644 --- a/biojava-modfinder/pom.xml +++ b/biojava-modfinder/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.5 + 7.2.6-SNAPSHOT biojava-modfinder biojava-modfinder @@ -31,7 +31,7 @@ org.biojava biojava-structure - 7.2.5 + 7.2.6-SNAPSHOT jar compile diff --git a/biojava-ontology/pom.xml b/biojava-ontology/pom.xml index f99f80e60c..dff3450bc9 100644 --- a/biojava-ontology/pom.xml +++ b/biojava-ontology/pom.xml @@ -4,7 +4,7 @@ org.biojava biojava - 7.2.5 + 7.2.6-SNAPSHOT biojava-ontology diff --git a/biojava-protein-comparison-tool/pom.xml b/biojava-protein-comparison-tool/pom.xml index cf5b3181fc..3b43bc492d 100644 --- a/biojava-protein-comparison-tool/pom.xml +++ b/biojava-protein-comparison-tool/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.5 + 7.2.6-SNAPSHOT biojava-protein-comparison-tool @@ -36,23 +36,23 @@ org.biojava biojava-alignment - 7.2.5 + 7.2.6-SNAPSHOT org.biojava biojava-core - 7.2.5 + 7.2.6-SNAPSHOT org.biojava biojava-structure - 7.2.5 + 7.2.6-SNAPSHOT org.biojava biojava-structure-gui - 7.2.5 + 7.2.6-SNAPSHOT net.sourceforge.jmol diff --git a/biojava-protein-disorder/pom.xml b/biojava-protein-disorder/pom.xml index 5fe23d98ea..d0267a7519 100644 --- a/biojava-protein-disorder/pom.xml +++ b/biojava-protein-disorder/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.5 + 7.2.6-SNAPSHOT biojava-protein-disorder jar @@ -63,7 +63,7 @@ org.biojava biojava-core - 7.2.5 + 7.2.6-SNAPSHOT diff --git a/biojava-structure-gui/pom.xml b/biojava-structure-gui/pom.xml index 36d676b671..314c32bf16 100644 --- a/biojava-structure-gui/pom.xml +++ b/biojava-structure-gui/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.5 + 7.2.6-SNAPSHOT 4.0.0 biojava-structure-gui @@ -27,13 +27,13 @@ org.biojava biojava-structure - 7.2.5 + 7.2.6-SNAPSHOT compile org.biojava biojava-core - 7.2.5 + 7.2.6-SNAPSHOT compile diff --git a/biojava-structure/pom.xml b/biojava-structure/pom.xml index 6c70ab9c58..c80a8490bd 100644 --- a/biojava-structure/pom.xml +++ b/biojava-structure/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.5 + 7.2.6-SNAPSHOT biojava-structure biojava-structure @@ -51,13 +51,13 @@ org.biojava biojava-alignment - 7.2.5 + 7.2.6-SNAPSHOT compile org.biojava biojava-core - 7.2.5 + 7.2.6-SNAPSHOT compile diff --git a/biojava-survival/pom.xml b/biojava-survival/pom.xml index e4ae967a4e..a8c8616fa1 100644 --- a/biojava-survival/pom.xml +++ b/biojava-survival/pom.xml @@ -4,7 +4,7 @@ org.biojava biojava - 7.2.5 + 7.2.6-SNAPSHOT biojava-survival diff --git a/biojava-ws/pom.xml b/biojava-ws/pom.xml index 920c77414d..eafd87dff8 100644 --- a/biojava-ws/pom.xml +++ b/biojava-ws/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.5 + 7.2.6-SNAPSHOT biojava-ws biojava-ws @@ -19,7 +19,7 @@ org.biojava biojava-core - 7.2.5 + 7.2.6-SNAPSHOT compile diff --git a/pom.xml b/pom.xml index 94ef570778..9fae069ae0 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ org.biojava biojava pom - 7.2.5 + 7.2.6-SNAPSHOT biojava BioJava is an open-source project dedicated to providing a Java framework for processing biological data. It provides analytical and statistical routines, parsers for common file formats and allows the @@ -51,7 +51,7 @@ scm:git:git@github.com:biojava/biojava.git https://github.com/biojava/biojava - biojava-7.2.5 + HEAD From 60450a29e04da5722c90cfb614baa691697e64db Mon Sep 17 00:00:00 2001 From: Sergey Pyatykh Date: Wed, 27 May 2026 09:15:50 +0200 Subject: [PATCH 18/67] Issue 1038 - Test was refactored by using junit5 features --- biojava-genome/pom.xml | 12 + .../nbio/genome/io/fastq/FastqToolsTest.java | 415 ++++++------------ 2 files changed, 143 insertions(+), 284 deletions(-) diff --git a/biojava-genome/pom.xml b/biojava-genome/pom.xml index 698fa280c7..8974fdbfa3 100644 --- a/biojava-genome/pom.xml +++ b/biojava-genome/pom.xml @@ -67,6 +67,18 @@ junit test + + org.junit.jupiter + junit-jupiter-engine + + + org.junit.jupiter + junit-jupiter-params + + + org.junit.vintage + junit-vintage-engine + org.biojava biojava-core diff --git a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/FastqToolsTest.java b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/FastqToolsTest.java index 469601e624..43672b517b 100644 --- a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/FastqToolsTest.java +++ b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/FastqToolsTest.java @@ -27,493 +27,340 @@ import org.biojava.nbio.core.sequence.features.QualityFeature; import org.biojava.nbio.core.sequence.features.QuantityFeature; import org.biojava.nbio.core.sequence.template.AbstractSequence; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; /** * Unit test for FastqTools. */ -public final class FastqToolsTest { +final class FastqToolsTest { private final FastqBuilder builder = new FastqBuilder().withDescription("foo").withSequence("ACTG").withQuality("ZZZZ"); @Test - public void testCreateDNASequence() throws CompoundNotFoundException + void testCreateDNASequence() throws CompoundNotFoundException { DNASequence sequence = FastqTools.createDNASequence(builder.build()); - Assert.assertNotNull(sequence); + Assertions.assertNotNull(sequence); } @Test - public void testCreateDNASequenceNullFastq() throws CompoundNotFoundException + void testCreateDNASequenceNullFastq() { - try - { - FastqTools.createDNASequence(null); - Assert.fail("createDNASequence(null) expected IllegalArgumentException"); - } - catch (IllegalArgumentException e) - { - // expected - } + Assertions.assertThrows(IllegalArgumentException.class, () -> FastqTools.createDNASequence(null)); } @Test - public void testCreateDNASequenceWithQualityScores() throws CompoundNotFoundException + void testCreateDNASequenceWithQualityScores() throws CompoundNotFoundException { DNASequence sequence = FastqTools.createDNASequenceWithQualityScores(builder.build()); - Assert.assertNotNull(sequence); + Assertions.assertNotNull(sequence); List, NucleotideCompound>> features = sequence.getFeaturesByType("qualityScores"); - Assert.assertNotNull(features); - Assert.assertEquals(1, features.size()); + Assertions.assertNotNull(features); + Assertions.assertEquals(1, features.size()); QualityFeature, NucleotideCompound> qualityScores = (QualityFeature, NucleotideCompound>) features.get(0); - Assert.assertEquals(sequence.getLength(), qualityScores.getQualities().size()); - Assert.assertEquals(sequence.getLength(), qualityScores.getLocations().getLength()); + Assertions.assertEquals(sequence.getLength(), qualityScores.getQualities().size()); + Assertions.assertEquals(sequence.getLength(), qualityScores.getLocations().getLength()); } @Test - public void testCreateDNASequenceWithQualityScoresNullFastq() throws CompoundNotFoundException + void testCreateDNASequenceWithQualityScoresNullFastq() { - try - { - FastqTools.createDNASequenceWithQualityScores(null); - Assert.fail("createDNASequenceWithQualityScores(null) expected IllegalArgumentException"); - } - catch (IllegalArgumentException e) - { - // expected - } + Assertions.assertThrows(IllegalArgumentException.class, () -> FastqTools.createDNASequenceWithQualityScores(null)); } @Test - public void testCreateDNASequenceWithErrorProbabilies() throws CompoundNotFoundException + void testCreateDNASequenceWithErrorProbabilies() throws CompoundNotFoundException { DNASequence sequence = FastqTools.createDNASequenceWithErrorProbabilities(builder.build()); - Assert.assertNotNull(sequence); + Assertions.assertNotNull(sequence); List, NucleotideCompound>> features = sequence.getFeaturesByType("errorProbabilities"); - Assert.assertNotNull(features); - Assert.assertEquals(1, features.size()); + Assertions.assertNotNull(features); + Assertions.assertEquals(1, features.size()); QuantityFeature, NucleotideCompound> errorProbabilities = (QuantityFeature, NucleotideCompound>) features.get(0); - Assert.assertEquals(sequence.getLength(), errorProbabilities.getQuantities().size()); - Assert.assertEquals(sequence.getLength(), errorProbabilities.getLocations().getLength()); + Assertions.assertEquals(sequence.getLength(), errorProbabilities.getQuantities().size()); + Assertions.assertEquals(sequence.getLength(), errorProbabilities.getLocations().getLength()); } @Test - public void testCreateDNASequenceWithErrorProbabilitiesNullFastq() throws CompoundNotFoundException + void testCreateDNASequenceWithErrorProbabilitiesNullFastq() { - try - { - FastqTools.createDNASequenceWithErrorProbabilities(null); - Assert.fail("createDNASequenceWithErrorProbabilities(null) expected IllegalArgumentException"); - } - catch (IllegalArgumentException e) - { - // expected - } + Assertions.assertThrows(IllegalArgumentException.class, () -> FastqTools.createDNASequenceWithErrorProbabilities(null)); } @Test - public void testCreateDNASequenceWithQualityScoresAndErrorProbabilities() throws CompoundNotFoundException + void testCreateDNASequenceWithQualityScoresAndErrorProbabilities() throws CompoundNotFoundException { DNASequence sequence = FastqTools.createDNASequenceWithQualityScoresAndErrorProbabilities(builder.build()); - Assert.assertNotNull(sequence); + Assertions.assertNotNull(sequence); List, NucleotideCompound>> qualityScoresFeatures = sequence.getFeaturesByType("qualityScores"); - Assert.assertNotNull(qualityScoresFeatures); - Assert.assertEquals(1, qualityScoresFeatures.size()); + Assertions.assertNotNull(qualityScoresFeatures); + Assertions.assertEquals(1, qualityScoresFeatures.size()); QualityFeature, NucleotideCompound> qualityScores = (QualityFeature, NucleotideCompound>) qualityScoresFeatures.get(0); - Assert.assertEquals(sequence.getLength(), qualityScores.getQualities().size()); - Assert.assertEquals(sequence.getLength(), qualityScores.getLocations().getLength()); + Assertions.assertEquals(sequence.getLength(), qualityScores.getQualities().size()); + Assertions.assertEquals(sequence.getLength(), qualityScores.getLocations().getLength()); List, NucleotideCompound>> errorProbabilitiesFeatures = sequence.getFeaturesByType("errorProbabilities"); - Assert.assertNotNull(errorProbabilitiesFeatures); - Assert.assertEquals(1, errorProbabilitiesFeatures.size()); + Assertions.assertNotNull(errorProbabilitiesFeatures); + Assertions.assertEquals(1, errorProbabilitiesFeatures.size()); QuantityFeature, NucleotideCompound> errorProbabilities = (QuantityFeature, NucleotideCompound>) errorProbabilitiesFeatures.get(0); - Assert.assertEquals(sequence.getLength(), errorProbabilities.getQuantities().size()); - Assert.assertEquals(sequence.getLength(), errorProbabilities.getLocations().getLength()); + Assertions.assertEquals(sequence.getLength(), errorProbabilities.getQuantities().size()); + Assertions.assertEquals(sequence.getLength(), errorProbabilities.getLocations().getLength()); } @Test - public void testCreateDNASequenceWithQualityScoresAndErrorProbabilitiesNullFastq() throws CompoundNotFoundException + void testCreateDNASequenceWithQualityScoresAndErrorProbabilitiesNullFastq() { - try - { - FastqTools.createDNASequenceWithQualityScoresAndErrorProbabilities(null); - Assert.fail("createDNASequenceWithQualityScoresAndErrorProbabilities(null) expected IllegalArgumentException"); - } - catch (IllegalArgumentException e) - { - // expected - } + Assertions.assertThrows(IllegalArgumentException.class, () -> FastqTools.createDNASequenceWithQualityScoresAndErrorProbabilities(null)); } @Test - public void testCreateQualityScores() + void testCreateQualityScores() { Fastq fastq = builder.build(); QualityFeature, NucleotideCompound> qualityScores = FastqTools.createQualityScores(fastq); - Assert.assertNotNull(qualityScores); - Assert.assertEquals(fastq.getSequence().length(), qualityScores.getQualities().size()); + Assertions.assertNotNull(qualityScores); + Assertions.assertEquals(fastq.getSequence().length(), qualityScores.getQualities().size()); } @Test - public void testCreateQualityScoresNullFastq() + void testCreateQualityScoresNullFastq() { - try - { - FastqTools.createQualityScores(null); - Assert.fail("createQualityScores(null) expected IllegalArgumentException"); - } - catch (IllegalArgumentException e) - { - // expected - } + Assertions.assertThrows(IllegalArgumentException.class, () -> FastqTools.createQualityScores(null)); } @Test - public void testCreateErrorProbabilities() + void testCreateErrorProbabilities() { Fastq fastq = builder.build(); QuantityFeature, NucleotideCompound> errorProbabilities = FastqTools.createErrorProbabilities(fastq); - Assert.assertNotNull(errorProbabilities); - Assert.assertEquals(fastq.getSequence().length(), errorProbabilities.getQuantities().size()); + Assertions.assertNotNull(errorProbabilities); + Assertions.assertEquals(fastq.getSequence().length(), errorProbabilities.getQuantities().size()); } @Test - public void testCreateErrorProbabilitiesNullFastq() + void testCreateErrorProbabilitiesNullFastq() { - try - { - FastqTools.createErrorProbabilities(null); - Assert.fail("createErrorProbabilities(null) expected IllegalArgumentException"); - } - catch (IllegalArgumentException e) - { - // expected - } + Assertions.assertThrows(IllegalArgumentException.class, () -> FastqTools.createErrorProbabilities(null)); } @Test - public void testQualityScores() + void testQualityScores() { Iterable qualityScores = FastqTools.qualityScores(builder.build()); - Assert.assertNotNull(qualityScores); - int count = 0; - for (Number qualityScore : qualityScores) - { - Assert.assertNotNull(qualityScore); - count++; - } - Assert.assertEquals(4, count); + List scoresList = StreamSupport.stream(qualityScores.spliterator(), false) + .collect(Collectors.toList()); + Assertions.assertAll( + () -> Assertions.assertEquals(4, scoresList.size()), + () -> Assertions.assertFalse(scoresList.contains(null)) + ); } @Test - public void testQualityScoresNullFastq() + void testQualityScoresNullFastq() { - try - { - FastqTools.qualityScores(null); - Assert.fail("qualityScores(null) expected IllegalArgumentException"); - } - catch (IllegalArgumentException e) - { - // expected - } + Assertions.assertThrows(IllegalArgumentException.class, () -> FastqTools.qualityScores(null)); } @Test - public void testQualityScoresIntArray() + void testQualityScoresIntArray() { int[] qualityScores = new int[4]; FastqTools.qualityScores(builder.build(), qualityScores); - for (int i = 0; i < 4; i++) - { - Assert.assertTrue(qualityScores[i] != 0); - } + + Assertions.assertTrue(Arrays.stream(qualityScores).allMatch(score -> score != 0), () -> + "Array contains zero at some position: " + Arrays.toString(qualityScores)); } @Test - public void testQualityScoresIntArrayNullFastq() + void testQualityScoresIntArrayNullFastq() { - try - { - FastqTools.qualityScores(null, new int[0]); - Assert.fail("qualityScores(null, int[]) expected IllegalArgumentException"); - } - catch (IllegalArgumentException e) - { - // expected - } + Assertions.assertThrows(IllegalArgumentException.class, () -> FastqTools.qualityScores(null, new int[0])); } @Test - public void testQualityScoresNullIntArray() + void testQualityScoresNullIntArray() { - try - { - FastqTools.qualityScores(builder.build(), null); - Assert.fail("qualityScores(fastq, null) expected IllegalArgumentException"); - } - catch (IllegalArgumentException e) - { - // expected - } + Fastq fastq = builder.build(); + Assertions.assertThrows(IllegalArgumentException.class, () -> FastqTools.qualityScores(fastq, null)); } @Test - public void testQualityScoresQualityScoresTooSmall() + void testQualityScoresQualityScoresTooSmall() { - try - { - FastqTools.qualityScores(builder.build(), new int[3]); - Assert.fail("expected IllegalArgumentException"); - } - catch (IllegalArgumentException e) - { - // expected - } + Fastq fastq = builder.build(); + Assertions.assertThrows(IllegalArgumentException.class, () -> FastqTools.qualityScores(fastq, new int[3])); } @Test - public void testQualityScoresQualityScoresTooLarge() + void testQualityScoresQualityScoresTooLarge() { - try - { - FastqTools.qualityScores(builder.build(), new int[5]); - Assert.fail("expected IllegalArgumentException"); - } - catch (IllegalArgumentException e) - { - // expected - } + Fastq fastq = builder.build(); + Assertions.assertThrows(IllegalArgumentException.class, () -> FastqTools.qualityScores(fastq, new int[5])); } @Test - public void testErrorProbabilities() + void testErrorProbabilities() { Iterable errorProbabilities = FastqTools.errorProbabilities(builder.build()); - Assert.assertNotNull(errorProbabilities); - int count = 0; - for (Number errorProbability : errorProbabilities) - { - Assert.assertNotNull(errorProbability); - count++; - } - Assert.assertEquals(4, count); + List scores = StreamSupport.stream(errorProbabilities.spliterator(), false) + .collect(Collectors.toList()); + + Assertions.assertNotNull(scores); + Assertions.assertEquals(4, scores.size()); + Assertions.assertTrue(scores.stream().allMatch(Objects::nonNull)); } @Test - public void testErrorProbabilitiesNullFastq() + void testErrorProbabilitiesNullFastq() { - try - { - FastqTools.errorProbabilities(null); - Assert.fail("errorProbabilities(null) expected IllegalArgumentException"); - } - catch (IllegalArgumentException e) - { - // expected - } + Assertions.assertThrows(IllegalArgumentException.class, () -> FastqTools.errorProbabilities(null)); } @Test - public void testErrorProbabilitiesDoubleArray() + void testErrorProbabilitiesDoubleArray() { double[] errorProbabilities = new double[4]; FastqTools.errorProbabilities(builder.build(), errorProbabilities); - for (int i = 0; i < 0; i++) - { - Assert.assertTrue(errorProbabilities[i] > 0.0d); - } + Assertions.assertTrue( + Arrays.stream(errorProbabilities).allMatch(p -> p > 0.0), + () -> "Expected all probabilities to be > 0.0, but got: " + Arrays.toString(errorProbabilities) + ); } @Test - public void testErrorProbabilitiesDoubleArrayNullFastq() + void testErrorProbabilitiesDoubleArrayNullFastq() { - try - { - FastqTools.errorProbabilities(null, new double[0]); - Assert.fail("errorProbabilities(null, double[]) expected IllegalArgumentException"); - } - catch (IllegalArgumentException e) - { - // expected - } + Assertions.assertThrows(IllegalArgumentException.class, () -> FastqTools.errorProbabilities(null, new double[0])); } @Test - public void testErrorProbabilitiesNullErrorProbabilities() + void testErrorProbabilitiesNullErrorProbabilities() { - try - { - FastqTools.errorProbabilities(builder.build(), null); - Assert.fail("errorProbabilities(fastq, null) expected IllegalArgumentException"); - } - catch (IllegalArgumentException e) - { - // expected - } + Fastq fastq = builder.build(); + Assertions.assertThrows(IllegalArgumentException.class, () -> FastqTools.errorProbabilities(fastq, null)); } @Test - public void testErrorProbabilitiesErrorProbabilitiesTooSmall() + void testErrorProbabilitiesErrorProbabilitiesTooSmall() { - try - { - FastqTools.errorProbabilities(builder.build(), new double[3]); - Assert.fail("expected IllegalArgumentException"); - } - catch (IllegalArgumentException e) - { - // expected - } + Fastq fastq = builder.build(); + Assertions.assertThrows(IllegalArgumentException.class, () -> FastqTools.errorProbabilities(fastq, new double[3])); } @Test - public void testErrorProbabilitiesErrorProbabilitiesTooLarge() + void testErrorProbabilitiesErrorProbabilitiesTooLarge() { - try - { - FastqTools.errorProbabilities(builder.build(), new double[5]); - Assert.fail("expected IllegalArgumentException"); - } - catch (IllegalArgumentException e) - { - // expected - } + Fastq fastq = builder.build(); + Assertions.assertThrows(IllegalArgumentException.class, () -> FastqTools.errorProbabilities(fastq, new double[5])); } @Test - public void testConvertNullFastq() + void testConvertNullFastq() { - try - { - FastqTools.convert(null, FastqVariant.FASTQ_SANGER); - Assert.fail("expected IllegalArgumentException"); - } - catch (IllegalArgumentException e) - { - // expected - } + Assertions.assertThrows(IllegalArgumentException.class, () -> FastqTools.convert(null, FastqVariant.FASTQ_SANGER)); } @Test - public void testConvertNullVariant() + void testConvertNullVariant() { - try - { - FastqTools.convert(builder.build(), null); - Assert.fail("expected IllegalArgumentException"); - } - catch (IllegalArgumentException e) - { - // expected - } + Fastq fastq = builder.build(); + Assertions.assertThrows(IllegalArgumentException.class, () -> FastqTools.convert(fastq, null)); } @Test - public void testConvertSameVariant() + void testConvertSameVariant() { Fastq fastq = builder.build(); - Assert.assertEquals(fastq, FastqTools.convert(fastq, fastq.getVariant())); + Assertions.assertEquals(fastq, FastqTools.convert(fastq, fastq.getVariant())); } @Test - public void testConvertQualitiesNullFastq() + void testConvertQualitiesNullFastq() { - try - { - FastqTools.convertQualities(null, FastqVariant.FASTQ_SANGER); - Assert.fail("expected IllegalArgumentException"); - } - catch (IllegalArgumentException e) - { - // expected - } + Assertions.assertThrows(IllegalArgumentException.class, () -> FastqTools.convertQualities(null, FastqVariant.FASTQ_SANGER)); } @Test - public void testConvertQualitiesNullVariant() + void testConvertQualitiesNullVariant() { - try - { - FastqTools.convertQualities(builder.build(), null); - Assert.fail("expected IllegalArgumentException"); - } - catch (IllegalArgumentException e) - { - // expected - } + Fastq fastq = builder.build(); + Assertions.assertThrows(IllegalArgumentException.class, () -> FastqTools.convertQualities(fastq, null)); } @Test - public void testConvertQualitiesSameVariant() + void testConvertQualitiesSameVariant() { Fastq fastq = builder.build(); - Assert.assertEquals(fastq.getQuality(), FastqTools.convertQualities(fastq, fastq.getVariant())); + Assertions.assertEquals(fastq.getQuality(), FastqTools.convertQualities(fastq, fastq.getVariant())); } @Test - public void testConvertQualitiesSangerToSolexa() + void testConvertQualitiesSangerToSolexa() { Fastq fastq = builder.build(); - Assert.assertEquals("yyyy", FastqTools.convertQualities(fastq, FastqVariant.FASTQ_SOLEXA)); + Assertions.assertEquals("yyyy", FastqTools.convertQualities(fastq, FastqVariant.FASTQ_SOLEXA)); } @Test - public void testConvertQualitiesSangerToIllumina() + void testConvertQualitiesSangerToIllumina() { Fastq fastq = builder.build(); - Assert.assertEquals("yyyy", FastqTools.convertQualities(fastq, FastqVariant.FASTQ_ILLUMINA)); + Assertions.assertEquals("yyyy", FastqTools.convertQualities(fastq, FastqVariant.FASTQ_ILLUMINA)); } @Test - public void testConvertQualitiesSolexaToSanger() + void testConvertQualitiesSolexaToSanger() { Fastq fastq = builder.withVariant(FastqVariant.FASTQ_SOLEXA).build(); - Assert.assertEquals(";;;;", FastqTools.convertQualities(fastq, FastqVariant.FASTQ_SANGER)); + Assertions.assertEquals(";;;;", FastqTools.convertQualities(fastq, FastqVariant.FASTQ_SANGER)); } @Test - public void testConvertQualitiesIlluminaToSanger() + void testConvertQualitiesIlluminaToSanger() { Fastq fastq = builder.withVariant(FastqVariant.FASTQ_ILLUMINA).build(); - Assert.assertEquals(";;;;", FastqTools.convertQualities(fastq, FastqVariant.FASTQ_SANGER)); + Assertions.assertEquals(";;;;", FastqTools.convertQualities(fastq, FastqVariant.FASTQ_SANGER)); } @Test - public void testConvertQualitiesSolexaToIllumina() + void testConvertQualitiesSolexaToIllumina() { Fastq fastq = builder.withVariant(FastqVariant.FASTQ_SOLEXA).build(); - Assert.assertEquals("ZZZZ", FastqTools.convertQualities(fastq, FastqVariant.FASTQ_ILLUMINA)); + Assertions.assertEquals("ZZZZ", FastqTools.convertQualities(fastq, FastqVariant.FASTQ_ILLUMINA)); } @Test - public void testConvertQualitiesIlluminaToSolexa() + void testConvertQualitiesIlluminaToSolexa() { Fastq fastq = builder.withVariant(FastqVariant.FASTQ_ILLUMINA).build(); - Assert.assertEquals("ZZZZ", FastqTools.convertQualities(fastq, FastqVariant.FASTQ_SOLEXA)); + Assertions.assertEquals("ZZZZ", FastqTools.convertQualities(fastq, FastqVariant.FASTQ_SOLEXA)); } @Test - public void testToList() + void testToList() { - List list = new ArrayList(); - Assert.assertSame(list, FastqTools.toList(list)); + List list = new ArrayList<>(); + Assertions.assertSame(list, FastqTools.toList(list)); } @Test - public void testToListNotAList() + void testToListNotAList() { - Collection collection = new HashSet(); - Assert.assertTrue(FastqTools.toList(collection) instanceof List); - Assert.assertNotSame(collection, FastqTools.toList(collection)); + Collection collection = new HashSet<>(); + Assertions.assertTrue(FastqTools.toList(collection) instanceof List); + Assertions.assertNotSame(collection, FastqTools.toList(collection)); } } From bcd2e0a602cf12ae0d8d3ae633889708cabd917e Mon Sep 17 00:00:00 2001 From: Sergey Pyatykh Date: Wed, 27 May 2026 09:30:53 +0200 Subject: [PATCH 19/67] All tests in the biojava-genome module were migrated to junit5 --- biojava-genome/pom.xml | 9 -- .../biojava/nbio/genome/FeatureListTest.java | 12 +- .../nbio/genome/GeneFeatureHelperTest.java | 24 +-- .../genome/TestChromosomeMappingTools.java | 17 +- .../nbio/genome/TestGenomeMapping.java | 26 +-- .../org/biojava/nbio/genome/TestIssue355.java | 17 +- .../org/biojava/nbio/genome/TestLocation.java | 67 ++++---- .../io/fastq/AbstractFastqReaderTest.java | 82 +++++----- .../io/fastq/AbstractFastqWriterTest.java | 48 +++--- .../nbio/genome/io/fastq/ConvertTest.java | 19 ++- .../genome/io/fastq/FastqBuilderTest.java | 153 +++++++++--------- .../nbio/genome/io/fastq/FastqTest.java | 81 +++++----- .../genome/io/fastq/FastqVariantTest.java | 58 +++---- .../io/fastq/IlluminaFastqReaderTest.java | 58 +++---- .../io/fastq/IlluminaFastqWriterTest.java | 6 +- .../io/fastq/SangerFastqReaderTest.java | 97 ++++++----- .../io/fastq/SangerFastqWriterTest.java | 6 +- .../io/fastq/SolexaFastqReaderTest.java | 59 ++++--- .../io/fastq/SolexaFastqWriterTest.java | 6 +- .../io/fastq/StreamingFastqParserTest.java | 18 +-- 20 files changed, 426 insertions(+), 437 deletions(-) diff --git a/biojava-genome/pom.xml b/biojava-genome/pom.xml index 8974fdbfa3..c5bccfa23c 100644 --- a/biojava-genome/pom.xml +++ b/biojava-genome/pom.xml @@ -62,11 +62,6 @@ guava compile - - junit - junit - test - org.junit.jupiter junit-jupiter-engine @@ -75,10 +70,6 @@ org.junit.jupiter junit-jupiter-params - - org.junit.vintage - junit-vintage-engine - org.biojava biojava-core diff --git a/biojava-genome/src/test/java/org/biojava/nbio/genome/FeatureListTest.java b/biojava-genome/src/test/java/org/biojava/nbio/genome/FeatureListTest.java index 6e1cae5d8b..8c50e19bc2 100644 --- a/biojava-genome/src/test/java/org/biojava/nbio/genome/FeatureListTest.java +++ b/biojava-genome/src/test/java/org/biojava/nbio/genome/FeatureListTest.java @@ -26,26 +26,26 @@ import org.biojava.nbio.genome.parsers.gff.Feature; import org.biojava.nbio.genome.parsers.gff.FeatureList; import org.biojava.nbio.genome.parsers.gff.Location; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; /** * @author mckeee1 * */ -public class FeatureListTest { +class FeatureListTest { @Test - public void testAddIndex() throws Exception + void testAddIndex() throws Exception { FeatureList fl = new FeatureList(); fl.add(new Feature("seqname", "source", "type", new Location(1, 2), (double)0, 0, "gene_id \"gene_id_1\"; transcript_id \"transcript_id_1\";")); fl.addIndex("transcript_id"); - Assert.assertEquals(1, fl.selectByAttribute("transcript_id").size()); + Assertions.assertEquals(1, fl.selectByAttribute("transcript_id").size()); FeatureList f2 = new FeatureList(); f2.addIndex("transcript_id"); f2.add(new Feature("seqname", "source", "type", new Location(1, 2), (double)0, 0, "gene_id \"gene_id_1\"; transcript_id \"transcript_id_1\";")); - Assert.assertEquals(1, f2.selectByAttribute("transcript_id").size()); + Assertions.assertEquals(1, f2.selectByAttribute("transcript_id").size()); } } diff --git a/biojava-genome/src/test/java/org/biojava/nbio/genome/GeneFeatureHelperTest.java b/biojava-genome/src/test/java/org/biojava/nbio/genome/GeneFeatureHelperTest.java index 3c81c50916..9c3a8877c6 100644 --- a/biojava-genome/src/test/java/org/biojava/nbio/genome/GeneFeatureHelperTest.java +++ b/biojava-genome/src/test/java/org/biojava/nbio/genome/GeneFeatureHelperTest.java @@ -28,9 +28,9 @@ import org.biojava.nbio.core.sequence.GeneSequence; import org.biojava.nbio.core.sequence.ProteinSequence; import org.biojava.nbio.core.sequence.io.FastaWriterHelper; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,20 +45,20 @@ * * @author Scooter Willis */ -public class GeneFeatureHelperTest { +class GeneFeatureHelperTest { private static final Logger logger = LoggerFactory.getLogger(GeneFeatureHelperTest.class); - @Before + @BeforeEach public void setUp() throws Exception { } - @After + @AfterEach public void tearDown() throws Exception { } @Test - public void testZeroLocation() throws Exception { + void testZeroLocation() throws Exception { @SuppressWarnings("unused") FeatureList listGenes = GFF3Reader.read("src/test/resources/amphimedon.gff3"); @@ -71,7 +71,7 @@ public void testZeroLocation() throws Exception { */ @Test - public void testLoadFastaAddGeneFeaturesFromUpperCaseExonFastaFile() throws Exception { + void testLoadFastaAddGeneFeaturesFromUpperCaseExonFastaFile() throws Exception { // logger.info("loadFastaAddGeneFeaturesFromUpperCaseExonFastaFile"); File fastaSequenceFile = new File("src/test/resources/volvox_all.fna"); File uppercaseFastaFile = new File("src/test/resources/volvox_all_genes_exon_uppercase.fna"); @@ -93,7 +93,7 @@ public void testLoadFastaAddGeneFeaturesFromUpperCaseExonFastaFile() throws Exce * Test of outputFastaSequenceLengthGFF3 method, of class GeneFeatureHelper. */ @Test - public void testOutputFastaSequenceLengthGFF3() throws Exception { + void testOutputFastaSequenceLengthGFF3() throws Exception { // logger.info("outputFastaSequenceLengthGFF3"); File fastaSequenceFile = new File("src/test/resources/volvox_all.fna"); @@ -112,7 +112,7 @@ public void testOutputFastaSequenceLengthGFF3() throws Exception { */ @Test - public void testAddGFF3Note() throws Exception { + void testAddGFF3Note() throws Exception { Map chromosomeSequenceList = GeneFeatureHelper .loadFastaAddGeneFeaturesFromGmodGFF3(new File("src/test/resources/volvox_all.fna"), new File( "src/test/resources/volvox.gff3"), false); @@ -128,7 +128,7 @@ public void testAddGFF3Note() throws Exception { * output. */ @Test - public void testGetProteinSequences() throws Exception { + void testGetProteinSequences() throws Exception { Map chromosomeSequenceList = GeneFeatureHelper .loadFastaAddGeneFeaturesFromGmodGFF3(new File("src/test/resources/volvox_all.fna"), new File( "src/test/resources/volvox.gff3"), false); @@ -148,7 +148,7 @@ public void testGetProteinSequences() throws Exception { * Test of getGeneSequences method, of class GeneFeatureHelper. */ @Test - public void testGetGeneSequences() throws Exception { + void testGetGeneSequences() throws Exception { // logger.info("getGeneSequences"); Map chromosomeSequenceList = GeneFeatureHelper .loadFastaAddGeneFeaturesFromGmodGFF3(new File("src/test/resources/volvox_all.fna"), new File( diff --git a/biojava-genome/src/test/java/org/biojava/nbio/genome/TestChromosomeMappingTools.java b/biojava-genome/src/test/java/org/biojava/nbio/genome/TestChromosomeMappingTools.java index 9ddd43357a..a86b8718c5 100644 --- a/biojava-genome/src/test/java/org/biojava/nbio/genome/TestChromosomeMappingTools.java +++ b/biojava-genome/src/test/java/org/biojava/nbio/genome/TestChromosomeMappingTools.java @@ -21,7 +21,8 @@ package org.biojava.nbio.genome; import org.biojava.nbio.genome.util.ChromosomeMappingTools; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.Arrays; @@ -32,10 +33,10 @@ /** * Created by Yana Valasatava on 8/14/17. */ -public class TestChromosomeMappingTools { +class TestChromosomeMappingTools { @Test - public void testGetCDSLengthForward() { + void testGetCDSLengthForward() { List exonStarts = new ArrayList<>(Arrays.asList(10, 30, 50, 70)); List exonEnds = new ArrayList<>(Arrays.asList(20, 40, 60, 80)); @@ -46,11 +47,11 @@ public void testGetCDSLengthForward() { ChromosomeMappingTools.setCoordinateSystem(0); int cdsTest = ChromosomeMappingTools.getCDSLengthForward(exonStarts, exonEnds, cdsStart, cdsEnd); - assertEquals(cdsDesired, cdsTest); + Assertions.assertEquals(cdsDesired, cdsTest); } @Test - public void testGetCDSLengthReverseAsc() { + void testGetCDSLengthReverseAsc() { List exonStarts = new ArrayList<>(Arrays.asList(10, 50, 70)); List exonEnds = new ArrayList<>(Arrays.asList(20, 60, 80)); @@ -61,11 +62,11 @@ public void testGetCDSLengthReverseAsc() { ChromosomeMappingTools.setCoordinateSystem(0); int cdsTest = ChromosomeMappingTools.getCDSLengthReverse(exonStarts, exonEnds, cdsStart, cdsEnd); - assertEquals(cdsDesired, cdsTest); + Assertions.assertEquals(cdsDesired, cdsTest); } @Test - public void testGetCDSLengthReverseDesc() { + void testGetCDSLengthReverseDesc() { List exonStarts = new ArrayList<>(Arrays.asList(70, 50, 10)); List exonEnds = new ArrayList<>(Arrays.asList(80, 60, 20)); @@ -76,6 +77,6 @@ public void testGetCDSLengthReverseDesc() { ChromosomeMappingTools.setCoordinateSystem(0); int cdsTest = ChromosomeMappingTools.getCDSLengthReverse(exonStarts, exonEnds, cdsStart, cdsEnd); - assertEquals(cdsDesired, cdsTest); + Assertions.assertEquals(cdsDesired, cdsTest); } } diff --git a/biojava-genome/src/test/java/org/biojava/nbio/genome/TestGenomeMapping.java b/biojava-genome/src/test/java/org/biojava/nbio/genome/TestGenomeMapping.java index 4999cfa6fb..257b88e3e1 100644 --- a/biojava-genome/src/test/java/org/biojava/nbio/genome/TestGenomeMapping.java +++ b/biojava-genome/src/test/java/org/biojava/nbio/genome/TestGenomeMapping.java @@ -22,8 +22,8 @@ import com.google.common.collect.Range; import org.biojava.nbio.genome.util.ChromosomeMappingTools; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.Arrays; @@ -32,10 +32,10 @@ /** * Created by andreas on 7/19/16. */ -public class TestGenomeMapping { +class TestGenomeMapping { @Test - public void testGenomeMappingToolGetCDSRanges(){ + void testGenomeMappingToolGetCDSRanges(){ List lst1 = new ArrayList<>(Arrays.asList( 86346823, 86352858, 86354529)); List lst2 = new ArrayList<>(Arrays.asList(86348878, 86352984, 86354692)); @@ -45,21 +45,21 @@ public void testGenomeMappingToolGetCDSRanges(){ List> result = ChromosomeMappingTools.getCDSRegions(lst1,lst2,cdsStart,cdsEnd); // makes sure the first list does not get changed; - Assert.assertEquals(86346823, (int) lst1.get(0)); + Assertions.assertEquals(86346823, (int) lst1.get(0)); - Assert.assertEquals(86348749, (int) result.get(0).lowerEndpoint()); - Assert.assertEquals(86352858, (int) result.get(1).lowerEndpoint()); - Assert.assertEquals(86354529, (int) result.get(2).lowerEndpoint()); + Assertions.assertEquals(86348749, (int) result.get(0).lowerEndpoint()); + Assertions.assertEquals(86352858, (int) result.get(1).lowerEndpoint()); + Assertions.assertEquals(86354529, (int) result.get(2).lowerEndpoint()); - Assert.assertEquals(86348878, (int) result.get(0).upperEndpoint()); - Assert.assertEquals(86352984, (int) result.get(1).upperEndpoint()); - Assert.assertEquals(86387027, (int) result.get(2).upperEndpoint()); + Assertions.assertEquals(86348878, (int) result.get(0).upperEndpoint()); + Assertions.assertEquals(86352984, (int) result.get(1).upperEndpoint()); + Assertions.assertEquals(86387027, (int) result.get(2).upperEndpoint()); } @Test - public void testGenomeMappingToolGetCDSRangesSERINC2(){ + void testGenomeMappingToolGetCDSRangesSERINC2(){ List lst1 = new ArrayList<>(Arrays.asList(31413812, 31415872, 31423692)); List lst2 = new ArrayList<>(Arrays.asList(31414777, 31415907, 31423854)); @@ -69,7 +69,7 @@ public void testGenomeMappingToolGetCDSRangesSERINC2(){ List> result = ChromosomeMappingTools.getCDSRegions(lst1,lst2,cdsStart,cdsEnd); // makes sure the first list does not get changed; - Assert.assertEquals(31423818, (int) result.get(0).lowerEndpoint()); + Assertions.assertEquals(31423818, (int) result.get(0).lowerEndpoint()); } } diff --git a/biojava-genome/src/test/java/org/biojava/nbio/genome/TestIssue355.java b/biojava-genome/src/test/java/org/biojava/nbio/genome/TestIssue355.java index 5543682f17..4c98225b04 100644 --- a/biojava-genome/src/test/java/org/biojava/nbio/genome/TestIssue355.java +++ b/biojava-genome/src/test/java/org/biojava/nbio/genome/TestIssue355.java @@ -20,29 +20,28 @@ */ package org.biojava.nbio.genome; -import static org.junit.Assert.*; - import org.biojava.nbio.genome.parsers.gff.Location; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; -public class TestIssue355 { +class TestIssue355 { @Test - public void testIssue1() { + void testIssue1() { Location l1 = Location.fromBio(51227320, 51227381, '+'); Location l2 = Location.fromBio(51227323, 51227382, '+'); Location union = l1.union(l2); - assertEquals(51227320,union.bioStart()); - assertEquals(51227382,union.bioEnd()); + Assertions.assertEquals(51227320, union.bioStart()); + Assertions.assertEquals(51227382, union.bioEnd()); } @Test - public void testIssue2() { + void testIssue2() { Location l1 = Location.fromBio(100, 200, '+'); Location l2 = Location.fromBio(1, 99, '+'); Location intersection = l1.intersection(l2); - assertNull(intersection); + Assertions.assertNull(intersection); } } diff --git a/biojava-genome/src/test/java/org/biojava/nbio/genome/TestLocation.java b/biojava-genome/src/test/java/org/biojava/nbio/genome/TestLocation.java index 1289cb757e..42893cf22b 100644 --- a/biojava-genome/src/test/java/org/biojava/nbio/genome/TestLocation.java +++ b/biojava-genome/src/test/java/org/biojava/nbio/genome/TestLocation.java @@ -20,15 +20,14 @@ */ package org.biojava.nbio.genome; -import static org.junit.Assert.*; - import org.biojava.nbio.genome.parsers.gff.Location; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; -public class TestLocation { +class TestLocation { @Test - public void testLocation() { + void testLocation() { // tests taken from Location.main() //Location p3_7= new Location( 3, 7 ); @@ -49,70 +48,70 @@ public void testLocation() { Location r5_8= new Location( 5, 8 ); //distance - assertEquals(7, L(14,14).distance( L(3,7) )); - assertEquals(7, L(3,7).distance( L(14,14) )); - assertEquals(3, L(1,4).distance( L(7, 10) )); + Assertions.assertEquals(7, L(14,14).distance( L(3,7) )); + Assertions.assertEquals(7, L(3,7).distance( L(14,14) )); + Assertions.assertEquals(3, L(1,4).distance( L(7, 10) )); //union - assertEquals(p10_17, p10_12.union( p14_17 )); - assertEquals(p10_17, p14_17.union( p10_12 )); - assertEquals(p15_19, p15_19.union( p15_16 )); + Assertions.assertEquals(p10_17, p10_12.union( p14_17 )); + Assertions.assertEquals(p10_17, p14_17.union( p10_12 )); + Assertions.assertEquals(p15_19, p15_19.union( p15_16 )); //intersection - assertEquals(new Location( 21, 25 ), r13_17.union( r21_25 ).intersection( r21_25 )); + Assertions.assertEquals(new Location( 21, 25 ), r13_17.union( r21_25 ).intersection( r21_25 )); //isBefore - assertTrue( r2_5.isBefore( r5_8 )); - assertTrue( !r2_5.isBefore( r4_7 )); + Assertions.assertTrue(r2_5.isBefore( r5_8 )); + Assertions.assertTrue(!r2_5.isBefore( r4_7 )); //isAfter - assertTrue(r5_8.isAfter( r2_5 )); - assertTrue(!r5_8.isAfter( r4_7 )); + Assertions.assertTrue(r5_8.isAfter( r2_5 )); + Assertions.assertTrue(!r5_8.isAfter( r4_7 )); //contains - assertTrue(p15_19.contains( p16_19 )); + Assertions.assertTrue(p15_19.contains( p16_19 )); //overlaps - assertTrue(r2_5.overlaps( r4_7 )); - assertTrue(r2_5.overlaps( r0_3 )); - assertTrue(!r5_8.overlaps( r2_5 )); - assertTrue(!r2_5.overlaps( r5_8 )); + Assertions.assertTrue(r2_5.overlaps( r4_7 )); + Assertions.assertTrue(r2_5.overlaps( r0_3 )); + Assertions.assertTrue(!r5_8.overlaps( r2_5 )); + Assertions.assertTrue(!r2_5.overlaps( r5_8 )); //prefix - assertEquals(L(2,3), L(2,20).prefix(1)); - assertEquals(L(2,19), L(2,20).prefix(-1)); - assertEquals( L(2,10), L(2,20).prefix( L(10,12))); + Assertions.assertEquals(L(2,3), L(2,20).prefix(1)); + Assertions.assertEquals(L(2,19), L(2,20).prefix(-1)); + Assertions.assertEquals(L(2,10), L(2,20).prefix( L(10,12))); //suffix - assertEquals(L(3,20), L(2,20).suffix(1)); - assertEquals(L(19,20), L(2,20).suffix(-1)); - assertEquals(L(12,20), L(2,20).suffix( L(10,12))); + Assertions.assertEquals(L(3,20), L(2,20).suffix(1)); + Assertions.assertEquals(L(19,20), L(2,20).suffix(-1)); + Assertions.assertEquals(L(12,20), L(2,20).suffix( L(10,12))); } @Test - public void testLocationIntersections() { + void testLocationIntersections() { // One inside another Location r21_25 = new Location( 21, 25 ); Location r1_100 = new Location(1, 100 ); - assertEquals(r21_25, r21_25.intersection( r1_100)); - assertEquals(r21_25, r1_100.intersection( r21_25)); + Assertions.assertEquals(r21_25, r21_25.intersection( r1_100)); + Assertions.assertEquals(r21_25, r1_100.intersection( r21_25)); // Non overlapping Location r10_100 = new Location(10, 100 ); Location r1_9 = new Location( 1, 9 ); - assertNull(r10_100.intersection( r1_9)); - assertNull(r1_9.intersection( new Location( 9, 10 ))); + Assertions.assertNull(r10_100.intersection( r1_9)); + Assertions.assertNull(r1_9.intersection( new Location( 9, 10 ))); // Partially overlappping Location r1_25 = new Location( 1, 25 ); Location r21_100 = new Location(21, 100 ); - assertEquals(r21_25, r1_25.intersection( r21_100)); - assertEquals(r21_25, r21_100.intersection( r1_25)); + Assertions.assertEquals(r21_25, r1_25.intersection( r21_100)); + Assertions.assertEquals(r21_25, r21_100.intersection( r1_25)); } //shorthand for testing diff --git a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/AbstractFastqReaderTest.java b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/AbstractFastqReaderTest.java index 7cde3d3ec9..3e1385f3a2 100644 --- a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/AbstractFastqReaderTest.java +++ b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/AbstractFastqReaderTest.java @@ -20,8 +20,8 @@ */ package org.biojava.nbio.genome.io.fastq; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import java.io.File; import java.io.IOException; @@ -34,7 +34,7 @@ /** * Abstract unit test for implementations of FastqReader. */ -public abstract class AbstractFastqReaderTest { +abstract class AbstractFastqReaderTest { /** Array of example files that should throw IOExceptions. */ static final String[] ERROR_EXAMPLES = new String[] { @@ -87,21 +87,21 @@ public abstract class AbstractFastqReaderTest { public void testCreateFastq() { Fastq fastq = createFastq(); - Assert.assertNotNull(fastq); + Assertions.assertNotNull(fastq); } @Test public void testCreateFastqReader() { FastqReader reader = createFastqReader(); - Assert.assertNotNull(reader); + Assertions.assertNotNull(reader); } @Test public void testCreateFastqWriter() { FastqWriter writer = createFastqWriter(); - Assert.assertNotNull(writer); + Assertions.assertNotNull(writer); } @Test @@ -111,7 +111,7 @@ public void testReadFile() throws Exception try { reader.read((File) null); - Assert.fail("read((File) null) expected IllegalArgumentException"); + Assertions.fail("read((File) null) expected IllegalArgumentException"); } catch (IllegalArgumentException e) { @@ -121,7 +121,7 @@ public void testReadFile() throws Exception { File noSuchFile = new File("no such file"); reader.read(noSuchFile); - Assert.fail("read(no such file) expected IOException"); + Assertions.fail("read(no such file) expected IOException"); } catch (IOException e) { @@ -135,14 +135,14 @@ public void testReadEmptyFile() throws Exception FastqReader reader = createFastqReader(); File empty = Files.createTempFile("abstractFastqReaderTest",null).toFile(); Iterable iterable = reader.read(empty); - Assert.assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - Assert.assertNotNull(f); + Assertions.assertNotNull(f); count++; } - Assert.assertEquals(0, count); + Assertions.assertEquals(0, count); } @Test @@ -154,14 +154,14 @@ public void testReadRoundTripSingleFile() throws Exception FastqWriter writer = createFastqWriter(); writer.write(single, fastq); Iterable iterable = reader.read(single); - Assert.assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - Assert.assertNotNull(f); + Assertions.assertNotNull(f); count++; } - Assert.assertEquals(1, count); + Assertions.assertEquals(1, count); } @Test @@ -175,14 +175,14 @@ public void testReadRoundTripMultipleFile() throws Exception FastqWriter writer = createFastqWriter(); writer.write(multiple, fastq0, fastq1, fastq2); Iterable iterable = reader.read(multiple); - Assert.assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - Assert.assertNotNull(f); + Assertions.assertNotNull(f); count++; } - Assert.assertEquals(3, count); + Assertions.assertEquals(3, count); } @Test @@ -192,7 +192,7 @@ public void testReadURL() throws Exception try { reader.read((URL) null); - Assert.fail("read((URL) null) expected IllegalArgumentException"); + Assertions.fail("read((URL) null) expected IllegalArgumentException"); } catch (IllegalArgumentException e) { @@ -202,7 +202,7 @@ public void testReadURL() throws Exception { URL noSuchURL = new URL("file:///no such url"); reader.read(noSuchURL); - Assert.fail("read(no such URL) expected IOException"); + Assertions.fail("read(no such URL) expected IOException"); } catch (IOException e) { @@ -216,14 +216,14 @@ public void testReadEmptyURL() throws Exception FastqReader reader = createFastqReader(); URL empty = getClass().getResource("empty.fastq"); Iterable iterable = reader.read(empty); - Assert.assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - Assert.assertNotNull(f); + Assertions.assertNotNull(f); count++; } - Assert.assertEquals(0, count); + Assertions.assertEquals(0, count); } @Test @@ -233,7 +233,7 @@ public void testReadInputStream() throws Exception try { reader.read((InputStream) null); - Assert.fail("read((InputStream) null) expected IllegalArgumentException"); + Assertions.fail("read((InputStream) null) expected IllegalArgumentException"); } catch (IllegalArgumentException e) { @@ -247,14 +247,14 @@ public void testReadEmptyInputStream() throws Exception FastqReader reader = createFastqReader(); InputStream empty = getClass().getResourceAsStream("empty.fastq"); Iterable iterable = reader.read(empty); - Assert.assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - Assert.assertNotNull(f); + Assertions.assertNotNull(f); count++; } - Assert.assertEquals(0, count); + Assertions.assertEquals(0, count); empty.close(); } @@ -264,15 +264,15 @@ public void testWrappedSequence() throws Exception FastqReader reader = createFastqReader(); InputStream wrappedSequence = getClass().getResourceAsStream("wrapped-sequence.fastq"); Iterable iterable = reader.read(wrappedSequence); - Assert.assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - Assert.assertNotNull(f); - Assert.assertEquals("ACTG", f.getSequence()); + Assertions.assertNotNull(f); + Assertions.assertEquals("ACTG", f.getSequence()); count++; } - Assert.assertEquals(1, count); + Assertions.assertEquals(1, count); wrappedSequence.close(); } @@ -282,15 +282,15 @@ public void testWrappedQuality() throws Exception FastqReader reader = createFastqReader(); InputStream wrappedQuality = getClass().getResourceAsStream("wrapped-quality.fastq"); Iterable iterable = reader.read(wrappedQuality); - Assert.assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - Assert.assertNotNull(f); - Assert.assertEquals("ZZZZ", f.getQuality()); + Assertions.assertNotNull(f); + Assertions.assertEquals("ZZZZ", f.getQuality()); count++; } - Assert.assertEquals(1, count); + Assertions.assertEquals(1, count); wrappedQuality.close(); } @@ -300,15 +300,15 @@ public void testMultipleWrappedQuality() throws Exception FastqReader reader = createFastqReader(); InputStream wrappedQuality = getClass().getResourceAsStream("multiple-wrapped-quality.fastq"); Iterable iterable = reader.read(wrappedQuality); - Assert.assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - Assert.assertNotNull(f); - Assert.assertEquals("ZZZZ", f.getQuality()); + Assertions.assertNotNull(f); + Assertions.assertEquals("ZZZZ", f.getQuality()); count++; } - Assert.assertEquals(4, count); + Assertions.assertEquals(4, count); wrappedQuality.close(); } @@ -322,7 +322,7 @@ public void testErrorExamples() throws Exception try { reader.read(inputStream); - Assert.fail("error example " + errorExample + " expected IOException"); + Assertions.fail("error example " + errorExample + " expected IOException"); } catch (IOException e) { @@ -430,7 +430,7 @@ public void complete() throws IOException { // empty } }); - Assert.fail("parse(null, ) expected IllegalArgumentException"); + Assertions.fail("parse(null, ) expected IllegalArgumentException"); } catch (IllegalArgumentException e) { @@ -446,7 +446,7 @@ public void testParseNullParseListener() throws Exception try { reader.parse(new StringReader(input), null); - Assert.fail("parse(, null) expected IllegalArgumentException"); + Assertions.fail("parse(, null) expected IllegalArgumentException"); } catch (IllegalArgumentException e) { diff --git a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/AbstractFastqWriterTest.java b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/AbstractFastqWriterTest.java index cf2b695968..f2000b5096 100644 --- a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/AbstractFastqWriterTest.java +++ b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/AbstractFastqWriterTest.java @@ -20,8 +20,8 @@ */ package org.biojava.nbio.genome.io.fastq; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import java.io.ByteArrayOutputStream; import java.io.File; @@ -34,7 +34,7 @@ /** * Abstract unit test for implementations of FastqWriter. */ -public abstract class AbstractFastqWriterTest { +abstract class AbstractFastqWriterTest { /** * Create and return a new FASTQ formatted sequence suitable for testing. @@ -54,14 +54,14 @@ public abstract class AbstractFastqWriterTest { public void testCreateFastq() { Fastq fastq = createFastq(); - Assert.assertNotNull(fastq); + Assertions.assertNotNull(fastq); } @Test public void testCreateFastqWriter() { FastqWriter writer = createFastqWriter(); - Assert.assertNotNull(writer); + Assertions.assertNotNull(writer); } @Test @@ -72,16 +72,16 @@ public void testAppendVararg() throws Exception Fastq fastq0 = createFastq(); Fastq fastq1 = createFastq(); Fastq fastq2 = createFastq(); - Assert.assertSame(appendable, writer.append(appendable, fastq0)); - Assert.assertSame(appendable, writer.append(appendable, fastq0, fastq1)); - Assert.assertSame(appendable, writer.append(appendable, fastq0, fastq1, fastq2)); - Assert.assertSame(appendable, writer.append(appendable, fastq0, fastq1, fastq2, null)); - Assert.assertSame(appendable, writer.append(appendable, (Fastq) null)); + Assertions.assertSame(appendable, writer.append(appendable, fastq0)); + Assertions.assertSame(appendable, writer.append(appendable, fastq0, fastq1)); + Assertions.assertSame(appendable, writer.append(appendable, fastq0, fastq1, fastq2)); + Assertions.assertSame(appendable, writer.append(appendable, fastq0, fastq1, fastq2, null)); + Assertions.assertSame(appendable, writer.append(appendable, (Fastq) null)); try { writer.append((Appendable) null, fastq0); - Assert.fail("append(null,) expected IllegalArgumentException"); + Assertions.fail("append(null,) expected IllegalArgumentException"); } catch (IllegalArgumentException e) { @@ -98,20 +98,20 @@ public void testAppendIterable() throws Exception Fastq fastq1 = createFastq(); Fastq fastq2 = createFastq(); List list = new ArrayList(); - Assert.assertSame(appendable, writer.append(appendable, list)); + Assertions.assertSame(appendable, writer.append(appendable, list)); list.add(fastq0); - Assert.assertSame(appendable, writer.append(appendable, list)); + Assertions.assertSame(appendable, writer.append(appendable, list)); list.add(fastq1); - Assert.assertSame(appendable, writer.append(appendable, list)); + Assertions.assertSame(appendable, writer.append(appendable, list)); list.add(fastq2); - Assert.assertSame(appendable, writer.append(appendable, list)); + Assertions.assertSame(appendable, writer.append(appendable, list)); list.add(null); - Assert.assertSame(appendable, writer.append(appendable, list)); + Assertions.assertSame(appendable, writer.append(appendable, list)); try { writer.append((Appendable) null, list); - Assert.fail("append(null,) expected IllegalArgumentException"); + Assertions.fail("append(null,) expected IllegalArgumentException"); } catch (IllegalArgumentException e) { @@ -120,7 +120,7 @@ public void testAppendIterable() throws Exception try { writer.append(appendable, (Iterable) null); - Assert.fail("append(,null) expected IllegalArgumentException"); + Assertions.fail("append(,null) expected IllegalArgumentException"); } catch (IllegalArgumentException e) { @@ -149,7 +149,7 @@ public void testWriteFileVararg() throws Exception try { writer.write((File) null, fastq0); - Assert.fail("append(null,) expected IllegalArgumentException"); + Assertions.fail("append(null,) expected IllegalArgumentException"); } catch (IllegalArgumentException e) { @@ -189,7 +189,7 @@ public void testWriteFileIterable() throws Exception try { writer.write((File) null, fastq0); - Assert.fail("append(null,) expected IllegalArgumentException"); + Assertions.fail("append(null,) expected IllegalArgumentException"); } catch (IllegalArgumentException e) { @@ -198,7 +198,7 @@ public void testWriteFileIterable() throws Exception try { writer.write(file5, (Iterable) null); - Assert.fail("append(,null) expected IllegalArgumentException"); + Assertions.fail("append(,null) expected IllegalArgumentException"); } catch (IllegalArgumentException e) { @@ -223,7 +223,7 @@ public void testWriteOutputStreamVararg() throws Exception try { writer.write((OutputStream) null, fastq0); - Assert.fail("append(null,) expected IllegalArgumentException"); + Assertions.fail("append(null,) expected IllegalArgumentException"); } catch (IllegalArgumentException e) { @@ -253,7 +253,7 @@ public void testWriteOutputStreamIterable() throws Exception try { writer.write((OutputStream) null, fastq0); - Assert.fail("append(null,) expected IllegalArgumentException"); + Assertions.fail("append(null,) expected IllegalArgumentException"); } catch (IllegalArgumentException e) { @@ -262,7 +262,7 @@ public void testWriteOutputStreamIterable() throws Exception try { writer.write(outputStream, (Iterable) null); - Assert.fail("append(,null) expected IllegalArgumentException"); + Assertions.fail("append(,null) expected IllegalArgumentException"); } catch (IllegalArgumentException e) { diff --git a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/ConvertTest.java b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/ConvertTest.java index b07e237ef2..c6789e622f 100644 --- a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/ConvertTest.java +++ b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/ConvertTest.java @@ -26,19 +26,18 @@ import java.util.List; import java.util.Map; -import org.junit.Test; -import static org.junit.Assert.*; - import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; /** * Round trip conversion functional tests. */ -public final class ConvertTest { +final class ConvertTest { @Test - public void testConvert() throws Exception + void testConvert() throws Exception { Map readers = Maps.newHashMap(); readers.put(FastqVariant.FASTQ_SANGER, new SangerFastqReader()); @@ -88,13 +87,13 @@ public void testConvert() throws Exception List observed = Lists.newArrayList(resultReader.read(tmp)); List expected = Lists.newArrayList(resultReader.read(getClass().getResource(expectedFileName))); - assertEquals(expected.size(), observed.size()); + Assertions.assertEquals(expected.size(), observed.size()); for (int i = 0; i < expected.size(); i++) { - assertEquals(expected.get(i).getDescription(), observed.get(i).getDescription()); - assertEquals(expected.get(i).getSequence(), observed.get(i).getSequence()); - assertEquals(expected.get(i).getQuality(), observed.get(i).getQuality()); - assertEquals(expected.get(i).getVariant(), observed.get(i).getVariant()); + Assertions.assertEquals(expected.get(i).getDescription(), observed.get(i).getDescription()); + Assertions.assertEquals(expected.get(i).getSequence(), observed.get(i).getSequence()); + Assertions.assertEquals(expected.get(i).getQuality(), observed.get(i).getQuality()); + Assertions.assertEquals(expected.get(i).getVariant(), observed.get(i).getVariant()); } } } diff --git a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/FastqBuilderTest.java b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/FastqBuilderTest.java index 5803068276..3307256126 100755 --- a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/FastqBuilderTest.java +++ b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/FastqBuilderTest.java @@ -21,24 +21,25 @@ package org.biojava.nbio.genome.io.fastq; import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import org.junit.function.ThrowingRunnable; /** * Unit test for FastqBuilder. */ -public final class FastqBuilderTest { +final class FastqBuilderTest { @Test - public void testConstructor() + void testConstructor() { FastqBuilder fastqBuilder = new FastqBuilder(); - Assert.assertNotNull(fastqBuilder); + Assertions.assertNotNull(fastqBuilder); } @Test - public void testConstructorFastq() + void testConstructorFastq() { FastqBuilder fastqBuilder = new FastqBuilder() .withDescription("description") @@ -49,17 +50,17 @@ public void testConstructorFastq() Fastq fastq = fastqBuilder.build(); FastqBuilder fastqBuilder2 = new FastqBuilder(fastq); - Assert.assertNotNull(fastqBuilder2); + Assertions.assertNotNull(fastqBuilder2); Fastq fastq2 = fastqBuilder2.build(); - Assert.assertEquals("description", fastq2.getDescription()); - Assert.assertEquals("sequence", fastq2.getSequence()); - Assert.assertEquals("quality_", fastq2.getQuality()); - Assert.assertEquals(FastqVariant.FASTQ_SOLEXA, fastq2.getVariant()); + Assertions.assertEquals("description", fastq2.getDescription()); + Assertions.assertEquals("sequence", fastq2.getSequence()); + Assertions.assertEquals("quality_", fastq2.getQuality()); + Assertions.assertEquals(FastqVariant.FASTQ_SOLEXA, fastq2.getVariant()); } @Test - public void testConstructorNullFastq() + void testConstructorNullFastq() { Assert.assertThrows(IllegalArgumentException.class, new ThrowingRunnable() { @Override @@ -70,13 +71,13 @@ public void run() { } @Test - public void testBuildDefault() + void testBuildDefault() { try { FastqBuilder fastqBuilder = new FastqBuilder(); fastqBuilder.build(); - Assert.fail("build default expected IllegalStateException"); + Assertions.fail("build default expected IllegalStateException"); } catch (IllegalStateException e) { @@ -85,7 +86,7 @@ public void testBuildDefault() } @Test - public void testBuildNullDescription() + void testBuildNullDescription() { try { @@ -96,7 +97,7 @@ public void testBuildNullDescription() .withVariant(FastqVariant.FASTQ_SOLEXA); fastqBuilder.build(); - Assert.fail("build null description expected IllegalArgumentException"); + Assertions.fail("build null description expected IllegalArgumentException"); } catch (IllegalArgumentException e) { @@ -105,7 +106,7 @@ public void testBuildNullDescription() } @Test - public void testBuildNullSequence() + void testBuildNullSequence() { try { @@ -116,7 +117,7 @@ public void testBuildNullSequence() .withVariant(FastqVariant.FASTQ_SOLEXA); fastqBuilder.build(); - Assert.fail("build null sequence expected IllegalArgumentException"); + Assertions.fail("build null sequence expected IllegalArgumentException"); } catch (IllegalArgumentException e) { @@ -125,7 +126,7 @@ public void testBuildNullSequence() } @Test - public void testBuildNullAppendSequence() + void testBuildNullAppendSequence() { try { @@ -136,7 +137,7 @@ public void testBuildNullAppendSequence() .withVariant(FastqVariant.FASTQ_SOLEXA); fastqBuilder.build(); - Assert.fail("build null append sequence expected IllegalArgumentException"); + Assertions.fail("build null append sequence expected IllegalArgumentException"); } catch (IllegalArgumentException e) { @@ -145,7 +146,7 @@ public void testBuildNullAppendSequence() } @Test - public void testBuildNullQuality() + void testBuildNullQuality() { try { @@ -156,7 +157,7 @@ public void testBuildNullQuality() .withVariant(FastqVariant.FASTQ_SOLEXA); fastqBuilder.build(); - Assert.fail("build null quality expected IllegalArgumentException"); + Assertions.fail("build null quality expected IllegalArgumentException"); } catch (IllegalArgumentException e) { @@ -165,7 +166,7 @@ public void testBuildNullQuality() } @Test - public void testBuildNullAppendQuality() + void testBuildNullAppendQuality() { try { @@ -176,7 +177,7 @@ public void testBuildNullAppendQuality() .withVariant(FastqVariant.FASTQ_SOLEXA); fastqBuilder.build(); - Assert.fail("build null append quality expected IllegalArgumentException"); + Assertions.fail("build null append quality expected IllegalArgumentException"); } catch (IllegalArgumentException e) { @@ -185,7 +186,7 @@ public void testBuildNullAppendQuality() } @Test - public void testBuildNullVariant() + void testBuildNullVariant() { try { @@ -196,7 +197,7 @@ public void testBuildNullVariant() .withVariant(null); fastqBuilder.build(); - Assert.fail("build null variant expected IllegalArgumentException"); + Assertions.fail("build null variant expected IllegalArgumentException"); } catch (IllegalArgumentException e) { @@ -205,7 +206,7 @@ public void testBuildNullVariant() } @Test - public void testBuildMissingDescription() + void testBuildMissingDescription() { try { @@ -215,7 +216,7 @@ public void testBuildMissingDescription() .withVariant(FastqVariant.FASTQ_SOLEXA); fastqBuilder.build(); - Assert.fail("build missing description expected IllegalStateException"); + Assertions.fail("build missing description expected IllegalStateException"); } catch (IllegalStateException e) { @@ -224,7 +225,7 @@ public void testBuildMissingDescription() } @Test - public void testBuildMissingSequence() + void testBuildMissingSequence() { try { @@ -234,7 +235,7 @@ public void testBuildMissingSequence() .withVariant(FastqVariant.FASTQ_SOLEXA); fastqBuilder.build(); - Assert.fail("build missing sequence expected IllegalStateException"); + Assertions.fail("build missing sequence expected IllegalStateException"); } catch (IllegalStateException e) { @@ -243,7 +244,7 @@ public void testBuildMissingSequence() } @Test - public void testBuildMissingQuality() + void testBuildMissingQuality() { try { @@ -253,7 +254,7 @@ public void testBuildMissingQuality() .withVariant(FastqVariant.FASTQ_SOLEXA); fastqBuilder.build(); - Assert.fail("build missing quality expected IllegalStateException"); + Assertions.fail("build missing quality expected IllegalStateException"); } catch (IllegalStateException e) { @@ -262,7 +263,7 @@ public void testBuildMissingQuality() } @Test - public void testBuildDefaultVariant() + void testBuildDefaultVariant() { FastqBuilder fastqBuilder = new FastqBuilder() .withDescription("description") @@ -270,16 +271,16 @@ public void testBuildDefaultVariant() .withQuality("quality_"); Fastq fastq = fastqBuilder.build(); - Assert.assertEquals("description", fastqBuilder.getDescription()); - Assert.assertTrue(fastqBuilder.sequenceAndQualityLengthsMatch()); - Assert.assertEquals("description", fastq.getDescription()); - Assert.assertEquals("sequence", fastq.getSequence()); - Assert.assertEquals("quality_", fastq.getQuality()); - Assert.assertEquals(FastqBuilder.DEFAULT_VARIANT, fastq.getVariant()); + Assertions.assertEquals("description", fastqBuilder.getDescription()); + Assertions.assertTrue(fastqBuilder.sequenceAndQualityLengthsMatch()); + Assertions.assertEquals("description", fastq.getDescription()); + Assertions.assertEquals("sequence", fastq.getSequence()); + Assertions.assertEquals("quality_", fastq.getQuality()); + Assertions.assertEquals(FastqBuilder.DEFAULT_VARIANT, fastq.getVariant()); } @Test - public void testBuild() + void testBuild() { FastqBuilder fastqBuilder = new FastqBuilder() .withDescription("description") @@ -287,16 +288,16 @@ public void testBuild() .withQuality("quality_") .withVariant(FastqVariant.FASTQ_SOLEXA); Fastq fastq = fastqBuilder.build(); - Assert.assertEquals("description", fastqBuilder.getDescription()); - Assert.assertTrue(fastqBuilder.sequenceAndQualityLengthsMatch()); - Assert.assertEquals("description", fastq.getDescription()); - Assert.assertEquals("sequence", fastq.getSequence()); - Assert.assertEquals("quality_", fastq.getQuality()); - Assert.assertEquals(FastqVariant.FASTQ_SOLEXA, fastq.getVariant()); + Assertions.assertEquals("description", fastqBuilder.getDescription()); + Assertions.assertTrue(fastqBuilder.sequenceAndQualityLengthsMatch()); + Assertions.assertEquals("description", fastq.getDescription()); + Assertions.assertEquals("sequence", fastq.getSequence()); + Assertions.assertEquals("quality_", fastq.getQuality()); + Assertions.assertEquals(FastqVariant.FASTQ_SOLEXA, fastq.getVariant()); } @Test - public void testBuildAppendSequence() + void testBuildAppendSequence() { FastqBuilder fastqBuilder = new FastqBuilder() .withDescription("description") @@ -305,16 +306,16 @@ public void testBuildAppendSequence() .withQuality("quality_") .withVariant(FastqVariant.FASTQ_SOLEXA); Fastq fastq = fastqBuilder.build(); - Assert.assertEquals("description", fastqBuilder.getDescription()); - Assert.assertTrue(fastqBuilder.sequenceAndQualityLengthsMatch()); - Assert.assertEquals("description", fastq.getDescription()); - Assert.assertEquals("sequence", fastq.getSequence()); - Assert.assertEquals("quality_", fastq.getQuality()); - Assert.assertEquals(FastqVariant.FASTQ_SOLEXA, fastq.getVariant()); + Assertions.assertEquals("description", fastqBuilder.getDescription()); + Assertions.assertTrue(fastqBuilder.sequenceAndQualityLengthsMatch()); + Assertions.assertEquals("description", fastq.getDescription()); + Assertions.assertEquals("sequence", fastq.getSequence()); + Assertions.assertEquals("quality_", fastq.getQuality()); + Assertions.assertEquals(FastqVariant.FASTQ_SOLEXA, fastq.getVariant()); } @Test - public void testBuildAppendQuality() + void testBuildAppendQuality() { FastqBuilder fastqBuilder = new FastqBuilder() .withDescription("description") @@ -323,48 +324,48 @@ public void testBuildAppendQuality() .appendQuality("ity_") .withVariant(FastqVariant.FASTQ_SOLEXA); Fastq fastq = fastqBuilder.build(); - Assert.assertEquals("description", fastqBuilder.getDescription()); - Assert.assertTrue(fastqBuilder.sequenceAndQualityLengthsMatch()); - Assert.assertEquals("description", fastq.getDescription()); - Assert.assertEquals("sequence", fastq.getSequence()); - Assert.assertEquals("quality_", fastq.getQuality()); - Assert.assertEquals(FastqVariant.FASTQ_SOLEXA, fastq.getVariant()); + Assertions.assertEquals("description", fastqBuilder.getDescription()); + Assertions.assertTrue(fastqBuilder.sequenceAndQualityLengthsMatch()); + Assertions.assertEquals("description", fastq.getDescription()); + Assertions.assertEquals("sequence", fastq.getSequence()); + Assertions.assertEquals("quality_", fastq.getQuality()); + Assertions.assertEquals(FastqVariant.FASTQ_SOLEXA, fastq.getVariant()); } @Test - public void testBuildNonMatchingSequenceQualityScoreLengthsBothNull() + void testBuildNonMatchingSequenceQualityScoreLengthsBothNull() { FastqBuilder fastqBuilder = new FastqBuilder() .withDescription("description") .withVariant(FastqVariant.FASTQ_SOLEXA); - Assert.assertTrue(fastqBuilder.sequenceAndQualityLengthsMatch()); + Assertions.assertTrue(fastqBuilder.sequenceAndQualityLengthsMatch()); } @Test - public void testBuildNonMatchingSequenceQualityScoreLengthsSequenceNull() + void testBuildNonMatchingSequenceQualityScoreLengthsSequenceNull() { FastqBuilder fastqBuilder = new FastqBuilder() .withDescription("description") .withQuality("0123") .withVariant(FastqVariant.FASTQ_SOLEXA); - Assert.assertEquals(false, fastqBuilder.sequenceAndQualityLengthsMatch()); + Assertions.assertEquals(false, fastqBuilder.sequenceAndQualityLengthsMatch()); } @Test - public void testBuildNonMatchingSequenceQualityScoreLengthsQualityNull() + void testBuildNonMatchingSequenceQualityScoreLengthsQualityNull() { FastqBuilder fastqBuilder = new FastqBuilder() .withDescription("description") .withSequence("ACTG") .withVariant(FastqVariant.FASTQ_SOLEXA); - Assert.assertEquals(false, fastqBuilder.sequenceAndQualityLengthsMatch()); + Assertions.assertEquals(false, fastqBuilder.sequenceAndQualityLengthsMatch()); } @Test - public void testBuildNonMatchingSequenceQualityScoreLengths0() + void testBuildNonMatchingSequenceQualityScoreLengths0() { try { @@ -375,7 +376,7 @@ public void testBuildNonMatchingSequenceQualityScoreLengths0() .withVariant(FastqVariant.FASTQ_SOLEXA); fastqBuilder.build(); - Assert.fail("build sequence length > quality length expected IllegalStateException"); + Assertions.fail("build sequence length > quality length expected IllegalStateException"); } catch (IllegalStateException e) { @@ -384,7 +385,7 @@ public void testBuildNonMatchingSequenceQualityScoreLengths0() } @Test - public void testBuildNonMatchingSequenceQualityScoreLengths1() + void testBuildNonMatchingSequenceQualityScoreLengths1() { try { @@ -395,7 +396,7 @@ public void testBuildNonMatchingSequenceQualityScoreLengths1() .withVariant(FastqVariant.FASTQ_SOLEXA); fastqBuilder.build(); - Assert.fail("build sequence length < quality length expected IllegalStateException"); + Assertions.fail("build sequence length < quality length expected IllegalStateException"); } catch (IllegalStateException e) { @@ -404,7 +405,7 @@ public void testBuildNonMatchingSequenceQualityScoreLengths1() } @Test - public void testBuildMultiple() + void testBuildMultiple() { FastqBuilder fastqBuilder = new FastqBuilder() .withDescription("description") @@ -414,12 +415,12 @@ public void testBuildMultiple() for (int i = 0; i < 10; i++) { Fastq fastq = fastqBuilder.withSequence("sequence" + i).build(); - Assert.assertEquals("description", fastqBuilder.getDescription()); - Assert.assertTrue(fastqBuilder.sequenceAndQualityLengthsMatch()); - Assert.assertEquals("description", fastq.getDescription()); - Assert.assertEquals("sequence" + i, fastq.getSequence()); - Assert.assertEquals("quality__", fastq.getQuality()); - Assert.assertEquals(FastqVariant.FASTQ_SOLEXA, fastq.getVariant()); + Assertions.assertEquals("description", fastqBuilder.getDescription()); + Assertions.assertTrue(fastqBuilder.sequenceAndQualityLengthsMatch()); + Assertions.assertEquals("description", fastq.getDescription()); + Assertions.assertEquals("sequence" + i, fastq.getSequence()); + Assertions.assertEquals("quality__", fastq.getQuality()); + Assertions.assertEquals(FastqVariant.FASTQ_SOLEXA, fastq.getVariant()); } } } diff --git a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/FastqTest.java b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/FastqTest.java index 62d7ee9368..31102b251d 100755 --- a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/FastqTest.java +++ b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/FastqTest.java @@ -21,25 +21,26 @@ package org.biojava.nbio.genome.io.fastq; import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import org.junit.function.ThrowingRunnable; /** * Unit test for Fastq. */ -public final class FastqTest { +final class FastqTest { @Test - public void testConstructor() + void testConstructor() { Fastq fastq = new Fastq("description", "sequence", "quality_", FastqVariant.FASTQ_SANGER); - Assert.assertNotNull(fastq); + Assertions.assertNotNull(fastq); try { new Fastq(null, "sequence", "quality_", FastqVariant.FASTQ_SANGER); - Assert.fail("ctr(null description) expected IllegalArgumentException"); + Assertions.fail("ctr(null description) expected IllegalArgumentException"); } catch (IllegalArgumentException e) { @@ -48,7 +49,7 @@ public void testConstructor() try { new Fastq("description", null, "quality_", FastqVariant.FASTQ_SANGER); - Assert.fail("ctr(null sequence) expected IllegalArgumentException"); + Assertions.fail("ctr(null sequence) expected IllegalArgumentException"); } catch (IllegalArgumentException e) { @@ -57,7 +58,7 @@ public void testConstructor() try { new Fastq("description", "sequence", null, FastqVariant.FASTQ_SANGER); - Assert.fail("ctr(null quality) expected IllegalArgumentException"); + Assertions.fail("ctr(null quality) expected IllegalArgumentException"); } catch (IllegalArgumentException e) { @@ -66,7 +67,7 @@ public void testConstructor() try { new Fastq("description", "sequence", "quality_", null); - Assert.fail("ctr(null variant) expected IllegalArgumentException"); + Assertions.fail("ctr(null variant) expected IllegalArgumentException"); } catch (IllegalArgumentException e) { @@ -75,45 +76,45 @@ public void testConstructor() } @Test - public void testDescription() + void testDescription() { Fastq fastq = new Fastq("description", "sequence", "quality_", FastqVariant.FASTQ_SANGER); - Assert.assertTrue(fastq.getDescription() != null); - Assert.assertEquals("description", fastq.getDescription()); + Assertions.assertTrue(fastq.getDescription() != null); + Assertions.assertEquals("description", fastq.getDescription()); } @Test - public void testSequence() + void testSequence() { Fastq fastq = new Fastq("description", "sequence", "quality_", FastqVariant.FASTQ_SANGER); - Assert.assertTrue(fastq.getSequence() != null); - Assert.assertEquals("sequence", fastq.getSequence()); + Assertions.assertTrue(fastq.getSequence() != null); + Assertions.assertEquals("sequence", fastq.getSequence()); } @Test - public void testQuality() + void testQuality() { Fastq fastq = new Fastq("description", "sequence", "quality_", FastqVariant.FASTQ_SANGER); - Assert.assertTrue(fastq.getQuality() != null); - Assert.assertEquals("quality_", fastq.getQuality()); + Assertions.assertTrue(fastq.getQuality() != null); + Assertions.assertEquals("quality_", fastq.getQuality()); } @Test - public void testVariant() + void testVariant() { Fastq fastq = new Fastq("description", "sequence", "quality_", FastqVariant.FASTQ_SANGER); - Assert.assertTrue(fastq.getVariant() != null); - Assert.assertEquals(FastqVariant.FASTQ_SANGER, fastq.getVariant()); + Assertions.assertTrue(fastq.getVariant() != null); + Assertions.assertEquals(FastqVariant.FASTQ_SANGER, fastq.getVariant()); } @Test - public void testBuilder() + void testBuilder() { - Assert.assertNotNull(Fastq.builder()); + Assertions.assertNotNull(Fastq.builder()); } @Test - public void testBuilderNullFastq() + void testBuilderNullFastq() { Assert.assertThrows(IllegalArgumentException.class, new ThrowingRunnable() { @Override @@ -124,39 +125,39 @@ public void run() { } @Test - public void testEquals() + void testEquals() { Fastq fastq0 = new Fastq("description", "sequence", "quality_", FastqVariant.FASTQ_SANGER); Fastq fastq1 = new Fastq("description", "sequence", "quality_", FastqVariant.FASTQ_SANGER); - Assert.assertFalse(fastq0.equals(null)); - Assert.assertFalse(fastq1.equals(null)); - Assert.assertFalse(fastq0.equals(new Object())); - Assert.assertFalse(fastq1.equals(new Object())); - Assert.assertTrue(fastq0.equals(fastq0)); - Assert.assertTrue(fastq1.equals(fastq1)); - Assert.assertFalse(fastq0 == fastq1); - Assert.assertFalse(fastq0.equals(fastq1)); - Assert.assertFalse(fastq1.equals(fastq0)); + Assertions.assertFalse(fastq0.equals(null)); + Assertions.assertFalse(fastq1.equals(null)); + Assertions.assertFalse(fastq0.equals(new Object())); + Assertions.assertFalse(fastq1.equals(new Object())); + Assertions.assertTrue(fastq0.equals(fastq0)); + Assertions.assertTrue(fastq1.equals(fastq1)); + Assertions.assertFalse(fastq0 == fastq1); + Assertions.assertFalse(fastq0.equals(fastq1)); + Assertions.assertFalse(fastq1.equals(fastq0)); } @Test - public void testHashCode() + void testHashCode() { Fastq fastq0 = new Fastq("description", "sequence", "quality_", FastqVariant.FASTQ_SANGER); Fastq fastq1 = new Fastq("description", "sequence", "quality_", FastqVariant.FASTQ_SANGER); - Assert.assertEquals(fastq0.hashCode(), fastq0.hashCode()); - Assert.assertEquals(fastq1.hashCode(), fastq1.hashCode()); + Assertions.assertEquals(fastq0.hashCode(), fastq0.hashCode()); + Assertions.assertEquals(fastq1.hashCode(), fastq1.hashCode()); if (fastq0.equals(fastq1)) { - Assert.assertEquals(fastq0.hashCode(), fastq1.hashCode()); - Assert.assertEquals(fastq1.hashCode(), fastq0.hashCode()); + Assertions.assertEquals(fastq0.hashCode(), fastq1.hashCode()); + Assertions.assertEquals(fastq1.hashCode(), fastq0.hashCode()); } if (fastq1.equals(fastq0)) { - Assert.assertEquals(fastq0.hashCode(), fastq1.hashCode()); - Assert.assertEquals(fastq1.hashCode(), fastq0.hashCode()); + Assertions.assertEquals(fastq0.hashCode(), fastq1.hashCode()); + Assertions.assertEquals(fastq1.hashCode(), fastq0.hashCode()); } } } diff --git a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/FastqVariantTest.java b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/FastqVariantTest.java index f8b0855a8e..a47896b714 100755 --- a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/FastqVariantTest.java +++ b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/FastqVariantTest.java @@ -22,66 +22,66 @@ import static org.biojava.nbio.genome.io.fastq.FastqVariant.*; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; /** * Unit test for FastqVariant. */ -public final class FastqVariantTest { +final class FastqVariantTest { @Test - public void testDescription() + void testDescription() { for (FastqVariant variant : values()) { - Assert.assertNotNull(variant.getDescription()); + Assertions.assertNotNull(variant.getDescription()); } } @Test - public void testIsSanger() + void testIsSanger() { - Assert.assertTrue(FASTQ_SANGER.isSanger()); - Assert.assertFalse(FASTQ_SOLEXA.isSanger()); - Assert.assertFalse(FASTQ_ILLUMINA.isSanger()); + Assertions.assertTrue(FASTQ_SANGER.isSanger()); + Assertions.assertFalse(FASTQ_SOLEXA.isSanger()); + Assertions.assertFalse(FASTQ_ILLUMINA.isSanger()); } @Test - public void testIsSolexa() + void testIsSolexa() { - Assert.assertFalse(FASTQ_SANGER.isSolexa()); - Assert.assertTrue(FASTQ_SOLEXA.isSolexa()); - Assert.assertFalse(FASTQ_ILLUMINA.isSolexa()); + Assertions.assertFalse(FASTQ_SANGER.isSolexa()); + Assertions.assertTrue(FASTQ_SOLEXA.isSolexa()); + Assertions.assertFalse(FASTQ_ILLUMINA.isSolexa()); } @Test - public void testIsIllumina() + void testIsIllumina() { - Assert.assertFalse(FASTQ_SANGER.isIllumina()); - Assert.assertFalse(FASTQ_SOLEXA.isIllumina()); - Assert.assertTrue(FASTQ_ILLUMINA.isIllumina()); + Assertions.assertFalse(FASTQ_SANGER.isIllumina()); + Assertions.assertFalse(FASTQ_SOLEXA.isIllumina()); + Assertions.assertTrue(FASTQ_ILLUMINA.isIllumina()); } @Test - public void testParseFastqVariant() + void testParseFastqVariant() { - Assert.assertEquals(null, parseFastqVariant(null)); - Assert.assertEquals(null, parseFastqVariant("")); - Assert.assertEquals(null, parseFastqVariant("not a valid FASTQ variant")); - Assert.assertEquals(FASTQ_SANGER, parseFastqVariant("FASTQ_SANGER")); - Assert.assertEquals(FASTQ_SANGER, parseFastqVariant("fastq-sanger")); + Assertions.assertEquals(null, parseFastqVariant(null)); + Assertions.assertEquals(null, parseFastqVariant("")); + Assertions.assertEquals(null, parseFastqVariant("not a valid FASTQ variant")); + Assertions.assertEquals(FASTQ_SANGER, parseFastqVariant("FASTQ_SANGER")); + Assertions.assertEquals(FASTQ_SANGER, parseFastqVariant("fastq-sanger")); } @Test - public void testQualityLessThanMinimumQualityScore() + void testQualityLessThanMinimumQualityScore() { for (FastqVariant variant : values()) { try { variant.quality(variant.minimumQualityScore() - 1); - Assert.fail("expected IllegalArgumentException"); + Assertions.fail("expected IllegalArgumentException"); } catch (IllegalArgumentException e) { @@ -91,14 +91,14 @@ public void testQualityLessThanMinimumQualityScore() } @Test - public void testQualityMoreThanMaximumQualityScore() + void testQualityMoreThanMaximumQualityScore() { for (FastqVariant variant : values()) { try { variant.quality(variant.maximumQualityScore() + 1); - Assert.fail("expected IllegalArgumentException"); + Assertions.fail("expected IllegalArgumentException"); } catch (IllegalArgumentException e) { @@ -108,13 +108,13 @@ public void testQualityMoreThanMaximumQualityScore() } @Test - public void testQualityQualityScoreRoundTrip() + void testQualityQualityScoreRoundTrip() { for (FastqVariant variant : values()) { for (int i = variant.minimumQualityScore(); i < (variant.maximumQualityScore() + 1); i++) { - Assert.assertEquals(i, variant.qualityScore(variant.quality(i))); + Assertions.assertEquals(i, variant.qualityScore(variant.quality(i))); } } } diff --git a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/IlluminaFastqReaderTest.java b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/IlluminaFastqReaderTest.java index d7b0a8b9d2..1d2adf76b6 100755 --- a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/IlluminaFastqReaderTest.java +++ b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/IlluminaFastqReaderTest.java @@ -20,8 +20,8 @@ */ package org.biojava.nbio.genome.io.fastq; -import org.junit.Test; -import static org.junit.Assert.*; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.InputStream; @@ -31,7 +31,7 @@ /** * Unit test for IlluminaFastqReader. */ -public final class IlluminaFastqReaderTest +final class IlluminaFastqReaderTest extends AbstractFastqReaderTest { @@ -59,119 +59,119 @@ public FastqWriter createFastqWriter() } @Test - public void testValidateDescription() throws Exception + void testValidateDescription() throws Exception { IlluminaFastqReader reader = new IlluminaFastqReader(); URL invalidDescription = getClass().getResource("illumina-invalid-description.fastq"); try { reader.read(invalidDescription); - fail("read(invalidDescription) expected IOException"); + Assertions.fail("read(invalidDescription) expected IOException"); } catch (IOException e) { - assertTrue(e.getMessage().contains("description must begin with a '@' character")); + Assertions.assertTrue(e.getMessage().contains("description must begin with a '@' character")); } } @Test - public void testValidateRepeatDescription() throws Exception + void testValidateRepeatDescription() throws Exception { IlluminaFastqReader reader = new IlluminaFastqReader(); URL invalidRepeatDescription = getClass().getResource("illumina-invalid-repeat-description.fastq"); try { reader.read(invalidRepeatDescription); - fail("read(invalidRepeatDescription) expected IOException"); + Assertions.fail("read(invalidRepeatDescription) expected IOException"); } catch (IOException e) { - assertTrue(e.getMessage().contains("repeat description must match description")); + Assertions.assertTrue(e.getMessage().contains("repeat description must match description")); } } @Test - public void testWrappingAsIllumina() throws Exception + void testWrappingAsIllumina() throws Exception { FastqReader reader = createFastqReader(); InputStream inputStream = getClass().getResourceAsStream("wrapping_as_illumina.fastq"); Iterable iterable = reader.read(inputStream); - assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - assertNotNull(f); + Assertions.assertNotNull(f); count++; } - assertEquals(3, count); + Assertions.assertEquals(3, count); inputStream.close(); } @Test - public void testFullRangeAsIllumina() throws Exception + void testFullRangeAsIllumina() throws Exception { FastqReader reader = createFastqReader(); InputStream inputStream = getClass().getResourceAsStream("illumina_full_range_as_illumina.fastq"); Iterable iterable = reader.read(inputStream); - assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - assertNotNull(f); + Assertions.assertNotNull(f); count++; } - assertEquals(2, count); + Assertions.assertEquals(2, count); inputStream.close(); } @Test - public void testMiscDnaAsIllumina() throws Exception + void testMiscDnaAsIllumina() throws Exception { FastqReader reader = createFastqReader(); InputStream inputStream = getClass().getResourceAsStream("misc_dna_as_illumina.fastq"); Iterable iterable = reader.read(inputStream); - assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - assertNotNull(f); + Assertions.assertNotNull(f); count++; } - assertEquals(4, count); + Assertions.assertEquals(4, count); inputStream.close(); } @Test - public void testMiscRnaAsIllumina() throws Exception + void testMiscRnaAsIllumina() throws Exception { FastqReader reader = createFastqReader(); InputStream inputStream = getClass().getResourceAsStream("misc_rna_as_illumina.fastq"); Iterable iterable = reader.read(inputStream); - assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - assertNotNull(f); + Assertions.assertNotNull(f); count++; } - assertEquals(4, count); + Assertions.assertEquals(4, count); inputStream.close(); } @Test - public void testLongReadsAsIllumina() throws Exception + void testLongReadsAsIllumina() throws Exception { FastqReader reader = createFastqReader(); InputStream inputStream = getClass().getResourceAsStream("longreads_as_illumina.fastq"); Iterable iterable = reader.read(inputStream); - assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - assertNotNull(f); + Assertions.assertNotNull(f); count++; } - assertEquals(10, count); + Assertions.assertEquals(10, count); inputStream.close(); } } diff --git a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/IlluminaFastqWriterTest.java b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/IlluminaFastqWriterTest.java index c9701595fa..384e204ff0 100755 --- a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/IlluminaFastqWriterTest.java +++ b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/IlluminaFastqWriterTest.java @@ -21,12 +21,12 @@ package org.biojava.nbio.genome.io.fastq; -import org.junit.Test; +import org.junit.jupiter.api.Test; /** * Unit test for IlluminaFastqWriter. */ -public final class IlluminaFastqWriterTest +final class IlluminaFastqWriterTest extends AbstractFastqWriterTest { @@ -48,7 +48,7 @@ public Fastq createFastq() } @Test - public void testConvertNotIlluminaVariant() throws Exception + void testConvertNotIlluminaVariant() throws Exception { IlluminaFastqWriter writer = new IlluminaFastqWriter(); Appendable appendable = new StringBuilder(); diff --git a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/SangerFastqReaderTest.java b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/SangerFastqReaderTest.java index af6f67319f..3e99a4cdc8 100755 --- a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/SangerFastqReaderTest.java +++ b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/SangerFastqReaderTest.java @@ -20,18 +20,17 @@ */ package org.biojava.nbio.genome.io.fastq; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.InputStream; import java.net.URL; -import static org.junit.Assert.*; - /** * Unit test for SangerFastqReader. */ -public final class SangerFastqReaderTest +final class SangerFastqReaderTest extends AbstractFastqReaderTest { @@ -65,197 +64,197 @@ public void testValidateDescription() throws Exception try { reader.read(invalidDescription); - fail("read(invalidDescription) expected IOException"); + Assertions.fail("read(invalidDescription) expected IOException"); } catch (IOException e) { - assertTrue(e.getMessage().contains("description must begin with a '@' character")); + Assertions.assertTrue(e.getMessage().contains("description must begin with a '@' character")); } } @Test - public void testValidateRepeatDescription() throws Exception + void testValidateRepeatDescription() throws Exception { SangerFastqReader reader = new SangerFastqReader(); URL invalidRepeatDescription = getClass().getResource("sanger-invalid-repeat-description.fastq"); try { reader.read(invalidRepeatDescription); - fail("read(invalidRepeatDescription) expected IOException"); + Assertions.fail("read(invalidRepeatDescription) expected IOException"); } catch (IOException e) { - assertTrue(e.getMessage().contains("repeat description must match description")); + Assertions.assertTrue(e.getMessage().contains("repeat description must match description")); } } @Test - public void testWrappingOriginal() throws Exception + void testWrappingOriginal() throws Exception { FastqReader reader = createFastqReader(); InputStream inputStream = getClass().getResourceAsStream("wrapping_original_sanger.fastq"); Iterable iterable = reader.read(inputStream); - assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - assertNotNull(f); + Assertions.assertNotNull(f); count++; } - assertEquals(3, count); + Assertions.assertEquals(3, count); inputStream.close(); } @Test - public void testWrappingAsSanger() throws Exception + void testWrappingAsSanger() throws Exception { FastqReader reader = createFastqReader(); InputStream inputStream = getClass().getResourceAsStream("wrapping_as_sanger.fastq"); Iterable iterable = reader.read(inputStream); - assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - assertNotNull(f); + Assertions.assertNotNull(f); count++; } - assertEquals(3, count); + Assertions.assertEquals(3, count); inputStream.close(); } @Test - public void testFullRangeOriginal() throws Exception + void testFullRangeOriginal() throws Exception { FastqReader reader = createFastqReader(); InputStream inputStream = getClass().getResourceAsStream("sanger_full_range_original_sanger.fastq"); Iterable iterable = reader.read(inputStream); - assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - assertNotNull(f); + Assertions.assertNotNull(f); count++; } - assertEquals(2, count); + Assertions.assertEquals(2, count); inputStream.close(); } @Test - public void testFullRangeAsSanger() throws Exception + void testFullRangeAsSanger() throws Exception { FastqReader reader = createFastqReader(); InputStream inputStream = getClass().getResourceAsStream("sanger_full_range_as_sanger.fastq"); Iterable iterable = reader.read(inputStream); - assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - assertNotNull(f); + Assertions.assertNotNull(f); count++; } - assertEquals(2, count); + Assertions.assertEquals(2, count); inputStream.close(); } @Test - public void testMiscDnaOriginal() throws Exception + void testMiscDnaOriginal() throws Exception { FastqReader reader = createFastqReader(); InputStream inputStream = getClass().getResourceAsStream("misc_dna_original_sanger.fastq"); Iterable iterable = reader.read(inputStream); - assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - assertNotNull(f); + Assertions.assertNotNull(f); count++; } - assertEquals(4, count); + Assertions.assertEquals(4, count); inputStream.close(); } @Test - public void testMiscDnaAsSanger() throws Exception + void testMiscDnaAsSanger() throws Exception { FastqReader reader = createFastqReader(); InputStream inputStream = getClass().getResourceAsStream("misc_dna_as_sanger.fastq"); Iterable iterable = reader.read(inputStream); - assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - assertNotNull(f); + Assertions.assertNotNull(f); count++; } - assertEquals(4, count); + Assertions.assertEquals(4, count); inputStream.close(); } @Test - public void testMiscRnaOriginal() throws Exception + void testMiscRnaOriginal() throws Exception { FastqReader reader = createFastqReader(); InputStream inputStream = getClass().getResourceAsStream("misc_rna_original_sanger.fastq"); Iterable iterable = reader.read(inputStream); - assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - assertNotNull(f); + Assertions.assertNotNull(f); count++; } - assertEquals(4, count); + Assertions.assertEquals(4, count); inputStream.close(); } @Test - public void testMiscRnaAsSanger() throws Exception + void testMiscRnaAsSanger() throws Exception { FastqReader reader = createFastqReader(); InputStream inputStream = getClass().getResourceAsStream("misc_rna_as_sanger.fastq"); Iterable iterable = reader.read(inputStream); - assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - assertNotNull(f); + Assertions.assertNotNull(f); count++; } - assertEquals(4, count); + Assertions.assertEquals(4, count); inputStream.close(); } @Test - public void testLongReadsOriginal() throws Exception + void testLongReadsOriginal() throws Exception { FastqReader reader = createFastqReader(); InputStream inputStream = getClass().getResourceAsStream("longreads_original_sanger.fastq"); Iterable iterable = reader.read(inputStream); - assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - assertNotNull(f); + Assertions.assertNotNull(f); count++; } - assertEquals(10, count); + Assertions.assertEquals(10, count); inputStream.close(); } @Test - public void testLongReadsAsSanger() throws Exception + void testLongReadsAsSanger() throws Exception { FastqReader reader = createFastqReader(); InputStream inputStream = getClass().getResourceAsStream("longreads_as_sanger.fastq"); Iterable iterable = reader.read(inputStream); - assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - assertNotNull(f); + Assertions.assertNotNull(f); count++; } - assertEquals(10, count); + Assertions.assertEquals(10, count); inputStream.close(); } } diff --git a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/SangerFastqWriterTest.java b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/SangerFastqWriterTest.java index f94db84e0a..fcb1638c71 100755 --- a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/SangerFastqWriterTest.java +++ b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/SangerFastqWriterTest.java @@ -21,12 +21,12 @@ package org.biojava.nbio.genome.io.fastq; -import org.junit.Test; +import org.junit.jupiter.api.Test; /** * Unit test for SangerFastqWriter. */ -public final class SangerFastqWriterTest +final class SangerFastqWriterTest extends AbstractFastqWriterTest { @@ -48,7 +48,7 @@ public Fastq createFastq() } @Test - public void testConvertNotSangerVariant() throws Exception + void testConvertNotSangerVariant() throws Exception { SangerFastqWriter writer = new SangerFastqWriter(); Appendable appendable = new StringBuilder(); diff --git a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/SolexaFastqReaderTest.java b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/SolexaFastqReaderTest.java index 5f6f041c84..cd87c53dc6 100755 --- a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/SolexaFastqReaderTest.java +++ b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/SolexaFastqReaderTest.java @@ -20,19 +20,18 @@ */ package org.biojava.nbio.genome.io.fastq; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.InputStream; import java.net.URL; -import static org.junit.Assert.*; - /** * Unit test for SolexaFastqReader. */ -public final class SolexaFastqReaderTest +final class SolexaFastqReaderTest extends AbstractFastqReaderTest { @@ -60,119 +59,119 @@ public FastqWriter createFastqWriter() } @Test - public void testValidateDescription() throws Exception + void testValidateDescription() throws Exception { SolexaFastqReader reader = new SolexaFastqReader(); URL invalidDescription = getClass().getResource("solexa-invalid-description.fastq"); try { reader.read(invalidDescription); - fail("read(invalidDescription) expected IOException"); + Assertions.fail("read(invalidDescription) expected IOException"); } catch (IOException e) { - assertTrue(e.getMessage().contains("description must begin with a '@' character")); + Assertions.assertTrue(e.getMessage().contains("description must begin with a '@' character")); } } @Test - public void testValidateRepeatDescription() throws Exception + void testValidateRepeatDescription() throws Exception { SolexaFastqReader reader = new SolexaFastqReader(); URL invalidRepeatDescription = getClass().getResource("solexa-invalid-repeat-description.fastq"); try { reader.read(invalidRepeatDescription); - fail("read(invalidRepeatDescription) expected IOException"); + Assertions.fail("read(invalidRepeatDescription) expected IOException"); } catch (IOException e) { - assertTrue(e.getMessage().contains("repeat description must match description")); + Assertions.assertTrue(e.getMessage().contains("repeat description must match description")); } } @Test - public void testWrappingAsSolexa() throws Exception + void testWrappingAsSolexa() throws Exception { FastqReader reader = createFastqReader(); InputStream inputStream = getClass().getResourceAsStream("wrapping_as_solexa.fastq"); Iterable iterable = reader.read(inputStream); - assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - assertNotNull(f); + Assertions.assertNotNull(f); count++; } - assertEquals(3, count); + Assertions.assertEquals(3, count); inputStream.close(); } @Test - public void testFullRangeAsSolexa() throws Exception + void testFullRangeAsSolexa() throws Exception { FastqReader reader = createFastqReader(); InputStream inputStream = getClass().getResourceAsStream("solexa_full_range_as_solexa.fastq"); Iterable iterable = reader.read(inputStream); - assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - assertNotNull(f); + Assertions.assertNotNull(f); count++; } - assertEquals(2, count); + Assertions.assertEquals(2, count); inputStream.close(); } @Test - public void testMiscDnaAsSolexa() throws Exception + void testMiscDnaAsSolexa() throws Exception { FastqReader reader = createFastqReader(); InputStream inputStream = getClass().getResourceAsStream("misc_dna_as_solexa.fastq"); Iterable iterable = reader.read(inputStream); - assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - assertNotNull(f); + Assertions.assertNotNull(f); count++; } - assertEquals(4, count); + Assertions.assertEquals(4, count); inputStream.close(); } @Test - public void testMiscRnaAsSolexa() throws Exception + void testMiscRnaAsSolexa() throws Exception { FastqReader reader = createFastqReader(); InputStream inputStream = getClass().getResourceAsStream("misc_rna_as_solexa.fastq"); Iterable iterable = reader.read(inputStream); - assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - assertNotNull(f); + Assertions.assertNotNull(f); count++; } - assertEquals(4, count); + Assertions.assertEquals(4, count); inputStream.close(); } @Test - public void testLongReadsAsSolexa() throws Exception + void testLongReadsAsSolexa() throws Exception { FastqReader reader = createFastqReader(); InputStream inputStream = getClass().getResourceAsStream("longreads_as_solexa.fastq"); Iterable iterable = reader.read(inputStream); - assertNotNull(iterable); + Assertions.assertNotNull(iterable); int count = 0; for (Fastq f : iterable) { - assertNotNull(f); + Assertions.assertNotNull(f); count++; } - assertEquals(10, count); + Assertions.assertEquals(10, count); inputStream.close(); } } diff --git a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/SolexaFastqWriterTest.java b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/SolexaFastqWriterTest.java index 2f2011e849..0927bf0cff 100755 --- a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/SolexaFastqWriterTest.java +++ b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/SolexaFastqWriterTest.java @@ -21,12 +21,12 @@ package org.biojava.nbio.genome.io.fastq; -import org.junit.Test; +import org.junit.jupiter.api.Test; /** * Unit test for SolexaFastqWriter. */ -public final class SolexaFastqWriterTest +final class SolexaFastqWriterTest extends AbstractFastqWriterTest { @@ -48,7 +48,7 @@ public Fastq createFastq() } @Test - public void testConvertNotSolexaVariant() throws Exception + void testConvertNotSolexaVariant() throws Exception { SolexaFastqWriter writer = new SolexaFastqWriter(); Appendable appendable = new StringBuilder(); diff --git a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/StreamingFastqParserTest.java b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/StreamingFastqParserTest.java index a80f44a43d..02d49d3177 100644 --- a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/StreamingFastqParserTest.java +++ b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/StreamingFastqParserTest.java @@ -20,8 +20,8 @@ */ package org.biojava.nbio.genome.io.fastq; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import java.io.StringReader; @@ -29,10 +29,10 @@ /** * Unit test for StreamingFastqParser. */ -public class StreamingFastqParserTest { +class StreamingFastqParserTest { @Test - public void testStreamNullReadable() throws Exception + void testStreamNullReadable() throws Exception { try { @@ -42,7 +42,7 @@ public void fastq(final Fastq fastq) { // empty } }); - Assert.fail("stream(null,,) expected IllegalArgumentException"); + Assertions.fail("stream(null,,) expected IllegalArgumentException"); } catch (IllegalArgumentException e) { @@ -51,7 +51,7 @@ public void fastq(final Fastq fastq) { } @Test - public void testStreamNullVariant() throws Exception + void testStreamNullVariant() throws Exception { try { @@ -62,7 +62,7 @@ public void fastq(final Fastq fastq) { // empty } }); - Assert.fail("stream(null,,) expected IllegalArgumentException"); + Assertions.fail("stream(null,,) expected IllegalArgumentException"); } catch (IllegalArgumentException e) { @@ -71,13 +71,13 @@ public void fastq(final Fastq fastq) { } @Test - public void testStreamNullListener() throws Exception + void testStreamNullListener() throws Exception { try { final String input = ""; StreamingFastqParser.stream(new StringReader(input), FastqVariant.FASTQ_SANGER, null); - Assert.fail("stream(null,,) expected IllegalArgumentException"); + Assertions.fail("stream(null,,) expected IllegalArgumentException"); } catch (IllegalArgumentException e) { From cb3019d1676085755d18ca50a9ee777b4500c5ed Mon Sep 17 00:00:00 2001 From: Jose Duarte Date: Wed, 29 Jul 2026 11:40:09 -0700 Subject: [PATCH 20/67] Bugfix: parsing of mmCIF files with empty database_PDB_rev.date --- CHANGELOG.md | 5 +++++ .../nbio/structure/io/cif/CifStructureConsumerImpl.java | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa0cc2b686..659ec4cdf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ BioJava Changelog ----------------- +BioJava 7.2.6 +============================== +### Fixed +* Parsing of PDBx/mmCIF with empty database_PDB_rev.date + BioJava 7.2.5 ============================== ### Fixed diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/CifStructureConsumerImpl.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/CifStructureConsumerImpl.java index 03ebd027c6..94b96b13f8 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/CifStructureConsumerImpl.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/CifStructureConsumerImpl.java @@ -630,7 +630,8 @@ public void consumeDatabasePDBRev(DatabasePDBRev databasePDBrev) { modDate = relDate; } else { String dbrev = databasePDBrev.getDate().get(rowIndex); - modDate = convert(LocalDate.parse(dbrev, DATE_FORMAT)); + if (dbrev != null && !dbrev.isBlank()) + modDate = convert(LocalDate.parse(dbrev, DATE_FORMAT)); } pdbHeader.setModDate(modDate); } From 456a774bbd8f88457984bb4ea11cc7b2cc898dd5 Mon Sep 17 00:00:00 2001 From: Jose Duarte Date: Thu, 30 Jul 2026 03:15:43 -0700 Subject: [PATCH 21/67] Disabling tests in release process. Tests are done at CI time --- pom.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9fae069ae0..6c6d3fa2a8 100644 --- a/pom.xml +++ b/pom.xml @@ -217,7 +217,8 @@ 3.1.1 true - clean install + -DskipTests + clean verify -DskipTests true From 82a6c0aa091a7e637563a9b52c1de7f27fe1daa1 Mon Sep 17 00:00:00 2001 From: josemduarte Date: Thu, 30 Jul 2026 03:17:14 -0700 Subject: [PATCH 22/67] [maven-release-plugin] prepare release biojava-7.2.6 --- biojava-aa-prop/pom.xml | 6 +++--- biojava-alignment/pom.xml | 4 ++-- biojava-core/pom.xml | 2 +- biojava-genome/pom.xml | 6 +++--- biojava-integrationtest/pom.xml | 4 ++-- biojava-modfinder/pom.xml | 4 ++-- biojava-ontology/pom.xml | 2 +- biojava-protein-comparison-tool/pom.xml | 10 +++++----- biojava-protein-disorder/pom.xml | 4 ++-- biojava-structure-gui/pom.xml | 6 +++--- biojava-structure/pom.xml | 6 +++--- biojava-survival/pom.xml | 2 +- biojava-ws/pom.xml | 4 ++-- pom.xml | 4 ++-- 14 files changed, 32 insertions(+), 32 deletions(-) diff --git a/biojava-aa-prop/pom.xml b/biojava-aa-prop/pom.xml index 1209448b5f..22a4008f86 100644 --- a/biojava-aa-prop/pom.xml +++ b/biojava-aa-prop/pom.xml @@ -2,7 +2,7 @@ biojava org.biojava - 7.2.6-SNAPSHOT + 7.2.6 4.0.0 biojava-aa-prop @@ -70,12 +70,12 @@ org.biojava biojava-core - 7.2.6-SNAPSHOT + 7.2.6 org.biojava biojava-structure - 7.2.6-SNAPSHOT + 7.2.6 diff --git a/biojava-alignment/pom.xml b/biojava-alignment/pom.xml index 6f57e23426..4c6948a4fd 100644 --- a/biojava-alignment/pom.xml +++ b/biojava-alignment/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.6-SNAPSHOT + 7.2.6 biojava-alignment biojava-alignment @@ -47,7 +47,7 @@ org.biojava biojava-core - 7.2.6-SNAPSHOT + 7.2.6 compile diff --git a/biojava-core/pom.xml b/biojava-core/pom.xml index 3ad7189c9e..cac3462199 100644 --- a/biojava-core/pom.xml +++ b/biojava-core/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.6-SNAPSHOT + 7.2.6 4.0.0 biojava-core diff --git a/biojava-genome/pom.xml b/biojava-genome/pom.xml index 698fa280c7..16610db1e1 100644 --- a/biojava-genome/pom.xml +++ b/biojava-genome/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.6-SNAPSHOT + 7.2.6 4.0.0 biojava-genome @@ -70,13 +70,13 @@ org.biojava biojava-core - 7.2.6-SNAPSHOT + 7.2.6 compile org.biojava biojava-alignment - 7.2.6-SNAPSHOT + 7.2.6 compile diff --git a/biojava-integrationtest/pom.xml b/biojava-integrationtest/pom.xml index 349393cfa6..9b5ab19e7c 100644 --- a/biojava-integrationtest/pom.xml +++ b/biojava-integrationtest/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.6-SNAPSHOT + 7.2.6 biojava-integrationtest jar @@ -40,7 +40,7 @@ org.biojava biojava-structure - 7.2.6-SNAPSHOT + 7.2.6 diff --git a/biojava-modfinder/pom.xml b/biojava-modfinder/pom.xml index cec4445407..7351214cab 100644 --- a/biojava-modfinder/pom.xml +++ b/biojava-modfinder/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.6-SNAPSHOT + 7.2.6 biojava-modfinder biojava-modfinder @@ -31,7 +31,7 @@ org.biojava biojava-structure - 7.2.6-SNAPSHOT + 7.2.6 jar compile diff --git a/biojava-ontology/pom.xml b/biojava-ontology/pom.xml index dff3450bc9..95fab178d6 100644 --- a/biojava-ontology/pom.xml +++ b/biojava-ontology/pom.xml @@ -4,7 +4,7 @@ org.biojava biojava - 7.2.6-SNAPSHOT + 7.2.6 biojava-ontology diff --git a/biojava-protein-comparison-tool/pom.xml b/biojava-protein-comparison-tool/pom.xml index 3b43bc492d..84e76c523f 100644 --- a/biojava-protein-comparison-tool/pom.xml +++ b/biojava-protein-comparison-tool/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.6-SNAPSHOT + 7.2.6 biojava-protein-comparison-tool @@ -36,23 +36,23 @@ org.biojava biojava-alignment - 7.2.6-SNAPSHOT + 7.2.6 org.biojava biojava-core - 7.2.6-SNAPSHOT + 7.2.6 org.biojava biojava-structure - 7.2.6-SNAPSHOT + 7.2.6 org.biojava biojava-structure-gui - 7.2.6-SNAPSHOT + 7.2.6 net.sourceforge.jmol diff --git a/biojava-protein-disorder/pom.xml b/biojava-protein-disorder/pom.xml index d0267a7519..006032e114 100644 --- a/biojava-protein-disorder/pom.xml +++ b/biojava-protein-disorder/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.6-SNAPSHOT + 7.2.6 biojava-protein-disorder jar @@ -63,7 +63,7 @@ org.biojava biojava-core - 7.2.6-SNAPSHOT + 7.2.6 diff --git a/biojava-structure-gui/pom.xml b/biojava-structure-gui/pom.xml index 314c32bf16..857645a396 100644 --- a/biojava-structure-gui/pom.xml +++ b/biojava-structure-gui/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.6-SNAPSHOT + 7.2.6 4.0.0 biojava-structure-gui @@ -27,13 +27,13 @@ org.biojava biojava-structure - 7.2.6-SNAPSHOT + 7.2.6 compile org.biojava biojava-core - 7.2.6-SNAPSHOT + 7.2.6 compile diff --git a/biojava-structure/pom.xml b/biojava-structure/pom.xml index c80a8490bd..dec517fe60 100644 --- a/biojava-structure/pom.xml +++ b/biojava-structure/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.6-SNAPSHOT + 7.2.6 biojava-structure biojava-structure @@ -51,13 +51,13 @@ org.biojava biojava-alignment - 7.2.6-SNAPSHOT + 7.2.6 compile org.biojava biojava-core - 7.2.6-SNAPSHOT + 7.2.6 compile diff --git a/biojava-survival/pom.xml b/biojava-survival/pom.xml index a8c8616fa1..13cae1870e 100644 --- a/biojava-survival/pom.xml +++ b/biojava-survival/pom.xml @@ -4,7 +4,7 @@ org.biojava biojava - 7.2.6-SNAPSHOT + 7.2.6 biojava-survival diff --git a/biojava-ws/pom.xml b/biojava-ws/pom.xml index eafd87dff8..0a38475285 100644 --- a/biojava-ws/pom.xml +++ b/biojava-ws/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.6-SNAPSHOT + 7.2.6 biojava-ws biojava-ws @@ -19,7 +19,7 @@ org.biojava biojava-core - 7.2.6-SNAPSHOT + 7.2.6 compile diff --git a/pom.xml b/pom.xml index 6c6d3fa2a8..d3d58328ff 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ org.biojava biojava pom - 7.2.6-SNAPSHOT + 7.2.6 biojava BioJava is an open-source project dedicated to providing a Java framework for processing biological data. It provides analytical and statistical routines, parsers for common file formats and allows the @@ -51,7 +51,7 @@ scm:git:git@github.com:biojava/biojava.git https://github.com/biojava/biojava - HEAD + biojava-7.2.6 diff --git a/biojava-alignment/pom.xml b/biojava-alignment/pom.xml index 4c6948a4fd..7ff8f29cf4 100644 --- a/biojava-alignment/pom.xml +++ b/biojava-alignment/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.6 + 7.2.7-SNAPSHOT biojava-alignment biojava-alignment @@ -47,7 +47,7 @@ org.biojava biojava-core - 7.2.6 + 7.2.7-SNAPSHOT compile diff --git a/biojava-core/pom.xml b/biojava-core/pom.xml index cac3462199..4826cf9d17 100644 --- a/biojava-core/pom.xml +++ b/biojava-core/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.6 + 7.2.7-SNAPSHOT 4.0.0 biojava-core diff --git a/biojava-genome/pom.xml b/biojava-genome/pom.xml index 16610db1e1..d71d25a80a 100644 --- a/biojava-genome/pom.xml +++ b/biojava-genome/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.6 + 7.2.7-SNAPSHOT 4.0.0 biojava-genome @@ -70,13 +70,13 @@ org.biojava biojava-core - 7.2.6 + 7.2.7-SNAPSHOT compile org.biojava biojava-alignment - 7.2.6 + 7.2.7-SNAPSHOT compile diff --git a/biojava-integrationtest/pom.xml b/biojava-integrationtest/pom.xml index 9b5ab19e7c..870612fc63 100644 --- a/biojava-integrationtest/pom.xml +++ b/biojava-integrationtest/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.6 + 7.2.7-SNAPSHOT biojava-integrationtest jar @@ -40,7 +40,7 @@ org.biojava biojava-structure - 7.2.6 + 7.2.7-SNAPSHOT diff --git a/biojava-modfinder/pom.xml b/biojava-modfinder/pom.xml index 7351214cab..e0db50f1db 100644 --- a/biojava-modfinder/pom.xml +++ b/biojava-modfinder/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.6 + 7.2.7-SNAPSHOT biojava-modfinder biojava-modfinder @@ -31,7 +31,7 @@ org.biojava biojava-structure - 7.2.6 + 7.2.7-SNAPSHOT jar compile diff --git a/biojava-ontology/pom.xml b/biojava-ontology/pom.xml index 95fab178d6..eb9e3564c1 100644 --- a/biojava-ontology/pom.xml +++ b/biojava-ontology/pom.xml @@ -4,7 +4,7 @@ org.biojava biojava - 7.2.6 + 7.2.7-SNAPSHOT biojava-ontology diff --git a/biojava-protein-comparison-tool/pom.xml b/biojava-protein-comparison-tool/pom.xml index 84e76c523f..1527d691f4 100644 --- a/biojava-protein-comparison-tool/pom.xml +++ b/biojava-protein-comparison-tool/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.6 + 7.2.7-SNAPSHOT biojava-protein-comparison-tool @@ -36,23 +36,23 @@ org.biojava biojava-alignment - 7.2.6 + 7.2.7-SNAPSHOT org.biojava biojava-core - 7.2.6 + 7.2.7-SNAPSHOT org.biojava biojava-structure - 7.2.6 + 7.2.7-SNAPSHOT org.biojava biojava-structure-gui - 7.2.6 + 7.2.7-SNAPSHOT net.sourceforge.jmol diff --git a/biojava-protein-disorder/pom.xml b/biojava-protein-disorder/pom.xml index 006032e114..827c708a2b 100644 --- a/biojava-protein-disorder/pom.xml +++ b/biojava-protein-disorder/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.6 + 7.2.7-SNAPSHOT biojava-protein-disorder jar @@ -63,7 +63,7 @@ org.biojava biojava-core - 7.2.6 + 7.2.7-SNAPSHOT diff --git a/biojava-structure-gui/pom.xml b/biojava-structure-gui/pom.xml index 857645a396..21e01ce42e 100644 --- a/biojava-structure-gui/pom.xml +++ b/biojava-structure-gui/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.6 + 7.2.7-SNAPSHOT 4.0.0 biojava-structure-gui @@ -27,13 +27,13 @@ org.biojava biojava-structure - 7.2.6 + 7.2.7-SNAPSHOT compile org.biojava biojava-core - 7.2.6 + 7.2.7-SNAPSHOT compile diff --git a/biojava-structure/pom.xml b/biojava-structure/pom.xml index dec517fe60..f881200d48 100644 --- a/biojava-structure/pom.xml +++ b/biojava-structure/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.6 + 7.2.7-SNAPSHOT biojava-structure biojava-structure @@ -51,13 +51,13 @@ org.biojava biojava-alignment - 7.2.6 + 7.2.7-SNAPSHOT compile org.biojava biojava-core - 7.2.6 + 7.2.7-SNAPSHOT compile diff --git a/biojava-survival/pom.xml b/biojava-survival/pom.xml index 13cae1870e..570a2dd99b 100644 --- a/biojava-survival/pom.xml +++ b/biojava-survival/pom.xml @@ -4,7 +4,7 @@ org.biojava biojava - 7.2.6 + 7.2.7-SNAPSHOT biojava-survival diff --git a/biojava-ws/pom.xml b/biojava-ws/pom.xml index 0a38475285..c3fe2a9513 100644 --- a/biojava-ws/pom.xml +++ b/biojava-ws/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.6 + 7.2.7-SNAPSHOT biojava-ws biojava-ws @@ -19,7 +19,7 @@ org.biojava biojava-core - 7.2.6 + 7.2.7-SNAPSHOT compile diff --git a/pom.xml b/pom.xml index d3d58328ff..aeaafe6ffe 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ org.biojava biojava pom - 7.2.6 + 7.2.7-SNAPSHOT biojava BioJava is an open-source project dedicated to providing a Java framework for processing biological data. It provides analytical and statistical routines, parsers for common file formats and allows the @@ -51,7 +51,7 @@ scm:git:git@github.com:biojava/biojava.git https://github.com/biojava/biojava - biojava-7.2.6 + HEAD From e0b7797fe4e5a1c0e5f53df4f6cddb6e615da42b Mon Sep 17 00:00:00 2001 From: Amr ALHOSSARY Date: Tue, 18 Aug 2026 10:22:02 -0400 Subject: [PATCH 38/67] Write the entry identifier as a data item, not only as the block name AbstractCifFileSupplier put the PDB identifier only in the data_ block header, so it was lost for every consumer that reads it as a data item: - BioJava's own CifStructureConsumerImpl.consumeStruct reads _struct.entry_id, so Structure.getPdbId() came back null after a write-then-read round trip; - Jmol's mmCIF reader takes _M.pdbID from _entry.id, so a structure handed over with openStringInline(structure.toMMCIF()) arrived with no identifier, which in turn breaks anything keyed on the entry (for instance "isosurface ... eds", which needs an entry to fetch the electron density map for). Write both categories when the structure has an identifier. Both are needed: _entry.id is the canonical item and the one Jmol reads, while BioJava's own reader only looks at _struct.entry_id. Adds a round-trip regression test; there was nothing asserting that an identifier survived toMMCIF(). Fixes #1143 --- .../io/cif/AbstractCifFileSupplier.java | 21 +++++++++- .../io/cif/CifFileSupplierImplTest.java | 40 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/AbstractCifFileSupplier.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/AbstractCifFileSupplier.java index 6bf8af90ef..e43565c827 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/AbstractCifFileSupplier.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/AbstractCifFileSupplier.java @@ -40,8 +40,27 @@ protected CifFile getInternal(Structure structure, List wrappedAtom // entity information List entityInfos = structure.getEntityInfos(); + PdbId pdbId = structure.getPdbId(); + MmCifBlockBuilder blockBuilder = CifBuilder.enterFile(StandardSchemata.MMCIF) - .enterBlock(structure.getPdbId() == null? "" : structure.getPdbId().getId()); + .enterBlock(pdbId == null? "" : pdbId.getId()); + + if (pdbId != null) { + // The block header alone does not carry the identifier for consumers: readers pick it up from + // _entry.id (e.g. Jmol) or from _struct.entry_id (BioJava's own CifStructureConsumerImpl). + // Both are written so that the identifier survives a write-then-read round trip either way. + blockBuilder.enterEntry() + .enterId() + .add(pdbId.getId()) + .leaveColumn() + .leaveCategory(); + + blockBuilder.enterStruct() + .enterEntryId() + .add(pdbId.getId()) + .leaveColumn() + .leaveCategory(); + } blockBuilder.enterStructKeywords().enterText() .add(String.join(", ", structure.getPDBHeader().getKeywords())) diff --git a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/cif/CifFileSupplierImplTest.java b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/cif/CifFileSupplierImplTest.java index df227a8669..1d5f496ff5 100644 --- a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/cif/CifFileSupplierImplTest.java +++ b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/cif/CifFileSupplierImplTest.java @@ -1,5 +1,6 @@ package org.biojava.nbio.structure.io.cif; +import org.biojava.nbio.structure.PdbId; import org.biojava.nbio.structure.Structure; import org.biojava.nbio.structure.io.FileParsingParameters; import org.biojava.nbio.structure.io.PDBFileParser; @@ -41,4 +42,43 @@ public void shouldReadRawPdbOutputtingCifWithEntity() throws IOException { } } + + /** + * The identifier must be written as a data item and not only as the name of the data block: consumers read it + * from _entry.id or from _struct.entry_id, so writing the block header alone loses it. See issue #1143. + */ + @Test + public void shouldWriteEntryIdAndSurviveRoundTrip() throws IOException { + Structure s; + try (InputStream inStream = new GZIPInputStream(this.getClass().getResourceAsStream("/4hhb.cif.gz"))) { + s = CifStructureConverter.fromInputStream(inStream); + } + assertEquals(new PdbId("4HHB"), s.getPdbId()); + + String cifText = CifStructureConverter.toText(s); + assertTrue("_entry.id must be written", cifText.contains("_entry.id")); + assertTrue("_struct.entry_id must be written", cifText.contains("_struct.entry_id")); + + Structure readStruct = CifStructureConverter.fromInputStream( + new ByteArrayInputStream(cifText.getBytes())); + + assertEquals(s.getPdbId(), readStruct.getPdbId()); + assertEquals(s.getPdbId(), readStruct.getPDBHeader().getPdbId()); + } + + /** + * Structures without an identifier must not gain empty entry categories. + */ + @Test + public void shouldNotWriteEntryIdWhenPdbIdIsAbsent() throws IOException { + Structure s; + try (InputStream inStream = new GZIPInputStream(this.getClass().getResourceAsStream("/4hhb.cif.gz"))) { + s = CifStructureConverter.fromInputStream(inStream); + } + s.setPdbId(null); + + String cifText = CifStructureConverter.toText(s); + assertFalse(cifText.contains("_entry.id")); + assertFalse(cifText.contains("_struct.entry_id")); + } } From 83fe61a4718e096bfca3f622d2b51dc247c35adc Mon Sep 17 00:00:00 2001 From: josemduarte Date: Wed, 26 Aug 2026 15:43:52 -0700 Subject: [PATCH 39/67] Safeguard for case of nothing parseable in file --- .../nbio/structure/cath/CathInstallation.java | 9 +- .../structure/cath/CathInstallationTest.java | 84 +++++++++++++++++++ 2 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 biojava-structure/src/test/java/org/biojava/nbio/structure/cath/CathInstallationTest.java diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/cath/CathInstallation.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/cath/CathInstallation.java index 737bee9441..7e4b9abbc9 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/cath/CathInstallation.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/cath/CathInstallation.java @@ -350,15 +350,15 @@ private void parseCathDomainList() throws IOException { parseCathDomainList(buffer); } - private void parseCathDomainList(BufferedReader bufferedReader) throws IOException{ + protected void parseCathDomainList(BufferedReader bufferedReader) throws IOException{ String line; - // int counter = 0; + int counter = 0; while ( (line = bufferedReader.readLine()) != null ) { if ( line.startsWith("#") ) continue; if ( line.trim().isEmpty() ) continue; CathDomain cathDomain = parseCathListFileLine(line); if ( cathDomain == null ) continue; - // counter++; + counter++; String pdbId = cathDomain.getPdbIdAndChain().substring(0,4); // includes chain letter @@ -374,6 +374,9 @@ private void parseCathDomainList(BufferedReader bufferedReader) throws IOExcepti domainMap.put( cathDomain.getDomainName(), cathDomain ); } + if (counter == 0) { + throw new IOException("Could not parse any CATH domains from the domain list file."); + } } private void parseCathNames() throws IOException { diff --git a/biojava-structure/src/test/java/org/biojava/nbio/structure/cath/CathInstallationTest.java b/biojava-structure/src/test/java/org/biojava/nbio/structure/cath/CathInstallationTest.java new file mode 100644 index 0000000000..efad1ef077 --- /dev/null +++ b/biojava-structure/src/test/java/org/biojava/nbio/structure/cath/CathInstallationTest.java @@ -0,0 +1,84 @@ +/* + * BioJava development code + * + * This code may be freely distributed and modified under the + * terms of the GNU Lesser General Public Licence. This should + * be distributed with the code. If you do not have a copy, + * see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual + * authors. These should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, + * or to join the biojava-l mailing list, visit the home page + * at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.cath; + +import org.junit.jupiter.api.Test; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class CathInstallationTest { + + @Test + public void testParseCathDomainListSuccess() throws IOException { + String data = "# CATH domain list\n" + + "\n" + + "1oaiA00 1 10 490 10 1 1 1 1 1 124 1.80\n" + + "1oaiA01 1 10 490 10 1 1 1 1 2 150 1.80\n"; + + CathInstallation installation = new CathInstallation(""); + BufferedReader reader = new BufferedReader(new StringReader(data)); + installation.parseCathDomainList(reader); + + CathDomain domain = installation.getDomainByCathId("1oaiA00"); + assertNotNull(domain); + assertEquals("1oaiA00", domain.getDomainName()); + assertEquals(1, domain.getClassId()); + assertEquals(10, domain.getArchitectureId()); + assertEquals(490, domain.getTopologyId()); + assertEquals(10, domain.getHomologyId()); + assertEquals(124, domain.getLength()); + assertEquals(1.80, domain.getResolution(), 0.001); + } + + @Test + public void testParseCathDomainListEmptyThrowsException() { + CathInstallation installation = new CathInstallation(""); + BufferedReader reader = new BufferedReader(new StringReader("")); + assertThrows(IOException.class, () -> installation.parseCathDomainList(reader)); + } + + @Test + public void testParseCathDomainListOnlyCommentsAndWhitespaceThrowsException() { + String data = "# comment 1\n" + + "# comment 2\n" + + " \n" + + "\t\n"; + CathInstallation installation = new CathInstallation(""); + BufferedReader reader = new BufferedReader(new StringReader(data)); + assertThrows(IOException.class, () -> installation.parseCathDomainList(reader)); + } + + @Test + public void testParseCathDomainListNoParsableLinesThrowsException() { + String data = "# comment\n" + + "invalid line with too few tokens\n" + + "another bad line\n"; + CathInstallation installation = new CathInstallation(""); + BufferedReader reader = new BufferedReader(new StringReader(data)); + IOException exception = assertThrows(IOException.class, () -> installation.parseCathDomainList(reader)); + assertNotNull(exception.getMessage()); + } +} From 27bb418c2787cc5848d871bb5a4ae2a0145b4456 Mon Sep 17 00:00:00 2001 From: josemduarte Date: Wed, 26 Aug 2026 15:50:44 -0700 Subject: [PATCH 40/67] Logging --- .../java/org/biojava/nbio/structure/cath/CathInstallation.java | 1 + 1 file changed, 1 insertion(+) diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/cath/CathInstallation.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/cath/CathInstallation.java index 7e4b9abbc9..2caa8652dc 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/cath/CathInstallation.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/cath/CathInstallation.java @@ -535,6 +535,7 @@ private CathDomain parseCathListFileLine(String line) { private CathNode parseCathNamesFileLine(String line) { String[] token = line.trim().split("\\s+",3); if (token.length < 3) { + LOGGER.info("Invalid line in cath names file, was expecting 3 tokens but got {} tokens: {}", token.length, line); return null; } CathNode cathNode = new CathNode(); From 26c03d12d0b557eb24c75d917bd98bf2e5109ed4 Mon Sep 17 00:00:00 2001 From: Jose Duarte Date: Wed, 26 Aug 2026 21:44:37 -0700 Subject: [PATCH 41/67] Demoting --- .../java/org/biojava/nbio/structure/cath/CathInstallation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/cath/CathInstallation.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/cath/CathInstallation.java index 2caa8652dc..4201f8d5ca 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/cath/CathInstallation.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/cath/CathInstallation.java @@ -535,7 +535,7 @@ private CathDomain parseCathListFileLine(String line) { private CathNode parseCathNamesFileLine(String line) { String[] token = line.trim().split("\\s+",3); if (token.length < 3) { - LOGGER.info("Invalid line in cath names file, was expecting 3 tokens but got {} tokens: {}", token.length, line); + LOGGER.debug("Invalid line in cath names file, was expecting 3 tokens but got {} tokens: {}", token.length, line); return null; } CathNode cathNode = new CathNode(); From 9cd05a88de9eddbd465ece540760799bb6512953 Mon Sep 17 00:00:00 2001 From: Jose Duarte Date: Wed, 26 Aug 2026 22:15:06 -0700 Subject: [PATCH 42/67] Avoiding sqrt when unnecessary. About 8% performance gain --- .../biojava/nbio/structure/contact/Grid.java | 11 ++++++ .../nbio/structure/contact/GridCell.java | 34 +++++++++++-------- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/Grid.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/Grid.java index 0047385ab2..a5e86fe568 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/Grid.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/Grid.java @@ -66,6 +66,7 @@ public class Grid { private GridCell[][][] cells; private double cutoff; + private double cutoffSq; private int cellSize; private Point3d[] iAtoms; @@ -91,6 +92,7 @@ public class Grid { */ public Grid(double cutoff) { this.cutoff = cutoff; + this.cutoffSq = cutoff * cutoff; this.cellSize = (int) Math.floor(cutoff*SCALE); this.noOverlap = false; } @@ -496,6 +498,15 @@ public double getCutoff() { return cutoff; } + /** + * Returns the square of the cutoff, precomputed at construction. Used by {@link GridCell} to + * compare squared distances, avoiding a square root per candidate pair. + * @return the squared cutoff + */ + protected double getCutoffSq() { + return cutoffSq; + } + /** * Tells whether (after having added atoms to grid) the i and j grids are not overlapping. * Overlap is defined as enclosing bounds of the 2 grids being no more than one cell size apart. diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/GridCell.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/GridCell.java index 6028110eb6..e0802f392a 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/GridCell.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/GridCell.java @@ -74,14 +74,16 @@ public List getContactsWithinCell(){ Point3d[] iAtoms = grid.getIAtoms(); Point3d[] jAtoms = grid.getJAtoms(); - double cutoff = grid.getCutoff(); + // we compare squared distances to the squared cutoff, so that the expensive square root is + // only computed for the pairs that are actually in contact (the large majority are not) + double cutoffSq = grid.getCutoffSq(); if (jAtoms==null) { for (int i:iIndices) { for (int j:iIndices) { if (j>i) { - double distance = iAtoms[i].distance(iAtoms[j]); - if (distance getContactsWithinCell(){ } else { for (int i:iIndices) { for (int j:jIndices) { - double distance = iAtoms[i].distance(jAtoms[j]); - if (distance getContactsToOtherCell(GridCell otherCell){ Point3d[] iAtoms = grid.getIAtoms(); Point3d[] jAtoms = grid.getJAtoms(); - double cutoff = grid.getCutoff(); + // we compare squared distances to the squared cutoff, so that the expensive square root is + // only computed for the pairs that are actually in contact (the large majority are not) + double cutoffSq = grid.getCutoffSq(); if (jAtoms==null) { @@ -119,8 +123,8 @@ public List getContactsToOtherCell(GridCell otherCell){ for (int i:iIndices) { for (int j:otherCell.iIndices) { if (j>i) { - double distance = iAtoms[i].distance(iAtoms[j]); - if (distance getContactsToOtherCell(GridCell otherCell){ for (int i:iIndices) { for (int j:otherCell.jIndices) { - double distance = iAtoms[i].distance(jAtoms[j]); - if (distance getContactsToOtherCell(GridCell otherCell){ * @return */ public boolean hasContactToAtom(Point3d[] iAtoms, Point3d[] jAtoms, Point3d query, double cutoff) { + // only the comparison matters here, so we can stay in squared distance space and avoid square roots altogether + double cutoffSq = cutoff * cutoff; for( int i : iIndices ) { - double distance = iAtoms[i].distance(query); - if( distance Date: Wed, 26 Aug 2026 22:35:26 -0700 Subject: [PATCH 43/67] Use int arrays for performance. Gain is ~ 1.5x --- .../nbio/structure/contact/GridCell.java | 98 ++++++++++++++----- 1 file changed, 73 insertions(+), 25 deletions(-) diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/GridCell.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/GridCell.java index e0802f392a..9c75fa1c75 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/GridCell.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/GridCell.java @@ -21,6 +21,7 @@ package org.biojava.nbio.structure.contact; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import javax.vecmath.Point3d; @@ -35,30 +36,61 @@ public class GridCell { + /** + * Shared empty array so that cells that never receive indices (e.g. the j indices when only one + * set of atoms was added to the grid) don't allocate anything at all. + */ + private static final int[] EMPTY = new int[0]; + + /** + * Capacity of the index arrays on first insertion. Cell occupancy depends on the cutoff (the cell + * side is the cutoff), ranging from a handful of atoms for small cutoffs to a few tens for large + * ones, so we start small and grow geometrically. + */ + private static final int INITIAL_CAPACITY = 8; + private Grid grid; - private ArrayList iIndices; - private ArrayList jIndices; + + /** + * The indices of the i atoms in this cell, held as a primitive array to avoid the boxing (and the + * pointer chasing it entails) of a Collection of Integers: these are read in the innermost loop of + * the contact calculation. Only the first {@link #numIindices} elements are meaningful. + */ + private int[] iIndices; + private int numIindices; + + /** + * The indices of the j atoms in this cell. See {@link #iIndices}. + */ + private int[] jIndices; + private int numJindices; public GridCell(Grid parent){ - iIndices = new ArrayList<>(); - jIndices = new ArrayList<>(); + iIndices = EMPTY; + jIndices = EMPTY; this.grid = parent; } public void addIindex(int serial){ - iIndices.add(serial); + if (numIindices == iIndices.length) { + iIndices = Arrays.copyOf(iIndices, numIindices == 0 ? INITIAL_CAPACITY : numIindices * 2); + } + iIndices[numIindices++] = serial; } public void addJindex(int serial){ - jIndices.add(serial); + if (numJindices == jIndices.length) { + jIndices = Arrays.copyOf(jIndices, numJindices == 0 ? INITIAL_CAPACITY : numJindices * 2); + } + jIndices[numJindices++] = serial; } public int getNumIindices() { - return iIndices.size(); + return numIindices; } public int getNumJindices() { - return jIndices.size(); + return numJindices; } /** @@ -79,19 +111,25 @@ public List getContactsWithinCell(){ double cutoffSq = grid.getCutoffSq(); if (jAtoms==null) { - for (int i:iIndices) { - for (int j:iIndices) { + for (int a=0; ai) { - double distanceSq = iAtoms[i].distanceSquared(iAtoms[j]); + double distanceSq = atomI.distanceSquared(iAtoms[j]); if (distanceSq getContactsToOtherCell(GridCell otherCell){ if (jAtoms==null) { - for (int i:iIndices) { - for (int j:otherCell.iIndices) { + int[] otherIndices = otherCell.iIndices; + int otherNum = otherCell.numIindices; + for (int a=0; ai) { - double distanceSq = iAtoms[i].distanceSquared(iAtoms[j]); + double distanceSq = atomI.distanceSquared(iAtoms[j]); if (distanceSq getContactsToOtherCell(GridCell otherCell){ } else { - for (int i:iIndices) { - for (int j:otherCell.jIndices) { - double distanceSq = iAtoms[i].distanceSquared(jAtoms[j]); + int[] otherIndices = otherCell.jIndices; + int otherNum = otherCell.numJindices; + for (int a=0; a getContactsToOtherCell(GridCell otherCell){ public boolean hasContactToAtom(Point3d[] iAtoms, Point3d[] jAtoms, Point3d query, double cutoff) { // only the comparison matters here, so we can stay in squared distance space and avoid square roots altogether double cutoffSq = cutoff * cutoff; - for( int i : iIndices ) { - double distanceSq = iAtoms[i].distanceSquared(query); + for (int a=0; a Date: Thu, 27 Aug 2026 08:56:43 -0700 Subject: [PATCH 44/67] Test fix suggested by AI: test was failing when download was too slow --- .../org/biojava/nbio/structure/cath/CathInstallationTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/biojava-structure/src/test/java/org/biojava/nbio/structure/cath/CathInstallationTest.java b/biojava-structure/src/test/java/org/biojava/nbio/structure/cath/CathInstallationTest.java index efad1ef077..69944cb51c 100644 --- a/biojava-structure/src/test/java/org/biojava/nbio/structure/cath/CathInstallationTest.java +++ b/biojava-structure/src/test/java/org/biojava/nbio/structure/cath/CathInstallationTest.java @@ -24,6 +24,7 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.StringReader; +import java.util.concurrent.atomic.AtomicBoolean; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -41,6 +42,8 @@ public void testParseCathDomainListSuccess() throws IOException { CathInstallation installation = new CathInstallation(""); BufferedReader reader = new BufferedReader(new StringReader(data)); installation.parseCathDomainList(reader); + installation.setInstalledDomainList(new AtomicBoolean(true)); //1 + installation.setInstalledDomall(new AtomicBoolean(true)); //2 CathDomain domain = installation.getDomainByCathId("1oaiA00"); assertNotNull(domain); From 8af5253c9a5390f9f24409aab463d5e7f51b31c1 Mon Sep 17 00:00:00 2001 From: josemduarte Date: Thu, 27 Aug 2026 11:33:29 -0700 Subject: [PATCH 45/67] Logging cosmetics --- .../org/biojava/nbio/structure/ecod/EcodInstallation.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/ecod/EcodInstallation.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/ecod/EcodInstallation.java index 5bd59dd14b..f5d8d11be3 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/ecod/EcodInstallation.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/ecod/EcodInstallation.java @@ -1021,10 +1021,10 @@ private void warnMissingColumns(int lineNum) { private void warnUnparseableLine(int lineNum, IllegalArgumentException e) { if(warnNumberFormat > 1) { - logger.warn("Error in ECOD parsing at line "+lineNum,e); + logger.warn("Error in ECOD parsing at line {}: {}", lineNum, e.getMessage()); warnNumberFormat--; } else if(warnNumberFormat == 1) { - logger.warn("Error in ECOD parsing at line "+lineNum+". Not printing future similar warnings",e); + logger.warn("Error in ECOD parsing at line {}: {}. Not printing future similar warnings", lineNum, e.getMessage()); warnNumberFormat--; } } From d6a960419d3690e472a6367aaec65952f4514a23 Mon Sep 17 00:00:00 2001 From: josemduarte Date: Thu, 27 Aug 2026 12:04:45 -0700 Subject: [PATCH 46/67] Logging for resolution parsing: warn only when they are actually different --- .../main/java/org/biojava/nbio/structure/io/PDBFileParser.java | 2 +- .../nbio/structure/io/cif/CifStructureConsumerImpl.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/PDBFileParser.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/PDBFileParser.java index f5cc851fec..b1d327599e 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/PDBFileParser.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/PDBFileParser.java @@ -1406,7 +1406,7 @@ public void handleResolutionLine(String line, Pattern pR) { try { float res = Float.parseFloat(resString); final float resInHeader = pdbHeader.getResolution(); - if (resInHeader!=PDBHeader.DEFAULT_RESOLUTION && resInHeader != res) { + if (resInHeader!=PDBHeader.DEFAULT_RESOLUTION && Math.abs(resInHeader - res) > 0.001) { logger.warn("More than 1 resolution value present, will use last one {} and discard previous {} " ,resString, String.format("%4.2f",resInHeader)); } diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/CifStructureConsumerImpl.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/CifStructureConsumerImpl.java index 94b96b13f8..f44eb44ab1 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/CifStructureConsumerImpl.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/CifStructureConsumerImpl.java @@ -856,7 +856,8 @@ public void consumeRefine(Refine refine) { // we take the last one found so that behaviour is like in PDB file parsing double lsDResHigh = refine.getLsDResHigh().get(rowIndex); // TODO this could use a check to keep reasonable values - 1.5 may be overwritten by 0.0 - if (pdbHeader.getResolution() != PDBHeader.DEFAULT_RESOLUTION) { + if (pdbHeader.getResolution() != PDBHeader.DEFAULT_RESOLUTION && + Math.abs(pdbHeader.getResolution() - lsDResHigh) > 0.001) { logger.warn("More than 1 resolution value present, will use last one {} and discard previous {}", lsDResHigh, String.format("%4.2f",pdbHeader.getResolution())); } From 51ae01507382d07dde999bc6225f832a6fe2af4b Mon Sep 17 00:00:00 2001 From: Jose Duarte Date: Thu, 27 Aug 2026 21:21:11 -0700 Subject: [PATCH 47/67] Presize the AtomContactSet HashMap. Performance gain is ~ 1.2x --- .../structure/contact/AtomContactSet.java | 20 +++++++++++++++++++ .../biojava/nbio/structure/contact/Grid.java | 6 ++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/AtomContactSet.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/AtomContactSet.java index 34de552786..a4757de486 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/AtomContactSet.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/AtomContactSet.java @@ -37,6 +37,12 @@ public class AtomContactSet implements Serializable, Iterable { private static final long serialVersionUID = 1L; + /** + * The default load factor of a {@link HashMap}, needed to size the map from an expected number of + * entries. + */ + private static final float DEFAULT_LOAD_FACTOR = 0.75f; + private HashMap, AtomContact> contacts; private double cutoff; @@ -45,6 +51,20 @@ public AtomContactSet(double cutoff) { this.contacts = new HashMap<>(); } + /** + * Creates an AtomContactSet sized to hold the given number of contacts, so that the underlying map + * doesn't have to be repeatedly resized and rehashed as contacts are added. Contact calculations + * produce hundreds of thousands of contacts for a large structure, where the repeated rehashing + * that growing from the default capacity entails is a significant part of the cost. + * @param cutoff the distance cutoff + * @param expectedSize the number of contacts expected to be added. Only affects performance: an + * over-estimate merely leaves the map larger than it needs to be. + */ + public AtomContactSet(double cutoff, int expectedSize) { + this.cutoff = cutoff; + this.contacts = new HashMap<>((int) (expectedSize / DEFAULT_LOAD_FACTOR) + 1); + } + public void add(AtomContact contact) { this.contacts.put(getAtomIdPairFromContact(contact), contact); } diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/Grid.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/Grid.java index a5e86fe568..ba2fd08cdb 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/Grid.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/Grid.java @@ -382,10 +382,12 @@ private int[] getIntBounds(BoundingBox coordbounds) { */ public AtomContactSet getAtomContacts() { - AtomContactSet contacts = new AtomContactSet(cutoff); - List list = getIndicesContacts(); + // each contact maps to at most one entry in the set, so the number of index contacts sizes it + // without ever under-allocating + AtomContactSet contacts = new AtomContactSet(cutoff, list.size()); + if (jAtomObjects == null) { for (Contact cont : list) { contacts.add(new AtomContact(new Pair(iAtomObjects[cont.getI()],iAtomObjects[cont.getJ()]),cont.getDistance())); From 92adddc79422cba001aa6ac31aafaf8249d4927b Mon Sep 17 00:00:00 2001 From: Sergey Pyatykh Date: Fri, 28 Aug 2026 13:11:00 +0200 Subject: [PATCH 48/67] Finish biojava-genome JUnit 5 migration and drop junit-addons Replace FileAssert and leftover JUnit 4 APIs so genome tests compile without junit-addons. --- biojava-genome/pom.xml | 16 ---------------- .../nbio/genome/GeneFeatureHelperTest.java | 14 +++++++++----- .../nbio/genome/TestChromosomeMappingTools.java | 2 -- .../biojava/nbio/genome/io/fastq/FastqTest.java | 10 +--------- 4 files changed, 10 insertions(+), 32 deletions(-) diff --git a/biojava-genome/pom.xml b/biojava-genome/pom.xml index 34b3facf81..e469de412c 100644 --- a/biojava-genome/pom.xml +++ b/biojava-genome/pom.xml @@ -82,22 +82,6 @@ 7.2.7-SNAPSHOT compile - - junit-addons - junit-addons - 1.4 - test - - - xerces - xmlParserAPIs - - - xerces - xercesImpl - - - org.slf4j diff --git a/biojava-genome/src/test/java/org/biojava/nbio/genome/GeneFeatureHelperTest.java b/biojava-genome/src/test/java/org/biojava/nbio/genome/GeneFeatureHelperTest.java index 9c3a8877c6..53ab7c20cb 100644 --- a/biojava-genome/src/test/java/org/biojava/nbio/genome/GeneFeatureHelperTest.java +++ b/biojava-genome/src/test/java/org/biojava/nbio/genome/GeneFeatureHelperTest.java @@ -20,7 +20,6 @@ */ package org.biojava.nbio.genome; -import junitx.framework.FileAssert; import org.biojava.nbio.genome.parsers.gff.FeatureList; import org.biojava.nbio.genome.parsers.gff.GFF3Reader; import org.biojava.nbio.genome.parsers.gff.GFF3Writer; @@ -29,6 +28,7 @@ import org.biojava.nbio.core.sequence.ProteinSequence; import org.biojava.nbio.core.sequence.io.FastaWriterHelper; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; @@ -100,8 +100,10 @@ void testOutputFastaSequenceLengthGFF3() throws Exception { File gffFile = Files.createTempFile("volvox_length","gff3").toFile(); gffFile.deleteOnExit(); GeneFeatureHelper.outputFastaSequenceLengthGFF3(fastaSequenceFile, gffFile); - FileAssert.assertEquals("volvox_length.gff3 and volvox_length_output.gff3 are not equal", gffFile, - new File("src/test/resources/volvox_length_reference.gff3")); + Assertions.assertEquals( + Files.readString(new File("src/test/resources/volvox_length_reference.gff3").toPath()), + Files.readString(gffFile.toPath()), + "volvox_length.gff3 and volvox_length_output.gff3 are not equal"); } @@ -140,8 +142,10 @@ void testGetProteinSequences() throws Exception { File tmp = Files.createTempFile("volvox_all","faa").toFile(); tmp.deleteOnExit(); FastaWriterHelper.writeProteinSequence(tmp, proteinSequenceList.values()); - FileAssert.assertEquals("volvox_all_reference.faa and volvox_all.faa are not equal", new File( - "src/test/resources/volvox_all_reference.faa"), tmp); + Assertions.assertEquals( + Files.readString(new File("src/test/resources/volvox_all_reference.faa").toPath()), + Files.readString(tmp.toPath()), + "volvox_all_reference.faa and volvox_all.faa are not equal"); } /** diff --git a/biojava-genome/src/test/java/org/biojava/nbio/genome/TestChromosomeMappingTools.java b/biojava-genome/src/test/java/org/biojava/nbio/genome/TestChromosomeMappingTools.java index a86b8718c5..b17539a0ac 100644 --- a/biojava-genome/src/test/java/org/biojava/nbio/genome/TestChromosomeMappingTools.java +++ b/biojava-genome/src/test/java/org/biojava/nbio/genome/TestChromosomeMappingTools.java @@ -28,8 +28,6 @@ import java.util.Arrays; import java.util.List; -import static org.junit.Assert.assertEquals; - /** * Created by Yana Valasatava on 8/14/17. */ diff --git a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/FastqTest.java b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/FastqTest.java index 31102b251d..5bcbb43ee9 100755 --- a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/FastqTest.java +++ b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/FastqTest.java @@ -20,12 +20,9 @@ */ package org.biojava.nbio.genome.io.fastq; -import org.junit.Assert; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import org.junit.function.ThrowingRunnable; - /** * Unit test for Fastq. */ @@ -116,12 +113,7 @@ void testBuilder() @Test void testBuilderNullFastq() { - Assert.assertThrows(IllegalArgumentException.class, new ThrowingRunnable() { - @Override - public void run() { - Fastq.builder(null); - } - }); + Assertions.assertThrows(IllegalArgumentException.class, () -> Fastq.builder(null)); } @Test From 0e18f042dad3228eda941499e395ecfc670bf233 Mon Sep 17 00:00:00 2001 From: Sergey Pyatykh Date: Fri, 28 Aug 2026 13:20:37 +0200 Subject: [PATCH 49/67] Finish biojava-genome JUnit 5 migration and drop junit-addons Replace leftover JUnit 4 APIs so genome tests compile without junit-addons. --- .../biojava/nbio/genome/io/fastq/FastqBuilderTest.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/FastqBuilderTest.java b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/FastqBuilderTest.java index 3307256126..46957aeb26 100755 --- a/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/FastqBuilderTest.java +++ b/biojava-genome/src/test/java/org/biojava/nbio/genome/io/fastq/FastqBuilderTest.java @@ -20,12 +20,9 @@ */ package org.biojava.nbio.genome.io.fastq; -import org.junit.Assert; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import org.junit.function.ThrowingRunnable; - /** * Unit test for FastqBuilder. */ @@ -62,12 +59,7 @@ void testConstructorFastq() @Test void testConstructorNullFastq() { - Assert.assertThrows(IllegalArgumentException.class, new ThrowingRunnable() { - @Override - public void run() { - new FastqBuilder(null); - } - }); + Assertions.assertThrows(IllegalArgumentException.class, () -> new FastqBuilder(null)); } @Test From dd9092b5301dca5eefb9798ff9f92eebc7720644 Mon Sep 17 00:00:00 2001 From: Amr ALHOSSARY Date: Sun, 30 Aug 2026 07:41:52 -0400 Subject: [PATCH 50/67] Keep the ECOD read lock balanced when loading fails getDomainsForPdb and getAllDomains release the read lock inside their try block, so that the loader they call can take the write lock, and re-acquire it afterwards. When that loader throws, the re-acquisition never happens, and the outer finally unlocks a lock the thread no longer holds. The resulting IllegalMonitorStateException is thrown from a finally block, so it supersedes the IOException that actually caused the failure. Last night's nightly showed this: one upstream change - ECOD now answers with a 308 that HttpURLConnection will not follow - produced eight failures with three different-looking causes, only three of which named the redirect. Re-acquiring in a finally of its own keeps the lock balanced on both paths, so the original exception propagates intact. --- .../nbio/structure/ecod/EcodInstallation.java | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/ecod/EcodInstallation.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/ecod/EcodInstallation.java index 5bd59dd14b..aeb1c50ee5 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/ecod/EcodInstallation.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/ecod/EcodInstallation.java @@ -137,9 +137,15 @@ public List getDomainsForPdb(String id) throws IOException { // unlock to allow ensureDomainsFileInstalled to get the write lock logger.trace("UNLOCK readlock"); domainsFileLock.readLock().unlock(); - indexDomains(); - domainsFileLock.readLock().lock(); - logger.trace("LOCK readlock"); + try { + indexDomains(); + } finally { + // re-acquire even if indexing failed, so the outer finally has a + // lock to release; otherwise IllegalMonitorStateException replaces + // the real cause and the failure becomes unreadable + domainsFileLock.readLock().lock(); + logger.trace("LOCK readlock"); + } } PdbId pdbId = null; @@ -244,9 +250,15 @@ public List getAllDomains() throws IOException { // unlock to allow ensureDomainsFileInstalled to get the write lock logger.trace("UNLOCK readlock"); domainsFileLock.readLock().unlock(); - ensureDomainsFileInstalled(); - domainsFileLock.readLock().lock(); - logger.trace("LOCK readlock"); + try { + ensureDomainsFileInstalled(); + } finally { + // re-acquire even if the download failed, so the outer finally has a + // lock to release; otherwise IllegalMonitorStateException replaces + // the real cause and the failure becomes unreadable + domainsFileLock.readLock().lock(); + logger.trace("LOCK readlock"); + } } return allDomains; } finally { From ce0f394f927feff0ad6a2077e441568fe2585dbf Mon Sep 17 00:00:00 2001 From: Amr ALHOSSARY Date: Sun, 30 Aug 2026 09:30:20 -0400 Subject: [PATCH 51/67] Follow the redirects HttpURLConnection declines The JDK follows 301, 302 and 303 within a protocol, but never follows 307 or 308, and never follows a redirect that changes http to https. Both gaps have broken this build: CATH began answering http with a 301 to https, and ECOD now answers with a 308 to a rewritten path. A browser follows either without comment, so neither service had reason to expect it would break us. openConnectionFollowingRedirects handles what the JDK leaves, resolving relative locations, capping the chain at five hops and reporting a loop rather than chasing it. A redirect from https to http is refused: the transport must never be downgraded silently. Anything refused is returned as it is, so checkHttpStatus still decides what a non-2xx status means. The decision is split into redirectTargetFor(code, location, url) so the rules can be tested without a server; the end-to-end tests use a local HttpServer rather than a real service. Fixes #1149 --- .../nbio/core/util/FileDownloadUtils.java | 127 +++++++++- .../core/util/FileDownloadRedirectTest.java | 226 ++++++++++++++++++ 2 files changed, 344 insertions(+), 9 deletions(-) create mode 100644 biojava-core/src/test/java/org/biojava/nbio/core/util/FileDownloadRedirectTest.java diff --git a/biojava-core/src/main/java/org/biojava/nbio/core/util/FileDownloadUtils.java b/biojava-core/src/main/java/org/biojava/nbio/core/util/FileDownloadUtils.java index 5b8c656658..63fee00d57 100644 --- a/biojava-core/src/main/java/org/biojava/nbio/core/util/FileDownloadUtils.java +++ b/biojava-core/src/main/java/org/biojava/nbio/core/util/FileDownloadUtils.java @@ -29,6 +29,7 @@ import java.io.InputStream; import java.io.PrintStream; import java.net.HttpURLConnection; +import java.net.MalformedURLException; import java.net.SocketTimeoutException; import java.net.URL; import java.net.URLConnection; @@ -38,7 +39,9 @@ import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.LinkedHashSet; import java.util.Scanner; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -54,6 +57,9 @@ public class FileDownloadUtils { /** Buffer used when streaming a file through a {@link MessageDigest}. */ private static final int DIGEST_BUFFER_SIZE = 64 * 1024; + /** Redirects to follow before giving up, in case a server sends us in a circle. */ + private static final int MAX_REDIRECTS = 5; + /** A bare hex digest, optionally followed by whitespace and a file name (the * layout written by md5sum, sha1sum and friends). */ private static final Pattern BARE_HEX_HASH = Pattern.compile("^([0-9a-fA-F]{32,128})(?:[\\s*].*)?$"); @@ -139,8 +145,7 @@ public static void downloadFile(URL url, File destination) throws IOException { try { while (true) { try { - URLConnection connection = prepareURLConnection(url.toString(), timeout); - connection.connect(); + URLConnection connection = openConnectionFollowingRedirects(url, timeout); checkHttpStatus(connection); try (InputStream inputStream = connection.getInputStream()) { // Files.copy loops until end of stream. FileChannel.transferFrom(), used @@ -199,8 +204,7 @@ public static void downloadFileWithValidation(URL url, File destination, URL has File tempFile = createTempFileFor(destination); try { - URLConnection connection = prepareURLConnection(url.toString(), timeout); - connection.connect(); + URLConnection connection = openConnectionFollowingRedirects(url, timeout); checkHttpStatus(connection); long declaredSize = connection.getContentLengthLong(); @@ -252,6 +256,111 @@ public static void downloadFileWithValidation(URL url, File destination, URL has } } + /** + * Opens a connection, following any redirect that {@link HttpURLConnection} + * declines to follow itself. + *

+ * The JDK follows 301, 302 and 303 within a protocol, but it never follows 307 or + * 308, and it never follows a redirect that changes http to https. Both gaps have + * broken downloads in practice: CATH began answering http with a 301 to https, and + * ECOD now answers with a 308 to a rewritten path. A browser follows either without + * comment, so a service making that change has no reason to expect it to break us. + *

+ * A redirect from https to http is deliberately not followed: a redirect + * must never silently downgrade the transport. Such a response is returned as it is, + * for {@link #checkHttpStatus(URLConnection)} to reject. + * + * @param url the URL to open + * @param timeout connect and read timeout, in milliseconds + * @return a connected {@link URLConnection} at the final location + * @throws HttpStatusException if the redirects loop or exceed the limit + * @throws IOException if the connection could not be opened + * @author Amr ALHOSSARY + * @since 7.3.0 + */ + public static URLConnection openConnectionFollowingRedirects(URL url, int timeout) throws IOException { + Set visited = new LinkedHashSet<>(); + URL current = url; + for (int hop = 0; hop <= MAX_REDIRECTS; hop++) { + if (!visited.add(current.toString())) { + throw new HttpStatusException(HttpURLConnection.HTTP_SEE_OTHER, url.toString(), + "Redirect loop: " + String.join(" -> ", visited)); + } + URLConnection connection = prepareURLConnection(current.toString(), timeout); + connection.connect(); + if (!(connection instanceof HttpURLConnection)) { + return connection; + } + URL next = redirectTarget((HttpURLConnection) connection, current); + if (next == null) { + // either not a redirect, or one we decline to follow; the caller's + // checkHttpStatus decides what a non-2xx status means + return connection; + } + logger.info("{} redirects to {}; following.", current, next); + ((HttpURLConnection) connection).disconnect(); + current = next; + } + throw new HttpStatusException(HttpURLConnection.HTTP_SEE_OTHER, url.toString(), + "More than " + MAX_REDIRECTS + " redirects starting at " + url); + } + + /** + * Works out where a response redirects to, for the redirects the JDK leaves to us. + * + * @param http a connected connection whose status has not yet been acted on + * @param current the URL that was requested, used to resolve a relative location + * @return the redirect target, or null if this is not a redirect we should follow + * @throws IOException if the status could not be read + * @since 7.3.0 + */ + private static URL redirectTarget(HttpURLConnection http, URL current) throws IOException { + return redirectTargetFor(http.getResponseCode(), http.getHeaderField("Location"), current); + } + + /** + * Decides where a response redirects to, given only its status and location. Split + * out from {@link #redirectTarget(HttpURLConnection, URL)} so that the rules can be + * tested without standing up a server. + * + * @param code the HTTP status + * @param location the Location header, may be null, relative or absolute + * @param current the URL that was requested, used to resolve a relative location + * @return the redirect target, or null if this is not a redirect we should follow + * @since 7.3.0 + */ + static URL redirectTargetFor(int code, String location, URL current) { + // 301, 302 and 303 only reach us when the JDK declined them, which it does when + // the protocol changes. 307 and 308 it never follows at all. + boolean redirect = code == HttpURLConnection.HTTP_MOVED_PERM + || code == HttpURLConnection.HTTP_MOVED_TEMP + || code == HttpURLConnection.HTTP_SEE_OTHER + || code == 307 + || code == 308; + if (!redirect) { + return null; + } + if (location == null || location.trim().isEmpty()) { + logger.warn("{} returned {} with no Location header.", current, code); + return null; + } + URL target; + try { + // resolves a relative Location, which is what ECOD sends + target = new URL(current, location.trim()); + } catch (MalformedURLException e) { + logger.warn("{} returned {} to an unusable Location [{}].", current, code, location); + return null; + } + if ("https".equalsIgnoreCase(current.getProtocol()) + && !"https".equalsIgnoreCase(target.getProtocol())) { + logger.warn("Refusing to follow {} from {} to [{}]: a redirect must not downgrade https to {}.", + code, current, target, target.getProtocol()); + return null; + } + return target; + } + /** * Verifies that an HTTP connection returned a 2xx status. Connections using a * non-HTTP protocol (file:, ftp:, ...) are left alone. @@ -277,10 +386,10 @@ public static void checkHttpStatus(URLConnection connection) throws IOException return; } if (code == 301 || code == 302 || code == 307 || code == 308) { - // The JDK follows redirects automatically, but never across protocols, so - // an http -> https redirect surfaces here and is worth naming explicitly. - logger.warn("{} returned redirect {} to [{}], which was not followed " - + "(the JDK does not follow redirects that change protocol).", + // openConnectionFollowingRedirects handles the redirects the JDK will not, + // so one reaching here was declined deliberately: an https to http + // downgrade, a missing or unusable Location, or too many hops. + logger.warn("{} returned redirect {} to [{}], which was not followed.", connection.getURL(), code, http.getHeaderField("Location")); } throw new HttpStatusException(code, connection.getURL().toString(), http.getResponseMessage()); @@ -318,7 +427,7 @@ public static void createValidationFiles(URL url, File localDestination, URL has public static void createValidationFiles(URL url, File localDestination, URL hashURL, Hash hash, ETagPolicy eTagPolicy){ try { - URLConnection resourceConnection = url.openConnection(); + URLConnection resourceConnection = openConnectionFollowingRedirects(url, 60000); createValidationFiles(resourceConnection, localDestination, hashURL, hash, eTagPolicy); } catch (IOException e) { logger.warn("could not open connection to resource file due to exception: {}", e.getMessage()); diff --git a/biojava-core/src/test/java/org/biojava/nbio/core/util/FileDownloadRedirectTest.java b/biojava-core/src/test/java/org/biojava/nbio/core/util/FileDownloadRedirectTest.java new file mode 100644 index 0000000000..2c46127ffc --- /dev/null +++ b/biojava-core/src/test/java/org/biojava/nbio/core/util/FileDownloadRedirectTest.java @@ -0,0 +1,226 @@ +/* + * BioJava development code + * + * This code may be freely distributed and modified under the + * terms of the GNU Lesser General Public Licence. This should + * be distributed with the code. If you do not have a copy, + * see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual + * authors. These should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, + * or to join the biojava-l mailing list, visit the home page + * at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.core.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import com.sun.net.httpserver.HttpServer; + +/** + * Checks that downloads follow the redirects {@link java.net.HttpURLConnection} does + * not follow by itself. + *

+ * The JDK handles 301, 302 and 303 within a protocol, but never 307 or 308, and never + * a redirect that changes http to https. Both gaps have broken this project's builds: + * CATH began answering http with a 301 to https, and ECOD now answers with a 308 to a + * rewritten path. A browser follows either without comment. + *

+ * The rule tests need no network and no server. The end-to-end tests use a local + * {@link HttpServer} rather than a real service, so that they cannot fail because a + * third party is having a bad day. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +class FileDownloadRedirectTest { + + private static final String PAYLOAD = "the file you were looking for\n"; + + @Nested + class RedirectRules { + + private final URL from = url("http://example.org/ecod/distributions/ecod.latest.domains.txt"); + + @Test + void aRelativeLocationIsResolvedAgainstTheRequest() { + // exactly what ECOD sends: same host, same protocol, relative path + assertEquals(url("http://example.org/ecod-legacy/distributions/ecod.latest.domains.txt"), + FileDownloadUtils.redirectTargetFor(308, + "/ecod-legacy/distributions/ecod.latest.domains.txt", from)); + } + + @Test + void anAbsoluteLocationIsUsedAsGiven() { + assertEquals(url("https://example.org/elsewhere.txt"), + FileDownloadUtils.redirectTargetFor(301, "https://example.org/elsewhere.txt", from)); + } + + @Test + void everyRedirectStatusWeHandleIsRecognised() { + for (int code : new int[] { 301, 302, 303, 307, 308 }) { + assertEquals(url("http://example.org/x"), + FileDownloadUtils.redirectTargetFor(code, "/x", from), + "status " + code + " should be followed"); + } + } + + @Test + void aSuccessIsNotARedirect() { + assertNull(FileDownloadUtils.redirectTargetFor(200, null, from)); + assertNull(FileDownloadUtils.redirectTargetFor(404, "/x", from)); + } + + /** + * A redirect must never quietly move us onto an unencrypted transport. + */ + @Test + void httpsIsNeverDowngradedToHttp() { + URL secure = url("https://example.org/file.txt"); + assertNull(FileDownloadUtils.redirectTargetFor(301, "http://example.org/file.txt", secure)); + assertNull(FileDownloadUtils.redirectTargetFor(308, "http://elsewhere.org/file.txt", secure)); + } + + @Test + void httpToHttpsIsFollowed() { + // the CATH case + assertEquals(url("https://example.org/file.txt"), + FileDownloadUtils.redirectTargetFor(301, "https://example.org/file.txt", + url("http://example.org/file.txt"))); + } + + @Test + void anUnusableLocationIsNotFollowed() { + assertNull(FileDownloadUtils.redirectTargetFor(308, null, from)); + assertNull(FileDownloadUtils.redirectTargetFor(308, " ", from)); + assertNull(FileDownloadUtils.redirectTargetFor(308, "gopher://example.org/x", from)); + } + } + + @Nested + class EndToEnd { + + private HttpServer server; + private String base; + private File dir; + + @BeforeEach + void start() throws IOException { + server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + base = "http://127.0.0.1:" + server.getAddress().getPort(); + dir = Files.createTempDirectory("redirectTest").toFile(); + + serve("/final", 200, null); + // the ECOD shape: 308 with a relative Location + serve("/moved", 308, "/final"); + serve("/temp", 307, "/final"); + // a chain that returns to its start + serve("/loop-a", 308, "/loop-b"); + serve("/loop-b", 308, "/loop-a"); + // longer than the hop limit + for (int i = 0; i < 9; i++) { + serve("/hop" + i, 308, "/hop" + (i + 1)); + } + serve("/hop9", 200, null); + serve("/nowhere", 308, null); + server.start(); + } + + private void serve(String path, int status, String location) { + server.createContext(path, exchange -> { + byte[] body = PAYLOAD.getBytes(StandardCharsets.UTF_8); + if (location != null) { + exchange.getResponseHeaders().add("Location", location); + } + exchange.sendResponseHeaders(status, status == 200 ? body.length : -1); + if (status == 200) { + try (OutputStream out = exchange.getResponseBody()) { + out.write(body); + } + } + exchange.close(); + }); + } + + @AfterEach + void stop() throws IOException { + server.stop(0); + FileDownloadUtils.deleteDirectory(dir.getAbsolutePath()); + } + + @Test + void a308IsFollowed() throws IOException { + File got = new File(dir, "moved.txt"); + FileDownloadUtils.downloadFile(new URL(base + "/moved"), got); + assertEquals(PAYLOAD, new String(Files.readAllBytes(got.toPath()), StandardCharsets.UTF_8)); + } + + @Test + void a307IsFollowed() throws IOException { + File got = new File(dir, "temp.txt"); + FileDownloadUtils.downloadFile(new URL(base + "/temp"), got); + assertEquals(PAYLOAD, new String(Files.readAllBytes(got.toPath()), StandardCharsets.UTF_8)); + } + + @Test + void theRedirectBodyIsNeverWhatWeStore() throws IOException { + File got = new File(dir, "validated.txt"); + FileDownloadUtils.downloadFileWithValidation(new URL(base + "/moved"), got, null, + FileDownloadUtils.Hash.UNKNOWN, FileDownloadUtils.ETagPolicy.IGNORE); + assertEquals(PAYLOAD, new String(Files.readAllBytes(got.toPath()), StandardCharsets.UTF_8)); + assertTrue(FileDownloadUtils.validateFile(got), "the recorded size must describe the real file"); + } + + @Test + void aLoopIsReportedRatherThanChasedForever() { + File got = new File(dir, "loop.txt"); + HttpStatusException e = assertThrows(HttpStatusException.class, + () -> FileDownloadUtils.downloadFile(new URL(base + "/loop-a"), got)); + assertTrue(e.getMessage().contains("loop"), e.getMessage()); + } + + @Test + void tooManyHopsGivesUp() { + File got = new File(dir, "hops.txt"); + assertThrows(HttpStatusException.class, + () -> FileDownloadUtils.downloadFile(new URL(base + "/hop0"), got)); + } + + @Test + void aRedirectWithNoDestinationIsAnError() { + File got = new File(dir, "nowhere.txt"); + assertThrows(HttpStatusException.class, + () -> FileDownloadUtils.downloadFile(new URL(base + "/nowhere"), got)); + } + } + + private static URL url(String spec) { + try { + return new URL(spec); + } catch (IOException e) { + throw new IllegalArgumentException(spec, e); + } + } +} From a203e679e83781eea57eb530275be8c5becf8dad Mon Sep 17 00:00:00 2001 From: Amr ALHOSSARY Date: Sat, 15 Aug 2026 15:26:58 -0400 Subject: [PATCH 52/67] Bump the development version to 7.3.0-SNAPSHOT The electron density work and the FileDownloadUtils changes that go with it add public API and change a default, which belongs in a minor release rather than a patch. Their @since tags say 7.3.0, so the project version should agree. Kept as a standalone commit on its own branch so it can be taken or dropped independently of the feature work, and so the release manager can decide when it lands. --- biojava-aa-prop/pom.xml | 6 +++--- biojava-alignment/pom.xml | 4 ++-- biojava-core/pom.xml | 2 +- biojava-genome/pom.xml | 6 +++--- biojava-integrationtest/pom.xml | 4 ++-- biojava-modfinder/pom.xml | 4 ++-- biojava-ontology/pom.xml | 2 +- biojava-protein-comparison-tool/pom.xml | 10 +++++----- biojava-protein-disorder/pom.xml | 4 ++-- biojava-structure-gui/pom.xml | 6 +++--- biojava-structure/pom.xml | 6 +++--- biojava-survival/pom.xml | 2 +- biojava-ws/pom.xml | 4 ++-- pom.xml | 2 +- 14 files changed, 31 insertions(+), 31 deletions(-) diff --git a/biojava-aa-prop/pom.xml b/biojava-aa-prop/pom.xml index 22bc632906..04db67d2ac 100644 --- a/biojava-aa-prop/pom.xml +++ b/biojava-aa-prop/pom.xml @@ -2,7 +2,7 @@ biojava org.biojava - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT 4.0.0 biojava-aa-prop @@ -70,12 +70,12 @@ org.biojava biojava-core - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT org.biojava biojava-structure - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT diff --git a/biojava-alignment/pom.xml b/biojava-alignment/pom.xml index 7ff8f29cf4..7f0594181f 100644 --- a/biojava-alignment/pom.xml +++ b/biojava-alignment/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT biojava-alignment biojava-alignment @@ -47,7 +47,7 @@ org.biojava biojava-core - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT compile diff --git a/biojava-core/pom.xml b/biojava-core/pom.xml index 4826cf9d17..2085e47b5b 100644 --- a/biojava-core/pom.xml +++ b/biojava-core/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT 4.0.0 biojava-core diff --git a/biojava-genome/pom.xml b/biojava-genome/pom.xml index e469de412c..956cfaa2dd 100644 --- a/biojava-genome/pom.xml +++ b/biojava-genome/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT 4.0.0 biojava-genome @@ -73,13 +73,13 @@ org.biojava biojava-core - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT compile org.biojava biojava-alignment - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT compile diff --git a/biojava-integrationtest/pom.xml b/biojava-integrationtest/pom.xml index 870612fc63..81ec390cc7 100644 --- a/biojava-integrationtest/pom.xml +++ b/biojava-integrationtest/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT biojava-integrationtest jar @@ -40,7 +40,7 @@ org.biojava biojava-structure - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT diff --git a/biojava-modfinder/pom.xml b/biojava-modfinder/pom.xml index e0db50f1db..343aa74f6c 100644 --- a/biojava-modfinder/pom.xml +++ b/biojava-modfinder/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT biojava-modfinder biojava-modfinder @@ -31,7 +31,7 @@ org.biojava biojava-structure - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT jar compile diff --git a/biojava-ontology/pom.xml b/biojava-ontology/pom.xml index eb9e3564c1..efc24f1290 100644 --- a/biojava-ontology/pom.xml +++ b/biojava-ontology/pom.xml @@ -4,7 +4,7 @@ org.biojava biojava - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT biojava-ontology diff --git a/biojava-protein-comparison-tool/pom.xml b/biojava-protein-comparison-tool/pom.xml index 1527d691f4..8f6fb7f420 100644 --- a/biojava-protein-comparison-tool/pom.xml +++ b/biojava-protein-comparison-tool/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT biojava-protein-comparison-tool @@ -36,23 +36,23 @@ org.biojava biojava-alignment - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT org.biojava biojava-core - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT org.biojava biojava-structure - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT org.biojava biojava-structure-gui - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT net.sourceforge.jmol diff --git a/biojava-protein-disorder/pom.xml b/biojava-protein-disorder/pom.xml index 827c708a2b..434f6d3f18 100644 --- a/biojava-protein-disorder/pom.xml +++ b/biojava-protein-disorder/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT biojava-protein-disorder jar @@ -63,7 +63,7 @@ org.biojava biojava-core - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT diff --git a/biojava-structure-gui/pom.xml b/biojava-structure-gui/pom.xml index 21e01ce42e..14a38eb37f 100644 --- a/biojava-structure-gui/pom.xml +++ b/biojava-structure-gui/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT 4.0.0 biojava-structure-gui @@ -27,13 +27,13 @@ org.biojava biojava-structure - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT compile org.biojava biojava-core - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT compile diff --git a/biojava-structure/pom.xml b/biojava-structure/pom.xml index 647ab49c72..e3b5163341 100644 --- a/biojava-structure/pom.xml +++ b/biojava-structure/pom.xml @@ -4,7 +4,7 @@ biojava org.biojava - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT biojava-structure biojava-structure @@ -51,13 +51,13 @@ org.biojava biojava-alignment - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT compile org.biojava biojava-core - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT compile diff --git a/biojava-survival/pom.xml b/biojava-survival/pom.xml index 570a2dd99b..8115693dfc 100644 --- a/biojava-survival/pom.xml +++ b/biojava-survival/pom.xml @@ -4,7 +4,7 @@ org.biojava biojava - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT biojava-survival diff --git a/biojava-ws/pom.xml b/biojava-ws/pom.xml index c3fe2a9513..ebc18949ed 100644 --- a/biojava-ws/pom.xml +++ b/biojava-ws/pom.xml @@ -3,7 +3,7 @@ biojava org.biojava - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT biojava-ws biojava-ws @@ -19,7 +19,7 @@ org.biojava biojava-core - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT compile diff --git a/pom.xml b/pom.xml index 79db94ee26..dbed771382 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ org.biojava biojava pom - 7.2.7-SNAPSHOT + 7.3.0-SNAPSHOT biojava BioJava is an open-source project dedicated to providing a Java framework for processing biological data. It provides analytical and statistical routines, parsers for common file formats and allows the From c39c285644625b70736a185c387daf765c1a9cd2 Mon Sep 17 00:00:00 2001 From: Amr ALHOSSARY Date: Sun, 30 Aug 2026 10:00:34 -0400 Subject: [PATCH 53/67] Draft the 7.3.0 changelog section Covers what is merged since 7.2.6 and what is open and expected to land: the download and checksum work, the CATH and ECOD fixes, the contact and ASA performance tweaks, electron density, and the JUnit 5 migration. Five entries are for pull requests that are still open - #1134, #1147, #1148, #1150 and #1151 - and should be checked against what actually merged before the release is tagged. --- CHANGELOG.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 659ec4cdf1..77848fde9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,55 @@ BioJava Changelog ----------------- +BioJava 7.3.0 +============================== +### Added +* Fetching, caching and display of electron density and cryo-EM maps #1134 +* Checksum verification (MD5, SHA-1, SHA-256) in `FileDownloadUtils`, replacing the previous + stub. Downloads from `files.wwpdb.org` and `files.rcsb.org` are checksummed from their `ETag` + without a second request #1133 +* `HttpStatusException`, so callers can tell "not there" from a transport failure #1133 +* `FileDownloadUtils.downloadFileWithValidation()`, which downloads and validates over a single + connection #1133 +* `LocalPDBDirectory.getMiddleHash(String)`, computing the two-character directory from the + right so that it is correct for both short and extended PDB IDs #1133 +* Support for the ECOD distribution format introduced at v294.1 #1141 #1139 + +### Performance +* Contact calculation is about 1.5x faster: squared distances compared against a squared cutoff, + primitive arrays in `GridCell` instead of boxed lists, and a pre-sized `AtomContactSet` #1147 +* ASA calculation is about 1.2-1.3x faster, by the same replacement of objects with flat + primitive arrays in the hot loops #1148 +* `EcodInstallation.getVersion()` reads the file header instead of parsing every domain. The + current release is 653 MB and holds nearly three million records #1141 + +### Fixed +* CATH downloads use https and check the response before caching it. The previous http URL began + redirecting, and the redirect body was cached as classification data #1133 #1138 +* CATH parsing no longer throws `ArrayIndexOutOfBoundsException` on blank or truncated lines, and + cached files are validated before being used #1145 +* Chemical component downloads reject redirects and error responses rather than caching them #1133 +* Redirects that `HttpURLConnection` does not follow by itself - 307, 308, and any that change + http to https - are now followed #1151 #1149 +* `FileDownloadUtils`: the `hash` argument was ignored by one overload; downloads could be + silently truncated; temporary files leaked on failure; `validateFile` threw on a file with no + parent directory and on an empty `.size` sidecar #1133 +* mmCIF writer emits the entry identifier as a data item, so `getPdbId()` survives a + write-then-read round trip #1144 #1143 +* The ECOD read lock is left balanced when a download fails, so the original error is no longer + replaced by `IllegalMonitorStateException` #1150 +* Resolution parsing warns only when the values actually differ + +### Changed +* `createValidationFiles()` now defaults to `ETagPolicy.USE_IF_HEX_DIGEST`, so existing callers + begin recording checksums where the server offers one #1133 +* Integration tests run nightly rather than on every pull request #1137 #1135 +* Tests migrated to JUnit 5 #1125 #1126 #1038 +* Library upgrades #1130 #1132 + +### Removed +* The `junit-addons` test dependency, no longer needed after the JUnit 5 migration #1126 + BioJava 7.2.6 ============================== ### Fixed From f6aefcebc8cdb7d1ca7909f1c85cac2e2621fcf5 Mon Sep 17 00:00:00 2001 From: Amr ALHOSSARY Date: Sat, 15 Aug 2026 14:52:25 -0400 Subject: [PATCH 54/67] Add electron density and cryo-EM map fetching with a fallback chain Adds org.biojava.nbio.structure.io.density, which downloads and caches density maps the same way LocalPDBDirectory caches coordinate files, and hands back a File that a viewer can contour. Closes #947. Several services publish density for the PDB and they differ enormously in size for the same entry, so rather than picking one, sources are tried in order until one answers. The order is smallest-adequate-first, because the smallest form is usually perfectly good to look at: X-ray: RCSB density server -> PDBe CCP4 -> PDBe density server -> wwPDB map coefficients (disabled by default) cryo-EM: RCSB density server -> PDBe density server -> EMDB primary map For 1cbs a density server slice is about a tenth the size of the equivalent pair of CCP4 files. For the map behind 6hu9 it is 3.7 MB against a 106 MB primary map. A size limit, 256 MiB by default, is checked against the size EMDB itself reports before any of the body is transferred, and exceeding it is not an error: the chain simply falls back to a smaller representation. wwPDB map coefficients are supported for completeness, since they are the route RCSB documents now that edmaps.rcsb.org has shut down, but they are structure factors rather than a sampled grid and cannot be displayed without a Fourier transform. They are therefore disabled by default, and DensityFileFormat carries an isJmolLoadable() flag so that a viewer can refuse them rather than silently drawing nothing. Notes on the design: * A source that has nothing for an entry is skipped and the next is tried, but any other transport failure aborts the chain. A network outage must never be reported as "this entry has no density". When every source is exhausted, NoDensityMapException carries the reason from each one, so a caller can say why rather than just that it failed. * A density server response contains both the 2Fo-Fc and the Fo-Fc blocks, so the two kinds share one cache entry instead of downloading the identical file twice. Which block to read is a display-time decision. * Cryo-EM entries are found through their EMDB identifier, looked up from EMDB's search API with RCSB as a fallback. That lookup also yields the contour level the depositors recommend, which is how an EM map should be contoured; it is attached to the result whichever source supplied the voxels. The experimental method is never inferred from resolution, which BioJava parses incorrectly for some cryo-EM entries (#1000). * Ccp4Header checks for the MAP stamp at byte 208, so a server that answers with an error page and HTTP 200 produces a clean cache miss rather than a corrupt cache entry. * Cached results are fully described by a .meta sidecar, so LOCAL_ONLY requests are served without opening a connection. DemoFetchElectronDensity exercises all three outcomes: an X-ray entry, a cryo-EM entry resolved through EMDB, and an entry deposited without structure factors. --- .../java/demo/DemoFetchElectronDensity.java | 90 +++ .../density/AbstractDensityMapProvider.java | 360 ++++++++++++ .../nbio/structure/io/density/Ccp4Header.java | 130 +++++ .../io/density/DensityCacheLayout.java | 195 +++++++ .../io/density/DensityFileFormat.java | 89 +++ .../structure/io/density/DensityMapCache.java | 526 ++++++++++++++++++ .../structure/io/density/DensityMapKind.java | 99 ++++ .../io/density/DensityMapProvider.java | 78 +++ .../io/density/DensityMapRequest.java | 275 +++++++++ .../io/density/DensityMapResult.java | 282 ++++++++++ .../io/density/DensityMapSource.java | 84 +++ .../density/DensityMapTooLargeException.java | 84 +++ .../structure/io/density/EmdbEntryInfo.java | 85 +++ .../io/density/EmdbEntryResolver.java | 382 +++++++++++++ .../structure/io/density/EmdbMapProvider.java | 142 +++++ .../io/density/NoDensityMapException.java | 99 ++++ .../io/density/PdbeCcp4MapProvider.java | 142 +++++ .../structure/io/density/UrlTemplates.java | 112 ++++ .../io/density/VolumeServerProvider.java | 215 +++++++ .../density/WwpdbMapCoefficientsProvider.java | 147 +++++ 20 files changed, 3616 insertions(+) create mode 100644 biojava-structure/src/main/java/demo/DemoFetchElectronDensity.java create mode 100644 biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/AbstractDensityMapProvider.java create mode 100644 biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/Ccp4Header.java create mode 100644 biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityCacheLayout.java create mode 100644 biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityFileFormat.java create mode 100644 biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapCache.java create mode 100644 biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapKind.java create mode 100644 biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapProvider.java create mode 100644 biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapRequest.java create mode 100644 biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapResult.java create mode 100644 biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapSource.java create mode 100644 biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapTooLargeException.java create mode 100644 biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/EmdbEntryInfo.java create mode 100644 biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/EmdbEntryResolver.java create mode 100644 biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/EmdbMapProvider.java create mode 100644 biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/NoDensityMapException.java create mode 100644 biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/PdbeCcp4MapProvider.java create mode 100644 biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/UrlTemplates.java create mode 100644 biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/VolumeServerProvider.java create mode 100644 biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/WwpdbMapCoefficientsProvider.java diff --git a/biojava-structure/src/main/java/demo/DemoFetchElectronDensity.java b/biojava-structure/src/main/java/demo/DemoFetchElectronDensity.java new file mode 100644 index 0000000000..f987fd7cfe --- /dev/null +++ b/biojava-structure/src/main/java/demo/DemoFetchElectronDensity.java @@ -0,0 +1,90 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package demo; + +import java.io.IOException; + +import org.biojava.nbio.structure.PdbId; +import org.biojava.nbio.structure.io.density.DensityMapCache; +import org.biojava.nbio.structure.io.density.DensityMapKind; +import org.biojava.nbio.structure.io.density.DensityMapResult; +import org.biojava.nbio.structure.io.density.NoDensityMapException; + +/** + * Fetches electron density and cryo-EM maps, printing which source answered. + *

+ * The three entries chosen exercise the three outcomes the fallback chain has to + * handle: + *

    + *
  • 1cbs — an X-ray structure with deposited structure factors, + * served by the first source tried.
  • + *
  • 6hu9 — a cryo-EM structure. Every X-ray source has nothing for + * it, so the chain resolves the associated EMDB entry instead and picks up the + * author-recommended contour level along the way.
  • + *
  • 4hhb — deposited in 1984 without structure factors, so no + * source has anything. This is a normal outcome, not an error, and the exception + * says which sources were tried and why each declined.
  • + *
+ * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public class DemoFetchElectronDensity { + + /** + * @param args ignored + * @throws IOException if a server could not be reached at all + */ + public static void main(String[] args) throws IOException { + DensityMapCache cache = new DensityMapCache(); + System.out.println("Caching under: " + cache.getCachePath()); + System.out.println(); + + show(cache, "1cbs", DensityMapKind.TWO_FO_FC); + show(cache, "1cbs", DensityMapKind.FO_FC); + show(cache, "6hu9", DensityMapKind.AUTO); + show(cache, "4hhb", DensityMapKind.AUTO); + } + + private static void show(DensityMapCache cache, String id, DensityMapKind kind) throws IOException { + System.out.printf("%s (%s)%n", id, kind); + try { + DensityMapResult result = cache.getDensityMap(new PdbId(id), kind); + System.out.printf(" source : %s%n", result.getSource()); + System.out.printf(" format : %s%s%n", result.getFormat(), + result.isRenderable() ? "" : " (needs an FFT before display)"); + System.out.printf(" kind : %s%n", result.getKind()); + System.out.printf(" file : %s (%,d bytes)%n", result.getFile(), result.getFileSizeBytes()); + System.out.printf(" cached : %s%n", result.isFromCache()); + if (result.getEmdbId() != null) { + System.out.printf(" EMDB : %s%n", result.getEmdbId()); + } + if (result.getRecommendedContourLevel() != null) { + System.out.printf(" contour : %s (author recommended)%n", result.getRecommendedContourLevel()); + } + if (result.getContourInSigma() != null) { + System.out.printf(" in sigma : %.2f%n", result.getContourInSigma()); + } + } catch (NoDensityMapException e) { + System.out.printf(" no map available%n"); + e.getAttempts().forEach((source, reason) -> + System.out.printf(" %-24s %s%n", source, reason)); + } + System.out.println(); + } +} diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/AbstractDensityMapProvider.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/AbstractDensityMapProvider.java new file mode 100644 index 0000000000..3ac4057ba2 --- /dev/null +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/AbstractDensityMapProvider.java @@ -0,0 +1,360 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.io.density; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; +import java.util.Date; + +import org.biojava.nbio.core.util.FileDownloadUtils; +import org.biojava.nbio.core.util.HttpStatusException; +import org.biojava.nbio.structure.PdbId; +import org.biojava.nbio.structure.io.LocalPDBDirectory; +import org.biojava.nbio.structure.io.LocalPDBDirectory.FetchBehavior; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Shared machinery for the concrete density map providers: cache lookup, + * download with validation, size limiting and format sanity checking. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public abstract class AbstractDensityMapProvider implements DensityMapProvider { + + private static final Logger logger = LoggerFactory.getLogger(AbstractDensityMapProvider.class); + + /** + * Anything smaller than this is not a usable map. Density servers answer with a + * short body rather than an error status when they have nothing, so a size + * floor is a necessary part of deciding whether a download succeeded. + */ + public static final long MIN_DENSITY_FILE_SIZE = 1024L; + + private File cacheRoot; + private FetchBehavior fetchBehavior = FetchBehavior.FETCH_FILES; + private long maxDownloadBytes = DensityMapCache.DEFAULT_MAX_DOWNLOAD_BYTES; + + /** + * @param cacheRoot the BioJava cache directory that the density directory sits in + */ + protected AbstractDensityMapProvider(File cacheRoot) { + this.cacheRoot = cacheRoot; + } + + /** @return the BioJava cache directory */ + public File getCacheRoot() { + return cacheRoot; + } + + /** @param cacheRoot the BioJava cache directory */ + public void setCacheRoot(File cacheRoot) { + this.cacheRoot = cacheRoot; + } + + /** @return how aggressively this provider re-fetches */ + public FetchBehavior getFetchBehavior() { + return fetchBehavior; + } + + /** @param fetchBehavior how aggressively to re-fetch */ + public void setFetchBehavior(FetchBehavior fetchBehavior) { + this.fetchBehavior = fetchBehavior == null ? FetchBehavior.FETCH_FILES : fetchBehavior; + } + + /** @return the download size limit in bytes, or 0 for no limit */ + public long getMaxDownloadBytes() { + return maxDownloadBytes; + } + + /** @param maxDownloadBytes the download size limit in bytes, or 0 for no limit */ + public void setMaxDownloadBytes(long maxDownloadBytes) { + this.maxDownloadBytes = maxDownloadBytes; + } + + @Override + public boolean supports(DensityMapKind kind) { + return kind != null && kind != DensityMapKind.AUTO; + } + + /** + * The identifier spelling to put into a URL: four characters and lower case + * where the entry has a short form. + * + * @param pdbId the entry + * @return the identifier for use in a URL + */ + protected String urlId(PdbId pdbId) { + return DensityCacheLayout.shortIdOrFull(pdbId).toLowerCase(); + } + + /** + * The effective fetch behaviour for a request, preferring the request's own + * setting over this provider's. + * + * @param request the request + * @return the behaviour to apply + */ + protected FetchBehavior effectiveFetchBehavior(DensityMapRequest request) { + return request.getFetchBehavior() == null ? fetchBehavior : request.getFetchBehavior(); + } + + /** + * The effective size limit for a request, preferring the request's own setting. + * + * @param request the request + * @return the limit in bytes, or 0 for no limit + */ + protected long effectiveMaxBytes(DensityMapRequest request) { + return request.getMaxDownloadBytes() < 0 ? maxDownloadBytes : request.getMaxDownloadBytes(); + } + + /** + * The cache directory to use for a request, preferring the request's override. + * + * @param request the request + * @return the cache directory + */ + protected File effectiveCacheRoot(DensityMapRequest request) { + return request.getCacheDir() == null ? cacheRoot : request.getCacheDir(); + } + + /** + * Obtains a map, serving it from the cache when the fetch behaviour permits and + * downloading it otherwise. + * + * @param request what was asked for + * @param url where to fetch from + * @param target where to cache it + * @param kind the concrete kind of map being fetched + * @param emdbId the EMDB entry, if this is an EM map; otherwise null + * @param contourLevel the author-recommended contour level, or null + * @param sigma the map RMS deviation, or null + * @return the result, or null if the source has nothing for this entry + * @throws IOException on transport failure + */ + protected DensityMapResult obtain(DensityMapRequest request, URL url, File target, DensityMapKind kind, + String emdbId, Double contourLevel, Double sigma) throws IOException { + + FetchBehavior behavior = effectiveFetchBehavior(request); + + if (isCacheUsable(target, url, behavior)) { + // The kind always comes from the request, never from the sidecar: a single + // cached file can hold more than one kind of map, so the sidecar's kind + // records what was asked for first, not what the caller wants now. + DensityMapResult cached = DensityMapResult.readMeta(target); + Double cachedContour = contourLevel != null || cached == null ? contourLevel + : cached.getRecommendedContourLevel(); + Double cachedSigma = sigma != null || cached == null ? sigma : cached.getSigma(); + DensityMapResult result = new DensityMapResult(target, getSource(), getFormat(), kind, + request.getPdbId(), emdbId, url.toString(), true, cachedContour, cachedSigma); + if (cached == null) { + // The file is good but its description was lost; write one rather than + // downloading several megabytes again. + result.writeMeta(); + } + return result; + } + + if (behavior == FetchBehavior.LOCAL_ONLY) { + throw new HttpStatusException(404, url.toString(), + "not in the local cache and downloads are disabled (FetchBehavior.LOCAL_ONLY)"); + } + + File dir = target.getAbsoluteFile().getParentFile(); + if (!dir.isDirectory() && !dir.mkdirs()) { + throw new IOException("Could not create density cache directory " + dir); + } + + enforceSizeLimit(url, effectiveMaxBytes(request), request); + + logger.info("Fetching {} density map for {} from {}", kind, + request.getPdbId() == null ? emdbId : request.getPdbId().getId(), url); + FileDownloadUtils.downloadFileWithValidation(url, target, null, FileDownloadUtils.Hash.UNKNOWN, + FileDownloadUtils.ETagPolicy.USE_IF_HEX_DIGEST); + + if (!isPlausibleMap(target)) { + // Do not leave a bad file behind to be picked up as a cache hit later. + deleteWithSidecars(target); + throw new IOException("The content downloaded from " + url + " is not a usable " + + getFormat() + " density map."); + } + + DensityMapResult result = new DensityMapResult(target, getSource(), getFormat(), kind, + request.getPdbId(), emdbId, url.toString(), false, contourLevel, sigma); + result.writeMeta(); + return result; + } + + /** + * Whether a cached file may be used as-is. + * + * @param target the cached file + * @param url where it came from + * @param behavior the fetch behaviour in force + * @return true if the cached file should be served + */ + protected boolean isCacheUsable(File target, URL url, FetchBehavior behavior) { + if (behavior == FetchBehavior.FORCE_DOWNLOAD) { + return false; + } + if (!target.isFile() || target.length() < MIN_DENSITY_FILE_SIZE) { + return false; + } + if (!FileDownloadUtils.validateFile(target)) { + logger.info("Cached density map [{}] failed validation and will be re-downloaded.", target); + return false; + } + if (!isPlausibleMap(target)) { + logger.info("Cached file [{}] is not a usable {} map and will be re-downloaded.", target, getFormat()); + return false; + } + if (behavior == FetchBehavior.LOCAL_ONLY) { + return true; + } + if (behavior == FetchBehavior.FETCH_IF_OUTDATED) { + Date serverDate = LocalPDBDirectory.getLastModifiedTime(url); + if (serverDate == null) { + // Density servers generate their responses on the fly and send no + // Last-Modified header. Treating that as "outdated" would re-download + // on every single call, so an unknown timestamp keeps the cache. + logger.debug("No server timestamp for {}; keeping the cached copy.", url); + return true; + } + return target.lastModified() >= serverDate.getTime(); + } + // FETCH_FILES, and FETCH_REMEDIATED which has no meaning for maps. + return true; + } + + /** + * Checks that a file looks like the format this provider delivers. Only the + * CCP4/MRC formats carry a recognisable stamp; the others are accepted on size + * alone. + * + * @param file the file to check + * @return true if the file is plausibly a map of this format + */ + protected boolean isPlausibleMap(File file) { + if (file == null || file.length() < MIN_DENSITY_FILE_SIZE) { + return false; + } + DensityFileFormat format = getFormat(); + if (format == DensityFileFormat.CCP4 || format == DensityFileFormat.CCP4_GZ) { + return Ccp4Header.isCcp4Quietly(file); + } + return true; + } + + /** + * Refuses a download whose declared size exceeds the limit, before any of the + * body is transferred. + * + * @param url the resource + * @param maxBytes the limit, or 0 for no limit + * @param request the request being served, used for reporting + * @throws DensityMapTooLargeException if the resource is too large + */ + protected void enforceSizeLimit(URL url, long maxBytes, DensityMapRequest request) + throws DensityMapTooLargeException { + if (maxBytes <= 0) { + return; + } + long size = declaredSize(url); + if (size > maxBytes) { + reportTooLarge(url, size, maxBytes, request); + throw new DensityMapTooLargeException(url.toString(), size, maxBytes); + } + } + + /** + * Announces that a map was skipped for being too large. + *

+ * This is reported through the logger and on both standard output and standard + * error. A user who asked for a map and silently got a coarser one from another + * source deserves to be told why, and a library log configuration that discards + * warnings should not be able to hide it. + * + * @param url the resource that was skipped + * @param size its size in bytes + * @param maxBytes the limit in bytes + * @param request the request being served + */ + protected void reportTooLarge(URL url, long size, long maxBytes, DensityMapRequest request) { + String entry = request.getPdbId() != null ? request.getPdbId().getId() + : (request.getEmdbId() != null ? request.getEmdbId() : "?"); + String message = String.format( + "BioJava density: skipping %s for %s (%,d bytes exceeds the %,d byte limit). " + + "Trying a smaller representation from another source; " + + "raise DensityMapCache.setMaxDownloadBytes() to allow it.", + url, entry, size, maxBytes); + logger.warn(message); + System.out.println(message); + System.err.println(message); + } + + /** + * The Content-Length a server declares for a resource. + * + * @param url the resource + * @return the size in bytes, or a negative value if the server did not say + */ + protected long declaredSize(URL url) { + try { + URLConnection connection = FileDownloadUtils.prepareURLConnection(url.toString(), 30000); + if (connection instanceof java.net.HttpURLConnection) { + ((java.net.HttpURLConnection) connection).setRequestMethod("HEAD"); + } + connection.connect(); + try { + return connection.getContentLengthLong(); + } finally { + if (connection instanceof java.net.HttpURLConnection) { + ((java.net.HttpURLConnection) connection).disconnect(); + } + } + } catch (IOException e) { + logger.debug("Could not determine the size of {}: {}", url, e.getMessage()); + return -1; + } + } + + /** + * Deletes a cached map along with its validation and metadata sidecars. + * + * @param target the cached map + */ + protected void deleteWithSidecars(File target) { + File dir = target.getAbsoluteFile().getParentFile(); + if (dir == null) { + return; + } + File[] siblings = dir.listFiles((d, name) -> name.startsWith(target.getName())); + if (siblings == null) { + return; + } + for (File f : siblings) { + if (!f.delete()) { + logger.debug("Could not delete [{}]", f); + } + } + } +} diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/Ccp4Header.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/Ccp4Header.java new file mode 100644 index 0000000000..de073d0c01 --- /dev/null +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/Ccp4Header.java @@ -0,0 +1,130 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.io.density; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.zip.GZIPInputStream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Recognises CCP4/MRC map files by their header. + *

+ * This is a cheap but effective guard against a cached file that is not + * actually a map. A misbehaving or overloaded server can answer with an HTML + * error page and an HTTP 200, in which case nothing about the status code or the + * content length reveals the problem — but the missing MAP  + * stamp does, turning a silent failure into a clean cache miss. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public class Ccp4Header { + + private static final Logger logger = LoggerFactory.getLogger(Ccp4Header.class); + + /** + * Byte offset of the four-character format stamp within a CCP4/MRC header. It + * sits in word 53 of the 256-word header. + */ + public static final int MAP_STAMP_OFFSET = 208; + + /** The stamp itself: the three letters of "MAP" followed by a space. */ + public static final String MAP_STAMP = "MAP "; + + /** Number of header bytes that must be readable for the check to be possible. */ + private static final int HEADER_BYTES = MAP_STAMP_OFFSET + 4; + + private Ccp4Header() { + } + + /** + * Checks whether a file is a CCP4/MRC map. Gzipped files are decompressed on + * the fly, so .map.gz works as well as .ccp4. + * + * @param file the file to check + * @return true if the CCP4 stamp is present + * @throws IOException if the file could not be read + */ + public static boolean isCcp4(File file) throws IOException { + if (file == null || !file.isFile() || file.length() < HEADER_BYTES) { + return false; + } + try (InputStream in = openPossiblyGzipped(file)) { + return isCcp4(in); + } + } + + /** + * Checks whether a stream carries a CCP4/MRC map header. The stream is read + * from its current position and is not closed; it is not un-read afterwards, + * so pass a fresh stream or one that supports marking. + * + * @param in the stream to check, already decompressed + * @return true if the CCP4 stamp is present + * @throws IOException if the stream could not be read + */ + public static boolean isCcp4(InputStream in) throws IOException { + byte[] header = new byte[HEADER_BYTES]; + int read = 0; + while (read < HEADER_BYTES) { + int n = in.read(header, read, HEADER_BYTES - read); + if (n < 0) { + return false; // shorter than a CCP4 header, so certainly not one + } + read += n; + } + String stamp = new String(header, MAP_STAMP_OFFSET, 4, StandardCharsets.US_ASCII); + return MAP_STAMP.equals(stamp); + } + + /** + * Same as {@link #isCcp4(File)} but reports a problem rather than propagating + * it, for use in cache-validity checks where an unreadable file and an invalid + * one lead to the same action. + * + * @param file the file to check + * @return true if the file is readable and carries the CCP4 stamp + */ + public static boolean isCcp4Quietly(File file) { + try { + return isCcp4(file); + } catch (IOException e) { + logger.debug("Could not read [{}] to check for a CCP4 header: {}", file, e.getMessage()); + return false; + } + } + + private static InputStream openPossiblyGzipped(File file) throws IOException { + InputStream in = new BufferedInputStream(Files.newInputStream(file.toPath())); + in.mark(2); + int b1 = in.read(); + int b2 = in.read(); + in.reset(); + if (b1 == 0x1F && b2 == 0x8B) { + return new GZIPInputStream(in); + } + return in; + } +} diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityCacheLayout.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityCacheLayout.java new file mode 100644 index 0000000000..476616f562 --- /dev/null +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityCacheLayout.java @@ -0,0 +1,195 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.io.density; + +import java.io.File; + +import org.biojava.nbio.structure.PdbId; +import org.biojava.nbio.structure.io.LocalPDBDirectory; + +/** + * Where cached density files live on disk. + *

+ * PDB-keyed maps follow the divided layout the rest of BioJava already uses, so + * that a cache with many entries does not end up with one enormous directory: + *

+ * <cache>/density/cb/1cbs_2fofc_pdbe.ccp4
+ * <cache>/density/cb/1cbs_fofc_pdbe.ccp4
+ * <cache>/density/cb/1cbs_2fofc_rcsbvs_d0.bcif
+ * <cache>/density/cb/1cbs_2fofc_wwpdb.cif.gz
+ * 
+ * Both the kind and the source appear in the file name, so no two combinations + * can collide. + *

+ * EMDB maps are keyed by EMDB identifier instead, mirroring the EMDB archive: + *

+ * <cache>/density/emd/EMD-0262/emd_0262.map.gz
+ * 
+ * Several PDB entries are often fitted into a single EM map, and those maps can + * be hundreds of megabytes, so keying them by PDB entry would cache the same + * enormous file many times over. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public class DensityCacheLayout { + + /** Name of the density sub-directory within the BioJava cache directory. */ + public static final String DENSITY_DIR = "density"; + + /** Name of the sub-directory holding EMDB-keyed maps. */ + public static final String EMDB_DIR = "emd"; + + /** Name of the sub-directory holding cached PDB-to-EMDB mappings. */ + public static final String EMDB_MAPPING_DIR = "emdb-mapping"; + + /** + * File-name token for a file that holds more than one kind of map, as a density + * server response does. + */ + public static final String BOTH_KINDS_TOKEN = "both"; + + private DensityCacheLayout() { + } + + /** + * The root density directory inside a cache directory. + * + * @param cacheRoot the BioJava cache directory + * @return the density directory, which need not exist yet + */ + public static File densityRoot(File cacheRoot) { + return new File(cacheRoot, DENSITY_DIR); + } + + /** + * The cache file for a PDB-keyed map. + * + * @param cacheRoot the BioJava cache directory + * @param pdbId the entry + * @param kind the kind of map + * @param source the service it came from + * @param format the file format + * @param qualifier an extra discriminator such as a detail level, or + * null. Anything that changes the content but not the + * entry, kind or source belongs here. + * @return the file, which need not exist + */ + public static File pdbMapFile(File cacheRoot, PdbId pdbId, DensityMapKind kind, DensityMapSource source, + DensityFileFormat format, String qualifier) { + return pdbMapFile(cacheRoot, pdbId, kind.getFileToken(), source, format, qualifier); + } + + /** + * The cache file for a PDB-keyed map, naming the kind explicitly. + *

+ * The separate kind token exists for sources that deliver more than one kind of + * map in a single file. A density server response, for instance, carries both + * the 2Fo-Fc and the Fo-Fc blocks, so it is cached once under + * {@link #BOTH_KINDS_TOKEN} rather than downloaded and stored twice. + * + * @param cacheRoot the BioJava cache directory + * @param pdbId the entry + * @param kindToken the token naming what the file holds + * @param source the service it came from + * @param format the file format + * @param qualifier an extra discriminator such as a detail level, or null + * @return the file, which need not exist + */ + public static File pdbMapFile(File cacheRoot, PdbId pdbId, String kindToken, DensityMapSource source, + DensityFileFormat format, String qualifier) { + String id = shortIdOrFull(pdbId).toLowerCase(); + File dir = new File(densityRoot(cacheRoot), LocalPDBDirectory.getMiddleHash(id)); + StringBuilder name = new StringBuilder(id) + .append('_').append(kindToken) + .append('_').append(source.getFileToken()); + if (qualifier != null && !qualifier.isEmpty()) { + name.append('_').append(qualifier); + } + name.append(format.getExtension()); + return new File(dir, name.toString()); + } + + /** + * The cache file for an EMDB-keyed map. + * + * @param cacheRoot the BioJava cache directory + * @param emdbId the EMDB entry, in any accepted form + * @param source the service it came from + * @param format the file format + * @param qualifier an extra discriminator such as a detail level, or null + * @return the file, which need not exist + */ + public static File emdbMapFile(File cacheRoot, String emdbId, DensityMapSource source, + DensityFileFormat format, String qualifier) { + String canonical = DensityMapRequest.normalizeEmdbId(emdbId); + String number = DensityMapRequest.emdbNumber(emdbId); + File dir = new File(new File(densityRoot(cacheRoot), EMDB_DIR), canonical); + StringBuilder name = new StringBuilder("emd_").append(number); + if (source != DensityMapSource.EMDB_MAP) { + name.append('_').append(source.getFileToken()); + } + if (qualifier != null && !qualifier.isEmpty()) { + name.append('_').append(qualifier); + } + name.append(format.getExtension()); + return new File(dir, name.toString()); + } + + /** + * The file caching the EMDB identifiers and author contour level associated + * with a PDB entry. + * + * @param cacheRoot the BioJava cache directory + * @param pdbId the entry + * @return the file, which need not exist + */ + public static File emdbMappingFile(File cacheRoot, PdbId pdbId) { + String id = shortIdOrFull(pdbId).toLowerCase(); + File dir = new File(new File(densityRoot(cacheRoot), EMDB_MAPPING_DIR), LocalPDBDirectory.getMiddleHash(id)); + return new File(dir, id + ".emdb.properties"); + } + + /** + * The file caching an EMDB entry's map metadata as served by the EMDB API. + * + * @param cacheRoot the BioJava cache directory + * @param emdbId the EMDB entry, in any accepted form + * @return the file, which need not exist + */ + public static File emdbMapInfoFile(File cacheRoot, String emdbId) { + String canonical = DensityMapRequest.normalizeEmdbId(emdbId); + File dir = new File(new File(densityRoot(cacheRoot), EMDB_DIR), canonical); + return new File(dir, canonical + ".map-info.json"); + } + + /** + * The short four-character spelling of an identifier where one exists. + *

+ * Every entry in the archive today has a four-character form, and the density + * services accept only that spelling. Extended-only identifiers will appear + * eventually; rather than refusing them, the extended spelling is passed + * through so that the services can start accepting it without a change here. + * + * @param pdbId the identifier + * @return the short spelling if available, otherwise the full one + */ + public static String shortIdOrFull(PdbId pdbId) { + return pdbId.getId(true); + } +} diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityFileFormat.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityFileFormat.java new file mode 100644 index 0000000000..11cbb0e27e --- /dev/null +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityFileFormat.java @@ -0,0 +1,89 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.io.density; + +/** + * The file format a density map was delivered in. + *

+ * The important distinction here is {@link #isJmolLoadable()}. Most of these + * formats are sampled grids that a viewer can contour directly; map + * coefficients are not, they are structure factors that require a Fourier + * transform first. Handing the latter to a viewer produces nothing at all, so + * the difference has to be visible to callers. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public enum DensityFileFormat { + + /** A CCP4/MRC sampled map, as served pre-computed by PDBe. */ + CCP4(".ccp4", true), + + /** A gzipped CCP4/MRC map, the form EMDB distributes its primary maps in. */ + CCP4_GZ(".map.gz", true), + + /** A BinaryCIF volume slice from a Mol* density server. */ + BCIF_VOLUME(".bcif", true), + + /** A text CIF volume slice from a Mol* density server. */ + CIF_VOLUME(".cif", true), + + /** + * Structure-factor amplitudes and phases in mmCIF, as published with the wwPDB + * validation reports. Not a density grid. A Fourier transform (for + * example gemmi sf2map, or CCP4's fft after + * cif2mtz) is needed before these can be displayed. + */ + MAP_COEFFICIENTS_CIF_GZ(".cif.gz", false); + + private final String extension; + private final boolean jmolLoadable; + + DensityFileFormat(String extension, boolean jmolLoadable) { + this.extension = extension; + this.jmolLoadable = jmolLoadable; + } + + /** + * @return the file extension used for cached files of this format, including + * the leading dot + */ + public String getExtension() { + return extension; + } + + /** + * Whether a viewer can contour this file as it stands. + * + * @return false for {@link #MAP_COEFFICIENTS_CIF_GZ}, which needs + * an FFT first; true for the sampled grid formats + */ + public boolean isJmolLoadable() { + return jmolLoadable; + } + + /** + * Whether files of this format are gzip-compressed on disk. Jmol detects gzip + * from the magic bytes, so such files do not need decompressing before display. + * + * @return true for the compressed formats + */ + public boolean isCompressed() { + return this == CCP4_GZ || this == MAP_COEFFICIENTS_CIF_GZ; + } +} diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapCache.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapCache.java new file mode 100644 index 0000000000..adbbd917a8 --- /dev/null +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapCache.java @@ -0,0 +1,526 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.io.density; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.biojava.nbio.core.util.FileDownloadUtils; +import org.biojava.nbio.core.util.HttpStatusException; +import org.biojava.nbio.structure.ExperimentalTechnique; +import org.biojava.nbio.structure.PdbId; +import org.biojava.nbio.structure.Structure; +import org.biojava.nbio.structure.align.util.UserConfiguration; +import org.biojava.nbio.structure.io.LocalPDBDirectory.FetchBehavior; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Downloads and caches electron density and cryo-EM maps, trying several sources + * in turn until one produces a map. + *

+ * Cached files live under the BioJava cache directory (PDB_CACHE_DIR), + * laid out as described in {@link DensityCacheLayout}. Typical use is simply: + *

+ * DensityMapCache cache = new DensityMapCache();
+ * DensityMapResult map = cache.getDensityMap(new PdbId("1cbs"), DensityMapKind.TWO_FO_FC);
+ * File file = map.getFile();
+ * 
+ *

+ * Source order. Sources are tried smallest-adequate-first, because they + * differ enormously in size for the same entry and the smallest is usually + * perfectly adequate to look at. For 1cbs a density-server slice is roughly a + * tenth the size of the equivalent pair of CCP4 files; for the cryo-EM entry + * behind EMD-0262 it is a few hundred kilobytes against 106 MB. Anything + * that cannot be displayed at all is tried last, and + * {@link DensityMapSource#WWPDB_MAP_COEFFICIENTS} is disabled altogether by + * default for that reason. Override with {@link #setSourceChain(DensityMapKind, + * java.util.List)} or {@link #setSourceEnabled(DensityMapSource, boolean)}. + *

+ * Failures. A source that has nothing for an entry is skipped and the next + * is tried; if every source is exhausted, {@link NoDensityMapException} is thrown + * carrying the reason from each one, so a caller can explain what happened. + * Genuine transport failures abort the chain instead, so that a network outage is + * never reported as "this entry has no density". + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public class DensityMapCache { + + private static final Logger logger = LoggerFactory.getLogger(DensityMapCache.class); + + /** + * Default ceiling on a single download, 256 MiB. Exceeding it is not an + * error: the chain moves on to a source that offers a smaller representation. + * Set to 0 to remove the limit. + */ + public static final long DEFAULT_MAX_DOWNLOAD_BYTES = 256L * 1024 * 1024; + + /** Order in which sources are tried for X-ray and neutron entries. */ + public static final List DEFAULT_XRAY_SOURCE_CHAIN = Collections.unmodifiableList(Arrays.asList( + DensityMapSource.RCSB_VOLUME_SERVER, + DensityMapSource.PDBE_CCP4, + DensityMapSource.PDBE_VOLUME_SERVER, + DensityMapSource.WWPDB_MAP_COEFFICIENTS)); + + /** Order in which sources are tried for cryo-EM entries. */ + public static final List DEFAULT_EM_SOURCE_CHAIN = Collections.unmodifiableList(Arrays.asList( + DensityMapSource.RCSB_VOLUME_SERVER, + DensityMapSource.PDBE_VOLUME_SERVER, + DensityMapSource.EMDB_MAP)); + + private static DensityMapCache instance; + + private File cacheRoot; + private FetchBehavior fetchBehavior = FetchBehavior.FETCH_FILES; + private long maxDownloadBytes = DEFAULT_MAX_DOWNLOAD_BYTES; + private final Map providers = new EnumMap<>(DensityMapSource.class); + private final Set disabled = EnumSet.of(DensityMapSource.WWPDB_MAP_COEFFICIENTS); + private List xraySourceChain = DEFAULT_XRAY_SOURCE_CHAIN; + private List emSourceChain = DEFAULT_EM_SOURCE_CHAIN; + private EmdbEntryResolver emdbResolver; + + /** + * Creates a cache using the standard BioJava cache directory, i.e. + * PDB_CACHE_DIR falling back to PDB_DIR. + */ + public DensityMapCache() { + this(new UserConfiguration().getCacheFilePath()); + } + + /** + * @param cachePath the directory to cache under + */ + public DensityMapCache(String cachePath) { + this.cacheRoot = new File(FileDownloadUtils.expandUserHome(cachePath)); + this.emdbResolver = new EmdbEntryResolver(cacheRoot); + buildDefaultProviders(); + } + + /** + * A lazily created shared instance, for callers that do not want to manage one. + * + * @return the shared cache + */ + public static synchronized DensityMapCache getInstance() { + if (instance == null) { + instance = new DensityMapCache(); + } + return instance; + } + + private void buildDefaultProviders() { + providers.clear(); + register(new VolumeServerProvider(cacheRoot, VolumeServerProvider.Host.RCSB)); + register(new VolumeServerProvider(cacheRoot, VolumeServerProvider.Host.PDBE)); + register(new PdbeCcp4MapProvider(cacheRoot)); + register(new EmdbMapProvider(cacheRoot, emdbResolver)); + register(new WwpdbMapCoefficientsProvider(cacheRoot)); + applySettingsToProviders(); + } + + private void register(DensityMapProvider provider) { + providers.put(provider.getSource(), provider); + } + + private void applySettingsToProviders() { + for (DensityMapProvider p : providers.values()) { + if (p instanceof AbstractDensityMapProvider) { + AbstractDensityMapProvider a = (AbstractDensityMapProvider) p; + a.setCacheRoot(cacheRoot); + a.setFetchBehavior(fetchBehavior); + a.setMaxDownloadBytes(maxDownloadBytes); + } + } + emdbResolver.setCacheRoot(cacheRoot); + emdbResolver.setFetchBehavior(fetchBehavior); + } + + /** @return the directory maps are cached under */ + public String getCachePath() { + return cacheRoot.getAbsolutePath(); + } + + /** @param cachePath the directory to cache under */ + public void setCachePath(String cachePath) { + this.cacheRoot = new File(FileDownloadUtils.expandUserHome(cachePath)); + applySettingsToProviders(); + } + + /** @return how aggressively cached maps are re-fetched */ + public FetchBehavior getFetchBehavior() { + return fetchBehavior; + } + + /** + * @param fetchBehavior how aggressively to re-fetch. {@link FetchBehavior#FETCH_REMEDIATED} + * behaves as {@link FetchBehavior#FETCH_FILES}: the 2011 remediation date + * is a coordinate-file concept with no meaning for maps. + */ + public void setFetchBehavior(FetchBehavior fetchBehavior) { + this.fetchBehavior = fetchBehavior == null ? FetchBehavior.FETCH_FILES : fetchBehavior; + applySettingsToProviders(); + } + + /** @return the ceiling on a single download in bytes, or 0 for no limit */ + public long getMaxDownloadBytes() { + return maxDownloadBytes; + } + + /** @param maxDownloadBytes the ceiling on a single download in bytes, or 0 for no limit */ + public void setMaxDownloadBytes(long maxDownloadBytes) { + this.maxDownloadBytes = maxDownloadBytes; + applySettingsToProviders(); + } + + /** + * The order sources are tried in for a given kind of map. + * + * @param kind the kind of map + * @return the source order + */ + public List getSourceChain(DensityMapKind kind) { + return kind == DensityMapKind.EM ? emSourceChain : xraySourceChain; + } + + /** + * Overrides the order sources are tried in. + * + * @param kind {@link DensityMapKind#EM} to set the cryo-EM order, anything else + * to set the X-ray order + * @param chain the new order + */ + public void setSourceChain(DensityMapKind kind, List chain) { + List copy = Collections.unmodifiableList(new ArrayList<>(chain)); + if (kind == DensityMapKind.EM) { + emSourceChain = copy; + } else { + xraySourceChain = copy; + } + } + + /** + * @param source the source + * @return whether it will be tried + */ + public boolean isSourceEnabled(DensityMapSource source) { + return !disabled.contains(source); + } + + /** + * Enables or disables a source. + *

+ * {@link DensityMapSource#WWPDB_MAP_COEFFICIENTS} is disabled by default + * because it delivers structure factors rather than a map; enable it explicitly + * if you want the archival form and are prepared to run a Fourier transform. + * + * @param source the source + * @param enabled whether to try it + */ + public void setSourceEnabled(DensityMapSource source, boolean enabled) { + if (enabled) { + disabled.remove(source); + } else { + disabled.add(source); + } + } + + /** + * Replaces the provider used for a source. Mainly a testing seam, but also the + * way to plug in a site-local mirror. + * + * @param provider the provider to use + */ + public void registerProvider(DensityMapProvider provider) { + register(provider); + applySettingsToProviders(); + } + + /** @return the resolver used to map PDB entries to EMDB entries */ + public EmdbEntryResolver getEmdbResolver() { + return emdbResolver; + } + + /** @param resolver the resolver used to map PDB entries to EMDB entries */ + public void setEmdbResolver(EmdbEntryResolver resolver) { + this.emdbResolver = resolver; + applySettingsToProviders(); + } + + /** + * Fetches a density map, using the cache where possible. + * + * @param pdbId the entry + * @param kind the kind of map, or {@link DensityMapKind#AUTO} to take whatever + * the entry has + * @return the map + * @throws NoDensityMapException if no enabled source has a map for this entry + * @throws IOException on transport failure + */ + public DensityMapResult getDensityMap(PdbId pdbId, DensityMapKind kind) throws IOException { + return getDensityMap(DensityMapRequest.builder(pdbId).kind(kind).build()); + } + + /** + * Fetches a density map for a PDB or EMDB identifier. + * + * @param id a PDB identifier, or an EMDB identifier such as EMD-0262 + * @param kind the kind of map wanted + * @return the map + * @throws NoDensityMapException if no enabled source has a map for this entry + * @throws IOException on transport failure + */ + public DensityMapResult getDensityMap(String id, DensityMapKind kind) throws IOException { + return getDensityMap(DensityMapRequest.builder(id).kind(kind).build()); + } + + /** + * Fetches a density map for an already-loaded structure. + *

+ * Knowing the structure lets the experimental method be read directly rather + * than guessed, so a cryo-EM entry goes straight to the EM sources instead of + * trying the X-ray ones first. No extra network request is involved. + * + * @param structure the structure + * @param kind the kind of map wanted + * @return the map + * @throws NoDensityMapException if no enabled source has a map for this entry + * @throws IOException on transport failure + */ + public DensityMapResult getDensityMap(Structure structure, DensityMapKind kind) throws IOException { + PdbId pdbId = structure.getPdbId(); + if (pdbId == null) { + throw new IOException("The structure has no PDB ID, so no density map can be looked up for it."); + } + DensityMapRequest request = DensityMapRequest.builder(pdbId).kind(kind).build(); + return getDensityMap(request, kindOrderFor(structure, kind)); + } + + /** + * Fetches a density map. + * + * @param request what is wanted + * @return the map + * @throws NoDensityMapException if no enabled source has a map for this entry + * @throws IOException on transport failure + */ + public DensityMapResult getDensityMap(DensityMapRequest request) throws IOException { + return getDensityMap(request, request.getKind().resolve()); + } + + /** + * As {@link #getDensityMap(PdbId, DensityMapKind)} but returning an empty + * {@link Optional} rather than throwing when nothing is available. Genuine + * transport failures are still logged and swallowed, so use this only where + * "no map" and "could not reach the server" need not be told apart. + * + * @param pdbId the entry + * @param kind the kind of map wanted + * @return the map, if one could be obtained + */ + public Optional findDensityMap(PdbId pdbId, DensityMapKind kind) { + try { + return Optional.of(getDensityMap(pdbId, kind)); + } catch (NoDensityMapException e) { + logger.debug("{}", e.getMessage()); + return Optional.empty(); + } catch (IOException e) { + logger.warn("Could not fetch a density map for {}: {}", pdbId.getId(), e.getMessage()); + return Optional.empty(); + } + } + + /** + * Fetches both halves of the conventional X-ray pair: the 2mFo-DFc map and the + * mFo-DFc difference map. + * + * @param pdbId the entry + * @return whichever of the two could be obtained, in that order; possibly empty + */ + public List getDifferenceMapPair(PdbId pdbId) { + List results = new ArrayList<>(2); + findDensityMap(pdbId, DensityMapKind.TWO_FO_FC).ifPresent(results::add); + findDensityMap(pdbId, DensityMapKind.FO_FC).ifPresent(results::add); + return results; + } + + /** + * Looks a map up in the cache without contacting any server. + * + * @param pdbId the entry + * @param kind the kind of map + * @param source the source it would have come from + * @return the cached map, or null if it is not cached + */ + public DensityMapResult getCached(PdbId pdbId, DensityMapKind kind, DensityMapSource source) { + DensityMapProvider provider = providers.get(source); + if (provider == null) { + return null; + } + File file = DensityCacheLayout.pdbMapFile(cacheRoot, pdbId, kind, source, provider.getFormat(), null); + return file.isFile() ? DensityMapResult.readMeta(file) : null; + } + + /** + * Removes every cached density file for an entry, including the sidecars. + * + * @param pdbId the entry + * @return the number of files deleted + */ + public int deleteDensityMaps(PdbId pdbId) { + String id = DensityCacheLayout.shortIdOrFull(pdbId).toLowerCase(); + File dir = DensityCacheLayout.pdbMapFile(cacheRoot, pdbId, DensityMapKind.TWO_FO_FC, + DensityMapSource.PDBE_CCP4, DensityFileFormat.CCP4, null).getParentFile(); + File[] files = dir == null ? null : dir.listFiles((d, name) -> name.startsWith(id + "_")); + if (files == null) { + return 0; + } + int deleted = 0; + for (File f : files) { + if (f.delete()) { + deleted++; + } + } + return deleted; + } + + private DensityMapResult getDensityMap(DensityMapRequest request, List kinds) throws IOException { + Map attempts = new LinkedHashMap<>(); + + for (DensityMapKind kind : kinds) { + DensityMapRequest kindRequest = request.withKind(kind); + + if (kind == DensityMapKind.EM && kindRequest.getEmdbId() == null) { + String emdbId = resolveEmdbId(kindRequest); + if (emdbId == null) { + attempts.put(DensityMapSource.EMDB_MAP, "no associated EMDB entry"); + continue; + } + kindRequest = kindRequest.withEmdbId(emdbId); + } + + List chain = kindRequest.getSourceChain() != null + ? kindRequest.getSourceChain() : getSourceChain(kind); + + for (DensityMapSource source : chain) { + if (!isSourceEnabled(source)) { + attempts.put(source, "disabled"); + continue; + } + DensityMapProvider provider = providers.get(source); + if (provider == null) { + attempts.put(source, "no provider registered"); + continue; + } + if (!provider.supports(kind)) { + continue; // structurally impossible; not worth reporting + } + if (!kindRequest.isAllowNonRenderableFormats() && !provider.getFormat().isJmolLoadable()) { + attempts.put(source, "cannot be displayed without a Fourier transform"); + continue; + } + + try { + DensityMapResult result = provider.fetch(kindRequest); + if (result != null) { + return withContourLevel(result); + } + attempts.put(source, "no map for this entry"); + } catch (HttpStatusException e) { + if (!e.isNotFound()) { + // A server error or an authentication problem is a real failure, + // not evidence that the entry has no density. + throw e; + } + attempts.put(source, "HTTP " + e.getStatusCode()); + } catch (DensityMapTooLargeException e) { + attempts.put(source, "too large (" + e.getSizeBytes() + " bytes)"); + } catch (IOException e) { + throw new IOException("Failed to fetch a density map for " + + (request.getPdbId() == null ? request.getEmdbId() : request.getPdbId().getId()) + + " from " + source + ": " + e.getMessage(), e); + } + } + } + + throw new NoDensityMapException(request.getPdbId(), request.getKind(), attempts); + } + + /** + * Fills in the author-recommended contour level for an EM map when the source + * that supplied the file did not know it. + *

+ * A density server returns voxels and nothing else, but an EM map is + * conventionally displayed at the level its depositors chose rather than at a + * multiple of sigma, so a viewer needs that number whichever source the map came + * from. It costs one small metadata request, cached thereafter. + */ + private DensityMapResult withContourLevel(DensityMapResult result) { + if (result.getKind() != DensityMapKind.EM || result.getRecommendedContourLevel() != null + || result.getEmdbId() == null || emdbResolver == null) { + return result; + } + EmdbEntryInfo info = emdbResolver.getEntryInfo(result.getEmdbId()); + if (info == null || (info.getRecommendedContourLevel() == null && info.getSigma() == null)) { + return result; + } + DensityMapResult enriched = new DensityMapResult(result.getFile(), result.getSource(), result.getFormat(), + result.getKind(), result.getPdbId(), result.getEmdbId(), result.getSourceUrl(), result.isFromCache(), + info.getRecommendedContourLevel(), info.getSigma()); + enriched.writeMeta(); + return enriched; + } + + private String resolveEmdbId(DensityMapRequest request) { + if (request.getPdbId() == null || emdbResolver == null) { + return null; + } + List ids = emdbResolver.getEmdbIds(request.getPdbId()); + return ids.isEmpty() ? null : ids.get(0); + } + + /** + * Chooses which kinds to try, and in what order, using the structure's declared + * experimental method. Reading it from the structure costs nothing; note that + * the resolution field is deliberately not consulted, since BioJava parses it + * incorrectly for some cryo-EM entries (biojava/biojava#1000). + */ + private List kindOrderFor(Structure structure, DensityMapKind kind) { + if (kind != DensityMapKind.AUTO) { + return kind.resolve(); + } + Set techniques = structure.getPDBHeader() == null + ? null : structure.getPDBHeader().getExperimentalTechniques(); + if (techniques != null && techniques.contains(ExperimentalTechnique.ELECTRON_MICROSCOPY) + && !ExperimentalTechnique.isCrystallographic(techniques)) { + return Arrays.asList(DensityMapKind.EM, DensityMapKind.TWO_FO_FC); + } + return kind.resolve(); + } +} diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapKind.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapKind.java new file mode 100644 index 0000000000..8b472092ef --- /dev/null +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapKind.java @@ -0,0 +1,99 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.io.density; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * The kind of density map being requested, i.e. what the values in the grid mean. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public enum DensityMapKind { + + /** + * The 2mFo-DFc "best" map: the electron density itself, showing how the model + * fits the experimental data. This is what is normally meant by "the electron + * density" of an X-ray structure. + */ + TWO_FO_FC("2fofc"), + + /** + * The mFo-DFc difference map, showing density that the model does not account + * for (positive) and model that has no density to support it (negative). It is + * conventionally displayed as a signed pair of surfaces. + */ + FO_FC("fofc"), + + /** + * The primary map of a cryo-EM or cryo-ET reconstruction. Strictly this is a + * Coulomb potential map rather than an electron density map, and it is + * conventionally contoured at an absolute author-recommended level rather than + * in multiples of sigma. + */ + EM("em"), + + /** + * Not a map in itself: asks for whichever map the entry actually has. Resolves + * to {@link #TWO_FO_FC} first and then {@link #EM}, which covers X-ray and + * cryo-EM entries without the caller having to know which it is holding. + */ + AUTO(null); + + private final String fileToken; + + DensityMapKind(String fileToken) { + this.fileToken = fileToken; + } + + /** + * The short token used to distinguish this kind in a cached file name. + * + * @return the token, or null for {@link #AUTO}, which is never + * itself cached + */ + public String getFileToken() { + return fileToken; + } + + /** + * Expands this kind into the concrete kinds to try, in order. + * + * @return a single-element list for a concrete kind, or the X-ray-then-EM order + * for {@link #AUTO} + */ + public List resolve() { + if (this == AUTO) { + return Arrays.asList(TWO_FO_FC, EM); + } + return Collections.singletonList(this); + } + + /** + * Whether this kind is conventionally displayed as a signed pair of surfaces, + * one positive and one negative. + * + * @return true only for {@link #FO_FC} + */ + public boolean isDifferenceMap() { + return this == FO_FC; + } +} diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapProvider.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapProvider.java new file mode 100644 index 0000000000..c628387ffd --- /dev/null +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapProvider.java @@ -0,0 +1,78 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.io.density; + +import java.io.IOException; + +import org.biojava.nbio.core.util.HttpStatusException; + +/** + * Fetches density maps from one particular service. + *

+ * Implementations are combined into an ordered chain by {@link DensityMapCache}, + * which tries each in turn until one produces a map. The contract around + * exceptions is what makes that chain safe: + *

    + *
  • Throw {@link HttpStatusException} with {@link HttpStatusException#isNotFound()} + * — or return null — when this service simply has nothing + * for the entry. The chain moves on to the next source.
  • + *
  • Throw {@link DensityMapTooLargeException} when the map exists but exceeds + * the caller's size limit. The chain also moves on, so a smaller representation + * from another source can be used instead.
  • + *
  • Throw any other {@link IOException} for a genuine transport failure. The + * chain stops, so that a network outage is never mistaken for "this entry has no + * density".
  • + *
+ * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public interface DensityMapProvider { + + /** + * @return which service this provider talks to + */ + DensityMapSource getSource(); + + /** + * @return the format this provider delivers + */ + DensityFileFormat getFormat(); + + /** + * Whether this provider can serve a given kind of map at all. Used to skip + * requests that could not possibly succeed, such as asking an X-ray map service + * for a cryo-EM map. + * + * @param kind the kind of map wanted + * @return true if it is worth trying + */ + boolean supports(DensityMapKind kind); + + /** + * Fetches a map, using the cache if the request's fetch behaviour allows. + * + * @param request what is wanted. Its kind is always concrete, never + * {@link DensityMapKind#AUTO}. + * @return the map, or null if this service has nothing for the entry + * @throws DensityMapTooLargeException if the map exceeds the request's size limit + * @throws IOException on transport failure; use {@link HttpStatusException} so + * that a missing resource can be told apart from a broken connection + */ + DensityMapResult fetch(DensityMapRequest request) throws IOException; +} diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapRequest.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapRequest.java new file mode 100644 index 0000000000..d6677a3835 --- /dev/null +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapRequest.java @@ -0,0 +1,275 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.io.density; + +import java.io.File; +import java.util.Collections; +import java.util.List; + +import org.biojava.nbio.structure.PdbId; +import org.biojava.nbio.structure.io.LocalPDBDirectory.FetchBehavior; + +/** + * A request for a density map, describing what is wanted and how hard to look + * for it. + *

+ * Instances are immutable; build them with {@link #builder(PdbId)} or + * {@link #builder(String)}. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public class DensityMapRequest { + + private final PdbId pdbId; + private final String emdbId; + private final DensityMapKind kind; + private final FetchBehavior fetchBehavior; + private final File cacheDir; + private final boolean allowNonRenderableFormats; + private final long maxDownloadBytes; + private final List sourceChain; + + private DensityMapRequest(Builder b) { + this.pdbId = b.pdbId; + this.emdbId = b.emdbId; + this.kind = b.kind; + this.fetchBehavior = b.fetchBehavior; + this.cacheDir = b.cacheDir; + this.allowNonRenderableFormats = b.allowNonRenderableFormats; + this.maxDownloadBytes = b.maxDownloadBytes; + this.sourceChain = b.sourceChain == null ? null : Collections.unmodifiableList(b.sourceChain); + } + + /** + * Starts a request for a PDB entry. + * + * @param pdbId the entry + * @return a new builder + */ + public static Builder builder(PdbId pdbId) { + return new Builder(pdbId, null); + } + + /** + * Starts a request from an identifier string. An identifier beginning with + * EMD- (case-insensitively) is taken as an EMDB entry, anything + * else as a PDB entry. + * + * @param id a PDB or EMDB identifier + * @return a new builder + */ + public static Builder builder(String id) { + if (id == null) { + throw new IllegalArgumentException("Identifier must not be null"); + } + String trimmed = id.trim(); + if (trimmed.toUpperCase().startsWith("EMD-") || trimmed.toUpperCase().startsWith("EMD_")) { + return new Builder(null, normalizeEmdbId(trimmed)).kind(DensityMapKind.EM); + } + return new Builder(new PdbId(trimmed), null); + } + + /** + * Normalises an EMDB identifier to the canonical EMD-1234 form. + * + * @param emdbId an identifier such as emd-1234, EMD_1234 + * or a bare number + * @return the canonical form + */ + public static String normalizeEmdbId(String emdbId) { + if (emdbId == null) { + return null; + } + String digits = emdbId.trim().toUpperCase().replaceFirst("^EMD[-_]?", ""); + return "EMD-" + digits; + } + + /** + * Extracts the numeric part of an EMDB identifier, as used in file names and + * some URL templates. + * + * @param emdbId an EMDB identifier in any accepted form + * @return the digits, without the EMD- prefix + */ + public static String emdbNumber(String emdbId) { + return normalizeEmdbId(emdbId).substring("EMD-".length()); + } + + /** @return the PDB entry requested, or null for an EMDB-only request */ + public PdbId getPdbId() { + return pdbId; + } + + /** @return the EMDB entry, if known or explicitly requested; otherwise null */ + public String getEmdbId() { + return emdbId; + } + + /** @return the kind of map wanted; never null */ + public DensityMapKind getKind() { + return kind; + } + + /** @return how aggressively to re-fetch, or null to use the cache's setting */ + public FetchBehavior getFetchBehavior() { + return fetchBehavior; + } + + /** @return an override for the cache directory, or null to use the cache's own */ + public File getCacheDir() { + return cacheDir; + } + + /** + * Whether sources that deliver something a viewer cannot contour directly + * — currently only map coefficients — may be used. + * + * @return true by default; a viewer should set it to + * false + */ + public boolean isAllowNonRenderableFormats() { + return allowNonRenderableFormats; + } + + /** @return the download size limit in bytes, or 0 for no limit */ + public long getMaxDownloadBytes() { + return maxDownloadBytes; + } + + /** @return an explicit source order, or null to let the cache choose */ + public List getSourceChain() { + return sourceChain; + } + + /** + * Returns a copy of this request with a different map kind, used when expanding + * {@link DensityMapKind#AUTO}. + * + * @param newKind the kind to use + * @return a new request + */ + public DensityMapRequest withKind(DensityMapKind newKind) { + return toBuilder().kind(newKind).build(); + } + + /** + * Returns a copy of this request with the EMDB entry filled in. + * + * @param newEmdbId the EMDB identifier + * @return a new request + */ + public DensityMapRequest withEmdbId(String newEmdbId) { + return toBuilder().emdbId(newEmdbId).build(); + } + + private Builder toBuilder() { + Builder b = new Builder(pdbId, emdbId); + b.kind = kind; + b.fetchBehavior = fetchBehavior; + b.cacheDir = cacheDir; + b.allowNonRenderableFormats = allowNonRenderableFormats; + b.maxDownloadBytes = maxDownloadBytes; + b.sourceChain = sourceChain; + return b; + } + + @Override + public String toString() { + return String.format("DensityMapRequest[%s%s, %s]", + pdbId == null ? "" : pdbId.getId(), + emdbId == null ? "" : (pdbId == null ? emdbId : "/" + emdbId), + kind); + } + + /** + * Builder for {@link DensityMapRequest}. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ + public static final class Builder { + + private final PdbId pdbId; + private String emdbId; + private DensityMapKind kind = DensityMapKind.AUTO; + private FetchBehavior fetchBehavior; + private File cacheDir; + private boolean allowNonRenderableFormats = true; + private long maxDownloadBytes = -1; + private List sourceChain; + + private Builder(PdbId pdbId, String emdbId) { + this.pdbId = pdbId; + this.emdbId = emdbId; + } + + /** @param kind the kind of map wanted; null means {@link DensityMapKind#AUTO} */ + public Builder kind(DensityMapKind kind) { + this.kind = kind == null ? DensityMapKind.AUTO : kind; + return this; + } + + /** @param emdbId the EMDB entry to use, skipping the PDB-to-EMDB lookup */ + public Builder emdbId(String emdbId) { + this.emdbId = emdbId == null ? null : normalizeEmdbId(emdbId); + return this; + } + + /** @param fetchBehavior how aggressively to re-fetch */ + public Builder fetchBehavior(FetchBehavior fetchBehavior) { + this.fetchBehavior = fetchBehavior; + return this; + } + + /** @param cacheDir an override for the cache directory */ + public Builder cacheDir(File cacheDir) { + this.cacheDir = cacheDir; + return this; + } + + /** + * @param allow whether formats that cannot be contoured directly may be used. + * A viewer should pass false. + */ + public Builder allowNonRenderableFormats(boolean allow) { + this.allowNonRenderableFormats = allow; + return this; + } + + /** @param bytes the download size limit, or 0 for no limit, or a negative value to use the cache's setting */ + public Builder maxDownloadBytes(long bytes) { + this.maxDownloadBytes = bytes; + return this; + } + + /** @param chain an explicit source order, overriding the cache's choice */ + public Builder sourceChain(List chain) { + this.sourceChain = chain; + return this; + } + + /** @return the finished request */ + public DensityMapRequest build() { + if (pdbId == null && emdbId == null) { + throw new IllegalArgumentException("A request needs either a PDB ID or an EMDB ID"); + } + return new DensityMapRequest(this); + } + } +} diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapResult.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapResult.java new file mode 100644 index 0000000000..7275466c9e --- /dev/null +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapResult.java @@ -0,0 +1,282 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.io.density; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.time.Instant; +import java.util.Properties; + +import org.biojava.nbio.structure.PdbId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A density map that was successfully obtained, together with everything a + * caller needs to know in order to use it. + *

+ * Which source answered matters to the caller and is therefore part of the + * result: the file might be a full CCP4 grid, a downsampled volume slice, or + * — if non-renderable formats were allowed — a set of structure + * factors that must be Fourier-transformed before anything can be drawn. See + * {@link #isRenderable()}. + *

+ * Every field is persisted to a .meta sidecar beside the cached + * file, so a result can be reconstructed later without contacting any server. + * That is what lets the cache serve + * {@link org.biojava.nbio.structure.io.LocalPDBDirectory.FetchBehavior#LOCAL_ONLY} + * requests fully offline. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public class DensityMapResult { + + private static final Logger logger = LoggerFactory.getLogger(DensityMapResult.class); + + /** Extension of the metadata sidecar written beside every cached map. */ + public static final String META_EXT = ".meta"; + + private final File file; + private final DensityMapSource source; + private final DensityFileFormat format; + private final DensityMapKind kind; + private final PdbId pdbId; + private final String emdbId; + private final String sourceUrl; + private final boolean fromCache; + private final Double recommendedContourLevel; + private final Double sigma; + + /** + * @param file the cached map file + * @param source which service supplied it + * @param format the file format + * @param kind what the values mean; never {@link DensityMapKind#AUTO} + * @param pdbId the PDB entry, may be null + * @param emdbId the EMDB entry, may be null + * @param sourceUrl the URL it came from + * @param fromCache whether it was already on disk rather than freshly downloaded + * @param recommendedContourLevel the author-recommended contour level in absolute + * map units, or null if unknown + * @param sigma the RMS deviation of the map, or null if unknown + */ + public DensityMapResult(File file, DensityMapSource source, DensityFileFormat format, DensityMapKind kind, + PdbId pdbId, String emdbId, String sourceUrl, boolean fromCache, + Double recommendedContourLevel, Double sigma) { + this.file = file; + this.source = source; + this.format = format; + this.kind = kind; + this.pdbId = pdbId; + this.emdbId = emdbId; + this.sourceUrl = sourceUrl; + this.fromCache = fromCache; + this.recommendedContourLevel = recommendedContourLevel; + this.sigma = sigma; + } + + /** @return the cached map file */ + public File getFile() { + return file; + } + + /** @return which service supplied the map */ + public DensityMapSource getSource() { + return source; + } + + /** @return the file format */ + public DensityFileFormat getFormat() { + return format; + } + + /** @return what the map values mean; never {@link DensityMapKind#AUTO} */ + public DensityMapKind getKind() { + return kind; + } + + /** @return the PDB entry, or null */ + public PdbId getPdbId() { + return pdbId; + } + + /** @return the EMDB entry, or null */ + public String getEmdbId() { + return emdbId; + } + + /** @return the URL the map was fetched from */ + public String getSourceUrl() { + return sourceUrl; + } + + /** @return true if the file was already cached rather than downloaded now */ + public boolean isFromCache() { + return fromCache; + } + + /** + * Whether a viewer can contour this file as it stands. + * + * @return false for map coefficients, which need a Fourier + * transform first + * @see DensityFileFormat#isJmolLoadable() + */ + public boolean isRenderable() { + return format != null && format.isJmolLoadable(); + } + + /** + * The contour level recommended by the depositors, in absolute map units. + * Normally only available for EMDB maps, where it is the conventional way to + * contour rather than a multiple of sigma. + * + * @return the level, or null if unknown + */ + public Double getRecommendedContourLevel() { + return recommendedContourLevel; + } + + /** + * @return the RMS deviation of the map values, or null if unknown + */ + public Double getSigma() { + return sigma; + } + + /** + * The recommended contour expressed in multiples of sigma, for viewers that + * prefer to work that way. + * + * @return the level divided by sigma, or null if either is unknown + * or sigma is zero + */ + public Double getContourInSigma() { + if (recommendedContourLevel == null || sigma == null || sigma == 0.0) { + return null; + } + return recommendedContourLevel / sigma; + } + + /** @return the size of the cached file in bytes, or 0 if it is missing */ + public long getFileSizeBytes() { + return file == null ? 0 : file.length(); + } + + /** + * The metadata sidecar file for a cached map. + * + * @param mapFile the cached map + * @return the sidecar path, which need not exist + */ + public static File metaFileFor(File mapFile) { + return new File(mapFile.getAbsoluteFile().getParentFile(), mapFile.getName() + META_EXT); + } + + /** + * Writes this result's metadata beside the cached file, so that it can be + * reconstructed later without any network access. + */ + public void writeMeta() { + Properties p = new Properties(); + put(p, "source", source); + put(p, "format", format); + put(p, "kind", kind); + put(p, "pdbId", pdbId == null ? null : pdbId.getId()); + put(p, "emdbId", emdbId); + put(p, "url", sourceUrl); + put(p, "downloaded", Instant.now().toString()); + put(p, "bytes", getFileSizeBytes()); + put(p, "contourLevel", recommendedContourLevel); + put(p, "sigma", sigma); + File meta = metaFileFor(file); + try (OutputStream out = Files.newOutputStream(meta.toPath())) { + p.store(out, "BioJava density map metadata"); + } catch (IOException e) { + // Losing the sidecar costs us the offline description, not the map itself. + logger.warn("Could not write density metadata [{}]: {}", meta, e.getMessage()); + } + } + + /** + * Reconstructs a result from a cached file and its metadata sidecar. + * + * @param mapFile the cached map + * @return the reconstructed result, or null if the sidecar is + * missing or unusable + */ + public static DensityMapResult readMeta(File mapFile) { + File meta = metaFileFor(mapFile); + if (!meta.isFile()) { + return null; + } + Properties p = new Properties(); + try (InputStream in = Files.newInputStream(meta.toPath())) { + p.load(in); + } catch (IOException e) { + logger.warn("Could not read density metadata [{}]: {}", meta, e.getMessage()); + return null; + } + try { + String pdb = p.getProperty("pdbId"); + return new DensityMapResult(mapFile, + DensityMapSource.valueOf(p.getProperty("source")), + DensityFileFormat.valueOf(p.getProperty("format")), + DensityMapKind.valueOf(p.getProperty("kind")), + pdb == null || pdb.isEmpty() ? null : new PdbId(pdb), + emptyToNull(p.getProperty("emdbId")), + p.getProperty("url"), + true, + parseDouble(p.getProperty("contourLevel")), + parseDouble(p.getProperty("sigma"))); + } catch (RuntimeException e) { + logger.warn("Density metadata [{}] is unusable: {}", meta, e.getMessage()); + return null; + } + } + + private static void put(Properties p, String key, Object value) { + p.setProperty(key, value == null ? "" : String.valueOf(value)); + } + + private static String emptyToNull(String s) { + return s == null || s.isEmpty() ? null : s; + } + + private static Double parseDouble(String s) { + if (s == null || s.isEmpty()) { + return null; + } + try { + return Double.valueOf(s); + } catch (NumberFormatException e) { + return null; + } + } + + @Override + public String toString() { + return String.format("%s %s map from %s (%s, %d bytes)%s", + pdbId == null ? emdbId : pdbId.getId(), kind, source, format, getFileSizeBytes(), + isRenderable() ? "" : " [needs an FFT before it can be displayed]"); + } +} diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapSource.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapSource.java new file mode 100644 index 0000000000..a0f2e1a365 --- /dev/null +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapSource.java @@ -0,0 +1,84 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.io.density; + +/** + * A remote service that density data can be fetched from. + *

+ * These differ enormously in size for the same entry, which is why more than one + * is supported. For PDB entry 1cbs a density-server slice at the coarsest detail + * level is about 210 kB and contains both the 2Fo-Fc and Fo-Fc maps, where + * the two pre-computed CCP4 files come to about 2.1 MB together. For cryo-EM + * the gap is far wider: the primary map of EMD-0262 is about 106 MB, against + * roughly 480 kB for the equivalent density-server slice. + *

+ * Note that RCSB's own edmaps.rcsb.org service, which used to serve + * DSN6 and MTZ files, was shut down in October 2024. The map coefficients + * published with the wwPDB validation reports replaced it, but those are + * structure factors rather than a sampled grid; see + * {@link DensityFileFormat#MAP_COEFFICIENTS_CIF_GZ}. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public enum DensityMapSource { + + /** + * RCSB's Mol* density server at maps.rcsb.org, which serves + * downsampled BinaryCIF volume slices for both X-ray and EM entries. + */ + RCSB_VOLUME_SERVER("rcsbvs"), + + /** + * PDBe's Mol* density server, equivalent to {@link #RCSB_VOLUME_SERVER}. + */ + PDBE_VOLUME_SERVER("pdbevs"), + + /** + * PDBe's pre-computed full CCP4 maps. This is the source Jmol itself uses for + * its built-in map-loading shortcuts. + */ + PDBE_CCP4("pdbe"), + + /** + * The primary map of an EMDB entry, at full resolution. Can be very large. + */ + EMDB_MAP("emdb"), + + /** + * Map coefficients from the wwPDB validation reports. Archival only: these + * cannot be displayed without an FFT. + */ + WWPDB_MAP_COEFFICIENTS("wwpdb"); + + private final String fileToken; + + DensityMapSource(String fileToken) { + this.fileToken = fileToken; + } + + /** + * The short token used to distinguish this source in a cached file name, so + * that maps of the same kind from different sources never collide. + * + * @return the token + */ + public String getFileToken() { + return fileToken; + } +} diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapTooLargeException.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapTooLargeException.java new file mode 100644 index 0000000000..ce62e36d1d --- /dev/null +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityMapTooLargeException.java @@ -0,0 +1,84 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.io.density; + +import java.io.IOException; + +/** + * Thrown when a density map exceeds the configured download size limit. + *

+ * Cryo-EM primary maps in particular can be very large — hundreds of + * megabytes is common and gigabyte maps exist — while a downsampled slice + * of the same map from a density server is usually a fraction of a percent of + * that size and quite adequate for display. Rather than failing, the fallback + * chain treats this as "try the next source", which is exactly why the density + * servers are tried ahead of the full-resolution archives. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public class DensityMapTooLargeException extends IOException { + + private static final long serialVersionUID = 1L; + + private final long sizeBytes; + private final long limitBytes; + + /** + * @param url the resource that was too large + * @param sizeBytes its size, or a negative value if only a lower bound is known + * @param limitBytes the configured limit + */ + public DensityMapTooLargeException(String url, long sizeBytes, long limitBytes) { + super(String.format("%s is %s, which exceeds the %s download limit.", + url, describe(sizeBytes), describe(limitBytes))); + this.sizeBytes = sizeBytes; + this.limitBytes = limitBytes; + } + + private static String describe(long bytes) { + if (bytes < 0) { + return "of unknown size"; + } + if (bytes < 1024) { + return bytes + " B"; + } + double value = bytes; + String[] units = {"kB", "MB", "GB", "TB"}; + int unit = -1; + while (value >= 1024 && unit < units.length - 1) { + value /= 1024; + unit++; + } + return String.format("%.1f %s", value, units[unit]); + } + + /** + * @return the size of the resource in bytes, or a negative value if unknown + */ + public long getSizeBytes() { + return sizeBytes; + } + + /** + * @return the configured limit in bytes + */ + public long getLimitBytes() { + return limitBytes; + } +} diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/EmdbEntryInfo.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/EmdbEntryInfo.java new file mode 100644 index 0000000000..7c8b0dd302 --- /dev/null +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/EmdbEntryInfo.java @@ -0,0 +1,85 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.io.density; + +/** + * The few facts about an EMDB entry's primary map that matter when fetching and + * displaying it. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public class EmdbEntryInfo { + + private final String emdbId; + private final Double recommendedContourLevel; + private final Double sigma; + private final Long mapSizeBytes; + + /** + * @param emdbId the entry identifier, canonical form + * @param recommendedContourLevel the author-recommended contour level in absolute + * map units, or null + * @param sigma the RMS deviation of the map values, or null + * @param mapSizeBytes the size of the primary map file, or null + */ + public EmdbEntryInfo(String emdbId, Double recommendedContourLevel, Double sigma, Long mapSizeBytes) { + this.emdbId = emdbId; + this.recommendedContourLevel = recommendedContourLevel; + this.sigma = sigma; + this.mapSizeBytes = mapSizeBytes; + } + + /** @return the entry identifier in canonical EMD-1234 form */ + public String getEmdbId() { + return emdbId; + } + + /** + * The contour level the depositors recommend, in absolute map units. EM maps + * are conventionally displayed at this level rather than at a multiple of + * sigma, so it is the right default for a viewer. + * + * @return the level, or null if the entry does not state one + */ + public Double getRecommendedContourLevel() { + return recommendedContourLevel; + } + + /** + * @return the RMS deviation of the map values, which converts between absolute + * and sigma-relative contour levels, or null if unknown + */ + public Double getSigma() { + return sigma; + } + + /** + * @return the size of the full primary map in bytes, or null if + * unknown. Used to decline a download before it starts. + */ + public Long getMapSizeBytes() { + return mapSizeBytes; + } + + @Override + public String toString() { + return String.format("%s[contour=%s, sigma=%s, %s bytes]", + emdbId, recommendedContourLevel, sigma, mapSizeBytes); + } +} diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/EmdbEntryResolver.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/EmdbEntryResolver.java new file mode 100644 index 0000000000..fab0101b34 --- /dev/null +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/EmdbEntryResolver.java @@ -0,0 +1,382 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.io.density; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.biojava.nbio.structure.PdbId; +import org.biojava.nbio.structure.align.util.URLConnectionTools; +import org.biojava.nbio.structure.io.LocalPDBDirectory.FetchBehavior; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Finds the EMDB entry, if any, associated with a PDB entry, and reads the few + * facts about its map that are needed to fetch and display it. + *

+ * The primary lookup is EMDB's own search API, which returns the identifier and + * the author-recommended contour level together in a single small CSV response. + * That is also the route Jmol uses, so BioJava's answer matches what a user sees + * elsewhere. If it fails, RCSB's entry API is consulted for the identifier alone. + *

+ * The experimental method is deliberately not inferred from a structure's + * resolution: BioJava is known to mis-parse resolution for some cryo-EM entries + * (biojava/biojava#1000), so the presence of an EMDB identifier is the reliable + * signal. + *

+ * Answers are cached on disk. Under + * {@link FetchBehavior#LOCAL_ONLY} the cached answer is used whatever its age and + * no connection is opened. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public class EmdbEntryResolver { + + private static final Logger logger = LoggerFactory.getLogger(EmdbEntryResolver.class); + + /** + * Default template for the EMDB search that maps a PDB entry to the EM + * reconstructions it was fitted into, returning the contour level as well. + */ + public static final String DEFAULT_SEARCH_URL_TEMPLATE = + "https://www.ebi.ac.uk/emdb/api/search/fitted_pdbs:{pdbid_lc}?fl=emdb_id,map_contour_level_value&wt=csv"; + + /** Default template for the EMDB entry map metadata. */ + public static final String DEFAULT_MAP_INFO_URL_TEMPLATE = + "https://www.ebi.ac.uk/emdb/api/entry/map/{emdb_id}"; + + /** Default template for the RCSB entry record, used as a fallback. */ + public static final String DEFAULT_RCSB_ENTRY_URL_TEMPLATE = + "https://data.rcsb.org/rest/v1/core/entry/{pdbid_lc}"; + + private static final int TIMEOUT_MILLIS = 30000; + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private static String searchUrlTemplate = DEFAULT_SEARCH_URL_TEMPLATE; + private static String mapInfoUrlTemplate = DEFAULT_MAP_INFO_URL_TEMPLATE; + private static String rcsbEntryUrlTemplate = DEFAULT_RCSB_ENTRY_URL_TEMPLATE; + + private File cacheRoot; + private FetchBehavior fetchBehavior = FetchBehavior.FETCH_FILES; + private int mappingMaxAgeDays = 30; + + /** + * @param cacheRoot the BioJava cache directory + */ + public EmdbEntryResolver(File cacheRoot) { + this.cacheRoot = cacheRoot; + } + + /** @param cacheRoot the BioJava cache directory */ + public void setCacheRoot(File cacheRoot) { + this.cacheRoot = cacheRoot; + } + + /** @param fetchBehavior how aggressively to refresh cached answers */ + public void setFetchBehavior(FetchBehavior fetchBehavior) { + this.fetchBehavior = fetchBehavior == null ? FetchBehavior.FETCH_FILES : fetchBehavior; + } + + /** + * @param days how long a cached mapping stays fresh. Mappings change only when + * an entry is re-released, so this can be generous. + */ + public void setMappingMaxAgeDays(int days) { + this.mappingMaxAgeDays = days; + } + + /** @param template the EMDB search URL template */ + public static void setSearchUrlTemplate(String template) { + searchUrlTemplate = template == null ? DEFAULT_SEARCH_URL_TEMPLATE : template; + } + + /** @param template the EMDB map metadata URL template */ + public static void setMapInfoUrlTemplate(String template) { + mapInfoUrlTemplate = template == null ? DEFAULT_MAP_INFO_URL_TEMPLATE : template; + } + + /** @param template the RCSB entry URL template used as a fallback */ + public static void setRcsbEntryUrlTemplate(String template) { + rcsbEntryUrlTemplate = template == null ? DEFAULT_RCSB_ENTRY_URL_TEMPLATE : template; + } + + /** Restores all default URL templates. */ + public static void resetToDefaults() { + searchUrlTemplate = DEFAULT_SEARCH_URL_TEMPLATE; + mapInfoUrlTemplate = DEFAULT_MAP_INFO_URL_TEMPLATE; + rcsbEntryUrlTemplate = DEFAULT_RCSB_ENTRY_URL_TEMPLATE; + } + + /** + * The EMDB entries a PDB entry was fitted into. + * + * @param pdbId the PDB entry + * @return the EMDB identifiers, most relevant first; empty if the entry is not + * an EM structure or has no associated map + */ + public List getEmdbIds(PdbId pdbId) { + Mapping cached = readMapping(pdbId); + if (cached != null && (fetchBehavior == FetchBehavior.LOCAL_ONLY || cached.isFresh(mappingMaxAgeDays))) { + return cached.emdbIds; + } + if (fetchBehavior == FetchBehavior.LOCAL_ONLY) { + return Collections.emptyList(); + } + + Mapping fetched = searchEmdb(pdbId); + if (fetched == null) { + fetched = queryRcsb(pdbId); + } + if (fetched == null) { + // Serve a stale answer rather than nothing when the services are unreachable. + return cached == null ? Collections.emptyList() : cached.emdbIds; + } + writeMapping(pdbId, fetched); + return fetched.emdbIds; + } + + /** + * The contour level the depositors recommend for the map a PDB entry was fitted + * into, as reported by the EMDB search. + * + * @param pdbId the PDB entry + * @return the level in absolute map units, or null if unknown + */ + public Double getContourLevelFor(PdbId pdbId) { + Mapping cached = readMapping(pdbId); + if (cached != null && (fetchBehavior == FetchBehavior.LOCAL_ONLY || cached.isFresh(mappingMaxAgeDays))) { + return cached.contourLevel; + } + getEmdbIds(pdbId); + Mapping refreshed = readMapping(pdbId); + return refreshed == null ? null : refreshed.contourLevel; + } + + /** + * Reads an EMDB entry's map metadata: contour level, RMS deviation and the size + * of the primary map file. + *

+ * The response is cached verbatim, so asking twice costs one request. + * + * @param emdbId the EMDB entry, in any accepted form + * @return the metadata, or null if it could not be obtained + */ + public EmdbEntryInfo getEntryInfo(String emdbId) { + String canonical = DensityMapRequest.normalizeEmdbId(emdbId); + File cacheFile = DensityCacheLayout.emdbMapInfoFile(cacheRoot, canonical); + + String json = null; + if (cacheFile.isFile()) { + try { + json = new String(Files.readAllBytes(cacheFile.toPath()), StandardCharsets.UTF_8); + } catch (IOException e) { + logger.debug("Could not read cached EMDB metadata [{}]: {}", cacheFile, e.getMessage()); + } + } + if (json == null) { + if (fetchBehavior == FetchBehavior.LOCAL_ONLY) { + return null; + } + String url = UrlTemplates.expand(mapInfoUrlTemplate, UrlTemplates.values(null, canonical, -1)); + try { + json = read(new URL(url)); + } catch (IOException e) { + logger.warn("Could not read EMDB metadata for {}: {}", canonical, e.getMessage()); + return null; + } + writeCache(cacheFile, json); + } + + try { + JsonNode map = MAPPER.readTree(json).path("map"); + Double contour = null; + JsonNode contours = map.path("contour_list").path("contour"); + for (JsonNode c : contours) { + if (contour == null || c.path("primary").asBoolean(false)) { + contour = c.path("level").isNumber() ? c.path("level").asDouble() : contour; + } + } + Double sigma = map.path("statistics").path("std").isNumber() + ? map.path("statistics").path("std").asDouble() : null; + Long bytes = map.path("size_kbytes").isNumber() + ? map.path("size_kbytes").asLong() * 1024L : null; + return new EmdbEntryInfo(canonical, contour, sigma, bytes); + } catch (IOException | RuntimeException e) { + logger.warn("Could not parse EMDB metadata for {}: {}", canonical, e.getMessage()); + return null; + } + } + + private Mapping searchEmdb(PdbId pdbId) { + String url = UrlTemplates.expand(searchUrlTemplate, + UrlTemplates.values(DensityCacheLayout.shortIdOrFull(pdbId), null, -1)); + try { + String csv = read(new URL(url)); + List ids = new ArrayList<>(); + Double contour = null; + String[] lines = csv.split("\\R"); + for (int i = 1; i < lines.length; i++) { // line 0 is the header + String line = lines[i].trim(); + if (line.isEmpty()) { + continue; + } + String[] cols = line.split(","); + if (cols.length > 0 && !cols[0].isEmpty()) { + ids.add(DensityMapRequest.normalizeEmdbId(cols[0])); + } + if (contour == null && cols.length > 1 && !cols[1].isEmpty()) { + try { + contour = Double.valueOf(cols[1].trim()); + } catch (NumberFormatException ignored) { + // the column is optional and occasionally blank + } + } + } + return new Mapping(ids, contour, Instant.now()); + } catch (IOException e) { + logger.debug("EMDB search for {} failed: {}", pdbId.getId(), e.getMessage()); + return null; + } + } + + private Mapping queryRcsb(PdbId pdbId) { + String url = UrlTemplates.expand(rcsbEntryUrlTemplate, + UrlTemplates.values(DensityCacheLayout.shortIdOrFull(pdbId), null, -1)); + try { + JsonNode root = MAPPER.readTree(read(new URL(url))); + JsonNode ids = root.path("rcsb_entry_container_identifiers").path("emdb_ids"); + List result = new ArrayList<>(); + for (JsonNode id : ids) { + result.add(DensityMapRequest.normalizeEmdbId(id.asText())); + } + return new Mapping(result, null, Instant.now()); + } catch (IOException | RuntimeException e) { + logger.debug("RCSB entry lookup for {} failed: {}", pdbId.getId(), e.getMessage()); + return null; + } + } + + private String read(URL url) throws IOException { + try (InputStream in = URLConnectionTools.getInputStream(url, true, TIMEOUT_MILLIS); + BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { + StringBuilder sb = new StringBuilder(); + char[] buffer = new char[8192]; + int n; + while ((n = reader.read(buffer)) != -1) { + sb.append(buffer, 0, n); + } + return sb.toString(); + } + } + + private Mapping readMapping(PdbId pdbId) { + File file = DensityCacheLayout.emdbMappingFile(cacheRoot, pdbId); + if (!file.isFile()) { + return null; + } + Properties p = new Properties(); + try (InputStream in = Files.newInputStream(file.toPath())) { + p.load(in); + } catch (IOException e) { + return null; + } + String ids = p.getProperty("emdbIds", ""); + List list = ids.isEmpty() ? Collections.emptyList() : Arrays.asList(ids.split(",")); + Double contour = null; + try { + String c = p.getProperty("contourLevel", ""); + contour = c.isEmpty() ? null : Double.valueOf(c); + } catch (NumberFormatException ignored) { + // leave it null + } + Instant retrieved; + try { + retrieved = Instant.parse(p.getProperty("retrieved")); + } catch (RuntimeException e) { + retrieved = Instant.EPOCH; + } + return new Mapping(list, contour, retrieved); + } + + private void writeMapping(PdbId pdbId, Mapping mapping) { + File file = DensityCacheLayout.emdbMappingFile(cacheRoot, pdbId); + File dir = file.getParentFile(); + if (!dir.isDirectory() && !dir.mkdirs()) { + logger.debug("Could not create [{}]", dir); + return; + } + Properties p = new Properties(); + p.setProperty("pdbId", DensityCacheLayout.shortIdOrFull(pdbId)); + p.setProperty("emdbIds", String.join(",", mapping.emdbIds)); + p.setProperty("contourLevel", mapping.contourLevel == null ? "" : mapping.contourLevel.toString()); + p.setProperty("retrieved", mapping.retrieved.toString()); + try (OutputStream out = Files.newOutputStream(file.toPath())) { + p.store(out, "BioJava PDB to EMDB mapping"); + } catch (IOException e) { + logger.debug("Could not cache the EMDB mapping for {}: {}", pdbId.getId(), e.getMessage()); + } + } + + private void writeCache(File file, String content) { + File dir = file.getParentFile(); + if (!dir.isDirectory() && !dir.mkdirs()) { + return; + } + try { + Files.write(file.toPath(), content.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + logger.debug("Could not cache [{}]: {}", file, e.getMessage()); + } + } + + /** A cached PDB-to-EMDB answer. */ + private static final class Mapping { + final List emdbIds; + final Double contourLevel; + final Instant retrieved; + + Mapping(List emdbIds, Double contourLevel, Instant retrieved) { + this.emdbIds = Collections.unmodifiableList(new ArrayList<>(emdbIds)); + this.contourLevel = contourLevel; + this.retrieved = retrieved; + } + + boolean isFresh(int maxAgeDays) { + return Duration.between(retrieved, Instant.now()).toDays() < maxAgeDays; + } + } +} diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/EmdbMapProvider.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/EmdbMapProvider.java new file mode 100644 index 0000000000..7f90814c30 --- /dev/null +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/EmdbMapProvider.java @@ -0,0 +1,142 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.io.density; + +import java.io.File; +import java.io.IOException; +import java.net.URL; + +/** + * Fetches the full-resolution primary map of an EMDB entry. + *

+ * These are gzipped CCP4/MRC files, and they are big: the primary map of + * EMD-0262 is about 106 MB, and gigabyte maps exist. For most display + * purposes a slice from a density server is a far better trade — a few + * hundred kilobytes for the same entry — which is why + * {@link DensityMapCache} tries {@link VolumeServerProvider} first and only falls + * back here. Use this source when the full sampling genuinely matters. + *

+ * The size limit is checked against the size EMDB itself reports before any of + * the body is transferred, so exceeding it costs one small metadata request + * rather than a partial download. + *

+ * Jmol recognises gzip from the file's magic bytes, so the cached + * .map.gz can be handed to it without decompressing first. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public class EmdbMapProvider extends AbstractDensityMapProvider { + + /** Default base URL of the EMDB archive. */ + public static final String DEFAULT_SERVER_URL = "https://ftp.ebi.ac.uk/pub/databases/emdb/structures/"; + + /** Default path template for an entry's primary map. */ + public static final String DEFAULT_MAP_TEMPLATE = "{emdb_id}/map/emd_{emdb_num}.map.gz"; + + private static String serverBaseUrl = DEFAULT_SERVER_URL; + private static String mapTemplate = DEFAULT_MAP_TEMPLATE; + + private final EmdbEntryResolver resolver; + + /** + * @param cacheRoot the BioJava cache directory + * @param resolver used to find the EMDB entry for a PDB entry and to read its + * contour level and size + */ + public EmdbMapProvider(File cacheRoot, EmdbEntryResolver resolver) { + super(cacheRoot); + this.resolver = resolver; + } + + /** @return the base URL of the EMDB archive */ + public static String getServerBaseUrl() { + return serverBaseUrl; + } + + /** @param url the base URL; a trailing slash is added if missing */ + public static void setServerBaseUrl(String url) { + serverBaseUrl = url == null ? DEFAULT_SERVER_URL : (url.endsWith("/") ? url : url + "/"); + } + + /** @param template the path template for an entry's primary map */ + public static void setMapUrlTemplate(String template) { + mapTemplate = template == null ? DEFAULT_MAP_TEMPLATE : template; + } + + /** Restores the default server and template. */ + public static void resetToDefaults() { + serverBaseUrl = DEFAULT_SERVER_URL; + mapTemplate = DEFAULT_MAP_TEMPLATE; + } + + @Override + public DensityMapSource getSource() { + return DensityMapSource.EMDB_MAP; + } + + @Override + public DensityFileFormat getFormat() { + return DensityFileFormat.CCP4_GZ; + } + + @Override + public boolean supports(DensityMapKind kind) { + return kind == DensityMapKind.EM; + } + + /** + * Builds the URL of an entry's primary map without fetching it. + * + * @param emdbId the EMDB entry, in any accepted form + * @return the URL as a string + */ + public String buildUrl(String emdbId) { + return serverBaseUrl + UrlTemplates.expand(mapTemplate, UrlTemplates.values(null, emdbId, -1)); + } + + @Override + public DensityMapResult fetch(DensityMapRequest request) throws IOException { + if (!supports(request.getKind())) { + return null; + } + String emdbId = request.getEmdbId(); + if (emdbId == null) { + return null; + } + + EmdbEntryInfo info = resolver == null ? null : resolver.getEntryInfo(emdbId); + + // Decline before transferring anything: EMDB reports the map size in its + // metadata, so an oversized map costs one small request rather than a + // partial multi-hundred-megabyte download. + long limit = effectiveMaxBytes(request); + if (limit > 0 && info != null && info.getMapSizeBytes() != null && info.getMapSizeBytes() > limit) { + URL url = new URL(buildUrl(emdbId)); + reportTooLarge(url, info.getMapSizeBytes(), limit, request); + throw new DensityMapTooLargeException(url.toString(), info.getMapSizeBytes(), limit); + } + + URL url = new URL(buildUrl(emdbId)); + File target = DensityCacheLayout.emdbMapFile(effectiveCacheRoot(request), emdbId, + getSource(), getFormat(), null); + return obtain(request, url, target, DensityMapKind.EM, emdbId, + info == null ? null : info.getRecommendedContourLevel(), + info == null ? null : info.getSigma()); + } +} diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/NoDensityMapException.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/NoDensityMapException.java new file mode 100644 index 0000000000..6691b584d9 --- /dev/null +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/NoDensityMapException.java @@ -0,0 +1,99 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.io.density; + +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.biojava.nbio.structure.PdbId; + +/** + * Thrown when no enabled source could supply a density map for an entry. + *

+ * This is a routine outcome rather than an error: not every entry has density. + * Structures deposited without structure factors have none at all (4HHB, for + * instance), and a cryo-EM entry has no X-ray maps by construction. The + * per-source reasons are kept so that a caller — particularly a user + * interface — can explain why rather than just reporting a failure. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public class NoDensityMapException extends IOException { + + private static final long serialVersionUID = 1L; + + private final PdbId pdbId; + private final DensityMapKind kind; + private final Map attempts; + + /** + * @param pdbId the entry that was requested, may be null for a + * request made by EMDB identifier + * @param kind the kind of map that was requested + * @param attempts what happened at each source that was tried, in the order tried + */ + public NoDensityMapException(PdbId pdbId, DensityMapKind kind, Map attempts) { + super(buildMessage(pdbId, kind, attempts)); + this.pdbId = pdbId; + this.kind = kind; + this.attempts = attempts == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(new LinkedHashMap<>(attempts)); + } + + private static String buildMessage(PdbId pdbId, DensityMapKind kind, Map attempts) { + StringBuilder sb = new StringBuilder("No "); + sb.append(kind == null ? "density" : kind).append(" map is available for "); + sb.append(pdbId == null ? "the requested entry" : pdbId.getId()).append('.'); + if (attempts != null && !attempts.isEmpty()) { + sb.append(" Tried:"); + for (Map.Entry e : attempts.entrySet()) { + sb.append(' ').append(e.getKey()).append(" (").append(e.getValue()).append(");"); + } + sb.setLength(sb.length() - 1); + } + return sb.toString(); + } + + /** + * @return the entry that was requested, or null + */ + public PdbId getPdbId() { + return pdbId; + } + + /** + * @return the kind of map that was requested + */ + public DensityMapKind getKind() { + return kind; + } + + /** + * What happened at each source, in the order they were tried. Suitable for + * building a human-readable explanation. + * + * @return an unmodifiable map from source to reason + */ + public Map getAttempts() { + return attempts; + } +} diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/PdbeCcp4MapProvider.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/PdbeCcp4MapProvider.java new file mode 100644 index 0000000000..a825ccfa70 --- /dev/null +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/PdbeCcp4MapProvider.java @@ -0,0 +1,142 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.io.density; + +import java.io.File; +import java.io.IOException; +import java.net.URL; + +import org.biojava.nbio.structure.PdbId; + +/** + * Fetches PDBe's pre-computed CCP4 maps. + *

+ * These are full-resolution sampled grids, one file per map kind, and are the + * source Jmol itself uses for its built-in map shortcuts. They are larger than a + * density-server slice — about 1 MB per map for a small entry such as + * 1cbs — but need no interpretation beyond contouring. + *

+ * The service accepts only the lower-case four-character spelling of an + * identifier; 1CBS.ccp4 and pdb_00001cbs.ccp4 both + * return HTTP 404. Entries deposited without structure factors, and cryo-EM + * entries, have no maps here at all. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public class PdbeCcp4MapProvider extends AbstractDensityMapProvider { + + /** Default base URL of the PDBe map service. */ + public static final String DEFAULT_SERVER_URL = "https://www.ebi.ac.uk/pdbe/coordinates/files/"; + + /** + * An equivalent base URL serving the same files, kept in the documentation as a + * ready alternative should the primary one change. + */ + public static final String ALTERNATIVE_SERVER_URL = "https://www.ebi.ac.uk/pdbe/entry-files/"; + + /** Default path template for the 2mFo-DFc map. */ + public static final String DEFAULT_TWO_FO_FC_TEMPLATE = "{pdbid_lc}.ccp4"; + + /** Default path template for the mFo-DFc difference map. */ + public static final String DEFAULT_FO_FC_TEMPLATE = "{pdbid_lc}_diff.ccp4"; + + private static String serverBaseUrl = DEFAULT_SERVER_URL; + private static String twoFoFcTemplate = DEFAULT_TWO_FO_FC_TEMPLATE; + private static String foFcTemplate = DEFAULT_FO_FC_TEMPLATE; + + /** + * @param cacheRoot the BioJava cache directory + */ + public PdbeCcp4MapProvider(File cacheRoot) { + super(cacheRoot); + } + + /** @return the base URL of the map service */ + public static String getServerBaseUrl() { + return serverBaseUrl; + } + + /** + * @param url the base URL of the map service; a trailing slash is added if missing + */ + public static void setServerBaseUrl(String url) { + serverBaseUrl = url == null ? DEFAULT_SERVER_URL : (url.endsWith("/") ? url : url + "/"); + } + + /** + * Overrides the path template for a map kind. + * + * @param kind {@link DensityMapKind#TWO_FO_FC} or {@link DensityMapKind#FO_FC} + * @param template a template understood by {@link UrlTemplates} + */ + public static void setPathUrlTemplate(DensityMapKind kind, String template) { + if (kind == DensityMapKind.TWO_FO_FC) { + twoFoFcTemplate = template == null ? DEFAULT_TWO_FO_FC_TEMPLATE : template; + } else if (kind == DensityMapKind.FO_FC) { + foFcTemplate = template == null ? DEFAULT_FO_FC_TEMPLATE : template; + } else { + throw new IllegalArgumentException("PDBe CCP4 maps exist only for 2Fo-Fc and Fo-Fc, not " + kind); + } + } + + /** Restores the default server and templates. */ + public static void resetToDefaults() { + serverBaseUrl = DEFAULT_SERVER_URL; + twoFoFcTemplate = DEFAULT_TWO_FO_FC_TEMPLATE; + foFcTemplate = DEFAULT_FO_FC_TEMPLATE; + } + + @Override + public DensityMapSource getSource() { + return DensityMapSource.PDBE_CCP4; + } + + @Override + public DensityFileFormat getFormat() { + return DensityFileFormat.CCP4; + } + + @Override + public boolean supports(DensityMapKind kind) { + return kind == DensityMapKind.TWO_FO_FC || kind == DensityMapKind.FO_FC; + } + + /** + * Builds the URL for a map without fetching it. + * + * @param pdbId the entry + * @param kind the kind of map + * @return the URL as a string + */ + public String buildUrl(PdbId pdbId, DensityMapKind kind) { + String template = kind == DensityMapKind.FO_FC ? foFcTemplate : twoFoFcTemplate; + return serverBaseUrl + UrlTemplates.expand(template, UrlTemplates.values(urlId(pdbId), null, -1)); + } + + @Override + public DensityMapResult fetch(DensityMapRequest request) throws IOException { + if (request.getPdbId() == null || !supports(request.getKind())) { + return null; + } + URL url = new URL(buildUrl(request.getPdbId(), request.getKind())); + File target = DensityCacheLayout.pdbMapFile(effectiveCacheRoot(request), request.getPdbId(), + request.getKind(), getSource(), getFormat(), null); + return obtain(request, url, target, request.getKind(), null, null, null); + } +} diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/UrlTemplates.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/UrlTemplates.java new file mode 100644 index 0000000000..b116386059 --- /dev/null +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/UrlTemplates.java @@ -0,0 +1,112 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.io.density; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Expands the named placeholders used in the configurable density-server URL + * templates. + *

+ * The recognised placeholders are: + * + * + * + * + * + * + * + * + *
{pdbid}the PDB identifier as given
{pdbid_lc}the PDB identifier in lower case
{pdbid_uc}the PDB identifier in upper case
{mid}the two-character divided-archive directory, + * e.g. cb for 1cbs
{emdb_id}the EMDB identifier, e.g. EMD-0262
{emdb_num}the EMDB number alone, e.g. 0262
{detail}the density-server detail level
+ * A placeholder with no supplied value is left in place rather than replaced by + * an empty string, so that a misconfigured template produces an obviously wrong + * URL instead of a subtly wrong one. + *

+ * This is deliberately separate from the template mechanism in + * {@code DownloadChemCompProvider}, which resolves a single chemical-component + * identifier with optional substring indices. The two solve different problems: + * here several distinct values are substituted by name. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public class UrlTemplates { + + private static final Pattern PLACEHOLDER = Pattern.compile("\\{([a-zA-Z_]+)\\}"); + + private UrlTemplates() { + } + + /** + * Expands a template against a set of named values. + * + * @param template the template string + * @param values the values, keyed by placeholder name without the braces + * @return the expanded string + */ + public static String expand(String template, Map values) { + if (template == null) { + return null; + } + Matcher m = PLACEHOLDER.matcher(template); + StringBuilder out = new StringBuilder(); + int last = 0; + while (m.find()) { + String value = values.get(m.group(1)); + out.append(template, last, m.start()); + // An unknown placeholder is kept verbatim: a template that was configured + // wrongly should fail loudly rather than quietly produce a plausible URL. + out.append(value == null ? m.group(0) : value); + last = m.end(); + } + out.append(template.substring(last)); + return out.toString(); + } + + /** + * Builds the standard value map for an entry. + * + * @param pdbId the PDB identifier, may be null + * @param emdbId the EMDB identifier in any accepted form, may be null + * @param detail the density-server detail level, or a negative value to omit it + * @return a map suitable for {@link #expand(String, Map)} + */ + public static Map values(String pdbId, String emdbId, int detail) { + Map values = new LinkedHashMap<>(); + if (pdbId != null) { + values.put("pdbid", pdbId); + values.put("pdbid_lc", pdbId.toLowerCase()); + values.put("pdbid_uc", pdbId.toUpperCase()); + if (pdbId.length() >= 3) { + values.put("mid", org.biojava.nbio.structure.io.LocalPDBDirectory.getMiddleHash(pdbId)); + } + } + if (emdbId != null) { + values.put("emdb_id", DensityMapRequest.normalizeEmdbId(emdbId)); + values.put("emdb_num", DensityMapRequest.emdbNumber(emdbId)); + } + if (detail >= 0) { + values.put("detail", Integer.toString(detail)); + } + return values; + } +} diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/VolumeServerProvider.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/VolumeServerProvider.java new file mode 100644 index 0000000000..f170446f56 --- /dev/null +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/VolumeServerProvider.java @@ -0,0 +1,215 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.io.density; + +import java.io.File; +import java.io.IOException; +import java.net.URL; + +/** + * Fetches downsampled volume slices from a Mol* density server. + *

+ * This is usually the most economical source by a wide margin. For PDB entry + * 1cbs the coarsest slice is about 210 kB against roughly 2.1 MB for + * the two pre-computed CCP4 files, and for cryo-EM the difference is far larger + * still: about 480 kB against the 106 MB primary map of EMD-0262. A + * single response carries both the 2Fo-Fc and Fo-Fc data blocks for X-ray + * entries. + *

+ * Caveats worth knowing. Responses are chunked, with no + * Content-Length, no ETag and no + * Last-Modified, so the usual server-provided validation metadata is + * unavailable; the size recorded for a cached slice is the byte count observed + * during our own download instead. Detail levels are not linear either — + * for a small entry the response stops growing past detail 1, while for a large + * EM map the step from detail 2 to 3 multiplies the size several-fold. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public class VolumeServerProvider extends AbstractDensityMapProvider { + + /** + * Which density server to talk to. + */ + public enum Host { + /** RCSB's server at maps.rcsb.org. */ + RCSB(DensityMapSource.RCSB_VOLUME_SERVER, "https://maps.rcsb.org/", 3), + /** PDBe's server at www.ebi.ac.uk/pdbe/volume-server. */ + PDBE(DensityMapSource.PDBE_VOLUME_SERVER, "https://www.ebi.ac.uk/pdbe/volume-server/", 6); + + private final DensityMapSource source; + private final String defaultBaseUrl; + private final int defaultDetail; + + Host(DensityMapSource source, String defaultBaseUrl, int defaultDetail) { + this.source = source; + this.defaultBaseUrl = defaultBaseUrl; + this.defaultDetail = defaultDetail; + } + + /** @return the source constant this host corresponds to */ + public DensityMapSource getSource() { + return source; + } + + /** @return the default base URL */ + public String getDefaultBaseUrl() { + return defaultBaseUrl; + } + + /** @return the detail level this host's own clients use by default */ + public int getDefaultDetail() { + return defaultDetail; + } + } + + /** Default path template for an X-ray entry's whole unit cell. */ + public static final String DEFAULT_XRAY_CELL_TEMPLATE = "x-ray/{pdbid_lc}/cell?detail={detail}&encoding={encoding}"; + + /** Default path template for an EM entry's whole cell. */ + public static final String DEFAULT_EM_CELL_TEMPLATE = "em/emd-{emdb_num}/cell?detail={detail}&encoding={encoding}"; + + private final Host host; + private String baseUrl; + private int detail; + private String encoding = "bcif"; + + /** + * @param cacheRoot the BioJava cache directory + * @param host which density server to use + */ + public VolumeServerProvider(File cacheRoot, Host host) { + super(cacheRoot); + this.host = host; + this.baseUrl = host.getDefaultBaseUrl(); + this.detail = host.getDefaultDetail(); + } + + /** @return which density server this instance uses */ + public Host getHost() { + return host; + } + + /** @return the base URL in use */ + public String getBaseUrl() { + return baseUrl; + } + + /** @param baseUrl the base URL; a trailing slash is added if missing */ + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl == null ? host.getDefaultBaseUrl() + : (baseUrl.endsWith("/") ? baseUrl : baseUrl + "/"); + } + + /** @return the detail level requested from the server */ + public int getDetail() { + return detail; + } + + /** + * Sets the detail level. Higher means a finer grid and a larger download; the + * relationship is neither linear nor the same for every entry. + * + * @param detail the level, normally 0 to 6 + */ + public void setDetail(int detail) { + this.detail = detail; + } + + /** @return the encoding requested, either bcif or cif */ + public String getEncoding() { + return encoding; + } + + /** + * @param encoding bcif for BinaryCIF (default, and much smaller) or + * cif for text + */ + public void setEncoding(String encoding) { + this.encoding = encoding == null ? "bcif" : encoding; + } + + @Override + public DensityMapSource getSource() { + return host.getSource(); + } + + @Override + public DensityFileFormat getFormat() { + return "cif".equalsIgnoreCase(encoding) ? DensityFileFormat.CIF_VOLUME : DensityFileFormat.BCIF_VOLUME; + } + + @Override + public boolean supports(DensityMapKind kind) { + return kind == DensityMapKind.TWO_FO_FC || kind == DensityMapKind.FO_FC || kind == DensityMapKind.EM; + } + + /** + * Builds the URL for a request without fetching it. + * + * @param request the request; its kind must be concrete + * @return the URL as a string, or null if the request cannot be served + */ + public String buildUrl(DensityMapRequest request) { + if (request.getKind() == DensityMapKind.EM) { + if (request.getEmdbId() == null) { + return null; + } + return baseUrl + UrlTemplates.expand(withEncoding(DEFAULT_EM_CELL_TEMPLATE), + UrlTemplates.values(null, request.getEmdbId(), detail)); + } + if (request.getPdbId() == null) { + return null; + } + return baseUrl + UrlTemplates.expand(withEncoding(DEFAULT_XRAY_CELL_TEMPLATE), + UrlTemplates.values(urlId(request.getPdbId()), null, detail)); + } + + private String withEncoding(String template) { + return template.replace("{encoding}", encoding); + } + + @Override + public DensityMapResult fetch(DensityMapRequest request) throws IOException { + if (!supports(request.getKind())) { + return null; + } + String urlString = buildUrl(request); + if (urlString == null) { + return null; + } + URL url = new URL(urlString); + + // The detail level changes the content, so it has to be part of the cache key. + String qualifier = "d" + detail; + File target; + if (request.getKind() == DensityMapKind.EM) { + target = DensityCacheLayout.emdbMapFile(effectiveCacheRoot(request), request.getEmdbId(), + getSource(), getFormat(), qualifier); + } else { + // One response carries both the 2Fo-Fc and the Fo-Fc blocks, so the two + // kinds share a cache entry: asking for each separately would otherwise + // download and store the identical file twice. Which block to read is + // decided at display time, not here. + target = DensityCacheLayout.pdbMapFile(effectiveCacheRoot(request), request.getPdbId(), + DensityCacheLayout.BOTH_KINDS_TOKEN, getSource(), getFormat(), qualifier); + } + return obtain(request, url, target, request.getKind(), request.getEmdbId(), null, null); + } +} diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/WwpdbMapCoefficientsProvider.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/WwpdbMapCoefficientsProvider.java new file mode 100644 index 0000000000..c6eed7306e --- /dev/null +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/WwpdbMapCoefficientsProvider.java @@ -0,0 +1,147 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.io.density; + +import java.io.File; +import java.io.IOException; +import java.net.URL; + +import org.biojava.nbio.structure.PdbId; + +/** + * Fetches the map coefficients published alongside the wwPDB validation reports. + *

+ * These are not density maps. They are structure-factor amplitudes and + * phases in mmCIF, exactly as used to produce the pictures in a validation + * report, and a Fourier transform is required before anything can be drawn from + * them — gemmi sf2map, or cif2mtz followed by + * CCP4's fft. Nothing in BioJava or Jmol will render them. + *

+ * They are supported because this is the route RCSB documents since + * edmaps.rcsb.org was shut down in October 2024, and because they + * are the authoritative archival form. For anything that needs to be displayed, + * prefer {@link PdbeCcp4MapProvider} or {@link VolumeServerProvider}. Accordingly + * this source is disabled by default in {@link DensityMapCache}. + *

+ * One useful property: these servers return the content MD5 as the HTTP + * ETag, so downloads from here are checksum-verified automatically. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public class WwpdbMapCoefficientsProvider extends AbstractDensityMapProvider { + + /** Default base URL, the wwPDB validation report archive. */ + public static final String DEFAULT_SERVER_URL = "https://files.wwpdb.org/pub/pdb/validation_reports/"; + + /** An RCSB mirror serving byte-identical files. */ + public static final String RCSB_MIRROR_URL = "https://files.rcsb.org/pub/pdb/validation_reports/"; + + /** An EBI mirror serving byte-identical files. */ + public static final String EBI_MIRROR_URL = "https://ftp.ebi.ac.uk/pub/databases/pdb/validation_reports/"; + + /** Default path template for the 2mFo-DFc coefficients. */ + public static final String DEFAULT_TWO_FO_FC_TEMPLATE = + "{mid}/{pdbid_lc}/{pdbid_lc}_validation_2fo-fc_map_coef.cif.gz"; + + /** Default path template for the mFo-DFc coefficients. */ + public static final String DEFAULT_FO_FC_TEMPLATE = + "{mid}/{pdbid_lc}/{pdbid_lc}_validation_fo-fc_map_coef.cif.gz"; + + private static String serverBaseUrl = DEFAULT_SERVER_URL; + private static String twoFoFcTemplate = DEFAULT_TWO_FO_FC_TEMPLATE; + private static String foFcTemplate = DEFAULT_FO_FC_TEMPLATE; + + /** + * @param cacheRoot the BioJava cache directory + */ + public WwpdbMapCoefficientsProvider(File cacheRoot) { + super(cacheRoot); + } + + /** @return the base URL of the validation report archive */ + public static String getServerBaseUrl() { + return serverBaseUrl; + } + + /** @param url the base URL; a trailing slash is added if missing */ + public static void setServerBaseUrl(String url) { + serverBaseUrl = url == null ? DEFAULT_SERVER_URL : (url.endsWith("/") ? url : url + "/"); + } + + /** + * Overrides the path template for a map kind. + * + * @param kind {@link DensityMapKind#TWO_FO_FC} or {@link DensityMapKind#FO_FC} + * @param template a template understood by {@link UrlTemplates} + */ + public static void setPathUrlTemplate(DensityMapKind kind, String template) { + if (kind == DensityMapKind.TWO_FO_FC) { + twoFoFcTemplate = template == null ? DEFAULT_TWO_FO_FC_TEMPLATE : template; + } else if (kind == DensityMapKind.FO_FC) { + foFcTemplate = template == null ? DEFAULT_FO_FC_TEMPLATE : template; + } else { + throw new IllegalArgumentException("Map coefficients exist only for 2Fo-Fc and Fo-Fc, not " + kind); + } + } + + /** Restores the default server and templates. */ + public static void resetToDefaults() { + serverBaseUrl = DEFAULT_SERVER_URL; + twoFoFcTemplate = DEFAULT_TWO_FO_FC_TEMPLATE; + foFcTemplate = DEFAULT_FO_FC_TEMPLATE; + } + + @Override + public DensityMapSource getSource() { + return DensityMapSource.WWPDB_MAP_COEFFICIENTS; + } + + @Override + public DensityFileFormat getFormat() { + return DensityFileFormat.MAP_COEFFICIENTS_CIF_GZ; + } + + @Override + public boolean supports(DensityMapKind kind) { + return kind == DensityMapKind.TWO_FO_FC || kind == DensityMapKind.FO_FC; + } + + /** + * Builds the URL for a set of coefficients without fetching them. + * + * @param pdbId the entry + * @param kind the kind of map + * @return the URL as a string + */ + public String buildUrl(PdbId pdbId, DensityMapKind kind) { + String template = kind == DensityMapKind.FO_FC ? foFcTemplate : twoFoFcTemplate; + return serverBaseUrl + UrlTemplates.expand(template, UrlTemplates.values(urlId(pdbId), null, -1)); + } + + @Override + public DensityMapResult fetch(DensityMapRequest request) throws IOException { + if (request.getPdbId() == null || !supports(request.getKind())) { + return null; + } + URL url = new URL(buildUrl(request.getPdbId(), request.getKind())); + File target = DensityCacheLayout.pdbMapFile(effectiveCacheRoot(request), request.getPdbId(), + request.getKind(), getSource(), getFormat(), null); + return obtain(request, url, target, request.getKind(), null, null, null); + } +} From 4f582384aa8ab17ac0b78862d1d30de4dffe6447 Mon Sep 17 00:00:00 2001 From: Amr ALHOSSARY Date: Sat, 15 Aug 2026 15:04:48 -0400 Subject: [PATCH 55/67] Display density maps in the Jmol panel Adds loadDensityMap and clearDensityMaps to JmolPanel, the single point every viewer in the module already goes through, so a map fetched by DensityMapCache can be contoured with one call. Surfaces are given stable ids so they can be addressed or removed individually, and the state that "Reset Display" restores is re-saved afterwards; otherwise the button would silently discard the map the user had just asked for. Three things had to be established by experiment against Jmol 14.31.10 rather than assumed, and each changed the implementation: * Option order in the isosurface command is load-bearing. With "mesh nofill" placed before the file name, Jmol accepts the command, reports no error, and draws nothing whatsoever. It has to follow the file name. * A negative sigma does not contour at a negative level. Jmol reserves negative sigma for its own internal signalling, so "sigma -3.0" silently contours at the default level instead: the intended red lobe of a difference map came out identical to the blue one. Difference maps are therefore drawn as a single signed surface, which also matches Jmol's own shortcut for them. * A density server response carries both the 2FO-FC and FO-FC blocks, and Jmol chooses between them by testing whether the file NAME contains "&diff=1". That marker normally arrives in the URL query string, which a cached local file does not have. Appending it to the file URL does not work, and neither does the "#diff=1" form the reader's own comment suggests: both were measured and both returned the 2FO-FC block or nothing. Embedding the marker in the file name does work, so the cache exposes the difference map under a companion name, hard-linked to the same bytes where the filesystem allows it. Verified by contouring the real cached files headlessly: the 2Fo-Fc map at 1 sigma gives cutoff 0.356 over a -1.31 to 3.78 range, and the difference map at 3 sigma gives 0.374 over -0.69 to 0.85 - a different data block, which is what proves the marker works rather than merely being accepted. A map that cannot be contoured without a Fourier transform is rejected with an explanatory exception rather than producing an empty surface. --- .../structure/align/gui/jmol/JmolPanel.java | 155 ++++++++++++++++++ .../io/density/DensityCacheLayout.java | 29 ++++ .../io/density/VolumeServerProvider.java | 33 +++- 3 files changed, 216 insertions(+), 1 deletion(-) diff --git a/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/jmol/JmolPanel.java b/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/jmol/JmolPanel.java index 23361e62ed..9108489d29 100644 --- a/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/jmol/JmolPanel.java +++ b/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/jmol/JmolPanel.java @@ -31,12 +31,14 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.BufferedInputStream; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.text.DecimalFormat; import java.util.List; +import java.util.Locale; import javax.swing.JComboBox; @@ -50,6 +52,8 @@ import org.biojava.nbio.structure.domain.pdp.Domain; import org.biojava.nbio.structure.domain.pdp.Segment; import org.biojava.nbio.structure.gui.util.color.ColorUtils; +import org.biojava.nbio.structure.io.density.DensityMapKind; +import org.biojava.nbio.structure.io.density.DensityMapResult; import org.biojava.nbio.structure.io.mmtf.MmtfActions; import org.biojava.nbio.structure.jama.Matrix; import org.biojava.nbio.structure.scop.ScopDatabase; @@ -166,6 +170,157 @@ public void setStructure(final Structure s) { setStructure(s, false); } + /** Isosurface id used for the 2mFo-DFc map, so that it can be addressed on its own. */ + public static final String ISOSURFACE_ID_2FOFC = "bj_density_2fofc"; + + /** Isosurface id used for the mFo-DFc difference map, drawn as a signed pair of lobes. */ + public static final String ISOSURFACE_ID_FOFC = "bj_density_fofc"; + + /** Isosurface id used for a cryo-EM map. */ + public static final String ISOSURFACE_ID_EM = "bj_density_em"; + + /** Default clipping radius, in Angstroms, around the selected atoms. */ + public static final double DEFAULT_WITHIN_RADIUS = 5.0; + + /** + * Displays a density map fetched through + * {@link org.biojava.nbio.structure.io.density.DensityMapCache}, clipped to + * {@value #DEFAULT_WITHIN_RADIUS} Angstroms around the whole model. + * + * @param map the map to display + * @throws IllegalArgumentException if the map cannot be displayed without a + * Fourier transform first + * @since 7.3.0 + */ + public void loadDensityMap(DensityMapResult map) { + loadDensityMap(map, "{*}", DEFAULT_WITHIN_RADIUS); + } + + /** + * Displays a density map, clipped to a distance around a selection. + *

+ * Contouring follows the convention for each kind of map: the 2mFo-DFc map at + * 1 sigma in blue, the mFo-DFc difference map as a signed red/green pair at 3 + * sigma, and a cryo-EM map at the level its depositors recommend when the entry + * states one, falling back to 3 sigma when it does not. + *

+ * Clipping matters for more than tidiness: contouring a whole cryo-EM grid at + * mesh resolution can take long enough to make the interface appear frozen. + * + * @param map the map to display + * @param atomSelection a Jmol atom expression such as {*} or + * {ligand}, or null to contour the whole cell + * @param withinRadius the clipping radius in Angstroms, ignored when + * atomSelection is null + * @throws IllegalArgumentException if the map cannot be displayed without a + * Fourier transform first + * @since 7.3.0 + */ + public void loadDensityMap(DensityMapResult map, String atomSelection, double withinRadius) { + if (!map.isRenderable()) { + throw new IllegalArgumentException("A " + map.getFormat() + " file holds structure factors, not a " + + "sampled map, and cannot be displayed as it stands. Convert it first, for example with " + + "'gemmi sf2map', or fetch the map from a source that serves a grid."); + } + + Double level = map.getRecommendedContourLevel(); + if (map.getKind() == DensityMapKind.EM && level != null) { + // EM maps are conventionally contoured at the absolute level the + // depositors chose rather than at a multiple of sigma. + loadDensityMap(map.getFile(), map.getKind(), level, false, atomSelection, withinRadius); + } else { + double sigma = map.getKind() == DensityMapKind.TWO_FO_FC ? 1.0 : 3.0; + loadDensityMap(map.getFile(), map.getKind(), sigma, true, atomSelection, withinRadius); + } + } + + /** + * Displays a density map file directly. + * + * @param mapFile the map file, in a format Jmol can contour (CCP4/MRC, or a + * BinaryCIF volume) + * @param kind what the map values mean, which decides the colouring and whether + * a signed pair of surfaces is drawn + * @param level the contour level + * @param levelIsSigma whether level is a multiple of the map's RMS + * deviation rather than an absolute value + * @param atomSelection a Jmol atom expression, or null to contour + * the whole cell + * @param withinRadius the clipping radius in Angstroms + * @since 7.3.0 + */ + public void loadDensityMap(File mapFile, DensityMapKind kind, double level, boolean levelIsSigma, + String atomSelection, double withinRadius) { + + String url = toJmolFileUrl(mapFile); + String within = atomSelection == null ? "" + : String.format(Locale.US, " within %.1f %s", withinRadius, atomSelection); + + if (kind == DensityMapKind.FO_FC) { + // One signed surface carrying both lobes, red for negative and green for + // positive. Drawing the negative lobe as a separate surface at "sigma -3" + // does not work: Jmol gives a negative sigma its own internal meaning and + // silently contours at the default level instead. + evalString(isosurfaceCommand(ISOSURFACE_ID_FOFC, "sign red green", level, levelIsSigma, within, url)); + } else if (kind == DensityMapKind.EM) { + evalString(isosurfaceCommand(ISOSURFACE_ID_EM, "color grey", level, levelIsSigma, within, url)); + } else { + evalString(isosurfaceCommand(ISOSURFACE_ID_2FOFC, "color blue", level, levelIsSigma, within, url)); + } + + // resetDisplay() restores "state_1", which is saved when the structure is + // loaded. Without re-saving here, pressing Reset Display would silently + // discard the map the user just asked for. + evalString("save STATE state_1"); + } + + /** + * Builds an isosurface command. + *

+ * The option order is not a matter of taste: mesh and + * nofill have to follow the file name. Placed before it, Jmol + * accepts the command without complaint and draws nothing at all. + */ + private static String isosurfaceCommand(String id, String colouring, double level, boolean levelIsSigma, + String within, String url) { + return String.format(Locale.US, + "isosurface ID \"%s\" delete; isosurface ID \"%s\" %s %s %.4f%s \"%s\" mesh nofill;", + id, id, colouring, levelIsSigma ? "sigma" : "cutoff", level, within, url); + } + + /** + * Removes any density surfaces this panel has drawn, leaving other isosurfaces + * alone. + * + * @since 7.3.0 + */ + public void clearDensityMaps() { + for (String id : new String[] {ISOSURFACE_ID_2FOFC, ISOSURFACE_ID_FOFC, ISOSURFACE_ID_EM}) { + evalString("isosurface ID \"" + id + "\" delete;"); + } + evalString("save STATE state_1"); + } + + /** + * Converts a file to the URL form Jmol expects. + *

+ * Going through {@link File#toURI()} percent-encodes spaces and removes + * backslashes, which Jmol would otherwise read as escape characters in a script + * string. On Windows the result is a single-slash file:/C:/..., + * which is normalised here to the usual three-slash form. + * + * @param file the file + * @return a URL string safe to embed in a Jmol script + * @since 7.3.0 + */ + public static String toJmolFileUrl(File file) { + String url = file.getAbsoluteFile().toURI().toString(); + if (url.startsWith("file:/") && !url.startsWith("file://")) { + url = "file:///" + url.substring("file:/".length()); + } + return url; + } + /** assign a custom color to the Jmol chains command. * */ diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityCacheLayout.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityCacheLayout.java index 476616f562..65691b627c 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityCacheLayout.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityCacheLayout.java @@ -178,6 +178,35 @@ public static File emdbMapInfoFile(File cacheRoot, String emdbId) { return new File(dir, canonical + ".map-info.json"); } + /** + * The companion name a density-server file must have for Jmol to read its + * difference-map block. + *

+ * A density server response holds both a 2FO-FC and an + * FO-FC data block, and Jmol's reader chooses between them by + * testing whether the file name contains the literal text + * &diff=1. That works for a URL fetched straight from the + * server, where the marker rides along in the query string, but a cached local + * file has no query string; appending the marker to the file URL only makes + * Jmol look for a file that does not exist. Putting the marker into the name + * itself is what actually selects the block. + *

+ * This was verified against Jmol 14.31.10 and is unchanged in current Jmol: the + * relevant line in BCifDensityReader still carries the author's + * "what about cached data" to-do beside it. Should Jmol gain a cleaner way to + * choose the block, this can be retired. + * + * @param mapFile the cached density-server file + * @return the sibling path that selects the difference map + */ + public static File differenceMarkerFile(File mapFile) { + String name = mapFile.getName(); + int dot = name.lastIndexOf('.'); + String stem = dot < 0 ? name : name.substring(0, dot); + String ext = dot < 0 ? "" : name.substring(dot); + return new File(mapFile.getAbsoluteFile().getParentFile(), stem + "&diff=1" + ext); + } + /** * The short four-character spelling of an identifier where one exists. *

diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/VolumeServerProvider.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/VolumeServerProvider.java index f170446f56..1700f3a472 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/VolumeServerProvider.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/VolumeServerProvider.java @@ -20,6 +20,8 @@ import java.io.File; import java.io.IOException; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; /** * Fetches downsampled volume slices from a Mol* density server. @@ -210,6 +212,35 @@ public DensityMapResult fetch(DensityMapRequest request) throws IOException { target = DensityCacheLayout.pdbMapFile(effectiveCacheRoot(request), request.getPdbId(), DensityCacheLayout.BOTH_KINDS_TOKEN, getSource(), getFormat(), qualifier); } - return obtain(request, url, target, request.getKind(), request.getEmdbId(), null, null); + DensityMapResult result = obtain(request, url, target, request.getKind(), request.getEmdbId(), null, null); + + if (request.getKind() == DensityMapKind.FO_FC) { + return presentAsDifferenceMap(result); + } + return result; + } + + /** + * Points the result at the companion file name that makes Jmol read the + * difference-map block. See + * {@link DensityCacheLayout#differenceMarkerFile(File)} for why the marker has + * to be in the name. + *

+ * The companion is a hard link where the filesystem allows one, so the second + * name costs no additional space; a copy is only made if linking is refused. + */ + private DensityMapResult presentAsDifferenceMap(DensityMapResult result) throws IOException { + File marker = DensityCacheLayout.differenceMarkerFile(result.getFile()); + if (!marker.isFile() || marker.length() != result.getFile().length()) { + Files.deleteIfExists(marker.toPath()); + try { + Files.createLink(marker.toPath(), result.getFile().toPath()); + } catch (IOException | UnsupportedOperationException e) { + Files.copy(result.getFile().toPath(), marker.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + } + return new DensityMapResult(marker, result.getSource(), result.getFormat(), result.getKind(), + result.getPdbId(), result.getEmdbId(), result.getSourceUrl(), result.isFromCache(), + result.getRecommendedContourLevel(), result.getSigma()); } } From e20069e6f0ec0fc5396d330ee65a03cb2940e195 Mon Sep 17 00:00:00 2001 From: Amr ALHOSSARY Date: Sat, 15 Aug 2026 15:07:57 -0400 Subject: [PATCH 56/67] Add a Show Electron Density menu item, listener and demo Puts the feature in reach from the alignment viewer's View menu. The fetch runs on a SwingWorker: even the smallest source is a few hundred kilobytes and a full-resolution map can be far larger, so fetching on the event dispatch thread would freeze the window for the duration. Requests are built with allowNonRenderableFormats(false), which keeps the map coefficient source out of the chain automatically rather than relying on the viewer to notice it cannot draw the result. When nothing is available the dialog explains why rather than listing HTTP codes: for an entry whose every source returns 404 the likely reason is that no structure factors were deposited and there is no associated EMDB map, which is worth saying plainly. The per-source detail is still shown underneath. AbstractAlignmentJmol gains getFrame() and setStatus() so a listener in the neighbouring package can own its dialogs and report progress; both were previously reachable only as protected fields. DemoShowElectronDensity displays 1CBS with both maps clipped around the bound retinoic acid. --- .../java/demo/DemoShowElectronDensity.java | 80 +++++++ .../nbio/structure/align/gui/MenuCreator.java | 20 ++ .../align/gui/MyShowDensityListener.java | 202 ++++++++++++++++++ .../align/gui/jmol/AbstractAlignmentJmol.java | 23 ++ 4 files changed, 325 insertions(+) create mode 100644 biojava-structure-gui/src/main/java/demo/DemoShowElectronDensity.java create mode 100644 biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/MyShowDensityListener.java diff --git a/biojava-structure-gui/src/main/java/demo/DemoShowElectronDensity.java b/biojava-structure-gui/src/main/java/demo/DemoShowElectronDensity.java new file mode 100644 index 0000000000..3948605744 --- /dev/null +++ b/biojava-structure-gui/src/main/java/demo/DemoShowElectronDensity.java @@ -0,0 +1,80 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package demo; + +import org.biojava.nbio.structure.PdbId; +import org.biojava.nbio.structure.Structure; +import org.biojava.nbio.structure.StructureIO; +import org.biojava.nbio.structure.align.gui.jmol.StructureAlignmentJmol; +import org.biojava.nbio.structure.io.density.DensityMapCache; +import org.biojava.nbio.structure.io.density.DensityMapKind; +import org.biojava.nbio.structure.io.density.DensityMapResult; + +/** + * Shows 1CBS with its electron density drawn around the bound retinoic acid. + *

+ * Both maps are displayed: the 2mFo-DFc map in blue at 1 sigma, which should hug + * the ligand closely, and the mFo-DFc difference map as a red and green pair at + * 3 sigma, which for a well refined structure should show very little. + *

+ * Once the window is up, the map can be manipulated from the Rasmol command box + * at the bottom, for instance + *

+ * isosurface ID "bj_density_2fofc" delete
+ * 
+ * to remove just the blue surface. Pressing Reset Display keeps the maps, since + * they are folded into the saved state when they are drawn. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public class DemoShowElectronDensity { + + /** + * @param args an optional PDB ID to display instead of the default + * @throws Exception if the structure or the map could not be fetched + */ + public static void main(String[] args) throws Exception { + String id = args.length > 0 ? args[0] : "1cbs"; + + Structure structure = StructureIO.getStructure(id); + StructureAlignmentJmol viewer = new StructureAlignmentJmol(); + viewer.setStructure(structure); + viewer.evalString("select all; cartoon on; color chain; " + + "select ligand; wireframe 0.16; spacefill 0.4; color cpk;"); + + DensityMapCache cache = new DensityMapCache(); + System.out.println("Density cache: " + cache.getCachePath()); + + // Clipping to the ligand keeps the surface readable and, for a large map, + // keeps the contouring quick enough not to stall the interface. + for (DensityMapKind kind : new DensityMapKind[] {DensityMapKind.TWO_FO_FC, DensityMapKind.FO_FC}) { + cache.findDensityMap(new PdbId(id), kind).ifPresent(map -> { + System.out.printf("%-8s from %-20s %s (%,d bytes)%n", + map.getKind(), map.getSource(), map.getFile().getName(), map.getFileSizeBytes()); + viewer.getJmolPanel().loadDensityMap(map, "{ligand}", 5.0); + }); + } + + DensityMapResult any = cache.findDensityMap(new PdbId(id), DensityMapKind.AUTO).orElse(null); + if (any == null) { + System.out.println("No density is available for " + id + + " - try an entry with deposited structure factors, such as 1cbs."); + } + } +} diff --git a/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/MenuCreator.java b/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/MenuCreator.java index 6a0e3c6e1a..197802a2c5 100644 --- a/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/MenuCreator.java +++ b/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/MenuCreator.java @@ -66,6 +66,8 @@ public class MenuCreator { public static final String PAIRWISE_ALIGN = "New Pairwise Alignment"; public static final String MULTIPLE_ALIGN = "New Multiple Alignment"; public static final String PHYLOGENETIC_TREE = "Phylogenetic Tree"; + /** @since 7.3.0 */ + public static final String SHOW_DENSITY = "Show Electron Density"; protected static final int keyMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); @@ -169,6 +171,8 @@ public static JMenuBar initJmolMenu(JFrame frame, distMax.setMnemonic(KeyEvent.VK_D); distMax.addActionListener(new MyDistMaxListener(parent)); view.add(distMax); + //Electron density + view.add(getShowDensityMenuItem(parent)); //Dot Plot - only if the alignment was an afpChain if (afpChain != null){ JMenuItem dotplot = new JMenuItem(DOT_PLOT); @@ -229,6 +233,22 @@ public static JMenuItem getOpenPDBMenuItem() { return openI; } + /** + * Menu item that fetches and displays the electron density or cryo-EM map for + * the structure currently on screen. + * + * @param parent the viewer to draw the map into + * @return the menu item + * @author Amr ALHOSSARY + * @since 7.3.0 + */ + public static JMenuItem getShowDensityMenuItem(AbstractAlignmentJmol parent) { + JMenuItem densityI = new JMenuItem(SHOW_DENSITY); + densityI.setMnemonic(KeyEvent.VK_E); + densityI.addActionListener(new MyShowDensityListener(parent)); + return densityI; + } + public static JMenuItem getLoadMenuItem() { diff --git a/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/MyShowDensityListener.java b/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/MyShowDensityListener.java new file mode 100644 index 0000000000..b1a9cf23ff --- /dev/null +++ b/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/MyShowDensityListener.java @@ -0,0 +1,202 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.align.gui; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +import javax.swing.JOptionPane; +import javax.swing.SwingWorker; + +import org.biojava.nbio.structure.PdbId; +import org.biojava.nbio.structure.Structure; +import org.biojava.nbio.structure.align.gui.jmol.AbstractAlignmentJmol; +import org.biojava.nbio.structure.io.density.DensityMapCache; +import org.biojava.nbio.structure.io.density.DensityMapKind; +import org.biojava.nbio.structure.io.density.DensityMapRequest; +import org.biojava.nbio.structure.io.density.DensityMapResult; +import org.biojava.nbio.structure.io.density.DensityMapSource; +import org.biojava.nbio.structure.io.density.NoDensityMapException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Fetches the density map for the structure on screen and draws it. + *

+ * The download happens on a {@link SwingWorker} rather than the event dispatch + * thread. Even the smallest source runs to a few hundred kilobytes and a + * full-resolution map can be far larger, so fetching inline would freeze the + * interface for as long as it took. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public class MyShowDensityListener implements ActionListener { + + private static final Logger logger = LoggerFactory.getLogger(MyShowDensityListener.class); + + private static final String OPTION_2FOFC = "2Fo-Fc (electron density)"; + private static final String OPTION_FOFC = "Fo-Fc (difference)"; + private static final String OPTION_BOTH = "Both"; + private static final String OPTION_AUTO = "Whatever is available"; + + private final AbstractAlignmentJmol parent; + + /** + * @param parent the viewer to draw into + */ + public MyShowDensityListener(AbstractAlignmentJmol parent) { + this.parent = parent; + } + + @Override + public void actionPerformed(ActionEvent e) { + Structure structure = parent == null ? null : parent.getStructure(); + if (structure == null) { + JOptionPane.showMessageDialog(parent == null ? null : parent.getFrame(), + "There is no structure on screen to fetch a density map for.", + "Show Electron Density", JOptionPane.INFORMATION_MESSAGE); + return; + } + + PdbId pdbId = structure.getPdbId(); + if (pdbId == null) { + String typed = JOptionPane.showInputDialog(parent.getFrame(), + "This structure has no PDB ID. Enter one to look up its density map:", + "Show Electron Density", JOptionPane.QUESTION_MESSAGE); + if (typed == null || typed.trim().isEmpty()) { + return; + } + try { + pdbId = new PdbId(typed.trim()); + } catch (IllegalArgumentException ex) { + JOptionPane.showMessageDialog(parent.getFrame(), typed + " is not a valid PDB ID.", + "Show Electron Density", JOptionPane.ERROR_MESSAGE); + return; + } + } + + Object[] options = {OPTION_2FOFC, OPTION_FOFC, OPTION_BOTH, OPTION_AUTO}; + Object choice = JOptionPane.showInputDialog(parent.getFrame(), + "Which map would you like to see for " + pdbId.getId() + "?", + "Show Electron Density", JOptionPane.QUESTION_MESSAGE, null, options, OPTION_2FOFC); + if (choice == null) { + return; + } + + List kinds = new ArrayList<>(2); + if (OPTION_BOTH.equals(choice)) { + kinds.add(DensityMapKind.TWO_FO_FC); + kinds.add(DensityMapKind.FO_FC); + } else if (OPTION_FOFC.equals(choice)) { + kinds.add(DensityMapKind.FO_FC); + } else if (OPTION_AUTO.equals(choice)) { + kinds.add(DensityMapKind.AUTO); + } else { + kinds.add(DensityMapKind.TWO_FO_FC); + } + + fetchAndShow(pdbId, kinds); + } + + private void fetchAndShow(PdbId pdbId, List kinds) { + parent.setStatus("Fetching density map for " + pdbId.getId() + " ..."); + + new SwingWorker, Void>() { + + private NoDensityMapException missing; + + @Override + protected List doInBackground() throws Exception { + DensityMapCache cache = DensityMapCache.getInstance(); + List results = new ArrayList<>(kinds.size()); + for (DensityMapKind kind : kinds) { + try { + // Restricting to displayable formats also excludes the map + // coefficient source automatically. + results.add(cache.getDensityMap(DensityMapRequest.builder(pdbId) + .kind(kind) + .allowNonRenderableFormats(false) + .build())); + } catch (NoDensityMapException ex) { + missing = ex; + } + } + return results; + } + + @Override + protected void done() { + List results; + try { + results = get(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + return; + } catch (ExecutionException ex) { + logger.error("Could not fetch a density map for {}", pdbId.getId(), ex.getCause()); + parent.setStatus("Could not fetch density map"); + JOptionPane.showMessageDialog(parent.getFrame(), + "Could not fetch the density map:\n" + ex.getCause().getMessage(), + "Show Electron Density", JOptionPane.ERROR_MESSAGE); + return; + } + + for (DensityMapResult result : results) { + parent.getJmolPanel().loadDensityMap(result); + } + + if (results.isEmpty()) { + parent.setStatus("No density map available"); + JOptionPane.showMessageDialog(parent.getFrame(), explain(pdbId, missing), + "Show Electron Density", JOptionPane.INFORMATION_MESSAGE); + } else { + DensityMapResult first = results.get(0); + parent.setStatus(String.format("Density from %s (%,d kB)", + first.getSource(), first.getFileSizeBytes() / 1024)); + } + } + }.execute(); + } + + /** + * Turns the per-source reasons into something a user can act on, rather than a + * list of HTTP codes. + */ + private static String explain(PdbId pdbId, NoDensityMapException missing) { + StringBuilder sb = new StringBuilder("No density map is available for ") + .append(pdbId.getId()).append(".\n\n"); + if (missing == null) { + return sb.toString(); + } + Map attempts = missing.getAttempts(); + boolean allNotFound = !attempts.isEmpty() && attempts.values().stream() + .allMatch(reason -> reason.startsWith("HTTP 404") || reason.startsWith("no ")); + if (allNotFound) { + sb.append("The most likely reason is that no structure factors were deposited\n") + .append("for this entry, and that it has no associated EMDB map.\n\n"); + } + sb.append("Sources tried:\n"); + attempts.forEach((source, reason) -> sb.append(" ").append(source).append(": ").append(reason).append('\n')); + return sb.toString(); + } +} diff --git a/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/jmol/AbstractAlignmentJmol.java b/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/jmol/AbstractAlignmentJmol.java index 535ad182fe..86be61d8fc 100644 --- a/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/jmol/AbstractAlignmentJmol.java +++ b/biojava-structure-gui/src/main/java/org/biojava/nbio/structure/align/gui/jmol/AbstractAlignmentJmol.java @@ -194,6 +194,29 @@ public Structure getStructure(){ */ public abstract List getDistanceMatrices(); + /** + * The window this viewer lives in, for use as the owner of dialogs raised from + * outside this package. + * + * @return the frame, which may be null before the window is built + * @since 7.3.0 + */ + public JFrame getFrame() { + return frame; + } + + /** + * Writes a short message into the viewer's status field. + * + * @param message the message; ignored if there is no status field yet + * @since 7.3.0 + */ + public void setStatus(String message) { + if (status != null) { + status.setText(message); + } + } + /** * Set the title of the AlignmentJmol window. * @param title From c8820ef168d9fd4124a1f7a25301f4f2db82d76f Mon Sep 17 00:00:00 2001 From: Amr ALHOSSARY Date: Sat, 15 Aug 2026 15:19:07 -0400 Subject: [PATCH 57/67] Test the density fetching, offline and against the live services Forty-three unit tests run with no network at all. The chain tests use stub providers, so the behaviour that matters can be pinned exactly: a 404 falls through to the next source, a too-large map falls through as well, and anything else aborts. That last one is the point of the design - reporting a dropped connection as "this entry has no density" would be worse than failing. The offline set also pins the two-character directory rule against both spellings of an entry (1cbs and pdb_00001cbs must land in "cb", not "db"), that every source and kind combination maps to a distinct file, and that a LOCAL_ONLY request is served entirely from disk with every server pointed at a dead port. Six integration tests exercise the real services for a couple of megabytes in total. The cryo-EM path is covered without downloading the 116 MB map: the EMDB entry is resolved, the author contour level checked against its known value, and the size guard then declines the full map before any of its body is transferred. The coefficient test corrupts a downloaded file afterwards to confirm the ETag-derived MD5 actually catches it rather than merely being recorded. Writing the header check turned up a real bug: isCcp4 rejected any file shorter than a CCP4 header, but a gzipped map compresses to a small fraction of the header it contains, so small EMDB maps would have been rejected as invalid. The length shortcut is gone; reading decides it. --- .../io/density/DensityMapIntegrationTest.java | 191 ++++++++++++ .../nbio/structure/io/density/Ccp4Header.java | 5 +- .../structure/io/density/TestCcp4Header.java | 112 +++++++ .../io/density/TestDensityCacheLayout.java | 145 +++++++++ .../io/density/TestDensityFallbackChain.java | 291 ++++++++++++++++++ .../density/TestDensityMapUrlTemplates.java | 147 +++++++++ .../io/density/TestEmdbAndLocalOnly.java | 221 +++++++++++++ .../io/density/emdb-map-EMD-0262.json | 1 + .../structure/io/density/emdb-search-6hu9.csv | 2 + .../structure/io/density/rcsb-entry-6hu9.json | 1 + 10 files changed, 1115 insertions(+), 1 deletion(-) create mode 100644 biojava-integrationtest/src/test/java/org/biojava/nbio/structure/test/io/density/DensityMapIntegrationTest.java create mode 100644 biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestCcp4Header.java create mode 100644 biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityCacheLayout.java create mode 100644 biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityFallbackChain.java create mode 100644 biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityMapUrlTemplates.java create mode 100644 biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestEmdbAndLocalOnly.java create mode 100644 biojava-structure/src/test/resources/org/biojava/nbio/structure/io/density/emdb-map-EMD-0262.json create mode 100644 biojava-structure/src/test/resources/org/biojava/nbio/structure/io/density/emdb-search-6hu9.csv create mode 100644 biojava-structure/src/test/resources/org/biojava/nbio/structure/io/density/rcsb-entry-6hu9.json diff --git a/biojava-integrationtest/src/test/java/org/biojava/nbio/structure/test/io/density/DensityMapIntegrationTest.java b/biojava-integrationtest/src/test/java/org/biojava/nbio/structure/test/io/density/DensityMapIntegrationTest.java new file mode 100644 index 0000000000..2b8cce095b --- /dev/null +++ b/biojava-integrationtest/src/test/java/org/biojava/nbio/structure/test/io/density/DensityMapIntegrationTest.java @@ -0,0 +1,191 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.test.io.density; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.List; + +import org.biojava.nbio.core.util.FileDownloadUtils; +import org.biojava.nbio.structure.PdbId; +import org.biojava.nbio.structure.io.density.Ccp4Header; +import org.biojava.nbio.structure.io.density.DensityFileFormat; +import org.biojava.nbio.structure.io.density.DensityMapCache; +import org.biojava.nbio.structure.io.density.DensityMapKind; +import org.biojava.nbio.structure.io.density.DensityMapRequest; +import org.biojava.nbio.structure.io.density.DensityMapResult; +import org.biojava.nbio.structure.io.density.DensityMapSource; +import org.biojava.nbio.structure.io.density.NoDensityMapException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Density fetching against the real services. + *

+ * Deliberately frugal: the entries chosen keep a full run to a couple of + * megabytes plus a few small metadata calls. In particular the cryo-EM path is + * exercised with the size limit set so low that the 116 MB map is declined + * before any of its body is transferred, which tests the whole resolution and + * guard sequence without the download. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public class DensityMapIntegrationTest { + + private File cacheRoot; + private DensityMapCache cache; + + @Before + public void setUp() throws IOException { + cacheRoot = Files.createTempDirectory("bj-density-it").toFile(); + cache = new DensityMapCache(cacheRoot.getAbsolutePath()); + } + + @After + public void tearDown() throws IOException { + FileDownloadUtils.deleteDirectory(cacheRoot.toPath()); + } + + /** The default path for an X-ray entry: the smallest source answers first. */ + @Test + public void fetchesAnXrayMapFromTheFirstSourceTried() throws IOException { + DensityMapResult result = cache.getDensityMap(new PdbId("1cbs"), DensityMapKind.TWO_FO_FC); + + assertEquals(DensityMapSource.RCSB_VOLUME_SERVER, result.getSource()); + assertEquals(DensityMapKind.TWO_FO_FC, result.getKind()); + assertTrue(result.isRenderable()); + assertFalse(result.isFromCache()); + assertTrue(result.getFileSizeBytes() > 1024); + assertTrue("a .meta sidecar makes the result reconstructible offline", + DensityMapResult.metaFileFor(result.getFile()).isFile()); + + // second call must come from the cache without another download + DensityMapResult again = cache.getDensityMap(new PdbId("1cbs"), DensityMapKind.TWO_FO_FC); + assertTrue(again.isFromCache()); + assertEquals(result.getFile(), again.getFile()); + } + + /** + * Both map kinds come out of one download, and the difference map is presented + * under the companion name that makes Jmol read the other data block. + */ + @Test + public void bothKindsShareASingleDownload() throws IOException { + DensityMapResult twoFoFc = cache.getDensityMap(new PdbId("1cbs"), DensityMapKind.TWO_FO_FC); + DensityMapResult foFc = cache.getDensityMap(new PdbId("1cbs"), DensityMapKind.FO_FC); + + assertEquals(DensityMapKind.FO_FC, foFc.getKind()); + assertFalse("the difference map needs its own file name", twoFoFc.getFile().equals(foFc.getFile())); + assertTrue("the marker has to be in the name for Jmol to select the FO-FC block", + foFc.getFile().getName().contains("&diff=1")); + assertEquals("both names must address the same bytes", + twoFoFc.getFileSizeBytes(), foFc.getFileSizeBytes()); + } + + /** PDBe serves real CCP4 files, which the header check should recognise. */ + @Test + public void pdbeServesAGenuineCcp4Map() throws IOException { + cache.setSourceChain(DensityMapKind.TWO_FO_FC, Arrays.asList(DensityMapSource.PDBE_CCP4)); + DensityMapResult result = cache.getDensityMap(new PdbId("1cbs"), DensityMapKind.TWO_FO_FC); + + assertEquals(DensityMapSource.PDBE_CCP4, result.getSource()); + assertEquals(DensityFileFormat.CCP4, result.getFormat()); + assertTrue("the CCP4 stamp should be present at byte 208", Ccp4Header.isCcp4(result.getFile())); + assertTrue(FileDownloadUtils.validateFile(result.getFile())); + } + + /** + * The whole cryo-EM route: resolve the EMDB entry, pick up the author contour + * level, and decline the full map on size without transferring it. + */ + @Test + public void resolvesCryoEmEntriesAndHonoursTheSizeLimit() throws IOException { + List emdbIds = cache.getEmdbResolver().getEmdbIds(new PdbId("6hu9")); + assertEquals(Arrays.asList("EMD-0262"), emdbIds); + + DensityMapResult result = cache.getDensityMap(new PdbId("6hu9"), DensityMapKind.AUTO); + assertEquals(DensityMapKind.EM, result.getKind()); + assertEquals("EMD-0262", result.getEmdbId()); + assertNotNull("EM maps need the author contour level to be displayed properly", + result.getRecommendedContourLevel()); + assertEquals(0.0263, result.getRecommendedContourLevel(), 1e-6); + assertNotNull(result.getContourInSigma()); + + // With only the full archive enabled and a tiny ceiling, the guard must fire + // rather than pulling down 116 MB. + DensityMapCache strict = new DensityMapCache(cacheRoot.getAbsolutePath()); + strict.setSourceChain(DensityMapKind.EM, Arrays.asList(DensityMapSource.EMDB_MAP)); + strict.setMaxDownloadBytes(1024); + try { + strict.getDensityMap(DensityMapRequest.builder(new PdbId("6hu9")).kind(DensityMapKind.EM).build()); + fail("the size guard should have declined the full EMDB map"); + } catch (NoDensityMapException e) { + assertTrue(e.getAttempts().get(DensityMapSource.EMDB_MAP).contains("too large")); + } + } + + /** 4HHB was deposited in 1984 without structure factors, so nothing has a map for it. */ + @Test + public void reportsWhyAnEntryHasNoDensity() throws IOException { + cache.setSourceEnabled(DensityMapSource.WWPDB_MAP_COEFFICIENTS, true); + try { + cache.getDensityMap(new PdbId("4hhb"), DensityMapKind.AUTO); + fail("4hhb has no deposited structure factors"); + } catch (NoDensityMapException e) { + assertFalse(e.getAttempts().isEmpty()); + assertTrue(e.getAttempts().values().stream().anyMatch(r -> r.contains("404"))); + } + } + + /** + * The wwPDB servers return the content MD5 as the ETag, so a coefficient + * download is checksum-verified without a separate hash file. + */ + @Test + public void mapCoefficientsArriveWithAVerifiableChecksum() throws IOException { + cache.setSourceEnabled(DensityMapSource.WWPDB_MAP_COEFFICIENTS, true); + cache.setSourceChain(DensityMapKind.TWO_FO_FC, Arrays.asList(DensityMapSource.WWPDB_MAP_COEFFICIENTS)); + + DensityMapResult result = cache.getDensityMap(DensityMapRequest.builder(new PdbId("1cbs")) + .kind(DensityMapKind.TWO_FO_FC) + .allowNonRenderableFormats(true) + .build()); + + assertEquals(DensityMapSource.WWPDB_MAP_COEFFICIENTS, result.getSource()); + assertFalse("structure factors are not a map and must not claim to be renderable", + result.isRenderable()); + + File hashFile = new File(result.getFile().getParentFile(), result.getFile().getName() + ".hash_MD5"); + assertTrue("an MD5 should have been recorded from the ETag", hashFile.isFile()); + assertTrue(FileDownloadUtils.validateFile(result.getFile())); + + // corrupt it and confirm the checksum actually catches it + Files.write(result.getFile().toPath(), new byte[] {0, 1, 2, 3}); + assertFalse(FileDownloadUtils.validateFile(result.getFile())); + } +} diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/Ccp4Header.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/Ccp4Header.java index de073d0c01..594ad91927 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/Ccp4Header.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/Ccp4Header.java @@ -68,9 +68,12 @@ private Ccp4Header() { * @throws IOException if the file could not be read */ public static boolean isCcp4(File file) throws IOException { - if (file == null || !file.isFile() || file.length() < HEADER_BYTES) { + if (file == null || !file.isFile()) { return false; } + // Deliberately no shortcut on file.length(): a gzipped map compresses to far + // less than the size of the header it contains, so a length test here would + // reject perfectly good small maps. Reading decides it instead. try (InputStream in = openPossiblyGzipped(file)) { return isCcp4(in); } diff --git a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestCcp4Header.java b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestCcp4Header.java new file mode 100644 index 0000000000..97707248ed --- /dev/null +++ b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestCcp4Header.java @@ -0,0 +1,112 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.io.density; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.zip.GZIPOutputStream; + +import org.biojava.nbio.core.util.FileDownloadUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * The CCP4 header check that keeps a server's error page out of the cache. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public class TestCcp4Header { + + private File dir; + + @Before + public void setUp() throws IOException { + dir = Files.createTempDirectory("bj-ccp4").toFile(); + } + + @After + public void tearDown() throws IOException { + FileDownloadUtils.deleteDirectory(dir.toPath()); + } + + /** A minimal file carrying the stamp at the offset a real CCP4 header uses. */ + private static byte[] fakeMap() { + byte[] bytes = new byte[2048]; + byte[] stamp = Ccp4Header.MAP_STAMP.getBytes(StandardCharsets.US_ASCII); + System.arraycopy(stamp, 0, bytes, Ccp4Header.MAP_STAMP_OFFSET, stamp.length); + return bytes; + } + + private File write(String name, byte[] content) throws IOException { + File f = new File(dir, name); + Files.write(f.toPath(), content); + return f; + } + + @Test + public void recognisesAMapByItsStamp() throws IOException { + assertTrue(Ccp4Header.isCcp4(write("good.ccp4", fakeMap()))); + } + + @Test + public void recognisesAGzippedMap() throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + try (GZIPOutputStream gz = new GZIPOutputStream(buffer)) { + gz.write(fakeMap()); + } + assertTrue("EMDB serves its maps gzipped", Ccp4Header.isCcp4(write("good.map.gz", buffer.toByteArray()))); + } + + /** + * The case this check exists for: a server answering with an error page and an + * HTTP 200, which nothing else would catch. + */ + @Test + public void rejectsAnHtmlErrorPage() throws IOException { + StringBuilder html = new StringBuilder("404 Not Found"); + while (html.length() < 1500) { + html.append("

The requested resource was not found on this server.

"); + } + html.append(""); + assertFalse(Ccp4Header.isCcp4(write("error.ccp4", html.toString().getBytes(StandardCharsets.UTF_8)))); + } + + @Test + public void rejectsRandomBytesAndShortFiles() throws IOException { + byte[] noise = new byte[2048]; + for (int i = 0; i < noise.length; i++) { + noise[i] = (byte) (i * 31); + } + assertFalse(Ccp4Header.isCcp4(write("noise.ccp4", noise))); + assertFalse("a file shorter than the header cannot be a map", + Ccp4Header.isCcp4(write("tiny.ccp4", new byte[10]))); + } + + @Test + public void quietVariantSwallowsUnreadableFiles() { + assertFalse(Ccp4Header.isCcp4Quietly(new File(dir, "does-not-exist.ccp4"))); + } +} diff --git a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityCacheLayout.java b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityCacheLayout.java new file mode 100644 index 0000000000..4477af693f --- /dev/null +++ b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityCacheLayout.java @@ -0,0 +1,145 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.io.density; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; + +import org.biojava.nbio.structure.PdbId; +import org.biojava.nbio.structure.io.LocalPDBDirectory; +import org.junit.Test; + +/** + * Cache layout: directory derivation, and that no two source and kind + * combinations can land on the same file. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public class TestDensityCacheLayout { + + private static final File ROOT = new File("/tmp/bjcache"); + + /** + * The divided-archive directory is taken from the end of the identifier, not + * the start. That is what makes both spellings of an entry agree: counting from + * the start would file the extended form under "db" instead of "cb". + */ + @Test + public void middleHashIsTakenFromTheEndOfTheIdentifier() { + assertEquals("cb", LocalPDBDirectory.getMiddleHash("1cbs")); + assertEquals("cb", LocalPDBDirectory.getMiddleHash("1CBS")); + assertEquals("cb", LocalPDBDirectory.getMiddleHash("pdb_00001cbs")); + assertEquals("cb", LocalPDBDirectory.getMiddleHash("PDB_00001CBS")); + + // Guard against a regression to the substring(1, 3) form. + assertFalse("db".equals(LocalPDBDirectory.getMiddleHash("pdb_00001cbs"))); + } + + @Test + public void bothSpellingsOfAnEntryShareADirectory() { + File shortForm = DensityCacheLayout.pdbMapFile(ROOT, new PdbId("1cbs"), + DensityMapKind.TWO_FO_FC, DensityMapSource.PDBE_CCP4, DensityFileFormat.CCP4, null); + File extendedForm = DensityCacheLayout.pdbMapFile(ROOT, new PdbId("PDB_00001CBS"), + DensityMapKind.TWO_FO_FC, DensityMapSource.PDBE_CCP4, DensityFileFormat.CCP4, null); + assertEquals(shortForm, extendedForm); + } + + @Test + public void pdbMapFileNamesAreLowerCaseAndDivided() { + File f = DensityCacheLayout.pdbMapFile(ROOT, new PdbId("1CBS"), + DensityMapKind.TWO_FO_FC, DensityMapSource.PDBE_CCP4, DensityFileFormat.CCP4, null); + assertEquals("1cbs_2fofc_pdbe.ccp4", f.getName()); + assertEquals("cb", f.getParentFile().getName()); + assertEquals(DensityCacheLayout.DENSITY_DIR, f.getParentFile().getParentFile().getName()); + } + + @Test + public void qualifierDistinguishesDetailLevels() { + File d0 = DensityCacheLayout.pdbMapFile(ROOT, new PdbId("1cbs"), DensityMapKind.TWO_FO_FC, + DensityMapSource.RCSB_VOLUME_SERVER, DensityFileFormat.BCIF_VOLUME, "d0"); + File d3 = DensityCacheLayout.pdbMapFile(ROOT, new PdbId("1cbs"), DensityMapKind.TWO_FO_FC, + DensityMapSource.RCSB_VOLUME_SERVER, DensityFileFormat.BCIF_VOLUME, "d3"); + assertFalse(d0.equals(d3)); + assertTrue(d0.getName().endsWith("_d0.bcif")); + } + + /** + * Every combination has to map to a distinct file, or one source would serve a + * cache hit belonging to another. + */ + @Test + public void everySourceAndKindCombinationIsDistinct() { + Set seen = new HashSet<>(); + PdbId id = new PdbId("1cbs"); + for (DensityMapSource source : DensityMapSource.values()) { + for (DensityMapKind kind : DensityMapKind.values()) { + if (kind == DensityMapKind.AUTO || kind == DensityMapKind.EM) { + continue; // AUTO is never cached; EM is keyed by EMDB id instead + } + for (DensityFileFormat format : DensityFileFormat.values()) { + File f = DensityCacheLayout.pdbMapFile(ROOT, id, kind, source, format, null); + assertTrue("duplicate cache path: " + f, seen.add(f.getPath())); + } + } + } + } + + @Test + public void emdbMapsAreKeyedByEmdbIdentifier() { + File f = DensityCacheLayout.emdbMapFile(ROOT, "emd-262", DensityMapSource.EMDB_MAP, + DensityFileFormat.CCP4_GZ, null); + assertEquals("emd_262.map.gz", f.getName()); + assertEquals("EMD-262", f.getParentFile().getName()); + assertEquals(DensityCacheLayout.EMDB_DIR, f.getParentFile().getParentFile().getName()); + } + + @Test + public void emdbIdentifiersAreNormalised() { + assertEquals("EMD-0262", DensityMapRequest.normalizeEmdbId("EMD-0262")); + assertEquals("EMD-0262", DensityMapRequest.normalizeEmdbId("emd-0262")); + assertEquals("EMD-0262", DensityMapRequest.normalizeEmdbId("EMD_0262")); + assertEquals("EMD-0262", DensityMapRequest.normalizeEmdbId("0262")); + assertEquals("0262", DensityMapRequest.emdbNumber("EMD-0262")); + } + + /** + * The marker has to sit inside the file name, before the extension. Jmol reads + * the difference block only when it finds that text in the name itself. + */ + @Test + public void differenceMarkerGoesInsideTheName() { + File plain = new File("/tmp/bjcache/density/cb/1cbs_both_rcsbvs_d3.bcif"); + File marked = DensityCacheLayout.differenceMarkerFile(plain); + assertEquals("1cbs_both_rcsbvs_d3&diff=1.bcif", marked.getName()); + assertEquals(plain.getAbsoluteFile().getParentFile(), marked.getParentFile()); + } + + @Test + public void emdbMappingFileIsDivided() { + File f = DensityCacheLayout.emdbMappingFile(ROOT, new PdbId("6hu9")); + assertEquals("6hu9.emdb.properties", f.getName()); + assertEquals("hu", f.getParentFile().getName()); + assertEquals(DensityCacheLayout.EMDB_MAPPING_DIR, f.getParentFile().getParentFile().getName()); + } +} diff --git a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityFallbackChain.java b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityFallbackChain.java new file mode 100644 index 0000000000..55f02f1131 --- /dev/null +++ b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityFallbackChain.java @@ -0,0 +1,291 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.io.density; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.File; +import java.io.IOException; +import java.net.SocketTimeoutException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.biojava.nbio.core.util.HttpStatusException; +import org.biojava.nbio.structure.PdbId; +import org.junit.Before; +import org.junit.Test; + +/** + * The fallback chain, exercised with stub providers so that no server is + * contacted. + *

+ * The behaviour that matters most here is the difference between "this source + * has nothing for the entry" and "this source could not be reached". The first + * must move on to the next source; the second must abort, because reporting a + * network outage as "no density exists" would be actively misleading. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public class TestDensityFallbackChain { + + private DensityMapCache cache; + private List called; + + @Before + public void setUp() { + cache = new DensityMapCache(System.getProperty("java.io.tmpdir")); + called = new ArrayList<>(); + } + + /** A provider that behaves however the test needs it to. */ + private class StubProvider implements DensityMapProvider { + + private final DensityMapSource source; + private final DensityFileFormat format; + private final IOException failure; + private final boolean returnsNull; + + StubProvider(DensityMapSource source, DensityFileFormat format, IOException failure, boolean returnsNull) { + this.source = source; + this.format = format; + this.failure = failure; + this.returnsNull = returnsNull; + } + + @Override + public DensityMapSource getSource() { + return source; + } + + @Override + public DensityFileFormat getFormat() { + return format; + } + + @Override + public boolean supports(DensityMapKind kind) { + return kind != DensityMapKind.AUTO; + } + + @Override + public DensityMapResult fetch(DensityMapRequest request) throws IOException { + called.add(source); + if (failure != null) { + throw failure; + } + if (returnsNull) { + return null; + } + return new DensityMapResult(new File("stub.map"), source, format, request.getKind(), + request.getPdbId(), request.getEmdbId(), "stub://" + source, false, null, null); + } + } + + private StubProvider missing(DensityMapSource source) { + return new StubProvider(source, DensityFileFormat.CCP4, + new HttpStatusException(404, "stub://" + source, "Not Found"), false); + } + + private StubProvider succeeds(DensityMapSource source) { + return new StubProvider(source, DensityFileFormat.CCP4, null, false); + } + + private void useOnly(DensityMapSource... sources) { + cache.setSourceChain(DensityMapKind.TWO_FO_FC, Arrays.asList(sources)); + for (DensityMapSource s : DensityMapSource.values()) { + cache.setSourceEnabled(s, Arrays.asList(sources).contains(s)); + } + } + + @Test + public void aMissingSourceFallsThroughToTheNext() throws IOException { + cache.registerProvider(missing(DensityMapSource.RCSB_VOLUME_SERVER)); + cache.registerProvider(succeeds(DensityMapSource.PDBE_CCP4)); + useOnly(DensityMapSource.RCSB_VOLUME_SERVER, DensityMapSource.PDBE_CCP4); + + DensityMapResult result = cache.getDensityMap(new PdbId("1cbs"), DensityMapKind.TWO_FO_FC); + + assertEquals(DensityMapSource.PDBE_CCP4, result.getSource()); + assertEquals(Arrays.asList(DensityMapSource.RCSB_VOLUME_SERVER, DensityMapSource.PDBE_CCP4), called); + } + + @Test + public void returningNullAlsoFallsThrough() throws IOException { + cache.registerProvider(new StubProvider(DensityMapSource.RCSB_VOLUME_SERVER, + DensityFileFormat.CCP4, null, true)); + cache.registerProvider(succeeds(DensityMapSource.PDBE_CCP4)); + useOnly(DensityMapSource.RCSB_VOLUME_SERVER, DensityMapSource.PDBE_CCP4); + + assertEquals(DensityMapSource.PDBE_CCP4, + cache.getDensityMap(new PdbId("1cbs"), DensityMapKind.TWO_FO_FC).getSource()); + } + + @Test + public void aTooLargeMapFallsThroughToASmallerSource() throws IOException { + cache.registerProvider(new StubProvider(DensityMapSource.EMDB_MAP, DensityFileFormat.CCP4_GZ, + new DensityMapTooLargeException("stub://big", 111426503L, 1024L), false)); + cache.registerProvider(succeeds(DensityMapSource.RCSB_VOLUME_SERVER)); + useOnly(DensityMapSource.EMDB_MAP, DensityMapSource.RCSB_VOLUME_SERVER); + + assertEquals(DensityMapSource.RCSB_VOLUME_SERVER, + cache.getDensityMap(new PdbId("1cbs"), DensityMapKind.TWO_FO_FC).getSource()); + } + + /** + * A dropped connection is not evidence that the entry has no density, so the + * chain must stop rather than quietly try the rest and report "none available". + */ + @Test + public void aTransportFailureAbortsTheChain() { + cache.registerProvider(new StubProvider(DensityMapSource.RCSB_VOLUME_SERVER, DensityFileFormat.CCP4, + new SocketTimeoutException("connection timed out"), false)); + cache.registerProvider(succeeds(DensityMapSource.PDBE_CCP4)); + useOnly(DensityMapSource.RCSB_VOLUME_SERVER, DensityMapSource.PDBE_CCP4); + + try { + cache.getDensityMap(new PdbId("1cbs"), DensityMapKind.TWO_FO_FC); + fail("a transport failure should not be reported as a missing map"); + } catch (NoDensityMapException e) { + fail("a transport failure must not be reported as NoDensityMapException"); + } catch (IOException expected) { + assertEquals(Arrays.asList(DensityMapSource.RCSB_VOLUME_SERVER), called); + } + } + + /** A 5xx is a server problem, not an absent entry, so it must abort too. */ + @Test + public void aServerErrorAbortsTheChain() { + cache.registerProvider(new StubProvider(DensityMapSource.RCSB_VOLUME_SERVER, DensityFileFormat.CCP4, + new HttpStatusException(503, "stub://x", "Service Unavailable"), false)); + cache.registerProvider(succeeds(DensityMapSource.PDBE_CCP4)); + useOnly(DensityMapSource.RCSB_VOLUME_SERVER, DensityMapSource.PDBE_CCP4); + + try { + cache.getDensityMap(new PdbId("1cbs"), DensityMapKind.TWO_FO_FC); + fail("HTTP 503 should abort the chain"); + } catch (IOException expected) { + assertTrue(expected instanceof HttpStatusException); + assertEquals(503, ((HttpStatusException) expected).getStatusCode()); + } + } + + @Test + public void exhaustingEverySourceReportsWhatWasTried() { + cache.registerProvider(missing(DensityMapSource.RCSB_VOLUME_SERVER)); + cache.registerProvider(missing(DensityMapSource.PDBE_CCP4)); + useOnly(DensityMapSource.RCSB_VOLUME_SERVER, DensityMapSource.PDBE_CCP4); + + try { + cache.getDensityMap(new PdbId("4hhb"), DensityMapKind.TWO_FO_FC); + fail("expected NoDensityMapException"); + } catch (NoDensityMapException e) { + assertEquals(2, e.getAttempts().size()); + assertTrue(e.getAttempts().get(DensityMapSource.RCSB_VOLUME_SERVER).contains("404")); + assertTrue(e.getMessage().contains("4HHB") || e.getMessage().contains("4hhb")); + } catch (IOException e) { + fail("expected NoDensityMapException but got " + e); + } + } + + @Test + public void aDisabledSourceIsNotCalled() { + cache.registerProvider(succeeds(DensityMapSource.PDBE_CCP4)); + useOnly(DensityMapSource.PDBE_CCP4); + cache.setSourceEnabled(DensityMapSource.PDBE_CCP4, false); + + try { + cache.getDensityMap(new PdbId("1cbs"), DensityMapKind.TWO_FO_FC); + fail("expected NoDensityMapException"); + } catch (NoDensityMapException e) { + assertTrue(called.isEmpty()); + assertEquals("disabled", e.getAttempts().get(DensityMapSource.PDBE_CCP4)); + } catch (IOException e) { + fail("expected NoDensityMapException but got " + e); + } + } + + /** Map coefficients are archival only, so they are off unless asked for. */ + @Test + public void mapCoefficientsAreDisabledByDefault() { + assertFalse(new DensityMapCache(System.getProperty("java.io.tmpdir")) + .isSourceEnabled(DensityMapSource.WWPDB_MAP_COEFFICIENTS)); + } + + /** + * A viewer asks for renderable formats only; that must exclude the coefficients + * even when the source is enabled. + */ + @Test + public void unrenderableFormatsAreSkippedWhenTheCallerCannotUseThem() { + cache.registerProvider(new StubProvider(DensityMapSource.WWPDB_MAP_COEFFICIENTS, + DensityFileFormat.MAP_COEFFICIENTS_CIF_GZ, null, false)); + useOnly(DensityMapSource.WWPDB_MAP_COEFFICIENTS); + + try { + cache.getDensityMap(DensityMapRequest.builder(new PdbId("1cbs")) + .kind(DensityMapKind.TWO_FO_FC) + .allowNonRenderableFormats(false) + .build()); + fail("expected NoDensityMapException"); + } catch (NoDensityMapException e) { + assertTrue(called.isEmpty()); + assertTrue(e.getAttempts().get(DensityMapSource.WWPDB_MAP_COEFFICIENTS).contains("Fourier")); + } catch (IOException e) { + fail("expected NoDensityMapException but got " + e); + } + } + + @Test + public void coefficientsAreUsedWhenTheCallerAllowsThem() throws IOException { + cache.registerProvider(new StubProvider(DensityMapSource.WWPDB_MAP_COEFFICIENTS, + DensityFileFormat.MAP_COEFFICIENTS_CIF_GZ, null, false)); + useOnly(DensityMapSource.WWPDB_MAP_COEFFICIENTS); + + DensityMapResult result = cache.getDensityMap(DensityMapRequest.builder(new PdbId("1cbs")) + .kind(DensityMapKind.TWO_FO_FC) + .allowNonRenderableFormats(true) + .build()); + assertFalse(result.isRenderable()); + } + + /** AUTO tries the X-ray map first, then falls through to the EM one. */ + @Test + public void autoFallsFromXrayToEm() { + assertEquals(Arrays.asList(DensityMapKind.TWO_FO_FC, DensityMapKind.EM), + DensityMapKind.AUTO.resolve()); + assertEquals(Arrays.asList(DensityMapKind.FO_FC), DensityMapKind.FO_FC.resolve()); + } + + /** X-ray and EM entries are served by deliberately different orders. */ + @Test + public void defaultChainsPreferTheSmallestSource() { + DensityMapCache fresh = new DensityMapCache(System.getProperty("java.io.tmpdir")); + assertEquals(DensityMapSource.RCSB_VOLUME_SERVER, + fresh.getSourceChain(DensityMapKind.TWO_FO_FC).get(0)); + assertEquals(DensityMapSource.RCSB_VOLUME_SERVER, + fresh.getSourceChain(DensityMapKind.EM).get(0)); + // the full-resolution archive is the last resort for EM + List em = fresh.getSourceChain(DensityMapKind.EM); + assertEquals(DensityMapSource.EMDB_MAP, em.get(em.size() - 1)); + } +} diff --git a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityMapUrlTemplates.java b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityMapUrlTemplates.java new file mode 100644 index 0000000000..db5e666935 --- /dev/null +++ b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityMapUrlTemplates.java @@ -0,0 +1,147 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.io.density; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.File; + +import org.biojava.nbio.structure.PdbId; +import org.junit.After; +import org.junit.Test; + +/** + * URL construction for each provider. These run offline: only the strings are + * built, nothing is fetched. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public class TestDensityMapUrlTemplates { + + private static final File ROOT = new File("/tmp/bjcache"); + + @After + public void restoreDefaults() { + PdbeCcp4MapProvider.resetToDefaults(); + WwpdbMapCoefficientsProvider.resetToDefaults(); + EmdbMapProvider.resetToDefaults(); + } + + /** + * PDBe answers only to the lower-case four-character spelling; an upper-case or + * extended one returns HTTP 404. + */ + @Test + public void pdbeUrlsAreLowerCase() { + PdbeCcp4MapProvider p = new PdbeCcp4MapProvider(ROOT); + assertEquals("https://www.ebi.ac.uk/pdbe/coordinates/files/1cbs.ccp4", + p.buildUrl(new PdbId("1CBS"), DensityMapKind.TWO_FO_FC)); + assertEquals("https://www.ebi.ac.uk/pdbe/coordinates/files/1cbs_diff.ccp4", + p.buildUrl(new PdbId("1cbs"), DensityMapKind.FO_FC)); + } + + @Test + public void pdbeServerIsConfigurable() { + PdbeCcp4MapProvider.setServerBaseUrl("http://localhost:1/maps"); + PdbeCcp4MapProvider p = new PdbeCcp4MapProvider(ROOT); + assertEquals("http://localhost:1/maps/1cbs.ccp4", + p.buildUrl(new PdbId("1cbs"), DensityMapKind.TWO_FO_FC)); + } + + /** The validation archive is divided by the same two characters as the rest of the PDB. */ + @Test + public void wwpdbUrlsUseTheDividedLayout() { + WwpdbMapCoefficientsProvider p = new WwpdbMapCoefficientsProvider(ROOT); + assertEquals("https://files.wwpdb.org/pub/pdb/validation_reports/" + + "cb/1cbs/1cbs_validation_2fo-fc_map_coef.cif.gz", + p.buildUrl(new PdbId("1cbs"), DensityMapKind.TWO_FO_FC)); + assertEquals("https://files.wwpdb.org/pub/pdb/validation_reports/" + + "cb/1cbs/1cbs_validation_fo-fc_map_coef.cif.gz", + p.buildUrl(new PdbId("1cbs"), DensityMapKind.FO_FC)); + } + + @Test + public void volumeServerUrlsCarryDetailAndEncoding() { + VolumeServerProvider rcsb = new VolumeServerProvider(ROOT, VolumeServerProvider.Host.RCSB); + rcsb.setDetail(0); + String url = rcsb.buildUrl(DensityMapRequest.builder(new PdbId("1cbs")) + .kind(DensityMapKind.TWO_FO_FC).build()); + assertEquals("https://maps.rcsb.org/x-ray/1cbs/cell?detail=0&encoding=bcif", url); + + VolumeServerProvider pdbe = new VolumeServerProvider(ROOT, VolumeServerProvider.Host.PDBE); + pdbe.setDetail(6); + assertEquals("https://www.ebi.ac.uk/pdbe/volume-server/x-ray/1cbs/cell?detail=6&encoding=bcif", + pdbe.buildUrl(DensityMapRequest.builder(new PdbId("1cbs")) + .kind(DensityMapKind.TWO_FO_FC).build())); + } + + @Test + public void volumeServerEmUrlUsesTheEmdbNumber() { + VolumeServerProvider rcsb = new VolumeServerProvider(ROOT, VolumeServerProvider.Host.RCSB); + rcsb.setDetail(3); + String url = rcsb.buildUrl(DensityMapRequest.builder("EMD-0262").build()); + assertEquals("https://maps.rcsb.org/em/emd-0262/cell?detail=3&encoding=bcif", url); + } + + @Test + public void encodingSelectsTheFormat() { + VolumeServerProvider p = new VolumeServerProvider(ROOT, VolumeServerProvider.Host.RCSB); + assertEquals(DensityFileFormat.BCIF_VOLUME, p.getFormat()); + p.setEncoding("cif"); + assertEquals(DensityFileFormat.CIF_VOLUME, p.getFormat()); + assertTrue(p.buildUrl(DensityMapRequest.builder(new PdbId("1cbs")) + .kind(DensityMapKind.TWO_FO_FC).build()).endsWith("encoding=cif")); + } + + @Test + public void emdbMapUrl() { + EmdbMapProvider p = new EmdbMapProvider(ROOT, null); + assertEquals("https://ftp.ebi.ac.uk/pub/databases/emdb/structures/EMD-0262/map/emd_0262.map.gz", + p.buildUrl("EMD-0262")); + assertEquals("https://ftp.ebi.ac.uk/pub/databases/emdb/structures/EMD-0262/map/emd_0262.map.gz", + p.buildUrl("emd_0262")); + } + + /** An unknown placeholder must survive verbatim, so a bad template is obvious. */ + @Test + public void unknownPlaceholdersAreLeftAlone() { + assertEquals("a/{nosuch}/b", + UrlTemplates.expand("a/{nosuch}/b", UrlTemplates.values("1cbs", null, -1))); + } + + @Test + public void providersDeclareWhatTheyCanServe() { + assertTrue(new PdbeCcp4MapProvider(ROOT).supports(DensityMapKind.TWO_FO_FC)); + assertTrue(!new PdbeCcp4MapProvider(ROOT).supports(DensityMapKind.EM)); + assertTrue(new EmdbMapProvider(ROOT, null).supports(DensityMapKind.EM)); + assertTrue(!new EmdbMapProvider(ROOT, null).supports(DensityMapKind.TWO_FO_FC)); + assertTrue(new VolumeServerProvider(ROOT, VolumeServerProvider.Host.RCSB).supports(DensityMapKind.EM)); + } + + /** Only the coefficients are unrenderable; every sampled grid format is fine. */ + @Test + public void onlyCoefficientsAreUnrenderable() { + assertTrue(!DensityFileFormat.MAP_COEFFICIENTS_CIF_GZ.isJmolLoadable()); + assertTrue(DensityFileFormat.CCP4.isJmolLoadable()); + assertTrue(DensityFileFormat.CCP4_GZ.isJmolLoadable()); + assertTrue(DensityFileFormat.BCIF_VOLUME.isJmolLoadable()); + assertTrue(DensityFileFormat.CIF_VOLUME.isJmolLoadable()); + } +} diff --git a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestEmdbAndLocalOnly.java b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestEmdbAndLocalOnly.java new file mode 100644 index 0000000000..80bf73fc95 --- /dev/null +++ b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestEmdbAndLocalOnly.java @@ -0,0 +1,221 @@ +/** + * BioJava development code + * + * This code may be freely distributed and modified under the terms of the GNU + * Lesser General Public Licence. This should be distributed with the code. If + * you do not have a copy, see: + * + * http://www.gnu.org/copyleft/lesser.html + * + * Copyright for this code is held jointly by the individual authors. These + * should be listed in @author doc comments. + * + * For more information on the BioJava project and its aims, or to join the + * biojava-l mailing list, visit the home page at: + * + * http://www.biojava.org/ + */ +package org.biojava.nbio.structure.io.density; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; + +import org.biojava.nbio.core.util.FileDownloadUtils; +import org.biojava.nbio.structure.PdbId; +import org.biojava.nbio.structure.io.LocalPDBDirectory.FetchBehavior; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * EMDB metadata parsing against captured responses, and the guarantee that + * LOCAL_ONLY never opens a connection. + * + * @author Amr ALHOSSARY + * @since 7.3.0 + */ +public class TestEmdbAndLocalOnly { + + /** + * Nothing listens here, so any attempt to reach a server fails immediately + * rather than hanging or, worse, quietly succeeding. + */ + private static final String DEAD_SERVER = "http://localhost:1/"; + + private File cacheRoot; + + @Before + public void setUp() throws IOException { + cacheRoot = Files.createTempDirectory("bj-density").toFile(); + } + + @After + public void tearDown() throws IOException { + FileDownloadUtils.deleteDirectory(cacheRoot.toPath()); + PdbeCcp4MapProvider.resetToDefaults(); + WwpdbMapCoefficientsProvider.resetToDefaults(); + EmdbMapProvider.resetToDefaults(); + EmdbEntryResolver.resetToDefaults(); + } + + private void copyResource(String resource, File target) throws IOException { + target.getParentFile().mkdirs(); + try (InputStream in = getClass().getResourceAsStream(resource)) { + assertNotNull("missing test resource " + resource, in); + Files.copy(in, target.toPath()); + } + } + + /** + * The map metadata carries the two numbers a viewer needs: the level the + * depositors recommend and the RMS deviation that converts it to sigma. + */ + @Test + public void parsesEmdbMapMetadata() throws IOException { + copyResource("emdb-map-EMD-0262.json", + DensityCacheLayout.emdbMapInfoFile(cacheRoot, "EMD-0262")); + + EmdbEntryResolver resolver = new EmdbEntryResolver(cacheRoot); + resolver.setFetchBehavior(FetchBehavior.LOCAL_ONLY); + EmdbEntryInfo info = resolver.getEntryInfo("EMD-0262"); + + assertNotNull(info); + assertEquals("EMD-0262", info.getEmdbId()); + assertEquals(0.0263, info.getRecommendedContourLevel(), 1e-9); + assertNotNull("sigma is needed to express the contour in sigma units", info.getSigma()); + // 119165 kB, which is well past the default download ceiling + assertEquals(119165L * 1024L, info.getMapSizeBytes().longValue()); + } + + /** + * The size of a real EM map, and why the source order rather than the size + * guard is what keeps it from being fetched. + *

+ * EMD-0262 is about 116 MB, which sits comfortably under the 256 MiB default + * ceiling: left to the ceiling alone it would be downloaded in full. What + * avoids that is putting the density servers ahead of the archive, which serve + * a few megabytes for the same entry. The ceiling is the backstop for the maps + * that run to gigabytes. + */ + @Test + public void theFullEmMapIsLargeButWithinTheDefaultCeiling() throws IOException { + copyResource("emdb-map-EMD-0262.json", + DensityCacheLayout.emdbMapInfoFile(cacheRoot, "EMD-0262")); + EmdbEntryResolver resolver = new EmdbEntryResolver(cacheRoot); + resolver.setFetchBehavior(FetchBehavior.LOCAL_ONLY); + + long bytes = resolver.getEntryInfo("EMD-0262").getMapSizeBytes(); + assertTrue("expected a map of order 100 MB, got " + bytes, bytes > 100L * 1024 * 1024); + assertTrue("the ceiling alone would not stop this download", + bytes < DensityMapCache.DEFAULT_MAX_DOWNLOAD_BYTES); + + DensityMapCache cache = new DensityMapCache(cacheRoot.getAbsolutePath()); + List em = cache.getSourceChain(DensityMapKind.EM); + assertTrue("a density server must be tried before the full archive", + em.indexOf(DensityMapSource.RCSB_VOLUME_SERVER) < em.indexOf(DensityMapSource.EMDB_MAP)); + } + + @Test + public void contourConvertsToSigma() throws IOException { + copyResource("emdb-map-EMD-0262.json", + DensityCacheLayout.emdbMapInfoFile(cacheRoot, "EMD-0262")); + EmdbEntryResolver resolver = new EmdbEntryResolver(cacheRoot); + resolver.setFetchBehavior(FetchBehavior.LOCAL_ONLY); + EmdbEntryInfo info = resolver.getEntryInfo("EMD-0262"); + + DensityMapResult result = new DensityMapResult(new File("x.map"), DensityMapSource.EMDB_MAP, + DensityFileFormat.CCP4_GZ, DensityMapKind.EM, new PdbId("6hu9"), "EMD-0262", "u", false, + info.getRecommendedContourLevel(), info.getSigma()); + + assertEquals(info.getRecommendedContourLevel() / info.getSigma(), result.getContourInSigma(), 1e-9); + } + + /** A cached mapping is used whatever its age when downloads are off. */ + @Test + public void localOnlyUsesACachedMappingWithoutAskingAnyServer() throws IOException { + File mappingFile = DensityCacheLayout.emdbMappingFile(cacheRoot, new PdbId("6hu9")); + mappingFile.getParentFile().mkdirs(); + Files.write(mappingFile.toPath(), + ("pdbId=6hu9\nemdbIds=EMD-0262\ncontourLevel=0.0263\nretrieved=1999-01-01T00:00:00Z\n") + .getBytes(StandardCharsets.UTF_8)); + + EmdbEntryResolver resolver = new EmdbEntryResolver(cacheRoot); + resolver.setFetchBehavior(FetchBehavior.LOCAL_ONLY); + // point every template at a dead port, so a lookup would fail loudly + EmdbEntryResolver.setSearchUrlTemplate(DEAD_SERVER + "{pdbid_lc}"); + EmdbEntryResolver.setRcsbEntryUrlTemplate(DEAD_SERVER + "{pdbid_lc}"); + + List ids = resolver.getEmdbIds(new PdbId("6hu9")); + assertEquals(1, ids.size()); + assertEquals("EMD-0262", ids.get(0)); + } + + @Test + public void localOnlyReturnsNothingWhenNothingIsCached() { + EmdbEntryResolver resolver = new EmdbEntryResolver(cacheRoot); + resolver.setFetchBehavior(FetchBehavior.LOCAL_ONLY); + EmdbEntryResolver.setSearchUrlTemplate(DEAD_SERVER + "{pdbid_lc}"); + EmdbEntryResolver.setRcsbEntryUrlTemplate(DEAD_SERVER + "{pdbid_lc}"); + + assertTrue(resolver.getEmdbIds(new PdbId("6hu9")).isEmpty()); + } + + /** + * A cached map is served without any network access at all, and its metadata is + * rebuilt from the sidecar rather than from a server. + */ + @Test + public void localOnlyServesACachedMapOffline() throws IOException { + DensityMapCache cache = new DensityMapCache(cacheRoot.getAbsolutePath()); + cache.setFetchBehavior(FetchBehavior.LOCAL_ONLY); + PdbeCcp4MapProvider.setServerBaseUrl(DEAD_SERVER); + cache.setSourceEnabled(DensityMapSource.RCSB_VOLUME_SERVER, false); + cache.setSourceEnabled(DensityMapSource.PDBE_VOLUME_SERVER, false); + + File cached = DensityCacheLayout.pdbMapFile(cacheRoot, new PdbId("1cbs"), + DensityMapKind.TWO_FO_FC, DensityMapSource.PDBE_CCP4, DensityFileFormat.CCP4, null); + cached.getParentFile().mkdirs(); + Files.write(cached.toPath(), fakeCcp4()); + + DensityMapResult result = cache.getDensityMap(new PdbId("1cbs"), DensityMapKind.TWO_FO_FC); + + assertEquals(DensityMapSource.PDBE_CCP4, result.getSource()); + assertTrue(result.isFromCache()); + assertEquals(cached, result.getFile()); + assertTrue("a sidecar should have been written for the recovered file", + DensityMapResult.metaFileFor(cached).isFile()); + } + + @Test + public void localOnlyRefusesWhenNothingIsCached() { + DensityMapCache cache = new DensityMapCache(cacheRoot.getAbsolutePath()); + cache.setFetchBehavior(FetchBehavior.LOCAL_ONLY); + PdbeCcp4MapProvider.setServerBaseUrl(DEAD_SERVER); + + try { + cache.getDensityMap(new PdbId("1cbs"), DensityMapKind.TWO_FO_FC); + fail("expected NoDensityMapException"); + } catch (NoDensityMapException e) { + assertFalse(e.getAttempts().isEmpty()); + } catch (IOException e) { + fail("LOCAL_ONLY must not attempt any connection, but got " + e); + } + } + + private static byte[] fakeCcp4() { + byte[] bytes = new byte[4096]; + byte[] stamp = Ccp4Header.MAP_STAMP.getBytes(StandardCharsets.US_ASCII); + System.arraycopy(stamp, 0, bytes, Ccp4Header.MAP_STAMP_OFFSET, stamp.length); + return bytes; + } +} diff --git a/biojava-structure/src/test/resources/org/biojava/nbio/structure/io/density/emdb-map-EMD-0262.json b/biojava-structure/src/test/resources/org/biojava/nbio/structure/io/density/emdb-map-EMD-0262.json new file mode 100644 index 0000000000..3067838be7 --- /dev/null +++ b/biojava-structure/src/test/resources/org/biojava/nbio/structure/io/density/emdb-map-EMD-0262.json @@ -0,0 +1 @@ +{"emdb_id": "EMD-0262", "map": {"format": "CCP4", "size_kbytes": 119165, "file": "emd_0262.map.gz", "symmetry": {"space_group": "1"}, "data_type": "IMAGE STORED AS FLOATING POINT NUMBER (4 BYTES)", "dimensions": {"col": 310, "row": 310, "sec": 310}, "origin": {"col": 0, "row": 0, "sec": 0}, "spacing": {"x": 310, "y": 310, "z": 310}, "cell": {"a": {"units": "\u212b", "valueOf_": "429.691"}, "b": {"units": "\u212b", "valueOf_": "429.691"}, "c": {"units": "\u212b", "valueOf_": "429.691"}, "alpha": {"units": "deg", "valueOf_": "90.0"}, "beta": {"units": "deg", "valueOf_": "90.0"}, "gamma": {"units": "deg", "valueOf_": "90.0"}}, "axis_order": {"fast": "X", "medium": "Y", "slow": "Z"}, "statistics": {"minimum": -0.14692926, "maximum": 0.2550798, "average": 0.0005225369, "std": 0.009231779}, "pixel_spacing": {"x": {"units": "\u212b", "valueOf_": "1.3861"}, "y": {"units": "\u212b", "valueOf_": "1.3861"}, "z": {"units": "\u212b", "valueOf_": "1.3861"}}, "contour_list": {"contour": [{"primary": true, "level": 0.0263, "source": "AUTHOR", "instance_type": "contour"}]}, "label": "::::EMDATABANK.org::::EMD-0262::::", "annotation_details": "The sharpened map of the III2IV2 supercomplex"}} \ No newline at end of file diff --git a/biojava-structure/src/test/resources/org/biojava/nbio/structure/io/density/emdb-search-6hu9.csv b/biojava-structure/src/test/resources/org/biojava/nbio/structure/io/density/emdb-search-6hu9.csv new file mode 100644 index 0000000000..8046fd8b78 --- /dev/null +++ b/biojava-structure/src/test/resources/org/biojava/nbio/structure/io/density/emdb-search-6hu9.csv @@ -0,0 +1,2 @@ +emdb_id,map_contour_level_value +EMD-0262,0.0263 diff --git a/biojava-structure/src/test/resources/org/biojava/nbio/structure/io/density/rcsb-entry-6hu9.json b/biojava-structure/src/test/resources/org/biojava/nbio/structure/io/density/rcsb-entry-6hu9.json new file mode 100644 index 0000000000..b200c1817f --- /dev/null +++ b/biojava-structure/src/test/resources/org/biojava/nbio/structure/io/density/rcsb-entry-6hu9.json @@ -0,0 +1 @@ +{"audit_author":[{"identifier_ORCID":"0000-0001-7986-6736","name":"Hartley, A.M.","pdbx_ordinal":1},{"identifier_ORCID":"0000-0002-5096-257X","name":"Pinotsis, N.","pdbx_ordinal":2},{"identifier_ORCID":"0000-0003-3460-3806","name":"Marechal, A.","pdbx_ordinal":3}],"citation":[{"country":"US","id":"primary","journal_abbrev":"Nat. Struct. Mol. Biol.","journal_id_ISSN":"1545-9985","journal_volume":"26","page_first":"78","page_last":"83","pdbx_database_id_DOI":"10.1038/s41594-018-0172-z","pdbx_database_id_PubMed":30598554,"rcsb_authors":["Hartley, A.M.","Lukoyanova, N.","Zhang, Y.","Cabrera-Orefice, A.","Arnold, S.","Meunier, B.","Pinotsis, N.","Marechal, A."],"rcsb_is_primary":"Y","rcsb_journal_abbrev":"Nat Struct Mol Biol","title":"Structure of yeast cytochrome c oxidase in a supercomplex with cytochrome bc1.","year":2019}],"database_2":[{"database_code":"6HU9","database_id":"PDB","pdbx_DOI":"10.2210/pdb6hu9/pdb","pdbx_database_accession":"pdb_00006hu9"},{"database_code":"D_1200012277","database_id":"WWPDB"}],"em_3d_fitting":[{"id":"1","ref_protocol":"RIGID BODY FIT","ref_space":"REAL"}],"em_3d_fitting_list":[{"3d_fitting_id":"1","id":"1","pdb_entry_id":"1KYO"},{"3d_fitting_id":"1","id":"2","pdb_entry_id":"1V54"}],"em_3d_reconstruction":[{"id":"1","image_processing_id":"1","num_particles":44915,"resolution":3.35,"resolution_method":"FSC 0.5 CUT-OFF","symmetry_type":"POINT"}],"em_ctf_correction":[{"em_image_processing_id":"1","id":"1","type":"NONE"}],"em_entity_assembly":[{"entity_id_list":["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22"],"id":"1","name":"III2-IV2 mitochondrial respiratory supercomplex","parent_id":0,"source":"NATURAL","type":"COMPLEX"}],"em_experiment":{"aggregation_state":"PARTICLE","entity_assembly_id":"1","id":"1","reconstruction_method":"SINGLE PARTICLE"},"em_image_recording":[{"average_exposure_time":8.0,"avg_electron_dose_per_image":1.645,"detector_mode":"COUNTING","film_or_detector_model":"GATAN K2 QUANTUM (4k x 4k)","id":"1","imaging_id":"1"}],"em_imaging":[{"accelerating_voltage":300,"c2_aperture_diameter":70.0,"cryogen":"NITROGEN","electron_source":"FIELD EMISSION GUN","id":"1","illumination_mode":"FLOOD BEAM","microscope_model":"FEI TITAN KRIOS","mode":"BRIGHT FIELD","nominal_magnification":130000,"specimen_id":"1"}],"em_single_particle_entity":[{"id":1,"image_processing_id":"1","point_symmetry":"C1"}],"em_software":[{"category":"PARTICLE SELECTION","id":"1","image_processing_id":"1","name":"Gautomatch","version":"0.53"},{"category":"IMAGE ACQUISITION","id":"2","imaging_id":"1","name":"EPU"},{"category":"MASKING","id":"3"},{"category":"CTF CORRECTION","id":"4","image_processing_id":"1","name":"CTFFIND"},{"category":"LAYERLINE INDEXING","id":"5"},{"category":"DIFFRACTION INDEXING","id":"6"},{"category":"MODEL FITTING","fitting_id":"1","id":"7","name":"UCSF Chimera"},{"category":"OTHER","id":"8"},{"category":"MODEL REFINEMENT","fitting_id":"1","id":"9","name":"PHENIX","version":"1.13_2998"},{"category":"INITIAL EULER ASSIGNMENT","id":"10","image_processing_id":"1","name":"RELION","version":"3.0"},{"category":"FINAL EULER ASSIGNMENT","id":"11","image_processing_id":"1","name":"RELION","version":"3.0"},{"category":"CLASSIFICATION","id":"12","image_processing_id":"1","name":"RELION","version":"3.0"},{"category":"RECONSTRUCTION","id":"13","image_processing_id":"1","name":"RELION","version":"3.0"}],"em_specimen":[{"embedding_applied":"NO","experiment_id":"1","id":"1","shadowing_applied":"NO","staining_applied":"NO","vitrification_applied":"YES"}],"em_vitrification":[{"chamber_temperature":277.15,"cryogen_name":"ETHANE","details":"3 microL of sample applied to negatively glow discharged grid, blot force -10; blotting time 8.5 sec","humidity":92.0,"id":"1","instrument":"FEI VITROBOT MARK IV","specimen_id":"1"}],"entry":{"id":"6HU9"},"exptl":[{"method":"ELECTRON MICROSCOPY"}],"pdbx_audit_revision_category":[{"category":"citation","data_content_type":"Structure model","ordinal":1,"revision_ordinal":2},{"category":"citation_author","data_content_type":"Structure model","ordinal":2,"revision_ordinal":2},{"category":"pdbx_database_proc","data_content_type":"Structure model","ordinal":3,"revision_ordinal":2},{"category":"citation","data_content_type":"Structure model","ordinal":4,"revision_ordinal":3},{"category":"citation_author","data_content_type":"Structure model","ordinal":5,"revision_ordinal":3},{"category":"pdbx_database_proc","data_content_type":"Structure model","ordinal":6,"revision_ordinal":3},{"category":"pdbx_validate_close_contact","data_content_type":"Structure model","ordinal":7,"revision_ordinal":4},{"category":"struct_conn","data_content_type":"Structure model","ordinal":8,"revision_ordinal":4},{"category":"struct_site","data_content_type":"Structure model","ordinal":9,"revision_ordinal":4},{"category":"struct_site_gen","data_content_type":"Structure model","ordinal":10,"revision_ordinal":4},{"category":"atom_sites","data_content_type":"Structure model","ordinal":11,"revision_ordinal":5},{"category":"cell","data_content_type":"Structure model","ordinal":12,"revision_ordinal":5},{"category":"chem_comp_atom","data_content_type":"Structure model","ordinal":13,"revision_ordinal":6},{"category":"chem_comp_bond","data_content_type":"Structure model","ordinal":14,"revision_ordinal":6},{"category":"database_2","data_content_type":"Structure model","ordinal":15,"revision_ordinal":6},{"category":"em_3d_fitting_list","data_content_type":"Structure model","ordinal":16,"revision_ordinal":6},{"category":"pdbx_entry_details","data_content_type":"Structure model","ordinal":17,"revision_ordinal":6},{"category":"pdbx_initial_refinement_model","data_content_type":"Structure model","ordinal":18,"revision_ordinal":6},{"category":"pdbx_modification_feature","data_content_type":"Structure model","ordinal":19,"revision_ordinal":6},{"category":"pdbx_validate_chiral","data_content_type":"Structure model","ordinal":20,"revision_ordinal":6},{"category":"struct_conn","data_content_type":"Structure model","ordinal":21,"revision_ordinal":6},{"category":"struct_conn_type","data_content_type":"Structure model","ordinal":22,"revision_ordinal":6}],"pdbx_audit_revision_details":[{"data_content_type":"Structure model","ordinal":1,"provider":"repository","revision_ordinal":1,"type":"Initial release"}],"pdbx_audit_revision_group":[{"data_content_type":"Structure model","group":"Data collection","ordinal":1,"revision_ordinal":2},{"data_content_type":"Structure model","group":"Database references","ordinal":2,"revision_ordinal":2},{"data_content_type":"Structure model","group":"Data collection","ordinal":3,"revision_ordinal":3},{"data_content_type":"Structure model","group":"Database references","ordinal":4,"revision_ordinal":3},{"data_content_type":"Structure model","group":"Advisory","ordinal":5,"revision_ordinal":4},{"data_content_type":"Structure model","group":"Data collection","ordinal":6,"revision_ordinal":4},{"data_content_type":"Structure model","group":"Derived calculations","ordinal":7,"revision_ordinal":4},{"data_content_type":"Structure model","group":"Other","ordinal":8,"revision_ordinal":5},{"data_content_type":"Structure model","group":"Data collection","ordinal":9,"revision_ordinal":6},{"data_content_type":"Structure model","group":"Database references","ordinal":10,"revision_ordinal":6},{"data_content_type":"Structure model","group":"Derived calculations","ordinal":11,"revision_ordinal":6},{"data_content_type":"Structure model","group":"Refinement description","ordinal":12,"revision_ordinal":6},{"data_content_type":"Structure model","group":"Structure summary","ordinal":13,"revision_ordinal":6}],"pdbx_audit_revision_history":[{"data_content_type":"Structure model","major_revision":1,"minor_revision":0,"ordinal":1,"revision_date":"2018-12-26T00:00:00.000+00:00"},{"data_content_type":"Structure model","major_revision":1,"minor_revision":1,"ordinal":2,"revision_date":"2019-01-09T00:00:00.000+00:00"},{"data_content_type":"Structure model","major_revision":1,"minor_revision":2,"ordinal":3,"revision_date":"2019-01-16T00:00:00.000+00:00"},{"data_content_type":"Structure model","major_revision":1,"minor_revision":3,"ordinal":4,"revision_date":"2019-01-23T00:00:00.000+00:00"},{"data_content_type":"Structure model","major_revision":1,"minor_revision":4,"ordinal":5,"revision_date":"2019-12-11T00:00:00.000+00:00"},{"data_content_type":"Structure model","major_revision":1,"minor_revision":5,"ordinal":6,"revision_date":"2024-11-20T00:00:00.000+00:00"}],"pdbx_audit_revision_item":[{"data_content_type":"Structure model","item":"_citation.journal_abbrev","ordinal":1,"revision_ordinal":2},{"data_content_type":"Structure model","item":"_citation.pdbx_database_id_PubMed","ordinal":2,"revision_ordinal":2},{"data_content_type":"Structure model","item":"_citation.title","ordinal":3,"revision_ordinal":2},{"data_content_type":"Structure model","item":"_citation_author.identifier_ORCID","ordinal":4,"revision_ordinal":2},{"data_content_type":"Structure model","item":"_citation_author.name","ordinal":5,"revision_ordinal":2},{"data_content_type":"Structure model","item":"_citation.journal_volume","ordinal":6,"revision_ordinal":3},{"data_content_type":"Structure model","item":"_citation.page_first","ordinal":7,"revision_ordinal":3},{"data_content_type":"Structure model","item":"_citation.page_last","ordinal":8,"revision_ordinal":3},{"data_content_type":"Structure model","item":"_citation.year","ordinal":9,"revision_ordinal":3},{"data_content_type":"Structure model","item":"_citation_author.identifier_ORCID","ordinal":10,"revision_ordinal":3},{"data_content_type":"Structure model","item":"_atom_sites.fract_transf_matrix[1][1]","ordinal":11,"revision_ordinal":5},{"data_content_type":"Structure model","item":"_atom_sites.fract_transf_matrix[2][2]","ordinal":12,"revision_ordinal":5},{"data_content_type":"Structure model","item":"_atom_sites.fract_transf_matrix[3][3]","ordinal":13,"revision_ordinal":5},{"data_content_type":"Structure model","item":"_cell.Z_PDB","ordinal":14,"revision_ordinal":5},{"data_content_type":"Structure model","item":"_database_2.pdbx_DOI","ordinal":15,"revision_ordinal":6},{"data_content_type":"Structure model","item":"_database_2.pdbx_database_accession","ordinal":16,"revision_ordinal":6},{"data_content_type":"Structure model","item":"_em_3d_fitting_list.accession_code","ordinal":17,"revision_ordinal":6},{"data_content_type":"Structure model","item":"_em_3d_fitting_list.initial_refinement_model_id","ordinal":18,"revision_ordinal":6},{"data_content_type":"Structure model","item":"_em_3d_fitting_list.source_name","ordinal":19,"revision_ordinal":6},{"data_content_type":"Structure model","item":"_em_3d_fitting_list.type","ordinal":20,"revision_ordinal":6},{"data_content_type":"Structure model","item":"_struct_conn.conn_type_id","ordinal":21,"revision_ordinal":6},{"data_content_type":"Structure model","item":"_struct_conn.id","ordinal":22,"revision_ordinal":6},{"data_content_type":"Structure model","item":"_struct_conn.pdbx_dist_value","ordinal":23,"revision_ordinal":6},{"data_content_type":"Structure model","item":"_struct_conn.pdbx_leaving_atom_flag","ordinal":24,"revision_ordinal":6},{"data_content_type":"Structure model","item":"_struct_conn.ptnr1_auth_asym_id","ordinal":25,"revision_ordinal":6},{"data_content_type":"Structure model","item":"_struct_conn.ptnr1_auth_comp_id","ordinal":26,"revision_ordinal":6},{"data_content_type":"Structure model","item":"_struct_conn.ptnr1_auth_seq_id","ordinal":27,"revision_ordinal":6},{"data_content_type":"Structure model","item":"_struct_conn.ptnr1_label_asym_id","ordinal":28,"revision_ordinal":6},{"data_content_type":"Structure model","item":"_struct_conn.ptnr1_label_atom_id","ordinal":29,"revision_ordinal":6},{"data_content_type":"Structure model","item":"_struct_conn.ptnr1_label_comp_id","ordinal":30,"revision_ordinal":6},{"data_content_type":"Structure model","item":"_struct_conn.ptnr1_label_seq_id","ordinal":31,"revision_ordinal":6},{"data_content_type":"Structure model","item":"_struct_conn.ptnr2_auth_asym_id","ordinal":32,"revision_ordinal":6},{"data_content_type":"Structure model","item":"_struct_conn.ptnr2_auth_comp_id","ordinal":33,"revision_ordinal":6},{"data_content_type":"Structure model","item":"_struct_conn.ptnr2_auth_seq_id","ordinal":34,"revision_ordinal":6},{"data_content_type":"Structure model","item":"_struct_conn.ptnr2_label_asym_id","ordinal":35,"revision_ordinal":6},{"data_content_type":"Structure model","item":"_struct_conn.ptnr2_label_atom_id","ordinal":36,"revision_ordinal":6},{"data_content_type":"Structure model","item":"_struct_conn.ptnr2_label_comp_id","ordinal":37,"revision_ordinal":6},{"data_content_type":"Structure model","item":"_struct_conn.ptnr2_label_seq_id","ordinal":38,"revision_ordinal":6},{"data_content_type":"Structure model","item":"_struct_conn_type.id","ordinal":39,"revision_ordinal":6}],"pdbx_audit_support":[{"country":"United Kingdom","funding_organization":"Medical Research Council (United Kingdom)","grant_number":"MR/M00936X/1","ordinal":1},{"country":"United Kingdom","funding_organization":"Wellcome Trust","grant_number":"105628/Z/14/Z","ordinal":2}],"pdbx_database_related":[{"content_type":"associated EM volume","db_id":"EMD-0262","db_name":"EMDB","details":"BC1 (CHAINS A,B,C,D,E,F,G,H,I,J,L,M,N,O,P,Q,R,S,T,U)"},{"content_type":"other EM volume","db_id":"EMD-0269","db_name":"EMDB","details":"CIV 1 (chains a,b,c,d,e,f,g,h,i,j,k,l,y)"},{"content_type":"other EM volume","db_id":"EMD-0268","db_name":"EMDB","details":"CIV 2 (chains m,n,o,p,q,r,s,t,u,v,w,z)"}],"pdbx_database_status":{"SG_entry":"N","deposit_site":"PDBE","pdb_format_compatible":"Y","process_site":"PDBE","recvd_initial_deposition_date":"2018-10-05T00:00:00.000+00:00","status_code":"REL"},"pdbx_initial_refinement_model":[{"accession_code":"1KYO","id":1,"source_name":"PDB","type":"experimental model"},{"accession_code":"1V54","id":2,"source_name":"PDB","type":"experimental model"}],"pdbx_vrpt_summary":{"attempted_validation_steps":"visualanalysis,mogul,buster-report,molprobity,validation-pack,validation_schema,percentiles,writexml,writecif,writepdf","ligands_for_buster_report":"Y","report_creation_date":"2026-03-05T21:53:00.000+00:00"},"pdbx_vrpt_summary_em":[{"Q_score":0.431,"atom_inclusion_all_atoms":0.76,"atom_inclusion_backbone":0.817,"author_provided_fsc_resolution_by_cutoff_halfbit":3.32,"author_provided_fsc_resolution_by_cutoff_onebit":3.55,"author_provided_fsc_resolution_by_cutoff_pt_143":3.28,"author_provided_fsc_resolution_by_cutoff_pt_333":3.54,"author_provided_fsc_resolution_by_cutoff_pt_5":3.79,"contour_level_primary_map":0.0263,"exp_method":"electron microscopy"}],"pdbx_vrpt_summary_geometry":[{"angles_RMSZ":0.55,"bonds_RMSZ":0.36,"clashscore":5.7,"num_H_reduce":63043,"num_angles_RMSZ":84548,"num_bonds_RMSZ":62220,"percent_ramachandran_outliers":0.04,"percent_rotamer_outliers":8.49}],"rcsb_accession_info":{"deposit_date":"2018-10-05T00:00:00.000+00:00","has_released_experimental_data":"Y","initial_release_date":"2018-12-26T00:00:00.000+00:00","major_revision":1,"minor_revision":5,"revision_date":"2024-11-20T00:00:00.000+00:00","status_code":"REL"},"rcsb_entry_container_identifiers":{"assembly_ids":["1"],"emdb_ids":["EMD-0262"],"entity_ids":["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24","25","26","27","28","29","30","31","32","33","34","35"],"entry_id":"6HU9","model_ids":[1],"non_polymer_entity_ids":["23","24","25","26","27","28","29","30","31","32","33","34","35"],"polymer_entity_ids":["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22"],"pubmed_id":30598554,"rcsb_id":"6HU9","related_emdb_ids":["EMD-0269","EMD-0268"]},"rcsb_entry_info":{"assembly_count":1,"branched_entity_count":0,"cis_peptide_count":4,"deposited_atom_count":63031,"deposited_deuterated_water_count":0,"deposited_hydrogen_atom_count":0,"deposited_model_count":1,"deposited_modeled_polymer_monomer_count":7636,"deposited_nonpolymer_entity_instance_count":69,"deposited_polymer_entity_instance_count":44,"deposited_polymer_monomer_count":7922,"deposited_solvent_atom_count":0,"deposited_unmodeled_polymer_monomer_count":286,"disulfide_bond_count":10,"entity_count":35,"experimental_method":"EM","experimental_method_count":1,"inter_mol_covalent_bond_count":4,"inter_mol_metalic_bond_count":70,"molecular_weight":936.43,"na_polymer_entity_types":"Other","nonpolymer_bound_components":["CA","CU","CUA","FES","HEA","HEC","HEM","MG","ZN"],"nonpolymer_entity_count":13,"nonpolymer_molecular_weight_maximum":1.46,"nonpolymer_molecular_weight_minimum":0.02,"polymer_composition":"heteromeric protein","polymer_entity_count":22,"polymer_entity_count_DNA":0,"polymer_entity_count_RNA":0,"polymer_entity_count_nucleic_acid":0,"polymer_entity_count_nucleic_acid_hybrid":0,"polymer_entity_count_protein":22,"polymer_entity_taxonomy_count":22,"polymer_molecular_weight_maximum":58.83,"polymer_molecular_weight_minimum":5.38,"polymer_monomer_count_maximum":534,"polymer_monomer_count_minimum":47,"resolution_combined":[3.35],"selected_polymer_entity_types":"Protein (only)","software_programs_combined":["CTFFIND","EPU","GAUTOMATCH","PHENIX","RELION","UCSF CHIMERA"],"solvent_entity_count":0,"structure_determination_methodology":"experimental","structure_determination_methodology_priority":10},"rcsb_external_references":[{"id":"EMD-0262","link":"https://www.emdataresource.org/EMD-0262","type":"EM DATA RESOURCE"}],"rcsb_primary_citation":{"country":"US","id":"primary","journal_abbrev":"Nat. Struct. Mol. Biol.","journal_id_ISSN":"1545-9985","journal_volume":"26","page_first":"78","page_last":"83","pdbx_database_id_DOI":"10.1038/s41594-018-0172-z","pdbx_database_id_PubMed":30598554,"rcsb_ORCID_identifiers":["?","?","?","?","?","?","?","?"],"rcsb_authors":["Hartley, A.M.","Lukoyanova, N.","Zhang, Y.","Cabrera-Orefice, A.","Arnold, S.","Meunier, B.","Pinotsis, N.","Marechal, A."],"rcsb_journal_abbrev":"Nat Struct Mol Biol","title":"Structure of yeast cytochrome c oxidase in a supercomplex with cytochrome bc1.","year":2019},"refine_ls_restr":[{"dev_ideal":0.008,"number":64705,"pdbx_refine_id":"ELECTRON MICROSCOPY","type":"f_bond_d"},{"dev_ideal":1.069,"number":87763,"pdbx_refine_id":"ELECTRON MICROSCOPY","type":"f_angle_d"},{"dev_ideal":17.856,"number":38221,"pdbx_refine_id":"ELECTRON MICROSCOPY","type":"f_dihedral_angle_d"},{"dev_ideal":0.057,"number":9460,"pdbx_refine_id":"ELECTRON MICROSCOPY","type":"f_chiral_restr"},{"dev_ideal":0.008,"number":10892,"pdbx_refine_id":"ELECTRON MICROSCOPY","type":"f_plane_restr"}],"struct":{"pdbx_CASP_flag":"N","title":"III2-IV2 mitochondrial respiratory supercomplex from S. cerevisiae"},"struct_keywords":{"pdbx_keywords":"OXIDOREDUCTASE/ELECTRON TRANSPORT","text":"Cytochrome c oxidase Cytochrome bc1 Mitochondria Respiratory chain Supercomplex, OXIDOREDUCTASE, ELECTRON TRANSPORT, OXIDOREDUCTASE-ELECTRON TRANSPORT complex"},"rcsb_id":"6HU9"} \ No newline at end of file From 1213547a8f9700136cd62dc3be5814ae7d2c0f74 Mon Sep 17 00:00:00 2001 From: Amr ALHOSSARY Date: Sat, 15 Aug 2026 18:41:19 -0400 Subject: [PATCH 58/67] Use a definition list rather than a table in UrlTemplates javadoc The form is obsolete: the summary attribute was removed in HTML5, and javadoc has generated HTML5 since JDK 15, so doclint rejects it whenever it is switched on. The build sets -Xdoclint:none so this never broke CI, but it would surface in the release profile and the attribute does nothing for accessibility any more. A definition list suits a list of placeholders and their meanings better than a two-column table in any case. --- .../structure/io/density/UrlTemplates.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/UrlTemplates.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/UrlTemplates.java index b116386059..2752f42a0d 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/UrlTemplates.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/UrlTemplates.java @@ -27,16 +27,16 @@ * templates. *

* The recognised placeholders are: - *

- * - * - * - * - * - * - * - *
{pdbid}the PDB identifier as given
{pdbid_lc}the PDB identifier in lower case
{pdbid_uc}the PDB identifier in upper case
{mid}the two-character divided-archive directory, - * e.g. cb for 1cbs
{emdb_id}the EMDB identifier, e.g. EMD-0262
{emdb_num}the EMDB number alone, e.g. 0262
{detail}the density-server detail level
+ *

+ *
{pdbid}
the PDB identifier as given
+ *
{pdbid_lc}
the PDB identifier in lower case
+ *
{pdbid_uc}
the PDB identifier in upper case
+ *
{mid}
the two-character divided-archive directory, + * e.g. cb for 1cbs
+ *
{emdb_id}
the EMDB identifier, e.g. EMD-0262
+ *
{emdb_num}
the EMDB number alone, e.g. 0262
+ *
{detail}
the density-server detail level
+ *
* A placeholder with no supplied value is left in place rather than replaced by * an empty string, so that a misconfigured template produces an obviously wrong * URL instead of a subtly wrong one. From 0b57360c96d1c744be9c63722b756e5fbc901334 Mon Sep 17 00:00:00 2001 From: Amr ALHOSSARY Date: Sat, 29 Aug 2026 15:41:50 -0400 Subject: [PATCH 59/67] Convert the density tests to JUnit 5 The project is migrating to jupiter, so tests added by this branch should not arrive as JUnit 4. Imports move to org.junit.jupiter.api, @Before and @After become @BeforeEach and @AfterEach, and the seventeen assertions carrying a message have it moved from the first argument to the last, which is where JUnit 5 expects it. The three assertEquals(expected, actual, delta) calls are left alone: the third argument there is a floating point tolerance, not a message, and that overload is unchanged between the two versions. No pom changes: both modules already declare junit-jupiter-engine and junit-jupiter-params. --- .../io/density/DensityMapIntegrationTest.java | 46 +++++++++---------- .../structure/io/density/TestCcp4Header.java | 20 ++++---- .../io/density/TestDensityCacheLayout.java | 10 ++-- .../io/density/TestDensityFallbackChain.java | 14 +++--- .../density/TestDensityMapUrlTemplates.java | 10 ++-- .../io/density/TestEmdbAndLocalOnly.java | 38 +++++++-------- 6 files changed, 69 insertions(+), 69 deletions(-) diff --git a/biojava-integrationtest/src/test/java/org/biojava/nbio/structure/test/io/density/DensityMapIntegrationTest.java b/biojava-integrationtest/src/test/java/org/biojava/nbio/structure/test/io/density/DensityMapIntegrationTest.java index 2b8cce095b..d36a9805a4 100644 --- a/biojava-integrationtest/src/test/java/org/biojava/nbio/structure/test/io/density/DensityMapIntegrationTest.java +++ b/biojava-integrationtest/src/test/java/org/biojava/nbio/structure/test/io/density/DensityMapIntegrationTest.java @@ -17,11 +17,11 @@ */ package org.biojava.nbio.structure.test.io.density; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import java.io.File; import java.io.IOException; @@ -39,9 +39,9 @@ import org.biojava.nbio.structure.io.density.DensityMapResult; import org.biojava.nbio.structure.io.density.DensityMapSource; import org.biojava.nbio.structure.io.density.NoDensityMapException; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; /** * Density fetching against the real services. @@ -60,13 +60,13 @@ public class DensityMapIntegrationTest { private File cacheRoot; private DensityMapCache cache; - @Before + @BeforeEach public void setUp() throws IOException { cacheRoot = Files.createTempDirectory("bj-density-it").toFile(); cache = new DensityMapCache(cacheRoot.getAbsolutePath()); } - @After + @AfterEach public void tearDown() throws IOException { FileDownloadUtils.deleteDirectory(cacheRoot.toPath()); } @@ -81,8 +81,8 @@ public void fetchesAnXrayMapFromTheFirstSourceTried() throws IOException { assertTrue(result.isRenderable()); assertFalse(result.isFromCache()); assertTrue(result.getFileSizeBytes() > 1024); - assertTrue("a .meta sidecar makes the result reconstructible offline", - DensityMapResult.metaFileFor(result.getFile()).isFile()); + assertTrue(DensityMapResult.metaFileFor(result.getFile()).isFile(), + "a .meta sidecar makes the result reconstructible offline"); // second call must come from the cache without another download DensityMapResult again = cache.getDensityMap(new PdbId("1cbs"), DensityMapKind.TWO_FO_FC); @@ -100,11 +100,11 @@ public void bothKindsShareASingleDownload() throws IOException { DensityMapResult foFc = cache.getDensityMap(new PdbId("1cbs"), DensityMapKind.FO_FC); assertEquals(DensityMapKind.FO_FC, foFc.getKind()); - assertFalse("the difference map needs its own file name", twoFoFc.getFile().equals(foFc.getFile())); - assertTrue("the marker has to be in the name for Jmol to select the FO-FC block", - foFc.getFile().getName().contains("&diff=1")); - assertEquals("both names must address the same bytes", - twoFoFc.getFileSizeBytes(), foFc.getFileSizeBytes()); + assertFalse(twoFoFc.getFile().equals(foFc.getFile()), "the difference map needs its own file name"); + assertTrue(foFc.getFile().getName().contains("&diff=1"), + "the marker has to be in the name for Jmol to select the FO-FC block"); + assertEquals(twoFoFc.getFileSizeBytes(), foFc.getFileSizeBytes(), + "both names must address the same bytes"); } /** PDBe serves real CCP4 files, which the header check should recognise. */ @@ -115,7 +115,7 @@ public void pdbeServesAGenuineCcp4Map() throws IOException { assertEquals(DensityMapSource.PDBE_CCP4, result.getSource()); assertEquals(DensityFileFormat.CCP4, result.getFormat()); - assertTrue("the CCP4 stamp should be present at byte 208", Ccp4Header.isCcp4(result.getFile())); + assertTrue(Ccp4Header.isCcp4(result.getFile()), "the CCP4 stamp should be present at byte 208"); assertTrue(FileDownloadUtils.validateFile(result.getFile())); } @@ -131,8 +131,8 @@ public void resolvesCryoEmEntriesAndHonoursTheSizeLimit() throws IOException { DensityMapResult result = cache.getDensityMap(new PdbId("6hu9"), DensityMapKind.AUTO); assertEquals(DensityMapKind.EM, result.getKind()); assertEquals("EMD-0262", result.getEmdbId()); - assertNotNull("EM maps need the author contour level to be displayed properly", - result.getRecommendedContourLevel()); + assertNotNull(result.getRecommendedContourLevel(), + "EM maps need the author contour level to be displayed properly"); assertEquals(0.0263, result.getRecommendedContourLevel(), 1e-6); assertNotNull(result.getContourInSigma()); @@ -177,11 +177,11 @@ public void mapCoefficientsArriveWithAVerifiableChecksum() throws IOException { .build()); assertEquals(DensityMapSource.WWPDB_MAP_COEFFICIENTS, result.getSource()); - assertFalse("structure factors are not a map and must not claim to be renderable", - result.isRenderable()); + assertFalse(result.isRenderable(), + "structure factors are not a map and must not claim to be renderable"); File hashFile = new File(result.getFile().getParentFile(), result.getFile().getName() + ".hash_MD5"); - assertTrue("an MD5 should have been recorded from the ETag", hashFile.isFile()); + assertTrue(hashFile.isFile(), "an MD5 should have been recorded from the ETag"); assertTrue(FileDownloadUtils.validateFile(result.getFile())); // corrupt it and confirm the checksum actually catches it diff --git a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestCcp4Header.java b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestCcp4Header.java index 97707248ed..3c5d25950c 100644 --- a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestCcp4Header.java +++ b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestCcp4Header.java @@ -17,8 +17,8 @@ */ package org.biojava.nbio.structure.io.density; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayOutputStream; import java.io.File; @@ -28,9 +28,9 @@ import java.util.zip.GZIPOutputStream; import org.biojava.nbio.core.util.FileDownloadUtils; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; /** * The CCP4 header check that keeps a server's error page out of the cache. @@ -42,12 +42,12 @@ public class TestCcp4Header { private File dir; - @Before + @BeforeEach public void setUp() throws IOException { dir = Files.createTempDirectory("bj-ccp4").toFile(); } - @After + @AfterEach public void tearDown() throws IOException { FileDownloadUtils.deleteDirectory(dir.toPath()); } @@ -77,7 +77,7 @@ public void recognisesAGzippedMap() throws IOException { try (GZIPOutputStream gz = new GZIPOutputStream(buffer)) { gz.write(fakeMap()); } - assertTrue("EMDB serves its maps gzipped", Ccp4Header.isCcp4(write("good.map.gz", buffer.toByteArray()))); + assertTrue(Ccp4Header.isCcp4(write("good.map.gz", buffer.toByteArray())), "EMDB serves its maps gzipped"); } /** @@ -101,8 +101,8 @@ public void rejectsRandomBytesAndShortFiles() throws IOException { noise[i] = (byte) (i * 31); } assertFalse(Ccp4Header.isCcp4(write("noise.ccp4", noise))); - assertFalse("a file shorter than the header cannot be a map", - Ccp4Header.isCcp4(write("tiny.ccp4", new byte[10]))); + assertFalse(Ccp4Header.isCcp4(write("tiny.ccp4", new byte[10])), + "a file shorter than the header cannot be a map"); } @Test diff --git a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityCacheLayout.java b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityCacheLayout.java index 4477af693f..7e5723e67e 100644 --- a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityCacheLayout.java +++ b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityCacheLayout.java @@ -17,9 +17,9 @@ */ package org.biojava.nbio.structure.io.density; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.File; import java.util.HashSet; @@ -27,7 +27,7 @@ import org.biojava.nbio.structure.PdbId; import org.biojava.nbio.structure.io.LocalPDBDirectory; -import org.junit.Test; +import org.junit.jupiter.api.Test; /** * Cache layout: directory derivation, and that no two source and kind @@ -99,7 +99,7 @@ public void everySourceAndKindCombinationIsDistinct() { } for (DensityFileFormat format : DensityFileFormat.values()) { File f = DensityCacheLayout.pdbMapFile(ROOT, id, kind, source, format, null); - assertTrue("duplicate cache path: " + f, seen.add(f.getPath())); + assertTrue(seen.add(f.getPath()), "duplicate cache path: " + f); } } } diff --git a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityFallbackChain.java b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityFallbackChain.java index 55f02f1131..ca33f34063 100644 --- a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityFallbackChain.java +++ b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityFallbackChain.java @@ -17,10 +17,10 @@ */ package org.biojava.nbio.structure.io.density; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import java.io.File; import java.io.IOException; @@ -31,8 +31,8 @@ import org.biojava.nbio.core.util.HttpStatusException; import org.biojava.nbio.structure.PdbId; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; /** * The fallback chain, exercised with stub providers so that no server is @@ -51,7 +51,7 @@ public class TestDensityFallbackChain { private DensityMapCache cache; private List called; - @Before + @BeforeEach public void setUp() { cache = new DensityMapCache(System.getProperty("java.io.tmpdir")); called = new ArrayList<>(); diff --git a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityMapUrlTemplates.java b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityMapUrlTemplates.java index db5e666935..487e221f08 100644 --- a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityMapUrlTemplates.java +++ b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityMapUrlTemplates.java @@ -17,14 +17,14 @@ */ package org.biojava.nbio.structure.io.density; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.File; import org.biojava.nbio.structure.PdbId; -import org.junit.After; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; /** * URL construction for each provider. These run offline: only the strings are @@ -37,7 +37,7 @@ public class TestDensityMapUrlTemplates { private static final File ROOT = new File("/tmp/bjcache"); - @After + @AfterEach public void restoreDefaults() { PdbeCcp4MapProvider.resetToDefaults(); WwpdbMapCoefficientsProvider.resetToDefaults(); diff --git a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestEmdbAndLocalOnly.java b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestEmdbAndLocalOnly.java index 80bf73fc95..97cef78926 100644 --- a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestEmdbAndLocalOnly.java +++ b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestEmdbAndLocalOnly.java @@ -17,11 +17,11 @@ */ package org.biojava.nbio.structure.io.density; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import java.io.File; import java.io.IOException; @@ -33,9 +33,9 @@ import org.biojava.nbio.core.util.FileDownloadUtils; import org.biojava.nbio.structure.PdbId; import org.biojava.nbio.structure.io.LocalPDBDirectory.FetchBehavior; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; /** * EMDB metadata parsing against captured responses, and the guarantee that @@ -54,12 +54,12 @@ public class TestEmdbAndLocalOnly { private File cacheRoot; - @Before + @BeforeEach public void setUp() throws IOException { cacheRoot = Files.createTempDirectory("bj-density").toFile(); } - @After + @AfterEach public void tearDown() throws IOException { FileDownloadUtils.deleteDirectory(cacheRoot.toPath()); PdbeCcp4MapProvider.resetToDefaults(); @@ -71,7 +71,7 @@ public void tearDown() throws IOException { private void copyResource(String resource, File target) throws IOException { target.getParentFile().mkdirs(); try (InputStream in = getClass().getResourceAsStream(resource)) { - assertNotNull("missing test resource " + resource, in); + assertNotNull(in, "missing test resource " + resource); Files.copy(in, target.toPath()); } } @@ -92,7 +92,7 @@ public void parsesEmdbMapMetadata() throws IOException { assertNotNull(info); assertEquals("EMD-0262", info.getEmdbId()); assertEquals(0.0263, info.getRecommendedContourLevel(), 1e-9); - assertNotNull("sigma is needed to express the contour in sigma units", info.getSigma()); + assertNotNull(info.getSigma(), "sigma is needed to express the contour in sigma units"); // 119165 kB, which is well past the default download ceiling assertEquals(119165L * 1024L, info.getMapSizeBytes().longValue()); } @@ -115,14 +115,14 @@ public void theFullEmMapIsLargeButWithinTheDefaultCeiling() throws IOException { resolver.setFetchBehavior(FetchBehavior.LOCAL_ONLY); long bytes = resolver.getEntryInfo("EMD-0262").getMapSizeBytes(); - assertTrue("expected a map of order 100 MB, got " + bytes, bytes > 100L * 1024 * 1024); - assertTrue("the ceiling alone would not stop this download", - bytes < DensityMapCache.DEFAULT_MAX_DOWNLOAD_BYTES); + assertTrue(bytes > 100L * 1024 * 1024, "expected a map of order 100 MB, got " + bytes); + assertTrue(bytes < DensityMapCache.DEFAULT_MAX_DOWNLOAD_BYTES, + "the ceiling alone would not stop this download"); DensityMapCache cache = new DensityMapCache(cacheRoot.getAbsolutePath()); List em = cache.getSourceChain(DensityMapKind.EM); - assertTrue("a density server must be tried before the full archive", - em.indexOf(DensityMapSource.RCSB_VOLUME_SERVER) < em.indexOf(DensityMapSource.EMDB_MAP)); + assertTrue(em.indexOf(DensityMapSource.RCSB_VOLUME_SERVER) < em.indexOf(DensityMapSource.EMDB_MAP), + "a density server must be tried before the full archive"); } @Test @@ -192,8 +192,8 @@ public void localOnlyServesACachedMapOffline() throws IOException { assertEquals(DensityMapSource.PDBE_CCP4, result.getSource()); assertTrue(result.isFromCache()); assertEquals(cached, result.getFile()); - assertTrue("a sidecar should have been written for the recovered file", - DensityMapResult.metaFileFor(cached).isFile()); + assertTrue(DensityMapResult.metaFileFor(cached).isFile(), + "a sidecar should have been written for the recovered file"); } @Test From 510798cfa5efd12be36ca1d584cde8beafe99619 Mon Sep 17 00:00:00 2001 From: Amr ALHOSSARY Date: Sun, 30 Aug 2026 10:34:45 -0400 Subject: [PATCH 60/67] Address map coefficients by name, not by a constructed path The coefficients were fetched through the divided archive path, built from the two-character hash and the identifier. That path stops being correct in July 2027, when the PDB moves to extended identifiers and a per-entry layout under data/entries///, and every file name changes with it. The documented download endpoint resolves an entry by file name instead, so it survives the move untouched: https://files.wwpdb.org/validation/download/1cbs_validation_2fo-fc_map_coef.cif.gz Verified against files.wwpdb.org, files.rcsb.org and files-beta.wwpdb.org: all three serve it, in both the short and the extended spelling, and the beta host already serves it from the new archive. This was the only provider here that ever constructed a path. The density servers, PDBe and the EMDB archive are addressed by identifier already, so nothing else in the package is exposed to the transition. The default host stays files.wwpdb.org rather than files-beta.wwpdb.org, which is deliberate and worth recording. The wwPDB describes the beta host as transitional: on 21 July 2027 the beta archive replaces the main one, after which the beta URL is supported by redirection for three years. Pointing at it would be the choice that has to be revisited, twice, while the main host simply becomes the new archive. Nothing is given up by preferring the durable name either: the two serve byte-identical files today, checked across nine entries and agreeing even on which ones 404. The beta host is kept as a constant precisely because it already holds the post-2027 content, which makes it the way to test this endpoint against the archive as it will be rather than as it is - and it resolves both spellings there, so the endpoint's semantics survive the cutover. The identifier spelling is left to PdbId. getId(true) yields the short form where an entry has one and the extended form otherwise, which is what the entries deposited after the four-character space is exhausted will need; hard-coding either spelling would replace a rule that adapts with a constant that does not. Mirrors that publish directories rather than an endpoint stay reachable. EBI is one - it offers no name-resolving endpoint at all - so the divided templates remain as DIVIDED_*_TEMPLATE constants, and the layout that arrives in 2027 is expressible as ENTRIES_*_TEMPLATE. Expressing the latter needed the extended identifier inside a template, which nothing provided: {pdbid_lc} yields whichever spelling PdbId chose, and the new tree needs the extended one in two positions regardless. Hence {extid} in UrlTemplates. A mirror of the new archive is now a configuration change rather than a release. The cache layout is deliberately unchanged. Its two-character directory is a way of spreading files over directories, not a copy of the archive's own layout, and getMiddleHash counts from the right hand end, so 1cbs and pdb_00001cbs both land in cb - which is also the rule the wwPDB documents for the new archive, confirmed against it: entries/cb/pdb_00001cbs/ resolves and entries/bs/pdb_00001cbs/ does not. Nothing there needs attention in 2027. The javadoc now says why the cache is not laid out like the archive: it cannot be a mirror, since the archive publishes structure factors and coefficients but never grids, and writing density into a directory that is meant to be an exact copy of upstream puts it at the mercy of the next rsync --delete. Should that be revisited, every cached path is computed in that one class. --- .../io/density/DensityCacheLayout.java | 18 ++++ .../structure/io/density/UrlTemplates.java | 40 +++++++ .../density/WwpdbMapCoefficientsProvider.java | 86 +++++++++++++-- .../density/TestDensityMapUrlTemplates.java | 100 ++++++++++++++++-- 4 files changed, 231 insertions(+), 13 deletions(-) diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityCacheLayout.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityCacheLayout.java index 65691b627c..60e6ae7b61 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityCacheLayout.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/DensityCacheLayout.java @@ -43,6 +43,24 @@ * Several PDB entries are often fitted into a single EM map, and those maps can * be hundreds of megabytes, so keying them by PDB entry would cache the same * enormous file many times over. + *

+ * The two-character directory needs no attention when the archive moves to + * extended identifiers in July 2027. It is a device for spreading files over + * directories, not a copy of the archive's own layout, and + * {@link LocalPDBDirectory#getMiddleHash(String)} counts from the right hand end + * of the identifier, so 1cbs and pdb_00001cbs both land + * in cb — which is also the rule the wwPDB documents for the + * new archive. + *

+ * Deliberately not the archive's per-entry layout. A cache is not a mirror + * and cannot become one: the archive publishes structure factors and map + * coefficients but never grids, so density has to be fetched whatever else is + * mirrored, and writing it into a directory whose contents are an exact copy of + * upstream puts it at the mercy of the next rsync --delete. Should + * that judgement ever be revisited, every cached path is computed here and + * nowhere else, so a different layout is a change to this class plus a fallback + * probe for files in the old places — not a cache that everyone has to + * discard. * * @author Amr ALHOSSARY * @since 7.3.0 diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/UrlTemplates.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/UrlTemplates.java index 2752f42a0d..0518f1d4a4 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/UrlTemplates.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/UrlTemplates.java @@ -22,6 +22,9 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.biojava.nbio.structure.PdbId; +import org.biojava.nbio.structure.StructureException; + /** * Expands the named placeholders used in the configurable density-server URL * templates. @@ -33,6 +36,9 @@ *

{pdbid_uc}
the PDB identifier in upper case
*
{mid}
the two-character divided-archive directory, * e.g. cb for 1cbs
+ *
{extid}
the extended identifier in lower case, the + * spelling the archive itself uses, e.g. pdb_00001cbs for + * 1cbs
*
{emdb_id}
the EMDB identifier, e.g. EMD-0262
*
{emdb_num}
the EMDB number alone, e.g. 0262
*
{detail}
the density-server detail level
@@ -41,6 +47,18 @@ * an empty string, so that a misconfigured template produces an obviously wrong * URL instead of a subtly wrong one. *

+ * {mid} and {extid} exist for mirrors rather than for + * the default URLs. The services BioJava fetches from resolve an entry by name + * alone, but a site pointing this code at its own copy of the archive has to + * spell out a directory path — and so does any mirror that publishes one, + * such as EBI. Both the present divided layout and the per-entry layout that + * replaces it in July 2027 are directory paths, and between them the two + * placeholders express either without a code change: + *

+ * {mid}/{pdbid_lc}/{pdbid_lc}_validation_2fo-fc_map_coef.cif.gz
+ * entries/{mid}/{extid}/validation_reports/{extid}_validation_2fo-fc_map_coef.cif.gz
+ * 
+ *

* This is deliberately separate from the template mechanism in * {@code DownloadChemCompProvider}, which resolves a single chemical-component * identifier with optional substring indices. The two solve different problems: @@ -97,8 +115,14 @@ public static Map values(String pdbId, String emdbId, int detail values.put("pdbid_lc", pdbId.toLowerCase()); values.put("pdbid_uc", pdbId.toUpperCase()); if (pdbId.length() >= 3) { + // Correct for both spellings: the hash is counted from the right hand + // end, so 1cbs and pdb_00001cbs both yield "cb". values.put("mid", org.biojava.nbio.structure.io.LocalPDBDirectory.getMiddleHash(pdbId)); } + String extendedId = extendedId(pdbId); + if (extendedId != null) { + values.put("extid", extendedId); + } } if (emdbId != null) { values.put("emdb_id", DensityMapRequest.normalizeEmdbId(emdbId)); @@ -109,4 +133,20 @@ public static Map values(String pdbId, String emdbId, int detail } return values; } + + /** + * The extended spelling of an identifier, in the lower case the archive uses. + * + * @param pdbId an identifier in either spelling + * @return the extended spelling, or null if the argument is + * neither a short nor an extended identifier, in which case + * {extid} is left unexpanded rather than guessed at + */ + private static String extendedId(String pdbId) { + try { + return PdbId.toExtendedId(pdbId).toLowerCase(); + } catch (StructureException e) { + return null; + } + } } diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/WwpdbMapCoefficientsProvider.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/WwpdbMapCoefficientsProvider.java index c6eed7306e..bd1b066a64 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/WwpdbMapCoefficientsProvider.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/density/WwpdbMapCoefficientsProvider.java @@ -40,29 +40,101 @@ *

* One useful property: these servers return the content MD5 as the HTTP * ETag, so downloads from here are checksum-verified automatically. + *

+ * The URLs are built against the documented download endpoint, which resolves an + * entry by file name, rather than against the divided archive path. This is the + * only provider here that ever had a choice — the density servers and the + * EMDB archive are addressed by identifier already — and it matters because + * the PDB moves to extended identifiers and a per-entry directory layout in July + * 2027. A name survives that move; a constructed directory path does not. Mirrors + * that publish directories instead of an endpoint are still reachable, through + * {@link #DIVIDED_TWO_FO_FC_TEMPLATE} and {@link #ENTRIES_TWO_FO_FC_TEMPLATE}. * * @author Amr ALHOSSARY * @since 7.3.0 */ public class WwpdbMapCoefficientsProvider extends AbstractDensityMapProvider { - /** Default base URL, the wwPDB validation report archive. */ - public static final String DEFAULT_SERVER_URL = "https://files.wwpdb.org/pub/pdb/validation_reports/"; + /** Default base URL, the wwPDB validation report download endpoint. */ + public static final String DEFAULT_SERVER_URL = "https://files.wwpdb.org/validation/download/"; /** An RCSB mirror serving byte-identical files. */ - public static final String RCSB_MIRROR_URL = "https://files.rcsb.org/pub/pdb/validation_reports/"; + public static final String RCSB_MIRROR_URL = "https://files.rcsb.org/validation/download/"; - /** An EBI mirror serving byte-identical files. */ + /** + * The wwPDB beta archive, which already holds the re-organised content that + * replaces the current archive on 21 July 2027 and serves the same endpoint. + *

+ * Deliberately not the default, despite being the newer archive. The wwPDB + * describes this host as transitional: on the cutover date the beta archive + * replaces the main one, after which the beta URL is supported by redirection + * for three years. So it is the hostname that needs changing, twice, whereas + * {@link #DEFAULT_SERVER_URL} becomes the new archive and needs changing never. + *

+ * Its value is in testing. Because this host is the post-2027 content today, a + * request against it checks the endpoint against the archive as it will be, + * rather than against the archive as it is. + */ + public static final String BETA_SERVER_URL = "https://files-beta.wwpdb.org/validation/download/"; + + /** + * An EBI mirror serving byte-identical files. + *

+ * Unlike the two above, EBI publishes no name-resolving endpoint — only + * full directory paths — so selecting it means setting the divided + * templates as well: + *

+	 * setServerBaseUrl(EBI_MIRROR_URL);
+	 * setPathUrlTemplate(DensityMapKind.TWO_FO_FC, DIVIDED_TWO_FO_FC_TEMPLATE);
+	 * setPathUrlTemplate(DensityMapKind.FO_FC, DIVIDED_FO_FC_TEMPLATE);
+	 * 
+ * Setting the base alone yields 404s, because the flat file names do not exist + * there. + */ public static final String EBI_MIRROR_URL = "https://ftp.ebi.ac.uk/pub/databases/pdb/validation_reports/"; - /** Default path template for the 2mFo-DFc coefficients. */ + /** + * Default path template for the 2mFo-DFc coefficients: the file name alone. + *

+ * The endpoint resolves an entry by name, so no directory path is built here. + * That is deliberate. In July 2027 the archive moves to extended identifiers + * and a per-entry directory layout, and a path assembled from a hash and an + * identifier would have to be rewritten for it; a name does not. Both spellings + * of an identifier resolve, so whichever {@code PdbId} yields is accepted. + */ public static final String DEFAULT_TWO_FO_FC_TEMPLATE = - "{mid}/{pdbid_lc}/{pdbid_lc}_validation_2fo-fc_map_coef.cif.gz"; + "{pdbid_lc}_validation_2fo-fc_map_coef.cif.gz"; - /** Default path template for the mFo-DFc coefficients. */ + /** Default path template for the mFo-DFc coefficients; see {@link #DEFAULT_TWO_FO_FC_TEMPLATE}. */ public static final String DEFAULT_FO_FC_TEMPLATE = + "{pdbid_lc}_validation_fo-fc_map_coef.cif.gz"; + + /** + * Path template for the 2mFo-DFc coefficients in the divided archive, for + * mirrors that publish directories rather than an endpoint. + */ + public static final String DIVIDED_TWO_FO_FC_TEMPLATE = + "{mid}/{pdbid_lc}/{pdbid_lc}_validation_2fo-fc_map_coef.cif.gz"; + + /** Path template for the mFo-DFc coefficients in the divided archive. */ + public static final String DIVIDED_FO_FC_TEMPLATE = "{mid}/{pdbid_lc}/{pdbid_lc}_validation_fo-fc_map_coef.cif.gz"; + /** + * Path template for the 2mFo-DFc coefficients in the per-entry archive that + * replaces the divided one in July 2027, relative to a base URL ending in + * .../pdb/data/. + *

+ * Provided so that a mirror of the new layout can be used the day it exists, + * without waiting for a release. + */ + public static final String ENTRIES_TWO_FO_FC_TEMPLATE = + "entries/{mid}/{extid}/validation_reports/{extid}_validation_2fo-fc_map_coef.cif.gz"; + + /** Path template for the mFo-DFc coefficients in the per-entry archive. */ + public static final String ENTRIES_FO_FC_TEMPLATE = + "entries/{mid}/{extid}/validation_reports/{extid}_validation_fo-fc_map_coef.cif.gz"; + private static String serverBaseUrl = DEFAULT_SERVER_URL; private static String twoFoFcTemplate = DEFAULT_TWO_FO_FC_TEMPLATE; private static String foFcTemplate = DEFAULT_FO_FC_TEMPLATE; diff --git a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityMapUrlTemplates.java b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityMapUrlTemplates.java index 487e221f08..716b02b632 100644 --- a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityMapUrlTemplates.java +++ b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/density/TestDensityMapUrlTemplates.java @@ -65,18 +65,106 @@ public void pdbeServerIsConfigurable() { p.buildUrl(new PdbId("1cbs"), DensityMapKind.TWO_FO_FC)); } - /** The validation archive is divided by the same two characters as the rest of the PDB. */ + /** + * The coefficients are addressed by file name, not by a constructed directory + * path, so that the July 2027 move to the per-entry archive does not invalidate + * the URLs. + */ @Test - public void wwpdbUrlsUseTheDividedLayout() { + public void wwpdbUrlsResolveByName() { WwpdbMapCoefficientsProvider p = new WwpdbMapCoefficientsProvider(ROOT); - assertEquals("https://files.wwpdb.org/pub/pdb/validation_reports/" - + "cb/1cbs/1cbs_validation_2fo-fc_map_coef.cif.gz", + assertEquals("https://files.wwpdb.org/validation/download/" + + "1cbs_validation_2fo-fc_map_coef.cif.gz", p.buildUrl(new PdbId("1cbs"), DensityMapKind.TWO_FO_FC)); - assertEquals("https://files.wwpdb.org/pub/pdb/validation_reports/" - + "cb/1cbs/1cbs_validation_fo-fc_map_coef.cif.gz", + assertEquals("https://files.wwpdb.org/validation/download/" + + "1cbs_validation_fo-fc_map_coef.cif.gz", p.buildUrl(new PdbId("1cbs"), DensityMapKind.FO_FC)); } + /** + * The identifier spelling is not hard-coded either way. An entry with a short + * form is asked for by it; one without — every entry deposited once the + * four-character space is exhausted — is asked for by its extended form. + * Both spellings resolve at the endpoint, so nothing here has to change in 2027. + */ + @Test + public void wwpdbUrlsFollowWhicheverSpellingTheEntryHas() { + WwpdbMapCoefficientsProvider p = new WwpdbMapCoefficientsProvider(ROOT); + assertTrue(p.buildUrl(new PdbId("pdb_00001cbs"), DensityMapKind.TWO_FO_FC) + .endsWith("/1cbs_validation_2fo-fc_map_coef.cif.gz"), + "a shortable entry is asked for by its short name"); + assertTrue(p.buildUrl(new PdbId("pdb_00012abc"), DensityMapKind.TWO_FO_FC) + .endsWith("/pdb_00012abc_validation_2fo-fc_map_coef.cif.gz"), + "an entry with no short form is asked for by its extended name"); + } + + /** + * The beta host is a host swap and nothing more: the same endpoint, the same + * file name. That is what makes it a test target rather than a default - it + * holds the post-2027 content, so a request against it exercises the archive as + * it will be, while the default host is the one that survives the cutover. + */ + @Test + public void betaHostChangesNothingButTheHost() { + WwpdbMapCoefficientsProvider p = new WwpdbMapCoefficientsProvider(ROOT); + String viaDefault = p.buildUrl(new PdbId("1cbs"), DensityMapKind.TWO_FO_FC); + WwpdbMapCoefficientsProvider.setServerBaseUrl(WwpdbMapCoefficientsProvider.BETA_SERVER_URL); + String viaBeta = p.buildUrl(new PdbId("1cbs"), DensityMapKind.TWO_FO_FC); + assertEquals(viaDefault.substring(viaDefault.lastIndexOf('/')), + viaBeta.substring(viaBeta.lastIndexOf('/'))); + assertTrue(viaBeta.startsWith("https://files-beta.wwpdb.org/validation/download/")); + } + + /** EBI publishes directories rather than an endpoint, so it needs the divided templates. */ + @Test + public void dividedTemplatesStillReachMirrors() { + WwpdbMapCoefficientsProvider.setServerBaseUrl(WwpdbMapCoefficientsProvider.EBI_MIRROR_URL); + WwpdbMapCoefficientsProvider.setPathUrlTemplate(DensityMapKind.TWO_FO_FC, + WwpdbMapCoefficientsProvider.DIVIDED_TWO_FO_FC_TEMPLATE); + WwpdbMapCoefficientsProvider p = new WwpdbMapCoefficientsProvider(ROOT); + assertEquals("https://ftp.ebi.ac.uk/pub/databases/pdb/validation_reports/" + + "cb/1cbs/1cbs_validation_2fo-fc_map_coef.cif.gz", + p.buildUrl(new PdbId("1cbs"), DensityMapKind.TWO_FO_FC)); + } + + /** + * The archive layout that arrives in July 2027 is expressible as a template, + * so a mirror of it needs configuration rather than a new release. + */ + @Test + public void perEntryTemplateUsesExtendedIdentifiers() { + WwpdbMapCoefficientsProvider.setServerBaseUrl("https://files-beta.wwpdb.org/pub/wwpdb/pdb/data/"); + WwpdbMapCoefficientsProvider.setPathUrlTemplate(DensityMapKind.TWO_FO_FC, + WwpdbMapCoefficientsProvider.ENTRIES_TWO_FO_FC_TEMPLATE); + WwpdbMapCoefficientsProvider p = new WwpdbMapCoefficientsProvider(ROOT); + assertEquals("https://files-beta.wwpdb.org/pub/wwpdb/pdb/data/" + + "entries/cb/pdb_00001cbs/validation_reports/" + + "pdb_00001cbs_validation_2fo-fc_map_coef.cif.gz", + p.buildUrl(new PdbId("1cbs"), DensityMapKind.TWO_FO_FC)); + } + + /** The extended spelling is available to any template, in the case the archive uses. */ + @Test + public void extendedIdentifierPlaceholder() { + assertEquals("pdb_00001cbs", UrlTemplates.values("1cbs", null, -1).get("extid")); + assertEquals("pdb_00001cbs", UrlTemplates.values("pdb_00001cbs", null, -1).get("extid")); + assertEquals("{extid}", + UrlTemplates.expand("{extid}", UrlTemplates.values("not an id", null, -1)), + "an unusable identifier leaves the placeholder alone rather than guessing"); + } + + /** + * The two-character directory is counted from the right hand end, so it is the + * same for both spellings. This is what lets the cache layout stay as it is + * across the 2027 transition, and it is the rule the wwPDB documents for the + * new archive. + */ + @Test + public void middleHashIsTheSameForBothSpellings() { + assertEquals("cb", UrlTemplates.values("1cbs", null, -1).get("mid")); + assertEquals("cb", UrlTemplates.values("pdb_00001cbs", null, -1).get("mid")); + } + @Test public void volumeServerUrlsCarryDetailAndEncoding() { VolumeServerProvider rcsb = new VolumeServerProvider(ROOT, VolumeServerProvider.Host.RCSB); From d5a8f1fb92ab3baaf724742809e23e0016f673d0 Mon Sep 17 00:00:00 2001 From: Amr ALHOSSARY Date: Sun, 30 Aug 2026 15:24:09 -0400 Subject: [PATCH 61/67] Assert the coefficient checksum only when the server offers one The flat /validation/download/ endpoint returns neither an ETag nor a Content-Length, so no digest can be recorded from it. The divided archive path does return the content MD5, but that path disappears at the July 2027 archive transition, and the beta archive returns neither header on any path on files.wwpdb.org or files.rcsb.org. So the digest is asserted when it was recorded and reported when it was not, rather than being required. Size validation applies either way, since the sidecar is written from the bytes actually read. --- .../io/density/DensityMapIntegrationTest.java | 38 +++++++++++++++---- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/biojava-integrationtest/src/test/java/org/biojava/nbio/structure/test/io/density/DensityMapIntegrationTest.java b/biojava-integrationtest/src/test/java/org/biojava/nbio/structure/test/io/density/DensityMapIntegrationTest.java index d36a9805a4..97bf76a294 100644 --- a/biojava-integrationtest/src/test/java/org/biojava/nbio/structure/test/io/density/DensityMapIntegrationTest.java +++ b/biojava-integrationtest/src/test/java/org/biojava/nbio/structure/test/io/density/DensityMapIntegrationTest.java @@ -25,6 +25,7 @@ import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.Arrays; import java.util.List; @@ -163,11 +164,22 @@ public void reportsWhyAnEntryHasNoDensity() throws IOException { } /** - * The wwPDB servers return the content MD5 as the ETag, so a coefficient - * download is checksum-verified without a separate hash file. + * Coefficients must arrive intact and be verifiable afterwards. Whether that + * verification includes a cryptographic digest depends on the server. + *

+ * The divided archive paths on files.wwpdb.org and files.rcsb.org return the content + * MD5 as the ETag. The flat /validation/download/ endpoint this provider now uses + * returns neither an ETag nor a Content-Length, and neither does the beta archive on + * those two hosts, so no digest can be recorded there. The size sidecar is written + * from the bytes actually read, so it exists either way. + *

+ * The digest is therefore asserted when the server offered one and skipped when it + * did not, rather than being required: requiring it would fail against the endpoint + * we use, and hard-coding the divided path would only work until the archive + * transition in July 2027. */ @Test - public void mapCoefficientsArriveWithAVerifiableChecksum() throws IOException { + public void mapCoefficientsArriveIntactAndVerifiable() throws IOException { cache.setSourceEnabled(DensityMapSource.WWPDB_MAP_COEFFICIENTS, true); cache.setSourceChain(DensityMapKind.TWO_FO_FC, Arrays.asList(DensityMapSource.WWPDB_MAP_COEFFICIENTS)); @@ -180,12 +192,24 @@ public void mapCoefficientsArriveWithAVerifiableChecksum() throws IOException { assertFalse(result.isRenderable(), "structure factors are not a map and must not claim to be renderable"); + // written from the observed byte count, so it is present whether or not the + // server declared a length + assertTrue(FileDownloadUtils.validateFile(result.getFile()), + "a freshly downloaded file must validate against its own sidecars"); + File hashFile = new File(result.getFile().getParentFile(), result.getFile().getName() + ".hash_MD5"); - assertTrue(hashFile.isFile(), "an MD5 should have been recorded from the ETag"); - assertTrue(FileDownloadUtils.validateFile(result.getFile())); + if (hashFile.isFile()) { + String recorded = new String(Files.readAllBytes(hashFile.toPath()), StandardCharsets.UTF_8).trim(); + assertTrue(FileDownloadUtils.verifyHash(result.getFile(), FileDownloadUtils.Hash.MD5, recorded), + "the recorded MD5 must match the file it describes"); + } else { + System.out.println("No MD5 recorded for " + result.getSourceUrl() + + " - the server offered no usable ETag. Size validation still applies."); + } - // corrupt it and confirm the checksum actually catches it + // corrupt it and confirm validation actually catches it Files.write(result.getFile().toPath(), new byte[] {0, 1, 2, 3}); - assertFalse(FileDownloadUtils.validateFile(result.getFile())); + assertFalse(FileDownloadUtils.validateFile(result.getFile()), + "a truncated file must not validate"); } } From e73ce56fb1fd9f17cc8238f9d38b420445e4ef93 Mon Sep 17 00:00:00 2001 From: Jose Duarte Date: Wed, 2 Sep 2026 21:07:13 -0700 Subject: [PATCH 62/67] Docs --- .../structure/contact/AtomContactSet.java | 58 +++++++++++++++++-- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/AtomContactSet.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/AtomContactSet.java index a4757de486..9748065524 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/AtomContactSet.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/AtomContactSet.java @@ -28,6 +28,22 @@ /** * A set of atom-atom contacts to hold the results of intra and inter-chain contact calculations + *

+ * Contacts are keyed by the ordered pair of {@link AtomIdentifier}s of the 2 atoms, i.e. the + * pair (a,b) and the pair (b,a) are 2 different keys. Thus look-ups ({@link #hasContact(Atom, Atom)}, + * {@link #getContact(Atom, Atom)}) must give the 2 atoms in the same order in which the contact was + * calculated. The order produced by the calculation ({@link Grid}) is: + *

    + *
  • for contacts within a single set of atoms (e.g. the intra-chain contacts of + * StructureTools.getAtomsInContact(Chain, double)), the atom that comes first in the + * atom array is the first member of the pair
  • + *
  • for contacts between 2 sets of atoms (e.g. the inter-chain contacts of + * StructureTools.getAtomsInContact(Chain, Chain, double, boolean), or a + * {@link StructureInterface}), the atom belonging to the first set is the first member of the pair
  • + *
+ *

+ * Note that the order is not the order of PDB serials or of any other property of the atoms + * themselves: it is only the order in which the atoms were given to the calculation. * * @author duarte_j * @@ -65,31 +81,65 @@ public AtomContactSet(double cutoff, int expectedSize) { this.contacts = new HashMap<>((int) (expectedSize / DEFAULT_LOAD_FACTOR) + 1); } + /** + * Adds the given contact to this set, keyed by the ordered pair of its 2 atoms. If a contact + * with the same ordered pair of atoms is already present it is replaced. + * @param contact the contact to add + */ public void add(AtomContact contact) { this.contacts.put(getAtomIdPairFromContact(contact), contact); } + /** + * Adds all given contacts to this set, see {@link #add(AtomContact)}. + * @param list the contacts to add + */ public void addAll(Collection list) { for (AtomContact contact:list) { this.contacts.put(getAtomIdPairFromContact(contact), contact); } } + /** + * Tells whether a contact exists between the 2 given atoms, in the given order. + *

+ * The 2 atoms have to be passed in the same order in which the contacts of this set were + * calculated, otherwise this returns false even if the 2 atoms are within the distance cutoff. + * See the class documentation for the ordering convention. If the order is not known, both orders + * have to be queried. + * @param atom1 the first atom of the pair + * @param atom2 the second atom of the pair + * @return true if the 2 atoms are in contact in the given order, false otherwise + * @see #getContact(Atom, Atom) + */ public boolean hasContact(Atom atom1, Atom atom2) { return hasContact( new AtomIdentifier(atom1.getPDBserial(),atom1.getGroup().getChainId()), new AtomIdentifier(atom2.getPDBserial(),atom2.getGroup().getChainId()) ); } + /** + * Tells whether a contact exists between the 2 given atom identifiers, in the given order, + * see {@link #hasContact(Atom, Atom)}. + * @param atomId1 the identifier of the first atom of the pair + * @param atomId2 the identifier of the second atom of the pair + * @return true if the 2 atoms are in contact in the given order, false otherwise + */ public boolean hasContact(AtomIdentifier atomId1, AtomIdentifier atomId2) { return contacts.containsKey(new Pair(atomId1,atomId2)); } /** - * Returns the corresponding AtomContact or null if no contact exists between the 2 given atoms - * @param atom1 - * @param atom2 - * @return + * Returns the contact between the 2 given atoms in the given order, or null if there is + * no such contact in this set. + *

+ * As in {@link #hasContact(Atom, Atom)} the order of the 2 atoms matters: they have to be passed + * in the same order in which the contacts of this set were calculated, otherwise null is returned + * even if the 2 atoms are within the distance cutoff. See the class documentation for the + * ordering convention. + * @param atom1 the first atom of the pair + * @param atom2 the second atom of the pair + * @return the contact between the 2 atoms in the given order, or null if there is none */ public AtomContact getContact(Atom atom1, Atom atom2) { return contacts.get(new Pair( From 30b71c7a419c6ca489e5b6f9d2ec0964834478da Mon Sep 17 00:00:00 2001 From: Amr ALHOSSARY Date: Sun, 6 Sep 2026 06:40:03 -0400 Subject: [PATCH 63/67] Do not pin the status an absent file returns files.wwpdb.org moved behind Amazon S3, which answers a missing key with 403 rather than 404 when the caller cannot list the bucket. The test asserted 404 and has failed the nightly since 2 September, hiding every later module. It now asserts the contract - an error status throws and nothing is written - and isNotFound() is pinned separately without a network. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_011NUW9k3A19Ee3MYMU7v7EV --- .../nbio/core/util/FileDownloadUtilsTest.java | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/biojava-core/src/test/java/org/biojava/nbio/core/util/FileDownloadUtilsTest.java b/biojava-core/src/test/java/org/biojava/nbio/core/util/FileDownloadUtilsTest.java index 4a3cae7fb7..bec6def90f 100644 --- a/biojava-core/src/test/java/org/biojava/nbio/core/util/FileDownloadUtilsTest.java +++ b/biojava-core/src/test/java/org/biojava/nbio/core/util/FileDownloadUtilsTest.java @@ -217,9 +217,19 @@ void testValidationFiles() throws IOException{ @Nested class HttpStatus { - @Test - void notFoundThrowsAndLeavesNothingBehind() throws IOException { - // A path that is guaranteed absent from the wwPDB archive. + /** + * Which status an absent file comes back with is the server's business, and it + * changes: files.wwpdb.org moved behind Amazon S3 in September 2026, and S3 + * answers a missing key with 403 rather than 404 when the caller cannot list + * the bucket. Pinning the code made this test fail on an upstream hosting + * change that broke nothing. + *

+ * What must hold is the contract: an error status throws, and nothing is left + * on disk for it. {@link HttpStatusException#isNotFound()} is pinned separately + * below, without a network. + */ + @Test + void anAbsentFileThrowsAndLeavesNothingBehind() throws IOException { URL missing = new URL("https://files.wwpdb.org/pub/pdb/data/structures/divided/mmCIF/zz/zzzz.cif.gz"); File dest = new File(System.getProperty("java.io.tmpdir"), "bj-missing.cif.gz"); File sizeFile = new File(dest.getParentFile(), dest.getName() + ".size"); @@ -228,14 +238,24 @@ void notFoundThrowsAndLeavesNothingBehind() throws IOException { HttpStatusException e = assertThrows(HttpStatusException.class, () -> FileDownloadUtils.downloadFile(missing, dest)); - assertEquals(404, e.getStatusCode()); - assertTrue(e.isNotFound()); - assertFalse(dest.exists(), "a 404 body must never be written to the destination"); + assertTrue(e.getStatusCode() >= 400, + "an absent file must report an error status, got " + e.getStatusCode()); + assertFalse(dest.exists(), "an error body must never be written to the destination"); // ... and no validation metadata may be recorded for it either, or the // cached error page would later pass validation. FileDownloadUtils.createValidationFiles(missing, dest, null, FileDownloadUtils.Hash.UNKNOWN); - assertFalse(sizeFile.exists(), "no size file should be written for a 404 response"); + assertFalse(sizeFile.exists(), "no size file should be written for an error response"); + } + + @Test + void isNotFoundCoversTheAbsentStatusesOnly() { + assertTrue(new HttpStatusException(404, "http://example.org/x", "Not Found").isNotFound()); + assertTrue(new HttpStatusException(410, "http://example.org/x", "Gone").isNotFound()); + // 403 is what an S3-backed archive returns for a missing key, but it is not a + // statement that the file does not exist, so it must not claim to be one + assertFalse(new HttpStatusException(403, "http://example.org/x", "Forbidden").isNotFound()); + assertFalse(new HttpStatusException(500, "http://example.org/x", "Server Error").isNotFound()); } } From dc9cdafce77c841286a6271d9680af7acb8c995c Mon Sep 17 00:00:00 2001 From: Jose Duarte Date: Mon, 14 Sep 2026 16:36:56 -0700 Subject: [PATCH 64/67] Bugfix: setParseCAOnly wasn't correctly filtering CA-only in CIF implementation --- .../biojava/nbio/structure/io/BondMaker.java | 14 +++- .../io/cif/CifStructureConsumerImpl.java | 3 +- .../io/cif/CifFileConsumerImplTest.java | 67 ++++++++++++++++++- 3 files changed, 80 insertions(+), 4 deletions(-) diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/BondMaker.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/BondMaker.java index e81f7fb867..b7c9a84dea 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/BondMaker.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/BondMaker.java @@ -463,14 +463,24 @@ public void formBondsFromStructConn(StructConn conn) { } catch (StructureException e) { - logger.warn("Could not find atom specified in struct_conn record: {}{}({}) in chain {}, atom {} {}", seqId1, insCode1, resName1, chainId1, atomName1, altLocStr1); + // Note, in Calpha only mode the struct_conn atoms may not be present. + if (! params.isParseCAOnly()) { + logger.warn("Could not find atom specified in struct_conn record: {}{}({}) in chain {}, atom {} {}", seqId1, insCode1, resName1, chainId1, atomName1, altLocStr1); + } else { + logger.debug("Could not find atom specified in struct_conn record while parsing in parseCAonly mode: {}{}({}) in chain {}, atom {} {}", seqId1, insCode1, resName1, chainId1, atomName1, altLocStr1); + } continue; } try { a2 = getAtomFromRecord(atomName2, altLoc2, chainId2, seqId2, insCode2); } catch (StructureException e) { - logger.warn("Could not find atom specified in struct_conn record: {}{}({}) in chain {}, atom {} {}", seqId2, insCode2, resName2, chainId2, atomName2, altLocStr2); + // Note, in Calpha only mode the struct_conn atoms may not be present. + if (! params.isParseCAOnly()) { + logger.warn("Could not find atom specified in struct_conn record: {}{}({}) in chain {}, atom {} {}", seqId2, insCode2, resName2, chainId2, atomName2, altLocStr2); + } else { + logger.debug("Could not find atom specified in struct_conn record while parsing in parseCAonly mode: {}{}({}) in chain {}, atom {} {}", seqId2, insCode2, resName2, chainId2, atomName2, altLocStr2); + } continue; } diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/CifStructureConsumerImpl.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/CifStructureConsumerImpl.java index f44eb44ab1..4832e1122f 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/CifStructureConsumerImpl.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/CifStructureConsumerImpl.java @@ -319,7 +319,8 @@ public void consumeAtomSite(AtomSite atomSite) { } if (params.isParseCAOnly()) { - if (!labelAtomId.get(atomIndex).equals(StructureTools.CA_ATOM_NAME) && "C".equals(typeSymbol.get(atomIndex))) { + // keep only C-alpha atoms; requiring element C excludes calcium ions, whose atom name is also CA + if (!(labelAtomId.get(atomIndex).equals(StructureTools.CA_ATOM_NAME) && "C".equals(typeSymbol.get(atomIndex)))) { continue; } } diff --git a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/cif/CifFileConsumerImplTest.java b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/cif/CifFileConsumerImplTest.java index 8fbd1060b0..43dde2c472 100644 --- a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/cif/CifFileConsumerImplTest.java +++ b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/cif/CifFileConsumerImplTest.java @@ -1,6 +1,10 @@ package org.biojava.nbio.structure.io.cif; +import org.biojava.nbio.structure.Atom; import org.biojava.nbio.structure.Chain; +import org.biojava.nbio.structure.Element; +import org.biojava.nbio.structure.Group; +import org.biojava.nbio.structure.StructureTools; import org.biojava.nbio.structure.EntityInfo; import org.biojava.nbio.structure.EntityType; import org.biojava.nbio.structure.Structure; @@ -17,6 +21,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; @@ -267,4 +272,64 @@ public void testAtomSiteWithMissingAuthFields() throws IOException { assertEquals(2, s.getPolyChain("A").getAtomGroups().size()); assertEquals(2, s.getPolyChainByPDB("A").getAtomGroups().size()); } -} \ No newline at end of file + + /** + * With parseCAOnly, only C-alpha atoms must be kept: no N/O/S or other non-carbon atoms, + * and no calcium ions (atom name CA, element Ca). + */ + @Test + public void testParseCAOnly() throws IOException { + String resource = "/org/biojava/nbio/structure/io/mmcif/1stp_v5.cif"; + String mmcifStr; + try (InputStream inputStream = getClass().getResourceAsStream(resource)) { + Objects.requireNonNull(inputStream, "could not acquire test resource " + resource); + mmcifStr = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + } + // turn the last water oxygen into an atom named CA with element Ca, like a calcium ion + String waterRow = "HETATM 1001 O O . HOH C 3 . ? 19.892 14.908 -13.679 0.90 40.00 ? 449 HOH A O 1"; + assertTrue(mmcifStr.contains(waterRow)); + mmcifStr = mmcifStr.replace(waterRow, "HETATM 1001 CA CA . HOH C 3 . ? 19.892 14.908 -13.679 0.90 40.00 ? 449 HOH A CA 1"); + + FileParsingParameters fullParams = new FileParsingParameters(); + fullParams.setCreateAtomBonds(true); + Structure full = CifStructureConverter.fromInputStream( + new ByteArrayInputStream(mmcifStr.getBytes(StandardCharsets.UTF_8)), fullParams); + + FileParsingParameters caParams = new FileParsingParameters(); + caParams.setCreateAtomBonds(true); + caParams.setParseCAOnly(true); + Structure caOnly = CifStructureConverter.fromInputStream( + new ByteArrayInputStream(mmcifStr.getBytes(StandardCharsets.UTF_8)), caParams); + + int expected = 0; + boolean hasCalcium = false; + for (int model = 0; model < full.nrModels(); model++) { + for (Chain chain : full.getChains(model)) { + for (Group group : chain.getAtomGroups()) { + Atom ca = group.getAtom(StructureTools.CA_ATOM_NAME); + if (group.isAminoAcid() && ca != null) { + expected++; + } else if (ca != null && ca.getElement() == Element.Ca) { + hasCalcium = true; + } + } + } + } + assertTrue(expected > 0); + assertTrue("test input should contain a calcium named CA", hasCalcium); + + int count = 0; + for (int model = 0; model < caOnly.nrModels(); model++) { + for (Chain chain : caOnly.getChains(model)) { + for (Group group : chain.getAtomGroups()) { + for (Atom atom : group.getAtoms()) { + assertEquals(StructureTools.CA_ATOM_NAME, atom.getName()); + assertEquals(Element.C, atom.getElement()); + count++; + } + } + } + } + assertEquals(expected, count); + } +} From 21b7346e2b6240d27a6cb47279646b6f3d3454f1 Mon Sep 17 00:00:00 2001 From: Jose Duarte Date: Mon, 14 Sep 2026 16:43:22 -0700 Subject: [PATCH 65/67] Updating changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77848fde9d..5693ca54bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,9 @@ BioJava 7.3.0 write-then-read round trip #1144 #1143 * The ECOD read lock is left balanced when a download fails, so the original error is no longer replaced by `IllegalMonitorStateException` #1150 +* `FileParsingParameters.setParseCAOnly(true)` keeps only C-alpha atoms when reading mmCIF and + BinaryCIF, as it does for PDB files. Since the unified CIF parser it had dropped only non-CA + carbons, keeping every N, O and S atom. * Resolution parsing warns only when the values actually differ ### Changed From 0b8427a68aa027e814babab7ccd435080481a8f1 Mon Sep 17 00:00:00 2001 From: Jose Duarte Date: Wed, 16 Sep 2026 21:59:48 -0700 Subject: [PATCH 66/67] Further fix: don't add empty groups or chains in parseCAOnly --- CHANGELOG.md | 3 ++- .../structure/io/cif/CifStructureConsumerImpl.java | 14 +++++++------- .../structure/io/cif/CifFileConsumerImplTest.java | 4 ++++ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5693ca54bb..fd2a848907 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,7 +40,8 @@ BioJava 7.3.0 replaced by `IllegalMonitorStateException` #1150 * `FileParsingParameters.setParseCAOnly(true)` keeps only C-alpha atoms when reading mmCIF and BinaryCIF, as it does for PDB files. Since the unified CIF parser it had dropped only non-CA - carbons, keeping every N, O and S atom. + carbons, keeping every N, O and S atom. Groups and chains that hold no C-alpha (waters, + ligands, nucleotides) are no longer created at all in this mode * Resolution parsing warns only when the values actually differ ### Changed diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/CifStructureConsumerImpl.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/CifStructureConsumerImpl.java index 4832e1122f..28bcb420a7 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/CifStructureConsumerImpl.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/io/cif/CifStructureConsumerImpl.java @@ -205,6 +205,13 @@ public void consumeAtomSite(AtomSite atomSite) { IntColumn pdbx_pdb_model_num = atomSite.getPdbxPDBModelNum(); for (int atomIndex = 0; atomIndex < atomSite.getRowCount(); atomIndex++) { + // skip before any chain or group is set up, so that groups and chains without a + // C-alpha (waters, ligands, nucleotides) are not created at all + if (params.isParseCAOnly() && + !(labelAtomId.get(atomIndex).equals(StructureTools.CA_ATOM_NAME) && "C".equals(typeSymbol.get(atomIndex)))) { + continue; + } + boolean startOfNewChain = false; Character oneLetterCode = StructureTools.get1LetterCodeAmino(labelCompId.get(atomIndex)); @@ -318,13 +325,6 @@ public void consumeAtomSite(AtomSite atomSite) { } } - if (params.isParseCAOnly()) { - // keep only C-alpha atoms; requiring element C excludes calcium ions, whose atom name is also CA - if (!(labelAtomId.get(atomIndex).equals(StructureTools.CA_ATOM_NAME) && "C".equals(typeSymbol.get(atomIndex)))) { - continue; - } - } - Atom atom = new AtomImpl(); atom.setPDBserial(id.get(atomIndex)); diff --git a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/cif/CifFileConsumerImplTest.java b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/cif/CifFileConsumerImplTest.java index 43dde2c472..3fb733f079 100644 --- a/biojava-structure/src/test/java/org/biojava/nbio/structure/io/cif/CifFileConsumerImplTest.java +++ b/biojava-structure/src/test/java/org/biojava/nbio/structure/io/cif/CifFileConsumerImplTest.java @@ -320,8 +320,12 @@ public void testParseCAOnly() throws IOException { int count = 0; for (int model = 0; model < caOnly.nrModels(); model++) { + // the water and ligand chains hold no C-alpha, so they should not have been created + assertEquals(full.getPolyChains(model).size(), caOnly.getChains(model).size()); for (Chain chain : caOnly.getChains(model)) { for (Group group : chain.getAtomGroups()) { + // no group without a C-alpha should have been created either + assertEquals(1, group.getAtoms().size()); for (Atom atom : group.getAtoms()) { assertEquals(StructureTools.CA_ATOM_NAME, atom.getName()); assertEquals(Element.C, atom.getElement()); From 9ae550cf70bdc63900dfa25f28afda1829307258 Mon Sep 17 00:00:00 2001 From: josemduarte Date: Thu, 17 Sep 2026 12:54:37 -0700 Subject: [PATCH 67/67] Fixing test to be compatible with all platforms and checkout styles --- .../org/biojava/nbio/genome/GeneFeatureHelperTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/biojava-genome/src/test/java/org/biojava/nbio/genome/GeneFeatureHelperTest.java b/biojava-genome/src/test/java/org/biojava/nbio/genome/GeneFeatureHelperTest.java index 53ab7c20cb..dac677800a 100644 --- a/biojava-genome/src/test/java/org/biojava/nbio/genome/GeneFeatureHelperTest.java +++ b/biojava-genome/src/test/java/org/biojava/nbio/genome/GeneFeatureHelperTest.java @@ -101,8 +101,8 @@ void testOutputFastaSequenceLengthGFF3() throws Exception { gffFile.deleteOnExit(); GeneFeatureHelper.outputFastaSequenceLengthGFF3(fastaSequenceFile, gffFile); Assertions.assertEquals( - Files.readString(new File("src/test/resources/volvox_length_reference.gff3").toPath()), - Files.readString(gffFile.toPath()), + Files.readAllLines(new File("src/test/resources/volvox_length_reference.gff3").toPath()), + Files.readAllLines(gffFile.toPath()), "volvox_length.gff3 and volvox_length_output.gff3 are not equal"); } @@ -143,8 +143,8 @@ void testGetProteinSequences() throws Exception { tmp.deleteOnExit(); FastaWriterHelper.writeProteinSequence(tmp, proteinSequenceList.values()); Assertions.assertEquals( - Files.readString(new File("src/test/resources/volvox_all_reference.faa").toPath()), - Files.readString(tmp.toPath()), + Files.readAllLines(new File("src/test/resources/volvox_all_reference.faa").toPath()), + Files.readAllLines(tmp.toPath()), "volvox_all_reference.faa and volvox_all.faa are not equal"); }