001package org.hl7.fhir.r5.conformance.profile;
002
003import java.util.ArrayList;
004import java.util.HashMap;
005import java.util.HashSet;
006import java.util.List;
007import java.util.Map;
008import java.util.Set;
009
010import org.hl7.fhir.r5.extensions.ExtensionDefinitions;
011import org.hl7.fhir.r5.extensions.ExtensionUtilities;
012import org.hl7.fhir.r5.model.Element;
013import org.hl7.fhir.r5.model.ElementDefinition;
014import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionMappingComponent;
015import org.hl7.fhir.r5.model.StructureDefinition;
016import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionMappingComponent;
017
018import org.hl7.fhir.r5.utils.UserDataNames;
019import org.hl7.fhir.utilities.CSVReader;
020import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
021import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage;
022import org.hl7.fhir.utilities.Utilities;
023import org.hl7.fhir.utilities.VersionUtilities;
024
025@MarkedToMoveToAdjunctPackage
026public class MappingAssistant {
027
028
029  public enum MappingMergeModeOption {
030    DUPLICATE, // if there's more than one mapping for the same URI, just keep them all
031    IGNORE, // if there's more than one, keep the first 
032    OVERWRITE, // if there's more than one, keep the last 
033    APPEND, // if there's more than one, append them with ';' 
034  }
035  
036  private MappingMergeModeOption mappingMergeMode = MappingMergeModeOption.APPEND;
037  private StructureDefinition base;
038  private StructureDefinition derived;
039  
040  private List<StructureDefinitionMappingComponent> masterList= new ArrayList<StructureDefinition.StructureDefinitionMappingComponent>();
041  private Map<String, String> renames = new HashMap<>();
042  private String version;
043  private List<String> suppressedMappings= new ArrayList<>();
044  
045  public MappingAssistant(MappingMergeModeOption mappingMergeMode, StructureDefinition base, StructureDefinition derived, String version, List<String> suppressedMappings) {
046    this.mappingMergeMode = mappingMergeMode;
047    this.base = base;
048    this.derived = derived;
049    this.version = version;
050    if (suppressedMappings != null) {
051      this.suppressedMappings = suppressedMappings;
052    }
053    
054    // figure out where we're going to be: 
055    // mappings declared in derived get priority; we do not change them either 
056    for (StructureDefinitionMappingComponent m : derived.getMapping()) {
057      masterList.add(m);
058      if (!isSuppressed(m)) {
059        m.setUserData(UserDataNames.mappings_inherited, true);
060      }
061    }
062    
063    // now, look at the base profile. If mappings in there match one in the derived, then we use that, otherwise, we add it to the list 
064    for (StructureDefinitionMappingComponent m : base.getMapping()) {
065      if (notExcluded(m)) {
066        StructureDefinitionMappingComponent md = findMatchInDerived(m);
067        if (md == null) {
068          if (nameExists(m.getIdentity())) {
069            int i = 1;
070            String n = m.getIdentity() + i;
071            while (nameExists(n)) {
072              i++;
073              n = m.getIdentity() + i;
074            }
075            renames.put(m.getIdentity(), n);
076            masterList.add(m.copy().setName(n));
077          } else {
078            masterList.add(m.copy());
079          }
080        } else {
081          if (!md.hasName() && m.hasName()) {
082            md.setName(m.getName());
083          }
084          if (!md.hasUri() && m.hasUri()) {
085            md.setUri(m.getUri());
086          }
087          if (!md.hasComment() && m.hasComment()) {
088            md.setComment(m.getComment());
089          }
090          if (!m.getIdentity().equals(md.getIdentity())) {
091            renames.put(m.getIdentity(), md.getIdentity());
092          }
093        }
094      }
095    }
096  }
097
098  private boolean notExcluded(StructureDefinitionMappingComponent m) {
099    if (!m.hasUri()) {
100      return true;
101    }
102    return !Utilities.existsInList(m.getUri(), suppressedMappings);
103  }
104
105  private boolean notExcluded(ElementDefinitionMappingComponent m) {
106    if (!m.hasIdentity()) {
107      return false;
108    }
109    StructureDefinitionMappingComponent mm = null;
110    for (StructureDefinitionMappingComponent t : base.getMapping()) {
111      if (m.getIdentity().equals(t.getIdentity())) {
112        mm = t;
113        break;
114      }
115    }
116    if (mm == null) {
117      return false;
118    } else {
119      return notExcluded(mm);
120    }
121  }
122  
123  private boolean nameExists(String n) {
124    for (StructureDefinitionMappingComponent md : masterList) {
125      if (n.equals(md.getIdentity())) {
126        return true;
127      }      
128    }
129    return false;
130  }
131
132  private StructureDefinitionMappingComponent findMatchInDerived(StructureDefinitionMappingComponent m) {
133    for (StructureDefinitionMappingComponent md : derived.getMapping()) {
134      // if the URIs match, they match, irregardless of anything else
135      if (md.hasUri() && m.hasUri() && md.getUri().equals(m.getUri())) {
136        return md;
137      }
138      // if the codes match
139      if (md.hasIdentity() && m.hasIdentity() && md.getIdentity().equals(m.getIdentity())) {
140        // the names have to match if present
141        if (!md.hasName() || !m.hasName() || md.getName().equals(m.getName())) {
142          return md;
143        }
144      }
145      
146    }
147    return null;
148  }
149
150  public void update() {
151
152    Set<StructureDefinitionMappingComponent> usedList= new HashSet<StructureDefinition.StructureDefinitionMappingComponent>();
153    for (ElementDefinition ed : derived.getSnapshot().getElement()) {
154      for (ElementDefinitionMappingComponent m : ed.getMapping()) {
155        StructureDefinitionMappingComponent def = findDefinition(m.getIdentity());
156        if (def != null && notExcluded(m)) {
157          usedList.add(def);
158        } else {
159          // not sure what to do?
160        }
161      }
162    }
163    
164    derived.getMapping().clear();
165    for (StructureDefinitionMappingComponent t : masterList) {
166      if (usedList.contains(t) || t.hasUserData(UserDataNames.mappings_inherited)) {
167        derived.getMapping().add(t);
168      }
169    }
170  }
171
172
173  public void merge(ElementDefinition base, ElementDefinition derived) {
174    List<ElementDefinitionMappingComponent> list = new ArrayList<>();
175    addMappings(list, base.getMapping(), renames);
176    if (derived.hasMapping()) {
177      addMappings(list, derived.getMapping(), null);
178    }
179    derived.setMapping(list);
180    
181    // trim anything
182    for (ElementDefinitionMappingComponent m : base.getMapping()) {
183      if (m.hasMap()) {
184        m.setMap(m.getMap().trim());
185      }
186    }
187
188  }
189
190  private void addMappings(List<ElementDefinitionMappingComponent> destination, List<ElementDefinitionMappingComponent> source, Map<String, String> renames2) {
191    for (ElementDefinitionMappingComponent s : source) {
192      if (!isSuppressed(s)) {
193        String name = s.getIdentity();
194        if (!isSuppressed(name)) {
195          if (renames2 != null && renames2.containsKey(name)) {
196            name = renames2.get(name);
197          }
198
199          boolean found = false;
200          for (ElementDefinitionMappingComponent d : destination) {
201            if (compareMaps(name, s, d)) {
202              found = true;
203              d.setUserData(UserDataNames.SNAPSHOT_DERIVATION_EQUALS, true);
204              break;
205            }
206          }
207          if (!found) {
208            destination.add(s.setIdentity(name));
209          }
210        }
211      }
212    }
213  }
214
215  private boolean isSuppressed(String name) {
216    StructureDefinitionMappingComponent m = findDefinition(name);
217    return m != null && isSuppressed(m);
218  }
219
220  private boolean isSuppressed(Element s) {
221    return ExtensionUtilities.readBoolExtension(s, ExtensionDefinitions.EXT_SUPPRESSED);
222  }
223
224  private StructureDefinitionMappingComponent findDefinition(String name) {
225    for (StructureDefinitionMappingComponent t : masterList) {
226      if (t.getIdentity().equals(name)) {
227        return t;
228      }
229    }
230    return null;
231  }
232
233  private boolean compareMaps(String name, ElementDefinitionMappingComponent s, ElementDefinitionMappingComponent d) {
234    
235    if (d.getIdentity().equals(name) && d.getMap().equals(s.getMap())) {
236      return true;
237    }
238    if (VersionUtilities.isR5Plus(version)) {
239      if (d.getIdentity().equals(name)) {
240        switch (mappingMergeMode) {
241        case APPEND:
242          if (!Utilities.splitStrings(d.getMap(), "\\,").contains(s.getMap())) {
243            d.setMap(mergeMaps(d.getMap(), s.getMap()));
244          }
245          return true;
246        case DUPLICATE:
247          return false;
248        case IGNORE:
249          d.setMap(s.getMap());
250          return true;
251        case OVERWRITE:
252          return true;
253        default:
254          return false;
255        }
256      } else {
257        return false;
258      }
259    } else {
260      return false;
261    }
262  }
263
264  private String mergeMaps(String map, String map2) {
265    List<String> csv1 = CSVReader.splitString(map);
266    List<String> csv2 = CSVReader.splitString(map2);
267    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(",");
268    for (String s : csv1) {
269      b.append(s);
270    }
271    for (String s : csv2) {
272      if (!csv1.contains(s)) {
273        b.append(s);
274      }
275    }
276    return b.toString();
277  }
278
279}