/*___INFO__MARK_BEGIN__*/
/*************************************************************************
 *
 *  The Contents of this file are made available subject to the terms of
 *  the Sun Industry Standards Source License Version 1.2
 *
 *  Sun Microsystems Inc., March, 2001
 *
 *
 *  Sun Industry Standards Source License Version 1.2
 *  =================================================
 *  The contents of this file are subject to the Sun Industry Standards
 *  Source License Version 1.2 (the "License"); You may not use this file
 *  except in compliance with the License. You may obtain a copy of the
 *  License at http://gridengine.sunsource.net/Gridengine_SISSL_license.html
 *
 *  Software provided under this License is provided on an "AS IS" basis,
 *  WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
 *  WITHOUT LIMITATION, WARRANTIES THAT THE SOFTWARE IS FREE OF DEFECTS,
 *  MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE, OR NON-INFRINGING.
 *  See the License for the specific provisions governing your rights and
 *  obligations concerning the Software.
 *
 *   The Initial Developer of the Original Code is: Sun Microsystems, Inc.
 *
 *   Copyright: 2001 by Sun Microsystems, Inc.
 *
 *   All Rights Reserved.
 *
 ************************************************************************/
/*___INFO__MARK_END__*/
package com.sun.grid.ca;

import com.sun.grid.util.expect.Expect;
import com.sun.grid.util.expect.ExpectBuffer;
import com.sun.grid.util.expect.ExpectHandler;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.RandomAccessFile;
import java.io.StringWriter;
import java.io.Writer;
import java.nio.channels.FileLock;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Default implementation of the <code>GridCA</ca>
 *
 * Uses the sge_ca script which is delivered with gridengine to perform
 * actions on the gridengine ca.
 *
 */
public class GridCAImpl implements GridCA {
    
    private static final Logger LOGGER = Logger.getLogger(GridCAImpl.class.getName(), GridCAConstants.BUNDLE);
    
    private GridCAConfiguration config;
    
    /**
     * Create a new instance of <code>GridCAImp</code>
     * @param config the configuration
     * @throws com.sun.grid.ca.GridCAException if the configuration is not valid
     */
    public GridCAImpl(GridCAConfiguration config) throws GridCAException {
        config.validate();
        this.config = config;
    }
  
    protected Expect createProcess() {
        Expect ret = new Expect();
        ret.command().add(config.getSgeCaScript().getAbsolutePath());
        ret.command().add("-catop");
        ret.command().add(config.getCaTop().getAbsolutePath());
        ret.command().add("-calocaltop");
        ret.command().add(config.getCaLocalTop().getAbsolutePath());
        ret.command().add("-cahost");
        ret.command().add(config.getCaHost());
        ret.command().add("-adminuser");
        ret.command().add(config.getAdminUser());
        ret.command().add("-nosge");
        ret.command().add("-days");
        ret.command().add(Integer.toString(config.getDaysValid()));
        return ret;
    }
    
    /**
     * Initialize the gridengine ca.
     *
     * @param  params  parmeters for the CA
     * @throws com.sun.grid.ca.GridCAException 
     */
    public void init(InitCAParameters params) throws GridCAException {
        LOGGER.entering("GridCAImpl", "init");

        params.validate();
        
        File autoFile = createTempFile("auto", ".conf");
        try {
            try {
                PrintWriter pw = new PrintWriter(new FileWriter(autoFile));

                pw.print("CSP_COUNTRY_CODE=\"");
                pw.print(params.getCountry());
                pw.println("\"");
                pw.print("CSP_STATE=\"");
                pw.print(params.getState());
                pw.println("\"");
                pw.print("CSP_LOCATION=\"");
                pw.print(params.getLocation());
                pw.println("\"");
                pw.print("CSP_ORGA=\"");
                pw.print(params.getOrganization());
                pw.println("\"");
                pw.print("CSP_ORGA_UNIT=\"");
                pw.print(params.getOrganizationUnit());
                pw.println("\"");
                pw.print("CSP_MAIL_ADDRESS=\"");
                pw.print(params.getAdminEmailAddress());
                pw.println("\"");
                pw.close();
                
                LOGGER.log(Level.FINE, "gridCAImpl.autoFileWritten");
            } catch(IOException ioe) {
                throw RB.newGridCAException(ioe, "gridCAImpl.error.autoFile",
                                            ioe.getLocalizedMessage());
            }
            
            Expect pb = createProcess();
            pb.command().add("-auto");
            pb.command().add(autoFile.getAbsolutePath());
            pb.command().add("-init");
            
            execute(pb, false);
            
            LOGGER.log(Level.FINE, "gridCAImpl.initSuccess");

        } finally {
            if(autoFile != null) {
                autoFile.delete();
            }
        }
    }
    
