Declarative services in OSGI - maven

I have created a (very) simple test to determine how to send and receive events using Apache Felix.
This is my sender:
package be.pxl;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
import java.util.HashMap;
#Component(name = "be.pxl.Publisher", immediate = true)
public class Publisher {
EventAdmin admin;
#Activate
public void run(Object object) {
System.out.println("IN PUBLISHER");
Event event = new Event("event", new HashMap<String, Object>());
System.out.println("\tEVENT: " + event);
admin.postEvent(event);
System.out.println("\tADMIN: " + admin);
}
#Reference(name="be.pxl.admin", service = EventAdmin.class)
protected void setEventAdmin(EventAdmin admin) {
this.admin = admin;
}
}
This is my receiver:
package be.pxl;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventConstants;
import org.osgi.service.event.EventHandler;
import java.util.Dictionary;
import java.util.Hashtable;
#Component(name = "be.pxl.Subscriber", immediate = true)
public class Subscriber implements EventHandler {
private BundleContext context;
#Activate
public void run(Object object) {
System.out.println("IN SUBSCRIBER");
System.out.println("\tIN RUN METHOD");
String[] topics = new String[]{"event"};
Dictionary props = new Hashtable();
props.put(EventConstants.EVENT_TOPIC, topics);
System.out.println("\t\tCONTEXT: " + context);
context.registerService(EventHandler.class.getName(), this, props);
System.out.println("\t\tCONTEXT AFTER REGISTERSERVICE: " + context);
}
public void handleEvent(Event event) {
System.out.println("IN SUBSCRIBER");
String text = event.getProperty("text").toString();
System.out.println("\tEVENT CALLED: " + text);
}
#Reference(name="be.pxl.context", service=BundleContext.class)
protected void setBundleContex(BundleContext context) {
this.context = context;
}
}
This is the pom of my sender:
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>be.pxl</groupId>
<artifactId>EventSender</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.service.event</artifactId>
<version>1.3.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.core</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.service.component.annotations</artifactId>
<version>1.3.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.osgi</groupId>
<artifactId>org.eclipse.osgi.services</artifactId>
<version>3.2.100.v20100503</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<version>2.4.0</version>
<extensions>true</extensions>
<configuration>
<instructions>
<Bundle-Vendor>SmartCampus</Bundle-Vendor>
<Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
<Export-Package>
be.pxl.*;version="1.0.0"
</Export-Package>
<Import-Package>
org.osgi.service.component.annotations
org.eclipse.osgi.service
org.osgi.core
org.osgi.service.event
</Import-Package>
<_dsannotations>*</_dsannotations>
</instructions>
</configuration>
</plugin>
</plugins>
</build>
</project>
Everything compiles fine. I create it using mvn clean package, then I install this jar file in my apache felix container and start it. However, nothing happens. Nothing get pritns out.
Thanks in advance!

