org.flowerplatform.web.git.GitUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.flowerplatform.web.git.GitUtils.java

Source

/* license-start
 * 
 * Copyright (C) 2008 - 2013 Crispico, <http://www.crispico.com/>.
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation version 3.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details, at <http://www.gnu.org/licenses/>.
 * 
 * Contributors:
 *   Crispico - Initial API and implementation
 *
 * license-end
 */
package org.flowerplatform.web.git;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchEvent.Kind;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

import org.apache.commons.io.IOUtils;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.jgit.api.GitCommand;
import org.eclipse.jgit.api.MergeResult;
import org.eclipse.jgit.api.errors.DetachedHeadException;
import org.eclipse.jgit.api.errors.InvalidConfigurationException;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.api.errors.NoHeadException;
import org.eclipse.jgit.errors.TransportException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryCache;
import org.eclipse.jgit.lib.RepositoryCache.FileKey;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.FetchResult;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RemoteRefUpdate;
import org.eclipse.jgit.transport.TrackingRefUpdate;
import org.eclipse.jgit.util.FS;
import org.eclipse.osgi.framework.internal.core.FrameworkProperties;
import org.flowerplatform.common.CommonPlugin;
import org.flowerplatform.common.FlowerProperties;
import org.flowerplatform.communication.CommunicationPlugin;
import org.flowerplatform.communication.stateful_service.NamedLockPool;
import org.flowerplatform.web.entity.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author Cristina Constantienscu
 */
public class GitUtils {

    private static Logger logger = LoggerFactory.getLogger(GitUtils.class);

    public static final String MAIN_REPOSITORY = "main";
    public static final String WORKING_DIRECTORY_PREFIX = "wd_";

    public static final String GIT_REPOSITORIES_NAME = ".git-repositories";

    /**
     * Flower Web Property.
     * @see /META-INF/flower-web.properties
     */
    public static final String GIT_INSTALL_DIR = "git.git-install-dir";

    /**
     * Name of the command file used to create a virtual repository on Windows.
     * @see /META-INF/git/git-new-workdir_win.cmd
     */
    private static final String GIT_NEW_WORKDIR_WIN = "git-new-workdir_win.cmd";

    /**
     * Name of the command file used to create a virtual repository on Linux.
     * @see /META-INF/git/git-new-workdir_linux
     */
    private static final String GIT_NEW_WORKDIR_LINUX = "git-new-workdir_linux.sh";

    static {
        FlowerProperties.AddProperty addProperty = new FlowerProperties.AddProperty(GIT_INSTALL_DIR, "") {
            /**
             * Verify if git.exe exists at given location.
             */
            @Override
            protected String validateProperty(String input) {
                String git = CommonPlugin.getInstance().getFlowerProperties().getProperty(GIT_INSTALL_DIR)
                        + "/cmd/git.exe";
                if (!new File(git).exists()) {
                    return String.format("Git executable wasn't found at '%s'! Please verify '%s' property!", git,
                            GIT_INSTALL_DIR);
                }
                return null;
            }
        }.setInputFromFileMandatory(System.getProperty("os.name").toLowerCase().indexOf("win") >= 0);

        CommonPlugin.getInstance().getFlowerProperties().addProperty(addProperty);

        // verify JavaVM version; it must be >= 1.7
        String jvmVersion = System.getProperty("java.vm.specification.version");
        if (jvmVersion.compareTo("1.7") < 0) {
            logger.error(
                    "Your current JVM version is {}. In order to use Git properly, the JVM version needs to be at least 1.7!",
                    jvmVersion);
        }

        /*
         * Each repository configures this property at creation:
         * core.fileMode
         * If false, the executable bit differences between the index and the working copy are ignored; 
         * useful on broken filesystems like FAT. See git-update-index(1).
         * 
         * The default is true, except git-clone(1) or git-init(1) will probe and set core.fileMode false if appropriate when the repository is created.
         * 
         * We set it always to false, because we don't want to add "execute" permission on files.
         */
        try {
            Class<?> fsPosixJava6 = Class.forName("org.eclipse.jgit.util.FS_POSIX_Java6");
            if (fsPosixJava6.isInstance(FS.DETECTED)) {
                Field field = fsPosixJava6.getDeclaredField("canExecute");
                field.setAccessible(true);

                // remove final modifier from field
                Field modifiersField = Field.class.getDeclaredField("modifiers");
                modifiersField.setAccessible(true);
                modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
                field.set(FS.DETECTED, null);

                field = fsPosixJava6.getDeclaredField("setExecute");
                field.setAccessible(true);
                modifiersField.setAccessible(true);
                modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
                field.set(FS.DETECTED, null);

                field = FS.class.getDeclaredField("DETECTED");
                modifiersField.setAccessible(true);
                modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
                field.set(FS.DETECTED, FS.detect());

            }
        } catch (Exception e) {
            throw new RuntimeException(
                    "Exception thrown while setting 'non-executable' state for git repository files!", e);
        }
    }

