tag:blogger.com,1999:blog-30270916496448020322024-02-07T11:58:28.254+01:00coder's lifeŁukasz Pawlikhttp://www.blogger.com/profile/04167291407338846196noreply@blogger.comBlogger26125tag:blogger.com,1999:blog-3027091649644802032.post-64232065963273383672013-12-01T20:03:00.002+01:002023-03-05T16:26:16.336+01:00Angularjs example MotoAds more advanced directiveAnother thing we will add to MotoAds demo application is a feature which allows us to comment each advert. A good idea would be to realize this as AngularJS directive. It will look like in the picture below. We can see all added comment to advert, click on Comment link activate the comment form with Preview, Send and Cancel buttons.
<br/>
<br/>
<table>
<tr>
<td style="vertical-align: top;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiamtyWO_yxzL7vLJ9e9bNacvBgclQxdqM8nVBGJUeKUtya5wbhYZ18XTlbP8nVCj5ni0UR7D7UazCijLwCehyp5pyMPBHFgVpNMINWNgG3mVkNeLezK8EV8eYFoTU8rFkbqWuT8KB0ZwQv/s1600/motoads_comment.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiamtyWO_yxzL7vLJ9e9bNacvBgclQxdqM8nVBGJUeKUtya5wbhYZ18XTlbP8nVCj5ni0UR7D7UazCijLwCehyp5pyMPBHFgVpNMINWNgG3mVkNeLezK8EV8eYFoTU8rFkbqWuT8KB0ZwQv/s640/motoads_comment.png" /></a>
</td>
</tr>
</table>
The comment form we will realize as AngularJS directive. Before we start let's look at changed <code>adverts.json</code>, there is the additional <code>comments</code> field (array of comments):
<pre type="json">
[
{
"brandName": "Audi",
"modelName": "A1",
"year": 2011,
"price": 35000,
"imageUrl": "img/audi_a1_1.jpg",
"countryName": "Germany",
"regionName": "Bavaria",
"comments" : [
"This car is awesome I want it\nBeatiful color\nFunny look",
"Very cool vehicle\nJust perfect"
]
}
]
]]></pre>
<br/>
<b>Step 1</b>
<br/>
We create <code>commenForm.html</code> template which we use in directive:
<xmp>
<div class="ma-advert-comment-form">
<form name="commentForm" novalidate>
<div ng-hide="previewModeOn">
<textarea ng-model="content" placeholder="Write a comment..."></textarea>
</div>
<div class="ma-advert-comment-preview" ng-show="previewModeOn" ng-bind-html="preview | newlines"></div>
<div class="control-group">
<div class="controls">
<button ng-click="togglePreviewMode()" ng-disabled="isUnchanged()"
class="btn btn-info btn-small">{{ previewModeOn && 'Edit' || 'Preview' }}</button>
<button ng-click="sendCommentForm()" ng-disabled="isUnchanged()"
class="btn btn-success btn-small">Send</button>
<button ng-click="closeCommentForm()"
class="btn btn-small">Cancel</button>
</div>
</div>
</form>
</div>
</xmp>
<br/>
<b>Step 2</b>
<br/>
We in <code>directives.js</code> we create AngularJS directive:
<pre type="javascript">
motoAdsApp.directive('commentForm', function(Advert) {
return {
restrict: "E",
replace: true,
scope: {
commentModeOn: '=',
advertId: '='
},
templateUrl: "views/commentForm.html",
link: function(scope, element, attrs) {
scope.content = '';
scope.previewModeOn = false;
scope.closeCommentForm = function() {
scope.previewModeOn = false;
scope.commentModeOn = false;
};
scope.isUnchanged = function() {
return angular.isUndefined(scope.content) || angular.equals(scope.content, '');
};
scope.togglePreviewMode = function() {
scope.preview = scope.content;
scope.previewModeOn = !scope.previewModeOn;
};
scope.sendCommentForm = function() {
var advert = Advert.get({advertId: scope.advertId}, function() {
advert.comments.push(scope.content);
Advert.update(advert, function() {
scope.closeCommentForm();
scope.$parent.advert.comments.push(scope.content);
scope.content = '';
});
});
scope.preview = scope.content;
scope.previewModeOn = true;
};
}
};
});
]]></pre>
<br/>
<b>Step 3</b>
<br/>
We add simple <code>CommentController</code> into <code>controllers.js</code>:
<pre type="javascript">
motoAdsApp.controller('CommentController', ['$scope',
function($scope) {
$scope.commentModeOn = false;
$scope.isAnyComment = function() {
return ($scope.advert.comments.length > 0);
};
}]);
</pre>
<br/>
<b>Step 4</b>
<br/>
We add simple <code>newlines</code> filter into <code>filters.js</code>:
<pre type="javascript">
motoAdsApp.filter('newlines', function() {
return function(text) {
if (text) {
return text.replace(/\n/g, '<br/>');
}
return text;
};
});
</pre>
<br/>
<b>Step 5</b>
<br/>
We <code>comment-form</code> directive into <code>adverts.html</code> and some additional code to list comments and activate the comment form:
<xmp>
<table class="ma-advert-table">
<thead>
<tr class="ma-advert-table-headers">
<th>Image/Country/Region</th>
<th>Brand</th>
<th>Model</th>
<th>Year</th>
<th>Price ($)</th>
<th>Operation</th>
</tr>
</thead>
<tbody ng-controller="CommentController" ng-repeat="advert in adverts">
<tr class="ma-advert-table-row">
<td><a href="#/"><img class="img-rounded"
ng-src="{{advert.imageUrl}}"></a><br/>
<span>{{advert.countryName}}, {{advert.regionName}}</span></td>
<td>{{advert.brandName}}</td>
<td>{{advert.modelName}}</td>
<td>{{advert.year}}</td>
<td>{{advert.price}}</td>
<td>
<input type="hidden" value="{{advert._id}}">
<a href="" ng-click="editAdvert(advert._id)">Edit</a>
<br/><a href="" ng-click="removeAdvert($index)">Remove</a>
<br/><a href="" ng-click="commentModeOn = true">Comment</a></td>
</tr>
<tr class="ma-advert-comment-list-row" ng-show="isAnyComment()">
<td colspan="6">
<div class="ma-advert-comment-title">Comment(s):</div>
<div class="ma-advert-comment-list" ng-repeat="comment in advert.comments">
<div class="ma-advert-comment" ng-bind-html="comment | newlines" />
</div>
</td>
</tr>
<tr class="ma-advert-comment-form-row" ng-show="commentModeOn">
<td colspan="6">
<comment-form comment-mode-on="commentModeOn" advert-id="advert._id" />
</td>
</tr>
</tbody>
</table>
</xmp>
If you want to run this example on your computer, you can download sources from <a href="https://github.com/lukpaw/motoads/tree/blog-post-5">GitHub</a>.
<br/>
<br/>
Any comment would be highly appreciated.Łukasz Pawlikhttp://www.blogger.com/profile/04167291407338846196noreply@blogger.com0tag:blogger.com,1999:blog-3027091649644802032.post-21913464804400458272013-11-30T21:12:00.013+01:002023-03-05T16:21:39.130+01:00Java ESL program for connecting to the FreeSWITCHNext simple Java program using Event Socket interface to control FreeSWITCH.
First you should read:
<ul>
<li><a href="http://wiki.freeswitch.org/wiki/Mod_event_socket">Mod event socket on wiki freeswitch</a></li>
<li><a href="http://wiki.freeswitch.org/wiki/Java_ESL_Client">Java ESL Client on wiki freeswitch</a></li>
</ul>
Next you should not forget to change <code>event_socket.conf.xml</code> (to allow connections from any host on the network):
<xmp>
<configuration name="event_socket.conf" description="Socket Client">
<settings>
<!-- Allow socket connections from any host -->
<param name="listen-ip" value="0.0.0.0"/>
<param name="listen-port" value="8021"/>
<param name="password" value="ClueCon"/>
</settings>
</configuration>
</xmp>
Now we can write simple java ESL program for connecting to the FreeSWITCH.
<br/><br/>
<b>MyEslEventListener.java</b>
<pre type="java">
package myeslevent;
import java.util.Map;
import java.util.Set;
import org.freeswitch.esl.client.IEslEventListener;
import org.freeswitch.esl.client.transport.event.EslEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MyEslEventListener implements IEslEventListener {
private final Logger log = LoggerFactory.getLogger(this.getClass());
@Override
public void eventReceived(EslEvent event) {
log.info("eventReceived [{}]\n[{}]\n", event, getEventToLog(event));
}
@Override
public void backgroundJobResultReceived(EslEvent event) {
log.info("backgroundJobResultReceived [{}]\n[{}]\n", event, getEventToLog(event));
}
private String getEventToLog(EslEvent event) {
StringBuffer buf = new StringBuffer();
Map<String, String> map = event.getEventHeaders();
Set<String> set = map.keySet();
for (String name : set) {
buf.append(name + " " + map.get(name) + "\n");
}
return buf.toString();
}
}
</pre>
<b>MyEslEventTest.java</b>
<pre type="java">
package myeslevent;
import java.util.Scanner;
import org.freeswitch.esl.client.inbound.Client;
import org.freeswitch.esl.client.inbound.InboundConnectionFailure;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MyEslEventTest {
private static final Logger log = LoggerFactory.getLogger(MyEslEventTest.class);
public static void main(String[] args) throws InterruptedException, InboundConnectionFailure {
System.out.println("--- hello freeswitch (terminate program to finish) ---\n");
Thread.sleep(2000);
log.info("--- connect client ---");
Client client = new Client();
client.addEventListener(new MyEslEventListener());
client.connect("fshost", 8021, "ClueCon", 1);
log.info("--- event subscription ---");
Thread.sleep(3000);
client.setEventSubscriptions("plain", "all");
log.info("--- call 1000 1001 ---");
client.sendSyncApiCommand("originate", "user/1000 1001");
new Scanner(System.in).nextLine();
client.close();
System.out.println("\n--- Bye freeswitch ---");
}
}
</pre>
When we run this program we can see on the console much more than below:
<pre type="text">
--- hello freeswitch (terminate program to finish) ---
--- connect client ---
Received message: [EslMessage: contentType=[auth/request] headers=1, body=0 lines.]
Received message: [EslMessage: contentType=[command/reply] headers=2, body=0 lines.]
--- event subscription ---
Received message: [EslMessage: contentType=[command/reply] headers=2, body=0 lines.]
--- call 1000 1001 ---
Received message: [EslMessage: contentType=[api/response] headers=2, body=1 lines.]
eventReceived [EslEvent: name=[API] headers=2, eventHeaders=16, eventBody=0 lines.]
[
Event-Name API
Event-Calling-Function switch_api_execute
API-Command sofia_contact
Event-Date-Timestamp 1385849755755588
API-Command-Argument */1000@[?].[?].[?].[?]
]
eventReceived [EslEvent: name=[CHANNEL_OUTGOING] headers=2, eventHeaders=49, eventBody=0 lines.]
[
Caller-Source src/switch_ivr_originate.c
Event-Calling-Function switch_core_session_outgoing_channel
Answer-State ringing
Caller-Orig-Caller-ID-Number 0000000000
Channel-Name sofia/internal/sip:1000@[?].[?].[?].[?]:24878
...
]
9302 [EslEventNotifier-1] INFO myeslevent.MyEslEventListener - eventReceived [EslEvent: name=[CHANNEL_STATE]]
[ ... ]
eventReceived [EslEvent: name=[CHANNEL_CREATE] headers=2, eventHeaders=76, eventBody=0 lines.]
[ ... ]
eventReceived [EslEvent: name=[CHANNEL_ORIGINATE] headers=2, eventHeaders=76, eventBody=0 lines.]
[ ... ]
eventReceived [EslEvent: name=[PRESENCE_IN] headers=2, eventHeaders=63, eventBody=0 lines.]
[ ... ]
eventReceived [EslEvent: name=[CHANNEL_STATE] headers=2, eventHeaders=50, eventBody=0 lines.]
[ ... ]
eventReceived [EslEvent: name=[CHANNEL_CALLSTATE] headers=2, eventHeaders=53, eventBody=0 lines.]
[ ... ]
eventReceived [EslEvent: name=[CHANNEL_STATE] headers=2, eventHeaders=50, eventBody=0 lines.]
[ ... ]
eventReceived [EslEvent: name=[CODEC] headers=2, eventHeaders=60, eventBody=0 lines.]
[ ... ]
eventReceived [EslEvent: name=[CODEC] headers=2, eventHeaders=58, eventBody=0 lines.]
[ ... ]
eventReceived [EslEvent: name=[CHANNEL_PROGRESS] headers=2, eventHeaders=102, eventBody=0 lines.]
[ ... ]
eventReceived [EslEvent: name=[CHANNEL_ANSWER] headers=2, eventHeaders=124, eventBody=0 lines.]
[ ... ]
9320 [EslEventNotifier-1] INFO myeslevent.MyEslEventListener - eventReceived [EslEvent: name=[CHANNEL_OUTGOING]]
[ ... ]
eventReceived [EslEvent: name=[MESSAGE_QUERY] headers=2, eventHeaders=17, eventBody=0 lines.]
[ ... ]
eventReceived [EslEvent: name=[PRESENCE_IN] headers=2, eventHeaders=70, eventBody=0 lines.]
[ ... ]
eventReceived [EslEvent: name=[CHANNEL_STATE] headers=2, eventHeaders=59, eventBody=0 lines.]
[ ... ]
eventReceived [EslEvent: name=[CHANNEL_EXECUTE_COMPLETE] headers=2, eventHeaders=154, eventBody=0 lines.]
[ ... ]
eventReceived [EslEvent: name=[CHANNEL_EXECUTE] headers=2, eventHeaders=153, eventBody=0 lines.]
[ ... ]
...
eventReceived [EslEvent: name=[HEARTBEAT] headers=2, eventHeaders=27, eventBody=0 lines.]
[ ... ]
eventReceived [EslEvent: name=[RE_SCHEDULE] headers=2, eventHeaders=18, eventBody=0 lines.]
[ ... ]
]]></pre>
Good start to do something cool :)Łukasz Pawlikhttp://www.blogger.com/profile/04167291407338846196noreply@blogger.com0tag:blogger.com,1999:blog-3027091649644802032.post-55838492539649968572013-11-28T21:45:00.003+01:002023-02-03T10:31:28.618+01:00Java program for connecting to the FreeSWITCH XML-RPCI would like to show simple Java program which use XML-RPC interface to do some freeswitch commands.
We should on freeswitch console load mod_xml_rpc:
<script type="syntaxhighlighter" class="brush: plain">
<![CDATA[
freeswitch@internal> load mod_xml_rpc
]]></script>
Worth checking out if it works, in web browser type:
<pre type="text">
http://fshost8080/webapi/help
</pre>
If you see "FreeSWITCH help" it works.
<br/>
<br/>
Next we download <a href="http://ws.apache.org/xmlrpc/">Apache XML-RPC</a> library.
<br/>
<br/>
Now we create Java program (using Apache XML-RPC):
<pre type="java">
package fstest1;
import java.net.URL;
import java.util.Scanner;
import org.apache.xmlrpc.client.XmlRpcClient;
import org.apache.xmlrpc.client.XmlRpcClientConfigImpl;
public class FsTest1 {
public static void main(String[] args) {
System.out.println("------ hello freeswitch -------\n");
XmlRpcClientConfigImpl config = new XmlRpcClientConfigImpl();
XmlRpcClient client = new XmlRpcClient();
try {
config.setServerURL(new URL("http://fshost:8080/RPC2"));
config.setBasicUserName("freeswitch");
config.setBasicPassword("works");
client.setConfig(config);
System.out.println("--- call 1000 1001 (press Enter) ---");
new Scanner(System.in).nextLine();
String resp = (String)client.execute("freeswitch.api", new Object[]{"originate", "user/1000 1001 xml default"});
System.out.println(resp);
System.out.println("--- show channels (press Enter) ---");
new Scanner(System.in).nextLine();
resp = (String)client.execute("freeswitch.api", new Object[]{"show", "channels"});
System.out.println(resp);
int idx = resp.indexOf("\n");
resp = resp.substring(idx);
idx = resp.indexOf(",");
String uuid = resp.substring(1, idx);
System.out.println("--- uuid_transfer " + uuid + " 5000 (press Enter) ---");
new Scanner(System.in).nextLine();
resp = (String)client.execute("freeswitch.api", new Object[]{"uuid_transfer", uuid + " 5000"});
System.out.println(resp);
System.out.println("--- show channels (press Enter) ---");
new Scanner(System.in).nextLine();
resp = (String)client.execute("freeswitch.api", new Object[]{"show", "channels"});
System.out.println(resp);
System.out.println("--- uuid_kill " + uuid + " (press Enter) ---");
new Scanner(System.in).nextLine();
resp = (String)client.execute("freeswitch.api", new Object[]{"uuid_kill", uuid});
System.out.println(resp);
} catch (Exception ex) {
ex.printStackTrace();
}
System.out.println("--- bye freeswitch ---");
}
}
</pre>
<br/>
After running this simple Java program we can see on Java console:
<pre type="text">
------ hello freeswitch -------
--- call 1000 1001 (press Enter) ---
+OK 0e913ce9-6c86-4f28-a6ed-7356084873bc
--- show channels (press Enter) ---
uuid,direction,(...),sent_callee_num
0e913ce9-6c86-4f28-a6ed-7356084873bc,outbound,(...),1001
cb6a78bf-ce83-4269-9af2-c100991adff3,outbound,(...),1000
2 total.
--- uuid_transfer 0e913ce9-6c86-4f28-a6ed-7356084873bc 5000 (press Enter) ---
+OK
--- show channels (press Enter) ---
uuid,direction,(...),cid_num,(...), dest,(...)
0e913ce9-6c86-4f28-a6ed-7356084873bc,outbound,(...)1000,(...),5000,(...)
1 total.
--- uuid_kill 0e913ce9-6c86-4f28-a6ed-7356084873bc (press Enter) ---
+OK
--- bye freeswitch ---
</pre>
A lot of fun :)Łukasz Pawlikhttp://www.blogger.com/profile/04167291407338846196noreply@blogger.com0tag:blogger.com,1999:blog-3027091649644802032.post-55101377338270860042013-11-23T16:00:00.003+01:002023-03-05T16:28:06.046+01:00AngularJS example MotoAds with NodeJS and MongoDBI built MotoAds demo application in AngularJS but it did not have any server layer. Data was read directly from the json files. So I was decided to build the server side services. I know pretty well JEE and relational database, so I could use it. But I want to know something new so I chose NodeJS nad MongoDB. Thanks to this decision I got a full stack JavaScript application. It's incredible to use JavaScript to build the complete application.
<br/>
<br/>
<table>
<tr>
<td style="vertical-align: top;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjT3g2nPuxMQ5f3Y95gyEEQ-VNmVnSg10UzGCSg1NWalsOxZGWs6xg4qLFDtFnqJmpflyPO82ZvF1H8YOhoGxmNMlL9zc2Jo9JCWEznNNw6bZMoo60njnMVmg_Y06ENUPZSrRvtrol0u-Uh/s1600/main.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjT3g2nPuxMQ5f3Y95gyEEQ-VNmVnSg10UzGCSg1NWalsOxZGWs6xg4qLFDtFnqJmpflyPO82ZvF1H8YOhoGxmNMlL9zc2Jo9JCWEznNNw6bZMoo60njnMVmg_Y06ENUPZSrRvtrol0u-Uh/s640/main.png" /></a>
</td>
<td style="vertical-align: top;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjBijNJLcUftpRCqEKeimVEwcuv3jlK68ng286Yrxh7fOKdl7Yx2C7lPRU3FTKU47v5UHHezqLNczvvPonGf86qKhARHY_GxWC5iNfARKjNZGhkO-s9OgRaBv2JbsPmOej_9oJjvnzel84c/s1600/edit.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjBijNJLcUftpRCqEKeimVEwcuv3jlK68ng286Yrxh7fOKdl7Yx2C7lPRU3FTKU47v5UHHezqLNczvvPonGf86qKhARHY_GxWC5iNfARKjNZGhkO-s9OgRaBv2JbsPmOej_9oJjvnzel84c/s320/edit.png" /></a>
</td>
</tr>
</table>
Now MotoAds demo application consists of:
<ul>
<li>User interface in AngularJS and Bootstrap with full CRUD operations: add, read, edit and remove adverts.</li>
<li>Server service layer was built in NodeJS with RESTful serrvices. To simplify the use of a NodeJS I used ExpressJS.</li>
<li>Database: all data except the pictures are stored in MongoDB.</li>
</ul>
So let's see how to add the CRUD operations in AngularJS with services in NodeJS with MongoDB.
<br/>
<br/>
<b>Step 1</b>
<br/>
We write in <code>services.js</code> access to our RESTful services:
<pre type="javascript">
'use strict';
var motoAdsServices = angular.module('motoAdsServices', ['ngResource']);
motoAdsServices.factory('Brand', ['$resource', function($resource) {
return $resource('/api/brands', {}, {});
}]);
motoAdsServices.factory('Country', ['$resource', function($resource) {
return $resource('/api/countries', {}, {});
}]);
motoAdsServices.factory('Advert', ['$resource', function($resource) {
return $resource('/api/adverts/:advertId', {}, {
update: {method:'PUT', params: {advertId: '@_id'}}
});
}]);
</pre>
<br/>
<b>Step 2</b>
<br/>
Inject services in <code>app.js</code>:
<pre type="javascript">
'use strict';
var motoAdsApp = angular.module('motoAdsApp', ['ngRoute', 'ui.bootstrap', 'motoAdsServices']);
motoAdsApp.config(['$routeProvider',
function($routeProvider) {
$routeProvider.
when('/', {
controller: 'AdvertsController',
templateUrl: 'views/adverts.html'
}).
when('/addAdvert', {
controller: 'AddAdvertController',
templateUrl: 'views/addAdvert.html'
}).
when('/editAdvert/:advertId', {
controller: 'EditAdvertController',
templateUrl: 'views/editAdvert.html'
});
}]);
</pre>
<br/>
We use the angularjs services in <code>controllers.js</code>:
<pre type="javascript">
motoAdsApp.controller('AdvertsController', ['$scope', '$window', 'Brand', 'Country', 'Advert',
function($scope, $window, Brand, Country, Advert) {
// ...
$scope.brands = Brand.query();
$scope.countries = Country.query();
// ...
$scope.adverts = [];
var allAdverts = Advert.query(filterAdverts);
// ...
$scope.$watch('filter', filterAdverts, true);
function filterAdverts() {
$scope.adverts = [];
angular.forEach(allAdverts, function(row) {
// ...
$scope.adverts.push(row);
});
}
$scope.removeAdvert = function(idx) {
var removeAdvert = $scope.adverts[idx];
Advert.remove({advertId: removeAdvert._id}, function() {
$scope.adverts.splice(idx, 1);
alert('Advert removed');
});
};
$scope.editAdvert = function(_advertId) {
$window.location = "#/editAdvert/" + _advertId;
};
}]);
motoAdsApp.controller('AddAdvertController', ['$scope', '$window', 'Brand', 'Country', 'Advert',
function($scope, $window, Brand, Country, Advert) {
$scope.brands = Brand.query();
$scope.countries = Country.query();
// ...
$scope.addAdvert = function() {
Advert.save($scope.newAdvert, function() {
alert('New advert added');
$window.location = "#/";
});
};
// ...
}]);
motoAdsApp.controller('EditAdvertController', ['$scope', '$routeParams', '$window', 'Brand', 'Country', 'Advert',
function($scope, $routeParams, $window, Brand, Country, Advert) {
$scope.brands = Brand.query();
//...
$scope.countries = Country.query();
// ...
var previousAdvert = null;
$scope.editAdvert = Advert.get({advertId: $routeParams.advertId}, function() {
// ...
});
$scope.updateAdvert = function() {
Advert.update($scope.editAdvert, function() {
alert('Advert updated');
$window.location = "#/";
});
};
// ...
}]);
</pre>
<br/>
<b>Step 3</b>
<br/>
In <code>server.js</code> we create the HTTP server with the RESTful service in NodeJS and ExpressJS. It is also used to host angularjs app:
<pre type="javascript">
var express = require('express');
var path = require('path');
var http = require('http');
var brands = require('./routes/brands');
var countries = require('./routes/countries');
var adverts = require('./routes/adverts');
var app = express();
app.configure(function() {
app.set('port', process.env.PORT || 3000);
app.use(express.logger('dev')); /* 'default', 'short', 'tiny', 'dev' */
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(express.static(path.join(__dirname, 'public')));
// TODO should be created separate test server
app.use('/test', express.static(path.join(__dirname, 'test')));
});
app.get('/api/brands', brands.findAll);
app.get('/api/countries', countries.findAll);
app.get('/api/adverts', adverts.findAll);
app.get('/api/adverts/:id', adverts.findById);
app.post('/api/adverts', adverts.add);
app.put('/api/adverts/:id', adverts.update);
app.delete('/api/adverts/:id', adverts.remove);
http.createServer(app).listen(app.get('port'), function() {
console.log("Express server listening on port " + app.get('port'));
});
</pre>
<br/>
<b>Step 4</b>
<br/>
In <code>adverts.js</code> we write some code in NodeJS which allows us to use MongoDB:
<pre type="javascript">
var mongo = require('mongodb');
var Server = mongo.Server;
var Db = mongo.Db;
var BSON = mongo.BSONPure;
var server = new Server('localhost', 27017, {auto_reconnect: true});
db = new Db('motoads', server);
db.open(function(err, db) {
if (!err) {
console.log("Connected to motoads database");
db.collection('adverts', {strict: true}, function(err, collection) {
if (err) {
console.log("The adverts collection does not exist. Creating it with sample data...");
populateDB();
}
});
}
});
exports.findAll = function(req, res) {
db.collection('adverts', function(err, collection) {
collection.find().toArray(function(err, items) {
console.log('adverts send from DB');
res.send(items);
});
});
};
exports.findById = function(req, res) {
var id = req.params.id;
console.log('Retrieving advert: ' + id);
db.collection('adverts', function(err, collection) {
collection.findOne({'_id': new BSON.ObjectID(id)}, function(err, item) {
res.send(item);
});
});
};
exports.add = function(req, res) {
var advert = req.body;
console.log('Adding advert: ' + JSON.stringify(advert));
db.collection('adverts', function(err, collection) {
collection.insert(advert, {safe: true}, function(err, result) {
if (err) {
res.send({'error': 'An error has occurred'});
} else {
console.log('Success: ' + JSON.stringify(result[0]));
res.send(result[0]);
}
});
});
};
exports.update = function(req, res) {
var id = req.params.id;
var advert = req.body;
console.log('Updating advert: ' + id);
console.log(JSON.stringify(advert));
delete advert._id;
db.collection('adverts', function(err, collection) {
collection.update({'_id': new BSON.ObjectID(id)}, advert, {safe: true}, function(err, result) {
if (err) {
console.log('Error updating advert: ' + err);
res.send({'error': 'An error has occurred'});
} else {
console.log('' + result + ' document(s) updated');
res.send(advert);
}
});
});
};
exports.remove = function(req, res) {
var id = req.params.id;
console.log('Removing advert: ' + id);
db.collection('adverts', function(err, collection) {
collection.remove({'_id': new BSON.ObjectID(id)}, {safe: true}, function(err, result) {
if (err) {
res.send({'error': 'An error has occurred - ' + err});
} else {
console.log('' + result + ' document(s) removed');
res.send(req.body);
}
});
});
};
var populateDB = function() {
var fs = require('fs');
var file = './data/adverts.json';
fs.readFile(file, 'utf8', function(err, data) {
if (err) {
throw err;
}
var adverts = JSON.parse(data);
db.collection('adverts', function(err, collection) {
if (err) {
throw err;
}
collection.insert(adverts, {safe: true}, function(err, result) {
if (err) {
throw err;
}
});
});
});
};
</pre>
<br/>
We run MongoDB and start our HTTP server (<code>node server.js</code>). Now we are typing in web browser URL <code>http://localhost:3000/#/</code>. In web browser we should see the MotoAds application and we can use CRUD operations.
<br/>
<br/>
If you want to run this example on your computer, you can download sources from <a href="https://github.com/lukpaw/motoads/tree/blog-post-4">GitHub</a>.
<br/>
<br/>
Any comment would be highly appreciated.Łukasz Pawlikhttp://www.blogger.com/profile/04167291407338846196noreply@blogger.com0tag:blogger.com,1999:blog-3027091649644802032.post-67950613890085023482013-11-18T19:51:00.007+01:002023-03-05T16:31:11.951+01:00AngularJS resource with JSONPLately I struggled to get access to facebook by JSONP. I try to do it in AngularJS with factory and resource. I want to share with people my solution. Let's try to connect to the Facebook Graph.
Firstly, we create <code>index.html</code> file with input <code>graph.username</code>, after type a value there is called <code>getGraph()</code>. The result of the calling this function is saved in <code>$scope.result</code> so we can display it.
<xmp>
<html ng-app="myApp">
<head>
<title>AngularJS resource with JSONP</title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular.min.js"/>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular-resource.min.js"/>
<script type="text/javascript" src="app.js"/>
</head>
<body>
<div ng-controller="MyController">
Graph for:
<input type="text" ng-model="graph.username" ng-blur="getGraph()">
<h3>{{result.name}}</h3>
<div>{{result.about}}</div>
</div>
</body>
</html>
</xmp>
Secondly, in <code>app.js</code> we write controller and factory which creates a resource object that call the Facebook Graph. So when it is called function <code>getGraph</code> we use GraphFacebook factory to call <code>getJSONP</code> passing in argument <code>$scope.graph</code> with <code>username</code> property. This property is used in building URL.
<pre type="javascript">
var myApp = angular.module('myApp', ['myServices']);
myApp.controller('MyController', ['$scope', 'GraphFacebook',
function($scope, GraphFacebook) {
$scope.getGraph = function() {
$scope.result = GraphFacebook.getJSONP($scope.graph);
};
}]);
var myServices = angular.module('myServices', ['ngResource']);
myServices.factory('GraphFacebook', ['$resource',
function($resource) {
return $resource('https://graph.facebook.com/:username', {}, {
getJSONP: {
method: 'JSONP',
params: {
callback: 'JSON_CALLBACK'
}
}
});
}
]);
</pre>
<br/>
The result of our work:
<br/>
<br/>
<div style="width: 100%">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiVpLX_vkVd2aB93kLoPSr1XYHutnbtBp8P8TJMZeYkNc1QsYzzb79Q5PP_JNaS8q1lt3R6YG84Odigz3b1DiDZPklxb691kuCw8tCxaquMshnQBOREQDXFvH-2TQKV6XyIHeokt3mn3qP2/s1600/angular-ex1.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiVpLX_vkVd2aB93kLoPSr1XYHutnbtBp8P8TJMZeYkNc1QsYzzb79Q5PP_JNaS8q1lt3R6YG84Odigz3b1DiDZPklxb691kuCw8tCxaquMshnQBOREQDXFvH-2TQKV6XyIHeokt3mn3qP2/s640/angular-ex1.png" /></a>
</div>
<br/>
<a href="https://github.com/lukpaw/angular-example-1">Source on GitHub</a>
Łukasz Pawlikhttp://www.blogger.com/profile/04167291407338846196noreply@blogger.com0tag:blogger.com,1999:blog-3027091649644802032.post-79853448851280313042013-11-09T10:13:00.004+01:002023-03-05T16:33:06.985+01:00AngularJS example MotoAds end-to-end tests<table>
<tr><td>
E2E (end-to-end) tests result:
</td>
<td>
Application folders tree:
</td>
</tr>
<tr>
<td style="vertical-align: top;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhRPk1xqAiVtkQG0fo6sazVX3UIMbOaVaC5-l9HFrEu8kBJ_B99UcWwpqTZoGR1UWMqbkgn-PGCghTzj9uOfMvnA92Iwyn2yUohal1OhKvAxJB1BrQY748-aVxt1B5-WDFPS0FzzoAKpTAf/s1600/motoads_test_result3.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhRPk1xqAiVtkQG0fo6sazVX3UIMbOaVaC5-l9HFrEu8kBJ_B99UcWwpqTZoGR1UWMqbkgn-PGCghTzj9uOfMvnA92Iwyn2yUohal1OhKvAxJB1BrQY748-aVxt1B5-WDFPS0FzzoAKpTAf/s640/motoads_test_result3.png" /></a>
</td>
<td style="vertical-align: top;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiXQu5YjkKriQTn2sQOwmp_t3XidUXQ8uu24hzqEictPBWPmUgI3mBC_T9ikVfBiWgKoM-_kI8I6_lHIrUPtkx8PmDYFv3eSmcUalvHiSXU0VDac1Aypfm8rr7vOt8vNava3bcPWy6HH-vn/s1600/motoads_folder_tree3.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiXQu5YjkKriQTn2sQOwmp_t3XidUXQ8uu24hzqEictPBWPmUgI3mBC_T9ikVfBiWgKoM-_kI8I6_lHIrUPtkx8PmDYFv3eSmcUalvHiSXU0VDac1Aypfm8rr7vOt8vNava3bcPWy6HH-vn/s640/motoads_folder_tree3.png" /></a>
</td>
</tr>
</table>
In previous post I showed how to write and run unit tests. Now, I will present how to test DOM manipulation or the wiring of our application. Angular Scenario Runner which simulates user interactions that will help us to do that.
<br/>
<br/>
Recall <code>adverts.html</code> code which we should know to simulate user interaction:
<xmp>
<div class="row-fluid">
<div class="span3">
<h4>Adverts</h4>
<div class="ma-left-filter-panel">
<accordion close-others="oneAtATime">
<accordion-group heading="{{brand.name}}" ng-repeat="brand in brands">
<ul ng-repeat="model in brand.models">
<li><a href="" ng-click="addBrandModelFilter(brand, model)">{{model.name}}</a></li>
</ul>
</accordion-group>
</accordion>
</div>
</div>
<div class="span9">
<div class="ma-top-filter-panel">
<h4>Adverts search</h4>
<form>
<div class="row-fluid">
<div class="span4">
<label>Country</label>
<select ng-model="filter.country" ng-options="c.name for c in countries">
<option value=""></option>
</select>
</div>
<div class="span4">
<label>Region</label>
<select ng-model="filter.region" ng-options="r.name for r in filter.country.regions">
<option value=""></option>
</select>
</div>
<div class="span4">
<label>Year of production</label>
<input ng-model="filter.yearFrom" class="input-small" min="1980" max="2013"
ng-minlength="4" ng-maxlength="4" type="number" placeholder="From">
<input ng-model="filter.yearTo" class="input-small" min="1980" max="2013"
ng-minlength="4" ng-maxlength="4" type="number" placeholder="To">
</div>
</div>
<div class="row-fluid">
<div class="span12">
<span ng-show="isAnyFilter()">Filters:</span>
<div class="ma-chosen-filter-div">
<span ng-show="filter.modelName">{{filter.brandName}} {{filter.modelName}}
<i class="icon-remove-sign icon-white" ng-click="filter.brandName = null;
filter.modelName = null"></i></span>
<span ng-show="filter.country.name">{{filter.country.name}}
<i class="icon-remove-sign icon-white" ng-click="filter.country = null;
filter.region = null"></i></span>
<span ng-show="filter.region.name">{{filter.region.name}}
<i class="icon-remove-sign icon-white" ng-click="filter.region = null"></i></span>
<span ng-show="filter.yearFrom">{{filter.yearFrom}}
<i class="icon-remove-sign icon-white" ng-click="filter.yearFrom = null"></i></span>
<span ng-show="filter.yearTo">{{filter.yearTo}}
<i class="icon-remove-sign icon-white" ng-click="filter.yearTo = null"></i></span>
</div>
<a ng-show="isAnyFilter()" href="" ng-click="removeAllFilter()">Remove all</a>
</div>
</div>
</form>
</div>
<div>
<div class="row-fluid">
<div class="span4">
<h4>Adverts found ({{adverts.length}})</h4>
</div>
<div class="offset3 span5">
<form class="form-inline">
<label>Sort by</label>
<select ng-model="sortByCol" ng-options="s.name for s in sortByCols">
<option value=""></option>
</select>
</form>
</div>
</div>
<table class="table table-striped ma-advert-table">
<tr class="ma-advert-table-headers">
<th>Image/Country/Region</th>
<th>Brand</th>
<th>Model</th>
<th>Year</th>
<th>Price ($)</th>
</tr>
<tr class="ma-advert-table-row" ng-repeat="advert in adverts| orderBy:sortByCol.key">
<td><a href="#/"><img class="img-rounded"
ng-src="{{advert.imageUrl}}"></a><br/>
<span>{{advert.countryName}}, {{advert.regionName}}</span></td>
<td>{{advert.brandName}}</td>
<td>{{advert.modelName}}</td>
<td>{{advert.year}}</td>
<td>{{advert.price}}</td>
</tr>
</table>
</div>
</div>
</div>
</xmp>
<br/>
We should write the simple html page for running unit tests (<code>runner.html</code>):
<xmp>
<html lang="en">
<head>
<title>End2end Test Runner</title>
<meta charset="utf-8">
<script src="../../app/lib/angular/angular-scenario.js" ng-autotest></script>
<script src="scenarios.js"></script>
</head>
<body>
</body>
</html>
</xmp>
<br/>
When everything is ready we can start writing tests in <code>scenarios.js</code>.
<br/>
<br/>
<b>Step 1</b>
<br/>
Navigate web browser to <code>index.html</code> which route to <code>adverts.html</code> view:
<pre type="javascript">
'use strict';
describe('MotoAds App', function() {
describe('Adverts view', function() {
beforeEach(function() {
browser().navigateTo('../../app/index.html#/');
});
// TEST WILL BE HERE
});
});
]]></pre>
<br/>
<b>Step 2</b>
<br/>
We write first E2E test - filtering by brand and model on real data:
<pre type="javascript">
it('should filter on brand and model link click', function() {
expect(repeater('.ma-advert-table-row').count()).toBe(29);
element('.accordion-group:eq(0) ul:eq(0) a').click();
expect(repeater('.ma-advert-table-row').count()).toBe(4);
element('.accordion-group:eq(3) ul:eq(0) a').click();
expect(repeater('.ma-advert-table-row').count()).toBe(2);
});
]]></pre>
<br/>
<b>Step 3</b>
<br/>
We write second E2E test - filtering by country and region:
<pre type="javascript">
it('should filter on country and region select choose', function() {
expect(repeater('.ma-advert-table-row').count()).toBe(29);
select('filter.country').option('Germany');
expect(repeater('.ma-advert-table-row').count()).toBe(20);
select('filter.region').option('Bavaria');
expect(repeater('.ma-advert-table-row').count()).toBe(18);
});
]]></pre>
<br/>
<b>Step 4</b>
<br/>
We write third E2E test - filtering by year:
<pre type="javascript">
it('should filter on yearFrom and yearTo type', function() {
expect(repeater('.ma-advert-table-row').count()).toBe(29);
input('filter.yearFrom').enter('2010');
expect(repeater('.ma-advert-table-row').count()).toBe(16);
input('filter.yearTo').enter('2011');
expect(repeater('.ma-advert-table-row').count()).toBe(14);
});
]]></pre>
<br/>
<b>Step 5</b>
<br/>
We write fourth E2E test - sorting by year:
<pre type="javascript">
it('should sort by year', function() {
expect(repeater('.ma-advert-table-row').count()).toBe(29);
select('sortByCol').option("Year");
expect(element('.ma-advert-table-row:eq(0) td:eq(3)').text()).toContain('2005');
expect(element('.ma-advert-table-row:eq(28) td:eq(3)').text()).toContain('2012');
});
]]></pre>
<br/>
<b>Step 6</b>
<br/>
We run E2E test by typing in web browser URL <code>http://localhost:8000/test/e2e/runner.html</code> (motoAds application must be run on the web server). In web browser we should see the same as the screenshot <i>E2E (end-to-end) tests result</i>.
<br/>
<br/>
If you want to run this example on your computer, you can download sources from <a href="https://github.com/lukpaw/motoads/tree/blog-post-3">GitHub</a>.
<br/>
<br/>
Any comment would be highly appreciated.Łukasz Pawlikhttp://www.blogger.com/profile/04167291407338846196noreply@blogger.com0tag:blogger.com,1999:blog-3027091649644802032.post-62765361289011786482013-11-08T22:37:00.003+01:002023-03-05T16:37:13.281+01:00AngularJS example MotoAds unit tests<table>
<tr><td>
Unit tests result:
</td>
<td>
Application folders tree:
</td>
</tr>
<tr>
<td style="vertical-align: top;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEir1YC6GTXF_jbXCedkyfYDDsLwPlvIWOK5WX6a2G1YJoj9PuXOztXKtdYGnwg9dSNXKAyZOBZ8atcIL31CGO0p6HNW8Xi1XDQZU2qTCwOAgVkN4awVbcvEpuyiY39dHTsYrE79OcQFKSN3/s1600/motoads_test_result2.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEir1YC6GTXF_jbXCedkyfYDDsLwPlvIWOK5WX6a2G1YJoj9PuXOztXKtdYGnwg9dSNXKAyZOBZ8atcIL31CGO0p6HNW8Xi1XDQZU2qTCwOAgVkN4awVbcvEpuyiY39dHTsYrE79OcQFKSN3/s640/motoads_test_result2.png" /></a>
</td>
<td style="vertical-align: top;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjL_ITelO4m4MlaYjZtEXfVcrC26b-cEXhq4KjIj8Md2e0f7hvoBgIY5ts1mcHQ6KJ03geBufferYbzzM0PjpkZLCgEdMyv7_UTOfik5OmpLyK-qkA0uCcnVnorGOU636K_e90bpzFVXgSE/s1600/motoads_folder_tree2.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjL_ITelO4m4MlaYjZtEXfVcrC26b-cEXhq4KjIj8Md2e0f7hvoBgIY5ts1mcHQ6KJ03geBufferYbzzM0PjpkZLCgEdMyv7_UTOfik5OmpLyK-qkA0uCcnVnorGOU636K_e90bpzFVXgSE/s640/motoads_folder_tree2.png" /></a>
</td>
</tr>
</table>
In Angular the controller is separated from the view so it is easy to add the unit tests to MotoAds application. Recall the controller code which will be tested (<code>controllers.js</code>):
<pre type="javascript">
motoAdsApp.controller('AdvertsController', ['$scope', 'Brand', 'Country', 'Advert',
function($scope, Brand, Country, Advert) {
$scope.oneAtATime = true;
$scope.brands = Brand.query();
$scope.countries = Country.query();
$scope.sortByCols = [{
"key": "year",
"name": "Year"
}, {
"key": "price",
"name": "Price"
}];
$scope.adverts = [];
var allAdverts = Advert.query(filterAdverts);
$scope.filter = {
brandName: null,
modelName: null,
country: null,
region: null,
yearFrom: null,
yearTo: null
};
$scope.isAnyFilter = function() {
var f = $scope.filter;
if (f.brandName || f.modelName || f.country || f.region || f.yearFrom || f.yearTo) {
return true;
}
return false;
};
$scope.removeAllFilter = function() {
$scope.filter = {
brandName: null,
modelName: null,
country: null,
region: null,
yearFrom: null,
yearTo: null
};
};
$scope.addBrandModelFilter = function(brand, model) {
$scope.filter.brandName = brand.name;
$scope.filter.modelName = model.name;
};
$scope.$watch('filter', filterAdverts, true);
function filterAdverts() {
$scope.adverts = [];
angular.forEach(allAdverts, function(row) {
if (!$scope.filter.country) {
$scope.filter.region = null;
}
if ($scope.filter.brandName && $scope.filter.brandName !== row.brandName) {
return;
}
if ($scope.filter.modelName && $scope.filter.modelName !== row.modelName) {
return;
}
if ($scope.filter.country && $scope.filter.country.name !== row.countryName) {
return;
}
if ($scope.filter.region && $scope.filter.region.name !== row.regionName) {
return;
}
if ($scope.filter.yearFrom && $scope.filter.yearFrom > row.year) {
return;
}
if ($scope.filter.yearTo && $scope.filter.yearTo < row.year) {
return;
}
$scope.adverts.push(row);
});
};
}]);
]]></pre>
We should write the script for running unit tests (<code>test.bat</code>):
<pre type="text">
@echo off
REM Windows script for running unit tests
REM You have to run server and capture some browser first
REM
REM Requirements:
REM - NodeJS (http://nodejs.org/)
REM - Karma (npm install -g karma)
set BASE_DIR=%~dp0
karma start "%BASE_DIR%\..\config\karma.conf.js" %*
]]></pre>
We configure Karma for our tests (karma.conf.js):
<pre type="javascript">
module.exports = function(config){
config.set({
basePath : '../',
files : [
'app/lib/angular/angular.js',
'app/lib/angular/angular-*.js',
'app/lib/ui-bootstrap/ui-bootstrap-*.js',
'app/lib/angular/angular-mocks.js',
'app/js/**/*.js',
'test/unit/**/*.js'
],
exclude: ['app/lib/angular/angular-scenario.js'],
autoWatch : true,
frameworks: ['jasmine'],
browsers : ['Chrome'],
plugins : [
'karma-junit-reporter',
'karma-chrome-launcher',
'karma-firefox-launcher',
'karma-jasmine'
],
junitReporter : {
outputFile: 'test_out/unit.xml',
suite: 'unit'
}
});};
]]></pre>
When everything is ready we can start writing tests in <code>controllersSpec.js</code>.
<br/>
<br/>
<b>Step 1</b>
<br/>
We prepare dummy data:
<pre type="javascript">
var BRANDS_RESPONSE = [
{"name": "Audi", "models": [{"name": "A1"}, {"name": "A3"}]},
{"name": "Dacia", "models": [{"name": "Duster"}, {"name": "Logan"}]}
];
var COUNTRIES_RESPONSE = [
{"name": "Germany", "regions": [{"name": "Bavaria"}, {"name": "Hesse"}]},
{"name": "Poland", "regions": [{"name": "Lesser Poland"}, {"name": "Masovia"}]}
];
var ADVERTS_RESPONSE = [
{
"brandName": "Audi",
"modelName": "A1",
"year": 2011,
"price": "35000",
"imageUrl": "img/audi_a1_1.jpg",
"countryName": "Germany",
"regionName": "Bavaria"
},
{
"brandName": "Audi",
"modelName": "A1",
"year": 2010,
"price": "30000",
"imageUrl": "img/audi_a1_2.jpg",
"countryName": "Poland",
"regionName": "Masovia"
},
{
"brandName": "Dacia",
"modelName": "Duster",
"year": 2012,
"price": "33000",
"imageUrl": "img/dacia_duster_1.jpg",
"countryName": "Poland",
"regionName": "Masovia"
},
{
"brandName": "Dacia",
"modelName": "Duster",
"year": 2009,
"price": "27000",
"imageUrl": "img/dacia_duster_2.jpg",
"countryName": "Germany",
"regionName": "Bavaria"
}];
]]></pre>
<br/>
<b>Step 2</b>
<br/>
Load modules, define dummy services, initialize scope, controller and filter:
<pre type="javascript">
describe('MotoAds controllers', function() {
beforeEach(module('motoAdsApp'));
beforeEach(module('ui.bootstrap'));
beforeEach(module('motoAdsServices'));
describe('AdvertsController', function() {
var scope, ctrl, $httpBackend, orderByFilter;
beforeEach(inject(function(_$httpBackend_, $rootScope, $controller, $filter) {
$httpBackend = _$httpBackend_;
$httpBackend.expectGET('data/brands.json').respond(BRANDS_RESPONSE);
$httpBackend.expectGET('data/countries.json').respond(COUNTRIES_RESPONSE);
$httpBackend.expectGET('data/adverts.json').respond(ADVERTS_RESPONSE);
orderByFilter = $filter('orderBy');
scope = $rootScope.$new();
ctrl = $controller('AdvertsController', {$scope: scope});
}));
// TEST WILL BE HERE
});
});
]]></pre>
<br/>
<b>Step 3</b>
<br/>
We write first test - filtering by brand and model:
<pre type="javascript">
it('should filter by brand and model', function() {
expect(scope.adverts).toEqual([]);
$httpBackend.flush();
expect(scope.adverts.length).toBe(4);
scope.$apply(function() {
scope.filter = {
brandName: "Audi",
modelName: "A1",
country: null,
region: null,
yearFrom: null,
yearTo: null
};
});
expect(scope.adverts.length).toBe(2);
});
]]></pre>
<br/>
<b>Step 4</b>
<br/>
We write second test - filtering by country and region:
<pre type="javascript">
it('should filter by country and region', function() {
expect(scope.adverts).toEqual([]);
$httpBackend.flush();
expect(scope.adverts.length).toBe(4);
scope.$apply(function() {
scope.filter = {
brandName: null,
modelName: null,
country: {name: "Germany"},
region: {name: "Bavaria"},
yearFrom: null,
yearTo: null
};
});
expect(scope.adverts.length).toBe(2);
});
]]></pre>
<br/>
<b>Step 5</b>
<br/>
We write third test - filtering by year:
<pre type="javascript">
it('should filter by from yearFrom to yearTo', function() {
expect(scope.adverts).toEqual([]);
$httpBackend.flush();
expect(scope.adverts.length).toBe(4);
scope.$apply(function() {
scope.filter = {
brandName: null,
modelName: null,
country: null,
region: null,
yearFrom: 2011,
yearTo: 2012
};
});
expect(scope.adverts.length).toBe(2);
});
]]></pre>
<br/>
<b>Step 6</b>
<br/>
We write fourth test - sorting by year:
<pre type="javascript">
it('should sort by year', function() {
expect(scope.adverts).toEqual([]);
$httpBackend.flush();
expect(scope.adverts.length).toBe(4);
var sortedAdverts = orderByFilter(scope.adverts, "year");
var prevYear = 0;
for (var i = 0; i < sortedAdverts.length; i++) {
var advert = sortedAdverts[i];
expect(advert.year).toBeGreaterThan(prevYear);
prevYear = advert.year;
}
});
</pre>
<br/>
<b>Step 7</b>
<br/>
We run unit test in Node.js command prompt by running <code>test.bat</code> and we should see:
<pre type="javascript">
D:\devhome\github\motoads>scripts\test.bat
INFO [karma]: Karma v0.10.4 server started at http://localhost:9876/
INFO [launcher]: Starting browser Chrome
INFO [Chrome 30.0.1599 (Windows Vista)]: Connected on socket QZD2lLpZ-KlcZE2pqcrE
Chrome 30.0.1599 (Windows Vista): Executed 4 of 4 SUCCESS (0.517 secs / 0.076 secs)
]]></pre>
<br/>
If you want to run this example on your computer, you can download sources from <a href="https://github.com/lukpaw/motoads/tree/blog-post-2">GitHub</a>.
<br/>
<br/>
Any comment would be highly appreciated.Łukasz Pawlikhttp://www.blogger.com/profile/04167291407338846196noreply@blogger.com0tag:blogger.com,1999:blog-3027091649644802032.post-67507320235916852872013-11-02T13:20:00.005+01:002023-03-05T16:45:25.946+01:00AngularJS example MotoAds more advanced than the tutorial on angularjs.orgI have just been learning AngularJS and decided to build demo application that cover more techniques than the tutorial on <a href="http://docs.angularjs.org/tutorial">angularjs.org</a>. This application is only demo so it do not have complete server side, some features have the todos and there is not any test.
<br/>
<br/>
This demo application is some kind of automotive adverts portal, so I called it MotoAds. It has two main features:
<ul>
<li>List of adverts – includes an advanced filtering and sorting.</li>
<li>Add advert – a form with some client side field validations.</li>
</ul>
<br/>
<table>
<tr><td>
MotoAds application looks like this:
</td>
<td>
Application folders tree:
</td>
</tr>
<tr>
<td style="vertical-align: top;">
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiw6keJAWpMyd723zvO1I1WqYW67p_Telk5houNBFsDdIdeX91O260CNpLLbOzI4rc2ezfUPZo1KRW-yJzb94D10S3ICDKsTvlvqxb8IGdc7x1Y48ym7E_R_v1WxDP3b02370LqefFFqlr0/s1600/motoads1.png" imageanchor="1" style="clear: left; float: left; margin-bottom: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiw6keJAWpMyd723zvO1I1WqYW67p_Telk5houNBFsDdIdeX91O260CNpLLbOzI4rc2ezfUPZo1KRW-yJzb94D10S3ICDKsTvlvqxb8IGdc7x1Y48ym7E_R_v1WxDP3b02370LqefFFqlr0/s640/motoads1.png" /></a></div>
</td>
<td style="vertical-align: top;">
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh-s0yNw37UnWLAbCA41mPRwexZNE_xRzPpWfqFMUkPaJ8zc17NJicmUDwdKjajioFRfQJdKCdZa8qjoMU6CFpodvkrM5HINR5_hhFN73e1PeuM6VJWyNWduvlIyJHz7jnkEhpIl77-Lwdq/s1600/motoads_foldertree2.png" imageanchor="1" style="clear: left; float: left; margin-bottom: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh-s0yNw37UnWLAbCA41mPRwexZNE_xRzPpWfqFMUkPaJ8zc17NJicmUDwdKjajioFRfQJdKCdZa8qjoMU6CFpodvkrM5HINR5_hhFN73e1PeuM6VJWyNWduvlIyJHz7jnkEhpIl77-Lwdq/s640/motoads_foldertree2.png" /></a></div>
</td>
</tr>
</table>
<br/>
Root file <code>index.html</code> included external resource:
<ul>
<li><code>bootstrap.css</code> (2.3.2)</li>
<li><code>angular.js, angular-resource.js</code> (1.0.8)</li>
<li><code>ui-bootstrap-tpls-0.6.0.js</code> (0.6.0)</li>
</ul>
and internal resource:
<ul>
<li><code>app.css</code></li>
<li><code>app.js</code></li>
<li><code>services.js</code></li>
<li><code>controllers.js</code></li>
</ul>
<br/>
<h3>Bootstrap angular application, add navigation and route templates</h3>
In <code>index.html</code> we auto-bootstrap the angular application by defining directive <code>ng-app</code>:
<xmp>
<html ng-app="motoAdsApp">
</xmp>
In <code>app.js</code> we define new angular module <code>motaAdsApp</code> (name must be the same as value of <code>ng-app</code>):
<pre type="javascript">
var motoAdsApp = angular.module('motoAdsApp', ['ui.bootstrap', 'motoAdsServices']);
</pre>
In <code>index.html</code> we use <a href="http://getbootstrap.com/components/#navbar">Navbar</a> component from the bootstrap library:
<xmp>
<div class="navbar" ng-controller="NavbarController">
<div class="navbar-inner">
<a class="brand" href="#/">MotoAds</a>
<ul class="nav">
<li ng-class="{active: routeIs('/')}"><a href="#/">Adverts</a></li>
<li ng-class="{active: routeIs('/addAdvert')}"><a
href="#/addAdvert">Add advert</a></li>
</ul>
</div>
</div>
</xmp>
We define <code>NavbarController</code> with function <code>routeIs</code> (used in <code>ng-class</code>). It allows us to highlight link which is active.
<pre type="javascript">
motoAdsApp.controller('NavbarController', function NavbarController($scope, $location) {
$scope.routeIs = function(routeName) {
return $location.path() === routeName;
};
});
</pre>
We add directive <code>ng-view</code> which renders the proper template into <code>index.html</code> in case of the route definition:
<pre type="javascript">
<ng-view></ng-view>
</pre>
Url routes are defined in <code>app.js</code>:
<pre type="javascript">
motoAdsApp.config(['$routeProvider',
function($routeProvider) {
$routeProvider.
when('/', {
controller: 'AdvertsController',
templateUrl: 'views/adverts.html'
}).
when('/addAdvert', {
controller: 'AddAdvertController',
templateUrl: 'views/addAdvert.html'
});
}]);
</pre>
<br/>
<h3>Adverts - list, filtering, sorting</h3>
In <code>adverts.html</code> we define the left filtering panel with brands and models. We use the <a href="http://angular-ui.github.io/bootstrap/">ui.bootstrap.accordion</a> component:
<xmp>
<accordion close-others="oneAtATime">
<accordion-group heading="{{brand.name}}" ng-repeat="brand in brands">
<ul ng-repeat="model in brand.models">
<li><a href="" ng-click="addBrandModelFilter(brand, model)">{{model.name}}</a></li>
</ul>
</accordion-group>
</accordion>
</xmp>
In <code>controller.js</code> we write <code>AdvertsController</code> in which we using method <code>Brand.query()</code> to fill <code>$scope.brands</code>:
<pre type="javascript">
motoAdsApp.controller('AdvertsController', ['$scope', 'Brand', 'Country', 'Advert',
function($scope, Brand, Country, Advert) {
$scope.oneAtATime = true;
$scope.brands = Brand.query();
// ...
}]);
</pre>
Definition of the <code>Brand</code> service is in <code>service.js</code> - it is the simple service which reads <code>brands.json</code>:
<pre type="javascript">
motoAdsServices.factory('Brand', ['$resource', function($resource) {
return $resource('data/:id.json', {}, {
query: {
method: 'GET',
params: {
id: 'brands'
},
isArray: true
}
});
}]);
</pre>
Next in <code>adverts.html</code> we write filters for country, region and year (from-to):
<xmp>
<div class="span4">
<label>Country</label>
<select ng-model="filter.country" ng-options="c.name for c in countries">
<option value=""></option>
</select>
</div>
<div class="span4">
<label>Region</label>
<select ng-model="filter.region" ng-options="r.name for r in filter.country.regions">
<option value=""></option>
</select>
</div>
<div class="span4">
<label>Year of production</label>
<input ng-model="filter.yearFrom" class="input-small" min="1980" max="2013"
ng-minlength="4" ng-maxlength="4" type="number" placeholder="From">
<input ng-model="filter.yearTo" class="input-small" min="1980" max="2013"
ng-minlength="4" ng-maxlength="4" type="number" placeholder="To">
</div>
</div>
</xmp>
We add to <code>adverts.html</code> the panel with information which filters are active and the options of removing them:
<xmp>
<div class="span12">
<span ng-show="isAnyFilter()">Filters:</span>
<div class="ma-filter-div">
<span ng-show="filter.modelName">{{filter.brandName}} {{filter.modelName}}
<i class="icon-remove-sign icon-white" ng-click="filter.brandName = null;
filter.modelName = null"></i></span>
<span ng-show="filter.country.name">{{filter.country.name}}
<i class="icon-remove-sign icon-white" ng-click="filter.country = null;
filter.region = null"></i></span>
<span ng-show="filter.region.name">{{filter.region.name}}
<i class="icon-remove-sign icon-white" ng-click="filter.region = null"></i></span>
<span ng-show="filter.yearFrom">{{filter.yearFrom}}
<i class="icon-remove-sign icon-white" ng-click="filter.yearFrom = null"></i></span>
<span ng-show="filter.yearTo">{{filter.yearTo}}
<i class="icon-remove-sign icon-white" ng-click="filter.yearTo = null"></i></span>
</div>
<a ng-show="isAnyFilter()" href="" ng-click="removeAllFilter()">Remove all</a>
</div>
</xmp>
In <code>controller.js</code> we have to fill angular models, define variables and functions which are used by the filters:
<pre type="javascript">
motoAdsApp.controller('AdvertsController', ['$scope', 'Brand', 'Country', 'Advert',
function($scope, Brand, Country, Advert) {
$scope.oneAtATime = true;
$scope.brands = Brand.query();
$scope.countries = Country.query();
$scope.sortByCols = [{
"key": "year",
"name": "Year"
}, {
"key": "price",
"name": "Price"
}];
var allAdverts = Advert.query(filterAdverts);
$scope.filter = {
brandName: null,
modelName: null,
country: null,
region: null,
yearFrom: null,
yearTo: null
};
$scope.isAnyFilter = function() {
var f = $scope.filter;
if (f.brandName || f.modelName || f.country || f.region || f.yearFrom || f.yearTo) {
return true;
}
return false;
};
$scope.removeAllFilter = function() {
$scope.filter = {
brandName: null,
modelName: null,
country: null,
region: null,
yearFrom: null,
yearTo: null
};
};
$scope.addBrandModelFilter = function(brand, model) {
$scope.filter.brandName = brand.name;
$scope.filter.modelName = model.name;
};
$scope.$watch('filter', filterAdverts, true);
function filterAdverts() {
$scope.adverts = [];
angular.forEach(allAdverts, function(row) {
if (!$scope.filter.country) {
$scope.filter.region = null;
}
if ($scope.filter.brandName && $scope.filter.brandName !== row.brandName) {
return;
}
if ($scope.filter.modelName && $scope.filter.modelName !== row.modelName) {
return;
}
if ($scope.filter.country && $scope.filter.country.name !== row.countryName) {
return;
}
if ($scope.filter.region && $scope.filter.region.name !== row.regionName) {
return;
}
if ($scope.filter.yearFrom && $scope.filter.yearFrom > row.year) {
return;
}
if ($scope.filter.yearTo && $scope.filter.yearTo < row.year) {
return;
}
$scope.adverts.push(row);
});
};
}]);
</pre>
We define the <code>Country</code> service in <code>service.js</code>:
<pre type="javascript">
motoAdsServices.factory('Country', ['$resource', function($resource) {
return $resource('data/:id.json', {}, {
query: {
method: 'GET',
params: {
id: 'countries'
},
isArray: true
}
});
}]);
</pre>
Now we add the sorting option and the table containing the list of adverts:
<xmp>
<div class="ma-adverts-list">
<div class="row-fluid">
<div class="span4">
<h4>Adverts found ({{adverts.length}})</h4>
</div>
<div class="offset3 span5">
<form class="form-inline">
<label>Sort by</label>
<select ng-model="sortByCol" ng-options="s.name for s in sortByCols">
<option value=""></option>
</select>
</form>
</div>
</div>
<table class="table table-striped">
<tr>
<th>Image/Country/Region</th>
<th>Brand</th>
<th>Model</th>
<th>Year</th>
<th>Price ($)</th>
</tr>
<tr ng-repeat="advert in adverts| orderBy:sortByCol.key">
<td><a href="#/"><img class="img-rounded"
ng-src="{{advert.imageUrl}}"></a><br/>
<span>{{advert.countryName}}, {{advert.regionName}}</span></td>
<td>{{advert.brandName}}</td>
<td>{{advert.modelName}}</td>
<td>{{advert.year}}</td>
<td>{{advert.price}}</td>
</tr>
</table>
</div>
</xmp>
The adverts table is filled in <code>controller.js</code> in this lines:
<pre type="javascript">
var allAdverts = Advert.query(filterAdverts);
function filterAdverts() {
$scope.adverts = [];
angular.forEach(allAdverts, function(row) {
// ...
$scope.adverts.push(row);
});
};
</pre>
The <code>Advert</code> service is defined in <code>services.js</code> - it reads <code>adverts.json</code>:
<pre type="javascript">
motoAdsServices.factory('Advert', ['$resource', function($resource) {
return $resource('data/:id.json', {}, {
query: {
method: 'GET',
params: {
id: 'adverts'
},
isArray: true
}
});
}]);
</pre>
<br/>
<h3>Add advert - validation and currency directive</h3>
<br/>
<table>
<tr><td>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjZu7Tya3qf9kGLrH183LITqtnFAX4GT3mqKB3X-WOpikacCmJgq498OBF80HDjYy5aJikS71o-u1jtPlPcRuHLUaAaut1UNxdZrvNC8lV4rEwzTXC_PuH1nR-iRmJjxqmpqqoZ-si2llvX/s1600/motoads_add2.png" imageanchor="1" style="clear: left; float: left; margin-bottom: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjZu7Tya3qf9kGLrH183LITqtnFAX4GT3mqKB3X-WOpikacCmJgq498OBF80HDjYy5aJikS71o-u1jtPlPcRuHLUaAaut1UNxdZrvNC8lV4rEwzTXC_PuH1nR-iRmJjxqmpqqoZ-si2llvX/s640/motoads_add2.png" /></a></div>
</td></tr>
</table>
<br/>
We define the adding advert form in <code>addAdverts.html</code>:
<xmp>
<div class="container">
<form name="advertForm" novalidate class="ma-add-advert-form form-horizontal">
<h4>Add new advert</h4>
<div class="control-group">
<label class="control-label">Brand</label>
<div class="controls">
<select name="brand" ng-model="newAdvert.brand" required ng-options="c.name for c in brands">
<option value=""></option>
</select>
<span class="ma-form-msg-error help-inline"
ng-show="advertForm.brand.$dirty && advertForm.brand.$invalid">Invalid brand</span>
</div>
</div>
<div class="control-group">
<label class="control-label">Model</label>
<div class="controls">
<select name="model" ng-model="newAdvert.model" required ng-options="r.name for r in newAdvert.brand.models">
<option value=""></option>
</select>
<span class="ma-form-msg-error help-inline"
ng-show="advertForm.model.$dirty && advertForm.model.$invalid">Invalid model</span>
</div>
</div>
<div class="control-group">
<label class="control-label">Year</label>
<div class="controls">
<input type="number" name="year" ng-model="newAdvert.year" required min="1900"
ng-minlength="4" ng-maxlength="4">
<span class="ma-form-msg-error help-inline"
ng-show="advertForm.year.$dirty && advertForm.year.$invalid">Invalid year</span>
</div>
</div>
<div class="control-group">
<label class="control-label">Price ($)</label>
<div class="controls">
<input type="text" ma-currency name="price" ng-model="newAdvert.price" required>
<span class="ma-form-msg-error help-inline"
ng-show="advertForm.price.$dirty && advertForm.price.$invalid">Invalid price</span>
</div>
</div>
<div class="control-group">
<label class="control-label">Country</label>
<div class="controls">
<select name="country" ng-model="newAdvert.country" required ng-options="c.name for c in countries">
<option value=""></option>
</select>
<span class="ma-form-msg-error help-inline"
ng-show="advertForm.country.$dirty && advertForm.country.$invalid">Invalid country</span>
</div>
</div>
<div class="control-group">
<label class="control-label">Region</label>
<div class="controls">
<select name="region" ng-model="newAdvert.region" required
ng-options="r.name for r in newAdvert.country.regions">
<option value=""></option>
</select>
<span class="ma-form-msg-error help-inline"
ng-show="advertForm.region.$dirty && advertForm.region.$invalid">Invalid region</span>
</div>
</div>
<div class="control-group">
<div class="controls">
<button ng-click="reset()" ng-disabled="isUnchanged()"
class="btn btn-primary">Reset</button>
<button ng-click="add()" ng-disabled="advertForm.$invalid || isUnchanged()"
class="btn btn-primary">Save</button>
</div>
</div>
<div class="muted">Notice! Images upload not supported yet.</div>
</form>
</div>
</xmp>
We write <code>AddAdvertController</code> in <code>controllers.js</code>:
<pre type="javascript">
motoAdsApp.controller('AddAdvertController', ['$scope', 'Brand', 'Country', 'Advert',
function($scope, Brand, Country) {
$scope.brands = Brand.query();
$scope.countries = Country.query();
$scope.emptyAdvert = {
brand: null,
model: null,
year: 2010,
price: 10000,
country: null,
region: null
};
$scope.add = function() {
alert('User added!');
// TODO: Store it!
$scope.reset();
};
$scope.reset = function() {
$scope.newAdvert = angular.copy($scope.emptyAdvert);
if ($scope.advertForm) {
// TODO Uncomment in angular 1.1.1 or higher
//$scope.advertForm.$setPristne();
}
};
$scope.isUnchanged = function() {
return angular.equals($scope.newAdvert, $scope.emptyAdvert);
};
$scope.reset();
}]);
</pre>
Last thing is custom directive <code>ma-currency</code> used in <code>addAdvert.html</code>:
<script type="syntaxhighlighter" class="brush: html">
<xmp>
<input type="text" ma-currency name="price" ng-model="newAdvert.price" required>
</xmp>
Definition of <code>ma-currency</code> directive is in <code>controller.js</code>:
<pre type="javascript">
var CURRENCY_REGEXP = /^\-?\d+((\.|\,)\d{0,2})?$/;
motoAdsApp.directive('maCurrency', function() {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
ctrl.$parsers.unshift(function(viewValue) {
if (CURRENCY_REGEXP.test(viewValue)) {
ctrl.$setValidity('float', true);
return parseFloat(viewValue.replace(',', '.'));
} else {
ctrl.$setValidity('float', false);
return undefined;
}
});
}
};
});
</pre>
<br/>
<br/>
If you want to run this example on your computer, you can download sources from <a href="https://github.com/lukpaw/motoads/tree/blog-post-1">GitHub</a> and run it on any web server. For example copy folder app to <code>D:\apache\htdocs\app</code> and run in web browser <code>http://localhost/app/index.html</code>.
<br/>
<br/>
Any comment would be highly appreciated.Łukasz Pawlikhttp://www.blogger.com/profile/04167291407338846196noreply@blogger.com0tag:blogger.com,1999:blog-3027091649644802032.post-80958974396236631302013-10-13T22:12:00.001+02:002023-03-05T16:48:27.360+01:00AngularJS tutorial - my problems and solutionsDue to the fact that AngularJS gaining wider group of supporters and I decided to become familiar with its capabilities. So I started from <a href="http://docs.angularjs.org/tutorial">AngularJS tutorial</a>. At first I had to install: node.js, karma, git I already had (went smoothly). Step 0, Step 1 and almost finished Step 2 if no Section Tests, with whom I had a few problems - we are happy to share them, because maybe some of my suggestions will benefit and save some time. Let me just mention that I am acting in a Windows environment.
<br/>
<br/>
<b>Step 0, 1 success</b>
<br/>
<br/>
<b>Step 2 problems</b>
<br/>
<br/>
<b>Problem #1</b>
<br/>
The node.js console show me wrong path to Chrome:
<pre type="javascript">
ERROR [launcher]: Cannot start Chrome
Can not find the binary C:\Users\myuser\AppData\Local\Google\Chrome\Application\chrome.exe
Please set env variable CHROME_BIN
</pre>
I set it at:
<pre type="javascript">
set CHROME_BIN=C:\Program Files\Google\Chrome\Application\chrome.exe
</pre>
Of course, best to add it directly to the system environment variables.
<br/>
<br/>
<b>Problem #2</b>
<br/>
In file <code>D:\angular-phonecat\test\unit\controllersSpec.js</code> test code lines were too much, I cut it to what follows:
<pre type="javascript">
'use strict';
/* jasmine specs for controllers go here */
describe('PhoneCat controllers', function() {
describe('PhoneListCtrl', function(){
beforeEach(module('phonecatApp'));
it('should create "phones" model with 3 phones', inject(function($controller) {
var scope = {},
ctrl = $controller('PhoneListCtrl', {$scope:scope});
expect(scope.phones.length).toBe(3);
}));
});
});
</pre>
<br/>
<b>Problem #3</b>
<br/>
I received a warning about the lack of food <code>karma-junit-reporter</code> so I installed what you need:
<pre type="javascript">
npm install karma-junit-reporter --save-dev
</pre>
<br/>
<b>Step 2 success</b>
<br/>
<pre type="javascript">
INFO [karma]: Karma v0.10.2 server started at http://localhost:9876/
INFO [launcher]: Starting browser Chrome
INFO [Chrome 30.0.1599 (Windows Vista)]: Connected on socket 7-3jlIVerJ0w2A4qbl30
Chrome 30.0.1599 (Windows Vista): Executed 1 of 1 SUCCESS (0.447 secs / 0.033 secs)
</pre>
<br/>
<b>Step 3, 4 success</b>
<br/>
In step 3, there were no problems, keep it up. However impressed me test end-to-end tool Angular Scenario Runner. Really great. I must admit that so far has not happened to me to make any test in JavaScript (shame) may hence the rapture.
<br/>
In step 4, very plainly explained by the binding of a model to the user interface and the user interface to the model (two-way data-binding) on the example of sorting.
<br/>
<br/>
<b>Step 5, 6 problems</b>
<br/>
<br/>
<b>Problem #1</b>
<br/>
When you run the example in the browser did not show the data (phones). So I looked at the console (Developer Tools), and there is an error:
<pre type="text">
ReferenceError: $http is not defined
at new PhoneListCtrl (http://localhost:8000/app/js/controllers.js:8:3)
at invoke (http://localhost:8000/app/lib/angular/angular.js:3162:28)
</pre>
The code controllers.js was:
<pre type="javascript">
phonecatApp.controller('PhoneListCtrl', function PhoneListCtrl($scope) {
</pre>
It should be (for the tutorial is OK, and the lack GitHub):
<pre type="javascript">
phonecatApp.controller('PhoneListCtrl', function PhoneListCtrl($scope, $http) {
</pre>
<br/>
<b>Step 5, 6, 7, 8, 9 success</b>
<br/>
In step 5, and finally found out the use of AJAX, and in step 6 displayed the pictures (thumbnails) phones. Step 7 was much more interesting, I learned to do a lot of views (phone lists, detail phone) and routing. <br/>
In step 8 views <code>phone-detail.html</code> and finally gained the contents, the application consists of two full view. I also wrote in the <em>Experiments</em>, the first test e2e:
<pre type="javascript">
it('should display 4 thumbnail images on nexus-s page', function() {
expect(repeater('.phone-thumbs li img').count()).toBe(4);
});
</pre>
Step 9 went smoothly, I know as I write my own filters.
<br/>
<br/>
<b>Step 10, 11 problems</b>
<br/>
<br/>
<b>Problem #1</b>
<br/>
When I run the example in the browser did not show the content (white page), the JavaScript console error occurred:
<pre type="text">
Uncaught Error: [$injector:modulerr] Failed to instantiate module phonecatApp due to:
Error: [$injector:unpr] Unknown provider: $routeProvider
http://errors.angularjs.org/1.2.0-rc.2/$injector/unpr?p0=%24routeProvider
at http://localhost:8000/app/lib/angular/angular.js:78:12
at http://localhost:8000/app/lib/angular/angular.js:2997:19
</pre>
In app.js code was:
<pre type="text">
var phonecatApp = angular.module('phonecatApp', [
'phonecatControllers',
</pre>
Should be:
<pre type="text">
var phonecatApp = angular.module('phonecatApp', [
'ngRoute',
'phonecatControllers',
</pre>
<br/>
<b>Step 10, 11, 12 success</b>
<br/>
Step 10 and I know how to handle the event. In step 11, I learned how to pick up AJAX to data on the server by creating your own website. In the last 12-step animations - perfect for the end of the tutorial.
<br/>
<br/>
Both the framework and its AngularJS tutorial made quite an impression on me. I would recommend.Łukasz Pawlikhttp://www.blogger.com/profile/04167291407338846196noreply@blogger.com0tag:blogger.com,1999:blog-3027091649644802032.post-91659142517727479752013-05-26T20:45:00.002+02:002023-03-05T16:51:32.920+01:00Backbone.js in actionThe last time JavaScript libraries, separating application code (as the MVC server's frameworks), are gaining on popularity. Below I will present a simple example implemented in backbone.js, which will show the possibility of the library and you will understand why it is worth to use it.
Let's start by creating a directory <code>empapp</code> and put in it the <code>index.html</code>:
<xmp>
<!DOCTYPE html>
<html>
<body>
<script type="text/javascript"
src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"/>
<script type="text/javascript"
src="http://ajax.cdnjs.com/ajax/libs/underscore.js/1.4.4/underscore-min.js"/>
<script type="text/javascript"
src="http://ajax.cdnjs.com/ajax/libs/backbone.js/1.0.0/backbone-min.js"/>
<script type="text/javascript"
src="main.js"/>
</body>
</html>
</xmp>
This page contains an element <code>body</code>, in which are included three libraries: jquery, underescore, backbone and <code>main.js</code>, which contains a code of sample application written in backbone.
So let's create a directory <code>empapp</code>, file <code>main.js</code> and fill out its contents:
<xmp>
(function($) {
// Override function syncro data with the server, because we do not have a server
Backbone.sync = function(method, model, success, error) {
success();
}
// Definition of the Employee model
var Employee = Backbone.Model.extend({
defaults : {
firstName : '',
lastName : '',
email : ''
}
});
// Definition of the Employee List
var EmployeeList = Backbone.Collection.extend({
model : Employee
});
// Defintion of the Single Employee View
// It can be consist of li tag
var EmployeeView = Backbone.View.extend({
tagName : 'li',
// Service of event for Delete buttons exists in every Employee
// Click in button cause calling function deleteEmployee
events : {
'click button.delete' : 'deleteEmployee'
},
// View constructor
initialize : function() {
var self = this;
// If in model will be removed element we have to remove li for this Employee
this.model.bind('remove', function() {
$(self.el).remove();
});
},
// Function responsible for drawing view this.el
// in this case li with content
render : function() {
$(this.el).html(//
// Display view of the fields from Employee model
'<span>' + //
' ' + this.model.get('firstName') + ' ' + //
' ' + this.model.get('lastName') + ' ' + //
' ' + this.model.get('email') + ' ' + //
'</span>' + ' ' + //
// Every displayed Employee will be attached button Delete (to remove this Employee)
'<button type="button" class="delete">Delete</button>');
return this;
},
// Function cause removing instance of Employee
// in this case will be called event remove
// and view for this Employee will be remove too
deleteEmployee : function() {
this.model.destroy();
}
});
// View definition for the Employee List
var EmployeeListView = Backbone.View.extend({
// Do not be create new element html
// but attach this.el directly to body element
el : $('body'),
// Event service for button add under form
// Click on this button cause calling function addEmployee
events : {
'click button#add' : 'addEmployee'
},
// View constructor
initialize : function() {
// Creating instance of the list
this.employeeList = new EmployeeList();
// If instance of model Employee is added to the list
// adding event trigger function which create for this instance
// EmplyeeView, which is attached to ul element existing in this.el (that is body)
this.employeeList.bind('add', function(employee) {
var employeeView = new EmployeeView({
model : employee
});
$('ul', this.el).append(employeeView.render().el);
});
// Manual call drawing this.el view (body)
this.render();
},
// Function responsible for drawing this.el view
// in this case that is body with content
render : function() {
$(this.el).append(//
'<div style="width: 250px; text-align: right; border: 1px gray solid;">' + //
' <div style="width: 100%; text-align: left; background-color: lightgray;">New Employee</div>' + //
// Form with Employee fields and Add button
' <form>' + //
' First name: <input type="text" id="firstName" value="John" /><br/> ' + //
' Last name: <input type="text" id="lastName" value="Smith" /><br/> ' + //
' Email: <input type="text" id="email" value="john.smith@yahoo.com" /><br/> ' + //
' <button type="button" id="add" style="width: 70px;">Add</button>' + //
' </form>' + //
'</div>');
// Adding under form ul, in which are attached li view
// for single instance Employee model
$(this.el).append('<ul></ul>');
},
// Function casue adding new instance Employee to the list
// in this case it will be call event add for this list
// and will be added new EmployeeView to the EmployeeListView
addEmployee : function() {
// New instance Employee model and setting values of fields
// read from form after press Add button
var employee = new Employee();
employee.set({
firstName : $('input#firstName').val(),
lastName : $('input#lastName').val(),
email : $('input#email').val()
});
// Adding instance Employee model to the list (it casue add event on the list)
this.employeeList.add(employee);
}
});
// Create application through the instance main EmployeeListView
var employeeListView = new EmployeeListView();
})(jQuery);
</xmp>
The effect may not be impressive :)
<br/>
<br/>
<div style="width: 400px; padding: 20px; border: 1px gray dotted;">
<div style="width: 250px; text-align: right; border: 1px gray solid;"> <div style="width: 100%; text-align: left; background-color: lightgray;">New Employee</div> <form> First name: <input type="text" id="firstName" value="Cindy"><br> Last name: <input type="text" id="lastName" value="Smith"><br> Email: <input type="text" id="email" value="cindy.smith@yahoo.com"><br> <button type="button" id="add" style="width: 70px;">Add</button> </form></div>
<ul><li><span> John Smith john.smith@yahoo.com </span> <button type="button" class="delete">Delete</button></li><li><span> Jim Bean jim.bean@yahoo.com </span> <button type="button" class="delete">Delete</button></li></ul>
</div>
<br/>
However, we obtained the objective because the model is separated from the view, and everything is in JavaScript, and yet what we wanted :)
<br/>
<br/>
<a href="https://github.com/lukpaw/backbone-example-1">Source on GitHub</a>Łukasz Pawlikhttp://www.blogger.com/profile/04167291407338846196noreply@blogger.com0tag:blogger.com,1999:blog-3027091649644802032.post-69748745571040552382013-04-07T17:50:00.002+02:002023-03-05T16:52:11.984+01:00Attributes data-* in HTML5Using HTML5 Web applications becomes a reality. Even the banks are starting to require users to use browsers that support this standard. An example is Getin Bank, which from May 1 recommends the use of new browsers (Firefox 12 +, Chrome 15 +, IE 9 +, Safari 5 +, Opera 10 +).
<br /><br />
Creating web application often have the need to create a javascript component (such as jQuery) in which we want to preserve its state. An example would be a simple text box that appears in the browser, and it is initiated by default:
<xmp>
<input name="fieldName" type="text" value="defaultVal" />
</xmp>
If user changes the value on <code>userVal</code>, and we want to add a button restores the default value of this field to <code>defaultVal</code> we have a problem.
<br />
<br />
Of course, this can be done in several ways such as:
<ul>
<li>variable in javascript, we need to ensure that the name of the variable to be unique</li>
<li>create a hidden field and in this field store the default value</li>
</ul>
However, HTML5 gives us new opportunities in this area. Namely allows you to define custom attributes in HTML elements - they have to start from <code>data-*</code>:
<xmp>
<input name="field1" type="text" value="defaultVal1" data-default-value="defaultVal1" />
</xmp>
There's nothing in this discovery, because their attributes in HTML you can create previously (nobody forbid), but in HTML5 document with attributes <code>data-*</code> is correct for HTML5 validation.
<br />
This syntax is also supported by frameworks such as jQuery javascript:
<xmp>
// add and init data-default-value attribute
$('input').data('default-value', 'defaultVal');
</xmp>Łukasz Pawlikhttp://www.blogger.com/profile/04167291407338846196noreply@blogger.com0tag:blogger.com,1999:blog-3027091649644802032.post-59546014315193954912013-04-01T13:55:00.002+02:002023-03-05T16:53:45.095+01:00Google Apps Script - and why do I need it?The last time I needed a single spreadsheet (changing over time) from time to time to generate the statistics in the second sheet. Because of that, not only was about the cyclical generation, but the analysis was important for me that they are wrought in colors, bold - just easy to read.
I would add that that these sheets are holding in Google Drive.
<br />
<br />
I was able to rely on macros in MS Word, but I do not know them and do not hurry me to their knowledge. I thought it perfect for me in this situation would be JavaScript and intuition did not fail me. It turned out that the Google Cloud can access Google Apps Script, which written in help <i>Google Apps Script is based upon JavaScript</i>. Thus after a few moments I had the first version of my statistics, and the next sprint <i>final release</i>.
<br />
And now a simple example that will bring the possibility of this tool.
<br />
<br />
Start by creating a spreadsheet with the objective to calculate the profit from the sale of shares:
<br />
<br />
<table border="1" style="border-width: 1px; border-collapse: collapse; table-layout: fixed; width: 300px;">
<tbody>
<tr><td>Symbol</td><td>Number</td><td>Buy</td><td>Sell</td><td>Profit</td></tr>
<tr><td>PGE</td><td>100</td><td>18</td><td>19</td><td>-</td></tr>
<tr><td>LTS</td><td>200</td><td>39</td><td>41</td><td>-</td></tr>
</tbody></table>
<br />
Sheet ready, so here we go: <i>Tools->Script Editor</i>, we create the project, then the file with the script and finally write in Google Apps Script. We will do three things: calculate the profit for each stock, we will give the background color and bold the font.
<pre type="javascript">
function calcProfit() {
var sheet = SpreadsheetApp.getActiveSheet();
var outputData = [];
var profitSum = 0;
// Read data
var inputData = sheet.getDataRange().getValues();
for (var i = 1; i <= inputData.length - 1; i++) {
var number = inputData[i][1];
var buy = inputData[i][2];
var sell = inputData[i][3];
var profit = (sell - buy) * number;
var outputValues = [];
outputValues.push(profit);
outputData.push(outputValues);
}
// Write data
var outputRange = sheet.getRange(2, 5, outputData.length, 1);
outputRange.setBackground("lightgray");
outputRange.setFontWeight("bold");
outputRange.setValues(outputData);
}
</pre>
Run the script and get the result:
<br />
<br />
<table border="1" style="border-width: 1px; border-collapse: collapse; table-layout: fixed; width: 300px;">
<tbody>
<tr><td>Symbol</td><td>Number</td><td>Buy</td><td>Sell</td><td>Profit</td></tr>
<tr><td>PGE</td><td>100</td><td>18</td><td>19</td><td style="background-color: lightgray; font-weight: bold;">100</td></tr>
<tr><td>LTS</td><td>200</td><td>39</td><td>41</td><td style="background-color: lightgray; font-weight: bold;">400</td></tr>
</tbody></table>
<br />
Because the sheet and the script is in a cloud, it can be shared with others.Łukasz Pawlikhttp://www.blogger.com/profile/04167291407338846196noreply@blogger.com0tag:blogger.com,1999:blog-3027091649644802032.post-65185500427630372092013-03-17T11:39:00.001+01:002023-03-05T16:54:18.371+01:00Unconscious use WebServices sessionReal life ... programmer copied the definition of service in <code>Service1WS</code> to <code>Service2WS</code>, changed the name of the service, the name of the class and obtained a description of the service:
<br />
<br />
<xmp>
<service name="Service2WS" provider="java:RPC" use="literal">
<parameter name="className" value="myapp.Service2WS" />
<parameter name="allowedMethods" value="*" />
<parameter name="scope" value="Session" />
</service>
</xmp>
<br />
He did not notice, however, that <code>Service1WS</code> contains a parameter that if <code>Service2WS</code> should not appear to have been:
<br />
<br />
<xmp>
<parameter name="scope" value="Session" />
</xmp>
<br />
The result was that when the server traffic appeared to increase there was <code>java.lang.OutOfMemoryError: Java heap space</code>. The increases in traffic were no frequent so error occurred sporadically (sic!).
<br />
<br />
During the tracking error, no one came up with the idea to look on cardinality of the Tomcat session (there are lots of them in heavy traffic). Only application profiling revealed that there are a lot of objects Axis'a session is indicated clue: enough to look at the number of sessions on Tomcat'cie, look for <code>deploy.wsdd</code> with the description <code>Service2WS</code> parameter, and see this thing was clear.
<br />
<br />
Why were so many sessions for one client WS and one server WS. The explanation is simple, <code>Service2WS</code> session was so involved sessions for each request to the URL or cookie does not transfer any <code>jsessionid = SES_ID</code>, and that the client <code>Service2WS</code> do not send this parameter (because the intended use of the no session services), each request assumed a new session. Even though each such session expire after 8 hours, it's still under heavy traffic arose so many objects session that could fullfill, or at any rate limited space Heap.
<br />
<br />
The proposal, still the same, carefully copying the code.Łukasz Pawlikhttp://www.blogger.com/profile/04167291407338846196noreply@blogger.com0tag:blogger.com,1999:blog-3027091649644802032.post-64561554455244378602008-05-05T16:39:00.007+02:002013-03-18T18:22:23.674+01:00BigDecimal w GWTZawsze gdy testujemy jakiś framework wybieramy najprostszą drogę, co między innymi oznacza, iż korzystamy z takich typów danych jak: <code>String</code> i <code>Integer</code>. Niemniej jednak przychodzi czas kiedy musimy zmierzyć się z obsługą co najmniej kilku typów danych m.in. z typem zmiennoprzecinkowym. Doświadczenie nauczyło mnie, że jeżeli chodzi o Javę to najlepszym typem zmiennoprzecinkowym do wykonywania operacji arytmetycznych jest <code>BigDecimal</code>. Pewnie co niektórym nasuwa się pytanie - dlaczego nie <code>double</code>. Moją odpowiedzią będzie przykład zaczerpniety ze strony <a href="http://epramono.blogspot.com/2005/01/double-vs-bigdecimal.html">http://epramono.blogspot.com</a>:
<script type="syntaxhighlighter" class="brush: java">
<![CDATA[
public static void main (String[] args) {
System.out.println(
"(0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1) = " +
(0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1));
double d = 0.0;
while (d <= 1.0) d += 0.1;
System.out.println ("d = " + d);
System.out.println ("0.0175 * 100000 = " + 0.0175 * 100000);
}
]]></script>
Wynik na wyjściu:
<script type="syntaxhighlighter" class="brush: plain">
<![CDATA[
(0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1) = 0.9999999999999999
d = 1.0999999999999999<br />0.0175 * 100000 = 1750.0000000000002
]]></script>
Upss... co za niemiła niespodzianka :(<br /><br />Nie o tym jednak zamierzam pisać moim głównym tematem jest GWT (wersja 1.4.60), a w zasadzie obsługa typu <code>BigDecimal</code> w GWT, której najogólniej rzecz ujmując po prostu nie ma. To oczywiście poważny problem... ale jest na to rozwiązanie:<ul><li>Wchodzimy na stronę: <a href="http://code.google.com/p/gwt-math/">http://code.google.com/p/gwt-math/</a>.</li><li>Ściągamy dwie biblioteki <code>gwt-math-2.0.1.jar</code>, <code>gwt-math-server-2.0.1.jar</code> i umieszczamy je w <code>classpath</code> naszego projektu.</li><li>W pliku <code>MyModule.gwt.xml</code> dodajemy sekcję:
<script type="syntaxhighlighter" class="brush: xml">
<![CDATA[
<inherits name="com.googlecode.gwt.math.Math" />
]]></script>
</li><li>Na tym koniec naszej pracy, możemy korzystać już z klas <code>java.math.BigDecimal</code>, <code>java.math.BigInteger</code>.</li></ul><br />Reasumując: często jest tak, gdy sytuacja wydaje się być beznadziena z pomocą przychodzi <em>Google</em>, poprzez które odkrywamy, że społeczność <em>Open Source</em> przewidziała, że możemy natknąć się na dany problem. Stąd lepiej najpierw poszukać, niż zrobić po swojemu :)Łukasz Pawlikhttp://www.blogger.com/profile/04167291407338846196noreply@blogger.com0tag:blogger.com,1999:blog-3027091649644802032.post-2261503794780175242008-04-25T22:13:00.010+02:002013-04-01T18:58:36.793+02:00GWT - internacjonalizacjaJeżeli docelową grupą użytkowników naszej aplikacji są ludzie o różnych narodowościach wówczas warto dostosować ją do obsługi wielu języków. Pierwszą rzeczą z którą przyjdzie się nam zmierzyć będzie tłumaczenie tekstów stałych. Samo tłumaczenie tekstów możemy pozostawić lingwistom, natomiast naszym zadaniem jest napisanie tak aplikacji, aby obsługa tych tekstów była możliwa i przy dodawaniu kolejnych tłumaczeń nie wymagała pracy programistycznej.
<br /><br />
Oczywiście każdy programista dość szybko poradziłby sobie z tym zagadnieniem wymyślając swój własny mechanizm, ale programista <em>leniwy</em> (czytaj: <em>skuteczny</em>) przegrzebie dokumentację w celu znalezieniu mechanizmu, który już został wymyślony. Jak to wygląda w GWT...
<br /><br />
Podstawowym elementem, który umożliwi nam lokalizację tekstów stałych jest moduł zawarty w pakiecie <code>com.google.gwt.i18n</code> oferuje on kilka mechanizmów: <code>Constants</code>, <code>Messages</code>, <code>ConstantsWithLookup</code>, <code>Dictionary</code>, <code>Localizable</code>. Skoncentrujmy się jednak na dwóch podstawowych <code>Constants</code> i <code>Messages</code>, które powinny być używane w większości przypadków.<br /><br />
Jeżeli chcemy skorzystać z tych mechanizmów nasz moduł musi dziedziczyć po module <code>com.google.gwt.i18n.I18N</code> (plik <code>MyModule.gwt.xml</code> w pakiecie <code>com.mycompany.mymodule</code>):
<script type="syntaxhighlighter" class="brush: xml">
<![CDATA[
<module>
<inherits name="com.google.gwt.i18n.I18N" />
</module>
<extend-property name="locale" values="pl,en"/>
]]></script>
Kolejnym krokiem jest stworzenie plików zawierających teksty stałe dla poszczególnych języków oraz plik domyślny.<br /><br />
Plik domyślny <code>MyConstans.properties</code> (w pakiecie <code>com.mycompany.mymodule.client</code>):
<script type="syntaxhighlighter" class="brush: plain">
<![CDATA[
helloWorld = Witaj Świecie
goodbyeWorld = Do widzenia Świecie
]]></script>
<br />
Plik dla języka polskiego <code>MyConstans_pl.properties</code> (w pakiecie <code>com.mycompany.mymodule.client</code>):
<script type="syntaxhighlighter" class="brush: plain">
<![CDATA[
helloWorld = Witaj Świecie
goodbyeWorld = Do widzenia Świecie
]]></script>
<br />
Plik dla języka angielskiego <code>MyConstans_en.properties</code> (w pakiecie <code>com.mycompany.mymodule.client</code>):
<script type="syntaxhighlighter" class="brush: plain">
<![CDATA[
helloWorld = Hello World
goodbyeWorld = Goodbye World
]]></script>
<br />
Kolejnym krokiem jest stworznie w pakiecie <code>com.mycompany.mymodule.client</code> interfejsu <code>MyConstans</code>, w którym nazwy metod będą odpowiadać kluczom w plikach z tekstami stałymi:
<script type="syntaxhighlighter" class="brush: java">
<![CDATA[
public interface MyConstants extends Constants {
String helloWorld();
String goodbyeWorld();
}
]]></script>
<br />
Aby użyć nasze teksty wystarczą dwie linie kodu w kodzie klasy klienta (np. <code>MyModule.java</code>):
<script type="syntaxhighlighter" class="brush: java">
<![CDATA[
MyConstants myConstants = (MyConstants)GWT.create(MyConstants.class);
Window.alert(myConstants.helloWorld());
]]></script>
<br />
Drugi mechanizm, który niejako stawi rozszerzenie <code>Constants</code> to <code>Messages</code>, różni się on jedynie tym, iż teksty stałe mogą posiadać argumenty. Załóżmy, iż w pliku <code>ErrorMessages.properties</code> mamy tekst:
<script type="syntaxhighlighter" class="brush: plain">
<![CDATA[
permissionDenied = Błąd {0}: Użytkownik {1} nie ma uprawnień dostęp do {2}
]]></script>
<br />
Wóczas definiujemy interfejs analogiczny do <code>Constants</code> przy czym metody powinny mieć tyle argumentów wejściowych ile zostało wymienonych w tekstach stałych. Definicja interfejsu <code>ErrorMessages.java</code>:
<script type="syntaxhighlighter" class="brush: java">
<![CDATA[
public interface ErrorMessages extends Messages {
String permissionDenied(int errorCode, String username, String resource);
}
]]></script>
<br />
Aby teksty stałe były wyświetlane w danym języku mamy dwie możliwości. Pierwsza polega na ustawieniu właściwości <code><meta name="gwt:property" content="locale=pl"></code> bezpośrednio w stronie html:
<script type="syntaxhighlighter" class="brush: html">
<![CDATA[
<html>
<head>
<meta name="gwt:module" content="com.mycompany.mymodule.MyModule">
<meta name="gwt:property" content="locale=pl">
</head>
<body>
<script src="gwt.js"></script>
</body>
</html>
]]></script>
<br />
Druga możliwość to dodanie parametru do URL:
<script type="syntaxhighlighter" class="brush: plain">
<![CDATA[
http://www.mycompany.com/MyModule.html?locale=pl
]]></script>
<br />
Mechanizmy, ktore zostały opisane można używać tylko w kodzie na podstawie, którego jest generowany JavaScript (czyli który jest umieszczony w pakiecie <code>client</code>). Podczas kompilacji GWT wytwarza dla każdej lokalizacji odrębny plik JavaScript.<br />Reasumując GWT dostarcza dość kompleksowy mechanizm internacjonalizacji, choć według mnie wystarczyłby mechanizm <code>Messages</code>, bo przecież nie mogę od razu przewidzieć, że mój tekst stały nie będzie miał kiedyś argumentów. Kolejną rzeczą, która stanowi dla mnie pewną wadę jest to, iż nie można używać tych mechanizmów po stronie serwera, co zmusza nas do korzystania ze standardowego rozwiązania dostarczonego przez Java <code>ResourceBundle</code>.Łukasz Pawlikhttp://www.blogger.com/profile/04167291407338846196noreply@blogger.com2tag:blogger.com,1999:blog-3027091649644802032.post-16485352697859488342008-01-21T20:26:00.007+01:002013-03-18T19:08:06.204+01:00Równy czy nierówny - oto jest pytanieKtoś kiedyś powiedział "mnie język w programowaniu nie przeszkadza"? Co mnie więcej oznacza, iż doświadczony programista, który miał do czynienia z kilkoma językami (np. C, Java) bez problemu powinien poradzić sobie z programowaniem w kolejnym języku (np. w C#). Jest w tym wiele prawdy, ale czy bez kompleksowej wiedzy z danego języka kod owego programisty będzie na pewno niezawodny?<br /><br />Rozważmy prosty przykład napisany w języku Java:
<script type="syntaxhighlighter" class="brush: java">
<![CDATA[
Integer i1 = 1000;
Integer i2 = 1000;
if (i1 == i2) {
System.out.println("ten sam obiekt");
}
else {
System.out.println("rozne obiekty");
}
if (i1.equals(i2)) {
System.out.println("wartosci obiektow sa takie same");
}
else {
System.out.println("wartosci obiektow nie sa takie same");
}
]]></script>
Czy jest coś w tym przykładzie co mogłoby nas skłonić do refleksji - raczej nie. Oczywiste wydaje się, iż w pierwszym warunku zostanie wyświetlone na konsoli <code>"rozne obiekty"</code>, natomiast w drugim warunku <code>"rowne obiekty"</code>. Pierwszy warunek będzie spełniony tylko wtedy gdy będziemy mieli do czynienia ze zmiennymi, które wskazują dokładnie na ten sam obiekt. Drugi warunek to już porównanie wartości tych obiektów więc sprawa jest dość oczywista.<br /><br />Zmodyfikujmy ten przykład:
<script type="syntaxhighlighter" class="brush: java">
<![CDATA[
Integer i1 = 10;
Integer i2 = 10;
if (i1 == i2) {
System.out.println("ten sam obiekt");
}
else {
System.out.println("rozne obiekty");
}
if (i1.equals(i2)) {
System.out.println("wartosci obiektow sa takie same");
}
else {
System.out.println("wartosci obiektow nie sa takie same");
}
]]></script>
Co się okazuje? Pierwszy warunek jest spełniony w efekcie na konsoli zostanie wyświetlone <code>"ten sam obiekt"</code>. Dlaczego? Odpowiedź jest prosta, chociaż nie oczywista: w języku Java w celu zaoszczędzenia pamięci instancje klas <code>Short</code> and <code>Integer</code> o wartościach od <code>-128</code> do <code>127</code> wskazują na ten sam obiekt (obszar pamięci). Inaczej się ma sytuacja jeżeli jawnie użyjemy operatora <code>new</code>:
<script type="syntaxhighlighter" class="brush: java">
<![CDATA[
Integer i1 = new Integer(10);
Integer i2 = new Integer(10);
if (i1 == i2) {
System.out.println("ten sam obiekt");
}
else {
System.out.println("rozne obiekty");
}
if (i1.equals(i2)) {
System.out.println("wartosci obiektow sa takie same");
}
else {
System.out.println("wartosci obiektow nie sa takie same");
}
]]></script>
Wówczas zostanie wyświetlone <code>"rozne obiekty"</code>.<br /><br />Wniosek z tego jest jeden, iż bez dogłębnej znajomości języka, konstrukcje, które mogłyby się wydawać oczywiste mogą nas zaskoczyć w najmniej oczekiwanym momencie. Jest na to jednak rada: jeżeli jesteś czegoś pewien sprawdź to, a jeżeli jesteś czegoś bardzo pewien sprawdź to dwa razy...Łukasz Pawlikhttp://www.blogger.com/profile/04167291407338846196noreply@blogger.com4tag:blogger.com,1999:blog-3027091649644802032.post-44417036618173020802007-12-30T16:28:00.000+01:002013-03-24T13:43:18.212+01:00Kreatory w GWTW dystrybucji GWT znajdują się cztery skrypty cmd, które w największym skrócie można nazwać kreatorami. Każdy z nich jest wykorzystywany na różnym etapie tworzenia aplikacji.
<br /><br />
<strong>projectCreator</strong>
<br />
GWT pozwala na łatwą integrację ze środowiskiem Eclipse, dlatego też jeżeli podczas tworzenia aplikacji programista zamierza korzystać z tego IDE, wówczas powinien użyć tego narzędzia. Kreator ten zakłada strukturę katalogów (wraz z plikami .project oraz .classpath) , która pozwoli łatwo zaimportować tworzoną aplikację jako projekt Eclipse.<br /><br />
Wykonując komendę:
<script type="syntaxhighlighter" class="brush: plain">
<![CDATA[
projectCreator -eclipse MyAppPrj -out MyAppDir
]]></script>
Utworzona zostanie następująca struktura:
<script type="syntaxhighlighter" class="brush: plain">
<![CDATA[
Created directory MyAppDir\src
Created directory MyAppDir\test
Created file MyAppDir\.project
Created file MyAppDir\.classpath
]]></script>
<strong>applicationCreator</strong><br />Kreator ten pełni trzy funkcje:<br /><br /><ul><li>Wytwarza strukturę pakietów dla aplikacji.</li><li>Generuje domyślne pliki HTML i Java spięte w prostym pliku module XML. Te pliki stanowią prostą aplikację od której można zacząć budowanie bardziej zaawansowanych funkcjonalności.</li><li>Tworzy skrypty cmd, które pozwalają przekompilować i uruchomić aplikację w trybie hosted mode.</li></ul>Wykonując komendę (bez Eclipse):
<script type="syntaxhighlighter" class="brush: plain">
<![CDATA[
applicationCreator -out MyAppDir com.example.client.MyApp
]]></script>
Wykonując komendę (z Eclipse):
<script type="syntaxhighlighter" class="brush: plain">
<![CDATA[
applicationCreator -eclipse MyAppPrj -out MyAppDir com.example.client.MyApp
]]></script>
Utworzona zostanie następująca struktura:
<script type="syntaxhighlighter" class="brush: plain">
<![CDATA[
Created directory MyAppDir\src\com\example
Created directory MyAppDir\src\com\example\client
Created directory MyAppDir\src\com\example\public
Created file MyAppDir\src\com\example\MyApp.gwt.xml
Created file MyAppDir\src\com\example\public\MyApp.html
Created file MyAppDir\src\com\example\client\MyApp.java
Created file MyAppDir\MyApp-shell.cmd
Created file MyAppDir\MyApp-compile.cmd
]]></script>
W przypadku parametru -eclipse dodatkowo powstanie:
<script type="syntaxhighlighter" class="brush: xml">
<![CDATA[
Created file MyAppDir\MyApp.launch
]]></script>
<strong>i18nCreator</strong><br />Jeżeli pojawia się skrót i18n to oczywiście chodzi o internacjonalizację. Tym kreatorem wytwarzamy proste pliki properties zawierające teksty stałe w formie klucz/wartość. Generowany jest również skrypt cmd [Appl]-i18n, który na podstawie plików properties generuje interfejs Java poprzez który mamy dostęp do tekstów stałych z kodu Java aplikacji GWT.<br /><br />Wykonując komendę (bez Eclipse):
<script type="syntaxhighlighter" class="brush: plain">
<![CDATA[
i18nCreator -out MyAppDir com.example.client.MyAppConstants
]]></script>
Wykonując komendę (z Eclipse):
<script type="syntaxhighlighter" class="brush: plain">
<![CDATA[
i18nCreator -eclipse MyAppPrj -out MyAppDir com.example.client.MyAppConstants
]]></script>
Utworzona zostanie następująca struktura:
<script type="syntaxhighlighter" class="brush: plain">
<![CDATA[
Created file MyAppDir\src\com\example\client\MyAppConstants.properties
Created file MyAppDir\MyAppConstants-i18n.cmd
]]></script>
W przypadku parametru -eclipse dodatkowo powstanie:
<script type="syntaxhighlighter" class="brush: plain">
<![CDATA[
Created file MyAppDir\MyAppConstants-i18n.launch
]]></script>
<strong>jUnitCreator</strong><br />Tworzy odpowiedni katalog oraz pliki, w którym należy umieścić testy jednostkowe aplikacji GWT.<br /><br />
Wykonując komendę (bez Eclipse):
<script type="syntaxhighlighter" class="brush: plain">
<![CDATA[
junitCreator -junit c:\junit.jar -module com.example.client.MyApp -out MyAppDir com.example.client.MyAppTest
]]></script>
Wykonując komendę (z Eclipse):
<script type="syntaxhighlighter" class="brush: plain">
<![CDATA[
junitCreator -junit c:\junit.jar -eclipse MyAppPrj -module com.example.client.MyApp
-out MyAppDir com.example.client.MyAppTest
]]></script>
Utworzona zostanie następująca struktura:
<script type="syntaxhighlighter" class="brush: plain">
<![CDATA[
Created directory MyAppDir\test\com\example\client\test
Created file MyAppDir\test\com\example\client\test\MyAppTest.java
Created file MyAppDir\MyAppTest-hosted.cmd<br />Created file MyAppDir\MyAppTest-web.cmd
]]></script>
W przypadku parametru -eclipse dodatkowo powstanie:
<script type="syntaxhighlighter" class="brush: plain">
<![CDATA[
Created file MyAppDir\MyAppTest-hosted.launch<br />Created file MyAppDir\MyAppTest-web.launch
]]></script>
Dzięki kreatorom można w ciągu dosłownie kilku chwil zbudować swoją pierwsza aplikację w GWT, dzięki czemu nawet niecierpliwi się nie zniechęcą. Oczywiście bardziej istotne jest to, że nie trzeba wykonywać powtarzalnych czynności, które może za nas wykonać automat.Łukasz Pawlikhttp://www.blogger.com/profile/04167291407338846196noreply@blogger.com0tag:blogger.com,1999:blog-3027091649644802032.post-70101967120326298342007-12-23T20:29:00.000+01:002013-03-24T13:46:30.810+01:00Dlaczego GWT jest lepsze od Echo2Pamiętam jak dawno dawno temu natrafiłem na bardzo ciekawy framework jakim był Echo2, sama koncepcja pisania dla mnie była genialna, ale jednak po dokładniejszej analizie tego rozwiązania stwierdziłem, iż w praktyce ma ona małe szanse powodzenia. Teraz nadeszedł moment, gdy postanowiłem porównać go z GWT, ponieważ sama idea pisania aplikacji w tych frameworkach jest analogiczna, ale zasada działania już zupełnie inna.<br /><br />Zarówno w GWT jak i w Echo2 programista używa API zbliżonego do Swinga (lub jak kto woli do AWT). Poniżej zamieszony został przykład jak dodać pole edycyjne i przycisk w Echo2:
<script type="syntaxhighlighter" class="brush: java">
<![CDATA[
final TextField text = new TextField();
text.setText("Pusta wartosc");
final Button button = new Button();
button.setText("Uzupelnij");
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
text.setText("Uzupelniona wartosc");
}
}
Window window = new Window();
window.setContent(new ContentPane());
window.getContent.add(text);
window.getContent.add(button);
]]></script>
Natomiast w GWT ten sam przykład wygląda tak:
<script type="syntaxhighlighter" class="brush: java">
<![CDATA[
final TextBox text = new TextBox();
text.setText("Pusta wartosc");
final Button button = new Button();
button.setText("Uzupelnij");
button.addClickListener(new ClickListener() {
public void onClick(Widget sender) {
textField.setText("Uzupelniona wartosc");
}
}
RootPanel.get().add(text);
RootPanel.get().add(button);
]]></script>
Jeżeli chodzi o porównanie kodu nie ma zasadniczych różnic, natomiast w działaniu sytuacja jest odmienna.<br /><br />GWT na podstawie kodu Java generuje JavaScript, który jest uruchamiany w przeglądarce Internetowej, natomiast Echo2 kompiluje kod do klasy Java, a następnie uruchamia go na serwerze. To powoduje, iż każde zdarzenie (np. naciśnięcie przycisku) generuje ruch pomiędzy przeglądarką Internetową, a serwerem. Wybraźmy sobie co by było gdyby po każdym wyjściu z pola edycyjnego było uruchamiane zdarzenie (Echo2 ograniczyło na szczęście uruchamianie takich zdarzeń). Stąd podejście proponowane przez Echo2 zupełnie do mnie nie przemawia, bo generuje zbyt duży ruch w sieci. Oczywiście są pewne zalety takiego podejścia np. na serwerze wiadomo jaki jest stan aplikacji, jest wysyłany tylko niezbędny JavaScript.<br /><br />Twórcy GWT wybrali wg mnie bardziej naturalną drogę skoro interfejs jest w przeglądarce to i aplikacja jest w niej uruchamiana, a komunikuje się z serwerem tylko wtedy gdy jest to niezbędne.Łukasz Pawlikhttp://www.blogger.com/profile/04167291407338846196noreply@blogger.com0tag:blogger.com,1999:blog-3027091649644802032.post-25564647485032201082007-11-11T19:42:00.000+01:002013-03-24T13:53:02.049+01:00Html Hack dobry na wszystkoPamiętam czasy gdy programy pisało się korzystając z biblioteki curses, a interfejs użytkownika stanowił terminal 80x24. Nie będę się na ten temat zbytnio rozwodził, ale wspominam jedną rzecz jak coś napisałem to działało na każdym terminalu. Później przyszła era aplikacji GUI (np. Delphi, Swing), aż wreszcie doszliśmy do przeglądarek Internetowych i tutaj zaczeły się problemy. Na Firefoxie coś działa, a na Internet Explorze nie itd. W poprzednim tygodniu natrafiłem właśnie na taki problem i jak zawsze z pomocą przyszedł "html hack", czyli wykorzystanie słynnej funkcji JavaScript <code>setTimeout</code>.<br /><br />Zacznę od początku (tak chyba najlepiej) czyli od przykładu, który zobrazuje problem na który natrafiłem. Otóż chodziło o to aby przycisk po zaznaczeniu checkboxa się pokazywał (dodatkowo atrybut <code>position</code> musiał być <code>relative</code>), a po odznaczeniu chował (dodatkowo atrybut <code>position</code> musiał być <code>absolute</code>). Cały kod html zawarłem poniżej:
<script type="syntaxhighlighter" class="brush: html">
<![CDATA[
<html>
<head>
<script type="text/javascript">
<!--
function changeDiv(checkboxEl) {
var divElStyle = document.getElementById("div1").style;
if (checkboxEl.checked) {
divElStyle.position = "relative";
divElStyle.visibility= "visible";
}
else {
divElStyle.position = "absolute";
divElStyle.visibility= "hidden";
}
}
//-->
</script>
</head>
<body>
<input type="checkbox"
onclick="changeDiv(this);">Test</input>
<p>
Poczatek
<div id="div1"
style="position: absolute; display: inline; visibility: hidden;">
<input type="button" value="Test" />
</div>
Koniec
</p>
</body>
</html>
]]></script>
Po uruchomieniu tego przykładu okazało się, że owszem w Firefoxie działa wszystko jak należy, ale w Internet Explorerze przycisk jest niewidoczny, przy czym jest rezerwowane na niego miejsce.<br /><br />Oczywiście pełen pokory, ale i wiary w sukces zabrałem się do pracy, już po kilku chwilach stwierdziłem, że problem jest związany z przypisywaniem do atrybutu <code>position</code> wartości <code>relative</code>. Niestety Internet Explorer z tym sobie nie radzi. Problem znaleziony, więc przeszedłem do rozwiązania, które sprowadziło się do pragmatycznego posunięcia jakim było zastosowanie funkcji <code>setTimeout</code> - popularnego i "lubianego" html hacku.<br /><br />Poniżej zamieszczam zmodyfikowany kod, który działa w Internet Explorerze:
<script type="syntaxhighlighter" class="brush: html">
<![CDATA[
<html>
<head>
<script type="text/javascript">
<!--
function changeDiv(checkboxEl) {
var divElStyle = document.getElementById("div1").style;
if (checkboxEl.checked) {
divElStyle.position = "relative";
setTimeout(
'document.getElementById("div1").style.visibility = "visible"',
1);
}
else {
divElStyle.position = "absolute";
divElStyle.visibility= "hidden";
}
}
//-->
</script>
</head>
<body>
<input type="checkbox"
onclick="changeDiv(this);">Test</input>
<p>
Poczatek
<div id="div1"
style="position: absolute; display: inline; visibility: hidden;">
<input type="button" value="Test" />
</div>
Koniec
</p>
</body>
</html>
]]></script>
Zachęcam Was do podzielania się swoimi ciekawymi doświadczeniami.Łukasz Pawlikhttp://www.blogger.com/profile/04167291407338846196noreply@blogger.com0tag:blogger.com,1999:blog-3027091649644802032.post-83742537571497817362007-11-03T18:22:00.000+01:002007-11-03T18:32:21.863+01:00Spotkaj swojego idola - James GoslingChyba nikomu nie trzeba przybliżać osoby Jamesa Goslinga. Ze względu na weekend zamieszczam link do krótkiego filmu, którego jest on głównym bohaterem. Przyznam, że film mnie bardzo rozbawił: <a href="http://www.javapolis.com/JP06/campaign/index.php?ID=53&PWD=342a7c479017e16647acb399b90a27b6">There are better ways to meet your idols</a>Łukasz Pawlikhttp://www.blogger.com/profile/04167291407338846196noreply@blogger.com0tag:blogger.com,1999:blog-3027091649644802032.post-71349799601123910022007-10-26T11:43:00.000+02:002013-03-18T22:52:32.300+01:00GWT - module czy entry-pointW programowaniu często jest tak, że zrobienie podstawowej rzeczy jest proste i szybkie, dotyczy to w szczególności frameworków. W dystrybucji danej biblioteki mamy często mnóstwo przykładów, które charakteryzują się jednym elementem - są proste, niekiedy zbyt proste. Podobnie zresztą jest z większością tutoriali. Jednak czy jest to problem. Na początku nie, ale później owszem. Bo co z tego, że po zapoznaniu się z przykładami, czy kursami potrafimy zrobić jeden formularz skoro nie wiemy jak zaprojektować, a później zaprogramować całą aplikację. Oczywiście intuicja podpowiada programiście, w którym kierunku należy podążać, jednak zawsze zadaje sobie on pytanie czy jest to kierunek słuszny, zgodny ze sztuką i czy architektura jego aplikacji jest optymalna.
<br /><br />
Właśnie na taki problem napotkałem tworząc aplikację w GWT. Moja aplikacja składać się miała z dwóch elementów: panelu dla użytkownika oraz panelu dla administratora. Jednak pewne elementy tych paneli miały być wspólne: zarówno elementy ekranu jak np. DialogBox, jak również serwisy RPC. Więc oto stanąłem przed dylematem, jak to rozwiązać w GWT - czy stworzyć jeden moduł z kilkoma elementami entry-point, czy kilka modułów. Na początku byłem skłonny tworzyć kilka entry-pointów, do czasu gdy uprzytomniłem sobie, iż moduły mogą po sobie dziedziczyć. Wówczas było już z górki, postanowiłem stowrzyć jeden moduł ogólny, w którym umieściłem wspólny kod dla obu paneli, oraz dwa moduły zawierające specyficzny kod, jeden dla panelu administratora, a drugi dla panelu użytkownika. Oczywiście te dwa moduły dziedziczyły po module ogólnym. Wszystko było w obrębie jednego projektu Eclipse.<br /><br />Teraz przedstawie to od strony technicznej.<br />W pierwszej kolejności tworzymy moduł ogólny, który będzie wyglądał następująco:
<br /><br />
Pakiety:
<script type="syntaxhighlighter" class="brush: plain">
<![CDATA[
com.blogspot.coderlife.app.common
com.blogspot.coderlife.app.common.client
com.blogspot.coderlife.app.common.server
com.blogspot.coderlife.app.common.public
]]></script>
Zawartość pakietów:
<script type="syntaxhighlighter" class="brush: plain">
<![CDATA[
com.blogspot.coderlife.app.common
Common.gwt.xml
com.blogspot.coderlife.app.common.client
CommonService.java
CommonServiceAsync.java
CommonDialogBox.java
com.blogspot.coderlife.app.common.server
CommonServiceImpl.java
com.blogspot.coderlife.app.common.public
(Pusty)
]]></script>
Zawartość pliku Common.gwt.xml:
<script type="syntaxhighlighter" class="brush: xml">
<![CDATA[
<module>
<inherits name="com.google.gwt.user.User">
<servlet
class="com.blogspot.coderlife.app.common.server.CommonServiceImpl"
path="/commonService">
</module>
]]></script>
Należy zwrócić uwage na to, iż ten moduł nie zawiera żadnego elementu entry-point, ponieważ będzie on przechowywał wszystkie elementy wspólne, a sam nie będzie uruchamiany, stąd też pakiet com.blogspot.coderlife.app.common.public jest pusty nie zawiera żadnej strony html.<br /><br />W drugim kroku tworzymy moduł administratora, który będzie wyglądał następująco:<br /><br />Pakiety:
<script type="syntaxhighlighter" class="brush: plain">
<![CDATA[
com.blogspot.coderlife.app.adminpanel
com.blogspot.coderlife.app.adminpanel.client
com.blogspot.coderlife.app.adminpanel.server
com.blogspot.coderlife.app.adminpanel.public
]]></script>
Zawartość pakietów:
<script type="syntaxhighlighter" class="brush: plain">
<![CDATA[
com.blogspot.coderlife.app.adminpanel
AdminPanel.gwt.xml
com.blogspot.coderlife.app.adminpanel.client
AdminPanel.java
com.blogspot.coderlife.app.adminpanel.server
(Pusty)
com.blogspot.coderlife.app.adminpanel.public
AdminPanel.html
AdminPanel.css
]]></script>
Zawartość pliku AdminPanel.gwt.xml:
<script type="syntaxhighlighter" class="brush: xml">
<![CDATA[
<module>
<inherits name="com.google.gwt.user.User">
<inherits name="com.blogspot.coderlife.app.common.server.Common">
<entry-point
class="com.blogspot.coderlife.app.adminpanel.client.AdminPanel">
</module>
]]></script>
Ten moduł już będzie uruchamiany poprzez plik AdminPanel.html stąd zawiera on entry-point. Najbardziej istotne jest dziedziczenie tutaj po module Common.<br /><br />Nie będę zamieszczał zawartości UserPanel ponieważ jest analogiczna do AdminPanel.
<br /><br />
Reasumując wg mnie jeżeli aplikacja ma dwa niezależne panele (ekrany) powinno stworzyć się dwa moduły dla każdego z nich, natomiast wspólne elementy umieścić w trzecim wspólnym module (nie zawierającym żadnego entry-pointa).
<br /><br />
Zachęcam Was do podzielenia się opiniami co do prawidłowego modelu tworzenia aplikacji w GWT.Łukasz Pawlikhttp://www.blogger.com/profile/04167291407338846196noreply@blogger.com1tag:blogger.com,1999:blog-3027091649644802032.post-46326001144066210812007-10-20T18:30:00.000+02:002013-03-18T22:56:32.332+01:00Parsowanie XML przekazanego przez socketyZacznę od przedstawienia problemu, który oczywiście nie istnieje dopóki się go nie napotka :) Problem tkwi w tym, iż część parserów XML zamyka wejściowy strumień (<code>InputStream</code>) kiedy napotka EOF (-1). Dotyczy to choćby Xercesa, który jest domyślnym parserem dla biblioteki JDOM. Oczywiście nie byłoby w tym nic złego gdyby wejściowym strumieniem nie był <code>SocketInputStream</code>, ponieważ jego zamknięcie powoduje jeden niekoniecznie pożądany skutek, a mianowicie zamknięcie socketu, który w efekcie jest już bezużyteczny co oznacza nic innego jak to, iż nie można przez niego wysłać odpowiedzi.
<br /><br />
W przypadku dużej części aplikacji sockety są wykorzystywane do realizacji klasycznego modelu żądanie-odpowiedź np. klient wysyła do serwera dokument XML, serwer go przetwarza, a następnie odpowiada klientowi innym dokumentem XML. Jednak w przypadku gdy po wczytaniu dokumentu XML przez serwer socket zostanie zamknięty, klientowi nie zostanie udzielona odpowiedź.
<br /><br />
Co można w takim razie zrobić aby osiągnąć pożądany efekt? Rozwiązania są dwa przy czym drugie z nich można zrealizować na kilka sposobów. Pierwsze rozwiązanie to opakowanie <code>InputStream</code> (z socketu) w taki sposób aby nie zamykał on połączenia, czyli przesłonięcie metody <code>close()</code>. Drugie rozwiązanie to przeczytanie wszystkiego do bufora, a następnie przekazanie jego zawartości do parsera XML. Przy czym co zrobić jeżeli wejściowy strumień danych jest duży i nie chcemy niepotrzebnie czekać aż zostanie cały wczytany i dopiero wówczas przekażemy go do parsera. Przecież przychodzące bajty mogą być przetwarzane przez parser XML zanim przyjdą kolejne.<br /><br />Jak to zrobić w takim razie efektywnie:
<script type="syntaxhighlighter" class="brush: java">
<![CDATA[
BufferedInputStream clientSocketInputStream = new BufferedInputStream(clientSocket.getInputStream());
]]></script>
Zestawiamy ze sobą dwa strumienie typu piped:
<script type="syntaxhighlighter" class="brush: java">
<![CDATA[
PipedOutputStream pipedOutputStream = new PipedOutputStream();
pipedInputStream = new PipedInputStream();
pipedOutputStream.connect(pipedInputStream);
]]></script>
Tworzymy wątek łączący strumień wyjściowy piped ze strumieniem wejściowym socketu. Jego zadaniem jest czytanie danych z <code>clientSocketInputStream</code> do <code>pipedOutputStream</code>, który jest połączony z <code>pipedInputStream</code>, który możemy bezpośrednio przekazać do parsera XML:
<script type="syntaxhighlighter" class="brush: java">
<![CDATA[
Output2InputThread output2InputThread = new Output2InputThread(pipedOutputStream, clientSocketInputStream);
output2InputThread.start();
SAXBuilder builder = new SAXBuilder();
Document inputDocument = builder.build(pipedInputStream);
]]></script>
Implementacji klasy <code>Output2InputThread</code> nie będę zamieszczał w całości, natomiast zamieszczę fragmenty, które pozwolą zorientować się co powinno się w niej znaleźć:
<script type="syntaxhighlighter" class="brush: java">
<![CDATA[
private static final int BUF_SIZE = 4096;
byte[] buf = new byte[BUF_SIZE];
public void run() {
int readedCount;
while (true) {
// Wczytuje z socketu do bufora
readedCount = clientSocketInputStream.read(buf);
// TODO Trzeba dodać jeszcze kod,
// TODO który sprawdzi kiedy należy przestać czytać
// Zapisuje bufor do pipedOutputStream
pipedOutputStream.write(buf, 0, readedCount);
}
}
]]></script>
Nie zamieściłem kodu, który sprawdzi kiedy należy przestać czytać, ale takie coś już każdy może zrealizować samodzielnie. Wcześniej oczywiście trzeba ustalić sposób informowania o zakończeniu przesyłania dokumentu XML, można to zrobić np. poprzez przesłanie przed dokumentem nagłówka z ilością bajtów, które należy wczytać lub na końcu dokumentu jakiegoś umownego znacznika. Zwracam też uwagę, że ustalenie rozmiaru bufora na określoną wartość wcale nie oznacza, że podaczas jednej iteracji zostanie on cały zapełniony, w szczególnym przypadku może zostać wczytany tylko jeden bajt.
<br /><br />
Zachęcam Was do podzielenia się swoimi sposobami na rozwiązanie przedstawionego przeze mnie problemu.Łukasz Pawlikhttp://www.blogger.com/profile/04167291407338846196noreply@blogger.com0tag:blogger.com,1999:blog-3027091649644802032.post-71611123243871135822007-10-18T16:22:00.000+02:002007-10-18T17:19:24.117+02:00GWT WYSIWYG powrót do korzeniKomu się nie zdarzyło czegoś skrobnąć w Swingu i/lub Delphi z wykorzystaniem graficznego designera, pewnie większości programistów. Owszem może kod który powstaje dzięki takiemu narzędziu nie jest najwyższych lotów, ale przynajmniej taki interfejs graficzny tworzy się po prostu szybko, nawet nie będąc ekspertem w zakresie danej biblioteki, koncentrujemy się tylko nad obsługą poszczególnych zdarzeń (np. onClick itd). No tak ale co z tego jak dzisiaj każdy (no może prawie każdy) oczekuje, że aplikacja będzie dostępna przez przeglądarkę Internetową i oczywiście aplety odpadają bo trzeba instalować JRE itd.<br /><br />Pamiętam historię, którą opowiedział mi mój znajomy o firmie w której programiści kilka lat temu tworzyli aplikację w Visual Basicu, a teraz robią to w JSP. Zapytałem czy są zadowoleni, a on odpowiedział, że nie do końca, ponieważ nie potrafią zrozumieć dlaczego, coś co tworzyli przez jeden dzień zajmuje im teraz trzy dni... Ta krótka opowieść zalatuję nostalgią za korzeniami, za środowiskami designerskimi typu WYSIWYG, w których interfejs użytkownika wyklikujemy, a oprogramowujemy tylko zdarzenia. Czy aby MVC było tym na co czekali programiści z opowieści... otóż nie.<br /><br />Czekali na GWT Designera narzędzie, które pozwala programować w taki sposób jakbyśmy to robili w Swingu, różnica jest tylko jedna efektem naszego klikania jest interfejs webowy.<br /><br />GWT Designer to kreator GUI, który wspiera również GWT. Posiada on zarówno możliwość wizualnego tworzenia interfejsu (WYSIWYG) jak i zestaw kreatorów, które generują za nas kod Java. Żeby stworzyć sam interfejs żytkownika nie trzeba pisać nawet jednej linijki kodu, tylko oprogramować zdarzenia. Chociaż jeżeli mamy ochotę wówczas oczywiście taki kod możemy sami zedytować, po czym ponownie przełączyć się do trybu wizualizacji. Wręcz w fantastyczny sposób jest wsparta internacjonalizacja - bez kompilacji po wyborze z listy rozwijanej języka możemy kontrolować jak będzie wyglądał nasz interfejs w różnych lokalizacjach.<br /><br />Korzystając z WYSWIG możemy:<br /><ul><li>dodawać kontrolki techniką drag&drop,</li><li>dodawać obsługę zdarzeń poprzez kliknięcie w kontrolkę,</li><li>zmieniać właściwości kontrolki poprzez użycie palety edytora właściwości,</li><li>zmieniać i wykonywać refactoring wygenerowanego kodu, efekty natychmiast są widoczne w edytorze wizualnym,</li><li>narzędzi radzi sobie również z kodem, który został napisany ręcznie, potrafi go przeanalizować i zaprezentować w wizualnym narzędziu,</li><li>a co najważniejsze sam GWT Designer jest niczym innym jak pluginem do Eclipse,</li><li>największą jego wadą jest to że nie jest darmowy.</li></ul><p>Najszybciej możecie zapoznać się z jego możliwościami poprzez filmiki demo dostępne pod adresem: <a href="http://www.instantiations.com/gwtdesigner/demos.html">http://www.instantiations.com/gwtdesigner/demos.html</a></p><p>Źródło: <a href="http://www.instantiations.com/gwtdesigner/index.html">http://www.instantiations.com/gwtdesigner/index.html</a></p>Łukasz Pawlikhttp://www.blogger.com/profile/04167291407338846196noreply@blogger.com1tag:blogger.com,1999:blog-3027091649644802032.post-16450300507586635602007-10-17T20:33:00.000+02:002013-03-18T23:05:35.176+01:00YUI rywal GWT?Google udostępniło programistom <b>Google Web Toolkit</b> (GWT), framework do szybkiego i przyjemnego tworzenia aplikacji webowych. Oczywiście słowo "przyjemnego" jest kwestią względną, ale na pewno tak mógłby powiedzieć programista Java, który stroni od JavaScriptu i HTMLa. Dlaczego? Bo w GWT nie musi stosować żadnego innego języka poza Javą. Ale czy to jest właśnie kierunek rozwoju frameworków webowych, który okaże się tym najwłaściwszym. W tym momencie ciężko przesądzić.<br /><br />Zupełnie inną koncepcję proponuje Yahoo - tak, tak Yahoo też postanowiło wystawić swojego zawodnika :) Nazywa się on <b>The Yahoo! User Interface</b> (YUI). Biblioteka ta jest zbiorem narzędzi i kontrolek napisanych w JavaScripcie, do budowania interaktywnych aplikacji webowych. Opiera się w dużej mierze na operowaniu na DOMie, DHTMLu i AJAXie. Wszytkie komponenty dostarczane przez Yahoo są open sourcowe na licencji BSD i można ich używać do woli.<br /><br />W celu zaprezentowania podstawowych możliwości Yahoo posłuże się prostym przykładem komponentu <b>AutoComplete</b>.<br /><br />Aby w ogóle użyc kontrolki musimy załączyć do naszej strony HTML pliki CSS i
JavaScript:
<script type="syntaxhighlighter" class="brush: html">
<![CDATA[
<link type="text/css" rel="stylesheet"
href="http://yui.yahooapis.com/2.3.1/build/autocomplete/assets/skins/sam/autocomplete.css" />
<script type="text/javascript"
src="http://yui.yahooapis.com/2.3.1/build/yahoo-dom-event/yahoo-dom-event.js"></script>
<script type="text/javascript"
src="http://yui.yahooapis.com/2.3.1/build/connection/connection-min.js"></script>
<script type="text/javascript"
src="http://yui.yahooapis.com/2.3.1/build/animation/animation-min.js"></script>
<script type="text/javascript"
src="http://www.json.org/json.js"></script>
<script type="text/javascript"
src="http://yui.yahooapis.com/2.3.1/build/autocomplete/autocomplete-min.js"></script>
]]></script>
Następnie tworzymy instancję pola AutoComplete w następujący sposób:
<script type="syntaxhighlighter" class="brush: html">
<![CDATA[
<div id="myAutoComplete">
<input id="myInput" type="text">
<div id="myContainer"></div>
</div>
<script type="text/javascript">
var myAutoComp = new YAHOO.widget.AutoComplete("myInput","myContainer", myDataSource);
</script>
]]></script>
Konstruktor AutoComplete zawiera trzy parametry:<ul><br /><li>Identyfikator pola typu texbox lub textarea w które użytkownik będzie wpisywał zapytanie.</li><br /><li>Identyfikator elementu HTML który będzie kontenerem zawierającym wyniki zapytania.</li><br /><li>Wreszcie trzeci parametr to DataSource, który będzie zawierał dane które będą zawężane przez zapytanie i prezentowane w kontenerze.</li><br /></ul>Zanim zostanie powołany obiekt AutoComplete, należy zainicjować trzeci parametr DataSource można to zrobić na trzy różne sposby w zależności od potrzeb.<br /><br />Prosta tablica po stronie przeglądarki:
<script type="syntaxhighlighter" class="brush: html">
<![CDATA[
<script type="text/javascript">
var myArray1 = ["a", "b", "c"];
var myDataSource1 = new YAHOO.widget.DS_JSArray(myArray1);
</script>
]]></script>
Funkcja JavaScript, która dostarczy dane:
<script type="syntaxhighlighter" class="brush: java">
<![CDATA[
<<script type="text/javascript">
var myFunction = function() {
var myArray2 = ["d", "e", "f"];
return myArray2.reverse();
}
var myDataSource2 = new YAHOO.widget.DS_JSFunction(myFunction);
</script>
]]></script>
Wreszcie najbardziej przydatny sposób pociągnięcie danych z serwera:
<script type="syntaxhighlighter" class="brush: html">
<![CDATA[
<script type="text/javascript">
var myServer = "someServlet";
var mySchema = ["ResultItem", "KeyDataField"];
var myDataSource3 = new YAHOO.widget.DS_XHR(myServer, mySchema);
</script>
]]></script>
Mam nadzieję, że przykład ten przybliżył zasadę działania YUI. Więcej informacji znajdziecie na stronie:<br /><a href="http://developer.yahoo.com/yui/">http://developer.yahoo.com/yui/</a><br /><br />Zachęcam również do dyskusji do jakich frameworków webowych należy przyszłość?Łukasz Pawlikhttp://www.blogger.com/profile/04167291407338846196noreply@blogger.com0tag:blogger.com,1999:blog-3027091649644802032.post-75516001273256815802007-10-13T20:40:00.004+02:002013-03-18T20:00:47.064+01:00Drag&Drop w GWTOd czasu kiedy pojawiło się GWT minęło już sporo czasu, dlatego można go już oceniać nie tylko pod kątem tego czy zaproponowana przez Google koncepcja jest właśnie tą na którą czekają programiści webowi, którym dzierganie w JavaScriptcie po prostu nie odpowiada, ale również można oceniać go pod kątem popularności jaką zdobywa i przyznam, że przeglądając zasoby Internetu wyraźnie widać, że pozostawia konkurencję w tyle jak choćby framework YUI tworzony przez Yahoo.
<br /><br />
Innym istotnym elementem, który wpływa w zasadniczy sposób na popularność danego frameworku jest wsparcie jakie zyskuje on w środowisku OpenSource'owym, głównie mam tutaj na myśli ilość darmowych rozszerzeń jaka jest tworzona przez to grono programistów.
<br />
Wczoraj przeszukując Internet natrafiłem na bardzo ciekawą bibliotekę <strong>gwt-dnd</strong>, która przy naprawdę niewielkim nakładzie pracy pozwoli na zbudowanie interfejsu użytkownika z elementami <strong>Drag&Drop</strong>.
<br /><br />
Podstawową zaletą tej biblioteki jest to, iż możemy korzystać ze standarowych widgetów i paneli dostarczanych przez GWT - nie muszą one implementować specjalnych klas lub interfejsów, po prostu wystarczy użyć istniejące widgety i panele. Nie będę się koncentrował w dalszym opisie nad tym jakie funkcjonalności dostarcza ta biblioteka, natomiast zaprezentuję fragment kodu, który zaprezentuje prostotę użycia tego rozszerzenia.
<br /><br />
W pierwszym kroku tworzymy główny panel poza który nie będą mogły wyjść przeciagane elementy:
<script type="syntaxhighlighter" class="brush: java">
<![CDATA[
AbsolutePanel boundaryPanel = new AbsolutePanel();
boundaryPanel.setPixelSize(400, 300);
RootPanel.get().add(boundaryPanel);
]]></script>
Następnie wewnątrz tego panelu tworzymy kolejny panel tym razem będzie on określał obszar poza którym nie będzie można upuścić widgetu (w naszym przypadku będzie to prostu label):
<script type="syntaxhighlighter" class="brush: java">
<![CDATA[
AbsolutePanel targetPanel = new AbsolutePanel();
targetPanel.setPixelSize(300, 200);
boundaryPanel.add(targetPanel, 10, 10);
]]></script>
Kolejny krok to stworzenie tzw. DragControllera, w którym umieszczamy panel poza który nie będzie można przeciągać widgetów (co nie znaczy, że będzie można w nim go upuszczać):
<script type="syntaxhighlighter" class="brush: java">
<![CDATA[
DragController dragController = new PickupDragController(boundaryPanel, true);
]]></script>
Teraz dodajemy analogicznie tzw. DropController, w którym umieszczamy panel w obrębie którego mogą zostać upuszczone widgety:
<script type="syntaxhighlighter" class="brush: java">
<![CDATA[
DropController dropController = new AbsolutePositionDropController(targetPanel);
]]></script>
Rejestrujemy DropController w DragControllerze:
<script type="syntaxhighlighter" class="brush: java">
<![CDATA[
dragController.registerDropController(dropController);
]]></script>
Tworzymy etykiete, którą będziemy przeciągać i upuszczać w obrębie targetPanel:
<script type="syntaxhighlighter" class="brush: java">
<![CDATA[
Label label = new Label("Rusz mnie :)", false);
targetPanel.add(label, 10, 1);
]]></script>
Za pośrednictwem DragControllera robimy z etykiety widget, który można przeciągać:
<script type="syntaxhighlighter" class="brush: java">
<![CDATA[
dragController.makeDraggable(label);
]]></script>
Siła tej biblioteki tkwi w prostocie jej użycia, w 12 liniach kodu można zrobić naprawdę dużo zamieszania :)
<br /><br />
Źródła z których korzystałem i wszystkim polecam się z nimi zapoznać:
<br />
<a href="http://code.google.com/p/gwt-dnd/">http://code.google.com/p/gwt-dnd/</a>
<br />
<a href="http://code.google.com/p/gwt-dnd/wiki/GettingStarted">http://code.google.com/p/gwt-dnd/wiki/GettingStarted</a><br /><br />
Dla zainteresowanych zamieszczam kody źródłowe.
<br /><br />
Kod źródłowy pliku <code>MyApplication.gwt.xml</code>:
<script type="syntaxhighlighter" class="brush: xml">
<![CDATA[
<module>
<inherits name="com.google.gwt.user.User"/>
<inherits name="com.allen_sauer.gwt.dragdrop.DragAndDrop"/>
<entry-point class="com.mycompany.project.client.MyApplication"/>
</module>
]]></script>
Kod źródłowy pliku <code>MyApplication.java</code>:
<script type="syntaxhighlighter" class="brush: java">
<![CDATA[
package com.mycompany.project.client;
import com.allen_sauer.gwt.dragdrop.client.DragController;
import com.allen_sauer.gwt.dragdrop.client.PickupDragController;
import com.allen_sauer.gwt.dragdrop.client.drop.AbsolutePositionDropController;
import com.allen_sauer.gwt.dragdrop.client.drop.DropController;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootPanel;
public class MyApplication implements EntryPoint {
public void onModuleLoad() {
AbsolutePanel boundaryPanel = new AbsolutePanel();
boundaryPanel.setPixelSize(400, 300);
RootPanel.get().add(boundaryPanel);
AbsolutePanel targetPanel = new AbsolutePanel();
targetPanel.setPixelSize(300, 200);
boundaryPanel.add(targetPanel, 10, 10);
DragController dragController = new PickupDragController(boundaryPanel, true);
DropController dropController = new AbsolutePositionDropController(targetPanel);
dragController.registerDropController(dropController);
Label label = new Label("Rusz mnie :)", false);
targetPanel.add(label, 10, 1);
dragController.makeDraggable(label);
}
}
]]></script>Łukasz Pawlikhttp://www.blogger.com/profile/04167291407338846196noreply@blogger.com2