I am beginner programmer. I am working in project with flyway-core 4.0.3 and I am migrating it to flyway 8.0.5. One thing is left and I don't know how to migrate this. I am pasting a class - this is a class that filters sql scripts. Apart from standard locations parameter we have also filtering mechanism based on some file name conditions (not prefix and not suffix). I am using free version of flyway, that's why I cannot use some features like placeholders.
This is a piece of code that filters scripts choosing only those that has DDL in name, just an example for business filter. There would be other things to filter in file name also..
try (Connection connectionMetaDataTable = JdbcUtils.openConnection(dataSource)) {
DbSupport dbSupport = DbSupportFactory.createDbSupport(connectionMetaDataTable, true);
String scriptsLocation = prepareLocation();
Flyway flyway = new Flyway();
flyway.setDataSource(dataSource);
flyway.setLocations(scriptsLocation);
flyway.setEncoding("UTF-8");
CustomMigrationResolver customMigrationResolver = new CustomMigrationResolver (
flyway,
dbSupport,
new Scanner(Thread.currentThread().getContextClassLoader()),
new Location(scriptsLocation));
flyway.setResolvers(customMigrationResolver );
flyway.setSkipDefaultResolvers(true);
flyway.migrate();
} catch (SQLException e) {
LOGGER.error(e.getMessage(), e);
}
public class CustomMigrationResolver extends SqlMigrationResolver {
public CustomMigrationResolver(Flyway flyway, DbSupport dbSupport, Scanner scanner, Location location) {
super(dbSupport, scanner, location, PlaceholderReplacer.NO_PLACEHOLDERS, flyway.getEncoding(), flyway.getSqlMigrationPrefix(), flyway.getRepeatableSqlMigrationPrefix(),
flyway.getSqlMigrationSeparator(), flyway.getSqlMigrationSuffix());
}
#Override
public List<ResolvedMigration> resolveMigrations() {
List<ResolvedMigration> allMigrations = super.resolveMigrations();
List<ResolvedMigration> onlyDdlMigrations = allMigrations.stream().filter(m -> m.getDescription().startsWith("DDL")).collect(Collectors.toList()); //some shortened business filtering condiitons
return onlyDdlMigrations;
}
In flyway 8.0.4 my customMigrationResolver could look like this:
public class Flyway8CustomMigrationResolver extends SqlMigrationResolver {
public Flyway8CustomMigrationResolver(ResourceProvider resourceProvider,
SqlScriptExecutorFactory sqlScriptExecutorFactory,
SqlScriptFactory sqlScriptFactory,
Configuration configuration,
ParsingContext parsingContext) {
super(resourceProvider, sqlScriptExecutorFactory, sqlScriptFactory, configuration, parsingContext);
}
#Override
public List<ResolvedMigration> resolveMigrations(Context context) {
List<ResolvedMigration> allMigrations = super.resolveMigrations(context);
//some business filtering of the files..
return onlyDdlMigrations;
}
}
There are no examples how to provide all parameters for Flyway8CustomMigrationResolver constructor, can you help me?
Related
TL:DR; When running tests with different #ResourceArgs, the configuration of different tests get thrown around and override others, breaking tests meant to run with specific configurations.
So, I have a service that has tests that run in different configuration setups. The main difference at the moment is the service can either manage its own authentication or get it from an external source (Keycloak).
I firstly control this using test profiles, which seem to work fine. Unfortunately, in order to support both cases, the ResourceLifecycleManager I have setup supports setting up a Keycloak instance and returns config values that break the config for self authentication (This is due primarily to the fact that I have not found out how to get the lifecycle manager to determine on its own what profile or config is currently running. If I could do this, I think I would be much better off than using #ResourceArg, so would love to know if I missed something here).
To remedy this shortcoming, I have attempted to use #ResourceArgs to convey to the lifecycle manager when to setup for external auth. However, I have noticed some really odd execution timings and the config that ends up at my test/service isn't what I intend based on the test class's annotations, where it is obvious the lifecycle manager has setup for external auth.
Additionally, it should be noted that I have my tests ordered such that the profiles and configs shouldn't be running out of order; all the tests that don't care are run first, then the 'normal' tests with self auth, then the tests with the external auth profile. I can see this working appropriately when I run in intellij, and the fact I can tell the time is being taken to start up the new service instance between the test profiles.
Looking at the logs when I throw a breakpoint in places, some odd things are obvious:
When breakpoint on an erring test (before the external-configured tests run)
The start() method of my TestResourceLifecycleManager has been called twice
The first run ran with Keycloak starting, would override/break config
though the time I would expect to need to be taken to start up keycloak not happening, a little confused here
The second run is correct, not starting keycloak
The profile config is what is expected, except for what the keycloak setup would override
When breakpoint on an external-configured test (after all self-configured tests run):
The start() method has now been called 4 times; appears that things were started in the same order as before again for the new run of the app
There could be some weirdness in how Intellij/Gradle shows logs, but I am interpreting this as:
Quarkus initting the two instances of LifecycleManager when starting the app for some reason, and one's config overrides the other, causing my woes.
The lifecycle manager is working as expected; it appropriately starts/ doesn't start keycloak when configured either way
At this point I can't tell if I'm doing something wrong, or if there's a bug.
Test class example for self-auth test (same annotations for all tests in this (test) profile):
#Slf4j
#QuarkusTest
#QuarkusTestResource(TestResourceLifecycleManager.class)
#TestHTTPEndpoint(Auth.class)
class AuthTest extends RunningServerTest {
Test class example for external auth test (same annotations for all tests in this (externalAuth) profile):
#Slf4j
#QuarkusTest
#TestProfile(ExternalAuthTestProfile.class)
#QuarkusTestResource(value = TestResourceLifecycleManager.class, initArgs = #ResourceArg(name=TestResourceLifecycleManager.EXTERNAL_AUTH_ARG, value="true"))
#TestHTTPEndpoint(Auth.class)
class AuthExternalTest extends RunningServerTest {
ExternalAuthTestProfile extends this, providing the appropriate profile name:
public class NonDefaultTestProfile implements QuarkusTestProfile {
private final String testProfile;
private final Map<String, String> overrides = new HashMap<>();
protected NonDefaultTestProfile(String testProfile) {
this.testProfile = testProfile;
}
protected NonDefaultTestProfile(String testProfile, Map<String, String> configOverrides) {
this(testProfile);
this.overrides.putAll(configOverrides);
}
#Override
public Map<String, String> getConfigOverrides() {
return new HashMap<>(this.overrides);
}
#Override
public String getConfigProfile() {
return testProfile;
}
#Override
public List<TestResourceEntry> testResources() {
return QuarkusTestProfile.super.testResources();
}
}
Lifecycle manager:
#Slf4j
public class TestResourceLifecycleManager implements QuarkusTestResourceLifecycleManager {
public static final String EXTERNAL_AUTH_ARG = "externalAuth";
private static volatile MongodExecutable MONGO_EXE = null;
private static volatile KeycloakContainer KEYCLOAK_CONTAINER = null;
private boolean externalAuth = false;
public synchronized Map<String, String> startKeycloakTestServer() {
if(!this.externalAuth){
log.info("No need for keycloak.");
return Map.of();
}
if (KEYCLOAK_CONTAINER != null) {
log.info("Keycloak already started.");
} else {
KEYCLOAK_CONTAINER = new KeycloakContainer()
// .withEnv("hello","world")
.withRealmImportFile("keycloak-realm.json");
KEYCLOAK_CONTAINER.start();
log.info(
"Test keycloak started at endpoint: {}\tAdmin creds: {}:{}",
KEYCLOAK_CONTAINER.getAuthServerUrl(),
KEYCLOAK_CONTAINER.getAdminUsername(),
KEYCLOAK_CONTAINER.getAdminPassword()
);
}
String clientId;
String clientSecret;
String publicKey = "";
try (
Keycloak keycloak = KeycloakBuilder.builder()
.serverUrl(KEYCLOAK_CONTAINER.getAuthServerUrl())
.realm("master")
.grantType(OAuth2Constants.PASSWORD)
.clientId("admin-cli")
.username(KEYCLOAK_CONTAINER.getAdminUsername())
.password(KEYCLOAK_CONTAINER.getAdminPassword())
.build();
) {
RealmResource appsRealmResource = keycloak.realms().realm("apps");
ClientRepresentation qmClientResource = appsRealmResource.clients().findByClientId("quartermaster").get(0);
clientSecret = qmClientResource.getSecret();
log.info("Got client id \"{}\" with secret: {}", "quartermaster", clientSecret);
//get private key
for (KeysMetadataRepresentation.KeyMetadataRepresentation curKey : appsRealmResource.keys().getKeyMetadata().getKeys()) {
if (!SIG.equals(curKey.getUse())) {
continue;
}
if (!"RSA".equals(curKey.getType())) {
continue;
}
String publicKeyTemp = curKey.getPublicKey();
if (publicKeyTemp == null || publicKeyTemp.isBlank()) {
continue;
}
publicKey = publicKeyTemp;
log.info("Found a relevant key for public key use: {} / {}", curKey.getKid(), publicKey);
}
}
// write public key
// = new File(TestResourceLifecycleManager.class.getResource("/").toURI().toString() + "/security/testKeycloakPublicKey.pem");
File publicKeyFile;
try {
publicKeyFile = File.createTempFile("oqmTestKeycloakPublicKey",".pem");
// publicKeyFile = new File(TestResourceLifecycleManager.class.getResource("/").toURI().toString().replace("/classes/java/", "/resources/") + "/security/testKeycloakPublicKey.pem");
log.info("path of public key: {}", publicKeyFile);
// if(publicKeyFile.createNewFile()){
// log.info("created new public key file");
//
// } else {
// log.info("Public file already exists");
// }
try (
FileOutputStream os = new FileOutputStream(
publicKeyFile
);
) {
IOUtils.write(publicKey, os, UTF_8);
} catch (IOException e) {
log.error("Failed to write out public key of keycloak: ", e);
throw new IllegalStateException("Failed to write out public key of keycloak.", e);
}
} catch (IOException e) {
log.error("Failed to create public key file: ", e);
throw new IllegalStateException("Failed to create public key file", e);
}
String keycloakUrl = KEYCLOAK_CONTAINER.getAuthServerUrl().replace("/auth", "");
return Map.of(
"test.keycloak.url", keycloakUrl,
"test.keycloak.authUrl", KEYCLOAK_CONTAINER.getAuthServerUrl(),
"test.keycloak.adminName", KEYCLOAK_CONTAINER.getAdminUsername(),
"test.keycloak.adminPass", KEYCLOAK_CONTAINER.getAdminPassword(),
//TODO:: add config for server to talk to
"service.externalAuth.url", keycloakUrl,
"mp.jwt.verify.publickey.location", publicKeyFile.getAbsolutePath()
);
}
public static synchronized void startMongoTestServer() throws IOException {
if (MONGO_EXE != null) {
log.info("Flapdoodle Mongo already started.");
return;
}
Version.Main version = Version.Main.V4_0;
int port = 27018;
log.info("Starting Flapdoodle Test Mongo {} on port {}", version, port);
IMongodConfig config = new MongodConfigBuilder()
.version(version)
.net(new Net(port, Network.localhostIsIPv6()))
.build();
try {
MONGO_EXE = MongodStarter.getDefaultInstance().prepare(config);
MongodProcess process = MONGO_EXE.start();
if (!process.isProcessRunning()) {
throw new IOException();
}
} catch (Throwable e) {
log.error("FAILED to start test mongo server: ", e);
MONGO_EXE = null;
throw e;
}
}
public static synchronized void stopMongoTestServer() {
if (MONGO_EXE == null) {
log.warn("Mongo was not started.");
return;
}
MONGO_EXE.stop();
MONGO_EXE = null;
}
public synchronized static void cleanMongo() throws IOException {
if (MONGO_EXE == null) {
log.warn("Mongo was not started.");
return;
}
log.info("Cleaning Mongo of all entries.");
}
#Override
public void init(Map<String, String> initArgs) {
this.externalAuth = Boolean.parseBoolean(initArgs.getOrDefault(EXTERNAL_AUTH_ARG, Boolean.toString(this.externalAuth)));
}
#Override
public Map<String, String> start() {
log.info("STARTING test lifecycle resources.");
Map<String, String> configOverride = new HashMap<>();
try {
startMongoTestServer();
} catch (IOException e) {
log.error("Unable to start Flapdoodle Mongo server");
}
configOverride.putAll(startKeycloakTestServer());
return configOverride;
}
#Override
public void stop() {
log.info("STOPPING test lifecycle resources.");
stopMongoTestServer();
}
}
The app can be found here: https://github.com/Epic-Breakfast-Productions/OpenQuarterMaster/tree/main/software/open-qm-base-station
The tests are currently failing in the ways I am describing, so feel free to look around.
Note that to run this, you will need to run ./gradlew build publishToMavenLocal in https://github.com/Epic-Breakfast-Productions/OpenQuarterMaster/tree/main/software/libs/open-qm-core to install a dependency locally.
Github issue also tracking this: https://github.com/quarkusio/quarkus/issues/22025
Any use of #QuarkusTestResource() without restrictToAnnotatedClass set to true, means that the QuarkusTestResourceLifecycleManager will be applied to all tests no matter where the annotation is placed.
Hope restrictToAnnotatedClass will solve the problem.
There is a method in a class like this down below :
#Override
#Transactional
#Cacheable(value = "products", key = "#id")
public Product getProduct(long id) throws ApplicationException {
Product product = null;
try {
ProductEntity productEntity = productDAO.getProduct(id);
product = productTransformer.toProduct(productEntity);
} catch (SystemException ex) {
throw new ApplicationException(ex.getCode(), ex.getMessage(), "Problem in DataLayer", "Data Layer Error",
new Object[] { ex });
}
return product;
}
The application is working fine. But I want to have a cache hit log when data is put into the cache. I want to log it through log4j.properties.
How can I configure the application.properties in such a way so that it can be logged ?
Spring internally logs its caching workflow at a TRACE level. To enable this , in your application.properties file, include the following.
logging.level.org.springframework.cache=TRACE
Including the following should help:
logging.level.org.springframework.cache=TRACE
I have some code that works properly on spring boot prior to 2 and I find it hard to convert it to work with spring boot 2.
Can somebody assist?
public static MutablePropertySources buildPropertySources(String propertyFile, String profile)
{
try
{
Properties properties = new Properties();
YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
// load common properties
PropertySource<?> applicationYamlPropertySource = loader.load("properties", new ClassPathResource(propertyFile), null);
Map<String, Object> source = ((MapPropertySource) applicationYamlPropertySource).getSource();
properties.putAll(source);
// load profile properties
if (null != profile)
{
applicationYamlPropertySource = loader.load("properties", new ClassPathResource(propertyFile), profile);
if (null != applicationYamlPropertySource)
{
source = ((MapPropertySource) applicationYamlPropertySource).getSource();
properties.putAll(source);
}
}
propertySources = new MutablePropertySources();
propertySources.addLast(new PropertiesPropertySource("apis", properties));
}
catch (Exception e)
{
log.error("{} file cannot be found.", propertyFile);
return null;
}
}
public static <T> void handleConfigurationProperties(T bean, MutablePropertySources propertySources) throws BindException
{
ConfigurationProperties configurationProperties = bean.getClass().getAnnotation(ConfigurationProperties.class);
if (null != configurationProperties && null != propertySources)
{
String prefix = configurationProperties.prefix();
String value = configurationProperties.value();
if (null == value || value.isEmpty())
{
value = prefix;
}
PropertiesConfigurationFactory<?> configurationFactory = new PropertiesConfigurationFactory<>(bean);
configurationFactory.setPropertySources(propertySources);
configurationFactory.setTargetName(value);
configurationFactory.bindPropertiesToTarget();
}
}
PropertiesConfigurationFactory doesnt exist anymore and the YamlPropertySourceLoader load method no longer accepts 3 parameters.
(the response is not the same either, when I have tried invoking the new method the response objects were wrapped instead of giving me the direct strings/integers etc...)
The PropertiesConfigurationFactory should be replaced with Binder class.
Binder class
Sample code:-
ConfigurationPropertySource source = new MapConfigurationPropertySource(
loadProperties(resource));
Binder binder = new Binder(source);
return binder.bind("initializr", InitializrProperties.class).get();
We were also using PropertiesConfigurationFactory to bind a POJO to a
prefix of the Environment. In 2.0, a brand new Binder API was
introduced that is more flexible and easier to use. Our binding that
took 10 lines of code could be reduced to 3 simple lines.
YamlPropertySourceLoader:-
Yes, this class has been changed in version 2. It doesn't accept the third parameter profile anymore. The method signature has been changed to return List<PropertySource<?>> rather than PropertySource<?>. If you are expecting single source, please get the first occurrence from the list.
Load the resource into one or more property sources. Implementations
may either return a list containing a single source, or in the case of
a multi-document format such as yaml a source for each document in the
resource.
Since there is no accepted answer yet, i post my full solution, which builds upon the answer from #nationquest:
private ConfigClass loadConfiguration(String path){
MutablePropertySources sources = new MutablePropertySources();
Resource res = new FileSystemResource(path);
PropertiesFactoryBean propFactory = new PropertiesFactoryBean();
propFactory.setLocation(res);
propFactory.setSingleton(false);
// resolve potential references to local environment variables
Properties properties = null;
try {
properties = propFactory.getObject();
for(String p : properties.stringPropertyNames()){
properties.setProperty(p, env.resolvePlaceholders(properties.getProperty(p)));
}
} catch (IOException e) {
e.printStackTrace();
}
sources.addLast(new PropertiesPropertySource("prefix", properties));
ConfigurationPropertySource propertySource = new MapConfigurationPropertySource(properties);
return new Binder(propertySource).bind("prefix", ConfigClass.class).get();
}
The last three lines are the relevant part here.
dirty code but this is how i solved it
https://github.com/mamaorha/easy-wire/tree/master/src/main/java/co/il/nmh/easy/wire/core/utils/properties
I am new to SAP JCo I have requirement to call multiple SAP System using SAP Jco. But I am unable to connect multiple sap system at the same time......
Here is code :
package com.sap.test;
import java.util.Properties;
import com.sap.conn.jco.JCoDestination;
import com.sap.conn.jco.JCoDestinationManager;
import com.sap.conn.jco.JCoException;
import com.sap.conn.jco.JCoRepository;
import com.sap.conn.jco.ext.DestinationDataProvider;
import com.sap.conn.jco.ext.Environment;
import com.sap.utils.MyDestinationDataProvider;
import com.sap.utils.SapSystem;
public class TestMultipleSAPConnection {
public static Properties properties;
public static JCoDestination dest = null;
public static JCoRepository repos = null;
public static SapSystem system = null;
String SAP_SERVER = "SAP_SERVER";
MyDestinationDataProvider myProvider = null;
public static void main(String[] args) throws JCoException {
getConnection_CRM();
getConnection_R3();
}
public static JCoDestination getConnection_R3() {
boolean connR3_flag = true;
JCoDestination dest = null;
JCoRepository repos = null;
String SAP_SERVER = "SAP_SERVER";
Properties properties = new Properties();
SapSystem system = new SapSystem();
system.setClient("100");
system.setHost("r3devsvr.myweb.com");
system.setLanguage("en");
system.setSystemNumber("00");
system.setUser("SAP-R3-USER");
system.setPassword("init1234");
properties.setProperty("jco.client.ashost", system.getHost());
properties.setProperty("jco.client.sysnr", system.getSystemNumber());
properties.setProperty("jco.client.client", system.getClient());
properties.setProperty("jco.client.user", system.getUser());
properties.setProperty("jco.client.passwd", system.getPassword());
properties.setProperty("jco.client.lang", system.getLanguage());
System.out.println("******* Connection Parameter Set *******");
MyDestinationDataProvider myProvider = new MyDestinationDataProvider();
System.out.println("******* Destination Provider Set *******");
myProvider.changePropertiesForABAP_AS(properties);
if (!Environment.isDestinationDataProviderRegistered()) {
System.out.println("Registering Destination Provider R3");
Environment.registerDestinationDataProvider((DestinationDataProvider) myProvider);
}else{
System.out.println("Destination Provider already set..R3");
connR3_flag = false;
}
try {
dest = JCoDestinationManager.getDestination((String) SAP_SERVER);
repos = dest.getRepository();
if (repos == null) {
System.out.println("Repos is null.....");
} else {
System.out.println("Repos is not null.....");
}
System.out.println("After getting repos...");
if(connR3_flag){
System.out.println("R3 Connection Successfull...");
}
} catch (Exception ex) {
System.out.println(ex);
}
return dest;
}
public static JCoDestination getConnection_CRM() {
boolean connCRM_flag = true;
JCoDestination dest = null;
JCoRepository repos = null;
String SAP_SERVER = "SAP_SERVER";
Properties properties = new Properties();
SapSystem system = new SapSystem();
system.setClient("200");
system.setHost("crmdevsvr.myweb.com");
system.setLanguage("en");
system.setSystemNumber("00");
system.setUser("SAP-CRM-USER");
system.setPassword("init1234");
properties.setProperty("jco.client.ashost", system.getHost());
properties.setProperty("jco.client.sysnr", system.getSystemNumber());
properties.setProperty("jco.client.client", system.getClient());
properties.setProperty("jco.client.user", system.getUser());
properties.setProperty("jco.client.passwd", system.getPassword());
properties.setProperty("jco.client.lang", system.getLanguage());
System.out.println("******* Connection Parameter Set *******");
MyDestinationDataProvider myProvider = new MyDestinationDataProvider();
System.out.println("******* Destination Provider Set *******");
myProvider.changePropertiesForABAP_AS(properties);
if (!Environment.isDestinationDataProviderRegistered()) {
System.out.println("Registering Destination Provider CRM");
Environment.registerDestinationDataProvider((DestinationDataProvider) myProvider);
}else{
System.out.println("Destination Provider already set..CRM");
connCRM_flag = false;
}
try {
dest = JCoDestinationManager.getDestination((String) SAP_SERVER);
repos = dest.getRepository();
if (repos == null) {
System.out.println("Repos is null.....");
} else {
System.out.println("Repos is not null.....");
}
System.out.println("After getting repos...");
if(connCRM_flag){
System.out.println("CRM Connection Successfull...");
}
} catch (Exception ex) {
System.out.println(ex);
}
return dest;
}
}
The JCo JavaDoc documentation says:
Only one implementation of DestinationDataProvider can be registered.
For registering another implementation the infrastructure has first to
unregister the implementation that is currently registered. It is not
recommended to permanently exchange DestinationDataProvider
registrations. The one registered instance should globally manage all
destination configurations for the whole infrastructure environment.
So you have to register ONE instance of the DestinationDataProvider, this is an instance of your class MyDestinationDataProvider.
This implementation needs to manage and store ALL the different logon configurations for all your SAP systems, accessible via a distinct destination name string. A simple HashMap<String, Properties> would be a sufficient storage form for this. Add the two Properties instances with distinct destination name strings to the HashMap and return the Properties instance associated to the passed destinationName from method MyDestinationDataProvider.getDestinationProperties(String destinationName).
So you can access the desired JCoDestination instance targeting any SAP system via its specific destination name. In your example you used "SAP_SERVER" for both destination configurations which won't work. For example, use the SAP System ID (SID) as the destination name (key) instead.
If using the names from your example, then JCoDestinationManager.getDestination("CRM") would return the JCoDestination instance for system "CRM" and JCoDestinationManager.getDestination("R3") the JCoDestination for system "R3". And both can be used independently and simultaneously. That's it.
Sharing with you a solution I recently came up with for this exact problem. I discovered two different ways to implement CustomDestinationDataProvider so that I could use multiple destinations.
Something that I did that helped out with both of my different solutions was change out the method in CustomDestinationDataProvider that instantiates the MyDestinationDataProvider inner class so that instead of returning ArrayList, it returns JCoDestination. I changed the name of this method from executeSAPCall to getDestination.
The first way that I discovered that allowed me to use multiple destinations, successfully changing out destinations, was to introduce a class variable for MyDestinationDataProvider so that I could keep my instantiated version. Please note that for this solution, the CustomDestinationDataProvider class is still embedded within my java application code.
I found that this solution only worked for one application. I was not able to use this mechanism in multiple applications on the same tomcat server, but at least I was finally able to successfully switch destinations. Here is the code for CustomDestinationDataProvider.java for this first solution:
public class CustomDestinationDataProvider {
private MyDestinationDataProvider gProvider; // class version of MyDestinationDataProvider
public class MyDestinationDataProvider implements DestinationDataProvider {
private DestinationDataEventListener eL;
private HashMap<String, Properties> secureDBStorage = new HashMap<String, Properties>();
public Properties getDestinationProperties(String destinationName) {
try {
Properties p = secureDBStorage.get(destinationName);
if(p!=null) {
if(p.isEmpty())
throw new DataProviderException(DataProviderException.Reason.INVALID_CONFIGURATION, "destination configuration is incorrect", null);
return p;
}
return null;
} catch(RuntimeException re) {
System.out.println("getDestinationProperties: Exception detected!!! message = " + re.getMessage());
throw new DataProviderException(DataProviderException.Reason.INTERNAL_ERROR, re);
}
}
public void setDestinationDataEventListener(DestinationDataEventListener eventListener) {
this.eL = eventListener;
}
public boolean supportsEvents() {
return true;
}
public void changeProperties(String destName, Properties properties) {
synchronized(secureDBStorage) {
if(properties==null) {
if(secureDBStorage.remove(destName)!=null) {
eL.deleted(destName);
}
} else {
secureDBStorage.put(destName, properties);
eL.updated(destName); // create or updated
}
}
}
}
public JCoDestination getDestination(String destName, Properties connectProperties) {
MyDestinationDataProvider myProvider = new MyDestinationDataProvider();
boolean destinationDataProviderRegistered = com.sap.conn.jco.ext.Environment.isDestinationDataProviderRegistered();
if (!destinationDataProviderRegistered) {
try {
com.sap.conn.jco.ext.Environment.registerDestinationDataProvider(myProvider);
gProvider = myProvider; // save our destination data provider in the class var
} catch(IllegalStateException providerAlreadyRegisteredException) {
throw new Error(providerAlreadyRegisteredException);
}
} else {
myProvider = gProvider; // get the destination data provider from the class var.
}
myProvider.changeProperties(destName, connectProperties);
JCoDestination dest = null;
try {
dest = JCoDestinationManager.getDestination(destName);
} catch(JCoException e) {
e.printStackTrace();
} catch (Exception e) {
// TODO Auto-generated catch block
}
return dest;
}
}
This is the code in my servlet class that I use to instantiate and call CustomDestinationDataProvider within my application code:
CustomDestinationDataProvider cddp = new CustomDestinationDataProvider();
SAPDAO sapDAO = new SAPDAO();
Properties p1 = getProperties("SAPSystem01");
Properties p2 = getProperties("SAPSystem02");
try {
JCoDestination dest = cddp.getDestination("SAP_R3_USERID_01", p1); // establish the first destination
sapDAO.searchEmployees(dest, searchCriteria); // call the first BAPI
dest = cddp.getDestination("SAP_R3_USERID_02", p2); // establish the second destination
sapDAO.searchAvailability(dest); // call the second BAPI
} catch (Exception e) {
e.printStackTrace();
}
Again, this solution only works within one application. If you implement this code directly into more than one application, the first app that calls this code gets the resource and the other one will error out.
The second solution that I came up with allows multiple java applications to use the CustomDestinationDataProvider class at the same time. I broke the CustomDestinationDataProvider class out of my application code and created a separate java spring application for it (not a web application) for the purpose of creating a jar. I then transformed the MyDestinationDataProvider inner class into a singleton. Here's the code for the singleton version of CustomDestinationDataProvider:
public class CustomDestinationDataProvider {
public static class MyDestinationDataProvider implements DestinationDataProvider {
////////////////////////////////////////////////////////////////////
// The following lines convert MyDestinationDataProvider into a singleton. Notice
// that the MyDestinationDataProvider class has now been declared as static.
private static MyDestinationDataProvider myDestinationDataProvider = null;
private MyDestinationDataProvider() {
}
public static MyDestinationDataProvider getInstance() {
if (myDestinationDataProvider == null) {
myDestinationDataProvider = new MyDestinationDataProvider();
}
return myDestinationDataProvider;
}
////////////////////////////////////////////////////////////////////
private DestinationDataEventListener eL;
private HashMap<String, Properties> secureDBStorage = new HashMap<String, Properties>();
public Properties getDestinationProperties(String destinationName) {
try {
Properties p = secureDBStorage.get(destinationName);
if(p!=null) {
if(p.isEmpty())
throw new DataProviderException(DataProviderException.Reason.INVALID_CONFIGURATION, "destination configuration is incorrect", null);
return p;
}
return null;
} catch(RuntimeException re) {
throw new DataProviderException(DataProviderException.Reason.INTERNAL_ERROR, re);
}
}
public void setDestinationDataEventListener(DestinationDataEventListener eventListener) {
this.eL = eventListener;
}
public boolean supportsEvents() {
return true;
}
public void changeProperties(String destName, Properties properties) {
synchronized(secureDBStorage) {
if(properties==null) {
if(secureDBStorage.remove(destName)!=null) {
eL.deleted(destName);
}
} else {
secureDBStorage.put(destName, properties);
eL.updated(destName); // create or updated
}
}
}
}
public JCoDestination getDestination(String destName, Properties connectProperties) throws Exception {
MyDestinationDataProvider myProvider = MyDestinationDataProvider.getInstance();
boolean destinationDataProviderRegistered = com.sap.conn.jco.ext.Environment.isDestinationDataProviderRegistered();
if (!destinationDataProviderRegistered) {
try {
com.sap.conn.jco.ext.Environment.registerDestinationDataProvider(myProvider);
} catch(IllegalStateException providerAlreadyRegisteredException) {
throw new Error(providerAlreadyRegisteredException);
}
}
myProvider.changeProperties(destName, connectProperties);
JCoDestination dest = null;
try {
dest = JCoDestinationManager.getDestination(destName);
} catch(JCoException ex) {
ex.printStackTrace();
throw ex;
} catch (Exception ex) {
ex.printStackTrace();
throw ex;
}
return dest;
}
}
After putting this code into the jar file application and creating the jar file (I call it JCOConnector.jar), I put the jar file on the shared library classpath of my tomcat server and restarted the tomcat server. In my case, this was /opt/tomcat/shared/lib. Check your /opt/tomcat/conf/catalina.properties file for the shared.loader line for the location of your shared library classpath. Mine looks like this:
shared.loader=\
${catalina.home}/shared/lib\*.jar,${catalina.home}/shared/lib
I also put a copy of this jar file in the "C:\Users\userid\Documents\jars" folder on my workstation so that the test application code could see the code in the jar and compile. I then referenced this copy of the jar file in my pom.xml file in both of my test applications:
<dependency>
<groupId>com.mycompany</groupId>
<artifactId>jcoconnector</artifactId>
<version>1.0</version>
<scope>system</scope>
<systemPath>C:\Users\userid\Documents\jars\JCOConnector.jar</systemPath>
</dependency>
After adding this to the pom.xml file, I right clicked on each project, selected Maven -> Update Project..., and I then right clicked again on each project and selected 'Refresh'. Something very important that I learned was to not add a copy of JCOConnector.jar directly to either of my test projects. The reason for this is because I want the code from the jar file in /opt/tomcat/shared/lib/JCOConnector.jar to be used. I then built and deployed each of my test apps to the tomcat server.
The code that calls my JCOConnector.jar shared library in my first test application looks like this:
CustomDestinationDataProvider cddp = new CustomDestinationDataProvider();
JCoDestination dest = null;
SAPDAO sapDAO = new SAPDAO();
Properties p1 = getProperties("SAPSystem01");
try {
dest = cddp.getDestination("SAP_R3_USERID_01", p1);
sapDAO.searchEmployees(dest);
} catch (Exception ex) {
ex.printStackTrace();
}
The code in my second test application that calls my JCOConnector.jar shared library looks like this:
CustomDestinationDataProvider cddp = new CustomDestinationDataProvider();
JCoDestination dest = null;
SAPDAO sapDAO = new SAPDAO();
Properties p2 = getProperties("SAPSystem02");
try {
dest = cddp.getDestination("SAP_R3_USERID_02", p2);
sapDAO.searchAvailability(dest);
} catch (Exception ex) {
ex.printStackTrace();
}
I know that I've left out a lot of the steps involved in first getting the SAP JCO 3 library installed on your workstation and server. I do hope that this helps out at least one other person of getting over the hill of trying to get multiple spring mvc java spplications talking to SAP on the same server.
I'd like to use a database to store i18n key/value pairs so we can modify / reload the i18n data at runtime. Has anyone done this? Or does anyone have an idea of how to implement this? I've read several threads on this, but I haven't seen a workable solution.
I'm specifically refering to something that would work with the jstl tags such as
<fmt:setlocale>
<fmt:bundle>
<fmt:setBundle>
<fmt:message>
I think this will involve extending ResourceBundle, but when I tried this I ran into problems that had to do with the way the jstl tags get the resource bundle.
I finally got this working with danb's help above.
This is my resource bundle class and resource bundle control class.
I used this code from #[danb]'s.
ResourceBundle bundle = ResourceBundle.getBundle("AwesomeBundle", locale, DbResourceBundle.getMyControl());
javax.servlet.jsp.jstl.core.Config.set(actionBeanContext.getRequest(), Config.FMT_LOCALIZATION_CONTEXT, new LocalizationContext(bundle, locale));
and wrote this class.
public class DbResourceBundle extends ResourceBundle
{
private Properties properties;
public DbResourceBundle(Properties inProperties)
{
properties = inProperties;
}
#Override
#SuppressWarnings(value = { "unchecked" })
public Enumeration<String> getKeys()
{
return properties != null ? ((Enumeration<String>) properties.propertyNames()) : null;
}
#Override
protected Object handleGetObject(String key)
{
return properties.getProperty(key);
}
public static ResourceBundle.Control getMyControl()
{
return new ResourceBundle.Control()
{
#Override
public List<String> getFormats(String baseName)
{
if (baseName == null)
{
throw new NullPointerException();
}
return Arrays.asList("db");
}
#Override
public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload) throws IllegalAccessException,
InstantiationException, IOException
{
if ((baseName == null) || (locale == null) || (format == null) || (loader == null))
throw new NullPointerException();
ResourceBundle bundle = null;
if (format.equals("db"))
{
Properties p = new Properties();
DataSource ds = (DataSource) ContextFactory.getApplicationContext().getBean("clinicalDataSource");
Connection con = null;
Statement s = null;
ResultSet rs = null;
try
{
con = ds.getConnection();
StringBuilder query = new StringBuilder();
query.append("select label, value from i18n where bundle='" + StringEscapeUtils.escapeSql(baseName) + "' ");
if (locale != null)
{
if (StringUtils.isNotBlank(locale.getCountry()))
{
query.append("and country='" + escapeSql(locale.getCountry()) + "' ");
}
if (StringUtils.isNotBlank(locale.getLanguage()))
{
query.append("and language='" + escapeSql(locale.getLanguage()) + "' ");
}
if (StringUtils.isNotBlank(locale.getVariant()))
{
query.append("and variant='" + escapeSql(locale.getVariant()) + "' ");
}
}
s = con.createStatement();
rs = s.executeQuery(query.toString());
while (rs.next())
{
p.setProperty(rs.getString(1), rs.getString(2));
}
}
catch (Exception e)
{
e.printStackTrace();
throw new RuntimeException("Can not build properties: " + e);
}
finally
{
DbUtils.closeQuietly(con, s, rs);
}
bundle = new DbResourceBundle(p);
}
return bundle;
}
#Override
public long getTimeToLive(String baseName, Locale locale)
{
return 1000 * 60 * 30;
}
#Override
public boolean needsReload(String baseName, Locale locale, String format, ClassLoader loader, ResourceBundle bundle, long loadTime)
{
return true;
}
};
}
Are you just asking how to store UTF-8/16 characters in a DB? in mysql it's just a matter of making sure you build with UTF8 support and setting that as the default, or specifying it at the column or table level. I've done this in oracle and mysql before. Create a table and cut and paste some i18n data into it and see what happens... you might be set already..
or am I completely missing your point?
edit:
to be more explicit... I usually implement a three column table... language, key, value... where "value" contains potentially foreign language words or phrases... "language" contains some language key and "key" is an english key (i.e. login.error.password.dup)... language and key are indexed...
I've then built interfaces on a structure like this that shows each key with all its translations (values)... it can get fancy and include audit trails and "dirty" markers and all the other stuff you need to enable translators and data entry folk to make use of it..
Edit 2:
Now that you added the info about the JSTL tags, I understand a bit more... I've never done that myself.. but I found this old info on theserverside...
HttpSession session = .. [get hold of the session]
ResourceBundle bundle = new PropertyResourceBundle(toInputStream(myOwnProperties)) [toInputStream just stores the properties into an inputstream]
Locale locale = .. [get hold of the locale]
javax.servlet.jsp.jstl.core.Config.set(session, Config.FMT_LOCALIZATION_CONTEXT, new LocalizationContext(bundle ,locale));
We have a database table with key/language/term where key is a n integer and is a combined primary key together with language.
We are using Struts, so we ended up writing our own PropertyMessageResources implementation which allows us to do something like <bean:message key="impressum.text" />.
It works very well and gives us the flexibility to do dynamically switch languages in the front-end as well as updating the translations on the fly.
Actuly what ScArcher2 needed is davids response which is not marked a correct or helpfull.
The solution ScArcher2 chose to use is imo terrible mestake:) Loading ALL the translations at one time... in any bigger application its gonna kill it. Loading thousends of translations each request...
david's method is more commonly used in real production environments.
Sometimes to limit db calls, which is with every message translated, you can create groups of translations by topic, functionality etc. to preload them. But this is little bit more complex and can be substituted with good cache system.