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