001package org.hl7.fhir.r5.renderers;
002
003import java.io.IOException;
004import java.io.UnsupportedEncodingException;
005import java.util.ArrayList;
006import java.util.Collections;
007import java.util.Comparator;
008import java.util.HashMap;
009import java.util.HashSet;
010import java.util.List;
011import java.util.Map;
012
013import org.hl7.fhir.exceptions.DefinitionException;
014import org.hl7.fhir.exceptions.FHIRException;
015import org.hl7.fhir.exceptions.FHIRFormatError;
016import org.hl7.fhir.r5.conformance.profile.ProfileUtilities;
017import org.hl7.fhir.r5.model.CodeSystem;
018import org.hl7.fhir.r5.model.Coding;
019import org.hl7.fhir.r5.model.ConceptMap;
020import org.hl7.fhir.r5.model.ConceptMap.ConceptMapGroupComponent;
021import org.hl7.fhir.r5.model.ConceptMap.ConceptMapGroupUnmappedMode;
022import org.hl7.fhir.r5.model.ConceptMap.MappingPropertyComponent;
023import org.hl7.fhir.r5.model.ConceptMap.OtherElementComponent;
024import org.hl7.fhir.r5.model.ConceptMap.SourceElementComponent;
025import org.hl7.fhir.r5.model.ConceptMap.TargetElementComponent;
026import org.hl7.fhir.r5.model.ContactDetail;
027import org.hl7.fhir.r5.model.ContactPoint;
028import org.hl7.fhir.r5.model.ElementDefinition;
029import org.hl7.fhir.r5.model.Enumerations.ConceptMapRelationship;
030import org.hl7.fhir.r5.model.Resource;
031import org.hl7.fhir.r5.model.StructureDefinition;
032import org.hl7.fhir.r5.renderers.utils.RenderingContext;
033import org.hl7.fhir.r5.renderers.utils.ResourceWrapper;
034import org.hl7.fhir.r5.utils.EOperationOutcome;
035import org.hl7.fhir.r5.utils.ToolingExtensions;
036import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
037import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage;
038import org.hl7.fhir.utilities.Utilities;
039import org.hl7.fhir.utilities.xhtml.NodeType;
040import org.hl7.fhir.utilities.xhtml.XhtmlNode;
041
042@MarkedToMoveToAdjunctPackage
043public class ConceptMapRenderer extends TerminologyRenderer {
044
045
046  public ConceptMapRenderer(RenderingContext context) { 
047    super(context); 
048  } 
049 
050  @Override
051  public void buildNarrative(RenderingStatus status, XhtmlNode x, ResourceWrapper r) throws FHIRFormatError, DefinitionException, IOException, FHIRException, EOperationOutcome {
052    if (r.isDirect()) {
053      renderResourceTechDetails(r, x);
054      genSummaryTable(status, x, (ConceptMap) r.getBase());
055      render(status, r, x, (ConceptMap) r.getBase(), false);      
056    } else {
057      // the intention is to change this in the future
058      x.para().tx("ConceptMapRenderer only renders native resources directly");
059    }
060  }
061  
062  @Override
063  public String buildSummary(ResourceWrapper r) throws UnsupportedEncodingException, IOException {
064    return canonicalTitle(r);
065  }
066
067  
068  
069  public static class CollateralDefinition {
070    private Resource resource;
071    private String label;
072    public CollateralDefinition(Resource resource, String label) {
073      super();
074      this.resource = resource;
075      this.label = label;
076    }
077    public Resource getResource() {
078      return resource;
079    }
080    public String getLabel() {
081      return label;
082    }
083  }
084
085  public enum RenderMultiRowSortPolicy {
086    UNSORTED, FIRST_COL, LAST_COL
087  }
088
089  public interface IMultiMapRendererAdvisor {
090    public RenderMultiRowSortPolicy sortPolicy(Object rmmContext);
091    public List<Coding> getMembers(Object rmmContext, String uri);
092    public boolean describeMap(Object rmmContext, ConceptMap map, XhtmlNode x);
093    public boolean hasCollateral(Object rmmContext);
094    public List<CollateralDefinition> getCollateral(Object rmmContext, String uri); // URI identifies which column the collateral is for
095    public String getLink(Object rmmContext, String system, String code);
096    public boolean makeMapLinks();
097  }
098  
099  public static class MultipleMappingRowSorter implements Comparator<MultipleMappingRow> {
100    private boolean first;
101    
102    protected MultipleMappingRowSorter(boolean first) {
103      super();
104      this.first = first;
105    }
106
107    @Override
108    public int compare(MultipleMappingRow o1, MultipleMappingRow o2) {
109      String s1 = first ? o1.firstCode() : o1.lastCode();
110      String s2 = first ? o2.firstCode() : o2.lastCode();
111      return s1.compareTo(s2);
112    }
113  }
114
115  public static class Cell {
116    private String system;
117    private String code;
118    private String display;
119    private String relationship;
120    private String relComment;
121    public boolean renderedRel;
122    public boolean renderedCode;
123    private Cell clone;
124    
125    protected Cell() {
126      super();
127    }
128
129    public Cell(String system, String code, String display) {
130      this.system = system;
131      this.code = code;
132      this.display = display;
133    }
134
135    public Cell(String system, String code, String relationship, String comment) {
136      this.system = system;
137      this.code = code;
138      this.relationship = relationship;
139      this.relComment = comment;
140    }
141
142    public boolean matches(String system, String code) {
143      return (system != null && system.equals(this.system)) && (code != null && code.equals(this.code));
144    }
145
146    public String present() {
147      if (system == null) {
148        return code;
149      } else {
150        return code; //+(clone == null ? "" : " (@"+clone.code+")");
151      }
152    }
153
154    public Cell copy(boolean clone) {
155      Cell res = new Cell();
156      res.system = system;
157      res.code = code;
158      res.display = display;
159      res.relationship = relationship;
160      res.relComment = relComment;
161      res.renderedRel = renderedRel;
162      res.renderedCode = renderedCode;
163      if (clone) {
164        res.clone = this;
165      }
166      return res;
167    }
168
169    @Override
170    public String toString() {
171      return relationship+" "+system + "#" + code + " \"" + display + "\"";
172    }
173    
174  }
175  
176
177  public static class MultipleMappingRowItem {
178    List<Cell> cells = new ArrayList<>();
179
180    @Override
181    public String toString() {
182      CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
183      for (Cell cell : cells) {
184        if (cell.relationship != null) {
185          b.append(cell.relationship+cell.code);
186        } else {
187          b.append(cell.code);
188        }
189      }
190      return b.toString();
191    }
192  }
193  
194  public static class MultipleMappingRow {
195    private List<MultipleMappingRowItem> rowSets = new ArrayList<>();
196    private MultipleMappingRow stickySource;
197
198    public MultipleMappingRow(int i, String system, String code, String display) {
199      MultipleMappingRowItem row = new MultipleMappingRowItem();
200      rowSets.add(row);
201      for (int c = 0; c < i; c++) {
202        row.cells.add(new Cell()); // blank cell spaces
203      }
204      row.cells.add(new Cell(system, code, display));
205    }
206
207
208    public MultipleMappingRow(MultipleMappingRow stickySource) {
209      this.stickySource = stickySource;
210    }
211
212    @Override
213    public String toString() {
214      CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
215      for (MultipleMappingRowItem rowSet : rowSets) {
216        b.append(""+rowSet.cells.size());
217      }
218      CommaSeparatedStringBuilder b2 = new CommaSeparatedStringBuilder(";");
219      for (MultipleMappingRowItem rowSet : rowSets) {
220        b2.append(rowSet.toString());
221      }
222      return ""+rowSets.size()+" ["+b.toString()+"] ("+b2.toString()+")";
223    }
224
225
226    public String lastCode() {
227      MultipleMappingRowItem first = rowSets.get(0);
228      for (int i = first.cells.size()-1; i >= 0; i--) {
229        if (first.cells.get(i).code != null) {
230          return first.cells.get(i).code;
231        }
232      }
233      return "";
234    }
235
236    public String firstCode() {
237      MultipleMappingRowItem first = rowSets.get(0);
238      for (int i = 0; i < first.cells.size(); i++) {
239        if (first.cells.get(i).code != null) {
240          return first.cells.get(i).code;
241        }
242      }
243      return "";
244    }
245
246    public void addSource(MultipleMappingRow sourceRow, List<MultipleMappingRow> rowList, ConceptMapRelationship relationship, String comment) {
247      // we already have a row, and we're going to collapse the rows on sourceRow into here, and add a matching terminus 
248      assert sourceRow.rowSets.get(0).cells.size() == rowSets.get(0).cells.size()-1;
249      rowList.remove(sourceRow);
250      Cell template = rowSets.get(0).cells.get(rowSets.get(0).cells.size()-1);
251      for (MultipleMappingRowItem row : sourceRow.rowSets) {
252        row.cells.add(new Cell(template.system, template.code, relationship.getSymbol(), comment));
253      }
254      rowSets.addAll(sourceRow.rowSets);
255    }
256
257    public void addTerminus() {
258      for (MultipleMappingRowItem row : rowSets) {
259        row.cells.add(new Cell(null, null, "X", null));
260      }
261    }
262
263    public void addTarget(String system, String code, ConceptMapRelationship relationship, String comment, List<MultipleMappingRow> sets, int colCount) {
264      if (rowSets.get(0).cells.size() == colCount+1) { // if it's already has a target for this col then we have to clone (and split) the rows
265        for (MultipleMappingRowItem row : rowSets) {
266          row.cells.add(new Cell(system, code, relationship.getSymbol(), comment));
267        }
268      } else {
269        MultipleMappingRow nrow = new MultipleMappingRow(this);
270        for (MultipleMappingRowItem row : rowSets) {
271          MultipleMappingRowItem n = new MultipleMappingRowItem();
272          for (int i = 0; i < row.cells.size()-1; i++) { // note to skip the last
273            n.cells.add(row.cells.get(i).copy(true));
274          }
275          n.cells.add(new Cell(system, code, relationship.getSymbol(), comment));
276          nrow.rowSets.add(n);
277        }
278        sets.add(sets.indexOf(this), nrow);
279      }
280    }
281
282    public String lastSystem() {
283      MultipleMappingRowItem first = rowSets.get(0);
284      for (int i = first.cells.size()-1; i >= 0; i--) {
285        if (first.cells.get(i).system != null) {
286          return first.cells.get(i).system;
287        }
288      }
289      return "";
290    }
291
292    public void addCopy(String system) {
293      for (MultipleMappingRowItem row : rowSets) {
294        row.cells.add(new Cell(system, lastCode(), "=", null));
295      }
296    }
297
298
299    public boolean alreadyHasMappings(int i) {
300      for (MultipleMappingRowItem row : rowSets) {
301        if (row.cells.size() > i+1) {
302          return true;
303        }
304      }
305      return false;
306    }
307
308
309    public Cell getLastSource(int i) {
310      for (MultipleMappingRowItem row : rowSets) {
311        return row.cells.get(i+1);
312      }
313      throw new Error("Should not get here");   // return null
314    }
315
316
317    public void cloneSource(int i, Cell cell) {
318      MultipleMappingRowItem row = new MultipleMappingRowItem();
319      rowSets.add(row);
320      for (int c = 0; c < i-1; c++) {
321        row.cells.add(new Cell()); // blank cell spaces
322      }
323      row.cells.add(cell.copy(true));
324      row.cells.add(rowSets.get(0).cells.get(rowSets.get(0).cells.size()-1).copy(false));      
325    }
326  }
327
328  
329
330  public void render(RenderingStatus status, ResourceWrapper res, XhtmlNode x, ConceptMap cm, boolean header) throws FHIRFormatError, DefinitionException, IOException {
331
332    if (context.isShowSummaryTable()) {
333      XhtmlNode h = x.h2();
334      h.addText(cm.hasTitle() ? cm.getTitle() : cm.getName());
335      addMarkdown(x, cm.getDescription());
336      if (cm.hasCopyright())
337        generateCopyright(x, res);
338    }
339
340    XhtmlNode p = x.para();
341    p.tx(context.formatPhrase(RenderingContext.CONC_MAP_FROM) + " ");
342    if (cm.hasSourceScope())
343      AddVsRef(cm.getSourceScope().primitiveValue(), p, cm);
344    else
345      p.tx(context.formatPhrase(RenderingContext.CONC_MAP_NOT_SPEC));
346    p.tx(" "+ (context.formatPhrase(RenderingContext.CONC_MAP_TO) + " "));
347    if (cm.hasTargetScope())
348      AddVsRef(cm.getTargetScope().primitiveValue(), p, cm);
349    else 
350      p.tx(context.formatPhrase(RenderingContext.CONC_MAP_NOT_SPEC));
351
352    x.br();
353    int gc = 0;
354
355    CodeSystem cs = getContext().getWorker().fetchCodeSystem("http://hl7.org/fhir/concept-map-relationship");
356    if (cs == null)
357      cs = getContext().getWorker().fetchCodeSystem("http://hl7.org/fhir/concept-map-equivalence");
358    String eqpath = cs == null ? null : cs.getWebPath();
359
360    for (ConceptMapGroupComponent grp : cm.getGroup()) {
361      boolean hasComment = false;
362      boolean hasProperties = false;
363      boolean ok = true;
364      Map<String, HashSet<String>> props = new HashMap<String, HashSet<String>>();
365      Map<String, HashSet<String>> sources = new HashMap<String, HashSet<String>>();
366      Map<String, HashSet<String>> targets = new HashMap<String, HashSet<String>>();
367      sources.put("code", new HashSet<String>());
368      targets.put("code", new HashSet<String>());
369      sources.get("code").add(grp.getSource());
370      targets.get("code").add(grp.getTarget());
371      for (SourceElementComponent ccl : grp.getElement()) {
372        ok = ok && (ccl.getNoMap() || (ccl.getTarget().size() == 1 && ccl.getTarget().get(0).getDependsOn().isEmpty() && ccl.getTarget().get(0).getProduct().isEmpty()));
373        if (ccl.hasExtension(ToolingExtensions.EXT_CM_NOMAP_COMMENT)) {
374          hasComment = true;
375        }
376        for (TargetElementComponent ccm : ccl.getTarget()) {
377          hasComment = hasComment || !Utilities.noString(ccm.getComment());
378          for (MappingPropertyComponent pp : ccm.getProperty()) {
379            if (!props.containsKey(pp.getCode()))
380              props.put(pp.getCode(), new HashSet<String>());            
381          }
382          for (OtherElementComponent d : ccm.getDependsOn()) {
383            if (!sources.containsKey(d.getAttribute()))
384              sources.put(d.getAttribute(), new HashSet<String>());
385          }
386          for (OtherElementComponent d : ccm.getProduct()) {
387            if (!targets.containsKey(d.getAttribute()))
388              targets.put(d.getAttribute(), new HashSet<String>());
389          }
390        }
391      }
392      if (props.size() > 0) {
393        hasProperties = true;
394      }
395      gc++;
396      if (gc > 1) {
397        x.hr();
398      }
399      StructureDefinition sdSrc = findSourceStructure(grp.getSource());
400      StructureDefinition sdTgt = findSourceStructure(grp.getTarget());
401      if (sdSrc != null && sdTgt != null) {
402        renderModelMap(sdSrc, sdTgt, status, res, x, gc, eqpath, grp, hasComment, hasProperties, ok, props, sources, targets);
403      } else {
404        renderCodeSystemMap(status, res, x, gc, eqpath, grp, hasComment, hasProperties, ok, props, sources, targets);
405      }
406    }
407  }
408
409  private StructureDefinition findSourceStructure(String source) {
410    if (source == null) {
411      return null;
412    }
413    String url = ProfileUtilities.getUrlFromCSUrl(source);
414    StructureDefinition sd = context.getContext().fetchResource(StructureDefinition.class, url);
415    if (sd != null) {
416      return sd;
417    }
418    for (StructureDefinition t : context.getContextUtilities().allStructures()) {
419      String u = ProfileUtilities.getCSUrl(t);
420      if (source.equals(u)) {
421        return t;
422      }
423    }
424    return null;
425  }
426
427  private void renderModelMap(StructureDefinition sdSrc, StructureDefinition sdTgt, RenderingStatus status, ResourceWrapper res, XhtmlNode x, int gc, String eqpath,
428      ConceptMapGroupComponent grp, boolean hasComment, boolean hasProperties, boolean ok,
429      Map<String, HashSet<String>> props, Map<String, HashSet<String>> sources, Map<String, HashSet<String>> targets)
430      throws UnsupportedEncodingException, IOException {
431    
432    XhtmlNode pp = x.para();
433    pp.b().tx(context.formatPhrase(RenderingContext.CONC_MAP_GRP, gc) + " ");
434    pp.tx(context.formatPhrase(RenderingContext.CONC_MAP_FROM) + " ");
435    pp.ah(sdSrc.getWebPath()).tx(sdSrc.present(context.getLocale().toLanguageTag()));
436    pp.tx(" to ");
437    pp.ah(sdTgt.getWebPath()).tx(sdTgt.present(context.getLocale().toLanguageTag()));
438    
439    XhtmlNode tbl = x.table( "grid", false);
440    XhtmlNode tr = tbl.tr();
441    tr.td().b().tx(context.formatPhrase(RenderingContext.CONC_MAP_SOURCE));
442    tr.td().b().tx(context.formatPhrase(RenderingContext.CONC_MAP_SOURCE_CARD));
443    tr.td().b().tx(context.formatPhrase(RenderingContext.CONC_MAP_SOURCE_TYPE));
444    tr.td().b().tx(context.formatPhrase(RenderingContext.CONC_MAP_REL));
445    tr.td().b().tx(context.formatPhrase(RenderingContext.CONC_MAP_TRGT));
446    tr.td().b().tx(context.formatPhrase(RenderingContext.CONC_MAP_TRGT_CARD));
447    tr.td().b().tx(context.formatPhrase(RenderingContext.CONC_MAP_TRGT_TYPE));
448    if (hasComment)
449      tr.td().b().tx(context.formatPhrase(RenderingContext.GENERAL_COMMENT));
450    for (SourceElementComponent ccl : grp.getElement()) {
451        tr = tbl.tr();
452        ElementDefinition edSrc = sdSrc.getSnapshot().getElementById(ccl.getCode());
453        if (edSrc == null) {        
454          tr.td().colspan(3).addText(ccl.getCode());
455        } else {
456          tr.td().ah(sdSrc.getWebPath()+"#"+ccl.getCode()).tx(ccl.getCode());
457          tr.td().tx(""+edSrc.getMin()+".."+edSrc.getMax());
458          tr.td().tx("todo");
459        }
460
461      if (ccl.getNoMap()) {
462        if (!hasComment) {
463          tr.td().colspan("5").style("background-color: #efefef").tx("(not mapped)");
464        } else if (ccl.hasExtension(ToolingExtensions.EXT_CM_NOMAP_COMMENT)) {
465          tr.td().colspan("5").style("background-color: #efefef").tx("(not mapped)");
466          tr.td().style("background-color: #efefef").tx(ccl.getExtensionString(ToolingExtensions.EXT_CM_NOMAP_COMMENT));
467        } else {
468          tr.td().colspan("4").style("background-color: #efefef").tx("(not mapped)");
469        }
470      } else {
471        boolean first = true;
472        for (TargetElementComponent ccm : ccl.getTarget()) {
473          if (first) {
474            first = false;
475          } else {
476            tr = tbl.tr();
477            tr.td().colspan(3).style("opacity: 0.5").addText("\"");
478          }
479          if (!ccm.hasRelationship()) {
480            tr.td().tx(":"+"("+ConceptMapRelationship.EQUIVALENT.toCode()+")");
481          } else {
482            if (ccm.hasExtension(ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE)) {
483              String code = ToolingExtensions.readStringExtension(ccm, ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE);
484              tr.td().ah(context.prefixLocalHref(eqpath+"#"+code), code).tx(presentEquivalenceCode(code));                
485            } else {
486              tr.td().ah(context.prefixLocalHref(eqpath+"#"+ccm.getRelationship().toCode()), ccm.getRelationship().toCode()).tx(presentRelationshipCode(ccm.getRelationship().toCode()));
487            }
488          }
489          ElementDefinition edTgt = sdTgt.getSnapshot().getElementById(ccm.getCode());
490          if (edTgt == null) {        
491            tr.td().colspan(3).addText(ccm.getCode());
492          } else {
493            tr.td().ah(sdTgt.getWebPath()+"#"+ccm.getCode()).tx(ccm.getCode());
494            tr.td().tx(""+edTgt.getMin()+".."+edTgt.getMax());
495            tr.td().tx("todo");
496          }
497  
498          if (hasComment)
499            tr.td().addText(ccm.getComment());
500        }
501      }
502    }
503  }
504  
505  private void renderCodeSystemMap(RenderingStatus status, ResourceWrapper res, XhtmlNode x, int gc, String eqpath,
506      ConceptMapGroupComponent grp, boolean hasComment, boolean hasProperties, boolean ok,
507      Map<String, HashSet<String>> props, Map<String, HashSet<String>> sources, Map<String, HashSet<String>> targets)
508      throws UnsupportedEncodingException, IOException {
509    XhtmlNode pp = x.para();
510    pp.b().tx(context.formatPhrase(RenderingContext.CONC_MAP_GRP, gc) + " ");
511    pp.tx(context.formatPhrase(RenderingContext.CONC_MAP_FROM) + " ");
512    if (grp.hasSource()) {
513      renderCanonical(status, res, pp, CodeSystem.class, grp.getSourceElement());
514    } else {
515      pp.code(context.formatPhrase(RenderingContext.CONC_MAP_CODE_SYS_UNSPEC));
516    }
517    pp.tx(" to ");
518    if (grp.hasTarget()) {
519      renderCanonical(status, res, pp, CodeSystem.class, grp.getTargetElement());
520    } else {
521      pp.code(context.formatPhrase(RenderingContext.CONC_MAP_CODE_SYS_UNSPEC));
522    }
523
524    String display;
525    if (ok) {
526      // simple
527      XhtmlNode tbl = x.table( "grid", false);
528      XhtmlNode tr = tbl.tr();
529      tr.td().b().tx(context.formatPhrase(RenderingContext.CONC_MAP_SOURCE));
530      tr.td().b().tx(context.formatPhrase(RenderingContext.CONC_MAP_REL));
531      tr.td().b().tx(context.formatPhrase(RenderingContext.CONC_MAP_TRGT));
532      if (hasComment)
533        tr.td().b().tx(context.formatPhrase(RenderingContext.GENERAL_COMMENT));
534      for (SourceElementComponent ccl : grp.getElement()) {
535        tr = tbl.tr();
536        XhtmlNode td = tr.td();
537        td.addText(ccl.getCode());
538        display = ccl.hasDisplay() ? ccl.getDisplay() : getDisplayForConcept(grp.getSource(), ccl.getCode());
539        if (display != null && !isSameCodeAndDisplay(ccl.getCode(), display))
540          td.tx(" ("+display+")");
541        if (ccl.getNoMap()) {
542          if (!hasComment) {
543            tr.td().colspan("2").style("background-color: #efefef").tx("(not mapped)");
544          } else if (ccl.hasExtension(ToolingExtensions.EXT_CM_NOMAP_COMMENT)) {
545            tr.td().colspan("2").style("background-color: #efefef").tx("(not mapped)");
546            tr.td().style("background-color: #efefef").tx(ccl.getExtensionString(ToolingExtensions.EXT_CM_NOMAP_COMMENT));
547          } else {
548            tr.td().colspan("3").style("background-color: #efefef").tx("(not mapped)");
549          }
550        } else {
551          boolean first = true;
552          for (TargetElementComponent ccm : ccl.getTarget()) {
553            if (first) {
554              first = false;
555            } else {
556              tr = tbl.tr();
557              tr.td().style("opacity: 0.5").tx("\"");
558            }
559            if (!ccm.hasRelationship())
560              tr.td().tx(":"+"("+ConceptMapRelationship.EQUIVALENT.toCode()+")");
561            else {
562              if (ccm.hasExtension(ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE)) {
563                String code = ToolingExtensions.readStringExtension(ccm, ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE);
564                tr.td().ah(context.prefixLocalHref(eqpath+"#"+code), code).tx(presentEquivalenceCode(code));                
565              } else {
566                tr.td().ah(context.prefixLocalHref(eqpath+"#"+ccm.getRelationship().toCode()), ccm.getRelationship().toCode()).tx(presentRelationshipCode(ccm.getRelationship().toCode()));
567              }
568            }
569            td = tr.td();
570            td.addText(ccm.getCode());
571            display = ccm.hasDisplay() ? ccm.getDisplay() : getDisplayForConcept(grp.getTarget(), ccm.getCode());
572            if (display != null && !isSameCodeAndDisplay(ccm.getCode(), display))
573              td.tx(" ("+display+")");
574            if (hasComment)
575              tr.td().addText(ccm.getComment());
576          }
577        }
578        addUnmapped(tbl, grp);
579      }      
580    } else {
581      boolean hasRelationships = false;
582      for (int si = 0; si < grp.getElement().size(); si++) {
583        SourceElementComponent ccl = grp.getElement().get(si);
584        for (int ti = 0; ti < ccl.getTarget().size(); ti++) {
585          TargetElementComponent ccm = ccl.getTarget().get(ti);
586          if (ccm.hasRelationship()) {
587            hasRelationships = true;
588          }  
589        }
590      }
591
592      XhtmlNode tbl = x.table("grid", false);
593      XhtmlNode tr = tbl.tr();
594      XhtmlNode td;
595      tr.td().colspan(Integer.toString(1+sources.size())).b().tx(context.formatPhrase(RenderingContext.CONC_MAP_SRC_DET));
596      if (hasRelationships) {
597        tr.td().b().tx(context.formatPhrase(RenderingContext.CONC_MAP_REL));
598      }
599      tr.td().colspan(Integer.toString(1+targets.size())).b().tx(context.formatPhrase(RenderingContext.CONC_MAP_TRGT_DET));
600      if (hasComment) {
601        tr.td().b().tx(context.formatPhrase(RenderingContext.GENERAL_COMMENT));
602      }
603      if (hasProperties) {
604        tr.td().colspan(Integer.toString(1+targets.size())).b().tx(context.formatPhrase(RenderingContext.GENERAL_PROPS));
605      }
606      tr = tbl.tr();
607      if (sources.get("code").size() == 1) {
608        String url = sources.get("code").iterator().next();
609        renderCSDetailsLink(tr, url, true);           
610      } else
611        tr.td().b().tx(context.formatPhrase(RenderingContext.GENERAL_CODE));
612      for (String s : sources.keySet()) {
613        if (s != null && !s.equals("code")) {
614          if (sources.get(s).size() == 1) {
615            String url = sources.get(s).iterator().next();
616            renderCSDetailsLink(tr, url, false);           
617          } else
618            tr.td().b().addText(getDescForConcept(s));
619        }
620      }
621      if (hasRelationships) {
622        tr.td();
623      }
624      if (targets.get("code").size() == 1) {
625        String url = targets.get("code").iterator().next();
626        renderCSDetailsLink(tr, url, true);           
627      } else
628        tr.td().b().tx(context.formatPhrase(RenderingContext.GENERAL_CODE));
629      for (String s : targets.keySet()) {
630        if (s != null && !s.equals("code")) {
631          if (targets.get(s).size() == 1) {
632            String url = targets.get(s).iterator().next();
633            renderCSDetailsLink(tr, url, false);           
634          } else
635            tr.td().b().addText(getDescForConcept(s));
636        }
637      }
638      if (hasComment) {
639        tr.td();
640      }
641      if (hasProperties) {
642        for (String s : props.keySet()) {
643          if (s != null) {
644            if (props.get(s).size() == 1) {
645              String url = props.get(s).iterator().next();
646              renderCSDetailsLink(tr.td(), url, false);           
647            } else
648              tr.td().b().addText(getDescForConcept(s));
649          }
650        }
651      }
652
653      for (int si = 0; si < grp.getElement().size(); si++) {
654        SourceElementComponent ccl = grp.getElement().get(si);
655        boolean slast = si == grp.getElement().size()-1;
656        boolean first = true;
657        if (ccl.hasNoMap() && ccl.getNoMap()) {
658          tr = tbl.tr();
659          td = tr.td().style("border-right-width: 0px");
660          if (!first)
661            td.style("border-top-style: none");
662          else 
663            td.style("border-bottom-style: none");
664          if (sources.get("code").size() == 1)
665            td.addText(ccl.getCode());
666          else
667            td.addText(grp.getSource()+" / "+ccl.getCode());
668          display = ccl.hasDisplay() ? ccl.getDisplay() : getDisplayForConcept(grp.getSource(), ccl.getCode());
669          tr.td().style("border-left-width: 0px").tx(display == null ? "" : display);
670          if (ccl.hasExtension(ToolingExtensions.EXT_CM_NOMAP_COMMENT)) {
671            tr.td().colspan("3").style("background-color: #efefef").tx("(not mapped)");
672            tr.td().style("background-color: #efefef").tx(ccl.getExtensionString(ToolingExtensions.EXT_CM_NOMAP_COMMENT));             
673          } else {
674            tr.td().colspan("4").style("background-color: #efefef").tx("(not mapped)");
675          }
676        } else {
677          for (int ti = 0; ti < ccl.getTarget().size(); ti++) {
678            TargetElementComponent ccm = ccl.getTarget().get(ti);
679            boolean last = ti == ccl.getTarget().size()-1;
680            tr = tbl.tr();
681            td = tr.td().style("border-right-width: 0px");
682            if (!first && !last)
683              td.style("border-top-style: none; border-bottom-style: none");
684            else if (!first)
685              td.style("border-top-style: none");
686            else if (!last)
687              td.style("border-bottom-style: none");
688            if (first) {
689              if (sources.get("code").size() == 1)
690                td.addText(ccl.getCode());
691              else
692                td.addText(grp.getSource()+" / "+ccl.getCode());
693              display = ccl.hasDisplay() ? ccl.getDisplay() : getDisplayForConcept(grp.getSource(), ccl.getCode());
694              td = tr.td();
695              if (!last)
696                td.style("border-left-width: 0px; border-bottom-style: none");
697              else
698                td.style("border-left-width: 0px");
699              td.tx(display == null ? "" : display);
700            } else {
701              td = tr.td(); // for display
702              if (!last)
703                td.style("border-left-width: 0px; border-top-style: none; border-bottom-style: none");
704              else
705                td.style("border-top-style: none; border-left-width: 0px");
706            }
707            for (String s : sources.keySet()) {
708              if (s != null && !s.equals("code")) {
709                td = tr.td();
710                td.addText(getValue(ccm.getDependsOn(), s, sources.get(s).size() != 1));
711                display = getDisplay(ccm.getDependsOn(), s);
712                if (display != null)
713                  td.tx(" ("+display+")");
714              }
715            }
716            first = false;
717            if (hasRelationships) {
718              if (!ccm.hasRelationship())
719                tr.td();
720              else {
721                if (ccm.hasExtension(ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE)) {
722                  String code = ToolingExtensions.readStringExtension(ccm, ToolingExtensions.EXT_OLD_CONCEPTMAP_EQUIVALENCE);
723                  tr.td().ah(context.prefixLocalHref(eqpath+"#"+code), code).tx(presentEquivalenceCode(code));                
724                } else {
725                  tr.td().ah(context.prefixLocalHref(eqpath+"#"+ccm.getRelationship().toCode()), ccm.getRelationship().toCode()).tx(presentRelationshipCode(ccm.getRelationship().toCode()));
726                }
727              }
728            }
729            td = tr.td().style("border-right-width: 0px");
730            if (targets.get("code").size() == 1)
731              td.addText(ccm.getCode());
732            else
733              td.addText(grp.getTarget()+" / "+ccm.getCode());
734            display = ccm.hasDisplay() ? ccm.getDisplay() : getDisplayForConcept(grp.getSource(), ccm.getCode());
735            tr.td().style("border-left-width: 0px").tx(display == null ? "" : display);
736
737            for (String s : targets.keySet()) {
738              if (s != null && !s.equals("code")) {
739                td = tr.td();
740                td.addText(getValue(ccm.getProduct(), s, targets.get(s).size() != 1));
741                display = getDisplay(ccm.getProduct(), s);
742                if (display != null)
743                  td.tx(" ("+display+")");
744              }
745            }
746            if (hasComment)
747              tr.td().addText(ccm.getComment());
748
749            for (String s : props.keySet()) {
750              if (s != null) {
751                td = tr.td();
752                td.addText(getValue(ccm.getProperty(), s));
753              }
754            }
755          }
756        }
757        addUnmapped(tbl, grp);
758      }
759    }
760  }
761
762  public void describe(XhtmlNode x, ConceptMap cm) {
763    x.tx(display(cm));
764  }
765
766  public String display(ConceptMap cm) {
767    return cm.present();
768  }
769
770  private boolean isSameCodeAndDisplay(String code, String display) {
771    String c = code.replace(" ", "").replace("-", "").toLowerCase();
772    String d = display.replace(" ", "").replace("-", "").toLowerCase();
773    return c.equals(d);
774  }
775
776
777  private String presentRelationshipCode(String code) {
778    if ("related-to".equals(code)) {
779      return "is related to";
780    } else if ("equivalent".equals(code)) {
781      return "is equivalent to";
782    } else if ("source-is-narrower-than-target".equals(code)) {
783      return "is narrower than";
784    } else if ("source-is-broader-than-target".equals(code)) {
785      return "is broader than";
786    } else if ("not-related-to".equals(code)) {
787      return "is not related to";
788    } else {
789      return code;
790    }
791  }
792
793  private String presentEquivalenceCode(String code) {
794    if ("relatedto".equals(code)) {
795      return "is related to";
796    } else if ("equivalent".equals(code)) {
797      return "is equivalent to";
798    } else if ("equal".equals(code)) {
799      return "is equal to";
800    } else if ("wider".equals(code)) {
801      return "maps to wider concept";
802    } else if ("subsumes".equals(code)) {
803      return "is subsumed by";
804    } else if ("source-is-broader-than-target".equals(code)) {
805      return "maps to narrower concept";
806    } else if ("specializes".equals(code)) {
807      return "has specialization";
808    } else if ("inexact".equals(code)) {
809      return "maps loosely to";
810    } else if ("unmatched".equals(code)) {
811      return "has no match";
812    } else if ("disjoint".equals(code)) {
813      return "is not related to";
814    } else {
815      return code;
816    }
817  }
818
819  public void renderCSDetailsLink(XhtmlNode tr, String url, boolean span2) {
820    CodeSystem cs;
821    XhtmlNode td;
822    cs = getContext().getWorker().fetchCodeSystem(url);
823    td = tr.td();
824    if (span2) {
825      td.colspan("2");
826    }
827    td.b().tx(context.formatPhrase(RenderingContext.CONC_MAP_CODES));
828    td.tx(" " + (context.formatPhrase(RenderingContext.CONC_MAP_FRM) + " "));
829    if (cs == null)
830      td.tx(url);
831    else
832      td.ah(context.prefixLocalHref(context.fixReference(cs.getWebPath()))).attribute("title", url).tx(cs.present());
833  }
834
835  private void addUnmapped(XhtmlNode tbl, ConceptMapGroupComponent grp) {
836    if (grp.hasUnmapped()) {
837//      throw new Error("not done yet");
838    }
839    
840  }
841
842  private String getDescForConcept(String s) {
843    if (s.startsWith("http://hl7.org/fhir/v2/element/"))
844        return "v2 "+s.substring("http://hl7.org/fhir/v2/element/".length());
845    return s;
846  }
847
848
849  private String getValue(List<MappingPropertyComponent> list, String s) {
850    StringBuilder b = new StringBuilder();
851    for (MappingPropertyComponent m : list) {
852      if (s.equals(m.getCode())) {
853        b.append(displayBase(m.getValue()));
854      }
855    }
856    return b.toString();
857  }
858
859  private String getValue(List<OtherElementComponent> list, String s, boolean withSystem) {
860    for (OtherElementComponent c : list) {
861      if (s.equals(c.getAttribute()))
862        if (withSystem)
863          return /*c.getSystem()+" / "+*/c.getValue().primitiveValue();
864        else
865          return c.getValue().primitiveValue();
866    }
867    return null;
868  }
869
870  private String getDisplay(List<OtherElementComponent> list, String s) {
871    for (OtherElementComponent c : list) {
872      if (s.equals(c.getAttribute())) {
873        // return getDisplayForConcept(systemFromCanonical(c.getSystem()), versionFromCanonical(c.getSystem()), c.getValue());
874      }
875    }
876    return null;
877  }
878
879  public static XhtmlNode renderMultipleMaps(String start, List<ConceptMap> maps, IMultiMapRendererAdvisor advisor, Object rmmContext) {
880    // 1+1 column for each provided map
881    List<MultipleMappingRow> rowSets = new ArrayList<>();
882    for (int i = 0; i < maps.size(); i++) {
883      populateRows(rowSets, maps.get(i), i, advisor, rmmContext);
884    }
885    collateRows(rowSets);
886    if (advisor.sortPolicy(rmmContext) != RenderMultiRowSortPolicy.UNSORTED) {
887      Collections.sort(rowSets, new MultipleMappingRowSorter(advisor.sortPolicy(rmmContext) == RenderMultiRowSortPolicy.FIRST_COL));
888    }
889    XhtmlNode div = new XhtmlNode(NodeType.Element, "div");
890    XhtmlNode tbl = div.table("none", false).style("text-align: var(--ig-left,left); border-spacing: 0; padding: 5px");
891    XhtmlNode tr = tbl.tr();
892    styleCell(tr.td(), false, true, 5).b().tx(start);
893    for (ConceptMap map : maps) {
894      XhtmlNode td = styleCell(tr.td(), false, true, 5).colspan(2);
895      if (!advisor.describeMap(rmmContext, map, td)) {
896        if (map.hasWebPath() && advisor.makeMapLinks()) {
897          td.b().ah(map.getWebPath(), map.getVersionedUrl()).tx(map.present());
898        } else {
899          td.b().tx(map.present());
900        }
901      }
902    }
903    if (advisor.hasCollateral(rmmContext)) {
904      tr = tbl.tr();
905      renderLinks(styleCell(tr.td(), false, true, 5), advisor.getCollateral(rmmContext, null));
906      for (ConceptMap map : maps) {
907        renderLinks(styleCell(tr.td(), false, true, 5).colspan(2), advisor.getCollateral(rmmContext, map.getUrl()));      
908      }
909    }
910    for (MultipleMappingRow row : rowSets) {
911      renderMultiRow(tbl, row, maps, advisor, rmmContext);
912    }
913    return div;
914  }
915
916  private static void renderLinks(XhtmlNode td, List<CollateralDefinition> collateral) {
917    if (collateral.size() > 0) {
918      td.tx( "Links:");
919      td.tx(" ");
920      boolean first = true;
921      for (CollateralDefinition c : collateral) {
922        if (first) first = false; else td.tx(", ");
923        td.ah(c.getResource().getWebPath()).tx(c.getLabel());
924      }
925    }
926  }
927
928  private static void collateRows(List<MultipleMappingRow> rowSets) {
929    List<MultipleMappingRow> toDelete = new ArrayList<ConceptMapRenderer.MultipleMappingRow>();
930    for (MultipleMappingRow rowSet : rowSets) {
931      MultipleMappingRow tgt = rowSet.stickySource;
932      while (toDelete.contains(tgt)) {
933        tgt = tgt.stickySource;
934      }
935      if (tgt != null && rowSets.contains(tgt)) {
936        tgt.rowSets.addAll(rowSet.rowSets);
937        toDelete.add(rowSet);
938      }
939    }
940    rowSets.removeAll(toDelete);    
941  }
942
943  private static void renderMultiRow(XhtmlNode tbl, MultipleMappingRow rows, List<ConceptMap> maps, IMultiMapRendererAdvisor advisor, Object rmmContext) {
944    int rowCounter = 0;
945    for (MultipleMappingRowItem row : rows.rowSets) {
946      XhtmlNode tr = tbl.tr();
947      boolean first = true;
948      int cellCounter = 0;
949      Cell last = null;
950      for (Cell cell : row.cells) {
951        if (first) {     
952          if (!cell.renderedCode) { 
953            int c = 1;
954            for (int i = rowCounter + 1; i < rows.rowSets.size(); i++) {
955              if (cell.code != null && rows.rowSets.get(i).cells.size() > cellCounter && cell.code.equals(rows.rowSets.get(i).cells.get(cellCounter).code)) {
956                rows.rowSets.get(i).cells.get(cellCounter).renderedCode = true;
957                c++;
958              } else {
959                break;
960              }
961            }  
962            if (cell.code == null) {
963              styleCell(tr.td(), rowCounter == 0, true, 5).rowspan(c).style("background-color: #eeeeee");
964            } else {
965              String link = advisor.getLink(rmmContext, cell.system, cell.code);
966              XhtmlNode x = null;
967              if (link != null) {
968                x = styleCell(tr.td(), rowCounter == 0, true, 5).attributeNN("title", cell.display).rowspan(c).ah(link);
969              } else {
970                x = styleCell(tr.td(), rowCounter == 0, true, 5).attributeNN("title", cell.display).rowspan(c);
971              }
972//              if (cell.clone != null) {
973//                x.style("color: grey");
974//              }
975              x.tx(cell.present());
976            }
977          }
978          first = false;
979        } else {
980          if (!cell.renderedRel) { 
981            int c = 1;
982            for (int i = rowCounter + 1; i < rows.rowSets.size(); i++) {
983              if ((cell.relationship != null && rows.rowSets.get(i).cells.size() > cellCounter && cell.relationship.equals(rows.rowSets.get(i).cells.get(cellCounter).relationship)) && 
984                  (cell.code != null && cell.code.equals(rows.rowSets.get(i).cells.get(cellCounter).code)) && 
985                  (last.code != null && cell.code.equals(rows.rowSets.get(i).cells.get(cellCounter-1).code))) {
986                rows.rowSets.get(i).cells.get(cellCounter).renderedRel = true;
987                c++;
988              } else {
989                break;
990              }
991            }
992            if (last.code == null || cell.code == null) {
993              styleCell(tr.td(), rowCounter == 0, true, 5).style("background-color: #eeeeee");
994            } else if (cell.relationship != null) {
995              styleCell(tr.tdW(16), rowCounter == 0, true, 0).attributeNN("title", cell.relComment).rowspan(c).style("background-color: LightGrey; text-align: center; vertical-align: middle; color: white").tx(cell.relationship);
996            } else {
997              styleCell(tr.tdW(16), rowCounter == 0, false, 0).rowspan(c);
998            }
999          }
1000          if (!cell.renderedCode) { 
1001            int c = 1;
1002            for (int i = rowCounter + 1; i < rows.rowSets.size(); i++) {
1003              if (cell.code != null && rows.rowSets.get(i).cells.size() > cellCounter && cell.code.equals(rows.rowSets.get(i).cells.get(cellCounter).code)) {
1004                rows.rowSets.get(i).cells.get(cellCounter).renderedCode = true;
1005                c++;
1006              } else {
1007                break;
1008              }
1009            }
1010            if (cell.code == null) {
1011              styleCell(tr.td(), rowCounter == 0, true, 5).rowspan(c).style("background-color: #eeeeee");
1012            } else {
1013              String link = advisor.getLink(rmmContext, cell.system, cell.code);
1014              XhtmlNode x = null;
1015              if (link != null) {
1016                x = styleCell(tr.td(), rowCounter == 0, true, 5).attributeNN("title", cell.display).rowspan(c).ah(link);
1017              } else {
1018                x = styleCell(tr.td(), rowCounter == 0, true, 5).attributeNN("title", cell.display).rowspan(c);                
1019              }
1020//              if (cell.clone != null) {
1021//                x.style("color: grey");
1022//              }
1023              x.tx(cell.present());
1024            }
1025          }
1026        }
1027        last = cell;
1028        cellCounter++;
1029      }
1030      rowCounter++;
1031    }    
1032  }
1033
1034  private static XhtmlNode styleCell(XhtmlNode td, boolean firstrow, boolean sides, int padding) {
1035    if (firstrow) {
1036      td.style("vertical-align: middle; border-top: 1px solid black; padding: "+padding+"px");
1037    } else {
1038      td.style("vertical-align: middle; border-top: 1px solid LightGrey; padding: "+padding+"px");
1039    }
1040    if (sides) {
1041      td.style("border-left: 1px solid LightGrey; border-right: 2px solid LightGrey");
1042    }
1043    return td;
1044  }
1045
1046  private static void populateRows(List<MultipleMappingRow> rowSets, ConceptMap map, int i, IMultiMapRendererAdvisor advisor, Object rmmContext) {
1047    // if we can resolve the value set, we create entries for it
1048    if (map.hasSourceScope()) {
1049      List<Coding> codings = advisor.getMembers(rmmContext, map.getSourceScope().primitiveValue());
1050      if (codings != null) {
1051        for (Coding c : codings) {
1052          MultipleMappingRow row = i == 0 ? null : findExistingRowBySource(rowSets, c.getSystem(), c.getCode(), i);
1053          if (row == null) {
1054            row = new MultipleMappingRow(i, c.getSystem(), c.getCode(), c.getDisplay());
1055            rowSets.add(row);
1056          } 
1057          
1058        }
1059      }
1060    }  
1061    
1062    for (ConceptMapGroupComponent grp : map.getGroup()) {
1063      for (SourceElementComponent src : grp.getElement()) {
1064        MultipleMappingRow row = findExistingRowBySource(rowSets, grp.getSource(), src.getCode(), i);
1065        if (row == null) {
1066          row = new MultipleMappingRow(i, grp.getSource(), src.getCode(), src.getDisplay());
1067          rowSets.add(row);
1068        } 
1069        if (src.getNoMap()) {
1070          row.addTerminus();
1071        } else {
1072          List<TargetElementComponent> todo = new ArrayList<>();
1073          for (TargetElementComponent tgt : src.getTarget()) {
1074            MultipleMappingRow trow = findExistingRowByTarget(rowSets, grp.getTarget(), tgt.getCode(), i);
1075            if (trow == null) {
1076              row.addTarget(grp.getTarget(), tgt.getCode(), tgt.getRelationship(), tgt.getComment(), rowSets, i);
1077            } else {
1078              todo.add(tgt);
1079            }
1080          }
1081          // we've already got a mapping to these targets. So we gather them under the one mapping - but do this after the others are done
1082          for (TargetElementComponent t : todo) {
1083            MultipleMappingRow trow = findExistingRowByTarget(rowSets, grp.getTarget(), t.getCode(), i);     
1084            if (row.alreadyHasMappings(i)) {
1085              // src is already mapped, and so is target, and now we need to map src to target too
1086              // we have to clone src, but we only clone the last
1087              trow.cloneSource(i, row.getLastSource(i));
1088            } else {
1089              trow.addSource(row, rowSets, t.getRelationship(), t.getComment());
1090            }
1091          }
1092        }
1093      }
1094      boolean copy = grp.hasUnmapped() && grp.getUnmapped().getMode() == ConceptMapGroupUnmappedMode.USESOURCECODE;
1095      if (copy) {
1096        for (MultipleMappingRow row : rowSets) {
1097          if (row.rowSets.get(0).cells.size() == i && row.lastSystem().equals(grp.getSource())) {
1098            row.addCopy(grp.getTarget());
1099          }
1100        }
1101      }
1102    } 
1103    for (MultipleMappingRow row : rowSets) {
1104      if (row.rowSets.get(0).cells.size() == i) {
1105        row.addTerminus();
1106      }
1107    }
1108    if (map.hasTargetScope()) {
1109      List<Coding> codings = advisor.getMembers(rmmContext, map.getTargetScope().primitiveValue());
1110      if (codings != null) {
1111        for (Coding c : codings) {
1112          MultipleMappingRow row = findExistingRowByTarget(rowSets, c.getSystem(), c.getCode(), i);
1113          if (row == null) {
1114            row = new MultipleMappingRow(i+1, c.getSystem(), c.getCode(), c.getDisplay());
1115            rowSets.add(row);
1116          } else {
1117            for (MultipleMappingRowItem cells : row.rowSets) {
1118              Cell last = cells.cells.get(cells.cells.size() -1);
1119              if (last.system != null && last.system.equals(c.getSystem()) && last.code.equals(c.getCode()) && last.display == null) {
1120                last.display = c.getDisplay();
1121              }
1122            }
1123          }
1124        }
1125      }
1126    }
1127
1128  }
1129
1130  private static MultipleMappingRow findExistingRowByTarget(List<MultipleMappingRow> rows, String system, String code, int i) {
1131    for (MultipleMappingRow row : rows) {
1132      for (MultipleMappingRowItem cells : row.rowSets) {
1133        if (cells.cells.size() > i + 1 && cells.cells.get(i+1).matches(system, code)) {
1134          return row;
1135        }
1136      }
1137    }
1138    return null;
1139  }
1140
1141  private static MultipleMappingRow findExistingRowBySource(List<MultipleMappingRow> rows, String system, String code, int i) {
1142    for (MultipleMappingRow row : rows) {
1143      for (MultipleMappingRowItem cells : row.rowSets) {
1144        if (cells.cells.size() > i && cells.cells.get(i).matches(system, code)) {
1145          return row;
1146        }
1147      }
1148    }
1149    return null;
1150  }
1151}