Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,4 @@ At this point you have the application running and admin account operational. To
and at least one CDM database. They are covered in the respective documentation sections.

## License
OHDSI WebAPI is licensed under Apache License 2.0
OHDSI WebAPI is licensed under Apache License 2.0
38 changes: 38 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,11 @@

<!-- Shiny -->
<shiny.enabled>false</shiny.enabled>

<!-- Trexsql -->
<trexsql.enabled>false</trexsql.enabled>
<trexsql.cache-path>./data/cache</trexsql.cache-path>
<trexsql.extensions-path></trexsql.extensions-path>
</properties>

<build>
Expand Down Expand Up @@ -420,6 +425,7 @@
<configuration>
<!-- Since we filter properties files via Maven, do not add resources. -->
<addResources>false</addResources>
<includeSystemScope>false</includeSystemScope>
<mainClass>org.ohdsi.webapi.WebApi</mainClass>
<jvmArguments>
--add-opens java.base/java.lang=ALL-UNNAMED
Expand Down Expand Up @@ -460,6 +466,9 @@
--add-opens java.naming/com.sun.jndi.ldap=ALL-UNNAMED
--add-exports java.naming/com.sun.jndi.ldap=ALL-UNNAMED
</argLine>
<systemPropertyVariables>
<org.springframework.boot.logging.LoggingSystem>org.springframework.boot.logging.log4j2.Log4J2LoggingSystem</org.springframework.boot.logging.LoggingSystem>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
Expand All @@ -482,6 +491,9 @@
<version>3.5.2</version>
<configuration>
<skipITs>${skipITtests}</skipITs>
<systemPropertyVariables>
<org.springframework.boot.logging.LoggingSystem>org.springframework.boot.logging.log4j2.Log4J2LoggingSystem</org.springframework.boot.logging.LoggingSystem>
</systemPropertyVariables>
</configuration>
<executions>
<execution>
Expand Down Expand Up @@ -559,6 +571,10 @@
<name>repo.ohdsi.org</name>
<url>https://repo.ohdsi.org/nexus/content/groups/public</url>
</repository>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>

<pluginRepositories>
Expand Down Expand Up @@ -1240,9 +1256,31 @@
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>com.github.p-hoffmann</groupId>
<artifactId>trexsql-ext</artifactId>
<version>v0.1.2</version>
</dependency>
</dependencies>

<profiles>
<profile>
<id>tcache</id>
<properties>
<trexsql.enabled>true</trexsql.enabled>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<includeSystemScope>true</includeSystemScope>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>webapi-oracle</id>
<properties>
Expand Down
1 change: 0 additions & 1 deletion src/main/java/org/ohdsi/webapi/WebApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
/**
* OHDSI WebAPI Spring Boot Application
*
* Supports both JAR and WAR deployment:
* - JAR: java -jar WebAPI.jar (embedded Tomcat)
* - WAR: Deploy to external servlet container (mvn package -Pwar)
*/
Expand Down
65 changes: 65 additions & 0 deletions src/main/java/org/ohdsi/webapi/trexsql/TrexSQLConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.ohdsi.webapi.trexsql;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
* Configuration properties for trexsql integration.
* Maps to trexsql.* in application properties.
*/
@Configuration
@ConfigurationProperties(prefix = "trexsql")
public class TrexSQLConfig {

private boolean enabled = false;
private String cachePath = "./data/cache";
private String extensionsPath;
private Map<String, TrexSQLSourceConfig> sources = new HashMap<>();

public boolean isEnabled() {
return enabled;
}

public void setEnabled(boolean enabled) {
this.enabled = enabled;
}

public String getCachePath() {
return cachePath;
}

public void setCachePath(String cachePath) {
this.cachePath = cachePath;
}

public String getExtensionsPath() {
return extensionsPath;
}

public void setExtensionsPath(String extensionsPath) {
this.extensionsPath = extensionsPath;
}

public Map<String, TrexSQLSourceConfig> getSources() {
return sources;
}

public void setSources(Map<String, TrexSQLSourceConfig> sources) {
this.sources = sources;
}

public TrexSQLSourceConfig getSourceConfig(String sourceKey) {
return sources.get(sourceKey);
}

public boolean isEnabledForSource(String sourceKey) {
if (!enabled) {
return false;
}
TrexSQLSourceConfig sourceConfig = sources.get(sourceKey);
return sourceConfig != null && sourceConfig.isEnabled();
}
}
105 changes: 105 additions & 0 deletions src/main/java/org/ohdsi/webapi/trexsql/TrexSQLInstanceManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package org.ohdsi.webapi.trexsql;

import org.trex.Trexsql;
import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

