001    /**
002     * Copyright 2004-2012 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.apache.torque.mojo;
017    
018    /*
019     * Licensed to the Apache Software Foundation (ASF) under one
020     * or more contributor license agreements.  See the NOTICE file
021     * distributed with this work for additional information
022     * regarding copyright ownership.  The ASF licenses this file
023     * to you under the Apache License, Version 2.0 (the
024     * "License"); you may not use this file except in compliance
025     * with the License.  You may obtain a copy of the License at
026     *
027     *   http://www.apache.org/licenses/LICENSE-2.0
028     *
029     * Unless required by applicable law or agreed to in writing,
030     * software distributed under the License is distributed on an
031     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
032     * KIND, either express or implied.  See the License for the
033     * specific language governing permissions and limitations
034     * under the License.
035     */
036    
037    import java.io.File;
038    import java.lang.reflect.InvocationTargetException;
039    import java.sql.Connection;
040    import java.sql.SQLException;
041    import java.util.Collection;
042    import java.util.Map;
043    import java.util.Properties;
044    import java.util.Vector;
045    
046    import org.apache.commons.beanutils.BeanUtils;
047    import org.apache.commons.lang.StringUtils;
048    import org.apache.maven.plugin.MojoExecutionException;
049    import org.apache.maven.settings.Server;
050    import org.apache.maven.shared.filtering.MavenFileFilter;
051    import org.apache.torque.engine.platform.Platform;
052    import org.apache.torque.engine.platform.PlatformFactory;
053    import org.apache.torque.util.JdbcConfigurer;
054    import org.apache.torque.util.MojoDatabaseListener;
055    import org.kuali.core.db.torque.PropertyHandlingException;
056    import org.kuali.core.db.torque.Utils;
057    import org.kuali.db.ConnectionHandler;
058    import org.kuali.db.Credentials;
059    import org.kuali.db.JDBCUtils;
060    import org.kuali.db.SQLExecutor;
061    import org.kuali.db.Transaction;
062    
063    import static org.apache.commons.lang.StringUtils.*;
064    
065    /**
066     * Abstract mojo for making use of SQLExecutor
067     */
068    public abstract class AbstractSQLExecutorMojo extends BaseMojo {
069            Utils utils = new Utils();
070            JDBCUtils jdbcUtils;
071            ConnectionHandler connectionHandler;
072            Platform platform;
073    
074            public static final String DRIVER_INFO_PROPERTIES_USER = "user";
075            public static final String DRIVER_INFO_PROPERTIES_PASSWORD = "password";
076    
077            /**
078             * Call {@link #setOrder(String)} with this value to sort in ascendant order the sql files.
079             */
080            public static final String FILE_SORTING_ASC = "ascending";
081    
082            /**
083             * Call {@link #setOrder(String)} with this value to sort in descendant order the sql files.
084             */
085            public static final String FILE_SORTING_DSC = "descending";
086    
087            // ////////////////////////// User Info ///////////////////////////////////
088    
089            /**
090             * The type of database we are targeting (eg oracle, mysql). This is optional if <code>url</code> is supplied as the
091             * database type will be automatically detected based on the <code>url</code>. If targetDatabase is explicitly
092             * supplied it will override the type selected by the automatic detection logic.
093             * 
094             * @parameter expression="${targetDatabase}"
095             */
096            String targetDatabase;
097    
098            /**
099             * Database username. If not given, it will be looked up through <code>settings.xml</code>'s server with
100             * <code>${settingsKey}</code> as key.
101             * 
102             * @parameter expression="${username}"
103             */
104            String username;
105    
106            /**
107             * Database password. If not given, it will be looked up through <code>settings.xml</code>'s server with
108             * <code>${settingsKey}</code> as key.
109             * 
110             * @parameter expression="${password}"
111             */
112            String password;
113    
114            /**
115             * Ignore the password and use anonymous access.
116             * 
117             * @parameter expression="${enableAnonymousPassword}" default-value="false"
118             */
119            boolean enableAnonymousPassword;
120    
121            /**
122             * Ignore the username and use anonymous access.
123             * 
124             * @parameter expression="${enableAnonymousUsername}" default-value="false"
125             */
126            boolean enableAnonymousUsername;
127    
128            /**
129             * Additional key=value pairs separated by a comma to be passed to JDBC driver.
130             * 
131             * @parameter expression="${driverProperties}" default-value=""
132             */
133            String driverProperties;
134    
135            /**
136             * If set to true the password being used to connect to the database will be displayed in log messages.
137             * 
138             * @parameter expression="${showPassword}" default-value="false"
139             */
140            boolean showPassword;
141    
142            /**
143             * The id of the server in settings.xml containing the username/password to use.
144             * 
145             * @parameter expression="${settingsKey}" default-value="impex.${project.artifactId}"
146             */
147            String settingsKey;
148    
149            /**
150             * Skip execution if there is an error obtaining a connection. If this is set to true, the build will continue even
151             * if there is an error obtaining a connection
152             * 
153             * @parameter expression="${skipOnConnectionError}" default-value="false"
154             */
155            boolean skipOnConnectionError;
156    
157            /**
158             * SQL input commands separated by <code>${delimiter}</code>.
159             * 
160             * @parameter expression="${sqlCommand}" default-value=""
161             */
162            String sqlCommand = "";
163    
164            /**
165             * List of files containing SQL statements to load.
166             * 
167             * @parameter expression="${srcFiles}"
168             */
169            File[] srcFiles;
170    
171            // //////////////////////////////// Database info /////////////////////////
172            /**
173             * Database URL.
174             * 
175             * @parameter expression="${url}"
176             */
177            String url;
178    
179            /**
180             * Database driver classname. This parameter is optional, as the correct JDBC driver to use is detected from the
181             * <code>url</code> in almost all cases (works for Oracle, MySQL, Derby, PostGresSQL, DB2, H2, HSQL, SQL Server). If
182             * a driver is explicitly supplied, it will be used in place of the JDBC driver the automatic detection logic would
183             * have chosen.
184             * 
185             * @parameter expression="${driver}"
186             */
187            String driver;
188    
189            // //////////////////////////// Operation Configuration ////////////////////
190            /**
191             * Set to <code>true</code> to execute non-transactional SQL.
192             * 
193             * @parameter expression="${autocommit}" default-value="false"
194             */
195            boolean autocommit;
196    
197            /**
198             * Action to perform if an error is found. Possible values are <code>abort</code> and <code>continue</code>.
199             * 
200             * @parameter expression="${onError}" default-value="abort"
201             */
202            String onError = SQLExecutor.ON_ERROR_ABORT;
203    
204            // //////////////////////////// Parser Configuration ////////////////////
205    
206            /**
207             * Set the delimiter that separates SQL statements.
208             * 
209             * @parameter expression="${delimiter}" default-value="/"
210             */
211            String delimiter = "/";
212    
213            /**
214             * The delimiter type takes two values - "normal" and "row". Normal means that any occurrence of the delimiter
215             * terminate the SQL command whereas with row, only a line containing just the delimiter is recognized as the end of
216             * the command.<br>
217             * <br>
218             * For example, set this to "go" and delimiterType to "row" for Sybase ASE or MS SQL Server.
219             * 
220             * @parameter expression="${delimiterType}" default-value="row"
221             */
222            String delimiterType = DelimiterType.ROW;
223    
224            /**
225             * Keep the format of an SQL block.
226             * 
227             * @parameter expression="${keepFormat}" default-value="true"
228             */
229            boolean keepFormat = true;
230    
231            /**
232             * Print header columns.
233             * 
234             * @parameter expression="${showheaders}" default-value="true"
235             */
236            boolean showheaders = true;
237    
238            /**
239             * If writing output to a file, append to an existing file or overwrite it?
240             * 
241             * @parameter expression="${append}" default-value="false"
242             */
243            boolean append = false;
244    
245            /**
246             * Argument to Statement.setEscapeProcessing If you want the driver to use regular SQL syntax then set this to
247             * false.
248             * 
249             * @parameter expression="${escapeProcessing}" default-value="true"
250             */
251            boolean escapeProcessing = true;
252    
253            // //////////////////////////////// Internal properties//////////////////////
254    
255            /**
256             * number of successful executed statements
257             */
258            int successfulStatements = 0;
259    
260            /**
261             * number of total executed statements
262             */
263            int totalStatements = 0;
264    
265            /**
266             * Database connection
267             */
268            Connection conn = null;
269    
270            /**
271             * SQL transactions to perform
272             */
273            Vector<Transaction> transactions = new Vector<Transaction>();
274    
275            /**
276             * @component role="org.apache.maven.shared.filtering.MavenFileFilter"
277             */
278            MavenFileFilter fileFilter;
279    
280            /**
281             * The credentials to use for database access
282             */
283            Credentials credentials;
284    
285            protected void configureTransactions() throws MojoExecutionException {
286                    // default implementation does nothing
287            }
288    
289            protected Properties getContextProperties() {
290                    Properties properties = new Properties();
291                    Map<String, String> environment = System.getenv();
292                    for (String key : environment.keySet()) {
293                            properties.put("env." + key, environment.get(key));
294                    }
295                    properties.putAll(getProject().getProperties());
296                    properties.putAll(System.getProperties());
297                    return properties;
298            }
299    
300            protected Credentials getNewCredentials() {
301                    Credentials credentials = new Credentials();
302                    credentials.setUsername(getUsername());
303                    credentials.setPassword(getPassword());
304                    return credentials;
305            }
306    
307            protected ConnectionHandler getNewConnectionHandler() throws MojoExecutionException {
308                    ConnectionHandler connectionHandler = new ConnectionHandler();
309                    try {
310                            BeanUtils.copyProperties(connectionHandler, this);
311                            return connectionHandler;
312                    } catch (Exception e) {
313                            throw new MojoExecutionException("Error establishing connection", e);
314                    }
315            }
316    
317            /**
318             * Validate our configuration and execute SQL as appropriate
319             * 
320             * @throws MojoExecutionException
321             */
322            public void executeMojo() throws MojoExecutionException {
323                    jdbcUtils = new JDBCUtils();
324                    updateConfiguration();
325                    Credentials credentials = getNewCredentials();
326                    updateCredentials(credentials);
327                    validateCredentials(credentials);
328                    setCredentials(credentials);
329                    validateConfiguration();
330    
331                    connectionHandler = getNewConnectionHandler();
332                    conn = getConnection();
333    
334                    if (connectionHandler.isConnectionError() && skipOnConnectionError) {
335                            // There was an error obtaining a connection
336                            // Do not fail the build but don't do anything more
337                            return;
338                    }
339    
340                    // Configure the transactions we will be running
341                    configureTransactions();
342    
343                    // Make sure our counters are zeroed out
344                    successfulStatements = 0;
345                    totalStatements = 0;
346    
347                    // Get an SQLExecutor
348                    SQLExecutor executor = getSqlExecutor();
349    
350                    try {
351                            executor.execute();
352                    } catch (SQLException e) {
353                            throw new MojoExecutionException("Error executing SQL", e);
354                    }
355            }
356    
357            /**
358             * Set an inline SQL command to execute.
359             * 
360             * @param sql
361             *            the sql statement to add
362             */
363            public void addText(String sql) {
364                    this.sqlCommand += sql;
365            }
366    
367            /**
368             * Set the delimiter that separates SQL statements. Defaults to &quot;;&quot;;
369             * 
370             * @param delimiter
371             *            the new delimiter
372             */
373            public void setDelimiter(String delimiter) {
374                    this.delimiter = delimiter;
375            }
376    
377            /**
378             * Set the delimiter type: "normal" or "row" (default "normal").
379             * 
380             * @param delimiterType
381             *            the new delimiterType
382             */
383            public void setDelimiterType(String delimiterType) {
384                    this.delimiterType = delimiterType;
385            }
386    
387            /**
388             * Print headers for result sets from the statements; optional, default true.
389             * 
390             * @param showheaders
391             *            <code>true</code> to show the headers, otherwise <code>false</code>
392             */
393            public void setShowheaders(boolean showheaders) {
394                    this.showheaders = showheaders;
395            }
396    
397            /**
398             * whether output should be appended to or overwrite an existing file. Defaults to false.
399             * 
400             * @param append
401             *            <code>true</code> to append, otherwise <code>false</code> to overwrite
402             */
403            public void setAppend(boolean append) {
404                    this.append = append;
405            }
406    
407            /**
408             * whether or not format should be preserved. Defaults to false.
409             * 
410             * @param keepformat
411             *            The keepformat to set
412             */
413            public void setKeepFormat(boolean keepformat) {
414                    this.keepFormat = keepformat;
415            }
416    
417            /**
418             * Set escape processing for statements.
419             * 
420             * @param enable
421             *            <code>true</code> to escape, otherwiser <code>false</code>
422             */
423            public void setEscapeProcessing(boolean enable) {
424                    escapeProcessing = enable;
425            }
426    
427            protected SQLExecutor getSqlExecutor() throws MojoExecutionException {
428                    try {
429                            SQLExecutor executor = new SQLExecutor();
430                            BeanUtils.copyProperties(executor, this);
431                            executor.addListener(new MojoDatabaseListener(getLog()));
432                            return executor;
433                    } catch (InvocationTargetException e) {
434                            throw new MojoExecutionException("Error copying properties from the mojo to the SQL executor", e);
435                    } catch (IllegalAccessException e) {
436                            throw new MojoExecutionException("Error copying properties from the mojo to the SQL executor", e);
437                    }
438            }
439    
440            /**
441             * Attempt to automatically detect the correct JDBC driver and database type (oracle, mysql, h2, derby, etc) given a
442             * JDBC url
443             */
444            protected void updateConfiguration() throws MojoExecutionException {
445                    try {
446                            new JdbcConfigurer().updateConfiguration(this);
447                    } catch (PropertyHandlingException e) {
448                            throw new MojoExecutionException("Error handling properties", e);
449                    }
450                    platform = PlatformFactory.getPlatformFor(targetDatabase);
451            }
452    
453            /**
454             * Validate that some essential configuration items are present
455             */
456            protected void validateConfiguration() throws MojoExecutionException {
457                    new JdbcConfigurer().validateConfiguration(this);
458            }
459    
460            protected void validateCredentials(Credentials credentials, boolean anonymousAccessAllowed, String validationFailureMessage) throws MojoExecutionException {
461                    if (anonymousAccessAllowed) {
462                            // If credentials aren't required, don't bother validating
463                            return;
464                    }
465                    String username = credentials.getUsername();
466                    String password = credentials.getPassword();
467                    if (!isEmpty(username) && !isEmpty(password)) {
468                            // Both are required, and both have been supplied
469                            return;
470                    }
471                    throw new MojoExecutionException(validationFailureMessage);
472            }
473    
474            protected void validateCredentials(Credentials credentials) throws MojoExecutionException {
475                    // Both are required but one (or both) are missing
476                    StringBuffer sb = new StringBuffer();
477                    sb.append("\n\n");
478                    sb.append("Username and password must be specified.\n");
479                    sb.append("Specify them in the plugin configuration or as a system property.\n");
480                    sb.append("\n");
481                    sb.append("For example:\n");
482                    sb.append("-Dusername=myuser\n");
483                    sb.append("-Dpassword=mypassword\n");
484                    sb.append("\n.");
485                    validateCredentials(credentials, enableAnonymousUsername && enableAnonymousPassword, sb.toString());
486            }
487    
488            protected boolean isNullOrEmpty(Collection<?> c) {
489                    if (c == null) {
490                            return true;
491                    }
492                    if (c.size() == 0) {
493                            return true;
494                    }
495                    return false;
496            }
497    
498            protected String convertNullToEmpty(String s) {
499                    if (s == null) {
500                            return "";
501                    } else {
502                            return s;
503                    }
504            }
505    
506            /**
507             * Load username/password from settings.xml if user has not set them in JVM properties
508             * 
509             * @throws MojoExecutionException
510             */
511            protected void updateCredentials(Credentials credentials) {
512                    Server server = getServerFromSettingsKey();
513                    String username = getUpdatedUsername(server, credentials.getUsername());
514                    String password = getUpdatedPassword(server, credentials.getPassword());
515                    credentials.setUsername(convertNullToEmpty(username));
516                    credentials.setPassword(convertNullToEmpty(password));
517            }
518    
519            protected Server getServerFromSettingsKey() {
520                    Server server = getSettings().getServer(getSettingsKey());
521                    if (server == null) {
522                            // Fall through to using the JDBC url as a key
523                            return getSettings().getServer("impex." + getUrl());
524                    } else {
525                            return null;
526                    }
527            }
528    
529            protected String getUpdatedPassword(Server server, String password) {
530                    // They already gave us a password, don't mess with it
531                    if (!isEmpty(password)) {
532                            return password;
533                    }
534                    if (server != null) {
535                            // We've successfully located a server in settings.xml, use the password from that
536                            getLog().info("Located a password in settings.xml under the server id '" + server.getId() + "' Password: " + getDisplayPassword(server.getPassword()));
537                            return server.getPassword();
538                    }
539                    getLog().info("Using default password generated from the artifact id");
540                    return platform.getSchemaName(getProject().getArtifactId());
541            }
542    
543            protected String getDisplayPassword(String password) {
544                    if (isShowPassword()) {
545                            return password;
546                    } else {
547                            return StringUtils.repeat("*", password.length());
548                    }
549            }
550    
551            protected String getUpdatedUsername(Server server, String username) {
552                    // They already gave us a username, don't mess with it
553                    if (!isEmpty(username)) {
554                            return username;
555                    }
556                    if (server != null) {
557                            // We've successfully located a server in settings.xml, use the username from that
558                            getLog().info("Located a username in settings.xml under the server id '" + server.getId() + "' Username: " + server.getUsername());
559                            return server.getUsername();
560                    }
561                    getLog().info("Using default username generated from the artifact id");
562                    return platform.getSchemaName(getProject().getArtifactId());
563            }
564    
565            /**
566             * Creates a new Connection as using the driver, url, userid and password specified.
567             * 
568             * The calling method is responsible for closing the connection.
569             * 
570             * @return Connection the newly created connection.
571             * @throws MojoExecutionException
572             *             if the UserId/Password/Url is not set or there is no suitable driver or the driver fails to load.
573             * @throws SQLException
574             *             if there is problem getting connection with valid url
575             * 
576             */
577            protected Connection getConnection() throws MojoExecutionException {
578                    try {
579                            return connectionHandler.getConnection();
580                    } catch (Exception e) {
581                            throw new MojoExecutionException("Error establishing connection", e);
582                    }
583            }
584    
585            /**
586             * parse driverProperties into Properties set
587             * 
588             * @return the driver properties
589             * @throws MojoExecutionException
590             */
591            protected Properties getDriverProperties() throws MojoExecutionException {
592                    Properties properties = new Properties();
593    
594                    if (isEmpty(this.driverProperties)) {
595                            return properties;
596                    }
597    
598                    String[] tokens = split(this.driverProperties, ",");
599                    for (int i = 0; i < tokens.length; ++i) {
600                            String[] keyValueTokens = split(tokens[i].trim(), "=");
601                            if (keyValueTokens.length != 2) {
602                                    throw new MojoExecutionException("Invalid JDBC Driver properties: " + this.driverProperties);
603                            }
604                            properties.setProperty(keyValueTokens[0], keyValueTokens[1]);
605                    }
606                    return properties;
607            }
608    
609            public String getUsername() {
610                    return this.username;
611            }
612    
613            public void setUsername(String username) {
614                    this.username = username;
615            }
616    
617            public String getPassword() {
618                    return this.password;
619            }
620    
621            public void setPassword(String password) {
622                    this.password = password;
623            }
624    
625            public String getUrl() {
626                    return this.url;
627            }
628    
629            public void setUrl(String url) {
630                    this.url = url;
631            }
632    
633            public String getDriver() {
634                    return this.driver;
635            }
636    
637            public void setDriver(String driver) {
638                    this.driver = driver;
639            }
640    
641            public void setAutocommit(boolean autocommit) {
642                    this.autocommit = autocommit;
643            }
644    
645            public File[] getSrcFiles() {
646                    return this.srcFiles;
647            }
648    
649            public void setSrcFiles(File[] files) {
650                    this.srcFiles = files;
651            }
652    
653            /**
654             * Number of SQL statements executed so far that caused errors.
655             * 
656             * @return the number
657             */
658            public int getSuccessfulStatements() {
659                    return successfulStatements;
660            }
661    
662            /**
663             * Number of SQL statements executed so far, including the ones that caused errors.
664             * 
665             * @return the number
666             */
667            public int getTotalStatements() {
668                    return totalStatements;
669            }
670    
671            public String getOnError() {
672                    return this.onError;
673            }
674    
675            public void setOnError(String action) {
676                    if (SQLExecutor.ON_ERROR_ABORT.equalsIgnoreCase(action)) {
677                            this.onError = SQLExecutor.ON_ERROR_ABORT;
678                    } else if (SQLExecutor.ON_ERROR_CONTINUE.equalsIgnoreCase(action)) {
679                            this.onError = SQLExecutor.ON_ERROR_CONTINUE;
680                    } else if (SQLExecutor.ON_ERROR_ABORT_AFTER.equalsIgnoreCase(action)) {
681                            this.onError = SQLExecutor.ON_ERROR_ABORT_AFTER;
682                    } else {
683                            throw new IllegalArgumentException(action + " is not a valid value for onError, only '" + SQLExecutor.ON_ERROR_ABORT + "', '" + SQLExecutor.ON_ERROR_ABORT_AFTER + "', or '" + SQLExecutor.ON_ERROR_CONTINUE + "'.");
684                    }
685            }
686    
687            public void setSettingsKey(String key) {
688                    this.settingsKey = key;
689            }
690    
691            public void setDriverProperties(String driverProperties) {
692                    this.driverProperties = driverProperties;
693            }
694    
695            public String getSqlCommand() {
696                    return sqlCommand;
697            }
698    
699            public void setSqlCommand(String sqlCommand) {
700                    this.sqlCommand = sqlCommand;
701            }
702    
703            public Vector<Transaction> getTransactions() {
704                    return transactions;
705            }
706    
707            public void setTransactions(Vector<Transaction> transactions) {
708                    this.transactions = transactions;
709            }
710    
711            public void setFileFilter(MavenFileFilter filter) {
712                    this.fileFilter = filter;
713            }
714    
715            public String getTargetDatabase() {
716                    return targetDatabase;
717            }
718    
719            public void setTargetDatabase(String targetDatabase) {
720                    this.targetDatabase = targetDatabase;
721            }
722    
723            public Connection getConn() {
724                    return conn;
725            }
726    
727            public void setConn(Connection conn) {
728                    this.conn = conn;
729            }
730    
731            public String getDelimiter() {
732                    return delimiter;
733            }
734    
735            public String getDelimiterType() {
736                    return delimiterType;
737            }
738    
739            public boolean isKeepFormat() {
740                    return keepFormat;
741            }
742    
743            public boolean isShowheaders() {
744                    return showheaders;
745            }
746    
747            public boolean isAppend() {
748                    return append;
749            }
750    
751            public boolean isEscapeProcessing() {
752                    return escapeProcessing;
753            }
754    
755            public boolean isSkipOnConnectionError() {
756                    return skipOnConnectionError;
757            }
758    
759            public void setSkipOnConnectionError(boolean skipOnConnectionError) {
760                    this.skipOnConnectionError = skipOnConnectionError;
761            }
762    
763            public MavenFileFilter getFileFilter() {
764                    return fileFilter;
765            }
766    
767            public boolean isShowPassword() {
768                    return showPassword;
769            }
770    
771            public void setShowPassword(boolean showPassword) {
772                    this.showPassword = showPassword;
773            }
774    
775            public boolean isEnableAnonymousPassword() {
776                    return enableAnonymousPassword;
777            }
778    
779            public void setEnableAnonymousPassword(boolean enableAnonymousPassword) {
780                    this.enableAnonymousPassword = enableAnonymousPassword;
781            }
782    
783            public String getSettingsKey() {
784                    return settingsKey;
785            }
786    
787            public boolean isAutocommit() {
788                    return autocommit;
789            }
790    
791            public void setSuccessfulStatements(int successfulStatements) {
792                    this.successfulStatements = successfulStatements;
793            }
794    
795            public void setTotalStatements(int totalStatements) {
796                    this.totalStatements = totalStatements;
797            }
798    
799            public void setCredentials(Credentials credentials) {
800                    this.credentials = credentials;
801            }
802    
803            public Credentials getCredentials() {
804                    return credentials;
805            }
806    
807    }