    protected File getLocalUserDir(String username) {
        return new File(config.getCaLocalTop(), "userkeys" + File.separatorChar + username);
    }
    
    protected File getCertFileForUser(String username) {
        return new File(getLocalUserDir(username), "cert.pem");
    }

    protected File getLocalDaemonDir(String daemon) {
        return new File(config.getCaLocalTop(), "daemons" + File.separatorChar + daemon);
    }
    
    protected File getCertFileForDaemon(String daemon) {
        return new File(getLocalDaemonDir(daemon), "cert.pem");
    }


    
    protected void execute(Expect pb)  throws GridCAException {
        execute(pb, true);
    } 
    
    protected void execute(Expect pb, boolean setLock) throws GridCAException {
        LOGGER.entering("GridCAImpl", "execute");

        // Since the sge_ca infrastructure can not be shared
        // between processes we have to create a file lock
        File lockFile = null;
        RandomAccessFile raf = null;
        FileLock lock = null;
        if(setLock) {
            lockFile = new File(config.getCaLocalTop(), "lock");
            
            try {
                raf = new RandomAccessFile(lockFile, "rw");
            } catch (IOException ex) {
                throw RB.newGridCAException(ex, "gridCAImpl.error.lockFileNotFound",
                        new Object [] { lockFile, ex.getLocalizedMessage() });
            }


            
            try {
                lock = raf.getChannel().lock();
            } catch (IOException ex) {
                throw RB.newGridCAException(ex, "gridCAImpl.error.lock", 
                          new Object [] { lockFile, ex.getLocalizedMessage() });
            }
        }        
        try {
            ErrorHandler eh = new ErrorHandler();
            pb.add(eh);
            int res;
            try {
                res = pb.exec(60 * 1000);
            } catch (IOException ex) {
                throw RB.newGridCAException(ex, "gridCAImpl.sge_ca.ioError",
                                            ex.getLocalizedMessage());
            } catch (InterruptedException ex) {
                throw RB.newGridCAException("gridCAImpl.sge_ca.interrupt");
            }
            if(res != 0) {
                String error = eh.getError();
                if(error == null || error.length() == 0) {
                    throw RB.newGridCAException("gridCAImpl.sge_ca.unknownError",
                                                new Integer(res));
                } else {
                    throw RB.newGridCAException("gridCAImpl.sge_ca.error",
                                                new Object [] { new Integer(res), error });
                }
            }
        } finally {
            if (setLock) {
                try {
                    lock.release();
                } catch (IOException ex) {
                    // Ingore
                }
                try {
                    raf.close();
                } catch (IOException ex) {
                    // Ingore
                }
            }
        }
        LOGGER.exiting("GridCAImpl", "execute");
    }
    
    class ErrorHandler implements ExpectHandler {
        
        private String error;
        
        public void handle(Expect expect, ExpectBuffer buffer) throws IOException {
            if(getError() == null) {
                error = buffer.consumeLine("Error:");
            }
        }

        public String getError() {
            return error;
        }
    }
    
