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.util;
041
042import org.dcm4che3.conf.core.Nodes;
043import org.dcm4che3.conf.core.api.Configuration;
044import org.dcm4che3.conf.core.api.ConfigurationException;
045import org.dcm4che3.conf.core.api.internal.ConfigProperty;
046import org.dcm4che3.conf.core.api.internal.ConfigReflection;
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049
050import java.util.*;
051import java.util.Map.Entry;
052
053@SuppressWarnings("unchecked")
054public class ConfigNodeTraverser {
055
056    private static Logger log = LoggerFactory.getLogger(ConfigNodeTraverser.class);
057
058    public interface ConfigNodeTypesafeFilter {
059        /**
060         * @return if returns true, traversal will skip going inside this node
061         */
062        boolean beforeNode(Map<String, Object> containerNode, Class containerNodeClass, ConfigProperty property) throws ConfigurationException;
063    }
064
065    public interface ConfigNodesTypesafeFilter {
066        /**
067         * --TODO Is skipping node indicated by returning true or throwing exception?
068         * @return if returns true, traversal will skip going inside this node
069         */
070        void beforeNodes(Map<String, Object> containerNode1, Map<String, Object> containerNode2, Class containerNodeClass, ConfigProperty property) throws ConfigurationException;
071    }
072
073    public static class AConfigNodeFilter {
074        public void beforeNodeElement(Map<String, Object> containerNode, String key, Object value) {
075        }
076
077        public void afterNodeElement(Map<String, Object> containerNode, String key, Object value) {
078        }
079
080        public void beforeNode(Map<String, Object> node) {
081        }
082
083        public void afterNode(Map<String, Object> node) {
084        }
085
086        public void beforeList(Collection list) {
087        }
088
089        public void beforeListElement(Collection list, int index, Object element) {
090        }
091
092        public void afterListElement(Collection list, int index, Object element) {
093        }
094
095        public void afterList(Collection list) {
096        }
097
098        /**
099         * Fired for Boolean,String,Number,null
100         */
101        public void onPrimitiveNodeElement(Map<String, Object> containerNode, String key, Object value) {
102        }
103
104        /**
105         * Fired for Boolean,String,Number,null
106         */
107        public void onPrimitiveListElement(Collection list, Object element) {
108        }
109    }
110
111    public static class ADualNodeFilter {
112        public void beforeNode(Map<String, Object> node1, Map<String, Object> node2) {
113
114        }
115
116        public void afterNode(Map<String, Object> node1, Map<String, Object> node2) {
117
118        }
119
120        public void afterNodeProperty(String key) {
121
122        }
123
124        public void beforeNodeProperty(String key) {
125
126        }
127
128        public void beforeListElement(int index1, int index2) {
129
130        }
131
132        public void afterListElement(int index1, int index2) {
133
134        }
135
136        /**
137         * @param node1
138         * @param node2
139         * @return false if the traverser should skip this list, true if it should proceed normally
140         */
141        public boolean beforeList(List node1, List node2) {
142            return true;
143        }
144
145        public void afterList(List node1, List node2) {
146
147        }
148    }
149
150    public static void traverseNodeTypesafe(Object node, ConfigProperty containerProperty, List<Class> allExtensionClasses, ConfigNodeTypesafeFilter filter) throws ConfigurationException {
151
152        // if because of any reason this is not a map (e.g. a reference or a custom adapter for a configurableclass),
153        // we don't go deeper
154        if (!(node instanceof Map)) return;
155
156        // if that's a reference, don't traverse deeper
157        if (containerProperty.isReference()) return;
158
159        Map<String, Object> containerNode = (Map<String, Object>) node;
160
161
162        List<ConfigProperty> properties = ConfigReflection.getAllConfigurableFields(containerProperty.getRawClass());
163        for (ConfigProperty property : properties) {
164            Object childNode = containerNode.get(property.getAnnotatedName());
165
166            if (filter.beforeNode(containerNode, containerProperty.getRawClass(), property)) continue;
167
168            if (childNode == null) continue;
169
170            // if the property is a configclass
171            if (property.isConfObject()) {
172                traverseNodeTypesafe(childNode, property, allExtensionClasses, filter);
173                continue;
174            }
175
176            // collection, where a generics parameter is a configurable class or it is an array with comp type of configurableClass
177            if (property.isCollectionOfConfObjects() || property.isArrayOfConfObjects()) {
178
179                Collection collection = (Collection) childNode;
180
181                for (Object object : collection) {
182                    traverseNodeTypesafe(object, property.getPseudoPropertyForConfigClassCollectionElement(), allExtensionClasses, filter);
183                }
184
185                continue;
186            }
187
188            // map, where a value generics parameter is a configurable class
189            if (property.isMapOfConfObjects()) {
190
191                try {
192                    Map<String, Object> collection = (Map<String, Object>) childNode;
193
194                    for (Object object : collection.values())
195                        traverseNodeTypesafe(object, property.getPseudoPropertyForConfigClassCollectionElement(), allExtensionClasses, filter);
196
197                } catch (ClassCastException e) {
198                    log.warn("Map is malformed", e);
199                }
200
201                continue;
202            }
203
204            // extensions map
205            if (property.isExtensionsProperty()) {
206
207                try {
208                    Map<String, Object> extensionsMap = (Map<String, Object>) childNode;
209
210                    for (Entry<String, Object> entry : extensionsMap.entrySet()) {
211                        try {
212                            traverseNodeTypesafe(
213                                    entry.getValue(),
214                                    ConfigReflection.getDummyPropertyForClass(Extensions.getExtensionClassBySimpleName(entry.getKey(), allExtensionClasses)),
215                                    allExtensionClasses,
216                                    filter
217                            );
218
219                        } catch (ClassNotFoundException e) {
220                            // noop
221                            log.debug("Extension class {} not found, parent node class {} ", entry.getKey(), containerProperty.getRawClass().getName());
222                        }
223                    }
224
225                } catch (ClassCastException e) {
226                    log.warn("Extensions are malformed", e);
227                }
228
229            }
230
231
232        }
233    }
234
235    public static void dualTraverseNodeTypesafe(Object node1, Object node2, ConfigProperty containerProperty, List<Class> allExtensionClasses, ConfigNodesTypesafeFilter filter) throws ConfigurationException {
236
237        // if because of any reason these are not maps (e.g. a reference or a custom adapter for a configurableclass),
238        // we don't go deeper
239        if (!(node1 instanceof Map) || !(node2 instanceof Map)) return;
240
241        // if that's a reference, don't traverse deeper
242        if (containerProperty.isReference()) return;
243
244        Map<String, Object> containerNode1 = (Map<String, Object>) node1;
245        Map<String, Object> containerNode2 = (Map<String, Object>) node2;
246
247
248        List<ConfigProperty> properties = ConfigReflection.getAllConfigurableFields(containerProperty.getRawClass());
249        for (ConfigProperty property : properties) {
250
251            filter.beforeNodes(containerNode1, containerNode2, containerProperty.getRawClass(), property);
252
253            Object childNode1 = containerNode1.get(property.getAnnotatedName());
254            Object childNode2 = containerNode2.get(property.getAnnotatedName());
255
256            if (childNode1 == null || childNode2 == null) continue;
257
258            // if the property is a configclass
259            if (property.isConfObject()) {
260                dualTraverseNodeTypesafe(childNode1, childNode2, property, allExtensionClasses, filter);
261                continue;
262            }
263
264            // collection, where a generics parameter is a configurable class or it is an array with comp type of configurableClass
265            if (property.isCollectionOfConfObjects() || property.isArrayOfConfObjects()) {
266
267                List<Map<String, Object>> nodeList1 = (List) childNode1;
268                List<Map<String, Object>> nodeList2 = (List) childNode2;
269
270                boolean allUuids = allElementsAreNodesAndHaveUuids(nodeList1)
271                        && allElementsAreNodesAndHaveUuids(nodeList2);
272
273                if (allUuids) {
274                    // match on #uuid
275                    // extract Map uuid -> index
276                    Map<String, Integer> index1 = new HashMap<String, Integer>();
277
278                    int i = 0;
279                    for (Map<String, Object> node : nodeList1) {
280                        index1.put((String) node.get(Configuration.UUID_KEY), i);
281                        i++;
282                    }
283
284                    for (Map<String, Object> node : nodeList2) {
285
286                        Object uuid = node.get(Configuration.UUID_KEY);
287                        Integer elem1Ind = index1.get(uuid);
288
289                        if (elem1Ind != null) {
290                            // found match
291                            dualTraverseNodeTypesafe(
292                                    nodeList1.get(elem1Ind),
293                                    node,
294                                    property.getPseudoPropertyForConfigClassCollectionElement(),
295                                    allExtensionClasses,
296                                    filter
297                            );
298                        }
299                    }
300                }
301                continue;
302            }
303
304            // map, where a value generics parameter is a configurable class
305            if (property.isMapOfConfObjects()) {
306
307
308                Map<String, Map<String, Object>> mapNode1 = (Map) childNode1;
309                Map<String, Map<String, Object>> mapNode2 = (Map) childNode2;
310
311                if (allEntriesAreNodesAndHaveUuids(mapNode1) &&
312                        allEntriesAreNodesAndHaveUuids(mapNode2)) {
313                    // conf objects with uuids
314
315                    // match on #uuid
316                    // extract Map uuid -> index
317                    Map<String, String> index1 = new HashMap<String, String>();
318                    for (Entry<String, Map<String, Object>> stringMapEntry : mapNode1.entrySet()) {
319                        index1.put(
320                                (String) stringMapEntry.getValue().get(Configuration.UUID_KEY),
321                                stringMapEntry.getKey()
322                        );
323                    }
324
325                    for (Entry<String, Map<String, Object>> stringMapEntry : mapNode2.entrySet()) {
326                        Object uuidInSecondMapValue = stringMapEntry.getValue().get(Configuration.UUID_KEY);
327                        String mapKeyInFirstMap = index1.get(uuidInSecondMapValue);
328
329                        if (mapKeyInFirstMap != null) {
330                            dualTraverseNodeTypesafe(
331                                    mapNode1.get(mapKeyInFirstMap),
332                                    stringMapEntry.getValue(),
333                                    property.getPseudoPropertyForConfigClassCollectionElement(),
334                                    allExtensionClasses,
335                                    filter);
336
337                        }
338
339                    }
340
341
342                } else {
343                    // conf objects without uuids
344
345                    try {
346                        Map<String, Object> collection1 = (Map<String, Object>) childNode1;
347                        Map<String, Object> collection2 = (Map<String, Object>) childNode2;
348
349                        for (String key : collection1.keySet()) {
350                            dualTraverseNodeTypesafe(
351                                    collection1.get(key),
352                                    collection2.get(key),
353                                    property.getPseudoPropertyForConfigClassCollectionElement(),
354                                    allExtensionClasses,
355                                    filter
356                            );
357                        }
358
359                    } catch (ClassCastException e) {
360                        log.warn("Map is malformed", e);
361                    }
362                }
363
364                continue;
365            }
366
367            // extensions map
368            if (property.isExtensionsProperty()) {
369
370                try {
371                    Map<String, Object> extensionsMap1 = (Map<String, Object>) childNode1;
372                    Map<String, Object> extensionsMap2 = (Map<String, Object>) childNode2;
373
374                    for (String key : extensionsMap1.keySet()) {
375                        try {
376
377                            dualTraverseNodeTypesafe(
378                                    extensionsMap1.get(key),
379                                    extensionsMap2.get(key),
380                                    ConfigReflection.getDummyPropertyForClass(Extensions.getExtensionClassBySimpleName(key, allExtensionClasses)),
381                                    allExtensionClasses,
382                                    filter
383                            );
384
385                        } catch (ClassNotFoundException e) {
386                            // noop
387                            log.debug("Extension class {} not found, parent node class {} ", key, containerProperty.getRawClass().getName());
388                        }
389                    }
390
391                } catch (ClassCastException e) {
392                    log.warn("Extensions are malformed", e);
393                }
394            }
395        }
396    }
397
398    public static void traverseMapNode(Object node, AConfigNodeFilter filter) {
399
400        if (node instanceof Map) {
401            Map<String, Object> map = (Map<String, Object>) node;
402
403            filter.beforeNode(map);
404            for (Entry<String, Object> stringObjectEntry : map.entrySet()) {
405
406                String key = stringObjectEntry.getKey();
407                Object value = stringObjectEntry.getValue();
408
409                filter.beforeNodeElement(map, key, value);
410
411                if (Nodes.isPrimitive(value)) {
412                    filter.onPrimitiveNodeElement(map, key, value);
413                } else if (value instanceof Map) {
414                    traverseMapNode(value, filter);
415                } else if (value instanceof List) {
416
417                    List list = (List) value;
418                    filter.beforeList(list);
419                    for (int i = 0; i < list.size(); i++) {
420
421                        Object o = list.get(i);
422
423                        filter.beforeListElement(list, i, o);
424
425                        if (Nodes.isPrimitive(o)) {
426                            filter.onPrimitiveListElement(list, o);
427                        } else if (o instanceof Map) {
428                            traverseMapNode(o, filter);
429                        } else {
430                            throw new IllegalArgumentException("List is only allowed to contain primitive elements and map nodes. " +
431                                    "Encountered " + o.getClass() + " in list " + key);
432                        }
433
434                        filter.afterListElement(list, i, o);
435                    }
436                    filter.afterList(list);
437                } else {
438                    throw new IllegalArgumentException("Illegal node type " + value.getClass() + ", node " + value);
439                }
440
441                filter.afterNodeElement(map, key, value);
442            }
443
444            filter.afterNode(map);
445
446        } else
447            throw new IllegalArgumentException("A composite config node must be a Map<String,Object>");
448    }
449
450    /**
451     * Traverses with in-depth search and applies dual filter.
452     *
453     * @param node1
454     * @param node2
455     */
456    public static void dualTraverseMapNodes(Map<String, Object> node1, Map<String, Object> node2, ADualNodeFilter filter) {
457
458        filter.beforeNode(node1, node2);
459
460        if (node1 != null && node2 != null) {
461
462
463
464            if (allEntriesAreNodesAndHaveUuids(node1) &&
465                    allEntriesAreNodesAndHaveUuids(node2)) {
466                // conf objects with uuids
467
468                Map<String, Map<String, Object>> mapNode1 = (Map) node1;
469                Map<String, Map<String, Object>> mapNode2 = (Map) node2;
470
471                // match on #uuid
472                // extract Map uuid -> index
473                Map<String, String> index1 = new HashMap<String, String>();
474                for (Entry<String, Map<String, Object>> stringMapEntry : mapNode1.entrySet()) {
475                    index1.put(
476                            (String) stringMapEntry.getValue().get(Configuration.UUID_KEY),
477                            stringMapEntry.getKey()
478                    );
479                }
480
481                for (Entry<String, Map<String, Object>> stringMapEntry : mapNode2.entrySet()) {
482                    Object uuidInSecondMapValue = stringMapEntry.getValue().get(Configuration.UUID_KEY);
483                    String mapKeyInFirstMap = index1.get(uuidInSecondMapValue);
484
485                    if (mapKeyInFirstMap != null) {
486                        dualTraverseProperty(
487                                mapNode1.get(mapKeyInFirstMap),
488                                stringMapEntry.getValue(),
489                                filter);
490                    }
491
492                }
493
494
495            } else {
496                // conf objects without uuids
497
498                for (Entry<String, Object> objectEntry : node1.entrySet())
499                    if (node2.containsKey(objectEntry.getKey())) {
500
501                        filter.beforeNodeProperty(objectEntry.getKey());
502
503                        Object node1El = objectEntry.getValue();
504                        Object node2El = node2.get(objectEntry.getKey());
505                        dualTraverseProperty(node1El, node2El, filter);
506
507                        filter.afterNodeProperty(objectEntry.getKey());
508                    }
509            }
510        }
511
512        filter.afterNode(node1, node2);
513    }
514
515    private static void dualTraverseProperty(Object node1El, Object node2El, ADualNodeFilter filter) {
516        if (node2El == null && node1El == null) return;
517
518        if (node1El instanceof List) {
519
520            if (!(node2El instanceof List)) return;
521
522            List list1 = (List) node1El;
523            List list2 = (List) node2El;
524
525            // if any of elements don't have #uuid defined, don't go deeper into the collection
526            // because there is no guarantee that elements will match
527
528            boolean allUuids = allElementsAreNodesAndHaveUuids(list1)
529                    && allElementsAreNodesAndHaveUuids(list2)
530                    && filter.beforeList(list1, list2);
531
532            if (allUuids) {
533
534                // we are sure by now
535                List<Map<String, Object>> nodeList1 = list1;
536                List<Map<String, Object>> nodeList2 = list2;
537
538                // match on #uuid
539
540                // extract Map uuid -> index
541                Map<String, Integer> index1 = new HashMap<String, Integer>();
542
543                int i = 0;
544                for (Map<String, Object> node : nodeList1) {
545                    index1.put((String) node.get(Configuration.UUID_KEY), i);
546                    i++;
547                }
548
549                i = 0;
550                for (Map<String, Object> node : nodeList2) {
551
552                    Object uuid = node.get(Configuration.UUID_KEY);
553                    Integer elem1Ind = index1.get(uuid);
554
555                    if (elem1Ind != null) {
556                        // found match
557                        filter.beforeListElement(elem1Ind, i);
558                        dualTraverseMapNodes(nodeList1.get(elem1Ind), node, filter);
559                        filter.afterListElement(elem1Ind, i);
560                    }
561
562                    i++;
563                }
564
565            }
566
567            filter.afterList(list1, list2);
568        }
569
570        if (node1El instanceof Map) {
571
572            if (!(node2El instanceof Map)) return;
573
574            dualTraverseMapNodes((Map) node1El, (Map) node2El, filter);
575        }
576
577    }
578
579    private static boolean allElementsAreNodesAndHaveUuids(List list) {
580        for (Object o : list) {
581            if (!(o instanceof Map)) return false;
582            if (!(((Map) o).get(Configuration.UUID_KEY) instanceof String)) return false;
583        }
584        return true;
585    }
586
587    private static boolean allEntriesAreNodesAndHaveUuids(Map map) {
588        for (Object o : map.values()) {
589            if (!(o instanceof Map)) return false;
590            if (!(((Map) o).get(Configuration.UUID_KEY) instanceof String)) return false;
591        }
592        return true;
593    }
594
595}