View Javadoc

1   /**
2    * This file Copyright (c) 2003-2014 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.jcr.util;
35  
36  import info.magnolia.cms.core.MgnlNodeType;
37  import info.magnolia.cms.util.OrderedProperties;
38  
39  import java.io.ByteArrayInputStream;
40  import java.io.IOException;
41  import java.io.InputStream;
42  import java.util.Arrays;
43  import java.util.Properties;
44  
45  import javax.jcr.Node;
46  import javax.jcr.RepositoryException;
47  
48  import org.apache.commons.beanutils.ConvertUtils;
49  import org.apache.commons.io.IOUtils;
50  import org.apache.commons.lang.StringUtils;
51  import org.apache.jackrabbit.util.ISO8601;
52  
53  /**
54   * Utility class providing support for properties-like format to import/export jcr data. Useful when data regularly
55   * needs to be bootstrapped, for instance, and the jcr xml format is too cumbersome to maintain.
56   *
57   * Caution: Binary data is represented as ByteArrayInputStream because of the lack of a proper javax.jcr.Binary implementation
58   *
59   * TODO : handle conflicts (already existing nodes, properties, what to do with existing properties if we don't create
60   * new nodes, ...)
61   *
62   * TODO dlipp - export is not yet implemented
63   */
64  public class PropertiesImportExport {
65  
66      /**
67       * Each property is one item in the properties varargs passed in.
68       */
69      public void createNodes(Node root, String... properties) throws IOException, RepositoryException {
70          createNodes(root, IOUtils.toInputStream(StringUtils.join(Arrays.asList(properties), "\n")));
71      }
72  
73      /**
74       * Each property or node in the stream has to be separated by the \n.
75       */
76      public void createNodes(Node root, InputStream propertiesStream) throws IOException, RepositoryException {
77          Properties properties = new OrderedProperties();
78  
79          properties.load(propertiesStream);
80  
81          properties = keysToInnerFormat(properties);
82  
83          for (Object o : properties.keySet()) {
84              String key = (String) o;
85              String valueStr = properties.getProperty(key);
86  
87              String propertyName = StringUtils.substringAfterLast(key, ".");
88              String path = StringUtils.substringBeforeLast(key, ".");
89  
90              String type = null;
91              if (propertyName.equals("@type")) {
92                  type = valueStr;
93              } else if (properties.containsKey(path + ".@type")) {
94                  type = properties.getProperty(path + ".@type");
95              }
96  
97              type = StringUtils.defaultIfEmpty(type, MgnlNodeType.NT_CONTENTNODE);
98              Node c = NodeUtil.createPath(root, path, type);
99              populateNode(c, propertyName, valueStr);
100         }
101     }
102 
103     /**
104      * Transforms the keys to the following inner notation: <code>/some/path/node.prop</code> or
105      * <code>/some/path/node.@type</code>.
106      */
107     private Properties keysToInnerFormat(Properties properties) {
108         Properties cleaned = new OrderedProperties();
109 
110         for (Object o : properties.keySet()) {
111             String orgKey = (String) o;
112             // explicitly enforce certain syntax
113             if (!orgKey.startsWith("/")) {
114                 throw new IllegalArgumentException("Missing trailing '/' for key: " + orgKey);
115             }
116             if (StringUtils.countMatches(orgKey, ".") > 1) {
117                 throw new IllegalArgumentException("Key must not contain more than one '.': " + orgKey);
118             }
119             if (orgKey.contains("@") && !orgKey.contains(".@")) {
120                 throw new IllegalArgumentException("Key containing '@' must be preceeded by a '.': " + orgKey);
121             }
122             // if this is a node definition (no property)
123             String newKey = orgKey;
124 
125             String propertyName = StringUtils.substringAfterLast(newKey, ".");
126             String keySuffix = StringUtils.substringBeforeLast(newKey, ".");
127             String path = StringUtils.removeStart(keySuffix, "/");
128 
129             // if this is a path (no property)
130             if (StringUtils.isEmpty(propertyName)) {
131                 // no value --> is a node
132                 if (StringUtils.isEmpty(properties.getProperty(orgKey))) {
133                     // make this the type property if not defined otherwise
134                     if (!properties.containsKey(orgKey + ".@type")) {
135                         cleaned.put(path + ".@type", MgnlNodeType.NT_CONTENTNODE);
136                     }
137                     continue;
138                 }
139                 throw new IllegalArgumentException("Key for a path (everything without a '.' is considered to be a path) must not contain a value ('='): " + orgKey);
140             }
141             cleaned.put(path + "." + propertyName, properties.get(orgKey));
142         }
143         return cleaned;
144     }
145 
146     protected void populateNode(Node node, String name, String valueStr) throws RepositoryException {
147         if (StringUtils.isEmpty(name) && StringUtils.isEmpty(valueStr)) {
148             // happens if the input properties file just created a node with no properties
149             return;
150         }
151         if (name.equals("@type")) {
152             // do nothing, this has been taken into account when creating the node.
153         } else if (name.equals("@uuid")) {
154             setIdentifier(node, valueStr);
155         } else {
156             Object valueObj = convertPropertyStringToObject(valueStr);
157             PropertyUtil.setProperty(node, name, valueObj);
158         }
159     }
160 
161     /**
162      * Intentionally created this method to allow simple creation of subclasses actually setting the identifier (e.g. in
163      * tests).
164      */
165     protected void setIdentifier(Node ignoredNode, String ignoredString) {
166         throw new UnsupportedOperationException("Can't see UUIDs on real node.");
167     }
168 
169     protected Object convertPropertyStringToObject(String valueStr) {
170         if (contains(valueStr, ':')) {
171             final String type = StringUtils.substringBefore(valueStr, ":");
172             final String value = StringUtils.substringAfter(valueStr, ":");
173 
174             // there is no beanUtils converter for Calendar
175             if (type.equalsIgnoreCase("date")) {
176                 return ISO8601.parse(value);
177             } else if (type.equalsIgnoreCase("binary")) {
178                 return new ByteArrayInputStream(value.getBytes());
179             } else {
180                 try {
181                     final Class<?> typeCl;
182                     if (type.equals("int")) {
183                         typeCl = Integer.class;
184                     } else {
185                         typeCl = Class.forName("java.lang." + StringUtils.capitalize(type));
186                     }
187                     return ConvertUtils.convert(value, typeCl);
188                 } catch (ClassNotFoundException e) {
189                     // possibly a stray :, let's ignore it for now
190                     return valueStr;
191                 }
192             }
193         }
194         // no type specified, we assume it's a string, no conversion
195         return valueStr;
196     }
197 
198     private static boolean contains(String s, char ch) {
199         return s.indexOf(ch) > -1;
200     }
201 }