Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ FROM eclipse-temurin:21.0.9_10-jre-noble AS smoketest-harness

COPY --from=builder /app/server/setup/server-lib /opt/engine/server-lib
COPY --from=builder /app/server/setup/extensions /opt/engine/extensions
COPY --from=builder /app/server/setup/conf /opt/engine/conf
COPY --from=builder /app/smoketest/build/install/smoketest-harness /harness

ENTRYPOINT ["/bin/bash", "/harness/run-harness.sh"]
Expand Down
9 changes: 9 additions & 0 deletions ci/configurations/alpine-temurin21-mysql.compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,12 @@ services:
timeout: 5s
retries: 30
start_period: 40s

# Lets the Database Connector metadata test reach this configuration's database. The URL is
# resolved by the oie service, not the harness, so it is the same one the engine uses.
harness:
environment:
OIE_DB_DRIVER: com.mysql.cj.jdbc.Driver
OIE_DB_URL: jdbc:mysql://db:3306/mirthdb
OIE_DB_USERNAME: mirthdb
OIE_DB_PASSWORD: mirthdb
11 changes: 10 additions & 1 deletion ci/configurations/alpine-temurin21-oracle.compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,13 @@ services:
interval: 3s
timeout: 5s
retries: 30
start_period: 40s
start_period: 40s

# Lets the Database Connector metadata test reach this configuration's database. The URL is
# resolved by the oie service, not the harness, so it is the same one the engine uses.
harness:
environment:
OIE_DB_DRIVER: oracle.jdbc.driver.OracleDriver
OIE_DB_URL: jdbc:oracle:thin:@//db:1521/FREEPDB1
OIE_DB_USERNAME: mirthdb
OIE_DB_PASSWORD: mirthdb
9 changes: 9 additions & 0 deletions ci/configurations/alpine-temurin21-postgres.compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,12 @@ services:
timeout: 5s
retries: 30
start_period: 40s

# Lets the Database Connector metadata test reach this configuration's database. The URL is
# resolved by the oie service, not the harness, so it is the same one the engine uses.
harness:
environment:
OIE_DB_DRIVER: org.postgresql.Driver
OIE_DB_URL: jdbc:postgresql://db:5432/mirthdb
OIE_DB_USERNAME: mirthdb
OIE_DB_PASSWORD: mirthdb
9 changes: 9 additions & 0 deletions ci/configurations/alpine-temurin21-sqlserver.compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,12 @@ services:
timeout: 5s
retries: 30
start_period: 40s

# Lets the Database Connector metadata test reach this configuration's database. The URL is
# resolved by the oie service, not the harness, so it is the same one the engine uses.
harness:
environment:
OIE_DB_DRIVER: net.sourceforge.jtds.jdbc.Driver
OIE_DB_URL: jdbc:jtds:sqlserver://db:1433/mirthdb
OIE_DB_USERNAME: sa
OIE_DB_PASSWORD: OieSqlServerPassw0rd!
9 changes: 9 additions & 0 deletions ci/configurations/ubuntu-temurin21-postgres.compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,12 @@ services:
timeout: 5s
retries: 30
start_period: 40s

# Lets the Database Connector metadata test reach this configuration's database. The URL is
# resolved by the oie service, not the harness, so it is the same one the engine uses.
harness:
environment:
OIE_DB_DRIVER: org.postgresql.Driver
OIE_DB_URL: jdbc:postgresql://db:5432/mirthdb
OIE_DB_USERNAME: mirthdb
OIE_DB_PASSWORD: mirthdb
5 changes: 5 additions & 0 deletions ci/run-harness.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@ mkdir -p "$results"
# Lock engine to junit-jupiter to prevent false-pass results.
status=0
java \
"@$ENGINE_HOME/conf/default_modules.vmoptions" \
-Doie.baseUrl="$OIE_BASE_URL" \
-Doie.configuration="$OIE_CONFIGURATION" \
-Doie.password="$OIE_PASSWORD" \
${OIE_DB_DRIVER:+-Doie.db.driver="$OIE_DB_DRIVER"} \
${OIE_DB_URL:+-Doie.db.url="$OIE_DB_URL"} \
${OIE_DB_USERNAME:+-Doie.db.username="$OIE_DB_USERNAME"} \
${OIE_DB_PASSWORD:+-Doie.db.password="$OIE_DB_PASSWORD"} \
${OIE_HARNESS_OPTS:-} \
-cp "$classpath" \
org.junit.platform.console.ConsoleLauncher execute \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,15 @@
package com.mirth.connect.connectors.jdbc;

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.regex.Matcher;

