001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://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, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://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.ui.apps.git;
029
030import org.opencms.configuration.CmsConfigurationException;
031import org.opencms.file.CmsObject;
032import org.opencms.importexport.CmsImportExportException;
033import org.opencms.main.CmsException;
034import org.opencms.main.CmsLog;
035import org.opencms.main.OpenCms;
036import org.opencms.module.CmsModule;
037import org.opencms.module.CmsModuleImportExportHandler;
038import org.opencms.module.CmsModuleManager;
039import org.opencms.report.CmsPrintStreamReport;
040import org.opencms.report.I_CmsReport;
041import org.opencms.security.CmsRoleViolationException;
042import org.opencms.util.CmsFileUtil;
043import org.opencms.util.CmsFileUtil.FileWalkState;
044import org.opencms.util.CmsStringUtil;
045import org.opencms.xml.CmsXmlEntityResolver;
046import org.opencms.xml.CmsXmlUtils;
047
048import java.io.File;
049import java.io.FileInputStream;
050import java.io.FileNotFoundException;
051import java.io.FileOutputStream;
052import java.io.IOException;
053import java.io.OutputStream;
054import java.io.PrintStream;
055import java.lang.ProcessBuilder.Redirect;
056import java.nio.file.Paths;
057import java.util.Collection;
058import java.util.Date;
059import java.util.HashSet;
060import java.util.LinkedList;
061import java.util.List;
062import java.util.Set;
063import java.util.zip.ZipEntry;
064import java.util.zip.ZipOutputStream;
065
066import org.apache.commons.collections.Closure;
067import org.apache.commons.logging.Log;
068
069import org.dom4j.Document;
070import org.dom4j.Node;
071
072import com.google.common.collect.HashMultimap;
073import com.google.common.collect.Lists;
074import com.google.common.collect.Multimap;
075import com.google.common.collect.Sets;
076
077/** The class provides methods to automatically export modules from OpenCms and check in the exported,
078 *  unzipped modules into some git repository.
079 *  The feature is only available under Linux at the moment. It uses a shell script.
080 *  Which modules are exported to and checked in to which git repository is configured in the file
081 *  <code>/WEB-INF/git-scripts/module-checkin.sh</code>.
082 *   */
083public class CmsGitCheckin { 
084
085    /** The log file for the git check in. */
086    private static final String DEFAULT_LOGFILE_PATH = OpenCms.getSystemInfo().getWebInfRfsPath() + "logs/git.log";
087    /** The variable under which the export path is set. */
088    /** The default path to the script. */
089    private static final String DEFAULT_RFS_PATH = OpenCms.getSystemInfo().getWebInfRfsPath() + "git-scripts/";
090    /** The default folder for configuration files. */
091    private static final String DEFAULT_CONFIG_FOLDER = DEFAULT_RFS_PATH + "config/";
092    /** The default script file used for the git check in. */
093    private static final String DEFAULT_SCRIPT_FILE = DEFAULT_RFS_PATH + "module-checkin.sh";
094    /** The default configuration file used for the git check in. */
095    private static final String DEFAULT_CONFIG_FILE = DEFAULT_RFS_PATH + "module-checkin.conf";
096    /** Logger instance for this class. */
097    private static final Log LOG = CmsLog.getLog(CmsGitCheckin.class);
098    /** Lock used to prevent simultaneous execution of checkIn method. */
099    private static final Object STATIC_LOCK = new Object();
100
101    /** Flag, indicating if an automatic pull should be performed after commit. */
102    private Boolean m_autoPullAfter;
103    /** Flag, indicating if an automatic pull should be performed first. */
104    private Boolean m_autoPullBefore;
105    /** Flag, indicating if an automatic push should be performed in the end. */
106    private Boolean m_autoPush;
107    /** The checkout flag. */
108    private boolean m_checkout;
109    /** The CMS context. */
110    private CmsObject m_cms;
111    /** The commit message. */
112    private String m_commitMessage;
113    /** Flag, indicating if modules should be exported and unzipped. */
114    private Boolean m_copyAndUnzip;
115
116    /** The commit mode. */
117    private Boolean m_commitMode;
118
119    /** Fetch and reset option. */
120    private boolean m_fetchAndResetBeforeImport;
121
122    /** Stream for the log file. */
123    private PrintStream m_logStream;
124
125    /** The modules that should really be exported and checked in. */
126    private Collection<String> m_modulesToExport;
127
128    /** Flag, indicating if reset on HEAD should be performed. */
129    private boolean m_resetHead;
130
131    /** Flag, indicating if reset on ${origin}/${branch} should be performed. */
132    private boolean m_resetRemoteHead;
133
134    /** Flag, indicating if the lib/ folder of the modules should be deleted before the commit. */
135    private Boolean m_excludeLibs;
136    /** The git user email. */
137    private String m_gitUserEmail;
138    /** The git user name. */
139    private String m_gitUserName;
140    /** Flag, indicating if execution of the script should go on for an unclean repository. */
141    private Boolean m_ignoreUnclean;
142
143    /** The current configuration. */
144    private CmsGitConfiguration m_currentConfiguration;
145    /** The available configurations. */
146    private List<CmsGitConfiguration> m_configurations = new LinkedList<CmsGitConfiguration>();
147
148    /**
149     * Default constructor. Initializing member variables with default values.
150     *
151     * @param cms the CMS context to use
152     */
153    public CmsGitCheckin(CmsObject cms) {
154
155        m_cms = cms;
156        m_configurations = readConfigFiles();
157        for (CmsGitConfiguration config : m_configurations) {
158            if (config.isValid()) {
159                m_currentConfiguration = config;
160                break;
161            }
162        }
163    }
164
165    /**
166     * Creates ZIP file data from the files / subfolders of the given root folder, and sends it to the given stream.<p>
167     *
168     * The stream passed as an argument is closed after the data is written.
169     *
170     * @param root the folder to zip
171     * @param zipOutput the output stream which the zip file data should be written to
172     *
173     * @throws Exception if something goes wrong
174     */
175    public static void zipRfsFolder(final File root, final OutputStream zipOutput) throws Exception {
176
177        final ZipOutputStream zip = new ZipOutputStream(zipOutput);
178        try {
179            CmsFileUtil.walkFileSystem(root, new Closure() {
180
181                @SuppressWarnings("resource")
182                public void execute(Object stateObj) {
183
184                    try {
185                        FileWalkState state = (FileWalkState)stateObj;
186                        for (File file : state.getFiles()) {
187                            String relativePath = Paths.get(root.getAbsolutePath()).relativize(
188                                Paths.get(file.getAbsolutePath())).toString();
189                            ZipEntry entry = new ZipEntry(relativePath);
190                            entry.setTime(file.lastModified());
191                            zip.putNextEntry(entry);
192                            zip.write(CmsFileUtil.readFully(new FileInputStream(file)));
193                            zip.closeEntry();
194                        }
195                    } catch (Exception e) {
196                        throw new RuntimeException(e);
197                    }
198                }
199
200            });
201        } catch (RuntimeException e) {
202            if (e.getCause() instanceof Exception) {
203                throw (Exception)(e.getCause());
204            } else {
205                throw e;
206            }
207
208        }
209        zip.flush();
210        zip.close();
211
212    }
213
214    /** Adds a module to the modules that should be exported.
215     * If called at least once, the explicitly added modules will be exported
216     * instead of the default modules.
217     *
218     * @param moduleName the name of the module to export.
219     */
220    public void addModuleToExport(final String moduleName) {
221
222        if (m_modulesToExport == null) {
223            m_modulesToExport = new HashSet<String>();
224        }
225        m_modulesToExport.add(moduleName);
226    }
227
228    /**
229     * Start export and check in of the selected modules.
230     * @return The exit code of the check in procedure (like a script's exit code).
231     */
232    public int checkIn() {
233
234        try {
235            synchronized (STATIC_LOCK) {
236                m_logStream = new PrintStream(new FileOutputStream(DEFAULT_LOGFILE_PATH, false));
237                CmsObject cms = getCmsObject();
238                if (cms != null) {
239                    return checkInInternal();
240                } else {
241                    m_logStream.println("No CmsObject given. Did you call init() first?");
242                    return -1;
243                }
244            }
245        } catch (FileNotFoundException e) {
246            e.printStackTrace();
247            return -2;
248        }
249    }
250
251    /**
252     * Clears the selected modules.<p>
253     */
254    public void clearModules() {
255
256        if (m_modulesToExport != null) {
257            m_modulesToExport.clear();
258        }
259    }
260
261    /**
262     * Gets the checkout flag.<p>
263     *
264     * @return the checkout flag
265     */
266    public boolean getCheckout() {
267
268        return m_checkout;
269    }
270
271    /**
272     * Gets the CMS context.<p>
273     *
274     * @return the CMS context
275     */
276    public CmsObject getCmsObject() {
277
278        return m_cms;
279    }
280
281    /**
282     * Returns the commitMessage.<p>
283     *
284     * @return the commitMessage
285     */
286    public String getCommitMessage() {
287
288        return m_commitMessage;
289    }
290
291    /** Returns the available configurations.
292     * @return the available configurations.
293     */
294    public Collection<CmsGitConfiguration> getConfigurations() {
295
296        return m_configurations;
297    }
298
299    /**
300     * Returns the currently used configuration.
301     * @return the currently used configuration.
302     */
303    public CmsGitConfiguration getCurrentConfiguration() {
304
305        return m_currentConfiguration;
306    }
307
308    /**
309     * Returns the gitUserEmail.<p>
310     *
311     * @return the gitUserEmail
312     */
313    public String getGitUserEmail() {
314
315        return m_gitUserEmail;
316    }
317
318    /**
319     * Returns the gitUserName.<p>
320     *
321     * @return the gitUserName
322     */
323    public String getGitUserName() {
324
325        return m_gitUserName;
326    }
327
328    /** Returns the collection of all installed modules.
329     *
330     * @return the collection of all installed modules.
331     */
332    public Collection<String> getInstalledModules() {
333
334        return OpenCms.getModuleManager().getModuleNames();
335    }
336
337    /** Returns the path to the log file.
338     * @return the path to the log file.
339     */
340    public String getLogFilePath() {
341
342        return DEFAULT_LOGFILE_PATH;
343    }
344
345    /**
346     * Gets the log text.<p>
347     *
348     * @return the log text
349     */
350    @SuppressWarnings("resource")
351    public String getLogText() {
352
353        try {
354            String logFilePath = getLogFilePath();
355            byte[] logData = CmsFileUtil.readFully(new FileInputStream(logFilePath));
356            return new String(logData, "UTF-8");
357        } catch (IOException e) {
358            return "Error reading log file: " + getLogFilePath();
359        }
360    }
361
362    /**
363     * Returns true if at least one valid configuration is present.
364     * @return true if at least one valid configuration is present.
365     */
366    public boolean hasValidConfiguration() {
367
368        return !m_configurations.isEmpty();
369    }
370
371    /**
372     * Returns true if the 'fetch and reset' flag is set.<p>
373     *
374     * @return true if 'fetch and reset' flag is set
375     */
376    public boolean isFetchAndResetBeforeImport() {
377
378        return m_fetchAndResetBeforeImport;
379    }
380
381    /** Tests if a module is installed.
382     * @param moduleName name of the module to check.
383     * @return Flag, indicating if the module is installed.
384     */
385    public boolean isModuleInstalled(final String moduleName) {
386
387        return (OpenCms.getModuleManager().getModule(moduleName) != null);
388    }
389
390    /**
391     * Sets the checkout flag.<p>
392     *
393     * @param checkout the checkout flag
394     */
395    public void setCheckout(boolean checkout) {
396
397        m_checkout = checkout;
398    }
399
400    /** Setter for the commit mode.
401     * @param autoCommit the commit mode to set.
402     */
403    public void setCommit(final boolean autoCommit) {
404
405        m_commitMode = Boolean.valueOf(autoCommit);
406    }
407
408    /** Setter for the commit message.
409     * @param message the commit message to set.
410     */
411    public void setCommitMessage(final String message) {
412
413        m_commitMessage = message;
414    }
415
416    /** Setter for the copy and unzip mode.
417     * @param copyAndUnzip the copy and unzip mode to set.
418     */
419    public void setCopyAndUnzip(final boolean copyAndUnzip) {
420
421        m_copyAndUnzip = Boolean.valueOf(copyAndUnzip);
422    }
423
424    /**
425     * Sets the current configuration if it is a valid configuration. Otherwise the configuration is not set.
426     * @param configuration the configuration to set.
427     * @return flag, indicating if the configuration is set.
428     */
429    public boolean setCurrentConfiguration(CmsGitConfiguration configuration) {
430
431        if ((null != configuration) && configuration.isValid()) {
432            m_currentConfiguration = configuration;
433            return true;
434        }
435        return false;
436    }
437
438    /** Setter for the exclude libs flag.
439     * @param excludeLibs flag, indicating if the lib/ folder of the modules should be deleted before the commit.
440     */
441    public void setExcludeLibs(final boolean excludeLibs) {
442
443        m_excludeLibs = Boolean.valueOf(excludeLibs);
444    }
445
446    /**
447     * Sets the 'fetch and reset' flag.<p>
448     *
449     * If this flag is set, the script will be used to fetch the remote branch and reset the current branch to the remote state before
450     * trying to import the modules.<p>
451     *
452     * @param fetchAndReset the 'fetch and reset' flag
453     */
454    public void setFetchAndResetBeforeImport(boolean fetchAndReset) {
455
456        m_fetchAndResetBeforeImport = fetchAndReset;
457    }
458
459    /** Setter for the git user email.
460     * @param useremail the git user email to set.
461     */
462    public void setGitUserEmail(final String useremail) {
463
464        m_gitUserEmail = useremail;
465    }
466
467    /** Setter for the git user name.
468     * @param username the git user name to set.
469     */
470    public void setGitUserName(final String username) {
471
472        m_gitUserName = username;
473    }
474
475    /** Setter for the ignore-unclean flag.
476     * @param ignore flag, indicating if an unclean repository should be ignored.
477     */
478    public void setIgnoreUnclean(final boolean ignore) {
479
480        m_ignoreUnclean = Boolean.valueOf(ignore);
481    }
482
483    /** Setter for the pull-after flag.
484     * @param autoPull flag, indicating if a pull should be performed directly after the commit.
485     */
486    public void setPullAfter(final boolean autoPull) {
487
488        m_autoPullAfter = Boolean.valueOf(autoPull);
489    }
490
491    /** Setter for the pull-before flag.
492     * @param autoPull flag, indicating if a pull should be performed first.
493     */
494    public void setPullBefore(final boolean autoPull) {
495
496        m_autoPullBefore = Boolean.valueOf(autoPull);
497    }
498
499    /** Setter for the auto-push flag.
500     * @param autoPush flag, indicating if a push should be performed in the end.
501     */
502    public void setPush(final boolean autoPush) {
503
504        m_autoPush = Boolean.valueOf(autoPush);
505    }
506
507    /** Setter for the reset-head flag.
508     * @param reset flag, indicating if a reset to the HEAD should be performed or not.
509     */
510    public void setResetHead(final boolean reset) {
511
512        m_resetHead = reset;
513    }
514
515    /** Setter for the reset-remote-head flag.
516     * @param reset flag, indicating if a reset to the ${origin}/${branch} should be performed or not.
517     */
518    public void setResetRemoteHead(final boolean reset) {
519
520        m_resetRemoteHead = reset;
521    }
522
523    /**
524     * Adds the configuration from <code>configFile</code> to the {@link Collection} of {@link CmsGitConfiguration},
525     * if a valid configuration is read. Otherwise it logs the failure.
526     * @param configurations Collection of configurations where the new configuration should be added.
527     * @param configFile file to read the new configuration from.
528     */
529    private void addConfigurationIfValid(final Collection<CmsGitConfiguration> configurations, final File configFile) {
530
531        CmsGitConfiguration config = null;
532        try {
533            if (configFile.isFile()) {
534                config = new CmsGitConfiguration(configFile);
535                if (config.isValid()) {
536                    configurations.add(config);
537                } else {
538                    throw new Exception(
539                        "Could not read git configuration file" + config.getConfigurationFile().getAbsolutePath());
540                }
541            }
542        } catch (NullPointerException npe) {
543            LOG.error("Could not read git configuration.", npe);
544        } catch (Exception e) {
545            String file = (null != config)
546                && (null != config.getConfigurationFile())
547                && (null != config.getConfigurationFile().getAbsolutePath())
548                ? config.getConfigurationFile().getAbsolutePath()
549                : "<unknown>";
550            LOG.warn("Trying to read invalid git configuration from " + file + ".", e);
551        }
552    }
553
554    /**
555     * Export modules and check them in. Assumes the log stream already open.
556     * @return exit code of the commit-script.
557     */
558    private int checkInInternal() {
559
560        m_logStream.println("[" + new Date() + "] STARTING Git task");
561        m_logStream.println("=========================");
562        m_logStream.println();
563
564        if (m_checkout) {
565            m_logStream.println("Running checkout script");
566
567        } else if (!(m_resetHead || m_resetRemoteHead)) {
568            m_logStream.println("Exporting relevant modules");
569            m_logStream.println("--------------------------");
570            m_logStream.println();
571
572            exportModules();
573
574            m_logStream.println();
575            m_logStream.println("Calling script to check in the exports");
576            m_logStream.println("--------------------------------------");
577            m_logStream.println();
578
579        } else {
580
581            m_logStream.println();
582            m_logStream.println("Calling script to reset the repository");
583            m_logStream.println("--------------------------------------");
584            m_logStream.println();
585
586        }
587
588        int exitCode = runCommitScript();
589        if (exitCode != 0) {
590            m_logStream.println();
591            m_logStream.println("ERROR: Something went wrong. The script got exitcode " + exitCode + ".");
592            m_logStream.println();
593        }
594        if ((exitCode == 0) && m_checkout) {
595            boolean importOk = importModules();
596            if (!importOk) {
597                return -1;
598            }
599        }
600        m_logStream.println("[" + new Date() + "] FINISHED Git task");
601        m_logStream.println();
602        m_logStream.close();
603
604        return exitCode;
605    }
606
607    /** Returns the command to run by the shell to normally run the checkin script.
608     * @return the command to run by the shell to normally run the checkin script.
609     */
610    private String checkinScriptCommand() {
611
612        String exportModules = "";
613        if ((m_modulesToExport != null) && !m_modulesToExport.isEmpty()) {
614            StringBuffer exportModulesParam = new StringBuffer();
615            for (String moduleName : m_modulesToExport) {
616                exportModulesParam.append(" ").append(moduleName);
617            }
618            exportModulesParam.replace(0, 1, " \"");
619            exportModulesParam.append("\" ");
620            exportModules = " --modules " + exportModulesParam.toString();
621
622        }
623        String commitMessage = "";
624        if (m_commitMessage != null) {
625            commitMessage = " -msg \"" + m_commitMessage.replace("\"", "\\\"") + "\"";
626        }
627        String gitUserName = "";
628        if (m_gitUserName != null) {
629            if (m_gitUserName.trim().isEmpty()) {
630                gitUserName = " --ignore-default-git-user-name";
631            } else {
632                gitUserName = " --git-user-name \"" + m_gitUserName + "\"";
633            }
634        }
635        String gitUserEmail = "";
636        if (m_gitUserEmail != null) {
637            if (m_gitUserEmail.trim().isEmpty()) {
638                gitUserEmail = " --ignore-default-git-user-email";
639            } else {
640                gitUserEmail = " --git-user-email \"" + m_gitUserEmail + "\"";
641            }
642        }
643        String autoPullBefore = "";
644        if (m_autoPullBefore != null) {
645            autoPullBefore = m_autoPullBefore.booleanValue() ? " --pull-before " : " --no-pull-before";
646        }
647        String autoPullAfter = "";
648        if (m_autoPullAfter != null) {
649            autoPullAfter = m_autoPullAfter.booleanValue() ? " --pull-after " : " --no-pull-after";
650        }
651        String autoPush = "";
652        if (m_autoPush != null) {
653            autoPush = m_autoPush.booleanValue() ? " --push " : " --no-push";
654        }
655        String exportFolder = " --export-folder \"" + m_currentConfiguration.getModuleExportPath() + "\"";
656        String exportMode = " --export-mode " + m_currentConfiguration.getExportMode();
657        String excludeLibs = "";
658        if (m_excludeLibs != null) {
659            excludeLibs = m_excludeLibs.booleanValue() ? " --exclude-libs" : " --no-exclude-libs";
660        }
661        String commitMode = "";
662        if (m_commitMode != null) {
663            commitMode = m_commitMode.booleanValue() ? " --commit" : " --no-commit";
664        }
665        String ignoreUncleanMode = "";
666        if (m_ignoreUnclean != null) {
667            ignoreUncleanMode = m_ignoreUnclean.booleanValue() ? " --ignore-unclean" : " --no-ignore-unclean";
668        }
669        String copyAndUnzip = "";
670        if (m_copyAndUnzip != null) {
671            copyAndUnzip = m_copyAndUnzip.booleanValue() ? " --copy-and-unzip" : " --no-copy-and-unzip";
672        }
673
674        String configFilePath = m_currentConfiguration.getFilePath();
675
676        return "\""
677            + DEFAULT_SCRIPT_FILE
678            + "\""
679            + exportModules
680            + commitMessage
681            + gitUserName
682            + gitUserEmail
683            + autoPullBefore
684            + autoPullAfter
685            + autoPush
686            + exportFolder
687            + exportMode
688            + excludeLibs
689            + commitMode
690            + ignoreUncleanMode
691            + copyAndUnzip
692            + " \""
693            + configFilePath
694            + "\"";
695    }
696
697    /** Returns the command to run by the shell to normally run the checkin script.
698     * @return the command to run by the shell to normally run the checkin script.
699     */
700    private String checkoutScriptCommand() {
701
702        String configFilePath = m_currentConfiguration.getFilePath();
703        return "\"" + DEFAULT_SCRIPT_FILE + "\"" + " --checkout " + " \"" + configFilePath + "\"";
704    }
705
706    /**
707     * Export the modules that should be checked in into git.
708     */
709    private void exportModules() {
710
711        // avoid to export modules if unnecessary
712        if (((null != m_copyAndUnzip) && !m_copyAndUnzip.booleanValue())
713            || ((null == m_copyAndUnzip) && !m_currentConfiguration.getDefaultCopyAndUnzip())) {
714            m_logStream.println();
715            m_logStream.println("NOT EXPORTING MODULES - you disabled copy and unzip.");
716            m_logStream.println();
717            return;
718        }
719        CmsModuleManager moduleManager = OpenCms.getModuleManager();
720
721        Collection<String> modulesToExport = ((m_modulesToExport == null) || m_modulesToExport.isEmpty())
722        ? m_currentConfiguration.getConfiguredModules()
723        : m_modulesToExport;
724
725        for (String moduleName : modulesToExport) {
726            CmsModule module = moduleManager.getModule(moduleName);
727            if (module != null) {
728                CmsModuleImportExportHandler handler = CmsModuleImportExportHandler.getExportHandler(
729                    getCmsObject(),
730                    module,
731                    "Git export handler");
732                try {
733                    handler.exportData(
734                        getCmsObject(),
735                        new CmsPrintStreamReport(
736                            m_logStream,
737                            OpenCms.getWorkplaceManager().getWorkplaceLocale(getCmsObject()),
738                            false));
739                } catch (CmsRoleViolationException | CmsConfigurationException | CmsImportExportException e) {
740                    e.printStackTrace(m_logStream);
741                }
742            }
743        }
744    }
745
746    /**
747     * Imports a module from the given zip file.<p>
748     *
749     * @param file the module file to import
750     * @throws CmsException if soemthing goes wrong
751     *
752     * @return true if there were no errors during the import
753     */
754    private boolean importModule(File file) throws CmsException {
755
756        m_logStream.println("Trying to import module from " + file.getAbsolutePath());
757        I_CmsReport report = new CmsPrintStreamReport(
758            m_logStream,
759            OpenCms.getWorkplaceManager().getWorkplaceLocale(getCmsObject()),
760            false);
761        OpenCms.getModuleManager().replaceModule(m_cms, file.getAbsolutePath(), report);
762        file.delete();
763        if (report.hasError() || report.hasWarning()) {
764            m_logStream.println("Import failed, see opencms.log for details");
765            return false;
766        }
767        return true;
768    }
769
770    /**
771     * Imports the selected modules from the git repository.<p>
772     *
773     * @return true if there were no errors during the import
774     */
775    @SuppressWarnings("resource")
776    private boolean importModules() {
777
778        boolean result = true;
779
780        try {
781            m_logStream.println("Checking module dependencies.");
782            Multimap<String, String> dependencies = HashMultimap.create();
783            Set<String> unsortedModules = Sets.newHashSet(m_modulesToExport);
784            for (String module : m_modulesToExport) {
785                String manifestPath = CmsStringUtil.joinPaths(
786                    m_currentConfiguration.getModulesPath(),
787                    module,
788                    m_currentConfiguration.getResourcesSubFolder(),
789                    "manifest.xml");
790                Document doc = CmsXmlUtils.unmarshalHelper(
791                    CmsFileUtil.readFully(new FileInputStream(manifestPath)),
792                    new CmsXmlEntityResolver(null));
793
794                List<?> depNodes = doc.getRootElement().selectNodes("//dependencies/dependency/@name");
795                for (Object nodeObj : depNodes) {
796                    Node node = ((Node)nodeObj);
797                    String dependency = node.getText();
798                    if (m_modulesToExport.contains(dependency)) {
799                        // we can only handle dependencies between selected modules
800                        // and just have to assume that other dependencies are fulfilled
801                        dependencies.put(module, dependency);
802                    }
803                }
804            }
805            List<String> sortedModules = Lists.newArrayList();
806            // if there are no cycles, this loop will find one element on each iteration
807            for (int i = 0; i < m_modulesToExport.size(); i++) {
808                String nextModule = null;
809                for (String key : unsortedModules) {
810                    if (dependencies.get(key).isEmpty()) {
811                        nextModule = key;
812                        break;
813                    }
814                }
815                if (nextModule != null) {
816                    sortedModules.add(nextModule);
817                    unsortedModules.remove(nextModule);
818                    for (String key : Sets.newHashSet(dependencies.keySet())) { // copy key set to avoid concurrent modification exception
819                        dependencies.get(key).remove(nextModule);
820                    }
821                }
822            }
823            m_logStream.println("Modules sorted by dependencies: " + sortedModules);
824            for (String moduleName : sortedModules) {
825                String dir = CmsStringUtil.joinPaths(
826                    m_currentConfiguration.getModulesPath(),
827                    moduleName,
828                    m_currentConfiguration.getResourcesSubFolder());
829                File dirEntry = new File(dir);
830                if (!dirEntry.exists()) {
831                    continue;
832                }
833                try {
834                    m_logStream.println("Creating temp file for module " + moduleName);
835                    File outputFile = File.createTempFile(moduleName + "-", ".zip");
836                    FileOutputStream fos = new FileOutputStream(outputFile);
837                    m_logStream.println("Zipping module structure to " + outputFile.getAbsolutePath());
838                    zipRfsFolder(dirEntry, fos);
839                    result &= importModule(outputFile);
840                    outputFile.delete();
841                } catch (Exception e) {
842                    LOG.error(e.getLocalizedMessage(), e);
843                    e.printStackTrace(m_logStream);
844                    result = false;
845                }
846            }
847        } catch (Exception e) {
848
849            LOG.error(e.getLocalizedMessage(), e);
850            m_logStream.println("Unable to check dependencies for modules, giving up.");
851            e.printStackTrace(m_logStream);
852            result = false;
853        }
854        return result;
855    }
856
857    /**
858     * Read all configuration files.
859     * @return the list with all available configurations
860     */
861    private List<CmsGitConfiguration> readConfigFiles() {
862
863        List<CmsGitConfiguration> configurations = new LinkedList<CmsGitConfiguration>();
864
865        // Default configuration file for backwards compatibility
866        addConfigurationIfValid(configurations, new File(DEFAULT_CONFIG_FILE));
867
868        // All files in the config folder
869        File configFolder = new File(DEFAULT_CONFIG_FOLDER);
870        if (configFolder.isDirectory()) {
871            for (File configFile : configFolder.listFiles()) {
872                addConfigurationIfValid(configurations, configFile);
873            }
874        }
875        return configurations;
876    }
877
878    /** Returns the command to run by the shell to reset to HEAD.
879     * @return the command to run by the shell to reset to HEAD.
880     */
881    private String resetHeadScriptCommand() {
882
883        String configFilePath = m_currentConfiguration.getFilePath();
884
885        return "\"" + DEFAULT_SCRIPT_FILE + "\" --reset-head" + " \"" + configFilePath + "\"";
886    }
887
888    /** Returns the command to run by the shell to reset to ${origin}/${branch}.
889     * @return the command to run by the shell to reset to ${origin}/${branch}.
890     */
891    private String resetRemoteHeadScriptCommand() {
892
893        String configFilePath = m_currentConfiguration.getFilePath();
894
895        return "\"" + DEFAULT_SCRIPT_FILE + "\" --reset-remote-head" + " \"" + configFilePath + "\"";
896    }
897
898    /**
899     * Runs the shell script for committing and optionally pushing the changes in the module.
900     * @return exit code of the script.
901     */
902    private int runCommitScript() {
903
904        if (m_checkout && !m_fetchAndResetBeforeImport) {
905            m_logStream.println("Skipping script....");
906            return 0;
907        }
908        try {
909            m_logStream.flush();
910            String commandParam;
911            if (m_resetRemoteHead) {
912                commandParam = resetRemoteHeadScriptCommand();
913            } else if (m_resetHead) {
914                commandParam = resetHeadScriptCommand();
915            } else if (m_checkout) {
916                commandParam = checkoutScriptCommand();
917            } else {
918                commandParam = checkinScriptCommand();
919            }
920            String[] cmd = {"bash", "-c", commandParam};
921            m_logStream.println("Calling the script as follows:");
922            m_logStream.println();
923            m_logStream.println(cmd[0] + " " + cmd[1] + " " + cmd[2]);
924            ProcessBuilder builder = new ProcessBuilder(cmd);
925            m_logStream.close();
926            m_logStream = null;
927            Redirect redirect = Redirect.appendTo(new File(DEFAULT_LOGFILE_PATH));
928            builder.redirectOutput(redirect);
929            builder.redirectError(redirect);
930            Process scriptProcess = builder.start();
931            int exitCode = scriptProcess.waitFor();
932            scriptProcess.getOutputStream().close();
933            m_logStream = new PrintStream(new FileOutputStream(DEFAULT_LOGFILE_PATH, true));
934            return exitCode;
935        } catch (InterruptedException | IOException e) {
936            e.printStackTrace(m_logStream);
937            return -1;
938        }
939
940    }
941}