001/* 002 * #%L 003 * Netarchivesuite - common 004 * %% 005 * Copyright (C) 2005 - 2014 The Royal Danish Library, the Danish State and University Library, 006 * the National Library of France and the Austrian National Library. 007 * %% 008 * This program is free software: you can redistribute it and/or modify 009 * it under the terms of the GNU Lesser General Public License as 010 * published by the Free Software Foundation, either version 2.1 of the 011 * License, or (at your option) any later version. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Lesser Public License for more details. 017 * 018 * You should have received a copy of the GNU General Lesser Public 019 * License along with this program. If not, see 020 * <http://www.gnu.org/licenses/lgpl-2.1.html>. 021 * #L% 022 */ 023package dk.netarkivet.common.utils; 024 025import java.io.File; 026import java.io.InputStream; 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.Collections; 030import java.util.HashMap; 031import java.util.List; 032import java.util.Map; 033 034import org.dom4j.Document; 035import org.dom4j.Element; 036import org.dom4j.Namespace; 037import org.dom4j.Node; 038import org.dom4j.XPath; 039import org.dom4j.dom.DOMDocument; 040 041import dk.netarkivet.common.exceptions.ArgumentNotValid; 042import dk.netarkivet.common.exceptions.IOFailure; 043import dk.netarkivet.common.exceptions.UnknownID; 044 045/** 046 * Utility class to load and save data from/to XML files using a very simple XML format. 047 */ 048@SuppressWarnings({"unused", "unchecked"}) 049public class SimpleXml { 050 /** The underlying XML Document object that we give access to. */ 051 private Document xmlDoc; 052 053 /** The file that this XML was read from, or a fixed string if it was created from scratch. */ 054 private String source; 055 056 /** 057 * Create a new SimpleXml object by loading a file. 058 * 059 * @param f XML file to load 060 */ 061 public SimpleXml(File f) { 062 ArgumentNotValid.checkNotNull(f, "File f"); 063 load(f); 064 } 065 066 /** 067 * Create a new SimpleXml just containing the root element. 068 * 069 * @param rootElement Name of the root element 070 */ 071 public SimpleXml(String rootElement) { 072 ArgumentNotValid.checkNotNullOrEmpty(rootElement, "String rootElement"); 073 xmlDoc = new DOMDocument(); 074 xmlDoc.addElement(rootElement); 075 source = "Newly creating XML file with root '" + rootElement + "'"; 076 } 077 078 /** 079 * Create a new SimpleXml object by loading a file. 080 * 081 * @param resourceAsStream XML file to load 082 */ 083 public SimpleXml(InputStream resourceAsStream) { 084 ArgumentNotValid.checkNotNull(resourceAsStream, "InputStream resourceAsStream"); 085 load(resourceAsStream); 086 } 087 088 /** 089 * Loads an xml stream. 090 * 091 * @param resourceAsStream a XML stream to load. 092 */ 093 private void load(InputStream resourceAsStream) { 094 xmlDoc = XmlUtils.getXmlDoc(resourceAsStream); 095 source = "XML file from input stream '" + resourceAsStream + "'"; 096 } 097 098 /** 099 * Loads an xml file. 100 * 101 * @param f a XML file 102 */ 103 private void load(File f) { 104 source = f.toString(); 105 106 if (!f.exists()) { 107 throw new IOFailure("XML file '" + f.getAbsolutePath() + "' does not exist"); 108 } 109 110 xmlDoc = XmlUtils.getXmlDoc(f); 111 } 112 113 /** 114 * Add entries to the current set of settings. If a node with this key already exists in the XML, the new nodes are 115 * added after that, otherwise the new nodes are added at the end. 116 * 117 * @param key the key to add 118 * @param values the values to add 119 * @throws ArgumentNotValid if the key is null or empty, or the value is null 120 */ 121 public void add(String key, String... values) { 122 ArgumentNotValid.checkNotNullOrEmpty(key, "key"); 123 ArgumentNotValid.checkNotNull(values, "values"); 124 125 // find values to set 126 List<String> allValues = new ArrayList<String>(getList(key)); 127 allValues.addAll(Arrays.asList(values)); 128 129 // ensure the key exists 130 Element newNode = addParents(key.split("\\.")); 131 132 // set values 133 update(key, allValues.toArray(new String[] {})); 134 } 135 136 /** 137 * Add all the necessary parents to have the given elements available, and add a new Element node at the lowest 138 * level. 139 * 140 * @param elementNames A list of tags, must start with the document root. 141 * @return The last element added. 142 */ 143 private Element addParents(String... elementNames) { 144 ArgumentNotValid.checkTrue(elementNames.length >= 2, "Must have at least root element and final element in " 145 + "element names, not just " + Arrays.asList(elementNames)); 146 Element currentNode = xmlDoc.getRootElement(); 147 if (!currentNode.getName().equals(elementNames[0])) { 148 throw new ArgumentNotValid("Document has root element '" + currentNode.getName() + "', not '" 149 + elementNames[0] + "'"); 150 } 151 for (int i = 1; i < elementNames.length - 1; i++) { 152 String elementName = elementNames[i]; 153 List<Element> nodes = currentNode.elements(elementName); 154 if (nodes == null || nodes.size() == 0) { 155 // Element not found, add at end 156 currentNode = currentNode.addElement(elementName); 157 } else { 158 currentNode = nodes.get(nodes.size() - 1); 159 } 160 } 161 return addAfterSameElement(currentNode, elementNames[elementNames.length - 1]); 162 } 163 164 /** 165 * Add another element either right after the last of its kind in currentNode or at the end of currentNode. 166 * 167 * @param currentNode A node that the new element will be a sub-node of 168 * @param elementName The name of the new element 169 * @return The new element, which is now placed under currentNode 170 */ 171 private Element addAfterSameElement(Element currentNode, String elementName) { 172 Element newElement = currentNode.addElement(elementName); 173 newElement.detach(); 174 // If there are already nodes of this type, add straight after them. 175 List<Element> existingNodes = currentNode.elements(); 176 for (int i = existingNodes.size() - 1; i >= 0; i--) { 177 if (existingNodes.get(i).getName().equals(elementName)) { 178 existingNodes.add(i + 1, newElement); 179 return newElement; 180 } 181 } 182 // Otherwise add at the end. 183 existingNodes.add(newElement); 184 return newElement; 185 } 186 187 /** 188 * Removes current settings for a key and adds new values for the same key. Calling update() is equivalent to 189 * calling delete() and add(), except the old value does not get destroyed on errors and order of the elements are 190 * kept. If no values are given, the key is removed. 191 * 192 * @param key The key for which the value should be updated. 193 * @param values The new values that should be set for the key. 194 * @throws UnknownID if the key does not exist 195 * @throws ArgumentNotValid if the key is null or empty, or any of the values are null 196 */ 197 public void update(String key, String... values) { 198 ArgumentNotValid.checkNotNullOrEmpty(key, "String key"); 199 ArgumentNotValid.checkNotNull(values, "String... values"); 200 for (int i = 0; i < values.length; i++) { 201 ArgumentNotValid.checkNotNull(values[i], "String values[" + i + "]"); 202 } 203 if (!hasKey(key)) { 204 throw new UnknownID("No key registered with the name: '" + key + "' in '" + source + "'"); 205 } 206 207 List<Node> nodes = getXPath(key).selectNodes(xmlDoc); 208 int i = 0; 209 for (; i < nodes.size() && i < values.length; i++) { 210 nodes.get(i).setText(values[i]); 211 } 212 if (i < nodes.size()) { 213 // Delete nodes if there were more nodes than values 214 for (; i < nodes.size(); i++) { 215 nodes.get(i).detach(); 216 } 217 } else { 218 // Add nodes if there were fewer nodes than values 219 for (; i < values.length; i++) { 220 Element newNode = addParents(key.split("\\.")); 221 newNode.setText(values[i]); 222 } 223 } 224 } 225 226 /** 227 * Get the first entry that matches the key. Keys are constructed as a dot separated path of xml tag names. Example: 228 * The following XML definition of a user name <dk><netarkivet><user>ssc</user> 229 * </netarkivet></dk> is accessed using the path: "dk.netarkivet.user" 230 * 231 * @param key the key of the entry. 232 * @return the first entry that matches the key. 233 * @throws UnknownID if no element matches the key 234 * @throws ArgumentNotValid if the key is null or empty 235 */ 236 public String getString(String key) { 237 ArgumentNotValid.checkNotNullOrEmpty(key, "key"); 238 239 XPath xpath = getXPath(key); 240 List<Node> nodes = xpath.selectNodes(xmlDoc); 241 if (nodes == null || nodes.size() == 0) { 242 throw new UnknownID("No elements exists for the path '" + key + "' in '" + source + "'"); 243 } 244 Node first = nodes.get(0); 245 return first.getStringValue().trim(); 246 } 247 248 /** 249 * Checks if a setting with the specified key exists. 250 * 251 * @param key a key for a setting 252 * @return true if the key exists 253 * @throws ArgumentNotValid if key is null or empty 254 */ 255 public boolean hasKey(String key) { 256 ArgumentNotValid.checkNotNullOrEmpty(key, "key"); 257 258 final List<Node> nodes = (List<Node>) getXPath(key).selectNodes(xmlDoc); 259 return nodes != null && nodes.size() > 0; 260 } 261 262 /** 263 * Get list of all items matching the key. If no items exist matching the key, an empty list is returned. 264 * 265 * @param key the path down to elements to get 266 * @return a list of items that match the supplied key 267 */ 268 public List<String> getList(String key) { 269 ArgumentNotValid.checkNotNullOrEmpty(key, "key"); 270 271 List<Node> nodes = (List<Node>) getXPath(key).selectNodes(xmlDoc); 272 if (nodes == null || nodes.size() == 0) { 273 return Collections.emptyList(); 274 } 275 List<String> results = new ArrayList<String>(nodes.size()); 276 for (Node node : nodes) { 277 results.add(node.getText()); 278 } 279 return results; 280 } 281 282 /** 283 * Save the current settings as an XML file. 284 * 285 * @param f the file to write the XML to. 286 */ 287 public void save(File f) { 288 ArgumentNotValid.checkNotNull(f, "f"); 289 XmlUtils.writeXmlToFile(xmlDoc, f); 290 } 291 292 /** 293 * Return a tree structure reflecting the XML and trimmed values. 294 * 295 * @param path Dotted path into the xml. 296 * @return A tree reflecting the xml at the given path. 297 * @throws UnknownID If the path does not exist in the tree or is ambiguous 298 */ 299 public StringTree<String> getTree(String path) { 300 ArgumentNotValid.checkNotNullOrEmpty(path, "String path"); 301 XPath xpath = getXPath(path); 302 List<Node> nodes = xpath.selectNodes(xmlDoc); 303 if (nodes == null || nodes.size() == 0) { 304 throw new UnknownID("No path '" + path + "' in XML document '" + source + "'"); 305 } else if (nodes.size() > 1) { 306 throw new UnknownID("More than one candidate for path '" + path + "' in XML document '" + source + "'"); 307 } 308 return XmlTree.getStringTree(nodes.get(0)); 309 } 310 311 /** 312 * Get an XPath version of the given dotted path. A dotted path foo.bar.baz corresponds to the XML node 313 * <foo><bar><baz> </baz></bar></foo> 314 * <p> 315 * Implementation note: If needed, this could be optimized by keeping a HashMap cache of the XPaths, since they 316 * don't change. 317 * 318 * @param path A dotted path 319 * @return An XPath that matches the dotted path equivalent, using "dk:" as namespace prefix for all but the first 320 * element. 321 */ 322 private XPath getXPath(String path) { 323 String[] pathParts = path.split("\\."); 324 StringBuilder result = new StringBuilder(); 325 result.append("/"); 326 result.append(pathParts[0]); 327 for (int i = 1; i < pathParts.length; i++) { 328 result.append("/dk:"); 329 result.append(pathParts[i]); 330 } 331 XPath xpath = xmlDoc.createXPath(result.toString()); 332 Namespace nameSpace = xmlDoc.getRootElement().getNamespace(); 333 Map<String, String> namespaceURIs = new HashMap<String, String>(1); 334 namespaceURIs.put("dk", nameSpace.getURI()); 335 xpath.setNamespaceURIs(namespaceURIs); 336 return xpath; 337 } 338 339}