View Javadoc

1   /**
2    * This file Copyright (c) 2003-2013 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 java.io.ByteArrayInputStream;
37  import java.io.IOException;
38  import java.io.InputStream;
39  import java.util.Arrays;
40  import java.util.Calendar;
41  import java.util.Properties;
42  import javax.jcr.Node;
43  import javax.jcr.Property;
44  import javax.jcr.PropertyIterator;
45  import javax.jcr.PropertyType;
46  import javax.jcr.RepositoryException;
47  import javax.jcr.Value;
48  
49  import org.apache.commons.beanutils.ConvertUtils;
50  import org.apache.commons.io.IOUtils;
51  import org.apache.commons.lang.StringUtils;
52  import org.apache.jackrabbit.util.ISO8601;
53  
54  import info.magnolia.cms.util.OrderedProperties;
55  import info.magnolia.jcr.predicate.AbstractPredicate;
56  
57  /**
58   * Utility class providing support for properties-like format to import/export jcr data. Useful when data regularly
59   * needs to be bootstrapped, for instance, and the jcr xml format is too cumbersome to maintain.
60   *
61   * Caution: Binary data is represented as ByteArrayInputStream because of the lack of a proper javax.jcr.Binary implementation
62   *
63   * TODO : handle conflicts (already existing nodes, properties, what to do with existing properties if we don't create
64   * new nodes, ...)
65   */
66  public class PropertiesImportExport {
67  
68      /**
69       * Each property is one item in the properties varargs passed in.
70       */
71      public void createNodes(Node root, String... properties) throws IOException, RepositoryException {
72          createNodes(root, IOUtils.toInputStream(StringUtils.join(Arrays.asList(properties), "\n")));
73      }
74  
75      /**
76       * Each property or node in the stream has to be separated by the \n.
77       */
78      public void createNodes(Node root, InputStream propertiesStream) throws IOException, RepositoryException {
79          Properties properties = new OrderedProperties();
80  
81          properties.load(propertiesStream);
82  
83          properties = keysToInnerFormat(properties);
84  
85          for (Object o : properties.keySet()) {
86              String key = (String) o;
87              String valueStr = properties.getProperty(key);
88  
89              String propertyName = StringUtils.substringAfterLast(key, ".");
90              String path = StringUtils.substringBeforeLast(key, ".");
91  
92              String type = null;
93              if (propertyName.equals("@type")) {
94                  type = valueStr;
95              } else if (properties.containsKey(path + ".@type")) {
96                  type = properties.getProperty(path + ".@type");
97              }
98  
99              type = StringUtils.defaultIfEmpty(type, NodeTypes.ContentNode.NAME);
100             Node c = NodeUtil.createPath(root, path, type);
101             populateNode(c, propertyName, valueStr);
102         }
103     }
104 
105     /**
106      * Transforms the keys to the following inner notation: <code>/some/path/node.prop</code> or
107      * <code>/some/path/node.@type</code>.
108      */
109     private Properties keysToInnerFormat(Properties properties) {
110         Properties cleaned = new OrderedProperties();
111 
112         for (Object o : properties.keySet()) {
113             String orgKey = (String) o;
114             // explicitly enforce certain syntax
115             if (!orgKey.startsWith("/")) {
116                 throw new IllegalArgumentException("Missing trailing '/' for key: " + orgKey);
117             }
118             if (StringUtils.countMatches(orgKey, ".") > 1) {
119                 throw new IllegalArgumentException("Key must not contain more than one '.': " + orgKey);
120             }
121             if (orgKey.contains("@") && !orgKey.contains(".@")) {
122                 throw new IllegalArgumentException("Key containing '@' must be preceded by a '.': " + orgKey);
123             }
124             // if this is a node definition (no property)
125             String newKey = orgKey;
126 
127             String propertyName = StringUtils.substringAfterLast(newKey, ".");
128             String keySuffix = StringUtils.substringBeforeLast(newKey, ".");
129             String path = StringUtils.removeStart(keySuffix, "/");
130 
131             // if this is a path (no property)
132             if (StringUtils.isEmpty(propertyName)) {
133                 // no value --> is a node
134                 if (StringUtils.isEmpty(properties.getProperty(orgKey))) {
135                     // make this the type property if not defined otherwise
136                     if (!properties.containsKey(orgKey + ".@type")) {
137                         cleaned.put(path + ".@type", NodeTypes.ContentNode.NAME);
138                     }
139                     continue;
140                 }
141                 throw new IllegalArgumentException("Key for a path (everything without a '.' is considered to be a path) must not contain a value ('='): " + orgKey);
142             }
143             cleaned.put(path + "." + propertyName, properties.get(orgKey));
144         }
145         return cleaned;
146     }
147 
148     protected void populateNode(Node node, String name, String valueStr) throws RepositoryException {
149         if (StringUtils.isEmpty(name) && StringUtils.isEmpty(valueStr)) {
150             // happens if the input properties file just created a node with no properties
151             return;
152         }
153         if (name.equals("@type")) {
154             // do nothing, this has been taken into account when creating the node.
155         } else if (name.equals("@uuid") || name.equals("uuid")) {
156             setIdentifier(node, valueStr);
157         } else {
158             Object valueObj = convertPropertyStringToObject(valueStr);
159             PropertyUtil.setProperty(node, name, valueObj);
160         }
161     }
162 
163     /**
164      * Intentionally created this method to allow simple creation of subclasses actually setting the identifier (e.g. in
165      * tests).
166      */
167     protected void setIdentifier(Node ignoredNode, String ignoredString) {
168         throw new UnsupportedOperationException("Can't see UUIDs on real node.");
169     }
170 
171     protected Object convertPropertyStringToObject(String valueStr) {
172         if (contains(valueStr, ':')) {
173             final String type = StringUtils.substringBefore(valueStr, ":");
174             final String value = StringUtils.substringAfter(valueStr, ":");
175 
176             // there is no beanUtils converter for Calendar
177             if (type.equalsIgnoreCase("date")) {
178                 return ISO8601.parse(value);
179             } else if (type.equalsIgnoreCase("binary")) {
180                 return new ByteArrayInputStream(value.getBytes());
181             } else {
182                 try {
183                     final Class<?> typeCl;
184                     if (type.equals("int")) {
185                         typeCl = Integer.class;
186                     } else {
187                         typeCl = Class.forName("java.lang." + StringUtils.capitalize(type));
188                     }
189                     return ConvertUtils.convert(value, typeCl);
190                 } catch (ClassNotFoundException e) {
191                     // possibly a stray :, let's ignore it for now
192                     return valueStr;
193                 }
194             }
195         }
196         // no type specified, we assume it's a string, no conversion
197         return valueStr;
198     }
199 
200     private static boolean contains(String s, char ch) {
201         return s.indexOf(ch) > -1;
202     }
203 
204     public Properties toProperties(Node node, final AbstractPredicate<Node> nodePredicate) throws RepositoryException {
205         final Properties out = new OrderedProperties();
206         NodeUtil.visit(
207                 node,
208                 new NodeVisitor() {
209                     @Override
210                     public void visit(Node node) throws RepositoryException {
211                         appendNodeTypeAndIdentifier(node, out);
212                         appendNodeProperties(node, out);
213                     }
214                 }, nodePredicate
215         );
216         return out;
217     }
218 
219     private void appendNodeTypeAndIdentifier(Node node, Properties out) throws RepositoryException {
220 
221         // we don't need to export the JCR root node.
222         if (node.getDepth() == 0) {
223             return;
224         }
225 
226         String path = getExportPath(node);
227 
228         String nodeTypeName = node.getPrimaryNodeType().getName();
229         if (nodeTypeName != null && StringUtils.isNotEmpty(nodeTypeName)) {
230             out.put(path + ".@type", nodeTypeName);
231         }
232 
233         String nodeIdentifier = node.getIdentifier();
234         if (nodeIdentifier != null && StringUtils.isNotEmpty(nodeIdentifier)) {
235             out.put(path + ".@uuid", nodeIdentifier);
236         }
237     }
238 
239     private void appendNodeProperties(Node node, Properties out) throws RepositoryException {
240         PropertyIterator propertyIterator = node.getProperties();
241         while (propertyIterator.hasNext()) {
242             Property property = propertyIterator.nextProperty();
243             String path = getExportPath(node) + "." + property.getName();
244 
245             String propertyValue = getPropertyString(property);
246 
247             if (propertyValue != null) {
248                 out.setProperty(path, propertyValue);
249             }
250         }
251     }
252 
253     private String getExportPath(Node node) throws RepositoryException {
254         return node.getPath();
255     }
256 
257     private String getPropertyString(Property property) throws RepositoryException {
258 
259         switch (property.getType()) {
260             case (PropertyType.STRING): {
261                 return property.getString();
262             }
263             case (PropertyType.BOOLEAN): {
264                 return convertBooleanToExportString(property.getBoolean());
265             }
266             case (PropertyType.BINARY): {
267                 return convertBinaryToExportString(property.getValue());
268             }
269             case (PropertyType.PATH): {
270                 return property.getString();
271             }
272             case (PropertyType.DATE): {
273                 return convertCalendarToExportString(property.getDate());
274             }
275             case (PropertyType.LONG): {
276                 return "" + property.getLong();
277             }
278             case (PropertyType.DOUBLE): {
279                 return "" + property.getDouble();
280             }
281             default: {
282                 return property.getString();
283             }
284         }
285     }
286 
287     private String convertBooleanToExportString(boolean b) {
288         return "boolean:" + (b ? "true" : "false");
289     }
290 
291     private String convertBinaryToExportString(Value value) throws RepositoryException {
292         return "binary:" + ConvertUtils.convert(value.getString());
293     }
294 
295     private String convertCalendarToExportString(Calendar calendar) {
296         return "date:" + ISO8601.format(calendar);
297     }
298 }