001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.synchronize;
029
030import org.opencms.db.CmsDbIoException;
031import org.opencms.file.*;
032import org.opencms.file.types.CmsResourceTypeFolder;
033import org.opencms.main.CmsException;
034import org.opencms.main.CmsLog;
035import org.opencms.main.OpenCms;
036import org.opencms.report.I_CmsReport;
037import org.opencms.util.CmsFileUtil;
038
039import java.io.DataOutputStream;
040import java.io.File;
041import java.io.FileOutputStream;
042import java.io.FileReader;
043import java.io.IOException;
044import java.io.LineNumberReader;
045import java.io.PrintWriter;
046import java.util.ArrayList;
047import java.util.HashMap;
048import java.util.Iterator;
049import java.util.List;
050import java.util.StringTokenizer;
051import java.util.regex.Pattern;
052
053import org.apache.commons.logging.Log;
054
055/**
056 * Contains all methods to synchronize the VFS with the "real" FS.<p>
057 *
058 * @since 6.0.0
059 */
060public class CmsSynchronize {
061
062    /** Flag to import a deleted resource in the VFS. */
063    static final int DELETE_VFS = 3;
064
065    /** Flag to export a resource from the VFS to the FS. */
066    static final int EXPORT_VFS = 1;
067
068    /** File name of the synclist file on the server FS. */
069    static final String SYNCLIST_FILENAME = "#synclist.txt";
070
071    /** Flag to import a resource from the FS to the VFS. */
072    static final int UPDATE_VFS = 2;
073
074    /** The log object for this class. */
075    private static final Log LOG = CmsLog.getLog(CmsSynchronize.class);
076
077    /** List to store all file modification interface implementations. */
078    private static List<I_CmsSynchronizeModification> m_synchronizeModifications = new ArrayList<I_CmsSynchronizeModification>();
079
080    /** The CmsObject. */
081    private CmsObject m_cms;
082
083    /** Counter for logging. */
084    private int m_count;
085
086    /** The path in the "real" file system where the resources have to be synchronized to. */
087    private String m_destinationPathInRfs;
088
089    /** Hash map for the new synchronization list of the current sync process. */
090    private HashMap<String, CmsSynchronizeList> m_newSyncList;
091
092    /** The report to write the output to. */
093    private I_CmsReport m_report;
094
095    /** Hash map for the synchronization list of the last sync process. */
096    private HashMap<String, CmsSynchronizeList> m_syncList;
097
098    /**
099     * Creates a new CmsSynchronize object which automatically start the
100     * synchronization process.<p>
101     *
102     * @param cms the current CmsObject
103     * @param settings the synchronization settings to use
104     * @param report the report to write the output to
105     *
106     * @throws CmsException if something goes wrong
107     * @throws CmsSynchronizeException if the synchronization process cannot be started
108     */
109    public CmsSynchronize(CmsObject cms, CmsSynchronizeSettings settings, I_CmsReport report)
110    throws CmsSynchronizeException, CmsException {
111
112        // TODO: initialize the list of file modification interface implementations
113
114        // do the synchronization only if the synchronization folders in the VFS
115        // and the FS are valid
116        if ((settings != null) && (settings.isSyncEnabled())) {
117
118            // create an independent copy of the provided user context
119            m_cms = OpenCms.initCmsObject(cms);
120            // set site to root site
121            m_cms.getRequestContext().setSiteRoot("/");
122
123            m_report = report;
124            m_count = 1;
125
126            // get the destination folder
127            m_destinationPathInRfs = settings.getDestinationPathInRfs();
128
129            // check if target folder exists and is writable
130            File destinationFolder = new File(m_destinationPathInRfs);
131            if (!destinationFolder.exists() || !destinationFolder.isDirectory()) {
132                // destination folder does not exist
133                throw new CmsSynchronizeException(
134                    Messages.get().container(Messages.ERR_RFS_DESTINATION_NOT_THERE_1, m_destinationPathInRfs));
135            }
136            if (!destinationFolder.canWrite()) {
137                // destination folder can't be written to
138                throw new CmsSynchronizeException(
139                    Messages.get().container(Messages.ERR_RFS_DESTINATION_NO_WRITE_1, m_destinationPathInRfs));
140            }
141
142            // create the sync list for this run
143            m_syncList = readSyncList();
144            m_newSyncList = new HashMap<String, CmsSynchronizeList>();
145
146            Iterator<String> i = settings.getSourceListInVfs().iterator();
147            while (i.hasNext()) {
148                // iterate all source folders
149                String sourcePathInVfs = i.next();
150                String destPath = m_destinationPathInRfs + sourcePathInVfs.replace('/', File.separatorChar);
151
152                report.println(
153                    org.opencms.workplace.threads.Messages.get().container(
154                        org.opencms.workplace.threads.Messages.RPT_SYNCHRONIZE_FOLDERS_2,
155                        sourcePathInVfs,
156                        destPath),
157                    I_CmsReport.FORMAT_HEADLINE);
158                // synchronize the VFS and the RFS
159                syncVfsToRfs(sourcePathInVfs);
160            }
161
162            // remove files from the RFS
163            removeFromRfs(m_destinationPathInRfs);
164            i = settings.getSourceListInVfs().iterator();
165
166            while (i.hasNext()) {
167                // add new files from the RFS
168                copyFromRfs(i.next());
169            }
170
171            // write the sync list
172            writeSyncList();
173
174            // free memory
175            m_syncList = null;
176            m_newSyncList = null;
177            m_cms = null;
178        } else {
179            throw new CmsSynchronizeException(Messages.get().container(Messages.ERR_INIT_SYNC_0));
180        }
181    }
182
183    /**
184     * Returns the count.<p>
185     *
186     * @return the count
187     */
188    public int getCount() {
189
190        return m_count;
191    }
192
193    /**
194     * Copys all resources from the FS which are not existing in the VFS yet. <p>
195     *
196     * @param folder the folder in the VFS to be synchronized with the FS
197     * @throws CmsException if something goes wrong
198     */
199    private void copyFromRfs(String folder) throws CmsException {
200
201        // get the corresponding folder in the FS
202        File[] res;
203        File fsFile = getFileInRfs(folder);
204        // first of all, test if this folder existis in the VFS. If not, create it
205        try {
206            m_cms.readFolder(translate(folder), CmsResourceFilter.IGNORE_EXPIRATION);
207        } catch (CmsException e) {
208            // the folder could not be read, so create it
209            String foldername = translate(folder);
210            m_report.print(org.opencms.report.Messages.get().container(
211                org.opencms.report.Messages.RPT_SUCCESSION_1,
212                String.valueOf(m_count++)), I_CmsReport.FORMAT_NOTE);
213            m_report.print(Messages.get().container(Messages.RPT_IMPORT_FOLDER_0), I_CmsReport.FORMAT_NOTE);
214            m_report.print(
215                org.opencms.report.Messages.get().container(
216                    org.opencms.report.Messages.RPT_ARGUMENT_1,
217                    fsFile.getAbsolutePath().replace('\\', '/')));
218            m_report.print(Messages.get().container(Messages.RPT_FROM_FS_TO_0), I_CmsReport.FORMAT_NOTE);
219            m_report.print(
220                org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_ARGUMENT_1, foldername));
221            m_report.print(org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_DOTS_0));
222
223            CmsResource newFolder = m_cms.createResource(foldername, CmsResourceTypeFolder.RESOURCE_TYPE_ID);
224            // now check if there is some external method to be called which
225            // should modify the imported resource in the VFS
226            Iterator<I_CmsSynchronizeModification> i = m_synchronizeModifications.iterator();
227            while (i.hasNext()) {
228                try {
229                    i.next().modifyVfs(m_cms, newFolder, fsFile);
230                } catch (CmsSynchronizeException e1) {
231                    break;
232                }
233            }
234            // we have to read the new resource again, to get the correct timestamp
235            newFolder = m_cms.readFolder(foldername, CmsResourceFilter.IGNORE_EXPIRATION);
236            String resourcename = m_cms.getSitePath(newFolder);
237            // add the folder to the sync list
238            CmsSynchronizeList sync = new CmsSynchronizeList(
239                folder,
240                resourcename,
241                newFolder.getDateLastModified(),
242                fsFile.lastModified());
243            m_newSyncList.put(resourcename, sync);
244
245            m_report.println(
246                org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0),
247                I_CmsReport.FORMAT_OK);
248        }
249        // since the check has been done on folder basis, this must be a folder
250        if (fsFile.isDirectory()) {
251            // get all resources in this folder
252            res = fsFile.listFiles();
253
254            // now loop through all resources
255            for (int i = 0; i < res.length; i++) {
256                if (isExcluded(res[i])) {
257                    continue;
258                }
259                // get the relative filename
260                String resname = res[i].getAbsolutePath();
261                resname = resname.substring(m_destinationPathInRfs.length());
262                // translate the folder separator if necessary
263                resname = resname.replace(File.separatorChar, '/');
264                // now check if this resource was already processed, by looking
265                // up the new sync list
266                if (res[i].isFile()) {
267                    if (!m_newSyncList.containsKey(translate(resname))) {
268                        // this file does not exist in the VFS, so import it
269                        importToVfs(res[i], resname, folder);
270                    }
271                } else {
272                    // do a recursion if the current resource is a folder
273                    copyFromRfs(resname + "/");
274                }
275            }
276        }
277    }
278
279    /**
280     * Creates a new file on the server FS.<p>
281     *
282     * @param newFile the file that has to be created
283     * @throws CmsException if something goes wrong
284     */
285    private void createNewLocalFile(File newFile) throws CmsException {
286
287        if (newFile.exists()) {
288            throw new CmsSynchronizeException(
289                Messages.get().container(Messages.ERR_EXISTENT_FILE_1, newFile.getPath()));
290        }
291        FileOutputStream fOut = null;
292        try {
293            File parentFolder = new File(
294                newFile.getPath().replace('/', File.separatorChar).substring(
295                    0,
296                    newFile.getPath().lastIndexOf(File.separator)));
297            parentFolder.mkdirs();
298            if (parentFolder.exists()) {
299                fOut = new FileOutputStream(newFile);
300            } else {
301                throw new CmsSynchronizeException(
302                    Messages.get().container(Messages.ERR_CREATE_DIR_1, newFile.getPath()));
303            }
304        } catch (IOException e) {
305            throw new CmsSynchronizeException(
306                Messages.get().container(Messages.ERR_CREATE_FILE_1, this.getClass().getName(), newFile.getPath()),
307                e);
308        } finally {
309            if (fOut != null) {
310                try {
311                    fOut.close();
312                } catch (IOException e) {
313                    // ignore
314                }
315            }
316        }
317    }
318
319    /**
320     * Deletes a resource in the VFS and updates the synchronisation lists.<p>
321     *
322     * @param res The resource to be deleted
323     *
324     * @throws CmsException if something goes wrong
325     */
326    private void deleteFromVfs(CmsResource res) throws CmsException {
327
328        String resourcename = m_cms.getSitePath(res);
329
330        m_report.print(
331            org.opencms.report.Messages.get().container(
332                org.opencms.report.Messages.RPT_SUCCESSION_1,
333                String.valueOf(m_count++)),
334            I_CmsReport.FORMAT_NOTE);
335        if (res.isFile()) {
336            m_report.print(Messages.get().container(Messages.RPT_DEL_FILE_0), I_CmsReport.FORMAT_NOTE);
337        } else {
338            m_report.print(Messages.get().container(Messages.RPT_DEL_FOLDER_0), I_CmsReport.FORMAT_NOTE);
339        }
340        m_report.print(
341            org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_ARGUMENT_1, resourcename));
342        m_report.print(org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_DOTS_0));
343
344        // lock the file in the VFS, so that it can be updated
345        m_cms.lockResource(resourcename);
346        m_cms.deleteResource(resourcename, CmsResource.DELETE_PRESERVE_SIBLINGS);
347        // Remove it from the sync list
348        m_syncList.remove(translate(resourcename));
349
350        m_report.println(
351            org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0),
352            I_CmsReport.FORMAT_OK);
353    }
354
355    /**
356     * Exports a resource from the VFS to the FS and updates the
357     * synchronization lists.<p>
358     *
359     * @param res the resource to be exported
360     *
361     * @throws CmsException if something goes wrong
362     */
363    private void exportToRfs(CmsResource res) throws CmsException {
364
365        CmsFile vfsFile;
366        File fsFile;
367        String resourcename;
368        // to get the name of the file in the FS, we must look it up in the
369        // sync list. This is necessary, since the VFS could use a translated
370        // filename.
371        CmsSynchronizeList sync = m_syncList.get(translate(m_cms.getSitePath(res)));
372        // if no entry in the sync list was found, its a new resource and we
373        // can use the name of the VFS resource.
374        if (sync != null) {
375            resourcename = sync.getResName();
376        } else {
377            // otherwise use the original non-translated name
378            resourcename = m_cms.getSitePath(res);
379
380            // the parent folder could contain a translated names as well, so
381            // make a lookup in the sync list to get its original
382            // non-translated name
383            String parent = CmsResource.getParentFolder(resourcename);
384            CmsSynchronizeList parentSync = m_newSyncList.get(parent);
385            // use the non-translated pathname
386            if (parentSync != null) {
387                resourcename = parentSync.getResName() + res.getName();
388            }
389        }
390        if ((res.isFolder()) && (!resourcename.endsWith("/"))) {
391            resourcename += "/";
392        }
393        fsFile = getFileInRfs(resourcename);
394
395        try {
396            // if the resource is marked for deletion, do not export it!
397            if (!res.getState().isDeleted()) {
398                // if its a file, create export the file to the FS
399                m_report.print(
400                    org.opencms.report.Messages.get().container(
401                        org.opencms.report.Messages.RPT_SUCCESSION_1,
402                        String.valueOf(m_count++)),
403                    I_CmsReport.FORMAT_NOTE);
404                if (res.isFile()) {
405
406                    m_report.print(Messages.get().container(Messages.RPT_EXPORT_FILE_0), I_CmsReport.FORMAT_NOTE);
407                    m_report.print(
408                        org.opencms.report.Messages.get().container(
409                            org.opencms.report.Messages.RPT_ARGUMENT_1,
410                            m_cms.getSitePath(res)));
411                    m_report.print(Messages.get().container(Messages.RPT_TO_FS_AS_0), I_CmsReport.FORMAT_NOTE);
412                    m_report.print(
413                        org.opencms.report.Messages.get().container(
414                            org.opencms.report.Messages.RPT_ARGUMENT_1,
415                            fsFile.getAbsolutePath().replace('\\', '/')));
416                    m_report.print(org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_DOTS_0));
417
418                    // create the resource if nescessary
419                    if (!fsFile.exists()) {
420                        createNewLocalFile(fsFile);
421                    }
422                    // write the file content to the FS
423                    vfsFile = m_cms.readFile(m_cms.getSitePath(res), CmsResourceFilter.IGNORE_EXPIRATION);
424                    try {
425                        writeFileByte(vfsFile.getContents(), fsFile);
426                    } catch (IOException e) {
427                        throw new CmsSynchronizeException(Messages.get().container(Messages.ERR_WRITE_FILE_0));
428                    }
429                    // now check if there is some external method to be called
430                    // which should modify the exported resource in the FS
431                    Iterator<I_CmsSynchronizeModification> i = m_synchronizeModifications.iterator();
432                    while (i.hasNext()) {
433                        try {
434                            i.next().modifyFs(m_cms, vfsFile, fsFile);
435                        } catch (CmsSynchronizeException e) {
436                            if (LOG.isWarnEnabled()) {
437                                LOG.warn(
438                                    Messages.get().getBundle().key(
439                                        Messages.LOG_SYNCHRONIZE_EXPORT_FAILED_1,
440                                        res.getRootPath()),
441                                    e);
442                            }
443                            break;
444                        }
445                    }
446                    fsFile.setLastModified(res.getDateLastModified());
447                } else {
448
449                    m_report.print(Messages.get().container(Messages.RPT_EXPORT_FOLDER_0), I_CmsReport.FORMAT_NOTE);
450                    m_report.print(
451                        org.opencms.report.Messages.get().container(
452                            org.opencms.report.Messages.RPT_ARGUMENT_1,
453                            m_cms.getSitePath(res)));
454                    m_report.print(Messages.get().container(Messages.RPT_TO_FS_AS_0), I_CmsReport.FORMAT_NOTE);
455                    m_report.print(
456                        org.opencms.report.Messages.get().container(
457                            org.opencms.report.Messages.RPT_ARGUMENT_1,
458                            fsFile.getAbsolutePath().replace('\\', '/')));
459                    m_report.print(org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_DOTS_0));
460
461                    // its a folder, so create a folder in the FS
462                    fsFile.mkdirs();
463                }
464                // add resource to synchronization list
465                CmsSynchronizeList syncList = new CmsSynchronizeList(
466                    resourcename,
467                    translate(resourcename),
468                    res.getDateLastModified(),
469                    fsFile.lastModified());
470                m_newSyncList.put(translate(resourcename), syncList);
471                // and remove it fomr the old one
472                m_syncList.remove(translate(resourcename));
473                m_report.println(
474                    org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0),
475                    I_CmsReport.FORMAT_OK);
476            }
477            // free mem
478            vfsFile = null;
479
480        } catch (CmsException e) {
481            throw new CmsSynchronizeException(e.getMessageContainer(), e);
482        }
483    }
484
485    /**
486     * Gets the corresponding file to a resource in the VFS. <p>
487     *
488     * @param res path to the resource inside the VFS
489     * @return the corresponding file in the FS
490     */
491    private File getFileInRfs(String res) {
492
493        String path = m_destinationPathInRfs + res.substring(0, res.lastIndexOf("/"));
494        String fileName = res.substring(res.lastIndexOf("/") + 1);
495        return new File(path, fileName);
496    }
497
498    /**
499     * Gets the corresponding filename of the VFS to a resource in the FS. <p>
500     *
501     * @param res the resource in the FS
502     * @return the corresponding filename in the VFS
503     */
504    private String getFilenameInVfs(File res) {
505
506        String resname = res.getAbsolutePath();
507        if (res.isDirectory()) {
508            resname += "/";
509        }
510        // translate the folder separator if necessary
511        resname = resname.replace(File.separatorChar, '/');
512        return resname.substring(m_destinationPathInRfs.length());
513    }
514
515    /**
516     * Imports a new resource from the FS into the VFS and updates the
517     * synchronization lists.<p>
518     *
519     * @param fsFile the file in the FS
520     * @param resName the name of the resource in the VFS
521     * @param folder the folder to import the file into
522     * @throws CmsException if something goes wrong
523     */
524    private void importToVfs(File fsFile, String resName, String folder) throws CmsException {
525
526        try {
527            // get the content of the FS file
528            byte[] content = CmsFileUtil.readFile(fsFile);
529
530            // create the file
531            String filename = translate(fsFile.getName());
532
533            m_report.print(
534                org.opencms.report.Messages.get().container(
535                    org.opencms.report.Messages.RPT_SUCCESSION_1,
536                    String.valueOf(m_count++)),
537                I_CmsReport.FORMAT_NOTE);
538            if (fsFile.isFile()) {
539                m_report.print(Messages.get().container(Messages.RPT_IMPORT_FILE_0), I_CmsReport.FORMAT_NOTE);
540            } else {
541                m_report.print(Messages.get().container(Messages.RPT_IMPORT_FOLDER_0), I_CmsReport.FORMAT_NOTE);
542            }
543
544            m_report.print(
545                org.opencms.report.Messages.get().container(
546                    org.opencms.report.Messages.RPT_ARGUMENT_1,
547                    fsFile.getAbsolutePath().replace('\\', '/')));
548            m_report.print(Messages.get().container(Messages.RPT_FROM_FS_TO_0), I_CmsReport.FORMAT_NOTE);
549
550            // get the file type of the FS file
551            int resType = OpenCms.getResourceManager().getDefaultTypeForName(resName).getTypeId();
552
553            // set property 'Title' for the new resource
554            CmsProperty titleProp = new CmsProperty();
555            titleProp.setName(CmsPropertyDefinition.PROPERTY_TITLE);
556
557            // in the unlikely case that the filename contains a full path, we only take the last part
558            String fsFileName = fsFile.getName();
559            String title = fsFileName.replace("\\", "/");
560
561            int lastSlashIndex = title.lastIndexOf('/');
562            if (lastSlashIndex != -1) {
563                title = title.substring(lastSlashIndex + 1);
564            }
565
566            // remove the file extension from the title
567            int lastDotIndex = title.lastIndexOf('.');
568            if (lastDotIndex != -1) {
569                title = title.substring(0, lastDotIndex);
570            }
571
572            if (OpenCms.getWorkplaceManager().isDefaultPropertiesOnStructure()) {
573                titleProp.setStructureValue(title);
574            } else {
575                titleProp.setResourceValue(title);
576            }
577
578            // list of properties to be set for the new resource
579            List<CmsProperty> properties = new ArrayList<>();
580            properties.add(titleProp);
581
582            CmsResource newFile = m_cms.createResource(
583                translate(folder) + filename,
584                resType,
585                content,
586                properties);
587
588            m_report.print(org.opencms.report.Messages.get().container(
589                org.opencms.report.Messages.RPT_ARGUMENT_1,
590                m_cms.getSitePath(newFile)));
591            m_report.print(org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_DOTS_0));
592
593            // now check if there is some external method to be called which
594            // should modify the imported resource in the VFS
595            Iterator<I_CmsSynchronizeModification> i = m_synchronizeModifications.iterator();
596            while (i.hasNext()) {
597                try {
598                    i.next().modifyVfs(m_cms, newFile, fsFile);
599                } catch (CmsSynchronizeException e) {
600                    break;
601                }
602            }
603            // we have to read the new resource again, to get the correct timestamp
604            m_cms.setDateLastModified(m_cms.getSitePath(newFile), fsFile.lastModified(), false);
605            CmsResource newRes = m_cms.readResource(m_cms.getSitePath(newFile));
606            // add resource to synchronization list
607            CmsSynchronizeList syncList = new CmsSynchronizeList(
608                resName,
609                translate(resName),
610                newRes.getDateLastModified(),
611                fsFile.lastModified());
612            m_newSyncList.put(translate(resName), syncList);
613
614            m_report.println(
615                org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0),
616                I_CmsReport.FORMAT_OK);
617        } catch (IOException e) {
618            throw new CmsSynchronizeException(
619                Messages.get().container(Messages.ERR_READING_FILE_1, fsFile.getName()),
620                e);
621        }
622    }
623
624    /**
625     * Determine if this file is to be excluded.<p>
626     *
627     * @param file the file to check
628     *
629     * @return <code>true</code> if the file should be excluded from synchronization
630     */
631    private boolean isExcluded(File file) {
632
633        ArrayList<Pattern> excludes = OpenCms.getWorkplaceManager().getSynchronizeExcludePatterns();
634        for (Pattern pattern : excludes) {
635            if (pattern.matcher(file.getName()).find()) {
636                m_report.print(
637                    org.opencms.report.Messages.get().container(
638                        org.opencms.report.Messages.RPT_SUCCESSION_1,
639                        String.valueOf(m_count++)),
640                    I_CmsReport.FORMAT_NOTE);
641                m_report.print(Messages.get().container(Messages.RPT_EXCLUDING_0), I_CmsReport.FORMAT_NOTE);
642                m_report.println(
643                    org.opencms.report.Messages.get().container(
644                        org.opencms.report.Messages.RPT_ARGUMENT_1,
645                        file.getAbsolutePath().replace("\\", "/")));
646
647                return true;
648            }
649        }
650        return false;
651    }
652
653    /**
654     * Reads the synchronization list from the last sync process form the file
655     * system and stores the information in a HashMap. <p>
656     *
657     * Filenames are stored as keys, CmsSynchronizeList objects as values.
658     * @return HashMap with synchronization information of the last sync process
659     * @throws CmsException if something goes wrong
660     */
661    private HashMap<String, CmsSynchronizeList> readSyncList() throws CmsException {
662
663        HashMap<String, CmsSynchronizeList> syncList = new HashMap<String, CmsSynchronizeList>();
664
665        // the sync list file in the server fs
666        File syncListFile;
667        syncListFile = new File(m_destinationPathInRfs, SYNCLIST_FILENAME);
668        // try to read the sync list file if it is there
669        if (syncListFile.exists()) {
670            // prepare the streams to write the data
671            FileReader fIn = null;
672            LineNumberReader lIn = null;
673            try {
674                fIn = new FileReader(syncListFile);
675                lIn = new LineNumberReader(fIn);
676                // read one line from the file
677                String line = lIn.readLine();
678                while (line != null) {
679                    line = lIn.readLine();
680                    // extract the data and create a CmsSychroizedList object
681                    //  from it
682                    if (line != null) {
683                        StringTokenizer tok = new StringTokenizer(line, ":");
684                        String resName = tok.nextToken();
685                        String tranResName = tok.nextToken();
686                        long modifiedVfs = Long.valueOf(tok.nextToken()).longValue();
687                        long modifiedFs = Long.valueOf(tok.nextToken()).longValue();
688                        CmsSynchronizeList sync = new CmsSynchronizeList(resName, tranResName, modifiedVfs, modifiedFs);
689                        syncList.put(translate(resName), sync);
690                    }
691                }
692            } catch (IOException e) {
693                throw new CmsSynchronizeException(Messages.get().container(Messages.ERR_READ_SYNC_LIST_0), e);
694            } finally {
695                // close all streams that were used
696                try {
697                    if (lIn != null) {
698                        lIn.close();
699                    }
700                    if (fIn != null) {
701                        fIn.close();
702                    }
703                } catch (IOException e) {
704                    // ignore
705                }
706            }
707        }
708
709        return syncList;
710    }
711
712    /**
713     * Removes all resources in the RFS which are deleted in the VFS.<p>
714     *
715     * @param folder the folder in the FS to check
716     * @throws CmsException if something goes wrong
717     */
718    private void removeFromRfs(String folder) throws CmsException {
719
720        // get the corresponding folder in the FS
721        File[] res;
722        File rfsFile = new File(folder);
723        // get all resources in this folder
724        res = rfsFile.listFiles();
725        // now loop through all resources
726        for (int i = 0; i < res.length; i++) {
727            if (isExcluded(res[i])) {
728                continue;
729            }
730            // get the corresponding name in the VFS
731            String vfsFile = getFilenameInVfs(res[i]);
732            // recurse if it is an directory, we must go depth first to delete
733            // files
734            if (res[i].isDirectory()) {
735                removeFromRfs(res[i].getAbsolutePath());
736            }
737            // now check if this resource is still in the old sync list.
738            // if so, then it does not exist in the FS anymore andm ust be
739            // deleted
740            CmsSynchronizeList sync = m_syncList.get(translate(vfsFile));
741
742            // there is an entry, so delete the resource
743            if (sync != null) {
744
745                m_report.print(
746                    org.opencms.report.Messages.get().container(
747                        org.opencms.report.Messages.RPT_SUCCESSION_1,
748                        String.valueOf(m_count++)),
749                    I_CmsReport.FORMAT_NOTE);
750                if (res[i].isFile()) {
751                    m_report.print(Messages.get().container(Messages.RPT_DEL_FS_FILE_0), I_CmsReport.FORMAT_NOTE);
752                } else {
753                    m_report.print(Messages.get().container(Messages.RPT_DEL_FS_FOLDER_0), I_CmsReport.FORMAT_NOTE);
754                }
755                m_report.print(
756                    org.opencms.report.Messages.get().container(
757                        org.opencms.report.Messages.RPT_ARGUMENT_1,
758                        res[i].getAbsolutePath().replace('\\', '/')));
759                m_report.print(org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_DOTS_0));
760
761                res[i].delete();
762                m_syncList.remove(translate(vfsFile));
763
764                m_report.println(
765                    org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0),
766                    I_CmsReport.FORMAT_OK);
767            }
768        }
769    }
770
771    /**
772     * Updates the synchronization lists if a resource is not used during the
773     * synchronization process.<p>
774     *
775     * @param res the resource whose entry must be updated
776     */
777    private void skipResource(CmsResource res) {
778
779        // add the file to the new sync list...
780        String resname = m_cms.getSitePath(res);
781        CmsSynchronizeList syncList = m_syncList.get(translate(resname));
782        m_newSyncList.put(translate(resname), syncList);
783        // .. and remove it from the old one
784        m_syncList.remove(translate(resname));
785        // update the report
786        m_report.print(org.opencms.report.Messages.get().container(
787            org.opencms.report.Messages.RPT_SUCCESSION_1,
788            String.valueOf(m_count++)), I_CmsReport.FORMAT_NOTE);
789        m_report.print(Messages.get().container(Messages.RPT_SKIPPING_0), I_CmsReport.FORMAT_NOTE);
790        m_report.println(
791            org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_ARGUMENT_1, resname));
792    }
793
794    /**
795     * Synchronizes resources from the VFS to the RFS. <p>
796     *
797     * During the synchronization process, the following actions will be done:<p>
798     *
799     * <ul>
800     * <li>Export modified resources from the VFS to the FS</li>
801     * <li>Update resources in the VFS if the corresponding resource in the FS
802     * has changed</li>
803     * <li>Delete resources in the VFS if the corresponding resource in the FS
804     * has been deleted</li>
805     * </ul>
806     *
807     * @param folder The folder in the VFS to be synchronized with the FS
808     * @throws CmsException if something goes wrong
809     */
810    private void syncVfsToRfs(String folder) throws CmsException {
811
812        int action = 0;
813        //get all resources in the given folder
814        List<CmsResource> resources = m_cms.getResourcesInFolder(folder, CmsResourceFilter.IGNORE_EXPIRATION);
815        // now look through all resources in the folder
816        for (int i = 0; i < resources.size(); i++) {
817            CmsResource res = resources.get(i);
818            // test if the resource is marked as deleted. if so,
819            // do nothing, the corresponding file in the FS will be removed later
820            if (!res.getState().isDeleted()) {
821                // do a recursion if the current resource is a folder
822                if (res.isFolder()) {
823                    // first check if this folder must be synchronized
824                    action = testSyncVfs(res);
825                    // do the correct action according to the test result
826                    if (action == EXPORT_VFS) {
827                        exportToRfs(res);
828                    } else if (action != DELETE_VFS) {
829                        skipResource(res);
830                    }
831                    // recurse into the sub folders. This must be done before
832                    // the folder might be deleted!
833                    syncVfsToRfs(m_cms.getSitePath(res));
834                    if (action == DELETE_VFS) {
835                        deleteFromVfs(res);
836                    }
837                } else {
838                    // if the current resource is a file, check if it has to
839                    // be synchronized
840                    action = testSyncVfs(res);
841                    // do the correct action according to the test result
842                    switch (action) {
843                        case EXPORT_VFS:
844                            exportToRfs(res);
845                            break;
846
847                        case UPDATE_VFS:
848                            updateFromRfs(res);
849                            break;
850
851                        case DELETE_VFS:
852                            deleteFromVfs(res);
853                            break;
854
855                        default:
856                            skipResource(res);
857
858                    }
859                }
860                // free memory
861                res = null;
862            }
863        }
864        //  free memory
865        resources = null;
866    }
867
868    /**
869     * Determines the synchronization status of a VFS resource. <p>
870     *
871     * @param res the VFS resource to check
872     * @return integer value for the action to be done for this VFS resource
873     */
874    private int testSyncVfs(CmsResource res) {
875
876        int action = 0;
877        File fsFile;
878        //data from sync list
879        String resourcename = m_cms.getSitePath(res);
880
881        if (m_syncList.containsKey(translate(resourcename))) {
882            // this resource was already used in a previous synchronization process
883            CmsSynchronizeList sync = m_syncList.get(translate(resourcename));
884            // get the corresponding resource from the FS
885            fsFile = getFileInRfs(sync.getResName());
886            // now check what to do with this resource.
887            // if the modification date is newer than the logged modification
888            // date in the sync list, this resource must be exported too
889            if (res.getDateLastModified() > sync.getModifiedVfs()) {
890                // now check if the resource in the FS is newer, then the
891                // resource from the FS must be imported
892
893                // check if it has been modified since the last sync process
894                // and its newer than the resource in the VFS, only then this
895                // resource must be imported form the FS
896                if ((fsFile.lastModified() > sync.getModifiedFs())
897                    && (fsFile.lastModified() > res.getDateLastModified())) {
898                    action = UPDATE_VFS;
899                } else {
900
901                    action = EXPORT_VFS;
902                }
903            } else {
904                // test if the resource in the FS does not exist anymore.
905                // if so, remove the resource in the VFS
906                if (!fsFile.exists()) {
907                    action = DELETE_VFS;
908                } else {
909                    // now check if the resource in the FS might have changed
910                    if (fsFile.lastModified() > sync.getModifiedFs()) {
911                        action = UPDATE_VFS;
912                    }
913                }
914            }
915        } else {
916            // the resource name was not found in the sync list
917            // this is a new resource
918            action = EXPORT_VFS;
919        }
920        //free memory
921        fsFile = null;
922        return action;
923    }
924
925    /**
926     * Translates the resource name.  <p>
927     *
928     * This is necessary since the server RFS does allow different naming
929     * conventions than the VFS.
930     *
931     * @param name the resource name to be translated
932     * @return the translated resource name
933     */
934    private String translate(String name) {
935
936        String translation = null;
937        // test if an external translation should be used
938        Iterator<I_CmsSynchronizeModification> i = m_synchronizeModifications.iterator();
939        while (i.hasNext()) {
940            try {
941                translation = i.next().translate(m_cms, name);
942            } catch (CmsSynchronizeException e) {
943                if (LOG.isInfoEnabled()) {
944                    LOG.info(Messages.get().getBundle().key(Messages.LOG_EXTERNAL_TRANSLATION_1, name), e);
945                }
946                break;
947            }
948        }
949        // if there was no external method called, do the default OpenCms
950        // RFS-VFS translation
951        if (translation == null) {
952            translation = m_cms.getRequestContext().getFileTranslator().translateResource(name);
953        }
954        return translation;
955    }
956
957    /**
958     * Imports a resource from the FS to the VFS and updates the
959     * synchronization lists.<p>
960     *
961     * @param res the resource to be exported
962     *
963     * @throws CmsSynchronizeException if the resource could not be synchronized
964     * @throws CmsException if something goes wrong
965     */
966    private void updateFromRfs(CmsResource res) throws CmsSynchronizeException, CmsException {
967
968        CmsFile vfsFile;
969        // to get the name of the file in the FS, we must look it up in the
970        // sync list. This is necessary, since the VFS could use a translated
971        // filename.
972        String resourcename = m_cms.getSitePath(res);
973        CmsSynchronizeList sync = m_syncList.get(translate(resourcename));
974        File fsFile = getFileInRfs(sync.getResName());
975
976        m_report.print(
977            org.opencms.report.Messages.get().container(
978                org.opencms.report.Messages.RPT_SUCCESSION_1,
979                String.valueOf(m_count++)),
980            I_CmsReport.FORMAT_NOTE);
981        m_report.print(Messages.get().container(Messages.RPT_UPDATE_FILE_0), I_CmsReport.FORMAT_NOTE);
982        m_report.print(
983            org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_ARGUMENT_1, resourcename));
984        m_report.print(org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_DOTS_0));
985
986        // lock the file in the VFS, so that it can be updated
987        m_cms.lockResource(resourcename);
988        // read the file in the VFS
989        vfsFile = m_cms.readFile(resourcename, CmsResourceFilter.IGNORE_EXPIRATION);
990        // import the content from the FS
991        try {
992            vfsFile.setContents(CmsFileUtil.readFile(fsFile));
993        } catch (IOException e) {
994            throw new CmsSynchronizeException(Messages.get().container(Messages.ERR_IMPORT_1, fsFile.getName()));
995        }
996        m_cms.writeFile(vfsFile);
997        // now check if there is some external method to be called which
998        // should modify
999        // the updated resource in the VFS
1000        Iterator<I_CmsSynchronizeModification> i = m_synchronizeModifications.iterator();
1001        while (i.hasNext()) {
1002            try {
1003                i.next().modifyVfs(m_cms, vfsFile, fsFile);
1004            } catch (CmsSynchronizeException e) {
1005                if (LOG.isInfoEnabled()) {
1006                    LOG.info(
1007                        Messages.get().getBundle().key(Messages.LOG_SYNCHRONIZE_UPDATE_FAILED_1, res.getRootPath()),
1008                        e);
1009                }
1010                break;
1011            }
1012        }
1013        // everything is done now, so unlock the resource
1014        // read the resource again, necessary to get the actual timestamp
1015        m_cms.setDateLastModified(resourcename, fsFile.lastModified(), false);
1016        res = m_cms.readResource(resourcename, CmsResourceFilter.IGNORE_EXPIRATION);
1017
1018        //add resource to synchronization list
1019        CmsSynchronizeList syncList = new CmsSynchronizeList(
1020            sync.getResName(),
1021            translate(resourcename),
1022            res.getDateLastModified(),
1023            fsFile.lastModified());
1024        m_newSyncList.put(translate(resourcename), syncList);
1025        // and remove it from the old one
1026        m_syncList.remove(translate(resourcename));
1027        vfsFile = null;
1028
1029        m_report.println(
1030            org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0),
1031            I_CmsReport.FORMAT_OK);
1032    }
1033
1034    /**
1035     * This writes the byte content of a resource to the file on the server
1036     * file system.<p>
1037     *
1038     * @param content the content of the file in the VFS
1039     * @param file the file in SFS that has to be updated with content
1040     *
1041     * @throws IOException if something goes wrong
1042     */
1043    private void writeFileByte(byte[] content, File file) throws IOException {
1044
1045        FileOutputStream fOut = null;
1046        DataOutputStream dOut = null;
1047        try {
1048            // write the content to the file in server filesystem
1049            fOut = new FileOutputStream(file);
1050            dOut = new DataOutputStream(fOut);
1051            dOut.write(content);
1052            dOut.flush();
1053        } catch (IOException e) {
1054            throw e;
1055        } finally {
1056            try {
1057                if (fOut != null) {
1058                    fOut.close();
1059                }
1060            } catch (IOException e) {
1061                // ignore
1062            }
1063            try {
1064                if (dOut != null) {
1065                    dOut.close();
1066                }
1067            } catch (IOException e) {
1068                // ignore
1069            }
1070        }
1071    }
1072
1073    /**
1074     * Writes the synchronization list of the current sync process to the
1075     * server file system. <p>
1076     *
1077     * The file can be found in the synchronization folder
1078     * @throws CmsException if something goes wrong
1079     */
1080    private void writeSyncList() throws CmsException {
1081
1082        // the sync list file in the server file system
1083        File syncListFile;
1084        syncListFile = new File(m_destinationPathInRfs, SYNCLIST_FILENAME);
1085
1086        // prepare the streams to write the data
1087        FileOutputStream fOut = null;
1088        PrintWriter pOut = null;
1089        try {
1090            fOut = new FileOutputStream(syncListFile);
1091            pOut = new PrintWriter(fOut);
1092            pOut.println(CmsSynchronizeList.getFormatDescription());
1093
1094            // get all keys from the hash map and make an iterator on it
1095            Iterator<CmsSynchronizeList> values = m_newSyncList.values().iterator();
1096            // loop through all values and write them to the sync list file in
1097            // a human readable format
1098            while (values.hasNext()) {
1099                CmsSynchronizeList sync = values.next();
1100                //fOut.write(sync.toString().getBytes());
1101                pOut.println(sync.toString());
1102            }
1103        } catch (IOException e) {
1104            throw new CmsDbIoException(Messages.get().container(Messages.ERR_IO_WRITE_SYNCLIST_0), e);
1105        } finally {
1106            // close all streams that were used
1107            try {
1108                if (pOut != null) {
1109                    pOut.flush();
1110                    pOut.close();
1111                }
1112                if (fOut != null) {
1113                    fOut.flush();
1114                    fOut.close();
1115                }
1116            } catch (IOException e) {
1117                // ignore
1118            }
1119        }
1120    }
1121}