import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.SecurityContext;

import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

Expand All @@ -41,7 +31,6 @@

public class DatabaseConnectorServlet extends MirthServlet implements DatabaseConnectorServletInterface {

private static final String[] TABLE_TYPES = { "TABLE", "VIEW" };
private static final Logger logger = LogManager.getLogger(DatabaseConnectorServlet.class);
private static final TemplateValueReplacer replacer = new TemplateValueReplacer();
private static final ContextFactoryController contextFactoryController = ControllerFactory.getFactory().createContextFactoryController();
Expand All @@ -52,15 +41,16 @@ public DatabaseConnectorServlet(@Context HttpServletRequest request, @Context Se

@Override
public SortedSet<Table> getTables(String channelId, String channelName, String driver, String url, String username, String password, Set<String> tableNamePatterns, String selectLimit, Set<String> resourceIds) {
// selectLimit is deprecated and ignored for security reasons. Kept for backcompat.
selectLimit = null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on @pacmano1's question, can we deprecate this entire method and have it call an overloaded method that drops the selectLimit parameter?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pacmano1 / @tonygermano,

I can, I have a commit hanging around somewhere that does this, but it increases things substantially. I am happy to put a separate PR that strips the functionality in the interest of getting this merged. If you think this is blocking, lmk.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not blocking for me.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. I'll open after this closes. Only one more review needed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tonygermano, do you need this in this PR? We're good to merge, otherwise. I'll open the "clean up" as soon as this merges, but that's not urgent to merge.


CustomDriver customDriver = null;
Connection connection = null;
try {
url = replacer.replaceValues(url, channelId, channelName);
username = replacer.replaceValues(username, channelId, channelName);
password = replacer.replaceValues(password, channelId, channelName);

String schema = null;

try {
MirthContextFactory contextFactory = contextFactoryController.getContextFactory(resourceIds);

Expand Down Expand Up @@ -93,130 +83,8 @@ public SortedSet<Table> getTables(String channelId, String channelName, String d
}

DriverManager.setLoginTimeout(oldLoginTimeout);
DatabaseMetaData dbMetaData = connection.getMetaData();

// the sorted set to hold the table information
SortedSet<Table> tableInfoList = new TreeSet<Table>();

// Use a schema if the user name matches one of the schemas.
// Fix for Oracle: MIRTH-1045
ResultSet schemasResult = null;
try {
schemasResult = dbMetaData.getSchemas();
while (schemasResult.next()) {
String schemaResult = schemasResult.getString(1);
if (username.equalsIgnoreCase(schemaResult)) {
schema = schemaResult;
}
}
} finally {
if (schemasResult != null) {
schemasResult.close();
}
}

// based on the table name pattern, attempt to retrieve the table information
tableNamePatterns = translateTableNamePatterns(tableNamePatterns);
List<String> tableNameList = new ArrayList<String>();

// go through each possible table name patterns and query for the tables
for (String tableNamePattern : tableNamePatterns) {
ResultSet rs = null;
try {
rs = dbMetaData.getTables(null, schema, tableNamePattern, TABLE_TYPES);

// based on the result set, loop through to store the table name so it can be used to
// retrieve the table's column information
while (rs.next()) {
tableNameList.add(rs.getString("TABLE_NAME"));
}
} finally {
if (rs != null) {
rs.close();
}
}
}

// for each table, grab their column information
for (String tableName : tableNameList) {
ResultSet rs = null;
ResultSet backupRs = null;
boolean fallback = false;
try {
// apparently it's much more efficient to use ResultSetMetaData to retrieve
// column information. So each driver is defined with their own unique SELECT
// statement to query the table columns and use ResultSetMetaData to retrieve
// the column information. If driver is not defined with the select statement
// then we'll define to the generic method of getting column information, but
// this could be extremely slow
List<Column> columnList = new ArrayList<Column>();
if (StringUtils.isEmpty(selectLimit)) {
logger.debug("No select limit is defined, using generic method");
rs = dbMetaData.getColumns(null, null, tableName, null);

// retrieve all relevant column information
for (int i = 0; rs.next(); i++) {
Column column = new Column(rs.getString("COLUMN_NAME"), rs.getString("TYPE_NAME"), rs.getInt("COLUMN_SIZE"));
columnList.add(column);
}
} else {
logger.debug("Select limit is defined, using specific select query : '" + selectLimit + "'");

// replace the '?' with the appropriate schema.table name, and use ResultSetMetaData to
// retrieve column information
final String schemaTableName = StringUtils.isNotEmpty(schema) ? "\"" + schema + "\".\"" + tableName + "\"" : "\"" + tableName + "\"";
final String queryString = selectLimit.trim().replaceAll("\\?", Matcher.quoteReplacement(schemaTableName));
Statement statement = connection.createStatement();
try {
rs = statement.executeQuery(queryString);
ResultSetMetaData rsmd = rs.getMetaData();

// retrieve all relevant column information
for (int i = 1; i < rsmd.getColumnCount() + 1; i++) {
Column column = new Column(rsmd.getColumnName(i), rsmd.getColumnTypeName(i), rsmd.getPrecision(i));
columnList.add(column);
}
} catch (SQLException sqle) {
logger.info("Failed to execute '" + queryString + "', fall back to generic approach to retrieve column information");
fallback = true;
} finally {
if (statement != null) {
statement.close();
}
}

// failed to use selectLimit method, so we need to fall back to generic
// if this generic approach fails, then there's nothing we can do
if (fallback) {
// Re-initialize in case some columns were added before failing
columnList = new ArrayList<Column>();

logger.debug("Using fallback method for retrieving columns");
backupRs = dbMetaData.getColumns(null, null, tableName.replace("/", "//"), null);

// retrieve all relevant column information
while (backupRs.next()) {
Column column = new Column(backupRs.getString("COLUMN_NAME"), backupRs.getString("TYPE_NAME"), backupRs.getInt("COLUMN_SIZE"));
columnList.add(column);
}
}
}

// create table object and add to the list of table definitions
Table table = new Table(tableName, columnList);
tableInfoList.add(table);
} finally {
if (rs != null) {
rs.close();
}

if (backupRs != null) {
backupRs.close();
}
}
}

return tableInfoList;
return TableMetadataReader.getTables(connection, tableNamePatterns, username);
} catch (Exception e) {
throw new MirthApiException(new Exception("Could not retrieve database tables and columns.", e));
} finally {
Expand All @@ -229,38 +97,4 @@ public SortedSet<Table> getTables(String channelId, String channelName, String d
}
}

/**
* Translate the given pattern expression so that it can be used properly for searching tables
* in the database. Multiple table name patterns are delimited by comma (,)
* <p>
* This interpret and translate to the following:
* <p>
* <ul>
* <li>"*" = wild card for more than one character, will be converted to be used as '%'</li>
* <li>"_" = one character wild card</li>
* <li>"" = empty string will retrieve all tables
* </ul>
* <p>
* <i>Eg. rad*,table*test => Find all tables starts with 'rad' AND tables prefix with 'table'
* and postfix with 'test'</i>
*
* @param tableNamePatternExpression
* pattern expression to translate, cannot be NULL.
* @return If table name pattern is an empty string, it'll never return NULL.
*/
private Set<String> translateTableNamePatterns(Set<String> tableNamePatterns) {
if (tableNamePatterns == null) {
throw new IllegalArgumentException("Parameter 'tableNamePatterns' cannot be NULL'");
}

Set<String> patterns = new HashSet<String>();
if (tableNamePatterns.isEmpty()) {
patterns.add("%");
} else {
for (String pattern : tableNamePatterns) {
patterns.add(pattern.trim().replaceAll("\\*", "%"));
}
}
return patterns;
}
}
Loading
Loading