    public File getGitRepositoriesFile(File orgFile) {
        return new File(CommonPlugin.getInstance().getWorkspaceRoot(),
                orgFile.getName() + "/" + GIT_REPOSITORIES_NAME + "/");
    }

    public Repository getRepository(File repoFile) {
        File gitDir = getGitDir(repoFile);
        if (gitDir != null) {
            try {
                Repository repository = RepositoryCache.open(FileKey.exact(gitDir, FS.DETECTED));
                return repository;
            } catch (IOException e) {
                // TODO CC: log
            }
        }
        return null;
    }

    public File getMainRepositoryFile(File repoFile, boolean createIfNecessary) {
        File mainRepoFile = new File(repoFile, MAIN_REPOSITORY);
        if (createIfNecessary && !mainRepoFile.exists()) {
            if (!mainRepoFile.mkdirs()) {
                logger.error("Cannot create main repository file for {}", repoFile);
                return null;
            }
        }
        return mainRepoFile;
    }

    public Repository getMainRepository(File repoFile) {
        return getRepository(getMainRepositoryFile(repoFile, false));
    }

    public File getGitDir(File file) {
        if (file.exists()) {
            while (file != null) {
                if (GIT_REPOSITORIES_NAME.equals(file.getName())) {
                    return null;
                }
                if (CommonPlugin.getInstance().getWorkspaceRoot().getName().equals(file.getName())) {
                    return null;
                }
                if (RepositoryCache.FileKey.isGitRepository(file, FS.DETECTED)) {
                    return file;
                } else if (RepositoryCache.FileKey.isGitRepository(new File(file, Constants.DOT_GIT),
                        FS.DETECTED)) {
                    return new File(file, Constants.DOT_GIT);
                }
                file = file.getParentFile();
            }
        }
        return null;
    }

    public boolean isRepository(File file) {
        return getGitDir(file) != null;
    }

    public String getRepositoryName(Repository repo) {
        return repo.getDirectory().getParentFile().getParentFile().getName();
    }

    /**
     * Deletes the given file and its content.
     * <p>
     * If the file is a symbolic link, deletes only the file.
     * Otherwise, deletes also the content from the original location
     * (file.listFiles() returns the children files from original location).
     */
    public void delete(File f) {
        if (f.isDirectory() && !Files.isSymbolicLink(Paths.get(f.toURI()))) {
            for (File c : f.listFiles()) {
                delete(c);
            }
        }
        f.delete();
    }

    public boolean isAuthentificationException(Exception e) {
        TransportException cause = null;
        if (e.getCause() instanceof TransportException) {
            cause = (TransportException) e.getCause();
        } else if (e instanceof TransportException) {
            cause = (TransportException) e;
        }

        if (cause != null && (matchMessage(JGitText.get().notAuthorized, cause.getMessage())
                || cause.getMessage().endsWith("username must not be null.")
                || cause.getMessage().endsWith("host must not be null."))) {
            return true;
        }
        return false;
    }

