001package org.hl7.fhir.r5.profilemodel.gen;
002
003import java.io.File;
004import java.io.IOException;
005import java.text.SimpleDateFormat;
006import java.util.List;
007import java.util.Locale;
008import java.util.ArrayList;
009import java.util.Date;
010
011import org.hl7.fhir.r5.context.IWorkerContext;
012import org.hl7.fhir.r5.model.CodeableConcept;
013import org.hl7.fhir.r5.model.Identifier;
014import org.hl7.fhir.r5.model.Observation;
015import org.hl7.fhir.r5.model.StructureDefinition;
016import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind;
017import org.hl7.fhir.r5.profilemodel.PEBuilder;
018import org.hl7.fhir.r5.profilemodel.PEBuilder.PEElementPropertiesPolicy;
019import org.hl7.fhir.r5.profilemodel.gen.PECodeGenerator.ExtensionPolicy;
020import org.hl7.fhir.r5.profilemodel.PEDefinition;
021import org.hl7.fhir.r5.profilemodel.PEInstance;
022import org.hl7.fhir.r5.profilemodel.PEType;
023import org.hl7.fhir.utilities.TextFile;
024import org.hl7.fhir.utilities.Utilities;
025
026public class PECodeGenerator {
027
028  public enum ExtensionPolicy {
029    None, Complexes, Primitives;
030  }
031  public static final SimpleDateFormat DATE_FORMAT() {
032    return new SimpleDateFormat("EEE, MMM d, yyyy HH:mmZ", new Locale("en", "US"));
033  }
034  private class PEGenClass {
035    private String name;
036    private String base;
037    private String doco;
038    private String url;
039    private boolean isResource;
040    private StringBuilder fields = new StringBuilder();
041    private StringBuilder load = new StringBuilder();
042    private StringBuilder save = new StringBuilder();
043    private StringBuilder clear = new StringBuilder();
044    private StringBuilder copy = new StringBuilder();
045    private StringBuilder accessors = new StringBuilder();
046    private StringBuilder hash = new StringBuilder();
047
048    public void genId() {
049      if (isResource) {
050        genField(true, "id", "String", "id", "", false, "");
051        genAccessors(true, false, "id", "String", "", "String", "String", "Id", "Ids", false, "", false);   
052        genLoad(true, false, "id", "IdType", "", "String", "String", "Id", "Ids", false, false, null);
053        genClear(false, "id");
054      }
055    }
056    public void write(StringBuilder b) {
057      w(b);
058      w(b, "// Generated by the HAPI Java Profile Generator, "+DATE_FORMAT().format(new Date()));      
059      w(b);      
060      jdoc(b, doco, 0, true);
061      w(b, "public class "+name+" extends PEGeneratedBase {");
062      w(b);
063      if (url != null) {
064        w(b, "  private static final String CANONICAL_URL = \""+url+"\";");
065        w(b);        
066      }
067      w(b, fields.toString());
068      jdoc(b, "Parameter-less constructor. If you use this, the fixed values won't be filled out - they'll be missing. They'll be filled in if/when you call build, so they won't be missing from the resource, only from this particular object model", 2, true);
069      w(b, "  public "+name+"() {");
070      w(b, "    // todo");
071      w(b, "  }");
072      w(b);
073      if (isResource) {
074        jdoc(b, "Construct an instance of the object, and fill out all the fixed values ", 2, true);
075        w(b, "  public "+name+"(IWorkerContext context) {");
076        w(b, "    workerContext = context;");
077        w(b, "    PEBuilder builder = new PEBuilder(context, PEElementPropertiesPolicy.EXTENSION, true);");
078        w(b, "    PEInstance src = builder.buildPEInstance(CANONICAL_URL, builder.createResource(CANONICAL_URL, false));");
079        w(b, "    load(src);");
080        w(b, "  }");
081        w(b);
082        jdoc(b, "Populate an instance of the object based on this source object ", 2, true);
083        w(b, "  public static "+name+" fromSource(IWorkerContext context, "+base+" source) {");
084        w(b, "    "+name+" theThing = new "+name+"();");
085        w(b, "    theThing.workerContext = context;");
086        w(b, "    PEBuilder builder = new PEBuilder(context, PEElementPropertiesPolicy.EXTENSION, true);");
087        w(b, "    PEInstance src = builder.buildPEInstance(CANONICAL_URL, source);");
088        w(b, "    theThing.load(src);");
089        w(b, "    return theThing;");
090        w(b, "  }");  
091        w(b);
092      } else {
093        jdoc(b, "Used when loading other models ", 2, true);
094        w(b, "  public static "+name+" fromSource(PEInstance source) {");
095        w(b, "    "+name+" theThing = new "+name+"();");
096        w(b, "    theThing.workerContext = source.getContext();");
097        w(b, "    theThing.load(source);");
098        w(b, "    return theThing;");
099        w(b, "  }");  
100      }
101      w(b);
102      w(b, "  public void load(PEInstance src) {");
103      w(b, "    clear();");
104      w(b, load.toString());
105      w(b, "  }");  
106      w(b);
107
108      if (isResource) {
109        jdoc(b, "Build an instance of the object based on this source object ", 2, true);
110        w(b, "  public "+base+" build(IWorkerContext context) {");
111        w(b, "    workerContext = context;");
112        w(b, "    "+base+" theThing = new "+base+"();");
113        w(b, "    PEBuilder builder = new PEBuilder(context, PEElementPropertiesPolicy.EXTENSION, true);");
114        w(b, "    PEInstance tgt = builder.buildPEInstance(CANONICAL_URL, theThing);");      
115        w(b, "    save(tgt, false);");
116        w(b, "    return theThing;");
117        w(b, "  }");
118        w(b);
119        jdoc(b, "Save this profile class into an existing resource (overwriting enything that exists in the profile) ", 2, true);
120        w(b, "  public void save(IWorkerContext context, "+base+" dest, boolean nulls) {");
121        w(b, "    workerContext = context;");
122        w(b, "    PEBuilder builder = new PEBuilder(context, PEElementPropertiesPolicy.EXTENSION, true);");
123        w(b, "    PEInstance tgt = builder.buildPEInstance(CANONICAL_URL, dest);");
124        w(b, "    save(tgt, nulls);");
125        w(b, "  }");
126        w(b);
127      }
128      w(b, "  public void save(PEInstance tgt, boolean nulls) {");
129      w(b, save.toString());
130      w(b, "  }");  
131      w(b);
132      w(b, accessors.toString());
133      w(b);
134      w(b, "  public void clear() {");
135      w(b, clear.toString());
136      w(b, "  }");  
137      w(b);
138      w(b, "}");  
139    }
140
141    private void defineField(PEDefinition source, PEDefinition field) {
142      if (field.types().size() == 1) {
143        StructureDefinition sd = workerContext.fetchTypeDefinition(field.types().get(0).getUrl());
144        if (sd != null) {
145          boolean isPrim = sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE;
146          boolean isAbstract = sd.getAbstract();
147          String name = field.name().replace("[x]", "");
148          String sname = name;
149          String type = null;
150          String init = "";
151          String ptype = type;
152          if (isPrim) {
153            // todo: are we extension-less?
154            type = Utilities.capitalize(field.types().get(0).getName()+"Type");
155            ptype = getPrimitiveType(sd);
156          } else {
157            type = field.types().get(0).getName();
158          }
159          String ltype = type;
160          if (field.isList()) {
161            ltype = "List<"+type+">";
162            init = "new ArrayList<>()";
163            if (!Utilities.existsInList(name, "contained")) {
164              name = Utilities.pluralize(name, 2);
165            }
166          }
167          String cname = Utilities.capitalize(name);
168          String csname = Utilities.capitalize(sname);
169          String nn = field.min() == 1 ? "// @NotNull" : "";
170          genField(isPrim, name, ptype, ltype, nn, field.isList(), field.shortDocumentation());
171          genAccessors(isPrim, isAbstract, name, type, init, ptype, ltype, cname, csname, field.isList(), field.documentation(), field.fixedValue());   
172          genLoad(isPrim, isAbstract, name, type, init, ptype, ltype, cname, csname, field.isList(), field.fixedValue(), field.types().get(0));
173          genClear(field.isList(), name);
174        }
175      } else {
176        // ignoring polymorphics for now
177      }
178
179    }
180
181    private void genClear(boolean list, String name) {
182      w(clear, "    "+name+" = null;");
183    }
184    private void genLoad(boolean isPrim, boolean isAbstract, String name, String type, String init, String ptype, String ltype, String cname, String csname, boolean isList, boolean isFixed, PEType typeInfo) {
185      if (isList) {
186        w(load, "    for (PEInstance item : src.children(\""+name+"\")) {");
187        w(load, "      "+name+".add(("+type+") item.asDataType());");
188        w(load, "    }");
189      } else if (isPrim) {
190        w(load, "    if (src.hasChild(\""+name+"\")) {");
191        w(load, "      "+name+" = (("+type+") src.child(\""+name+"\").asDataType()).getValue();");
192        w(load, "    }");      
193      } else if (typeInfo != null && typeInfo.getUrl() != null && !typeInfo.getUrl().startsWith("http://hl7.org/fhir/StructureDefinition")) {
194        w(load, "    if (src.hasChild(\""+name+"\")) {");
195        w(load, "      "+name+" = "+type+".fromSource(src.child(\""+name+"\"));");
196        w(load, "    }");
197      } else {
198        w(load, "    if (src.hasChild(\""+name+"\")) {");
199        w(load, "      "+name+" = ("+type+") src.child(\""+name+"\").asDataType();");
200        w(load, "    }");
201      }
202    }
203
204    private void genAccessors(boolean isPrim, boolean isAbstract, String name, String type, String init, String ptype, String ltype, String cname, String csname, boolean isList, String shortDoco, boolean isFixed) {
205      jdoc(accessors, doco, 2, true);
206      if (isPrim && extensionPolicy != ExtensionPolicy.Primitives && !isList) {
207        w(accessors, "  public "+ptype+" get"+cname+"() {");
208        w(accessors, "    return "+name+";");
209        w(accessors, "  }");
210        w(accessors);
211        w(accessors, "  public "+this.name+" set"+cname+"("+ptype+" value) {");
212        w(accessors, "    this."+name+" = value;");
213        w(accessors, "    return this;");
214        w(accessors, "  }");
215        w(accessors);
216        w(accessors, "  public boolean has"+cname+"() {");
217        w(accessors, "    return "+name+" != null;");
218        w(accessors, "  }");
219      } else {
220        if (isPrim && !isList) {
221          w(accessors, "  public "+ptype+" get"+cname+"() {");
222          w(accessors, "    if ("+name+" == null) { "+name+" = new "+type+"(); }");
223          w(accessors, "    return "+name+".getValue();");
224          w(accessors, "  }");
225          w(accessors, "  public "+ltype+" get"+cname+"Element() {");
226        } else if (isAbstract && !isList) {
227          w(accessors, "  public @Nullable "+ltype+" get"+cname+"() { // "+ltype+" is abstract ");
228        } else {
229          w(accessors, "  public "+ltype+" get"+cname+"() {");
230        }
231        if (isList) {
232          w(accessors, "    if ("+name+" == null) { "+name+" = "+init+"; }");
233        } else if (!isAbstract) {
234          w(accessors, "    if ("+name+" == null) { "+name+" = new "+type+"(); }");
235        }
236        w(accessors, "    return "+name+";");
237        w(accessors, "  }");
238        w(accessors);
239        if (isList) {
240          w(accessors, "  public boolean has"+cname+"() {");
241          w(accessors, "    return "+name+" != null && !"+name+".isEmpty();");
242          w(accessors, "  }");
243          w(accessors);
244          if (!isAbstract) {
245            w(accessors, "  public "+type+" add"+csname+"() {");
246            w(accessors, "    "+type+" theThing = new "+type+"();");
247            w(accessors, "    get"+cname+"().add(theThing);");
248            w(accessors, "    return theThing;");
249            w(accessors, "  }");
250            w(accessors); 
251          }
252          w(accessors, "  public boolean has"+csname+"("+type+" item) {");
253          w(accessors, "    return has"+cname+"() && "+name+".contains(item);");
254          w(accessors, "  }");
255          w(accessors);        
256          w(accessors, "  public void remove"+csname+"("+type+" item) {");
257          w(accessors, "    if (has"+csname+"(item)) {");
258          w(accessors, "      "+name+".remove(item);");
259          w(accessors, "    }");
260          w(accessors, "  }");
261          w(accessors);        
262        } else if (isPrim) {
263          if (!isFixed) {
264            w(accessors, "  public "+this.name+" set"+cname+"("+ptype+" value) {");
265            w(accessors, "    if ("+name+" == null) { "+name+" = new "+type+"(); }");
266            w(accessors, "    "+name+".setValue(value);");
267            w(accessors, "    return this;");
268            w(accessors, "  }");
269            w(accessors, "  public "+this.name+" set"+cname+"Element("+type+" value) {");
270            w(accessors, "    this."+name+" = value;");
271            w(accessors, "    return this;");
272            w(accessors, "  }");
273          }
274          w(accessors, "  public boolean has"+cname+"() {");
275          w(accessors, "    return "+name+" != null && "+name+".hasValue();");
276          w(accessors, "  }");
277          w(accessors); 
278        } else {
279          if (!isFixed) {
280            w(accessors, "  public "+this.name+" set"+cname+"("+type+" value) {");
281            w(accessors, "    this."+name+" = value;");
282            w(accessors, "    return this;");
283            w(accessors, "  }");
284          }
285          w(accessors, "  public boolean has"+cname+"() {");
286          w(accessors, "    return "+name+" != null;");
287          w(accessors, "  }");
288        }
289      }
290      w(accessors);
291    }
292
293    private void genField(boolean isPrim, String name, String ptype, String ltype, String nn, boolean isList, String shortDoco) {
294      // jdoc(fields, field.documentation(), 2, true);
295      if (isPrim && extensionPolicy != ExtensionPolicy.Primitives && !isList) {
296        w(fields, "  private "+ptype+" "+name+";"+nn+"  // "+shortDoco);
297      } else {
298        w(fields, "  private "+ltype+" "+name+";"+nn+"  // "+shortDoco);             
299      }
300    }
301  }
302
303  private String folder;
304  private IWorkerContext workerContext;
305  private String canonical;
306  private String pkgName;
307
308  // options:
309  private ExtensionPolicy extensionPolicy;
310  private boolean narrative;
311  private boolean contained;
312  private boolean meta;
313  private String language;
314  private boolean keyELementsOnly;
315
316  public PECodeGenerator(IWorkerContext workerContext) {
317    super();
318    this.workerContext = workerContext;
319  }
320
321  public String getFolder() {
322    return folder;
323  }
324
325
326  public void setFolder(String folder) {
327    this.folder = folder;
328  }
329
330
331  public String getCanonical() {
332    return canonical;
333  }
334
335  public void setCanonical(String canonical) {
336    this.canonical = canonical;
337  }
338
339
340  public String getPkgName() {
341    return pkgName;
342  }
343
344  public void setPkgName(String pkgName) {
345    this.pkgName = pkgName;
346  }
347
348  public ExtensionPolicy getExtensionPolicy() {
349    return extensionPolicy;
350  }
351
352  public void setExtensionPolicy(ExtensionPolicy extensionPolicy) {
353    this.extensionPolicy = extensionPolicy;
354  }
355
356  public boolean isNarrative() {
357    return narrative;
358  }
359
360  public void setNarrative(boolean narrative) {
361    this.narrative = narrative;
362  }
363
364  public boolean isMeta() {
365    return meta;
366  }
367
368  public void setMeta(boolean meta) {
369    this.meta = meta;
370  }
371
372  public String getLanguage() {
373    return language;
374  }
375
376  public void setLanguage(String language) {
377    this.language = language;
378  }
379
380  public boolean isKeyELementsOnly() {
381    return keyELementsOnly;
382  }
383
384  public void setKeyELementsOnly(boolean keyELementsOnly) {
385    this.keyELementsOnly = keyELementsOnly;
386  }
387
388  public boolean isContained() {
389    return contained;
390  }
391
392  public void setContained(boolean contained) {
393    this.contained = contained;
394  }
395
396  private StringBuilder imports = new StringBuilder();
397
398  /**
399   * @throws IOException 
400   * 
401   */
402  public void execute() throws IOException {
403    PEDefinition source = new PEBuilder(workerContext, PEElementPropertiesPolicy.EXTENSION, true).buildPEDefinition(canonical);
404    w(imports, "import java.util.List;");
405    w(imports, "import java.util.ArrayList;");
406    w(imports, "import javax.annotation.Nullable;");
407    w(imports, "import java.util.Date;\r\n");
408    w(imports);
409    w(imports, "import org.hl7.fhir.r5.context.IWorkerContext;");
410    w(imports, "import org.hl7.fhir.r5.model.*;");
411    w(imports, "import org.hl7.fhir.r5.profilemodel.PEBuilder;");
412    w(imports, "import org.hl7.fhir.r5.profilemodel.PEInstance;");
413    w(imports, "import org.hl7.fhir.r5.profilemodel.PEBuilder.PEElementPropertiesPolicy;");
414    w(imports, "import org.hl7.fhir.r5.profilemodel.gen.PEGeneratedBase;");
415    PEGenClass cls = genClass(source);
416    StringBuilder b = new StringBuilder();
417    w(b, "package "+pkgName+";");
418    w(b);
419    if (source.getProfile().hasCopyright()) {
420      jdoc(b, source.getProfile().getCopyright(), 0, false);
421    }
422    w(b, imports.toString());
423    cls.write(b);
424    TextFile.stringToFile(b.toString(), Utilities.path(folder, cls.name+".java"));
425  }
426
427  public void jdoc(StringBuilder b, String doco, int indent, boolean jdoc) {
428    if (!Utilities.noString(doco)) {
429      String pfx = Utilities.padLeft("", ' ', indent);
430      w(b, pfx+"/*"+(jdoc ? "*" : ""));
431      for (String line : doco.split("\\R")) {
432        for (String nl : naturalLines(line))
433          w(b, pfx+" * "+nl);
434        w(b, pfx+" *");
435      }
436      w(b, pfx+" */");
437    }    
438  }
439
440  private List<String> naturalLines(String line) {
441    List<String> lines = new ArrayList<>();
442    while (line.length() > 80) {
443      int cutpoint = 80;
444      while (cutpoint > 0 && line.charAt(cutpoint) != ' ') {
445        cutpoint--;
446      }
447      if (cutpoint == 0) {
448        cutpoint = 80;
449      } else {
450        cutpoint++;
451      }
452      lines.add(line.substring(0, cutpoint));
453      line = line.substring(cutpoint);
454    }
455    lines.add(line);
456    return lines;
457  }
458
459  private void w(StringBuilder b) {
460    b.append("\r\n");
461
462  }
463
464  private void w(StringBuilder b, String line) {
465    b.append(line);
466    w(b);    
467  }
468
469  private PEGenClass genClass(PEDefinition source) {
470    PEGenClass cls = new PEGenClass();
471    cls.name = source.getProfile().getName();
472    cls.base = source.getProfile().getType();
473    cls.doco = source.documentation();
474    cls.url = source.getProfile().getVersionedUrl();
475    cls.isResource = source.getProfile().getKind() == StructureDefinitionKind.RESOURCE;
476    cls.genId();
477    for (PEDefinition child : source.children()) {
478      if (genForField(source, child)) {
479        cls.defineField(source, child);
480      }
481    }    
482    return cls;
483  }
484
485  private boolean genForField(PEDefinition source, PEDefinition child) {
486    if (child.definition().getBase().getPath().equals("Resource.meta")) {
487      return meta;
488    }
489    if (child.definition().getBase().getPath().equals("DomainResource.text")) {
490      return narrative;
491    }
492    if (child.definition().getBase().getPath().equals("Resource.language")) {
493      return language == null;
494    }
495    if (child.definition().getBase().getPath().endsWith(".extension") || child.definition().getBase().getPath().endsWith(".modifierExtension")) {
496      return extensionPolicy == ExtensionPolicy.Complexes;
497    }
498    if (child.definition().getBase().getPath().equals("DomainResource.contained")) {
499      return contained;
500    }
501    return !keyELementsOnly || (child.isKeyElement());
502  }
503
504
505  private String getPrimitiveType(StructureDefinition sd) {
506
507    if (sd.getType().equals("string"))
508      return "String";
509    if (sd.getType().equals("code"))
510      return "String";
511    if (sd.getType().equals("markdown"))
512      return "String";
513    if (sd.getType().equals("base64Binary"))
514      return "byte[]";
515    if (sd.getType().equals("uri"))
516      return "String";
517    if (sd.getType().equals("url"))
518      return "String";
519    if (sd.getType().equals("canonical"))
520      return "String";
521    if (sd.getType().equals("oid"))
522      return "String";
523    if (sd.getType().equals("integer"))
524      return "int";
525    if (sd.getType().equals("integer64"))
526      return "long";
527    if (sd.getType().equals("unsignedInt"))
528      return "int";
529    if (sd.getType().equals("positiveInt"))
530      return "int";
531    if (sd.getType().equals("boolean"))
532      return "boolean";
533    if (sd.getType().equals("decimal"))
534      return "BigDecimal";
535    if (sd.getType().equals("dateTime"))
536      return "Date";
537    if (sd.getType().equals("date"))
538      return "Date";
539    if (sd.getType().equals("id"))
540      return "String";
541    if (sd.getType().equals("instant"))
542      return "Date";
543    if (sd.getType().equals("time"))
544      return "String";
545
546    return "??";
547  }
548
549}