Merge branch 'master' of git://git.springsource.org/spring-integration/spring-integration

This commit is contained in:
David Turanski
2010-11-08 12:38:53 -05:00
296 changed files with 5087 additions and 4279 deletions

1
.gitignore vendored
View File

@@ -17,3 +17,4 @@ pom.xml
si.java.hsp
spring-integration-jms/activemq-data/
spring-integration-samples/loanshark/application.log*
target

View File

@@ -85,6 +85,10 @@ configure(javaprojects) {
apply plugin: 'eclipse' // `gradle eclipse` to generate .classpath/.project
apply plugin: 'idea' // `gradle idea` to generate .ipr/.iml
// ensure JDK 5 compatibility
sourceCompatibility=1.5
targetCompatibility=1.5
// set up dedicated directories for jars and source jars.
// this makes it easier when putting together the distribution
libsBinDir = new File(libsDir, 'bin')
@@ -107,7 +111,7 @@ configure(javaprojects) {
log4jVersion = '1.2.12'
mockitoVersion = '1.8.4'
springVersion = '3.0.5.RELEASE'
springSecurityVersion = '3.0.3.RELEASE'
springSecurityVersion = '3.0.4.RELEASE'
springWsVersion = '1.5.9'
sourceSets {
@@ -259,6 +263,7 @@ project('spring-integration-jms') {
description = 'Spring Integration JMS Support'
dependencies {
compile project(":spring-integration-core")
compile "org.springframework:spring-context:$springVersion"
compile "org.springframework:spring-jms:$springVersion"
compile "org.springframework:spring-tx:$springVersion"
compile ("org.apache.geronimo.specs:geronimo-jms_1.1_spec:1.1") { provided = true }
@@ -399,6 +404,7 @@ project('spring-integration-xmpp') {
compile "jivesoftware:smackx:3.1.0"
compile "org.springframework:spring-context-support:$springVersion"
testCompile project(":spring-integration-test")
testCompile project(":spring-integration-stream")
}
}
@@ -412,6 +418,8 @@ apply from: "$rootDir/gradle/dist.gradle"
// add tasks like 'snapshotDependencyCheck'
apply from: "${rootDir}/gradle/checks.gradle"
// add 'generatePom' task to generate root pom with <modules> section
apply from: "$rootDir/gradle/maven-root-pom.gradle"
// -----------------------------------------------------------------------------
// Import tasks related to releasing and managing the project

View File

@@ -186,7 +186,7 @@ dependencies {
scpAntTask("org.apache.ant:ant-jsch:1.8.1")
}
checkForProps(taskPath: project.path + ':uploadArchives', requiredProps: ['sshHost', 'sshUsername'])
checkForProps(taskPath: project.path + ':uploadArchives', requiredProps: ['sshHost', 'sshUsername', 'sshPrivateKey'])
uploadArchives {
def sshHost = project.properties.sshHost
@@ -206,8 +206,8 @@ uploadArchives {
name = 'sshHost: ' + sshHost // used for debugging
host = sshHost
user = sshUsername
if (project.hasProperty('remoteSiteDir')) {
keyFile = sshPrivateKey as File
if (project.hasProperty('sshPrivateKey')) {
keyFile = sshPrivateKey ? sshPrivateKey as File : null
}
addArtifactPattern "${remoteDocsDir}/${archive.archiveName}"
}
@@ -227,13 +227,20 @@ uploadArchives {
classpath: configurations.scpAntTask.asPath)
// copy the archive, unpack it, then delete it
def unpackCommand = "cd ${remoteDocsDir} && unzip ${archive.archiveName}"
def deleteCommand = "rm ${remoteDocsDir}/${archive.archiveName}"
def unpackCommand = "cd ${remoteDocsDir} && rm -rf ${version} && unzip -qKo ${archive.archiveName} && rm ${archive.archiveName}"
def wildcardSymlinkCommand = "cd ${remoteDocsDir} && rm -f ${version.wildcardValue} && ln -s ${version} ${version.wildcardValue}"
def latestGASymlinkCommand = "cd ${remoteDocsDir} && rm -f latest-ga && ln -s ${version} latest-ga"
println "sshexec ${unpackCommand}"
println "Unpacking docs archive: (${unpackCommand})"
sshexec(host: sshHost, username: sshUsername, keyfile: sshPrivateKey, command: unpackCommand)
println "sshexec ${deleteCommand}"
sshexec(host: sshHost, username: sshUsername, keyfile: sshPrivateKey, command: deleteCommand)
if (version.releaseType != 'SNAPSHOT') {
println "Creating wildcard symlink: (${wildcardSymlinkCommand})"
sshexec(host: sshHost, username: sshUsername, keyfile: sshPrivateKey, command: wildcardSymlinkCommand)
}
if (version.releaseType == 'RELEASE') {
println "Creating latest-ga symlink: (${latestGASymlinkCommand})"
sshexec(host: sshHost, username: sshUsername, keyfile: sshPrivateKey, command: latestGASymlinkCommand)
}
println "UPLOAD SUCCESSFUL - validate by visiting ${docUrl}"
}
}

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="aggregator"
<section xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="aggregator"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Aggregator</title>
@@ -622,4 +622,4 @@
the @MessageEndpoint is defined on the class, detected automatically
through classpath scanning.</para>
</section>
</chapter>
</section>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="bridge"
<section xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="bridge"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Messaging Bridge</title>
@@ -56,4 +56,4 @@
</note>
</section>
</chapter>
</section>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="chain"
<section xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="chain"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Message Handler Chain</title>
@@ -111,4 +111,4 @@ When light version of &lt;gateway&gt; element is defined in the chain SI will co
</para>
</section>
</chapter>
</section>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.4//EN"
"http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd">
<chapter id="channel-adapter">
<section id="channel-adapter">
<title>Channel Adapter</title>
<para>
A Channel Adapter is a Message Endpoint that enables connecting a single sender or receiver to a Message Channel.
@@ -75,4 +75,4 @@
</para>
</section>
</chapter>
</section>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="channel"
<section xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="channel"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Message Channels</title>
<para>
@@ -599,4 +599,4 @@ public Message<?> receive(final PollableChannel<?> channel) { ... }]]></programl
</note>
</section>
</chapter>
</section>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="delayer"
<section xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="delayer"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Delayer</title>
@@ -58,4 +58,4 @@
</para>
</section>
</chapter>
</section>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="endpoint"
<section xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="endpoint"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Message Endpoints</title>
<para>
@@ -346,4 +346,4 @@ any transaction configuration essentially allowing you to enhance the behavior o
to - <emphasis>Section 25 - Task Execution and Scheduling</emphasis> of Spring reference manual.
</para>
</section>
</chapter>
</section>

View File

@@ -1,62 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="applicationevent"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Spring ApplicationEvent Support</title>
<para>
Spring Integration provides support for inbound and outbound <classname>ApplicationEvents</classname>
as defined by the underlying Spring Framework. For more information about the events and listeners,
as defined by the underlying Spring Framework. For more information about Spring's support for events and listeners,
refer to the <ulink url="http://static.springsource.org/spring/docs/2.5.x/reference/beans.html#context-functionality-events">Spring Reference Manual</ulink>.
</para>
<section id="applicationevent-inbound">
<title>Receiving Spring ApplicationEvents</title>
<para>
To receive events and send them to a channel, simply define an instance of Spring Integration's
<classname>ApplicationEventListeningChannelAdapter</classname>. This class is an implementation of
Spring's <interfacename>ApplicationListener</interfacename> interface. By default it will pass all
received events as Spring Integration Messages. To limit based on the type of event, configure the
list of event types that you want to receive with the 'eventTypes' property.
</para>
<para>
For convenience namespace support was provided to configure <classname>ApplicationEventListeningChannelAdapter</classname> via <emphasis>inbound-channel-adapter</emphasis>
<programlisting language="xml"><![CDATA[<int-event:inbound-channel-adapter channel="input" event-types="foo.bar.FooApplicationEvent, foo.bar.BarApplicationEvent"/>
<int:publish-subscribe-channel id="sampleEventChannel"/>]]></programlisting>
In the above sample, all Application Context events that are of type specified by the 'event-types' (optional) attribute will be
delivered as Spring Integration Messages to 'sampleEventChannel'.
</para>
<para>
To receive events and send them to a channel, simply define an instance of Spring Integration's
<classname>ApplicationEventListeningMessageProducer</classname>. This class is an implementation of
Spring's <interfacename>ApplicationListener</interfacename> interface. By default it will pass all
received events as Spring Integration Messages. To limit based on the type of event, configure the
list of event types that you want to receive with the 'eventTypes' property. If a received event
has a Message instance as its 'source', then that will be passed as-is. Otherwise, if a SpEL-based
"payloadExpression" has been provided, that will be evaluated against the ApplicationEvent instance.
If the event's source is not a Message instance and no "payloadExpression" has been provided, then
the ApplicationEvent itself will be passed as the payload.
</para>
<para>
For convenience namespace support is provided to configure an <classname>ApplicationEventListeningMessageProducer</classname>
via the <emphasis>inbound-channel-adapter</emphasis> element.
<programlisting language="xml"><![CDATA[<int-event:inbound-channel-adapter channel="eventChannel" event-types="example.FooEvent, example.BarEvent"/>
<int:publish-subscribe-channel id="eventChannel"/>]]></programlisting>
In the above example, all Application Context events that match one of the types specified by the 'event-types' (optional) attribute will be
delivered as Spring Integration Messages to the Message Channel named 'eventChannel'.
</para>
</section>
<section id="applicationevent-outbound">
<title>Sending Spring ApplicationEvents</title>
<para>
To send Spring <classname>ApplicationEvents</classname>, create an instance of the
<classname>ApplicationEventPublishingMessageHandler</classname> and register it within an endpoint.
This implementation of the <interfacename>MessageHandler</interfacename> interface also implements
Spring's <interfacename>ApplicationEventPublisherAware</interfacename> interface and thus acts as a
bridge between Spring Integration Messages and <classname>ApplicationEvents</classname>.
</para>
<para>
For convenience namespace support was provided to configure <classname>ApplicationEventPublishingMessageHandler</classname> via <emphasis>outbound-channel-adapter</emphasis> element
<programlisting language="xml"><![CDATA[<int:channel id="input"/>
<title>Sending Spring ApplicationEvents</title>
<para>
To send Spring <classname>ApplicationEvents</classname>, create an instance of the
<classname>ApplicationEventPublishingMessageHandler</classname> and register it within an endpoint.
This implementation of the <interfacename>MessageHandler</interfacename> interface also implements
Spring's <interfacename>ApplicationEventPublisherAware</interfacename> interface and thus acts as a
bridge between Spring Integration Messages and <classname>ApplicationEvents</classname>.
</para>
<int-event:outbound-channel-adapter channel="input"/>]]></programlisting>
If you are using PollableChannel (e.g., Queue), you can also provide <emphasis>poller</emphasis> as sub-element of <emphasis>outbound-channel-adapter</emphasis>, optionally providing <emphasis>task-executor</emphasis>
<programlisting language="xml"><![CDATA[<int:channel id="input">
<para>
For convenience namespace support is provided to configure an <classname>ApplicationEventPublishingMessageHandler</classname>
via the <emphasis>outbound-channel-adapter</emphasis> element.
<programlisting language="xml"><![CDATA[<int:channel id="eventChannel"/>
<int-event:outbound-channel-adapter channel="eventChannel"/>]]></programlisting>
If you are using a PollableChannel (e.g., Queue), you can also provide <emphasis>poller</emphasis> as a sub-element of the
<emphasis>outbound-channel-adapter</emphasis> element. You can also optionally provide a <emphasis>task-executor</emphasis>
reference for that poller. The following example demonstrates both.
<programlisting language="xml"><![CDATA[<int:channel id="eventChannel">
<int:queue/>
</int:channel>
<int-event:outbound-channel-adapter channel="input">
<int-event:outbound-channel-adapter channel="eventChannel">
<int:poller max-messages-per-poll="1" task-executor="executor" fixed-rate="100"/>
</int-event:outbound-channel-adapter>
<task:executor id="executor" pool-size="5"/>]]></programlisting>
In the above sample, all messages sent to an 'input' channel will be published as ApplicationEvents to Spring Application sContext
</para>
In the above example, all messages sent to the 'eventChannel' channel will be published as ApplicationEvents to any relevant
ApplicationListener instances that are registered within the same Spring ApplicationContext. If the payload of the Message is
an ApplicationEvent, it will be passed as-is. Otherwise the Message itself will be wrapped in a MessagingEvent instance.
</para>
</section>
</chapter>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="feed"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Feed Adapter</title>
<para>
Spring Integration provides support for Feed (RSS, Atom)
</para>
<section id="feed-intro">
<title>Introduction</title>
<para>
TODO
</para>
</section>
</chapter>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="filter"
<section xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="filter"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Filter</title>
@@ -128,4 +128,4 @@
to be treated as Message Channel names by a router component.</note>
</para>
</section>
</chapter>
</section>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="ftp"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>FTP Adapter</title>
<para>
Spring Integration provides support for FTP
</para>
<section id="ftp-intro">
<title>Introduction</title>
<para>
TODO
</para>
</section>
</chapter>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="gateway"
<section xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="gateway"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Inbound Messaging Gateways</title>
@@ -251,4 +251,4 @@ For a more detailed example, please refer to the <emphasis>async-gateway</emphas
</para>
</section>
</chapter>
</section>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="groovy"
<section xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="groovy"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Groovy support</title>
<para>
@@ -69,4 +69,4 @@ You see that script could be included inline or via <code>location</code> attrib
</section>
</chapter>
</section>

View File

@@ -18,7 +18,7 @@
support the HTTP inbound adapters need to be deployed within a servlet container. The easiest way to do this is to provide a servlet
definition in <emphasis>web.xml</emphasis>, see
<xref linkend="httpinvoker-inbound"/> for further details. Below is an example bean definition for a simple HTTP inbound endpoint.
<programlisting language="xml"><![CDATA[<bean id="httpInbound" class="org.springframework.integration.http.HttpRequestHandlingMessagingGateway">
<programlisting language="xml"><![CDATA[<bean id="httpInbound" class="org.springframework.integration.http.inbound.HttpRequestHandlingMessagingGateway">
<property name="requestChannel" ref="httpRequestChannel" />
<property name="replyChannel" ref="httpReplyChannel" />
</bean>]]></programlisting>
@@ -51,7 +51,7 @@
configured to serve as a Spring MVC Controller with a view name. Because of the constructor arg value of TRUE, it wait for a reply. This also shows
how to customize the HTTP methods accepted by the gateway, which
are <emphasis>POST</emphasis> and <emphasis>GET</emphasis> by default.
<programlisting language="xml"><![CDATA[<bean id="httpInbound" class="org.springframework.integration.http.HttpRequestHandlingController">
<programlisting language="xml"><![CDATA[<bean id="httpInbound" class="org.springframework.integration.http.inbound.HttpRequestHandlingController">
<constructor-arg value="true" /> <!-- indicates that a reply is expected -->
<property name="requestChannel" ref="httpRequestChannel" />
<property name="replyChannel" ref="httpReplyChannel" />
@@ -75,14 +75,14 @@
<para>
To configure the <classname>HttpRequestExecutingMessageHandler</classname> write a bean definition like this:
<programlisting language="xml"><![CDATA[<bean id="httpOutbound" class="org.springframework.integration.http.HttpRequestExecutingMessageHandler" >
<programlisting language="xml"><![CDATA[<bean id="httpOutbound" class="org.springframework.integration.http.outbound.HttpRequestExecutingMessageHandler" >
<constructor-arg value="http://localhost:8080/example" />
<property name="outputChannel" ref="responseChannel" />
</bean>]]></programlisting>
This bean definition will execute HTTP requests by delegating to a <classname>RestTemplate</classname>. That template in turn delegates
to a list of HttpMessageConverters to generate the HTTP request body from the Message payload. You can configure those converters as well
as the ClientHttpRequestFactory instance to use:
<programlisting language="xml"><![CDATA[<bean id="httpOutbound" class="org.springframework.integration.http.HttpRequestExecutingMessageHandler" >
<programlisting language="xml"><![CDATA[<bean id="httpOutbound" class="org.springframework.integration.http.outbound.HttpRequestExecutingMessageHandler" >
<constructor-arg value="http://localhost:8080/example" />
<property name="outputChannel" ref="responseChannel" />
<property name="messageConverters" ref="messageConverterList" />
@@ -176,7 +176,7 @@ On the server side we have the following configuration:
<int:channel id="receiveChannel"/>
<int:service-activator input-channel="receiveChannel">
<bean class="org.springframework.integration.samples.multipart.MultipartReceiever"/>
<bean class="org.springframework.integration.samples.multipart.MultipartReceiver"/>
</int:service-activator>
<bean id="multipartResolver"
@@ -186,8 +186,8 @@ On the server side we have the following configuration:
<para>
The 'httpInboundAdapter' will receive the request, convert it to a <classname>Message</classname> with a payload as <classname>LinkedMultiValueMap</classname> which
we are parsing in the 'multipartReceiver' service-activator;
<programlisting language="java"><![CDATA[public void recieve(LinkedMultiValueMap<String, Object> multipartRequest){
System.out.println("### Successfully recieved multipart request ###");
<programlisting language="java"><![CDATA[public void receive(LinkedMultiValueMap<String, Object> multipartRequest){
System.out.println("### Successfully received multipart request ###");
for (String elementName : multipartRequest.keySet()) {
if (elementName.equals("company")){
System.out.println("\t" + elementName + " - " +
@@ -201,7 +201,7 @@ we are parsing in the 'multipartReceiver' service-activator;
]]></programlisting>
You should see the following output:
<programlisting language="xml"><![CDATA[### Successfully recieved multipart request ###
<programlisting language="xml"><![CDATA[### Successfully received multipart request ###
company - SpringSource
company-logo - as UploadedMultipartFile: spring09_logo.png]]></programlisting>
</para>

View File

@@ -55,44 +55,61 @@
</bookinfo>
<toc></toc>
<xi:include href="./overview.xml"/>
<xi:include href="./message.xml"/>
<xi:include href="./channel.xml"/>
<xi:include href="./endpoint.xml"/>
<xi:include href="./service-activator.xml"/>
<xi:include href="./channel-adapter.xml"/>
<xi:include href="./router.xml"/>
<xi:include href="./filter.xml"/>
<xi:include href="./transformer.xml"/>
<xi:include href="./splitter.xml"/>
<xi:include href="./aggregator.xml"/>
<xi:include href="./resequencer.xml"/>
<xi:include href="./delayer.xml"/>
<xi:include href="./chain.xml"/>
<xi:include href="./bridge.xml"/>
<xi:include href="./gateway.xml"/>
<xi:include href="./message-publishing.xml"/>
<xi:include href="./transactions.xml"/>
<xi:include href="./message-history.xml"/>
<xi:include href="./file.xml"/>
<xi:include href="./jdbc.xml"/>
<xi:include href="./jms.xml"/>
<xi:include href="./ws.xml"/>
<xi:include href="./rmi.xml"/>
<xi:include href="./httpinvoker.xml"/>
<xi:include href="./http.xml"/>
<xi:include href="./ip.xml"/>
<xi:include href="./mail.xml"/>
<xi:include href="./jmx.xml"/>
<xi:include href="./xmpp.xml"/>
<xi:include href="./stream.xml"/>
<xi:include href="./event.xml"/>
<xi:include href="./xml.xml"/>
<xi:include href="./security.xml"/>
<xi:include href="./groovy.xml"/>
<xi:include href="./samples.xml"/>
<xi:include href="./configuration.xml"/>
<xi:include href="./resources.xml"/>
<part id="spring-integration-introduction">
<title>Overview of Spring Integration Framework</title>
<partintro id="spring-integration-intro">
<para>The Spring Integration Framework is a lightweight solution TODO</para>
</partintro>
<xi:include href="./overview.xml"/>
</part>
<part id="spring-integration-core-messaging">
<title>Core Messaging</title>
<partintro id="spring-integration-core-msg">
<para>Core Messaging TODO</para>
</partintro>
<xi:include href="./messaging-channels.xml"/>
<xi:include href="./message-construction.xml"/>
<xi:include href="./message-routing.xml"/>
<xi:include href="./message-transformation.xml"/>
<xi:include href="./messaging-endpoints.xml"/>
<xi:include href="./system-management.xml"/>
</part>
<part id="spring-integration-adapters">
<title>Integration Adapters</title>
<partintro id="spring-integration-adapters">
<para>Adapters TODO</para>
</partintro>
<xi:include href="./event.xml"/>
<xi:include href="./feed.xml"/>
<xi:include href="./file.xml"/>
<xi:include href="./ftp.xml"/>
<xi:include href="./http.xml"/>
<xi:include href="./httpinvoker.xml"/>
<xi:include href="./mail.xml"/>
<xi:include href="./ip.xml"/>
<xi:include href="./jdbc.xml"/>
<xi:include href="./jms.xml"/>
<xi:include href="./rmi.xml"/>
<xi:include href="./sftp.xml"/>
<xi:include href="./stream.xml"/>
<xi:include href="./twitter.xml"/>
<xi:include href="./ws.xml"/>
<xi:include href="./xml.xml"/>
<xi:include href="./xmpp.xml"/>
</part>
<part id="spring-integration-appendices">
<title>Appendices</title>
<partintro id="spring-integration-adapters">
<para>Appendices TODO</para>
</partintro>
<xi:include href="./message-publishing.xml"/>
<xi:include href="./transactions.xml"/>
<xi:include href="./security.xml"/>
<xi:include href="./samples.xml"/>
<xi:include href="./configuration.xml"/>
<xi:include href="./resources.xml"/>
</part>
</book>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="jmx"
<section xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="jmx"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>JMX Support</title>
@@ -187,4 +187,4 @@
foundation is available, however, we will be able to extend the attributes
and operations that are being exposed.</para>
</section>
</chapter>
</section>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="messaging-construction-chapter"
xmlns:xi="http://www.w3.org/2001/XInclude"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Message Construction</title>
<xi:include href="./message.xml"/>
</chapter>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="message-history"
<section xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="message-history"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Message History</title>
<para>
@@ -61,4 +61,4 @@ assertEquals("sampleChain", chainHistory.get("name"));]]></programlisting>
be changed once written. Every attempt will end in exception.
</note>
</section>
</chapter>
</section>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="messaging-routing-chapter"
xmlns:xi="http://www.w3.org/2001/XInclude"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Message Routing</title>
<xi:include href="./router.xml"/>
<xi:include href="./filter.xml"/>
<xi:include href="./splitter.xml"/>
<xi:include href="./aggregator.xml"/>
<xi:include href="./resequencer.xml"/>
<xi:include href="./chain.xml"/>
</chapter>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="messaging-transformation-chapter"
xmlns:xi="http://www.w3.org/2001/XInclude"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Message Transformation</title>
<xi:include href="./transformer.xml"/>
</chapter>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="message"
<section xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="message"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Message Construction</title>
<title>Message</title>
<para>
The Spring Integration <interfacename>Message</interfacename> is a generic container for data. Any object can
be provided as the payload, and each <interfacename>Message</interfacename> also includes headers containing
@@ -219,4 +219,4 @@ assertEquals(MessagePriority.HIGHEST, anotherMessage.getHeaders().getPriority())
</para>
</section>
</chapter>
</section>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="messaging-channels-section"
xmlns:xi="http://www.w3.org/2001/XInclude"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Messaging Channels</title>
<xi:include href="./channel.xml"/>
<xi:include href="./channel-adapter.xml"/>
<xi:include href="./bridge.xml"/>
</chapter>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="messaging-endpoints-chapter"
xmlns:xi="http://www.w3.org/2001/XInclude"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Messaging Endpoints</title>
<xi:include href="./endpoint.xml"/>
<xi:include href="./gateway.xml"/>
<xi:include href="./service-activator.xml"/>
<xi:include href="./delayer.xml"/>
<xi:include href="./groovy.xml"/>
</chapter>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="resequencer"
<section xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="resequencer"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Resequencer</title>
@@ -123,4 +123,4 @@
Since there is no custom behavior to be implemented in Java classes for resequencers, there is no annotation support for it.
</note>
</section>
</chapter>
</section>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="router"
<section xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="router"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Router</title>
@@ -402,4 +402,4 @@ public List&lt;String&gt; route(@Header("orderStatus") OrderStatus status)</prog
</para>
</section>
</chapter>
</section>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="service-activator"
<section xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="service-activator"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Service Activator</title>
@@ -68,4 +68,4 @@
</note>
</section>
</chapter>
</section>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="Sftp"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>SFTP Adapter</title>
<para>
Spring Integration provides support for SFTP
</para>
<section id="sftp-intro">
<title>Introduction</title>
<para>
TODO
</para>
</section>
</chapter>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="splitter"
<section xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="splitter"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Splitter</title>
@@ -154,4 +154,4 @@ List&lt;LineItem&gt; extractItems(Order order) {
}</programlisting></para>
</section>
</chapter>
</section>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="system-management-chapter"
xmlns:xi="http://www.w3.org/2001/XInclude"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>System Management</title>
<xi:include href="./message-history.xml"/>
<xi:include href="./jmx.xml"/>
</chapter>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="transformer"
<section xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="transformer"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Transformer</title>
@@ -180,4 +180,4 @@ Order generateOrder(String productId, @Header("customerName") String customer) {
</para>
</section>
</chapter>
</section>

View File

@@ -0,0 +1,218 @@
<?xml version="1.0" encoding="UTF-8"?>
<chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="twitter"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Twitter Adapter</title>
<para>
Spring Integration provides support for interacting with Twitter via Twitter adapters. With Twitter adapters you can both
receive and send Twitter messages.
</para>
<section id="twitter-intro">
<title>Introduction</title>
<para>
Twitter is a social networking and microblogging service that enables its users to send and read messages known as tweets.
Tweets are text-based posts of up to 140 characters displayed on the author's profile page and delivered to the author's
subscribers who are known as followers.
</para>
<para>
<important>
Current Twitter support is based on <link linkend="http://twitter4j.org/en/index.html">Twitter4J API</link>,
however future version will be changed to use <link linkend="https://jira.springframework.org/browse/SOCIAL">Spring Social</link>
project as it is nearing its first release at the time of writing.
</important>
</para>
<para>
Spring Integration provides a convenient namespace configuration to define Twitter artifacts.
<programlisting language="xml"><![CDATA[xmlns:twitter="http://www.springframework.org/schema/integration/twitter"
xsi:schemaLocation="http://www.springframework.org/schema/integration/twitter
http://www.springframework.org/schema/integration/twitter/spring-integration-twitter-2.0.xsd"]]></programlisting>
</para>
</section>
<section id="twitter-oauth">
<title>Twitter Connection and OAuth Configuration</title>
<para>
Before using inbound or outbound Twitter adapters you must establish secured Twitter connection. This connection object
could be shared by all twitter adapters connected to a particular account.
</para>
<para>
Twitter uses OAuth - an authentication protocol that allows users to approve application to act on their behalf without
sharing their password. More information can be found at <link linkend="http://oauth.net/">http://oauth.net/</link> or
in this article <link linkend="http://hueniverse.com/oauth/">http://hueniverse.com/oauth/</link> from Hueniverse.
Please also see <link linkend="http://dev.twitter.com/pages/oauth_faq">OAuth FAQ</link> for more information about OAuth and Twitter.
</para>
<para>
In order to use OAuth authentication/authorization with Twitter you must create new Application on Twitter Developers site.
Follow the directions below to create a new application and obtain consumer keys and access token:
</para>
<para>
<itemizedlist>
<listitem>
<para>Go to <link linkend="http://dev.twitter.com/">http://dev.twitter.com/</link></para>
</listitem>
<listitem>
<para>Click on <code>Register an app</code> link and fill out all required fields on the form provided;
set <code>Application Type</code> to <code>Client</code> and depending on the nature of your application select
<code>Default Access Type</code> as <emphasis>Read &amp; Write</emphasis> or <emphasis>Read-only</emphasis>
and Submit the form. If everything is successful you'll be presented with the <code>Consumer Key</code>
and <code>Consumer Secret</code>. Copy both values in the safe place.</para>
</listitem>
<listitem>
<para>On the same page you should see <code>My Access Token</code> button on the side bar (right).
Click on it and you'll be presented with two more values: <code>Access Token</code> and <code>Access Token Secret</code>.
Copy these values in a safe place as well.</para>
</listitem>
</itemizedlist>
</para>
<para>
<emphasis>Twitter Connection</emphasis>
</para>
<para>
Spring Integration provides a convenient namespace configuration to define Twitter Connection. As you can see form the
configuration below you configure Twitter connection via <code>twitter-connection</code> element while providing
OAuth <code>attributes</code> filling them with values you have obtained in the previous step.
<programlisting language="xml"><![CDATA[<twitter:twitter-connection id="tc"
access-token="21691649-4XYZY5iJEOfz2A9qCFd9SjBRGb3HLmIm4HNE6AMv4"
access-token-secret="AbRxUAvyNCtqQtvxFK8w5ZMtMj20KFhB6oEfTA0"
consumer-key="4XzBPabcJQxyBzzzH3TrRQ"
consumer-secret="ab2piKdMfPu8bVa3ab6DAIvIWEVZyMDL0RSEN2I8"/>]]></programlisting>
<note>The values above are not real</note>
</para>
<para>
However a more practical way to manage OAuth connection attributes would be via Spring's placeholder support by simply
creating a property file (e.g., oauth.properties):
<programlisting language="java"><![CDATA[twitter.oauth.consumerKey=4XzBPabcJQxyBzzzH3TrRQ
twitter.oauth.consumerSecret=ab2piKdMfPu8bVa3ab6DAIvIWEVZyMDL0RSEN2I8
twitter.oauth.accessToken=21691649-4XYZY5iJEOfz2A9qCFd9SjBRGb3HLmIm4HNE6AMv4
twitter.oauth.accessTokenSecret=AbRxUAvyNCtqQtvxFK8w5ZMtMj20KFhB6oEfTA0]]></programlisting>
and configuring a <code>property-placeholder</code> pointing to he above property file:
<programlisting language="java"><![CDATA[<context:property-placeholder
location="classpath:oauth.properties"/>
<twitter:twitter-connection id="tc"
access-token="${twitter.oauth.accessToken}"
access-token-secret="${twitter.oauth.accessTokenSecret}"
consumer-key="${twitter.oauth.consumerKey}"
consumer-secret="${twitter.oauth.consumerSecret}"/>]]></programlisting>
</para>
</section>
<section id="twitter-inbound">
<title>Twitter Inbound Adapters</title>
<para>
Twitter inbound adapters allow you to receive Twitter Messages. There are several types of
<link linkend="http://support.twitter.com/groups/31-twitter-basics/topics/109-tweets-messages/articles/119138-types-of-tweets-and-where-they-appear">twitter messages - tweets</link>
</para>
<para>
Current release of Spring Integration provides support for receiving <emphasis>Public Messages</emphasis>,
<emphasis>Direct Messages</emphasis> as well as <emphasis>Mention Messages</emphasis>
</para>
<para>
Every Inbound Twitter Channel Adapter is a <emphasis>Polling consumer</emphasis> which means you have to provide a poller
configuration. However, one important thing you must understand with regard to Twitter since its inner-workings are slightly
different then any other poling consumer. Twitter defines a concept of Rate Limiting. You can read more about
it here: <link linkend="http://dev.twitter.com/pages/rate-limiting">Rate Limiting</link> . In the nutshell Rate Limiting
is the way Twitter manages how often an application can poll for updates. Luckily for you you don't have to worry about it
since the special Rate limit aware polling thread is created when any Twitter adapter is started. This thread will
poll Messages (Tweets) from the Twitter account at the rate allowed by Twitter at the time (it may change after every poll). The Tweets
will be stored in the instance of the <classname>org.springframework.integration.store.MetadataStore</classname> which is a
strategy interface designed for storing various types of metadata (e.g., last retrieved tweet) to help components such as Twitter
to deal with duplicates. By default, Spring Integration will look for a bean of type <classname>org.springframework.integration.store.MetadataStore</classname>
in the ApplicationContext. If one found then it will be used, otherwise it will create a new instance of <classname>SimpleMetadataStore</classname>
which is a simple in-memory implementation that will only persist meta-data within the life-cycle of the application context
which means upon restart you may end up with duplicate entries. If you need to persist meta-data between Application Context
restarts, you may use <classname>PropertiesPersistingMetadataStore</classname> (property file based persister) or provide your
own implementation of the <classname>MetedataStore</classname> interface (e.g., JdbcMetadatStore) and configure it
as bean in the Application Context.
<programlisting language="java"><![CDATA[<bean class="org.springframework.integration.store.PropertiesPersistingMetadataStore"/>
]]></programlisting>
The Poller that is configured as part of the any Inbound Twitter Adapter (see below) will simply poll from this MetadataStore
</para>
<section id="inbound-twitter-update">
<title>Inbound Update Channel Adapter</title>
<para>
This adapter allows you to receive updates from everyone you follow.
<programlisting language="java"><![CDATA[<twitter:inbound-mention-channel-adapter twitter-connection="tc" channel="inbound_mentions">
<poller fixed-rate="5000" max-messages-per-poll="3"/>
</twitter:inbound-mention-channel-adapter>]]></programlisting>
</para>
</section>
<section id="inbound-twitter-direct">
<title>Inbound Direct Message Channel Adapter</title>
<para>
This adapter allows you to receive Twitter Messages that were sent directly to you
<programlisting language="java"><![CDATA[<twitter:inbound-dm-channel-adapter twitter-connection="tc" channel="inbound_dm">
<poller fixed-rate="5000" max-messages-per-poll="3"/>
</twitter:inbound-dm-channel-adapter>]]></programlisting>
</para>
</section>
<section id="inbound-twitter-mention">
<title>Inbound Mention Message Channel Adapter</title>
<para>
This adapter allows you to receive Twitter Messages that Mention you via @user
<programlisting language="java"><![CDATA[<twitter:inbound-mention-channel-adapter twitter-connection="tc" channel="inbound_mentions">
<poller fixed-rate="5000" max-messages-per-poll="3"/>
</twitter:inbound-mention-channel-adapter>]]></programlisting>
</para>
</section>
<para>
As you can see the configuration of all of these adapters is very similar to other inbound adapters with one exception.
Each one needs to be injected with the <code>twitter-connection</code>. Once configured the Twitter Messages would be
encapsulated into a Spring Integration Message and sent to a channel specified via <code>channel</code> attribute.
Currently the Payload of the Message is <classname>twitter4j.DirectMessage</classname> for <emphasis>Inbound Direct Message Channel Adapter</emphasis>
or <classname>twitter4j.Status</classname> for <emphasis>Inbound Update Channel Adapter</emphasis> and
<emphasis>Inbound Mention Message Channel Adapter</emphasis>.
</para>
<para>
For example; to get the text from the <classname>twitter4j.DirectMessage</classname> or <classname>twitter4j.Status</classname>
simply invoke <code>getText()</code> method. For more information please refer to <link linkend="http://twitter4j.org/en/index.html">Twitter4J API</link>
</para>
</section>
<section id="twitter-outbound">
<title>Twitter Outbound Adapter</title>
<para>
Twitter outbound channels adapters allow you to send Twitter Messages - tweets
</para>
<para>
Current release of Spring Integration supports sending <emphasis>Status Update Messages</emphasis> and <emphasis>Direct Messages</emphasis>.
Twitter outbound channels adapters as any other outbound adapter will take the Message payload and send it as
Twitter message. Currently the only supported payload type is <classname>String</classname>, so consider adding a <emphasis>transformer</emphasis>
if the payload of the incoming message is not a String.
</para>
<section id="outbound-twitter-update">
<title>Twitter Outbound Update Channel Adapter</title>
<para>
This adapter allows you to send regular status updates by simply sending a Message to a channel
identified via <code>channel</code> attribute.
<programlisting language="java"><![CDATA[<twitter:outbound-update-channel-adapter twitter-connection="tc" channel="out"/>]]></programlisting>
The only extra configuration that is required for this adapter is <code>twitter-connection</code>
</para>
</section>
<section id="outbound-twitter-direct">
<title>Twitter Outbound Direct Message Channel Adapter</title>
<para>
This adapter allows you to send Direct Twitter Messages (i.e., @user) by simply sending a Message to a channel
identified via <code>channel</code> attribute.
<programlisting language="java"><![CDATA[<twitter:outbound-dm-channel-adapter twitter-connection="tc" channel="inputChannel"/>]]></programlisting>
The only extra configuration that is required for this adapter is <code>twitter-connection</code>
</para>
</section>
<para>
<important>Twitter does not allow you to post duplicate Messages. This is a common problem during testing when
the same code works the first time but doesn't work the second time,so make sure to change the content of the Message.
One thing that works good for testing is appent timestamp to the end of the message.
</important>
</para>
</section>
</chapter>

View File

@@ -123,6 +123,7 @@ uploadArchives {
configurePom(deployer.pom)
}
/**
* Install gradle-built artifacts to the local m2 maven cache.
* Further customizes the 'install' task contributed by the 'maven' plugin.
@@ -134,6 +135,105 @@ install {
configurePom(repositories.mavenInstaller.pom)
}
/**
* Generate a Maven pom.xml for use at build time. Dependency information will
* be based on Gradle metadata for the project, and other customizations such
* as licensing information and source compatibility settings are configured
* within.
*
* @author Chris Beams
* @see http://gradle.org/0.9-preview-3/docs/userguide/userguide_single.html#pomBuilder
*/
task generatePom {
group = 'Build'
description = 'Generates a Maven POM file suitable for use in building the project'
generatedPomFileName = "pom.xml"
// enable partial cleaning with `gradle cleanGeneratePom`
outputs.files(generatedPomFileName)
doLast() {
// customize the pom creation process
p = pom {
project {
name = project.description
properties {
setProperty('project.build.sourceEncoding', 'UTF8')
}
build {
plugins {
plugin {
groupId = 'org.apache.maven.plugins'
artifactId = 'maven-compiler-plugin'
configuration {
source = '1.5'
target = '1.5'
}
}
plugin {
groupId = 'org.apache.maven.plugins'
artifactId = 'maven-surefire-plugin'
configuration {
includes {
include = '**/*Tests.java'
}
excludes {
exclude = '**/*Abstract*.java'
}
}
}
}
resources {
resource {
directory = 'src/main/java'
includes = ['**/*']
excludes = ['**/*.java']
}
resource {
directory = 'src/main/resources'
includes = ['**/*']
}
}
testResources {
testResource {
directory = 'src/test/java'
includes = ['**/*']
excludes = ['**/*.java']
}
testResource {
directory = 'src/test/resources'
includes = ['**/*']
}
}
}
}
}
// customizing the artifact id is a special case that must be configured
// after the pom is fully configured, otherwise it'll be overwritten
p.whenConfigured { pom -> pom.artifactId = project.name }
configurePom(p)
// write the pom.xml file out to the filesystem
p.writeTo(generatedPomFileName)
}
// ensure that pom generation happens every time resources are processed
// (which practically means any time a build happens). if the dependencies
// for the project have been updated (in $rootDir/build.gradle), the pom
// will have diffs in it and the developer will be reminded to check in
// the change during the next commit cycle.
//processResources.dependsOn generatePom
}
/**
* Read dynamic 'optional' and 'provided' properties from gradle dependencies
* and translate them to their maven POM equivalents.
*/
def configurePom(def pom) {
pom.whenConfigured { generatedPom ->
def optionalDeps = configurations.testRuntime.allDependencies.findAll { gradleDep ->

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2002-2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Generate a root Maven pom.xml for use at build time. Contains nothing other
* than a 'modules' section aggregating child projects. This pom will never be
* installed locally or deployed remotely. Child projects will not explicitly
* declare this pom as their parent, given than nothing need be inherited from
* it.
*
* @author Chris Beams
* @see maven-deployment.gradle for per-project generatePom task
*/
task generatePom {
apply plugin: 'maven'
group = 'Build'
description = 'Generates a root Maven pom for convenience.'
generatedPomFileName = "pom.xml"
// enable partial cleaning with `gradle cleanGeneratePom`
outputs.files(generatedPomFileName)
doLast() {
// customize the pom creation process
p = pom {
project {
name = project.description
packaging = 'pom'
modules = javaprojects.collect { project -> project.name }
}
}
// customizing the artifact id is a special case that must be configured
// after the pom is fully configured, otherwise it'll be overwritten
p.whenConfigured { pom -> pom.artifactId = project.name }
// write the pom.xml file out to the filesystem
p.writeTo(generatedPomFileName)
}
// ensure that pom generation happens every time resources are processed
// (which practically means any time a build happens). if the dependencies
// for the project have been updated (in $rootDir/build.gradle), the pom
// will have diffs in it and the developer will be reminded to check in
// the change during the next commit cycle.
//processResources.dependsOn generatePom
}

View File

@@ -1,11 +1,31 @@
TODO: complete
============================== Spring Integration =============================
To check out the project and build from source, do the following:
project distribution zip file can be built by executing
git clone git://git.springsource.org/spring-integration/spring-integration.git
cd spring-integration
./gradlew build
src/main/scripts/build-distribution.sh
-------------------------------------------------------------------------------
To generate Eclipse metadata (.classpath and .project files), do the following:
./gradlew eclipse
licensing, changelog and other project information can be found under
Once complete, you may then import the projects into Eclipse as usual:
src/main/resources
File -> Import -> Existing projects into workspace
Browse to the 'spring-integration' root directory. All projects should import
free of errors.
-------------------------------------------------------------------------------
To generate IDEA metadata (.iml and .ipr files), do the following:
./gradlew idea
-------------------------------------------------------------------------------
To build the JavaDoc, do the following from within the root directory:
./gradlew :docs:api
The result will be available in 'docs/build/api'.
===============================================================================

View File

@@ -14,6 +14,8 @@
* limitations under the License.
*/
rootProject.name = 'spring-integration'
include 'docs'
include 'spring-integration-core'
include 'spring-integration-event'

View File

@@ -33,7 +33,20 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* The headers for a {@link Message}.
* The headers for a {@link Message}.<br>
* IMPORTANT: MessageHeaders are immutable. Any mutating operation (e.g., put(..), putAll(..) etc.)
* will result in {@link UnsupportedOperationException}
* To create MessageHeaders instance use fluent MessageBuilder API
* <pre>
* MessageBuilder.withPayload("foo").setHeader("key1", "value1").setHeader("key2", "value2");
* </pre>
* or create an instance of GenericMessage passing payload as {@link Object} and headers as a regular {@link Map}
* <pre>
* Map headers = new HashMap();
* headers.put("key1", "value1");
* headers.put("key2", "value2");
* new GenericMessage("foo", headers);
* </pre>
*
* @author Arjen Poutsma
* @author Mark Fisher
@@ -192,19 +205,27 @@ public final class MessageHeaders implements Map<String, Object>, Serializable {
/*
* Unsupported operations
*/
/**
* Since MessageHeaders are immutable the call to this method will result in {@link UnsupportedOperationException}
*/
public Object put(String key, Object value) {
throw new UnsupportedOperationException("MessageHeaders is immutable.");
}
/**
* Since MessageHeaders are immutable the call to this method will result in {@link UnsupportedOperationException}
*/
public void putAll(Map<? extends String, ? extends Object> t) {
throw new UnsupportedOperationException("MessageHeaders is immutable.");
}
/**
* Since MessageHeaders are immutable the call to this method will result in {@link UnsupportedOperationException}
*/
public Object remove(Object key) {
throw new UnsupportedOperationException("MessageHeaders is immutable.");
}
/**
* Since MessageHeaders are immutable the call to this method will result in {@link UnsupportedOperationException}
*/
public void clear() {
throw new UnsupportedOperationException("MessageHeaders is immutable.");
}

View File

@@ -57,6 +57,22 @@ public abstract class MessageProducerSupport extends AbstractEndpoint implements
Assert.notNull(this.outputChannel, "outputChannel is required");
}
/**
* Takes no action by default. Subclasses may override this if they
* need lifecycle-managed behavior.
*/
@Override
protected void doStart() {
}
/**
* Takes no action by default. Subclasses may override this if they
* need lifecycle-managed behavior.
*/
@Override
protected void doStop() {
}
protected void sendMessage(Message<?> message) {
if (message == null) {
throw new MessagingException("cannot send a null message");

View File

@@ -31,11 +31,13 @@ public abstract class AbstractScriptExecutingMessageProcessor<T> implements Mess
public final T processMessage(Message<?> message) {
try {
return this.executeScript(getScriptSource(message), message);
} catch (Exception e) {
}
catch (Exception e) {
throw new MessageHandlingException(message, "failed to execute script", e);
}
}
/**
* Subclasses must implement this method to create a script source, optionally using the message to locate or
* create the script.

View File

@@ -20,6 +20,7 @@ import java.util.Map;
/**
* A message implementation that accepts a {@link Throwable} payload.
* Once created this object is immutable.
*
* @author Mark Fisher
* @author Oleg Zhurakousky

View File

@@ -27,6 +27,7 @@ import org.springframework.util.ObjectUtils;
/**
* Base Message class defining common properties such as id, payload, and headers.
* Once created this object is immutable.
*
* @author Mark Fisher
*/

View File

@@ -16,23 +16,25 @@
package org.springframework.integration.event.config;
import org.w3c.dom.Element;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.xml.AbstractChannelAdapterParser;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
import org.springframework.integration.event.ApplicationEventInboundChannelAdapter;
import org.w3c.dom.Element;
/**
* @author Oleg Zhurakousky
* @author Mark Fisher
* @since 2.0
*/
public class EventInboundChannelAdapterParser extends AbstractChannelAdapterParser{
public class EventInboundChannelAdapterParser extends AbstractChannelAdapterParser {
@Override
protected AbstractBeanDefinition doParse(Element element, ParserContext parserContext, String channelName) {
BeanDefinitionBuilder adapterBuilder = BeanDefinitionBuilder.rootBeanDefinition(ApplicationEventInboundChannelAdapter.class);
BeanDefinitionBuilder adapterBuilder = BeanDefinitionBuilder.rootBeanDefinition(
"org.springframework.integration.event.inbound.ApplicationEventListeningMessageProducer");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(adapterBuilder, element, "channel", "outputChannel");
IntegrationNamespaceUtils.setValueIfAttributeDefined(adapterBuilder, element, "event-types");
IntegrationNamespaceUtils.setValueIfAttributeDefined(adapterBuilder, element, "payload-expression");

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2009 the original author or authors.
* Copyright 2002-2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -13,44 +13,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.event.config;
import org.w3c.dom.Element;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.xml.AbstractOutboundChannelAdapterParser;
import org.springframework.integration.event.ApplicationEventPublishingMessageHandler;
import org.w3c.dom.Element;
/**
* @author Oleg Zhurakousky
* @since 2.0
*/
public class EventOutboundChannelAdapterParser extends AbstractOutboundChannelAdapterParser{
@Override
protected AbstractBeanDefinition parseConsumer(Element element,
ParserContext parserContext) {
BeanDefinitionBuilder invokerBuilder = BeanDefinitionBuilder.genericBeanDefinition(ApplicationEventPublishingMessageHandler.class);
// BeanComponentDefinition innerHandlerDefinition =
// IntegrationNamespaceUtils.parseInnerHandlerDefinition(element, parserContext);
// if (innerHandlerDefinition == null){
// Assert.hasText(element.getAttribute(IntegrationNamespaceUtils.REF_ATTRIBUTE),
// "You must provide 'ref' attribute or register inner bean for " +
// "Outbound Channel consumer.");
// invokerBuilder.addConstructorArgReference(element.getAttribute(IntegrationNamespaceUtils.REF_ATTRIBUTE));
// } else {
// invokerBuilder.addConstructorArgValue(innerHandlerDefinition);
// }
// invokerBuilder.addConstructorArgValue(element.getAttribute(IntegrationNamespaceUtils.METHOD_ATTRIBUTE));
// String order = element.getAttribute(IntegrationNamespaceUtils.ORDER);
// if (StringUtils.hasText(order)) {
// invokerBuilder.addPropertyValue(IntegrationNamespaceUtils.ORDER, order);
// }
return invokerBuilder.getBeanDefinition();
protected AbstractBeanDefinition parseConsumer(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(
"org.springframework.integration.event.outbound.ApplicationEventPublishingMessageHandler");
return builder.getBeanDefinition();
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.integration.event;
package org.springframework.integration.event.core;
import org.springframework.context.ApplicationEvent;
import org.springframework.integration.Message;

View File

@@ -14,15 +14,17 @@
* limitations under the License.
*/
package org.springframework.integration.event;
package org.springframework.integration.event.inbound;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ApplicationContextEvent;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.integration.Message;
import org.springframework.integration.endpoint.MessageProducerSupport;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.util.Assert;
@@ -31,16 +33,18 @@ import org.springframework.util.CollectionUtils;
/**
* An inbound Channel Adapter that passes Spring {@link ApplicationEvent ApplicationEvents} within messages.
* If a {@link #setPayloadExpression(String) payloadExpression} is provided, it will be evaluated against
* the ApplicationEvent instance to create the Message payload.
* the ApplicationEvent instance to create the Message payload. Otherwise, the event itself will be the payload.
*
* @author Mark Fisher
*/
public class ApplicationEventInboundChannelAdapter extends MessageProducerSupport implements ApplicationListener<ApplicationEvent> {
public class ApplicationEventListeningMessageProducer extends MessageProducerSupport implements ApplicationListener<ApplicationEvent> {
private final Set<Class<? extends ApplicationEvent>> eventTypes = new CopyOnWriteArraySet<Class<? extends ApplicationEvent>>();
private volatile Expression payloadExpression;
private volatile boolean active;
private final SpelExpressionParser parser = new SpelExpressionParser();
@@ -77,29 +81,38 @@ public class ApplicationEventInboundChannelAdapter extends MessageProducerSuppor
}
public void onApplicationEvent(ApplicationEvent event) {
if (CollectionUtils.isEmpty(this.eventTypes)) {
this.sendEventAsMessage(event);
return;
}
for (Class<? extends ApplicationEvent> eventType : this.eventTypes) {
if (eventType.isAssignableFrom(event.getClass())) {
if (this.active || event instanceof ApplicationContextEvent) {
if (CollectionUtils.isEmpty(this.eventTypes)) {
this.sendEventAsMessage(event);
return;
}
for (Class<? extends ApplicationEvent> eventType : this.eventTypes) {
if (eventType.isAssignableFrom(event.getClass())) {
this.sendEventAsMessage(event);
return;
}
}
}
}
private void sendEventAsMessage(ApplicationEvent event) {
Object payload = (this.payloadExpression != null) ? this.payloadExpression.getValue(event) : event;
this.sendMessage(MessageBuilder.withPayload(payload).build());
}
@Override
protected void doStart() {
this.active = true;
}
@Override
protected void doStop() {
this.active = false;
}
private void sendEventAsMessage(ApplicationEvent event) {
if (event.getSource() instanceof Message<?>) {
this.sendMessage((Message<?>) event.getSource());
}
else {
Object payload = (this.payloadExpression != null) ? this.payloadExpression.getValue(event) : event;
this.sendMessage(MessageBuilder.withPayload(payload).build());
}
}
}

View File

@@ -14,12 +14,13 @@
* limitations under the License.
*/
package org.springframework.integration.event;
package org.springframework.integration.event.outbound;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.integration.Message;
import org.springframework.integration.event.core.MessagingEvent;
import org.springframework.integration.handler.AbstractMessageHandler;
import org.springframework.util.Assert;
@@ -31,7 +32,7 @@ import org.springframework.util.Assert;
*
* @author Mark Fisher
*/
public class ApplicationEventPublishingMessageHandler<T> extends AbstractMessageHandler implements ApplicationEventPublisherAware {
public class ApplicationEventPublishingMessageHandler extends AbstractMessageHandler implements ApplicationEventPublisherAware {
private ApplicationEventPublisher applicationEventPublisher;
@@ -43,7 +44,12 @@ public class ApplicationEventPublishingMessageHandler<T> extends AbstractMessage
@Override
protected void handleMessageInternal(Message<?> message) {
Assert.notNull(this.applicationEventPublisher, "applicationEventPublisher is required");
this.applicationEventPublisher.publishEvent(new MessagingEvent(message));
if (message.getPayload() instanceof ApplicationEvent) {
this.applicationEventPublisher.publishEvent((ApplicationEvent) message.getPayload());
}
else {
this.applicationEventPublisher.publishEvent(new MessagingEvent(message));
}
}
}

View File

@@ -1,104 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.springframework.org/schema/integration/event"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:tool="http://www.springframework.org/schema/tool"
xmlns:integration="http://www.springframework.org/schema/integration"
targetNamespace="http://www.springframework.org/schema/integration/event"
elementFormDefault="qualified"
attributeFormDefault="unqualified">
xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:tool="http://www.springframework.org/schema/tool"
xmlns:integration="http://www.springframework.org/schema/integration"
targetNamespace="http://www.springframework.org/schema/integration/event"
elementFormDefault="qualified" attributeFormDefault="unqualified">
<xsd:import namespace="http://www.springframework.org/schema/beans"/>
<xsd:import namespace="http://www.springframework.org/schema/integration"
schemaLocation="http://www.springframework.org/schema/integration/spring-integration-2.0.xsd"/>
<xsd:import namespace="http://www.springframework.org/schema/beans" />
<xsd:import namespace="http://www.springframework.org/schema/integration"
schemaLocation="http://www.springframework.org/schema/integration/spring-integration-2.0.xsd" />
<xsd:annotation>
<xsd:documentation><![CDATA[
Defines the configuration elements for Spring Integration Event Adapters.
<xsd:annotation>
<xsd:documentation><![CDATA[
Defines the configuration elements for Spring Integration Event Adapters.
]]></xsd:documentation>
</xsd:annotation>
</xsd:annotation>
<xsd:element name="inbound-channel-adapter">
<xsd:annotation>
<xsd:documentation>
Configures an inbound Channel Adapter which listens for an Application Context events, converts them to
Messages and sends them to a 'channel'
</xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:attribute name="id" type="xsd:ID" use="optional" />
<xsd:attribute name="channel" type="xsd:string" use="required">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.integration.core.MessageChannel"/>
</tool:annotation>
</xsd:appinfo>
<xsd:documentation>
Identifies inbound 'channel' which accepts Messages generated from Application Context events.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="event-types" type="xsd:string" use="optional">
<xsd:annotation>
<xsd:documentation>
Comma delimited list of event types (classes that extend ApplicationEvent) that
this adapter should send to the message channel. By default, all event
types will be sent [OPTIONAL]
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="payload-expression">
<xsd:annotation>
<xsd:documentation><![CDATA[
SpEL expression to be evaluated against the ApplicationEvent to create the payload instance.
If not provided, the ApplicationEvent itself will be used as the payload.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
</xsd:element>
<xsd:element name="outbound-channel-adapter">
<xsd:element name="inbound-channel-adapter">
<xsd:annotation>
<xsd:documentation>
Defines a Channel Adapter that receives from a MessageChannel and passes to
a method-invoking
MessageHandler.
Configures an inbound Channel Adapter which listens for Application Context
events, converts them to Messages and sends them to a Message Channel.
</xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:attribute name="id" type="xsd:ID" use="optional" />
<xsd:attribute name="channel" type="xsd:string" use="required">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type
type="org.springframework.integration.core.MessageChannel" />
</tool:annotation>
</xsd:appinfo>
<xsd:documentation>
The channel to which Messages generated from Application Context events will be sent.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="event-types" type="xsd:string" use="optional">
<xsd:annotation>
<xsd:documentation>
Comma delimited list of event types (classes that extend ApplicationEvent) that this adapter
should send to the message channel. By default, all event types will be sent [OPTIONAL]
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="payload-expression">
<xsd:annotation>
<xsd:documentation><![CDATA[
SpEL expression to be evaluated against the ApplicationEvent to create the payload instance.
If not provided, the ApplicationEvent itself will be the payload.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="auto-startup" type="xsd:string" default="true" />
</xsd:complexType>
</xsd:element>
<xsd:element name="outbound-channel-adapter">
<xsd:annotation>
<xsd:documentation>
Defines a Channel Adapter that receives Messages from a MessageChannel and then publishes
MessagingEvents containing those Messages.
</xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="channelAdapterType">
<xsd:attribute name="order">
<xsd:annotation>
<xsd:documentation>
Specifies the order for invocation when this endpoint is connected as a
subscriber to a
SubscribableChannel.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:extension>
</xsd:complexContent>
<xsd:all>
<xsd:element ref="integration:poller" minOccurs="0" maxOccurs="1" />
</xsd:all>
<xsd:attribute name="id" type="xsd:ID" />
<xsd:attribute name="channel" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type
type="org.springframework.integration.core.MessageChannel" />
</tool:annotation>
<xsd:documentation>
The Message Channel from which this adapter receives Messages.
</xsd:documentation>
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="order">
<xsd:annotation>
<xsd:documentation>
Specifies the order for invocation when this endpoint is connected as a
subscriber to a SubscribableChannel.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="auto-startup" type="xsd:string" default="true" />
</xsd:complexType>
</xsd:element>
<xsd:complexType name="channelAdapterType">
<xsd:all>
<xsd:element ref="integration:poller" minOccurs="0" maxOccurs="1" />
</xsd:all>
<xsd:attribute name="id" type="xsd:ID" />
<xsd:attribute name="channel" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.integration.core.MessageChannel" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="auto-startup" type="xsd:string" default="true" />
</xsd:complexType>
</xsd:schema>

View File

@@ -36,7 +36,7 @@ import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.expression.Expression;
import org.springframework.integration.Message;
import org.springframework.integration.core.PollableChannel;
import org.springframework.integration.event.ApplicationEventInboundChannelAdapter;
import org.springframework.integration.event.inbound.ApplicationEventListeningMessageProducer;
import org.springframework.integration.history.MessageHistory;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.test.context.ContextConfiguration;
@@ -59,7 +59,7 @@ public class EventInboundChannelAdapterParserTests {
public void validateEventParser() {
Object adapter = context.getBean("eventAdapterSimple");
Assert.assertNotNull(adapter);
Assert.assertTrue(adapter instanceof ApplicationEventInboundChannelAdapter);
Assert.assertTrue(adapter instanceof ApplicationEventListeningMessageProducer);
DirectFieldAccessor adapterAccessor = new DirectFieldAccessor(adapter);
Assert.assertEquals(context.getBean("input"), adapterAccessor.getPropertyValue("outputChannel"));
}
@@ -69,7 +69,7 @@ public class EventInboundChannelAdapterParserTests {
public void validateEventParserWithEventTypes() {
Object adapter = context.getBean("eventAdapterFiltered");
Assert.assertNotNull(adapter);
Assert.assertTrue(adapter instanceof ApplicationEventInboundChannelAdapter);
Assert.assertTrue(adapter instanceof ApplicationEventListeningMessageProducer);
DirectFieldAccessor adapterAccessor = new DirectFieldAccessor(adapter);
Assert.assertEquals(context.getBean("inputFiltered"), adapterAccessor.getPropertyValue("outputChannel"));
Set<Class<? extends ApplicationEvent>> eventTypes = (Set<Class<? extends ApplicationEvent>>) adapterAccessor.getPropertyValue("eventTypes");
@@ -84,7 +84,7 @@ public class EventInboundChannelAdapterParserTests {
public void validateEventParserWithEventTypesAndPlaceholder() {
Object adapter = context.getBean("eventAdapterFilteredPlaceHolder");
Assert.assertNotNull(adapter);
Assert.assertTrue(adapter instanceof ApplicationEventInboundChannelAdapter);
Assert.assertTrue(adapter instanceof ApplicationEventListeningMessageProducer);
DirectFieldAccessor adapterAccessor = new DirectFieldAccessor(adapter);
Assert.assertEquals(context.getBean("inputFilteredPlaceHolder"), adapterAccessor.getPropertyValue("outputChannel"));
Set<Class<? extends ApplicationEvent>> eventTypes = (Set<Class<? extends ApplicationEvent>>) adapterAccessor.getPropertyValue("eventTypes");
@@ -113,7 +113,7 @@ public class EventInboundChannelAdapterParserTests {
public void validatePayloadExpression() {
Object adapter = context.getBean("eventAdapterSpel");
Assert.assertNotNull(adapter);
Assert.assertTrue(adapter instanceof ApplicationEventInboundChannelAdapter);
Assert.assertTrue(adapter instanceof ApplicationEventListeningMessageProducer);
DirectFieldAccessor adapterAccessor = new DirectFieldAccessor(adapter);
Expression expression = (Expression) adapterAccessor.getPropertyValue("payloadExpression");
Assert.assertEquals("source + '-test'", expression.getExpressionString());

View File

@@ -16,6 +16,7 @@
package org.springframework.integration.event.config;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import junit.framework.Assert;
@@ -34,7 +35,7 @@ import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.core.MessageHandler;
import org.springframework.integration.endpoint.EventDrivenConsumer;
import org.springframework.integration.event.ApplicationEventPublishingMessageHandler;
import org.springframework.integration.event.outbound.ApplicationEventPublishingMessageHandler;
import org.springframework.integration.message.GenericMessage;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -46,13 +47,15 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class EventOutboundChannelAdapterParserTests {
@Autowired
private ConfigurableApplicationContext context;
private boolean recievedEvent;
private volatile ConfigurableApplicationContext context;
private volatile boolean receivedEvent;
@Test
public void validateEventParser(){
public void validateEventParser() {
EventDrivenConsumer adapter = context.getBean("eventAdapter", EventDrivenConsumer.class);
Assert.assertNotNull(adapter);
DirectFieldAccessor adapterAccessor = new DirectFieldAccessor(adapter);
@@ -60,16 +63,16 @@ public class EventOutboundChannelAdapterParserTests {
Assert.assertTrue(handler instanceof ApplicationEventPublishingMessageHandler);
Assert.assertEquals(context.getBean("input"), adapterAccessor.getPropertyValue("inputChannel"));
}
@Test
public void validateUsage(){
ApplicationListener listener = new ApplicationListener<ApplicationEvent>() {
public void validateUsage() {
ApplicationListener<?> listener = new ApplicationListener<ApplicationEvent>() {
public void onApplicationEvent(ApplicationEvent event) {
Object source = event.getSource();
if (source instanceof Message){
String payload = (String) ((Message)source).getPayload();
if (payload.equals("hello")){
recievedEvent = true;
String payload = (String) ((Message<?>) source).getPayload();
if (payload.equals("hello")) {
receivedEvent = true;
}
}
}
@@ -77,34 +80,38 @@ public class EventOutboundChannelAdapterParserTests {
context.addApplicationListener(listener);
DirectChannel channel = context.getBean("input", DirectChannel.class);
channel.send(new GenericMessage<String>("hello"));
Assert.assertTrue(recievedEvent);
Assert.assertTrue(receivedEvent);
}
@Test(timeout=2000)
public void validateUsageWithPollableChannel() throws Exception {
ConfigurableApplicationContext ac = new ClassPathXmlApplicationContext("EventOutboundChannelAdapterParserTestsWithPollable-context.xml", EventOutboundChannelAdapterParserTests.class);
ConfigurableApplicationContext context = new ClassPathXmlApplicationContext("EventOutboundChannelAdapterParserTestsWithPollable-context.xml", EventOutboundChannelAdapterParserTests.class);
final CyclicBarrier barier = new CyclicBarrier(2);
ApplicationListener listener = new ApplicationListener<ApplicationEvent>() {
ApplicationListener<?> listener = new ApplicationListener<ApplicationEvent>() {
public void onApplicationEvent(ApplicationEvent event) {
Object source = event.getSource();
if (source instanceof Message){
String payload = (String) ((Message)source).getPayload();
String payload = (String) ((Message<?>) source).getPayload();
if (payload.equals("hello")){
recievedEvent = true;
receivedEvent = true;
try {
barier.await();
} catch (Exception e) {
e.printStackTrace();
}
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
catch (BrokenBarrierException e) {
throw new IllegalStateException("broken barrier", e);
}
}
}
}
};
ac.addApplicationListener(listener);
QueueChannel channel = ac.getBean("input", QueueChannel.class);
context.addApplicationListener(listener);
QueueChannel channel = context.getBean("input", QueueChannel.class);
channel.send(new GenericMessage<String>("hello"));
barier.await();
Assert.assertTrue(recievedEvent);
Assert.assertTrue(receivedEvent);
}
}

View File

@@ -15,9 +15,7 @@
</int:channel>
<int-event:outbound-channel-adapter id="eventAdapter" channel="input">
<int:poller max-messages-per-poll="1" task-executor="executor">
<int:interval-trigger interval="100" time-unit="MILLISECONDS"/>
</int:poller>
<int:poller max-messages-per-poll="1" task-executor="executor" fixed-delay="100"/>
</int-event:outbound-channel-adapter>
<task:executor id="executor" pool-size="5"/>

View File

@@ -14,9 +14,14 @@
* limitations under the License.
*/
package org.springframework.integration.event;
package org.springframework.integration.event.inbound;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import org.junit.Test;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.ContextRefreshedEvent;
@@ -26,21 +31,20 @@ import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.integration.Message;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.core.PollableChannel;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import org.springframework.integration.event.core.MessagingEvent;
import org.springframework.integration.message.GenericMessage;
/**
* @author Mark Fisher
*/
public class ApplicationEventInboundChannelAdapterTests {
public class ApplicationEventListeningMessageProducerTests {
@Test
public void anyApplicationEventSentByDefault() {
QueueChannel channel = new QueueChannel();
ApplicationEventInboundChannelAdapter adapter = new ApplicationEventInboundChannelAdapter();
ApplicationEventListeningMessageProducer adapter = new ApplicationEventListeningMessageProducer();
adapter.setOutputChannel(channel);
adapter.start();
Message<?> message1 = channel.receive(0);
assertNull(message1);
adapter.onApplicationEvent(new TestApplicationEvent1());
@@ -57,9 +61,10 @@ public class ApplicationEventInboundChannelAdapterTests {
@SuppressWarnings("unchecked")
public void onlyConfiguredEventTypesAreSent() {
QueueChannel channel = new QueueChannel();
ApplicationEventInboundChannelAdapter adapter = new ApplicationEventInboundChannelAdapter();
ApplicationEventListeningMessageProducer adapter = new ApplicationEventListeningMessageProducer();
adapter.setOutputChannel(channel);
adapter.setEventTypes(new Class[]{TestApplicationEvent1.class});
adapter.start();
Message<?> message1 = channel.receive(0);
assertNull(message1);
adapter.onApplicationEvent(new TestApplicationEvent1());
@@ -96,9 +101,10 @@ public class ApplicationEventInboundChannelAdapterTests {
@Test
public void payloadExpressionEvaluatedAgainstApplicationEvent() {
QueueChannel channel = new QueueChannel();
ApplicationEventInboundChannelAdapter adapter = new ApplicationEventInboundChannelAdapter();
ApplicationEventListeningMessageProducer adapter = new ApplicationEventListeningMessageProducer();
adapter.setPayloadExpression("'received: ' + source");
adapter.setOutputChannel(channel);
adapter.start();
Message<?> message1 = channel.receive(0);
assertNull(message1);
adapter.onApplicationEvent(new TestApplicationEvent1());
@@ -111,6 +117,34 @@ public class ApplicationEventInboundChannelAdapterTests {
assertEquals("received: event2", message3.getPayload());
}
@Test
public void messagingEventReceived() {
QueueChannel channel = new QueueChannel();
ApplicationEventListeningMessageProducer adapter = new ApplicationEventListeningMessageProducer();
adapter.setOutputChannel(channel);
adapter.start();
Message<?> message1 = channel.receive(0);
assertNull(message1);
adapter.onApplicationEvent(new MessagingEvent(new GenericMessage<String>("test")));
Message<?> message2 = channel.receive(20);
assertNotNull(message2);
assertEquals("test", message2.getPayload());
}
@Test
public void messageAsSourceOrCustomEventType() {
QueueChannel channel = new QueueChannel();
ApplicationEventListeningMessageProducer adapter = new ApplicationEventListeningMessageProducer();
adapter.setOutputChannel(channel);
adapter.start();
Message<?> message1 = channel.receive(0);
assertNull(message1);
adapter.onApplicationEvent(new TestMessagingEvent(new GenericMessage<String>("test")));
Message<?> message2 = channel.receive(20);
assertNotNull(message2);
assertEquals("test", message2.getPayload());
}
@SuppressWarnings("serial")
private static class TestApplicationEvent1 extends ApplicationEvent {
@@ -118,8 +152,6 @@ public class ApplicationEventInboundChannelAdapterTests {
public TestApplicationEvent1() {
super("event1");
}
}
@@ -131,4 +163,13 @@ public class ApplicationEventInboundChannelAdapterTests {
}
}
@SuppressWarnings("serial")
private static class TestMessagingEvent extends ApplicationEvent {
public TestMessagingEvent(Message<?> message) {
super(message);
}
}
}

View File

@@ -9,7 +9,7 @@
<int:queue capacity="5"/>
</int:channel>
<bean id="adapter" class="org.springframework.integration.event.ApplicationEventInboundChannelAdapter">
<bean id="adapter" class="org.springframework.integration.event.inbound.ApplicationEventListeningMessageProducer">
<property name="outputChannel" ref="channel"/>
</bean>

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.integration.event;
package org.springframework.integration.event.outbound;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
@@ -24,6 +24,8 @@ import org.junit.Test;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.integration.Message;
import org.springframework.integration.event.core.MessagingEvent;
import org.springframework.integration.event.outbound.ApplicationEventPublishingMessageHandler;
import org.springframework.integration.message.GenericMessage;
/**
@@ -32,8 +34,7 @@ import org.springframework.integration.message.GenericMessage;
public class ApplicationEventPublishingMessageHandlerTests {
@Test
@SuppressWarnings("unchecked")
public void testSendingEvent() throws InterruptedException {
public void messagingEvent() throws InterruptedException {
TestApplicationEventPublisher publisher = new TestApplicationEventPublisher();
ApplicationEventPublishingMessageHandler handler = new ApplicationEventPublishingMessageHandler();
handler.setApplicationEventPublisher(publisher);
@@ -45,6 +46,19 @@ public class ApplicationEventPublishingMessageHandlerTests {
assertEquals(message, ((MessagingEvent) event).getMessage());
}
@Test
public void payloadAsEvent() {
TestApplicationEventPublisher publisher = new TestApplicationEventPublisher();
ApplicationEventPublishingMessageHandler handler = new ApplicationEventPublishingMessageHandler();
handler.setApplicationEventPublisher(publisher);
assertNull(publisher.getLastEvent());
Message<?> message = new GenericMessage<TestEvent>(new TestEvent("foo"));
handler.handleMessage(message);
ApplicationEvent event = publisher.getLastEvent();
assertEquals(TestEvent.class, event.getClass());
assertEquals("foo", ((TestEvent) event).getSource());
}
private static class TestApplicationEventPublisher implements ApplicationEventPublisher {
@@ -59,4 +73,13 @@ public class ApplicationEventPublishingMessageHandlerTests {
}
}
@SuppressWarnings("serial")
private static class TestEvent extends ApplicationEvent {
public TestEvent(String text) {
super(text);
}
}
}

View File

@@ -38,7 +38,7 @@ public class FeedInboundChannelAdapterParser extends AbstractPollingInboundChann
@Override
protected BeanMetadataElement parseSource(final Element element, final ParserContext parserContext) {
BeanDefinitionBuilder sourceBuilder = BeanDefinitionBuilder.genericBeanDefinition(
"org.springframework.integration.feed.FeedEntryMessageSource");
"org.springframework.integration.feed.inbound.FeedEntryMessageSource");
sourceBuilder.addConstructorArgValue(element.getAttribute("url"));
String feedFetcherRef = element.getAttribute("feed-fetcher");
if (StringUtils.hasText(feedFetcherRef)) {

View File

@@ -14,11 +14,12 @@
* limitations under the License.
*/
package org.springframework.integration.feed;
package org.springframework.integration.feed.inbound;
import java.net.URL;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
@@ -73,7 +74,7 @@ public class FeedEntryMessageSource extends IntegrationObjectSupport implements
private final Object monitor = new Object();
private final Comparator<SyndEntry> syndEntryComparator = new SyndEntryComparator();
private final Comparator<SyndEntry> syndEntryComparator = new SyndEntryPublishedDateComparator();
private final Object feedMonitor = new Object();
@@ -167,7 +168,12 @@ public class FeedEntryMessageSource extends IntegrationObjectSupport implements
if (next == null) {
return null;
}
this.lastTime = next.getPublishedDate().getTime();
if (next.getPublishedDate() != null) {
this.lastTime = next.getPublishedDate().getTime();
}
else {
this.lastTime += 1;
}
this.metadataStore.put(this.metadataKey, this.lastTime + "");
return next;
}
@@ -178,10 +184,14 @@ public class FeedEntryMessageSource extends IntegrationObjectSupport implements
if (syndFeed != null) {
List<SyndEntry> retrievedEntries = (List<SyndEntry>) syndFeed.getEntries();
if (!CollectionUtils.isEmpty(retrievedEntries)) {
boolean withinNewEntries = false;
Collections.sort(retrievedEntries, this.syndEntryComparator);
for (SyndEntry entry : retrievedEntries) {
if (entry.getPublishedDate().getTime() > this.lastTime) {
Date entryDate = getLastModifiedDate(entry);
if ((entryDate != null && entryDate.getTime() > this.lastTime)
|| (entryDate == null && withinNewEntries)) {
this.entries.add(entry);
withinNewEntries = true;
}
}
}
@@ -210,11 +220,23 @@ public class FeedEntryMessageSource extends IntegrationObjectSupport implements
return feed;
}
private static Date getLastModifiedDate(SyndEntry entry) {
return (entry.getUpdatedDate() != null) ? entry.getUpdatedDate() : entry.getPublishedDate();
}
private static class SyndEntryComparator implements Comparator<SyndEntry> {
private static class SyndEntryPublishedDateComparator implements Comparator<SyndEntry> {
public int compare(SyndEntry entry1, SyndEntry entry2) {
return entry1.getPublishedDate().compareTo(entry2.getPublishedDate());
Date date1 = getLastModifiedDate(entry1);
Date date2 = getLastModifiedDate(entry2);
if (date1 != null && date2 != null) {
return date1.compareTo(date2);
}
if (date1 == null && date2 == null) {
return 0;
}
return (date2 == null) ? 1 : 0;
}
}
@@ -227,14 +249,20 @@ public class FeedEntryMessageSource extends IntegrationObjectSupport implements
public void fetcherEvent(final FetcherEvent event) {
String eventType = event.getEventType();
if (FetcherEvent.EVENT_TYPE_FEED_POLLED.equals(eventType)) {
logger.debug("\tEVENT: Feed Polled. URL = " + event.getUrlString());
if (logger.isDebugEnabled()) {
logger.debug("\tEVENT: Feed Polled. URL = " + event.getUrlString());
}
}
else if (FetcherEvent.EVENT_TYPE_FEED_RETRIEVED.equals(eventType)) {
logger.debug("\tEVENT: Feed Retrieved. URL = " + event.getUrlString());
if (logger.isDebugEnabled()) {
logger.debug("\tEVENT: Feed Retrieved. URL = " + event.getUrlString());
}
feeds.add(event.getFeed());
}
else if (FetcherEvent.EVENT_TYPE_FEED_UNCHANGED.equals(eventType)) {
logger.debug("\tEVENT: Feed Unchanged. URL = " + event.getUrlString());
if (logger.isDebugEnabled()) {
logger.debug("\tEVENT: Feed Unchanged. URL = " + event.getUrlString());
}
}
}
}

View File

@@ -11,7 +11,7 @@
auto-startup="false"
feed-fetcher="fileUrlFeedFetcher"
metadata-store="customMetadataStore"
url="file:src/test/java/org/springframework/integration/feed/config/sample.rss">
url="file:src/test/java/org/springframework/integration/feed/sample.rss">
<int:poller fixed-rate="10000" max-messages-per-poll="100" />
</feed:inbound-channel-adapter>
@@ -19,7 +19,7 @@
<int:queue/>
</int:channel>
<bean id="fileUrlFeedFetcher" class="org.springframework.integration.feed.FileUrlFeedFetcher"/>
<bean id="fileUrlFeedFetcher" class="org.springframework.integration.feed.inbound.FileUrlFeedFetcher"/>
<bean id="customMetadataStore" class="org.springframework.integration.feed.config.FeedInboundChannelAdapterParserTests.SampleMetadataStore"/>

View File

@@ -10,7 +10,7 @@
<int-feed:inbound-channel-adapter id="feedAdapterUsage"
channel="feedChannelUsage"
url="file:src/test/java/org/springframework/integration/feed/config/sample.rss"
url="file:src/test/java/org/springframework/integration/feed/sample.rss"
feed-fetcher="fileUrlFeedFetcher">
<int:poller fixed-rate="10000" max-messages-per-poll="100" fixed-delay="10000"/>
</int-feed:inbound-channel-adapter>
@@ -19,7 +19,7 @@
<bean class="org.springframework.integration.feed.config.FeedInboundChannelAdapterParserTests$SampleService" />
</int:service-activator>
<bean id="fileUrlFeedFetcher" class="org.springframework.integration.feed.FileUrlFeedFetcher"/>
<bean id="fileUrlFeedFetcher" class="org.springframework.integration.feed.inbound.FileUrlFeedFetcher"/>
<bean id="metadataStore" class="org.springframework.integration.store.PropertiesPersistingMetadataStore"/>

View File

@@ -7,7 +7,7 @@
http://www.springframework.org/schema/integration/feed http://www.springframework.org/schema/integration/feed/spring-integration-feed-2.0.xsd">
<int-feed:inbound-channel-adapter channel="feedChannelUsage"
url="file:src/test/java/org/springframework/integration/feed/config/sample.rss"
url="file:src/test/java/org/springframework/integration/feed/sample.rss"
feed-fetcher="fileUrlFeedFetcher">
<int:poller fixed-rate="10000" max-messages-per-poll="100" fixed-delay="10000"/>
</int-feed:inbound-channel-adapter>
@@ -16,6 +16,6 @@
<bean class="org.springframework.integration.feed.config.FeedInboundChannelAdapterParserTests$SampleServiceNoHistory" />
</int:service-activator>
<bean id="fileUrlFeedFetcher" class="org.springframework.integration.feed.FileUrlFeedFetcher"/>
<bean id="fileUrlFeedFetcher" class="org.springframework.integration.feed.inbound.FileUrlFeedFetcher"/>
</beans>

View File

@@ -40,7 +40,7 @@ import org.springframework.integration.MessagingException;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.core.MessageHandler;
import org.springframework.integration.endpoint.SourcePollingChannelAdapter;
import org.springframework.integration.feed.FeedEntryMessageSource;
import org.springframework.integration.feed.inbound.FeedEntryMessageSource;
import org.springframework.integration.history.MessageHistory;
import org.springframework.integration.store.MetadataStore;
import org.springframework.integration.test.util.TestUtils;

View File

@@ -1,53 +0,0 @@
<rss version="2.0">
<channel>
<title>Spring Integration</title>
<link>http://www.springsource.org/spring-integration</link>
<description>
Spring Integration is a really cool framework
</description>
<language>en-us</language>
<copyright>Copyright 2004-2010 SpringSource/VMWare
All Rights Reserved.</copyright>
<lastBuildDate>Tue, 12 Apr 2010 18:21:32 EST</lastBuildDate>
<ttl>240</ttl>
<image>
<url>http://www.springsource.org/sites/all/themes/dotorg09/images/dotorg09_logo.png</url>
<title>Spring Integration</title>
<link>http://www.springsource.org/spring-integration</link>
</image>
<item>
<title>
Spring Integration adapters
</title>
<link>http://www.springsource.org/extensions/se-sia</link>
<description>
Spring Integration adapters are realy cool
</description>
<pubDate>Tue, 23 Apr 2010 12:34:58 EST</pubDate>
</item>
<item>
<title>
Spring Integration download
</title>
<link>http://www.springsource.com/products/spring-community-download</link>
<description>
Download Spring Integration
</description>
<pubDate>Sun, 13 Feb 2010 14:12:17 EST</pubDate>
</item>
<item>
<title>
Check out Spring Integration forums
</title>
<link>http://forum.springsource.org/forumdisplay.php?f=42</link>
<description>
Spring Integration forums are awesome
</description>
<pubDate>Wed, 13 Mar 2010 03:38:21 EST</pubDate>
</item>
</channel>
</rss>

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.integration.feed;
package org.springframework.integration.feed.inbound;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNull;
@@ -27,6 +27,7 @@ import org.junit.Before;
import org.junit.Test;
import org.springframework.integration.Message;
import org.springframework.integration.feed.inbound.FeedEntryMessageSource;
import org.springframework.integration.store.PropertiesPersistingMetadataStore;
import com.sun.syndication.feed.synd.SyndEntry;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.integration.feed;
package org.springframework.integration.feed.inbound;
import java.io.BufferedInputStream;
import java.io.IOException;
@@ -39,7 +39,7 @@ import com.sun.syndication.io.XmlReader;
* @author Mark Fisher
* @since 2.0
*/
public class FileUrlFeedFetcher extends AbstractFeedFetcher {
class FileUrlFeedFetcher extends AbstractFeedFetcher {
/**
* Retrieve a SyndFeed for the given URL.

View File

@@ -1,161 +0,0 @@
package org.springframework.integration.file;
import org.springframework.core.io.Resource;
import org.springframework.integration.endpoint.AbstractEndpoint;
import org.springframework.integration.file.entries.AcceptAllEntryListFilter;
import org.springframework.integration.file.entries.EntryListFilter;
import org.springframework.scheduling.Trigger;
import org.springframework.util.Assert;
import java.util.concurrent.ScheduledFuture;
/**
* Strategy class charged with knowing how to connect to a remote file system, scan it for new files and then downloading the file.
* <p/>
* The implementation should run through any configured {@link org.springframework.integration.file.entries.EntryListFilter}s
* to ensure the entry is worth downloading.
*
* @author Josh Long
*/
public abstract class AbstractInboundRemoteFileSystemSychronizer<T> extends AbstractEndpoint {
/**
* Should we <emphasis>delete</emphasis> the <b>source</b> file?
* For an FTP server, for example, this would delete the original FTPFile instance
* <p/>
* At the moment I can simply see this triggering an implementation specific {@link org.springframework.integration.file.AbstractInboundRemoteFileSystemSychronizer.EntryAcknowledgmentStrategy}
* implementation that knows how to delete an entry on the remote file system.
*/
protected boolean shouldDeleteSourceFile;
/**
* the directory we're writing our synchronizations to
*/
protected volatile Resource localDirectory;
/**
* a {@link org.springframework.integration.file.entries.EntryListFilter} that we're running against the <emphasis>remote</emphasis> file system view!
*/
protected volatile EntryListFilter<T> filter = new AcceptAllEntryListFilter<T>();
/**
* the {@link java.util.concurrent.ScheduledFuture} instance we get when we schedule our {@link AbstractInboundRemoteFileSystemSychronizer.SynchronizeTask}
*/
protected ScheduledFuture<?> scheduledFuture;
/**
* Used to store the {@link org.springframework.integration.file.AbstractInboundRemoteFileSystemSychronizer.EntryAcknowledgmentStrategy} implementation
*/
protected EntryAcknowledgmentStrategy<T> entryAcknowledgmentStrategy;
/**
* Obviously thread safe - simply provides a NOOP impl so we don't have to keep dancing around NPE's
*/
private EntryAcknowledgmentStrategy<T> noOpEntryAcknowledgmentStrategy = new EntryAcknowledgmentStrategy<T>() {
public void acknowledge(Object o, T msg) {
}
};
public void setEntryAcknowledgmentStrategy(EntryAcknowledgmentStrategy<T> entryAcknowledgmentStrategy) {
this.entryAcknowledgmentStrategy = entryAcknowledgmentStrategy;
}
public void setShouldDeleteSourceFile(boolean shouldDeleteSourceFile) {
this.shouldDeleteSourceFile = shouldDeleteSourceFile;
}
public void setLocalDirectory(Resource localDirectory) {
this.localDirectory = localDirectory;
}
public void setFilter(EntryListFilter<T> filter) {
this.filter = filter;
}
/**
* @param usefulContextOrClientData this is context information to be passed to the individual {@link org.springframework.integration.file.AbstractInboundRemoteFileSystemSychronizer.EntryAcknowledgmentStrategy} implementation.
* {@link org.springframework.integration.file.AbstractInboundRemoteFileSystemSychronizer.EntryAcknowledgmentStrategy#acknowledge(Object, Object)} will be called
* in line with the {@link org.springframework.integration.core.MessageSource#receive()} call so this could conceivably be a 'live' stateful
* client (a connection?) that is inappropriate to cache as it has per-request state.
* @param t leverages strategy implementations to enable different behavior. It's a hook to the entry ({@link T}) after it's been successfully downloaded.
* Conceptually, you might delete the remote one or rename it or something
* @throws Throwable escape hatch exception, let the adapter deal with it.
*/
protected void acknowledge(Object usefulContextOrClientData, T t)
throws Throwable {
Assert.notNull(this.entryAcknowledgmentStrategy != null, "entryAcknowledgmentStrategy can't be null!");
this.entryAcknowledgmentStrategy.acknowledge(usefulContextOrClientData, t);
}
/**
* This is the callback where we need the implementation to do some specific work
*
* @throws Exception thrown if anything goes wrong
*/
protected abstract void syncRemoteToLocalFileSystem()
throws Exception;
/**
* {@inheritDoc}
*/
protected void doStop() {
Assert.notNull(this.scheduledFuture, "the 'scheduledFuture' can't be null!");
this.scheduledFuture.cancel(true);
}
/**
* Returns a value in millis dictating how frequently the trigger should fire
*
* @return a {@link org.springframework.scheduling.Trigger} implementation (likely,
* {@link org.springframework.scheduling.support.PeriodicTrigger})
*/
protected abstract Trigger getTrigger();
/**
* {@inheritDoc}
*/
protected void doStart() {
if (this.entryAcknowledgmentStrategy == null) {
this.entryAcknowledgmentStrategy = noOpEntryAcknowledgmentStrategy;
}
this.scheduledFuture = this.getTaskScheduler().schedule(new SynchronizeTask(), this.getTrigger());
}
/**
* Strategy interface to expose a hook for dispatching, moving, or deleting the file once it's been delivered.
* This will typically be a NOOP for the implementation. Adapters should (for consistency) expose an attribute
* dictating whether the adapter will delete the <emphasis>source</emphasis> entry on the remote file system.
* This is the file-system version of an <code>ack-mode</code>. Future implementations should consider
* exposing a custom attribute that plugs a custom {@link org.springframework.integration.file.AbstractInboundRemoteFileSystemSychronizer.EntryAcknowledgmentStrategy}
* into the pipeline and also some more advanced scenarios (i.e., 'move file to another folder on delete ', or 'rename on delete')
*
* @param <T> the entry type (file, sftp, ftp, ...)
*/
public static interface EntryAcknowledgmentStrategy<T> {
/**
* Semantics are simple. You get a pointer to the entry just processed and any kind of helper data you could ask for. Since the strategy is a
* singleton and the clients you might ask for as context data are pooled, it's not recommended that you try to cache them.
*
* @param useful any context data
* @param msg the data / file / entry you want to process -- specific to sublcasses
* @throws Exception thrown for any old reason
*/
void acknowledge(Object useful, T msg) throws Exception;
}
/**
* This {@link Runnable} is launched as a background thread and is used to babysit the
* {@link org.springframework.integration.file.AbstractInboundRemoteFileSystemSychronizer#localDirectory},
* queueing and delivering accumulated files as possible.
*/
class SynchronizeTask implements Runnable {
public void run() {
try {
syncRemoteToLocalFileSystem();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}

View File

@@ -1,135 +0,0 @@
package org.springframework.integration.file;
import org.springframework.core.io.Resource;
import org.springframework.integration.Message;
import org.springframework.integration.MessagingException;
import org.springframework.integration.core.MessageSource;
import org.springframework.integration.endpoint.AbstractEndpoint;
import org.springframework.integration.endpoint.MessageProducerSupport;
import org.springframework.integration.file.entries.*;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Arrays;
import java.util.regex.Pattern;
/**
* Ultimately, this factors out a lot of the common logic between the FTP and SFTP adapters. Designed to be extendable to handle
* adapters whose task it is to synchronize a remote file system with a local file system (NB: this does *NOT* handle pushing files TO the remote
* file system that exist uniquely in the local file system. It only handles bringing down the remote file system - as you'd expect
* an 'inbound' adapter would).
* <p/>
* The base class supports configuration of whether the remote file system and local file system's directories should
* be created on start (what 'creating a directory' means to the specific adapter is of course implementaton specific).
* <p/>
* This class is to be used as a pair with an implementation of
* {@link org.springframework.integration.file.AbstractInboundRemoteFileSystemSychronizer<T>}. This synchronizer
* must handle the work of actually connecting to the remote file system and delivering new {@link java.io.File}s.
* The synchronizer is designed to be
*
* @author Josh Long
*/
public abstract class AbstractInboundRemoteFileSystemSynchronizingMessageSource<Y, T extends AbstractInboundRemoteFileSystemSychronizer<Y>> extends MessageProducerSupport implements MessageSource<File> {
/**
* Extension used when downloading files. We change it right after we know it's downloaded
*/
public static final String INCOMPLETE_EXTENSION = ".INCOMPLETE";
/**
* Should the endpoint attempt to create the local directory and / or the remote directory?
*/
protected volatile boolean autoCreateDirectories = true;
/**
* An implementation that will handle the chores of actually connecting to and syncing up the remote FS with the local one, in an inbound direction
*/
protected volatile T synchronizer;
/**
* What directory should things be synced to locally ?
*/
protected volatile Resource localDirectory;
/**
* The actual {@link FileReadingMessageSource} that we continue to trust to do the job monitoring the filesystem once files are moved down
*/
protected volatile FileReadingMessageSource fileSource;
/**
* The predicate to use in scanning the remote Fs for downloads
*/
protected EntryListFilter<Y> remotePredicate;
public void setAutoCreateDirectories(boolean autoCreateDirectories) {
this.autoCreateDirectories = autoCreateDirectories;
}
public void setSynchronizer(T synchronizer) {
this.synchronizer = synchronizer;
}
public void setLocalDirectory(Resource localDirectory) {
this.localDirectory = localDirectory;
}
public void setRemotePredicate(EntryListFilter<Y> remotePredicate) {
this.remotePredicate = remotePredicate;
}
@SuppressWarnings("unchecked")
private EntryListFilter<File> buildFilter() {
FileEntryNamer fileEntryNamer = new FileEntryNamer();
Pattern completePattern = Pattern.compile("^.*(?<!" + INCOMPLETE_EXTENSION + ")$");
return new CompositeEntryListFilter<File>(
Arrays.asList(
new AcceptOnceEntryFileListFilter<File>(), new PatternMatchingEntryListFilter<File>(fileEntryNamer, completePattern)));
}
@Override
protected void onInit() {
try {
if (this.remotePredicate != null) {
this.synchronizer.setFilter(this.remotePredicate);
}
if (this.localDirectory != null && !this.localDirectory.exists()){
if (this.autoCreateDirectories){
logger.debug("The '" + localDirectory + "' directory doesn't exist. Creating " + this.localDirectory);
this.localDirectory.getFile().mkdirs();
} else {
throw new FileNotFoundException(localDirectory.getFilename());
}
}
/**
* Handles making sure the remote files get here in one piece
*/
this.synchronizer.setLocalDirectory(this.localDirectory);
this.synchronizer.setTaskScheduler(this.getTaskScheduler());
this.synchronizer.setBeanFactory(this.getBeanFactory());
this.synchronizer.setPhase(this.getPhase());
this.synchronizer.setBeanName(this.getComponentName());
/**
* Handles forwarding files once they ultimately appear in the {@link #localDirectory}
*/
this.fileSource = new FileReadingMessageSource();
this.fileSource.setFilter(buildFilter());
this.fileSource.setDirectory(this.localDirectory.getFile());
this.fileSource.afterPropertiesSet();
this.synchronizer.afterPropertiesSet();
} catch (Exception e) {
if (e instanceof RuntimeException){
throw (RuntimeException)e;
} else {
throw new MessagingException("Failure during initialization of MessageSource for: " + this.getComponentType(), e);
}
}
}
public Message<File> receive() {
return this.fileSource.receive();
}
}

View File

@@ -13,66 +13,71 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.file;
import java.io.File;
import java.util.Arrays;
import java.util.List;
import org.springframework.integration.MessagingException;
import org.springframework.integration.file.entries.AcceptOnceEntryFileListFilter;
import org.springframework.integration.file.entries.EntryListFilter;
import java.io.File;
import java.util.List;
/**
* Default directory scanner and base class for other directory scanners. It takes care of the default interrelations
* between filtering, scanning and locking.
*
* Default directory scanner and base class for other directory scanners.
* Manages the default interrelations between filtering, scanning and locking.
*
* @author Iwein Fuld
* @since 2.0
*/
public class DefaultDirectoryScanner implements DirectoryScanner {
private EntryListFilter<File> filter = new AcceptOnceEntryFileListFilter<File>();
private FileLocker locker;
public final List<File> listFiles(File directory) throws IllegalArgumentException {
File[] files = listEligibleFiles(directory);
private volatile EntryListFilter<File> filter = new AcceptOnceEntryFileListFilter<File>();
if (files == null) {
throw new MessagingException("The path [" + directory + "] does not denote a properly accessible directory.");
}
private volatile FileLocker locker;
return this.filter.filterEntries(files);
}
/**
* Subclasses may refine the listing strategy by overriding this method. The files returned here are passed onto the
* filter.
*
* @param directory root directory to use for listing
* @return the files this scanner should consider
*/
protected File[] listEligibleFiles(File directory) {
return directory.listFiles();
}
public void setFilter(EntryListFilter<File> filter) {
this.filter = filter;
}
public void setFilter(EntryListFilter<File> filter) {
this.filter = filter;
}
/**
* {@inheritDoc}
*/
public final void setLocker(FileLocker locker) {
this.locker = locker;
}
/**
* {@inheritDoc}
* <p/>
* This class takes the minimal implementation and merely delegates to the locker if set.
*/
public final boolean tryClaim(File file) {
return (locker == null) || locker.lock(file);
}
/**
* {@inheritDoc}
*/
public final void setLocker(FileLocker locker) {
this.locker = locker;
}
/**
* {@inheritDoc}
* <p/>
* This class takes the minimal implementation and merely delegates to the
* locker if set.
*/
public final boolean tryClaim(File file) {
return (this.locker == null) || this.locker.lock(file);
}
public final List<File> listFiles(File directory) throws IllegalArgumentException {
File[] files = listEligibleFiles(directory);
if (files == null) {
throw new MessagingException("The path [" + directory
+ "] does not denote a properly accessible directory.");
}
return (this.filter != null) ? this.filter.filterEntries(files) : Arrays.asList(files);
}
/**
* Subclasses may refine the listing strategy by overriding this method. The
* files returned here are passed onto the filter.
*
* @param directory root directory to use for listing
* @return the files this scanner should consider
*/
protected File[] listEligibleFiles(File directory) {
return directory.listFiles();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2009 the original author or authors.
* Copyright 2002-2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -49,8 +49,7 @@ public class DefaultFileNameGenerator implements FileNameGenerator {
public String generateFileName(Message<?> message) {
Object filenameProperty = message.getHeaders().get(this.headerName);
if (filenameProperty instanceof String
&& StringUtils.hasText((String) filenameProperty)) {
if (filenameProperty instanceof String && StringUtils.hasText((String) filenameProperty)) {
return (String) filenameProperty;
}
if (message.getPayload() instanceof File) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2009 the original author or authors.
* Copyright 2002-2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,52 +22,57 @@ import java.io.File;
import java.util.List;
/**
* Strategy for scanning directories. Implementations may select all children and grandchildren of the scanned directory
* in any order. This interface is intended to enable the customization of selection, locking and ordering of files in a
* directory like RecursiveDirectoryScanner. If the only requirement is to ignore certain files a EntryListFilter
* implementation should suffice.
*
* Strategy for scanning directories. Implementations may select all children
* and grandchildren of the scanned directory in any order. This interface is
* intended to enable the customization of selection, locking and ordering of
* files in a directory like RecursiveDirectoryScanner. If the only requirement
* is to ignore certain files a EntryListFilter implementation should suffice.
*
*
* @author Iwein Fuld
*/
public interface DirectoryScanner {
/**
* Scans the directory according to the strategy particular to this implementation and returns the selected files as
* a File array. This method may never return files that are rejected by the filter.
*
* @param directory the directory to scan for files
* @return a list of files representing the content of the directory
* @throws IllegalArgumentException thrown if the input is incorrect
*/
List<File> listFiles(File directory) throws IllegalArgumentException;
/**
* Scans the directory according to the strategy particular to this
* implementation and returns the selected files as a File array. This
* method may never return files that are rejected by the filter.
*
* @param directory the directory to scan for files
* @return a list of files representing the content of the directory
* @throws IllegalArgumentException if the input is incorrect
*/
List<File> listFiles(File directory) throws IllegalArgumentException;
/**
* Sets a custom filter to be used by this scanner. The filter will get a chance to reject files before the scanner
* presents them through its listFiles method. A scanner may use additional filtering that is out of the control of
* the provided filter.
*
* @param filter the custom filter to be used
*/
void setFilter(EntryListFilter<File> filter);
/**
* Sets a custom filter to be used by this scanner. The filter will get a
* chance to reject files before the scanner presents them through its
* listFiles method. A scanner may use additional filtering that is out of
* the control of the provided filter.
*
* @param filter
* the custom filter to be used
*/
void setFilter(EntryListFilter<File> filter);
/**
* Sets a custom locker to be used by this scanner. The locker will get a
* chance to lock files and reject claims on files that are already locked.
*
* @param locker
* the custom locker to be used
*/
void setLocker(FileLocker locker);
/**
* Claim the file to process. It is up to the implementation to decide what additional safe guards are required to
* attain a claim to the file. But if a locker is set implementations MUST invoke its <code>lock</code> method and
* MUST return <code>false<code> if the locker did not grant the lock.
*
* @param file file to be claimed
* @return true if the claim was granted false otherwise
*/
boolean tryClaim(File file);
/**
* Claim the file to process. It is up to the implementation to decide what
* additional safe guards are required to attain a claim to the file. But if
* a locker is set implementations MUST invoke its <code>lock</code> method
* and MUST return <code>false<code> if the locker did not grant the lock.
*
* @param file
* file to be claimed
* @return true if the claim was granted false otherwise
*/
boolean tryClaim(File file);
/**
* Sets a custom locker to be used by this scanner. The locker will get a chance to lock files and reject claims on
* files that are already locked.
*
* @param locker the custom locker to be used
*/
void setLocker(FileLocker locker);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2009 the original author or authors.
* Copyright 2002-2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2009 the original author or authors.
* Copyright 2002-2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2008 the original author or authors.
* Copyright 2002-2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2009 the original author or authors.
* Copyright 2002-2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.file;
import java.io.File;
@@ -34,232 +35,275 @@ import org.springframework.integration.file.entries.EntryListFilter;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.util.Assert;
/**
* {@link MessageSource} that creates messages from a file system directory. To prevent messages for certain files, you
* may supply a {@link org.springframework.integration.file.entries.EntryListFilter}. By default,
* an {@link org.springframework.integration.file.entries.AcceptOnceEntryFileListFilter} is used. It ensures files are
* picked up only once from the directory.
* {@link MessageSource} that creates messages from a file system directory. To
* prevent messages for certain files, you may supply a
* {@link org.springframework.integration.file.entries.EntryListFilter}. By
* default, an
* {@link org.springframework.integration.file.entries.AcceptOnceEntryFileListFilter}
* is used. It ensures files are picked up only once from the directory.
* <p/>
* A common problem with reading files is that a file may be detected before it is ready. The default {@link
* org.springframework.integration.file.entries.AcceptOnceEntryFileListFilter} does not prevent this. In most cases, this can be prevented if the file-writing process
* renames each file as soon as it is ready for reading. A pattern-matching filter that accepts only files that are
* ready (e.g. based on a known suffix), composed with the default {@link org.springframework.integration.file.entries.AcceptOnceEntryFileListFilter} would allow for
* this. See {@link org.springframework.integration.file.entries.CompositeEntryListFilter} for a way to do this.
* A common problem with reading files is that a file may be detected before it
* is ready. The default
* {@link org.springframework.integration.file.entries.AcceptOnceEntryFileListFilter}
* does not prevent this. In most cases, this can be prevented if the
* file-writing process renames each file as soon as it is ready for reading. A
* pattern-matching filter that accepts only files that are ready (e.g. based on
* a known suffix), composed with the default
* {@link org.springframework.integration.file.entries.AcceptOnceEntryFileListFilter}
* would allow for this. See
* {@link org.springframework.integration.file.entries.CompositeEntryListFilter}
* for a way to do this.
* <p/>
* A {@link Comparator} can be used to ensure internal ordering of the Files in a {@link PriorityBlockingQueue}. This
* does not provide the same guarantees as a {@link ResequencingMessageGroupProcessor}, but in cases where writing files and failure
* downstream are rare it might be sufficient.
* A {@link Comparator} can be used to ensure internal ordering of the Files in
* a {@link PriorityBlockingQueue}. This does not provide the same guarantees as
* a {@link ResequencingMessageGroupProcessor}, but in cases where writing files
* and failure downstream are rare it might be sufficient.
* <p/>
* FileReadingMessageSource is fully thread-safe under concurrent <code>receive()</code> invocations and message
* delivery callbacks.
*
* FileReadingMessageSource is fully thread-safe under concurrent
* <code>receive()</code> invocations and message delivery callbacks.
*
* @author Iwein Fuld
* @author Mark Fisher
* @author Oleg Zhurakousky
*/
public class FileReadingMessageSource extends IntegrationObjectSupport implements MessageSource<File>{
private static final int DEFAULT_INTERNAL_QUEUE_CAPACITY = 5;
private static final Log logger = LogFactory.getLog(FileReadingMessageSource.class);
private volatile File directory;
private volatile DirectoryScanner scanner = new DefaultDirectoryScanner();
private volatile boolean autoCreateDirectory = true;
public class FileReadingMessageSource extends IntegrationObjectSupport implements MessageSource<File> {
/*
* {@link PriorityBlockingQueue#iterator()} throws {@link java.util.ConcurrentModificationException} in Java 5.
* There is no locking around the queue, so there is also no iteration.
*/
private final Queue<File> toBeReceived;
private boolean scanEachPoll = false;
private static final int DEFAULT_INTERNAL_QUEUE_CAPACITY = 5;
/**
* Creates a FileReadingMessageSource with a naturally ordered queue of unbounded capacity.
*/
public FileReadingMessageSource() {
this(null);
}
private static final Log logger = LogFactory.getLog(FileReadingMessageSource.class);
/**
* Creates a FileReadingMessageSource with a bounded queue of the given capacity. This can be used to reduce the
* memory footprint of this component when reading from a large directory.
*
* @param internalQueueCapacity the size of the queue used to cache files to be received internally. This queue can
* be made larger to optimize the directory scanning. With scanEachPoll set to false
* and the queue to a large size, it will be filled once and then completely emptied
* before a new directory listing is done. This is particularly useful to reduce scans
* of large numbers of files in a directory.
*/
public FileReadingMessageSource(int internalQueueCapacity) {
this(null);
Assert.isTrue(internalQueueCapacity > 0, "Cannot create a queue with non positive capacity");
this.setScanner(new HeadDirectoryScanner(internalQueueCapacity));
}
/**
* Creates a FileReadingMessageSource with a {@link PriorityBlockingQueue} ordered with the passed in {@link
* Comparator}
* <p/>
* The size of the queue used should be large enough to hold all the files in the input directory in order to sort
* all of them, so restricting the size of the queue is mutually exclusive with ordering. No guarantees about file
* delivery order can be made under concurrent access.
* <p/>
*
* @param receptionOrderComparator the comparator to be used to order the files in the internal queue
*/
public FileReadingMessageSource(Comparator<File> receptionOrderComparator) {
toBeReceived = new PriorityBlockingQueue<File>(DEFAULT_INTERNAL_QUEUE_CAPACITY, receptionOrderComparator);
}
private volatile File directory;
/**
* Specify the input directory.
*
* @param directory to monitor
*/
public void setDirectory(File directory) {
Assert.notNull(directory, "directory must not be null");
this.directory = directory;
}
private volatile DirectoryScanner scanner = new DefaultDirectoryScanner();
/**
* Optionally specify a custom scanner, for example the {@link org.springframework.integration.file.RecursiveLeafOnlyDirectoryScanner}
*
* @param scanner scanner impl
*/
public void setScanner(DirectoryScanner scanner) {
this.scanner = scanner;
}
private volatile boolean autoCreateDirectory = true;
/**
* Specify whether to create the source directory automatically if it does not yet exist upon initialization. By
* default, this value is <emphasis>true</emphasis>. If set to <emphasis>false</emphasis> and the source directory
* does not exist, an Exception will be thrown upon initialization.
*
* @param autoCreateDirectory should the directory to be monitored be created when this component starts up?
*/
public void setAutoCreateDirectory(boolean autoCreateDirectory) {
this.autoCreateDirectory = autoCreateDirectory;
}
/*
* {@link PriorityBlockingQueue#iterator()} throws
* {@link java.util.ConcurrentModificationException} in Java 5.
* There is no locking around the queue, so there is also no iteration.
*/
private final Queue<File> toBeReceived;
/**
* Sets a {@link org.springframework.integration.file.entries.EntryListFilter}. By default a {@link org.springframework.integration.file.entries.AcceptOnceEntryFileListFilter} with no bounds is used. In most
* cases a customized {@link org.springframework.integration.file.entries.EntryListFilter} will be needed to deal with modification and duplication concerns. If
* multiple filters are required a {@link org.springframework.integration.file.entries.CompositeEntryListFilter} can be used to group them together.
* <p/>
* <b>The supplied filter must be thread safe.</b>.
*
* @param filter a filter
*/
public void setFilter(EntryListFilter<File> filter) {
Assert.notNull(filter, "'filter' must not be null");
this.scanner.setFilter(filter);
}
private volatile boolean scanEachPoll = false;
/**
* Optional. Sets a {@link FileLocker} to be used to guard files
* against duplicate processing.
* <p/>
* <b>The supplied FileLocker must be thread safe</b>
*
* @param locker a locker
*/
public void setLocker(FileLocker locker) {
Assert.notNull(locker, "'fileLocker' must not be null.");
this.scanner.setLocker(locker);
}
/**
* Optional. Set this flag if you want to make sure the internal queue is refreshed with the latest content of the
* input directory on each poll.
* <p/>
* By default this implementation will empty its queue before looking at the directory again. In cases where order
* is relevant it is important to consider the effects of setting this flag. The internal {@link
* java.util.concurrent.BlockingQueue} that this class is keeping will more likely be out of sync with the file
* system if this flag is set to <code>false</code>, but it will change more often (causing expensive reordering) if
* it is set to <code>true</code>.
*
* @param scanEachPoll whether or not the component should re-scan (as opposed to not rescanning until the entire backlog has been delivered)
*/
public void setScanEachPoll(boolean scanEachPoll) {
this.scanEachPoll = scanEachPoll;
}
/**
* Creates a FileReadingMessageSource with a naturally ordered queue of unbounded capacity.
*/
public FileReadingMessageSource() {
this(null);
}
protected void onInit() {
Assert.notNull(directory, "'directory' must not be set before initialization");
/**
* Creates a FileReadingMessageSource with a bounded queue of the given
* capacity. This can be used to reduce the memory footprint of this
* component when reading from a large directory.
*
* @param internalQueueCapacity
* the size of the queue used to cache files to be received
* internally. This queue can be made larger to optimize the
* directory scanning. With scanEachPoll set to false and the
* queue to a large size, it will be filled once and then
* completely emptied before a new directory listing is done.
* This is particularly useful to reduce scans of large numbers
* of files in a directory.
*/
public FileReadingMessageSource(int internalQueueCapacity) {
this(null);
Assert.isTrue(internalQueueCapacity > 0,
"Cannot create a queue with non positive capacity");
this.setScanner(new HeadDirectoryScanner(internalQueueCapacity));
}
if (!this.directory.exists() && this.autoCreateDirectory) {
this.directory.mkdirs();
}
/**
* Creates a FileReadingMessageSource with a {@link PriorityBlockingQueue}
* ordered with the passed in {@link Comparator}
* <p/>
* The size of the queue used should be large enough to hold all the files
* in the input directory in order to sort all of them, so restricting the
* size of the queue is mutually exclusive with ordering. No guarantees
* about file delivery order can be made under concurrent access.
* <p/>
*
* @param receptionOrderComparator
* the comparator to be used to order the files in the internal
* queue
*/
public FileReadingMessageSource(Comparator<File> receptionOrderComparator) {
this.toBeReceived = new PriorityBlockingQueue<File>(
DEFAULT_INTERNAL_QUEUE_CAPACITY, receptionOrderComparator);
}
Assert.isTrue(this.directory.exists(), "Source directory [" + directory + "] does not exist.");
Assert.isTrue(this.directory.isDirectory(), "Source path [" + this.directory + "] does not point to a directory.");
Assert.isTrue(this.directory.canRead(), "Source directory [" + this.directory + "] is not readable.");
}
public Message<File> receive() throws MessagingException {
Message<File> message = null;
/**
* Specify the input directory.
*
* @param directory to monitor
*/
public void setDirectory(File directory) {
Assert.notNull(directory, "directory must not be null");
this.directory = directory;
}
// rescan only if needed or explicitly configured
if (scanEachPoll || toBeReceived.isEmpty()) {
scanInputDirectory();
}
/**
* Optionally specify a custom scanner, for example the
* {@link org.springframework.integration.file.RecursiveLeafOnlyDirectoryScanner}
*
* @param scanner scanner implementation
*/
public void setScanner(DirectoryScanner scanner) {
this.scanner = scanner;
}
File file = toBeReceived.poll();
/**
* Specify whether to create the source directory automatically if it does
* not yet exist upon initialization. By default, this value is
* <emphasis>true</emphasis>. If set to <emphasis>false</emphasis> and the
* source directory does not exist, an Exception will be thrown upon
* initialization.
*
* @param autoCreateDirectory
* should the directory to be monitored be created when this
* component starts up?
*/
public void setAutoCreateDirectory(boolean autoCreateDirectory) {
this.autoCreateDirectory = autoCreateDirectory;
}
// file == null means the queue was empty
// we can't rely on isEmpty for concurrency reasons
while ((file != null) && !scanner.tryClaim(file)) {
file = toBeReceived.poll();
}
/**
* Sets a
* {@link org.springframework.integration.file.entries.EntryListFilter}. By
* default a
* {@link org.springframework.integration.file.entries.AcceptOnceEntryFileListFilter}
* with no bounds is used. In most cases a customized
* {@link org.springframework.integration.file.entries.EntryListFilter} will
* be needed to deal with modification and duplication concerns. If multiple
* filters are required a
* {@link org.springframework.integration.file.entries.CompositeEntryListFilter}
* can be used to group them together.
* <p/>
* <b>The supplied filter must be thread safe.</b>.
*
* @param filter a filter
*/
public void setFilter(EntryListFilter<File> filter) {
Assert.notNull(filter, "'filter' must not be null");
this.scanner.setFilter(filter);
}
if (file != null) {
message = MessageBuilder.withPayload(file).build();
/**
* Optional. Sets a {@link FileLocker} to be used to guard files against
* duplicate processing.
* <p/>
* <b>The supplied FileLocker must be thread safe</b>
*
* @param locker a locker
*/
public void setLocker(FileLocker locker) {
Assert.notNull(locker, "'fileLocker' must not be null.");
this.scanner.setLocker(locker);
}
if (logger.isInfoEnabled()) {
logger.info("Created message: [" + message + "]");
}
}
return message;
}
private void scanInputDirectory() {
List<File> filteredFiles = scanner.listFiles(directory);
Set<File> freshFiles = new HashSet<File>(filteredFiles);
if (!freshFiles.isEmpty()) {
toBeReceived.addAll(freshFiles);
if (logger.isDebugEnabled()) {
logger.debug("Added to queue: " + freshFiles);
}
}
}
/**
* Adds the failed message back to the 'toBeReceived' queue if there is room.
*
* @param failedMessage the {@link org.springframework.integration.Message} that blew up
*/
public void onFailure(Message<File> failedMessage) {
if (logger.isWarnEnabled()) {
logger.warn("Failed to send: " + failedMessage);
}
toBeReceived.offer(failedMessage.getPayload());
}
/**
* The message is just logged. It was already removed from the queue during the call to <code>receive()</code>
*
* @param sentMessage the message that was successfully delivered
*/
public void onSend(Message<File> sentMessage) {
if (logger.isDebugEnabled()) {
logger.debug("Sent: " + sentMessage);
}
}
/**
* Optional. Set this flag if you want to make sure the internal queue is
* refreshed with the latest content of the input directory on each poll.
* <p/>
* By default this implementation will empty its queue before looking at the
* directory again. In cases where order is relevant it is important to
* consider the effects of setting this flag. The internal
* {@link java.util.concurrent.BlockingQueue} that this class is keeping
* will more likely be out of sync with the file system if this flag is set
* to <code>false</code>, but it will change more often (causing expensive
* reordering) if it is set to <code>true</code>.
*
* @param scanEachPoll
* whether or not the component should re-scan (as opposed to not
* rescanning until the entire backlog has been delivered)
*/
public void setScanEachPoll(boolean scanEachPoll) {
this.scanEachPoll = scanEachPoll;
}
public String getComponentType() {
return "file:inbound-channel-adapter";
}
protected void onInit() {
Assert.notNull(directory, "'directory' must not be null");
if (!this.directory.exists() && this.autoCreateDirectory) {
this.directory.mkdirs();
}
Assert.isTrue(this.directory.exists(),
"Source directory [" + directory + "] does not exist.");
Assert.isTrue(this.directory.isDirectory(),
"Source path [" + this.directory + "] does not point to a directory.");
Assert.isTrue(this.directory.canRead(),
"Source directory [" + this.directory + "] is not readable.");
}
public Message<File> receive() throws MessagingException {
Message<File> message = null;
// rescan only if needed or explicitly configured
if (scanEachPoll || toBeReceived.isEmpty()) {
scanInputDirectory();
}
File file = toBeReceived.poll();
// file == null means the queue was empty
// we can't rely on isEmpty for concurrency reasons
while ((file != null) && !scanner.tryClaim(file)) {
file = toBeReceived.poll();
}
if (file != null) {
message = MessageBuilder.withPayload(file).build();
if (logger.isInfoEnabled()) {
logger.info("Created message: [" + message + "]");
}
}
return message;
}
private void scanInputDirectory() {
List<File> filteredFiles = scanner.listFiles(directory);
Set<File> freshFiles = new HashSet<File>(filteredFiles);
if (!freshFiles.isEmpty()) {
toBeReceived.addAll(freshFiles);
if (logger.isDebugEnabled()) {
logger.debug("Added to queue: " + freshFiles);
}
}
}
/**
* Adds the failed message back to the 'toBeReceived' queue if there is room.
*
* @param failedMessage
* the {@link org.springframework.integration.Message} that failed
*/
public void onFailure(Message<File> failedMessage) {
if (logger.isWarnEnabled()) {
logger.warn("Failed to send: " + failedMessage);
}
toBeReceived.offer(failedMessage.getPayload());
}
/**
* The message is just logged. It was already removed from the queue during
* the call to <code>receive()</code>
*
* @param sentMessage
* the message that was successfully delivered
*/
public void onSend(Message<File> sentMessage) {
if (logger.isDebugEnabled()) {
logger.debug("Sent: " + sentMessage);
}
}
}

View File

@@ -204,9 +204,7 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand
return resultFile;
}
private File handleByteArrayMessage(byte[] bytes, File originalFile, File tempFile, File resultFile)
throws IOException {
private File handleByteArrayMessage(byte[] bytes, File originalFile, File tempFile, File resultFile) throws IOException {
FileCopyUtils.copy(bytes, tempFile);
tempFile.renameTo(resultFile);
if (this.deleteSourceFiles && originalFile != null) {
@@ -215,11 +213,8 @@ public class FileWritingMessageHandler extends AbstractReplyProducingMessageHand
return resultFile;
}
private File handleStringMessage(String content, File originalFile, File tempFile, File resultFile)
throws IOException {
OutputStreamWriter writer = new OutputStreamWriter(
new FileOutputStream(tempFile), this.charset);
private File handleStringMessage(String content, File originalFile, File tempFile, File resultFile) throws IOException {
OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(tempFile), this.charset);
FileCopyUtils.copy(content, writer);
tempFile.renameTo(resultFile);
if (this.deleteSourceFiles && originalFile != null) {

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.file;
import org.springframework.integration.file.entries.EntryListFilter;
@@ -22,28 +23,32 @@ import java.io.File;
import java.util.Arrays;
import java.util.List;
/**
* A custom scanner that only returns the first <code>maxNumberOfFiles</code> elements from a directory listing. This is
* useful to limit the number of File objects in memory and therefore mutually exclusive with AcceptOnceFileListFilter.
*
* A custom scanner that only returns the first <code>maxNumberOfFiles</code>
* elements from a directory listing. This is useful to limit the number of File
* objects in memory and therefore mutually exclusive with AcceptOnceFileListFilter.
*
* @author Iwein Fuld
* @since 2.0.0
* @since 2.0
*/
public class HeadDirectoryScanner extends DefaultDirectoryScanner {
public HeadDirectoryScanner(int maxNumberOfFiles) {
this.setFilter(new HeadFilter(maxNumberOfFiles));
}
private class HeadFilter implements EntryListFilter<File> {
private final int maxNumberOfFiles;
public HeadDirectoryScanner(int maxNumberOfFiles) {
this.setFilter(new HeadFilter(maxNumberOfFiles));
}
public HeadFilter(int maxNumberOfFiles) {
this.maxNumberOfFiles = maxNumberOfFiles;
}
public List<File> filterEntries(File[] files) {
return Arrays.asList(files).subList(0, Math.min(files.length, maxNumberOfFiles));
}
}
private static class HeadFilter implements EntryListFilter<File> {
private final int maxNumberOfFiles;
public HeadFilter(int maxNumberOfFiles) {
this.maxNumberOfFiles = maxNumberOfFiles;
}
public List<File> filterEntries(File[] files) {
return Arrays.asList(files).subList(0, Math.min(files.length, maxNumberOfFiles));
}
}
}

View File

@@ -1,3 +1,19 @@
/*
* Copyright 2002-2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.file;
import java.io.File;
@@ -6,23 +22,27 @@ import java.util.Arrays;
import java.util.List;
/**
* DirectoryScanner that lists all files inside a directory and subdirectories, without limit. This scanner should not
* be used with directories that contain a vast number of files or on deep trees, as all the file names will be read
* DirectoryScanner that lists all files inside a directory and subdirectories,
* without limit. This scanner should not be used with directories that contain
* a vast number of files or on deep trees, as all the file names will be read
* into memory and the scanning will be done recursively.
*
*
* @author Iwein Fuld
*/
public class RecursiveLeafOnlyDirectoryScanner extends DefaultDirectoryScanner {
protected File[] listEligibleFiles(File directory) throws IllegalArgumentException {
File[] rootFiles = directory.listFiles();
List<File> files = new ArrayList<File>(rootFiles.length);
for (File rootFile : rootFiles) {
if (rootFile.isDirectory()) {
files.addAll(Arrays.asList(listEligibleFiles(rootFile)));
} else {
files.add(rootFile);
}
}
return files.toArray(new File[files.size()]);
}
protected File[] listEligibleFiles(File directory) throws IllegalArgumentException {
File[] rootFiles = directory.listFiles();
List<File> files = new ArrayList<File>(rootFiles.length);
for (File rootFile : rootFiles) {
if (rootFile.isDirectory()) {
files.addAll(Arrays.asList(listEligibleFiles(rootFile)));
}
else {
files.add(rootFile);
}
}
return files.toArray(new File[files.size()]);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2009 the original author or authors.
* Copyright 2002-2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2009 the original author or authors.
* Copyright 2002-2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -31,73 +31,72 @@ import org.springframework.util.xml.DomUtils;
/**
* Parser for the &lt;inbound-channel-adapter&gt; element of the 'file' namespace.
*
*
* @author Iwein Fuld
* @author Mark Fisher
*/
public class FileInboundChannelAdapterParser extends AbstractPollingInboundChannelAdapterParser {
private static final String PACKAGE_NAME = "org.springframework.integration.file";
private static final String PACKAGE_NAME = "org.springframework.integration.file";
@Override
protected BeanMetadataElement parseSource(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(
PACKAGE_NAME + ".config.FileReadingMessageSourceFactoryBean");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "comparator");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "scanner");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "directory");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "auto-create-directory");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "queue-size");
String filterBeanName = this.registerFilter(element, parserContext);
String lockerBeanName = registerLocker(element, parserContext);
if (lockerBeanName != null) {
builder.addPropertyReference("locker", lockerBeanName);
}
builder.addPropertyReference("filter", filterBeanName);
String beanName = BeanDefinitionReaderUtils.registerWithGeneratedName(builder.getBeanDefinition(), parserContext.getRegistry());
return new RuntimeBeanReference(beanName);
}
@Override
protected BeanMetadataElement parseSource(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(
PACKAGE_NAME + ".config.FileReadingMessageSourceFactoryBean");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "comparator");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "scanner");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "directory");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "auto-create-directory");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "queue-size");
String filterBeanName = this.registerFilter(element, parserContext);
String lockerBeanName = registerLocker(element, parserContext);
if (lockerBeanName != null) {
builder.addPropertyReference("locker", lockerBeanName);
}
builder.addPropertyReference("filter", filterBeanName);
String beanName = BeanDefinitionReaderUtils.registerWithGeneratedName(
builder.getBeanDefinition(), parserContext.getRegistry());
return new RuntimeBeanReference(beanName);
}
private String registerLocker(Element element, ParserContext parserContext) {
String lockerBeanName = null;
Element nioLocker = DomUtils.getChildElementByTagName(element, "nio-locker");
if (nioLocker != null) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(PACKAGE_NAME + ".locking.NioFileLocker");
lockerBeanName = BeanDefinitionReaderUtils.registerWithGeneratedName(builder.getBeanDefinition(),
parserContext.getRegistry());
} else {
Element locker = DomUtils.getChildElementByTagName(element, "locker");
if (locker != null) {
lockerBeanName = locker.getAttribute("ref");
}
}
return lockerBeanName;
}
private String registerLocker(Element element, ParserContext parserContext) {
String lockerBeanName = null;
Element nioLocker = DomUtils.getChildElementByTagName(element, "nio-locker");
if (nioLocker != null) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(
PACKAGE_NAME + ".locking.NioFileLocker");
lockerBeanName = BeanDefinitionReaderUtils.registerWithGeneratedName(
builder.getBeanDefinition(), parserContext.getRegistry());
}
else {
Element locker = DomUtils.getChildElementByTagName(element, "locker");
if (locker != null) {
lockerBeanName = locker.getAttribute("ref");
}
}
return lockerBeanName;
}
private String registerFilter(Element element, ParserContext parserContext) {
BeanDefinitionBuilder factoryBeanBuilder = BeanDefinitionBuilder.genericBeanDefinition(
PACKAGE_NAME + ".config.FileListFilterFactoryBean");
factoryBeanBuilder.setRole(BeanDefinition.ROLE_SUPPORT);
String filter = element.getAttribute("filter");
if (StringUtils.hasText(filter)) {
factoryBeanBuilder.addPropertyReference("filterReference", filter);
}
String filenamePattern = element.getAttribute("filename-pattern");
if (StringUtils.hasText(filenamePattern)) {
if (StringUtils.hasText(filter)) {
parserContext.getReaderContext().error(
"At most one of 'filter' and 'filename-pattern' may be provided.", element);
}
factoryBeanBuilder.addPropertyValue("filenamePattern", filenamePattern);
}
String preventDuplicates = element.getAttribute("prevent-duplicates");
if (StringUtils.hasText(preventDuplicates)) {
factoryBeanBuilder.addPropertyValue("preventDuplicates", preventDuplicates);
}
return BeanDefinitionReaderUtils.registerWithGeneratedName(
factoryBeanBuilder.getBeanDefinition(), parserContext.getRegistry());
}
private String registerFilter(Element element, ParserContext parserContext) {
BeanDefinitionBuilder factoryBeanBuilder = BeanDefinitionBuilder.genericBeanDefinition(
PACKAGE_NAME + ".config.FileListFilterFactoryBean");
factoryBeanBuilder.setRole(BeanDefinition.ROLE_SUPPORT);
String filter = element.getAttribute("filter");
if (StringUtils.hasText(filter)) {
factoryBeanBuilder.addPropertyReference("filterReference", filter);
}
String filenamePattern = element.getAttribute("filename-pattern");
if (StringUtils.hasText(filenamePattern)) {
if (StringUtils.hasText(filter)) {
parserContext.getReaderContext().error(
"At most one of 'filter' and 'filename-pattern' may be provided.", element);
}
factoryBeanBuilder.addPropertyValue("filenamePattern", filenamePattern);
}
IntegrationNamespaceUtils.setValueIfAttributeDefined(factoryBeanBuilder, element, "prevent-duplicates");
return BeanDefinitionReaderUtils.registerWithGeneratedName(
factoryBeanBuilder.getBeanDefinition(), parserContext.getRegistry());
}
}

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.file.config;
import org.springframework.beans.factory.FactoryBean;
@@ -22,107 +23,111 @@ import org.springframework.integration.file.filters.SimplePatternFileListFilter;
import java.io.File;
import java.util.Collection;
/**
* @author Mark Fisher
* @since 1.0.3
*/
public class FileListFilterFactoryBean implements FactoryBean<EntryListFilter<File>> {
private volatile EntryListFilter<File> fileListFilter;
private volatile EntryListFilter<File> filterReference;
private volatile String filenamePattern;
private volatile Boolean preventDuplicates;
private final Object monitor = new Object();
private volatile Collection<EntryListFilter<File>> filterReferences;
private FileEntryNamer fileNamer = new FileEntryNamer();
public void setFilterReferences(Collection<EntryListFilter<File>> filterReferences) {
this.filterReferences = filterReferences;
}
private volatile EntryListFilter<File> fileListFilter;
public void setFilterReference(EntryListFilter<File> filterReference) {
this.filterReference = filterReference;
}
private volatile EntryListFilter<File> filterReference;
public void setFilenamePattern(String filenamePattern) {
this.filenamePattern = filenamePattern;
}
private volatile String filenamePattern;
public void setPreventDuplicates(Boolean preventDuplicates) {
this.preventDuplicates = preventDuplicates;
}
private volatile Boolean preventDuplicates;
public EntryListFilter<File> getObject() throws Exception {
if (this.fileListFilter == null) {
synchronized (this.monitor) {
this.intializeFileListFilter();
}
}
private final Object monitor = new Object();
return this.fileListFilter;
}
private volatile Collection<EntryListFilter<File>> filterReferences;
public Class<?> getObjectType() {
return (this.fileListFilter != null) ? this.fileListFilter.getClass() : EntryListFilter.class;
}
public boolean isSingleton() {
return true;
}
public void setFilterReferences(Collection<EntryListFilter<File>> filterReferences) {
this.filterReferences = filterReferences;
}
private void intializeFileListFilter() {
if (this.fileListFilter != null) {
return;
}
public void setFilterReference(EntryListFilter<File> filterReference) {
this.filterReference = filterReference;
}
EntryListFilter<File> flf ;
public void setFilenamePattern(String filenamePattern) {
this.filenamePattern = filenamePattern;
}
if ((this.filterReference != null) && (this.filenamePattern != null)) {
throw new IllegalArgumentException("The 'filter' reference and " + "'filename-pattern' attributes are mutually exclusive.");
}
public void setPreventDuplicates(Boolean preventDuplicates) {
this.preventDuplicates = preventDuplicates;
}
if (this.filterReference != null) {
if (Boolean.TRUE.equals(this.preventDuplicates)) {
flf = this.createCompositeWithAcceptOnceFilter(this.filterReference);
} else { // preventDuplicates is either FALSE or NULL
flf = this.filterReference;
}
} else if (this.filenamePattern != null) {
SimplePatternFileListFilter patternFilter = new SimplePatternFileListFilter(this.filenamePattern);
public EntryListFilter<File> getObject() throws Exception {
if (this.fileListFilter == null) {
synchronized (this.monitor) {
this.intializeFileListFilter();
}
}
return this.fileListFilter;
}
if (Boolean.FALSE.equals(this.preventDuplicates)) {
flf = patternFilter;
} else { // preventDuplicates is either TRUE or NULL
flf = this.createCompositeWithAcceptOnceFilter(patternFilter);
}
} else if (Boolean.FALSE.equals(this.preventDuplicates)) {
flf = new AcceptAllEntryListFilter<File>();
} else { // preventDuplicates is either TRUE or NULL
flf = new AcceptOnceEntryFileListFilter<File>();
}
public Class<?> getObjectType() {
return (this.fileListFilter != null) ? this.fileListFilter.getClass() : EntryListFilter.class;
}
// finally, it might be that they simply want a {@link CompositeEntryListFilter}
if ((this.filterReferences != null) && (this.filterReferences.size() > 0)) {
CompositeEntryListFilter<File> flfc = new CompositeEntryListFilter<File>();
public boolean isSingleton() {
return true;
}
for (EntryListFilter<File> ff : filterReferences)
flfc.addFilter(ff);
private void intializeFileListFilter() {
if (this.fileListFilter != null) {
return;
}
EntryListFilter<File> filter;
if ((this.filterReference != null) && (this.filenamePattern != null)) {
throw new IllegalArgumentException("The 'filter' reference and "
+ "'filename-pattern' attributes are mutually exclusive.");
}
flf = flfc;
}
if (this.filterReference != null) {
if (Boolean.TRUE.equals(this.preventDuplicates)) {
filter = this.createCompositeWithAcceptOnceFilter(this.filterReference);
}
else { // preventDuplicates is either FALSE or NULL
filter = this.filterReference;
}
}
else if (this.filenamePattern != null) {
SimplePatternFileListFilter patternFilter = new SimplePatternFileListFilter(this.filenamePattern);
if (Boolean.FALSE.equals(this.preventDuplicates)) {
filter = patternFilter;
}
else { // preventDuplicates is either TRUE or NULL
filter = this.createCompositeWithAcceptOnceFilter(patternFilter);
}
}
else if (Boolean.FALSE.equals(this.preventDuplicates)) {
filter = new AcceptAllEntryListFilter<File>();
}
else { // preventDuplicates is either TRUE or NULL
filter = new AcceptOnceEntryFileListFilter<File>();
}
if (flf == null) {
flf = new CompositeEntryListFilter<File>();
}
// finally, it might be that they simply want a {@link CompositeEntryListFilter}
if ((this.filterReferences != null) && (this.filterReferences.size() > 0)) {
CompositeEntryListFilter<File> compositeFilter = new CompositeEntryListFilter<File>();
for (EntryListFilter<File> ff : filterReferences) {
compositeFilter.addFilter(ff);
}
filter = compositeFilter;
}
if (filter == null) {
filter = new CompositeEntryListFilter<File>();
}
this.fileListFilter = filter;
}
this.fileListFilter = flf;
}
private CompositeEntryListFilter<File> createCompositeWithAcceptOnceFilter(EntryListFilter<File> otherFilter) {
CompositeEntryListFilter<File> compositeFilter = new CompositeEntryListFilter<File>();
compositeFilter.addFilter(new AcceptOnceEntryFileListFilter<File>());
compositeFilter.addFilter(otherFilter);
return compositeFilter;
}
private CompositeEntryListFilter<File> createCompositeWithAcceptOnceFilter(EntryListFilter<File> otherFilter) {
CompositeEntryListFilter<File> compositeFilter = new CompositeEntryListFilter<File>();
compositeFilter.addFilter(new AcceptOnceEntryFileListFilter<File>());
compositeFilter.addFilter(otherFilter);
return compositeFilter;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2009 the original author or authors.
* Copyright 2002-2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2009 the original author or authors.
* Copyright 2002-2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2009 the original author or authors.
* Copyright 2002-2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -13,10 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.file.config;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.integration.file.DirectoryScanner;
import org.springframework.integration.file.FileReadingMessageSource;
@@ -27,134 +29,129 @@ import org.springframework.integration.file.locking.AbstractFileLockerFilter;
import java.io.File;
import java.util.Comparator;
/**
* @author Mark Fisher
* @author Iwein Fuld
* @since 1.0.3
*/
public class FileReadingMessageSourceFactoryBean implements FactoryBean<FileReadingMessageSource> {
private static Log logger = LogFactory.getLog(FileReadingMessageSourceFactoryBean.class);
private volatile FileReadingMessageSource source;
private volatile File directory;
private volatile EntryListFilter<File> filter;
private volatile AbstractFileLockerFilter locker;
private volatile Comparator<File> comparator;
private volatile DirectoryScanner scanner;
private volatile Boolean scanEachPoll;
private volatile Boolean autoCreateDirectory;
private volatile Integer queueSize;
private final Object initializationMonitor = new Object();
@SuppressWarnings("unused")
public void setDirectory(File directory) {
this.directory = directory;
}
private static Log logger = LogFactory.getLog(FileReadingMessageSourceFactoryBean.class);
@SuppressWarnings("unused")
public void setComparator(Comparator<File> comparator) {
this.comparator = comparator;
}
private volatile FileReadingMessageSource source;
@SuppressWarnings("unused")
public void setScanner(DirectoryScanner scanner) {
this.scanner = scanner;
}
private volatile File directory;
@SuppressWarnings("unused")
public void setFilter(EntryListFilter<File> filter) {
if (filter instanceof AbstractFileLockerFilter && (this.locker == null)) {
this.setLocker((AbstractFileLockerFilter) filter);
}
private volatile EntryListFilter<File> filter;
this.filter = filter;
}
private volatile AbstractFileLockerFilter locker;
@SuppressWarnings("unused")
public void setScanEachPoll(Boolean scanEachPoll) {
this.scanEachPoll = scanEachPoll;
}
private volatile Comparator<File> comparator;
@SuppressWarnings("unused")
public void setAutoCreateDirectory(Boolean autoCreateDirectory) {
this.autoCreateDirectory = autoCreateDirectory;
}
private volatile DirectoryScanner scanner;
@SuppressWarnings("unused")
public void setQueueSize(Integer queueSize) {
this.queueSize = queueSize;
}
private volatile Boolean scanEachPoll;
public void setLocker(AbstractFileLockerFilter locker) {
this.locker = locker;
}
private volatile Boolean autoCreateDirectory;
public FileReadingMessageSource getObject() throws Exception {
if (this.source == null) {
initSource();
}
private volatile Integer queueSize;
return this.source;
}
private final Object initializationMonitor = new Object();
public Class<?> getObjectType() {
return FileReadingMessageSource.class;
}
public boolean isSingleton() {
return true;
}
public void setDirectory(File directory) {
this.directory = directory;
}
private void initSource() {
synchronized (this.initializationMonitor) {
if (this.source != null) {
return;
}
public void setComparator(Comparator<File> comparator) {
this.comparator = comparator;
}
boolean comparatorSet = this.comparator != null;
boolean queueSizeSet = this.queueSize != null;
public void setScanner(DirectoryScanner scanner) {
this.scanner = scanner;
}
if (comparatorSet) {
if (queueSizeSet) {
logger.warn("'comparator' and 'queueSize' are mutually exclusive. Ignoring 'queueSize'");
}
public void setFilter(EntryListFilter<File> filter) {
if (filter instanceof AbstractFileLockerFilter && (this.locker == null)) {
this.setLocker((AbstractFileLockerFilter) filter);
}
this.filter = filter;
}
this.source = new FileReadingMessageSource(this.comparator);
} else if (queueSizeSet) {
this.source = new FileReadingMessageSource(queueSize);
} else {
this.source = new FileReadingMessageSource();
}
public void setScanEachPoll(Boolean scanEachPoll) {
this.scanEachPoll = scanEachPoll;
}
this.source.setDirectory(this.directory);
public void setAutoCreateDirectory(Boolean autoCreateDirectory) {
this.autoCreateDirectory = autoCreateDirectory;
}
if (this.scanner != null) {
this.source.setScanner(this.scanner);
}
public void setQueueSize(Integer queueSize) {
this.queueSize = queueSize;
}
if (this.filter != null) {
if (this.locker == null) {
this.source.setFilter(this.filter);
} else {
CompositeEntryListFilter<File> fileCompositeEntryListFilter = new CompositeEntryListFilter<File>();
public void setLocker(AbstractFileLockerFilter locker) {
this.locker = locker;
}
public FileReadingMessageSource getObject() throws Exception {
if (this.source == null) {
initSource();
}
return this.source;
}
public Class<?> getObjectType() {
return FileReadingMessageSource.class;
}
public boolean isSingleton() {
return true;
}
private void initSource() {
synchronized (this.initializationMonitor) {
if (this.source != null) {
return;
}
boolean comparatorSet = this.comparator != null;
boolean queueSizeSet = this.queueSize != null;
if (comparatorSet) {
if (queueSizeSet) {
logger.warn("'comparator' and 'queueSize' are mutually exclusive. Ignoring 'queueSize'");
}
this.source = new FileReadingMessageSource(this.comparator);
}
else if (queueSizeSet) {
this.source = new FileReadingMessageSource(queueSize);
}
else {
this.source = new FileReadingMessageSource();
}
this.source.setDirectory(this.directory);
if (this.scanner != null) {
this.source.setScanner(this.scanner);
}
if (this.filter != null) {
if (this.locker == null) {
this.source.setFilter(this.filter);
}
else {
CompositeEntryListFilter<File> fileCompositeEntryListFilter = new CompositeEntryListFilter<File>();
fileCompositeEntryListFilter.addFilter(this.filter);
fileCompositeEntryListFilter.addFilter(this.locker);
this.source.setFilter(fileCompositeEntryListFilter);
this.source.setLocker(locker);
}
}
if (this.scanEachPoll != null) {
this.source.setScanEachPoll(this.scanEachPoll);
}
if (this.autoCreateDirectory != null) {
this.source.setAutoCreateDirectory(this.autoCreateDirectory);
}
this.source.afterPropertiesSet();
}
}
this.source.setFilter(fileCompositeEntryListFilter);
this.source.setLocker(locker);
}
}
if (this.scanEachPoll != null) {
this.source.setScanEachPoll(this.scanEachPoll);
}
if (this.autoCreateDirectory != null) {
this.source.setAutoCreateDirectory(this.autoCreateDirectory);
}
this.source.afterPropertiesSet();
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2008 the original author or authors.
* Copyright 2002-2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2008 the original author or authors.
* Copyright 2002-2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2009 the original author or authors.
* Copyright 2002-2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -32,8 +32,7 @@ import java.io.File;
* @author Iwein Fuld
* @since 1.0.3
*/
public class FileWritingMessageHandlerFactoryBean implements FactoryBean<FileWritingMessageHandler>,
BeanFactoryAware {
public class FileWritingMessageHandlerFactoryBean implements FactoryBean<FileWritingMessageHandler>, BeanFactoryAware {
private volatile FileWritingMessageHandler handler;
@@ -61,6 +60,7 @@ public class FileWritingMessageHandlerFactoryBean implements FactoryBean<FileWri
private final Object initializationMonitor = new Object();
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.file.entries;
import org.springframework.beans.factory.InitializingBean;
@@ -20,7 +21,6 @@ import org.springframework.beans.factory.InitializingBean;
import java.util.ArrayList;
import java.util.List;
/**
* A convenience base class for any {@link EntryListFilter} whose criteria can be
* evaluated against each File in isolation. If the entire List of files is
@@ -31,14 +31,18 @@ import java.util.List;
* @author Josh Long
*/
public abstract class AbstractEntryListFilter<T> implements InitializingBean, EntryListFilter<T> {
public abstract boolean accept(T t);
/**
* subclasses may override this if initialization is required
*/
public void afterPropertiesSet() throws Exception {
}
/**
* {@inheritDoc}
*/
public List<T> filterEntries(T[] entries) {
List<T> accepted = new ArrayList<T>();
if (entries != null) {
for (T t : entries) {
if (this.accept(t)) {
@@ -46,11 +50,12 @@ public abstract class AbstractEntryListFilter<T> implements InitializingBean, En
}
}
}
return accepted;
}
public void afterPropertiesSet() throws Exception {
// its all you!
}
/**
* subclasses must implement this method
*/
public abstract boolean accept(T entry);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2008 the original author or authors.
* Copyright 2002-2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,18 +16,19 @@
package org.springframework.integration.file.entries;
/**
* Simple NOOP implementation for {@link org.springframework.integration.file.entries.EntryListFilter} implementation.
* Suitable as a default in implementations.
* Simple NO-OP implementation of {@link org.springframework.integration.file.entries.EntryListFilter}.
* Suitable as a default.
*
* @author Iwein Fuld
* @author Josh Long
* @param <T>
*/
public class AcceptAllEntryListFilter<T> extends AbstractEntryListFilter<T> {
@Override
public boolean accept(T t) {
public boolean accept(T entry) {
return true;
}
}

View File

@@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.file.entries;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* {@link EntryListFilter} that passes files only one time. This can
* conveniently be used to prevent duplication of files, as is done in
@@ -30,16 +30,18 @@ import java.util.concurrent.LinkedBlockingQueue;
* @since 1.0.0
*/
public class AcceptOnceEntryFileListFilter<T> extends AbstractEntryListFilter<T> {
private final Queue<T> seen;
private final Object monitor = new Object();
/**
* Creates an {@link org.springframework.integration.file.entries.AcceptOnceEntryFileListFilter} that is based on a bounded queue. If the
* queue overflows, files that fall out will be passed through this filter
* again if passed to the {@link #filterEntries(Object[])} method.
* Creates an {@link org.springframework.integration.file.entries.AcceptOnceEntryFileListFilter}
* that is based on a bounded queue. If the queue overflows, files that fall out will be passed
* through this filter again if passed to the {@link #filterEntries(Object[])} method.
*
* @param maxCapacity the maximum number of Files to maintain in the 'seen'
* queue.
* @param maxCapacity the maximum number of Files to maintain in the 'seen' queue.
*/
public AcceptOnceEntryFileListFilter(int maxCapacity) {
this.seen = new LinkedBlockingQueue<T>(maxCapacity);
@@ -52,18 +54,18 @@ public class AcceptOnceEntryFileListFilter<T> extends AbstractEntryListFilter<T>
this.seen = new LinkedBlockingQueue<T>();
}
public boolean accept(T pathname) {
synchronized (this.monitor) {
if (seen.contains(pathname)) {
if (this.seen.contains(pathname)) {
return false;
}
if (!seen.offer(pathname)) {
seen.poll();
seen.add(pathname);
if (!this.seen.offer(pathname)) {
this.seen.poll();
this.seen.add(pathname);
}
return true;
}
}
}

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.file.entries;
import org.springframework.beans.factory.InitializingBean;
@@ -30,8 +31,10 @@ import java.util.*;
* @param <T>
*/
public class CompositeEntryListFilter<T> implements EntryListFilter<T> {
private final Set<EntryListFilter<T>> fileFilters;
public CompositeEntryListFilter() {
this.fileFilters = new LinkedHashSet<EntryListFilter<T>>();
}
@@ -40,23 +43,9 @@ public class CompositeEntryListFilter<T> implements EntryListFilter<T> {
this.fileFilters = new LinkedHashSet<EntryListFilter<T>>(fileFilters);
}
@SuppressWarnings("unchecked")
public List<T> filterEntries(T[] entries) {
Assert.notNull(entries, "'files' should not be null");
List<T> leftOver = Arrays.asList(entries);
for (EntryListFilter<T> fileFilter : this.fileFilters) {
T[] ts = (T[]) leftOver.toArray();
leftOver = fileFilter.filterEntries(ts);
}
return leftOver;
}
@SuppressWarnings("unchecked") //to please the eclipse compiler
public CompositeEntryListFilter<T> addFilter(EntryListFilter<T> filter) {
return this.addFilters(filter);
return this.addFilters(Collections.singletonList(filter));
}
/**
@@ -76,19 +65,31 @@ public class CompositeEntryListFilter<T> implements EntryListFilter<T> {
* @param filtersToAdd a list of filters to add
* @return this CompositeEntryListFilter instance with the added filters
*/
@SuppressWarnings("unchecked")
public CompositeEntryListFilter<T> addFilters(Collection<? extends EntryListFilter<T>> filtersToAdd) {
for (EntryListFilter<? extends T> elf : filtersToAdd)
for (EntryListFilter<? extends T> elf : filtersToAdd) {
if (elf instanceof InitializingBean) {
try {
((InitializingBean) elf).afterPropertiesSet();
} catch (Exception e) {
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}
this.fileFilters.addAll(filtersToAdd);
return this;
}
@SuppressWarnings("unchecked")
public List<T> filterEntries(T[] entries) {
Assert.notNull(entries, "'files' should not be null");
List<T> leftOver = Arrays.asList(entries);
for (EntryListFilter<T> fileFilter : this.fileFilters) {
T[] ts = (T[]) leftOver.toArray();
leftOver = fileFilter.filterEntries(ts);
}
return leftOver;
}
}

View File

@@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.file.entries;
import java.util.List;
/**
* Strategy interface for filtering entries representing files on a local or remote file system. This is a generic
* variant of FileListFilter that also works with references to remote files.
@@ -26,16 +26,15 @@ import java.util.List;
*
* @author Josh Long
* @author Iwein Fuld
*
* @since 2.0.0
*
* @since 2.0
* @see org.springframework.integration.file.filters.FileListFilter
*/
public interface EntryListFilter<T> {
/**
* Filters out entries and returns the entries that are left in a list, or an
* empty list when a null is passed in.
* empty list when null is passed in.
*/
List<T> filterEntries(T[] entries);
}

View File

@@ -13,22 +13,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.file.entries;
/**
* Responsible for coercing a String identification out of the T entry.
*
* @author Josh Long
* @param <T> the type of entry (there's an implementation for FTP, SFTP, and plain-old java.io.Files)
* @param <T> the type of entry (there's an implementation for FTP, SFTP, and plain-old java.io.Files)
*/
public interface EntryNamer<T> {
/**
* This is the one place I couldn't spackle over the interface differences between an FTPFile (FTP adapter), File (File adapter), and LsEntry (SFTP adapter)
* with generics alone. So we have a typed strategy implementation for accessing a property ....
* with generics alone. So we have a typed strategy implementation for accessing a property....
*
* @param entry the entry in a file system listing
* @return the String name that might be used to reference that entry or to do regular expression checks against
*/
String nameOf(T entry);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2008 the original author or authors.
* Copyright 2002-2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,16 +18,16 @@ package org.springframework.integration.file.entries;
import java.io.File;
/**
* {@link java.io.File} implementation of the {@link org.springframework.integration.file.entries.EntryNamer} strategy.
* <p/>
* This part feels a little over-engineered...
*
* {@link java.io.File}-based implementation of the {@link EntryNamer} strategy.
*
* @author Josh Long
* @since 2.0
*/
public class FileEntryNamer implements EntryNamer<File> {
public String nameOf(File entry) {
return (entry != null) ? entry.getName() : null;
}
}

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.file.entries;
import org.springframework.beans.factory.InitializingBean;
@@ -21,7 +22,6 @@ import org.springframework.util.StringUtils;
import java.util.regex.Pattern;
/**
* Filters a listing of entries (T) by qualifying their 'name' (as determined by {@link org.springframework.integration.file.entries.EntryNamer})
* against a regular expression (an instance of {@link java.util.regex.Pattern})
@@ -29,13 +29,16 @@ import java.util.regex.Pattern;
* @author Iwein Fuld
* @author Josh Long
* @param <T> the type of entry
*
* @since 2.0.0
* @since 2.0
*/
public class PatternMatchingEntryListFilter<T> extends AbstractEntryListFilter<T> implements InitializingBean {
private Pattern pattern;
private String patternExpression;
private EntryNamer<T> entryNamer;
private volatile EntryNamer<T> entryNamer;
private volatile Pattern pattern;
private volatile String patternExpression;
public PatternMatchingEntryListFilter(EntryNamer<T> en, String p) {
this.entryNamer = en;
@@ -47,6 +50,11 @@ public class PatternMatchingEntryListFilter<T> extends AbstractEntryListFilter<T
this.pattern = p;
}
public void setEntryNamer(EntryNamer<T> entryNamer) {
this.entryNamer = entryNamer;
}
public void setPattern(Pattern pattern) {
this.pattern = pattern;
}
@@ -64,11 +72,8 @@ public class PatternMatchingEntryListFilter<T> extends AbstractEntryListFilter<T
}
@Override
public boolean accept(T t) {
return (t != null) && this.pattern.matcher(this.entryNamer.nameOf(t)).matches();
public boolean accept(T entry) {
return (entry != null) && this.pattern.matcher(this.entryNamer.nameOf(entry)).matches();
}
public void setEntryNamer(EntryNamer<T> entryNamer) {
this.entryNamer = entryNamer;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2008 the original author or authors.
* Copyright 2002-2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,10 +18,9 @@ package org.springframework.integration.file.entries;
import org.springframework.util.Assert;
/**
* this simply takes an {@link org.springframework.integration.file.entries.EntryListFilter}
* and produces an object that can field just <b>one</b> argument instea of an array
* This simply takes an {@link org.springframework.integration.file.entries.EntryListFilter}
* and produces an object that can field just <b>one</b> argument instead of an array.
*
* @author Josh Long
*/
@@ -30,17 +29,19 @@ public class SingleEntryAdaptingEntryListFilter<T> extends AbstractEntryListFilt
/**
* the {@link org.springframework.integration.file.entries.EntryListFilter} that you'd like to delegate to
*/
private volatile EntryListFilter<T> entryFilter;
private volatile EntryListFilter<T> entryListFilter;
public SingleEntryAdaptingEntryListFilter(EntryListFilter<T> ef) {
this.entryFilter = ef;
Assert.notNull(this.entryFilter, "the entryFilter can't be null");
public SingleEntryAdaptingEntryListFilter(EntryListFilter<T> entryListFilter) {
Assert.notNull(entryListFilter, "entryListFilter must not be null");
this.entryListFilter = entryListFilter;
}
@Override
@SuppressWarnings("unchecked")
public boolean accept(T t) {
T[] ts = (T[]) new Object[]{t};
return this.entryFilter.filterEntries(ts).size() == 1;
return this.entryListFilter.filterEntries(ts).size() == 1;
}
}

View File

@@ -0,0 +1,190 @@
/*
* Copyright 2002-2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.file.synchronization;
import org.springframework.core.io.Resource;
import org.springframework.integration.MessagingException;
import org.springframework.integration.endpoint.AbstractEndpoint;
import org.springframework.integration.file.entries.AcceptAllEntryListFilter;
import org.springframework.integration.file.entries.EntryListFilter;
import org.springframework.scheduling.Trigger;
import org.springframework.util.Assert;
import java.util.concurrent.ScheduledFuture;
/**
* Base class charged with knowing how to connect to a remote file system,
* scan it for new files and then download the files.
* <p/>
* The implementation should run through any configured
* {@link org.springframework.integration.file.entries.EntryListFilter}s to
* ensure the entry is acceptable.
*
* @author Josh Long
*/
public abstract class AbstractInboundRemoteFileSystemSychronizer<T> extends AbstractEndpoint {
/**
* Should we <emphasis>delete</emphasis> the <b>source</b> file? For an FTP
* server, for example, this would delete the original FTPFile instance.
*/
protected boolean shouldDeleteSourceFile;
/**
* The directory to which we write our synchronizations.
*/
protected volatile Resource localDirectory;
/**
* An {@link EntryListFilter} that runs against the <emphasis>remote</emphasis> file system view.
*/
protected volatile EntryListFilter<T> filter = new AcceptAllEntryListFilter<T>();
/**
* The {@link ScheduledFuture} instance we get when we
* schedule our {@link SynchronizeTask}
*/
protected ScheduledFuture<?> scheduledFuture;
/**
* The {@link EntryAcknowledgmentStrategy} implementation.
*/
protected EntryAcknowledgmentStrategy<T> entryAcknowledgmentStrategy;
public void setEntryAcknowledgmentStrategy(EntryAcknowledgmentStrategy<T> entryAcknowledgmentStrategy) {
this.entryAcknowledgmentStrategy = entryAcknowledgmentStrategy;
}
public void setShouldDeleteSourceFile(boolean shouldDeleteSourceFile) {
this.shouldDeleteSourceFile = shouldDeleteSourceFile;
}
public void setLocalDirectory(Resource localDirectory) {
this.localDirectory = localDirectory;
}
public void setFilter(EntryListFilter<T> filter) {
this.filter = filter;
}
/**
* @param usefulContextOrClientData
* this is context information to be passed to the individual {@link EntryAcknowledgmentStrategy}.
* {@link EntryAcknowledgmentStrategy#acknowledge(Object, Object)} will be called in line with the
* {@link org.springframework.integration.core.MessageSource#receive()} call so this could conceivably
* be a 'live' stateful client (a connection?) that is inappropriate to cache as it has per-request state.
* @param t
* leverages strategy implementations to enable different
* behavior. It's a hook to the entry ({@link T}) after it's been
* successfully downloaded. Conceptually, you might delete the
* remote one or rename it, etc.
* @throws Throwable
* escape hatch exception, let the adapter deal with it.
*/
protected void acknowledge(Object usefulContextOrClientData, T t) throws Throwable {
Assert.notNull(this.entryAcknowledgmentStrategy != null,
"entryAcknowledgmentStrategy can't be null!");
this.entryAcknowledgmentStrategy.acknowledge(usefulContextOrClientData, t);
}
/**
* {@inheritDoc}
*/
protected void doStart() {
if (this.entryAcknowledgmentStrategy == null) {
this.entryAcknowledgmentStrategy = new EntryAcknowledgmentStrategy<T>() {
public void acknowledge(Object o, T msg) {
// no-op
}
};
}
this.scheduledFuture = this.getTaskScheduler().schedule(new SynchronizeTask(), this.getTrigger());
}
/**
* {@inheritDoc}
*/
protected void doStop() {
if (this.scheduledFuture != null) {
this.scheduledFuture.cancel(true);
}
}
/**
* Returns the {@link Trigger} that dictates how frequently the trigger should fire.
*/
protected abstract Trigger getTrigger();
/**
* This is the callback where we need the implementation to do some specific work
*/
protected abstract void syncRemoteToLocalFileSystem() throws Exception;
/**
* This {@link Runnable} is launched as a background thread and is used to manage the
* {@link AbstractInboundRemoteFileSystemSychronizer#localDirectory} by queueing and
* delivering accumulated files as possible.
*/
class SynchronizeTask implements Runnable {
public void run() {
try {
syncRemoteToLocalFileSystem();
}
catch (RuntimeException e) {
throw e;
}
catch (Exception e) {
throw new MessagingException("failure occurred in synchronization task", e);
}
}
}
/**
* Strategy interface to expose a hook for dispatching, moving, or deleting
* the file once it has been delivered. This will typically be a NOOP for the
* implementation. Adapters should (for consistency) expose an attribute
* dictating whether the adapter will delete the <emphasis>source</emphasis>
* entry on the remote file system. This is the file-system version of an
* <code>ack-mode</code>. Future implementations should consider exposing a
* custom attribute that plugs a custom {@link EntryAcknowledgmentStrategy}
* into the pipeline and also some more advanced scenarios (i.e., 'move file
* to another folder on delete ', or 'rename on delete')
*
* @param <T> the entry type (file, sftp, ftp, ...)
*/
public static interface EntryAcknowledgmentStrategy<T> {
/**
* Semantics are simple. You get a pointer to the entry just processed
* and any kind of helper data you could ask for. Since the strategy is
* a singleton and the clients you might ask for as context data are
* pooled, it's not recommended that you try to cache them.
*
* @param useful
* any context data
* @param msg
* the data / file / entry you want to process -- specific to subclasses
* @throws Exception in case of an error while acknowledging
*/
void acknowledge(Object useful, T msg) throws Exception;
}
}

View File

@@ -0,0 +1,165 @@
/*
* Copyright 2002-2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.file.synchronization;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Arrays;
import java.util.regex.Pattern;
import org.springframework.core.io.Resource;
import org.springframework.integration.Message;
import org.springframework.integration.MessagingException;
import org.springframework.integration.core.MessageSource;
import org.springframework.integration.endpoint.MessageProducerSupport;
import org.springframework.integration.file.FileReadingMessageSource;
import org.springframework.integration.file.entries.AcceptOnceEntryFileListFilter;
import org.springframework.integration.file.entries.CompositeEntryListFilter;
import org.springframework.integration.file.entries.EntryListFilter;
import org.springframework.integration.file.entries.FileEntryNamer;
import org.springframework.integration.file.entries.PatternMatchingEntryListFilter;
/**
* Factors out the common logic between the FTP and SFTP adapters. Designed to
* be extensible to handle adapters whose task it is to synchronize a remote
* file system with a local file system (NB: this does *NOT* handle pushing
* files TO the remote file system that exist uniquely in the local file system.
* It only handles pulling from the remote file system - as you would expect
* from an 'inbound' adapter).
* <p/>
* The base class supports configuration of whether the remote file system and
* local file system's directories should be created on start (what 'creating a
* directory' means to the specific adapter is of course implementaton
* specific).
* <p/>
* This class is to be used as a pair with an implementation of
* {@link AbstractInboundRemoteFileSystemSychronizer}. The synchronizer must
* handle the work of actually connecting to the remote file system and
* delivering new {@link File}s.
*
* @author Josh Long
*/
public abstract class AbstractInboundRemoteFileSystemSynchronizingMessageSource<Y, T extends AbstractInboundRemoteFileSystemSychronizer<Y>>
extends MessageProducerSupport implements MessageSource<File> {
/**
* Extension used when downloading files. We change it right after we know it's downloaded.
*/
public static final String INCOMPLETE_EXTENSION = ".INCOMPLETE";
/**
* Should the endpoint attempt to create the local directory and/or the remote directory?
*/
protected volatile boolean autoCreateDirectories = true;
/**
* An implementation that will handle the chores of actually connecting to and synching up
* the remote file system with the local one, in an inbound direction.
*/
protected volatile T synchronizer;
/**
* Directory to which things should be synched locally.
*/
protected volatile Resource localDirectory;
/**
* The actual {@link FileReadingMessageSource} that monitors the local filesystem once files are synched.
*/
protected volatile FileReadingMessageSource fileSource;
/**
* The predicate to use in scanning the remote File system for downloads.
*/
protected EntryListFilter<Y> remotePredicate;
public void setAutoCreateDirectories(boolean autoCreateDirectories) {
this.autoCreateDirectories = autoCreateDirectories;
}
public void setSynchronizer(T synchronizer) {
this.synchronizer = synchronizer;
}
public void setLocalDirectory(Resource localDirectory) {
this.localDirectory = localDirectory;
}
public void setRemotePredicate(EntryListFilter<Y> remotePredicate) {
this.remotePredicate = remotePredicate;
}
@Override
protected void onInit() {
try {
if (this.remotePredicate != null) {
this.synchronizer.setFilter(this.remotePredicate);
}
if (this.localDirectory != null && !this.localDirectory.exists()) {
if (this.autoCreateDirectories) {
if (logger.isDebugEnabled()) {
logger.debug("The '" + this.localDirectory + "' directory doesn't exist; Will create.");
}
this.localDirectory.getFile().mkdirs();
}
else {
throw new FileNotFoundException(this.localDirectory.getFilename());
}
}
/**
* Make sure the remote files get here.
*/
this.synchronizer.setLocalDirectory(this.localDirectory);
this.synchronizer.setTaskScheduler(this.getTaskScheduler());
this.synchronizer.setBeanFactory(this.getBeanFactory());
this.synchronizer.setPhase(this.getPhase());
this.synchronizer.setBeanName(this.getComponentName());
/**
* Forwards files once they ultimately appear in the {@link #localDirectory}.
*/
this.fileSource = new FileReadingMessageSource();
this.fileSource.setFilter(this.buildFilter());
this.fileSource.setDirectory(this.localDirectory.getFile());
this.fileSource.afterPropertiesSet();
this.synchronizer.afterPropertiesSet();
}
catch (RuntimeException e) {
throw e;
}
catch (Exception e) {
throw new MessagingException("Failure during initialization of MessageSource for: "
+ this.getComponentType(), e);
}
}
public Message<File> receive() {
return this.fileSource.receive();
}
@SuppressWarnings("unchecked")
private EntryListFilter<File> buildFilter() {
FileEntryNamer fileEntryNamer = new FileEntryNamer();
Pattern completePattern = Pattern.compile("^.*(?<!" + INCOMPLETE_EXTENSION + ")$");
return new CompositeEntryListFilter<File>(Arrays.asList(
new AcceptOnceEntryFileListFilter<File>(),
new PatternMatchingEntryListFilter<File>(fileEntryNamer, completePattern)));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2009 the original author or authors.
* Copyright 2002-2010 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -35,7 +35,6 @@ import static org.mockito.Mockito.*;
* @author Iwein Fuld
* @author Mark Fisher
*/
@SuppressWarnings("unchecked")
@RunWith(MockitoJUnitRunner.class)
public class FileReadingMessageSourceTests {
@@ -76,7 +75,7 @@ public class FileReadingMessageSourceTests {
@Test
public void requeueOnFailure() throws Exception {
when(inputDirectoryMock.listFiles()).thenReturn(new File[]{fileMock});
Message received = source.receive();
Message<File> received = source.receive();
assertNotNull(received);
source.onFailure(received);
assertEquals(received.getPayload(), source.receive().getPayload());

Some files were not shown because too many files have changed in this diff Show More