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 "GroupPointer.java".  Description:
010    "A GroupPointer is used when parsing traditionally encoded HL7 messages"
011    
012    The Initial Developer of the Original Code is University Health Network. Copyright (C)
013    2001.  All Rights Reserved.
014    
015    Contributor(s): ______________________________________.
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    package ca.uhn.hl7v2.parser;
028    
029    import java.io.BufferedReader;
030    import java.io.IOException;
031    import java.io.InputStreamReader;
032    import java.net.URL;
033    import java.net.URLConnection;
034    import java.util.ArrayList;
035    import java.util.Arrays;
036    import java.util.HashMap;
037    import java.util.Iterator;
038    import java.util.List;
039    import java.util.Map;
040    import java.util.Stack;
041    import java.util.StringTokenizer;
042    
043    import ca.uhn.hl7v2.HL7Exception;
044    import ca.uhn.hl7v2.model.Message;
045    import ca.uhn.hl7v2.model.Primitive;
046    import ca.uhn.hl7v2.model.Segment;
047    import ca.uhn.hl7v2.model.Type;
048    import ca.uhn.hl7v2.util.Terser;
049    import ca.uhn.log.HapiLog;
050    import ca.uhn.log.HapiLogFactory;
051    
052    /**
053     * A Parser for the ER7 encoding, which is faster than PipeParser, but fussier and harder to use.
054     * It's harder to use in that you must tell it ahead of time which segments and fields to parse 
055     * for each event, as well as where in a message structure to find each segment.  It's fussier in 
056     * that each segment you identify as to-be-parsed must always be present in the message (although 
057     * it can be empty -- the minimum needed is the segment name and a carriage return).  
058     * 
059     * Note that an instance of configuration data (see StructRef below) takes on some state during parsing, 
060     * so it can only be used to parse one message at a time.  There is a synchronized block to ensure this, 
061     * but if parallel parsing with the same configuration (e.g. parsing multiple messages of the same event 
062     * at once) is needed, you may want to pool some FastParsers or use separate ones in separate threads. 
063     *    
064     * @author <a href="mailto:bryan.tripp@uhn.on.ca">Bryan Tripp</a>
065     * @version $Revision: 1.3 $ updated on $Date: 2009/10/03 15:25:46 $ by $Author: jamesagnew $
066     */
067    public class FastParser extends Parser {
068    
069        private static final HapiLog ourLog = HapiLogFactory.getHapiLog(FastParser.class);
070        
071        private static char ourSegmentSeparator = '\r';
072        private Map myEventGuideMap;
073        private PipeParser myPipeParser;
074        
075        /**
076         * @param theEventGuideMap a map with keys in the form "type^event" (like MSH-9 
077         *      components 1 and 2).  Values are corresponding parsing guides for those events.  
078         *      A parsing guide is a group of StructRef that identify which segments to parse, 
079         *      the relationships between them, and where to find them in a message hierarchy.
080         *      The value in the map is the RootRef of the message root.  It must return the 
081         *      StructRef for the MSH segment from getSuccessor("MSH").  References to other 
082         *      segments can be included as needed.   
083         */
084        public FastParser(Map theEventGuideMap) {
085            this(null, theEventGuideMap);
086        }
087    
088        /**
089         * @param theFactory custom factory to use for model class lookup 
090         * @param theEventGuideMap a map with keys in the form "type^event" (like MSH-9 
091         *      components 1 and 2).  Values are corresponding parsing guides for those events.  
092         *      A parsing guide is a group of StructRef that identify which segments to parse, 
093         *      the relationships between them, and where to find them in a message hierarchy.
094         *      The value in the map is the RootRef of the message root.  It must return the 
095         *      StructRef for the MSH segment from getSuccessor("MSH").  References to other 
096         *      segments can be included as needed.   
097         */
098        public FastParser(ModelClassFactory theFactory, Map theEventGuideMap) {
099            super(theFactory);
100            myEventGuideMap = theEventGuideMap;
101            myPipeParser = new PipeParser();
102        }
103        
104        /**
105         * Loads a parsing guide map (as required for FastParser instantiation).  The URL should 
106         * point to a file with one or more guides in sections delimited by blank lines.  Within 
107         * a section, the first line must contain an event name of the for "type^event".  Subsequent 
108         * lines define the parsed parts of messages with that event.  Each line begins with either 
109         * a segment name or "{" (indicating group start) or "}" (indicating group end).  Group  
110         * start lines then have whitespace and a Terser path to the group (relative to the closest 
111         * ancestor group listed in the parsin guide).  Segment lines then have whitespace and a 
112         * relative Terser path to the segment, followed by a colon and a comma-delimited list of field 
113         * numbers, which indicates which fields for that segment are to be parsed.  Within Terser
114         * paths, repetition numbers must be replaced with asterisks. An example follows: 
115         * 
116         * ORU^R01
117         * MSH MSH:9,12
118         * { ORU_R01_PIDNTEPV1ORCOBRNTEOBXNTE(*)
119         *     { ORU_R01_PIDNTEPV1
120         *         PID PID:3-5
121         *     }
122         *     { ORU_R01_ORCOBRNTEOBXNTE(*)
123         *         { ORU_R01_OBXNTE(*)
124         *             OBX OBX:2,5
125         *         }
126         *     }
127         * }
128         * 
129         * ADT^A01
130         * MSH MSH:9,12
131         * PID PID:3
132         * PV1 PV1:7-9
133         * 
134         * @param theMapURL an URL to a file of the form desribed above
135         * @return the corresponding Map 
136         */
137        public static Map loadEventGuideMap(URL theMapURL) throws HL7Exception {
138            Map result = new HashMap();
139            
140            try {
141                URLConnection conn = theMapURL.openConnection();
142                BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
143                
144                String eventName = null;
145                StringBuffer spec = new StringBuffer();
146                String line = null;
147                while ((line = reader.readLine()) != null) {
148                    if (line.length() == 0) {
149                        finish(eventName, spec, result);
150                        eventName = null;
151                        spec = new StringBuffer();
152                    } else {
153                        if (eventName == null) {
154                            eventName = line;
155                        } else {
156                            spec.append(line + "\r");
157                        }
158                    }
159                }
160                reader.close();            
161                finish(eventName, spec, result);
162            } catch (IOException e) {
163                throw new HL7Exception(e);
164            }
165            
166            return result;
167        }
168        
169        private static void finish(String theEventName, StringBuffer theSpec, Map theMap) {
170            if (theEventName != null) {
171                RootRef root = parseGuide(theSpec.toString());
172                theMap.put(theEventName, root);
173            }        
174        }
175        
176        private static RootRef parseGuide(String theSpec) {
177            StringTokenizer lines = new StringTokenizer(theSpec, "\r", false);
178            RootRef result = new RootRef();
179            Stack ancestry = new Stack();
180            ancestry.push(result);
181            Map successors = new HashMap();
182            
183            StructRef previous = result;
184            while (lines.hasMoreTokens()) {
185                String line = lines.nextToken();
186                StringTokenizer parts = new StringTokenizer(line, "\t ", false);
187                String segName = parts.nextToken();
188                String path = parts.hasMoreTokens() ? parts.nextToken() : "";
189                parts = new StringTokenizer(path, ":", false);
190                path = parts.hasMoreTokens() ? parts.nextToken() : null;
191                
192                int[] fields = getFieldList(parts.hasMoreTokens() ? parts.nextToken() : ""); 
193                
194                if (segName.equals("}")) {                
195                    StructRef parent = (StructRef) ancestry.pop();
196                    if (parent.getChildName() != null && parent.getRelativePath().indexOf('*') >= 0) { //repeating group
197                        previous.setSuccessor(parent.getChildName(), parent);                    
198                    }
199                } else {
200                    boolean isSegment = !(segName.equals("{"));
201                    StructRef ref = new StructRef((StructRef) ancestry.peek(), path, isSegment, fields);
202                    if (isSegment) {
203                        previous.setSuccessor(segName, ref);
204                        if (path.indexOf('*') >= 0) ref.setSuccessor(segName, ref);
205                        setGroupSuccessors(successors, segName);
206                    } else {
207                        successors.put(previous, ref);
208                    }
209                    if (!isSegment) ancestry.push(ref);
210                    previous = ref;
211                }
212            }
213            
214            return result;
215        }
216        
217        private static void setGroupSuccessors(Map theSuccessors, String theSegName) {
218            for (Iterator it = theSuccessors.keySet().iterator(); it.hasNext(); ) {
219                StructRef from = (StructRef) it.next();
220                StructRef to = (StructRef) theSuccessors.get(from);
221                from.setSuccessor(theSegName, to);
222            }
223            theSuccessors.clear();
224        }
225        
226        private static int[] getFieldList(String theSpec) {
227            StringTokenizer tok = new StringTokenizer(theSpec, ",", false); 
228            List fieldList = new ArrayList(30);
229            while (tok.hasMoreTokens()) {
230                String token = tok.nextToken();
231                int index = token.indexOf('-');
232                if (index >= 0) { //it's a range
233                    int start = Integer.parseInt(token.substring(0, index));
234                    int end = Integer.parseInt(token.substring(index+1));
235                    for (int i = start; i <= end; i++) {
236                        fieldList.add(new Integer(i));
237                    }
238                } else {
239                    fieldList.add(Integer.valueOf(token));
240                }
241            }
242            
243            int[] result = new int[fieldList.size()];
244            for (int i = 0; i < result.length; i++) {
245                result[i] = ((Integer) fieldList.get(i)).intValue();
246            }
247            
248            return result;
249        }
250    
251        /** 
252         * @see ca.uhn.hl7v2.parser.Parser#getEncoding(java.lang.String)
253         */
254        public String getEncoding(String message) {
255            return myPipeParser.getEncoding(message);
256        }
257    
258        /** 
259         * @see ca.uhn.hl7v2.parser.Parser#supportsEncoding(java.lang.String)
260         */
261        public boolean supportsEncoding(String encoding) {
262            return myPipeParser.supportsEncoding(encoding);
263        }
264        
265        /**
266         * @return the preferred encoding of this Parser
267         */
268        public String getDefaultEncoding() {
269            return "VB";
270        }    
271    
272        /** 
273         * @see ca.uhn.hl7v2.parser.Parser#doParse(java.lang.String, java.lang.String)
274         */
275        protected Message doParse(String message, String version) throws HL7Exception, EncodingNotSupportedException {
276            Message result = null;
277            
278            char fieldSep = message.charAt(3);
279            EncodingCharacters ec = new EncodingCharacters(fieldSep, message.substring(4, 8));
280            
281            StringTokenizer tok = new StringTokenizer(message.substring(4), 
282                    String.valueOf(new char[]{fieldSep, ourSegmentSeparator}), true);
283            
284            String[] mshFields = getMSHFields(tok, fieldSep);
285            Object[] structure = getStructure(mshFields[8], ec.getComponentSeparator());
286            
287            StructRef root = (StructRef) myEventGuideMap.get(structure[0]);
288            if (root == null) {
289                ourLog.debug("FastParser delegating to PipeParser because no metadata available for event " 
290                        + structure[0]);
291                result = myPipeParser.parse(message);
292            } else {
293                int csIndex = mshFields[11].indexOf(ec.getComponentSeparator());
294                result = instantiateMessage((String) structure[1], version, ((Boolean) structure[2]).booleanValue());
295                    
296                StructRef mshRef = null;
297                synchronized (root) {
298                    mshRef = root.getSuccessor("MSH");
299                    root.reset();
300                }
301                Segment msh = (Segment) result.get("MSH");
302                for (int i = 0; i < mshRef.getFields().length; i++) {
303                    int fieldNum = mshRef.getFields()[i];
304                    parse(mshFields[fieldNum-1], msh, fieldNum, ec);
305                }            
306                
307                parse(tok, result, root, ec);
308            }
309            
310            return result;
311        }
312        
313        private String[] getMSHFields(StringTokenizer tok, char fieldSep) {
314            String[] result = new String[21];
315            result[0] = String.valueOf(fieldSep);
316            String token = null;
317            int field = 1;
318            while (tok.hasMoreTokens() && (token = tok.nextToken()).charAt(0) != ourSegmentSeparator) {
319                if (token.charAt(0) == fieldSep) {
320                    field++;
321                } else {
322                    result[field] = token;
323                }
324            } 
325            return result;
326        }
327        
328        private void parse(StringTokenizer tok, Message message, StructRef root, EncodingCharacters ec) 
329                throws HL7Exception {
330            
331            Terser t = new Terser(message);
332            
333            synchronized (root) {
334                StructRef ref = root.getSuccessor("MSH");            
335                
336                int field = 0;
337                Segment segment = null;
338                int[] fields = new int[0];
339                
340                while (tok.hasMoreTokens()) {
341                    String token = tok.nextToken();
342                    if (token.charAt(0) == ec.getFieldSeparator()) {
343                        field++;
344                    } else if (token.charAt(0) == ourSegmentSeparator) {
345                        field = 0;
346                    } else if (field == 0) {
347                        StructRef newref = drill(ref, token);
348                        if (newref == null) {
349                            segment = null;
350                            fields = new int[0];
351                        } else {
352                            ref = newref;
353                            if (ourLog.isDebugEnabled()) {
354                                ourLog.debug("Parsing into segment " + ref.getFullPath());
355                            }
356                            segment = t.getSegment(ref.getFullPath());
357                            fields = ref.getFields();
358                        }
359                    } else if (segment != null && Arrays.binarySearch(fields, field) >= 0) {
360                        parse(token, segment, field, ec);
361                    }
362                }
363                root.reset();
364            }        
365        }
366        
367        //drill through groups to a segment 
368        private StructRef drill(StructRef ref, String name) {
369            ref = ref.getSuccessor(name);
370            while (ref != null && !ref.isSegment()) {
371                ref = ref.getSuccessor(name);
372            }
373            return ref;
374        }
375        
376        private void parse(String field, Segment segment, int num, EncodingCharacters ec) throws HL7Exception {
377            if (field != null) {
378                int rep = 0;
379                int component = 1;
380                int subcomponent = 1;
381                Type type = segment.getField(num, rep);
382                
383                String delim = String.valueOf(new char[]{ec.getRepetitionSeparator(), 
384                        ec.getComponentSeparator(), ec.getSubcomponentSeparator()});
385                for (StringTokenizer tok = new StringTokenizer(field, delim, true); tok.hasMoreTokens(); ) {
386                    String token = tok.nextToken();
387                    char c = token.charAt(0);
388                    if (c == ec.getRepetitionSeparator()) {
389                        rep++;
390                        component = 1;
391                        subcomponent = 1;
392                        type = segment.getField(num, rep);
393                    } else if (c == ec.getComponentSeparator()) {
394                        component++;
395                        subcomponent = 1;
396                    } else if (c == ec.getSubcomponentSeparator()) {
397                        subcomponent++;
398                    } else {
399                        Primitive p = Terser.getPrimitive(type, component, subcomponent);
400                        p.setValue(token);
401                    }
402                }               
403            }
404        }
405        
406        /**
407         * @returns the message structure from MSH-9-3
408         */
409        private Object[] getStructure(String msh9, char compSep) throws HL7Exception {
410            String structure = null;
411            String event = null;
412            
413            String[] components = new String[3];
414            StringTokenizer tok = new StringTokenizer(msh9, String.valueOf(compSep), true);
415            for (int i = 0; tok.hasMoreTokens() && i < components.length; ) {
416                String token = tok.nextToken();
417                if (token.charAt(0) == compSep) {
418                    i++;
419                } else {
420                    components[i] = token;                
421                }
422            }
423    
424            boolean explicitlyDefined = (components[2] == null) ? false : true;
425    
426            if (explicitlyDefined) {
427                structure = components[2];
428            } else if (components[0] != null && components[0].equals("ACK")) {
429                structure = "ACK";
430            } else if (components[0] != null && components[1] != null) {
431                structure = components[0] + "_" + components[1];
432            } else {
433                throw new HL7Exception("Can't determine message structure from MSH-9: " + msh9, 
434                        HL7Exception.UNSUPPORTED_MESSAGE_TYPE);
435            }
436            
437            if (components[1] == null) {
438                event = components[0];
439            } else {
440                event = components[0] + "^" + components[1];
441            }
442            
443            return new Object[] {event, structure, Boolean.valueOf(explicitlyDefined)};
444        }
445        
446    
447        /** 
448         * @see ca.uhn.hl7v2.parser.Parser#encode(ca.uhn.hl7v2.model.Message, java.lang.String)
449         */
450        protected String doEncode(Message source, String encoding) throws HL7Exception,
451                EncodingNotSupportedException {
452            return myPipeParser.doEncode(source, encoding);
453        }
454    
455        /** 
456         * @see ca.uhn.hl7v2.parser.Parser#encode(ca.uhn.hl7v2.model.Message)
457         */
458        protected String doEncode(Message source) throws HL7Exception {
459            return myPipeParser.doEncode(source);
460        }
461    
462        /** 
463         * @see ca.uhn.hl7v2.parser.Parser#getCriticalResponseData(java.lang.String)
464         */
465        public Segment getCriticalResponseData(String message) throws HL7Exception {
466            return myPipeParser.getCriticalResponseData(message);
467        }
468    
469        /** 
470         * @see ca.uhn.hl7v2.parser.Parser#getAckID(java.lang.String)
471         */
472        public String getAckID(String message) {
473            return myPipeParser.getAckID(message);
474        }
475    
476        /** 
477         * @see ca.uhn.hl7v2.parser.Parser#getVersion(java.lang.String)
478         */
479        public String getVersion(String message) throws HL7Exception {
480            return myPipeParser.getVersion(message);
481        }
482    
483        /**
484         * Not supported, throws UnsupportedOperationException
485         *
486         * @throws UnsupportedOperationException
487         */
488        @Override
489        public String doEncode(Segment structure, EncodingCharacters encodingCharacters) throws HL7Exception {
490            throw new UnsupportedOperationException("Not supported yet.");
491        }
492    
493        /**
494         * Not supported, throws UnsupportedOperationException
495         *
496         * @throws UnsupportedOperationException
497         */
498        @Override
499        public String doEncode(Type type, EncodingCharacters encodingCharacters) throws HL7Exception {
500            throw new UnsupportedOperationException("Not supported yet.");
501        }
502    
503        /**
504         * Not supported, throws UnsupportedOperationException
505         *
506         * @throws UnsupportedOperationException
507         */
508        @Override
509        public void parse(Type type, String string, EncodingCharacters encodingCharacters) throws HL7Exception {
510            throw new UnsupportedOperationException("Not supported yet.");
511        }
512    
513        /**
514         * Not supported, throws UnsupportedOperationException
515         *
516         * @throws UnsupportedOperationException
517         */
518        @Override
519        public void parse(Segment segment, String string, EncodingCharacters encodingCharacters) throws HL7Exception {
520            throw new UnsupportedOperationException("Not supported yet.");
521        }
522    
523        
524        /**
525         * Not supported, throws UnsupportedOperationException
526         *
527         * @throws UnsupportedOperationException
528         */
529        @Override
530        public void parse(Message message, String string) throws HL7Exception {
531            throw new UnsupportedOperationException("Not supported yet.");
532        }
533        
534        /**
535         * A pointer to a distinct segment or group position in a message.  
536         *  
537         * @author <a href="mailto:bryan.tripp@uhn.on.ca">Bryan Tripp</a>
538         * @version $Revision: 1.3 $ updated on $Date: 2009/10/03 15:25:46 $ by $Author: jamesagnew $
539         */
540        public static class StructRef {
541    
542            private StructRef myParent;
543            private String myRelativePath;
544            private Map mySuccessors;
545            private int myRep;
546            private boolean mySegmentFlag;
547            //private boolean myResettableFlag;
548            private int[] myFields;
549            private List myChildren;
550            
551            /**
552             * @param theParent a StructRef for the parent Group of the referenced Structure
553             * @param theRelativePath the relative (from the parent) Terser path to the referenced 
554             *      structure.  If the structure repeats, the rep number should be replaced with "*"
555             *      (it will be incremented as needed). 
556             * @param isSegment true iff the referenced Structure is a Segment (rather than a Group)
557             * @param theFields a list of fields to be parsed for this segment (null or empty for groups)
558             */
559            public StructRef(StructRef theParent, String theRelativePath, boolean isSegment, int[] theFields) {
560                myParent = theParent;
561                myChildren = new ArrayList();
562                if (myParent != null) myParent.addChild(this);
563                
564                myRelativePath = theRelativePath;
565                if (!myRelativePath.startsWith("/")) {
566                    myRelativePath = "/" + myRelativePath;
567                }
568                mySegmentFlag = isSegment;
569                mySuccessors = new HashMap();
570                myRep = -1;
571                if (mySegmentFlag) {
572                    myFields = theFields;
573                    Arrays.sort(myFields);                
574                } else {
575                    myFields = new int[0];
576                }
577                //myResettableFlag = (myParent == null) ? true : false;
578            }
579            
580            /**
581             * Indicates an immediately subsequent structure in parsing order.  A Structure in a list 
582             * should point to the next Structure in the list.  A Structure that repeats should point to 
583             * itself.  A Structure at the end of a repeating Group should point to the Group. 
584             * A Group should point to its first child.  
585             * 
586             * @param theName name of the next Segment in this direction (ie if the next structure is a group, 
587             *      not that one)
588             * @param theSuccessor the immediately next StructRef in that direction
589             */
590            public void setSuccessor(String theName, StructRef theSuccessor) {
591                mySuccessors.put(theName, theSuccessor);
592            }
593            
594            /**
595             * @return full Terser path, including parent and repetition information.  
596             */
597            public String getFullPath() {
598                return myParent.getFullPath() + myRelativePath.replaceAll("\\*", String.valueOf(myRep));
599            }
600            
601            /**
602             * @return relative Terser path as defined in constructor
603             */
604            public String getRelativePath() {
605                return myRelativePath;
606            }
607            
608            /**
609             * @param theName name of a successor in parse order, as set in setSuccessor()
610             * @return the StructRef under that name 
611             */
612            public StructRef getSuccessor(String theName) {
613                StructRef ref = (StructRef) mySuccessors.get(theName);
614                if (ref != null) {
615                    ref.next();
616                } 
617                return ref;
618            }
619            
620            /**
621             * @return name of first successor, if available and if this is not a segment reference, 
622             *      otherwise null 
623             */
624            public String getChildName() {
625                String result = null;
626                if (!mySegmentFlag && !mySuccessors.isEmpty()) {
627                    result = (String) mySuccessors.keySet().iterator().next();                
628                }
629                return result;
630            }
631            
632            /**
633             * @return true iff referenced Structure is a Segment 
634             */
635            public boolean isSegment() {
636                return mySegmentFlag;
637            }
638            
639            /**
640             * Increments the repetition number of the underlying Structure, which is used in getFullPath() 
641             */
642            private void next() {
643                myRep++;
644                resetChildren();
645            }
646            
647            private void addChild(StructRef theChild) {
648                if (!isSegment()) {
649                    myChildren.add(theChild);
650                }
651            }
652            
653            /**
654             * Resets the StructRef to its starting state, before its first iteration, and resets 
655             * its children as well.  
656             */
657            public void reset() {
658                myRep = -1;
659                resetChildren();
660            }
661            
662            private void resetChildren() {
663                for (int i = 0; i < myChildren.size(); i++) {
664                    StructRef child = (StructRef) myChildren.get(i);
665                    child.reset();
666                }            
667            }
668            
669            /**
670             * @return an ordered list of fields to be parsed for this segment (empty if not a segment)
671             */
672            public int[] getFields() {
673                return myFields;
674            }
675            
676        }
677        
678        /**
679         * A convenience StructRef that points to a message root.  
680         * 
681         * @author <a href="mailto:bryan.tripp@uhn.on.ca">Bryan Tripp</a>
682         * @version $Revision: 1.3 $ updated on $Date: 2009/10/03 15:25:46 $ by $Author: jamesagnew $
683         */
684        public static class RootRef extends StructRef {
685            public RootRef() {
686                super(null, "", false, null);
687            }
688            
689            public String getFullPath() { 
690                return "";
691            }
692        }
693    
694    }