From fdea751663ec46a587cfdf45bfae9ec667043efb Mon Sep 17 00:00:00 2001 From: gmungoc Date: Thu, 6 Apr 2017 16:08:36 +0100 Subject: [PATCH] JAL-2446 added contains, delete, checks for inclusion on add + tests --- src/jalview/datamodel/features/FeatureStore.java | 119 +++++++++++++--- src/jalview/datamodel/features/NCList.java | 148 ++++++++++++++++++-- src/jalview/datamodel/features/NCNode.java | 40 ++++++ .../datamodel/features/SequenceFeatures.java | 29 +++- .../datamodel/features/FeatureStoreTest.java | 102 +++++++++++++- test/jalview/datamodel/features/NCListTest.java | 81 ++++++++++- test/jalview/datamodel/features/NCNodeTest.java | 24 ++++ .../datamodel/features/SequenceFeaturesTest.java | 22 ++- 8 files changed, 520 insertions(+), 45 deletions(-) diff --git a/src/jalview/datamodel/features/FeatureStore.java b/src/jalview/datamodel/features/FeatureStore.java index f7757be..cb4bd6f 100644 --- a/src/jalview/datamodel/features/FeatureStore.java +++ b/src/jalview/datamodel/features/FeatureStore.java @@ -59,66 +59,84 @@ public class FeatureStore public FeatureStore() { nonNestedFeatures = new ArrayList(); - // we only construct contactFeatures and the NCList if we need to + // we only construct nonPositionalFeatures, contactFeatures + // or the NCList if we need to } /** - * Add one entry to the data store + * Adds one sequence feature to the store, and returns true, unless the + * feature is already contained in the store, in which case this method + * returns false. Containment is determined by SequenceFeature.equals() + * comparison. * * @param feature */ - public void addFeature(SequenceFeature feature) + public boolean addFeature(SequenceFeature feature) { + boolean added = false; + if (feature.isContactFeature()) { - addContactFeature(feature); + added = addContactFeature(feature); } else if (feature.isNonPositional()) { - addNonPositionalFeature(feature); + added = addNonPositionalFeature(feature); } else { - boolean added = addNonNestedFeature(feature); - if (!added) + if (!nonNestedFeatures.contains(feature)) { - /* - * detected a nested feature - put it in the NCList structure - */ - addNestedFeature(feature); + added = addNonNestedFeature(feature); + if (!added) + { + /* + * detected a nested feature - put it in the NCList structure + */ + added = addNestedFeature(feature); + } } } + + return added; } /** * Adds the feature to the list of non-positional features (with lazy - * instantiation of the list if it is null) + * instantiation of the list if it is null), and returns true. If the + * non-positional features already include the new feature (by equality test), + * then it is not added, and this method returns false. * * @param feature */ - protected void addNonPositionalFeature(SequenceFeature feature) + protected boolean addNonPositionalFeature(SequenceFeature feature) { if (nonPositionalFeatures == null) { nonPositionalFeatures = new ArrayList(); } + if (nonPositionalFeatures.contains(feature)) + { + return false; + } nonPositionalFeatures.add(feature); + return true; } /** * Adds one feature to the NCList that can manage nested features (creating - * the NCList if necessary) + * the NCList if necessary), and returns true. If the feature is already + * stored in the NCList (by equality test), then it is not added, and this + * method returns false. */ - protected synchronized void addNestedFeature(SequenceFeature feature) + protected synchronized boolean addNestedFeature(SequenceFeature feature) { if (nestedFeatures == null) { nestedFeatures = new NCList(feature); + return true; } - else - { - nestedFeatures.add(feature); - } + return nestedFeatures.add(feature, false); } /** @@ -225,13 +243,14 @@ public class FeatureStore /** * Add a contact feature to the lists that hold them ordered by start (first * contact) and by end (second contact) position, ensuring the lists remain - * ordered + * ordered, and returns true. If the contact feature lists already contain the + * given feature (by test for equality), does not add it and returns false. * * @param feature + * @return */ - protected synchronized void addContactFeature(SequenceFeature feature) + protected synchronized boolean addContactFeature(SequenceFeature feature) { - // TODO binary search for insertion points! if (contactFeatureStarts == null) { contactFeatureStarts = new ArrayList(); @@ -240,10 +259,19 @@ public class FeatureStore { contactFeatureEnds = new ArrayList(); } + + // TODO binary search for insertion points! + if (contactFeatureStarts.contains(feature)) + { + return false; + } + contactFeatureStarts.add(feature); Collections.sort(contactFeatureStarts, startOrdering); contactFeatureEnds.add(feature); Collections.sort(contactFeatureEnds, endOrdering); + + return true; } /** @@ -534,4 +562,51 @@ public class FeatureStore } return new ArrayList(nonPositionalFeatures); } + + /** + * Deletes the given feature from the store, returning true if it was found + * (and deleted), else false. This method makes no assumption that the feature + * is in the 'expected' place in the store, in case it has been modified since + * it was added. + * + * @param sf + */ + public boolean delete(SequenceFeature sf) + { + /* + * try the non-nested positional features first + */ + boolean removed = nonNestedFeatures.remove(sf); + + /* + * if not found, try contact positions (and if found, delete + * from both lists of contact positions) + */ + if (!removed && contactFeatureStarts != null) + { + removed = contactFeatureStarts.remove(sf); + if (removed) + { + contactFeatureEnds.remove(sf); + } + } + + /* + * if not found, try non-positional features + */ + if (!removed && nonPositionalFeatures != null) + { + removed = nonPositionalFeatures.remove(sf); + } + + /* + * if not found, try nested features + */ + if (!removed && nestedFeatures != null) + { + removed = nestedFeatures.delete(sf); + } + + return removed; + } } diff --git a/src/jalview/datamodel/features/NCList.java b/src/jalview/datamodel/features/NCList.java index 02f94b6..17e08eb 100644 --- a/src/jalview/datamodel/features/NCList.java +++ b/src/jalview/datamodel/features/NCList.java @@ -60,7 +60,7 @@ public class NCList */ Collections.sort(ranges, intervalSorter); - List sublists = findSubranges(ranges); + List sublists = buildSubranges(ranges); /* * convert each subrange to an NCNode consisting of a range and @@ -95,7 +95,7 @@ public class NCList * @param ranges * @return */ - protected List findSubranges(List ranges) + protected List buildSubranges(List ranges) { List sublists = new ArrayList(); @@ -124,12 +124,31 @@ public class NCList } /** - * Adds one entry to the stored set + * Adds one entry to the stored set (with duplicates allowed) * * @param entry */ - public synchronized void add(T entry) + public void add(T entry) { + add(entry, true); + } + + /** + * Adds one entry to the stored set, and returns true, unless allowDuplicates + * is set to false and it is already contained (by object equality test), in + * which case it is not added and this method returns false. + * + * @param entry + * @param allowDuplicates + * @return + */ + public synchronized boolean add(T entry, boolean allowDuplicates) + { + if (!allowDuplicates && contains(entry)) + { + return false; + } + size++; long start = entry.getBegin(); long end = entry.getEnd(); @@ -153,7 +172,7 @@ public class NCList * all subranges precede this one - add it on the end */ subranges.add(new NCNode(entry)); - return; + return true; } /* @@ -175,7 +194,7 @@ public class NCList * new entry lies between subranges j-1 j */ subranges.add(j, new NCNode(entry)); - return; + return true; } if (subrange.getStart() <= start && subrange.getEnd() >= end) @@ -184,7 +203,7 @@ public class NCList * push new entry inside this subrange as it encloses it */ subrange.add(entry); - return; + return true; } if (start <= subrange.getStart()) @@ -214,7 +233,7 @@ public class NCList * entry encloses one or more preceding subranges */ addEnclosingRange(entry, firstEnclosed, lastEnclosed); - return; + return true; } else { @@ -223,7 +242,7 @@ public class NCList * so just add it */ subranges.add(j, new NCNode(entry)); - return; + return true; } } } @@ -245,9 +264,51 @@ public class NCList { subranges.add(new NCNode(entry)); } + + return true; } /** + * Answers true if this NCList contains the given entry (by object equality + * test), else false + * + * @param entry + * @return + */ + public boolean contains(T entry) + { + /* + * find the first sublist that might overlap, i.e. + * the first whose end position is >= from + */ + int candidateIndex = findFirstOverlap(entry.getBegin()); + + if (candidateIndex == -1) + { + return false; + } + + int to = entry.getEnd(); + + for (int i = candidateIndex; i < subranges.size(); i++) + { + NCNode candidate = subranges.get(i); + if (candidate.getStart() > to) + { + /* + * we are past the end of our target range + */ + break; + } + if (candidate.contains(entry)) + { + return true; + } + } + return false; + } + + /** * Update the tree so that the range of the new entry encloses subranges i to * j (inclusive). That is, replace subranges i-j (inclusive) with a new * subrange that contains them. @@ -296,8 +357,7 @@ public class NCList * @param to * @param result */ - protected void findOverlaps(long from, long to, - List result) + protected void findOverlaps(long from, long to, List result) { /* * find the first sublist that might overlap, i.e. @@ -494,4 +554,70 @@ public class NCList subrange.getEntries(result); } } + + /** + * Deletes the given entry from the store, returning true if it was found (and + * deleted), else false. This method makes no assumption that the entry is in + * the 'expected' place in the store, in case it has been modified since it + * was added. Only the first 'same object' match is deleted, not 'equal' or + * multiple objects. + * + * @param entry + */ + public synchronized boolean delete(T entry) + { + if (entry == null) + { + return false; + } + for (int i = 0; i < subranges.size(); i++) + { + NCNode subrange = subranges.get(i); + NCList subRegions = subrange.getSubRegions(); + + if (subrange.getRegion() == entry) + { + /* + * if the subrange is rooted on this entry, promote its + * subregions (if any) to replace the subrange here + */ + subranges.remove(i); + if (subRegions != null) + { + subranges.addAll(i, subRegions.subranges); + } + size--; + return true; + } + else + { + if (subRegions != null && subRegions.delete(entry)) + { + size--; + return true; + } + } + } + return false; + } + + /** + * Answers true if this contains no ranges + * + * @return + */ + public boolean isEmpty() + { + return getSize() == 0; + } + + /** + * Answer the list of subranges held in this NCList + * + * @return + */ + List> getSubregions() + { + return subranges; + } } diff --git a/src/jalview/datamodel/features/NCNode.java b/src/jalview/datamodel/features/NCNode.java index 6af913a..0755614 100644 --- a/src/jalview/datamodel/features/NCNode.java +++ b/src/jalview/datamodel/features/NCNode.java @@ -178,4 +178,44 @@ class NCNode subregions.getEntries(entries); } } + + /** + * Answers true if this object contains the given entry (by object equals + * test), else false + * + * @param entry + * @return + */ + boolean contains(V entry) + { + if (entry == null) + { + return false; + } + if (entry.equals(region)) + { + return true; + } + return subregions == null ? false : subregions.contains(entry); + } + + /** + * Answers the 'root' region modelled by this object + * + * @return + */ + V getRegion() + { + return region; + } + + /** + * Answers the (possibly null) contained regions within this object + * + * @return + */ + NCList getSubRegions() + { + return subregions; + } } diff --git a/src/jalview/datamodel/features/SequenceFeatures.java b/src/jalview/datamodel/features/SequenceFeatures.java index c947407..64aa63e 100644 --- a/src/jalview/datamodel/features/SequenceFeatures.java +++ b/src/jalview/datamodel/features/SequenceFeatures.java @@ -34,11 +34,14 @@ public class SequenceFeatures } /** - * Add one sequence feature to the store + * Adds one sequence feature to the store, and returns true, unless the + * feature is already contained in the store, in which case this method + * returns false. Containment is determined by SequenceFeature.equals() + * comparison. * * @param sf */ - public void add(SequenceFeature sf) + public boolean add(SequenceFeature sf) { String type = sf.getType(); @@ -46,7 +49,7 @@ public class SequenceFeatures { featureStore.put(type, new FeatureStore()); } - featureStore.get(type).addFeature(sf); + return featureStore.get(type).addFeature(sf); } /** @@ -167,4 +170,24 @@ public class SequenceFeatures } return result; } + + /** + * Deletes the given feature from the store, returning true if it was found + * (and deleted), else false. This method makes no assumption that the feature + * is in the 'expected' place in the store, in case it has been modified since + * it was added. + * + * @param sf + */ + public boolean delete(SequenceFeature sf) + { + for (FeatureStore featureSet : featureStore.values()) + { + if (featureSet.delete(sf)) + { + return true; + } + } + return false; + } } diff --git a/test/jalview/datamodel/features/FeatureStoreTest.java b/test/jalview/datamodel/features/FeatureStoreTest.java index 7147c51..88a7616 100644 --- a/test/jalview/datamodel/features/FeatureStoreTest.java +++ b/test/jalview/datamodel/features/FeatureStoreTest.java @@ -19,7 +19,8 @@ public class FeatureStoreTest FeatureStore fs = new FeatureStore(); fs.addFeature(new SequenceFeature("", "", 10, 20, Float.NaN, null)); - fs.addFeature(new SequenceFeature("", "", 10, 20, Float.NaN, null)); + // same range different description + fs.addFeature(new SequenceFeature("", "desc", 10, 20, Float.NaN, null)); fs.addFeature(new SequenceFeature("", "", 15, 25, Float.NaN, null)); fs.addFeature(new SequenceFeature("", "", 20, 35, Float.NaN, null)); @@ -49,7 +50,10 @@ public class FeatureStoreTest SequenceFeature sf1 = addFeature(fs, 10, 50); SequenceFeature sf2 = addFeature(fs, 10, 40); SequenceFeature sf3 = addFeature(fs, 20, 30); - SequenceFeature sf4 = addFeature(fs, 20, 30); + // fudge feature at same location but different group (so is added) + SequenceFeature sf4 = new SequenceFeature("", "", 20, 30, Float.NaN, + "different group"); + fs.addFeature(sf4); SequenceFeature sf5 = addFeature(fs, 35, 36); List overlaps = fs.findOverlappingFeatures(1, 9); @@ -237,8 +241,8 @@ public class FeatureStoreTest SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20, Float.NaN, null); store.addFeature(sf1); - // same range - SequenceFeature sf2 = new SequenceFeature("Metal", "desc", 10, 20, + // same range, different description + SequenceFeature sf2 = new SequenceFeature("Metal", "desc2", 10, 20, Float.NaN, null); store.addFeature(sf2); // discontiguous range @@ -272,4 +276,94 @@ public class FeatureStoreTest assertTrue(features.contains(sf6)); assertTrue(features.contains(sf7)); } + + @Test(groups = "Functional") + public void testDelete() + { + FeatureStore store = new FeatureStore(); + SequenceFeature sf1 = addFeature(store, 10, 20); + assertTrue(store.getFeatures().contains(sf1)); + + /* + * simple deletion + */ + assertTrue(store.delete(sf1)); + assertTrue(store.getFeatures().isEmpty()); + + /* + * non-positional feature deletion + */ + SequenceFeature sf2 = addFeature(store, 0, 0); + assertTrue(store.getFeatures().contains(sf2)); + assertTrue(store.delete(sf2)); + assertTrue(store.getFeatures().isEmpty()); + + /* + * contact feature deletion + */ + SequenceFeature sf3 = new SequenceFeature("", "Disulphide Bond", 11, + 23, Float.NaN, null); + store.addFeature(sf3); + assertEquals(store.getFeatures().size(), 1); + assertTrue(store.getFeatures().contains(sf3)); + assertTrue(store.delete(sf3)); + assertTrue(store.getFeatures().isEmpty()); + + /* + * nested feature deletion + */ + SequenceFeature sf4 = addFeature(store, 20, 30); + SequenceFeature sf5 = addFeature(store, 22, 26); // to NCList + SequenceFeature sf6 = addFeature(store, 23, 24); // child of sf5 + SequenceFeature sf7 = addFeature(store, 25, 25); // sibling of sf6 + SequenceFeature sf8 = addFeature(store, 24, 24); // child of sf6 + SequenceFeature sf9 = addFeature(store, 23, 23); // child of sf6 + assertEquals(store.getFeatures().size(), 6); + + // delete a node with children - they take its place + assertTrue(store.delete(sf6)); // sf8, sf9 should become children of sf5 + assertEquals(store.getFeatures().size(), 5); + assertFalse(store.getFeatures().contains(sf6)); + + // delete a node with no children + assertTrue(store.delete(sf7)); + assertEquals(store.getFeatures().size(), 4); + assertFalse(store.getFeatures().contains(sf7)); + + // delete root of NCList + assertTrue(store.delete(sf5)); + assertEquals(store.getFeatures().size(), 3); + assertFalse(store.getFeatures().contains(sf5)); + + // continue the killing fields + assertTrue(store.delete(sf4)); + assertEquals(store.getFeatures().size(), 2); + assertFalse(store.getFeatures().contains(sf4)); + + assertTrue(store.delete(sf9)); + assertEquals(store.getFeatures().size(), 1); + assertFalse(store.getFeatures().contains(sf9)); + + assertTrue(store.delete(sf8)); + assertTrue(store.getFeatures().isEmpty()); + } + + @Test(groups = "Functional") + public void testAddFeature() + { + FeatureStore fs = new FeatureStore(); + + SequenceFeature sf1 = new SequenceFeature("Cath", "", 10, 20, + Float.NaN, null); + SequenceFeature sf2 = new SequenceFeature("Cath", "", 10, 20, + Float.NaN, null); + + assertTrue(fs.addFeature(sf1)); + + /* + * re-adding the same or an identical feature should fail + */ + assertFalse(fs.addFeature(sf1)); + assertFalse(fs.addFeature(sf2)); + } } diff --git a/test/jalview/datamodel/features/NCListTest.java b/test/jalview/datamodel/features/NCListTest.java index ca4288c..e0d3659 100644 --- a/test/jalview/datamodel/features/NCListTest.java +++ b/test/jalview/datamodel/features/NCListTest.java @@ -1,7 +1,11 @@ package jalview.datamodel.features; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertSame; import static org.testng.Assert.assertTrue; +import jalview.datamodel.SequenceFeature; + import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -355,7 +359,46 @@ public class NCListTest @Test(groups = "Functional") public void testDelete() { - assertTrue(false, "todo"); + List ranges = new ArrayList(); + Range r1 = new Range(20, 30); + ranges.add(r1); + NCList ncl = new NCList(ranges); + assertTrue(ncl.getEntries().contains(r1)); + + Range r2 = new Range(20, 30); + assertFalse(ncl.delete(null)); // null argument + assertFalse(ncl.delete(r2)); // never added + assertTrue(ncl.delete(r1)); // success + assertTrue(ncl.getEntries().isEmpty()); + + /* + * tests where object.equals() == true + */ + NCList features = new NCList(); + SequenceFeature sf1 = new SequenceFeature("type", "desc", 1, 10, 2f, + "group"); + SequenceFeature sf2 = new SequenceFeature("type", "desc", 1, 10, 2f, + "group"); + features.add(sf1); + assertEquals(sf1, sf2); // sf1.equals(sf2) + assertFalse(features.delete(sf2)); // equality is not enough for deletion + assertTrue(features.getEntries().contains(sf1)); // still there! + assertTrue(features.delete(sf1)); + assertTrue(features.getEntries().isEmpty()); // gone now + + /* + * test with duplicate objects in NCList + */ + features.add(sf1); + features.add(sf1); + assertEquals(features.getEntries().size(), 2); + assertSame(features.getEntries().get(0), sf1); + assertSame(features.getEntries().get(1), sf1); + assertTrue(features.delete(sf1)); // first match only is deleted + assertTrue(features.contains(sf1)); + assertEquals(features.getSize(), 1); + assertTrue(features.delete(sf1)); + assertTrue(features.getEntries().isEmpty()); } @Test(groups = "Functional") @@ -389,4 +432,40 @@ public class NCListTest assertEquals(ncl.toString(), "[15-25, 20-30, 25-35, 40-50, 45-55]"); assertTrue(ncl.isValid()); } + + /** + * Test the contains method (which uses object equals test) + */ + @Test(groups = "Functional") + public void testContains() + { + NCList ncl = new NCList(); + SequenceFeature sf1 = new SequenceFeature("type", "desc", 1, 10, 2f, + "group"); + SequenceFeature sf2 = new SequenceFeature("type", "desc", 1, 10, 2f, + "group"); + SequenceFeature sf3 = new SequenceFeature("type", "desc", 1, 10, 2f, + "anothergroup"); + ncl.add(sf1); + + assertTrue(ncl.contains(sf1)); + assertTrue(ncl.contains(sf2)); // sf1.equals(sf2) + assertFalse(ncl.contains(sf3)); // !sf1.equals(sf3) + + /* + * make some deeper structure in the NCList + */ + SequenceFeature sf4 = new SequenceFeature("type", "desc", 2, 9, 2f, + "group"); + ncl.add(sf4); + assertTrue(ncl.contains(sf4)); + SequenceFeature sf5 = new SequenceFeature("type", "desc", 4, 5, 2f, + "group"); + SequenceFeature sf6 = new SequenceFeature("type", "desc", 6, 8, 2f, + "group"); + ncl.add(sf5); + ncl.add(sf6); + assertTrue(ncl.contains(sf5)); + assertTrue(ncl.contains(sf6)); + } } diff --git a/test/jalview/datamodel/features/NCNodeTest.java b/test/jalview/datamodel/features/NCNodeTest.java index da0aa4e..73f957e 100644 --- a/test/jalview/datamodel/features/NCNodeTest.java +++ b/test/jalview/datamodel/features/NCNodeTest.java @@ -1,8 +1,11 @@ package jalview.datamodel.features; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertTrue; +import jalview.datamodel.SequenceFeature; + import java.util.ArrayList; import java.util.List; @@ -75,4 +78,25 @@ public class NCNodeTest assertTrue(entries.contains(r1)); assertTrue(entries.contains(r2)); } + + /** + * Tests for the contains method (uses entry.equals() test) + */ + @Test(groups = "Functional") + public void testContains() + { + SequenceFeature sf1 = new SequenceFeature("type", "desc", 1, 10, 2f, + "group"); + SequenceFeature sf2 = new SequenceFeature("type", "desc", 1, 10, 2f, + "group"); + SequenceFeature sf3 = new SequenceFeature("type", "desc", 1, 10, 2f, + "anothergroup"); + NCNode node = new NCNode(sf1); + + assertFalse(node.contains(null)); + assertTrue(node.contains(sf1)); + assertTrue(node.contains(sf2)); // sf1.equals(sf2) + assertFalse(node.contains(sf3)); // !sf1.equals(sf3) + } + } diff --git a/test/jalview/datamodel/features/SequenceFeaturesTest.java b/test/jalview/datamodel/features/SequenceFeaturesTest.java index 7edd67d..d340490 100644 --- a/test/jalview/datamodel/features/SequenceFeaturesTest.java +++ b/test/jalview/datamodel/features/SequenceFeaturesTest.java @@ -19,8 +19,8 @@ public class SequenceFeaturesTest SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20, Float.NaN, null); store.add(sf1); - // same range - SequenceFeature sf2 = new SequenceFeature("Metal", "desc", 10, 20, + // same range, different description + SequenceFeature sf2 = new SequenceFeature("Metal", "desc2", 10, 20, Float.NaN, null); store.add(sf2); // discontiguous range @@ -170,8 +170,8 @@ public class SequenceFeaturesTest SequenceFeature sf6 = new SequenceFeature("Disulfide bond", "desc", 18, 45, Float.NaN, null); store.add(sf6); - // on more non-positional - SequenceFeature sf7 = new SequenceFeature("Pfam", "desc", 0, 0, + // one more non-positional, different description + SequenceFeature sf7 = new SequenceFeature("Pfam", "desc2", 0, 0, Float.NaN, null); store.add(sf7); @@ -283,4 +283,18 @@ public class SequenceFeaturesTest assertEquals(overlaps.size(), 1); assertTrue(overlaps.contains(sf13)); } + + @Test(groups = "Functional") + public void testDelete() + { + SequenceFeatures sf = new SequenceFeatures(); + SequenceFeature sf1 = addFeature(sf, "Pfam", 10, 50); + assertTrue(sf.getFeatures().contains(sf1)); + + assertFalse(sf.delete(null)); + SequenceFeature sf2 = new SequenceFeature("Cath", "", 10, 15, 0f, null); + assertFalse(sf.delete(sf2)); // not added, can't delete it + assertTrue(sf.delete(sf1)); + assertTrue(sf.getFeatures().isEmpty()); + } } -- 1.7.10.2