diff --git a/README.adoc b/README.adoc index ee2db50..c65024c 100644 --- a/README.adoc +++ b/README.adoc @@ -1,24 +1,36 @@ --- -tags: [] -projects: [] +tags: [SOAP] +projects: [spring-ws] --- :spring_version: current :spring_boot_version: 1.0.2.RELEASE +:spring_ws_version: 2.0 :Component: http://docs.spring.io/spring/docs/{spring_version}/javadoc-api/org/springframework/stereotype/Component.html :Controller: http://docs.spring.io/spring/docs/{spring_version}/javadoc-api/org/springframework/stereotype/Controller.html :DispatcherServlet: http://docs.spring.io/spring/docs/{spring_version}/javadoc-api/org/springframework/web/servlet/DispatcherServlet.html :SpringApplication: http://docs.spring.io/spring-boot/docs/{spring_boot_version}/api/org/springframework/boot/SpringApplication.html :ResponseBody: http://docs.spring.io/spring/docs/{spring_version}/javadoc-api/org/springframework/web/bind/annotation/ResponseBody.html :EnableAutoConfiguration: http://docs.spring.io/spring-boot/docs/{spring_boot_version}/api/org/springframework/boot/autoconfigure/EnableAutoConfiguration.html +:Endpoint: http://docs.spring.io/spring-ws/sites/{spring_ws_version}/apidocs/org/springframework/ws/server/endpoint/annotation/Endpoint.html +:PayloadRoot: http://docs.spring.io/spring-ws/sites/{spring_ws_version}/apidocs/org/springframework/ws/server/endpoint/annotation/PayloadRoot.html +:RequestPayload: http://docs.spring.io/spring-ws/sites/{spring_ws_version}/apidocs/org/springframework/ws/server/endpoint/annotation/RequestPayload.html +:ResponsePayload: http://docs.spring.io/spring-ws/sites/2.0/apidocs/org/springframework/ws/server/endpoint/annotation/ResponsePayload.html +:MessageDispatcherServlet: http://docs.spring.io/spring-ws/sites/2.0/apidocs/org/springframework/ws/transport/http/MessageDispatcherServlet.html +:DefaultMethodEndpointAdapter: http://docs.spring.io/spring-ws/sites/2.0/apidocs/org/springframework/ws/server/endpoint/adapter/DefaultMethodEndpointAdapter.html +:ApplicationContext: http://docs.spring.io/spring/docs/{spring_version}/javadoc-api/org/springframework/web/context/ApplicationContext.html +:DefaultWsdl11Definition: http://docs.spring.io/spring-ws/sites/2.0/apidocs/org/springframework/ws/wsdl/wsdl11/DefaultWsdl11Definition.html +:XsdSchema: http://docs.spring.io/spring-ws/sites/2.0/apidocs/org/springframework/xml/xsd/XsdSchema.html :toc: :icons: font :source-highlighter: prettify -:project_id: draft-gs-template -This guide walks you through the process of creating a Spring application. +:project_id: gs-soap-service +This guide walks you through the process of creating a SOAP-based web service server with Spring. == What you'll build -You'll build a Spring application. +You will build a server that exposes European countries data using WSDL-based SOAP web service. + +In order to simplify the example you will use hardcoded data for couple of countries only: United Kingdom, Spain and Poland. == What you'll need @@ -39,7 +51,7 @@ include::https://raw.githubusercontent.com/spring-guides/getting-started-macros/ `build.gradle` // AsciiDoc source formatting doesn't support groovy, so using java instead -[source,java] +[source,java,indent=0] ---- include::initial/build.gradle[] ---- @@ -48,18 +60,104 @@ include::https://raw.githubusercontent.com/spring-guides/getting-started-macros/ [[initial]] -== Create a resource controller +== Add Spring-WS dependency + +Project you create will be based on Spring WS. You need to add dependency to Spring WS in your build file. For Maven: + +[source,xml,indent=0] +---- +include::complete/pom.xml[tags=springws] +---- + +If you use gradle, make sure to have dependency to `spring-ws-core` added: +[source,java,indent=0] +---- +include::complete/build.gradle[tags=dependencies] +---- + +== Create XSD + +Web service domain is defined in XSD file that is later on wrapped with WSDL. Create XSD file describing our domain, which is a service exposing one method returning basic information about requested country: name, population, capital and currency. + +`src/main/resources/countries.xsd` +[source,xml,indent=0] +---- +include::complete/src/main/resources/countries.xsd[] +---- + +== Generate domain classes based on a XSD + +XSD is just the beginning. The next step is to generate Java classes from it. The right approach is go generate classes during build time using Maven or Gradle plugin. + +Plugin configuration for Maven: + +[source,xml,indent=0] +---- +include::complete/pom.xml[tags=xsd] +---- + +Generated classes are placed in `target/generated-sources/jaxb/` directory. + +To do the same with gradle, first you need to configure JAXB in your build file: + +[source,java,indent=0] +---- +include::complete/build.gradle[tags=jaxb] +---- + +Next step is to add task `genJaxb` used by gradle to generate Java classes: + +[source,java,indent=0] +---- +include::complete/build.gradle[tags=xsd] +---- + +As gradle does not have a JAXB plugin (yet), it involves an ant task, which makes it a bit more complex than in maven. + +In both cases, the JAXB domain object generation process has been wired into the build tool’s lifecycle so there are no extra steps to run. + +== Create country repository + +In order to provide data to web service, create country repository. In this guide you create dummy country repository implementation with hardcoded data. + +[source,java,indent=0] +---- +include::complete/src/main/java/hello/CountryRepository.java[] +---- + +== Create country service endpoint + +To create service endpoint, you have to create POJO class and annotate it with Spring WS annotations. + +[source,java,indent=0] +---- +include::complete/src/main/java/hello/CountryEndpoint.java[] +---- + +When Spring WS receives SOAP message it searches all defined endpoints for methods matching namespace and localPart. An endpoint is created typically by annotating class with the {Endpoint}[`@Endpoint`] annotation. + +In order to route incoming XML message to correct method use {PayloadRoot}[`@PayloadRoot`] annotation that indicates that all messages with namespace `http://spring.io/guides/gs-soap-service` and containing local name `getCountryRequest` will be routed to this method. + +{RequestPayload}[`@RequestPayload`] indicates that message will be mapped to `request` parameter. + +Analogously, {ResponsePayload}[`@ResponsePayload`] annotation makes Spring WS map returned value to response payload. + +== Configure web service components + +There are two efficient ways of configuring Spring WS beans: using XML namespaces or using Java config. In this tutorial you will create beans using plain Java. -Create a new controller for your Spring application: +Create a new class with Spring WS related beans configuration: -`src/main/java/hello/GreetingController.java` -[source,java] +[source,java,indent=0] ---- -include::complete/src/main/java/hello/GreetingController.java[] +include::complete/src/main/java/hello/WebServiceConfig.java[] ---- -NOTE: The above example does not specify `GET` vs. `PUT`, `POST`, and so forth, because `@RequestMapping` maps all HTTP operations by default. Use `@RequestMapping(method=GET)` to narrow this mapping. +* Spring WS uses different servlet type for handling SOAP messages: {MessageDispatcherServlet}[`MessageDispatcherServlet`]. It is important to inject and set {ApplicationContext}[`ApplicationContext`] to {MessageDispatcherServlet}[`MessageDispatcherServlet`]. Without that, Spring WS will not detect Spring Beans automatically. +* {DefaultMethodEndpointAdapter}[`DefaultMethodEndpointAdapter`] configures annotation driven Spring WS programming model - makes possible to use for example {Endpoint}[`@Endpoint`] annotation +* define bean {DefaultWsdl11Definition}[`DefaultWsdl11Definition`] for exposing WSDL using {XsdSchema}[`XsdSchema`] +It's important to notice that you need to specify bean names for {MessageDispatcherServlet}[`MessageDispatcherServlet`] and {DefaultWsdl11Definition}[`DefaultWsdl11Definition`]. Bean names determine the URL under which web service and generated WSDL file is available. In this case, WSDL will be available under `http://:/ws/countries.wsdl` == Make the application executable @@ -67,14 +165,14 @@ Although it is possible to package this service as a traditional link:/understan `src/main/java/hello/Application.java` -[source,java] +[source,java,indent=0] ---- include::complete/src/main/java/hello/Application.java[] ---- The `main()` method defers to the {SpringApplication}[`SpringApplication`] helper class, providing `Application.class` as an argument to its `run()` method. This tells Spring to read the annotation metadata from `Application` and to manage it as a component in the link:/understanding/application-context[Spring application context]. -The `@ComponentScan` annotation tells Spring to search recursively through the `hello` package and its children for classes marked directly or indirectly with Spring's {Component}[`@Component`] annotation. This directive ensures that Spring finds and registers the `GreetingController`, because it is marked with `@Controller`, which in turn is a kind of `@Component` annotation. +The `@ComponentScan` annotation tells Spring to search recursively through the `hello` package and its children for classes marked directly or indirectly with Spring's {Component}[`@Component`] annotation. This directive ensures that Spring finds and registers the `CountryRepository` and `CountriesEndpoint`, because they are marked marked with `@Component` and `@Endpoint`, which in turn is a kind of `@Component` annotation. The {EnableAutoConfiguration}[`@EnableAutoConfiguration`] annotation switches on reasonable default behaviors based on the content of your classpath. For example, because the application depends on the embeddable version of Tomcat (tomcat-embed-core.jar), a Tomcat server is set up and configured with reasonable defaults on your behalf. And because the application also depends on Spring MVC (spring-webmvc.jar), a Spring MVC {DispatcherServlet}[`DispatcherServlet`] is configured and registered for you — no `web.xml` necessary! Auto-configuration is a powerful, flexible mechanism. See the {EnableAutoConfiguration}[API documentation] for further details. @@ -90,12 +188,35 @@ Logging output is displayed. The service should be up and running within a few s == Test the application -Now that the application is running, you can test it. +Now that the application is running, you can test it. Create a file `request.xml` containing example SOAP request: +[source,xml,indent=0] +---- +include::test/request.xml[] +---- + +Execute from command line `curl --header "content-type: text/xml" -d @request.xml "http://localhost:8080/ws/"`. As a result you should see the response: + +[source,xml] +---- + + + + + + Spain + 46704314 + Madrid + EUR + + + + +---- == Summary -Congratulations! You've just developed a Spring application! +Congratulations! You've developed Spring WS based SOAP service! All without single line of XML configuration! diff --git a/complete/build.gradle b/complete/build.gradle index d155b12..50a5d2c 100644 --- a/complete/build.gradle +++ b/complete/build.gradle @@ -13,19 +13,69 @@ apply plugin: 'eclipse' apply plugin: 'idea' apply plugin: 'spring-boot' -jar { - baseName = 'draft-gs-template' - version = '0.1.0' -} - repositories { mavenCentral() maven { url "http://repo.spring.io/libs-milestone" } } +// tag::xsd[] +task genJaxb { + ext.sourcesDir = "${buildDir}/generated-sources/jaxb" + ext.classesDir = "${buildDir}/classes/jaxb" + ext.schema = "src/main/resources/countries.xsd" + + outputs.dir classesDir + + doLast() { + project.ant { + taskdef name: "xjc", classname: "com.sun.tools.xjc.XJCTask", + classpath: configurations.jaxb.asPath + mkdir(dir: sourcesDir) + mkdir(dir: classesDir) + + xjc(destdir: sourcesDir, schema: schema) { + arg(value: "-wsdl") + produces(dir: sourcesDir, includes: "**/*.java") + } + + javac(destdir: classesDir, source: 1.6, target: 1.6, debug: true, + debugLevel: "lines,vars,source", + classpath: configurations.jaxb.asPath) { + src(path: sourcesDir) + include(name: "**/*.java") + include(name: "*.java") + } + + copy(todir: classesDir) { + fileset(dir: sourcesDir, erroronmissingdir: false) { + exclude(name: "**/*.java") + } + } + } + } +} +// end::xsd[] + +// tag::jaxb[] +configurations { + jaxb +} + +jar { + baseName = 'gs-soap-service' + version = '0.1.0' + from genJaxb.classesDir +} + +// tag::dependencies[] dependencies { compile("org.springframework.boot:spring-boot-starter-web") + compile("org.springframework.ws:spring-ws-core:2.1.4.RELEASE") + jaxb("com.sun.xml.bind:jaxb-xjc:2.2.4-1") + compile(files(genJaxb.classesDir).builtBy(genJaxb)) } +// end::dependencies[] +// end::jaxb[] task wrapper(type: Wrapper) { gradleVersion = '1.11' diff --git a/complete/pom.xml b/complete/pom.xml index fffd998..8d91774 100644 --- a/complete/pom.xml +++ b/complete/pom.xml @@ -4,7 +4,7 @@ 4.0.0 org.springframework - draft-gs-template + gs-soap-service 0.1.0 @@ -18,6 +18,13 @@ org.springframework.boot spring-boot-starter-web + + + org.springframework.ws + spring-ws-core + 2.1.4.RELEASE + + @@ -36,6 +43,25 @@ org.springframework.boot spring-boot-maven-plugin + + + + org.codehaus.mojo + jaxb2-maven-plugin + 1.6 + + + xjc + + xjc + + + + + ${project.basedir}/src/main/resources/ + + + diff --git a/complete/src/main/java/hello/CountryEndpoint.java b/complete/src/main/java/hello/CountryEndpoint.java new file mode 100644 index 0000000..5aff9b9 --- /dev/null +++ b/complete/src/main/java/hello/CountryEndpoint.java @@ -0,0 +1,30 @@ +package hello; + +import io.spring.guides.gs_soap_service.GetCountryRequest; +import io.spring.guides.gs_soap_service.GetCountryResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.ws.server.endpoint.annotation.Endpoint; +import org.springframework.ws.server.endpoint.annotation.PayloadRoot; +import org.springframework.ws.server.endpoint.annotation.RequestPayload; +import org.springframework.ws.server.endpoint.annotation.ResponsePayload; + +@Endpoint +public class CountryEndpoint { + private static final String NAMESPACE_URI = "http://spring.io/guides/gs-soap-service"; + + private CountryRepository countryRepository; + + @Autowired + public CountryEndpoint(CountryRepository countryRepository) { + this.countryRepository = countryRepository; + } + + @PayloadRoot(namespace = NAMESPACE_URI, localPart = "getCountryRequest") + @ResponsePayload + public GetCountryResponse getCountry(@RequestPayload GetCountryRequest request) { + GetCountryResponse response = new GetCountryResponse(); + response.setCountry(countryRepository.findCountry(request.getName())); + + return response; + } +} diff --git a/complete/src/main/java/hello/CountryRepository.java b/complete/src/main/java/hello/CountryRepository.java new file mode 100644 index 0000000..3fe343b --- /dev/null +++ b/complete/src/main/java/hello/CountryRepository.java @@ -0,0 +1,56 @@ +package hello; + +import io.spring.guides.gs_soap_service.Country; +import io.spring.guides.gs_soap_service.Currency; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.List; + +@Component +public class CountryRepository { + private static final List countries = new ArrayList(); + + @PostConstruct + public void initData() { + Country spain = new Country(); + spain.setName("Spain"); + spain.setCapital("Madrid"); + spain.setCurrency(Currency.EUR); + spain.setPopulation(46704314); + + countries.add(spain); + + Country poland = new Country(); + poland.setName("Poland"); + poland.setCapital("Warsaw"); + poland.setCurrency(Currency.PLN); + poland.setPopulation(38186860); + + countries.add(poland); + + Country uk = new Country(); + uk.setName("United Kingdom"); + uk.setCapital("London"); + uk.setCurrency(Currency.GBP); + uk.setPopulation(63705000); + + countries.add(uk); + } + + public Country findCountry(String name) { + Assert.notNull(name); + + Country result = null; + + for (Country country : countries) { + if (name.equals(country.getName())) { + result = country; + } + } + + return result; + } +} diff --git a/complete/src/main/java/hello/GreetingController.java b/complete/src/main/java/hello/GreetingController.java deleted file mode 100644 index 3d85f1a..0000000 --- a/complete/src/main/java/hello/GreetingController.java +++ /dev/null @@ -1,14 +0,0 @@ -package hello; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseBody; - -@Controller -public class GreetingController { - - @RequestMapping("/greeting") - public @ResponseBody String greeting() { - return "Hello World"; - } -} diff --git a/complete/src/main/java/hello/WebServiceConfig.java b/complete/src/main/java/hello/WebServiceConfig.java new file mode 100644 index 0000000..10ec129 --- /dev/null +++ b/complete/src/main/java/hello/WebServiceConfig.java @@ -0,0 +1,45 @@ +package hello; + +import org.springframework.boot.context.embedded.ServletRegistrationBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.core.io.ClassPathResource; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.ws.server.endpoint.adapter.DefaultMethodEndpointAdapter; +import org.springframework.ws.transport.http.MessageDispatcherServlet; +import org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition; +import org.springframework.xml.xsd.SimpleXsdSchema; +import org.springframework.xml.xsd.XsdSchema; + +@Configuration +public class WebServiceConfig { + @Bean(name = "ws") + public ServletRegistrationBean messageDispatcherServlet(ApplicationContext applicationContext) { + MessageDispatcherServlet servlet = new MessageDispatcherServlet(); + servlet.setApplicationContext(applicationContext); + + return new ServletRegistrationBean(servlet, "/ws/*"); + } + + @Bean + public DefaultMethodEndpointAdapter defaultMethodEndpointAdapter() { + return new DefaultMethodEndpointAdapter(); + } + + @Bean(name = "countries") + public DefaultWsdl11Definition defaultWsdl11Definition(XsdSchema countriesSchema) { + DefaultWsdl11Definition wsdl11Definition = new DefaultWsdl11Definition(); + wsdl11Definition.setPortTypeName("CountriesPort"); + wsdl11Definition.setLocationUri("/countries/"); + wsdl11Definition.setTargetNamespace("http://spring.io/guides/gs-soap-service"); + wsdl11Definition.setSchema(countriesSchema); + return wsdl11Definition; + } + + @Bean + public XsdSchema countriesSchema() { + return new SimpleXsdSchema(new ClassPathResource("countries.xsd")); + } +} diff --git a/complete/src/main/resources/countries.xsd b/complete/src/main/resources/countries.xsd new file mode 100644 index 0000000..e3a68e5 --- /dev/null +++ b/complete/src/main/resources/countries.xsd @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/initial/build.gradle b/initial/build.gradle index d155b12..85198e8 100644 --- a/initial/build.gradle +++ b/initial/build.gradle @@ -14,7 +14,7 @@ apply plugin: 'idea' apply plugin: 'spring-boot' jar { - baseName = 'draft-gs-template' + baseName = 'gs-soap-service' version = '0.1.0' } diff --git a/initial/pom.xml b/initial/pom.xml index fffd998..10da5da 100644 --- a/initial/pom.xml +++ b/initial/pom.xml @@ -4,7 +4,7 @@ 4.0.0 org.springframework - draft-gs-template + gs-soap-service 0.1.0 @@ -29,8 +29,8 @@ - - maven-compiler-plugin + + maven-compiler-plugin org.springframework.boot diff --git a/test/request.xml b/test/request.xml new file mode 100644 index 0000000..7d30795 --- /dev/null +++ b/test/request.xml @@ -0,0 +1,8 @@ + + + + + Spain + + +