View Javadoc

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