- First i changed the main or landing page of my app to mark the div where the new page should be inserted, i did that by setting value of id element to
pagePort
like this!DOCTYPE html> <html> <head> .... </head> <body onload="WL.Client.init({showLogger:true})" id="content" style="display: none"> <div data-role="page" id="page1"> <div data-theme="a" data-role="header"> <h3>Contact DB App</h3> </div> <div data-role="content" id="pagePort" > <div data-role="fieldcontain"> <fieldset data-role="controlgroup"> <label for="textinput1"> First Name </label> <input id="contactName" placeholder="" value="" type="text" /> </fieldset> </div> <a data-role="button" data-transition="fade" href="javascript:getContact()" id="searchContact"> Search Contact </a> <ul data-role="listview" data-inset="true" data-filter="true" id="displayContact"> </ul> </div> <div data-theme="a" data-role="footer"> <h3>Copyright stuff</h3> </div> </div> <script src="js/HelloDatabase.js"></script> <script src="js/messages.js"></script> <script src="js/auth.js"></script> </body> </html>
- Then i did create this new contactDetail.html page in the same directory as that of my first/landing page
The Details page shows the form with contact details and it has a Summary button that allows user to go back to the summary page, when you click on that button it will pass control to<div data-role="content" id="mainContent"> <div data-role="fieldcontain"> <fieldset data-role="controlgroup"> <label for="firstName"> First Name </label> <input id="firstName" placeholder="" value="" type="text" readonly="readonly" /> <label for="lastName"> Last Name </label> <input id="lastName" placeholder="" value="" type="text" readonly="readonly"/> <label for="email"> Email </label> <input id="email" placeholder="" value="" type="text" readonly="readonly"/> <a data-role="button" data-transition="fade" href="javascript:showSummary()" id="searchContact"> Summary </a> </fieldset> </div> </div>
showSummary()
method - I had to make changes to my main JavaScript function to make it look like this
I had to make quite a few changes in my JavaScript they are as followsvar busyIndicator; function wlCommonInit() { busyIndicator = new WL.BusyIndicator('page1'); } function getContact() { console.log("Entering getContact() REST service based version"); var contactName = $('contactName').getValue(); var invocationData = { adapter : "ContactWSService", procedure : "searchContact", parameters : [ contactName ] } var options = { onSuccess : loadContactSuccess, onFailure : loadContactFailure } busyIndicator.show(); WL.Client.invokeProcedure(invocationData, options); } function loadContactSuccess(result) { console.log("Inside loadContactSuccess " + result); var html = ''; try { if (result.status == 200) { var contactList = result.invocationResult.Envelope.Body.searchContactResponse.contactList; var i = 0; for (i = 0; i < contactList.length; i++) { var currentContact = contactList[i]; html = html + '<li><a href="javascript:showContactDetail(' + currentContact.contactId + ')">' + currentContact.firstName + ' ' + currentContact.lastName + '</a></li>'; } } jq("#displayContact").html(html); jq("#displayContact").listview('refresh'); busyIndicator.hide(); } catch (e) { busyIndicator.hide(); } } function showContactDetail(contactId) { console.log("Show Contact Detail is clicked " + contactId); WL.Page.load("contactDetail.html", { onComplete : function() { console.log("After fragment is loadded "); jq('#mainContent').trigger("create"); getContactDetails(contactId); }, onUnload : function() { console.log("After fragment is unloadded "); } }); } function showSummary() { WL.Page.load("contactSummary.html", { onComplete : function() { console.log("After fragment is loadded "); jq('#mainContent').trigger("create"); }, onUnload : function() { console.log("After fragment is unloadded "); } }); } function getContactDetails(contactId) { console.log("Entering getContactDetails()"); var invocationData = { adapter : "ContactWSService", procedure : "getContact", parameters : [ contactId ] } var options = { onSuccess : getContactDetailsSuccess, onFailure : getContactDetailsFailure } busyIndicator.show(); WL.Client.invokeProcedure(invocationData, options); } function getContactDetailsSuccess(result) { console.log("Entering getContactDetailsSuccess"); try { if (result.status == 200) { var displayContact = result.invocationResult.Envelope.Body.getContactResponse.contact; $('firstName').value=displayContact.firstName; $('lastName').value=displayContact.lastName; $('email').value=displayContact.email; } busyIndicator.hide(); } catch (e) { busyIndicator.hide(); } } function getContactDetailsFailure(result) { console.log("Entering getContactDetailsFailure"); }
loadContactSuccess
: TheloadContactSuccess
method gets called when you execute search and it gets back the result, this method generates one row each for the result. I changed this method so that when it was generating the list i did attachgetContactDetails(contactId)
method to each rowshowContactDetail
: TheshowContactDetail
method will get called when user clicks on any of the user name, when that happens i am callingWL.Page.load("contactDetails.html"
method which replaces the markup inside the page with contactDetail.html, this method also calls thegetContactDetails
which calls SOAP service with contactId to get details of the contact.showSummary
: TheshowSummary
method will be called when user clicks on the Summary button on the details page, it again callsWL.Page.load("contactSummary.html"
to replace the markup in the current page with the contact details
Creating multi page application in WorkLight
WorkLight allows you to create multi-page application, i wanted to try this feature, so i changed the Contact Search application that i created in Using BusyIndicator common control entry, so that, when executes a search for contact, it returns list of contact names, user can click on one of the contact name to go to the Contact Details page and that page has button that allows user to go back to Summary page.
This is how my summary page looks like
When you click on the arrow button next to any user name it takes you to the Details page for that contact which looks like this
You can click on the Summary button on the Details page to go back to the Contact Summary page. I followed these steps to create this sample application
Forcing jQuery Mobile to re-evaluate styles/theme on dynamically inserted content
Today i was trying to build a JQuery Mobile application that has multiple pages. When user clicks on the page switch button i take the markup on the new page and insert it in the old page. One problem that i noticed is when you insert HTML into a page and that HTML has jQuery UI widgets then those widgets do not get evaluated, instead jQuery Displays them without any styles.
You can solve this problem by calling the
$('changedDiv').trigger("create")
method with value of the div equal to id where the new markup got inserted
<div id='changedDiv'>
//Insert jQuery widgets here
</div>
Using WL.SimpleDialog() to display error
I wanted to figure out how to use WL.SimpleDialog() so i used it to change the contact application that i developed in Using BusyIndicator common control so that if there is JavaScript Exception during searching of contact it displays that error in Dialog Box like this
This is how my JavaScript looks like after the changes
var busyIndicator;
function wlCommonInit(){
busyIndicator = new WL.BusyIndicator('page1');
}
function getContact(){
console.log("Entering getContact() REST service based version");
var contactName = $('contactName').getValue();
var invocationData = {
adapter:"ContactWSService",
procedure:"searchContact",
parameters:[contactName]
}
var options ={
onSuccess:loadContactSuccess,
onFailure:loadContactFailure
}
busyIndicator.show();
WL.Client.invokeProcedure(invocationData, options);
}
function loadContactSuccess(result){
console.log("Inside loadContactSuccess " + result);
var html = '';
try{
if(result.status == 200){
var contactList = result.invocationResult.Envelope.Body.
searchContactResponse.contactList;
var i = 0;
for(i =0 ; i < contactList.length ; i++){
var currentContact = contactList[i];
html = html + '<li><a href="#">'+currentContact.firstName
+' ' +currentContact.lastName +'</a></li>';
}
}
jq("#displayContact").html(html);
jq("#displayContact").listview('refresh');
busyIndicator.hide();
}catch(e){
busyIndicator.hide();
displayError(e.toString());
}
}
function loadContactFailure(result){
console.log("Inside loadContactError " + result);
busyIndicator.hide();
displayError(result);
}
function displayError(errorString) {
var dialogTitle = "Error";
WL.SimpleDialog.show(dialogTitle, errorString, [ {
text : 'OK',
handler : simpleDialogButton1Click
}
]);
}
function simpleDialogButton1Click() {
}
First i did create a displayError()
method that takes error message as input and displays it in Modal dialog that has only one button. The simpleDialogButton1Click() would get called when user clicks on OK in the Error dialog but it does not do anything.
The displayError() method is getting called from the exception handler class as well as the loadContactFailure() class.
Using BusyIndicator common control
I wanted to try using the
BusyIndicator
so i decided to change the Contact Search application that i developed in Using JQuery Mobile in WorkLight application entry, so that as soon as user clicks on Search Contact it starts showing the busy indicator and that indicator stays still the results are updated.
I made following changes in my JavaScript file.
var busyIndicator;
function wlCommonInit(){
busyIndicator = new WL.BusyIndicator('page1');
}
function getContact(){
var contactName = $('contactName').getValue();
var invocationData = {
adapter:"ContactWSService",
procedure:"searchContact",
parameters:[contactName]
}
var options ={
onSuccess:loadContactSuccess,
onFailure:loadContactFailure
}
busyIndicator.show();
WL.Client.invokeProcedure(invocationData, options);
}
function loadContactSuccess(result){
console.log("Inside loadContactSuccess " + result);
var html = '';
try{
if(result.status == 200){
var contactList = result.invocationResult.Envelope.Body.searchContactResponse.contactList;
var i = 0;
for(i =0 ; i < contactList.length ; i++){
var currentContact = contactList[i];
html = html + '<li><a href="#">'+currentContact.firstName +' '
+currentContact.lastName +'</a></li>';
}
}
jq("#displayContact").html(html);
jq("#displayContact").listview('refresh');
busyIndicator.hide();
}catch(e){
busyIndicator.hide();
}
}
function loadContactFailure(result){
console.log("Inside loadContactError " + result);
busyIndicator.hide();
}
First i had to create object of WL.BusyIndicator
with value of page1
as input, page1 is the value of id attribute on enclosing div in my page. Without that i was getting JavaScript initialization error.
Then the getContact()
which is responsible for initiating the contact search call invokes the show()
method to start the BusyIndicator
. I am calling the hide() method of BusyIndicator
in the loadContactSuccess()
method which is a callback method that gets called once the results are ready and the UI is updated.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0,
minimum-scale=1.0, user-scalable=0" />
<title>HelloDatabase</title>
<link rel="shortcut icon" href="images/favicon.png" />
<link rel="apple-touch-icon" href="images/apple-touch-icon.png" />
<link rel="stylesheet" href="css/reset.css" />
<link rel="stylesheet" href="css/HelloDatabase.css" />
<link rel="stylesheet" href="css/jquery.mobile-1.0.min.css" />
<script src="js/jquery-1.7.1.min.js"></script>
<script>
var jq = jQuery.noConflict();
</script>
<script src="js/jquery.mobile-1.0.min.js"></script>
</head>
<body onload="WL.Client.init({showLogger:true})" id="content" style="display: none">
<div data-role="page" id="page1">
<div data-theme="a" data-role="header">
<h3>Contact DB App</h3>
</div>
<div data-role="content">
<div data-role="fieldcontain">
<fieldset data-role="controlgroup">
<label for="textinput1">
First Name
</label>
<input id="contactName" placeholder="" value="" type="text" />
</fieldset>
</div>
<a data-role="button" data-transition="fade"
href="javascript:getContact()" id="searchContact">
Search Contact
</a>
<ul data-role="listview" data-inset="true" data-filter="true" id="displayContact">
</ul>
</div>
<div data-theme="a" data-role="footer">
<h3>Copyright stuff</h3>
</div>
</div>
<script src="js/HelloDatabase.js"></script>
<script src="js/messages.js"></script>
<script src="js/auth.js"></script>
</body>
</html>
WorkLight SOAP Service debugging
I ran into few issues while working on Accessing SOAP service from the WorkLight app blog entry and these are the solutions to those problems
First problem was i had to build the SOAP request in JavaScript manually for use in the HTTP Adapter class and since this code runs on server i was not able to debug it, and i was not sure what message is getting built. So i used the
WL.Logger.debug("SOAP Request " + searchContactRequest)
call in my Adapter code and then when i hit the adapter it did write this log statement in \Worklight\server\log\server\server.log
directory like this
2012-03-29 15:40:30,469 DEBUG [developer] (pool-7-thread-2:0ad7210e-9ed8-4c58-9b38-853d1918131b) SOAP Request <soapenv:Envelope xmlns:q0="http://webspherenotes.com" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <soapenv:Header> </soapenv:Header> <soapenv:Body> <q0:searchContact> <arg0>at</arg0> </q0:searchContact> </soapenv:Body> </soapenv:Envelope>
The worklight server does take care of writing the response of the SOAP request into the same log file, so having log enabled for com.srndpt.adapters
helps
Also note that the WorkLight server takes care of converting the SOAP XML response into JSON
This is the response of my SOAP Service
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
- <S:Body>
- <ns2:searchContactResponse xmlns:ns2="http://webspherenotes.com">
- <contactList>
<contactId>1</contactId>
<email>sdpatil@gmail.com</email>
<firstName>Sunil</firstName>
<lastName>Patil</lastName>
</contactList>
- <contactList>
<contactId>2</contactId>
<email>patil.jiyas@gmail.com</email>
<firstName>Jiya</firstName>
<lastName>Patil</lastName>
</contactList>
- <contactList>
<contactId>3</contactId>
<email>patil.navyas@gmail.com</email>
<firstName>Navya</firstName>
<lastName>Patil</lastName>
</contactList>
</ns2:searchContactResponse>
</S:Body>
</S:Envelope>
The WorkLight server takes that response and converts it into JSON object and sticks it into Result element like this
{"responseID":"10","statusCode":200,"errors":[],"isSuccessful":true,"statusReason":"OK","Envelope":{"Body":{"searchContactResponse":{"ns2":"http://webspherenotes.com","contactList":[{"lastName":"Patil","contactId":"1","email":"sdpatil@gmail.com","firstName":"Sunil"},{"lastName":"Patil","contactId":"2","email":"patil.jiyas@gmail.com","firstName":"Jiya"},{"lastName":"Patil","contactId":"3","email":"patil.navyas@gmail.com","firstName":"Navya"}]}},"S":"http://schemas.xmlsoap.org/soap/envelope/"},"warnings":[],"info":[]}
Accessing SOAP service from the WorkLight app
In the Using JQuery Mobile in WorkLight application entry i built a WorkLight application that takes contact last name as input and uses it to search for contact by using WorkLight SQL adapter, i wanted to check if i can achieve same functionality by using SOAP service so these are the steps that i used
- First i did build a simple JAXWS service that takes last name of the contact as input parameter and returns list of contacts with matching last name, you can download the service that i used from here
- Next i used the WorkLight studio to create a HTTP Adapter, i used
ContactWSService
as name for that adapter - After creating the adapter i change the ContactWSService-impl.js that was generated to look like this
In order to make a SOAP request call you will have to create the SOAP message first, i used the Eclipse Web Services Explorer tool to first test my SOAP service and then copied the XML SOAP message that it used for request into my JavaScript file. Once i have the SOAP message in String format i used it to make HTTP POST callfunction searchContact(lastName) { var searchContactRequest = '<soapenv:Envelope '+ 'xmlns:q0="http://webspherenotes.com" '+ 'xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" '+ 'xmlns:xsd="http://www.w3.org/2001/XMLSchema" '+ 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> '+ '<soapenv:Header> '+ '</soapenv:Header> '+ '<soapenv:Body> '+ '<q0:searchContact> '+ '<arg0>'+lastName+'</arg0> '+ '</q0:searchContact> '+ '</soapenv:Body> '+ '</soapenv:Envelope> '; WL.Logger.debug("SOAP Request " + searchContactRequest); var input = { method : 'post', returnedContentType : 'xml', path : '/ManageContactWS/contactws', body:{ content: searchContactRequest.toString(), contentType: 'text/xml; charset=utf-8' } }; return WL.Server.invokeHttp(input); }
- Next i had to change the ContactWSService.xml the deployment descriptor for my adapter to declare
searchContact
procedure, after changes the file looks like this<?xml version="1.0" encoding="UTF-8"?> <wl:adapter name="ContactWSService" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:wl="http://www.worklight.com/integration" xmlns:http="http://www.worklight.com/integration/http"> <displayName>ContactWSService</displayName> <description>ContactWSService</description> <connectivity> <connectionPolicy xsi:type="http:HTTPConnectionPolicyType"> <protocol>http</protocol> <domain>localhost</domain> <port>9000</port> </connectionPolicy> <loadConstraints maxConcurrentConnectionsPerNode="2" /> </connectivity> <procedure name="searchContact"/> </wl:adapter>
- The last change was in the WorkLight application code where i make the adapter call and parse the returned results so that i can display them, This is how my .js file looks like
Important Note: By default the JAXWS wraps the return value in XML element namedfunction getContact(){ console.log("Entering getContact() REST service based version"); var contactName = $('contactName').getValue(); var invocationData = { adapter:"ContactWSService", procedure:"searchContact", parameters:[contactName] } var options ={ onSuccess:loadContactSuccess, onFailure:loadContactFailure } WL.Client.invokeProcedure(invocationData, options); } function loadContactSuccess(result){ console.log("Inside loadContactSuccess " + result); var html = ''; if(result.status == 200){ var contactList = result.invocationResult.Envelope.Body.searchContactResponse.contactList; var i = 0; for(i =0 ; i < contactList.length ; i++){ var currentContact = contactList[i]; html = html + '
- '+currentContact.firstName +' ' +currentContact.lastName +'
'; } } jq("#displayContact").html(html); jq("#displayContact").listview('refresh'); } function loadContactFailure(result){ console.log("Inside loadContactError " + result); }return
and the WorkLight server simply uses the same name while converting the XML result into JSON object, but that makes accessing the return element difficult in the client javascript becausereturn
is the JavaScript keyword and you can not use it in your code. So to get around this problem i had to change my sample web service and use JAXWS annotation to customize name of the return element to contactList
Developing JAXWS Service for deployment in GlassFish/JEE 5 compliant container
In the newer version of JEE you can use annotations for creating Web Service, i wanted to try this feature so i did create this simple JAX WS service that takes name/part of the name of contact as input and return list of contacts with same last name. You can download the JAXWS service from here
First i did create a ContactWS.java class that looks like this
import java.util.List;
import javax.jws.*;
@WebService(name="ContactWS", serviceName="ContactWS",
targetNamespace="http://webspherenotes.com")
public class ContactWS {
@WebMethod(operationName="searchContact")
@WebResult(name="contactList",partName="contactList")
public List searchContact(String lastName) {
ContactDAO contactDAO = new ContactDAOImpl();
return contactDAO.searchContact(lastName);
}
}
The ContactWS
class implements web service. The next step is to declare this class as servlet in web.xml like this
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<display-name>Contact Web Service</display-name>
<servlet>
<servlet-name>ContactWS</servlet-name>
<servlet-class>com.webspherenotes.ws.ContactWS</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>ContactWS</servlet-name>
<url-pattern>/contactws/*</url-pattern>
</servlet-mapping>
<
Once the application is deployed in the GlassFish server you can access it by going to http://localhost:9000/ManageContactWS/contactws
WorkLight HTTP Adapter returning XML
In the Invoking REST service from WorkLight entry i blogged about how to make a REST call that returns XML from WorkLight application using HTTP adapter. One thing that i noticed during that is even though my REST service returns XML WorkLight converts it into JSON for me and the generated JSON follows the same structure as that of the XML.
This is how my XML response of the REST service looks like
The WorkLight server converts it into JSON and return's JSON that looks like this
Invoking REST service from WorkLight
In the Using JQuery Mobile in WorkLight application entry i blogged about how to create a simple Contact Search application that takes user's last name as parameter and searches all the contacts with that name displays to the user. In that example i used the WorkLight SQL adapter for making query, but i already have my own REST service that can do same thing and i wanted to use it.
This service takes part of last name as query parameter and returns all the contacts that match the name in XML format. I wanted to use this service in my WorkLight application, so that i could use the Http Adapter so i followed these steps
- First i did create a ContactRESTService adapter
- Then i changed the ContactRESTService-impl.js like this
My adapter has 2 methods first isfunction getContactList() { var input = { method : 'get', returnedContentType : 'xml', path : '/ManageContact/rest/contact' }; return WL.Server.invokeHttp(input); } function searchContact(lastName){ var input = { method : 'get', returnedContentType : 'xml', path : '/ManageContact/rest/contact/search?lastName='+lastName }; return WL.Server.invokeHttp(input); } function getStoriesFiltered(){ }
getContactList()
that returns all the contacts in the database by calling/ManageContact/rest/contact
URL and it does not take any parameter. ThesearchContact()
takes lastName as parameter and makes GET call to'/ManageContact/rest/contact/search?lastName='+lastName
URL. -
I also had to change the ContactRESTService.xml so that my adapter descriptor looks like this
<?xml version="1.0" encoding="UTF-8"?> <wl:adapter name="ContactRESTService" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:wl="http://www.worklight.com/integration" xmlns:http="http://www.worklight.com/integration/http"> <displayName>ContactRESTService</displayName> <description>ContactRESTService</description> <connectivity> <connectionPolicy xsi:type="http:HTTPConnectionPolicyType"> <protocol>http</protocol> <domain>localhost</domain> <port>9000</port> </connectionPolicy> <loadConstraints maxConcurrentConnectionsPerNode="2" /> </connectivity> <procedure name="getContactList"/> <procedure name="searchContact"/> <procedure name="getStoriesFiltered"/> </wl:adapter>
-
After that i had to make couple of my minor change in my WorkLight application so that it would use the HTTP adapter instead of the SQL adapter, This is how my JavaScript that makes call to the adapter looks like
First thefunction getContact(){ console.log("Entering getContact() REST service based version"); var contactName = $('contactName').getValue(); var invocationData = { adapter:"ContactRESTService", procedure:"searchContact", parameters:[contactName] } var options ={ onSuccess:loadContactSuccess, onFailure:loadContactFailure } WL.Client.invokeProcedure(invocationData, options); } function loadContactSuccess(result){ console.log("Inside loadContactSuccess " + result); var html = ''; if(result.invocationResult.isSuccessful){ var contactList = result.invocationResult.contacts.contact; var i = 0; for(i =0 ; i < contactList.length ; i++){ var currentContact = contactList[i]; html = html + '<li><a href="#">'+currentContact.firstName +' ' +currentContact.lastName +'</a></li>'; } } jq("#displayContact").html(html); jq("#displayContact").listview('refresh'); } function loadContactFailure(result){ console.log("Inside loadContactError " + result); }
getContact()
which makes call to the HttpAdapter had to changed to use the name of theContactRESTService
as adapter andsearchContact
as procedure name. Then i had to change the loadContactSuccess() method the part which reads the search results. Worklight makes sure that i get results in JSON format with little bit different structure.
WorkLight resources directory
You might have noticed that when you make changes in any HTML or JavaScript file in the WorkLight Studio those changes do not get reflected right away instead you will have to Right Click on the Project and say Run -< Build All and Deploy. It seems that when you click on Build and Deploy the Studio generates the files required for WorkLight and for every Environment in the
Worklight\server\widget-resources
directory like this
My HelloWorkLight project supports ipad and iphone environment so every time i click on the Build All and Deploy, WorkLight studio creates relevant directories under Worklight\server\widget-resources
directory one is HelloWorlight-common-*, HelloWorkLight-ipad-*,HelloWorkLight-iphone-*
the * represents a integer that worklight keeps incrementing. WorkLight studio copies all the application related files in these folders, if you make a change directly in of these files those changes get reflected right away but then you will not have those changes in your project and you those changes wont get picked after next deployment.
If you open the files in these directories you will notice that WorkLight adds quite a few elements to it. For example this is my html file
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0,
maximum-scale=1.0, minimum-scale=1.0, user-scalable=0" />
<title>HelloWorklight</title>
<link rel="shortcut icon" href="images/favicon.png" />
<link rel="apple-touch-icon" href="images/apple-touch-icon.png" />
<link rel="stylesheet" href="css/reset.css" />
<link rel="stylesheet" href="css/HelloWorklight.css" />
</head>
<body onload="WL.Client.init({})" id="content" style='display: none'>
<div id="AppBody">
<div id="header">
<div id="ReloadButton" onclick="reload();"></div>
<h1>Basic Development</h1>
</div>
<div id="wrapper">
<label for="actions">Display: </label>
<select id="actions" onchange="displayInfo();">
<option value="appEnvironment" selected="selected">Application Environment</option>
<option value="language">Language</option>
</select>
<div id="info"></div>
</div>
<div id="worklight" onclick="loadWebPage();"></div>
</div>
<script src="js/HelloWorklight.js"></script>
<script src="js/messages.js"></script>
<script src="js/auth.js"></script>
</body>
</html>
After deploy the same file gets changed to
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0,
maximum-scale=1.0, minimum-scale=1.0, user-scalable=0" />
<title>HelloWorklight</title>
<link rel="shortcut icon" href="images/favicon.png" />
<link rel="apple-touch-icon" href="images/apple-touch-icon.png" />
<link rel="stylesheet" href="css/reset.css" />
<link rel="stylesheet" href="css/HelloWorklight.css" />
<link rel="stylesheet" href="wlclient/css/wlclient.css" />
<link rel="stylesheet" href="xilinus/css/default.css" />
<link rel="stylesheet" href="xilinus/css/alphacube.css" />
<link rel="stylesheet" href="xilinus/css/debug.css" />
<script type="text/javascript">
// Define WL namespace.
var WL = WL ? WL : {};
/**
* WLClient configuration variables.
* Values are injected by the deployer that packs the gadget.
*/
WL.StaticAppProps = {
"APP_DISPLAY_NAME": "HelloWorklight",
"APP_LOGIN_TYPE": "never",
"APP_SERVICES_URL": "/apps/services/",
"APP_VERSION": "1.0",
"ENVIRONMENT": "preview",
"HEIGHT": 460,
"IID": 0,
"LOGIN_DISPLAY_TYPE": "popup",
"LOGIN_POPUP_HEIGHT": 610,
"LOGIN_POPUP_WIDTH": 920,
"LOGIN_REALM": null,
"PREVIEW_ENVIRONMENT": "common",
"TIMESTAMP": "192.168.94.131 on 2012-03-28 at 17:00:00",
"WIDTH": 320,
"WORKLIGHT_ROOT_URL": "/apps/services/api/HelloWorklight/common/0/"
};
</script>
<script src="common/js/prototype.js"></script>
<script src="common/js/containerCommunicationAPI.js"></script>
<script src="common/js/base.js"></script>
<script src="wlclient/js/messages.js"></script>
<script src="common/js/wlcommon.js"></script>
<script src="common/js/busy.js"></script>
<script src="xilinus/js/window.js"></script>
<script src="xilinus/js/debug.js"></script>
<script src="wlclient/js/worklight.js"></script>
<script src="wlclient/js/gadgetCommunicationAPI.js"></script>
<script src="wlclient/js/wlclient.js"></script>
<script src="wlclient/js/wlfragments.js"></script>
<script src="wlclient/js/encryptedcache.js"></script>
<script src="wlclient/js/blockTEA.js"></script>
</head>
<body onload="WL.Client.init({})" id="content" style='display: none'>
<div id="AppBody">
<div id="header">
<div id="ReloadButton" onclick="reload();"></div>
<h1>Basic Development</h1>
</div>
<div id="wrapper">
<label for="actions">Display: </label>
<select id="actions" onchange="displayInfo();">
<option value="appEnvironment" selected="selected">Application Environment</option>
<option value="language">Language</option>
</select>
<div id="info"></div>
</div>
<div id="worklight" onclick="loadWebPage();"></div>
</div>
<script src="js/HelloWorklight.js"></script>
<script src="js/messages.js"></script>
<script src="js/auth.js"></script>
</body>
</html>
Enable tracing for WorkLight
When i was developing application accessing database from Worklight i wanted to debug to figure out what Query the SQL Adapter is making also i wanted to figure out how to enable the logging tracing for WorkLight product itself.
There is a log4j.xml file in the
Worklight\server\lib
folder that you can change and restart the WorkLight server and then you can check the Worklight\server\log\server\server.log
file for actual log statements
For Example i did change the log level for com.srndpt.adapters
package to DEBUG and then when i tried executing the code that uses adapter to make SQL query can i could see following log statements
2012-03-28 15:50:27,326 DEBUG [SQLQuery] (pool-1-thread-2:682f4681-e757-4a24-b20a-34f655f48c68) Prepare statement: select * FROM CONTACT WHERE FIRSTNAME LIKE ?
2012-03-28 15:50:27,326 DEBUG [SQLQuery] (pool-1-thread-2:682f4681-e757-4a24-b20a-34f655f48c68) Execute the query
2012-03-28 15:50:27,326 DEBUG [SQLQuery] (pool-1-thread-2:682f4681-e757-4a24-b20a-34f655f48c68) payload received.
Using JQuery Mobile in WorkLight application
In the Connecting to Database from WorkLight i blogged about how to build WorkLight application that talks to database, that application did not had good looking UI, so i made changes to use JQuery Mobile that makes building good looking UI much simpler. This is how my application looks like now
These are the steps that i followed
- Create a simple WorkLight application that invokes a SQL query you can use the steps mentioned in Connecting to Database from WorkLight
- First download the Worklight Starter Application for JQuery Mobile from Worklight Getting started page
- Expand the WorklightStarter_jQueryMobile.zip some where on your local disk
- Copy jquery.mobile*.js and jquery-*.js from the WorklightStarter_jQueryMobile.zip to the js folder of your application, Also copy jquery.mobile.*.css into css folder of your application
- Change the html page of your application to include the JQuery related css and js, this is how the html page looks for me
In the head section first you should include jquery related css and js and also call<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=0" /> <title>HelloDatabase</title> <link rel="shortcut icon" href="images/favicon.png" /> <link rel="apple-touch-icon" href="images/apple-touch-icon.png" /> <link rel="stylesheet" href="css/reset.css" /> <link rel="stylesheet" href="css/HelloDatabase.css" /> <link rel="stylesheet" href="css/jquery.mobile-1.0.min.css" /> <script src="js/jquery-1.7.1.min.js"></script> <script> var jq = jQuery.noConflict(); </script> <script src="js/jquery.mobile-1.0.min.js"></script> </head> <body onload="WL.Client.init({})" id="content" style="display: none"> <div data-role="page" id="page1"> <div data-theme="a" data-role="header"> <h3>Contact DB App</h3> </div> <div data-role="content"> <div data-role="fieldcontain"> <fieldset data-role="controlgroup"> <label for="textinput1"> First Name </label> <input id="contactName" placeholder="" value="" type="text" /> </fieldset> </div> <a data-role="button" data-transition="fade" href="javascript:getContact()" id="searchContact"> Search Contact </a> <ul data-role="listview" data-inset="true" data-filter="true" id="displayContact"> </ul> </div> <div data-theme="a" data-role="footer"> <h3>Copyright stuff</h3> </div> </div> <script src="js/HelloDatabase.js"></script> <script src="js/messages.js"></script> <script src="js/auth.js"></script> </body> </html>
var jq = jQuery.noConflict();
so that the jQuery $ character does not conflict with $ used by the prototype javascript framework used by worklight. Next use the jquery mobile template in the body section that divided the page into head, content and footer section. - Add following code to the javascript file for your application
As you can can see i am usingfunction wlCommonInit(){ // Common initialization code goes here } function getContact(){ console.log("Entering getContact()"); var contactName = '%'+$('contactName').getValue()+'%'; var invocationData = { adapter:"mySQLAdapter", procedure:"searchContactByFirstName", parameters:[contactName] } var options ={ onSuccess:loadContactSuccess, onFailure:loadContactFailure } WL.Client.invokeProcedure(invocationData, options); } function loadContactSuccess(result){ console.log("Inside loadContactSuccess " + result); var html = ''; if(result.invocationResult.isSuccessful){ var contactList = result.invocationResult.resultSet; var i = 0; for(i =0 ; i < contactList.length ; i++){ var currentContact = contactList[i]; html = html + '<li><a href="#">'+currentContact.FIRSTNAME +' ' +currentContact.LASTNAME +'</a></li>'; } } jq("#displayContact").html(html); jq("#displayContact").listview('refresh'); } function loadContactFailure(result){ console.log("Inside loadContactError " + result); }
jq("#displayContact")
> instead of regular$("#displayContact")
this is the nonconflict version of JQuery that you must use in the WorkLight, but your still allowed to use the prototype based syntax for looking up objects as well like i did in$('contactName').getValue()
Debugging problems with worklight database access
In the Connecting to Database from WorkLight entry i developed a simple application to demonstrate how to connect to a database from Worklight, when i was developing that application i used these tools to debug.
It seems that when i make a DB call using
WL.Client.invokeProcedure()
method it makes REST call and gets the response back in JSON format, i used the firebug tool to see what is being passed in and out.
This is what data gets passed to the server
This is the response of the server that contains the response data in JSON format.The response in green displays what happens in case of success and the response with red boundary displays what happens in case of error
On the server side after deploying adapter i looked at the Worklight\server\log\server\server.log
file to get detailed information about the errors and Worklight\server\log\server\error.log
for short information about the errors
Connecting to Database from WorkLight
I wanted to figure out how to use SQL Adapter provided by WorkLight to create application that talks with database, with the difference that i want to access Apache Derby instead of MySQL that is used by WorkLight, so i built this simple application that takes contactId as input then use it to execute SELECT * from CONTACT where CONTACTID=contactId query and display the result.
I followed these steps to build my application
-
First i did open the Worklight\server\conf\worklight.properties file in the text editor and i did add a section to define the JDBC connection parameters for connecting to Derby at the end of the file like this
training-jndi-name=${custom-db.1.jndi-name} custom-db.1.relative-jndi-name=jdbc/worklight_training custom-db.1.driver=org.apache.derby.jdbc.ClientDriver custom-db.1.url=jdbc:derby://localhost:1527/C:/data/contact custom-db.1.username=dbadmin custom-db.1.password=dbadmin
- Then i did copy the derbyclient.jar which is JDBC driver for Apache Derby in the
Worklight\server\lib
folder, when i was copying the derbyclient.jar i noticed that, the same directory also has the mysql-connector-java-*.jar that worklight needs for its own database connectivity - After that i had to restart the server for my changes to take effect
- Next i did create mySQLAdapter project by following the instructions on Creating SQL Adapters
- I changed the mySQLAdapter-impl.js like this
var procedure1Statement = WL.Server.createSQLStatement("select * from CONTACT where CONTACTID = ?"); function procedure1(param) { return WL.Server.invokeSQLStatement({ preparedStatement : procedure1Statement, parameters : [param] }); }
- Then i did use the following mySQLAdapter.xml
<?xml version="1.0" encoding="UTF-8"?> <wl:adapter name="mySQLAdapter" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:wl="http://www.worklight.com/integration" xmlns:sql="http://www.worklight.com/integration/sql"> <displayName>mySQLAdapter</displayName> <description>mySQLAdapter</description> <connectivity> <connectionPolicy xsi:type="sql:SQLConnectionPolicy"> <!-- Replace 'data-source-jndi-name' with the jndi name as defined in the data source. --> <!-- Example using jndi name: java:/comp/env/jdbc/ProjectDS or using a place holder: ${project.db.jndi-name} --> <dataSourceJNDIName>${training-jndi-name}</dataSourceJNDIName> </connectionPolicy> <loadConstraints maxConcurrentConnectionsPerNode="5" /> </connectivity> <!-- Replace this with appropriate procedures --> <procedure name="procedure1"/> </wl:adapter>
- Once the adapter was ready i did deploy it on the server making sure that the deployment was successful
- Then i did create HelloDatabase application in the same project that has the adapter and i changed the main html to look like this
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=0" /> <title>HelloDatabase</title> <link rel="shortcut icon" href="images/favicon.png" /> <link rel="apple-touch-icon" href="images/apple-touch-icon.png" /> <link rel="stylesheet" href="css/reset.css" /> <link rel="stylesheet" href="css/HelloDatabase.css" /> </head> <body onload="WL.Client.init({})" id="content" style="display: none"> <table> <tr> <td>Contact Id</td> <td><input type="text" name="contactId" id="contactId"/></td> </tr> <tr> <td><button onclick="getContact()" title="GetContact" label="GetContact">GetContact</button></td> </tr> </table> <div id="displayContact"> </div> <script src="js/HelloDatabase.js"></script> <script src="js/messages.js"></script> <script src="js/auth.js"></script> </body> </html>
- Last step was to change the HelloDatabase.js like this
function wlCommonInit(){ // Common initialization code goes here } function getContact(){ var contactId = $("contactId").getValue(); console.log("Contact id " + contactId); var invocationData = { adapter:"mySQLAdapter", procedure:"procedure1", parameters:[contactId] } var options ={ onSuccess:loadContactSuccess, onFailure:loadContactFailure } WL.Client.invokeProcedure(invocationData, options); } function loadContactSuccess(result){ console.log("Inside loadContactSuccess " + result); console.log(result.invocationResult.resultSet[0].FIRSTNAME) $("displayContact").innerHTML = result.invocationResult.resultSet[0].FIRSTNAME + " " + result.invocationResult.resultSet[0].LASTNAME +" " + result.invocationResult.resultSet[0].EMAIL; } function loadContactFailure(result){ console.log("Inside loadContactError " + result); }
Enabling debug/log console in Worklight
Recently i started learning about the IBM WorkLight and i followed the instructions on the Your First application to build a simple HelloWorld type of application.
As a developer one of the first thing that i like to figure out how the application is actually working so i wanted to figure out a way to debug/enable log for worklight, and i followed these steps
- WOrklight generates log information as well as it allows you to use the same logger to write your own log information, How the log information is displayed depends on the client. For example in case of desktop client you can ask Worklight to display the log in a popup window by calling
WL.Client.init({showLogger:true});
JavaScript method during onload event - If you want you can use the WorkLight logger to write application specific log messages in the same logger you can do that by calling
WL.Logger.debug("Sample debug statement");
WL.Logger.error("Sample error statement");
function wlCommonInit(){
console.log("Entering wlCommonInit");
WL.Client.init({showLogger:true});
WL.Logger.debug("Sample debug statement");
WL.Logger.error("Sample error statement");
console.log("Exiting wlCommonInit");
}
Now when i access the application through browser a logger windows gets popped up and it has the log messages like these
Setting up Worklight development environment
I want to learn about Worklight which is mobile application development platform that you can use for developing HTML5, native and hybrid applications. So i decided to install it on my machine and i followed these steps
- First i did go to Worklight Evaluation Version Download Page and i did download all the software listed on the page
- I followed the same steps mentioned in the document to install it on my Windows XP machine, The first time i installed MySQL on my machine i did not follow the MySQL Configuration Guide for Windows and my Worklight server installation failed to start on the first page where it asks for the MySQL connection information. So i did have to reconfigure it to use the steps mentioned in the MySQL Configuration Guide for Windows guide. I think unless you disable Strict Mode the WorkLight server install does not work.
In the How to either publish or consume message using STOMP in ActiveMQ entry i talked about how to use STOMP protocol for both publishing and consuming messages. I wanted to check if i can mix JMS + STOMP together, so i changed the sample application to use JMS API for publishing a message and use the STOMP for consuming messages, this works because no matter which API you use for communicating with the broker the message gets stored in the same messaging provider same this while consuming messages. You can download the sample code from here
First configure your ActiveMQ so that it supports both JMS and STOMP connectors like this
<transportConnectors>
<transportConnector name="openwire" uri="tcp://localhost:61616?trace=true"/>
<transportConnector name="stomp" uri="stomp://localhost:61613?trace=true"/>
</transportConnectors>
Then create a jndi.properties file that defines the ConnectionFactory and Queue in the JNDI context for your application like this
java.naming.factory.initial = org.apache.activemq.jndi.ActiveMQInitialContextFactory
java.naming.provider.url = tcp://localhost:61616
java.naming.security.principal=system
java.naming.security.credentials=manager
connectionFactoryNames = QueueCF
queue.stomptest=stomptest
This jndi.properties file makes sure that the objects of ConnectionFactory and Queue and bound in the JNDI context so that your java code remains JMS compliant with no reference to ActiveMQ. Then create OpenWireMessagePublisher.java like this
package com.webspherenotes.stomp;
import javax.jms.JMSException;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueConnectionFactory;
import javax.jms.QueueSender;
import javax.jms.QueueSession;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class OpenWireMessagePublisher {
public static void main(String[] argv){
try {
InitialContext context = new InitialContext();
QueueConnectionFactory queueConnectionFactory = (QueueConnectionFactory)context.lookup("QueueCF");
QueueConnection queueConnection = queueConnectionFactory.createQueueConnection();
QueueSession queueSession = queueConnection.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = (Queue)context.lookup("stomptest");
TextMessage textMessage = queueSession.createTextMessage();
textMessage.setText("This is sample message for stomp queue");
QueueSender queueSender = queueSession.createSender(queue);
queueSender.send(textMessage);
queueConnection.close();
} catch (NamingException e) {
e.printStackTrace();
} catch (JMSException e) {
e.printStackTrace();
}
}
}
The OpenWireMessagePublisher.java is using JMS API to publish a message to stomptest queue. Now lets create a StompMessageConsumer.java like this
package com.webspherenotes.stomp;
import java.io.IOException;
import java.net.UnknownHostException;
import org.apache.activemq.transport.stomp.StompConnection;
import org.apache.activemq.transport.stomp.StompFrame;
import org.apache.activemq.transport.stomp.Stomp.Headers.Subscribe;
public class StompMessageConsumer {
public static void main(String[] args) {
try {
StompConnection connection = new StompConnection();
connection.open("localhost", 61613);
connection.connect("system", "manager");
connection.subscribe("/queue/stomptest", Subscribe.AckModeValues.CLIENT);
connection.begin("tx2");
StompFrame message = connection.receive();
System.out.println(message.getBody());
connection.ack(message, "tx2");
connection.commit("tx2");
connection.disconnect();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
The StompConsumer code does not care about if the message was published using JMS or STOMP it remains same.
How to either publish or consume message using STOMP in ActiveMQ
The ActiveMQ server supports Simple Text-oriented Messaging Protocol(STOMP) that we can use for communicating between client and server. I wanted to try out this feature so i built a sample application that has two classes one is
StompMessagePublisher.java
that publishes a message using STOMP and StompMessageConsumer
that consumes a message. You can download the sample application from here
By default the STOMP connector is disabled in the ActiveMQ so first thing that you would have to do is enable it. First open <activemq_installroot>\conf\activemq.xml
file and find the transportConnectors
element by default it will have only one transportConnector
child element with name equal to openwire add stomp element in it like this
<transportConnectors>
<transportConnector name="openwire" uri="tcp://localhost:61616?trace=true"/>
<transportConnector name="stomp" uri="stomp://localhost:61613?trace=true"/>
</transportConnectors>
Then create a StompMessagePublisher.java like this
package com.webspherenotes.stomp;
import java.io.IOException;
import java.net.UnknownHostException;
import org.apache.activemq.transport.stomp.StompConnection;
public class StompMessagePublisher {
public static void main(String[] args) {
try {
StompConnection connection = new StompConnection();
connection.open("localhost", 61613);
connection.connect("system", "manager");
connection.begin("tx1");
connection.send("/queue/stomptest", "This is test message 1");
connection.commit("tx1");
connection.disconnect();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
In this class first we are opening a StompConnection
with localhost:61613
, after establishing connection we are sending message to stomptest
queue in a transaction
Next create StompMessageConsumer.java class which looks like this
package com.webspherenotes.stomp;
import java.io.IOException;
import java.net.UnknownHostException;
import org.apache.activemq.transport.stomp.StompConnection;
import org.apache.activemq.transport.stomp.StompFrame;
import org.apache.activemq.transport.stomp.Stomp.Headers.Subscribe;
public class StompMessageConsumer {
public static void main(String[] args) {
try {
StompConnection connection = new StompConnection();
connection.open("localhost", 61613);
connection.connect("system", "manager");
connection.subscribe("/queue/stomptest", Subscribe.AckModeValues.CLIENT);
connection.begin("tx2");
StompFrame message = connection.receive();
System.out.println(message.getBody());
connection.ack(message, "tx2");
connection.commit("tx2");
connection.disconnect();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
In the StompMessageConsumer.java, first i am establishing STOMP connection to the localhost
and then subscribing to stomptest
queue, after subscribing to the queue we have to call the receive()
method on the connection to receive the message from the queue.
How to use ActiveMQ in Jetty
I wanted to figure out how to use Apache ActiveMQ in a web application that is running in Jetty, Also i wanted to use the Maven Jetty Plugin so i built this sample application which contains a , when i make GET request to servelt it takes value of message query parameter and publishes it as a TextMessage to a Queue, you can download the source code for the sample application from here
First thing that i did is create a pom.xml file that looks like this
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.webspherenotes.jms</groupId>
<artifactId>HelloJettyActiveMQ</artifactId>
<version>1.0</version>
<packaging>war</packaging>
<name>HelloJettyActiveMQ</name>
<description>Sample app to demonstrate how to use
ActiveMQ in Jetty</description>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.4</version>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-core</artifactId>
<version>5.5.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.5.11</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jms</artifactId>
<version>3.0.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-spring</artifactId>
<version>3.9</version>
</dependency>
</dependencies>
<build>
<finalName>HelloJettyActiveMQ</finalName>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>7.2.2.v20101205</version>
<configuration>
<scanIntervalSeconds>10</scanIntervalSeconds>
<webAppConfig>
<jettyEnvXml>${basedir}/src/main/resources/jetty-env.xml</jettyEnvXml>
</webAppConfig>
</configuration>
</plugin>
</plugins>
</build>
</project>
I am using version 7.2.2 of Jetty server in the jetty-maven-plugin, also note that i configured a jetty-env.xml file which defines the JMS resources in the JNDI context. This is how my jetty-env.xml file looks like
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN"
"http://jetty.mortbay.org/configure.dtd">
<Configure id='jms-webapp-wac' class="org.eclipse.jetty.webapp.WebAppContext">
<New id="connectionFactory" class="org.eclipse.jetty.plus.jndi.Resource">
<Arg>
<Ref id='jms-webapp-wac' />
</Arg>
<Arg>jms/ConnectionFactory</Arg>
<Arg>
<New class="org.apache.activemq.ActiveMQConnectionFactory">
<Arg>tcp://localhost:61616</Arg>
</New>
</Arg>
</New>
<New id="fooQueue" class="org.eclipse.jetty.plus.jndi.Resource">
<Arg>jms/FooQueue</Arg>
<Arg>
<New class="org.apache.activemq.command.ActiveMQQueue">
<Arg>FOO.QUEUE</Arg>
</New>
</Arg>
</New>
</Configure>
The jetty-env.xml file defines 2 resources on is the ActiveMQConnectionFactory and second is the ActiveMQQueue. After that i did declare the messaging related resources in web.xml, so my web.xml file looks like this
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<display-name>HelloEmbeddedServer</display-name>
<servlet>
<servlet-name>MessagePublishingServlet</servlet-name>
<servlet-class>com.webspherenotes.jms.MessagePublishingServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>MessagePublishingServlet</servlet-name>
<url-pattern>/MessagePublishingServlet/*</url-pattern>
</servlet-mapping>
<resource-ref>
<description>JMS Connection</description>
<res-ref-name>jms/ConnectionFactory</res-ref-name>
<res-type>javax.jms.ConnectionFactory</res-type>
<res-auth>Container</res-auth>
<res-sharing-scope>Shareable</res-sharing-scope>
</resource-ref>
<message-destination-ref>
<message-destination-ref-name>jms/FooQueue</message-destination-ref-name>
<message-destination-type>javax.jms.Queue</message-destination-type>
<message-destination-usage>Produces</message-destination-usage>
<message-destination-link>jms/FooQueue</message-destination-link>
</message-destination-ref>
</web-app>
This is how my MessagePublishingServlet.java looks like
package com.webspherenotes.jms;
import java.io.IOException;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.naming.InitialContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MessagePublishingServlet extends HttpServlet{
Logger logger = LoggerFactory.getLogger(MessagePublishingServlet.class);
Connection connection;
Queue queue;
@Override
public void init() throws ServletException {
logger.debug("Entering MessagePublishingServlet.init()");
try {
InitialContext context = new InitialContext();
ConnectionFactory connectionFactory = (ConnectionFactory)context.lookup("java:comp/env/jms/ConnectionFactory");
logger.debug("Connection Factory " + connectionFactory);
connection = connectionFactory.createConnection();
queue =(Queue) context.lookup("jms/FooQueue");
logger.debug("After looking up the queue " + queue);
} catch (Exception e) {
logger.error("Error occured in MessagePublishingServlet.init() " + e.getMessage(),e);
}
logger.debug("Exiting MessagePublishingServlet.init()");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
logger.debug("Entering MessagePublishingServlet.doGet()");
resp.setContentType("text/html");
resp.getWriter().println("Hello from MessagePublishingServlet.doGet()");
try {
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
TextMessage textMessage = session.createTextMessage();
textMessage.setText(req.getParameter("message"));
MessageProducer queueSender = session.createProducer(queue);
queueSender.send(textMessage);
} catch (JMSException e) {
logger.error("Error occured in MessagePublishingServlet.doGet() " + e.getMessage(),e);
}
logger.debug("Exiting MessagePublishingServlet.doGet()");
}
}
The init() method looks up the JMS objects from the InitialContext, in the doGet() method i am reading the value of message query parameter and using it to send a TextMessage.
Using Spring message pojo
The Spring Framework has concept of message driven pojo's which are similar to MDB that you can use for receiving messages asynchronously. I wanted to try that out so i changed the sample application that i developed in Using amq namespace for building Spring JMS application for ActiveMQ post. In my sample application i did create a simple
MessageListener
class that gets called whenever there is a message, you can download the source code for sample application from here
First i did create a simple MessageListener
POJO class like this
package com.webspherenotes.jms;
public class MessageListener {
public void handleMessage(String message){
System.out.println("Inside MessageListener.handleMessage() "
+ message);
}
}
The handleMessage()
method of the MessageListener
will get called whenever the message is available. Next define the message listener class in the spring configuration like this.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jms="http://www.springframework.org/schema/jms"
xmlns:amq="http://activemq.apache.org/schema/core"
xsi:schemaLocation="http://activemq.apache.org/schema/core
http://activemq.apache.org/schema/core/activemq-core-5.5.0.xsd
http://www.springframework.org/schema/jms
http://www.springframework.org/schema/jms/spring-jms-3.0.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<amq:connectionFactory id="connectionFactory"
brokerURL="tcp://localhost:61616" />
<bean id="jmsTemplate"
class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory" ref="connectionFactory" />
<property name="defaultDestinationName" value="queue1" />
</bean>
<bean id="messageListener"
class="com.webspherenotes.jms.MessageListener" />
<jms:listener-container connection-factory="connectionFactory">
<jms:listener destination="queue1" ref="messageListener"
method="handleMessage"/>
</jms:listener-container>
</beans>
Now when you run the publisher mvn exec:java -Dexec.mainClass=com.webspherenotes.jms.MessagePublisher
it will initialize the spring context and as part of that process it will create MessageListner class and attach it as listener to the destination, so when the message gets published your MessageListner will get called automatically to handle/consume the message.
Maven build file(pom.xml) for Spring Active MQ JMS application
In the Using amq namespace for building Spring JMS application for ActiveMQ entry i built a sample Spring Active MQ JMS application, this is the maven pom.xml file for it.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.webspherenotes.jms</groupId>
<artifactId>HelloSpringActiveMQ</artifactId>
<version>1.0</version>
<name>HelloSpringActiveMQ</name>
<description>Sample Spring ActiveMQ JMS application</description>
<dependencies>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-core</artifactId>
<version>5.5.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.5.11</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jms</artifactId>
<version>3.0.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-spring</artifactId>
<version>3.9</version>
</dependency>
</dependencies>
</project>
Once my pom.xml is ready i can use following commands to run MessagePublisher.java and MessageReceiver.java
mvn exec:java -Dexec.mainClass=com.webspherenotes.jms.MessagePublisher
mvn exec:java -Dexec.mainClass=com.webspherenotes.jms.MessageReceiver
Using amq namespace for building Spring JMS application for ActiveMQ
Using amq namespace makes developing Spring application for ActiveMQ very easy, i wanted to try that so i built this sample application, This is how my applicationContext.xml file looks like
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jms="http://www.springframework.org/schema/jms"
xmlns:amq="http://activemq.apache.org/schema/core"
xsi:schemaLocation="http://activemq.apache.org/schema/core
http://activemq.apache.org/schema/core/activemq-core-5.5.0.xsd
http://www.springframework.org/schema/jms
http://www.springframework.org/schema/jms/spring-jms-3.0.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<amq:connectionFactory id="connectionFactory"
brokerURL="tcp://localhost:61616" />
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory" ref="connectionFactory" />
<property name="defaultDestinationName" value="queue1" />
</bean>
</beans>
As you can see i have only two beans one for ConnectionFactory
and other for JmsTemplate
. This is how my MessagePublisher.java
looks like
package com.webspherenotes.jms;
import java.util.Date;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessageCreator;
public class MessagePublisher {
public static void main(String[] args)throws Exception {
ApplicationContext context =
new ClassPathXmlApplicationContext("applicationContext.xml");
JmsTemplate jmsTemplate =(JmsTemplate) context.getBean("jmsTemplate");
MessageCreator message = new MessageCreator() {
public Message createMessage(Session session) throws JMSException {
TextMessage textMessage = session.createTextMessage();
String messageStr = "This message is sent using MessageCreator" + new Date();
textMessage.setText(messageStr);
return textMessage;
}
};
jmsTemplate.send(message);
}
}
This is how my MessageReceiver class looks like
package com.webspherenotes.jms;
import javax.jms.TextMessage;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jms.core.JmsTemplate;
public class MessageReceiver {
public static void main(String[] args)throws Exception {
ApplicationContext context =
new ClassPathXmlApplicationContext("applicationContext.xml");
JmsTemplate jmsTemplate =(JmsTemplate) context.getBean("jmsTemplate");
TextMessage message = (TextMessage)jmsTemplate.receive();
System.out.println("Message received " + message.getText());
}
}
This is sample of how to wait for message synchronously.
Apache Maven pom.xml for activemq standalone application
I wanted to check what pom.xml should i use to create ActiveMQ publisher and consumer in standalone java application so i built a sample application, that you can download from here
First i had to use the following directory structure
Then i did use the following pom.xml, only dependencies i have is on
activemq-core
, and slf4j-log4j and it takes care of downloading necessary jars
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.webspherenotes.jms</groupId>
<artifactId>HelloActiveMQMaven</artifactId>
<version>1.0</version>
<name>HelloActiveMQMaven</name>
<description>How to use ActiveMQ in Maven </description>
<dependencies>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-core</artifactId>
<version>5.5.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.4</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.1.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>java</goal>
</goals>
<configuration>
<mainClass>
com.webspherenotes.jms.HelloActiveMQPublisher</mainClass>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Then i can execute the publisher by executing
mvn exec:java -Dexec.mainClass=com.webspherenotes.jms.HelloActiveMQPublisher
and i can execute the consumer by executing mvn exec:java -Dexec.mainClass=com.webspherenotes.jms.HelloActiveMQConsumer
Embed ActiveMQ using Java Code
You can use embedded ActiveMQ which means you can create object of BrokerService and then use java code to configure it instead of regular approach of using activemq.xml file, i wanted to try that so i did create this sample application which you can download from here
First create HelloEmbeddedBrokerService class like this, in this class i am just creating object of BrokerService, after that you can call methods to configure it and once your done call brokerService.start() to start the broker.
package com.webspherenotes.jms;
import org.apache.activemq.broker.BrokerService;
public class HelloEmbeddedBrokerService {
public static void main(String[] args) {
try {
BrokerService brokerService = new BrokerService();
brokerService.addConnector("tcp://localhost:61616");
brokerService.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Then use the jndi.properties file like this to configure JNDI context for the JMS client code
java.naming.factory.initial = org.apache.activemq.jndi.ActiveMQInitialContextFactory
java.naming.provider.url = tcp://localhost:61616
java.naming.security.principal=system
java.naming.security.credentials=manager
connectionFactoryNames = QueueCF
queue.SampleQ = jms.SampleQ
This is how the code for the message publisher would look like, as you can see your client code does not care how the message broker is started.
package com.webspherenotes.jms;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.naming.InitialContext;
import org.apache.activemq.ActiveMQConnectionFactory;
public class HelloActiveMQPublisher {
/**
* @param args
*/
public static void main(String[] args) throws Exception{
InitialContext context = new InitialContext();
ConnectionFactory connectionFactory = (ConnectionFactory)context.lookup("QueueCF");
Connection connection = connectionFactory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = (Queue)context.lookup("SampleQ");
MessageProducer messageProducer = session.createProducer(queue);
TextMessage textMessage = session.createTextMessage();
textMessage.setText("Lets see if i can send messages using Embedded Broker");
messageProducer.send(textMessage);
connection.close();
}
}
Using MessageAuthorizationPolicy in ActiveMQ
The ActiveMQ broker allows message level security, that means you can ask ActiveMQ to call your business logic before consuming every message, and as a result get control on which message can be consumed by which
MessageListener
. I wanted to try that, so i created this SimpleMessagePolicy
.java class which checks if the message body contains Secrete message for clientId
com.webspherenotes.secret
text, if yes it checks if the consumers's caller Id is com.webspherenotes.secret
if yes then only it will allow the consumer to consume message. You can download the sample code for SampleMessagePolicy
from here
First i had to create SampleMessagePolicy
class that implements MessageAuthorizationPolicy
interface.The isAllowedToConsume()
method of your class gets before a consumer is trying to consume every message. Create this class in separate java project, compile that project and copy the .jar file in the activemq/lib directory
package com.webspherenotes.jms;
import javax.jms.JMSException;
import javax.jms.TextMessage;
import org.apache.activemq.broker.ConnectionContext;
import org.apache.activemq.command.Message;
import org.apache.activemq.security.MessageAuthorizationPolicy;
public class SampleMessagePolicy implements MessageAuthorizationPolicy {
@Override
public boolean isAllowedToConsume(ConnectionContext messageContext,
Message message) {
try {
System.out
.println("Inside SampleMessagePolicy.isAllowedToConsume() ");
System.out.println("Client Id " + messageContext.getClientId());
if (message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
System.out.println("Text message is " + textMessage.getText());
String messageStr = textMessage.getText();
if (messageStr
.equals("Secrete message for clientId com.webspherenotes.secret")) {
System.out
.println("Secret message received check the clientId");
if (messageContext.getClientId().equals(
"com.webspherenotes.secret")) {
System.out
.println("Got request from com.webspherenotes.secret, for secret message returning message");
return true;
} else {
System.out
.println("Got request from some other client, for secret message returning, hidding message");
return false;
}
} else {
System.out
.println("Non secret message received, returning message");
return true;
}
}
} catch (JMSException e) {
e.printStackTrace();
}
return true;
}
}
Next configure the ActiveMQ so that it will use SampleMessagePolicy
as MessageAuthorizationPolicy
like this
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:amq="http://activemq.apache.org/schema/core"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://activemq.apache.org/schema/core
http://activemq.apache.org/schema/core/activemq-core.xsd
http://activemq.apache.org/camel/schema/spring
http://activemq.apache.org/camel/schema/spring/camel-spring.xsd">
<!-- Allows us to use system properties as variables in this configuration file -->
<bean
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer" />
<broker xmlns="http://activemq.apache.org/schema/core"
brokerName="localhost" dataDirectory="${activemq.base}/data">
<messageAuthorizationPolicy>
<bean class="com.webspherenotes.jms.SampleMessagePolicy"
xmlns="http://www.springframework.org/schema/beans" />
</messageAuthorizationPolicy>
<!-- The transport connectors ActiveMQ will listen to -->
<transportConnectors>
<transportConnector name="openwire"
uri="tcp://localhost:61616" />
</transportConnectors>
</broker>
</beans>
After configuring the ActiveMQ restart the server for your changes to take effect. Then create MessageConsumer.java
like this, in this file set the ClientId
to com.webspherenotes.secret
so that it will receive the secrete message.
package com.webspherenotes.jms;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueConnectionFactory;
import javax.jms.QueueReceiver;
import javax.jms.QueueSession;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class MessageConsumer implements MessageListener{
QueueConnection queueConnection;
QueueSession queueSession;
public MessageConsumer() {
try {
InitialContext context = new InitialContext();
QueueConnectionFactory queueConnectionFactory = (QueueConnectionFactory) context
.lookup("QueueCF");
queueConnection = queueConnectionFactory.createQueueConnection("consumer","password");
queueConnection.setClientID("com.webspherenotes.secret");
queueSession = queueConnection.createQueueSession(false,
Session.AUTO_ACKNOWLEDGE);
Queue queue = (Queue) context.lookup("SampleQ");
QueueReceiver queueReceiver = queueSession.createReceiver(queue);
queueReceiver.setMessageListener(this);
queueConnection.start();
} catch (NamingException e) {
e.printStackTrace();
} catch (JMSException e) {
e.printStackTrace();
}
}
@Override
public void onMessage(Message message) {
try {
TextMessage textMessage = (TextMessage) message;
System.out.println("Inside MessageConsumer.onMessage "
+ textMessage.getText());
} catch (JMSException e) {
e.printStackTrace(System.out);
}
}
public static void main(String[] argv) {
try {
MessageConsumer messageConsumer = new MessageConsumer();
BufferedReader stdin = new BufferedReader(new InputStreamReader(
System.in));
System.out.println("Press enter to quit application");
stdin.readLine();
messageConsumer.queueConnection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Using ActiveMQConnectionFactory for creating connection factory
When developing a messaging application for ActiveMQ, say for standalone client, normally we create a jndi.properties file like this in the source folder.
java.naming.factory.initial = org.apache.activemq.jndi.ActiveMQInitialContextFactory
java.naming.provider.url = tcp://localhost:61616
java.naming.security.principal=system
java.naming.security.credentials=manager
connectionFactoryNames = QueueCF
queue.ActiveMQ = jms.ActiveMQ
After that we can look up both QueueCF
and ActiveMQ
from inside the code by looking them up in the InitialContext
like this
InitialContext context = new InitialContext();
QueueConnectionFactory queueConnectionFactory = (QueueConnectionFactory)context.lookup("QueueCF");
QueueConnection queueConnection = queueConnectionFactory.createQueueConnection();
QueueSession queueSession = queueConnection.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = (Queue)context.lookup("ActiveMQ");
Using the jndi.properties option makes your code JMS compliant, but you will have to define the queues and connection factories before hand, with ActiveMQ we have another option which is to use ActiveMQConnectionFactory
like this, in this example first we create object of ActiveMQConnectionFactory
by passing URL to the broker then we use session.createQueue("ActiveMQ")
to create the Queue, It allows us to create Queues
dynamically at run time.
package com.webspherenotes.jms;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.apache.activemq.ActiveMQConnectionFactory;
public class HelloActiveMQPublisher {
public static void main(String[] args) throws Exception{
String brokerURL = "tcp://localhost:61616";
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(brokerURL);
Connection connection = connectionFactory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue("ActiveMQ");
MessageProducer messageProducer = session.createProducer(queue);
TextMessage textMessage = session.createTextMessage();
textMessage.setText("This is a message for dynamically create message q");
messageProducer.send(textMessage);
connection.close();
}
}
package com.webspherenotes.jms;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageListener;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.apache.activemq.ActiveMQConnectionFactory;
public class HelloActiveMQConsumer implements MessageListener{
public static void main(String[] args) throws Exception{
String brokerURL = "tcp://localhost:61616";
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(brokerURL);
Connection connection = connectionFactory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue("ActiveMQ");
MessageConsumer messageConsumer = session.createConsumer(queue);
messageConsumer.setMessageListener(new HelloActiveMQConsumer());
BufferedReader stdin = new BufferedReader(new InputStreamReader(
System.in));
System.out.println("Press enter to quit application");
stdin.readLine();
connection.close();
}
@Override
public void onMessage(Message message) {
try {
TextMessage textMessage =(TextMessage)message;
System.out.println("Thie message is " + textMessage.getText());
} catch (JMSException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
Subscribe to:
Posts (Atom)