    /**
     * Creates a string message to be displayed on client side
     * to inform the user about push result operation.
     */
    public String handlePushResult(PushResult pushResult) {
        StringBuilder sb = new StringBuilder();

        sb.append(pushResult.getMessages());
        sb.append("\n");

        for (RemoteRefUpdate rru : pushResult.getRemoteUpdates()) {
            String rm = rru.getRemoteName();
            RemoteRefUpdate.Status status = rru.getStatus();
            sb.append(rm);
            sb.append(" -> ");
            sb.append(status.name());
            sb.append("\n");
        }
        return sb.toString();
    }

    public String handleMergeResult(MergeResult mergeResult) {
        StringBuilder sb = new StringBuilder();
        if (mergeResult == null) {
            return sb.toString();
        }
        sb.append("Status: ");
        sb.append(mergeResult.getMergeStatus());
        sb.append("\n");

        if (mergeResult.getMergedCommits() != null) {
            sb.append("\nMerged commits: ");
            sb.append("\n");
            for (ObjectId id : mergeResult.getMergedCommits()) {
                sb.append(id.getName());
                sb.append("\n");
            }
        }
        if (mergeResult.getCheckoutConflicts() != null) {
            sb.append("\nConflicts: ");
            sb.append("\n");
            for (String conflict : mergeResult.getCheckoutConflicts()) {
                sb.append(conflict);
                sb.append("\n");
            }
        }

        if (mergeResult.getFailingPaths() != null) {
            sb.append("\nFailing paths: ");
            sb.append("\n");
            for (String path : mergeResult.getFailingPaths().keySet()) {
                sb.append(path);
                sb.append(" -> ");
                sb.append(mergeResult.getFailingPaths().get(path).toString());
                sb.append("\n");
            }
        }
        return sb.toString();
    }

    public boolean matchMessage(String pattern, String message) {
        if (message == null) {
            return false;
        }
        int argsNum = 0;
        for (int i = 0; i < pattern.length(); i++) {
            if (pattern.charAt(i) == '{') {
                argsNum++;
            }
        }
        Object[] args = new Object[argsNum];
        for (int i = 0; i < args.length; i++) {
            args[i] = ".*"; //$NON-NLS-1$
        }

        return Pattern.matches(".*" + MessageFormat.format(pattern, args) + ".*", message); //$NON-NLS-1$ //$NON-NLS-2$
    }

    /**
     * Creates a string message to be displayed on client side
     * to inform the user about fetch result operation.
     */
    public String handleFetchResult(FetchResult fetchResult) {
        StringBuilder sb = new StringBuilder();
        if (fetchResult.getTrackingRefUpdates().size() > 0) {
            // handle result
            for (TrackingRefUpdate updateRes : fetchResult.getTrackingRefUpdates()) {
                sb.append(updateRes.getRemoteName());
                sb.append(" -> ");
                sb.append(updateRes.getLocalName());
                sb.append(" ");
                sb.append(
                        updateRes.getOldObjectId() == null ? "" : updateRes.getOldObjectId().abbreviate(7).name());
                sb.append("..");
                sb.append(
                        updateRes.getNewObjectId() == null ? "" : updateRes.getNewObjectId().abbreviate(7).name());
                sb.append(" ");
                Result res = updateRes.getResult();
                switch (res) {
                case NOT_ATTEMPTED:
                case NO_CHANGE:
                case NEW:
                case FORCED:
                case FAST_FORWARD:
                case RENAMED:
                    sb.append("OK.");
                    break;
                case REJECTED:
                    sb.append("Fetch rejected, not a fast-forward.");
                case REJECTED_CURRENT_BRANCH:
                    sb.append("Rejected because trying to delete the current branch.");
                default:
                    sb.append(res.name());
                }
                sb.append("\n");
            }
        } else {
            sb.append("OK.");
        }
        return sb.toString();
    }

