001package io.ebean.datasource;
002
003import java.sql.Connection;
004import java.util.ArrayList;
005import java.util.LinkedHashMap;
006import java.util.List;
007import java.util.Map;
008import java.util.Properties;
009
010/**
011 * Configuration information for a DataSource.
012 */
013public class DataSourceConfig {
014
015  private static final String POSTGRES = "postgres";
016
017  private InitDatabase initDatabase;
018
019  private String url;
020
021  private String username;
022
023  private String password;
024
025  private String schema;
026
027  /**
028   * The name of the database platform (for use with ownerUsername and InitDatabase).
029   */
030  private String platform;
031
032  /**
033   * The optional database owner username (for running InitDatabase).
034   */
035  private String ownerUsername;
036
037  /**
038   * The optional database owner password (for running InitDatabase).
039   */
040  private String ownerPassword;
041
042  private String driver;
043
044  private int minConnections = 2;
045
046  private int maxConnections = 200;
047
048  private int isolationLevel = Connection.TRANSACTION_READ_COMMITTED;
049
050  private boolean autoCommit;
051
052  private boolean readOnly;
053
054  private String heartbeatSql;
055
056  private int heartbeatFreqSecs = 30;
057
058  private int heartbeatTimeoutSeconds = 3;
059
060  private boolean captureStackTrace;
061
062  private int maxStackTraceSize = 5;
063
064  private int leakTimeMinutes = 30;
065
066  private int maxInactiveTimeSecs = 300;
067
068  private int maxAgeMinutes = 0;
069
070  private int trimPoolFreqSecs = 59;
071
072  private int pstmtCacheSize = 20;
073
074  private int cstmtCacheSize = 20;
075
076  private int waitTimeoutMillis = 1000;
077
078  private String poolListener;
079
080  private boolean offline;
081
082  private boolean failOnStart = true;
083
084  private Map<String, String> customProperties;
085
086  private List<String> initSql;
087
088
089  private DataSourceAlert alert;
090
091  private DataSourcePoolListener listener;
092
093  /**
094   * Return a copy of the DataSourceConfig.
095   */
096  public DataSourceConfig copy() {
097
098    DataSourceConfig copy = new DataSourceConfig();
099    copy.initDatabase = initDatabase;
100    copy.url = url;
101    copy.username = username;
102    copy.password = password;
103    copy.schema = schema;
104    copy.platform = platform;
105    copy.ownerUsername = ownerUsername;
106    copy.ownerPassword = ownerPassword;
107    copy.driver = driver;
108    copy.minConnections = minConnections;
109    copy.maxConnections = maxConnections;
110    copy.isolationLevel = isolationLevel;
111    copy.autoCommit = autoCommit;
112    copy.readOnly = readOnly;
113    copy.heartbeatSql = heartbeatSql;
114    copy.heartbeatFreqSecs = heartbeatFreqSecs;
115    copy.heartbeatTimeoutSeconds = heartbeatTimeoutSeconds;
116    copy.captureStackTrace = captureStackTrace;
117    copy.maxStackTraceSize = maxStackTraceSize;
118    copy.leakTimeMinutes = leakTimeMinutes;
119    copy.maxInactiveTimeSecs = maxInactiveTimeSecs;
120    copy.maxAgeMinutes = maxAgeMinutes;
121    copy.trimPoolFreqSecs = trimPoolFreqSecs;
122    copy.pstmtCacheSize = pstmtCacheSize;
123    copy.cstmtCacheSize = cstmtCacheSize;
124    copy.waitTimeoutMillis = waitTimeoutMillis;
125    copy.poolListener = poolListener;
126    copy.offline = offline;
127    copy.failOnStart = failOnStart;
128    if (customProperties != null) {
129      copy.customProperties = new LinkedHashMap<>(customProperties);
130    }
131    copy.initSql = initSql;
132    copy.alert = alert;
133    copy.listener = listener;
134
135    return copy;
136  }
137
138  /**
139   * Default the values for driver, url, username and password from another config if
140   * they have not been set.
141   */
142  public DataSourceConfig setDefaults(DataSourceConfig other) {
143    if (driver == null) {
144      driver = other.driver;
145    }
146    if (url == null) {
147      url = other.url;
148    }
149    if (username == null) {
150      username = other.username;
151    }
152    if (password == null) {
153      password = other.password;
154    }
155    if (schema == null) {
156      schema = other.schema;
157    }
158    return this;
159  }
160
161  /**
162   * Return true if there are no values set for any of url, driver, username and password.
163   */
164  public boolean isEmpty() {
165    return url == null
166      && driver == null
167      && username == null
168      && password == null;
169  }
170
171  /**
172   * Return the connection URL.
173   */
174  public String getUrl() {
175    return url;
176  }
177
178  /**
179   * Set the connection URL.
180   */
181  public DataSourceConfig setUrl(String url) {
182    this.url = url;
183    return this;
184  }
185
186  /**
187   * Return the database username.
188   */
189  public String getUsername() {
190    return username;
191  }
192
193  /**
194   * Set the database username.
195   */
196  public DataSourceConfig setUsername(String username) {
197    this.username = username;
198    return this;
199  }
200
201  /**
202   * Return the database password.
203   */
204  public String getPassword() {
205    return password;
206  }
207
208  /**
209   * Set the database password.
210   */
211  public DataSourceConfig setPassword(String password) {
212    this.password = password;
213    return this;
214  }
215
216  /**
217   * Return the database username.
218   */
219  public String getSchema() {
220    return schema;
221  }
222
223  /**
224   * Set the default database schema to use.
225   */
226  public DataSourceConfig setSchema(String schema) {
227    this.schema = schema;
228    return this;
229  }
230
231  /**
232   * Return the database driver.
233   */
234  public String getDriver() {
235    return driver;
236  }
237
238  /**
239   * Set the database driver.
240   */
241  public DataSourceConfig setDriver(String driver) {
242    this.driver = driver;
243    return this;
244  }
245
246  /**
247   * Return the transaction isolation level.
248   */
249  public int getIsolationLevel() {
250    return isolationLevel;
251  }
252
253  /**
254   * Set the transaction isolation level.
255   */
256  public DataSourceConfig setIsolationLevel(int isolationLevel) {
257    this.isolationLevel = isolationLevel;
258    return this;
259  }
260
261  /**
262   * Return autoCommit setting.
263   */
264  public boolean isAutoCommit() {
265    return autoCommit;
266  }
267
268  /**
269   * Set to true to turn on autoCommit.
270   */
271  public DataSourceConfig setAutoCommit(boolean autoCommit) {
272    this.autoCommit = autoCommit;
273    return this;
274  }
275
276  /**
277   * Return the read only setting.
278   */
279  public boolean isReadOnly() {
280    return readOnly;
281  }
282
283  /**
284   * Set to true to for read only.
285   */
286  public DataSourceConfig setReadOnly(boolean readOnly) {
287    this.readOnly = readOnly;
288    return this;
289  }
290
291  /**
292   * Return the minimum number of connections the pool should maintain.
293   */
294  public int getMinConnections() {
295    return minConnections;
296  }
297
298  /**
299   * Set the minimum number of connections the pool should maintain.
300   */
301  public DataSourceConfig setMinConnections(int minConnections) {
302    this.minConnections = minConnections;
303    return this;
304  }
305
306  /**
307   * Return the maximum number of connections the pool can reach.
308   */
309  public int getMaxConnections() {
310    return maxConnections;
311  }
312
313  /**
314   * Set the maximum number of connections the pool can reach.
315   */
316  public DataSourceConfig setMaxConnections(int maxConnections) {
317    this.maxConnections = maxConnections;
318    return this;
319  }
320
321  /**
322   * Return the alert implementation to use.
323   */
324  public DataSourceAlert getAlert() {
325    return alert;
326  }
327
328  /**
329   * Set the alert implementation to use.
330   */
331  public DataSourceConfig setAlert(DataSourceAlert alert) {
332    this.alert = alert;
333    return this;
334  }
335
336  /**
337   * Return the listener to use.
338   */
339  public DataSourcePoolListener getListener() {
340    return listener;
341  }
342
343  /**
344   * Set the listener to use.
345   */
346  public DataSourceConfig setListener(DataSourcePoolListener listener) {
347    this.listener = listener;
348    return this;
349  }
350
351  /**
352   * Return a SQL statement used to test the database is accessible.
353   * <p>
354   * Note that if this is not set then it can get defaulted from the
355   * DatabasePlatform.
356   * </p>
357   */
358  public String getHeartbeatSql() {
359    return heartbeatSql;
360  }
361
362  /**
363   * Set a SQL statement used to test the database is accessible.
364   * <p>
365   * Note that if this is not set then it can get defaulted from the
366   * DatabasePlatform.
367   * </p>
368   */
369  public DataSourceConfig setHeartbeatSql(String heartbeatSql) {
370    this.heartbeatSql = heartbeatSql;
371    return this;
372  }
373
374  /**
375   * Return the heartbeat frequency in seconds.
376   * <p>
377   * This is the expected frequency in which the DataSource should be checked to
378   * make sure it is healthy and trim idle connections.
379   * </p>
380   */
381  public int getHeartbeatFreqSecs() {
382    return heartbeatFreqSecs;
383  }
384
385  /**
386   * Set the expected heartbeat frequency in seconds.
387   */
388  public DataSourceConfig setHeartbeatFreqSecs(int heartbeatFreqSecs) {
389    this.heartbeatFreqSecs = heartbeatFreqSecs;
390    return this;
391  }
392
393  /**
394   * Return the heart beat timeout in seconds.
395   */
396  public int getHeartbeatTimeoutSeconds() {
397    return heartbeatTimeoutSeconds;
398  }
399
400  /**
401   * Set the heart beat timeout in seconds.
402   */
403  public DataSourceConfig setHeartbeatTimeoutSeconds(int heartbeatTimeoutSeconds) {
404    this.heartbeatTimeoutSeconds = heartbeatTimeoutSeconds;
405    return this;
406  }
407
408  /**
409   * Return true if a stack trace should be captured when obtaining a connection
410   * from the pool.
411   * <p>
412   * This can be used to diagnose a suspected connection pool leak.
413   * </p>
414   * <p>
415   * Obviously this has a performance overhead.
416   * </p>
417   */
418  public boolean isCaptureStackTrace() {
419    return captureStackTrace;
420  }
421
422  /**
423   * Set to true if a stack trace should be captured when obtaining a connection
424   * from the pool.
425   * <p>
426   * This can be used to diagnose a suspected connection pool leak.
427   * </p>
428   * <p>
429   * Obviously this has a performance overhead.
430   * </p>
431   */
432  public DataSourceConfig setCaptureStackTrace(boolean captureStackTrace) {
433    this.captureStackTrace = captureStackTrace;
434    return this;
435  }
436
437  /**
438   * Return the max size for reporting stack traces on busy connections.
439   */
440  public int getMaxStackTraceSize() {
441    return maxStackTraceSize;
442  }
443
444  /**
445   * Set the max size for reporting stack traces on busy connections.
446   */
447  public DataSourceConfig setMaxStackTraceSize(int maxStackTraceSize) {
448    this.maxStackTraceSize = maxStackTraceSize;
449    return this;
450  }
451
452  /**
453   * Return the time in minutes after which a connection could be considered to
454   * have leaked.
455   */
456  public int getLeakTimeMinutes() {
457    return leakTimeMinutes;
458  }
459
460  /**
461   * Set the time in minutes after which a connection could be considered to
462   * have leaked.
463   */
464  public DataSourceConfig setLeakTimeMinutes(int leakTimeMinutes) {
465    this.leakTimeMinutes = leakTimeMinutes;
466    return this;
467  }
468
469  /**
470   * Return the size of the PreparedStatement cache (per connection).
471   */
472  public int getPstmtCacheSize() {
473    return pstmtCacheSize;
474  }
475
476  /**
477   * Set the size of the PreparedStatement cache (per connection).
478   */
479  public DataSourceConfig setPstmtCacheSize(int pstmtCacheSize) {
480    this.pstmtCacheSize = pstmtCacheSize;
481    return this;
482  }
483
484  /**
485   * Return the size of the CallableStatement cache (per connection).
486   */
487  public int getCstmtCacheSize() {
488    return cstmtCacheSize;
489  }
490
491  /**
492   * Set the size of the CallableStatement cache (per connection).
493   */
494  public DataSourceConfig setCstmtCacheSize(int cstmtCacheSize) {
495    this.cstmtCacheSize = cstmtCacheSize;
496    return this;
497  }
498
499  /**
500   * Return the time in millis to wait for a connection before timing out once
501   * the pool has reached its maximum size.
502   */
503  public int getWaitTimeoutMillis() {
504    return waitTimeoutMillis;
505  }
506
507  /**
508   * Set the time in millis to wait for a connection before timing out once the
509   * pool has reached its maximum size.
510   */
511  public DataSourceConfig setWaitTimeoutMillis(int waitTimeoutMillis) {
512    this.waitTimeoutMillis = waitTimeoutMillis;
513    return this;
514  }
515
516  /**
517   * Return the time in seconds a connection can be idle after which it can be
518   * trimmed from the pool.
519   * <p>
520   * This is so that the pool after a busy period can trend over time back
521   * towards the minimum connections.
522   * </p>
523   */
524  public int getMaxInactiveTimeSecs() {
525    return maxInactiveTimeSecs;
526  }
527
528  /**
529   * Return the maximum age a connection is allowed to be before it is closed.
530   * <p>
531   * This can be used to close really old connections.
532   * </p>
533   */
534  public int getMaxAgeMinutes() {
535    return maxAgeMinutes;
536  }
537
538  /**
539   * Set the maximum age a connection can be in minutes.
540   */
541  public DataSourceConfig setMaxAgeMinutes(int maxAgeMinutes) {
542    this.maxAgeMinutes = maxAgeMinutes;
543    return this;
544  }
545
546  /**
547   * Set the time in seconds a connection can be idle after which it can be
548   * trimmed from the pool.
549   * <p>
550   * This is so that the pool after a busy period can trend over time back
551   * towards the minimum connections.
552   * </p>
553   */
554  public DataSourceConfig setMaxInactiveTimeSecs(int maxInactiveTimeSecs) {
555    this.maxInactiveTimeSecs = maxInactiveTimeSecs;
556    return this;
557  }
558
559
560  /**
561   * Return the minimum time gap between pool trim checks.
562   * <p>
563   * This defaults to 59 seconds meaning that the pool trim check will run every
564   * minute assuming the heart beat check runs every 30 seconds.
565   * </p>
566   */
567  public int getTrimPoolFreqSecs() {
568    return trimPoolFreqSecs;
569  }
570
571  /**
572   * Set the minimum trim gap between pool trim checks.
573   */
574  public DataSourceConfig setTrimPoolFreqSecs(int trimPoolFreqSecs) {
575    this.trimPoolFreqSecs = trimPoolFreqSecs;
576    return this;
577  }
578
579  /**
580   * Return the pool listener.
581   */
582  public String getPoolListener() {
583    return poolListener;
584  }
585
586  /**
587   * Set a pool listener.
588   */
589  public DataSourceConfig setPoolListener(String poolListener) {
590    this.poolListener = poolListener;
591    return this;
592  }
593
594  /**
595   * Return true if the DataSource should be left offline.
596   * <p>
597   * This is to support DDL generation etc without having a real database.
598   * </p>
599   */
600  public boolean isOffline() {
601    return offline;
602  }
603
604  /**
605   * Return true (default) if the DataSource should be fail on start.
606   * <p>
607   * This enables to initialize the Ebean-Server if the db-server is not yet up.
608   * ({@link DataSourceAlert#dataSourceUp(javax.sql.DataSource)} is fired when DS gets up later.)
609   * </p>
610   */
611  public boolean isFailOnStart() {
612    return failOnStart;
613  }
614
615  /**
616   * Set to false, if DataSource should not fail on start. (e.g. DataSource is not available)
617   */
618  public DataSourceConfig setFailOnStart(boolean failOnStart) {
619    this.failOnStart = failOnStart;
620    return this;
621  }
622
623  /**
624   * Set to true if the DataSource should be left offline.
625   */
626  public DataSourceConfig setOffline(boolean offline) {
627    this.offline = offline;
628    return this;
629  }
630
631  /**
632   * Return a map of custom properties for the jdbc driver connection.
633   */
634  public Map<String, String> getCustomProperties() {
635    return customProperties;
636  }
637
638  /**
639   * Return a list of init queries, that are executed after a connection is opened.
640   */
641  public List<String> getInitSql() {
642    return initSql;
643  }
644
645  /**
646   * Set custom init queries for each query.
647   */
648  public DataSourceConfig setInitSql(List<String> initSql) {
649    this.initSql = initSql;
650    return this;
651  }
652
653  /**
654   * Set custom properties for the jdbc driver connection.
655   */
656  public DataSourceConfig setCustomProperties(Map<String, String> customProperties) {
657    this.customProperties = customProperties;
658    return this;
659  }
660
661  /**
662   * Return the database owner username.
663   */
664  public String getOwnerUsername() {
665    return ownerUsername;
666  }
667
668  /**
669   * Set the database owner username (used to create connection for use with InitDatabase).
670   */
671  public DataSourceConfig setOwnerUsername(String ownerUsername) {
672    this.ownerUsername = ownerUsername;
673    return this;
674  }
675
676  /**
677   * Return the database owner password.
678   */
679  public String getOwnerPassword() {
680    return ownerPassword;
681  }
682
683  /**
684   * Set the database owner password (used to create connection for use with InitDatabase).
685   */
686  public DataSourceConfig setOwnerPassword(String ownerPassword) {
687    this.ownerPassword = ownerPassword;
688    return this;
689  }
690
691  /**
692   * Return the database platform.
693   */
694  public String getPlatform() {
695    return platform;
696  }
697
698  /**
699   * Set the database platform (for use with ownerUsername and InitDatabase.
700   */
701  public DataSourceConfig setPlatform(String platform) {
702    this.platform = platform;
703    if (initDatabase != null) {
704      setInitDatabaseForPlatform(platform);
705    }
706    return this;
707  }
708
709  /**
710   * Return the InitDatabase to use with ownerUsername.
711   */
712  public InitDatabase getInitDatabase() {
713    return initDatabase;
714  }
715
716  /**
717   * Set the InitDatabase to use with ownerUsername.
718   */
719  public DataSourceConfig setInitDatabase(InitDatabase initDatabase) {
720    this.initDatabase = initDatabase;
721    return this;
722  }
723
724  /**
725   * Set InitDatabase based on the database platform.
726   */
727  public DataSourceConfig setInitDatabaseForPlatform(String platform) {
728    if (platform != null) {
729      switch (platform.toLowerCase()) {
730        case POSTGRES:
731          initDatabase = new PostgresInitDatabase();
732          break;
733      }
734    }
735    return this;
736  }
737
738  /**
739   * Return true if InitDatabase should be used (when the pool initialises and a connection can't be obtained).
740   *
741   * @return True to obtain a connection using ownerUsername and run InitDatabase.
742   */
743  public boolean useInitDatabase() {
744    if (ownerUsername != null && ownerPassword != null) {
745      if (initDatabase == null) {
746        // default to postgres
747        initDatabase = new PostgresInitDatabase();
748      }
749      return true;
750    }
751    return false;
752  }
753
754  /**
755   * Load the settings from the properties supplied.
756   * <p>
757   * You can use this when you have your own properties to use for configuration.
758   * </p>
759   *
760   * @param properties the properties to configure the dataSource
761   * @param serverName the name of the specific dataSource (optional)
762   */
763  public DataSourceConfig loadSettings(Properties properties, String serverName) {
764    ConfigPropertiesHelper dbProps = new ConfigPropertiesHelper("datasource", serverName, properties);
765    loadSettings(dbProps);
766    return this;
767  }
768
769  /**
770   * Load the settings from the PropertiesWrapper.
771   */
772  private void loadSettings(ConfigPropertiesHelper properties) {
773
774    username = properties.get("username", username);
775    password = properties.get("password", password);
776    schema = properties.get("schema", schema);
777    platform = properties.get("platform", platform);
778    ownerUsername = properties.get("ownerUsername", ownerUsername);
779    ownerPassword = properties.get("ownerPassword", ownerPassword);
780    if (initDatabase == null && platform != null) {
781      setInitDatabaseForPlatform(platform);
782    }
783
784    driver = properties.get("driver", properties.get("databaseDriver", driver));
785    url = properties.get("url", properties.get("databaseUrl", url));
786    autoCommit = properties.getBoolean("autoCommit", autoCommit);
787    readOnly = properties.getBoolean("readOnly", readOnly);
788    captureStackTrace = properties.getBoolean("captureStackTrace", captureStackTrace);
789    maxStackTraceSize = properties.getInt("maxStackTraceSize", maxStackTraceSize);
790    leakTimeMinutes = properties.getInt("leakTimeMinutes", leakTimeMinutes);
791    maxInactiveTimeSecs = properties.getInt("maxInactiveTimeSecs", maxInactiveTimeSecs);
792    trimPoolFreqSecs = properties.getInt("trimPoolFreqSecs", trimPoolFreqSecs);
793    maxAgeMinutes = properties.getInt("maxAgeMinutes", maxAgeMinutes);
794
795    minConnections = properties.getInt("minConnections", minConnections);
796    maxConnections = properties.getInt("maxConnections", maxConnections);
797    pstmtCacheSize = properties.getInt("pstmtCacheSize", pstmtCacheSize);
798    cstmtCacheSize = properties.getInt("cstmtCacheSize", cstmtCacheSize);
799
800    waitTimeoutMillis = properties.getInt("waitTimeout", waitTimeoutMillis);
801
802    heartbeatSql = properties.get("heartbeatSql", heartbeatSql);
803    heartbeatTimeoutSeconds = properties.getInt("heartbeatTimeoutSeconds", heartbeatTimeoutSeconds);
804    poolListener = properties.get("poolListener", poolListener);
805    offline = properties.getBoolean("offline", offline);
806
807    String isoLevel = properties.get("isolationLevel", getTransactionIsolationLevel(isolationLevel));
808    this.isolationLevel = getTransactionIsolationLevel(isoLevel);
809
810    this.initSql = parseSql(properties.get("initSql", null));
811    this.failOnStart = properties.getBoolean("failOnStart", failOnStart);
812
813    String customProperties = properties.get("customProperties", null);
814    if (customProperties != null && customProperties.length() > 0) {
815      this.customProperties = parseCustom(customProperties);
816    }
817  }
818
819  private List<String> parseSql(String sql) {
820    List<String> ret = new ArrayList<>();
821    if (sql != null) {
822      String[] queries = sql.split(";");
823      for (String query : queries) {
824        query = query.trim();
825        if (!query.isEmpty()) {
826          ret.add(query);
827        }
828      }
829    }
830    return ret;
831  }
832
833  Map<String, String> parseCustom(String customProperties) {
834
835    Map<String, String> propertyMap = new LinkedHashMap<String, String>();
836    String[] pairs = customProperties.split(";");
837    for (String pair : pairs) {
838      String[] split = pair.split("=");
839      if (split.length == 2) {
840        propertyMap.put(split[0], split[1]);
841      }
842    }
843    return propertyMap;
844  }
845
846  /**
847   * Return the isolation level description from the associated Connection int value.
848   */
849  private String getTransactionIsolationLevel(int level) {
850    switch (level) {
851      case Connection.TRANSACTION_NONE:
852        return "NONE";
853      case Connection.TRANSACTION_READ_COMMITTED:
854        return "READ_COMMITTED";
855      case Connection.TRANSACTION_READ_UNCOMMITTED:
856        return "READ_UNCOMMITTED";
857      case Connection.TRANSACTION_REPEATABLE_READ:
858        return "REPEATABLE_READ";
859      case Connection.TRANSACTION_SERIALIZABLE:
860        return "SERIALIZABLE";
861      default:
862        throw new RuntimeException("Transaction Isolation level [" + level + "] is not known.");
863    }
864  }
865
866  /**
867   * Return the isolation level for a given string description.
868   */
869  private int getTransactionIsolationLevel(String level) {
870    level = level.toUpperCase();
871    if (level.startsWith("TRANSACTION")) {
872      level = level.substring("TRANSACTION".length());
873    }
874    level = level.replace("_", "");
875    if ("NONE".equalsIgnoreCase(level)) {
876      return Connection.TRANSACTION_NONE;
877    }
878    if ("READCOMMITTED".equalsIgnoreCase(level)) {
879      return Connection.TRANSACTION_READ_COMMITTED;
880    }
881    if ("READUNCOMMITTED".equalsIgnoreCase(level)) {
882      return Connection.TRANSACTION_READ_UNCOMMITTED;
883    }
884    if ("REPEATABLEREAD".equalsIgnoreCase(level)) {
885      return Connection.TRANSACTION_REPEATABLE_READ;
886    }
887    if ("SERIALIZABLE".equalsIgnoreCase(level)) {
888      return Connection.TRANSACTION_SERIALIZABLE;
889    }
890
891    throw new RuntimeException("Transaction Isolation level [" + level + "] is not known.");
892  }
893}