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 ValidationContext myContext;
060        private static final Pattern ourVersionPattern = Pattern.compile("\\.(v2[0-9][0-9]?)\\.");
061        private String myVersion;
062    private transient Parser myParser;
063        
064    /**
065     * @param theFactory factory for model classes (e.g. group, segment) for this message 
066     */
067    public AbstractMessage(ModelClassFactory theFactory) {
068        super(null, theFactory);
069    }
070    
071    /** 
072     * Returns this Message object.  
073     */
074    public Message getMessage() {
075       return this; 
076    }
077    
078        public Group getParent() {
079                return this;
080        }
081
082        /**
083     * Returns the version number.  This default implementation inspects 
084     * this.getClass().getName().  This should be overridden if you are putting
085     * a custom message definition in your own package, or it will default.  
086     * @see Message#getVersion()
087     * 
088     * @return lowest available version if not obvious from package name
089     */
090    public String getVersion() {
091        if (myVersion != null) {
092                return myVersion;
093        }
094        
095        String version = null;
096        Pattern p = ourVersionPattern;
097        Matcher m = p.matcher(this.getClass().getName());
098        if (m.find()) {
099            String verFolder = m.group(1);
100            if (verFolder.length() > 0) {
101                char[] chars = verFolder.toCharArray();
102                StringBuilder buf = new StringBuilder();
103                for (int i = 1; i < chars.length; i++) { //start at 1 to avoid the 'v'
104                    buf.append(chars[i]);
105                    if (i < chars.length - 1) buf.append('.');
106                }
107                version = buf.toString();
108            }
109        }
110        
111        if (version == null) 
112            version = Version.lowestAvailableVersion().getVersion();
113        
114        myVersion = version;
115        return version;
116    }
117    
118    /**
119     * @return the set of validation rules that applies to this message
120     */
121    public ValidationContext getValidationContext() {
122        return myContext;
123    }
124    
125    /**
126     * @param theContext the set of validation rules that are to apply to this message
127     */
128    public void setValidationContext(ValidationContext theContext) {
129        myContext = theContext;
130    }
131
132    /**
133     * {@inheritDoc }
134     */
135    public Character getFieldSeparatorValue() throws HL7Exception {
136        Segment firstSegment = (Segment) get(getNames()[0]);
137        Primitive value = (Primitive) firstSegment.getField(1, 0);
138        String valueString = value.getValue();
139        if (valueString == null || valueString.length() == 0) {
140            return null;
141        }
142        return valueString.charAt(0);
143    }
144
145
146    /**
147     * {@inheritDoc }
148     */
149    public String getEncodingCharactersValue() throws HL7Exception {
150        Segment firstSegment = (Segment) get(getNames()[0]);
151        Primitive value = (Primitive) firstSegment.getField(2, 0);
152        return value.getValue();
153    }
154
155
156    /**
157     * <p>Sets the parser to be used when parse/encode methods are called on this
158     * Message, as well as its children. It is recommended that if these methods
159     * are going to be called, a parser be supplied with the validation context
160     * wanted. Where possible, the parser should be reused for best performance,
161     * unless thread safety is an issue.</p>
162     *
163     * <p>Note that not all parsers can be used. As of version 1.0, only {@link PipeParser}
164     * supports this functionality</p>
165     * 
166     * <p>Serialization note: The message parser is marked as transient, so it will not
167     * survive serialization.</p>
168     */
169    public void setParser(Parser parser) {
170        if (parser == null) {
171            throw new NullPointerException("Value may not be null");
172        }
173
174        myParser = parser;
175    }
176
177
178    /**
179     * <p>Returns the parser to be used when parse/encode methods are called on this
180     * Message, as well as its children. The default value is a new {@link PipeParser}.</p>
181     * 
182     * <p>Serialization note: The message parser is marked as transient, so it will not
183     * survive serialization.</p>
184     */
185    public Parser getParser() {
186        if (myParser == null) {
187            myParser = new PipeParser();
188        }
189
190        return myParser;
191    }
192
193
194    /**
195     * {@inheritDoc }
196     */
197    public void parse(String string) throws HL7Exception {
198        clear();
199                getParser().parse(this, string);
200    }
201
202    
203    /**
204     * {@inheritDoc }
205     */
206    public String encode() throws HL7Exception {
207        return getParser().encode(this);
208    }
209
210    /**
211     * {@inheritDoc }
212     */
213    public Message generateACK() throws HL7Exception, IOException {
214        return generateACK(AcknowledgmentCode.AA, null);
215    }
216
217    /**
218     * {@inheritDoc }
219     * @deprecated
220     */
221    public Message generateACK(String theAcknowledgementCode, HL7Exception theException) throws HL7Exception, IOException {
222        AcknowledgmentCode theCode = theAcknowledgementCode == null ? 
223                        AcknowledgmentCode.AA :
224                        AcknowledgmentCode.valueOf(theAcknowledgementCode);
225        return generateACK(theCode, theException);
226    }
227
228    /**
229     * {@inheritDoc }
230     */    
231    public Message generateACK(AcknowledgmentCode theAcknowledgementCode, HL7Exception theException) throws HL7Exception, IOException {
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        Terser.set(msh, 1, 0, 1, 1, "|");
385        Terser.set(msh, 2, 0, 1, 1, "^~\\&");
386        GregorianCalendar now = new GregorianCalendar();
387        Terser.set(msh, 7, 0, 1, 1, CommonTS.toHl7TSFormat(now));
388        Terser.set(msh, 9, 0, 1, 1, messageCode);
389        Terser.set(msh, 9, 0, 2, 1, messageTriggerEvent);
390        Terser.set(msh, 10, 0, 1, 1, getParser().getParserConfiguration().getIdGenerator().getID());
391        Terser.set(msh, 11, 0, 1, 1, processingId);
392        Terser.set(msh, 12, 0, 1, 1, getVersion());
393        
394        // Add structure information if version is 2.4 or better
395        if (!Version.V24.isGreaterThan(Version.versionOf(getVersion()))) {
396                if (this instanceof SuperStructure) {
397                        Version version = Version.versionOf(getVersion());
398                        Map<String, String> eventMap = new DefaultModelClassFactory().getEventMapForVersion(version);
399                        if (StringUtil.isNotBlank(messageCode) && StringUtil.isNotBlank(messageTriggerEvent)) {
400                                String structure = eventMap.get(messageCode + "_" + messageTriggerEvent);
401                                Terser.set(msh, 9, 0, 3, 1, structure);
402                        }                       
403                } else {
404                        String className = getClass().getName();
405                        int lastIndexOf = className.lastIndexOf('.');
406                                className = className.substring(lastIndexOf + 1);
407                                if (className.matches("[A-Z]{3}_[A-Z0-9]{3}")) {
408                                Terser.set(msh, 9, 0, 3, 1, className);
409                        }
410                }
411        }
412        
413    }
414
415    @Override
416    public boolean accept(MessageVisitor visitor, Location location) throws HL7Exception {
417        if (visitor.start(this)) {
418            visitNestedStructures(visitor, location);
419        }
420        return visitor.end(this);
421    }
422
423
424}