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.importexport;
35  
36  import info.magnolia.cms.beans.runtime.Document;
37  import info.magnolia.cms.core.Content;
38  import info.magnolia.cms.core.HierarchyManager;
39  import info.magnolia.cms.core.ItemType;
40  import info.magnolia.cms.core.SystemProperty;
41  import info.magnolia.cms.util.ContentUtil;
42  import info.magnolia.cms.util.NodeDataUtil;
43  import info.magnolia.context.MgnlContext;
44  import info.magnolia.importexport.filters.AccesscontrolNodeFilter;
45  import info.magnolia.importexport.filters.ImportXmlRootFilter;
46  import info.magnolia.importexport.filters.MagnoliaV2Filter;
47  import info.magnolia.importexport.filters.MetadataUuidFilter;
48  import info.magnolia.importexport.filters.RemoveMixversionableFilter;
49  import info.magnolia.importexport.filters.VersionFilter;
50  import info.magnolia.importexport.postprocessors.ActivationStatusImportPostProcessor;
51  import info.magnolia.importexport.postprocessors.MetaDataImportPostProcessor;
52  import info.magnolia.jcr.util.NodeUtil;
53  
54  import java.io.File;
55  import java.io.FileInputStream;
56  import java.io.FileNotFoundException;
57  import java.io.FileOutputStream;
58  import java.io.IOException;
59  import java.io.InputStream;
60  import java.io.OutputStream;
61  import java.io.UnsupportedEncodingException;
62  import java.net.URLDecoder;
63  import java.net.URLEncoder;
64  import java.text.MessageFormat;
65  import java.util.Iterator;
66  import java.util.List;
67  import java.util.Properties;
68  import java.util.regex.Matcher;
69  import java.util.regex.Pattern;
70  import java.util.zip.DeflaterOutputStream;
71  import java.util.zip.GZIPInputStream;
72  import java.util.zip.GZIPOutputStream;
73  import java.util.zip.ZipInputStream;
74  import java.util.zip.ZipOutputStream;
75  
76  import javax.jcr.ImportUUIDBehavior;
77  import javax.jcr.Node;
78  import javax.jcr.NodeIterator;
79  import javax.jcr.PathNotFoundException;
80  import javax.jcr.RepositoryException;
81  import javax.jcr.Session;
82  import javax.jcr.Workspace;
83  import javax.xml.transform.Source;
84  import javax.xml.transform.sax.SAXTransformerFactory;
85  import javax.xml.transform.stream.StreamSource;
86  
87  import org.apache.commons.io.IOUtils;
88  import org.apache.commons.lang.StringUtils;
89  import org.apache.xml.serialize.OutputFormat;
90  import org.apache.xml.serialize.XMLSerializer;
91  import org.slf4j.Logger;
92  import org.slf4j.LoggerFactory;
93  import org.xml.sax.ContentHandler;
94  import org.xml.sax.InputSource;
95  import org.xml.sax.SAXException;
96  import org.xml.sax.XMLFilter;
97  import org.xml.sax.XMLReader;
98  import org.xml.sax.helpers.XMLReaderFactory;
99  
100 
101 /**
102  * Utility class for manipulation of XML files (mainly JCR XML).
103  * @author <a href="mailto:niko@macnica.com">Nicolas Modrzyk</a>
104  * @author Oliver Lietz
105  */
106 public class DataTransporter {
107 
108     private static final Pattern DOT_NAME_PATTERN = Pattern.compile("[\\w\\-]*\\.*[\\w\\-]*");
109 
110     private static final int INDENT_VALUE = 2;
111 
112     private static Logger log = LoggerFactory.getLogger(DataTransporter.class.getName());
113 
114     final static int BOOTSTRAP_IMPORT_MODE = ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING;
115 
116     public static final String ZIP = ".zip";
117 
118     public static final String GZ = ".gz";
119 
120     public static final String XML = ".xml";
121 
122     public static final String PROPERTIES = ".properties";
123 
124     public static final String DOT = ".";
125 
126     public static final String SLASH = "/";
127 
128     public static final String UTF8 = "UTF-8";
129 
130     public static final String JCR_ROOT = "jcr:root";
131 
132     /**
133      * Converts a xml document into a file.
134      * @param xmlDocument uploaded file
135      * @param repositoryName selected repository
136      * @param basepath base path in repository
137      * @param keepVersionHistory if <code>false</code> version info will be stripped before importing the document
138      * @param importMode a valid value for ImportUUIDBehavior
139      * @param saveAfterImport
140      * @param createBasepathIfNotExist
141      * @throws IOException
142      * @see ImportUUIDBehavior
143      */
144     public static synchronized void importDocument(Document xmlDocument, String repositoryName, String basepath,
145                                                    boolean keepVersionHistory, int importMode, boolean saveAfterImport,
146                                                    boolean createBasepathIfNotExist)
147             throws IOException {
148         File xmlFile = xmlDocument.getFile();
149         importFile(xmlFile, repositoryName, basepath, keepVersionHistory, importMode, saveAfterImport,
150                 createBasepathIfNotExist);
151     }
152 
153     /**
154      * Creates an <code>InputStream</code> backed by the specified xml file.
155      * @param xmlFile (zipped/gzipped) XML file to import
156      * @param repositoryName selected repository
157      * @param basepath base path in repository
158      * @param keepVersionHistory if <code>false</code> version info will be stripped before importing the document
159      * @param importMode a valid value for ImportUUIDBehavior
160      * @param saveAfterImport
161      * @param createBasepathIfNotExist
162      * @throws IOException
163      * @see ImportUUIDBehavior
164      */
165     public static synchronized void importFile(File xmlFile, String repositoryName, String basepath,
166                                                boolean keepVersionHistory, int importMode, boolean saveAfterImport,
167                                                boolean createBasepathIfNotExist)
168             throws IOException {
169         String name = xmlFile.getAbsolutePath();
170 
171         InputStream xmlStream = getInputStreamForFile(xmlFile);
172         importXmlStream(xmlStream, repositoryName, basepath, name, keepVersionHistory, importMode, saveAfterImport,
173                 createBasepathIfNotExist);
174     }
175 
176     /**
177      * @param xmlFile
178      * @param repositoryName
179      * @throws IOException
180      */
181     public static void executeBootstrapImport(File xmlFile, String repositoryName) throws IOException {
182         String filenameWithoutExt = StringUtils.substringBeforeLast(xmlFile.getName(), DOT);
183         if (filenameWithoutExt.endsWith(XML)) {
184             // if file ends in .xml.gz or .xml.zip
185             // need to keep the .xml to be able to view it after decompression
186             filenameWithoutExt = StringUtils.substringBeforeLast(xmlFile.getName(), DOT);
187         }
188         String pathName = StringUtils.substringAfter(StringUtils.substringBeforeLast(filenameWithoutExt, DOT), DOT);
189 
190         pathName = decodePath(pathName,  UTF8);
191 
192         String basepath = SLASH + StringUtils.replace(pathName, DOT, SLASH);
193 
194         if (xmlFile.getName().endsWith(PROPERTIES)) {
195             Properties properties = new Properties();
196             FileInputStream stream = new FileInputStream(xmlFile);
197             properties.load(stream);
198             stream.close();
199             importProperties(properties, repositoryName);
200         } else {
201             DataTransporter.importFile(xmlFile, repositoryName, basepath, false, BOOTSTRAP_IMPORT_MODE, true, true);
202         }
203     }
204 
205     /**
206      * Deprecated.
207      * @param properties
208      * @param repositoryName
209      * @deprecated since 4.0 - use the PropertiesImportExport class instead.
210      */
211     public static void importProperties(Properties properties, String repositoryName) {
212         for (Iterator iter = properties.keySet().iterator(); iter.hasNext();) {
213             String key = (String) iter.next();
214             String value = (String) properties.get(key);
215 
216             String name = StringUtils.substringAfterLast(key, ".");
217             String path = StringUtils.substringBeforeLast(key, ".").replace('.', '/');
218             Content node = ContentUtil.getContent(repositoryName, path);
219             if (node != null) {
220                 try {
221                     NodeDataUtil.getOrCreate(node, name).setValue(value);
222                     node.save();
223                 }
224                 catch (RepositoryException e) {
225                     log.error("can't set property " + key, e);
226                 }
227             }
228         }
229 
230     }
231 
232     /**
233      * Imports XML stream into repository.
234      * XML is filtered by <code>MagnoliaV2Filter</code>, <code>VersionFilter</code> and <code>ImportXmlRootFilter</code>
235      * if <code>keepVersionHistory</code> is set to <code>false</code>
236      * @param xmlStream XML stream to import
237      * @param repositoryName selected repository
238      * @param basepath base path in repository
239      * @param name (absolute path of <code>File</code>)
240      * @param keepVersionHistory if <code>false</code> version info will be stripped before importing the document
241      * @param importMode a valid value for ImportUUIDBehavior
242      * @param saveAfterImport
243      * @param createBasepathIfNotExist
244      * @throws IOException
245      * @see ImportUUIDBehavior
246      * @see ImportXmlRootFilter
247      * @see VersionFilter
248      * @see MagnoliaV2Filter
249      */
250     public static synchronized void importXmlStream(InputStream xmlStream, String repositoryName, String basepath,
251                                                     String name, boolean keepVersionHistory, int importMode,
252                                                     boolean saveAfterImport, boolean createBasepathIfNotExist)
253             throws IOException {
254         importXmlStream(xmlStream, repositoryName, basepath, name, keepVersionHistory, false, importMode, saveAfterImport, createBasepathIfNotExist);
255 
256     }
257 
258     /**
259      * Imports XML stream into repository.
260      * XML is filtered by <code>MagnoliaV2Filter</code>, <code>VersionFilter</code> and <code>ImportXmlRootFilter</code> if <code>keepVersionHistory</code> is set to <code>false</code>
261      * 
262      * @param xmlStream XML stream to import
263      * @param repositoryName selected repository
264      * @param basepath base path in repository
265      * @param name (absolute path of <code>File</code>)
266      * @param keepVersionHistory if <code>false</code> version info will be stripped before importing the document
267      * @param forceUnpublishState if <code>true</code> then activation state of node will be change to unpublished.
268      * @param importMode a valid value for ImportUUIDBehavior
269      * @param saveAfterImport
270      * @param createBasepathIfNotExist
271      * @throws IOException
272      * @see ImportUUIDBehavior
273      * @see ImportXmlRootFilter
274      * @see VersionFilter
275      * @see MagnoliaV2Filter
276      */
277     public static synchronized void importXmlStream(InputStream xmlStream, String repositoryName, String basepath,
278             String name, boolean keepVersionHistory, boolean forceUnpublishState, int importMode,
279                                                     boolean saveAfterImport, boolean createBasepathIfNotExist)
280             throws IOException {
281 
282         // TODO hopefully this will be fixed with a more useful message with the Bootstrapper refactoring
283         if (xmlStream == null) {
284             throw new IOException("Can't import a null stream into repository: " + repositoryName + ", basepath: " + basepath + ", name: " + name);
285         }
286 
287         HierarchyManager hm = MgnlContext.getHierarchyManager(repositoryName);
288         if (hm == null) {
289             throw new IllegalStateException("Can't import " + name + " since repository " + repositoryName + " does not exist.");
290         }
291         Workspace ws = hm.getWorkspace();
292 
293         if (log.isDebugEnabled()) {
294             log.debug("Importing content into repository: [{}] from: [{}] into path: [{}]",
295                     new Object[]{repositoryName, name, basepath});
296         }
297 
298         if (!hm.isExist(basepath) && createBasepathIfNotExist) {
299             try {
300                 ContentUtil.createPath(hm, basepath, ItemType.CONTENT);
301             }
302             catch (RepositoryException e) {
303                 log.error("can't create path [{}]", basepath);
304             }
305         }
306 
307         Session session = ws.getSession();
308 
309         try {
310 
311             // Collects a list with all nodes at the basepath before import so we can see exactly which nodes were imported afterwards
312             List<Node> nodesBeforeImport = NodeUtil.asList(NodeUtil.asIterable(session.getNode(basepath).getNodes()));
313 
314             if (keepVersionHistory) {
315                 // do not manipulate
316                 session.importXML(basepath, xmlStream, importMode);
317             } else {
318                 // create readers/filters and chain
319                 XMLReader initialReader = XMLReaderFactory.createXMLReader(org.apache.xerces.parsers.SAXParser.class.getName());
320                 try{
321                     initialReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
322                 }catch (SAXException e) {
323                     log.error("could not set parser feature");
324                 }
325 
326 
327                 XMLFilter magnoliaV2Filter = null;
328 
329                 // if stream is from regular file, test for belonging XSL file to apply XSL transformation to XML
330                 if (new File(name).isFile()) {
331                     InputStream xslStream = getXslStreamForXmlFile(new File(name));
332                     if (xslStream != null) {
333                         Source xslSource = new StreamSource(xslStream);
334                         SAXTransformerFactory saxTransformerFactory = (SAXTransformerFactory) SAXTransformerFactory.newInstance();
335                         XMLFilter xslFilter = saxTransformerFactory.newXMLFilter(xslSource);
336                         magnoliaV2Filter = new MagnoliaV2Filter(xslFilter);
337                     }
338                 }
339 
340                 if (magnoliaV2Filter == null) {
341                     magnoliaV2Filter = new MagnoliaV2Filter(initialReader);
342                 }
343 
344                 XMLFilter versionFilter = new VersionFilter(magnoliaV2Filter);
345 
346                 // enable this to strip useless "name" properties from dialogs
347                 // versionFilter = new UselessNameFilter(versionFilter);
348 
349                 // enable this to strip mix:versionable from pre 3.6 xml files
350                 versionFilter = new RemoveMixversionableFilter(versionFilter);
351 
352                 // strip rep:accesscontrol node
353                 versionFilter = new AccesscontrolNodeFilter(versionFilter);
354 
355                 XMLReader finalReader = new ImportXmlRootFilter(versionFilter);
356 
357                 ContentHandler handler = session.getImportContentHandler(basepath, importMode);
358                 finalReader.setContentHandler(handler);
359 
360                 // parse XML, import is done by handler from session
361                 try {
362                     finalReader.parse(new InputSource(xmlStream));
363                 }
364                 finally {
365                     IOUtils.closeQuietly(xmlStream);
366                 }
367 
368                 if (((ImportXmlRootFilter) finalReader).rootNodeFound) {
369                     String path = basepath;
370                     if (!path.endsWith(SLASH)) {
371                         path += SLASH;
372                     }
373 
374                     Node dummyRoot = (Node) session.getItem(path + JCR_ROOT);
375                     for (Iterator iter = dummyRoot.getNodes(); iter.hasNext();) {
376                         Node child = (Node) iter.next();
377                         // move childs to real root
378 
379                         if (session.itemExists(path + child.getName())) {
380                             session.getItem(path + child.getName()).remove();
381                         }
382 
383                         session.move(child.getPath(), path + child.getName());
384                     }
385                     // delete the dummy node
386                     dummyRoot.remove();
387                 }
388 
389                 // Post process all nodes that were imported
390                 NodeIterator nodesAfterImport = session.getNode(basepath).getNodes();
391                 while (nodesAfterImport.hasNext()) {
392                     Node nodeAfterImport = nodesAfterImport.nextNode();
393                     boolean existedBeforeImport = false;
394                     for (Node nodeBeforeImport : nodesBeforeImport) {
395                         if (NodeUtil.isSame(nodeAfterImport, nodeBeforeImport)) {
396                             existedBeforeImport = true;
397                             break;
398                         }
399                     }
400                     if (!existedBeforeImport) {
401                         postProcessAfterImport(nodeAfterImport, forceUnpublishState);
402                     }
403                 }
404             }
405         }
406         catch (Exception e) {
407             throw new RuntimeException("Error importing " + name + ": " + e.getMessage(), e);
408         }
409         finally {
410             IOUtils.closeQuietly(xmlStream);
411         }
412 
413         try {
414             if (saveAfterImport) {
415                 session.save();
416             }
417         }
418         catch (RepositoryException e) {
419             log.error(MessageFormat.format(
420                     "Unable to save changes to the [{0}] repository due to a {1} Exception: {2}.",
421                     new Object[]{repositoryName, e.getClass().getName(), e.getMessage()}), e);
422             throw new IOException(e.getMessage());
423         }
424     }
425 
426     private static void postProcessAfterImport(Node node, boolean forceUnpublishState) throws RepositoryException {
427         try {
428             new MetaDataImportPostProcessor().postProcessNode(node);
429             if (forceUnpublishState) {
430                 new ActivationStatusImportPostProcessor().postProcessNode(node);
431             }
432         } catch (RepositoryException e) {
433             throw new RepositoryException("Failed to post process imported nodes at path " + NodeUtil.getNodePathIfPossible(node) + ": " + e.getMessage(), e);
434         }
435     }
436 
437     /**
438      * @param file
439      * @return XSL stream for Xml file or <code>null</code>
440      */
441     protected static InputStream getXslStreamForXmlFile(File file) {
442         InputStream xslStream = null;
443         String xlsFilename = StringUtils.substringBeforeLast(file.getAbsolutePath(), ".") + ".xsl";
444         File xslFile = new File(xlsFilename);
445         if (xslFile.exists()) {
446             try {
447                 xslStream = new FileInputStream(xslFile);
448                 log.info("XSL file for [" + file.getName() + "] found (" + xslFile.getName() + ")");
449             } catch (FileNotFoundException e) { // should never happen (xslFile.exists())
450                 e.printStackTrace();
451             }
452         }
453         return xslStream;
454     }
455 
456     /**
457      * Creates a stream from the (zipped/gzipped) XML file.
458      * @param xmlFile
459      * @return stream of the file
460      * @throws IOException
461      */
462     private static InputStream getInputStreamForFile(File xmlFile) throws IOException {
463         InputStream xmlStream;
464         // looks like the zip one is buggy. It throws exception when trying to use it
465         if (xmlFile.getName().endsWith(ZIP)) {
466             xmlStream = new ZipInputStream((new FileInputStream(xmlFile)));
467         } else if (xmlFile.getName().endsWith(GZ)) {
468             xmlStream = new GZIPInputStream((new FileInputStream(xmlFile)));
469         } else { // if(fileName.endsWith(XML))
470             xmlStream = new FileInputStream(xmlFile);
471         }
472         return xmlStream;
473     }
474 
475     public static void executeExport(OutputStream baseOutputStream, boolean keepVersionHistory, boolean format,
476                                      Session session, String basepath, String repository, String ext) throws IOException {
477         OutputStream outputStream = baseOutputStream;
478         if (ext.endsWith(ZIP)) {
479             outputStream = new ZipOutputStream(baseOutputStream);
480         } else if (ext.endsWith(GZ)) {
481             outputStream = new GZIPOutputStream(baseOutputStream);
482         }
483 
484         try {
485             if (keepVersionHistory) {
486                 // use exportSystemView in order to preserve property types
487                 // http://issues.apache.org/jira/browse/JCR-115
488                 if (!format) {
489                     session.exportSystemView(basepath, outputStream, false, false);
490                 } else {
491                     parseAndFormat(outputStream, null, repository, basepath, session, false);
492                 }
493             } else {
494                 // use XMLSerializer and a SAXFilter in order to rewrite the
495                 // file
496                 XMLReader reader = new VersionFilter(XMLReaderFactory
497                         .createXMLReader(org.apache.xerces.parsers.SAXParser.class.getName()));
498                 reader = new AccesscontrolNodeFilter(reader);
499                 parseAndFormat(outputStream, reader, repository, basepath, session, false);
500             }
501         }
502         catch (IOException e) {
503             throw new RuntimeException(e);
504         }
505         catch (SAXException e) {
506             throw new RuntimeException(e);
507         }
508         catch (RepositoryException e) {
509             throw new RuntimeException(e);
510         }
511 
512         // finish the stream properly if zip stream
513         // this is not done by the IOUtils
514         if (outputStream instanceof DeflaterOutputStream) {
515             ((DeflaterOutputStream) outputStream).finish();
516         }
517 
518         baseOutputStream.flush();
519         IOUtils.closeQuietly(baseOutputStream);
520     }
521 
522     /**
523      * Exports the content of the repository, and format it if necessary.
524      * @param stream the stream to write the content to
525      * @param reader the reader to use to parse the xml content (so that we can perform filtering), if null instanciate
526      * a default one
527      * @param repository the repository to export
528      * @param basepath the basepath in the repository
529      * @param session the session to use to export the data from the repository
530      * @param noRecurse
531      * @throws IOException
532      * @throws SAXException
533      * @throws RepositoryException
534      * @throws PathNotFoundException
535      */
536     public static void parseAndFormat(OutputStream stream, XMLReader reader, String repository, String basepath,
537                                       Session session, boolean noRecurse)
538             throws IOException, SAXException, PathNotFoundException, RepositoryException {
539 
540         if (reader == null) {
541             reader = XMLReaderFactory.createXMLReader(org.apache.xerces.parsers.SAXParser.class.getName());
542         }
543 
544         // write to a temp file and then re-read it to remove version history
545         File tempFile = File.createTempFile("export-" + repository + session.getUserID(), ".xml");
546         OutputStream fileStream = new FileOutputStream(tempFile);
547 
548         try {
549             session.exportSystemView(basepath, fileStream, false, noRecurse);
550         }
551         finally {
552             IOUtils.closeQuietly(fileStream);
553         }
554 
555         readFormatted(reader, tempFile, stream);
556 
557         if (!tempFile.delete()) {
558             log.warn("Could not delete temporary export file {}", tempFile.getAbsolutePath());
559         }
560     }
561 
562     /**
563      * @param reader
564      * @param inputFile
565      * @param outputStream
566      * @throws FileNotFoundException
567      * @throws IOException
568      * @throws SAXException
569      */
570     protected static void readFormatted(XMLReader reader, File inputFile, OutputStream outputStream)
571             throws FileNotFoundException, IOException, SAXException {
572         InputStream fileInputStream = new FileInputStream(inputFile);
573         readFormatted(reader, fileInputStream, outputStream);
574         IOUtils.closeQuietly(fileInputStream);
575     }
576 
577     /**
578      * @param reader
579      * @param inputStream
580      * @param outputStream
581      * @throws FileNotFoundException
582      * @throws IOException
583      * @throws SAXException
584      */
585     protected static void readFormatted(XMLReader reader, InputStream inputStream, OutputStream outputStream)
586             throws FileNotFoundException, IOException, SAXException {
587 
588         OutputFormat outputFormat = new OutputFormat();
589 
590         outputFormat.setPreserveSpace(false); // this is ok, doesn't affect text nodes??
591         outputFormat.setIndenting(true);
592         outputFormat.setIndent(INDENT_VALUE);
593         outputFormat.setLineWidth(120); // need to be set after setIndenting()!
594 
595         final boolean removeUnwantedNamespaces = !SystemProperty.getBooleanProperty("magnolia.export.keep_extra_namespaces"); // MAGNOLIA-2960
596         MetadataUuidFilter metadataUuidFilter = new MetadataUuidFilter(reader, removeUnwantedNamespaces); // MAGNOLIA-1650
597         metadataUuidFilter.setContentHandler(new XMLSerializer(outputStream, outputFormat));
598         metadataUuidFilter.parse(new InputSource(inputStream));
599 
600         IOUtils.closeQuietly(inputStream);
601     }
602 
603     /**
604      *
605      * @param path path to encode
606      * @param separator "." (dot) or "/", it will be not encoded if found
607      * @param enc charset
608      * @return the path encoded
609      */
610     public static String encodePath(String path, String separator, String enc)
611     {
612         StringBuilder pathEncoded = new StringBuilder();
613         try
614         {
615             if (!StringUtils.contains(path, separator))
616             {
617                 return URLEncoder.encode(path, enc);
618             }
619             for(int i=0; i < path.length(); i++) {
620                 String ch = String.valueOf(path.charAt(i));
621                 if(separator.equals(ch)) {
622                     pathEncoded.append(ch);
623                 } else {
624                     pathEncoded.append(URLEncoder.encode(ch, enc));
625                 }
626             }
627         }
628         catch (UnsupportedEncodingException e)
629         {
630             return path;
631         }
632         return pathEncoded.toString();
633     }
634 
635     /**
636      * decode a path (ex. %D0%9D%D0%B0.%B2%D0%BE%D0%BB%D0%BD)
637      * @param path path to decode
638      * @param enc charset
639      * @return the path decoded
640      */
641     public static String decodePath(String path, String enc)
642     {
643         String pathEncoded = StringUtils.EMPTY;
644         try
645         {
646             pathEncoded = URLDecoder.decode(path, enc);
647         }
648         catch (UnsupportedEncodingException e)
649         {
650             return path;
651         }
652         return pathEncoded;
653     }
654 
655     /**
656      * Prior to 4.5 Magnolia used to produce export xml filenames where the / (slash) separating sub nodes was replaced by a dot.
657      * Since 4.5, Magnolia enables dots in path names, therefore dots which are part of the node name have to be escaped by doubling them.
658      * I.e. given a path like this <code>/foo/bar.baz/test../dir/baz..bar</code>, this method will produce
659      * <code>.foo.bar..baz.test.....dir.baz....bar</code>.
660      */
661     public static String createExportPath(String path) {
662         //TODO if someone is smarter than me (not an impossible thing) and can do this with one single elegant regex, please do it.
663         String newPath = path.replace(".", "..");
664         newPath = newPath.replace("/", ".");
665         return newPath;
666     }
667     /**
668      * The opposite of {@link #createExportPath(String)}.
669      * I.e. given a path like this <code>.foo.bar..baz.test.....dir.baz....bar</code>, this method will produce <code>/foo/bar.baz/test../dir/baz..bar</code>.
670      */
671     public static String revertExportPath(String exportPath) {
672         if(".".equals(exportPath)) {
673             return "/";
674         }
675 
676         //TODO I have a feeling there's a simpler way to achieve our goal.
677         Matcher matcher = DOT_NAME_PATTERN.matcher(exportPath);
678 
679         StringBuilder reversed = new StringBuilder(exportPath.length());
680 
681         while(matcher.find()){
682             String group = matcher.group();
683             int dotsNumber = StringUtils.countMatches(group, ".");
684             if(dotsNumber == 1) {
685                 reversed.append(group.replaceFirst("\\.", "/"));
686             } else {
687                  String dots = StringUtils.substringBeforeLast(group, ".").replace("..", ".");
688                  String name = StringUtils.substringAfterLast(group, ".");
689                  reversed.append(dots);
690                  //if number is odd, the last dot has to be replaced with a slash
691                  if(dotsNumber % 2 != 0) {
692                      reversed.append("/");
693                  }
694                  reversed.append(name);
695             }
696         }
697         return reversed.toString();
698     }
699 
700 }