View Javadoc

1   /**
2    * This file Copyright (c) 2003-2011 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   * TODO : handle conflicts (already existing nodes, properties, what to do with existing properties if we don't create
58   * new nodes, ...)
59   *
60   * TODO dlipp - export is not yet implemented
61   *
62   * @version $Id$
63   */
64  public class PropertiesImportExport {
65  
66      /**
67       * Each property is one item in the properties varargs passed in.
68       * 
69       * @param root
70       * @param properties
71       * @throws IOException
72       * @throws RepositoryException
73       */
74      public void createNodes(Node root, String... properties) throws IOException, RepositoryException {
75          createNodes(root, IOUtils.toInputStream(StringUtils.join(Arrays.asList(properties), "\n")));
76      }
77  
78      /**
79       * Each property or node in the stream has to be separated by the \n.
80       * 
81       * @param root
82       * @param propertiesStream
83       * @throws IOException
84       * @throws RepositoryException
85       */
86      public void createNodes(Node root, InputStream propertiesStream) throws IOException, RepositoryException {
87          Properties properties = new OrderedProperties();
88  
89          properties.load(propertiesStream);
90  
91          properties = keysToInnerFormat(properties);
92  
93          for (Object o : properties.keySet()) {
94              String key = (String) o;
95              String valueStr = properties.getProperty(key);
96  
97              String propertyName = StringUtils.substringAfterLast(key, ".");
98              String path = StringUtils.substringBeforeLast(key, ".");
99  
100             String type = null;
101             if (propertyName.equals("@type")) {
102                 type = valueStr;
103             } else if (properties.containsKey(path + ".@type")) {
104                 type = properties.getProperty(path + ".@type");
105             }
106 
107             type = StringUtils.defaultIfEmpty(type, MgnlNodeType.NT_CONTENTNODE);
108             Node c = NodeUtil.createPath(root, path, type);
109             populateNode(c, propertyName, valueStr);
110         }
111     }
112 
113     /**
114      * Transforms the keys to the following inner notation: <code>/some/path/node.prop</code> or
115      * <code>/some/path/node.@type</code>.
116      */
117     private Properties keysToInnerFormat(Properties properties) {
118         Properties cleaned = new OrderedProperties();
119 
120         for (Object o : properties.keySet()) {
121             String orgKey = (String) o;
122             // explicitly enforce certain syntax
123             if (!orgKey.startsWith("/")) {
124                 throw new IllegalArgumentException("Missing trailing '/' for key: " + orgKey);
125             }
126             if (StringUtils.countMatches(orgKey, ".") > 1) {
127                 throw new IllegalArgumentException("Key must not contain more than one '.': " + orgKey);
128             }
129             if (orgKey.contains("@") && !orgKey.contains(".@")) {
130                 throw new IllegalArgumentException("Key containing '@' must be preceeded by a '.': " + orgKey);
131             }
132             // if this is a node definition (no property)
133             String newKey = orgKey;
134 
135             String propertyName = StringUtils.substringAfterLast(newKey, ".");
136             String keySuffix = StringUtils.substringBeforeLast(newKey, ".");
137             String path = StringUtils.removeStart(keySuffix, "/");
138 
139             // if this is a path (no property)
140             if (StringUtils.isEmpty(propertyName)) {
141                 // no value --> is a node
142                 if (StringUtils.isEmpty(properties.getProperty(orgKey))) {
143                     // make this the type property if not defined otherwise
144                     if (!properties.containsKey(orgKey + ".@type")) {
145                         cleaned.put(path + ".@type", MgnlNodeType.NT_CONTENTNODE);
146                     }
147                     continue;
148                 }
149                 throw new IllegalArgumentException("Key for a path (everything without a '.' is considered to be a path) must not contain a value ('='): " + orgKey);
150             }
151             cleaned.put(path + "." + propertyName, properties.get(orgKey));
152         }
153         return cleaned;
154     }
155 
156     protected void populateNode(Node node, String name, String valueStr) throws RepositoryException {
157         if (StringUtils.isEmpty(name) && StringUtils.isEmpty(valueStr)) {
158             // happens if the input properties file just created a node with no properties
159             return;
160         }
161         if (name.equals("@type")) {
162             // do nothing, this has been taken into account when creating the node.
163         } else if (name.equals("@uuid")) {
164             setIdentifier(node, valueStr);
165         } else {
166             Object valueObj = convertPropertyStringToObject(valueStr);
167             PropertyUtil.setProperty(node, name, valueObj);
168         }
169     }
170 
171     /**
172      * Intentionally created this method to allow simple creation of sublcasses actually setting the identifier (e.g. in
173      * tests).
174      */
175     protected void setIdentifier(Node ignoredNode, String ignoredString) {
176         throw new UnsupportedOperationException("Can't see UUIDs on real node.");
177     }
178 
179     protected Object convertPropertyStringToObject(String valueStr) {
180         if (contains(valueStr, ':')) {
181             final String type = StringUtils.substringBefore(valueStr, ":");
182             final String value = StringUtils.substringAfter(valueStr, ":");
183 
184             // there is no beanUtils converter for Calendar
185             if (type.equalsIgnoreCase("date")) {
186                 return ISO8601.parse(value);
187             } else if (type.equalsIgnoreCase("binary")) {
188                 return new ByteArrayInputStream(value.getBytes());
189             } else {
190                 try {
191                     final Class<?> typeCl;
192                     if (type.equals("int")) {
193                         typeCl = Integer.class;
194                     } else {
195                         typeCl = Class.forName("java.lang." + StringUtils.capitalize(type));
196                     }
197                     return ConvertUtils.convert(value, typeCl);
198                 } catch (ClassNotFoundException e) {
199                     // possibly a stray :, let's ignore it for now
200                     return valueStr;
201                 }
202             }
203         }
204         // no type specified, we assume it's a string, no conversion
205         return valueStr;
206     }
207 
208     private static boolean contains(String s, char ch) {
209         return s.indexOf(ch) > -1;
210     }
211 }