Just to note that I wont be moderating comments very frequently over the holidays. So don't worry if I don't post a comment right away, I will get around to them in the new year....
Happy Christmas and a Happy New Year for one and all.
Just to note that I wont be moderating comments very frequently over the holidays. So don't worry if I don't post a comment right away, I will get around to them in the new year....
Happy Christmas and a Happy New Year for one and all.
Well not 100% true, the first one did just do hello world to check it all worked. as you will have seen in previous posts; but this is the first time I have tried to use JAX-RS in anger.
So the problem I was trying to solve is that whilst our bug system allows you to do lots of static queries they don't give you a feel for what bugs have been updated when. So I wanted to put together some Atom feeds of recently updated bugs that I am associated with to give me a better overview. This seems like a job of JAX-RS.
Now the JAX-RS examples do use the Rome libraries; but I followed the example of this blog and simple generated a JAXB content model from the Atom Schema. This gave me a basis of my application; but I needed to work out what my resource URL looks like. My first guess had:
http://localhost:7101/bugfeed/reportedBy/gdavison
But to my eyes this didn't seem right as reportedBy is a property of the user, and I needed some room for expansion. Also it take some time to swap your thinking around from RPC/OO to REST style of thinking....
http://localhost:7101/bugfeed/user/gdavison/repotedBy
Having decided that I need to create my top level class. You will see that I also created a bug resource which is a simple redirection to the bug web site currently. It would also be possible to return computer readable xml of json objects depending on the content type asked for; but at the moment I am going to assume real human users.
Now you can match complex paths in one step with JAX-RS, for example "user/{username}/reportedBy" but I wanted more resource steps for later flexibility. So we just return a POJO which represents the user.
@Path("/") public class BugDBFeed { @Path("/bug/{bug_num}") @GET public Response getBug(@PathParam("bug_num") String bugNum) { return Response.temporaryRedirect( UriBuilder.fromUri("https://xxxx/query") .queryParam("bugNo",bugNum).build()).build(); } @Path("/user/{username}/") public UserResource getUser( @PathParam("username") String username) { return new UserResource(username); } }
There really isn't anything here, at the moment the user resource doesn't produce anything - it just represents part of the resource path in the URL. It does expose three sub paths which represent different ways I could be associated with a bug. Compare that with the "getBug" method previously where it has a @GET on it to say what HTTP method it accepts.
public class UserResource { private String username; public UserResource(String username) { super(); this.username = username; } @Path("/assigned") public UpdateResource getAssignedBugs() { return new UpdateResource(username, "programmer"); } @Path("/reportedBy") public UpdateResource getReportedByBugs() { return new UpdateResource(username,"rptd_by"); } @Path("/supportContact") public UpdateResource getSuuportedContactBugs() { return new UpdateResource(username,"support_contact"); } }
Now the next level of resource classes is where it becomes really interesting as we start to produce new content. The getBugsAtom method declares that it returns an xml type and JAX-RS will convert the JAXB classes in the background for you. You will notice that there are two parameters injected: the first in UriInfo which gives you a context to build uris that point to other resources; and the second is an optional query parameter on the feed to control the number of days you want the query to go back. You can inject quite a bit of information into parameters, even as far as details such as cookies.
UriInfo is a really powerful class for referencing other resources, in this case we are asking to the root of the application so that we can us that bug indirection used in the top level class. Notice like most object here they use the builder pattern to reduce the number of code steps required.
The rest of the code is straight forward SQL queries and creating the right POJO classes to later marshal into XML. JAX-RS does most of the work for you. I couldn't get the DataSource to inject properly, this might have been because I was only using Jersey 0.9; but most likely it required further integration into the server for this to work. No matter JNDI can do the same work.
public class UpdateResource { // @Resource(name = "jdbc/BugDS") // DataSource ds; private String username; private String property; public UpdateResource(String username, String property) { super(); this.property = property; this.username = username; } @GET @Produces("application/atom+xml") public Feed getBugsAtom( @javax.ws.rs.core.Context UriInfo info, @QueryParam(value = "days") @DefaultValue("10") int days) { try { ObjectFactory fact = new ObjectFactory(); Feed feed = fact.createFeed(); // List<Object> authorOrCategoryOrContributor = feed.getAuthorsAndCategoriesAndContributors(); PersonType personType = fact.createPersonType(); TextType name = fact.createTextType(); name.getContent().add(username); personType.getNamesAndUrisAndEmails().add(fact.createFeedTitle(name)); authorOrCategoryOrContributor.add(fact.createFeedAuthor(personType)); // Put in a title // TextType title = fact.createTextType(); title.getContent().add(info.getAbsolutePath().toASCIIString()); authorOrCategoryOrContributor.add(fact.createFeedTitle(title)); // Put some entries in // Connection otherConnection = getConnection("java:comp/env/jdbc/BugDS"); try { //Connection connection = dbConnection.getConnection(); OraclePreparedStatement prep = (OraclePreparedStatement)otherConnection.prepareCall("..."); try { prep.setStringAtName("nxuser", username.toUpperCase()); prep.setIntAtName("nxdays", days); // Last thirty days ResultSet rs = prep.executeQuery(); try { while (rs.next()) { // Entry entry = fact.createEntry(); authorOrCategoryOrContributor.add(entry); // Get data out of the columns String bugNumber = Integer.toString(rs.getInt(1)); String titleText = rs.getString(2); GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("PST")); Date date = rs.getTimestamp(3, calendar); DateTimeType updatedOn = fact.createDateTimeType(); String status = Integer.toString(rs.getInt(4)); // TextType entryTtitle = fact.createTextType(); entryTtitle.getContent().add(status + " " + titleText + " on " + date); entry.getAuthorsAndCategoriesAndContents() .add(fact.createEntryTitle(entryTtitle)); LinkType entryLink = fact.createLinkType(); UriBuilder toBuild = info.getBaseUriBuilder(); entryLink.setHref(toBuild.path("bug") .path(bugNumber).build().toASCIIString()); entry.getAuthorsAndCategoriesAndContents() .add(fact.createEntryLink(entryLink)); calendar.setTime(date); XMLGregorianCalendar updated = DatatypeFactory.newInstance().newXMLGregorianCalendar(calendar); updatedOn.setValue(updated); entry.getAuthorsAndCategoriesAndContents() .add(fact.createEntryUpdated(updatedOn)); } } finally { rs.close(); } } finally { prep.close(); } } finally { otherConnection.close(); } // return feed; } catch (Exception ex) { throw new RuntimeException(ex); } } private Connection getConnection(String dataSourceLocation) throws NamingException, SQLException { Connection conn = null; // Get a context for the JNDI look up Context ctx = new InitialContext(); // Look up a data source javax.sql.DataSource ds = (javax.sql.DataSource)ctx.lookup(dataSourceLocation); // Create a connection object conn = ds.getConnection(); return conn; } }
So after working with an playing with the service for a little while I was ready to try in Thunderbird where all my other feeds are. It turns out that at least 2.x doesn't like all Atom feed, so I decided that rather than try to find another reader that instead I would also produce a RSS stream. One of the cool things about JAX-RS is that you can very easily overload a give resource with different media types depending in the client accept headers.
I decided that I would much rather use XSL rather than write all that JAXB code again for RSS. So I dug up a link on the internet and started work. It turns out that the JAX-RS method can return a simple source object so all we really needed to do was to marshal the Atom class, transform and return. Thunderbird will now get a RSS feed which at least in my case it seemed to prefer.
@GET @Produces("application/rss+xml") public Source getBugsRSS( @javax.ws.rs.core.Context UriInfo info, @QueryParam(value = "days") @DefaultValue("10") int days) throws ... { // Get the atom version // Feed feed = getBugsAtom(info, days); // Jaxb it DOMResult atomModel = new DOMResult(); JAXBContext jc = JAXBContext.newInstance(ObjectFactory.class); jc.createMarshaller().marshal( feed, atomModel); // Transform it // Transformer t = TransformerFactory.newInstance().newTransformer( new StreamSource( UpdateResource.class.getResourceAsStream("atom2rss.xsl"))); DOMResult rssModel = new DOMResult(); t.transform( new DOMSource(atomModel.getNode()), rssModel); // Return RSS version // return new DOMSource(rssModel.getNode()); }
It is worth taking a quick look at the WADL view of the application one it was deployed. It am not sure why the last two paths are not correctly described but can get the general idea. I would be relatively easy to parse this and generate a client of some kind; but this isn't part of the JAX-RS project as yet.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <application xmlns="http://research.sun.com/wadl/2006/10"> <doc xmlns:jersey="http://jersey.dev.java.net/" jersey:generatedBy="Jersey: 0.9-ea 08/22/2008 04:48 PM"/> <resources base="http://xxxxx:7001/bugfeed/"> <resource path="/"> <resource path="/bug/{bug_num}"> <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="bug_num"/> <method name="GET" id="getBug"> <response> <representation mediaType="*/*"/> </response> </method> </resource> <resource path="/user/{username}/"> <param xmlns:xs="http://www.w3.org/2001/XMLSchema" type="xs:string" style="template" name="username"/> <resource path="/assigned"> <method name="GET" id="getBugsRSS"> <request> <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="10" type="xs:int" style="query" name="days"/> </request> <response> <representation mediaType="application/xml+rss"/> </response> </method> <method name="GET" id="getBugsAtom"> <request> <param xmlns:xs="http://www.w3.org/2001/XMLSchema" default="10" type="xs:int" style="query" name="days"/> </request> <response> <representation mediaType="application/xml+atom"/> </response> </method> </resource> <resource path="/reportedBy"/> <resource path="/supportContact"/> </resource> </resource> </resources> </application>
Well that is the entire application, it is quite simple in that it doesn't put, update or delete resources; but I was trying to solve a problem not write a tutorial! (A nice tutorial is here.) I also haven't taken any care to map exception to anything more user friendly. This can be done easily with the use of @Provider/ExceptionMapper to convert you application specific error to a WebApplicationException instance. But for the moment the default 500 error will have to do.
Update 13 Feb '09 Whoops there was a typo in the @Produces annotation I had "xml+atom" rather than "atom+xml" and the same mix up for rss. I have fixed this typo now. The early version of Jersey I wrote this with didn't really mind; but 1.0.x onwards is more strict. In case you are interested the error message I got back from Jersey when requesting context type application/xml+atom was:
A message body writer for Java type, class org.w3._2005.atom.Feed, and MIME media type, application/xml+atom, was not found
The actual mime type wild card that would match JAXB elements is application/*+xml. Took a little while to work our the mistake; but it was something I have learnt from.
This blog represents the non-policy part of my demo at UKOUG'08 this year. In that scenario we had a simple loan application that made use of a "mocked" version of a credit rating service during developement and a more real service for production. (Mocked up using the HTTP Analyzer in this case) This following screen grab gives you an idea of the project structure:
If we ignore the deploy directory for now we can take a look at the code in LoanApprover. The key points to notice here is the @WebServiceRef which injects the endpoint from the web.xml and the callout method to provision security. The important thing here is that the new WSDL might have different security policy; but it is possible to write the code fairly generically to take this into account because of the way that the web logic client auto-configures. In this case the mock service had no security and the production service had WS-Security with plain username/password.
@WebService public class LoansApprover { /** * Credit rating service injected from web.xml **/ @WebServiceRef(name = "CreditRatingService") CreditRating creditRating; /** * @return Loan application with approval code if * approved. */ public LoanApprovalReponse approveLoan(LoanApplication la) { LoanApprovalReponse approvalReponse = new LoanApprovalReponse(); // Provision credentials // CredentialProvisioner.provisionPort(creditRating); // Start looking up credit rating ResponseratingReponse = creditRating.lookupRatingAsync( la.getSsid()); // Retrieve any customer records // ... // Process Credit rating try { int creditRating = ratingReponse.get(); if (creditRating > 30) { approvalReponse.setApprovalCode( UUID.randomUUID().toString()); } } catch (Exception e) { e.printStackTrace(); // Do nothing } return approvalReponse; } }
So the @WebServiceRef uses a name resource, defined some where in JNDI. In this case it is in web.xml for an EJB it would be in ejb-jar.xml. Lets take a quick look, note that because we specify the service class name the deployer runtime code will work out to inject the port to save you a little bit of typing.
<?xml version = '1.0' encoding = 'windows-1252'?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"> ... <service-ref> <service-ref-name>CreditRatingService</service-ref-name> <service-interface>com.somecreditrating.xmlns.rating.CreditRating_Service</service-interface> </service-ref> </web-app>
So in development the service class has all the hard coded references to the mock service deployed on localhost. For production we are going to change the WSDL that the proxy loads.
If you look at the project structure you will notice that there is an production.ear file in a rather novel directory structure. You will notice that the ear lives under the "app" sub dir and there is a "plan" directory. This structure will be created for you if you do something like change a policy on a deployed web service; but we need this all in place from the start. (Take a look at the weblogic documentation for more information on this structure)
Now you have to create a plan.xml, see this information on how to create one from the command line, but in the best Blue Peter tradition here is one I created earlier. The key parts are the variable definition and the assignment later on that uses xpath to create a new wsdl reference. Be carefull with the xpath expression as a space in the wrong place can cause the expression not to work. When deployed the proxy created for @WebServiceRef will read from this WSDL not the hard coded one.
<?xml version='1.0' encoding='UTF-8'?> <deployment-plan xmlns="http://www.bea.com/ns/weblogic/deployment-plan" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.bea.com/ns/weblogic/deployment-plan http://www.bea.com/ns/weblogic/deployment-plan/1.0/deployment-plan.xsd" global-variables="false"> <application-name>production</application-name> <variable-definition> <variable> <name>CreditRatingService</name> <value>http://www.somecreditrating.com/xmlns/rating?WSDL</value> </variable> </variable-definition> <module-override> <module-name>production.ear</module-name> <module-type>ear</module-type> <module-descriptor external="false"> <root-element>weblogic-application</root-element> <uri>META-INF/weblogic-application.xml</uri> </module-descriptor> <module-descriptor external="false"> <root-element>application</root-element> <uri>META-INF/application.xml</uri> </module-descriptor> <module-descriptor external="true"> <root-element>wldf-resource</root-element> <uri>META-INF/weblogic-diagnostics.xml</uri> </module-descriptor> </module-override> <module-override> <module-name>LoanApplication-LoanApprover-context-root.war</module-name> <module-type>war</module-type> <module-descriptor external="false"> <root-element>weblogic-web-app</root-element> <uri>WEB-INF/weblogic.xml</uri> </module-descriptor> <module-descriptor external="false"> <root-element>web-app</root-element> <uri>WEB-INF/web.xml</uri> <variable-assignment> <name>CreditRatingService</name> <xpath>/web-app/service-ref/[service-ref-name="CreditRatingService"]/wsdl-file</xpath> <operation>add</operation> </variable-assignment> </module-descriptor> <module-descriptor external="true"> <root-element>weblogic-webservices</root-element> <uri>WEB-INF/weblogic-webservices.xml</uri> </module-descriptor> <module-descriptor external="false"> <root-element>webservices</root-element> <uri>WEB-INF/webservices.xml</uri> </module-descriptor> <module-descriptor external="true"> <root-element>webservice-policy-ref</root-element> <uri>WEB-INF/weblogic-webservices-policy.xml</uri> </module-descriptor> </module-override> <config-root>D:\prom-demo\jdeveloper\mywork\LoanApplication\deploy\production\.\plan</config-root> </deployment-plan>
Now the deployment plan dir also allows you to add files to the classpath. In this case we have a simple properties file that provides a username and password to be later configured as a credential provider. (Do consider storing your password somewhere more safe) For completeness lets just check out the code that provisions the proxy with the username and password token. Nothing very special there.
private static Properties passwordStore; public static void provisionPort(Object port) { assert port instanceof BindingProvider; BindingProvider bp = (BindingProvider)port; // Get class and find the port name, for the moment // lets just cheat // Class proxyPort = port.getClass(); Class ifClass = proxyPort.getInterfaces()[0]; String portName = ifClass.getName(); // Do some clever lookup in a keystore // Pairusernamepassword = getCredentials(portName); if (usernamepassword!=null) { ClientUNTCredentialProvider credentialProvider = new ClientUNTCredentialProvider(usernamepassword.getLeft().getBytes(), usernamepassword.getRight().getBytes()); bp.getRequestContext().put( WSSecurityContext.CREDENTIAL_PROVIDER_LIST, Collections.singletonList(credentialProvider)); } } private static Pair getCredentials(String portName) { if (passwordStore==null) { passwordStore = new Properties(); URL password_properties = CredentialProvisioner.class.getResource( "password.properties"); if (password_properties!=null) { try { passwordStore.load(password_properties.openStream()); } catch (IOException e) { e.printStackTrace(); } } } // Get the packed version of the string // String packedVersion = passwordStore.getProperty(portName); if (packedVersion!=null) { String splitVersion[] = packedVersion.split("::"); assert splitVersion.length == 2; return new Pair (splitVersion[0],splitVersion[1]); } else { return null; } }
Unfortunatelly you cannot deploy this special directory structure from within JDeveloper, so you need to go into the weblogic console, Deployments->Install, but as you can see from the screen grab it will recognize the app/plan combo as a directory you can deploy:
And that is that, you application will deploy, read the new WSDL and start to use the external service.
This post builds on code in my previous blog so take a quick look at that for some context.
One of the problems with dealing with asynchronous responses is having to deal with correlating the responses. Now you could do this using the message ID from the original message; but this is a pain and not very application specific. Instead the WS-Addressing specification allows you to specify application specific reference parameters that are used in the response. This can be any XML element so you have a lot of control over what is sent back.
For the purposes of this blog lets create a simple header bean class to be used in the communication. It needs to be a JAXB bean with @XmlRootElement and a namespace. (If you leave the namespace blank you get a null pointer exception somewhere deep within JAX-WS-RI):
import javax.xml.bind.annotation.XmlRootElement; @XmlRootElement(namespace="http://www.example.org") public class HeaderBean { String property; public void setProperty(String property) { this.property = property; } public String getProperty() { return property; } }
Let modify the callback class from the previous blog to read the header value. In this case I am using @WebParam(header=true) for the convenience of mapping the header to java.
@WebService(targetNamespace = "http://project1/", name = "Hello") @XmlSeeAlso( { ObjectFactory.class }) public class HelloCallback { @WebMethod @Action(input = "http://project1/Hello/sayHelloResponse") @RequestWrapper(localName = "sayHelloResponse", targetNamespace = "http://project1/", className = "project1.SayHelloResponse") @Oneway public void sayHello( @WebParam(targetNamespace = "", name = "return") String ret, @WebParam(header = true, targetNamespace = "http://www.example.org", name = "headerBean") HeaderBean hb) { System.out.println(ret); } }
Now we modify the client to include the extra header. Unfortunately WSEndpointReference is in terms of Element so we have a little bit of extra code to marshal the bean in to XML. In this simple case it might have been easier to just write the XML directly; but I would see it is worth the extra work for more complicated constructs:
public static void main(String[] args) throws JAXBException { helloService = new HelloService(); Endpoint e = Endpoint.publish("http://localhost:7890/endpoint", new HelloCallback()); HeaderBean n = new HeaderBean(); n.setProperty("applicationModuleID"); DOMResult result = new DOMResult(); JAXBContext.newInstance(HeaderBean.class).createMarshaller().marshal(n, result); WSEndpointReference replyTo = new WSEndpointReference(e.getEndpointReference((Element)result.getNode().getFirstChild())); Hello hello = helloService.getHelloPort(new WebServiceFeature[] { new OneWayFeature(true, replyTo) }); // This method will return null // Object ret = hello.sayHello("Bob"); }
So here is the outgoing message, note that the extra header information is passed in the reference parameters field. You need to consider whether this information need to be encrypted in some way.
<?xml version = '1.0' encoding = 'UTF-8'?> <S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"> <S:Header> <To xmlns="http://www.w3.org/2005/08/addressing">http://localhost.localdomain:7101/Application1-Project1-context-root/HelloPort</To> <Action xmlns="http://www.w3.org/2005/08/addressing">http://project1/Hello/sayHelloRequest</Action> <ReplyTo xmlns="http://www.w3.org/2005/08/addressing"> <Address>http://localhost:7890/endpoint</Address> <ReferenceParameters> <headerBean xmlns:ns2="http://www.w3.org/2005/08/addressing" xmlns="http://www.example.org" xmlns:wsa="http://www.w3.org/2005/08/addressing"> <property>applicationModuleID</property> </headerBean> </ReferenceParameters> <Metadata/> </ReplyTo> <MessageID xmlns="http://www.w3.org/2005/08/addressing">uuid:3b9e7b20-3aa0-4a4a-9422-470fa7b9ada1</MessageID> </S:Header> <S:Body> <ns2:sayHello xmlns:ns2="http://project1/"> <arg0>Bob</arg0> </ns2:sayHello> </S:Body> </S:Envelope>
And here is the response:
<?xml version = '1.0' encoding = 'UTF-8'?> <S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"> <S:Header> <To xmlns="http://www.w3.org/2005/08/addressing">http://localhost:7890/endpoint</To> <Action xmlns="http://www.w3.org/2005/08/addressing">http://project1/Hello/sayHelloResponse</Action> <MessageID xmlns="http://www.w3.org/2005/08/addressing">uuid:9d0be951-79fc-4a56-b3e6-4775bde2bd82</MessageID> <RelatesTo xmlns="http://www.w3.org/2005/08/addressing">uuid:3b9e7b20-3aa0-4a4a-9422-470fa7b9ada1</RelatesTo> <headerBean xmlns:wsa="http://www.w3.org/2005/08/addressing" wsa:IsReferenceParameter="1" xmlns:ns2="http://www.w3.org/2005/08/addressing" xmlns="http://www.example.org"> <property>applicationModuleID</property> </headerBean> </S:Header> <S:Body> <ns2:sayHelloResponse xmlns:ns2="http://project1/"> <return>Bob</return> </ns2:sayHelloResponse> </S:Body> </S:Envelope>
The reason I am looking into this is that we were wondering what the best way would be for a ADF JSF page to invoke a BPEL service and properly channel the response back to the originating AM. Using the reference parameters makes this job much easier as you can more easily relate it to the domain.
I was looking over the WSTF pages and found the use cases that include a single port reply to another address. I did wonder if you could safely invoke this using JAX-WS. So I decided to find out.
To make things simple I used the following hello world with addressing turned on. Note this is not a true asynchronous service as the rely is sent before the server returns with a 202 code; but it is close enough for the purposes of this presentation.
@Addressing @WebService public class Hello { @Action(input="http://project1/Hello/sayHello" output="http://project1/Hello/sayHelloReponse") public String sayHello(String name) { return name; } }
So I published this service and then generated a client to another project. We need to create a suitable callback service so first of all you have to find the port SEI:
@WebService(wsdlLocation="HelloService.wsdl", targetNamespace="http://project1/", name="Hello") @XmlSeeAlso( { project1.ObjectFactory.class }) public interface Hello { @WebMethod @Action(input="http://project1/Hello/sayHelloRequest", output="http://project1/Hello/sayHelloResponse") @ResponseWrapper(localName="sayHelloResponse", targetNamespace="http://project1/", className="project1.SayHelloResponse") @RequestWrapper(localName="sayHello", targetNamespace="http://project1/", className="project1.SayHello") @WebResult(targetNamespace="") public String sayHello(@WebParam(targetNamespace="", name="arg0") String arg0); }
Then I used Refactor->Duplicate to create a copy and converted the interface into a class. As you can see you need to turn the interface in-side out. The changes I made are:
If would be really nice to have this done automatically when the usingAddressing tag is found in the WSDL, perhaps it would be possible to extend the web services generation tool to do this.
@WebService(targetNamespace = "http://project1/", name = "Hello") @XmlSeeAlso( { ObjectFactory.class }) @Addressing public class HelloCallback { @WebMethod @Action(input = "http://project1/Hello/sayHelloResponse") @RequestWrapper(localName = "sayHelloResponse", targetNamespace = "http://project1/", className = "project1.SayHelloResponse") @Oneway public void sayHello(@WebParam(targetNamespace = "", name = "return") String ret) { System.out.println(ret); } }
And then the code is really quite simple, in this case we publish the endpoint and then invoke the service. Note we use the @OneWayFeature to populate the required WS-Addressing features, this is a bit naughty as it is part of the -RI and doesn't support faults. For the purposes of this blog though it makes the code a mite easier to read.
The other thing to note is that the call to sayHello returns a "null" object, you can use the MessageContext to check the response code if you want to confirm a 202; but otherwise the rest of the action is deferred to the callback class at this point.
public static void main(String[] args) { helloService = new HelloService(); Endpoint e = Endpoint.publish( "http://localhost:7890/endpoint", new HelloCallback()); WSEndpointReference replyTo = new WSEndpointReference(e.getEndpointReference()); Hello hello = helloService.getHelloPort(new WebServiceFeature[] { new OneWayFeature(true, replyTo)}); // This method will return null // Object ret = hello.sayHello("Bob"); // Check the message response code // Integer responseCode = (Integer) ((BindingProvider)hello).getReponseContext() .get(MessageContext.HTTP_RESPONSE_CODE); }
One final note is that you need to force the client to exit as Endpoint.publish create a non-daemon thread.
So I little while ago someone asked me how to using a weblogic JAX-WS client to connect to a service that required plain text WS-Security username / token headers. It turns out that non of the default policies that come with weblogic will work in this configuration because they all have a sensible level of security applied. This doesn't help when trying to deal with an older system so in the end I modified the existing Https username token policy and came up with this file:
<?xml version="1.0"?> <wsp:Policy xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy" xmlns:sp="http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200512" > <sp:SupportingTokens> <wsp:Policy> <sp:UsernameToken sp:IncludeToken="http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200512/IncludeToken/AlwaysToRecipient"> <wsp:Policy> <sp:WssUsernameToken10/> </wsp:Policy> </sp:UsernameToken> </wsp:Policy> </sp:SupportingTokens> </wsp:Policy>
Now there is a lot on the weblogic documentation site about configuring security; but it took a while to absorb into something that worked. (My fault I guess) The most convenient option is to place the policy file in the "policies" sub dir under WEB-INF. So you ended up with a project that looks kinda like this:
You web service can then just refer to the policy using the "policy:" url as you would the built in policies:
package project1; import javax.jws.WebService; import weblogic.jws.Policies; import weblogic.jws.Policy; @WebService @Policies( { @Policy(uri = "policy:usernametoken.xml") } ) public class Hello { public String sayHello(String name) { return "Hello " + name; } }
In JDeveloper at least you have to type this by hand because the custom policy button is very broken in the current released version of JDeveloper. This we hope to address at some point in the future. This is all that is required to set up the service to use the default credential providers. Check out the link above for more information on configuring this.
So if you need to actually invoke this service you can skip some steps if the policies are advertised in the WSDL. Fortunately they are by default so when the JAX-WS client starts it will parse the polices from the WSDL and set itself up to expect the correct configuration to work. Now you might expect BindingProvider username / password properties to be used here; but instead you need to configure a credential provider as follows. Note there is no reference to the policy here.
public static void main(String[] args) { helloService = new HelloService(); Hello hello = helloService.getHelloPort(); // Add your code to call the desired methods. List<CredentialProvider> credProviders = new ArrayList<CredentialProvider>(); String username = "weblogic"; String password = "weblogic"; CredentialProvider cp = new ClientUNTCredentialProvider(username.getBytes(), password.getBytes()); credProviders.add(cp); Map<String, Object> rc = ((BindingProvider)hello).getRequestContext(); rc.put(WSSecurityContext.CREDENTIAL_PROVIDER_LIST, credProviders); hello.sayHello("Bob"); }
In some cases you might have a WSDL that for various reasons doesn't publish the policies as you get them through some other side channel. This is fortunately relatively easy to fix with the use of the ClientPolicyFeature as shown in the next code snippet:
public static void main(String[] args) { helloService = new HelloService(); ClientPolicyFeature cpf = new ClientPolicyFeature(); InputStream asStream = HelloPortClient.class.getResourceAsStream("usernametoken.xml"); cpf.setEffectivePolicy(new InputStreamPolicySource(asStream)); Hello hello = helloService.getHelloPort( new WebServiceFeature[] {cpf}); // Add your code to call the desired methods. List<CredentialProvider> credProviders = new ArrayList<CredentialProvider>(); String username = "weblogic"; String password = "weblogic"; CredentialProvider cp = new ClientUNTCredentialProvider(username.getBytes(), password.getBytes()); credProviders.add(cp); Map<String, Object> rc = ((BindingProvider)hello).getRequestContext(); rc.put(WSSecurityContext.CREDENTIAL_PROVIDER_LIST, credProviders); hello.sayHello("Bob"); }
And that is it, quite simple once I got used to the weblogic way of working.
Really strange thing, every time I re-open my VNC session I find that characters have appeared while I was disconnected. Today I found:
9,.2abdhimnpqstuxy
Not quite sure that it is trying to tell me though...
Unfortunately the Maven extension for JDeveloper still hasn't seen the light of day; but I was wanting to work with the source for Jersey so I need to get something working otherwise getting all the dependencies by hand would be a nightmare. For Maven 1.x you used to be able to just run "jdev:jdev" to generate projects and workspaces in much the same way "eclipse:eclipse" does the same for erm, well Eclipse.
For 2.x there is no easy to use command in the central repositories, but you can use a plugin from the "myfaces" to generate JDeveloper 10.1.3 project. For of all you need to check out the myfaces build extensions from:
svn checkout http://svn.apache.org/repos/asf/myfaces/trinidad-maven/\ trunk/maven-jdev-plugin maven-jdev-plugin -r 711393
If you don't use the revision modifier then you might get the tip which in my case didn't have all the dependencies published yet. You can try a more recent version if you like, let me know how you got on. Then when you have the source just perform the "mvn install" step to put this plugin in your local repository. (Under ~/.m2/repository on my Linux box)
You can now run the command "mvn -Djdev.release=11.1.1.0.0 org.apache.myfaces.trinidadbuild:maven-jdev-plugin:jdev" in the root of your project and you get a project file for each sub pom and a workspace to contain them all.
I couple of items of note:
Having said that this is a good first step to get working. You can use "Tools->External Tools" so you can run Maven commands from inside of JDeveloper.
I have uploaded the slide set from UKOUG'08 for a service called SlideShare which is useful as blogspot wont let you just upload those files yourself. Not so much detail in the testing presentation, that was mostly demos, but there are some code snippets in the promotion presentation that could be useful.
If you want to store a password in your deployed application, perhaps for an external service you need to make sure it is saved somewhere safe. Internally at oracle we are never allowed to store a password in plain text, ideally it has to be stored somewhere encoded to prevent a idle HD search from finding it. This made me wonder how I could store a simple character password in a keystore for later use to invoke a remote web service.
Keys are complicated beasties as you can see from the number of sub classes in java. In the most part they are going to be public/private key pair which are the mainstay of the internet. But if you just want to store a simple password you need to create a SecretKey.
Now the complicated this is that you have to create a key spec, which is transparent and create new opaque secret key from it for storage in the keystore. In case you are wonder PBE stands for "Password Based Encryption" which is the simplest type I could find in the security spec that would suit my needs.
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBE"); SecretKey generatedSecret = factory.generateSecret(new PBEKeySpec( password));
It turns out that the default "JKS" KeyStore type will not allow you to store a SecretKey. So instead you need the code to create the right kind. It might look something like this:
KeyStore ks = KeyStore.getInstance("JCEKS"); ks.load(null, keyStorePassword); PasswordProtection keyStorePP = new PasswordProtection(keyStorePassword); SecretKeyFactory factory = SecretKeyFactory.getInstance("PBE"); SecretKey generatedSecret = factory.generateSecret(new PBEKeySpec( password)); ks.setEntry(key, new SecretKeyEntry( generatedSecret), keyStorePP); ks.save(…, // Output stream for keystore keyStorePassword);
Note that you can't just add PBEKeySpec to a keystore because while it is a "Key" it is not of type "SecretKey" so you have to convert. When loading the password out from the keystore you need the SecretKeyFactory again to convert the SecretKey back into the PBEKeySpec that you can extract the password from.
KeyStore ks = KeyStore.getInstance("JCEKS"); ks.load(…, // InputStrweam to keystore keyStorePassword); SecretKeyFactory factory = SecretKeyFactory.getInstance("PBE"); SecretKeyEntry ske = (SecretKeyEntry)ks.getEntry(key, keyStorePP); PBEKeySpec keySpec = (PBEKeySpec)factory.getKeySpec( ske.getSecretKey(), PBEKeySpec.class); char[] password = keySpec.getPassword();
Now in general use the JCEKS keystore is far more secure than the default JKS format. The reason it is not the default is back in the old days they encryption was treated as a munition. I think this is only a problem for a few black listed countries now - although this restriction seems a bit pointless given the reach of the internet.
One of the more useful things about web services endpoint is normally you can stick "?WSDL" on the end and get a description of the service. Now for rest full services you find that WSDL does work to described resource based services so sun came up with the WADL language.
The Jersey implementation will automatically generate this description for you simply GET application.wadl from the root of your service, here is an example for a simple hello world example: (More complicated example)
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <application xmlns="http://research.sun.com/wadl/2006/10"> <doc xmlns:jersey="http://jersey.dev.java.net/" jersey:generatedBy="Jersey: 0.9-ea 08/22/2008 04:48 PM"/> <resources base="http://localhost:7101/Application1-Project1-context-root/"> <resource path="/helloworld"> <method name="GET" id="getClichedMessage"> <response> <representation mediaType="text/plain"/> </response> </method> </resource> </resources> </application>
You can also request the WADL for a particular resource; but you have to use the OPTIONS method rather than a GET, here is a quite screen grab in the HTTP Analyzer. Annoyingly enough it doesn't use pretty formatting:
It is possible to use this description to generate a java client; but this is part of Jersey rather than part of the JAX-RS standard to be aware it is more subject to change.
Update: For those in the MS world the WCF REST framework uses a /help postfix to generate similar information. In a human rather than computer readable format from what I can see.
So far today I appear to have a demo Jinx. In the SOA presentation I managed to pick a broken BPEL service so was unable to get a response back. In the JDeveloper SCM presentation there was no network connection for me in the room so I ended up having to sit in the stairwell some distance from the room. Then it went a bit Pete Tong in the room and I was left outside as a confused bunny. Still people came out of that one smiling!
Still I have the mulit-teir webservice deployment in my presentation this afternoon, what could possible go wrong...
Hiding out in the Speakers lounge now, the trade floor was full or lots of very bored sales people, many having Wii tournaments. Feel bad when they find out I work for Oracle so perhaps am not the best person to talk to. I did notice that the RittmanMead stand has the best beer fridge on the floor. Most Sci-Fi technology goes to KeyWay who were showing a Flash card with R/W bandwidths of 500Mbit +. Parrallel writes to 22 flash chips whifch is very nice.
Right well no presentation for a little while, better delete some email. I am not on until 17:30 so goodness knows how many people that will net given the general feeling that everybody has already gone home. But then I have an hour or so in the Xmas market until I get the train home.
So first day at UKOUG'08, just in the speakers loung waiting for the entertainment to start. The main impression of the day is just how quiet it seems compared to last year. Although I can say for sure there were also quite a few more cancellations from speakers. Still those that have made it here seem to be in good spirits.
Our testing presentation went well today, relatively full room and people seem to be really interested in what we had to say. No takers to come and talk to us in the Oracle lounge afterwards, I don't think anybody has taken this up for any of the bg O speakers, but otherwise the feedback was good. One demo failure; but we considered this a sucess when testing so many bits and bobs. We overan a bit but that was because both mysef and Goeff were really quite relaxed and therefore took more time than we really had to talk.
Prep for tommorrows presentation is going well, although we are havig to rely on a machine back in the office for the SOA demo which has some risks. Still should be okay.
Last thing the RittmanMead chaps have been nice enough to leave some beer in the speakers lounge. Unforunately it is a little bit flowery for my liking. See we have a free beer token for the 25th aniversary of UKOUG that starts soon. There is some Jazz laid on and if Susan Duncan had anyting to to with it we should be in for a treat.
Right I hear the ents being anounced....
The views expressed on this blog are my own and do not necessarily reflect the views of Oracle.