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