5 Commits

111 changed files with 1496 additions and 1853 deletions

BIN
.DS_Store vendored

Binary file not shown.

1
.gitignore vendored
View File

@@ -33,3 +33,4 @@
/cygnus-installer/src/test
/cygnus-cloud-service/target
/cygnus-installer/src/target
/cygnus-installer/target

5
.vscode/launch.json vendored
View File

@@ -59,14 +59,15 @@
"REDIS_HOST": "103.125.129.116",
"REDIS_PORT": "7901",
"REDIS_PASSWORD": "M@triXR3d1s@6202",
"REDIS_DATABASE": "1",
"REDIS_SSL": "false",
"CYGNUS_CLOUD_BASE_URL": "http://localhost:8090",
"CYGNUS_TOKEN_URL": "http://localhost:8090/oauth2/token",
"CYGNUS_CLIENT_ID": "matrix",
"CYGNUS_INSTALLATION_ID": "matrix-delhi-cygnus-01",
"CYGNUS_CLIENT_ASSERTION": "file:${workspaceFolder}/matrix-installation/config/machine-assertion.jwt",
"CYGNUS_CLIENT_ASSERTION": "file:${workspaceFolder}/config/clients/matrix/matrix-matrix-delhi-cygnus-01-assertion.jwt",
"CYGNUS_LOGIN_KEY_ID": "cygnus-login-2026-01",
"CYGNUS_LOGIN_PUBLIC_KEY": "file:${workspaceFolder}/matrix-installation/config/keys/login-public.pem",
"CYGNUS_LOGIN_PUBLIC_KEY": "file:${workspaceFolder}/config/keys/login-public.pem",
"CYGNUS_CLOUD_REQUEST_TIMEOUT": "PT10S",
"CYGNUS_TOKEN_REFRESH_SKEW": "PT30S"
},

View File

@@ -1,6 +1,7 @@
package com.cygnus.client;
import com.cygnus.client.model.CloudIdentitySession;
import com.cygnus.client.model.CloudQueryResponse;
import com.cygnus.client.model.LoginPayload;
import com.cygnus.client.security.LoginEnvelopeEncryptor;
import com.cygnus.client.security.MachineTokenProvider;
@@ -50,4 +51,15 @@ public class CloudIdentityClient {
.bodyToMono(CloudIdentitySession.class))
.timeout(properties.requestTimeout());
}
public Mono<CloudQueryResponse> fetchQuery(int queryId) {
return tokenProvider.accessToken()
.flatMap(token -> webClient.get()
.uri(properties.baseUri().resolve("/api/v1/queries/" + queryId))
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(CloudQueryResponse.class))
.timeout(properties.requestTimeout());
}
}

View File

@@ -0,0 +1,4 @@
package com.cygnus.client.model;
public record CloudQueryResponse(int queryId, String query) {
}

View File

@@ -0,0 +1,4 @@
package com.cygnus.cloud.query;
public record CloudQuery(int queryId, String query) {
}

View File

@@ -0,0 +1,28 @@
package com.cygnus.cloud.query;
import jakarta.validation.constraints.Min;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@Validated
@RestController
@RequestMapping("/api/v1/queries")
public class CloudQueryController {
private final QueryCatalogRepository repository;
public CloudQueryController(QueryCatalogRepository repository) {
this.repository = repository;
}
@GetMapping("/{queryId}")
public Mono<CloudQuery> query(
@PathVariable @Min(1) int queryId) {
return repository.findEnabled(queryId)
.switchIfEmpty(Mono.error(new QueryNotFoundException(queryId)));
}
}

View File

@@ -0,0 +1,48 @@
package com.cygnus.cloud.query;
import com.cygnus.cloud.database.ReactiveDatabaseClient;
import io.vertx.sqlclient.Tuple;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Mono;
@Repository
public class QueryCatalogRepository {
private static final String INITIALIZE = """
CREATE SCHEMA IF NOT EXISTS platform;
CREATE TABLE IF NOT EXISTS platform.application_query (
query_id integer PRIMARY KEY,
query_text text NOT NULL,
enabled boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT current_timestamp,
updated_at timestamptz NOT NULL DEFAULT current_timestamp,
CONSTRAINT ck_platform_application_query_id
CHECK (query_id > 0),
CONSTRAINT ck_platform_application_query_text
CHECK (length(btrim(query_text)) > 0)
)
""";
private static final String FIND = """
SELECT query_id, query_text
FROM platform.application_query
WHERE query_id = $1
AND enabled = true
""";
private final ReactiveDatabaseClient database;
public QueryCatalogRepository(ReactiveDatabaseClient database) {
this.database = database;
}
public Mono<Void> initialize() {
return database.query(INITIALIZE).then();
}
public Mono<CloudQuery> findEnabled(int queryId) {
return database.preparedQuery(FIND, Tuple.of(queryId))
.flatMapMany(rows -> reactor.core.publisher.Flux.fromIterable(rows))
.next()
.map(row -> new CloudQuery(
row.getInteger("query_id"), row.getString("query_text")));
}
}

View File

@@ -0,0 +1,22 @@
package com.cygnus.cloud.query;
import java.time.Duration;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component
public class QueryCatalogSchemaInitializer implements ApplicationRunner {
private final QueryCatalogRepository repository;
public QueryCatalogSchemaInitializer(QueryCatalogRepository repository) {
this.repository = repository;
}
@Override
public void run(ApplicationArguments arguments) {
repository.initialize()
.block(Duration.ofMinutes(2));
}
}

View File

@@ -0,0 +1,12 @@
package com.cygnus.cloud.query;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class QueryNotFoundException extends RuntimeException {
public QueryNotFoundException(int queryId) {
super("Query was not found: " + queryId);
}
}

