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