    /**
     *  Create private key and certificate for a user.
     *
     *  @param username  name of the user
     *  @param gecos     gecos field of the user
     *  @param email     email address of the user
     *  @deprecated the gecos field is no longer used, use @{link #createUser(String,String)} instead
     *  @throws GridCAException if the creation of the private key or the certificate fails
     */
    public void createUser(String username, String gecos, String email) throws GridCAException {
        LOGGER.entering("GridCAImpl", "createUser");
        createUser(username, email);
        LOGGER.exiting("GridCAImpl", "createUser");
    }
    /**
     *  Create private key and certificate for a user.
     *
     *  @param username  name of the user
     *  @param email     email address of the user
     *  @throws GridCAException if the creation of the private key or the certificate fails
     */
    public void createUser(String username, String email) throws GridCAException {
        LOGGER.entering("GridCAImpl", "createUser");
        Expect pb = createProcess();
        pb.command().add("-user");
        pb.command().add(username + ":" + username + ":" + email);

        execute(pb);
        LOGGER.exiting("GridCAImpl", "createUser");
    }
    
    
    /**
     * Create private key and certificate for a sdm daemon.
     *
     * @param daemon name of the daemon
     * @param user   username of the daemon (owner of the process)
     * @param email  email address of the process owner
     * @throws com.sun.grid.ca.GridCAException if the create of the daemon failed
     */
    public void createDaemon(String daemon, String user, String email) throws GridCAException {
        LOGGER.entering("GridCAImpl", "createDaemon");
        Expect pb = createProcess();
        pb.command().add("-sdm_daemon");
        pb.command().add(user + ":" + daemon + ":" + email);

        execute(pb);
        
        LOGGER.exiting("GridCAImpl", "createDaemon");
    }
    
    
    
    private X509Certificate readCertificate(File certFile) throws GridCAException {
        LOGGER.entering("GridCAImpl", "readCertificate");
        try {
            if(!certFile.exists()) {
                throw RB.newGridCAException("gridCAImpl.error.certFileNotFound",
                                            certFile);
            }
            if(!certFile.canRead()) {
                throw RB.newGridCAException("gridCAImpl.error.certFileReadable",
                                            certFile);
            }
            
            FileInputStream in = new FileInputStream(certFile);
            try {
                LOGGER.exiting("GridCAImpl", "readCertificate");
                return (X509Certificate)CertificateFactory.getInstance("X.509").generateCertificate(in);
            } finally {
                in.close();
            }
        } catch(IOException ioe) {
            throw RB.newGridCAException(ioe, "gridCAImpl.error.certFileIOError",
                                        ioe.getLocalizedMessage());
        } catch(CertificateException ex) {
            throw RB.newGridCAException(ex, "gridCAImpl.error.certFileInvalid",
                                        new Object [] { certFile, ex.getLocalizedMessage() });
        }
    }
    
    /**
     *  Get the X.509 certificate of a user.
     *
     *  @param username  name of the user
     *  @return X.509 certificate
     *  @throws GridCAException if the certificate does not exist
     */
    public X509Certificate getCertificate(String username) throws GridCAException {
        LOGGER.entering("GridCAImpl", "getCertificate");
        File certFile = getCertFileForUser(username);

        X509Certificate ret = readCertificate(certFile);
        LOGGER.exiting("GridCAImpl", "getCertificate");
        return ret;
    }
    
    /**
     *  Get the X.509 certificate of a daemon.
     *
     *  @param daemon  name of the daemon
     *  @return X.509 certificate
     *  @throws GridCAException if the certificate does not exist
     */
    public X509Certificate getDaemonCertificate(String daemon) throws GridCAException {
        LOGGER.entering("GridCAImpl", "getDaemonCertificate");
        File certFile = getCertFileForDaemon(daemon);

        X509Certificate ret = readCertificate(certFile);
        LOGGER.exiting("GridCAImpl", "getDaemonCertificate");
        return ret;
    }
    
    
    /**
     *  Renew the certificate of a user.
     *
     *  @param username  name of the user
     *  @param days      validity of the new certificate in days
     *  @return the renewed certificate
     *  @throws GridCAException if the certificate can not be renewed
     */
    public X509Certificate renewCertificate(String username, int days) throws GridCAException {
        LOGGER.entering("GridCAImpl", "renewCertificate");
        
        Expect pb = createProcess();
        pb.command().add("-renew");
        pb.command().add(username);
        pb.command().add("-days");
        pb.command().add(Integer.toString(days));

        execute(pb);
        
        X509Certificate ret = getCertificate(username);
        LOGGER.exiting("GridCAImpl", "renewCertificate");
        return ret;
    }
    
