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