/** * This file Copyright (c) 2003-2012 Magnolia International * Ltd. (http://www.magnolia-cms.com). All rights reserved. * * * This file is dual-licensed under both the Magnolia * Network Agreement and the GNU General Public License. * You may elect to use one or the other of these licenses. * * This file is distributed in the hope that it will be * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the * implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. * Redistribution, except as permitted by whichever of the GPL * or MNA you select, is prohibited. * * 1. For the GPL license (GPL), you can redistribute and/or * modify this file under the terms of the GNU General * Public License, Version 3, as published by the Free Software * Foundation. You should have received a copy of the GNU * General Public License, Version 3 along with this program; * if not, write to the Free Software Foundation, Inc., 51 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * 2. For the Magnolia Network Agreement (MNA), this file * and the accompanying materials are made available under the * terms of the MNA which accompanies this distribution, and * is available at http://www.magnolia-cms.com/mna.html * * Any modifications to this file must keep this entire header * intact. * */ package info.magnolia.importexport; import info.magnolia.cms.core.Content; import info.magnolia.cms.core.HierarchyManager; import info.magnolia.cms.core.ItemType; import info.magnolia.cms.core.NodeData; import info.magnolia.cms.util.ContentUtil; import info.magnolia.cms.util.OrderedProperties; import org.apache.commons.beanutils.ConvertUtils; import org.apache.commons.lang.StringUtils; import org.apache.jackrabbit.util.ISO8601; import javax.jcr.PropertyType; import javax.jcr.RepositoryException; import javax.jcr.Value; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Calendar; import java.util.Collection; import java.util.Properties; import java.util.Set; /** * Utility class providing support for properties-like format to import/export jcr data. Useful when * data regularly needs to be bootstrapped, for instance, and the jcr xml format is too cumbersome to maintain. * * TODO : handle conflicts (already existing nodes, properties, what to do with existing properties if we don't create new nodes, ...) * TODO : consolidate syntax * * @author gjoseph * @version $Revision: $ ($Author: $) */ public class PropertiesImportExport { public void createContent(Content root, InputStream propertiesStream) throws IOException, RepositoryException { Properties properties = new OrderedProperties(); properties.load(propertiesStream); properties = keysToInnerFormat(properties); for (Object o : properties.keySet()) { String key = (String) o; String valueStr = properties.getProperty(key); String propertyName = StringUtils.substringAfterLast(key, "."); String path = StringUtils.substringBeforeLast(key, "."); String type = null; if (propertyName.equals("@type")) { type = valueStr; } else if (properties.containsKey(path + ".@type")) { type = properties.getProperty(path + ".@type"); } type = StringUtils.defaultIfEmpty(type, ItemType.CONTENTNODE.getSystemName()); Content c = ContentUtil.createPath(root, path, new ItemType(type)); populateContent(c, propertyName, valueStr); } } /** * Transforms the keys to the following inner notation: <code>some/path/node.prop</code> or <code>some/path/node.@type</code>. */ private Properties keysToInnerFormat(Properties properties) { Properties cleaned = new OrderedProperties(); for (Object o : properties.keySet()) { String orgKey = (String) o; //if this is a node definition (no property) String newKey = orgKey; // make sure we have a dot as a property separator newKey = StringUtils.replace(newKey, "@", ".@"); // avoid double dots newKey = StringUtils.replace(newKey, "..@", ".@"); String propertyName = StringUtils.substringAfterLast(newKey, "."); String keySuffix = StringUtils.substringBeforeLast(newKey, "."); String path = StringUtils.replace(keySuffix, ".", "/"); path = StringUtils.removeStart(path, "/"); // if this is a path (no property) if (StringUtils.isEmpty(propertyName)){ // no value --> is a node if(StringUtils.isEmpty(properties.getProperty(orgKey))) { // make this the type property if not defined otherwise if(!properties.containsKey(orgKey + "@type")){ cleaned.put(path + ".@type", ItemType.CONTENTNODE.getSystemName()); } continue; } propertyName = StringUtils.substringAfterLast(path, "/"); path = StringUtils.substringBeforeLast(path, "/"); } cleaned.put(path + "." + propertyName, properties.get(orgKey)); } return cleaned; } protected void populateContent(Content c, String name, String valueStr) throws RepositoryException { if (StringUtils.isEmpty(name) && StringUtils.isEmpty(valueStr)) { // happens if the input properties file just created a node with no properties return; } if (name.equals("@type")) { // do nothing, this has been taken into account when creating the node. } else if (name.equals("@uuid")) { throw new UnsupportedOperationException("Can't see UUIDs on real node. Use MockUtil if you are using MockContent instances."); } else { Object valueObj = convertNodeDataStringToObject(valueStr); c.setNodeData(name, valueObj); } } protected Object convertNodeDataStringToObject(String valueStr) { if (contains(valueStr, ':')) { final String type = StringUtils.substringBefore(valueStr, ":"); final String value = StringUtils.substringAfter(valueStr, ":"); // there is no beanUtils converter for Calendar if (type.equalsIgnoreCase("date")) { return ISO8601.parse(value); } else if (type.equalsIgnoreCase("binary")) { return new ByteArrayInputStream(value.getBytes()); } else { try { final Class<?> typeCl; if (type.equals("int")) { typeCl = Integer.class; } else { typeCl = Class.forName("java.lang." + StringUtils.capitalize(type)); } return ConvertUtils.convert(value, typeCl); } catch (ClassNotFoundException e) { // possibly a stray :, let's ignore it for now return valueStr; } } } // no type specified, we assume it's a string, no conversion return valueStr; } /** * This method is deprecated, it returns results in a format that does not match * the format that the import method uses (doesn't include @uuid or @type properties) * * It is kept here to support existing test and applications that might break * as a result of these changes (i.e. unit tests that are expecting a specific number of * properties returned, etc) * * @deprecated since 4.3 - use {@link info.magnolia.jcr.util.PropertiesImportExport#toProperties(javax.jcr.Node, info.magnolia.jcr.predicate.AbstractPredicate)} */ @Deprecated public static Properties toProperties(HierarchyManager hm) throws Exception { return toProperties(hm.getRoot()); } /** * @deprecated since 5.0 - use {@link info.magnolia.jcr.util.PropertiesImportExport#toProperties(javax.jcr.Node, info.magnolia.jcr.predicate.AbstractPredicate)} */ public static Properties toProperties(Content rootContent) throws Exception { return toProperties(rootContent, ContentUtil.EXCLUDE_META_DATA_CONTENT_FILTER, true); } /** * @deprecated since 5.0 - use {@link info.magnolia.jcr.util.PropertiesImportExport#toProperties(javax.jcr.Node, info.magnolia.jcr.predicate.AbstractPredicate)} */ public static Properties contentToProperties(HierarchyManager hm) throws Exception { return contentToProperties(hm.getRoot()); } /** * @deprecated since 5.0 - use {@link info.magnolia.jcr.util.PropertiesImportExport#toProperties(javax.jcr.Node, info.magnolia.jcr.predicate.AbstractPredicate)} */ public static Properties contentToProperties(Content rootContent) throws Exception { return toProperties(rootContent, ContentUtil.EXCLUDE_META_DATA_CONTENT_FILTER, false); } /** * @deprecated since 5.0 - use {@link info.magnolia.jcr.util.PropertiesImportExport#toProperties(javax.jcr.Node, info.magnolia.jcr.predicate.AbstractPredicate)} */ public static Properties contentToProperties(Content rootContent, Content.ContentFilter filter) throws Exception { return toProperties(rootContent, filter, false); } /** * This method is private because it includes the boolean "legacymode" filter which * shouldn't be exposed as part of the API because when "legacymode" is removed, it will * force an API change. * * @param rootContent root node to convert into properties * @param contentFilter a content filter to use in selecting what content to export * @param legacyMode if true, will not include @uuid and @type nodes * @return a Properties object representing the content starting at rootContent * @throws Exception * @deprecated since 5.0 - use {@link info.magnolia.jcr.util.PropertiesImportExport#toProperties(javax.jcr.Node, info.magnolia.jcr.predicate.AbstractPredicate)} */ private static Properties toProperties(Content rootContent, Content.ContentFilter contentFilter, final boolean legacyMode) throws Exception { final Properties out = new OrderedProperties(); ContentUtil.visit(rootContent, new ContentUtil.Visitor() { @Override public void visit(Content node) throws Exception { if (!legacyMode) { appendNodeTypeAndUUID(node, out, true); } appendNodeProperties(node, out); } }, contentFilter); return out; } private static void appendNodeTypeAndUUID(Content node, Properties out, final boolean dumpMetaData) throws RepositoryException { String path = getExportPath(node); // we don't need to export the JCR root node. if (path.equals("/jcr:root")) { return; } String nodeTypeName = node.getNodeTypeName(); if (nodeTypeName != null && StringUtils.isNotEmpty(nodeTypeName)) { out.put(path + "@type", nodeTypeName); } String nodeUUID = node.getUUID(); if (nodeUUID != null && StringUtils.isNotEmpty(nodeUUID)) { out.put(path + "@uuid", node.getUUID()); } } private static void addBooleanProeprty(Properties out, String path, boolean prop) { out.put(path, convertBooleanToExportString(prop)); } private static void addDateProperty(Properties out, String path, Calendar date) { if (date != null) { out.put(path, convertCalendarToExportString(date)); } } private static void addStringProperty(Properties out, String path, String stringProperty) { if (StringUtils.isNotEmpty(stringProperty)) { out.put(path, stringProperty); } } public static void appendNodeProperties(Content node, Properties out) { final Collection<NodeData> props = node.getNodeDataCollection(); for (NodeData prop : props) { final String path = getExportPath(node) + "." + prop.getName(); String propertyValue = getPropertyString(prop); if (propertyValue != null) { out.setProperty(path, propertyValue); } } } private static String getExportPath(Content node) { return node.getHandle(); } private static String getPropertyString(NodeData prop) { int propType = prop.getType(); switch (propType) { case (PropertyType.STRING): { return prop.getString(); } case (PropertyType.BOOLEAN): { return convertBooleanToExportString(prop.getBoolean()); } case (PropertyType.BINARY): { return convertBinaryToExportString(prop.getValue()); } case (PropertyType.PATH): { return prop.getString(); } case (PropertyType.DATE): { return convertCalendarToExportString(prop.getDate()); } case (PropertyType.LONG): { return "" + prop.getLong(); } case (PropertyType.DOUBLE): { return "" + prop.getDouble(); } default: { return prop.getString(); } } } private static String convertBooleanToExportString(boolean b) { return "boolean:" + (b ? "true" : "false"); } private static String convertBinaryToExportString(Value value) { return "binary:" + ConvertUtils.convert(value); } private static String convertCalendarToExportString(Calendar calendar) { return "date:" + ISO8601.format(calendar); } /** * Dumps content starting at the content node out to a string in the format that matches the * import method. */ public static String dumpPropertiesToString(Content content, Content.ContentFilter filter) throws Exception { Properties properties = PropertiesImportExport.contentToProperties(content, filter); return dumpPropertiesToString(properties); } public static String dumpPropertiesToString(Properties properties) { final StringBuilder sb = new StringBuilder(); final Set<Object> propertyNames = properties.keySet(); for (Object propertyKey : propertyNames) { final String name = propertyKey.toString(); final String value = properties.getProperty(name); sb.append(name); sb.append("="); sb.append(value); sb.append("\n"); } return sb.toString(); } private static boolean contains(String s, char ch) { return s.indexOf(ch) > -1; } }