    /**
     *  Renew the certificate of a daemon.
     *
     *  @param daemon  name of the daemon
     *  @param days      validity of the new certificate in days
     *  @return the renewed certificate
     *  @throws GridCAException if the certificate can not be renewed
     */
    public X509Certificate renewDaemonCertificate(String daemon, int days) throws GridCAException {
        LOGGER.entering("GridCAImpl", "renewDaemonCertificate");
        Expect pb = createProcess();
        pb.command().add("-renew_sdm");
        pb.command().add(daemon);
        pb.command().add("-days");
        pb.command().add(Integer.toString(days));

        execute(pb);

        X509Certificate ret = getDaemonCertificate(daemon);
        LOGGER.exiting("GridCAImpl", "renewDaemonCertificate");
        return ret;
    }
    
    public void renewCaCertificate(int days) throws GridCAException {
        LOGGER.entering("GridCAImpl", "renewCaCertificate");
        Expect pb = createProcess();
        pb.command().add("-renew_ca");
        pb.command().add("-days");
        pb.command().add(Integer.toString(days));

        execute(pb);

        LOGGER.exiting("GridCAImpl", "renewCaCertificate");
    }
    
    private File createTempFile(String prefix, String suffix) throws GridCAException {
        try {
            return File.createTempFile(prefix, suffix, config.getTmpDir());
        } catch(IOException ioe) {
            throw RB.newGridCAException(ioe, "gridCAImpl.error.tmpFile",
                                        new Object [] { config.getTmpDir(), ioe.getLocalizedMessage() });
        }
    }
    
    /**
     *  Create a keystore which contains the private key and
     *  certificate of an user.
     *
     *  @param  username         name of the user
     *  @param  keystorePassword password used for encrypt the keystore
     *  @param  privateKeyPassword password for the private key
     *  @return the keystore
     * @throws GridCAException if the keystore could not be created
     */
    public KeyStore createKeyStore(String username, char[] keystorePassword, char[] privateKeyPassword) throws GridCAException {
         return createKeyStore(GridCAX500Name.TYPE_USER, username, keystorePassword, privateKeyPassword);
    }
    
    /**
     * Get the keystore for a daemon.
     *
     * This method can be used be the installation to create keystore for
     * the daemon of a sdm system.
     *
     * @param daemon name of the daemon
     * @throws com.sun.grid.ca.GridCAException 
     * @return the keystore of the daemon
     */
    public KeyStore createDaemonKeyStore(String daemon) throws GridCAException {
         return createKeyStore(GridCAX500Name.TYPE_SDM_DAEMON, daemon, new char[0], new char[0]);
    }
    
