I was looking at the spring-boot documentation located here
Specifically the section regarding the order in which the properties are considered:
More specifically:
Profile-specific application properties packaged inside your jar (application-{profile}.properties and YAML variants)
Let me first mention that I am not having any issues loading profile specific configurations using this approach(provided that the files are located in classpath:/ or classpath:/config.
However, what I am hoping to do is implement a convention like the following:
classpath:/default/application.properties
classpath:/{profile}/application.properties
Furthermore I'd like to achieve this configuration without making use of the spring.config.location property. I'm pretty new to Spring Boot so I'm looking for some hints as how to how I would implement this convention. Based on my research It seems that this might be achievable by adding a custom ConfigFileApplicationListener. Please let me know if that is a sensible starting point or any other ideas that might be better.
Update:
It seems that if I could programmatically build out the spring.config.location list of properties I could pass in locations such as classpath:/default, classpath:{profile}. based on the spring.profiles.active environment variable. The following ConfigFileApplicationListener seems like its the one I want to call:
public void setSearchLocations(String locations)
However, I'm not sure where in the lifecycle I would make such a call.
So here is what I managed to come up with, not sure if I'll even go with this solution but I figured I'll offer it up in case there is any helpful feedback.
So I resorted to trying to set the call the setSearchLocations(String locations) method on the ConfigFileApplicationListener after it has been added to the SpringApplication but before its triggered. I did this by adding a new listener that also implements Ordered and made sure it ran before ConfigFileApplicationListener. This seems to do what I want but I'm still thinking there is a more elegant approach. I especially dont like having to iterate over the Listeners.
public class LocationsSettingConfigFileApplicationListener implements
ApplicationListener<ApplicationEnvironmentPreparedEvent>, Ordered {
/**
* this should run before ConfigFileApplicationListener so it can set its
* state accordingly
*/
#Override
public int getOrder() {
return ConfigFileApplicationListener.DEFAULT_ORDER - 1;
}
#Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
SpringApplication app = event.getSpringApplication();
ConfigurableEnvironment env = event.getEnvironment();
for (ApplicationListener<?> listener : app.getListeners()) {
if (listener instanceof ConfigFileApplicationListener) {
ConfigFileApplicationListener cfal = (ConfigFileApplicationListener) listener;
//getSearchLocations omitted
cfal.setSearchLocations(getSearchLocations(env));
}
}
}
A solution that doesn't require writing a new class:
public static void main(String[] args) {
SpringApplication app = new SpringApplication();
app.getListeners().stream()
.filter(listener -> listener instanceof ConfigFileApplicationListener)
.forEach(configListener -> {
((ConfigFileApplicationListener) configListener).setSearchLocations(mySearchLocations);
((ConfigFileApplicationListener) configListener).setSearchNames(mySearchNames);
});
app.setSources(singleton(MyClassName.class));
app.run(args);
}
We did something similar with an EnvironmentPostProcessor to achieve the following naming convention:
System properties
environment variables
"random" (not used, but we kept that default PropertySource)
file:${foo.home}/foo-<profile>.properties
classpath*:<appName-profile>.properties
classpath*:application-profile.properties
classpath*:<appName>.properties
classpath*:application.properties
classpath*:meta.properties
Some applications do not have their own <appName>; those that do call setApplicationName in the main class's static initializer to use those two additional files.
The hacky part here is that we do not exclude the default ConfigFileApplicationListener, but undo it by removing PropertySource ConfigFileApplicationListener.APPLICATION_CONFIGURATION_PROPERTY_SOURCE_NAME.
File FooPropertiesEnvPostProcessor.java
package example.foo.utils.spring;
import static org.springframework.core.env.AbstractEnvironment.DEFAULT_PROFILES_PROPERTY_NAME;
import java.io.IOException;
import java.util.List;
import java.util.Spliterators;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.config.ConfigFileApplicationListener;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.env.PropertySourceLoader;
import org.springframework.boot.env.PropertySourcesLoader;
import org.springframework.boot.logging.LoggingApplicationListener;
import org.springframework.core.Ordered;
import org.springframework.core.env.AbstractEnvironment;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertyResolver;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.core.io.support.SpringFactoriesLoader;
/**
* Configures environment properties according to the FOO conventions.
*/
public class FooPropertiesEnvPostProcessor implements EnvironmentPostProcessor, Ordered {
/**
* Order before LoggingApplicationListener and before
* AutowiredAnnotationBeanPostProcessor. The position relative to
* ConfigFileApplicationListener (which we want to override) should not
* matter: If it runs before this, we remove its PropertySource; otherwise,
* its PropertySource remains but should do no harm as it is added at the
* end.
*/
public static final int ORDER
= Math.min(LoggingApplicationListener.DEFAULT_ORDER, new AutowiredAnnotationBeanPostProcessor().getOrder()) - 1;
static {
System.setProperty(AbstractEnvironment.DEFAULT_PROFILES_PROPERTY_NAME,
System.getProperty(AbstractEnvironment.DEFAULT_PROFILES_PROPERTY_NAME, "production"));
}
public FooPropertiesEnvPostProcessor() {
}
/**
* Property key used as the application (sub-project) specific part in
* properties file names.
* <p>
* <strong>Note:</strong> Direct access to this property key is meant for
* tests which set the property in an annotation (e.g.
* {#link IntegrationTest}). However, SpringBootApplications which need to
* set this system property before Spring initialization should call
* {#link #setApplicationName(String) setApplicationName} instead.
* </p>
*/
public static final String APP_KEY = "foo.config.name";
/**
* Sets the application name used to find property files (using
* {#link FooPropertiesEnvPostProcessor}).
*
* #param appName
* the application name
*/
public static void setApplicationName(String appName) {
System.setProperty(APP_KEY, appName);
}
/**
* Replacement for logging, which is not yet initialized during
* postProcessEnvironment.
*/
static void log(String format, Object... args) {
System.out.println(String.format(format, args));
}
static void debug(PropertyResolver env, String format, Object... args) {
String level = env.getProperty("logging.level." + FooPropertiesEnvPostProcessor.class.getName());
if ("trace".equalsIgnoreCase(level) || "debug".equalsIgnoreCase(level)) {
log(format, args);
}
}
static void trace(PropertyResolver env, String format, Object... args) {
String level = env.getProperty("logging.level." + FooPropertiesEnvPostProcessor.class.getName());
if ("trace".equalsIgnoreCase(level)) {
log(format, args);
}
}
#Override
public int getOrder() {
return ORDER;
}
#Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
addProperties(environment.getPropertySources(), application.getResourceLoader(), environment);
}
public static void addProperties(MutablePropertySources propSources, ResourceLoader resLoader, ConfigurableEnvironment propRes) {
trace(propRes, "FooPropertiesEnvPostProcessor.addProperties(..)");
List<PropertySourceLoader> psls = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
PropertySourcesLoader.class.getClassLoader());
// ResourcePatternUtils does not accept null yet
// (https://jira.spring.io/browse/SPR-14500)
ResourcePatternResolver rpr = resLoader != null ? ResourcePatternUtils.getResourcePatternResolver(resLoader)
: new PathMatchingResourcePatternResolver();
final String suffix = ".properties"; // SonarQube made me declare this
String[] profiles = propRes.getActiveProfiles();
if (profiles.length == 0) {
profiles = new String[] { System.getProperty(DEFAULT_PROFILES_PROPERTY_NAME) };
}
// ConfigFileApplicationListener adds PropertySource "applicationConfigurationProperties" consisting of
// - "applicationConfig: [classpath:/${spring.config.name}-<profile>.properties]"
// - "applicationConfig: [classpath:/${spring.config.name}.properties]"
// Since we want the profile to have higher priority than the app name, we cannot just set
// "spring.config.name" to the app name, use ConfigFileApplicationListener, and add
// "application-<profile>.properties" and "application.properties".
// Instead, remove ConfigFileApplicationListener:
PropertySource<?> removedPropSource = propSources.remove(ConfigFileApplicationListener.APPLICATION_CONFIGURATION_PROPERTY_SOURCE_NAME);
trace(propRes, "removed %s from %s", removedPropSource, propSources);
// add meta.properties at last position, then others before the previously added. => resulting order:
// - { systemProperties
// - systemEnvironment
// - random } - already added automatically elsewhere
// - file:${foo.home}/foo-<profile>.properties
// - classpath:<appName>-<profile>.properties
// - classpath:application-<profile>.properties
// - classpath:<appName>.properties
// - classpath:application.properties
// - classpath:meta.properties
// By adding ${foo.home}/... (chronlogically) last, the property can be set in the previously added resources.
boolean defaultAppName = "application".equals(propRes.resolveRequiredPlaceholders("${" + APP_KEY + ":application}"));
String psn = null;
psn = addProperties(propSources, propRes, rpr, psls, true, psn, propRes.resolveRequiredPlaceholders("classpath*:meta" + suffix));
psn = addProperties(propSources, propRes, rpr, psls, true, psn, propRes.resolveRequiredPlaceholders("classpath*:application" + suffix));
if (!defaultAppName) {
psn = addProperties(propSources, propRes, rpr, psls, false,
psn, propRes.resolveRequiredPlaceholders("classpath*:${" + APP_KEY + ":application}" + suffix));
}
for (String profile : profiles) {
psn = addProperties(propSources, propRes, rpr, psls, false, psn,
propRes.resolveRequiredPlaceholders("classpath*:application-" + profile + suffix));
}
if (!defaultAppName) {
for (String profile : profiles) {
psn = addProperties(propSources, propRes, rpr, psls, false,
psn, propRes.resolveRequiredPlaceholders("classpath*:${" + APP_KEY + ":application}-" + profile + suffix));
}
}
for (String profile : profiles) {
psn = addProperties(propSources, propRes, rpr, psls, false,
psn, propRes.resolveRequiredPlaceholders("file:${foo.home:.}/foo-" + profile + suffix));
}
Stream<PropertySource<?>> propSourcesStream = StreamSupport.stream(Spliterators.spliteratorUnknownSize(propSources.iterator(), 0), false);
debug(propRes, "Property sources: %s%n", propSourcesStream.map(PropertySource::getName).collect(Collectors.joining(", ")));
}
/**
* Adds a resource given by location string to the given PropertySources, if
* it exists.
*
* #param propSources
* the property sources to modify
* #param successorName
* the name of the (already added) successor resource, i.e. the
* resource before which the new one should be added; if null,
* add as last resource
* #param location
* the location of the resource to add
* #return the name of the newly added resource, or {#code successorName} if
* not added
*/
private static String addProperties(MutablePropertySources propSources, PropertyResolver propRes, ResourcePatternResolver resLoader,
List<PropertySourceLoader> propLoaders, boolean required, String successorName, String location) {
Resource[] resources;
try {
resources = resLoader.getResources(location);
} catch (IOException e) {
throw new IllegalStateException("failed to load property source " + location + ": " + e, e);
}
if (resources.length == 0) {
debug(propRes, "%s property resource not found: %s", required ? "required" : "optional", location);
if (required) {
throw new IllegalStateException("required property source " + location + " not found");
} else {
return successorName;
}
}
String newSuccessorName = successorName;
for (Resource resource : resources) {
boolean exists = resource.exists();
debug(propRes, "%s property resource %sfound: %s%s", required ? "required" : "optional", exists ? "" : "not ", location,
uriDescription(resource, propRes));
if (!required && !exists) {
continue;
}
boolean loaded = false;
for (PropertySourceLoader propLoader : propLoaders) {
if (canLoadFileExtension(propLoader, resource)) {
newSuccessorName = addResource(propSources, propRes, resource, propLoader, newSuccessorName);
loaded = true;
break;
}
}
if (!loaded && required) {
throw new IllegalStateException("No PropertySourceLoader found to load " + resource);
}
}
return newSuccessorName;
}
private static String addResource(MutablePropertySources propSources, PropertyResolver propRes, Resource resource,
PropertySourceLoader propLoader, String successorName) {
try {
PropertySource<?> propSource = propLoader.load(resource.getDescription(), resource, null);
if (propSource == null) {
// e.g. a properties file with everything commented;
// org.springframework.boot.env.PropertiesPropertySourceLoader
// converts empty to null
return successorName;
}
if (successorName == null) {
propSources.addLast(propSource);
} else if (successorName.equals(propSource.getName())) {
// happens if APP_KEY is not set, so that
// "${APP_KEY:application}" == "application"
trace(propRes, "skipping duplicate resource %s", successorName);
} else {
propSources.addBefore(successorName, propSource);
}
return propSource.getName();
} catch (IOException e) {
throw new IllegalStateException("Unable to load configuration file " + resource + ": " + e, e);
}
}
/**
* Stolen from {#link PropertySourcesLoader}
*/
private static boolean canLoadFileExtension(PropertySourceLoader loader, Resource resource) {
String filename = resource.getFilename().toLowerCase();
for (String extension : loader.getFileExtensions()) {
if (filename.endsWith("." + extension.toLowerCase())) {
return true;
}
}
return false;
}
private static String uriDescription(Resource resource, PropertyResolver propRes) {
try {
return resource.exists() ? (" in " + resource.getURI()) : "";
} catch (IOException e) {
trace(propRes, "getURI: %s", e);
return "";
}
}
}
File META-INF/spring.factories
org.springframework.boot.env.EnvironmentPostProcessor = example.foo.utils.spring.FooPropertiesEnvPostProcessor
To get the same properties in tests, they have #ContextConfiguration(..., initializers = TestAppContextInitializer.class).
TestAppContextInitializer implements ApplicationContextInitializer<GenericApplicationContext> and calls
FooPropertiesEnvPostProcessor.addProperties in its initialize method.
Unfortunately, the EnvironmentPostProcessor seems to be missing Spring Shell by default, too. In our case (since only a tiny
part of the application uses Spring Shell), it was sufficient to restrict the <context:component-scan base-package=.../>
scope in META-INF/spring/spring-shell-plugin.xml to contain only stuff which does not need any properties set by the EnvironmentPostProcessor.
# override from outer config , eg. java -jar --spring.profiles.active=your config
spring.profiles.active=dev
spring.config.location=classpath:/${spring.profiles.active}/application.properties
Related
I know that request template supports XPath, so that I can get value from request like {{xPath request.body '/outer/inner/text()'}}. I already have a XML file as response, and I want to inject this value I got from request, but keep the other parts of this response XML intact. For example, I want to inject it to XPATH /svc_result/slia/pos/msid.
And I need to use it in standalone mode.
I see another question(Wiremock Stand alone - How to manipulate response with request data) but that was with JSON, I have XML request/response.
How can it be done? Thanks.
For example, I have this definition of mapping:
{
"request": {
"method": "POST",
"bodyPatterns": [
{
"matchesXPath": {
"expression": "/svc_init/slir/msids/msid[#type='MSISDN']/text()",
"equalTo": "200853000105614"
}
},
{
"matchesXPath": "/svc_init/hdr/client[id and pwd]"
}
]
},
"response": {
"status": 200,
"bodyFileName": "slia.xml",
"headers": {
"Content-Type": "application/xml;charset=UTF-8"
}
}
}
And this request:
<?xml version="1.0"?>
<!DOCTYPE svc_init>
<svc_init ver="3.2.0">
<hdr ver="3.2.0">
<client>
<id>dummy</id>
<pwd>dummy</pwd>
</client>
</hdr>
<slir ver="3.2.0" res_type="SYNC">
<msids>
<msid type="MSISDN">200853000105614</msid>
</msids>
</slir>
</svc_init>
I expect this response, with xxxxxxxxxxx replaced with the <msid> in the request.
<?xml version="1.0" ?>
<!DOCTYPE svc_result SYSTEM "MLP_SVC_RESULT_320.DTD">
<svc_result ver="3.2.0">
<slia ver="3.0.0">
<pos>
<msid type="MSISDN" enc="ASC">xxxxxxxxxxx</msid>
<pd>
<time utc_off="+0800">20111122144915</time>
<shape>
<EllipticalArea srsName="www.epsg.org#4326">
<coord>
<X>00 01 01N</X>
<Y>016 31 53E</Y>
</coord>
<angle>0</angle>
<semiMajor>2091</semiMajor>
<semiMinor>2091</semiMinor>
<angularUnit>Degrees</angularUnit>
</EllipticalArea>
</shape>
<lev_conf>90</lev_conf>
</pd>
<gsm_net_param>
<cgi>
<mcc>100</mcc>
<mnc>01</mnc>
<lac>2222</lac>
<cellid>10002</cellid>
</cgi>
<neid>
<vmscid>
<vmscno>00004946000</vmscno>
</vmscid>
<vlrid>
<vlrno>99994946000</vlrno>
</vlrid>
</neid>
</gsm_net_param>
</pos>
</slia>
</svc_result>
My first thought was to use transformerParameters to change the response file by inserting the value from the body. Unfortunately, WireMock doesn't resolve the helpers before inserting them into the body response. So while we can reference that MSID value via an xpath helper like
{{xPath request.body '/svc_init/slir/msids/msid/text()'}}
if we try to insert that as a custom transformer parameter, it won't resolve. (I've written up an issue on the WireMock github about this.)
Unfortunately, I think this leaves us with having to write a custom extension that will take the request and find the value and then modify the response file. More information on creating a custom transformer extensions can be found here.
At last I created my own transformer:
package com.company.department.app.extensions;
import com.github.tomakehurst.wiremock.common.FileSource;
import com.github.tomakehurst.wiremock.extension.Parameters;
import com.github.tomakehurst.wiremock.extension.ResponseTransformer;
import com.github.tomakehurst.wiremock.http.Request;
import com.github.tomakehurst.wiremock.http.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
public class NLGResponseTransformer extends ResponseTransformer {
private static final Logger LOG = LoggerFactory.getLogger(NLGResponseTransformer.class);
private static final String SLIA_FILE = "/stubs/__files/slia.xml";
private static final String REQ_IMSI_XPATH = "/svc_init/slir/msids/msid";
private static final String[] RES_IMSI_XPATHS = {
"/svc_result/slia/pos/msid",
"/svc_result/slia/company_mlp320_slia/company_netinfo/company_ms_netinfo/msid"
};
private static final String[] RES_TIME_XPATHS = {
// for slia.xml
"/svc_result/slia/company_mlp320_slia/company_netinfo/company_ms_netinfo/time",
// for slia_poserror.xml
"/svc_result/slia/pos/poserror/time"
};
private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance();
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
private static final String UTC_OFF = "utc_off";
private static final String TRANSFORM_FACTORY_ATTRIBUTE_INDENT_NUMBER = "indent-number";
protected static final String COMPANY_MLP_320_SLIA_EXTENSION_DTD = "company_mlp320_slia_extension.dtd";
protected static final String MLP_SVC_RESULT_320_DTD = "MLP_SVC_RESULT_320.DTD";
#Override
public String getName() {
return "inject-request-values";
}
#Override
public Response transform(Request request, Response response, FileSource fileSource, Parameters parameters) {
Document responseDocument = injectValuesFromRequest(request);
String transformedResponse = transformToString(responseDocument);
if (transformedResponse == null) {
return response;
}
return Response.Builder.like(response)
.but()
.body(transformedResponse)
.build();
}
private Document injectValuesFromRequest(Request request) {
// NOTE: according to quickscan:
// "time" element in the MLP is the time MME reports cell_id to GMLC (NLG), NOT the time when MME got the cell_id.
LocalDateTime now = LocalDateTime.now();
Document responseTemplate = readDocument(SLIA_FILE);
Document requestDocument = readDocumentFromBytes(request.getBody());
if (responseTemplate == null || requestDocument == null) {
return null;
}
try {
injectIMSI(responseTemplate, requestDocument);
injectTime(responseTemplate, now);
} catch (XPathExpressionException e) {
LOG.error("Cannot parse XPath expression {}. Cause: ", REQ_IMSI_XPATH, e);
}
return responseTemplate;
}
private Document readDocument(String inputStreamPath) {
try {
DocumentBuilder builder = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder();
// ignore missing dtd
builder.setEntityResolver((publicId, systemId) -> {
if (systemId.contains(COMPANY_MLP_320_SLIA_EXTENSION_DTD) ||
systemId.contains(MLP_SVC_RESULT_320_DTD)) {
return new InputSource(new StringReader(""));
} else {
return null;
}
});
return builder.parse(this.getClass().getResourceAsStream(inputStreamPath));
} catch (Exception e) {
LOG.error("Cannot construct document from resource path. ", e);
return null;
}
}
private Document readDocumentFromBytes(byte[] array) {
try {
DocumentBuilder builder = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder();
// ignore missing dtd
builder.setEntityResolver((publicId, systemId) -> {
if (systemId.contains(COMPANY_MLP_320_SLIA_EXTENSION_DTD) ||
systemId.contains(MLP_SVC_RESULT_320_DTD)) {
return new InputSource(new StringReader(""));
} else {
return null;
}
});
return builder.parse(new ByteArrayInputStream(array));
} catch (Exception e) {
LOG.error("Cannot construct document from byte array. ", e);
return null;
}
}
private XPath newXPath() {
return XPathFactory.newInstance().newXPath();
}
private void injectTime(Document responseTemplate, LocalDateTime now) throws XPathExpressionException {
for (String timeXPath: RES_TIME_XPATHS) {
Node timeTarget = (Node) (newXPath().evaluate(timeXPath, responseTemplate, XPathConstants.NODE));
if (timeTarget != null) {
// set offset in attribute
Node offset = timeTarget.getAttributes().getNamedItem(UTC_OFF);
offset.setNodeValue(getOffsetString());
// set value
timeTarget.setTextContent(TIME_FORMAT.format(now));
}
}
}
private void injectIMSI(Document responseTemplate, Document requestDocument) throws XPathExpressionException {
Node imsiSource = (Node) (newXPath().evaluate(REQ_IMSI_XPATH, requestDocument, XPathConstants.NODE));
String imsi = imsiSource.getTextContent();
for (String xpath : RES_IMSI_XPATHS) {
Node imsiTarget = (Node) (newXPath().evaluate(xpath, responseTemplate, XPathConstants.NODE));
if (imsiTarget != null) {
imsiTarget.setTextContent(imsi);
}
}
}
private String transformToString(Document document) {
if (document == null) {
return null;
}
document.setXmlStandalone(true); // make document to be standalone, so we can avoid outputing standalone="no" in first line
TransformerFactory tf = TransformerFactory.newInstance();
Transformer trans;
try {
trans = tf.newTransformer();
trans.setOutputProperty(OutputKeys.INDENT, "no"); // no extra indent; file already has intent of 4
// cannot find a workaround to inject dtd in doctype line. TODO
//trans.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, "MLP_SVC_RESULT_320.DTD [<!ENTITY % extension SYSTEM \"company_mlp320_slia_extension.dtd\"> %extension;]");
StringWriter sw = new StringWriter();
trans.transform(new DOMSource(document), new StreamResult(sw));
// Spaces between tags are considered as text node, so when outputing we need to remove the extra empty lines
return sw.toString().replaceAll("\\n\\s*\\n", "\n");
} catch (TransformerException e) {
LOG.error("Cannot transform response document to String. ", e);
return null;
}
}
/**
* Compare system default timezone with UTC and get zone offset in form of (+/-)XXXX.
* Dependent on the machine default timezone/locale.
* #return
*/
private String getOffsetString() {
// getting offset in (+/-)XX:XX format, or "Z" if is UTC
String offset = ZonedDateTime.ofInstant(Instant.now(), ZoneId.systemDefault()).getOffset().toString();
if (offset.equals("Z")) {
return "+0000";
}
return offset.replace(":", "");
}
}
And use it like this:
mvn package it as a JAR(non-runnable), put it aside wiremock standalone jar, for example libs
Run this:
java -cp libs/* com.github.tomakehurst.wiremock.standalone.WireMockServerRunner --extensions com.company.department.app.extensions NLGResponseTransformer --https-port 8443 --verbose
Put the whole command on the same line.
Notice the app jar which contains this transformer and wiremock standalone jar should be among classpath. Also, other dependencies under libs are needed. (I use jib maven plugin which copies all dependencies under libs/; I also move app and wiremock jars to libs/, so I can put "-cp libs/*"). If that does not work, try to specify the location of these two jars in -cp. Be ware that Wiremock will runs OK even when the extension class is not found. So maybe add some loggings.
You can use --root-dir to point to stubs files root, for example --root-dir resources/stubs in my case. By default it points to .(where java runs).
There is already a question asking for logging the active configuration, there is a correct answer but the problem is that the configuration is logged only if all beans are correctly instantiated. I would like to log all properties even (mainly) if the application crash at startup. My question is more specific:
How to log all active properties of a spring boot application before the beans instantiation?
For doing this you need to register an ApplicationListener. The event to catch is the ApplicationPreparedEvent, according to the documentation:
ApplicationPreparedEvent is an event published when a SpringApplication is starting up and the
ApplicationContext is fully prepared but not refreshed. The bean
definitions will be loaded and the Environment is ready for use at
this stage.
The main method would look like this:
public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication(MyApplication.class);
springApplication.addListeners(new PropertiesLogger());
springApplication.run(args);
}
I've reused the code of the answer cited in the current question but I've modified it because the context you get is not already refreshed and the structure of the environment is not exactly the same as after the startup of the application. I've also printed the properties by property sources: one for the the system environment, one for the system properties, one for the application configuration properties, etc... Note also that the ApplicationPreparedEvent can be triggered multiple times, and that properties are printed only the first time. See Spring Boot issue #8899 for details.
package com.toto.myapp.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationPreparedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.PropertySource;
import java.util.LinkedList;
import java.util.List;
public class PropertiesLogger implements ApplicationListener<ApplicationPreparedEvent> {
private static final Logger log = LoggerFactory.getLogger(PropertiesLogger.class);
private ConfigurableEnvironment environment;
private boolean isFirstRun = true;
#Override
public void onApplicationEvent(ApplicationPreparedEvent event) {
if (isFirstRun) {
environment = event.getApplicationContext().getEnvironment();
printProperties();
}
isFirstRun = false;
}
public void printProperties() {
for (EnumerablePropertySource propertySource : findPropertiesPropertySources()) {
log.info("******* " + propertySource.getName() + " *******");
String[] propertyNames = propertySource.getPropertyNames();
Arrays.sort(propertyNames);
for (String propertyName : propertyNames) {
String resolvedProperty = environment.getProperty(propertyName);
String sourceProperty = propertySource.getProperty(propertyName).toString();
if(resolvedProperty.equals(sourceProperty)) {
log.info("{}={}", propertyName, resolvedProperty);
}else {
log.info("{}={} OVERRIDDEN to {}", propertyName, sourceProperty, resolvedProperty);
}
}
}
}
private List<EnumerablePropertySource> findPropertiesPropertySources() {
List<EnumerablePropertySource> propertiesPropertySources = new LinkedList<>();
for (PropertySource<?> propertySource : environment.getPropertySources()) {
if (propertySource instanceof EnumerablePropertySource) {
propertiesPropertySources.add((EnumerablePropertySource) propertySource);
}
}
return propertiesPropertySources;
}
}
📝 Show the Properties BEFORE application is ready
In my case, I needed to show the properties before the context is loaded. While debugging the app, I would like to log all the properties so that I know what's going on...
☕ Kotlin Implementation
As described at https://www.baeldung.com/spring-boot-environmentpostprocessor, the properties can be collected before the context is loaded through the use of EnvironmentPostProcessor, which is instantiated as part of Spring Factories from the call ConfigFileApplicationListener.loadPostProcessors(). At this point, you can collect all the properties and show in any specific way.
NOTE: While loading properties during this event, the context isn't ready. So, are the loggers. For this reason, the properties can be loaded before the App Banner (if any)
Also, the entry for the spring factory must be present, so create it first
org.springframework.boot.env.EnvironmentPostProcessor=\
cash.app.PropertiesLoggerEnvironmentPostProcessor
Then, create the logger
package cash.app
import org.springframework.boot.SpringApplication
import org.springframework.boot.env.EnvironmentPostProcessor
import org.springframework.core.Ordered
import org.springframework.core.annotation.Order
import org.springframework.core.env.ConfigurableEnvironment
import org.springframework.core.env.EnumerablePropertySource
import java.util.*
/**
* This is to log the properties (config and system) before the app loads. This way, we know what will be loaded
* on the app.
* Note that we can't use the logger because the context hasn't built yet at the time it loads the properties twice.
*
* As an event consumer, the method ConfigFileApplicationListener.onApplicationEnvironmentPreparedEvent is called
* while the context is building. The process is described at https://www.baeldung.com/spring-boot-environmentpostprocessor
* and one important aspect is that this class is an EnvironmentPostProcessor, only loaded before the App is loaded
* with the assistance of the "src/main/resources/META-INF/spring.factories". It is loaded by the
* ConfigFileApplicationListener.loadPostProcessors(), which looks for the list of classses in the factories.
*
* https://www.baeldung.com/spring-boot-environmentpostprocessor explains how to create AutoConfiguration classes for
* shared libraries. For the case of config, the reload of properties is detailed and explained on the docs at
* https://www.baeldung.com/spring-reloading-properties
*
* TODO: We need to hide the secrets, if they are defined here.
*
* #author Marcello.DeSales#gmail.com
*/
#Order(Ordered.LOWEST_PRECEDENCE)
class PropertiesLoggerEnvironmentPostProcessor : EnvironmentPostProcessor {
companion object {
/**
* Sharing is started immediately and never stops.
*/
private var numberOfPasses: Int = 0
private var systemProperties: MutableMap<String, String> = mutableMapOf()
}
override fun postProcessEnvironment(environment: ConfigurableEnvironment, application: SpringApplication) {
for (propertySource in findPropertiesPropertySources(environment)) {
// Avoid printing the systemProperties twice
if (propertySource.name.equals("systemProperties")) {
numberOfPasses = numberOfPasses?.inc()
} else {
System.out.println("******* \" + ${propertySource.getName()} + \" *******" )
}
// Adaptation of https://stackoverflow.com/questions/48212761/how-to-log-all-active-properties-of-a-spring-boot-application-before-the-beans-i/48212783#48212783
val propertyNames = propertySource.propertyNames
Arrays.sort(propertyNames)
for (propertyName in propertyNames) {
val resolvedProperty = environment!!.getProperty(propertyName!!)
val sourceProperty = propertySource.getProperty(propertyName).toString()
if (resolvedProperty == sourceProperty) {
if (propertySource.name.equals("systemProperties")) {
systemProperties.put(propertyName, resolvedProperty)
} else {
System.out.println( "${propertyName}=${resolvedProperty}" )
}
} else {
if (propertySource.name.equals("systemProperties")) {
systemProperties.put(propertyName, resolvedProperty ?: "")
} else {
System.out.println( "${propertyName}=${sourceProperty} ----- OVERRIDDEN =>>>>>> ${propertyName}=${resolvedProperty}" )
}
}
}
}
// The system properties show up twice in the process... The class is called twice and we only print it in the end.
if (numberOfPasses == 2) {
System.out.println("******* \" System Properties \" *******")
val sysPropertyNames = systemProperties.keys.sorted()
for (sysPropertyName in sysPropertyNames) {
val sysPropertyValue = systemProperties!!.get(sysPropertyName!!)
System.out.println( "${sysPropertyName}=${sysPropertyValue}" )
}
}
}
private fun findPropertiesPropertySources(environment: ConfigurableEnvironment): List<EnumerablePropertySource<*>> {
val propertiesPropertySources: MutableList<EnumerablePropertySource<*>> = LinkedList()
for (propertySource in environment!!.propertySources) {
if (propertySource is EnumerablePropertySource<*>) {
if (propertySource.name.equals("systemProperties") || propertySource.name.contains("applicationConfig:")) {
propertiesPropertySources.add(propertySource)
}
}
}
return propertiesPropertySources.asReversed()
}
}
🔊 Example Logs
Here's the loggers during the bootstrap of one of my services
/Users/marcellodesales/.gradle/jdks/jdk-14.0.2+12/Contents/Home/bin/java -XX:TieredStopAtLevel=1 -noverify -Dspring.output.ansi.enabled=always
....
....
2022-02-22T21:24:39 INFO [app=springAppName_IS_UNDEFINED,prof=observed,db,ppd_dev][tid=,sid=,sxp=][uid=] 74720 --- [ restartedMain] o.s.b.devtools.restart.ChangeableUrls : The Class-Path manifest attribute in /Users/marcellodesales/.gradle/caches/modules-2/files-2.1/com.sun.xml.bind/jaxb-core/2.2.7s-codec-1.11.jar
2022-02-22T21:24:39 INFO [app=springAppName_IS_UNDEFINED,prof=observed,db,ppd_dev][tid=,sid=,sxp=][uid=] 74720 --- [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
******* " + applicationConfig: [classpath:/application.yaml] + " *******
management.endpoint.health.show-details=always
management.endpoints.web.base-path=/actuator ==========>>>>>> OVERRIDDEN =========>>>>>> management.endpoints.web.base-path=/orchestrator/actuator
management.endpoints.web.exposure.include=*
management.metrics.web.server.request.autotime.enabled=true
spring.application.name=orchestrator-service
spring.boot.admin.client.enabled=false ==========>>>>>> OVERRIDDEN =========>>>>>> spring.boot.admin.client.enabled=true
spring.cloud.discovery.client.composite-indicator.enabled=false
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
******* " + applicationConfig: [classpath:/application-ppd_dev.yaml] + " *******
spring.datasource.url=jdbc:postgresql://localhost:6433/supercash?createDatabaseIfNotExist=true ==========>>>>>> OVERRIDDEN
=========>>>>>> spring.datasource.url=jdbc:postgresql://localhost:6433/supercash?createDatabaseIfNotExist\=true
spring.devtools.livereload.enabled=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.test-connection=true
******* " System Properties " *******
LOG_LEVEL_PATTERN=%5p [,%X{X-B3-TraceId:-},%X{X-B3-SpanId:-},%X{X-Span-Export:-}]
PID=74720
com.sun.management.jmxremote=
file.encoding=UTF-8
ftion
java.vm.specification.version=14
java.vm.vendor=AdoptOpenJDK
java.vm.version=14.0.2+12
jboss.modules.system.pkgs=com.intellij.rt
jdk.debug=release
line.separator=
os.arch=x86_64
os.name=Mac OS X
os.version=10.16
user.name=marcellodesales
user.timezone=America/Los_Angeles
2022-02-22T21:25:16 DEBUG [app=orchestrator-service,prof=observed,db,ppd_dev][tid=,sid=,sxp=][uid=] 74720 --- [ restartedMain] o.s.b.c.c.ConfigFileApplicationListener : Activated activeProfiles observed,db,ppd_dev
_____ _____ _
/ ____| / ____| | |
| (___ _ _ _ __ ___ _ __| | __ _ ___| |__
2022-02-22T20:41:08 INFO [app=orchestrator-service,prof=observed,db,ppd_dev][tid=,sid=,sxp=][uid=] 74181 --- [ restartedMain]
I am searching a way to check if a template exists before returning it in a view with Thymeleaf and Spring.
In my controller i was trying to do something like this:
String finalTemplate = template.getPrefix() + "/" + templateName;
try {
return finalTemplate;
} catch(TemplateInputException e) {
logger.debug("Sono qua");
return templateName;
}
but the exception is not catch...
The way you are handling exception here will not work for template exceptions.
Check below threads regarding this in issues section github.
https://github.com/thymeleaf/thymeleaf-spring/issues/94
https://github.com/thymeleaf/thymeleaf-spring/issues/81
StringTemplateResource from the comment above seems to return true in every case for thymeleaf 3.0. Here is a method to do this for classpath included templates:
public static String baseDir = "templates";
/**
* Check if a template exists in subfolder templates.
*
* #param templateName relative name of template below the basedir.
* #return true if exists, otherwise false
*/
public static boolean templateExists(final String templateName)
{
final ClassLoaderTemplateResource iTemplateResource =
new ClassLoaderTemplateResource(baseDir + "/" + templateName, "UTF8");
return iTemplateResource.exists();
}
Testcase:
#Test
public void testNonExisting()
{
assertFalse(templateExists("foo"));
}
#Test
public void testExisting()
{
assertTrue(templateExists("foo.txt"));
}
Assuming a classpath resource templates/foo.txt exists.
I am working on an existing system that uses log4j, I want to update to log4j2.
There is a custom spring bean that loads the configuration from a file. I need to keep this approach. I cannot use the "log4j.configurationFile" system property.
We have a properties file where the path to the current log4j.xml is specified ( NFS share )
The spring bean has this code ...
public class Log4jConfigurationBean implements ResourceLoaderAware,
InitializingBean {
private ResourceLoader resourceLoader;
private boolean enabled;
private String location;
/**
* Default, no argument constructor.
*/
public Log4jConfigurationBean() {
enabled = true;
}
/**
* Sets whether or not this bean should load an external configuration
* defined by {#link #setLocation(Resource)}. If <code>false</code>, this
* bean does nothing.
*
* <p>
* Default value is <code>true</code>.
* </p>
*
* #param enabled
* <code>false</code> causes this bean to do nothing
*/
public void setEnabled(final boolean enabled) {
this.enabled = enabled;
}
/**
* Sets the location of the external log4j configuration (xml or properties)
* to be loaded.
*
* #param location
* the location of the external configuration to be loaded.
* #throws IllegalStateException
* if there is a problem resolving the location resource
* #throws NullPointerException
* if <code>resource</code> is <code>null</code>
*/
public void setLocation(final String location) {
this.location = StringUtils.trimToNull(location);
}
#Override
public void setResourceLoader(final ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
/**
* #throws IllegalStateException
* if enabled and no location has be set, or if the external
* configuration is neither xml or properties.
*/
#Override
public void afterPropertiesSet() throws Exception {
URL configURL = null;
if (null != location) {
try {
final Resource resource = resourceLoader.getResource(location);
if (null != resource) {
configURL = resource.getURL();
}
} catch (IOException e) {
throw new IllegalArgumentException(
"Could not resolve configuration location due to error: ",
e);
}
}
if (enabled && null == configURL) {
throw new IllegalStateException(
"Log4j configuration enabled, but configuration location is not set.");
}
if (enabled) {
if (configURL.getFile().toLowerCase().endsWith(".xml")) {
DOMConfigurator.configure(configURL);
} else if (configURL.getFile().toLowerCase()
.endsWith(".properties")) {
PropertyConfigurator.configure(configURL);
} else {
throw new IllegalStateException(
"Configuration must be properties or xml: "
+ configURL.getFile());
}
}
}
}
In log4j2 there is no PropertyConfigurator.
How can I load the log4j2.xml file the same way.
The file path to the log4j2.xml file is specified in a spring property file.
The goal is to have the war files contain a log4j2.xml file in the classpath. This will be used when developing on your local box.
When the web apps are deployed to a qa environment, there is a property file containing the following key/value pair...
# Should an external file be used for log4j configuration
log4j.enabled=true
log4j.location=file:/paht to log4j2.xml
A spring bean is using these values to decide if an external log4j2.xml file should be used instead of the one on the classpath.
I tried with a spring bean like this... the code is executed, but it still uses the configuration file on the classpath.
public class Log4j2ConfigurationBean implements ResourceLoaderAware, InitializingBean {
private static final Logger log = LoggerFactory.getLogger(Log4j2ConfigurationBean.class);
private ResourceLoader resourceLoader;
private boolean enabled;
private String location;
/**
* Default, no argument constructor.
*/
public Log4j2ConfigurationBean() {
enabled = true;
}
/**
* Sets whether or not this bean should load an external configuration defined by {#link #setLocation(Resource)}. If <code>false</code>, this bean does nothing.
*
* <p>
* Default value is <code>true</code>.
* </p>
*
* #param enabled
* <code>false</code> causes this bean to do nothing
*/
public void setEnabled(final boolean enabled) {
this.enabled = enabled;
}
/**
* Sets the location of the external log4j configuration (xml or properties) to be loaded.
*
* #param location
* the location of the external configuration to be loaded.
* #throws IllegalStateException
* if there is a problem resolving the location resource
* #throws NullPointerException
* if <code>resource</code> is <code>null</code>
*/
public void setLocation(final String location) {
this.location = StringUtils.trimToNull(location);
}
#Override
public void setResourceLoader(final ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
/**
* #throws IllegalStateException
* if enabled and no location has be set, or if the external configuration is neither xml or properties.
*/
#Override
public void afterPropertiesSet() throws Exception {
URL configURL = null;
if (enabled) {
if (StringUtils.isBlank(location)) {
throw new IllegalStateException("Log4j2 configuration enabled, but configuration location is not set.");
}
try {
System.out.println(this.getClass().getName() + " : Loading log4j2 configuration with " + location);
final Resource resource = resourceLoader.getResource(location);
if (null != resource) {
configURL = resource.getURL();
}
} catch (IOException e) {
throw new IllegalArgumentException("Could not resolve configuration location due to error: ", e);
}
if (configURL.getFile().toLowerCase().endsWith(".xml")) {
try {
System.setProperty("Log4jContextSelector", "org.apache.logging.log4j.core.async.AsyncLoggerContextSelector");
System.setProperty("AsyncLogger.RingBufferSize", "8192");
ConfigurationFactory configurationFactory = XmlConfigurationFactory.getInstance();
ConfigurationSource configurationSource = new ConfigurationSource(configURL.openStream(), configURL);
Configuration configuration = configurationFactory.getConfiguration(configurationSource);
configuration.start();
log.info("Log4j2 configured with {}", location);
log.info("System property Log4jContextSelector set to {}", System.getProperty("Log4jContextSelector"));
log.info("System property AsyncLogger.RingBufferSize set to {}", System.getProperty("AsyncLogger.RingBufferSize"));
} catch (Exception e) {
System.out.println(this.getClass().getName() + " : Could not initialize log4j2 with resource " + location);
System.out.println(e.getStackTrace());
}
} else {
throw new IllegalStateException("Configuration must be xml: " + configURL.getFile());
}
} else {
System.out.println(this.getClass().getName() + " : External log4j2 configuration not configured.");
}
}
}
Thanks.
Check out the How do I configure log4j2 in code without a configuration file? section here - http://logging.apache.org/log4j/2.x/faq.html
According to my requirement, I need to externalize the message.properties file (Keep outside from war file) and in same time it should be automatically re loadable on update.
So I achieved both by following code and its working fine with Jetty Server. But in when I use Tomcat Server that externalized property file is not picking up by the system, instead its uses only the file inside the war.
public final class Messages
{
public static final String BUNDLE_NAME = "com.sample.project.core.ui.resources.messages";
// private static ResourceBundle resourceBundle = ResourceBundle.getBundle(BUNDLE_NAME);
private static ResourceBundle resourceBundle;
private static final Logger LOGGER = Logger.getLogger(Messages.class);
private static ReloadableResourceBundleMessageSource messageSource;
static
{
try
{
FileInputStream fis =
new FileInputStream(System.getProperty("resources.messages.file.path"));
resourceBundle = new PropertyResourceBundle(fis);
}
catch (FileNotFoundException e)
{
LOGGER.error("messages.properties file not found: " + e);
resourceBundle = ResourceBundle.getBundle(BUNDLE_NAME);
}
catch (Exception e)
{
LOGGER.error("messages.properties file reading failed: " + e);
resourceBundle = ResourceBundle.getBundle(BUNDLE_NAME);
}
}
private Messages()
{
}
/**
* <p>
* setter methos to ReloadableResourceBundleMessageSource object.
* </p>
*
*
* #param inMessageSource
* set reloadable resources bundle
**/
public static void setMessageSource(final ReloadableResourceBundleMessageSource inMessageSource)
{
Messages.messageSource = inMessageSource;
Messages.messageSource.setBasename(System.getProperty("resources.messages.file.path"));
}
/**
* <p>
* Resolve a message by a key and argument replacements.
* </p>
*
* #see MessageFormat#format(String, Object...)
* #param key
* the message to look up
* #param arguments
* optional message arguments
* #return the resolved message
**/
public static String getMessage(final String key, final Object... arguments)
{
try
{
if (messageSource != null)
{
return messageSource.getMessage(key, arguments, Locale.getDefault());
}
else
{
if (arguments != null)
return MessageFormat.format(resourceBundle.getString(key), arguments);
return resourceBundle.getString(key);
}
}
catch (NoSuchMessageException e)
{
LOGGER.error("Message key not found: " + key);
return '!' + key + '!';
}
catch (MissingResourceException e)
{
LOGGER.error("Message key not found: " + key);
return '!' + key + '!';
}
}
}
(Here file path I'm passing as a VM argument using "resources.messages.file.path" key)
First I thought it was a problem with accessing the file system and tried many ways. Then I heard about catalina.policy file and I added some lines like this..
grant codeBase "file:${catalina.base}/webapps/sample.war/-" {
permission java.security.AllPermission;
permission java.io.FilePermission "file:${catalina.base}${file.separator}webapps${file.separator}messages.properties", "read, write";
permission java.util.PropertyPermission "resources.messages.file.path", "read";
}
But non of them gave me luck. I'm desperate. Any idea what this issue is? Please help me. Thank you in advance. (Tested on Tomcat6)
Finally I found where I screwed up.
This setter method of messageSource should not be static and I removed static access of messageSource
messageSource = inMessageSource;
messageSource.setBasename(System.getProperty("resources.messages.file.path"));
Now code is working fine. And no need of that permission entry in catalina.policy file. Thanks to everyone who helped me.