View File

@@ -36,6 +36,8 @@ public class CloudSecurityConfiguration {
.permitAll()
.pathMatchers("/api/v1/identity/login")
.hasAuthority("SCOPE_identity.login")
.pathMatchers("/api/v1/queries/**")
.hasAuthority("SCOPE_identity.login")
.pathMatchers("/api/v1/admin/**")
.hasAuthority("SCOPE_cygnus.admin")
.anyExchange().authenticated())

View File

@@ -0,0 +1,13 @@
CREATE SCHEMA IF NOT EXISTS platform;
CREATE TABLE IF NOT EXISTS platform.application_query (
query_id integer PRIMARY KEY,
query_text text NOT NULL,
enabled boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT current_timestamp,
updated_at timestamptz NOT NULL DEFAULT current_timestamp,
CONSTRAINT ck_platform_application_query_id
CHECK (query_id > 0),
CONSTRAINT ck_platform_application_query_text
CHECK (length(btrim(query_text)) > 0)
);

View File

@@ -0,0 +1,25 @@
DO $$
DECLARE
current_type text;
BEGIN
SELECT data_type
INTO current_type
FROM information_schema.columns
WHERE table_schema = 'platform'
AND table_name = 'application_query'
AND column_name = 'query_id';
IF current_type IN ('character varying', 'text') THEN
ALTER TABLE platform.application_query
DROP CONSTRAINT IF EXISTS ck_platform_application_query_id;
ALTER TABLE platform.application_query
ALTER COLUMN query_id TYPE integer
USING regexp_replace(query_id, '^Query', '')::integer;
ALTER TABLE platform.application_query
ADD CONSTRAINT ck_platform_application_query_id
CHECK (query_id > 0);
END IF;
END
$$;

View File

