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 "FixOBX5.java".  Description:
010 ""
011
012 The Initial Developer of the Original Code is University Health Network. Copyright (C)
013 2015.  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
027package ca.uhn.hl7v2.parser;
028
029import java.lang.reflect.Constructor;
030
031import ca.uhn.hl7v2.ErrorCode;
032import ca.uhn.hl7v2.HL7Exception;
033import ca.uhn.hl7v2.Version;
034import ca.uhn.hl7v2.model.*;
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038/**
039 * Utility class that provides methods for fixing OBX-5 data type. This has been refactored out
040 * of {@link ca.uhn.hl7v2.model.Varies}.
041 */
042public final class FixFieldDataType {
043
044    private FixFieldDataType() {}
045
046    /**
047     * System property key: The value may be set to provide a default
048     * datatype ("ST", "NM", etc) for an OBX segment with a missing
049     * OBX-2 value.
050     */
051    public static final String DEFAULT_OBX2_TYPE_PROP = "ca.uhn.hl7v2.model.varies.default_obx2_type";
052
053    /**
054     * System property key: The value may be set to provide a default
055     * datatype ("ST", "NM", etc) for an OBX segment with an invalid
056     * OBX-2 value type. In other words, if OBX-2 has a value of "ZYZYZ",
057     * which is not a valid value, but this property is set to "ST", then
058     * OBX-5 will be parsed as an ST.
059     */
060    public static final String INVALID_OBX2_TYPE_PROP = "ca.uhn.hl7v2.model.varies.invalid_obx2_type";
061
062    /**
063     * System property key: The value may be set to provide a default
064     * datatype ("ST", "NM", etc) for an MFE segment with a missing
065     * MFE-5 value.
066     */
067    public static final String DEFAULT_MFE5_TYPE_PROP = "ca.uhn.hl7v2.model.varies.default_mfe5_type";
068
069    /**
070     * System property key: The value may be set to provide a default
071     * datatype ("ST", "NM", etc) for an MFE segment with an invalid
072     * MFE-5 value type. In other words, if MFE-5 has a value of "ZYZYZ",
073     * which is not a valid value, but this property is set to "ST", then
074     * MFE-4 will be parsed as an ST.
075     */
076    public static final String INVALID_MFE5_TYPE_PROP = "ca.uhn.hl7v2.model.varies.invalid_mfe5_type";
077
078
079    /**
080     * <p>
081     * System property key: If this is not set, or set to "true", and a subcomponent delimiter is found within the
082     * value of a Varies of a primitive type, this subcomponent delimiter will be treated as a literal
083     * character instead of a subcomponent delimiter, and will therefore be escaped if the message is
084     * re-encoded. This is handy when dealing with non-conformant sending systems which do not correctly
085     * escape ampersands in OBX-5 values.
086     * </p>
087     * <p>
088     * For example, consider the following OBX-5 segment:
089     * <pre>
090     *    OBX||ST|||Apples, Pears &amp; Bananas|||
091     * </pre>
092     * In this example, the data type is a primitive ST and does not support subcomponents, and the
093     * ampersand is obviously not intended to represent a subcomponent delimiter. If this
094     * property is set to <code>true</code>, the entire string will be treated as the
095     * value of OBX-5, and if the message is re-encoded the string will appear
096     * as "Apples, Pears \T\ Bananas".
097     * </p>
098     * <p>
099     * If this property is set to anything other than "true", the subcomponent delimiter is treated as a component delimiter,
100     * so the value after the ampersand is placed into an {@link ExtraComponents extra component}.
101     * </p>
102     */
103    public static final String ESCAPE_SUBCOMPONENT_DELIM_IN_PRIMITIVE = "ca.uhn.hl7v2.model.varies.escape_subcomponent_delim_in_primitive";
104
105
106    private static final Logger LOG = LoggerFactory.getLogger(Varies.class);
107
108
109    /**
110     * <p>
111     * Sets the data type of field 5 in the given OBX segment to the value of OBX-2.  The argument
112     * is a Segment as opposed to a particular OBX because it is meant to work with any version.
113     * </p>
114     * <p>
115     * Note that if no value is present in OBX-2, or an invalid value is present in
116     * OBX-2, this method will throw an error. This behaviour can be corrected by using the
117     * following system properties: {@link #DEFAULT_OBX2_TYPE_PROP} and {@link #INVALID_OBX2_TYPE_PROP}
118     * or by using configuration in {@link ParserConfiguration}
119     * </p>
120     *
121     * @param segment OBX segment instance to be modified
122     * @param factory ModelClassFactory to be used
123     * @param parserConfiguration configuration that influences setting OBX-5
124     * @throws ca.uhn.hl7v2.HL7Exception if the operation fails
125     */
126    public static void fixOBX5(Segment segment, ModelClassFactory factory, ParserConfiguration parserConfiguration)
127            throws HL7Exception {
128        if (!segment.getName().contains("OBX")) {
129            throw new IllegalArgumentException("Expected OBX segment, but was: " + segment.getName());
130        }
131        String defaultOBX2Type = parserConfiguration.getDefaultObx2Type();
132        if (defaultOBX2Type == null) {
133            defaultOBX2Type = System.getProperty(DEFAULT_OBX2_TYPE_PROP);
134        }
135        String invalidOBX2Type = parserConfiguration.getInvalidObx2Type();
136        if (invalidOBX2Type == null) {
137            invalidOBX2Type = System.getProperty(INVALID_OBX2_TYPE_PROP);
138        }
139
140        fix(segment, 2, 5, defaultOBX2Type, invalidOBX2Type, factory, parserConfiguration);
141    }
142
143    /**
144     * <p>
145     * Sets the data type of field 4 in the given MFE segment to the value of MFE-5.  The argument
146     * is a Segment as opposed to a particular MFE because it is meant to work with any version.
147     * </p>
148     * <p>
149     * Note that if no value is present in MFE-5, or an invalid value is present in
150     * MFE-5, this method will throw an error. This behaviour can be corrected by using the
151     * following system properties: {@link #DEFAULT_MFE5_TYPE_PROP} and {@link #INVALID_MFE5_TYPE_PROP}
152     * or by using configuration in {@link ParserConfiguration}
153     * </p>
154     *
155     * @param segment MFE segment instance to be modified
156     * @param factory ModelClassFactory to be used
157     * @param parserConfiguration configuration that influences setting MFE-5
158     * @throws ca.uhn.hl7v2.HL7Exception if the operation fails
159     */
160    public static void fixMFE4(Segment segment, ModelClassFactory factory, ParserConfiguration parserConfiguration)
161            throws HL7Exception {
162        if (!(segment.getName().contains("MFE")) &&
163                Version.versionOf(segment.getMessage().getVersion()).isGreaterThan(Version.V23)) {
164            throw new IllegalArgumentException("Expected MFE segment, but was: " + segment.getName());
165        }
166
167        String defaultMFE5Type = parserConfiguration.getDefaultMfe5Type();
168        if (defaultMFE5Type == null) {
169            defaultMFE5Type = System.getProperty(DEFAULT_MFE5_TYPE_PROP);
170        }
171
172        String invalidMFE5Type = parserConfiguration.getInvalidMfe5Type();
173        if (invalidMFE5Type == null) {
174            invalidMFE5Type = System.getProperty(INVALID_MFE5_TYPE_PROP);
175        }
176
177        fix(segment, 5, 4, defaultMFE5Type, invalidMFE5Type, factory, parserConfiguration);
178    }
179
180    /**
181     * A more generic version of the task of adapting a varies field to a given type
182     *
183     * @param segment segment instance
184     * @param typeField field number of the specified data type
185     * @param dataField field number of the varies data field
186     * @param defaultType default type if the typeField is empty
187     * @param invalidType default type if the typeField is invalid
188     * @param factory ModelClassFactory to be used
189     * @param parserConfiguration parser config
190     * @throws HL7Exception if the operation fails
191     */
192    public static void fix(Segment segment, int typeField, int dataField, String defaultType, String invalidType, ModelClassFactory factory, ParserConfiguration parserConfiguration)
193        throws HL7Exception {
194        try {
195            //get unqualified class name
196            Primitive type = (Primitive) segment.getField(typeField, 0);
197            Type[] reps = segment.getField(dataField);
198            for (Type rep : reps) {
199                Varies v = (Varies)rep;
200                if (type.getValue() == null) {
201                    if (defaultType != null) {
202                        LOG.debug("setting default {}-{} type to {}", segment.getName(), typeField, defaultType);
203                        type.setValue(defaultType);
204                    }
205                } // if
206
207                if (type.getValue() == null) {
208                    if (v.getData() != null) {
209                        if (!(v.getData() instanceof Primitive) || ((Primitive) v.getData()).getValue() != null) {
210                            throw new HL7Exception(String.format(
211                                    "A datatype for %s-%d must be specified in %s-%d.", segment.getName(), dataField, segment.getName(), typeField),
212                                    ErrorCode.REQUIRED_FIELD_MISSING);
213                        }
214                    }
215                }
216                else {
217                    //set class
218                    String version = segment.getMessage().getVersion();
219                    String typeValue = type.getValue();
220                    Class<? extends Type> c = factory.getTypeClass(typeValue, version);
221                    if (c == null) {
222                        if (invalidType != null) {
223                            c = factory.getTypeClass(invalidType, version);
224                        }
225
226                        if (c == null) {
227                            Primitive obx1 = (Primitive) segment.getField(1, 0);
228                            HL7Exception h = new HL7Exception("'" +
229                                    type.getValue() + "' in record " +
230                                    obx1.getValue() + " is invalid for version " + version,
231                                    ErrorCode.DATA_TYPE_ERROR);
232                            h.setSegmentName(segment.getName());
233                            h.setFieldPosition(typeField);
234                            throw h;
235                        }
236                    }
237
238                    Type newTypeInstance;
239                    try {
240                        Constructor<? extends Type> constr = c.getConstructor(Message.class);
241                        newTypeInstance = constr.newInstance(v.getMessage());
242                    } catch (NoSuchMethodException e) {
243                        Constructor<? extends Type> constr = c.getConstructor(Message.class, Integer.class);
244                        newTypeInstance = constr.newInstance(v.getMessage(), 0);
245                    }
246
247                    boolean escapeSubcomponentDelimInPrimitive =
248                            parserConfiguration.isEscapeSubcomponentDelimiterInPrimitive() ||
249                                    escapeSubcomponentDelimInPrimitive();
250
251
252                    if (newTypeInstance instanceof Primitive) {
253                        Type[] subComponentsInFirstField = getFirstComponentSubcomponentsOnlyIfMoreThanOne(v);
254                        if (subComponentsInFirstField != null) {
255
256                            if (escapeSubcomponentDelimInPrimitive) {
257
258                                StringBuilder firstComponentValue = new StringBuilder();
259                                for (Type stype : subComponentsInFirstField) {
260                                    if (firstComponentValue.length() != 0) {
261                                        char subComponentSeparator = EncodingCharacters.getInstance(segment.getMessage()).getSubcomponentSeparator();
262                                        firstComponentValue.append(subComponentSeparator);
263                                    }
264                                    firstComponentValue.append(stype.encode());
265                                }
266
267                                setFirstComponentPrimitiveValue(v, firstComponentValue.toString());
268
269                            }
270
271                        }
272                    }
273
274                    v.setData(newTypeInstance);
275                }
276
277            } // for reps
278
279        }
280        catch (HL7Exception e) {
281            throw e;
282        }
283        catch (Exception e) {
284            throw new HL7Exception(
285                    e.getClass().getName() + " trying to set data type of " + segment.getName() + "-" + dataField, e);
286        }
287    }
288
289    private static boolean escapeSubcomponentDelimInPrimitive() {
290        String property = System.getProperty(ESCAPE_SUBCOMPONENT_DELIM_IN_PRIMITIVE);
291        return property == null || "true".equalsIgnoreCase(property);
292    }
293
294    private static void setFirstComponentPrimitiveValue(Varies v, String theValue) throws DataTypeException {
295        Composite c = (Composite) v.getData();
296        Type firstComponent = c.getComponent(0);
297        setFirstComponentPrimitiveValue(firstComponent, theValue);
298    }
299
300
301    private static void setFirstComponentPrimitiveValue(Type theFirstComponent, String theValue)
302            throws DataTypeException {
303
304        if (theFirstComponent instanceof Varies) {
305            Varies firstComponentVaries = (Varies)theFirstComponent;
306            if (((Varies) theFirstComponent).getData() instanceof Composite) {
307                Type[] subComponents = ((Composite)firstComponentVaries.getData()).getComponents();
308                setFirstComponentPrimitiveValue(subComponents[0], theValue);
309                for (int i = 1; i < subComponents.length; i++) {
310                    setFirstComponentPrimitiveValue(subComponents[i], "");
311                }
312            } else {
313                Primitive p = (Primitive) firstComponentVaries.getData();
314                p.setValue(theValue);
315            }
316        } else if (theFirstComponent instanceof Composite) {
317            Type[] subComponents = ((Composite)theFirstComponent).getComponents();
318            setFirstComponentPrimitiveValue(subComponents[0], theValue);
319            for (int i = 1; i < subComponents.length; i++) {
320                setFirstComponentPrimitiveValue(subComponents[i], "");
321            }
322        } else {
323            ((Primitive)theFirstComponent).setValue(theValue);
324        }
325    }
326
327    /**
328     * Returns an array containing the subcomponents within the first component of this Varies
329     * object only if there are more than one of them. Otherwise, returns null.
330     */
331    private static Type[] getFirstComponentSubcomponentsOnlyIfMoreThanOne(Varies v) throws DataTypeException {
332        if (v.getData() instanceof Composite) {
333            Composite c = (Composite) v.getData();
334            Type firstComponent = c.getComponent(0);
335            if (firstComponent instanceof Varies) {
336                Varies firstComponentVaries = (Varies) firstComponent;
337                if (firstComponentVaries.getData() instanceof Composite) {
338                    return ((Composite)firstComponentVaries.getData()).getComponents();
339                }
340            }
341        }
342        return null;
343    }
344}