/**
* Singleton manager for the TrexSQL instance.
* Provides lazy initialization and graceful shutdown.
*/
@Component
@ConditionalOnProperty(name = "trexsql.enabled", havingValue = "true", matchIfMissing = false)
public class TrexSQLInstanceManager {

private static final Logger log = LoggerFactory.getLogger(TrexSQLInstanceManager.class);

private final TrexSQLConfig config;
private volatile Object trexsqlDb = null;
private final ReentrantLock initLock = new ReentrantLock();

public TrexSQLInstanceManager(TrexSQLConfig config) {
this.config = config;
}

public Object getInstance() {
if (!config.isEnabled()) {
throw new IllegalStateException("TrexSQL is not enabled");
}

if (trexsqlDb == null) {
initLock.lock();
try {
if (trexsqlDb == null) {
log.info("Initializing TrexSQL instance");
trexsqlDb = Trexsql.init(buildConfig());
log.info("TrexSQL instance initialized successfully");
}
} finally {
initLock.unlock();
}
}
return trexsqlDb;
}

public boolean isAvailable() {
if (!config.isEnabled() || trexsqlDb == null) {
return false;
}
try {
return Trexsql.isRunning(trexsqlDb);
} catch (Exception e) {
log.warn("Error checking TrexSQL status: {}", e.getMessage());
return false;
}
}

public boolean isAttached(String databaseCode) {
if (trexsqlDb == null) {
return false;
}
try {
return Trexsql.isAttached(trexsqlDb, databaseCode);
} catch (Exception e) {
log.warn("Error checking if database {} is attached: {}", databaseCode, e.getMessage());
return false;
}
}

private Map<String, Object> buildConfig() {
Map<String, Object> initConfig = new HashMap<>();

if (config.getExtensionsPath() != null && !config.getExtensionsPath().isEmpty()) {
initConfig.put("extensions-path", config.getExtensionsPath());
}

return initConfig;
}

@PreDestroy
public void shutdown() {
initLock.lock();
try {
if (trexsqlDb != null) {
log.info("Shutting down TrexSQL instance");
try {
Trexsql.shutdown(trexsqlDb);
log.info("TrexSQL instance shut down successfully");
} catch (Exception e) {
log.error("Error shutting down TrexSQL instance: {}", e.getMessage(), e);
} finally {
trexsqlDb = null;
}
}
} finally {
initLock.unlock();
}
}
}
122 changes: 122 additions & 0 deletions src/main/java/org/ohdsi/webapi/trexsql/TrexSQLSearchProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package org.ohdsi.webapi.trexsql;

import org.ohdsi.vocabulary.Concept;
import org.ohdsi.vocabulary.SearchProvider;
import org.ohdsi.vocabulary.SearchProviderConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

/**
* SearchProvider implementation using TrexSQL.
*/
@Component
@ConditionalOnProperty(name = "trexsql.enabled", havingValue = "true", matchIfMissing = false)
public class TrexSQLSearchProvider implements SearchProvider {

private static final Logger log = LoggerFactory.getLogger(TrexSQLSearchProvider.class);

private static final int TREXSQL_PRIORITY = 1;

private final TrexSQLService trexsqlService;
private final TrexSQLConfig config;

public TrexSQLSearchProvider(TrexSQLService trexsqlService, TrexSQLConfig config) {
this.trexsqlService = trexsqlService;
this.config = config;
}

@Override
public boolean supports(String vocabularyVersionKey) {
return config.isEnabled()
&& trexsqlService.isEnabledForSource(vocabularyVersionKey)
&& trexsqlService.isCacheAvailable(vocabularyVersionKey);
}

@Override
public int getPriority() {
return TREXSQL_PRIORITY;
}

@Override
public Collection<Concept> executeSearch(SearchProviderConfig searchConfig, String query, String rows) throws Exception {
String sourceKey = searchConfig.getSourceKey();

if (!trexsqlService.isEnabledForSource(sourceKey)) {
log.debug("TrexSQL not enabled for source {}", sourceKey);
throw new IllegalStateException("TrexSQL not enabled for source: " + sourceKey);
}

if (!trexsqlService.isCacheAvailable(sourceKey)) {
log.debug("Cache not available for source {}", sourceKey);
throw new IllegalStateException("TrexSQL cache not available for source: " + sourceKey);
}

int maxRows = parseRows(rows);
log.debug("TrexSQL search for source {} with query: {}", sourceKey, query);

try {
List<Map<String, Object>> results = trexsqlService.searchVocab(sourceKey, query, maxRows);
return mapToConcepts(results);
} catch (Exception e) {
log.error("TrexSQL search failed for source {}: {}", sourceKey, e.getMessage(), e);
throw new RuntimeException("TrexSQL search failed: " + e.getMessage(), e);
}
}

private int parseRows(String rows) {
if (rows == null || rows.isEmpty()) {
return 1000;
}
try {
return Integer.parseInt(rows);
} catch (NumberFormatException e) {
return 1000;
}
}

private Collection<Concept> mapToConcepts(List<Map<String, Object>> results) {
List<Concept> concepts = new ArrayList<>();

for (Map<String, Object> row : results) {
Concept concept = new Concept();

Object conceptId = row.get("concept_id");
if (conceptId != null) {
concept.conceptId = ((Number) conceptId).longValue();
}

concept.conceptName = (String) row.get("concept_name");
concept.domainId = (String) row.get("domain_id");
concept.vocabularyId = (String) row.get("vocabulary_id");
concept.conceptClassId = (String) row.get("concept_class_id");
concept.standardConcept = (String) row.get("standard_concept");
concept.conceptCode = (String) row.get("concept_code");
concept.invalidReason = (String) row.get("invalid_reason");

Object validStartDate = row.get("valid_start_date");
if (validStartDate instanceof java.sql.Date) {
concept.validStartDate = new java.util.Date(((java.sql.Date) validStartDate).getTime());
} else if (validStartDate instanceof java.util.Date) {
concept.validStartDate = (java.util.Date) validStartDate;
}

Object validEndDate = row.get("valid_end_date");
if (validEndDate instanceof java.sql.Date) {
concept.validEndDate = new java.util.Date(((java.sql.Date) validEndDate).getTime());
} else if (validEndDate instanceof java.util.Date) {
concept.validEndDate = (java.util.Date) validEndDate;
}

concepts.add(concept);
}

return concepts;
}
}
Loading
Loading