001/**
002The 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. 
004You may obtain a copy of the License at http://www.mozilla.org/MPL/ 
005Software distributed under the License is distributed on an "AS IS" basis, 
006WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 
007specific language governing rights and limitations under the License. 
008
009The Original Code is "AbstractMessage.java".  Description: 
010"A default implementation of Message" 
011
012The Initial Developer of the Original Code is University Health Network. Copyright (C) 
0132001.  All Rights Reserved. 
014
015Contributor(s): ______________________________________. 
016
017Alternatively, the contents of this file may be used under the terms of the 
018GNU General Public License (the  �GPL�), in which case the provisions of the GPL are 
019applicable instead of those above.  If you wish to allow use of your version of this 
020file only under the terms of the GPL and not to allow others to use your version 
021of this file under the MPL, indicate your decision by deleting  the provisions above 
022and replace  them with the notice and other provisions required by the GPL License.  
023If you do not delete the provisions above, a recipient may use your version of 
024this file under either the MPL or the GPL. 
025
026*/
027
028package ca.uhn.hl7v2.model;
029
030import java.io.IOException;
031import java.util.Date;
032import java.util.GregorianCalendar;
033import java.util.Map;
034import java.util.regex.Matcher;
035import java.util.regex.Pattern;
036
037import ca.uhn.hl7v2.AcknowledgmentCode;
038import ca.uhn.hl7v2.HL7Exception;
039import ca.uhn.hl7v2.Location;
040import ca.uhn.hl7v2.Version;
041import ca.uhn.hl7v2.model.primitive.CommonTS;
042import ca.uhn.hl7v2.parser.DefaultModelClassFactory;
043import ca.uhn.hl7v2.parser.ModelClassFactory;
044import ca.uhn.hl7v2.parser.Parser;
045import ca.uhn.hl7v2.parser.PipeParser;
046import ca.uhn.hl7v2.util.ArrayUtil;
047import ca.uhn.hl7v2.util.ReflectionUtil;
048import ca.uhn.hl7v2.util.StringUtil;
049import ca.uhn.hl7v2.util.Terser;
050import ca.uhn.hl7v2.validation.ValidationContext;
051
052/**
053 * A default implementation of Message. 
054 * @author Bryan Tripp (bryan_tripp@sourceforge.net)
055 */
056@SuppressWarnings("serial")
057public abstract class AbstractMessage extends AbstractGroup implements Message {
058
059        private static final Pattern ourVersionPattern = Pattern.compile("\\.(v2[0-9][0-9]?)\\.");
060        private String myVersion;
061    private transient Parser myParser;
062        
063    /**
064     * @param theFactory factory for model classes (e.g. group, segment) for this message 
065     */
066    public AbstractMessage(ModelClassFactory theFactory) {
067        super(null, theFactory);
068    }
069    
070    /** 
071     * Returns this Message object.  
072     */
073    public Message getMessage() {
074       return this; 
075    }
076    
077        public Group getParent() {
078                return this;
079        }
080
081        /**
082     * Returns the version number.  This default implementation inspects 
083     * this.getClass().getName().  This should be overridden if you are putting
084     * a custom message definition in your own package, or it will default.  
085     * @see Message#getVersion()
086     * 
087     * @return lowest available version if not obvious from package name
088     */
089    public String getVersion() {
090        if (myVersion != null) {
091                return myVersion;
092        }
093        
094        String version = null;
095        Pattern p = ourVersionPattern;
096        Matcher m = p.matcher(this.getClass().getName());
097        if (m.find()) {
098            String verFolder = m.group(1);
099            if (verFolder.length() > 0) {
100                char[] chars = verFolder.toCharArray();
101                StringBuilder buf = new StringBuilder();
102                for (int i = 1; i < chars.length; i++) { //start at 1 to avoid the 'v'
103                    buf.append(chars[i]);
104                    if (i < chars.length - 1) buf.append('.');
105                }
106                version = buf.toString();
107            }
108        }
109        
110        if (version == null) 
111            version = Version.lowestAvailableVersion().getVersion();
112        
113        myVersion = version;
114        return version;
115    }
116    
117    /**
118     * Returns the set of validation rules that applied to this message. If the parser
119     * was set to "not-validating", this method returns null
120     *
121     * @return the set of validation rules that applied to this message
122     */
123    public ValidationContext getValidationContext() {
124        if (getParser() == null || !getParser().getParserConfiguration().isValidating()) return null;
125        return getParser().getHapiContext().getValidationContext();
126    }
127
128
129    /**
130     * {@inheritDoc }
131     */
132    public Character getFieldSeparatorValue() throws HL7Exception {
133        Segment firstSegment = (Segment) get(getNames()[0]);
134        Primitive value = (Primitive) firstSegment.getField(1, 0);
135        String valueString = value.getValue();
136        if (valueString == null || valueString.length() == 0) {
137            return null;
138        }
139        return valueString.charAt(0);
140    }
141
142
143    /**
144     * {@inheritDoc }
145     */
146    public String getEncodingCharactersValue() throws HL7Exception {
147        Segment firstSegment = (Segment) get(getNames()[0]);
148        Primitive value = (Primitive) firstSegment.getField(2, 0);
149        return value.getValue();
150    }
151
152
153    /**
154     * <p>Sets the parser to be used when parse/encode methods are called on this
155     * Message, as well as its children. It is recommended that if these methods
156     * are going to be called, a parser be supplied with the validation context
157     * wanted. Where possible, the parser should be reused for best performance,
158     * unless thread safety is an issue.</p>
159     *
160     * <p>Note that not all parsers can be used. As of version 1.0, only {@link PipeParser}
161     * supports this functionality</p>
162     * 
163     * <p>Serialization note: The message parser is marked as transient, so it will not
164     * survive serialization.</p>
165     */
166    public void setParser(Parser parser) {
167        if (parser == null) {
168            throw new NullPointerException("Value may not be null");
169        }
170
171        myParser = parser;
172    }
173
174
175    /**
176     * <p>Returns the parser to be used when parse/encode methods are called on this
177     * Message, as well as its children. The default value is a new {@link PipeParser}.</p>
178     * 
179     * <p>Serialization note: The message parser is marked as transient, so it will not
180     * survive serialization.</p>
181     */
182    public Parser getParser() {
183        if (myParser == null) {
184            myParser = new PipeParser();
185        }
186
187        return myParser;
188    }
189
190
191    /**
192     * {@inheritDoc }
193     */
194    public void parse(String string) throws HL7Exception {
195        clear();
196                getParser().parse(this, string);
197    }
198
199    
200    /**
201     * {@inheritDoc }
202     */
203    public String encode() throws HL7Exception {
204        return getParser().encode(this);
205    }
206
207    /**
208     * {@inheritDoc }
209     */
210    public Message generateACK() throws HL7Exception, IOException {
211        return generateACK(AcknowledgmentCode.AA, null);
212    }
213
214    /**
215     * {@inheritDoc }
216     * @deprecated
217     */
218    public Message generateACK(String theAcknowledgementCode, HL7Exception theException) throws HL7Exception, IOException {
219        AcknowledgmentCode theCode = theAcknowledgementCode == null ? 
220                        AcknowledgmentCode.AA :
221                        AcknowledgmentCode.valueOf(theAcknowledgementCode);
222        return generateACK(theCode, theException);
223    }
224
225    /**
226     * {@inheritDoc }
227     */    
228    public Message generateACK(AcknowledgmentCode theAcknowledgementCode, HL7Exception theException) throws HL7Exception, IOException {
229        if (theException != null && theException.getResponseMessage() != null) {
230            return theException.getResponseMessage();
231        }
232                Message out = instantiateACK();
233                out.setParser(getParser());
234                fillResponseHeader(out, theAcknowledgementCode);
235        if (theException != null) {
236            theException.populateResponse(out, theAcknowledgementCode, 0);
237        }
238        return out;
239    }
240
241        private Message instantiateACK() throws HL7Exception {
242                ModelClassFactory mcf = getParser() != null ? 
243                                getParser().getFactory() : 
244                                new DefaultModelClassFactory();
245                Version version = Version.versionOf(getVersion());
246                Message out = null;
247                if (version != null && version.available()) {
248                        Class<? extends Message> clazz = mcf.getMessageClass("ACK", version.getVersion(), false);
249                        if (clazz != null) {
250                            out = ReflectionUtil.instantiateMessage(clazz, mcf);
251                        }
252                }
253                if (out == null) {
254                        out = new GenericMessage.UnknownVersion(mcf);
255                }
256                
257                if (out instanceof GenericMessage) {
258                        if (!ArrayUtil.contains(out.getNames(), "MSA")) {
259                                out.addNonstandardSegment("MSA");
260                        }
261                        if (!ArrayUtil.contains(out.getNames(), "ERR")) {
262                                out.addNonstandardSegment("ERR");
263                        }
264                }
265                
266                return out;
267        }
268
269        /**
270         * Populates certain required fields in a response message header, using
271         * information from the corresponding inbound message. The current time is
272         * used for the message time field, and <code>MessageIDGenerator</code> is
273         * used to create a unique message ID. Version and message type fields are
274         * not populated.
275     *
276     * @param out outgoing message to be populated
277     * @param code acknowledgment code
278     * @return outgoing message
279     * @throws HL7Exception if header cannot be filled
280     * @throws IOException if message ID could not be generated
281         */
282        public Message fillResponseHeader(Message out, AcknowledgmentCode code)
283                        throws HL7Exception, IOException {
284                Segment mshIn = (Segment) get("MSH");
285                Segment mshOut = (Segment) out.get("MSH");
286
287                // get MSH data from incoming message ...
288                String fieldSep = Terser.get(mshIn, 1, 0, 1, 1);
289                String encChars = Terser.get(mshIn, 2, 0, 1, 1);
290                String procID = Terser.get(mshIn, 11, 0, 1, 1);
291
292                // populate outbound MSH using data from inbound message ...
293                Terser.set(mshOut, 1, 0, 1, 1, fieldSep);
294                Terser.set(mshOut, 2, 0, 1, 1, encChars);
295                GregorianCalendar now = new GregorianCalendar();
296                now.setTime(new Date());
297                Terser.set(mshOut, 7, 0, 1, 1, CommonTS.toHl7TSFormat(now));
298                Terser.set(mshOut, 9, 0, 1, 1, "ACK");
299                Terser.set(mshOut, 9, 0, 2, 1, Terser.get(mshIn, 9, 0, 2, 1));
300                String v = mshOut.getMessage().getVersion();
301                if (v != null) {
302                        Version version = Version.versionOf(v);
303                        if (version != null && !Version.V25.isGreaterThan(version)) {
304                                Terser.set(mshOut, 9, 0, 3, 1, "ACK");
305                        }
306                }
307                Terser.set(mshOut, 10, 0, 1, 1, mshIn.getMessage().getParser().getParserConfiguration().getIdGenerator().getID());
308                Terser.set(mshOut, 11, 0, 1, 1, procID);
309                
310                String versionId = Terser.get(mshIn, 12, 0, 1, 1);
311                if (StringUtil.isBlank(versionId)) {
312                        versionId = Version.highestAvailableVersionOrDefault().getVersion();
313                }
314                Terser.set(mshOut, 12, 0, 1, 1, versionId);
315
316                // revert sender and receiver
317                Terser.set(mshOut, 3, 0, 1, 1, Terser.get(mshIn, 5, 0, 1, 1));
318                Terser.set(mshOut, 4, 0, 1, 1, Terser.get(mshIn, 6, 0, 1, 1));
319                Terser.set(mshOut, 5, 0, 1, 1, Terser.get(mshIn, 3, 0, 1, 1));
320                Terser.set(mshOut, 6, 0, 1, 1, Terser.get(mshIn, 4, 0, 1, 1));
321                
322                // fill MSA for the happy case
323                Segment msaOut = (Segment) out.get("MSA");
324                Terser.set(msaOut, 1, 0, 1, 1, code.name());
325                Terser.set(msaOut, 2, 0, 1, 1, Terser.get(mshIn, 10, 0, 1, 1));
326                return out;
327        }
328    
329
330    /**
331     * Provides an overview of the type and structure of this message
332     */
333    @Override
334    public String toString() {
335        try {
336            return encode();
337        } catch (HL7Exception e) {
338            return (getClass().getName() + " - Failed to create toString(): " + e.getMessage());
339        }
340    }
341
342    /**
343     * {@inheritDoc}
344     */
345    public String printStructure() throws HL7Exception {
346        StringBuilder builder = new StringBuilder();    
347        appendStructureDescription(builder, 0, false, false, true, true, true);
348        return builder.toString();
349    }
350    
351    /**
352     * Prints the message structure in a similar way to {@link #printStructure()} but
353     * optionally excludes elements with no contents.
354     */
355    public String printStructure(boolean includeEmptyElements) throws HL7Exception {
356        StringBuilder builder = new StringBuilder();    
357        appendStructureDescription(builder, 0, false, false, true, true, includeEmptyElements);
358        return builder.toString();
359    }
360
361    /**
362     * Quickly initializes this message with common values in the first (MSH) segment.
363     * 
364     * <p>
365     * Settings include:
366     *  <ul>
367     *          <li>MSH-1 (Field Separator) is set to "|"</li>
368     *          <li>MSH-2 (Encoding Characters) is set to "^~\&"</li>
369     *          <li>MSH-7 (Date/Time of Message) is set to current time</li>
370     *          <li>MSH-10 (Control ID) is populated using next value generated by a
371     *          {@link ca.uhn.hl7v2.util.idgenerator.IDGenerator IDGenerator}</li>
372     *  </ul>
373     * </p>
374     * 
375     * @param messageCode The message code (aka message type) to insert into MSH-9-1. Example: "ADT"
376     * @param messageTriggerEvent The message trigger event to insert into MSG-9-2. Example: "A01"
377     * @param processingId The message processing ID to insert into MSH-11. Examples: "T" (for TEST) or "P" for (PRODUCTION)
378     * 
379     * @throws IOException If the message ID generation fails for some reason 
380     * @throws HL7Exception If the message rejects any of the values which are generated to setting
381     */
382    public void initQuickstart(String messageCode, String messageTriggerEvent, String processingId) throws HL7Exception, IOException {
383        Segment msh = (Segment) get("MSH");
384        Version version = Version.versionOf(getVersion());
385        Terser.set(msh, 1, 0, 1, 1, "|");
386        Terser.set(msh, 2, 0, 1, 1, Version.V27.isGreaterThan(version) ?
387                "^~\\&" : "^~\\&#");
388        GregorianCalendar now = new GregorianCalendar();
389        Terser.set(msh, 7, 0, 1, 1, CommonTS.toHl7TSFormat(now));
390        Terser.set(msh, 9, 0, 1, 1, messageCode);
391        Terser.set(msh, 9, 0, 2, 1, messageTriggerEvent);
392        Terser.set(msh, 10, 0, 1, 1, getParser().getParserConfiguration().getIdGenerator().getID());
393        Terser.set(msh, 11, 0, 1, 1, processingId);
394        Terser.set(msh, 12, 0, 1, 1, getVersion());
395        
396        // Add structure information if version is 2.4 or better
397        if (!Version.V24.isGreaterThan(version)) {
398                if (this instanceof SuperStructure) {
399                        Map<String, String> eventMap = new DefaultModelClassFactory().getEventMapForVersion(version);
400                        if (StringUtil.isNotBlank(messageCode) && StringUtil.isNotBlank(messageTriggerEvent)) {
401                                String structure = eventMap.get(messageCode + "_" + messageTriggerEvent);
402                                Terser.set(msh, 9, 0, 3, 1, structure);
403                        }                       
404                } else {
405                        String className = getClass().getName();
406                        int lastIndexOf = className.lastIndexOf('.');
407                                className = className.substring(lastIndexOf + 1);
408                                if (className.matches("[A-Z]{3}_[A-Z0-9]{3}")) {
409                                Terser.set(msh, 9, 0, 3, 1, className);
410                        }
411                }
412        }
413        
414    }
415
416    @Override
417    public boolean accept(MessageVisitor visitor, Location location) throws HL7Exception {
418        if (visitor.start(this)) {
419            visitNestedStructures(visitor, location);
420        }
421        return visitor.end(this);
422    }
423
424
425}