    public boolean findProjectFiles(final Collection<File> files, File directory, Set<String> visistedDirs) {
        if (directory == null)
            return false;

        if (directory.getName().equals(Constants.DOT_GIT) && FileKey.isGitRepository(directory, FS.DETECTED)) {
            return false;
        }
        File[] contents = directory.listFiles();
        if (contents == null || contents.length == 0) {
            return false;
        }
        Set<String> directoriesVisited;
        // Initialize recursion guard for recursive symbolic links
        if (visistedDirs == null) {
            directoriesVisited = new HashSet<String>();
            try {
                directoriesVisited.add(directory.getCanonicalPath());
            } catch (IOException exception) {
                return false;
            }
        } else {
            directoriesVisited = visistedDirs;
        }
        // first look for project description files
        String dotProject = IProjectDescription.DESCRIPTION_FILE_NAME;
        for (int i = 0; i < contents.length; i++) {
            File file = contents[i];
            if (file.isFile() && file.getName().equals(dotProject) && !files.contains(file)) {
                files.add(file);
            }
        }
        // recurse into sub-directories (even when project was found above, for nested projects)
        for (int i = 0; i < contents.length; i++) {
            // Skip non-directories
            if (!contents[i].isDirectory()) {
                continue;
            }
            // Skip .metadata folders
            if (contents[i].getName().equals(".metadata")) {
                continue;
            }
            try {
                String canonicalPath = contents[i].getCanonicalPath();
                if (!directoriesVisited.add(canonicalPath)) {
                    // already been here --> do not recurse
                    continue;
                }
            } catch (IOException exception) {
                return false;
            }
            findProjectFiles(files, contents[i], directoriesVisited);
        }
        return true;
    }

    public RevCommit getHeadCommit(Repository repository) {
        RevCommit headCommit = null;
        try {
            ObjectId parentId = repository.resolve(Constants.HEAD);
            if (parentId != null) {
                headCommit = new RevWalk(repository).parseCommit(parentId);
            }
        } catch (IOException e) {
            return null;
        }
        return headCommit;
    }

    @SuppressWarnings("restriction")
    public Object[] getFetchPushUpstreamDataRefSpecAndRemote(Repository repository)
            throws InvalidConfigurationException, NoHeadException, DetachedHeadException {

        String branchName;
        String fullBranch;
        try {
            fullBranch = repository.getFullBranch();
            if (fullBranch == null) {
                throw new NoHeadException(JGitText.get().pullOnRepoWithoutHEADCurrentlyNotSupported);
            }
            if (!fullBranch.startsWith(Constants.R_HEADS)) {
                // we can not pull if HEAD is detached and branch is not
                // specified explicitly
                throw new DetachedHeadException();
            }
            branchName = fullBranch.substring(Constants.R_HEADS.length());
        } catch (IOException e) {
            throw new JGitInternalException(JGitText.get().exceptionCaughtDuringExecutionOfPullCommand, e);
        }
        // get the configured remote for the currently checked out branch
        // stored in configuration key branch.<branch name>.remote
        Config repoConfig = repository.getConfig();
        String remote = repoConfig.getString(ConfigConstants.CONFIG_BRANCH_SECTION, branchName,
                ConfigConstants.CONFIG_KEY_REMOTE);
        if (remote == null) {
            // fall back to default remote
            remote = Constants.DEFAULT_REMOTE_NAME;
        }

        // get the name of the branch in the remote repository
        // stored in configuration key branch.<branch name>.merge
        String remoteBranchName = repoConfig.getString(ConfigConstants.CONFIG_BRANCH_SECTION, branchName,
                ConfigConstants.CONFIG_KEY_MERGE);

        if (remoteBranchName == null) {
            String missingKey = ConfigConstants.CONFIG_BRANCH_SECTION + "." + branchName + "."
                    + ConfigConstants.CONFIG_KEY_MERGE;
            throw new InvalidConfigurationException(
                    MessageFormat.format(JGitText.get().missingConfigurationForKey, missingKey));
        }

        // check if the branch is configured for pull-rebase
        boolean doRebase = repoConfig.getBoolean(ConfigConstants.CONFIG_BRANCH_SECTION, branchName,
                ConfigConstants.CONFIG_KEY_REBASE, false);

        final boolean isRemote = !remote.equals("."); //$NON-NLS-1$
        String remoteUri;
        if (isRemote) {
            remoteUri = repoConfig.getString(ConfigConstants.CONFIG_REMOTE_SECTION, remote,
                    ConfigConstants.CONFIG_KEY_URL);
            if (remoteUri == null) {
                String missingKey = ConfigConstants.CONFIG_REMOTE_SECTION + "." + remote + "."
                        + ConfigConstants.CONFIG_KEY_URL;
                throw new InvalidConfigurationException(
                        MessageFormat.format(JGitText.get().missingConfigurationForKey, missingKey));
            }

            return new Object[] { fullBranch, remoteBranchName, remoteUri, doRebase };
        }
        return null;
    }

