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 "ConformanceProfileRule.java".  Description: 
010"A MessageRule that checks conformance to message profiles." 
011
012The Initial Developer of the Original Code is University Health Network. Copyright (C) 
0132005.  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*/
026package ca.uhn.hl7v2.validation.impl;
027
028import java.io.IOException;
029import java.util.ArrayList;
030import java.util.Arrays;
031import java.util.LinkedHashMap;
032import java.util.List;
033import java.util.Map;
034
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038import ca.uhn.hl7v2.HL7Exception;
039import ca.uhn.hl7v2.HapiContext;
040import ca.uhn.hl7v2.conf.ProfileException;
041import ca.uhn.hl7v2.conf.check.Validator;
042import ca.uhn.hl7v2.conf.parser.ProfileParser;
043import ca.uhn.hl7v2.conf.spec.RuntimeProfile;
044import ca.uhn.hl7v2.conf.store.ProfileStore;
045import ca.uhn.hl7v2.model.Message;
046import ca.uhn.hl7v2.util.Terser;
047import ca.uhn.hl7v2.validation.ValidationException;
048
049/**
050 * A MessageRule that checks conformance to message profiles. Messages can either be tested 
051 * against the profiles they declare, or against a pre-defined profile. If you want both, 
052 * use two <code>ConformanceProfileRule</code>s.  
053 * 
054 * @author Bryan Tripp
055 * @version $Revision: 1.1 $ updated on $Date: 2007-02-19 02:24:40 $ by $Author: jamesagnew $
056 */
057@SuppressWarnings("serial")
058public class ConformanceProfileRule extends AbstractMessageRule {
059
060    private static final Logger log = LoggerFactory.getLogger(ConformanceProfileRule.class);
061    private static final ProfileParser PARSER = new ProfileParser(true);
062    private String myProfileID;
063    private boolean enableCaching = true;
064
065    private static final LinkedHashMap<String, RuntimeProfile> PROFILE_CACHE = new LinkedHashMap<String, RuntimeProfile>(100, 0.75f, true) {
066        @Override
067        protected boolean removeEldestEntry(Map.Entry<String, RuntimeProfile> eldest) {
068            return size() > 100;
069        }
070    };
071
072    /**
073     * Creates an instance that tests messages against whatever profiles they declare in 
074     * MSH-21. The ID declared in MSH-21 is evaluated in a way that the corresponding
075     * profile file is expected to be BASEDIR/profiles/ID.xml.
076     */
077    public ConformanceProfileRule() {
078        super();
079        setDescription("Unknown segments found in message");
080        setSectionReference("HL7 2.5 section 2.12");
081    }
082    
083    /**
084     * @param theProfileID the ID of a constant profile against which to test all messages
085     *      (instead of the profiles they declare in MSH-21). The ID is evaluated in a way
086     *      that the corresponding profile file is expected to be BASEDIR/profiles/ID.xml.
087     */
088    public ConformanceProfileRule(String theProfileID) {
089        this();
090        myProfileID = theProfileID;
091    }
092    
093
094    /** 
095     * @see ca.uhn.hl7v2.validation.MessageRule#test(ca.uhn.hl7v2.model.Message)
096     */
097    public ValidationException[] apply(Message msg) {
098        List<ValidationException> problems = new ArrayList<ValidationException>();
099        String[] ids = {myProfileID};
100        
101        try {
102            if (myProfileID == null) {
103                ids = getDeclaredProfileIDs(msg);
104            }
105            
106            for (String id : ids) {
107                log.debug("Testing message against profile: {}", id);
108                try {
109                    ValidationException[] shortList = testAgainstProfile(msg, id);
110                    log.debug("{} non-conformances", shortList.length);
111                    problems.addAll(Arrays.asList(shortList));
112                } catch (ProfileException e) {
113                    problems.add(new ValidationException("Can't validate against profile: " + e.getMessage(), e));
114                }
115            }            
116        } catch (HL7Exception e) {
117            problems.add(new ValidationException("Can't validate against profile: " + e.getMessage(), e));
118        }
119        
120        return problems.toArray(new ValidationException[problems.size()]);
121    }
122    
123    private String[] getDeclaredProfileIDs(Message theMessage) throws HL7Exception {
124        Terser t = new Terser(theMessage);
125        boolean noMore = false;
126        int c = 0;
127        List<String> declaredProfiles = new ArrayList<String>(8);
128        while (!noMore) {
129            String path = "MSH-21(" + c++ + ")";
130            String idRep = t.get(path);
131            //FIXME fails if empty rep precedes full rep ... should add getAll() to Terser and use that
132            if (idRep == null || idRep.equals("")) {
133                noMore = true;
134            } else {
135                declaredProfiles.add(idRep);
136            }
137        }
138        return declaredProfiles.toArray(new String[declaredProfiles.size()]);
139    }
140
141    private synchronized RuntimeProfile getProfile(String profileString) throws ProfileException {
142        RuntimeProfile profile = PROFILE_CACHE.get(profileString);
143        if (profile == null) {
144            profile = PARSER.parse(profileString);
145            if (enableCaching) PROFILE_CACHE.put(profileString, profile);
146        }
147        return profile;
148    }
149
150    private ValidationException[] testAgainstProfile(Message message, String id) throws ProfileException, HL7Exception {
151        HL7Exception[] exceptions;
152        HapiContext context = message.getParser().getHapiContext();
153        Validator validator = context.getConformanceValidator();
154        try {
155            ProfileStore profileStore = context.getProfileStore();
156            String profileString = profileStore.getProfile(id);
157            if (profileString != null) {
158                RuntimeProfile profile = getProfile(profileString);
159                exceptions = validator.validate(message, profile.getMessage());
160            } else {
161                throw new ProfileException("Unable to find the profile " + id);
162            }
163        } catch (IOException e) {
164            throw new ProfileException("Error retreiving profile " + id, e);
165        }
166        
167        ValidationException[] result = new ValidationException[exceptions.length];
168        for (int i = 0; i < exceptions.length; i++) {
169            result[i] = ValidationException.fromHL7Exception(exceptions[i]);
170        }
171        return result;
172    }
173    
174
175    /** 
176     * @see ca.uhn.hl7v2.validation.Rule#getDescription()
177     */
178    public String getDescription() {
179        return "expected conformance to declared or predefined message profiles";
180    }
181
182    /** 
183     * @see ca.uhn.hl7v2.validation.Rule#getSectionReference()
184     */
185    public String getSectionReference() {
186        return "HL7 2.5 section 2.12";
187    }
188
189        public String getProfileID() {
190                return myProfileID;
191        }
192
193    public void setEnableCaching(boolean enableCaching) {
194        this.enableCaching = enableCaching;
195    }
196}