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