    public void listenForChanges(File file) throws IOException {
        Path path = file.toPath();
        if (file.isDirectory()) {
            WatchService ws = path.getFileSystem().newWatchService();
            path.register(ws, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE,
                    StandardWatchEventKinds.ENTRY_MODIFY);
            WatchKey watch = null;
            while (true) {
                System.out.println("Watching directory: " + file.getPath());
                try {
                    watch = ws.take();
                } catch (InterruptedException ex) {
                    System.err.println("Interrupted");
                }
                List<WatchEvent<?>> events = watch.pollEvents();
                if (!watch.reset()) {
                    break;
                }
                for (WatchEvent<?> event : events) {
                    Kind<Path> kind = (Kind<Path>) event.kind();
                    Path context = (Path) event.context();
                    if (kind.equals(StandardWatchEventKinds.OVERFLOW)) {
                        System.out.println("OVERFLOW");
                    } else if (kind.equals(StandardWatchEventKinds.ENTRY_CREATE)) {
                        System.out.println("Created: " + context.getFileName());
                    } else if (kind.equals(StandardWatchEventKinds.ENTRY_DELETE)) {
                        System.out.println("Deleted: " + context.getFileName());
                    } else if (kind.equals(StandardWatchEventKinds.ENTRY_MODIFY)) {
                        System.out.println("Modified: " + context.getFileName());
                    }
                }
            }
        } else {
            System.err.println("Not a directory. Will exit.");
        }
    }

    private NamedLockPool namedLockPool = new NamedLockPool();

    /**
     * This method must be used to set user configuration before running
     * some GIT commands that uses it.
     * 
     * <p>
     * A lock/unlock on repository is done before/after the command is executed
     * because the configuration modifies the same file and this will not be
     * thread safe any more.
     */
    public Object runGitCommandInUserRepoConfig(Repository repo, GitCommand<?> command) throws Exception {
        namedLockPool.lock(repo.getDirectory().getPath());

        try {
            StoredConfig c = repo.getConfig();
            c.load();
            User user = (User) CommunicationPlugin.tlCurrentPrincipal.get().getUser();

            c.setString(ConfigConstants.CONFIG_USER_SECTION, null, ConfigConstants.CONFIG_KEY_NAME, user.getName());
            c.setString(ConfigConstants.CONFIG_USER_SECTION, null, ConfigConstants.CONFIG_KEY_EMAIL,
                    user.getEmail());

            c.save();

            return command.call();
        } catch (Exception e) {
            throw e;
        } finally {
            namedLockPool.unlock(repo.getDirectory().getPath());
        }
    }

