In a multi-tenant Spring Boot application, I'm trying to load configuration objects. Ideally, I'd like to load certain properties file into a configuration object programmatically. I'm looking for a simple way to load the configuration by passing a properties file and the final class to map it to. The following is just an example of what I'm trying to achieve.
Directory structure of the configurations:
config/
- common.properties
all_tenants_config/
- foo_tenant/
- database.properties
- api.properties
- bar_tenant/
- database.properties
- api.properties
Configuration POJOs:
class DatabaseProperties {
#Value("${database.url}")
private String url;
}
class APIProperties {
#Value("${api.endPoint}")
private String endPoint;
}
Configuration Provider:
#Singleton
class ConfigurationProvider {
private Map<String, DatabaseProperties> DB_PROPERTIES = new HashMap<>();
private Map<String, APIProperties> API_PROPERTIES = new HashMap<>();
public ConfigurationProvider(#Value(${"tenantsConfigPath"}) String tenantsConfigPath) {
for (File tenant : Path.of(tenantsConfigPath).toFile().listFiles()) {
String tenantName = tenant.getName();
for (File configFile : tenant.listFiles()) {
String configName = configFile.getName();
if ("database.properties".equals(configName)) {
// This is what I'm looking for. An easy way to load the configuration by passing a properties file and the final class to map it to.
DB_PROPERTIES.put(tenant, SPRING_CONFIG_LOADER.load(configFile, DatabaseProperties.class));
} else if ("api.properties".equals(configName)) {
API_PROPERTIES.put(tenant, SPRING_CONFIG_LOADER.load(configFile, API.class));
}
}
}
}
public currentTenantDBProperties() {
return DB_PROPERTIES.get(CURRENT_TENANT_ID);
}
public currentTenantAPIProperties() {
return API_PROPERTIES.get(CURRENT_TENANT_ID);
}
}
In short, is there a way in Spring that allows to map a properties file to an object without using the default Spring's configuration annotations.
Well, in this case you do not need any Spring's feature.
Spring is a bean container, but in this place you just new an object by yourself and put it on your map cache.
Step 1: decode property file to Java Properties Class Object
Step 2: turn your properties object to your target object, just use some utils like objectmapper
FileReader reader = new FileReader("db.properties"); // replace file name with your variable
Properties p = new Properties();
p.load(reader);
ObjectMapper mapper = new ObjectMapper();
DatabaseProperties databaseProperties = mapper.convertValue(p,
DatabaseProperties.class);
Related
Values from .properties file could not read due to exception (org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'genderOptions' cannot be found)
I have configured the property place holder. My property file is having two entries (M=MALE, F=FEMALE) I wanted to populate this as a list of options in checkbox while submitting the form.
#Bean
public static PropertySourcesPlaceholderConfigurer placeHolderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
#Controller
#RequestMapping("/player")
#PropertySource(ignoreResourceNotFound = true, value =
"classpath:gender.properties")
public class PlayerController {
#Value("#{genderOptions}")
public Map<String, String> genderOptions;
#RequestMapping("/playerForm")
public String showPlayerForm(Model model) {
Player player = new Player();
model.addAttribute("player", player);
model.addAttribute("genderOptions", genderOptions);
return "player-form";
}
If you want to use genderOptions as Map in the Controller, then first you need specify it in the form of key-value in gender.properties file.
genderOptions = {M:'Male', F:'Female'}
And while accessing it in the controller, you need to make following changes in order to let spring cast it in Map.
#Value("#{${genderOptions}}")
private Map<String, String> mapValues;
And if you need to get the value of a specific key in the Map, all you have to do is add the key's name in the expression:
#Value("#{${genderOptions}.M}")
private String maleKey;
My application expects to find a configuration file called MyPojo.json, loaded into MyPojo class by MyService class:
#Data // (Lombok's) getters and setters
public class MyPojo {
int foo = 42;
int bar = 1337;
}
It's not a problem if it doesn't exist: in that case, the application will create it with default values.
The path where to read/write MyPojo.json is stored in /src/main/resources/settings.properties:
the.path=cfg/MyPojo.json
which is passed to MyService through Spring's #PropertySource as follows:
#Configuration
#PropertySource("classpath:settings.properties")
public class MyService {
#Inject
Environment settings; // "src/main/resources/settings.properties"
#Bean
public MyPojo load() throws Exception {
MyPojo pojo = null;
// "cfg/MyPojo.json"
Path path = Paths.get(settings.getProperty("the.path"));
if (Files.exists(confFile)){
pojo = new ObjectMapper().readValue(path.toFile(), MyPojo.class);
} else { // JSON file is missing, I create it.
pojo = new MyPojo();
Files.createDirectory(path.getParent()); // create "cfg/"
new ObjectMapper().writeValue(path.toFile(), pojo); // create "cfg/MyPojo.json"
}
return pojo;
}
}
Since MyPojo's path is relative, when I run this from a Unit Test
#Test
public void testCanRunMockProcesses() {
try (AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(MyService.class)){
MyPojo pojo = ctx.getBean(MyPojo.class);
String foo = pojo.getFoo();
...
// do assertion
}
}
the cfg/MyPojo.json is created under the root of my project, which is definitely not what I want.
I would like MyPojo.json to be created under my target folder, eg. /build in Gradle projects, or /target in Maven projects.
To do that, I've created a secondary settings.properties under src/test/resources, containing
the.path=build/cfg/MyPojo.json
and tried to feed it to MyService in several ways, without success.
Even if called by the test case, MyService is always reading src/main/resources/settings.properties instead of src/test/resources/settings.properties.
With two log4j2.xml resources instead (src/main/resources/log4j2.xml and src/test/resources/log4j2-test.xml), it worked :/
Can I do the same with a property file injected by Spring with #PropertySource ?
You can use #TestPropertySource annotation for this.
Example:
For single property:
#TestPropertySource(properties = "property.name=value")
For property file
#TestPropertySource(
locations = "classpath:yourproperty.properties")
So, you provide path for MyPojo.json like
#TestPropertySource(properties = "path=build/cfg/MyPojo.json")
I've a spring bean which loads the property file depending upon their availability as shown below:-
#PropertySources({ #PropertySource(value = "classpath:user.properties"),
#PropertySource(value = "file:./config/user.properties", ignoreResourceNotFound = true) })
The property file is getting loaded, but when I try to read entire property file in one go via :-
Properties properties = PropertiesLoaderUtils.loadAllProperties("user.properties");
then I only get the properties from classpath. Do spring provide any mechanism to read all properties in one go?
That code of yours doesn't do what the annotations do. You have a couple of annotations that declare what to do. That logic isn't present at all in the code snippet.
There's no magic, if you want the same result, you need to translate the declarative aspects of those annotations in code (i.e. reading the classpath file then the file one and check if it exists and then merge those properties).
If you're ok to get extra keys, you could also simply inject the Environment as #PropertySource is going to update that.
Answering my own question, may be this may help someone.
Since I need to override the properties file contained in jar with external properties file (if present in specified folder) also I need to read entire property file in one go.
I've leveraged the spring behavior of loading last property read.
#PropertySources({ #PropertySource(value = "classpath:application.properties"),
#PropertySource(value = "file:./config/application.properties", ignoreResourceNotFound = true) })
Now if application.properties is present in ./config/ location then it'll override application.properties from classpath.
In main application.properties I've defined from where the external properties should get loaded i.e.
config.location=./config/
./config/ attribute can be overridden in case of production and test environment.
After this I've defined a bean to load all properties files (import statement skipped):-
#Component
public class PropertiesConfig {
private final Logger logger = LoggerFactory.getLogger(PropertiesConfig.class);
private final String[] PROPERTIES_FILENAMES = { "prop1.properties", "prop2.properties",
"prop3.properties" };
private String configLocation;
private Map<String, Properties> configProperties;
#Autowired
public PropertiesConfig(#Value("${config.location}") String configLocation) {
this.configLocation = configLocation;
configProperties = Arrays.stream(PROPERTIES_FILENAMES)
.collect(Collectors.toMap(filename -> filename, this::loadProperties));
}
public Properties getProperties(String fileName) {
if (StringUtils.isEmpty(fileName) || !configProperties.containsKey(fileName)) {
logger.info(String.format("Invalid property name : %s", fileName));
throw new IllegalArgumentException(
String.format("Invalid property name : %s", fileName));
}
return configProperties.get(fileName);
}
private Properties loadProperties(final String filename) {
final Resource[] possiblePropertiesResources = { new ClassPathResource(filename),
new PathResource(getCustomPath(filename)) };
final Resource resource = Arrays.stream(possiblePropertiesResources)
.filter(Resource::exists).reduce((previous, current) -> current).get();
final Properties properties = new Properties();
try {
properties.load(resource.getInputStream());
} catch (final IOException exception) {
throw new RuntimeException(exception);
}
logger.info("Using {} as user resource", resource);
return properties;
}
private String getCustomPath(final String filename) {
return configLocation.endsWith(".properties") ? configLocation : configLocation + filename;
}
}
Now you have a bean containing all the properties file in map which can be injected in any bean and can be overridden for any environment.
I am using the dropwizard framework which uses Yaml for configuration.I wanted to specify class names in the config file.Is there a way in which Yaml will parse it to Class objects or do I have to do it using Strings ?
class Config {
.........
List<Class<?> resourceClasses;
}
YAML file
#resource classes
resourceClasses :
- com.abc.StoresResource
- com.abc.MerchantResource
Are you just looking for a convenient way to register your resources or do you really want to configure the resources in the YAML configuration? If it's the former this is what I'd do:
for (ClassInfo classInfo: ClassPath.from(getClass().getClassLoader()).getTopLevelClasses("com.abc")) {
environment.jersey().register(i.getInstance(classInfo.load()));
}
If it's the latter you'd have to parse those strings yourself. I don't think that the YAML configuration will do that for you. Would something like this work:
class Config {
List<Class<?>> resourceClasses = new ArrayList<Class<?>>();
#JsonProperty("resourceClasses")
public void setResourceClasses(Collection<String> classNames) throws ClassNotFoundException {
for (String className : classNames) {
resourceClasses.add(Class.forName(className));
}
}
}
I have a web application project, and I'm trying to unit test a method that creates a file using a FreeMarker template. My method createFile() should take a MyFile type - that contains the File name to create and the rootMap FreeMarker needs and the template name - and create a file using the template I provide.
I am following the Freemarker manual to set a Template Loader. The problem is, I'm using the TemplateLoader setClassForTemplateLoading(Class, String) method to find the template path. This template loader uses the Class.getResource() to get the classpath.
But, since I'm using Maven, I have my java code in /src/main/java, my template in /src/main/webapp/templates/ and my test code in /src/test/java. Therefore, my Class.getResource("/") (root classpath) always returns <PATH_TO_PROJECT>/target/test-classes/.
Since I will be deploying a war, I cannot use the setDirectoryForTemplateLoading(File). Also, since I'm testing my app I don't have a ServletContext to use with setServletContextForTemplateLoading(Object, String).
How can I access my template folder from the test case?
Here's a simplified example of my test code (I use mockito to mock the behaviour of the MyFile class):
private MyFile myFile;
private FileGenerator fileGenerator;
#Before
public void setUp() {
myFile = new MyFile(...);
fileGenerator = new FileGenerator(myFile, ...);
}
#Test
public void shouldCreateFile() {
final MyFile mockedMyFile = spy(file);
final Map<String, Object> rootMap = new HashMap<String, Object>();
// populates rootMap with stuff needed for the Template
// mocking method return
when(mockedMyFile.getRootMap()).thenReturn(rootMap);
// replacing the MyFile implementation with my Mock
fileGenerator.setMyFile(mockedMyFile);
// calling the method I want to test
fileGenerator.createFile();
assertTrue(MyFile.getFile().exists());
}
And here is a simplification of the code I'm testing:
public void createFile() {
final Configuration cfg = new Configuration();
cfg.setClassForTemplateLoading(getClass(), "templates/");
try {
myFile.getFile().createNewFile();
final Template template = cfg.getTemplate("template.ftl");
final Writer writer = new FileWriter(myFile.getFile());
template.process(myFile.getRootMap(), writer);
writer.flush();
writer.close();
}
// exception handling
}
I applied Charles Forsythe's suggestion, it worked out fine.
I just added a templateLoader member to the FileGenerator class, with its own getter and setter.
Next, in my createFile method, I use the method setTemplateLoader(TemplateLoader) from the Configuration class, as such:
public void createFile() {
final Configuration cfg = new Configuration();
// Changed
cfg.setTemplateLoader(templateLoader);
// the rest
}
Finally, I just create a template loader for my test:
#Test
public void shouldCreateFile() {
final MyFile mockedMyFile = spy(file);
final Map<String, Object> rootMap = new HashMap<String, Object>();
TemplateLoader templateLoader = null;
try {
templateLoader = new FileTemplateLoader(new File("<PATH TO TEMPLATE FOLDER>"));
} catch (final IOException e) {
e.printStackTrace();
}
gerador.setTemplateLoader(templateLoader);
// the rest
}
And problem solved. In my production code, I use ClassTemplateLoader instead of FileTemplateLoader.