    /**
     *  Get the keystore for a SGE daemon.
     *
     *  This method can be used be the installation to create keystore for
     *  the daemon of a sdm system.
     *
     *  @param daemon name of the daemon
     *  @param  keystorePassword password used to encrypt the keystore
     *  @param  privateKeyPassword password used to encrypt the key
     *  @throws com.sun.grid.ca.GridCAException 
     *  @return the keystore of the daemon
     */
    public KeyStore createSGEDaemonKeyStore(String daemon, char[] keystorePassword, char[] privateKeyPassword) throws GridCAException {
         return createKeyStore(GridCAX500Name.TYPE_SGE_DAEMON, daemon, keystorePassword, privateKeyPassword);
    }
    
    
    private KeyStore createKeyStore(String type, String entity, char[] keystorePassword, char[] privateKeyPassword) throws GridCAException {
        LOGGER.entering("GridCAImpl", "createKeyStore");
        
        if(keystorePassword == null) {
            throw RB.newGridCAException("gridCAImpl.error.emptyKeystorePassword", GridCAConstants.BUNDLE);
        }
        if(privateKeyPassword == null) {
            throw RB.newGridCAException("gridCAImpl.error.emptyPKPassword", GridCAConstants.BUNDLE);
        }
        
        KeyStore ks;
        try {
            ks = KeyStore.getInstance("JKS");
        } catch (KeyStoreException ex) {
            throw RB.newGridCAException(ex, "gridCAImpl.error.createKS",
                                        ex.getLocalizedMessage());
        }
        
        char [] pkcs12PW = "changeit".toCharArray();
        try {
            ks.load(null, keystorePassword);
        } catch (NoSuchAlgorithmException ex) {
            throw RB.newGridCAException(ex, "gridCAImpl.error.ksInit",
                                        ex.getLocalizedMessage());
        } catch (CertificateException ex) {
            throw RB.newGridCAException(ex, "gridCAImpl.error.ksInit",
                                        ex.getLocalizedMessage());
        } catch (IOException ex) {
            throw RB.newGridCAException(ex, "gridCAImpl.error.ksInit",
                                        ex.getLocalizedMessage());
        }

        KeyStore pkcs12Ks = createPKCS12KeyStore(type, entity, pkcs12PW);
        Enumeration aliases;
        try {
            aliases = pkcs12Ks.aliases();
        } catch (KeyStoreException ex) {
            throw RB.newGridCAException(ex, "gridCAImpl.error.pkcs12.aliases",
                                        ex.getLocalizedMessage());
        }

        while(aliases.hasMoreElements()) {
            String alias = (String)aliases.nextElement();

            boolean isKeyEntry = false;
            try {
                isKeyEntry = pkcs12Ks.isKeyEntry(alias);
            } catch (KeyStoreException ex) {
                throw RB.newGridCAException(ex, "gridCAImpl.error.pkcs12.isKeyEntry",
                                            new Object [] { alias, ex.getLocalizedMessage() });
            }
            if (isKeyEntry) {
                LOGGER.log(Level.FINE, "gridCAImpl.addKey", alias);

                Key key = null;
                try {
                    key = pkcs12Ks.getKey(alias, pkcs12PW);
                } catch(NoSuchAlgorithmException ex) {
                    throw RB.newGridCAException(ex, "gridCAImpl.error.pkcs12.getKey",
                                                new Object [] { alias, ex.getLocalizedMessage() });
                } catch(UnrecoverableKeyException ex) {
                    throw RB.newGridCAException(ex, "gridCAImpl.error.pkcs12.getKey",
                                                new Object [] { alias, ex.getLocalizedMessage() });
                } catch (KeyStoreException ex) {
                    throw RB.newGridCAException(ex, "gridCAImpl.error.pkcs12.getKey",
                                                new Object [] { alias, ex.getLocalizedMessage() });
                }
                Certificate[] chain;
                try {
                    chain = pkcs12Ks.getCertificateChain(alias);
                } catch (KeyStoreException ex) {
                    throw RB.newGridCAException(ex, "gridCAImpl.error.pkcs12.getCert",
                                                new Object [] { alias, ex.getLocalizedMessage() });
                }

                if(!(chain[0] instanceof X509Certificate)) {
                    throw RB.newGridCAException("gridCAImpl.error.pkcs12.invalidCert",
                                                alias);
                }
                X509Certificate cert = (X509Certificate)chain[0];

                GridCAX500Name name = GridCAX500Name.parse(cert.getSubjectX500Principal().getName());

                
                if(GridCAX500Name.TYPE_SDM_DAEMON.equals(type)) {
                    if(!name.isDaemon()) {
                        throw RB.newGridCAException("gridCAImpl.error.notADaemonCert", 
                                                    cert.getSubjectX500Principal().getName());
                    }
                    alias = name.getDaemonName();
                } else if(GridCAX500Name.TYPE_SGE_DAEMON.equals(type)) {
                    alias = name.getUsername();
                } else if (GridCAX500Name.TYPE_USER.endsWith(type)) {
                    if(name.isDaemon()) {
                        throw RB.newGridCAException("gridCAImpl.error.notAUserCert", 
                                                    cert.getSubjectX500Principal().getName());
                    }
                    alias = name.getUsername();
                }
                try {
                    ks.setKeyEntry(alias,key,privateKeyPassword,chain);
                } catch (KeyStoreException ex) {
                    throw RB.newGridCAException(ex, "gridCAImpl.error.ks.setKeyEntry",
                                                new Object [] { alias, ex.getLocalizedMessage() });
                }
                LOGGER.exiting("GridCAImpl", "createKeyStore");
                return ks;
            }
        }
        throw RB.newGridCAException("gridCaImpl.error.pkcs12.entityNotFound", entity);
    }
    
    
    
