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 Initial Developer of the Original Code is University Health Network. Copyright (C)
0102001.  All Rights Reserved.
011
012Contributor(s): ______________________________________.
013
014Alternatively, the contents of this file may be used under the terms of the
015GNU General Public License (the  �GPL�), in which case the provisions of the GPL are
016applicable instead of those above.  If you wish to allow use of your version of this
017file only under the terms of the GPL and not to allow others to use your version
018of this file under the MPL, indicate your decision by deleting  the provisions above
019and replace  them with the notice and other provisions required by the GPL License.
020If you do not delete the provisions above, a recipient may use your version of
021this file under either the MPL or the GPL.
022
023*/
024package ca.uhn.hl7v2.parser;
025
026import java.util.HashMap;
027import java.util.Map;
028import java.util.Map.Entry;
029import java.util.concurrent.ConcurrentHashMap;
030import java.util.concurrent.ConcurrentMap;
031
032import ca.uhn.hl7v2.model.*;
033import org.slf4j.Logger;
034import org.slf4j.LoggerFactory;
035
036import ca.uhn.hl7v2.HL7Exception;
037import ca.uhn.hl7v2.Version;
038import ca.uhn.hl7v2.util.StringUtil;
039
040/**
041 * ModelClassFactory which allows custom packages to search to be specified.
042 * These packages will be searched first, and if nothing is found for a particular
043 * structure, a delegate ModelClassFactory (by default DefaultModelClassFactory) is used. 
044 * <p>
045 * Also, a custom event map location is supported (by setting {@link #setEventMapDirectory(String)}
046 * in a way that only new or modified structures needs to be defined there. If no structure was
047 * found, the event map of the delegate ModelClassFactory serves as fallback.
048 *
049 * @author Christian Ohr
050 * @author James Agnew
051 * @since 1.0
052 */
053public class CustomModelClassFactory extends AbstractModelClassFactory {
054
055    private static final long serialVersionUID = 1;
056    private static Logger LOG = LoggerFactory.getLogger(CustomModelClassFactory.class);
057
058    private final ModelClassFactory delegate;
059    private Map<String, String[]> customModelClasses;
060
061    // some optimization
062    private ConcurrentMap<String, Class> cache = new ConcurrentHashMap<String, Class>();
063
064    /**
065     * Constructor which just delegated to {@link DefaultModelClassFactory}
066     */
067    public CustomModelClassFactory() {
068        this((Map<String, String[]>)null);
069    }
070
071
072    /**
073     * Constructor
074     *
075     * @param packageName The base package name to use.
076     * <p>
077     * When searching, package specified here will be appended with .[version].[structure type].
078     * </p>
079     * <p>
080     * So, for instance, when looking for a v2.5 segment object, if "<code>com.foo</code>" is passed in, HAPI will look in "<code>com.foo.v25.segment.*</code>"
081     * </p>
082     */
083    public CustomModelClassFactory(String packageName) {
084        this(new HashMap<String, String[]>());
085
086        if (!packageName.endsWith(".")) {
087            packageName += ".";
088        }
089        for (Version v : Version.values()) {
090                addModel(v.getVersion(), new String[] {packageName + v.getPackageVersion()});
091        }
092    }
093
094    
095    /**
096     * Constructor
097     * @param map Map of packages to include.
098     * <p>
099     * Keys are versions of HL7, e.g. "2.5".
100     * </p>
101     * <p>
102     * Values are an array of packages to search in for custom model classes.
103     * When searching, the package name here will be appended with "<b>.[structure type]</b>".
104     * So, for example, to specify a custom message type, you could create the class
105     * <code>foo.example.v23.message.ZRM_Z01</code>, and pass in the string "<code>foo.example.v23</code>".
106     * </p>
107     */
108    public CustomModelClassFactory(Map<String, String[]> map) {
109        this(new DefaultModelClassFactory(), map);
110    }
111    
112    /**
113     * Set an explicit {@link ModelClassFactory} is underlying delegate
114     * @param defaultFactory default factory to be delegated to
115     * @param map custom model map
116     */
117    public CustomModelClassFactory(ModelClassFactory defaultFactory, Map<String, String[]> map) {
118        this.delegate = defaultFactory;
119        customModelClasses = map;
120    }    
121
122    /**
123     * {@inheritDoc }
124     */
125        public Class<? extends Message> getMessageClass(String name, String version, boolean isExplicit) throws HL7Exception {
126        if (!isExplicit) {
127            name = getMessageStructureForEvent(name, Version.versionOf(version));
128        }
129        String key = "message" + name + version;
130        Class<? extends Message> retVal = cache.get(key);
131        if (retVal != null) return retVal;
132
133        retVal = findClass("message", name, version);
134        if (retVal == null) {
135            retVal = delegate.getMessageClass(name, version, isExplicit);
136        }
137        if (retVal != null) cache.putIfAbsent(key, retVal);
138        return retVal;
139    }
140
141    /**
142     * {@inheritDoc }
143     */
144        public Class<? extends Group> getGroupClass(String name, String version) throws HL7Exception {
145        String key = "group" + name + version;
146        Class<? extends Group> retVal = cache.get(key);
147        if (retVal != null) return retVal;
148        retVal = findClass("group", name, version);
149        if (retVal == null) {
150            retVal = delegate.getGroupClass(name, version);
151        }
152        if (retVal != null) cache.putIfAbsent(key, retVal);
153        return retVal;
154    }
155
156    /**
157     * {@inheritDoc }
158     */
159        public Class<? extends Segment> getSegmentClass(String name, String version) throws HL7Exception {
160        String key = "segment" + name + version;
161        Class<? extends Segment> retVal = cache.get(key);
162        if (retVal != null) return retVal;
163        retVal = findClass("segment", name, version);
164        if (retVal == null) {
165            retVal = delegate.getSegmentClass(name, version);
166        }
167        if (retVal != null) cache.putIfAbsent(key, retVal);
168        return retVal;
169    }
170
171    /**
172     * {@inheritDoc }
173     */
174        public Class<? extends Type> getTypeClass(String name, String version) throws HL7Exception {
175        String key = "datatype" + name + version;
176        Class<? extends Type> retVal = cache.get(key);
177        if (retVal != null) return retVal;
178        retVal = findClass("datatype", name, version);
179        if (retVal == null) {
180            retVal = delegate.getTypeClass(name, version);
181        }
182        if (retVal != null) cache.putIfAbsent(key, retVal);
183        return retVal;
184    }
185
186    /**
187     * Finds appropriate classes to be loaded for the given structure/type
188     */
189    @SuppressWarnings("unchecked")
190        protected <T> Class<T> findClass(String subpackage, String name, String version) throws HL7Exception {
191        Class<T> classLoaded = null;
192        if (customModelClasses != null) {
193            if (customModelClasses.containsKey(version)) {
194                for (String next : customModelClasses.get(version)) {
195                    if (!next.endsWith(".")) {
196                        next += ".";
197                    }
198                    String fullyQualifiedName = next + subpackage + '.' + name;
199                    try {
200                        classLoaded = (Class<T>) Class.forName(fullyQualifiedName);
201                        LOG.debug("Found " + fullyQualifiedName + " in custom HL7 model");
202                    } catch (ClassNotFoundException e) {
203                        // ignore
204                    }
205                }
206            }
207        }
208        return classLoaded;
209    }
210
211    /**
212     * Delegates calls to {@link DefaultModelClassFactory#getMessageClassInASpecificPackage(String, String, boolean, String)}
213     */
214        public Class<? extends Message> getMessageClassInASpecificPackage(String theName, String theVersion, boolean theIsExplicit, String thePackageName) throws HL7Exception {
215                return delegate.getMessageClassInASpecificPackage(theName, theVersion, theIsExplicit, thePackageName);
216        }
217
218    /**
219     * Returns the configured custom model classes
220     * @return a map of custom model classes
221     */
222    public Map<String, String[]> getCustomModelClasses() {
223                return customModelClasses;
224        }
225
226        /**
227         * Add model class packages after the object has been instantiated
228         * 
229         * @param addedModelClasses map with version number as key and package names has value
230         */
231        public void addModels(Map<String, String[]> addedModelClasses) {
232        if (customModelClasses == null) {
233                customModelClasses = new HashMap<String, String[]>();
234        }
235        for (Entry<String, String[]> entry : addedModelClasses.entrySet()) {
236                addModel(entry.getKey(), entry.getValue());
237        }
238    }
239        
240        private void addModel(String version, String[] newPackageNames) {
241        if (customModelClasses.containsKey(version)) {
242            // the new packages must be added after the existing ones.
243            String[] existingPackageNames = customModelClasses.get(version);
244            customModelClasses.put(version, StringUtil.concatenate(existingPackageNames, newPackageNames));
245        } else {
246                customModelClasses.put(version, newPackageNames);
247        }               
248        }
249
250
251        /**
252         * Looks up its own event map. If no structure was found, the call is delegated to
253         * the default ModelClassFactory. If nothing can be found, the eventName is returned
254         * as structure.
255         * 
256         * @see ca.uhn.hl7v2.parser.AbstractModelClassFactory#getMessageStructureForEvent(java.lang.String, ca.uhn.hl7v2.Version)
257         */
258        @Override
259        public String getMessageStructureForEvent(String eventName, Version version) throws HL7Exception {
260                String structure = super.getMessageStructureForEvent(eventName, version);
261        if (structure != null) return structure;
262                structure = delegate.getMessageStructureForEvent(eventName, version);
263                return structure != null ? structure : eventName;
264        }
265        
266}