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.importexport;
35  
36  import info.magnolia.cms.core.Content;
37  import info.magnolia.cms.core.HierarchyManager;
38  import info.magnolia.cms.core.ItemType;
39  import info.magnolia.cms.core.NodeData;
40  import info.magnolia.cms.util.ContentUtil;
41  import info.magnolia.cms.util.OrderedProperties;
42  
43  import java.io.ByteArrayInputStream;
44  import java.io.IOException;
45  import java.io.InputStream;
46  import java.util.Calendar;
47  import java.util.Collection;
48  import java.util.Properties;
49  import java.util.Set;
50  
51  import javax.jcr.PropertyType;
52  import javax.jcr.RepositoryException;
53  import javax.jcr.Value;
54  
55  import org.apache.commons.beanutils.ConvertUtils;
56  import org.apache.commons.lang3.StringUtils;
57  import org.apache.jackrabbit.util.ISO8601;
58  
59  /**
60   * Utility class providing support for properties-like format to import/export jcr data. Useful when
61   * data regularly needs to be bootstrapped, for instance, and the jcr xml format is too cumbersome to maintain.
62   *
63   * TODO : handle conflicts (already existing nodes, properties, what to do with existing properties if we don't create new nodes, ...)
64   * TODO : consolidate syntax
65   *
66   * @deprecated since 5.2.2 - use {@link info.magnolia.jcr.util.PropertiesImportExport}.
67   */
68  public class PropertiesImportExport {
69  
70      public void createContent(Content root, InputStream propertiesStream) throws IOException, RepositoryException {
71          Properties properties = new OrderedProperties();
72  
73          properties.load(propertiesStream);
74  
75          properties = keysToInnerFormat(properties);
76  
77          for (Object o : properties.keySet()) {
78              String key = (String) o;
79              String valueStr = properties.getProperty(key);
80  
81              String propertyName = StringUtils.substringAfterLast(key, ".");
82              String path = StringUtils.substringBeforeLast(key, ".");
83  
84              String type = null;
85              if (propertyName.equals("@type")) {
86                  type = valueStr;
87              } else if (properties.containsKey(path + ".@type")) {
88                  type = properties.getProperty(path + ".@type");
89              }
90  
91              type = StringUtils.defaultIfEmpty(type, ItemType.CONTENTNODE.getSystemName());
92              Content c = ContentUtil.createPath(root, path, new ItemType(type));
93              populateContent(c, propertyName, valueStr);
94          }
95      }
96  
97      /**
98       * Transforms the keys to the following inner notation: <code>some/path/node.prop</code> or <code>some/path/node.@type</code>.
99       */
100     private Properties keysToInnerFormat(Properties properties) {
101         Properties cleaned = new OrderedProperties();
102 
103         for (Object o : properties.keySet()) {
104             String orgKey = (String) o;
105 
106             //if this is a node definition (no property)
107             String newKey = orgKey;
108 
109             // make sure we have a dot as a property separator
110             newKey = StringUtils.replace(newKey, "@", ".@");
111             // avoid double dots
112             newKey = StringUtils.replace(newKey, "..@", ".@");
113 
114             String propertyName = StringUtils.substringAfterLast(newKey, ".");
115             String keySuffix = StringUtils.substringBeforeLast(newKey, ".");
116             String path = StringUtils.replace(keySuffix, ".", "/");
117             path = StringUtils.removeStart(path, "/");
118 
119             // if this is a path (no property)
120             if (StringUtils.isEmpty(propertyName)) {
121                 // no value --> is a node
122                 if (StringUtils.isEmpty(properties.getProperty(orgKey))) {
123                     // make this the type property if not defined otherwise
124                     if (!properties.containsKey(orgKey + "@type")) {
125                         cleaned.put(path + ".@type", ItemType.CONTENTNODE.getSystemName());
126                     }
127                     continue;
128                 }
129                 propertyName = StringUtils.substringAfterLast(path, "/");
130                 path = StringUtils.substringBeforeLast(path, "/");
131             }
132             cleaned.put(path + "." + propertyName, properties.get(orgKey));
133         }
134         return cleaned;
135     }
136 
137     protected void populateContent(Content c, String name, String valueStr) throws RepositoryException {
138         if (StringUtils.isEmpty(name) && StringUtils.isEmpty(valueStr)) {
139             // happens if the input properties file just created a node with no properties
140             return;
141         }
142         if (name.equals("@type")) {
143             // do nothing, this has been taken into account when creating the node.
144         } else if (name.equals("@uuid")) {
145             throw new UnsupportedOperationException("Can't see UUIDs on real node. Use MockUtil if you are using MockContent instances.");
146         } else {
147             Object valueObj = convertNodeDataStringToObject(valueStr);
148             c.setNodeData(name, valueObj);
149         }
150     }
151 
152     protected Object convertNodeDataStringToObject(String valueStr) {
153         if (contains(valueStr, ':')) {
154             final String type = StringUtils.substringBefore(valueStr, ":");
155             final String value = StringUtils.substringAfter(valueStr, ":");
156 
157             // there is no beanUtils converter for Calendar
158             if (type.equalsIgnoreCase("date")) {
159                 return ISO8601.parse(value);
160             } else if (type.equalsIgnoreCase("binary")) {
161                 return new ByteArrayInputStream(value.getBytes());
162             } else {
163                 try {
164                     final Class<?> typeCl;
165                     if (type.equals("int")) {
166                         typeCl = Integer.class;
167                     } else {
168                         typeCl = Class.forName("java.lang." + StringUtils.capitalize(type));
169                     }
170                     return ConvertUtils.convert(value, typeCl);
171                 } catch (ClassNotFoundException e) {
172                     // possibly a stray :, let's ignore it for now
173                     return valueStr;
174                 }
175             }
176         }
177         // no type specified, we assume it's a string, no conversion
178         return valueStr;
179     }
180 
181     /**
182      * This method is deprecated, it returns results in a format that does not match
183      * the format that the import method uses (doesn't include @uuid or @type properties)
184      *
185      * It is kept here to support existing test and applications that might break
186      * as a result of these changes (i.e. unit tests that are expecting a specific number of
187      * properties returned, etc)
188      *
189      * @deprecated since 4.3 - use {@link info.magnolia.jcr.util.PropertiesImportExport#toProperties(javax.jcr.Node, info.magnolia.jcr.predicate.AbstractPredicate)}
190      */
191     @Deprecated
192     public static Properties toProperties(HierarchyManager hm) throws Exception {
193         return toProperties(hm.getRoot());
194     }
195 
196     /**
197      * @deprecated since 5.0 - use {@link info.magnolia.jcr.util.PropertiesImportExport#toProperties(javax.jcr.Node, info.magnolia.jcr.predicate.AbstractPredicate)}
198      */
199     public static Properties toProperties(Content rootContent) throws Exception {
200         return toProperties(rootContent, ContentUtil.EXCLUDE_META_DATA_CONTENT_FILTER, true);
201     }
202 
203     /**
204      * @deprecated since 5.0 - use {@link info.magnolia.jcr.util.PropertiesImportExport#toProperties(javax.jcr.Node, info.magnolia.jcr.predicate.AbstractPredicate)}
205      */
206     public static Properties contentToProperties(HierarchyManager hm) throws Exception {
207         return contentToProperties(hm.getRoot());
208     }
209 
210     /**
211      * @deprecated since 5.0 - use {@link info.magnolia.jcr.util.PropertiesImportExport#toProperties(javax.jcr.Node, info.magnolia.jcr.predicate.AbstractPredicate)}
212      */
213     public static Properties contentToProperties(Content rootContent) throws Exception {
214         return toProperties(rootContent, ContentUtil.EXCLUDE_META_DATA_CONTENT_FILTER, false);
215     }
216 
217     /**
218      * @deprecated since 5.0 - use {@link info.magnolia.jcr.util.PropertiesImportExport#toProperties(javax.jcr.Node, info.magnolia.jcr.predicate.AbstractPredicate)}
219      */
220     public static Properties contentToProperties(Content rootContent, Content.ContentFilter filter) throws Exception {
221         return toProperties(rootContent, filter, false);
222     }
223 
224     /**
225      * This method is private because it includes the boolean "legacymode" filter which
226      * shouldn't be exposed as part of the API because when "legacymode" is removed, it will
227      * force an API change.
228      *
229      * @param rootContent root node to convert into properties
230      * @param contentFilter a content filter to use in selecting what content to export
231      * @param legacyMode if true, will not include @uuid and @type nodes
232      * @return a Properties object representing the content starting at rootContent
233      * @deprecated since 5.0 - use {@link info.magnolia.jcr.util.PropertiesImportExport#toProperties(javax.jcr.Node, info.magnolia.jcr.predicate.AbstractPredicate)}
234      */
235     private static Properties toProperties(Content rootContent, Content.ContentFilter contentFilter, final boolean legacyMode) throws Exception {
236         final Properties out = new OrderedProperties();
237         ContentUtil.visit(rootContent, new ContentUtil.Visitor() {
238             @Override
239             public void visit(Content node) throws Exception {
240                 if (!legacyMode) {
241                     appendNodeTypeAndUUID(node, out, true);
242                 }
243                 appendNodeProperties(node, out);
244             }
245         }, contentFilter);
246         return out;
247     }
248 
249     private static void appendNodeTypeAndUUID(Content node, Properties out, final boolean dumpMetaData) throws RepositoryException {
250         String path = getExportPath(node);
251         // we don't need to export the JCR root node.
252         if (path.equals("/jcr:root")) {
253             return;
254         }
255 
256         String nodeTypeName = node.getNodeTypeName();
257         if (nodeTypeName != null && StringUtils.isNotEmpty(nodeTypeName)) {
258             out.put(path + "@type", nodeTypeName);
259         }
260         String nodeUUID = node.getUUID();
261         if (nodeUUID != null && StringUtils.isNotEmpty(nodeUUID)) {
262             out.put(path + "@uuid", node.getUUID());
263         }
264     }
265 
266     public static void appendNodeProperties(Content node, Properties out) {
267         final Collection<NodeData> props = node.getNodeDataCollection();
268         for (NodeData prop : props) {
269             final String path = getExportPath(node) + "." + prop.getName();
270 
271             String propertyValue = getPropertyString(prop);
272 
273             if (propertyValue != null) {
274                 out.setProperty(path, propertyValue);
275             }
276         }
277     }
278 
279     private static String getExportPath(Content node) {
280         return node.getHandle();
281     }
282 
283     private static String getPropertyString(NodeData prop) {
284         int propType = prop.getType();
285 
286         switch (propType) {
287         case (PropertyType.STRING): {
288             return prop.getString();
289         }
290         case (PropertyType.BOOLEAN): {
291             return convertBooleanToExportString(prop.getBoolean());
292         }
293         case (PropertyType.BINARY): {
294             return convertBinaryToExportString(prop.getValue());
295         }
296         case (PropertyType.PATH): {
297             return prop.getString();
298         }
299         case (PropertyType.DATE): {
300             return convertCalendarToExportString(prop.getDate());
301         }
302         case (PropertyType.LONG): {
303             return "" + prop.getLong();
304         }
305         case (PropertyType.DOUBLE): {
306             return "" + prop.getDouble();
307         }
308         default: {
309             return prop.getString();
310         }
311         }
312     }
313 
314     private static String convertBooleanToExportString(boolean b) {
315         return "boolean:" + (b ? "true" : "false");
316     }
317 
318     private static String convertBinaryToExportString(Value value) {
319         return "binary:" + ConvertUtils.convert(value);
320     }
321 
322     private static String convertCalendarToExportString(Calendar calendar) {
323         return "date:" + ISO8601.format(calendar);
324     }
325 
326     /**
327      * Dumps content starting at the content node out to a string in the format that matches the
328      * import method.
329      */
330     public static String dumpPropertiesToString(Content content, Content.ContentFilter filter) throws Exception {
331         Properties properties = PropertiesImportExport.contentToProperties(content, filter);
332         return dumpPropertiesToString(properties);
333     }
334 
335     public static String dumpPropertiesToString(Properties properties) {
336         final StringBuilder sb = new StringBuilder();
337         final Set<Object> propertyNames = properties.keySet();
338         for (Object propertyKey : propertyNames) {
339             final String name = propertyKey.toString();
340             final String value = properties.getProperty(name);
341             sb.append(name);
342             sb.append("=");
343             sb.append(value);
344             sb.append("\n");
345         }
346         return sb.toString();
347     }
348 
349     private static boolean contains(String s, char ch) {
350         return s.indexOf(ch) > -1;
351     }
352 }