@@ -30,6 +30,7 @@ public class DeploymentWriter {
try {
Path normalizedOutput = output.toAbsolutePath().normalize();
Path config = normalizedOutput.resolve("config");
protectCloudConfiguration(config, profile);
Path keyDirectory = config.resolve("keys");
Files.createDirectories(keyDirectory);
@@ -82,6 +83,23 @@ public class DeploymentWriter {
}
}
private void protectCloudConfiguration(Path deploymentConfig, ProductProfile profile) {
if (isInside(deploymentConfig, profile.assertionEncryptionPublicKey())
|| isInside(deploymentConfig, profile.loginEncryptionPublicKey())) {
throw new IllegalArgumentException(
"Refusing to write deployment files over the cloud-service "
+ "configuration directory");
}
}
private boolean isInside(Path directory, Path file) {
if (file == null) {
return false;
}
return file.toAbsolutePath().normalize().startsWith(
directory.toAbsolutePath().normalize());
}
private String installationConfiguration(
UUID installationUuid,
String installationCode,
@@ -174,6 +192,7 @@ public class DeploymentWriter {
appendEnvironment(env, "REDIS_HOST", runtime.redisHost());
appendEnvironment(env, "REDIS_PORT", runtime.redisPort());
appendEnvironment(env, "REDIS_PASSWORD", runtime.redisPassword());
appendEnvironment(env, "REDIS_DATABASE", "1");
appendEnvironment(env, "REDIS_SSL", Boolean.toString(runtime.redisSsl()));
appendEnvironment(
env, "CYGNUS_CLOUD_BASE_URL", normalizedCloudServiceUrl(runtime));

View File

@@ -156,6 +156,7 @@ public class InstallationService {
if (request.outputDirectory() == null) {
throw new IllegalArgumentException("Output directory is required");
}
validateOutputIsolation(request.outputDirectory());
RuntimeConfiguration runtime = request.runtimeConfiguration();
if (runtime == null) {
throw new IllegalArgumentException("Runtime configuration is required");
@@ -204,6 +205,27 @@ public class InstallationService {
}
}
private void validateOutputIsolation(Path outputDirectory) {
Path deploymentConfig = outputDirectory
.toAbsolutePath()
.normalize()
.resolve("config");
if (isInside(deploymentConfig, profile.assertionEncryptionPublicKey())
|| isInside(deploymentConfig, profile.loginEncryptionPublicKey())) {
throw new IllegalArgumentException(
"Output directory would overwrite the cloud-service config folder. "
+ "Select a dedicated deployment directory, such as "
+ "'matrix-installation'.");
}
}
private boolean isInside(Path directory, Path file) {
if (file == null) {
return false;
}
return file.toAbsolutePath().normalize().startsWith(directory);
}
private String normalizedCloudServiceUrl(String value) {
String normalized = value.trim();
return normalized.endsWith("/")

View File

@@ -1,6 +1,7 @@
package com.cygnus.installer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.net.URI;
import java.nio.file.Files;
@@ -108,6 +109,7 @@ class DeploymentWriterTest {
.contains("MATRIX_IMAGE=\"registry.example.com/matrix:1.0.0\"")
.contains("MATRIX_DB_URL=\"jdbc:postgresql://db:5432/matrix\"")
.contains("REDIS_HOST=\"redis\"")
.contains("REDIS_DATABASE=\"1\"")
.contains("CYGNUS_CLOUD_BASE_URL=\"http://host.docker.internal:8090\"")
.contains("CYGNUS_TOKEN_URL=\"http://host.docker.internal:8090/oauth2/token\"")
.contains("CYGNUS_CLIENT_ID=\"matrix-client\"")
@@ -121,4 +123,84 @@ class DeploymentWriterTest {
assertThat(output.resolve("config/keys/client-signing-private.pem"))
.isRegularFile();
}
@Test
void refusesToOverwriteCloudServiceConfiguration() throws Exception {
Path cloudConfig = temporaryDirectory.resolve("config");
Path cloudKeys = cloudConfig.resolve("keys");
Files.createDirectories(cloudKeys);
Path assertionPublic = cloudKeys.resolve("assertion-decryption-public.pem");
Path loginPublic = cloudKeys.resolve("login-public.pem");
Files.writeString(assertionPublic, "assertion-public-key");
Files.writeString(loginPublic, "login-public-key");
var profile = new ProductProfile(
"matrix",
"Matrix",
"matrix-onprem",
"MATRIX_IMAGE",
"matrix",
"installation.yml",
"MATRIX_INSTALLATION_CONFIG",
"/srv/matrix/config/installation.yml",
"8080:8080",
"/matrix/",
"/oauth2/token",
assertionPublic,
loginPublic,
"cygnus-login-2026-01",
List.of());
assertThatThrownBy(() -> new DeploymentWriter().write(
temporaryDirectory,
UUID.randomUUID(),
"primary",
"matrix-client",
new ActivationDtos.ValidationResponse(
"activation-token",
OffsetDateTime.now().plusMinutes(5),
UUID.randomUUID(),
"matrix-client",
"FULL",
2),
new ActivationDtos.RegistrationResponse(
UUID.randomUUID(),
UUID.randomUUID(),
UUID.randomUUID(),
null,
"primary",
1,
"ACTIVE"),
new InstallationKeyService().generate(),
"assertion",
new InstallerSettings(
"matrix",
URI.create("https://cloud.example.com"),
URI.create("https://cloud.example.com"),
"/api/v1/installations",
"production",
temporaryDirectory,
temporaryDirectory,
"1",
new IniDocument(Map.of())),
profile,
"registry.example.com/matrix:1.0.0",
Map.of(),
new RuntimeConfiguration(
"https://cloud.example.com",
"jdbc:postgresql://db/matrix",
"postgres",
"secret",
"redis",
"7901",
"secret",
false,
"PT10S",
"PT30S")))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("cloud-service configuration");
assertThat(assertionPublic).hasContent("assertion-public-key");
assertThat(loginPublic).hasContent("login-public-key");
}
}

View File

@@ -1,12 +0,0 @@
spring:
main:
banner-mode: "off"
application:
name: technobee-service-installer
technobee:
installer:
config: ${TECHNOBEE_INSTALLER_CONFIG:./installer.ini}
version: ${TECHNOBEE_INSTALLER_VERSION:1.0.0}
logging:
level:
root: WARN

View File

@@ -1,3 +0,0 @@
artifactId=technobee-service-installer
groupId=com.cygnus
version=1.0.0-SNAPSHOT

View File

@@ -1,36 +0,0 @@
com/cygnus/installer/MachineAssertionService.class
com/cygnus/installer/ActivationDtos$ValidationResponse.class
com/cygnus/installer/SwingInstallationWizard$2.class
com/cygnus/installer/CommandResult.class
com/cygnus/installer/InstallationProgressListener.class
com/cygnus/installer/ProductProfileRegistry.class
com/cygnus/installer/InstallationRequest.class
com/cygnus/installer/InstallerConfiguration.class
com/cygnus/installer/ProductProfile.class
com/cygnus/installer/InstallationWizard.class
com/cygnus/installer/TechnobeeServiceInstallerApplication.class
com/cygnus/installer/DeploymentWriter.class
com/cygnus/installer/InstallationKeyService.class
com/cygnus/installer/DockerComposeDeploymentService.class
com/cygnus/installer/ActivationClient.class
com/cygnus/installer/ActivationDtos$RegistrationResponse.class
com/cygnus/installer/IniDocument.class
com/cygnus/installer/SwingInstallationWizard$3.class
com/cygnus/installer/InstallerConfigurationException.class
com/cygnus/installer/DockerRegistryLoginService.class
com/cygnus/installer/SwingInstallationWizard.class
com/cygnus/installer/ActivationDtos.class
com/cygnus/installer/DockerPrerequisiteChecker.class
com/cygnus/installer/InstallerBootstrapProperties.class
com/cygnus/installer/SwingInstallationWizard$1.class
com/cygnus/installer/CommandExecutor.class
com/cygnus/installer/IniConfigurationLoader.class
com/cygnus/installer/InstallationService.class
com/cygnus/installer/InstallationResult.class
com/cygnus/installer/RuntimeConfiguration.class
com/cygnus/installer/ActivationDtos$RegistrationRequest.class
com/cygnus/installer/ActivationDtos$ValidationRequest.class
com/cygnus/installer/PrerequisiteException.class
com/cygnus/installer/InstallerSettings.class
com/cygnus/installer/InstallationKeyPair.class
com/cygnus/installer/SwingInstallationWizard$ProgressUpdate.class

View File

@@ -1,28 +0,0 @@
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/ActivationClient.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/ActivationDtos.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/CommandExecutor.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/CommandResult.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/DeploymentWriter.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/DockerComposeDeploymentService.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/DockerPrerequisiteChecker.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/DockerRegistryLoginService.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/IniConfigurationLoader.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/IniDocument.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallationKeyPair.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallationKeyService.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallationProgressListener.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallationRequest.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallationResult.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallationService.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallationWizard.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallerBootstrapProperties.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallerConfiguration.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallerConfigurationException.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/InstallerSettings.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/MachineAssertionService.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/PrerequisiteException.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/ProductProfile.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/ProductProfileRegistry.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/RuntimeConfiguration.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/SwingInstallationWizard.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/main/java/com/cygnus/installer/TechnobeeServiceInstallerApplication.java

View File

@@ -1,11 +0,0 @@
com/cygnus/installer/MachineAssertionServiceTest.class
com/cygnus/installer/DockerRegistryLoginServiceTest$CapturingExecutor.class
com/cygnus/installer/DockerRegistryLoginServiceTest.class
com/cygnus/installer/DockerComposeDeploymentServiceTest$CapturingExecutor.class
com/cygnus/installer/InstallationServiceTest.class
com/cygnus/installer/DockerPrerequisiteCheckerTest$1.class
com/cygnus/installer/ProductProfileRegistryTest.class
com/cygnus/installer/IniConfigurationLoaderTest.class
com/cygnus/installer/DockerComposeDeploymentServiceTest.class
com/cygnus/installer/DeploymentWriterTest.class
com/cygnus/installer/DockerPrerequisiteCheckerTest.class

View File

@@ -1,8 +0,0 @@
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/test/java/com/cygnus/installer/DeploymentWriterTest.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/test/java/com/cygnus/installer/DockerComposeDeploymentServiceTest.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/test/java/com/cygnus/installer/DockerPrerequisiteCheckerTest.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/test/java/com/cygnus/installer/DockerRegistryLoginServiceTest.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/test/java/com/cygnus/installer/IniConfigurationLoaderTest.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/test/java/com/cygnus/installer/InstallationServiceTest.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/test/java/com/cygnus/installer/MachineAssertionServiceTest.java
/Users/maddy/Projects/cygnus/matrix/cygnus-installer/src/test/java/com/cygnus/installer/ProductProfileRegistryTest.java

View File

@@ -1,4 +0,0 @@
-------------------------------------------------------------------------------
Test set: com.cygnus.installer.DeploymentWriterTest
-------------------------------------------------------------------------------
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.174 s -- in com.cygnus.installer.DeploymentWriterTest

View File

@@ -1,4 +0,0 @@
-------------------------------------------------------------------------------
Test set: com.cygnus.installer.DockerComposeDeploymentServiceTest
-------------------------------------------------------------------------------
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.002 s -- in com.cygnus.installer.DockerComposeDeploymentServiceTest

View File

@@ -1,4 +0,0 @@
-------------------------------------------------------------------------------
Test set: com.cygnus.installer.DockerPrerequisiteCheckerTest
-------------------------------------------------------------------------------
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.004 s -- in com.cygnus.installer.DockerPrerequisiteCheckerTest

View File

@@ -1,4 +0,0 @@
-------------------------------------------------------------------------------
Test set: com.cygnus.installer.DockerRegistryLoginServiceTest
-------------------------------------------------------------------------------
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.010 s -- in com.cygnus.installer.DockerRegistryLoginServiceTest

View File

@@ -1,4 +0,0 @@
-------------------------------------------------------------------------------
Test set: com.cygnus.installer.IniConfigurationLoaderTest
-------------------------------------------------------------------------------
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.008 s -- in com.cygnus.installer.IniConfigurationLoaderTest

View File

@@ -1,4 +0,0 @@
-------------------------------------------------------------------------------
Test set: com.cygnus.installer.InstallationServiceTest
-------------------------------------------------------------------------------
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.002 s -- in com.cygnus.installer.InstallationServiceTest

View File

@@ -1,4 +0,0 @@
-------------------------------------------------------------------------------
Test set: com.cygnus.installer.MachineAssertionServiceTest
-------------------------------------------------------------------------------
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.570 s -- in com.cygnus.installer.MachineAssertionServiceTest

View File

@@ -1,4 +0,0 @@
-------------------------------------------------------------------------------
Test set: com.cygnus.installer.ProductProfileRegistryTest
-------------------------------------------------------------------------------
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.005 s -- in com.cygnus.installer.ProductProfileRegistryTest

File diff suppressed because one or more lines are too long

View File

@@ -15,6 +15,7 @@ import io.lettuce.core.api.sync.RedisCommands;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import matrix.nimble.query.EncryptedQueryCache;
/**
* Best-effort shared cache for the on-premises MVC application. Redis failures
@@ -22,7 +23,7 @@ import org.springframework.stereotype.Service;
* application.
*/
@Service
public class OnPremRedisCacheService implements DisposableBean {
public class OnPremRedisCacheService implements DisposableBean, EncryptedQueryCache {
private static final Logger LOGGER = Logger.getLogger(OnPremRedisCacheService.class.getName());
@@ -53,6 +54,21 @@ public class OnPremRedisCacheService implements DisposableBean {
}
}
public Optional<String> getRaw(String key) {
try {
return Optional.ofNullable(commands().get(key));
} catch (RedisException exception) {
logUnavailable(exception);
resetConnection();
return Optional.empty();
}
}
@Override
public Optional<String> get(String queryId) {
return getRaw(queryId);
}
public <T> Optional<T> get(String namespace, String key, Class<T> type) {
return get(namespace, key).flatMap(json -> deserialize(json, objectMapper.constructType(type)));
}
@@ -79,6 +95,37 @@ public class OnPremRedisCacheService implements DisposableBean {
}
}
public boolean putRaw(String key, String value, Duration ttl) {
try {
commands().setex(key, ttl.toSeconds(), value);
return true;
} catch (RedisException exception) {
logUnavailable(exception);
resetConnection();
return false;
}
}
@Override
public boolean put(String queryId, String encryptedQuery, Duration ttl) {
return putRaw(queryId, encryptedQuery, ttl);
}
public boolean evictRaw(String key) {
try {
return commands().del(key) > 0;
} catch (RedisException exception) {
logUnavailable(exception);
resetConnection();
return false;
}
}
@Override
public boolean evict(String queryId) {
return evictRaw(queryId);
}
public boolean evict(String namespace, String key) {
try {
return commands().del(cacheKey(namespace, key)) > 0;

View File

@@ -16,11 +16,16 @@ public class OnPremRedisConfiguration {
@Value("${REDIS_HOST:192.168.0.111}") String host,
@Value("${REDIS_PORT:7901}") int port,
@Value("${REDIS_PASSWORD:}") String password,
@Value("${REDIS_DATABASE:1}") int database,
@Value("${REDIS_SSL:false}") boolean ssl,
@Value("${REDIS_CONNECT_TIMEOUT_SECONDS:3}") long connectTimeoutSeconds) {
if (database < 0) {
throw new IllegalArgumentException("REDIS_DATABASE must be zero or greater");
}
RedisURI.Builder uri = RedisURI.builder()
.withHost(host)
.withPort(port)
.withDatabase(database)
.withSsl(ssl)
.withTimeout(Duration.ofSeconds(connectTimeoutSeconds));
if (password != null && !password.isBlank()) {

View File

@@ -19,109 +19,109 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
@Controller
@SessionAttributes({"Sessvals"})
@SessionAttributes({ "Sessvals" })
public class CaseUpload {
@RequestMapping(value="caseupload",method=RequestMethod.POST )
public String UploadCases(ModelMap model,HttpServletRequest request,@ModelAttribute(value="Sessvals") Session Sessvals)
{
model.addAttribute("portlist",FillPortList(Sessvals.getBranchID(),"download"));
DownloadUploadSettings us=new DownloadUploadSettings();
@RequestMapping(value = "caseupload", method = RequestMethod.POST)
public String UploadCases(ModelMap model, HttpServletRequest request,
@ModelAttribute(value = "Sessvals") Session Sessvals) {
model.addAttribute("portlist", FillPortList(Sessvals.getBranchID(), "download"));
DownloadUploadSettings us = new DownloadUploadSettings();
us.setPortfolioid("-1");
model.addAttribute("uploadsettings",us);
model.addAttribute("uploadsettings", us);
return "general/onlinedownload";
}
@RequestMapping(value="downloadsettings",method=RequestMethod.POST )
public String FetchDownloadSettings(ModelMap model,HttpServletRequest request,@ModelAttribute(value="Sessvals") Session Sessvals,@ModelAttribute(value="uploadsettings") DownloadUploadSettings us)
{
UploadHandler UH=new UploadHandler();
@RequestMapping(value = "downloadsettings", method = RequestMethod.POST)
public String FetchDownloadSettings(ModelMap model, HttpServletRequest request,
@ModelAttribute(value = "Sessvals") Session Sessvals,
@ModelAttribute(value = "uploadsettings") DownloadUploadSettings us) {
UploadHandler UH = new UploadHandler();
UH.setErrCode("1111");
UH.setProcessFlag(true);
model.addAttribute("portlist",FillPortList(Sessvals.getBranchID(),"download"));
UH.FetchSettings(us,158);
model.addAttribute("portlist", FillPortList(Sessvals.getBranchID(), "download"));
UH.FetchSettings(us, 158);
model.addAttribute("uploadsettings", us);
if(us.getDomainname().equals("localhost"))
{
if (us.getDomainname().equals("localhost")) {
return "general/offlinedownload";
}
else
{
} else {
return "general/onlinedownload";
}
}
@RequestMapping(value = "/startexcelupload", method = RequestMethod.POST)
public String StartExcelUpload(ModelMap model,HttpServletRequest request,@ModelAttribute(value="Sessvals") Session Sessvals,@ModelAttribute(value="uploadsettings") DownloadUploadSettings us,@RequestParam MultipartFile file)
{
public String StartExcelUpload(ModelMap model, HttpServletRequest request,
@ModelAttribute(value = "Sessvals") Session Sessvals,
@ModelAttribute(value = "uploadsettings") DownloadUploadSettings us, @RequestParam MultipartFile file) {
HSSFWorkbook excel = null;
XSSFWorkbook excelx = null;
UploadHandler UH=new UploadHandler();
UploadHandler UH = new UploadHandler();
UH.setErrCode("1111");
UH.setProcessFlag(true);
try
{
if(file.getOriginalFilename().endsWith("xlsx"))
{
try {
if (file.getOriginalFilename().endsWith("xlsx")) {
excelx = new XSSFWorkbook(file.getInputStream());
UH.StartXLXUpload(us,Sessvals.getUserID(),Sessvals.getCompanyID(),Sessvals.getBranchID(),excelx);
}
else if(file.getOriginalFilename().endsWith("xls"))
{
UH.StartXLXUpload(us, Sessvals.getUserID(), Sessvals.getCompanyID(), Sessvals.getBranchID(), excelx);
} else if (file.getOriginalFilename().endsWith("xls")) {
excel = new HSSFWorkbook(file.getInputStream());
UH.StartXLUpload(us,Sessvals.getUserID(),Sessvals.getCompanyID(),Sessvals.getBranchID(),excel);
}
else
{
UH.setErrMsg(UH.getErrCode()+"INFIL:error:Invlaid file format. Please check the format of file you are uploading.");
UH.StartXLUpload(us, Sessvals.getUserID(), Sessvals.getCompanyID(), Sessvals.getBranchID(), excel);
} else {
UH.setErrMsg(UH.getErrCode()
+ "INFIL:error:Invlaid file format. Please check the format of file you are uploading.");
UH.setProcessFlag(false);
}
}catch(Exception exce)
{
UH.setErrMsg(UH.getErrCode()+"INFIL:error:"+exce.getMessage());
} catch (Exception exce) {
UH.setErrMsg(UH.getErrCode() + "INFIL:error:" + exce.getMessage());
UH.setProcessFlag(false);
}
model.addAttribute("portlist",FillPortList(Sessvals.getBranchID(),"download"));
model.addAttribute("portlist", FillPortList(Sessvals.getBranchID(), "download"));
model.addAttribute("uploadsettings", us);
model.addAttribute("msg",UH.getErrMsg());
model.addAttribute("msg", UH.getErrMsg());
return "general/offlinedownload";
}
@RequestMapping(value="startupload",method=RequestMethod.POST )
public String StartUpload(ModelMap model,HttpServletRequest request,@ModelAttribute(value="Sessvals") Session Sessvals,@ModelAttribute(value="uploadsettings") DownloadUploadSettings us)
{
UploadHandler UH=new UploadHandler();
@RequestMapping(value = "startupload", method = RequestMethod.POST)
public String StartUpload(ModelMap model, HttpServletRequest request,
@ModelAttribute(value = "Sessvals") Session Sessvals,
@ModelAttribute(value = "uploadsettings") DownloadUploadSettings us) {
UploadHandler UH = new UploadHandler();
UH.setErrCode("1111");
UH.setProcessFlag(true);
model.addAttribute("portlist",FillPortList(Sessvals.getBranchID(),"download"));
UH.StartUpload(us,Sessvals.getUserID(),Sessvals.getCompanyID(),Sessvals.getBranchID());
model.addAttribute("portlist", FillPortList(Sessvals.getBranchID(), "download"));
UH.StartUpload(us, Sessvals.getUserID(), Sessvals.getCompanyID(), Sessvals.getBranchID());
model.addAttribute("uploadsettings", us);
model.addAttribute("msg",UH.getErrMsg());
model.addAttribute("msg", UH.getErrMsg());
return "general/onlinedownload";
}
@RequestMapping(value="reportupload",method=RequestMethod.POST )
public String UploadReports(ModelMap model,HttpServletRequest request,@ModelAttribute(value="Sessvals") Session Sessvals)
{
model.addAttribute("portlist",FillPortList(Sessvals.getBranchID(),"upload"));
DownloadUploadSettings us=new DownloadUploadSettings();
@RequestMapping(value = "reportupload", method = RequestMethod.POST)
public String UploadReports(ModelMap model, HttpServletRequest request,
@ModelAttribute(value = "Sessvals") Session Sessvals) {
model.addAttribute("portlist", FillPortList(Sessvals.getBranchID(), "upload"));
DownloadUploadSettings us = new DownloadUploadSettings();
us.setPortfolioid("-1");
model.addAttribute("uploadsettings",us);
model.addAttribute("uploadsettings", us);
return "general/uploadreports";
}
@RequestMapping(value="fileuploadsettings",method=RequestMethod.POST )
public String FetchUploadSettings(ModelMap model,HttpServletRequest request,@ModelAttribute(value="Sessvals") Session Sessvals,@ModelAttribute(value="uploadsettings") DownloadUploadSettings us)
{
UploadHandler UH=new UploadHandler();
@RequestMapping(value = "fileuploadsettings", method = RequestMethod.POST)
public String FetchUploadSettings(ModelMap model, HttpServletRequest request,
@ModelAttribute(value = "Sessvals") Session Sessvals,
@ModelAttribute(value = "uploadsettings") DownloadUploadSettings us) {
UploadHandler UH = new UploadHandler();
UH.setErrCode("1112");
UH.setProcessFlag(true);
model.addAttribute("portlist",FillPortList(Sessvals.getBranchID(),"upload"));
UH.FetchSettings(us,159);
model.addAttribute("portlist", FillPortList(Sessvals.getBranchID(), "upload"));
UH.FetchSettings(us, 159);
UH.FetchFilesToUpload(us);
model.addAttribute("uploadsettings", us);
return "general/uploadreports";
}
public String [][] FillPortList(String BranchId,String Action)
{
return new ModuleFunctions("1001").GetResultArray(157,(BranchId+GlobalClass.ColDelim+Action+GlobalClass.ColDelim).split(GlobalClass.ColDelim));
public String[][] FillPortList(String BranchId, String Action) {
return new ModuleFunctions("1001").GetResultArray(157,
(BranchId + GlobalClass.ColDelim + Action + GlobalClass.ColDelim).split(GlobalClass.ColDelim));
}
}

View File

@@ -1,7 +1,5 @@
package matrix.nimble.controller;
import java.util.Map;
import matrix.nimble.cloud.identity.CloudAuthenticationException;
import matrix.nimble.cloud.identity.CloudAuthenticationGateway;
import matrix.nimble.model.Login;
@@ -15,7 +13,6 @@ import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
@@ -28,16 +25,10 @@ public class SessionController {
public SessionController(CloudAuthenticationGateway cloudAuthenticationGateway) {
this.cloudAuthenticationGateway = cloudAuthenticationGateway;
}
@RequestMapping(value="login",method=RequestMethod.POST )
public String LoginPage(ModelMap model,@RequestHeader Map<String, String> headers)
@RequestMapping(value="login", method={RequestMethod.GET, RequestMethod.POST})
public String LoginPage(ModelMap model)
{
model.addAttribute("login", new Login());
String host = headers.get("host").toString();
if(!host.contains("192.168.10.205:8585") &&
!host.contains("192.168.10.250:8585") &&
!host.startsWith("localhost:") && !host.startsWith("127.0.0.1:")) {
return "error";
}
return "login";
}
@RequestMapping(value="logout",method=RequestMethod.POST )

View File

@@ -0,0 +1,74 @@
package matrix.nimble.query;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public final class AesGcmQueryCipher implements QueryCipher {
private static final String VERSION = "v1";
private static final int IV_LENGTH = 12;
private static final int TAG_BITS = 128;
private final SecretKeySpec key;
private final SecureRandom random;
public AesGcmQueryCipher(byte[] keyBytes) {
this(keyBytes, new SecureRandom());
}
AesGcmQueryCipher(byte[] keyBytes, SecureRandom random) {
if (keyBytes == null || keyBytes.length != 32) {
throw new IllegalArgumentException("Query cache AES key must contain 32 bytes");
}
this.key = new SecretKeySpec(keyBytes.clone(), "AES");
this.random = random;
}
@Override
public String encrypt(String queryId, String query) {
try {
byte[] iv = new byte[IV_LENGTH];
random.nextBytes(iv);
Cipher cipher = cipher(Cipher.ENCRYPT_MODE, queryId, iv);
byte[] encrypted = cipher.doFinal(query.getBytes(StandardCharsets.UTF_8));
Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();
return VERSION + '.' + encoder.encodeToString(iv) + '.'
+ encoder.encodeToString(encrypted);
} catch (GeneralSecurityException exception) {
throw new IllegalStateException("Unable to encrypt cached query", exception);
}
}
@Override
public String decrypt(String queryId, String encryptedQuery) {
try {
String[] parts = encryptedQuery.split("\\.", -1);
if (parts.length != 3 || !VERSION.equals(parts[0])) {
throw new IllegalArgumentException("Unsupported encrypted query format");
}
Base64.Decoder decoder = Base64.getUrlDecoder();
byte[] iv = decoder.decode(parts[1]);
if (iv.length != IV_LENGTH) {
throw new IllegalArgumentException("Invalid encrypted query IV");
}
Cipher cipher = cipher(Cipher.DECRYPT_MODE, queryId, iv);
return new String(cipher.doFinal(decoder.decode(parts[2])), StandardCharsets.UTF_8);
} catch (GeneralSecurityException | IllegalArgumentException exception) {
throw new IllegalStateException("Unable to decrypt cached query", exception);
}
}
private Cipher cipher(int mode, String queryId, byte[] iv)
throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(mode, key, new GCMParameterSpec(TAG_BITS, iv));
cipher.updateAAD(queryId.getBytes(StandardCharsets.UTF_8));
return cipher;
}
}

View File

@@ -0,0 +1,22 @@
package matrix.nimble.query;
import com.cygnus.client.CloudIdentityClient;
public final class CloudQuerySource implements QuerySource {
private final CloudIdentityClient client;
public CloudQuerySource(CloudIdentityClient client) {
this.client = client;
}
@Override
public String fetch(int queryId) {
return client.fetchQuery(queryId)
.map(response -> response.query())
.blockOptional()
.filter(query -> !query.isBlank())
.orElseThrow(() -> new QueryProviderException(
"Cloud query was empty: " + queryId));
}
}

View File

@@ -0,0 +1,14 @@
package matrix.nimble.query;
import java.time.Duration;
import java.util.Optional;
public interface EncryptedQueryCache {
Optional<String> get(String queryId);
boolean put(String queryId, String encryptedQuery, Duration ttl);
boolean evict(String queryId);
}

View File

@@ -0,0 +1,9 @@
package matrix.nimble.query;
public interface QueryCipher {
String encrypt(String queryId, String query);
String decrypt(String queryId, String encryptedQuery);
}

View File

@@ -0,0 +1,6 @@
package matrix.nimble.query;
public interface QueryProvider {
String getQuery(int queryId);
}

View File

@@ -0,0 +1,65 @@
package matrix.nimble.query;
import com.cygnus.client.CloudClientProperties;
import com.cygnus.client.CloudIdentityClient;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.Base64;
import matrix.nimble.cloud.cache.OnPremRedisCacheService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.beans.factory.DisposableBean;
@Configuration
public class QueryProviderConfiguration implements DisposableBean {
private QueryProvider installed;
@Bean
QueryProvider queryProvider(
OnPremRedisCacheService cache,
CloudIdentityClient cloudClient,
CloudClientProperties cloudProperties,
@Value("${CYGNUS_QUERY_CACHE_AES_KEY:}") String configuredKey) {
byte[] key = queryCacheKey(configuredKey, cloudProperties.clientAssertion());
installed = new RedisCachingQueryProvider(
cache,
new CloudQuerySource(cloudClient),
new AesGcmQueryCipher(key));
QueryProviders.install(installed);
return installed;
}
@Override
public void destroy() {
if (installed != null) {
QueryProviders.clear(installed);
}
}
private byte[] queryCacheKey(String configuredKey, String assertionLocation) {
try {
if (configuredKey != null && !configuredKey.isBlank()) {
byte[] decoded = Base64.getDecoder().decode(configuredKey.trim());
if (decoded.length != 32) {
throw new IllegalStateException(
"CYGNUS_QUERY_CACHE_AES_KEY must be a Base64-encoded 256-bit key");
}
return decoded;
}
String assertion = assertionLocation.startsWith("file:")
? Files.readString(Path.of(assertionLocation.substring(5)),
StandardCharsets.US_ASCII).trim()
: assertionLocation;
return MessageDigest.getInstance("SHA-256")
.digest(assertion.getBytes(StandardCharsets.UTF_8));
} catch (IllegalStateException exception) {
throw exception;
} catch (Exception exception) {
throw new IllegalStateException("Unable to initialize query cache encryption", exception);
}
}
}

View File

@@ -0,0 +1,13 @@
package matrix.nimble.query;
public class QueryProviderException extends RuntimeException {
public QueryProviderException(String message) {
super(message);
}
public QueryProviderException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,28 @@
package matrix.nimble.query;
import java.util.concurrent.atomic.AtomicReference;
public final class QueryProviders {
private static final AtomicReference<QueryProvider> CURRENT = new AtomicReference<>();
private QueryProviders() {
}
public static QueryProvider current() {
QueryProvider provider = CURRENT.get();
if (provider == null) {
throw new QueryProviderException("QueryProvider has not been initialized");
}
return provider;
}
public static void install(QueryProvider provider) {
CURRENT.set(java.util.Objects.requireNonNull(provider));
}
static void clear(QueryProvider provider) {
CURRENT.compareAndSet(provider, null);
}
}

View File

@@ -0,0 +1,7 @@
package matrix.nimble.query;
@FunctionalInterface
public interface QuerySource {
String fetch(int queryId);
}

View File

@@ -0,0 +1,80 @@
package matrix.nimble.query;
import java.time.Duration;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
public final class RedisCachingQueryProvider implements QueryProvider {
public static final Duration QUERY_TTL = Duration.ofHours(12);
private static final Logger LOGGER = Logger.getLogger(RedisCachingQueryProvider.class.getName());
private static final int LOCK_COUNT = 64;
private final EncryptedQueryCache cache;
private final QuerySource cloudSource;
private final QueryCipher cipher;
private final Object[] locks = new Object[LOCK_COUNT];
public RedisCachingQueryProvider(
EncryptedQueryCache cache,
QuerySource cloudSource,
QueryCipher cipher) {
this.cache = cache;
this.cloudSource = cloudSource;
this.cipher = cipher;
for (int index = 0; index < locks.length; index++) {
locks[index] = new Object();
}
}
@Override
public String getQuery(int queryId) {
String cacheKey = cacheKey(queryId);
Optional<String> cached = cached(cacheKey);
if (cached.isPresent()) {
return cached.get();
}
synchronized (lock(cacheKey)) {
cached = cached(cacheKey);
if (cached.isPresent()) {
return cached.get();
}
try {
String query = cloudSource.fetch(queryId);
cache.put(cacheKey, cipher.encrypt(cacheKey, query), QUERY_TTL);
return query;
} catch (QueryProviderException exception) {
throw exception;
} catch (RuntimeException exception) {
throw new QueryProviderException(
"Unable to retrieve query: " + queryId, exception);
}
}
}
private Optional<String> cached(String queryId) {
return cache.get(queryId).flatMap(encrypted -> {
try {
return Optional.of(cipher.decrypt(queryId, encrypted));
} catch (RuntimeException exception) {
LOGGER.log(Level.WARNING,
"Discarding an invalid encrypted query cache entry: {0}", queryId);
cache.evict(queryId);
return Optional.empty();
}
});
}
private Object lock(String queryId) {
return locks[(queryId.hashCode() & Integer.MAX_VALUE) % locks.length];
}
private String cacheKey(int queryId) {
if (queryId <= 0) {
throw new IllegalArgumentException("Invalid query ID");
}
return Integer.toString(queryId);
}
}

View File

@@ -4,15 +4,9 @@ import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import matrix.nimble.query.QueryProviders;
public class FileFunctions {
private static final Object QUERY_CACHE_LOCK = new Object();
private static volatile Map<Integer, String> queryCache = Collections.emptyMap();
private static volatile long queryCacheLastModified = Long.MIN_VALUE;
private static volatile String queryCachePath = "";
private String ConnString;
private String DBDriver;
private String DBUser;
@@ -207,53 +201,6 @@ public class FileFunctions {
}
}
public String GetQuery(int QueryIndex) throws IOException {
String realPath = (FileFunctions.class
.getResource("FileFunctions.class")).toString()
.replace("utilities/FileFunctions.class", "conf/")
.replace("file:/", "");
if(!isWindows())
{
realPath="/"+realPath;
}
else
{
realPath=realPath.replace("%20", " ");
}
File queryFile = new File(realPath + "nimble.qry");
refreshQueryCacheIfRequired(queryFile);
return queryCache.get(QueryIndex);
}
private static void refreshQueryCacheIfRequired(File queryFile) throws IOException {
String absolutePath = queryFile.getAbsolutePath();
long lastModified = queryFile.lastModified();
if (absolutePath.equals(queryCachePath) && lastModified == queryCacheLastModified) {
return;
}
synchronized (QUERY_CACHE_LOCK) {
if (absolutePath.equals(queryCachePath) && lastModified == queryCacheLastModified) {
return;
}
Map<Integer, String> loadedQueries = new HashMap<>();
try (BufferedReader reader = new BufferedReader(new FileReader(queryFile))) {
String line;
while ((line = reader.readLine()) != null) {
int delimiterIndex = line.indexOf(GlobalClass.ColDelim);
if (delimiterIndex <= 5 || !line.startsWith("Query")) {
continue;
}
try {
int queryIndex = Integer.parseInt(line.substring(5, delimiterIndex));
loadedQueries.put(queryIndex, line.substring(delimiterIndex + GlobalClass.ColDelim.length()));
} catch (NumberFormatException ignored) {
// Ignore malformed/non-query lines, matching the legacy lookup behavior.
}
}
}
queryCache = Collections.unmodifiableMap(loadedQueries);
queryCachePath = absolutePath;
queryCacheLastModified = lastModified;
}
return QueryProviders.current().getQuery(QueryIndex);
}
}

View File

@@ -1,21 +1,17 @@
package matrix.nimble;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import matrix.nimble.utilities.FileFunctions;
import matrix.nimble.query.QueryProviders;
class FileFunctionsQueryCacheTest {
@Test
void loadsQueriesFromNimbleQueryFileAndHandlesMissingCodes() throws Exception {
void delegatesLegacyQueryLookupToQueryProvider() throws Exception {
QueryProviders.install(queryId -> "provided:" + queryId);
FileFunctions files = new FileFunctions("TEST");
String query = files.GetQuery(3);
assertTrue(query.startsWith("select!C0L!select portfolio_id"));
assertNull(files.GetQuery(1));
assertNull(files.GetQuery(Integer.MAX_VALUE));
assertEquals("provided:3", files.GetQuery(3));
}
}

Some files were not shown because too many files have changed in this diff Show More