commit
This commit is contained in:
@@ -173,6 +173,16 @@
|
||||
<build>
|
||||
<finalName>matrix</finalName>
|
||||
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<excludes>
|
||||
<!-- Queries are supplied by the cloud query catalog at runtime. -->
|
||||
<exclude>matrix/nimble/conf/nimble.qry</exclude>
|
||||
</excludes>
|
||||
</resource>
|
||||
</resources>
|
||||
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package matrix.nimble.controller;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
//spring libraries
|
||||
import matrix.nimble.model.DownloadUploadSettings;
|
||||
@@ -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 {
|
||||
return "general/onlinedownload";
|
||||
}
|
||||
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());
|
||||
return "general/offlinedownload";
|
||||
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());
|
||||
return "general/onlinedownload";
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
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(String queryId) {
|
||||
return client.fetchQuery(queryId)
|
||||
.map(response -> response.query())
|
||||
.blockOptional()
|
||||
.filter(query -> !query.isBlank())
|
||||
.orElseThrow(() -> new QueryProviderException(
|
||||
"Cloud query was empty: " + queryId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package matrix.nimble.query;
|
||||
|
||||
public interface QueryCipher {
|
||||
|
||||
String encrypt(String queryId, String query);
|
||||
|
||||
String decrypt(String queryId, String encryptedQuery);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package matrix.nimble.query;
|
||||
|
||||
public interface QueryProvider {
|
||||
|
||||
String getQuery(String queryId);
|
||||
|
||||
default String getQuery(int queryId) {
|
||||
return getQuery("Query" + queryId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package matrix.nimble.query;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface QuerySource {
|
||||
|
||||
String fetch(String queryId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
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(3);
|
||||
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(String queryId) {
|
||||
String normalized = normalize(queryId);
|
||||
Optional<String> cached = cached(normalized);
|
||||
if (cached.isPresent()) {
|
||||
return cached.get();
|
||||
}
|
||||
|
||||
synchronized (lock(normalized)) {
|
||||
cached = cached(normalized);
|
||||
if (cached.isPresent()) {
|
||||
return cached.get();
|
||||
}
|
||||
try {
|
||||
String query = cloudSource.fetch(normalized);
|
||||
cache.put(normalized, cipher.encrypt(normalized, query), QUERY_TTL);
|
||||
return query;
|
||||
} catch (QueryProviderException exception) {
|
||||
throw exception;
|
||||
} catch (RuntimeException exception) {
|
||||
throw new QueryProviderException(
|
||||
"Unable to retrieve query: " + normalized, 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 normalize(String queryId) {
|
||||
if (queryId == null || !queryId.matches("^[A-Za-z0-9._-]+$")) {
|
||||
throw new IllegalArgumentException("Invalid query ID");
|
||||
}
|
||||
return queryId;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:Query3", files.GetQuery(3));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package matrix.nimble.query;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.Arrays;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class AesGcmQueryCipherTest {
|
||||
|
||||
@Test
|
||||
void encryptsAndAuthenticatesQueryAndQueryId() {
|
||||
byte[] key = new byte[32];
|
||||
Arrays.fill(key, (byte) 7);
|
||||
AesGcmQueryCipher cipher = new AesGcmQueryCipher(key);
|
||||
|
||||
String encrypted = cipher.encrypt("Query3", "select * from portfolio");
|
||||
|
||||
assertNotEquals("select * from portfolio", encrypted);
|
||||
assertTrue(encrypted.startsWith("v1."));
|
||||
org.junit.jupiter.api.Assertions.assertEquals(
|
||||
"select * from portfolio", cipher.decrypt("Query3", encrypted));
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> cipher.decrypt("Query4", encrypted));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package matrix.nimble.query;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class RedisCachingQueryProviderTest {
|
||||
|
||||
@Test
|
||||
void usesEncryptedRedisValueAndThreeHourTtl() {
|
||||
MemoryCache cache = new MemoryCache();
|
||||
AtomicInteger cloudCalls = new AtomicInteger();
|
||||
QueryCipher cipher = new AesGcmQueryCipher(new byte[32]);
|
||||
RedisCachingQueryProvider provider = new RedisCachingQueryProvider(
|
||||
cache,
|
||||
queryId -> {
|
||||
cloudCalls.incrementAndGet();
|
||||
return "select!C0L!select 1";
|
||||
},
|
||||
cipher);
|
||||
|
||||
assertEquals("select!C0L!select 1", provider.getQuery("Query10"));
|
||||
assertEquals("select!C0L!select 1", provider.getQuery("Query10"));
|
||||
|
||||
assertEquals(1, cloudCalls.get());
|
||||
assertEquals(Duration.ofHours(3), cache.ttl.get());
|
||||
org.junit.jupiter.api.Assertions.assertNotEquals(
|
||||
"select!C0L!select 1", cache.values.get("Query10"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void collapsesConcurrentMissesForTheSameQuery() throws Exception {
|
||||
MemoryCache cache = new MemoryCache();
|
||||
AtomicInteger cloudCalls = new AtomicInteger();
|
||||
RedisCachingQueryProvider provider = new RedisCachingQueryProvider(
|
||||
cache,
|
||||
queryId -> {
|
||||
cloudCalls.incrementAndGet();
|
||||
try {
|
||||
Thread.sleep(30);
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
return "select!C0L!select 1";
|
||||
},
|
||||
new AesGcmQueryCipher(new byte[32]));
|
||||
var executor = Executors.newFixedThreadPool(8);
|
||||
try {
|
||||
for (int index = 0; index < 20; index++) {
|
||||
executor.submit(() -> provider.getQuery("Query20"));
|
||||
}
|
||||
} finally {
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(5, TimeUnit.SECONDS);
|
||||
}
|
||||
assertEquals(1, cloudCalls.get());
|
||||
}
|
||||
|
||||
private static final class MemoryCache implements EncryptedQueryCache {
|
||||
private final Map<String, String> values = new ConcurrentHashMap<>();
|
||||
private final AtomicReference<Duration> ttl = new AtomicReference<>();
|
||||
|
||||
@Override
|
||||
public Optional<String> get(String queryId) {
|
||||
return Optional.ofNullable(values.get(queryId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean put(String queryId, String encryptedQuery, Duration duration) {
|
||||
ttl.set(duration);
|
||||
values.put(queryId, encryptedQuery);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evict(String queryId) {
|
||||
return values.remove(queryId) != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user