You appear to be most of the way there! As you've identified, Event Admin uses a whiteboard model to receive events. The important thing is that you need to tell the whiteboard which topics you want to listen to, which you do.
%%% Update %%%
Event admin topic names use a hierarchy of tokens separated by / characters. When publishing an event you do so to a specific topic, for example foo/bar/baz. When receiving events the EventHandler will be called for topics that match its registered interest(s). These interests can either be for a specific topic, or they can end with a * to indicate a wildcard match. For example foo/bar/* would receive events sent to foo/bar/baz and events sent to foo/bar/fizzbuzz.
%%% Back to the original %%%
There are, however a couple of issues with your code:
Firstly:
#Reference(name="be.pxl.context", service=BundleContext.class)
protected void setBundleContex(BundleContext context) {
this.context = context;
}
This is not how you access the BundleContext for your bundle. If you do need a BundleContext then it should be injected as a parameter into your #Activate annotated method. A BundleContext should never be registered as a service (it represents your bundle's private access to the OSGi framework), and it would not surprise me to find that this reference is unsatisfied in your example. You don't actually need the BundleContext however because...
Secondly:
#Activate
public void run(Object object) {
System.out.println("IN SUBSCRIBER");
System.out.println("\tIN RUN METHOD");
String[] topics = new String[]{"event"};
Dictionary props = new Hashtable();
props.put(EventConstants.EVENT_TOPIC, topics);
System.out.println("\t\tCONTEXT: " + context);
context.registerService(EventHandler.class.getName(), this, props);
System.out.println("\t\tCONTEXT AFTER REGISTERSERVICE: " + context);
}
This is not the right way to write an activate method (and as a result it may not be being called), nor should you be registering your component as a service here. When you make your class an #Component it will automatically be registered as a service using each directly implemented interface. This means that:
#Component(name = "be.pxl.Subscriber", immediate = true)
public class Subscriber implements EventHandler {
...
}
is already an OSGi EventHandler service!
You can add service properties to your component using the #Component annotation, or from the OSGi R7 release (due in a couple of months) using Component Property annotations. In this case you want to set your event.topics property like this:
#Component(property="event.topics=event")
You can then get rid of the activate method completely if you like.
Finally:
Event Admin is not a message queue, and your publisher is a one-shot send. Therefore if your publisher sends the event before the handler is fully registered then it will never receive the event. Consider making the publisher send periodic events, or be certain that the receiver starts before the publisher so that you see the message.
P.S.
It's not technically a problem, but I see that you're using version 2.4 of the maven-bundle-plugin. This is very old and the current released version of bnd is 3.5.0. The Bnd team have also started providing their own Maven plugins (such as the bnd-maven-plugin) that you might want to look at.

Related

Spring boot and Swagger url and startup questions

I come from programming in c# and now I have to create a couple of Rest Apis in Spring Boot.
Everything is working ok and I can show the API in Swagger with springfox-swagger-ui
But I have two questions that I could not find in Internet
Is there any way to show the url ui in the console app with server, port, etc?
Is there any way to open the swagger url everytime I run the app in the localhost?
Thanks
Spring boot version
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>3.0.0</version>
</dependency>
I know it's not the question you're asking, but springfox is currently having issues with newer versions of spring. The spring version you're using is still working but as of 2.6 there are bugs, and it looks like the project is not well maintained. Since you're at the beginning of the project, switching is not too hard. You could move to springdocs for example (for migration: https://springdoc.org/#migrating-from-springfox).
With respect to opening a url, there are some good solutions mentioned here: How to open the default webbrowser using java . You could make your swagger url a property and have swagger configure it accordingly, then you can reuse the property to call the url on run-time. If you want to differentiate between environments I'd suggest use profiles. Only open the url in the browser if you start the app on dev environment, and not on prod is then specified by using #Profile("dev"). Create a commandline/application runner with the profile annotation (https://www.tutorialspoint.com/spring_boot/spring_boot_runners.htm), and call the url from there.
That said, combining it gives:
package com.example.demo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import java.awt.*;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
#Profile("dev")
#Component
public class SwaggerRunner implements ApplicationRunner {
#Value("${springdoc.swagger-ui.path}")
private String swaggerPath;
#Override
public void run(ApplicationArguments args) throws Exception {
log("\nWelcome to Multi Brow Pop.\nThis aims to popup a browsers in multiple operating systems.\nGood luck!\n");
final String swaggerUrl = "http://localhost:8000/" + swaggerPath;
log("We're going to this page: " + swaggerUrl);
String myOS = System.getProperty("os.name").toLowerCase();
log("(Your operating system is: " + myOS + ")\n");
try {
if (Desktop.isDesktopSupported()) { // Probably Windows
log(" -- Going with Desktop.browse ...");
Desktop desktop = Desktop.getDesktop();
desktop.browse(new URI(swaggerUrl));
} else { // Definitely Non-windows
Runtime runtime = Runtime.getRuntime();
if (myOS.contains("mac")) { // Apples
log(" -- Going on Apple with 'open'...");
runtime.exec("open " + swaggerUrl);
} else if (myOS.contains("nix") || myOS.contains("nux")) { // Linux flavours
log(" -- Going on Linux with 'xdg-open'...");
runtime.exec("xdg-open " + swaggerUrl);
} else
log("I was unable/unwilling to launch a browser in your OS :( #SadFace");
}
log("\nThings have finished.\nI hope you're OK.");
} catch (IOException | URISyntaxException eek) {
log("**Stuff wrongly: " + eek.getMessage());
}
}
private static void log(String log) {
System.out.println(log);
}
}
put springdoc.swagger-ui.path=/custom/path in your application.properties to change the path to your swagger-ui

How to access package from another service within the same parent

I have created 5 microservices, all having the same parent in pom. I am trying to access a package from one child service core-service into another data-processor.
Below is my data-processor code which is trying to access com.itqe.tdm.core.model.Customer from core-service
package com.itqe.dataprocessor.util;
import org.springframework.batch.item.excel.RowMapper;
import org.springframework.batch.item.excel.support.rowset.RowSet;
import org.springframework.stereotype.Component;
import com.itqe.tdm.core.model.Customer;
#Component
public class RowMapperImpl implements RowMapper<Customer> {
#Override
public Customer mapRow(RowSet rs) throws Exception {
if (rs == null || rs.getCurrentRow() == null) {
return null;
}
Customer cust = new Customer();
cust.setFirstName(rs.getColumnValue(0));
cust.setLastName(rs.getColumnValue(1));
return cust;
}
}
When I try to build the code mvn clean install I am getting the following error
package com.itqe.tdm.core.model does not exist
I already did mvn install on the parent service as well as core-service. The jar is present in my m2. I added the dependency in the pom of data-processor
<dependency>
<groupId>com.itqe</groupId>
<artifactId>core-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
Could someone please help me understand what am I missing here?
Appreciate the help.
Parent pom.xml
<modules>
<module>config-server</module>
<module>registry-service</module>
<module>gateway-service</module>
<module>core-service</module>
<module>data-processor</module>
</modules>

Issue getting MessageAttributes in spring-cloud SNS/SQS listener

I am using spring-boot-1.5.10 and spring-cloud and using spring-cloud-starter-aws-messaging. I am able to send and receive the message but couldn't get the SNS message attributes. Any help would be really appreciable. Please find the code below,
pom.xml
<?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 http://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>1.5.19.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.aws.sample</groupId>
<artifactId>aws</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>aws</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Edgware.SR5</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-aws</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-aws-messaging</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
controller.java
#RestController
#RequestMapping(value = "/sns")
#AllArgsConstructor
public class SimpleSnsController {
private NotificationMessagingTemplate notificationMessagingTemplate;
#PostMapping("/saveEmployee")
public String save(#RequestBody Employee employee){
Map<String,Object> headers = new HashMap<>();
headers.put("subject", "send employee details to sqs");
headers.put("name","murugan");
headers.put("traceId","sample");
//notificationMessagingTemplate.sendNotification("sample-sns", employee, "send employee details to sqs");
notificationMessagingTemplate.convertAndSend("sample-sns", employee, headers);
return "success";
}
//#SqsListener(value = "sample-queue")
#SqsListener(value = "${sqs.consumer.name}")
public void receiveSnsSqs(String message, #NotificationMessage Employee employee) {
System.out.println("SNS Consumer received the message::"+message);
System.out.println("SNS Consumer received the notificationMessage::"+employee);
//Here i would like to get the message attribute
}
}
output message received:
{
"Type" : "Notification",
"MessageId" : "ba9dab52-aae8-5940-a3e2-ff8c8458ef52",
"TopicArn" : "arn:aws:sns:XXX",
"Message" : "{\"name\":\"David\",\"age\":\"31\",\"designation\":\"developer\"}",
"Timestamp" : "2019-02-13T14:40:48.501Z",
"SignatureVersion" : "1",
"Signature" : "XXX",
"SigningCertURL" : "XXX",
"UnsubscribeURL" : "XXX",
"MessageAttributes" : {
"traceId" : {"Type":"String","Value":"sample"},
"subject" : {"Type":"String","Value":"send employee details to sqs"},
"name" : {"Type":"String","Value":"murugan"},
"id" : {"Type":"String","Value":"68bf17f2-0f88-4cc5-0609-0ccd42b19ce4"},
"SenderId" : {"Type":"String","Value":"David"},
"contentType" : {"Type":"String","Value":"application/json;charset=UTF-8"},
"timestamp" : {"Type":"Number.java.lang.Long","Value":"1550068848349"}
}
}
I would like to fetch the messageAttribute like name,traceId in consumer which I set in SNS producer. I have browsed a lot but couldn't find any solution. Any help would be really appreciable.
Try enabling Raw Message Delivery.
It will not wrap the original SNS message, and allow you to get the message and headers through the normal annotations #Header, #Headers
https://docs.aws.amazon.com/sns/latest/dg/sns-large-payload-raw-message-delivery.html
If you can't use Raw Message Delivery, I made a new annotation to assist in retrieving a Notification Header
#NotificationHeader
import org.springframework.core.annotation.AliasFor;
import org.springframework.messaging.handler.annotation.ValueConstants;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.PARAMETER)
public #interface NotificationHeader {
/**
* Alias for {#link #name}.
*/
#AliasFor("name")
String value() default "";
/**
* The name of the request header to bind to.
*/
#AliasFor("value")
String name() default "";
/**
* Whether the header is required.
* <p>Default is {#code true}, leading to an exception if the header is
* missing. Switch this to {#code false} if you prefer a {#code null}
* value in case of a header missing.
* #see #defaultValue
*/
boolean required() default true;
/**
* The default value to use as a fallback.
* <p>Supplying a default value implicitly sets {#link #required} to {#code false}.
*/
String defaultValue() default ValueConstants.DEFAULT_NONE;
}
*NotificationHeaderArgumentResolver
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.cloud.aws.messaging.support.NotificationMessageArgumentResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.handler.annotation.support.HeaderMethodArgumentResolver;
import org.springframework.util.Assert;
public class NotificationHeaderArgumentResolver extends HeaderMethodArgumentResolver {
private NotificationMessageArgumentResolver notificationArgumentResolver;
public NotificationHeaderArgumentResolver(ConversionService cs, ConfigurableBeanFactory beanFactory) {
super(cs, beanFactory);
notificationArgumentResolver = new NotificationMessageArgumentResolver(new NoOptMessageConverter());
}
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(NotificationHeader.class);
}
#Override
#Nullable
protected Object resolveArgumentInternal(MethodParameter parameter, Message<?> message, String name)
throws Exception {
Message notificationMessage = (Message) notificationArgumentResolver.resolveArgument(parameter, message);
return super.resolveArgumentInternal(parameter, notificationMessage, name);
}
#Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
NotificationHeader annotation = parameter.getParameterAnnotation(NotificationHeader.class);
Assert.state(annotation != null, "No Header annotation");
return new HeaderNamedValueInfo(annotation);
}
private static class HeaderNamedValueInfo extends NamedValueInfo {
private HeaderNamedValueInfo(NotificationHeader annotation) {
super(annotation.name(), annotation.required(), annotation.defaultValue());
}
}
public static class NoOptMessageConverter implements MessageConverter {
#Override
public Message<?> toMessage(Object payload, #Nullable MessageHeaders headers) {
return null;
}
#Override
public Object fromMessage(Message<?> message, Class<?> targetClass) {
return message;
}
}
}
*NotificationHeaderConfiguration
#Bean
public QueueMessageHandlerFactory queueMessageHandlerFactory() {
QueueMessageHandlerFactory queueMessageHandlerFactory = new QueueMessageHandlerFactory();
queueMessageHandlerFactory.setArgumentResolvers(Collections.singletonList(new NotificationHeaderArgumentResolver(null, null)));
return queueMessageHandlerFactory;
}

