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