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;
041
042import org.apache.commons.jxpath.AbstractFactory;
043import org.apache.commons.jxpath.JXPathContext;
044import org.apache.commons.jxpath.JXPathNotFoundException;
045import org.apache.commons.jxpath.Pointer;
046import org.dcm4che3.conf.core.api.Configuration;
047import org.dcm4che3.conf.core.util.XNodeUtil;
048
049import java.util.*;
050import java.util.Map.Entry;
051import java.util.regex.Matcher;
052import java.util.regex.Pattern;
053
054public class Nodes {
055
056
057    private static Pattern itemPattern = Pattern.compile("/(?<item>(\\\\/|[^/\\[\\]@\\*])*)");
058    private static Pattern simplePathPattern = Pattern.compile("(" + itemPattern + ")*");
059
060    public static String concat(String path1, String path2) {
061        String res = path1 + "/" + path2;
062        return res.replace("///", "/").replace("//", "/");
063    }
064
065    public static Object getNode(Object rootConfigNode, String path) {
066        try {
067            return JXPathContext.newContext(rootConfigNode).getValue(path);
068        } catch (JXPathNotFoundException e) {
069            return null;
070        }
071    }
072
073    public static boolean nodeExists(Map<String, Object> rootConfigNode, String path) {
074        return getNode(rootConfigNode, path) != null;
075    }
076
077    public static void removeNodes(Map<String, Object> configurationRoot, String path) {
078        JXPathContext.newContext(configurationRoot).removeAll(path);
079    }
080
081    public static Iterator search(Map<String, Object> configurationRoot, String liteXPathExpression) throws IllegalArgumentException {
082        return JXPathContext.newContext(configurationRoot).iterate(liteXPathExpression);
083
084    }
085
086    /**
087     * Clones structure but re-uses primitives
088     *
089     * @param node
090     * @return
091     */
092    @SuppressWarnings("unchecked")
093    public static Object deepCloneNode(Object node) {
094
095
096        if (isPrimitive(node)) return node;
097
098        if (node instanceof Collection) {
099            Collection givenCollection = (Collection) node;
100            ArrayList<Object> newCollection = new ArrayList<Object>(givenCollection.size());
101            for (Object o : givenCollection) newCollection.add(deepCloneNode(o));
102            return newCollection;
103        }
104
105        if (node instanceof Map) {
106            Map givenMapNode = (Map) node;
107            Map newMapNode = new HashMap(givenMapNode.size());
108            for (Entry e : (Set<Entry>) givenMapNode.entrySet()) {
109                newMapNode.put(e.getKey(), deepCloneNode(e.getValue()));
110            }
111
112            return newMapNode;
113        }
114
115        throw new IllegalArgumentException("Unexpected node type " + node.getClass());
116    }
117
118    public static boolean isPrimitive(Object value) {
119        return value == null ||
120                value instanceof Number ||
121                value instanceof String ||
122                value instanceof Boolean;
123    }
124
125
126    /**
127     * traverses nodenames and also @name predicates, so something like this
128     * /dicomConfigurationRoot/dicomDevicesRoot[@name='deviceName']/deviceExtensions
129     * will return
130     * dicomConfigurationRoot,dicomDevicesRoot,deviceName,deviceExtensions
131     *
132     * @param path
133     * @return
134     */
135    public static List<Object> getPathItems(String path) {
136        List<Map<String, Object>> refItems = XNodeUtil.parseReference(path);
137        List<Object> names = new ArrayList<Object>();
138
139        for (Map<String, Object> refItem : refItems) {
140            names.add((String) refItem.get("$name"));
141            if (refItem.containsKey("@name"))
142                names.add((String) refItem.get("@name"));
143        }
144
145        return names;
146    }
147
148    /**
149     * @param path path str to parse
150     * @return list of path items in case the provided path
151     * <ul>
152     * <li>is simple (e.g. "/dicomConfigurationRoot/globalConfiguration/dcmTransferCapabilities" )</li>
153     * <li>is persistable (e.g. "/dicomConfigurationRoot/dicomDevicesRoot[@name='someName']")</li>
154     * </ul>
155     * <p/>
156     * otherwise <b>null</b>
157     */
158    public static List<Object> simpleOrPersistablePathToPathItemsOrNull(String path) {
159
160        List<Map<String, Object>> refItems;
161        try {
162            refItems = XNodeUtil.parseReference(path);
163        } catch (IllegalArgumentException e) {
164            return null;
165        }
166
167        List<Object> pathItems = new ArrayList<Object>();
168
169        for (Map<String, Object> refItem : refItems) {
170
171            String name = null;
172            String attrName = null;
173
174            for (Entry<String, Object> stringObjectEntry : refItem.entrySet()) {
175
176                if (stringObjectEntry.getKey().equals("$name"))
177                    name = ((String) stringObjectEntry.getValue());
178                else if (stringObjectEntry.getKey().equals("@name"))
179                    attrName = ((String) stringObjectEntry.getValue());
180                else {
181                    // this path is neither simple nor persistable
182                    return null;
183                }
184
185                if (((String) stringObjectEntry.getValue()).contains("*"))
186                    return null;
187            }
188
189            // this path is neither simple nor persistable
190            if (name == null)
191                return null;
192
193            pathItems.add(name);
194
195            if (attrName != null)
196                pathItems.add(attrName);
197
198        }
199
200        return pathItems;
201    }
202
203    public static String toSimpleEscapedPath(Iterator<Object> items) {
204        ArrayList<Object> strings = new ArrayList<Object>();
205        while (items.hasNext())
206            strings.add(items.next());
207        return toSimpleEscapedPath(strings);
208    }
209
210    public static String toSimpleEscapedPath(Iterable<Object> items) {
211        String s = "";
212        for (Object item : items) s += "/" + item.toString().replace("/", "\\/");
213        return s;
214    }
215
216    public static List<String> fromSimpleEscapedPath(String path) {
217        List<String> strings = fromSimpleEscapedPathOrNull(path);
218        if (strings == null)
219            throw new IllegalArgumentException("Simple path " + path + " is invalid");
220        return strings;
221    }
222
223    public static List<String> fromSimpleEscapedPathOrNull(String path) {
224
225        if (!simplePathPattern.matcher(path).matches())
226            return null;
227
228        Matcher matcher = itemPattern.matcher(path);
229
230        List<String> list = new ArrayList<String>();
231        while (matcher.find()) {
232            list.add(matcher.group("item").replace("\\/", "/"));
233        }
234
235        return list;
236    }
237
238    public static void replacePrimitive(Map<String, Object> map, Object replacement, List<Object> pathItems) {
239
240        if (pathItems.isEmpty())
241            throw new IllegalArgumentException("Cannot replace root with a primitive");
242
243        replaceObject(map, replacement, pathItems);
244
245    }
246
247    public static Map<String, Object> replaceNode(Map<String, Object> map, Map<String, Object> replacement, List<Object> pathItems) {
248        return (Map<String, Object>) replaceObject(map, replacement, pathItems);
249    }
250
251    private static Object replaceObject(Map<String, Object> map, Object replacement, List<Object> pathItems) {
252        Object node = map;
253        Object subNode = node;
254        Object name = null;
255
256        if (pathItems.isEmpty())
257            return replacement;
258
259        for (Object pathItem : pathItems) {
260            name = pathItem;
261
262            node = subNode;
263            subNode = getElement(node, pathItem);
264
265            if (subNode == null) {
266
267                if (!(name instanceof String))
268                    throw new IllegalStateException("Cannot create lists items with replace, path " + pathItems + " , item '" + name + "' , node " + node);
269
270                subNode = Configuration.NodeFactory.emptyNode();
271                ((Map)node).put(name, subNode);
272            }
273        }
274
275        ((Map)node).put(name, replacement);
276        return map;
277    }
278
279
280    public static void removeNode(Map<String, Object> map, List<Object> pathItems) {
281        Object node = map;
282        Object subNode = node;
283        Object name = null;
284
285        for (Object pathItem : pathItems) {
286
287            name = pathItem;
288
289            node = subNode;
290            subNode = getElement(node, pathItem);
291
292            if (subNode == null) return;
293        }
294
295        // remove leaf
296        if (node instanceof List) {
297            ((List) node).remove(name);
298        } else if (node instanceof Map) {
299            ((Map) node).remove(name);
300        }
301    }
302
303    private static Object getElement(Object node, Object pathItem) {
304        Object subNode;
305        if (node instanceof List && pathItem instanceof Integer) {
306            subNode = ((List) node).get((Integer) pathItem);
307        } else if (node instanceof Map && pathItem instanceof String) {
308            subNode = ((Map) node).get(pathItem);
309        } else
310            throw new IllegalArgumentException("Unexpected node/path: node " + node + " , path item " + pathItem);
311        return subNode;
312    }
313
314    public static boolean nodeExists(Object node, List<Object> pathItems) {
315        return getNode(node, pathItems) != null;
316    }
317
318    public static Object getNode(Object node, List<Object> pathItems) {
319        for (Object pathItem : pathItems) {
320            if (node == null) {
321                return null;
322            } else {
323                node = getElement(node, pathItem);
324            }
325        }
326
327        return node;
328    }
329
330
331}