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