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 "Terser.java". Description:
10 * "Wraps a message to provide access to fields using a more terse syntax."
11 *
12 * The Initial Developer of the Original Code is University Health Network. Copyright (C)
13 * 2002. All Rights Reserved.
14 *
15 * Contributor(s): Ryan W. Gross (General Electric Corporation - Healthcare IT).
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.util;
29
30 import ca.uhn.hl7v2.Location;
31 import ca.uhn.hl7v2.model.*;
32 import ca.uhn.hl7v2.HL7Exception;
33 import java.util.StringTokenizer;
34
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37
38 /**
39 * <p>
40 * Wraps a message to provide access to fields using a terse location specification syntax. For
41 * example:
42 * </p>
43 * <p>
44 * <code>terser.set("MSH-9-3", "ADT_A01");</code> <br>
45 * can be used instead of <br>
46 * <code>message.getMSH().getMessageType().getMessageStructure().setValue("ADT_A01"); </code>
47 * </p>
48 * <p>
49 * The syntax of a location spec is as follows:
50 * </p>
51 * <p>
52 * location_spec:
53 * <code>segment_path_spec "-" field ["(" rep ")"] ["-" component ["-" subcomponent]] </code>
54 * </p>
55 * <p>
56 * ... where rep, field, component, and subcomponent are integers (representing, respectively, the
57 * field repetition (starting at 0), and the field number, component number, and subcomponent
58 * numbers (starting at 1). Omitting the rep is equivalent to specifying 0; omitting the component
59 * or subcomponent is equivalent to specifying 1.
60 * </p>
61 * <p>
62 * The syntax for the segment_path_spec is as follows:
63 * </p>
64 * <p>
65 * segment_path_spec: </code> ["/"] (group_spec ["(" rep ")"] "/")* segment_spec ["(" rep
66 * ")"]</code>
67 * </p>
68 * <p>
69 * ... where rep has the same meaning as for fields.
70 * </p>
71 * <p>
72 * A leading "/" indicates that navigation to the
73 * location begins at the root of the message; omitting this indicates that navigation begins at
74 * the current location of the underlying SegmentFinder (see getFinder() -- this allows manual
75 * navigation if desired). The syntax for group_spec is:
76 * </p>
77 * <p>
78 * group_spec: <code>["."] group_name_pattern</code>
79 * </p>
80 * <p>
81 * Here, a . indicates that the group should be searched for (using a SegmentFinder) starting at the
82 * current location in the message. The wildcards "*" and "?" represent any number of arbitrary
83 * characters, and a single arbitrary character, respectively. For example, "M*" and "?S?" match
84 * MSH. The first group with a name that matches the given group_name_pattern will be matched.
85 * </p>
86 * <p>
87 * The segment_spec is analogous to the group_spec.
88 * </p>
89 * <p>
90 * As another example, the following subcomponent in an SIU_S12 message:
91 * <p>
92 * <p>
93 * <code>msg.getSIU_S12_RGSAISNTEAIGNTEAILNTEAIPNTE(1).getSIU_S12_AIGNTE().getAIG().getResourceGroup(1).getIdentifier();</code>
94 * </p>
95 * </p> ... is referenced by all of the following location_spec: </p>
96 * <p>
97 * <code>/SIU_S12_RGSAISNTEAIGNTEAILNTEAIPNTE(1)/SIU_S12_AIGNTE/AIG-5(1)-1 <br>
98 * /*AIG*(1)/SIU_S12_AIGNTE/AIG-5(1)-1 <br>
99 * /*AIG*(1)/.AIG-5(1) <code>
100 * </p>
101 * <p>
102 * The search function only iterates through rep 0 of each group. Thus if rep 0 of the first group
103 * in this example was desired instead of rep 1, the following syntax would also work (since there
104 * is only one AIG segment position in SUI_S12):
105 * </p>
106 * <p>
107 * <code>/.AIG-5(1)</code>
108 * </p>
109 *
110 * @author Bryan Tripp
111 * @author Ryan W. Gross (General Electric Corporation - Healthcare IT).
112 */
113 public class Terser {
114
115 private final SegmentFinder finder;
116 private static final Logger log = LoggerFactory.getLogger(Terser.class);
117
118 /** Creates a new instance of Terser
119 *
120 * @param message message for which the Terser is created
121 */
122 public Terser(Message message) {
123 if (message == null) {
124 throw new NullPointerException("Message may not be null");
125 }
126 finder = new SegmentFinder(message);
127 }
128
129 /**
130 * Returns the string value of the Primitive at the given location.
131 *
132 * @param segment the segment from which to get the primitive
133 * @param field the field number (indexed from 1)
134 * @param rep the field repetition (indexed from 0)
135 * @param component the component number (indexed from 1, use 1 for primitive field)
136 * @param subcomponent the subcomponent number (indexed from 1, use 1 for primitive component)
137 * @return string value of the Primitive at the given location
138 * @throws HL7Exception if the primitive could not be obtained
139 */
140 public static String get(Segment segment, int field, int rep, int component, int subcomponent) throws HL7Exception {
141 if (segment == null) {
142 throw new NullPointerException("segment may not be null");
143 }
144 if (rep < 0) {
145 throw new IllegalArgumentException("rep must not be negative");
146 }
147 if (component < 1) {
148 throw new IllegalArgumentException(
149 "component must not be 1 or more (note that this parameter is 1-indexed, not 0-indexed)");
150 }
151 if (subcomponent < 1) {
152 throw new IllegalArgumentException(
153 "subcomponent must not be 1 or more (note that this parameter is 1-indexed, not 0-indexed)");
154 }
155
156 Primitive prim = getPrimitive(segment, field, rep, component, subcomponent);
157 return prim.getValue();
158 }
159
160 /**
161 * Sets the string value of the Primitive at the given location.
162 *
163 * @param segment the segment from which to get the primitive
164 * @param field the field number (indexed from 1)
165 * @param rep the field repetition (indexed from 0)
166 * @param component the component number (indexed from 1, use 1 for primitive field)
167 * @param subcomponent the subcomponent number (indexed from 1, use 1 for primitive component)
168 * @param value value to be set
169 * @throws HL7Exception if the value could not be set
170 */
171 public static void set(Segment segment, int field, int rep, int component, int subcomponent, String value)
172 throws HL7Exception {
173 if (segment == null) {
174 throw new NullPointerException("segment may not be null");
175 }
176 if (rep < 0) {
177 throw new IllegalArgumentException("rep must not be negative");
178 }
179 if (component < 1) {
180 throw new IllegalArgumentException(
181 "component must be 1 or more (note that this parameter is 1-indexed, not 0-indexed)");
182 }
183 if (subcomponent < 1) {
184 throw new IllegalArgumentException(
185 "subcomponent must be 1 or more (note that this parameter is 1-indexed, not 0-indexed)");
186 }
187
188 Primitive prim = getPrimitive(segment, field, rep, component, subcomponent);
189 prim.setValue(value);
190 }
191
192 public static void set(Segment segment, Location location, String value) throws HL7Exception {
193 set(segment,
194 location.getField(),
195 location.getFieldRepetition(),
196 location.getComponent(),
197 location.getSubcomponent(),
198 value);
199 }
200
201 /**
202 * Returns the Primitive object at the given location.
203 */
204 private static Primitive getPrimitive(Segment segment, int field, int rep, int component, int subcomponent)
205 throws HL7Exception {
206 Type type = segment.getField(field, rep);
207 return getPrimitive(type, component, subcomponent);
208 }
209
210 /**
211 * Returns the Primitive object at the given location in the given field. It is intended that
212 * the given type be at the field level, although extra components will be added blindly if, for
213 * example, you provide a primitive subcomponent instead and specify component or subcomponent >
214 * 1
215 *
216 * @param type the type from which to get the primitive
217 * @param component the component number (indexed from 1, use 1 for primitive field)
218 * @param subcomponent the subcomponent number (indexed from 1, use 1 for primitive component)
219 * @return the Primitive object at the given location
220 */
221 public static Primitive getPrimitive(final Type type, final int component, final int subcomponent) {
222 if (type == null) {
223 throw new NullPointerException("type may not be null");
224 }
225 if (component < 1) {
226 throw new IllegalArgumentException(
227 "component must not be 1 or more (note that this parameter is 1-indexed, not 0-indexed)");
228 }
229 if (subcomponent < 1) {
230 throw new IllegalArgumentException(
231 "subcomponent must not be 1 or more (note that this parameter is 1-indexed, not 0-indexed)");
232 }
233
234 Type comp = getComponent(type, component);
235 if (type instanceof Varies && comp instanceof GenericPrimitive && subcomponent > 1) {
236 try {
237 final Varies../../../../ca/uhn/hl7v2/model/Varies.html#Varies">Varies varies = (Varies) type;
238 final GenericCompositetml#GenericComposite">GenericComposite comp2 = new GenericComposite(type.getMessage());
239 varies.setData(comp2);
240 comp = getComponent(type, component);
241 } catch (final DataTypeException de) {
242 final String message = "Unexpected exception copying data to generic composite. This is probably a bug within HAPI. "
243 + de.getMessage();
244 log.error(message, de);
245 throw new Error(message);
246 }
247 }
248 final Type sub = getComponent(comp, subcomponent);
249 return getPrimitive(sub);
250 }
251
252 /**
253 * Attempts to extract a Primitive from the given type. If it's a composite, drills down through
254 * first components until a primitive is reached.
255 */
256 private static Primitive getPrimitive(Type type) {
257 if (type instanceof Primitive) {
258 return (Primitive) type;
259 }
260 if (type instanceof Composite) {
261 try {
262 return getPrimitive(((Composite) type).getComponent(0));
263 } catch (HL7Exception e) {
264 throw new RuntimeException("Internal error: HL7Exception thrown on Composite.getComponent(0).");
265 }
266 }
267 return getPrimitive(((Varies) type).getData());
268 }
269
270 /**
271 * Returns the component (or sub-component, as the case may be) at the given index. If it does
272 * not exist, it is added as an "extra component". If comp > 1 is requested from a Varies with
273 * GenericPrimitive data, the data is set to GenericComposite (this avoids the creation of a
274 * chain of ExtraComponents on GenericPrimitives). Components are numbered from 1.
275 */
276 private static Type./../../../ca/uhn/hl7v2/model/Type.html#Type">Type getComponent(Type type, int comp) {
277
278 if (type instanceof Primitive && comp == 1) {
279 return type;
280 }
281 if (type instanceof Composite) {
282 if (comp <= numStandardComponents(type) || type instanceof GenericComposite) {
283 try {
284 return ((Composite) type).getComponent(comp - 1);
285 } catch (DataTypeException e) {
286 throw new RuntimeException(
287 "Internal error: HL7Exception thrown on getComponent(x) where x < # standard components.",
288 e);
289 }
290 }
291 }
292 if (Varies.class.isAssignableFrom(type.getClass())) {
293 Variesref="../../../../ca/uhn/hl7v2/model/Varies.html#Varies">Varies v = (Varies) type;
294 try {
295 if (comp > 1 && GenericPrimitive.class.isAssignableFrom(v.getData().getClass()))
296 v.setData(new GenericComposite(v.getMessage()));
297 } catch (DataTypeException e) {
298 throw new RuntimeException("Unexpected exception copying data to generic composite: " + e.getMessage(),
299 e);
300 }
301
302 return getComponent(v.getData(), comp);
303 }
304
305 return type.getExtraComponents().getComponent(comp - numStandardComponents(type) - 1);
306 }
307
308 /**
309 * <p>
310 * Gets the string value of the field specified. See the class docs for syntax of the location
311 * spec.
312 * </p>
313 * <p>
314 * If a repetition is omitted for a repeating segment or field, the first rep is used. If the
315 * component or subcomponent is not specified for a composite field, the first component is used
316 * (this allows one to write code that will work with later versions of the HL7 standard).
317 *
318 * @param spec field specification
319 * @return string value of the specified field
320 * @throws HL7Exception if the primitive could not be obtained
321 */
322 public String get(String spec) throws HL7Exception {
323 StringTokenizer tok = new StringTokenizer(spec, "-", false);
324 Segment segment = getSegment(tok.nextToken());
325
326 int[] ind = getIndices(spec);
327 return get(segment, ind[0], ind[1], ind[2], ind[3]);
328 }
329
330 /**
331 * Returns the segment specified in the given segment_path_spec.
332 *
333 * @param segSpec segment specification
334 * @return the segment specified
335 * @throws HL7Exception if the segment could not be obtained
336 */
337 public Segment getSegment(String segSpec) throws HL7Exception {
338 Segment seg = null;
339
340 if (segSpec.startsWith("/")) {
341 getFinder().reset();
342 }
343
344 StringTokenizer tok = new StringTokenizer(segSpec, "/", false);
345 SegmentFinder finder = getFinder();
346 while (tok.hasMoreTokens()) {
347 String pathSpec = tok.nextToken();
348 Terser.PathSpec ps = parsePathSpec(pathSpec);
349 ps.isGroup = tok.hasMoreTokens();
350 if (ps.isGroup) {
351 Group g = ps.find ?
352 finder.findGroup(ps.pattern, ps.rep) :
353 finder.getGroup(ps.pattern, ps.rep);
354 finder = new SegmentFinder(g);
355 } else {
356 seg = ps.find ?
357 finder.findSegment(ps.pattern, ps.rep) :
358 finder.getSegment(ps.pattern, ps.rep);
359 }
360 }
361
362 return seg;
363 }
364
365 /** Gets path information from a path spec. */
366 private PathSpec parsePathSpec(String spec) throws HL7Exception {
367 PathSpec ps = new PathSpec();
368
369 if (spec.startsWith(".")) {
370 ps.find = true;
371 spec = spec.substring(1);
372 } else {
373 ps.find = false;
374 }
375
376 if (spec.length() == 0) {
377 throw new HL7Exception("Invalid path (some path element is either empty or contains only a dot)");
378 }
379 StringTokenizer tok = new StringTokenizer(spec, "()", false);
380 ps.pattern = tok.nextToken();
381 if (tok.hasMoreTokens()) {
382 String repString = tok.nextToken();
383 try {
384 ps.rep = Integer.parseInt(repString);
385 } catch (NumberFormatException e) {
386 throw new HL7Exception(repString + " is not a valid rep #");
387 }
388 } else {
389 ps.rep = 0;
390 }
391 return ps;
392 }
393
394 /**
395 * Given a Terser path, returns an array containing field num, field rep, component, and
396 * subcomponent.
397 *
398 * @param spec field specification
399 * @return an array containing field num, field rep, component, and subcomponent
400 * @throws HL7Exception if the field does not exist
401 */
402 public static int[] getIndices(String spec) throws HL7Exception {
403 StringTokenizer tok = new StringTokenizer(spec, "-", false);
404 tok.nextToken(); // skip over segment
405 if (!tok.hasMoreTokens())
406 throw new HL7Exception("Must specify field in spec " + spec);
407 try {
408 StringTokenizer fieldSpec = new StringTokenizer(tok.nextToken(), "()", false);
409 int fieldNum = Integer.parseInt(fieldSpec.nextToken());
410 int fieldRep = fieldSpec.hasMoreTokens() ?
411 Integer.parseInt(fieldSpec.nextToken()) : 0;
412 int component = tok.hasMoreTokens() ?
413 Integer.parseInt(tok.nextToken()) : 1;
414 int subcomponent = tok.hasMoreTokens() ?
415 Integer.parseInt(tok.nextToken()) : 1;
416 return new int[] { fieldNum, fieldRep, component, subcomponent };
417 } catch (NumberFormatException e) {
418 throw new HL7Exception("Invalid integer in spec " + spec);
419 }
420 }
421
422
423 /**
424 * Sets the string value of the field specified. See class docs for location spec syntax.
425 *
426 * @param spec primitive path specification
427 * @param value value to be set
428 * @throws HL7Exception if the primitive does not exist
429 */
430 public void set(String spec, String value) throws HL7Exception {
431 StringTokenizer tok = new StringTokenizer(spec, "-", false);
432 Segment segment = getSegment(tok.nextToken());
433
434 int[] ind = getIndices(spec);
435 log.trace("Setting {} seg: {} ind: {} {} {} {}", spec, segment.getName(), ind[0], ind[1],
436 ind[2], ind[3]);
437 set(segment, ind[0], ind[1], ind[2], ind[3], value);
438 }
439
440 /**
441 * Returns the number of sub-components in the specified component, i.e. the number of standard
442 * sub-components (e.g. 6 for CE) plus any extra components that that have been added at
443 * runtime.
444 *
445 * @param type composite type
446 * @param component numbered from 1
447 * @return number of sub-components in the specified component
448 */
449 public static int numSubComponents(Type type, int component) {
450 if (component == 1 && Primitive.class.isAssignableFrom(type.getClass())) {
451 // note that getComponent(primitive, 1) below returns the primitive
452 // itself -- if we do numComponents on it, we'll end up with the
453 // number of components in the field, not the number of subcomponents
454 return 1;
455 }
456 Type comp = getComponent(type, component);
457 return numComponents(comp);
458 }
459
460 /**
461 * Returns the number of components in the given type, i.e. the number of standard components
462 * (e.g. 6 for CE) plus any extra components that have been added at runtime.
463 *
464 * @param t composite type
465 * @return the number of components in the given type
466 */
467 public static int numComponents(Type t) {
468 if (!(t instanceof Varies)) {
469 return numStandardComponents(t) + t.getExtraComponents().numComponents();
470 }
471 return numComponents(((Varies) t).getData());
472 }
473
474 private static int numStandardComponents(Type t) {
475 if (t instanceof Composite) {
476 return ((Composite) t).getComponents().length;
477 }
478 if (t instanceof Varies) {
479 return numStandardComponents(((Varies) t).getData());
480 }
481 return 1;
482 }
483
484 /**
485 * Returns the segment finder used by this Terser. Navigating the finder will influence the
486 * behaviour of the Terser accordingly. Ie when the full path of the segment is not specified
487 * the segment will be sought beginning at the current location of the finder.
488 *
489 * @return the segment finder used by this Terser
490 */
491 public SegmentFinder getFinder() {
492 return finder;
493 }
494
495 /** Struct for information about a step in a segment path. */
496 private static class PathSpec {
497 public String pattern;
498 public boolean isGroup;
499 public boolean find;
500 public int rep;
501 }
502 }