    /**
     * Executes the corresponding Windows/Linux script to create a virtual repository.
     */
    @SuppressWarnings("restriction")
    public String run_git_workdir_cmd(String source, String destination) {
        File file = null;
        try {
            String OS = System.getProperty("os.name").toLowerCase();
            boolean isWindows = true;
            if (OS.indexOf("nix") >= 0 || OS.indexOf("nux") >= 0 || OS.indexOf("aix") > 0) {
                isWindows = false;
            } else if (!(OS.indexOf("win") >= 0)) {
                return "git-new-workdir command only supports format for Windows/Linux!";
            }

            String cmdName = isWindows ? GIT_NEW_WORKDIR_WIN : GIT_NEW_WORKDIR_LINUX;
            file = File.createTempFile("git", isWindows ? ".cmd" : ".sh",
                    new File(FrameworkProperties.getProperty("flower.server.tmpdir")));
            InputStream is = getClass().getClassLoader().getResourceAsStream("META-INF/git/" + cmdName);
            OutputStream out = new FileOutputStream(file);
            IOUtils.copy(is, out);

            is.close();
            out.close();

            if (!file.exists()) {
                return String.format("%s wasn't found at '%s'!", cmdName, file.getAbsolutePath());
            }

            file.setExecutable(true);

            List<String> cmd = new ArrayList<String>();
            cmd.add(file.getAbsolutePath());
            cmd.add(source);
            cmd.add(destination);
            if (isWindows) {
                String git = CommonPlugin.getInstance().getFlowerProperties().getProperty(GIT_INSTALL_DIR)
                        + "/cmd/git.exe";
                if (!new File(git).exists()) {
                    return String.format("Git executable wasn't found at '%s'! Please verify '%s' property!", git,
                            GIT_INSTALL_DIR);
                }
                cmd.add(git);
            }

            Process proc = Runtime.getRuntime().exec(cmd.toArray(new String[cmd.size()]));

            if (logger.isDebugEnabled()) {
                // any error message?
                StreamGobbler errorGobbler = new StreamGobbler(proc.getErrorStream(), "ERROR");
                errorGobbler.start();
                // any output?
                StreamGobbler outputGobbler = new StreamGobbler(proc.getInputStream(), "OUTPUT");
                outputGobbler.start();
            }

            // any error???
            int exitVal = proc.waitFor();
            switch (exitVal) {
            case 0:
                return null; // OK
            case 1:
                return String.format("Usage: %s ^<repository^> ^<new_workdir^> %s[^<branch^>]", cmdName,
                        isWindows ? "^<git_exe_location^> " : "");
            case 2:
                return String.format("Directory not found: '%s'!", source);
            case 3:
                return String.format("Not a git repository: '%s'!", source);
            case 4:
                return String.format("'%s' is a bare repository!", source);
            case 5:
                return String.format("Destination directory '%s' already exists!", destination);
            case 6:
                return String.format("Unable to create '%s'!", destination);
            }
        } catch (Exception e) {
            logger.error("Exception thrown while running git-new-workdir command!", e);
            return "Exception thrown while creating working directory!";
        } finally {
            if (file != null) {
                file.delete();
            }
        }
        return null;
    }

    /**
     * Class used to get the output data while executing a runtime process.
     * 
     * @see GitService#run_git_workdir_cmd(String, String)
     */
    class StreamGobbler extends Thread {
        private InputStream is;
        private String type;

        private String message;

        public StreamGobbler(InputStream is, String type) {
            this.is = is;
            this.type = type;
        }

        public String getMessage() {
            return message;
        }

        public void run() {
            try {
                InputStreamReader isr = new InputStreamReader(is);
                BufferedReader br = new BufferedReader(isr);
                String line = null;
                while ((line = br.readLine()) != null) {
                    logger.debug(type + ">" + line);
                }
            } catch (IOException ioe) {
                logger.error("Exception thrown while writing command line text", ioe);
            }
        }
    }

}