How do I write an appender that only handles exceptions and still have all other logs logged normally through a root ConsoleAppender - log4j2

I have a situation where when log.error("message", exception); is called, I want some logic to happen around sending the exception to an external tool, while still maintaining the regular logging for the line to the root appender.
As an example, here would be some code and the expected outcome:
try {
...
} catch (Exception ex) {
LOG.info("abcd");
LOG.error("failed to XYZ", ex);
}
Rough Outcome:
2019-03-05 13:00:20 INFO Main:75 - abcd
2019-03-05 13:00:20 ERROR Main:76 - failed to XYZ -
Exception: exception message
stacktrace
stacktrace
...
While that is logged, I also want the exception sent through another code path.
How can I do this? I'm a bit stuck, does anyone know any good guides for this?

I don't think you really want an Appender here. It would be simpler to write a Filter instead. For reference you can find information regarding creating extensions for log4j2 on the Extending Log4j2 page of the manual
In the example below I created a simple filter that matches when the log event has a Throwable associated with it and mismatches when there is no Throwable (i.e. the Throwable is null or the log was generated from a method call that did not include a Throwable parameter).
In the example I send all matching log events to a simple file appender to illustrate that in fact it does capture only events with a Throwable. You could modify this filter to do whatever you need. You could change it to always be NEUTRAL to every event, but when a non-null Throwable is found it then triggers some special logic to send that Throwable to your external tool. Basically you would be using the filter as more of an interceptor. I will describe that change at the end.
First, here's some basic Java code to generate some logging including a log event that has a Throwable.
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class SomeClass {
private static final Logger log = LogManager.getLogger();
public static void main(String[] args){
if(log.isDebugEnabled())
log.debug("This is some debug!");
log.info("Here's some info!");
log.error("Some error happened!");
try{
specialLogic();
}catch(RuntimeException e){
log.error("Woops, an exception was detected.", e);
}
}
public static void specialLogic(){
throw new RuntimeException("Hey an exception happened! Oh no!");
}
}
Next, here's the class I call ThrowableFilter:
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.filter.AbstractFilter;
import org.apache.logging.log4j.message.Message;
#Plugin(name = "ThrowableFilter", category = "Core", elementType = "filter", printObject = true)
public final class ThrowableFilter extends AbstractFilter {
private ThrowableFilter(Result onMatch, Result onMismatch) {
super(onMatch, onMismatch);
}
public Result filter(Logger logger, Level level, Marker marker, String msg, Object[] params) {
return onMismatch;
}
public Result filter(Logger logger, Level level, Marker marker, Object msg, Throwable t) {
return filter(t);
}
public Result filter(Logger logger, Level level, Marker marker, Message msg, Throwable t) {
return filter(t);
}
#Override
public Result filter(LogEvent event) {
return filter(event.getThrown());
}
private Result filter(Throwable t) {
return t != null ? onMatch : onMismatch;
}
/**
* Create a ThrowableFilter.
* #param match The action to take on a match.
* #param mismatch The action to take on a mismatch.
* #return The created ThrowableFilter.
*/
#PluginFactory
public static ThrowableFilter createFilter(#PluginAttribute(value = "onMatch", defaultString = "NEUTRAL") Result onMatch,
#PluginAttribute(value = "onMismatch", defaultString = "DENY") Result onMismatch) {
return new ThrowableFilter(onMatch, onMismatch);
}
}
Finally, here is the log4j2.xml configuration file I used to test this:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
</Console>
<File name="ExceptionFile" fileName="logs/exception.log" immediateFlush="true"
append="true">
<ThrowableFilter onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout
pattern="%d{yyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
</File>
</Appenders>
<Loggers>
<Root level="debug">
<AppenderRef ref="Console" />
<AppenderRef ref="ExceptionFile" />
</Root>
</Loggers>
</Configuration>
Running the logic in SomeClass produces the following output:
On the console:
23:23:25.931 [main] DEBUG example.SomeClass - This is some debug!
23:23:25.946 [main] INFO example.SomeClass - Here's some info!
23:23:25.946 [main] ERROR example.SomeClass - Some error happened!
23:23:25.946 [main] ERROR example.SomeClass - Woops, an exception was detected.
java.lang.RuntimeException: Hey an exception happened! Oh no!
at example.SomeClass.specialLogic(SomeClass.java:25) ~[classes/:?]
at example.SomeClass.main(SomeClass.java:18) [classes/:?]
In the logs/exception.log file:
2019-03-06 23:23:25.946 [main] ERROR example.SomeClass - Woops, an exception was detected.
java.lang.RuntimeException: Hey an exception happened! Oh no!
at example.SomeClass.specialLogic(SomeClass.java:25) ~[classes/:?]
at example.SomeClass.main(SomeClass.java:18) [classes/:?]
Now to change the filter to act more as an interceptor you could alter the following methods:
//Remove parameters from constructor as they will not be used.
private ThrowableFilter() {
super();
}
...
public Result filter(Logger logger, Level level, Marker marker, String msg, Object[] params) {
//Changed to always return NEUTRAL result
return Result.NEUTRAL;
//old logic: return onMismatch;
}
...
private Result filter(Throwable t) {
//TODO: trigger the external tool here when t != null, pass t if needed.
//Changed to always return NEUTRAL result
return Result.NEUTRAL;
//old logic: return t != null ? onMatch : onMismatch;
}
/**
* Create a ThrowableFilter.
* #return The created ThrowableFilter.
*/
#PluginFactory
public static ThrowableFilter createFilter() {
return new ThrowableFilter();
}
Then in the configuration you would remove the parameters to the filter. It would now look like this:
<ThrowableFilter/>
Hope this helps you.

