View Javadoc
1   /**
2    * The contents of this file are subject to the Mozilla Public License Version 1.1
3    * (the "License"); you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at http://www.mozilla.org/MPL/
5    * Software distributed under the License is distributed on an "AS IS" basis,
6    * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
7    * specific language governing rights and limitations under the License.
8    *
9    * The Original Code is "MessageGenerator.java".  Description:
10   * "Creates source code for HL7 Message objects, using the normative DB"
11   *
12   * The Initial Developer of the Original Code is University Health Network. Copyright (C)
13   * 2001.  All Rights Reserved.
14   *
15   * Contributor(s):  Eric Poiseau. 
16   *
17   * Alternatively, the contents of this file may be used under the terms of the
18   * GNU General Public License (the  �GPL�), in which case the provisions of the GPL are
19   * applicable instead of those above.  If you wish to allow use of your version of this
20   * file only under the terms of the GPL and not to allow others to use your version
21   * of this file under the MPL, indicate your decision by deleting  the provisions above
22   * and replace  them with the notice and other provisions required by the GPL License.
23   * If you do not delete the provisions above, a recipient may use your version of
24   * this file under either the MPL or the GPL.
25   *
26   */
27  
28  package ca.uhn.hl7v2.sourcegen;
29  
30  import java.io.BufferedWriter;
31  import java.io.File;
32  import java.io.FileOutputStream;
33  import java.io.IOException;
34  import java.io.OutputStreamWriter;
35  import java.sql.Connection;
36  import java.sql.ResultSet;
37  import java.sql.SQLException;
38  import java.sql.Statement;
39  import java.util.ArrayList;
40  import java.util.Arrays;
41  import java.util.HashMap;
42  import java.util.List;
43  import java.util.Map;
44  import java.util.TreeMap;
45  
46  import org.apache.velocity.Template;
47  import org.apache.velocity.VelocityContext;
48  import org.apache.velocity.context.Context;
49  import org.slf4j.Logger;
50  import org.slf4j.LoggerFactory;
51  
52  import ca.uhn.hl7v2.HL7Exception;
53  import ca.uhn.hl7v2.database.NormativeDatabase;
54  import ca.uhn.hl7v2.parser.DefaultModelClassFactory;
55  import ca.uhn.hl7v2.sourcegen.util.VelocityFactory;
56  
57  /**
58   * Creates source code for HL7 Message objects, using the normative DB. HL7
59   * Group objects are also created as a byproduct.
60   * 
61   * @author Bryan Tripp (bryan_tripp@sourceforge.net)
62   * @author Eric Poiseau
63   */
64  public class MessageGenerator {
65  
66  	private static final Logger log = LoggerFactory.getLogger(MessageGenerator.class);
67  
68  	/**
69  	 * If the system property by this name is true, groups are generated to use
70  	 * a ModelClassFactory for segment class lookup. This makes segment creation
71  	 * more flexible, but may slow down parsing substantially.
72  	 */
73  	public static final String MODEL_CLASS_FACTORY_KEY = "ca.uhn.hl7v2.sourcegen.modelclassfactory";
74  
75  	/** Creates new MessageGenerator */
76  	public MessageGenerator() {
77  	}
78  
79  	public static File determineTargetDir(String baseDirectory, String version) throws IOException, HL7Exception {
80  		return SourceGenerator.makeDirectory(baseDirectory + DefaultModelClassFactory.getVersionPackagePath(version) + "message");
81  	}
82  
83  	/**
84  	 * Returns an SQL query with which to get a list of messages from the
85  	 * normative database.
86  	 */
87  	private static String getMessageListQuery(String version) {
88  		// UNION because the messages are defined in different tables for
89  		// different versions.
90  		// ACCESS return
91  		// "SELECT distinct  [message_type]+'_'+[event_code] AS msg_struct, '?'"
92  		// //no chapters in DB
93  		return "SELECT distinct  message_type + '_' + event_code AS msg_struct, '?'" // no
94  																						// chapters
95  																						// in
96  																						// DB
97  				+ " FROM HL7Versions RIGHT JOIN HL7EventMessageTypeSegments ON HL7EventMessageTypeSegments.version_id = HL7Versions.version_id "
98  				+ "WHERE HL7Versions.hl7_version ='"
99  				+ version
100 				+ "' and Not (message_type='ACK') "
101 				+ "UNION "
102 				+ "select distinct HL7MsgStructIDs.message_structure, section from HL7Versions RIGHT JOIN (HL7MsgStructIDSegments "
103 				+ " inner join HL7MsgStructIDs on HL7MsgStructIDSegments.message_structure = HL7MsgStructIDs.message_structure "
104 				+ " and HL7MsgStructIDSegments.version_id = HL7MsgStructIDs.version_id) " + " ON HL7MsgStructIDSegments.version_id = HL7Versions.version_id " + " where HL7Versions.hl7_version = '" + version + "' and HL7MsgStructIDs.message_structure not like 'ACK_%'"; // note:
105 																																																																				// allows
106 																																																																				// "ACK"
107 																																																																				// itself
108 	}
109 
110 	public static MessageListAndChapterList getMessages(String theVersion) throws SQLException {
111 		// get list of messages ...
112 		NormativeDatabase normativeDatabase = NormativeDatabase.getInstance();
113 		Connection conn = normativeDatabase.getConnection();
114 		Statement stmt = conn.createStatement();
115 		String sql = getMessageListQuery(theVersion);
116 		ResultSet rs = stmt.executeQuery(sql);
117 		ArrayList<String> messages = new ArrayList<>();
118 		ArrayList<String> chapters = new ArrayList<>();
119 		while (rs.next()) {
120 			String name = rs.getString(1);
121 			if ("0".equals(name)) {
122 				continue;
123 			}
124 			messages.add(name);
125 			chapters.add(rs.getString(2));
126 		}
127 		stmt.close();
128 		normativeDatabase.returnConnection(conn);
129 
130 		MessageListAndChapterList retVal = new MessageListAndChapterList();
131 		retVal.chapters = chapters;
132 		retVal.messages = messages;
133 
134 		return retVal;
135 	}
136 
137 	/**
138 	 * Returns an SQL query with which to get a list of the segments that are
139 	 * part of the given message from the normative database. The query varies
140 	 * with different versions. The fields returned are as follows:
141 	 * segment_code, repetitional, optional, description
142 	 */
143 	private static String getSegmentListQuery(String message, String version) {
144 		String sql;
145 
146 		sql = "SELECT HL7Segments.seg_code, repetitional, optional, HL7Segments.description, seq_no, groupname " + "FROM HL7Versions RIGHT JOIN (HL7Segments INNER JOIN HL7EventMessageTypeSegments ON (HL7Segments.version_id = HL7EventMessageTypeSegments.version_id) "
147 				+ "AND (HL7Segments.seg_code = HL7EventMessageTypeSegments.seg_code)) " + "ON HL7Segments.version_id = HL7Versions.version_id " + "WHERE (((HL7Versions.hl7_version)= '" + version + "') "
148 				// ACCESS + "AND (([message_type]+'_'+[event_code])='"
149 				+ "AND ((message_type + '_' + event_code)='" + message + "')) order by seq_no UNION "
150 				// + "')) UNION "
151 				+ "select HL7Segments.seg_code, repetitional, optional, HL7Segments.description, seq_no, groupname  " + "from HL7Versions RIGHT JOIN (HL7MsgStructIDSegments inner join HL7Segments on HL7MsgStructIDSegments.seg_code = HL7Segments.seg_code "
152 				+ "and HL7MsgStructIDSegments.version_id = HL7Segments.version_id) " + "ON HL7Segments.version_id = HL7Versions.version_id " + "where HL7Versions.hl7_version = '" + version + "' and message_structure = '" + message + "' order by seq_no";
153 		return sql;
154 	}
155 
156 	/**
157 	 * Queries the normative database for a list of segments comprising the
158 	 * message structure. The returned list may also contain strings that denote
159 	 * repetition and optionality. Choice indicators (i.e. begin choice, next
160 	 * choice, end choice) for alternative segments are ignored, so that the
161 	 * class structure allows all choices. The matter of enforcing that only a
162 	 * single choice is populated can't be handled by the class structure, and
163 	 * should be handled elsewhere.
164 	 *
165 	 */
166 	private static SegmentDef[] getSegments(String message, String version) throws SQLException {
167 		/*
168 		 * String sql =
169 		 * "select HL7Segments.seg_code, repetitional, optional, description " +
170 		 * "from (HL7MsgStructIDSegments inner join HL7Segments on HL7MsgStructIDSegments.seg_code = HL7Segments.seg_code "
171 		 * + "and HL7MsgStructIDSegments.version_id = HL7Segments.version_id) "
172 		 * + "where HL7Segments.version_id = 6 and message_structure = '" +
173 		 * message + "' order by seq_no";
174 		 */
175 		String sql = getSegmentListQuery(message, version);
176 		// System.out.println(sql.toString());
177 		SegmentDef[] segments = new SegmentDef[200]; // presumably there won't
178 														// be more than 200
179 		NormativeDatabase normativeDatabase = NormativeDatabase.getInstance();
180 		Connection conn = normativeDatabase.getConnection();
181 		Statement stmt = conn.createStatement();
182 		ResultSet rs = stmt.executeQuery(sql);
183 		int c = -1;
184 		boolean inChoice = false;
185 		while (rs.next()) {
186 			String name = SegmentGenerator.altSegName(rs.getString(1));
187 			boolean repeating = rs.getBoolean(2);
188 			boolean optional = rs.getBoolean(3);
189 			String desc = rs.getString(4);
190 
191 			// group names are defined in DB for 2.3.1 but not used in the
192 			// schema
193 			String groupName;
194 			if (version.equalsIgnoreCase("2.3.1") && !"true".equals(System.getProperty("force.group"))) {
195 				groupName = null;
196 			} else {
197 				groupName = rs.getString(6);
198 			}
199 			
200 			if (groupName != null) {
201 				// Don't allow spaces in the names
202 				groupName = groupName.replaceAll(" ", "_");
203 				groupName = groupName.replaceAll("/", "_");
204 			}
205 
206 			if (name.equals("<")) {
207 				inChoice = true;
208 			} else if (name.equals(">")) {
209 				inChoice = false;
210 			} else if (!name.equals("|")) {
211 				c++;
212 				segments[c] = new SegmentDef(name, groupName, !optional, repeating, inChoice, desc);
213 			}
214 		}
215 		SegmentDef[] ret = new SegmentDef[c + 1];
216 		System.arraycopy(segments, 0, ret, 0, c + 1);
217 
218 		normativeDatabase.returnConnection(conn);
219 
220 		return ret;
221 	}
222 
223 	/**
224 	 * Creates source code for a specific message structure and writes it under
225 	 * the specified directory. throws IllegalArgumentException if there is no
226 	 * message structure for this message in the normative database
227 	 */
228 	public static void make(String message, String baseDirectory, String chapter, String version, String theTemplatePackage, String theFileExt) throws Exception {
229 
230 		// Make sure this structure has a corresponding definition in the
231 		// structure map
232 		// Parser.getMessageStructureForEvent(message, version);
233 
234 		try {
235 			SegmentDef[] segments = getSegments(message, version);
236 			// System.out.println("Making: " + message + " with " +
237 			// segments.length +
238 			// " segments (not writing message code - just groups)");
239 
240 			GroupDef group = GroupGenerator.getGroupDef(segments, null, baseDirectory, version, message, theTemplatePackage, theFileExt);
241 			StructureDef[] contents = group.getStructures();
242 
243 			// make base directory
244 			if (!(baseDirectory.endsWith("\\") || baseDirectory.endsWith("/"))) {
245 				baseDirectory = baseDirectory + "/";
246 			}
247 			File targetDir = determineTargetDir(baseDirectory, version);
248 			System.out.println("Writing " + message + " to " + targetDir.getPath());
249 			String fileName = targetDir.getPath() + "/" + message + "." + theFileExt;
250 
251 			writeMessage(fileName, contents, message, chapter, version, DefaultModelClassFactory.getVersionPackageName(version), true, theTemplatePackage, null);
252 
253 		} catch (SQLException | IOException e) {
254 			throw new HL7Exception(e);
255 		}
256 		// catch (Exception e) {
257 		// log.error("Error while creating source code", e);
258 		//
259 		// log.warn("Warning: could not write source code for message structure "
260 		// + message
261 		// + " - "
262 		// + e.getClass().getName()
263 		// + ": "
264 		// + e.getMessage());
265 		// }
266 	}
267 
268 	/**
269 	 * Creates and writes source code for all Messages and Groups.
270 	 */
271 	public static void makeAll(String baseDirectory, String version, boolean failOnError, String theTemplatePackage, String theFileExt) throws Exception {
272 		MessageListAndChapterList mac = getMessages(version);
273 		ArrayList<String> messages = mac.messages;
274 		ArrayList<String> chapters = mac.chapters;
275 		
276 		if (messages.size() == 0) {
277 			log.warn("No version {} messages found in database {}", version, System.getProperty("ca.on.uhn.hl7.database.url"));
278 		}
279 
280 		for (int i = 0; i < messages.size(); i++) {
281 			String message = messages.get(i);
282 
283 			String hapiTestGenMessage = System.getProperty("hapi.test.genmessage");
284 			if (hapiTestGenMessage != null && !hapiTestGenMessage.contains(message)) {
285 				continue;
286 			}
287 
288 			try {
289 				make(message, baseDirectory, chapters.get(i), version, theTemplatePackage, theFileExt);
290 			} catch (HL7Exception e) {
291 				if (failOnError) {
292 					throw e;
293 				} else {
294 					log.error("Failed to generate message" + message + ": ", e);
295 				}
296 			}
297 		}
298 	}
299 
300 	/**
301 	 * Returns source code for the contructor for this Message class.
302 	 */
303 	public static String makeConstructor(StructureDef[] structs, String messageName, String version) {
304 		boolean useFactory = System.getProperty(MODEL_CLASS_FACTORY_KEY, "FALSE").equalsIgnoreCase("TRUE");
305 
306 		StringBuilder source = new StringBuilder();
307 
308 		source.append("\t/** \r\n");
309 		source.append("\t * Creates a new ");
310 		source.append(messageName);
311 		source.append(" Group with custom ModelClassFactory.\r\n");
312 		source.append("\t */\r\n");
313 		source.append("\tpublic ");
314 		source.append(messageName);
315 		source.append("(ModelClassFactory factory) {\r\n");
316 		source.append("\t   super(factory);\r\n");
317 		source.append("\t   init(factory);\r\n");
318 		source.append("\t}\r\n\r\n");
319 		source.append("\t/**\r\n");
320 		source.append("\t * Creates a new ");
321 		source.append(messageName);
322 		source.append(" Group with DefaultModelClassFactory. \r\n");
323 		source.append("\t */ \r\n");
324 		source.append("\tpublic ");
325 		source.append(messageName);
326 		source.append("() { \r\n");
327 		source.append("\t   super(new DefaultModelClassFactory());\r\n");
328 		source.append("\t   init(new DefaultModelClassFactory());\r\n");
329 		source.append("\t}\r\n\r\n");
330 		source.append("\tprivate void init(ModelClassFactory factory) {\r\n");
331 		source.append("\t   try {\r\n");
332 		int numStructs = structs.length;
333 		for (StructureDef def : structs) {
334 			if (useFactory) {
335 				source.append("\t      this.add(factory.get");
336 				source.append((def instanceof GroupDef) ? "Group" : "Segment");
337 				source.append("Class(\"");
338 				source.append(def.getName());
339 				source.append("\", \"");
340 				source.append(version);
341 				source.append("\"), ");
342 			} else {
343 				source.append("\t      this.add(");
344 				source.append(def.getName());
345 				source.append(".class, ");
346 			}
347 			source.append(def.isRequired());
348 			source.append(", ");
349 			source.append(def.isRepeating());
350 			source.append(");\r\n");
351 		}
352 		source.append("\t   } catch(HL7Exception e) {\r\n");
353 		source.append("\t      HapiLogFactory.getHapiLog(this.getClass()).error(\"Unexpected error creating ");
354 		source.append(messageName);
355 		source.append(" - this is probably a bug in the source code generator.\", e);\r\n");
356 		source.append("\t   }\r\n");
357 		source.append("\t}\r\n\r\n");
358 		return source.toString();
359 	}
360 
361 	/**
362 	 * @param fileName
363 	 * @param contents
364 	 * @param message
365 	 * @param chapter
366 	 * @param version
367 	 * @param basePackageName
368 	 * @param haveGroups
369 	 * @param theTemplatePackage
370 	 * @param theStructureNameToChildNames
371 	 *            Only required for superstructure generation
372 	 * @throws Exception
373 	 */
374 	public static void writeMessage(String fileName, StructureDef[] contents, String message, String chapter, String version, String basePackageName, boolean haveGroups, String theTemplatePackage, Map<String, List<String>> theStructureNameToChildNames) throws Exception {
375 
376 		BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName, false), SourceGenerator.ENCODING));
377 
378 		theTemplatePackage = theTemplatePackage.replace(".", "/");
379 		String template2 = theTemplatePackage + "/messages.vsm";
380 
381 		// InputStream resis =
382 		// MessageGenerator.class.getClassLoader().getResourceAsStream(template2);
383 		// System.out.println(resis);
384 		// System.out.println(resis);
385 		// System.out.println(resis);
386 		//
387 		// URL resUrl =
388 		// MessageGenerator.class.getClassLoader().getResource(template2);
389 		// System.out.println(resUrl);
390 		// System.out.println(resUrl);
391 		// System.out.println(resUrl);
392 		//
393 		// Template template = new Template();
394 
395 		if (theStructureNameToChildNames != null && theStructureNameToChildNames.size() > 0) {
396 			theStructureNameToChildNames = new TreeMap<>(theStructureNameToChildNames);
397 		} else {
398 			theStructureNameToChildNames = new HashMap<>();
399 		}
400 		
401 		Template template = VelocityFactory.getClasspathTemplateInstance(template2);
402 		Context ctx = new VelocityContext();
403 		ctx.put("message", message);
404 		ctx.put("specVersion", version);
405 		ctx.put("chapter", chapter);
406 		ctx.put("haveGroups", haveGroups);
407 		ctx.put("basePackageName", basePackageName);
408 		ctx.put("segments", Arrays.asList(contents));
409 		ctx.put("structureNameToChildNames", theStructureNameToChildNames);
410 		ctx.put("HASH", "#");
411 		template.merge(ctx, out);
412 
413 		out.flush();
414 		out.close();
415 	}
416 
417 	public static class MessageListAndChapterList {
418 		ArrayList<String> chapters;
419 		ArrayList<String> messages;
420 	}
421 
422 }