WebDriverManager for PhantomJSDriver not working

I cannot get WebDriverManager to work. I would like to use PhantomJSDriver without having to set a system property like this:
System.setProperty("phantomjs.binary.path", "E:/phantomjs-2.1.1-windows/bin/phantomjs.exe");
I have these dependencies in my pom.xml:
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-server</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>1.5.1</version>
</dependency>
This is my code/test:
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class TestA {
WebDriver driver;
#BeforeClass
public static void setupClass() {
PhantomJsDriverManager.getInstance().setup();
}
#Before
public void setUp() {
driver = new PhantomJSDriver();
}
#Test
public void test() {
driver.get("https://www.google.de/");
System.out.println(driver.getTitle());
assertEquals("Google", driver.getTitle());
}
}
The test fails:
org.junit.ComparisonFailure: expected:<[Google]> but was:<[]>
Does anybody know what I am doing wrong? Thanks in advance!
UPDATE: Now I have another problem. Before using the webdrivermanager I had this:
DesiredCapabilities dc = DesiredCapabilities.phantomjs();
dc.setJavascriptEnabled(true);
dc.setCapability(PhantomJSDriverService.PHANTOMJS_CLI_ARGS,
new String[] { "--web-security=no", "--ignore-ssl-errors=yes" });
System.setProperty("phantomjs.binary.path", "E:/phantomjs-2.1.1-windows/bin/phantomjs.exe");
WebDriver driver = new PhantomJSDriver(dc);
Now, when I delete the line with System.setProperty(...), it is not working anymore. Thanks for helping.
Looks like your making the assertion to early, so the page is not loaded when you call getTitle() on it. What does your println print out?
Try adding a wait to to your test, if you know the page title should be "Google" then why not wait for that to be true before doing any further assertions? When the page title is equal to what your expecting you can be reasonably confident the page is loaded. Try this:
public Boolean waitForPageIsLoaded(String title) {
return new WebDriverWait(driver, 10).until(ExpectedConditions.titleIs(title));
}