    private final KeyStore createPKCS12KeyStore(String type, String username, char [] password) throws GridCAException {
        LOGGER.entering("GridCAImpl", "createPKCS12KeyStore");
        File userFile = null;
        File passwordFile = null;
        File pkcs12File = new File(config.getTmpDir(), username + ".p12");        

        try {
            try {
                // write an empty password file, since the private keys are not crypted
                passwordFile = createTempFile("passin",".txt");

                try {
                    PrintWriter pw = new PrintWriter(new FileWriter(passwordFile));
                    pw.println(password);
                    pw.flush();
                    pw.close();
                } catch(IOException ex) {
                    throw RB.newGridCAException(ex, "gridCAImpl.error.io",ex.getLocalizedMessage());
                }

                Expect pb = createProcess();

                if(type.equals("user")) {
                    pb.command().add("-pkcs12");
                } else if (type.equals("sdm_daemon")) {
                    pb.command().add("-sdm_pkcs12");
                } else if (type.equals("sge_daemon")) {
                    pb.command().add("-sys_pkcs12");
                }
                pb.command().add(username);
                pb.command().add("-pkcs12pwf");
                pb.command().add(passwordFile.getAbsolutePath());
                pb.command().add("-pkcs12dir");
                pb.command().add(config.getTmpDir().getAbsolutePath());
                
                execute(pb);

            } finally {
                if(userFile != null) {
                    userFile.delete();
                }
                if(passwordFile != null) {
                    passwordFile.delete();
                }
            }
            try {
                KeyStore kspkcs12 = KeyStore.getInstance("PKCS12");
                kspkcs12.load(new FileInputStream(pkcs12File), password);
                
                LOGGER.exiting("GridCAImpl", "createPKCS12KeyStore");
                return kspkcs12;
            } catch (CertificateException ex) {
                throw RB.newGridCAException(ex, "gridCAImpl.error.pkcs12.load",
                                            new Object [] { pkcs12File, ex.getLocalizedMessage() } );
            } catch (IOException ex) {
                throw RB.newGridCAException(ex, "gridCAImpl.error.pkcs12.load",
                                            new Object [] { pkcs12File, ex.getLocalizedMessage() } );
            } catch (NoSuchAlgorithmException ex) {
                throw RB.newGridCAException(ex, "gridCAImpl.error.pkcs12.load",
                                            new Object [] { pkcs12File, ex.getLocalizedMessage() } );
            } catch (KeyStoreException ex) {
                throw RB.newGridCAException(ex, "gridCAImpl.error.pkcs12.load",
                                            new Object [] { pkcs12File, ex.getLocalizedMessage() } );
            }        
        } finally {
            if(pkcs12File.exists()) {
                if(!pkcs12File.delete()) {
                    LOGGER.log(Level.WARNING,"gridCAImpl.warn.pkcs12.delete", pkcs12File);
                }
            }
        }
    }

    private static class OutputHandler implements Runnable {
        private InputStream in;
        private final StringWriter errors = new StringWriter();
        private PrintWriter pw;
        
        public OutputHandler(InputStream in, Writer out) {
            this.in = in;
            pw = new PrintWriter(out);
            
        }

        private static String [] ERROR_PATTERNS = {
            "Error:",
            "Error"
        };
        
        public void run() {
            
            PrintWriter epw = new PrintWriter(errors);
            try {
                String line = null;
                BufferedReader br = new BufferedReader(new InputStreamReader(in));
                boolean firstError = false;
                while((line = br.readLine()) != null) {
                    pw.println(line);
                    for(int i = 0; i < ERROR_PATTERNS.length; i++) {
                        if(line.startsWith(ERROR_PATTERNS[i])) {
                            if(firstError) {
                                firstError = false;
                            } else {
                                epw.println();
                            }
                            epw.print(line.substring(ERROR_PATTERNS[i].length()).trim());
                        }
                    }
                }
            } catch(IOException ioe) {
                LOGGER.log(Level.SEVERE, "gridCAImpl.error.outputIO", ioe);
            } finally {
                epw.close();
                pw.close();
            }
        }

        public String getError() {
            return errors.getBuffer().toString();
        }
    }
}


