View Javadoc

1   /*
2    * To change this template, choose Tools | Templates
3    * and open the template in the editor.
4    */
5   
6   package ca.uhn.hl7v2.sourcegen.conf;
7   
8   import java.io.BufferedWriter;
9   import java.io.File;
10  import java.io.FileOutputStream;
11  import java.io.IOException;
12  import java.io.OutputStreamWriter;
13  import java.util.ArrayList;
14  import java.util.Collections;
15  import java.util.HashMap;
16  import java.util.HashSet;
17  import java.util.Map;
18  import java.util.Set;
19  import java.util.regex.Matcher;
20  import java.util.regex.Pattern;
21  
22  import org.apache.commons.io.FileUtils;
23  import org.apache.commons.lang.StringUtils;
24  import org.apache.velocity.Template;
25  import org.apache.velocity.VelocityContext;
26  import org.apache.velocity.context.Context;
27  import org.slf4j.Logger;
28  import org.slf4j.LoggerFactory;
29  
30  import ca.uhn.hl7v2.conf.ProfileException;
31  import ca.uhn.hl7v2.conf.parser.ProfileParser;
32  import ca.uhn.hl7v2.conf.spec.RuntimeProfile;
33  import ca.uhn.hl7v2.conf.spec.message.AbstractSegmentContainer;
34  import ca.uhn.hl7v2.conf.spec.message.Component;
35  import ca.uhn.hl7v2.conf.spec.message.Field;
36  import ca.uhn.hl7v2.conf.spec.message.ProfileStructure;
37  import ca.uhn.hl7v2.conf.spec.message.Seg;
38  import ca.uhn.hl7v2.conf.spec.message.SegGroup;
39  import ca.uhn.hl7v2.conf.spec.message.StaticDef;
40  import ca.uhn.hl7v2.conf.spec.message.SubComponent;
41  import ca.uhn.hl7v2.parser.DefaultModelClassFactory;
42  import ca.uhn.hl7v2.sourcegen.DataTypeGenerator;
43  import ca.uhn.hl7v2.sourcegen.DatatypeComponentDef;
44  import ca.uhn.hl7v2.sourcegen.DatatypeDef;
45  import ca.uhn.hl7v2.sourcegen.GroupDef;
46  import ca.uhn.hl7v2.sourcegen.GroupGenerator;
47  import ca.uhn.hl7v2.sourcegen.MessageGenerator;
48  import ca.uhn.hl7v2.sourcegen.SegmentDef;
49  import ca.uhn.hl7v2.sourcegen.SegmentElement;
50  import ca.uhn.hl7v2.sourcegen.SegmentGenerator;
51  import ca.uhn.hl7v2.sourcegen.SourceGenerator;
52  import ca.uhn.hl7v2.sourcegen.util.VelocityFactory;
53  
54  /**
55   * Generates HAPI custom model classes using HL7 conformance profiles
56   */
57  public class ProfileSourceGenerator {
58  
59      private static final Logger ourLog = LoggerFactory.getLogger(ProfileSourceGenerator.class);
60  
61      private final RuntimeProfile myProfile;
62      private String myTargetDirectory;
63      private String myBasePackage;
64      private String myMessageName;
65      private Set<String> mySegmentDefNames;
66      private Set<String> myGroupDefNames;
67      private final ArrayList<SegmentDef> mySegmentDefs;
68      private final Map<String, ArrayList<SegmentElement>> mySegmentNameToSegmentElements;
69      private final ArrayList<GroupDef> myGroupDefs;
70      private GenerateDataTypesEnum myGenerateDataTypes;
71      private String myTemplatePackage;
72      private String myFileExt;
73  
74      public ProfileSourceGenerator(RuntimeProfile theProfile, String theTargetDirectory, String theBasePackage, GenerateDataTypesEnum theGenDt, String theTemplatePackage,
75              String theFileExt) {
76          myProfile = theProfile;
77          myGenerateDataTypes = theGenDt;
78          myTemplatePackage = theTemplatePackage;
79          myFileExt = theFileExt;
80  
81          myTargetDirectory = theTargetDirectory;
82          if (!myTargetDirectory.endsWith("/")) {
83              myTargetDirectory += "/";
84          }
85  
86          myBasePackage = theBasePackage;
87          if (!myBasePackage.endsWith(".")) {
88              myBasePackage += ".";
89          }
90  
91          myTargetDirectory += myBasePackage.replaceAll("\\.", "/");
92  
93          mySegmentDefs = new ArrayList<SegmentDef>();
94          myGroupDefs = new ArrayList<GroupDef>();
95          mySegmentNameToSegmentElements = new HashMap<String, ArrayList<SegmentElement>>();
96          mySegmentDefNames = new HashSet<String>();
97          myGroupDefNames = new HashSet<String>();
98      }
99  
100     public void generate() throws Exception {
101         StaticDef staticDef = myProfile.getMessage();
102         myMessageName = staticDef.getMsgStructID();
103 
104         SourceGenerator.makeDirectory(myTargetDirectory + "/message");
105         SourceGenerator.makeDirectory(myTargetDirectory + "/segment");
106         SourceGenerator.makeDirectory(myTargetDirectory + "/group");
107 
108         String chapter = "";
109         String version = myProfile.getHL7Version();
110         GroupDef group = convertToGroupDef(staticDef, version);
111 
112         String basePackageName = DefaultModelClassFactory.getVersionPackageName(version);
113         String[] datatypePackages;
114 
115         switch (myGenerateDataTypes) {
116         default:
117         case NONE:
118             datatypePackages = new String[] { basePackageName + "datatype" };
119             break;
120         case SINGLE:
121             datatypePackages = new String[] { myBasePackage + "datatype" };
122             SourceGenerator.makeDirectory(myTargetDirectory + "/datatype");
123         }
124 
125         boolean haveGroups = myGroupDefs.size() > 0;
126 
127         // Write Message
128         {
129             String parent = myTargetDirectory + "message/";
130             FileUtils.forceMkdir(new File(parent));
131 			String fileName = parent + staticDef.getMsgStructID() + "." + myFileExt;
132             ourLog.debug("Writing Message file: " + fileName);
133             MessageGenerator.writeMessage(fileName, group.getStructures(), myMessageName, chapter, version, myBasePackage, haveGroups, myTemplatePackage, null);
134         }
135 
136         for (GroupDef next : myGroupDefs) {
137             String parent = myTargetDirectory + "group/";
138             FileUtils.forceMkdir(new File(parent));
139             String fileName = parent + next.getName() + "." + myFileExt;
140             ourLog.debug("Writing Group file: " + fileName);
141             GroupGenerator.writeGroup(next.getName(), fileName, next, version, myBasePackage, myTemplatePackage, next.getDescription());
142         }
143 
144         // Write Segments
145         Set<String> alreadyWrittenDatatypes = new HashSet<String>();
146         Set<String> alreadyWrittenSegments = new HashSet<String>();
147         for (SegmentDef next : mySegmentDefs) {
148             alreadyWrittenSegments.add(next.getName());
149 
150             String parent = myTargetDirectory + "segment/";
151             FileUtils.forceMkdir(new File(parent));
152             String fileName = parent + next.getName() + "." + myFileExt;
153             ourLog.debug("Writing Segment file: " + fileName);
154             String segmentName = next.getName();
155             String description = next.getDescription();
156             ArrayList<SegmentElement> elements = mySegmentNameToSegmentElements.get(segmentName);
157 
158             for (SegmentElement nextElement : elements) {
159                 if ("*".equals(nextElement.type) || "VARIES".equals(nextElement.type)) {
160                     nextElement.type = "Varies";
161                 }
162             }
163 
164             SegmentGenerator.writeSegment(fileName, version, segmentName, elements, description, myBasePackage, datatypePackages, myTemplatePackage);
165 
166             switch (myGenerateDataTypes) {
167             case SINGLE:
168                 for (DatatypeDef nextFieldDef : next.getFieldDefs()) {
169                     writeDatatype(nextFieldDef, alreadyWrittenDatatypes, version);
170                 }
171             }
172 
173         }
174 
175         if ("json".equals(myFileExt)) {
176             String fileName = myTargetDirectory + "/structures." + myFileExt;
177             ourLog.debug("Writing Structures file: " + fileName);
178             BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName, false), SourceGenerator.ENCODING));
179             String templatePackage = myTemplatePackage.replace(".", "/");
180             Template template = VelocityFactory.getClasspathTemplateInstance(templatePackage + "/available_structures.vsm");
181             Context ctx = new VelocityContext();
182             ctx.put("messages", Collections.singletonList(myMessageName));
183             ctx.put("segments", alreadyWrittenSegments);
184             ctx.put("datatypes", alreadyWrittenDatatypes);
185 
186             template.merge(ctx, out);
187 
188             out.flush();
189             out.close();
190         }
191 
192     }
193 
194     private void writeDatatype(DatatypeDef theFieldDef, Set<String> theAlreadyWrittenDatatypes, String version) throws Exception {
195         if (theAlreadyWrittenDatatypes != null) {
196             if (theAlreadyWrittenDatatypes.contains(theFieldDef.getType())) {
197                 return;
198             } else {
199                 theAlreadyWrittenDatatypes.add(theFieldDef.getType());
200             }
201         }
202 
203         String parent = myTargetDirectory + "datatype/";
204         FileUtils.forceMkdir(new File(parent));
205         String fileName = parent + theFieldDef.getType() + "." + myFileExt;
206         DataTypeGenerator.writeDatatype(fileName, version, theFieldDef, myBasePackage, myTemplatePackage);
207 
208         for (DatatypeDef next : theFieldDef.getSubComponentDefs()) {
209             writeDatatype(next, theAlreadyWrittenDatatypes, version);
210         }
211     }
212 
213     private GroupDef convertToGroupDef(StaticDef staticDef, String theVersion) {
214 
215         boolean required = true;
216         boolean repeating = true;
217         String description = staticDef.getDescription();
218         GroupDef retVal = new GroupDef(myMessageName, null, required, repeating, description);
219 
220         populateChildren(retVal, staticDef, theVersion);
221 
222         return retVal;
223     }
224 
225     private GroupDef convertToGroupDef(GroupDef theParent, SegGroup segGroup, String theVersion) {
226 
227         /*
228          * MWB exports optional repeating groups as an optional group with a
229          * single child which is a required repeating group If we encounter
230          * this, we flatten this to be an optional repeating group
231          */
232         boolean required = true;
233         if (segGroup.getChildren() == 1 && segGroup.getChild(1) instanceof SegGroup) {
234             required = segGroup.getMin() > 0;
235             segGroup = (SegGroup) segGroup.getChild(1);
236         }
237 
238         String name = segGroup.getName();
239         required = required && (segGroup.getMin() > 0);
240         boolean repeating = segGroup.getMax() != 1;
241         String description = segGroup.getLongName();
242 
243         GroupDef retVal = new GroupDef(myMessageName, name, required, repeating, description);
244 
245         populateChildren(retVal, segGroup, theVersion);
246 
247         if (!myGroupDefNames.contains(name)) {
248             myGroupDefNames.add(name);
249             myGroupDefs.add(retVal);
250         }
251 
252         return retVal;
253     }
254 
255     private void populateChildren(GroupDef retVal, AbstractSegmentContainer segGroup, String theVersion) {
256 
257         for (int i = 0; i < segGroup.getChildren(); i++) {
258             ProfileStructure nextProfileStructure = segGroup.getChild(i + 1);
259 
260             if (nextProfileStructure instanceof SegGroup) {
261 
262                 SegGroup nextSegGroup = (SegGroup) nextProfileStructure;
263                 GroupDef nextGroupDef = convertToGroupDef(retVal, nextSegGroup, theVersion);
264                 retVal.addStructure(nextGroupDef);
265 
266             } else if (nextProfileStructure instanceof Seg) {
267 
268                 Seg nextSeg = (Seg) nextProfileStructure;
269                 SegmentDef nextSegmentDef = convertToSegmentDef(retVal, nextSeg, theVersion);
270                 retVal.addStructure(nextSegmentDef);
271 
272             } else {
273 
274                 throw new IllegalStateException("Unknown profile structure: " + nextProfileStructure);
275 
276             } // if-else
277 
278         } // for
279 
280     }
281 
282     private SegmentDef convertToSegmentDef(GroupDef theParent, Seg nextSeg, String theVersion) {
283         String name = nextSeg.getName();
284         String groupName = null;
285         boolean required = nextSeg.getMin() > 0;
286         boolean repeating = nextSeg.getMax() > 1 || nextSeg.getMax() == -1;
287         String description = nextSeg.getLongName();
288 
289         SegmentDef retVal = new SegmentDef(name, groupName, required, repeating, false, description);
290         ArrayList<SegmentElement> segmentElements = new ArrayList<SegmentElement>();
291 
292         // Extract fields from the segment definition
293         for (int i = 0; i < nextSeg.getFields(); i++) {
294             Field nextField = nextSeg.getField(i + 1);
295 
296             DatatypeDef nextFieldDef = convertToDatatypeDef(nextField);
297             retVal.addFieldDef(nextFieldDef);
298 
299             SegmentElement nextSegmentElement = new SegmentElement(name, theVersion, i);
300 
301             nextSegmentElement.desc = nextField.getName();
302             nextSegmentElement.field = i + 1;
303             nextSegmentElement.length = (int) nextField.getLength();
304             nextSegmentElement.opt = nextField.getUsage();
305             nextSegmentElement.rep = (nextField.getMax() != 1) ? "Y" : "N";
306             nextSegmentElement.repetitions = nextField.getMax();
307             nextSegmentElement.type = nextField.getDatatype();
308 
309             if (nextSegmentElement.type.startsWith("CM_")) {
310                 nextSegmentElement.type = nextSegmentElement.type.substring(3);
311             }
312 
313             String table = nextField.getTable();
314             if (table != null && table.length() > 0) {
315                 extractTableInfo(nextSegmentElement, table);
316             }
317 
318             segmentElements.add(nextSegmentElement);
319         }
320 
321         if (!mySegmentDefNames.contains(name)) {
322             mySegmentDefNames.add(name);
323             mySegmentDefs.add(retVal);
324             mySegmentNameToSegmentElements.put(name, segmentElements);
325         }
326 
327         return retVal;
328     }
329 
330 	static void extractTableInfo(SegmentElement nextSegmentElement, String table) {
331 		Pattern p = Pattern.compile("^([a-zA-Z]+)([0-9]+)$");
332 		Matcher m = p.matcher(table);
333 		if (m.find()) {
334 			String namespace = m.group(1);
335 			nextSegmentElement.tableNamespace = namespace;
336 			
337 			String tableNum = m.group(2);
338 			nextSegmentElement.table = Integer.parseInt(tableNum);
339 			
340 			String alternateType = nextSegmentElement.getAlternateType();
341 			if ("ID".equals(alternateType)) {
342 				nextSegmentElement.setAlternateType("ca.uhn.hl7v2.model.primitive.IDWithNamespace");
343 			} else if ("IS".equals(alternateType)) {
344 				nextSegmentElement.setAlternateType("ca.uhn.hl7v2.model.primitive.IDWithNamespace");
345 			}
346 			
347 		} else {
348 			try {
349 		    	nextSegmentElement.table = Integer.parseInt(table);
350 		    } catch (NumberFormatException e) {
351 		    	ourLog.warn("Unable to parse number out of table name: \"" + table +"\" for field \"" + nextSegmentElement.desc + "\". Ignoring this value and setting table number to 0.");
352 		    }
353 		}
354 	}
355 
356     private DatatypeDef convertToDatatypeDef(Field theField) {
357         String type = theField.getDatatype();
358         String name = theField.getName();
359 
360         DatatypeDef retVal = new DatatypeDef(type, name);
361 
362         for (int i = 0; i < theField.getComponents(); i++) {
363             Component nextComponent = theField.getComponent(i + 1);
364             DatatypeComponentDef nextDef = convertToDatatypeComponentDef(nextComponent, type, i);
365             retVal.addSubcomponentDef(nextDef);
366         }
367 
368         return retVal;
369     }
370 
371     private DatatypeComponentDef convertToDatatypeComponentDef(Component theComponent, String parentType, int indexWithinParent) {
372 
373         String type = theComponent.getDatatype();
374         String name = theComponent.getName();
375 
376         int table = 0;
377         if (StringUtils.isNotBlank(theComponent.getTable())) {
378             try {
379                 table = Integer.parseInt(theComponent.getTable());
380             } catch (NumberFormatException e) {
381                 // TODO: handle this somehow?
382             }
383         }
384 
385         DatatypeComponentDef retVal = new DatatypeComponentDef(parentType, indexWithinParent, type, name, table);
386 
387         for (int i = 0; i < theComponent.getSubComponents(); i++) {
388             SubComponent next = theComponent.getSubComponent(i + 1);
389             DatatypeComponentDef def = convertToDatatypeComponentDef(next, type, i);
390             retVal.addSubcomponentDef(def);
391         }
392 
393         return retVal;
394     }
395 
396     private DatatypeComponentDef convertToDatatypeComponentDef(SubComponent theComponent, String parentType, int indexWithinParent) {
397 
398         String type = theComponent.getDatatype();
399         String desc = theComponent.getName();
400 
401         int table = 0;
402         if (StringUtils.isNotBlank(theComponent.getTable())) {
403             try {
404                 table = Integer.parseInt(theComponent.getTable());
405             } catch (NumberFormatException e) {
406                 // TODO: handle this somehow?
407             }
408         }
409 
410         DatatypeComponentDef retVal = new DatatypeComponentDef(parentType, indexWithinParent, type, desc, table);
411         return retVal;
412     }
413 
414     public static void main(String[] args) throws ProfileException, IOException, Exception {
415         RuntimeProfile rp = new ProfileParser(false).parseClasspath("ca/uhn/hl7v2/conf/parser/ADT_A01.xml");
416         new ProfileSourceGenerator(rp, "hapi-test/target/generated-sources/confgen", "hapi.on.olis", GenerateDataTypesEnum.SINGLE, "ca.uhn.hl7v2.sourcegen.templates.json", "json")
417                 .generate();
418     }
419 }