How to shutdown embedded Derby which is defined as a DataSource properly on WildFly

When I defined a Derby DataSource on WildFly normally, db.lck file is left undeleted and it indicates the database was not shutdown properly at every shutdown of WildFly. because embedded Derby requires a special shutdown procedure that is acquiring a new connection with JDBC URL which ended with ";shutdown=true" string.
So, I need something like a shutdown-hook which executes the shutdown procedure. I found that old JBoss have a way to achieve it:
https://developer.jboss.org/wiki/SetUpADerbyDatasource
http://grepcode.com/file/repository.jboss.org/nexus/content/repositories/releases/org.jboss.jbossas/jboss-as-varia/6.0.0.Final/org/jboss/jdbc/DerbyDatabase.java
But I don't know how to apply that on recent WildFly because it looks like "mbean" and "depends" element not allowed anymore on its data source definition, and I can't find its equivalent on recent WildFly.
I think "connection-listener-class" variable of datasource definition is considerable, and it might be a way to achieve it. I haven't tried it yet but it looks like bit complex and I'm not sure it works as expected.
So, is there a way to define a shutdown-hook which executes shutdown procedure of Derby with recent WildFly?
EDIT:
I posted an instruction of installing Apache Derby to WildFly which includes my solution.
http://www.nailedtothex.org/roller/kyle/entry/installing-apache-derby-to-wildfly
I found a better solution with MBean. it simply executes the shutdown procedure at every WildFly shutdown.
Just clone this repository and build a jar, put it to $WILDFLY_HOME/standalone/deployments. there are various files in the project so it's annoying to paste all of files here.
A dependency to org.jboss.ironjacamar.jdbcadapters, connection-listener-class and connection-listener-property are unnecessary so now $WILDFLY_HOME/modules/org/apache/derby/main/module.xml can be simplified like the following:
module.xml
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.0" name="org.apache.derby">
<resources>
<resource-root path="derby.jar"/>
</resources>
<dependencies>
<module name="javax.api"/>
<module name="javax.transaction.api"/>
</dependencies>
</module>
connection-listener-class worked as I expected. I registered a listener class then Derby starts shutdown properly at every shutdown of WildFly.
But I should remember one thing - test-connection-in-pool won't fire connection-listener-class. if I just launch WildFly, then exec test-connection-in-pool to datasource of Derby, then shutdown WildFly, then the listener will never be invoked and db.lck will left undeleted. so, it might be not a ideal solution. something like more straightforward shutdown-hook would be better.
What I did are following:
Put derby.jar, a jar which contains ConnectionListener implementation and module.xml into $WILDFLY_HOME/modules/org/apache/derby/main
Register JDBC driver of Derby (following commands are for jboss-cli)
/subsystem=datasources/jdbc-driver=derby:add(driver-name=derby, driver-module-name=org.apache.derby, driver-class-name=org.apache.derby.jdbc.EmbeddedDriver)
Register a datasource
data-source add --name=DerbyDS --driver-name=derby --connection-url=jdbc:derby:/Users/kyle/tmp/derbytest --jndi-name=java:jboss/jdbc/DerbyDS --user-name=sa --password=sa
Configure connection-listener-class
/subsystem=datasources/data-source=DerbyDS:write-attribute(name=connection-listener-class, value=org.nailedtothex.derby.DerbyShutdownConnectionListener)
Configure connection-listener-property
/subsystem=datasources/data-source=DerbyDS:write-attribute(name=connection-listener-property,value={"url"=>"jdbc:derby:/Users/kyle/tmp/derbytest"}
module.xml
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.0" name="org.apache.derby">
<resources>
<resource-root path="derby.jar"/>
<resource-root path="derby-shutdown-hook-1.0-SNAPSHOT.jar"/>
</resources>
<dependencies>
<module name="javax.api"/>
<module name="javax.transaction.api"/>
<module name="org.jboss.ironjacamar.jdbcadapters"/>
</dependencies>
</module>
DerbyShutdownConnectionListener.java
Please refer GitHub for whole project contains pom.xml
import org.jboss.jca.adapters.jdbc.spi.listener.ConnectionListener;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.logging.Level;
import java.util.logging.Logger;
public class DerbyShutdownConnectionListener implements ConnectionListener {
private static final Logger log = Logger.getLogger(DerbyShutdownConnectionListener.class.getName());
private static final String DEFAULT_URL = "jdbc:derby:";
private static final String SHUTDOWN_SUFFIX = ";shutdown=true";
private String url;
private String urlForShutdown;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
#Override
synchronized public void initialize(final ClassLoader classLoader) throws SQLException {
urlForShutdown = createUrlForShutdown();
Runtime.getRuntime().addShutdownHook(new Thread() {
#Override
public void run() {
log.log(Level.INFO, "Shutdown derby. URL to use: {0}", urlForShutdown);
shutdown(urlForShutdown);
}
});
log.log(Level.INFO, "Derby shutdown hook added. URL to use: {0}", urlForShutdown);
}
private String createUrlForShutdown() {
return (url == null ? DEFAULT_URL : url) + SHUTDOWN_SUFFIX;
}
private void shutdown(String url) {
Connection cn = null;
try {
cn = DriverManager.getConnection(url);
} catch (SQLException e) {
if ("08006".equals(e.getSQLState()) || "XJ015".equals(e.getSQLState())) {
log.log(Level.INFO, "Derby shutdown succeeded. SQLState={0}", e.getSQLState());
return;
}
log.log(Level.SEVERE, "Derby shutdown failed", e);
} finally {
if (cn != null) {
try {
cn.close();
} catch (Exception e) {
}
}
}
}
#Override
public void activated(Connection connection) throws SQLException {
}
#Override
public void passivated(Connection connection) throws SQLException {
}
}

Resources