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.core.adapters;
041
042import com.google.common.util.concurrent.SettableFuture;
043import org.dcm4che3.conf.core.api.*;
044import org.dcm4che3.conf.core.api.internal.ConfigProperty;
045import org.dcm4che3.conf.core.api.internal.ConfigReflection;
046import org.dcm4che3.conf.core.api.internal.ConfigTypeAdapter;
047import org.dcm4che3.conf.core.context.LoadingContext;
048import org.dcm4che3.conf.core.context.ProcessingContext;
049import org.dcm4che3.conf.core.context.Referable;
050import org.dcm4che3.conf.core.context.SavingContext;
051import org.dcm4che3.conf.core.util.PathFollower;
052import org.slf4j.Logger;
053import org.slf4j.LoggerFactory;
054
055import java.lang.reflect.Field;
056import java.util.*;
057import java.util.concurrent.Future;
058
059/**
060 * Reflective adapter that handles classes with ConfigurableClass annotations.<br/>
061 * <br/>
062 * <p/>
063 * User has to use the special constructor and initialize providedConfObj when the
064 * already created conf object should be used instead of instantiating one
065 */
066@SuppressWarnings("unchecked")
067public class ReflectiveAdapter<T> implements ConfigTypeAdapter<T, Map<String, Object>> {
068
069
070    private static Logger log = LoggerFactory.getLogger(ReflectiveAdapter.class);
071
072    private T providedConfObj;
073
074    /**
075     * stateless
076     */
077    public ReflectiveAdapter() {
078    }
079
080    /**
081     * stateful
082     */
083    public ReflectiveAdapter(T providedConfigurationObjectInstance) {
084        this.providedConfObj = providedConfigurationObjectInstance;
085    }
086
087    @Override
088    public T fromConfigNode(Map<String, Object> configNode, ConfigProperty property, LoadingContext ctx, Object parent) throws ConfigurationException {
089
090        if (configNode == null) return null;
091
092        Class<T> clazz = (Class<T>) property.getType();
093
094        if (!Map.class.isAssignableFrom(configNode.getClass()))
095            throw new ConfigurationException("Provided configuration node is not a map (type " + clazz.getName() + ")");
096
097
098        // figure out UUID
099        String uuid;
100        try {
101            uuid = (String) configNode.get(Configuration.UUID_KEY);
102        } catch (RuntimeException e) {
103            throw new ConfigurationException("UUID is malformed: " + configNode.get(Configuration.UUID_KEY));
104        }
105
106
107        // if the object is provided - just populate and return
108        if (providedConfObj != null) {
109            populate(configNode, ctx, clazz, providedConfObj, parent, uuid);
110            return providedConfObj;
111        }
112
113
114        // if uuid not present - simply create new instance
115        if (uuid == null) {
116            T confObj = ctx.getVitalizer().newInstance(clazz);
117            populate(configNode, ctx, clazz, confObj, parent, uuid);
118            return confObj;
119        }
120
121        //// uuid present - need to coordinate with the context
122
123        // first check the context
124        Referable existingReferable = ctx.getReferable(uuid);
125        if (existingReferable != null) {
126            // TODO: proper cast!
127            return (T) existingReferable.getConfObject();
128        }
129
130        SettableFuture<Object> confObjFuture = SettableFuture.create();
131        T confObj = ctx.getVitalizer().newInstance(clazz);
132        Referable createdReferable = new Referable(confObjFuture, confObj);
133
134        // cover non-atomicity above
135        Referable suddenlyExistingReferable = ctx.registerReferableIfAbsent(uuid, createdReferable);
136        if (suddenlyExistingReferable != null) {
137            // TODO: proper cast!
138            return (T) suddenlyExistingReferable.getConfObject();
139        }
140
141        // now it's for sure me who is responsible for loading this object
142        try {
143            populate(configNode, ctx, clazz, confObj, parent, uuid);
144            confObjFuture.set(confObj);
145            return confObj;
146        } catch (RuntimeException e) {
147            confObjFuture.setException(e);
148            throw e;
149        } catch (Error e) {
150            confObjFuture.setException(e);
151            throw e;
152        }
153    }
154
155    private void populate(Map<String, Object> configNode, LoadingContext ctx, Class<T> clazz, T confObj, Object parent, String uuid) {
156
157        // this differentiation is needed for historical reasons .. two examples is Device and the addApplicationEntity method, another example is HL7DeviceExtension....
158
159        if (ConfigReflection.getUUIDPropertyForClass(clazz) != null) {
160            // if class has uuid => it's 'standalone' conf class => initialize fields before parent
161
162            populateFields(configNode, ctx, clazz, confObj);
163            injectParent(ctx, clazz, confObj, parent, uuid);
164        } else {
165            // if class has no uuid => it's either an extension or a simple conf class => initialize parent before fields
166
167            injectParent(ctx, clazz, confObj, parent, uuid);
168            populateFields(configNode, ctx, clazz, confObj);
169        }
170
171    }
172
173    private void populateFields(Map<String, Object> configNode, LoadingContext ctx, Class<T> clazz, T confObj) {
174        for (ConfigProperty fieldProperty : ConfigReflection.getAllConfigurableFields(clazz))
175            try {
176                Object fieldValue = DefaultConfigTypeAdapters.delegateGetChildFromConfigNode(configNode, fieldProperty, ctx, confObj);
177                ConfigReflection.setProperty(confObj, fieldProperty, fieldValue);
178            } catch (RuntimeException e) {
179                throw new ConfigurationException("Error while reading configuration property '" + fieldProperty.getAnnotatedName() + "' (field " + fieldProperty.getName() + ") in class " + clazz.getSimpleName(), e);
180            }
181    }
182
183    private void injectParent(LoadingContext ctx, Class<T> clazz, T confObj, Object parent, String uuid) {
184
185        Field parentField = ConfigReflection.getParentPropertyForClass(clazz);
186        if (parentField == null) return;
187
188        // if parent was provided - use it
189        if (parent != null) {
190            try {
191                ConfigReflection.setProperty(confObj, parentField.getName(), parent);
192                return;
193            } catch (RuntimeException e) {
194                throw new ConfigurationException("Could not 'inject' parent object into the @Parent field (class " + clazz.getName() + ")", e);
195            }
196        }
197
198        // if no provided parent and no uuid - we cannot really find the parent, so just leave it null
199        if (uuid == null) return;
200
201
202        // TODO: replace with proxy
203
204        // if no config context - leave the parent unset
205        TypeSafeConfiguration typeSafeConfig = ctx.getTypeSafeConfiguration();
206        if (typeSafeConfig == null) return;
207
208        // Get path of this object in the storage
209        Path pathByUUID = typeSafeConfig.getLowLevelAccess().getPathByUUID(uuid);
210        if (pathByUUID == null) return;
211        Deque<ConfigProperty> configProperties = PathFollower.traceProperties(typeSafeConfig.getRootClass(), pathByUUID);
212
213        // parent is either the first or the second in the path (otherwise cannot really get the parent)
214
215        if (configProperties.size() < 1) return;
216        configProperties.removeLast();
217        int nodesAbove = 1;
218
219        // this can be still a map/collection, try one level above
220        if (!configProperties.peekLast().isConfObject()) {
221            configProperties.removeLast();
222            nodesAbove++;
223            if (configProperties.size() == 0) return;
224            if (!configProperties.peekLast().isConfObject()) return;
225        }
226
227        // now we are looking at the parent
228        ConfigProperty parentProp = configProperties.peekLast();
229
230        if (!parentField.getType().isAssignableFrom(parentProp.getRawClass())) {
231            log.warn("Parent type mismatch: config structure denotes " + parentProp.getRawClass() + ", but the class has a field of type " + parentField.getType()
232                    + " config object uuid=" + uuid + ", config class " + clazz);
233            return;
234        }
235
236        Path parentPath = pathByUUID.subPath(0, pathByUUID.getPathItems().size() - nodesAbove);
237
238        // load parent
239        Object loadedParent = typeSafeConfig.load(parentPath, parentProp.getRawClass(), ctx);
240        ConfigReflection.setProperty(confObj, parentField.getName(), loadedParent);
241    }
242
243
244    @Override
245    public Map<String, Object> toConfigNode(T object, ConfigProperty property, SavingContext ctx) throws ConfigurationException {
246
247        if (object == null) return null;
248
249        Class<T> clazz = (Class<T>) object.getClass();
250
251        Map<String, Object> configNode = new TreeMap<String, Object>();
252
253        // get data from all the configurable fields
254        for (ConfigProperty fieldProperty : ConfigReflection.getAllConfigurableFields(clazz)) {
255            try {
256                Object value = ConfigReflection.getProperty(object, fieldProperty);
257                DefaultConfigTypeAdapters.delegateChildToConfigNode(value, configNode, fieldProperty, ctx);
258            } catch (Exception e) {
259                throw new ConfigurationException("Error while serializing configuration field '" + fieldProperty.getName() + "' in class " + clazz.getSimpleName(), e);
260            }
261        }
262
263        return configNode;
264    }
265
266
267    @Override
268    public Map<String, Object> getSchema(ConfigProperty property, ProcessingContext ctx) throws ConfigurationException {
269
270        Class<T> clazz = (Class<T>) property.getType();
271
272        Map<String, Object> classMetaDataWrapper = new HashMap<String, Object>();
273        Map<String, Object> classMetaData = new HashMap<String, Object>();
274        classMetaDataWrapper.put("properties", classMetaData);
275        classMetaDataWrapper.put("type", "object");
276        classMetaDataWrapper.put("class", clazz.getSimpleName());
277
278        // find out if we need to include uiOrder metadata
279        boolean includeOrder = false;
280
281
282        for (ConfigProperty configurableChildProperty : ConfigReflection.getAllConfigurableFields(clazz))
283            if (configurableChildProperty.getAnnotation(ConfigurableProperty.class).order() != 0) includeOrder = true;
284        
285        // populate properties
286
287        for (ConfigProperty prop : ConfigReflection.getAllConfigurableFields(clazz)) {
288
289            ConfigTypeAdapter childAdapter = ctx.getVitalizer().lookupTypeAdapter(prop);
290            Map<String, Object> childPropertyMetadata = new LinkedHashMap<String, Object>();
291            classMetaData.put(prop.getAnnotatedName(), childPropertyMetadata);
292
293            if (!"".equals( prop.getLabel() ) )
294                childPropertyMetadata.put("title", prop.getLabel());
295
296            if (!"".equals( prop.getDescription() ) )
297                childPropertyMetadata.put("description", prop.getDescription());
298            try {
299                if (!prop.getDefaultValue().equals(ConfigurableProperty.NO_DEFAULT_VALUE))
300                    childPropertyMetadata.put("default", childAdapter.normalize(prop.getDefaultValue(), prop, ctx));
301            } catch (ClassCastException e) {
302                childPropertyMetadata.put("default", 0);
303            }
304            if (!prop.getTags().isEmpty())
305                childPropertyMetadata.put("tags", prop.getTags());
306
307            if (includeOrder)
308                childPropertyMetadata.put("uiOrder", prop.getOrder());
309
310            childPropertyMetadata.put("uiGroup", prop.getGroup());
311
312            // also merge in the metadata from this child itself
313            Map<String, Object> childMetaData = childAdapter.getSchema(prop, ctx);
314            if (childMetaData != null) childPropertyMetadata.putAll(childMetaData);
315        }
316
317        return classMetaDataWrapper;
318    }
319
320    @Override
321    public Map<String, Object> normalize(Object configNode, ConfigProperty property, ProcessingContext ctx) throws ConfigurationException {
322        return (Map<String, Object>) configNode;
323    }
324}