Related

Configuring a neo4j test container in Spring Boot with #Configuration file

I'm trying to run spock tests against a neo4j container using the TestContainers library. Previously, I used the test-harness library to run tests against an embedded neo4j database but the need to migrate to using TestContainers has come up. With test-harness I used a configuration file to include some necessary beans for callback functionality. Without these beans, some unit tests are failing.
Previous test-harness method
#ContextConfiguration(classes = [
Neo4jTestHarnessAutoConfiguration,
Neo4jDriverAutoConfiguration,
Neo4jDataAutoConfiguration,
CustomNeo4jConfiguration
])
#ImportAutoConfiguration(classes = [
Neo4jTestHarnessAutoConfiguration,
Neo4jDriverAutoConfiguration,
Neo4jDataAutoConfiguration
])
#Transactional
trait EmbeddedNeo4j {
// Neo4j embedded database is created in Neo4jTestHarnessAutoConfiguration
}
CustomNeo4jConfiguration
#Configuration
#EntityScan(value = {
"data.neo4j.nodes",
"data.neo4j.relationships",
"data.neo4j.queryresults"
})
#EnableNeo4jRepositories("data.neo4j.repository")
#EnableNeo4jAuditing(auditorAwareRef = "auditorAware")
public class CustomNeo4jConfiguration {
#Bean
public AuditorAware<String> auditorAware(){
return new CustomAuditorAwareImpl();
}
#Bean
public BeforeBindCallback neo4jCustomEntitiesCallback(AuditorAware<String> auditorAware) {
return new CustomNeo4jEntitiesCallback(auditorAware);
}
#Bean
public Neo4jConversions neo4jCustomConversions() {
Set<GenericConverter> additionalConverters = Collections.singleton(new InstantStringConverter());
return new Neo4jConversions(additionalConverters);
}
}
The above method works fine. All tests run properly and all beans are created.
TestContainers attempt
#Testcontainers
#Transactional
trait EmbeddedNeo4j {
#Shared
static final Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:4.3.9")
.withReuse(true)
#DynamicPropertySource
static void neo4jProperties(DynamicPropertyRegistry registry) {
neo4jContainer.start()
registry.add("spring.neo4j.uri", neo4jContainer::getBoltUrl)
registry.add("spring.neo4j.authentication.username", () -> "neo4j")
registry.add("spring.neo4j.authentication.password", neo4jContainer::getAdminPassword)
}
}
With the above, all tests successfully run against the test container. However, any test that needs the functionality of the beans in the CustomNeo4jConfiguration fails.
What I've tried
I've attempted to use the same #ContextConfiguration annotations in various configurations, such as...
#ContextConfiguration(classes = [
Neo4jDriverAutoConfiguration,
Neo4jDataAutoConfiguration,
CustomNeo4jConfiguration
])
#ImportAutoConfiguration(classes = [
Neo4jDriverAutoConfiguration,
Neo4jDataAutoConfiguration
])
#Transactional
trait EmbeddedNeo4j {
TestContainer code here...
However, this fails with the following stack trace:
Failed to load ApplicationContext
java.lang.IllegalStateException: Failed to load ApplicationContext
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:132)
at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:124)
at org.springframework.test.context.web.ServletTestExecutionListener.setUpRequestContextIfNecessary(ServletTestExecutionListener.java:190)
at org.springframework.test.context.web.ServletTestExecutionListener.prepareTestInstance(ServletTestExecutionListener.java:132)
at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:248)
at org.spockframework.spring.SpringTestContextManager.prepareTestInstance(SpringTestContextManager.java:56)
at org.spockframework.spring.SpringInterceptor.interceptInitializerMethod(SpringInterceptor.java:43)
at org.spockframework.runtime.extension.AbstractMethodInterceptor.intercept(AbstractMethodInterceptor.java:24)
at org.spockframework.runtime.extension.MethodInvocation.proceed(MethodInvocation.java:101)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$prepare$2(NodeTestTask.java:123)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.prepare(NodeTestTask.java:123)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:90)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at org.spockframework.runtime.model.MethodInfo.invoke(MethodInfo.java:148)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:99)
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:79)
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:75)
at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:61)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.stop(TestWorker.java:133)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:182)
at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:164)
at org.gradle.internal.remote.internal.hub.MessageHub$Handler.run(MessageHub.java:414)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:56)
at java.base/java.lang.Thread.run(Thread.java:829)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'auditFieldRepository' defined in data.neo4j.repository.AuditFieldRepository defined in #EnableNeo4jRepositories declared on CustomNeo4jConfiguration: Cannot resolve reference to bean 'neo4jTemplate' while setting bean property 'neo4jOperations'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'neo4jTemplate' available
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:342)
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:113)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1707)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1452)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:619)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:936)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:734)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:308)
at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:132)
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:99)
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:124)
... 64 more
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'neo4jTemplate' available
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:874)
at org.springframework.beans.factory.support.AbstractBeanFactory.getMergedLocalBeanDefinition(AbstractBeanFactory.java:1344)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:309)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:330)
... 82 more
I've also attempted in various ways to create the missing beans, but that doesn't feel like the right path to take and that didn't produce any results.
What's the right way to use a #Configuration file with TestContainers?
Appreciate any help!
If I interpret your mixture of Java and Scala code correctly, I would recommend the following approach, but only if you insist on not using #SpringBootTest.
Also: You are using the outdated neo4j-java-driver-spring-boot-starter classes, they aren't needed for a while now in recent Spring Boot and also will potentially break SDN6+.
Please note: You should never import auto configuration classes and use them as ContextConfiguration at the same time. They are only meant to be imported via #ImportAutoConfiguration, but even that is redundant or overly complicated most of the time.
First of all, here's the pom so you get the proper dependencies:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>neowithapoc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>neowithapoc</name>
<description>neowithapoc</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-neo4j</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.17.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>neo4j</artifactId>
<version>1.17.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Solution 1
Here's a solution that goes into what you have there, with a trait - or in case of pure java - abstract test class:
package com.example.neowithapoc;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration;
import org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.testcontainers.containers.Neo4jContainer;
import org.testcontainers.containers.Neo4jLabsPlugin;
#TestConfiguration
#ImportAutoConfiguration({
Neo4jAutoConfiguration.class,
Neo4jDataAutoConfiguration.class
})
#EnableTransactionManagement
#EnableNeo4jRepositories(considerNestedRepositories = true)
public abstract class EmbeddedNeo4jConfig {
static final Neo4jContainer<?> neo4j = new Neo4jContainer<>("neo4j:4.4")
.withLabsPlugins(Neo4jLabsPlugin.APOC)
.withReuse(true);
#DynamicPropertySource
static void neo4jProperties(DynamicPropertyRegistry registry) {
neo4j.start();
registry.add("spring.neo4j.uri", neo4j::getBoltUrl);
registry.add("spring.neo4j.authentication.username", () -> "neo4j");
registry.add("spring.neo4j.authentication.password", neo4j::getAdminPassword);
}
}
Use like this:
package com.example.neowithapoc;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
#ExtendWith(SpringExtension.class)
#ContextConfiguration(classes = EmbeddedNeo4jConfig.class)
public class SomeTest extends EmbeddedNeo4jConfig {
#Test
void whatever(#Autowired Neo4jTemplate template, #Autowired Neo4jClient client) {
assertThat(template).isNotNull();
assertThat(client).isNotNull();
String apoc = client
.query("RETURN apoc.version() AS output")
.fetchAs(String.class)
.first().get();
assertThat(apoc).startsWith("4.4");
}
}
Solution 2
I do prefer this way simpler and less complicated approach:
package com.example.neowithapoc;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.annotation.Transactional;
import org.testcontainers.containers.Neo4jContainer;
import org.testcontainers.containers.Neo4jLabsPlugin;
import org.testcontainers.junit.jupiter.Testcontainers;
#SpringBootTest
#Transactional
#Testcontainers(disabledWithoutDocker = true)
public class SomeSpringBootTest {
static final Neo4jContainer<?> neo4j = new Neo4jContainer<>("neo4j:4.4")
.withLabsPlugins(Neo4jLabsPlugin.APOC)
.withReuse(true);
#DynamicPropertySource
static void neo4jProperties(DynamicPropertyRegistry registry) {
neo4j.start();
registry.add("spring.neo4j.uri", neo4j::getBoltUrl);
registry.add("spring.neo4j.authentication.username", () -> "neo4j");
registry.add("spring.neo4j.authentication.password", neo4j::getAdminPassword);
}
#Test
void whatever(#Autowired Neo4jTemplate template, #Autowired Neo4jClient client) {
assertThat(template).isNotNull();
assertThat(client).isNotNull();
String apoc = client
.query("RETURN apoc.version() AS output")
.fetchAs(String.class)
.first().get();
assertThat(apoc).startsWith("4.4");
}
}
Bonus questions custom conversions
Given the following class:
package com.example.neowithapoc;
public class SomeObject {
private final String value;
public SomeObject(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
It is not an entity and it needs some special treatment during conversions.
It can be converted back and forth like this:
import java.util.Set;
import org.neo4j.driver.Value;
import org.neo4j.driver.Values;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
public class SomeObjectConverter implements GenericConverter {
#Override public Set<ConvertiblePair> getConvertibleTypes() {
return Set.of(
new ConvertiblePair(Value.class, SomeObject.class),
new ConvertiblePair(SomeObject.class, Value.class)
);
}
#Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return Value.class.isAssignableFrom(targetType.getType()) ? Values.NULL : null;
} else if (source instanceof SomeObject someObject) {
return Values.value(someObject.getValue());
} else if (source instanceof Value value) {
return new SomeObject(value.asString());
} else
throw new IllegalArgumentException();
}
}
Configure it with a #TestConfiguration (or on your main config) like this
#TestConfiguration
static class AdditionalBeans {
#Bean
SomeObjectConverter someObjectConverter() {
return new SomeObjectConverter();
}
#Bean
Neo4jConversions neo4jConversions(SomeObjectConverter someObjectConverter) {
return new Neo4jConversions(List.of(someObjectConverter));
}
}
In both cases this class is a static nested class of either EmbeddedNeo4jConfig and SomeSpringBootTest.
In the former you normally would just declare the bean method directly, but as I tried to recreate your "trait" as much as possible in Java and I do extend that class, this is not possible. Tests must not have their own #Bean methods.
After that, test like this
#Node
static class ContainerNode {
#Id #GeneratedValue
Long id;
SomeObject someObject;
public Long getId() {
return id;
}
public SomeObject getSomeObject() {
return someObject;
}
}
#Test
void conversionsShouldWork(#Autowired Neo4jTemplate template) {
var container = template.find(ContainerNode.class)
.matching("CREATE (n:ContainerNode {someObject: 'Hello'}) RETURN n")
.one();
assertThat(container).map(ContainerNode::getSomeObject).map(SomeObject::getValue).hasValue("Hello");
}
will work just fine, also with the repository.
This test here will fail
#Test
void conversionsShouldWork(#Autowired Neo4jClient client) {
var anObject = client.query("RETURN 'whatever'").fetchAs(SomeObject.class).first();
assertThat(anObject).map(SomeObject::getValue).hasValue("whatever");
}
but the Spring Data Neo4j fixed this in 6.3.3 https://github.com/spring-projects/spring-data-neo4j/issues/2594 and you would be able to manually add it to the client like this:
#TestConfiguration
static class AdditionalBeans {
#Bean
SomeObjectConverter someObjectConverter() {
return new SomeObjectConverter();
}
#Bean
Neo4jConversions neo4jConversions(SomeObjectConverter someObjectConverter) {
return new Neo4jConversions(List.of(someObjectConverter));
}
#Bean
public Neo4jClient neo4jClient(
Driver driver,
DatabaseSelectionProvider databaseSelectionProvider,
Neo4jConversions neo4jConversions) {
return Neo4jClient.with(driver)
.withDatabaseSelectionProvider(databaseSelectionProvider)
.withNeo4jConversions(neo4jConversions)
.build();
}
}
Than this test will work just fine:
#Test
void clientConversionsShouldWork(#Autowired Neo4jClient client) {
var anObject = client.query("RETURN 'whatever'").fetchAs(SomeObject.class).first();
assertThat(anObject).map(SomeObject::getValue).hasValue("whatever");
}

