001/**
002 * The contents of this file are subject to the Mozilla Public License Version 1.1
003 * (the "License"); you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at http://www.mozilla.org/MPL/
005 * Software distributed under the License is distributed on an "AS IS" basis,
006 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
007 * specific language governing rights and limitations under the License.
008 *
009 * The Original Code is "Terser.java".  Description:
010 * "Wraps a message to provide access to fields using a more terse syntax."
011 *
012 * The Initial Developer of the Original Code is University Health Network. Copyright (C)
013 * 2002.  All Rights Reserved.
014 *
015 * Contributor(s): Ryan W. Gross (General Electric Corporation - Healthcare IT).
016 *
017 * Alternatively, the contents of this file may be used under the terms of the
018 * GNU General Public License (the  �GPL�), in which case the provisions of the GPL are
019 * applicable instead of those above.  If you wish to allow use of your version of this
020 * file only under the terms of the GPL and not to allow others to use your version
021 * of this file under the MPL, indicate your decision by deleting  the provisions above
022 * and replace  them with the notice and other provisions required by the GPL License.
023 * If you do not delete the provisions above, a recipient may use your version of
024 * this file under either the MPL or the GPL.
025 *
026 */
027
028package ca.uhn.hl7v2.util;
029
030import ca.uhn.hl7v2.Location;
031import ca.uhn.hl7v2.model.*;
032import ca.uhn.hl7v2.HL7Exception;
033import java.util.StringTokenizer;
034
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038/**
039 * <p>
040 * Wraps a message to provide access to fields using a terse location specification syntax. For
041 * example:
042 * </p>
043 * <p>
044 * <code>terser.set("MSH-9-3", "ADT_A01");</code> <br>
045 * can be used instead of <br>
046 * <code>message.getMSH().getMessageType().getMessageStructure().setValue("ADT_A01"); </code>
047 * </p>
048 * <p>
049 * The syntax of a location spec is as follows:
050 * </p>
051 * <p>
052 * location_spec:
053 * <code>segment_path_spec "-" field ["(" rep ")"] ["-" component ["-" subcomponent]] </code>
054 * </p>
055 * <p>
056 * ... where rep, field, component, and subcomponent are integers (representing, respectively, the
057 * field repetition (starting at 0), and the field number, component number, and subcomponent
058 * numbers (starting at 1). Omitting the rep is equivalent to specifying 0; omitting the component
059 * or subcomponent is equivalent to specifying 1.
060 * </p>
061 * <p>
062 * The syntax for the segment_path_spec is as follows:
063 * </p>
064 * <p>
065 * segment_path_spec: </code> ["/"] (group_spec ["(" rep ")"] "/")* segment_spec ["(" rep
066 * ")"]</code>
067 * </p>
068 * <p>
069 * ... where rep has the same meaning as for fields.
070 * </p>
071 * <p>
072 * A leading "/" indicates that navigation to the
073 * location begins at the root of the message; omitting this indicates that navigation begins at
074 * the current location of the underlying SegmentFinder (see getFinder() -- this allows manual
075 * navigation if desired). The syntax for group_spec is:
076 * </p>
077 * <p>
078 * group_spec: <code>["."] group_name_pattern</code>
079 * </p>
080 * <p>
081 * Here, a . indicates that the group should be searched for (using a SegmentFinder) starting at the
082 * current location in the message. The wildcards "*" and "?" represent any number of arbitrary
083 * characters, and a single arbitrary character, respectively. For example, "M*" and "?S?" match
084 * MSH. The first group with a name that matches the given group_name_pattern will be matched.
085 * </p>
086 * <p>
087 * The segment_spec is analogous to the group_spec.
088 * </p>
089 * <p>
090 * As another example, the following subcomponent in an SIU_S12 message:
091 * <p>
092 * <p>
093 * <code>msg.getSIU_S12_RGSAISNTEAIGNTEAILNTEAIPNTE(1).getSIU_S12_AIGNTE().getAIG().getResourceGroup(1).getIdentifier();</code>
094 * </p>
095 * </p> ... is referenced by all of the following location_spec: </p>
096 * <p>
097 * <code>/SIU_S12_RGSAISNTEAIGNTEAILNTEAIPNTE(1)/SIU_S12_AIGNTE/AIG-5(1)-1 <br>
098 * /*AIG*(1)/SIU_S12_AIGNTE/AIG-5(1)-1 <br>
099 * /*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 */
113public class Terser {
114
115    private SegmentFinder finder;
116    private static 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 varies = (Varies) type;
238                final 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 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            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: {} {} {} {}", new Object[] { 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}