Ticket #18138: 18138.6.patch

File 18138.6.patch, 25.4 KB (added by Traaker_L, 6 years ago)

New patch, am still working on addressing some sonar lint cognitive complexity issues with a few functions, and I'm still adding javadoc stuff, but functionally the patch is good to go. It checks for default connectivity based on placement tags, lane tags, and intersections as described on the wiki page, and I added a specific warning for missing commas in the connectivity tag.

  • data/defaultpresets.xml

     
    75807580                <role key="to" text="to way" requisite="required" count="1" type="way" />
    75817581            </roles>
    75827582        </item> <!-- Turn Restriction -->
     7583        <item name="Lane Connectivity" type="relation" preset_name_label="true" icon="presets/transport/way/relation_connectivity.svg">
     7584            <link wiki="Relation:connectivity" />
     7585            <space />
     7586            <key key="type" value="connectivity" />
     7587            <text key="connectivity" text="Lane Connectivity" />
     7588            <roles>
     7589                <role key="from" text="from way" requisite="required" count="1" type="way" />
     7590                <role key="via" text="via node or ways" requisite="required" type="way,node" />
     7591                <role key="to" text="to way" requisite="required" count="1" type="way" />
     7592            </roles>
     7593        </item> <!-- Lane Connectivity -->
    75837594        <item name="Enforcement" icon="presets/vehicle/restriction/speed_camera.svg" type="relation" preset_name_label="true">
    75847595            <link wiki="Relation:enforcement" />
    75857596            <space />
  • images/presets/transport/way/relation_connectivity.svg

     
     1<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 16 16" width="16" height="16">
     2  <path d="M0,0 h16v16h-16z" fill="#8d8"/>
     3  <path d="M1.5,16v-6.5l4,-4v-6l8,0v16.5" stroke="#fff" fill="#888"/>
     4  <path d="M9.5,1v2m0,2v2m0,2v2m0,2v2m-4,0v-2m0,-2v-2" stroke="#fff"/>
     5</svg>
  • src/org/openstreetmap/josm/data/validation/OsmValidator.java

     
    4242import org.openstreetmap.josm.data.validation.tests.BarriersEntrances;
    4343import org.openstreetmap.josm.data.validation.tests.Coastlines;
    4444import org.openstreetmap.josm.data.validation.tests.ConditionalKeys;
     45import org.openstreetmap.josm.data.validation.tests.ConnectivityRelations;
    4546import org.openstreetmap.josm.data.validation.tests.CrossingWays;
    4647import org.openstreetmap.josm.data.validation.tests.DuplicateNode;
    4748import org.openstreetmap.josm.data.validation.tests.DuplicateRelation;
     
    150151        PublicTransportRouteTest.class, // 3600 .. 3699
    151152        RightAngleBuildingTest.class, // 3700 .. 3799
    152153        SharpAngles.class, // 3800 .. 3899
     154        ConnectivityRelations.class, // 3900 .. 3999
    153155    };
    154156
    155157    /**
  • src/org/openstreetmap/josm/data/validation/tests/ConnectivityRelations.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.validation.tests;
     3
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5import static org.openstreetmap.josm.tools.I18n.trn;
     6
     7import java.util.ArrayList;
     8import java.util.Collections;
     9import java.util.Comparator;
     10import java.util.HashMap;
     11import java.util.List;
     12import java.util.Map;
     13import java.util.Map.Entry;
     14import java.util.Set;
     15import java.util.regex.Pattern;
     16
     17import org.openstreetmap.josm.data.osm.Node;
     18import org.openstreetmap.josm.data.osm.OsmPrimitive;
     19import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
     20import org.openstreetmap.josm.data.osm.Relation;
     21import org.openstreetmap.josm.data.osm.RelationMember;
     22import org.openstreetmap.josm.data.osm.Way;
     23import org.openstreetmap.josm.data.validation.Severity;
     24import org.openstreetmap.josm.data.validation.Test;
     25import org.openstreetmap.josm.data.validation.TestError;
     26
     27/**
     28 * Check for inconsistencies in lane information between relation and members.
     29 */
     30public class ConnectivityRelations extends Test {
     31
     32    protected static final int INCONSISTENT_LANE_COUNT = 3900;
     33
     34    protected static final int UNKNOWN_CONNECTIVITY_ROLE = 3901;
     35
     36    protected static final int NO_CONNECTIVITY_TAG = 3902;
     37
     38    protected static final int MALFORMED_CONNECTIVITY_TAG = 3903;
     39
     40    protected static final int MISSING_COMMA_CONNECTIVITY_TAG = 3904;
     41
     42    protected static final int TOO_MANY_ROLES = 3905;
     43
     44    protected static final int MISSING_ROLE = 3906;
     45
     46    protected static final int MEMBER_MISSING_LANES = 3907;
     47
     48    protected static final int CONNECTIVITY_IMPLIED = 3908;
     49
     50    private static final String CONNECTIVITY_TAG = "connectivity";
     51    private static final String VIA = "via";
     52    private static final String TO = "to";
     53    private static final String FROM = "from";
     54    private static final int BW = -1000;
     55    private static final Pattern OPTIONAL_LANE_PATTERN = Pattern.compile("\\([0-9-]+\\)");
     56    private static final Pattern TO_LANE_PATTERN = Pattern.compile("\\p{Zs}*[,:;]\\p{Zs}*");
     57    private static final Pattern MISSING_COMMA_PATTERN = Pattern.compile("[0-9]+\\([0-9]+\\)|\\([0-9]+\\)[0-9]+");
     58    private static final Pattern LANE_TAG_PATTERN = Pattern.compile(".*:lanes");
     59
     60    /**
     61    * Constructor
     62    */
     63    public ConnectivityRelations() {
     64        super(tr("Connectivity Relation Check"), tr("Checks that lane count of relation matches with lanes of members"));
     65    }
     66
     67    /**
     68     * Convert the connectivity tag into a map of values
     69     *
     70     * @param relation A relation with a {@code connectivity} tag.
     71     * @return A Map in the form of {@code Map<Lane From, Map<Lane To, Optional>>} May contain nulls when errors are encountered
     72     * @since xxx
     73     */
     74    public static Map<Integer, Map<Integer, Boolean>> parseConnectivityTag(Relation relation) {
     75        final String joined = relation.get(CONNECTIVITY_TAG).replace("bw", Integer.toString(BW));
     76
     77        if (joined == null) {
     78            return Collections.emptyMap();
     79        }
     80
     81        final Map<Integer, Map<Integer, Boolean>> result = new HashMap<>();
     82        String[] lanes = joined.split("\\|", -1);
     83        for (int i = 0; i < lanes.length; i++) {
     84            String[] lane = lanes[i].split(":", -1);
     85            int laneNumber;
     86            //Ignore connections from bw, since we cannot derive a lane number from bw
     87            if (!"bw".equals(lane[0])) {
     88                laneNumber = Integer.parseInt(lane[0].trim());
     89            } else {
     90                laneNumber = BW;
     91            }
     92            Map<Integer, Boolean> connections = new HashMap<>();
     93            String[] toLanes = TO_LANE_PATTERN.split(lane[1]);
     94            for (int j = 0; j < toLanes.length; j++) {
     95                String toLane = toLanes[j].trim();
     96                try {
     97                    if (OPTIONAL_LANE_PATTERN.matcher(toLane).matches()) {
     98                        toLane = toLane.replace("(", "").replace(")", "").trim();
     99                        if (!toLane.equals("bw")) {
     100                            connections.put(Integer.parseInt(toLane), Boolean.TRUE);
     101                        } else
     102                            connections.put(BW, Boolean.TRUE);
     103                    } else {
     104                        if (!toLane.contains("bw")) {
     105                            connections.put(Integer.parseInt(toLane), Boolean.FALSE);
     106                        } else {
     107                            connections.put(BW, Boolean.FALSE);
     108                        }
     109                    }
     110                } catch (NumberFormatException e) {
     111                    if (MISSING_COMMA_PATTERN.matcher(toLane).matches()) {
     112                        connections.put(null, true);
     113                    } else {
     114                        connections.put(null, null);
     115                    }
     116                }
     117            }
     118            result.put(laneNumber, connections);
     119        }
     120        return result;
     121    }
     122
     123    @Override
     124    public void visit(Relation r) {
     125        if (r.hasTag("type", CONNECTIVITY_TAG)) {
     126            if (!r.hasKey(CONNECTIVITY_TAG)) {
     127                errors.add(TestError.builder(this, Severity.WARNING, NO_CONNECTIVITY_TAG)
     128                        .message(tr("No connectivity tag in connectivity relation")).primitives(r).build());
     129            } else if (!r.hasIncompleteMembers()) {
     130                boolean badRole = checkForBadRole(r);
     131                boolean missingRole = checkForMissingRole(r);
     132                if (!badRole && !missingRole) {
     133                    Map<String, Integer> roleLanes = checkForInconsistentLanes(r);
     134                    checkForImpliedConnectivity(r, roleLanes);
     135                } else if (missingRole) {
     136                    createMissingRole(r);
     137                }
     138            }
     139        }
     140    }
     141
     142    /**
     143     * Compare lane tags of members to values in the {@code connectivity} tag of the relation
     144     *
     145     * @param relation A relation with a {@code connectivity} tag.
     146     * @return A Map in the form of {@code Map<Role, Lane Count>}
     147     * @since xxx
     148     */
     149    private Map<String, Integer> checkForInconsistentLanes(Relation relation) {
     150        StringBuilder lanelessRoles = new StringBuilder();
     151        int lanelessRolesCount = 0;
     152        // Lane count from connectivity tag
     153        Map<Integer, Map<Integer, Boolean>> connTagLanes = parseConnectivityTag(relation);
     154        // Lane count from member tags
     155        Map<String, Integer> roleLanes = new HashMap<>();
     156        for (RelationMember rM : relation.getMembers()) {
     157            // Check lanes
     158            if (rM.getType() == OsmPrimitiveType.WAY) {
     159                OsmPrimitive prim = rM.getMember();
     160                if (!VIA.equals(rM.getRole())) {
     161                    Map<String,String> primKeys = prim.getKeys();
     162                    List<Long> laneCounts = new ArrayList<>();
     163                    long maxLaneCount;
     164                    if (prim.hasTag("lanes")) {
     165                        laneCounts.add(Long.parseLong(prim.get("lanes")));
     166                    }
     167                    for (Entry<String,String> entry : primKeys.entrySet()) {
     168                        String thisKey = entry.getKey();
     169                        String thisValue = entry.getValue();
     170                        if (LANE_TAG_PATTERN.matcher(thisKey).matches()) {
     171                            //Count bar characters
     172                            long count = thisValue.chars().filter(ch -> ch == '|').count() + 1;
     173                            laneCounts.add(count);
     174                        }
     175                    }
     176
     177                    if (!laneCounts.equals(Collections.emptyList())) {
     178                        maxLaneCount = Collections.max(laneCounts);
     179                        roleLanes.put(rM.getRole(), (int)maxLaneCount);
     180                    }else {
     181                        String addString = "'" + rM.getRole() + "'";
     182                        StringBuilder sb = new StringBuilder(addString);
     183                        if (lanelessRoles.length() > 0) {
     184                            sb.insert(0, ", ");
     185                        }
     186                        lanelessRoles.append(sb.toString());
     187                        lanelessRolesCount++;
     188                    }
     189                }
     190            }
     191        }
     192
     193        if (lanelessRoles.toString().isEmpty()) {
     194            boolean fromCheck = roleLanes.get(FROM) < Collections
     195                    .max(connTagLanes.entrySet(), Comparator.comparingInt(Map.Entry::getKey)).getKey();
     196            boolean toCheck = false;
     197            for (Entry<Integer, Map<Integer, Boolean>> to : connTagLanes.entrySet()) {
     198                if (!to.getValue().containsKey(null)) {
     199                    toCheck = roleLanes.get(TO) < Collections
     200                            .max(to.getValue().entrySet(), Comparator.comparingInt(Map.Entry::getKey)).getKey();
     201                } else {
     202                    if (to.getValue().containsValue(true)) {
     203                        errors.add(TestError.builder(this, Severity.ERROR, MISSING_COMMA_CONNECTIVITY_TAG)
     204                                .message(tr("Connectivity tag missing comma between optional and non-optional values")).primitives(relation)
     205                                .build());
     206                    }else {
     207                        errors.add(TestError.builder(this, Severity.ERROR, MALFORMED_CONNECTIVITY_TAG)
     208                                .message(tr("Connectivity tag contains unusual data")).primitives(relation)
     209                                .build());
     210                    }
     211                }
     212            }
     213            if (fromCheck || toCheck) {
     214                errors.add(TestError.builder(this, Severity.WARNING, INCONSISTENT_LANE_COUNT)
     215                        .message(tr("Inconsistent lane numbering between relation and members")).primitives(relation)
     216                        .build());
     217            }
     218        } else {
     219            errors.add(TestError.builder(this, Severity.OTHER, MEMBER_MISSING_LANES)
     220                    .message(trn("Relation {0} member missing lanes tag", "Relation {0} members missing lanes tag",
     221                            lanelessRolesCount, lanelessRoles)).primitives(relation)
     222                    .build());
     223        }
     224        return roleLanes;
     225    }
     226
     227    /**
     228     * Check the relation to see if the connectivity described is already implied by other data
     229     *
     230     * @param relation A relation with a {@code connectivity} tag.
     231     * @param roleLanes The lane counts for each relation role
     232     * @since xxx
     233     */
     234    private void checkForImpliedConnectivity(Relation relation, Map<String, Integer> roleLanes) {
     235        boolean connImplied = true;
     236        Map<Integer, Map<Integer, Boolean>> connTagLanes = parseConnectivityTag(relation);
     237        // Don't flag connectivity as already implied when:
     238        // - Lane counts are different on the roads
     239        // - Placement tags convey the connectivity
     240        // - The relation passes through an intersection
     241        //   - If via member is a node, it's connected to ways not in the relation
     242        //   - If a via member is a way, ways not in the relation connect to its nodes
     243        // - Highways that appear to be merging have a different cumulative number of lanes than
     244        //   the highway that they're merging into
     245
     246        connImplied = checkMemberTagsForImpliedConnectivity(relation, roleLanes) && !checkForIntersectionAtMembers(relation);
     247        // Check if connectivity tag implies default connectivity
     248        if (connImplied) {
     249            for (Entry<Integer, Map<Integer, Boolean>> to : connTagLanes.entrySet()) {
     250                int fromLane = to.getKey();
     251                for (Entry<Integer, Boolean> lane : to.getValue().entrySet()) {
     252                    if (lane.getKey() != null && fromLane != lane.getKey()) {
     253                        connImplied = false;
     254                        break;
     255                    }
     256                }
     257                if (!connImplied)
     258                    break;
     259            }
     260        }
     261
     262        if (connImplied) {
     263            errors.add(TestError.builder(this, Severity.WARNING, CONNECTIVITY_IMPLIED)
     264                    .message(tr("This connectivity may already be implied")).primitives(relation)
     265                    .build());
     266        }
     267    }
     268
     269    /**
     270     * Check to see if there is an intersection present at the via member
     271     *
     272     * @param relation A relation with a {@code connectivity} tag.
     273     * @return A Boolean that indicates whether an intersection is present at the via member
     274     * @since xxx
     275     */
     276    private boolean checkForIntersectionAtMembers(Relation relation) {
     277        OsmPrimitive viaPrim = relation.findRelationMembers("via").get(0);
     278        Set<OsmPrimitive> relationMembers = relation.getMemberPrimitives();
     279
     280        if (viaPrim.getType() == OsmPrimitiveType.NODE) {
     281            Node viaNode = (Node)viaPrim;
     282            List<Way> parentWays = viaNode.getParentWays();
     283            if( parentWays.size() > 2) {
     284                for (Way thisWay : parentWays) {
     285                    if (!relationMembers.contains(thisWay) && thisWay.hasTag("highway")) {
     286                        return true;
     287                    }
     288                }
     289            }
     290        } else if (viaPrim.getType() == OsmPrimitiveType.WAY){
     291            Way viaWay = (Way)viaPrim;
     292            for (Node thisNode : viaWay.getNodes()) {
     293                List<Way> parentWays = thisNode.getParentWays();
     294                if( parentWays.size() > 2) {
     295                    for (Way thisWay : parentWays) {
     296                        if (!relationMembers.contains(thisWay) && thisWay.hasTag("highway")) {
     297                            return true;
     298                        }
     299                    }
     300                }
     301            }
     302        }
     303        return false;
     304    }
     305
     306    /**
     307     * Check the relation to see if the connectivity described is already implied by the relation members' tags
     308     *
     309     * @param relation A relation with a {@code connectivity} tag.
     310     * @param roleLanes The lane counts for each relation role
     311     * @return Whether connectivity is already implied by tags on relation members
     312     * @since xxx
     313     */
     314    private boolean checkMemberTagsForImpliedConnectivity(Relation relation, Map<String, Integer> roleLanes) {
     315        // The members have different lane counts
     316        if (roleLanes.containsKey(TO) && roleLanes.containsKey(FROM) && (!roleLanes.get(TO).equals(roleLanes.get(FROM)))) {
     317            return false;
     318        }
     319
     320        // The members don't have placement tags defining the connectivity
     321        List<RelationMember> members = relation.getMembers();
     322        Map<String,OsmPrimitive> toFromMembers = new HashMap<>();
     323        for (RelationMember mem : members) {
     324            if ( mem.getRole().equals(FROM)) {
     325                toFromMembers.put(FROM, mem.getMember());
     326            } else if (mem.getRole().equals(TO)){
     327                toFromMembers.put(TO, mem.getMember());
     328            }
     329        }
     330
     331        return toFromMembers.get(TO).hasKey("placement") || toFromMembers.get(FROM).hasKey("placement");
     332    }
     333
     334    private boolean checkForBadRole(Relation relation) {
     335        // Check role names
     336        int viaWays = 0;
     337        int viaNodes = 0;
     338        int toWays = 0;
     339        int fromWays = 0;
     340        for (RelationMember relationMember : relation.getMembers()) {
     341            if (relationMember.getMember() instanceof Way) {
     342                if (relationMember.hasRole(FROM))
     343                    fromWays++;
     344                else if (relationMember.hasRole(TO))
     345                    toWays++;
     346                else if (relationMember.hasRole(VIA))
     347                    viaWays++;
     348                else {
     349                    createUnknownRole(relation, relationMember.getMember());
     350                    return true;
     351                }
     352            } else if (relationMember.getMember() instanceof Node) {
     353                if (!relationMember.hasRole(VIA)) {
     354                    createUnknownRole(relation, relationMember.getMember());
     355                    return true;
     356                }
     357                viaNodes++;
     358            }
     359        }
     360        return mixedViaNodeAndWay(relation, viaWays, viaNodes, toWays, fromWays);
     361    }
     362
     363    private boolean checkForMissingRole(Relation relation) {
     364        List<String> necessaryRoles = new ArrayList<>();
     365        necessaryRoles.add(FROM);
     366        necessaryRoles.add(VIA);
     367        necessaryRoles.add(TO);
     368
     369        List<String> roleList = new ArrayList<>();
     370        for (RelationMember relationMember: relation.getMembers()) {
     371            roleList.add(relationMember.getRole());
     372        }
     373
     374        return !roleList.containsAll(necessaryRoles);
     375    }
     376
     377    private boolean mixedViaNodeAndWay(Relation relation, int viaWays, int viaNodes, int toWays, int fromWays) {
     378        String message = "";
     379        if ((viaWays != 0 && viaNodes != 0) || viaNodes > 1) {
     380            message = tr("Relation contains {1} {0} roles.", VIA, viaWays + viaNodes);
     381        } else if (toWays > 1) {
     382            message = tr("Relation contains too many {0} roles", TO);
     383        } else if (fromWays > 1) {
     384            message = tr("Relation contains too many {0} roles", FROM);
     385        }
     386        if (message.isEmpty()) {
     387            return false;
     388        } else {
     389            errors.add(TestError.builder(this, Severity.WARNING, TOO_MANY_ROLES)
     390                    .message(message).primitives(relation).build());
     391            return true;
     392        }
     393    }
     394
     395    private void createUnknownRole(Relation relation, OsmPrimitive primitive) {
     396        errors.add(TestError.builder(this, Severity.WARNING, UNKNOWN_CONNECTIVITY_ROLE)
     397                .message(tr("Unkown role in connectivity relation")).primitives(relation).highlight(primitive).build());
     398    }
     399
     400    private void createMissingRole(Relation relation) {
     401        errors.add(TestError.builder(this, Severity.WARNING, MISSING_ROLE)
     402                .message(tr("Connectivity relation is missing at least one necessary role")).primitives(relation)
     403                .build());
     404    }
     405}
  • test/unit/org/openstreetmap/josm/data/validation/tests/ConnectivityRelationsTest.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.validation.tests;
     3
     4import org.junit.Assert;
     5import org.junit.Before;
     6import org.junit.Test;
     7import org.openstreetmap.josm.JOSMFixture;
     8import org.openstreetmap.josm.TestUtils;
     9import org.openstreetmap.josm.data.coor.LatLon;
     10import org.openstreetmap.josm.data.osm.Node;
     11import org.openstreetmap.josm.data.osm.Relation;
     12import org.openstreetmap.josm.data.osm.RelationMember;
     13
     14/**
     15 * Test the ConnectivityRelations validation test
     16 *
     17 * @author Taylor Smock
     18 */
     19public class ConnectivityRelationsTest {
     20    private ConnectivityRelations check;
     21    private static final String CONNECTIVITY = "connectivity";
     22    /**
     23     * Setup test.
     24     *
     25     * @throws Exception if an error occurs
     26     */
     27    @Before
     28    public void setUp() throws Exception {
     29        JOSMFixture.createUnitTestFixture().init();
     30        check = new ConnectivityRelations();
     31    }
     32
     33    private Relation createDefaultTestRelation() {
     34        Node connection = new Node(new LatLon(0, 0));
     35        return TestUtils.newRelation("type=connectivity connectivity=1:1",
     36                new RelationMember("from", TestUtils.newWay("lanes=4", new Node(new LatLon(-0.1, -0.1)), connection)),
     37                new RelationMember("via", connection),
     38                new RelationMember("to", TestUtils.newWay("lanes=4", connection, new Node(new LatLon(0.1, 0.1)))));
     39    }
     40
     41    /**
     42     * Test for connectivity relations without a connectivity tag
     43     */
     44    @Test
     45    public void testNoConnectivityTag() {
     46        Relation relation = createDefaultTestRelation();
     47        check.visit(relation);
     48
     49        Assert.assertEquals(0, check.getErrors().size());
     50
     51        relation.remove(CONNECTIVITY);
     52        check.visit(relation);
     53        Assert.assertEquals(1, check.getErrors().size());
     54    }
     55
     56    /**
     57     * Check for lanes that don't make sense
     58     */
     59    @Test
     60    public void testMisMatchedLanes() {
     61        Relation relation = createDefaultTestRelation();
     62        check.visit(relation);
     63        int expectedFailures = 0;
     64
     65        Assert.assertEquals(expectedFailures, check.getErrors().size());
     66
     67        relation.put(CONNECTIVITY, "45000:1");
     68        check.visit(relation);
     69        Assert.assertEquals(++expectedFailures, check.getErrors().size());
     70
     71        relation.put(CONNECTIVITY, "1:45000");
     72        check.visit(relation);
     73        Assert.assertEquals(++expectedFailures, check.getErrors().size());
     74
     75        relation.put(CONNECTIVITY, "1:1,2");
     76        check.visit(relation);
     77        Assert.assertEquals(expectedFailures, check.getErrors().size());
     78
     79        relation.put(CONNECTIVITY, "1:1,(2)");
     80        check.visit(relation);
     81        Assert.assertEquals(expectedFailures, check.getErrors().size());
     82
     83        relation.put(CONNECTIVITY, "1:1,(20000)");
     84        check.visit(relation);
     85        Assert.assertEquals(++expectedFailures, check.getErrors().size());
     86    }
     87
     88    /**
     89     * Check for bad roles (not from/via/to)
     90     */
     91    @Test
     92    public void testForBadRole() {
     93        Relation relation = createDefaultTestRelation();
     94        check.visit(relation);
     95        int expectedFailures = 0;
     96
     97        Assert.assertEquals(expectedFailures, check.getErrors().size());
     98
     99        for (int i = 0; i < relation.getMembers().size(); i++) {
     100            String tRole = replaceMember(relation, i, "badRole");
     101            check.visit(relation);
     102            Assert.assertEquals(++expectedFailures, check.getErrors().size());
     103            replaceMember(relation, i, tRole);
     104            check.visit(relation);
     105            Assert.assertEquals(expectedFailures, check.getErrors().size());
     106        }
     107    }
     108
     109    private String replaceMember(Relation relation, int index, String replacementRole) {
     110        RelationMember relationMember = relation.getMember(index);
     111        String currentRole = relationMember.getRole();
     112        relation.removeMember(index);
     113        relation.addMember(index, new RelationMember(replacementRole, relationMember.getMember()));
     114        return currentRole;
     115    }
     116}