QuickFIX/J: Type 'quickfix/field/HaltReason' is not assignable to 'quickfix/IntField'

Summary
We have a quickfix client which receives SecurityDefiniton and SecurityDefinitionUpdateReport messages. These are bulk datas. When we logged to the server they send messages around 8000. At the end they're sending SecurityStatus message. In this part we are getting an exception.
Fix protocol version: FIX50SP2 with FIXT1.1
Quickfix message dependency
<dependency>
<groupId>org.quickfixj</groupId>
<artifactId>quickfixj-messages-fix50sp2</artifactId>
<version>2.3.0</version>
</dependency>
Quickfix Core
<dependency>
<groupId>org.quickfixj</groupId>
<artifactId>quickfixj-core</artifactId>
<version>2.0.0</version>
</dependency>
Exception we received;
Exception in thread "pool-5-thread-1" java.lang.VerifyError: Bad type on operand stack
Exception Details:
Location:
quickfix/fix50sp2/SecurityStatus.get(Lquickfix/field/HaltReason;)Lquickfix/field/HaltReason; #2: invokevirtual
Reason:
Type 'quickfix/field/HaltReason' (current frame, stack[1]) is not assignable to 'quickfix/IntField'
Current Frame:
bci: #2
flags: { }
locals: { 'quickfix/fix50sp2/SecurityStatus', 'quickfix/field/HaltReason' }
stack: { 'quickfix/fix50sp2/SecurityStatus', 'quickfix/field/HaltReason' }
Bytecode:
0x0000000: 2a2b b600 1557 2bb0
at quickfix.fix50sp2.MessageFactory.create(MessageFactory.java:297)
at foo.bar.data.plugin.fix.api.MessageFactory.MessageFactorySp2.create(MessageFactorySp2.java:93)
at quickfix.MessageUtils.parse(MessageUtils.java:145)
at quickfix.mina.AbstractIoHandler.messageReceived(AbstractIoHandler.java:131)
at org.apache.mina.core.filterchain.DefaultIoFilterChain$TailFilter.messageReceived(DefaultIoFilterChain.java:858)
at org.apache.mina.core.filterchain.DefaultIoFilterChain.callNextMessageReceived(DefaultIoFilterChain.java:542)
at org.apache.mina.core.filterchain.DefaultIoFilterChain.access$1300(DefaultIoFilterChain.java:48)
at org.apache.mina.core.filterchain.DefaultIoFilterChain$EntryImpl$1.messageReceived(DefaultIoFilterChain.java:947)
at org.apache.mina.filter.codec.ProtocolCodecFilter$ProtocolDecoderOutputImpl.flush(ProtocolCodecFilter.java:398)
at org.apache.mina.filter.codec.ProtocolCodecFilter.messageReceived(ProtocolCodecFilter.java:234)
at org.apache.mina.core.filterchain.DefaultIoFilterChain.callNextMessageReceived(DefaultIoFilterChain.java:542)
at org.apache.mina.core.filterchain.DefaultIoFilterChain.access$1300(DefaultIoFilterChain.java:48)
at org.apache.mina.core.filterchain.DefaultIoFilterChain$EntryImpl$1.messageReceived(DefaultIoFilterChain.java:947)
at org.apache.mina.core.filterchain.IoFilterAdapter.messageReceived(IoFilterAdapter.java:109)
at org.apache.mina.core.filterchain.DefaultIoFilterChain.callNextMessageReceived(DefaultIoFilterChain.java:542)
at org.apache.mina.core.filterchain.DefaultIoFilterChain.fireMessageReceived(DefaultIoFilterChain.java:535)
at org.apache.mina.core.polling.AbstractPollingIoProcessor.read(AbstractPollingIoProcessor.java:703)
at org.apache.mina.core.polling.AbstractPollingIoProcessor.process(AbstractPollingIoProcessor.java:659)
at org.apache.mina.core.polling.AbstractPollingIoProcessor.process(AbstractPollingIoProcessor.java:648)
at org.apache.mina.core.polling.AbstractPollingIoProcessor.access$600(AbstractPollingIoProcessor.java:68)
at org.apache.mina.core.polling.AbstractPollingIoProcessor$Processor.run(AbstractPollingIoProcessor.java:1120)
at org.apache.mina.util.NamePreservingRunnable.run(NamePreservingRunnable.java:64)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
We couldn't get SecurityStatus fix message. The problem we thought was about datadictionary. As a field HaltReason is a CharField
public class HaltReason extends CharField {
static final long serialVersionUID = 20050617L;
public static final int FIELD = 327;
public static final char NEWS_DISSEMINATION = 'D';
public static final char ORDER_INFLUX = 'E';
public static final char ORDER_IMBALANCE = 'I';
public static final char ADDITIONAL_INFORMATION = 'M';
public static final char NEWS_PENDING = 'P';
public static final char EQUIPMENT_CHANGEOVER = 'X';
When we look FIX50SP2 default datadictionary halt reason field is INT.
<field number="327" name="HaltReasonInt" type="INT">
<value enum="0" description="NEWS_DISSEMINATION"/>
<value enum="1" description="ORDER_INFLUX"/>
<value enum="2" description="ORDER_IMBALANCE"/>
<value enum="3" description="ADDITIONAL_INFORMATION"/>
<value enum="4" description="NEWS_PENDING"/>
<value enum="5" description="EQUIPMENT_CHANGEOVER"/>
</field>
We tried convert dictionary field to HaltReasonChar and the type CHAR but it didn't work. Did you ever get an exception like this?
Here's the security status message which I received.
8=FIXT.1.19=00017835=f49=BI_TEST56=LIABR34=589857=TRTK152=20220208-20:07:15.9281180=R1181=28513331350=285133255=T2-ON48=3762690422=M336=148325=N60=20220208-20:07:15.92810=245
It doesn't look like the HaltReason field (tag 327) is included in your status message. Is it an optional or required field for the SecurityStatus message type? You can control that in your data dictionary definition.
The dictionary for FIX50SP2.xml I have indicates this field is not required, but yours could be different.
Be careful of broker-supplied dictionaries; they are frequently wrong or outdated.
The following worked fine for me: report is of type quickfix.fix44.SecurityStatus
try {
response.setHaltReason(report.getString(327));
} catch (Exception e) {
log.error...
}

Configuring message-bundle with UTF-8 encoded messages

What I'm trying to achieve is UTF-8 encoded JSF validation and converter messages (message-bundle). Also, I would like custom resource bundle for labels, etc (resource-bundle).
I managed to create and use custom resource bundle successfully, but I have problem with defining message-bundle.
I have the following structure under src/ directory:
- com.example.i18n.resources
- FacesMsgs.properties
- FacesMsgs_de.properties
- UTF8FacesMsgs.java
- Msgs.properties
- Msgs_de.properties
- UTF8Msgs.java
Faces-config.xml snippet:
<application>
<message-bundle>com.example.i18n.resources.UTF8FacesMsgs</message-bundle>
<resource-bundle>
<base-name>com.example.i18n.resources.UTF8Msgs</base-name>
<var>msg</var>
</resource-bundle>
<locale-config>
<default-locale>de</default-locale>
</locale-config>
</application>
In my implementation I have followed the instructions given here:
Internationalization in JSF with UTF-8 encoded properties files
These are resulting classes:
public class ResourceBundleWrapper extends ResourceBundle {
public ResourceBundleWrapper(ResourceBundle parent) {
setParent(parent);
}
#Override
protected Object handleGetObject(String key) {
return parent.getObject(key);
}
#Override
public Enumeration<String> getKeys() {
return parent.getKeys();
}
}
EncodingControl.java
public class EncodingControl extends Control {
private String encoding;
private String extension;
public EncodingControl(String encoding, String extension) {
this.encoding = encoding;
this.extension = extension;
}
#Override
public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload)
throws IllegalAccessException, InstantiationException, IOException {
String bundleName = toBundleName(baseName, locale);
String resourceName = toResourceName(bundleName, extension);
ResourceBundle bundle = null;
InputStream stream = null;
if (reload) {
URL url = loader.getResource(resourceName);
if (url != null) {
URLConnection connection = url.openConnection();
if (connection != null) {
connection.setUseCaches(false);
stream = connection.getInputStream();
}
}
} else {
stream = loader.getResourceAsStream(resourceName);
}
if (stream != null) {
try {
bundle = new PropertyResourceBundle(new InputStreamReader(stream, encoding));
} finally {
stream.close();
}
}
return bundle;
}
}
UTF8FacesMsgs.java
public class UTF8FacesMsgs extends ResourceBundleWrapper {
public UTF8FacesMsgs() {
super(ResourceBundle.getBundle("com.example.i18n.resources.FacesMsgs",
FacesContext.getCurrentInstance().getViewRoot().getLocale(),
new EncodingControl("UTF-8", "properties"));
}
}
UTF8Msgs.java
public class UTF8Msgs extends ResourceBundleWrapper {
public UTF8Msgs() {
super(ResourceBundle.getBundle("com.example.i18n.resources.Msgs",
FacesContext.getCurrentInstance().getViewRoot().getLocale(),
new EncodingControl("UTF-8", "properties"));
}
}
Resulting behaviour:
resource-bundle configuration seems to be working without issue: default UTF8Msgs.java is used and all labels on the page are rendered successfully.
On the other hand, behaviour with message-bundle is quite different although everything is configured likewise: when validation fails (submiting the button triggers validation) I get MissingResourceException - the default UTF8FacesMsgs.java is not used and UTF8FacesMsgs_de.java is expected.
Behaviour with changes introduced:
When I configure faces-config.xml like this (directly using properties file, not java class):
<application>
<message-bundle>com.example.i18n.resources.FacesMsgs</message-bundle>
<resource-bundle>
<base-name>com.example.i18n.resources.UTF8Msgs</base-name>
<var>msg</var>
</resource-bundle>
<locale-config>
<default-locale>de</default-locale>
</locale-config>
</application>
and delete FacesMsgs_de.properties everything runs without issue. So - default properties file is used and default java file is not.
Does anybody know what is the problem with message-bundle configuration?

Can't catch success authorization even on Spring Security

Problem:
I have implemented the following application event listener and it can catch authentication (both cases success and failure) and authorization ( fail). However, while authorization is successful, the even does not be triggered. I traced the code and figured out publishAuthorizationSuccess in AbstractSecurityInterceptor class is always false so it doesn’t publish AuthorizedEvent.
Environment:
Run it on JUnit
The execution sequence of my program:
Run MySampleApp -> SomeService -> ResourcePatternBaseVoter -> AbstractSecurityInterceptor -> SecurityAuditor (not triggered when authorized successfully)
My code and config are shown as follows:
MySampleApp.class
public class MySampleApp{
#Test
public void test2() {
Authentication authentication = providerManager
.authenticate(new UsernamePasswordAuthenticationToken("admin", "admin"));
SecurityContextHolder.getContext().setAuthentication(authentication);
logger.debug(someService1.someMethod6());
}
SomeService.java
#Service
public class SomeService1 {
#Secured("rpb:reports:a.b.c:create")
public String someMethod6() {
return String.valueOf(Math.random());
}
ResourcePatternBaseVoter.java
#Component
public class ResourcePatternBaseVoter implements org.springframework.security.access.AccessDecisionVoter<Object> {
private static final Log logger = LogFactory.getLog(ResourcePatternBaseVoter.class);
#Autowired
private ResourcePatternBaseAuthorizer resourcePatternBaseAuthorizer;
#Override
public boolean supports(ConfigAttribute attribute) {
if ((attribute.getAttribute() != null) && attribute.getAttribute().startsWith("rpb:")) {
logger.debug("support attribute: " + attribute.getAttribute());
return true;
} else {
logger.debug("not support attribute: " + attribute.getAttribute());
return false;
}
}
#Override
public boolean supports(Class<?> clazz) {
return true;
}
#Override
public int vote(Authentication authentication, Object secureObject, Collection<ConfigAttribute> attributes) {
/* doSomething */
return ACCESS_GRANTED;
}
}
SecurityAuditor.java
#Component
public class SecurityAuditor implements ApplicationListener<AuthorizedEvent> {
#Override
public void onApplicationEvent(AuthorizedEvent event) {
logger.info("Here");
}
myAcl.xml
<bean id="methodAccessDecisionManager"
class="org.springframework.security.access.vote.AffirmativeBased">
<constructor-arg name="decisionVoters">
<list>
<bean class="org.springframework.security.access.vote.AuthenticatedVoter" />
<bean class="com.ibm.gbsc.ty.acl.rpb.ResourcePatternBaseVoter" />
</list>
</constructor-arg>
</bean>
AbstractSecurityInterceptor.class
if (publishAuthorizationSuccess) {
publishEvent(new AuthorizedEvent(object, attributes, authenticated));
}
This article got me started, but that bean does not exist in Spring Security 4.1.3 anymore. However, I found it hidden inside FilterChainProxy.
Not sure how ugly this hack is, but works:
#Configuration
#EnableWebSecurity
#EnableJpaAuditing
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private ApplicationContext applicationContext;
#EventListener
public void handle(ContextRefreshedEvent event) {
FilterChainProxy proxy = applicationContext.getBean(FilterChainProxy.class);
for (Filter f : proxy.getFilters("/")) {
if (f instanceof FilterSecurityInterceptor) {
((FilterSecurityInterceptor)f).setPublishAuthorizationSuccess(true);
}
}
}
...
}
Then my listener finally receives the AuthorizedEvent:
#Component
public class AppEventListener implements ApplicationListener {
private static final Logger logger = LoggerFactory.getLogger(AppEventListener.class);
#Override
#EventListener(value = {AuthorizedEvent.class})
public void onApplicationEvent(ApplicationEvent event)
{
if (event instanceof InteractiveAuthenticationSuccessEvent) {
Authentication auth = ((InteractiveAuthenticationSuccessEvent)event).getAuthentication();
logger.info("Login success: " + auth.getName() + ", details: " + event.toString());
} else if (event instanceof AbstractAuthenticationFailureEvent) {
logger.error("Login failed: " + event.toString());
} else if (event instanceof AuthorizedEvent) {
Authentication auth = ((AuthorizedEvent)event).getAuthentication();
logger.debug("Authorized: " + auth.getName() + ", details: " + event.toString());
} else if (event instanceof AuthorizationFailureEvent) {
Authentication auth = ((AuthorizationFailureEvent)event).getAuthentication();
logger.error("Authorization failed: " + auth.getName() + ", details: " + event.toString());
}
}
}
I tried to use the solution proposed by Arthur, however it throws a UnsupportedOperationException at proxy.getFilters("/").
Caused by: java.lang.UnsupportedOperationException: public abstract javax.servlet.ServletContext javax.servlet.ServletRequest.getServletContext() is not supported
at org.springframework.security.web.UnsupportedOperationExceptionInvocationHandler.invoke(FilterInvocation.java:235) ~[spring-security-web-5.3.8.RELEASE.jar:5.3.8.RELEASE]
at com.sun.proxy.$Proxy269.getServletContext(Unknown Source) ~[na:na]
at javax.servlet.ServletRequestWrapper.getServletContext(ServletRequestWrapper.java:369) ~[tomcat-embed-core-9.0.43.jar:4.0.FR]
at javax.servlet.ServletRequestWrapper.getServletContext(ServletRequestWrapper.java:369) ~[tomcat-embed-core-9.0.43.jar:4.0.FR]
at org.springframework.boot.security.servlet.ApplicationContextRequestMatcher.matches(ApplicationContextRequestMatcher.java:58) ~[spring-boot-2.3.9.RELEASE.jar:2.3.9.RELEASE]
at org.springframework.security.web.util.matcher.OrRequestMatcher.matches(OrRequestMatcher.java:67) ~[spring-security-web-5.3.8.RELEASE.jar:5.3.8.RELEASE]
at org.springframework.security.web.DefaultSecurityFilterChain.matches(DefaultSecurityFilterChain.java:57) ~[spring-security-web-5.3.8.RELEASE.jar:5.3.8.RELEASE]
at org.springframework.security.web.FilterChainProxy.getFilters(FilterChainProxy.java:226) ~[spring-security-web-5.3.8.RELEASE.jar:5.3.8.RELEASE]
at org.springframework.security.web.FilterChainProxy.getFilters(FilterChainProxy.java:241) ~[spring-security-web-5.3.8.RELEASE.jar:5.3.8.RELEASE]
In order to fix this I changed the implementation to
#EventListener
public void handle ( ContextRefreshedEvent event ) {
applicationContext.getBean ( FilterChainProxy.class )
.getFilterChains ()
.stream ()
.map ( SecurityFilterChain::getFilters )
.flatMap ( Collection::stream )
.filter ( filter -> filter instanceof FilterSecurityInterceptor )
.map ( filter -> (FilterSecurityInterceptor) filter)
.forEach ( filterSecurityInterceptor -> filterSecurityInterceptor.setPublishAuthorizationSuccess ( true ) );
}
While this works this will apply to all filter chains and all instances of the FilterSecurityInterceptor.
It would be possible to filter these further since the FilterSecurityInterceptor maintains a map of which the keys are RequestMatchers and these could be used to narrow it down further, for example to those instances of the FilterSecurityInterceptor that are applied to a certain route or those that require a certain authority. However since the map is private and there is no way to access the keys of the map, Reflection is required in order to do this.
Since I want to avoid using Reflection, I would rather suggest carefully configuring the Security Filter Chain so that it does not throw unnecessary AuthorizedEvents and to ensure that whatever is listening to these events is cheap and fast to execute.
I'm using Spring Boot 2.3.9.RELEASE which depends on Spring Security 5.3.8.RELEASE
It worth pointing that Spring Security is currently adding the so called AuthorizationManager, hopefully this will allow the configuration of this option in more natural way or make it obsolete.

JSF 2.X UI Component use of ValueExpression

I am trying to build a simple component to understand how and why JSF 2.X works the way it does. I have been using the newer annotations and have been trying to piece together a clear example.
So I have built my component and deployed it in a xhtml file as follows:
<kal:day value="K.Day" title="Kalendar" model="#{kalendarDay}"/>
The within the UIComponent I do the following:
ValueExpression ve = getValueExpression("model");
if (ve != null)
{
System.out.println("model expression "+ve.getExpressionString());
model = (KalendarDay) ve.getValue(getFacesContext().getELContext());
System.out.println("model "+model);
}
The expression "#{kalendarDay}" is correctly displayed indicating that the value has been successfully transmitted between the page and the component. However the evaluation of the expression results in "null".
This seems to indicate that the backing bean is unavailable at this point, although the page correctly validates and deploys. So I am 95% certain that the bean is there at run time.
So perhaps this is a phase thing? Should I be evaluating this in the decode of the renderer and setting the value in the attributes map? I am still a little confused about the combination of actual values and value expressions.
So my question is where should I fetch and evaluate the valueExpression for model and should I store the result of the evaluation in the UIComponent or should I simply evaluate it every time?
SSCCE files below I think these are the only required files to demonstrate the problem
Bean Interface -----
/**
*
*/
package com.istana.kalendar.fixture;
import java.util.Date;
/**
* #author User
*
*/
public interface KalendarDay
{
public Date getDate();
}
Bean Implementation ---
/**
*
*/
package com.istana.kalendar.session.wui;
import java.util.Calendar;
import java.util.Date;
import javax.ejb.Stateful;
import javax.inject.Named;
import com.istana.kalendar.fixture.KalendarDay;
/**
* #author User
*
*/
#Named ("kalendarDay")
#Stateful
public class KalKalendarDay
implements KalendarDay
{
private Calendar m_date = Calendar.getInstance();
/* (non-Javadoc)
* #see com.istana.kalendar.fixture.KalendarDay#getDate()
*/
#Override
public Date getDate()
{
return m_date.getTime();
}
}
UIComponent ---
/**
*
*/
package com.istana.kalendar.fixture.jsf;
import javax.el.ValueExpression;
import javax.faces.component.FacesComponent;
import javax.faces.component.UIOutput;
import com.istana.kalendar.fixture.KalendarDay;
/**
* #author User
*
*/
#FacesComponent (value=UIDay.COMPONENT_TYPE)
public class UIDay extends UIOutput
{
static final
public String COMPONENT_TYPE = "com.istana.kalendar.fixture.jsf.Day";
static final
public String COMPONENT_FAMILY = "com.istana.kalendar.fixture.jsf.Day";
private KalendarDay m_model;
private String m_title;
#Override
public String getRendererType()
{
return UIDayRenderer.RENDERER_TYPE;
}
#Override
public String getFamily()
{
return COMPONENT_FAMILY;
}
public KalendarDay getModel()
{
KalendarDay model = (KalendarDay) getStateHelper().eval("model");
System.out.println("model "+model);
return model;
}
public void setModel(KalendarDay model)
{
getStateHelper().put("model",model);
}
public String getTitle()
{
return (String) getStateHelper().eval("title");
}
public void setTitle(String title)
{
getStateHelper().put("title",title);
}
}
UIComponentRenderer ---
/**
*
*/
package com.istana.kalendar.fixture.jsf;
import java.io.IOException;
import javax.el.ValueExpression;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.render.FacesRenderer;
import javax.faces.render.Renderer;
import com.istana.kalendar.fixture.KalendarDay;
/**
* #author User
*
*/
#FacesRenderer (componentFamily = UIDay.COMPONENT_FAMILY
,rendererType = UIDayRenderer.RENDERER_TYPE
)
public class UIDayRenderer extends Renderer
{
static final
public String RENDERER_TYPE = "com.istana.kalendar.fixture.jsf.DayRenderer";
#Override
public void encodeBegin (FacesContext context,UIComponent component)
throws IOException
{
UIDay uic = (UIDay) component;
ResponseWriter writer = context.getResponseWriter();
writer.startElement("p", uic);
/*
* This is the call that triggers the println
*/
writer.write("Day - "+uic.getModel().getDate());
}
#Override
public void encodeEnd (FacesContext context,UIComponent component)
throws IOException
{
ResponseWriter writer = context.getResponseWriter();
writer.endElement("p");
writer.flush();
}
}
kalendar.taglib.xml ---
<facelet-taglib
id="kalendar"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facelettaglibrary_2_0.xsd"
version="2.0"
>
<namespace>http://istana.com/kalendar</namespace>
<tag>
<tag-name>day</tag-name>
<component>
<component-type>com.istana.kalendar.fixture.jsf.Day</component-type>
</component>
</tag>
</facelet-taglib>
I'm not sure why it's null, but the symptoms indicate that the #{kalendarDay} is been specified during view render time while you're trying to evaluate it during the view build time.
So perhaps this is a phase thing? Should I be evaluating this in the decode of the renderer and setting the value in the attributes map? I am still a little confused about the combination of actual values and value expressions.
You should use the encodeXxx() methods of the component or the associated renderer (if any) to generate HTML based on the component's attributes/properties.
You should use the decode() method of the component or the associated renderer (if any) to set component's attributes/properties based on HTTP request parameters which are been sent along with a HTML form submit.
So my question is where should I fetch and evaluate the valueExpression for model and should I store the result of the evaluation in the UIComponent or should I simply evaluate it every time?
Since JSF 2.x it's recommended to explicitly specify a getter and setter for component attributes which in turn delegates to UIComponent#getStateHelper().
public String getValue() {
return (String) getStateHelper().eval("value");
}
public void setValue(String value) {
getStateHelper().put("value", value);
}
public String getTitle() {
return (String) getStateHelper().eval("title");
}
public void setTitle(String title) {
getStateHelper().put("title", title);
}
public Object getModel() {
return getStateHelper().eval("model");
}
public void setModel(Object model) {
getStateHelper().put("model", model);
}
That's all you basically need (note that the getter and setter must exactly match the attribute name as per Javabeans specification). Then in the encodeXxx() method(s) just call getModel() to get (and evaluate) the value of the model attribute.

Resources