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.model.*;
031import ca.uhn.hl7v2.HL7Exception;
032import java.util.StringTokenizer;
033
034import org.slf4j.Logger;
035import org.slf4j.LoggerFactory;
036
037/**
038 * <p>Wraps a message to provide access to fields using a terse location
039 * specification syntax.  For example: </p>
040 * <p><code>terser.set("MSH-9-3", "ADT_A01");</code>  <br>
041 * can be used instead of <br>
042 * <code>message.getMSH().getMessageType().getMessageStructure().setValue("ADT_A01"); </code> </p>
043 * <p>The syntax of a location spec is as follows: </p>
044 * <p>location_spec: <code>segment_path_spec "-" field ["(" rep ")"] ["-" component ["-" subcomponent]] </code></p>
045 * <p>... where rep, field, component, and subcomponent are integers (representing, respectively,
046 * the field repetition (starting at 0), and the field number, component number, and subcomponent
047 * numbers (starting at 1).  Omitting the rep is equivalent to specifying 0; omitting the
048 * component or subcomponent is equivalent to specifying 1.</p>
049 * <p>The syntax for the segment_path_spec is as follows: </p>
050 * <p>segment_path_spec: </code> ["/"] (group_spec ["(" rep ")"] "/")* segment_spec ["(" rep ")"]</code></p>
051 * <p> ... where rep has the same meaning as for fields.  A leading "/" indicates that navigation to the
052 * location begins at the root of the message; ommitting this indicates that navigation begins at the
053 * current location of the underlying SegmentFinder (see getFinder() -- this allows manual navigation
054 * if desired).  The syntax for group_spec is: </p>
055 * <p>group_spec: <code>["."] group_name_pattern</code></p>
056 * <p>Here, a . indicates that the group should be searched for (using a SegmentFinder) starting at the
057 * current location in the message.  The wildcards "*" and "?" represent any number of arbitrary characters, 
058 * and a single arbitrary character, respectively.  For example, "M*" and "?S?" match MSH.  The first
059 * group with a name that matches the given group_name_pattern will be matched.  </p>
060 * <p>The segment_spec is analogous to the group_spec. </p>
061 * <p>As another example, the following subcomponent in an SIU_S12 message: <p>
062 * <p><code>msg.getSIU_S12_RGSAISNTEAIGNTEAILNTEAIPNTE(1).getSIU_S12_AIGNTE().getAIG().getResourceGroup(1).getIdentifier();</code></p>
063 * </p> ... is referenced by all of the following location_spec: </p>
064 * <p><code>/SIU_S12_RGSAISNTEAIGNTEAILNTEAIPNTE(1)/SIU_S12_AIGNTE/AIG-5(1)-1 <br>
065 * /*AIG*(1)/SIU_S12_AIGNTE/AIG-5(1)-1 <br>
066 * /*AIG*(1)/.AIG-5(1) <code></p>
067 * <p>The search function only iterates through rep 0 of each group.  Thus if rep 0 of the first group
068 * in this example was desired instead of rep 1, the following syntax would also work (since there is
069 * only one AIG segment position in SUI_S12): </p>
070 * <p><code>/.AIG-5(1)</code></p>
071 * 
072 * @author Bryan Tripp
073 * @author Ryan W. Gross (General Electric Corporation - Healthcare IT).
074 */
075public class Terser {
076    
077    private SegmentFinder finder;
078    private static Logger log = LoggerFactory.getLogger(Terser.class);
079    
080    /** Creates a new instance of Terser */
081    public Terser(Message message) {
082        finder = new SegmentFinder(message);
083    }
084    
085    /**
086     * Returns the string value of the Primitive at the given location.
087     * @param segment the segment from which to get the primitive
088     * @param field the field number (indexed from 1)
089     * @param rep the field repetition (indexed from 0)
090     * @param component the component number (indexed from 1, use 1 for primitive field)
091     * @param subcomponent the subcomponent number (indexed from 1, use 1 for primitive component)
092     */
093    public static String get(Segment segment, int field, int rep, int component, int subcomponent) throws HL7Exception {
094        if (segment == null) {
095                throw new NullPointerException("segment may not be null");
096        }
097        if (rep < 0) {
098                throw new IllegalArgumentException("rep must not be negative");
099        }
100        if (component < 1) {
101                throw new IllegalArgumentException("component must not be 1 or more (note that this parameter is 1-indexed, not 0-indexed)");
102        }
103        if (subcomponent < 1) {
104                throw new IllegalArgumentException("subcomponent must not be 1 or more (note that this parameter is 1-indexed, not 0-indexed)");
105        }
106        
107        Primitive prim = getPrimitive(segment, field, rep, component, subcomponent);
108        return prim.getValue();
109    }
110    
111    /**
112     * Sets the string value of the Primitive at the given location.
113     * @param segment the segment from which to get the primitive
114     * @param field the field number (indexed from 1)
115     * @param rep the field repetition (indexed from 0)
116     * @param component the component number (indexed from 1, use 1 for primitive field)
117     * @param subcomponent the subcomponent number (indexed from 1, use 1 for primitive component)
118     */
119    public static void set(Segment segment, int field, int rep, int component, int subcomponent, String value) throws HL7Exception {
120        if (segment == null) {
121                throw new NullPointerException("segment may not be null");
122        }
123        if (rep < 0) {
124                throw new IllegalArgumentException("rep must not be negative");
125        }
126        if (component < 1) {
127                throw new IllegalArgumentException("component must not be 1 or more (note that this parameter is 1-indexed, not 0-indexed)");
128        }
129        if (subcomponent < 1) {
130                throw new IllegalArgumentException("subcomponent must not be 1 or more (note that this parameter is 1-indexed, not 0-indexed)");
131        }
132
133        Primitive prim = getPrimitive(segment, field, rep, component, subcomponent);
134        prim.setValue(value);
135    }
136    
137    /**
138     * Returns the Primitive object at the given location.
139     */
140    private static Primitive getPrimitive(Segment segment, int field, int rep, int component, int subcomponent) throws HL7Exception {
141        Type type = segment.getField(field, rep);
142        return getPrimitive(type, component, subcomponent);
143    }
144    
145    
146        /**
147         * Returns the Primitive object at the given location in the given field.
148         * It is intended that the given type be at the field level, although extra components
149         * will be added blindly if, for example, you provide a primitive subcomponent instead
150         * and specify component or subcomponent > 1
151     * @param type the type from which to get the primitive
152     * @param component the component number (indexed from 1, use 1 for primitive field)
153     * @param subcomponent the subcomponent number (indexed from 1, use 1 for primitive component)
154         */
155        public static Primitive getPrimitive(final Type type, final int component, final int subcomponent) {
156        if (type == null) {
157                throw new NullPointerException("type may not be null");
158        }
159        if (component < 1) {
160                throw new IllegalArgumentException("component must not be 1 or more (note that this parameter is 1-indexed, not 0-indexed)");
161        }
162        if (subcomponent < 1) {
163                throw new IllegalArgumentException("subcomponent must not be 1 or more (note that this parameter is 1-indexed, not 0-indexed)");
164        }
165
166
167        Type comp = getComponent(type, component);
168                if(type instanceof Varies && comp instanceof GenericPrimitive && subcomponent > 1) {
169                        try {
170                                final Varies varies = (Varies)type;
171                                final GenericComposite comp2 = new GenericComposite(type.getMessage());
172                                varies.setData(comp2);
173                                comp = getComponent(type, component);
174                        } catch (final DataTypeException de) {
175                                final String message = "Unexpected exception copying data to generic composite. This is probably a bug within HAPI. " + de.getMessage();
176                                log.error(message, de);
177                                throw new Error(message);
178                        }
179                }
180                final Type sub = getComponent(comp, subcomponent);
181                return getPrimitive(sub);
182        }
183
184    
185    /** 
186     * Attempts to extract a Primitive from the given type. If it's a composite, 
187     * drills down through first components until a primitive is reached. 
188     */
189    private static Primitive getPrimitive(Type type) {
190        Primitive p = null;
191        if (Varies.class.isAssignableFrom(type.getClass())) {
192            p = getPrimitive(((Varies) type).getData());
193        } else if (Composite.class.isAssignableFrom(type.getClass())) {
194            try {
195                p = getPrimitive(((Composite) type).getComponent(0));            
196            } catch (HL7Exception e) {
197                throw new Error("Internal error: HL7Exception thrown on Composite.getComponent(0)."); 
198            }
199        } else if (type instanceof Primitive) {
200            p = (Primitive) type;
201        } 
202        return p;
203    }
204    
205    /**
206     * Returns the component (or sub-component, as the case may be) at the given
207     * index.  If it does not exist, it is added as an "extra component".  
208     * If comp > 1 is requested from a Varies with GenericPrimitive data, the 
209     * data is set to GenericComposite (this avoids the creation of a chain of 
210     * ExtraComponents on GenericPrimitives).  
211     * Components are numbered from 1.  
212     */
213    private static Type getComponent(Type type, int comp) {
214        Type ret = null;
215        if (Varies.class.isAssignableFrom(type.getClass())) {
216            Varies v = (Varies) type;
217            
218            try {
219                if (comp > 1 && GenericPrimitive.class.isAssignableFrom(v.getData().getClass())) 
220                    v.setData(new GenericComposite(v.getMessage()));
221            } catch (DataTypeException de) {
222                String message = "Unexpected exception copying data to generic composite: " + de.getMessage(); 
223                log.error(message, de);
224                throw new Error(message);
225            }
226            
227            ret = getComponent(v.getData(), comp);
228        } else {
229            if (Primitive.class.isAssignableFrom(type.getClass()) && comp == 1) {
230                ret = type;
231            } else if (GenericComposite.class.isAssignableFrom(type.getClass()) 
232                || (Composite.class.isAssignableFrom(type.getClass()) && comp <= numStandardComponents(type))) {
233                //note that GenericComposite can return components > number of standard components
234                
235                try {
236                    ret = ((Composite) type).getComponent(comp - 1);
237                } catch (Exception e) {
238                    throw new Error("Internal error: HL7Exception thrown on getComponent(x) where x < # standard components.", e);
239                }
240            } else {
241                ret = type.getExtraComponents().getComponent(comp - numStandardComponents(type) - 1);
242            }
243        }
244        return ret;
245    }
246    
247    /**
248     * <p>Gets the string value of the field specified.  See the class docs for syntax
249     * of the location spec.  </p>
250     * <p>If a repetition is omitted for a repeating segment or field, the first rep is used.
251     * If the component or subcomponent is not specified for a composite field, the first
252     * component is used (this allows one to write code that will work with later versions of
253     * the HL7 standard).
254     */
255    public String get(String spec) throws HL7Exception {
256        StringTokenizer tok = new StringTokenizer(spec, "-", false);
257        Segment segment = getSegment(tok.nextToken());
258        
259        int[] ind = getIndices(spec);
260        return get(segment, ind[0], ind[1], ind[2], ind[3]);
261    }
262    
263    /** 
264     * Returns the segment specified in the given segment_path_spec. 
265     */
266    public Segment getSegment(String segSpec) throws HL7Exception {
267        Segment seg = null;
268        
269        if (segSpec.substring(0, 1).equals("/")) {
270            getFinder().reset();
271        }
272        
273        StringTokenizer tok = new StringTokenizer(segSpec, "/", false);
274        SegmentFinder finder = getFinder();
275        while(tok.hasMoreTokens()) {
276            String pathSpec = tok.nextToken();
277            Terser.PathSpec ps = parsePathSpec(pathSpec);
278            if (tok.hasMoreTokens()) {
279                ps.isGroup = true;
280            } else {
281                ps.isGroup = false;
282            }
283            
284            if (ps.isGroup) {
285                Group g = null;
286                if (ps.find) {
287                    g = finder.findGroup(ps.pattern, ps.rep);
288                } else {
289                    g = finder.getGroup(ps.pattern, ps.rep);
290                }
291                finder = new SegmentFinder(g);
292            } else {
293                if (ps.find) {
294                    seg = finder.findSegment(ps.pattern, ps.rep);
295                } else {
296                    seg = finder.getSegment(ps.pattern, ps.rep);
297                }
298            }
299        }
300        
301        return seg;
302    }
303    
304    /** Gets path information from a path spec. */
305    private PathSpec parsePathSpec(String spec) throws HL7Exception {
306        PathSpec ps = new PathSpec();
307        
308        if (spec.startsWith(".")) {
309            ps.find = true;
310            spec = spec.substring(1);
311        } else {
312            ps.find = false;
313        }
314        
315        if (spec.length() == 0) {
316            throw new HL7Exception("Invalid path (some path element is either empty or contains only a dot)");
317        }
318        StringTokenizer tok = new StringTokenizer(spec, "()", false);
319        ps.pattern = tok.nextToken();
320        if (tok.hasMoreTokens()) {
321            String repString = tok.nextToken();
322            try {
323                ps.rep = Integer.parseInt(repString);
324            } catch (NumberFormatException e) {
325                throw new HL7Exception(repString + " is not a valid rep #", HL7Exception.APPLICATION_INTERNAL_ERROR);
326            }
327        } else {
328            ps.rep = 0;
329        }
330        return ps;
331    }
332    
333    /** 
334     * Given a Terser path, returns an array containing field num, field rep, 
335     * component, and subcomponent.  
336     */
337    public static int[] getIndices(String spec) throws HL7Exception {
338        StringTokenizer tok = new StringTokenizer(spec, "-", false);
339        tok.nextToken();  //skip over segment
340        if (!tok.hasMoreTokens())
341            throw new HL7Exception("Must specify field in spec " + spec, HL7Exception.APPLICATION_INTERNAL_ERROR);
342        
343        int[] ret = null;
344        try {
345            StringTokenizer fieldSpec = new StringTokenizer(tok.nextToken(), "()", false);
346            int fieldNum = Integer.parseInt(fieldSpec.nextToken());
347            int fieldRep = 0;
348            if (fieldSpec.hasMoreTokens()) {
349                fieldRep = Integer.parseInt(fieldSpec.nextToken());
350            }
351            
352            int component = 1;
353            if (tok.hasMoreTokens()) {
354                component = Integer.parseInt(tok.nextToken());
355            }
356            
357            int subcomponent = 1;
358            if (tok.hasMoreTokens()) {
359                subcomponent = Integer.parseInt(tok.nextToken());
360            }
361            int[] result = {fieldNum, fieldRep, component, subcomponent};
362            ret = result;
363        } catch (NumberFormatException e) {
364            throw new HL7Exception("Invalid integer in spec " + spec, HL7Exception.APPLICATION_INTERNAL_ERROR);
365        }
366        
367        return ret;
368    }
369    
370    /**
371     * Sets the string value of the field specified.  See class docs for location spec syntax.
372     */
373    public void set(String spec, String value) throws HL7Exception {
374        StringTokenizer tok = new StringTokenizer(spec, "-", false);
375        Segment segment = getSegment(tok.nextToken());
376        
377        int[] ind = getIndices(spec);
378        log.debug("Setting {} seg: {} ind: {} {} {} {}", 
379                        new Object[] {spec, segment.getName(), ind[0], ind[1], ind[2], ind[3]});            
380        set(segment, ind[0], ind[1], ind[2], ind[3], value);
381    }
382    
383    /**
384     * Returns the number of components in the given field, i.e. the
385     * number of standard components (e.g. 6 for CE) plus any extra components that
386     * have been added at runtime.  This may vary by repetition, as different reps
387     * may have different extra components.
388     */
389    /*public static int numComponents(Type field) throws HL7Exception {
390        return numComponents(seg.getField(field, rep));
391    }*/
392    
393    /**
394     * Returns the number of sub-components in the specified component, i.e. 
395     * the number of standard sub-components (e.g. 6 for CE) plus any extra components that
396     * that have been added at runtime.
397     * @param component numbered from 1 
398     */
399    public static int numSubComponents(Type type, int component) {
400        int n = -1;
401        if (component == 1 && Primitive.class.isAssignableFrom(type.getClass())) {
402            //note that getComponent(primitive, 1) below returns the primitive 
403            //itself -- if we do numComponents on it, we'll end up with the 
404            //number of components in the field, not the number of subcomponents
405            n = 1;
406        } else {
407            Type comp = getComponent(type, component);
408            n = numComponents(comp);
409        }
410        return n;
411        /*
412        //Type t = seg.getField(field, rep);
413        if (Varies.class.isAssignableFrom(type.getClass())) {
414            return numSubComponents(((Varies) type).getData(), component);
415        } else if (Primitive.class.isAssignableFrom(type.getClass()) && component == 1) {
416            n = 1;  
417        } else if (Composite.class.isAssignableFrom(type.getClass()) && component <= numStandardComponents(t)) {
418            n = numComponents(((Composite) type).getComponent(component - 1));
419        } else { //we're being asked about subcomponents of an extra component
420            n = numComponents(t.getExtraComponents().getComponent(component - numStandardComponents(t) - 1));
421        }
422        return n;
423         */
424    }
425    
426    /**
427     * Returns the number of components in the given type, i.e. the
428     * number of standard components (e.g. 6 for CE) plus any extra components that
429     * have been added at runtime.  
430     */
431    public static int numComponents(Type type) {
432        if (Varies.class.isAssignableFrom(type.getClass())) {
433            return numComponents(((Varies) type).getData());
434        } else {
435            return numStandardComponents(type) + type.getExtraComponents().numComponents();
436        }
437    }
438    
439    private static int numStandardComponents(Type t) {
440        int n = 0;
441        if (Varies.class.isAssignableFrom(t.getClass())) {
442            n = numStandardComponents(((Varies) t).getData());
443        } else if (Composite.class.isAssignableFrom(t.getClass())) {
444            n = ((Composite) t).getComponents().length;
445        } else {
446            n = 1;
447        }
448        return n;
449    }
450    
451    /**
452     * Returns the segment finder used by this Terser.  Navigating the
453     * finder will influence the behaviour of the Terser accordingly.  Ie
454     * when the full path of the segment is not specified the segment will
455     * be sought beginning at the current location of the finder.
456     */
457    public SegmentFinder getFinder() {
458        return finder;
459    }
460    
461    /** Struct for information about a step in a segment path. */
462    private class PathSpec {
463        public String pattern;
464        public boolean isGroup;
465        public boolean find;
466        public int rep;
467    }
468}