001/*
002 * **** BEGIN LICENSE BLOCK *****
003 *  Version: MPL 1.1/GPL 2.0/LGPL 2.1
004 *
005 *  The contents of this file are subject to the Mozilla Public License Version
006 *  1.1 (the "License"); you may not use this file except in compliance with
007 *  the License. You may obtain a copy of the License at
008 *  http://www.mozilla.org/MPL/
009 *
010 *  Software distributed under the License is distributed on an "AS IS" basis,
011 *  WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
012 *  for the specific language governing rights and limitations under the
013 *  License.
014 *
015 *  The Original Code is part of dcm4che, an implementation of DICOM(TM) in
016 *  Java(TM), hosted at https://github.com/gunterze/dcm4che.
017 *
018 *  The Initial Developer of the Original Code is
019 *  Agfa Healthcare.
020 *  Portions created by the Initial Developer are Copyright (C) 2014
021 *  the Initial Developer. All Rights Reserved.
022 *
023 *  Contributor(s):
024 *  See @authors listed below
025 *
026 *  Alternatively, the contents of this file may be used under the terms of
027 *  either the GNU General Public License Version 2 or later (the "GPL"), or
028 *  the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
029 *  in which case the provisions of the GPL or the LGPL are applicable instead
030 *  of those above. If you wish to allow use of your version of this file only
031 *  under the terms of either the GPL or the LGPL, and not to allow others to
032 *  use your version of this file under the terms of the MPL, indicate your
033 *  decision by deleting the provisions above and replace them with the notice
034 *  and other provisions required by the GPL or the LGPL. If you do not delete
035 *  the provisions above, a recipient may use your version of this file under
036 *  the terms of any one of the MPL, the GPL or the LGPL.
037 *
038 *  ***** END LICENSE BLOCK *****
039 */
040package org.dcm4che3.conf.dicom;
041
042import org.dcm4che3.audit.EventID;
043import org.dcm4che3.audit.EventTypeCode;
044import org.dcm4che3.audit.ObjectFactory;
045import org.dcm4che3.audit.RoleIDCode;
046import org.dcm4che3.conf.api.*;
047import org.dcm4che3.conf.api.internal.DicomConfigurationManager;
048import org.dcm4che3.conf.core.DefaultBeanVitalizer;
049import org.dcm4che3.conf.core.DefaultTypeSafeConfiguration;
050import org.dcm4che3.conf.core.Nodes;
051import org.dcm4che3.conf.core.adapters.NullToNullDecorator;
052import org.dcm4che3.conf.core.api.BatchRunner.Batch;
053import org.dcm4che3.conf.core.api.Configuration;
054import org.dcm4che3.conf.core.api.ConfigurationException;
055import org.dcm4che3.conf.core.api.TypeSafeConfiguration;
056import org.dcm4che3.conf.core.api.internal.BeanVitalizer;
057import org.dcm4che3.conf.core.context.LoadingContext;
058import org.dcm4che3.conf.dicom.adapters.*;
059import org.dcm4che3.data.Code;
060import org.dcm4che3.data.Issuer;
061import org.dcm4che3.data.ValueSelector;
062import org.dcm4che3.net.ApplicationEntity;
063import org.dcm4che3.net.Device;
064import org.dcm4che3.net.DeviceInfo;
065import org.dcm4che3.util.AttributesFormat;
066import org.dcm4che3.util.Property;
067import org.slf4j.Logger;
068import org.slf4j.LoggerFactory;
069
070import java.util.*;
071
072/**
073 * @author Roman K
074 */
075@SuppressWarnings("unchecked")
076public class CommonDicomConfiguration implements DicomConfigurationManager, TransferCapabilityConfigExtension {
077
078    private static final Logger log = LoggerFactory.getLogger(CommonDicomConfiguration.class);
079
080
081    /**
082     * see preventDeviceModifications(org.dcm4che3.net.Device)
083     */
084    private final Map<Device, Object> readOnlyDevices = Collections.synchronizedMap(new WeakHashMap<Device, Object>());
085
086    protected final Configuration lowLevelConfig;
087    private final BeanVitalizer vitalizer;
088
089    private final Map<Class, List<Class>> extensionsByClass;
090    private final TypeSafeConfiguration<DicomConfigurationRoot> config;
091    private AlternativeTCLoader alternativeTCLoader;
092
093    public CommonDicomConfiguration(Configuration configurationStorage, Map<Class, List<Class>> extensionsByClass, boolean doCacheTCGroups) {
094        this(configurationStorage, extensionsByClass);
095        alternativeTCLoader = new AlternativeTCLoader(this, doCacheTCGroups);
096    }
097
098    public CommonDicomConfiguration(Configuration configStorage, Map<Class, List<Class>> extensionsByClass) {
099
100        config = new DefaultTypeSafeConfiguration<DicomConfigurationRoot>(
101                configStorage,
102                DicomConfigurationRoot.class,
103                extensionsByClass
104        );
105
106        this.lowLevelConfig = configStorage;
107        this.extensionsByClass = extensionsByClass;
108
109        vitalizer = config.getVitalizer();
110
111        addCustomAdapters(vitalizer);
112
113        // quick init
114        try {
115            if (!configurationExists()) {
116                lowLevelConfig.persistNode(DicomPath.CONFIG_ROOT_PATH, createInitialConfigRootNode(), null);
117
118            }
119        } catch (ConfigurationException e) {
120            throw new RuntimeException("Dicom configuration cannot be initialized", e);
121        }
122
123        alternativeTCLoader = new AlternativeTCLoader(this, false);
124    }
125
126    /**
127     * Returns a list of registered extensions for a specified base extension class
128     *
129     * @param clazz
130     * @param <T>
131     * @return
132     */
133    @Override
134    public <T> List<Class<? extends T>> getExtensionClassesByBaseClass(Class<T> clazz) {
135        List<Class> classes = extensionsByClass.get(clazz);
136
137        List<Class<? extends T>> list = new ArrayList<Class<? extends T>>();
138
139        if (classes != null)
140            for (Class<?> aClass : classes) list.add((Class<? extends T>) aClass);
141
142        return list;
143    }
144
145    public static void addCustomAdapters(BeanVitalizer defaultBeanVitalizer) {
146
147        // register DICOM type adapters
148        defaultBeanVitalizer.registerCustomConfigTypeAdapter(AttributesFormat.class, new NullToNullDecorator(new AttributeFormatTypeAdapter()));
149        defaultBeanVitalizer.registerCustomConfigTypeAdapter(Code.class, new NullToNullDecorator(new CodeTypeAdapter()));
150        defaultBeanVitalizer.registerCustomConfigTypeAdapter(Issuer.class, new NullToNullDecorator(new IssuerTypeAdapter()));
151        defaultBeanVitalizer.registerCustomConfigTypeAdapter(ValueSelector.class, new NullToNullDecorator(new ValueSelectorTypeAdapter()));
152        defaultBeanVitalizer.registerCustomConfigTypeAdapter(Property.class, new NullToNullDecorator(new PropertyTypeAdapter()));
153
154        // register audit log type adapters
155        defaultBeanVitalizer.registerCustomConfigTypeAdapter(EventTypeCode.class, new NullToNullDecorator(new AuditSimpleTypeAdapters.EventTypeCodeAdapter()));
156        defaultBeanVitalizer.registerCustomConfigTypeAdapter(EventID.class, new NullToNullDecorator(new AuditSimpleTypeAdapters.EventIDTypeAdapter()));
157        defaultBeanVitalizer.registerCustomConfigTypeAdapter(RoleIDCode.class, new NullToNullDecorator(new AuditSimpleTypeAdapters.RoleIDCodeTypeAdapter()));
158    }
159
160    protected HashMap<String, Object> createInitialConfigRootNode() {
161        HashMap<String, Object> rootNode = new HashMap<String, Object>();
162        rootNode.put("dicomDevicesRoot", new HashMap<String, Object>());
163
164        List<Object> pathItems = new ArrayList<Object>(METADATA_ROOT_PATH.getPathItems());
165        pathItems.remove(0);
166
167        Nodes.replaceNode(rootNode, new HashMap(), pathItems);
168        return rootNode;
169    }
170
171    @Override
172    public TypeSafeConfiguration<DicomConfigurationRoot> getTypeSafeConfiguration() {
173        return config;
174    }
175
176    @Override
177    public boolean configurationExists() throws ConfigurationException {
178        return lowLevelConfig.nodeExists(DicomPath.CONFIG_ROOT_PATH);
179    }
180
181    @Override
182    public boolean purgeConfiguration() throws ConfigurationException {
183        if (!configurationExists()) return false;
184        lowLevelConfig.persistNode(DicomPath.CONFIG_ROOT_PATH, new HashMap<String, Object>(), null);
185        return true;
186    }
187
188    @Override
189    public void preventDeviceModifications(Device d) {
190        readOnlyDevices.put(d, true);
191    }
192
193    @Override
194    public void refreshTCGroups() {
195        alternativeTCLoader.refreshTCGroups();
196    }
197
198    @Override
199    public boolean registerAETitle(String aet) throws ConfigurationException {
200        return true;
201    }
202
203    @Override
204    public void unregisterAETitle(String aet) throws ConfigurationException {
205    }
206
207
208    @Override
209    public ApplicationEntity findApplicationEntity(String aet) throws ConfigurationException {
210
211        if (aet == null) throw new IllegalArgumentException("Requested AE's title cannot be null");
212
213        Iterator<?> search = lowLevelConfig.search(DicomPath.DeviceNameByAEName.set("aeName", aet).path());
214
215        if (!search.hasNext()) {
216            search = lowLevelConfig.search(DicomPath.DeviceNameByAENameAlias.set("aeNameAlias", aet).path());
217
218            if (!search.hasNext())
219                throw new ConfigurationNotFoundException("AE '" + aet + "' not found");
220        }
221
222        String deviceNameNode = (String) search.next();
223        if (search.hasNext())
224            log.warn("Application entity title '{}' is not unique. Check the configuration!", aet);
225        Device device = findDevice(deviceNameNode);
226
227        ApplicationEntity ae = device.getApplicationEntity(aet);
228        if (ae == null)
229            throw new NoSuchElementException("Unexpected error");
230        return ae;
231
232    }
233
234    @Override
235    public ApplicationEntity findApplicationEntityByUUID(String uuid) throws ConfigurationException {
236
237        if (uuid == null) throw new IllegalArgumentException("Requested AE's uuid cannot be null");
238
239        Iterator search = lowLevelConfig.search(DicomPath.DeviceNameByAEUUID.set("aeUUID", uuid).path());
240
241        try {
242            String deviceNameNode = (String) search.next();
243            Device device = findDevice(deviceNameNode);
244
245            for (ApplicationEntity applicationEntity : device.getApplicationEntities()) {
246                if (uuid.equals(applicationEntity.getUuid())) {
247                    return applicationEntity;
248                }
249            }
250
251            throw new NoSuchElementException("Unexpected error");
252
253        } catch (NoSuchElementException e) {
254            throw new ConfigurationNotFoundException("AE with UUID '" + uuid + "' not found", e);
255        }
256    }
257
258    @Override
259    public Device findDeviceByUUID(String uuid) throws ConfigurationException {
260        if (uuid == null) throw new IllegalArgumentException("Requested Device's uuid cannot be null");
261
262        Iterator search = lowLevelConfig.search(DicomPath.DeviceNameByUUID.set("deviceUUID", uuid).path());
263
264        try {
265            String deviceNameNode = (String) search.next();
266            return findDevice(deviceNameNode);
267        } catch (NoSuchElementException e) {
268            throw new ConfigurationNotFoundException("Device with UUID '" + uuid + "' not found", e);
269        }
270    }
271
272    @Override
273    public Device findDevice(String name, DicomConfigOptions options) throws ConfigurationException {
274
275        options = options == null ? new DicomConfigOptions() : options;
276
277        if (name == null) throw new IllegalArgumentException("Requested device name cannot be null");
278
279        try {
280            Object deviceConfigurationNode = lowLevelConfig.getConfigurationNode(DicomPath.devicePath(name), Device.class);
281            if (deviceConfigurationNode == null)
282                throw new ConfigurationNotFoundException("Device " + name + " not found");
283
284            LoadingContext ctx = config.getContextFactory().newLoadingContext();
285            if (options.getIgnoreUnresolvedReferences() == Boolean.TRUE) {
286                ctx.setIgnoreUnresolvedReferences(true);
287            }
288
289            Device device = vitalizer.newConfiguredInstance((Map<String, Object>) deviceConfigurationNode, Device.class, ctx);
290
291            // perform alternative TC init in case an extension is present
292            alternativeTCLoader.initGroupBasedTCs(device);
293
294            return device;
295        } catch (ConfigurationNotFoundException e) {
296            throw e;
297        } catch (RuntimeException e) {
298            throw new ConfigurationException("Configuration for device " + name + " cannot be loaded", e);
299        }
300
301    }
302
303    @Override
304    public Device findDevice(String name) throws ConfigurationException {
305        return findDevice(name, null);
306    }
307
308    @Override
309    public DeviceInfo[] listDeviceInfos(DeviceInfo keys) throws ConfigurationException {
310        throw new RuntimeException("Not yet implemented");
311    }
312
313    @Override
314    public String[] listDeviceNames() throws ConfigurationException {
315        Iterator search = lowLevelConfig.search(DicomPath.AllDeviceNames.path());
316        List<String> deviceNames = null;
317        try {
318            deviceNames = new ArrayList<String>();
319            while (search.hasNext())
320                deviceNames.add((String) search.next());
321        } catch (Exception e) {
322            throw new ConfigurationException("Error while getting list of device names", e);
323        }
324        return deviceNames.toArray(new String[deviceNames.size()]);
325    }
326
327    @Override
328    public List<String> listAllAETitles() throws ConfigurationException {
329        List<String> aeNames = new ArrayList<String>();
330        try {
331            Iterator search = lowLevelConfig.search(DicomPath.AllAETitles.path());
332            while (search.hasNext())
333                aeNames.add((String) search.next());
334        } catch (Exception e) {
335            throw new ConfigurationException("Error while getting the list of registered AE titles", e);
336        }
337        return aeNames;
338    }
339
340    @Override
341    public void persist(Device device) throws ConfigurationException {
342
343        if (readOnlyDevices.containsKey(device)) handleReadOnlyDeviceModification();
344
345        if (device.getDeviceName() == null) throw new ConfigurationException("The name of the device must not be null");
346        if (lowLevelConfig.nodeExists(DicomPath.devicePath(device.getDeviceName())))
347            throw new ConfigurationAlreadyExistsException("Device " + device.getDeviceName() + " already exists");
348        // otherwise it is the same as merge
349        merge(device);
350    }
351
352    private void handleReadOnlyDeviceModification() {
353
354        String message = "Persisting the config for a Device object that is marked as read-only. " +
355                "This warning is not affecting the behavior for now, but soon it will be replaced with throwing an exception!" +
356                "If you want to make config modifications, use a separate instance of Device! See CSP configuration docs for details.";
357
358        // create exception to log the stacktrace
359        ConfigurationException exception = new ConfigurationException();
360
361        log.warn(message, exception);
362    }
363
364    @Override
365    public void merge(Device device) throws ConfigurationException {
366
367        if (readOnlyDevices.containsKey(device)) handleReadOnlyDeviceModification();
368
369        if (device.getDeviceName() == null) throw new ConfigurationException("The name of the device must not be null");
370        Map<String, Object> configNode = createDeviceConfigNode(device);
371        lowLevelConfig.persistNode(DicomPath.devicePath(device.getDeviceName()), configNode, Device.class);
372    }
373
374    protected Map<String, Object> createDeviceConfigNode(Device device) throws ConfigurationException {
375
376        final Map<String, Object> deviceConfigNode = vitalizer.createConfigNodeFromInstance(device, Device.class);
377
378        // wipe out TCs in case an extension is present
379        alternativeTCLoader.cleanUpTransferCapabilitiesInDeviceNode(device, deviceConfigNode);
380
381
382        return deviceConfigNode;
383    }
384
385    @Override
386    public void removeDevice(String name) throws ConfigurationException {
387        lowLevelConfig.removeNode(DicomPath.devicePath(name));
388    }
389
390
391    @Override
392    public void close() {
393
394    }
395
396    @Override
397    public void sync() throws ConfigurationException {
398        lowLevelConfig.refreshNode(DicomPath.CONFIG_ROOT_PATH);
399    }
400
401    @Override
402    public <T> T getDicomConfigurationExtension(Class<T> clazz) {
403
404        // trick CDI
405        if (TransferCapabilityConfigExtension.class.equals(clazz)) {
406            return (T) new TransferCapabilityConfigExtension() {
407                @Override
408                public void persistTransferCapabilityConfig(TCConfiguration tcConfig) throws ConfigurationException {
409                    CommonDicomConfiguration.this.persistTransferCapabilityConfig(tcConfig);
410                }
411
412                @Override
413                public TCConfiguration getTransferCapabilityConfig() throws ConfigurationException {
414                    return CommonDicomConfiguration.this.getTransferCapabilityConfig();
415                }
416            };
417        }
418
419        if (!clazz.isAssignableFrom(this.getClass())) {
420            throw new IllegalArgumentException("Cannot find a configuration extension for class " + clazz.getName());
421        }
422
423        return (T) this;
424    }
425
426    @Override
427    public BeanVitalizer getVitalizer() {
428        return vitalizer;
429    }
430
431
432    @Override
433    public Configuration getConfigurationStorage() {
434        return lowLevelConfig;
435    }
436
437    @Override
438    public void persistTransferCapabilityConfig(TCConfiguration tcConfig) throws ConfigurationException {
439        Map<String, Object> configNode = vitalizer.createConfigNodeFromInstance(tcConfig);
440        lowLevelConfig.persistNode(DicomPath.TC_GROUPS_PATH, configNode, TCConfiguration.class);
441    }
442
443    @Override
444    public TCConfiguration getTransferCapabilityConfig() throws ConfigurationException {
445        Map<String, Object> configurationNode = (Map<String, Object>) lowLevelConfig.getConfigurationNode(DicomPath.TC_GROUPS_PATH, TCConfiguration.class);
446
447        if (configurationNode == null)
448            return new TCConfiguration();
449
450        return vitalizer.newConfiguredInstance(
451                configurationNode,
452                TCConfiguration.class);
453    }
454
455    @Override
456    public void runBatch(final DicomConfigBatch dicomConfigBatch) {
457        /*
458         * Use the batch support of underlying configuration storage to execute batch
459         */
460        lowLevelConfig.runBatch(new Batch() {
461
462            @Override
463            public void run() {
464                dicomConfigBatch.run();
465            }
466
467        });
468    }
469
470    public static BeanVitalizer createDefaultDicomVitalizer() {
471        DefaultBeanVitalizer defaultBeanVitalizer = new DefaultBeanVitalizer();
472        addCustomAdapters(defaultBeanVitalizer);
473        return defaultBeanVitalizer;
474    }
475}
476