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