Instituto Superior de Engenharia de Lisboa
Licenciatura em Engenharia Informática e de Computadores Programação na Internet
Semestre de Verão de 2015/2016 - 1ª Época (5 de Fevereiro de 2015) – Duração: 2 horas Responda a cada um dos grupos em folhas separadas e devidamente identificadas.
Grupo 1
1. [1] Considere o excerto de uma resposta HTTP apresentado em seguida:
HTTP/1.1 404 Not Found Server: SAPOttpd/2.0
Set-Cookie: uu=698b0f739d294727a8d9237c1e826995; Max-Age=3600; Expires=Thu, 04 Feb 2016 23:12:33 GMT; Path=/
Set-Cookie: uu=55cb5402a90a4894a44f786b96275f8f; Max-Age=3600; Expires=Thu, 12 Mar 2016 23:12:33 GMT; Path=/public
Content-Type: text/html; charset=UTF-8 Date: Thu, 04 Feb 2016 22:12:33 GMT Content-Length: 87537
Indique e justifique toda a informação que pode ser obtida desta resposta.
NOTA: A informação obtida, não se resume à enumeração do conteúdo do headers, mas sim às conclusões que se podem tirar da sua presença na resposta bem como dos seus valores.
Esta resposta corresponde a um pedido para um recurso que não existe no servidor (código 404). Foi servido pelo servidor SAPOttpd/2.0 (header Server) no dia 4/02/2016 às 22:12:33, hora GMT (header Date), o conteúdo da resposta está no formato text/html codificado em UTF-8 (header Content-Type) e tem 87537 bytes. A resposta inclui ainda 2 cookies para o mesmo domínio onde foi feito o pedido; um para a path “/” com o nome “uu” e valor “698b0f739d294727a8d9237c1e826995” válido até 04/02/2016, 23:12:33 hora GMT e que expira em 3600s. Outro para a path /public com o nome “uu” e valor “55cb5402a90a4894a44f786b96275f8f” válido até 12/03/2016, 23:12:33 hora GMT e que expira em 3600s.
2. [3] A função mapEachOf(obj, mapper, resultCallback), aplica a função mapper a cada propriedade de obj. A função mapper recebe como parâmetros: (1) o valor de uma propriedade; e (2) um
callback, que deve ser chamado com o resultado do mapeamento, ou o erro, caso exista (seguindo o idioma
Node.js para callbacks de funções assíncronas: (err, data) => void).
Quando ocorrer um erro, ou todos os mapeamentos tiverem concluídos, é chamada a função resultCallback, com o erro, ou um objeto que tem as mesmas propriedades de obj, com os valores transformados por mapper (seguindo o idioma Node.js para callbacks)
O código seguinte apresenta um exemplo de utilização da função mapEachOf. Neste exemplo todos as asserções são verificadas com sucesso.
var obj = { dev: "/dev.json", test: "/test.json", prod: "/prod.json" }; mapEachOf(obj, function (value, callback) {
setTimeout(_ => callback(null, value.toUpperCase()), 1000); }, function (err, resultObj) {
if (err) console.error(err.message); // configs is now a map of JSON data
console.log("resultObj: " + JSON.stringify(resultObj)); assert(resultObj.dev == "/DEV.JSON"); assert(resultObj.test == "/TEST.JSON"); assert(resultObj.prod == "/PROD.JSON"); })
function mapEachOf(obj, mapper, cb) {
var retObj = {};
let keys = Object.keys(obj); let count = keys.length; keys.forEach( function(key) {
mapper(obj[key], function (err, value) { if (err) { cb(err); return; } retObj[key] = value; if (--count == 0) { cb(null, retObj); } }) }); }
b. [1] Realize as alterações necessárias de modo a que a função possa ser chamada da seguinte forma:
obj.mapEachOf(function(value, callback) { ... },function(err, resultObj){...}); Object.prototype.mapEachOf = function (mapper, cb) {
mapEachOf(this, mapper, cb); }
3. [4] Considere o serviço http://api.super-soccer.org/ que disponibiliza uma API com os seguintes endpoints: • GET http://api.super-soccer.org/leagues/{league-id}/teams -- retorna as equipas que
constituem a liga identificada por league-id. O resultado JSON obedece ao esquema seguinte:
{“teams”: [{“name”: String, “teamId”: String}, …]}
• GET http://api.super-soccer.org/teams/{team-id} -- retorna informação da equipa identificada por team-id. O resultado JSON obedece ao esquema: {“name”: String, “shortName”: String, “squadMarketValue”: Number}
Implemente um módulo em Node.js, que exporta as seguintes funções (implemente as alíneas em conjunto reutilizando as funções utilitárias às várias alíneas):
Código auxiliar para as 3 alíneas do grupo:
const http = require('http') const API = {
hostname: 'localhost', port: 3001,
getTeamUri: (id) => '/teams/' + id,
getLeagueUri: (leagueId) => '/leagues/' + leagueId + '/teams', getTeamId: (leagueTeam) => leagueTeam.teamId
}
function httpGet(path, callback){
const opt = new Options(path)
const request = http.request(opt, resp => { let result = ''
resp.on('error', callback)
resp.on('data', data => result += data)
resp.on('end', () => { callback(null, JSON.parse(result)) }) }) request.on('error', callback) request.end(); } function Options(p, m) { this.hostname = API.hostname this.port = API.port this.method = m || 'GET' this.path = p }
a. [1] getTeam(teamId, callback) que passa a callback um objeto com as propriedades teamId, name,
shortName e squadMarketValue, da equipa identificada por teamId, ou erro em caso de falha.
function getTeam(id, callback) {
httpGet(API.getTeamUri(id), (err, obj) => { if(err) return callback(err);
callback(err, new Team(id, obj)) })
}
b. [2] getLeagueTeams(leagueId, callback) que passa a callback um array com as equipas constituintes da liga identificada por leagueId.
callback tem a assinatura: (err, [team, ...]) => void. function getLeagueTeams(leagueId, callback) {
const res = []
httpGet(API.getLeagueUri(leagueId), (err, league) => { if(err) { callback(err); return; }
const total = league.teams.length; league.teams.forEach(team => {
const teamId = API.getTeamId(team) getTeam(teamId, (err, team) => { res.push(team) if(res.length >= total){ callback(null, res) } }) }) }) }
c. [1] getLeagueMarketValue(leagueId, callback) que passa a callback o valor de mercado da liga identificada por leagueId.
callback tem a assinatura: (err, marketValue) => void.
O valor de mercado da liga é o somatório do valor de mercado das suas equipas.
function getMarketValue(leagueId, callback) {
getLeagueTeams(leagueId, (err, teams) => { if(err) { callback(err); return; }
const total = teams.reduce((prev, curr) => { return prev + curr.squadMarketValue }, 0)
callback(err, total) })
}
Grupo 2
1. [9] Implemente uma aplicação web em Node.js, com recurso aos módulos express, handlebars e o módulo desenvolvido na pergunta anterior.
a. [2] Implemente o endpoint: GET /league/{league-id}, que devolve uma view com uma tabela de 4 colunas (teamId, name, shortName e squadMarketValue) com as equipas da liga identificada por
league-id.
Este endpoint pode receber um parâmetro minValue na query string que específica o valor mínimo de mercado das equipas apresentadas, logo, serão excluídas equipas com squadMarketValue abaixo de minValue.
// leaguerate.js
const app = require('express')(); const soccerapi = require('./soccerapi')
const handlebars = require('express-handlebars').create({'defaultLayout': 'default'}) app.use(express.static(__dirname + '/public'))
app.get('/league/:id', (req, resp, next) =>{
soccerapi.getLeagueTeams(req.params.id, (err, teams) => { if(err) next(err)
else {
const filters = getFilterActions(req.params.id, teams) // Necessário para a alínea b) if(req.query.minValue) {
teams = teams.filter(team => team.squadMarketValue > req.query.minValue) }
resp.render('league', { 'teams': teams,
'teamsJson': JSON.stringify(teams),
'filters': filters // Necessário para a alínea b })
} }) })
// Necessário para a alínea b
function getFilterActions(leagueId, teams){
const values = teams.map(t => t.squadMarketValue) const maxVal = Math.max.apply(Math, values); const res = []
for (var index = 0; index < maxVal; index+=50000000) { res.push({
'label': (index/1000000) + 'M',
'href': '/league/' + leagueId + '?minValue=' + index }) } return res } // View league.handlebars <h1>Primeira Liga</h1>
<table class="table table-hover"> <thead> <tr> <th>Id</th> <th>ShortName</th> <th>Name</th> <th>Market Value</th> </tr> </thead> <tbody> {{#each teams}} <tr> <td>{{teamId}}</td> <td>{{shortName}}</td> <td>{{name}}</td> <td>{{squadMarketValue}}</td> </tr> {{/each}} </tbody> </table>
b. [2] Adicione à view a possibilidade de filtrar as equipas por valor de mercado. Para tal, a view deve apresentar N links com a legenda “> valor M”, em intervalos de 50.000.000 de euros até ao valor da equipa com maior cotação. Exemplo: “> 0M” “> 50M” “> 100M” “> 150M”. Cada link inclui o parâmetro
minValue com o respetivo valor.
// O código JavaScript no endpoint para esta alínea já consta em a) nas linhas assinaladas. // View league.handlebars. Acrescentar no início da view apresentada em a)
{{#each filters}}
<a class="btn btn-default" href={{href}}>{{label}}</a> {{/each}}
<hr />
c. [2] Adicione o necessário à view principal da aplicação para que inclua a seguinte UI:
O botão ADD adiciona à lista o shortName da equipa com o indentificador indicado em Team Id. ATENÇÃO: inclua os requisitos que necessitar para a alínea seguinte.
// View league.handlebars. Acrescentar no início da view apresentada em a) e b) <script src="/assets/js/leaguerateCtr.js"></script> <script> window.onload = function() { leaguerateCtr({{{teamsJson}}}) } </script> <hr />
<div class="form-group form-inline"> <label>Team Id: </label>
<input type="text" name="inTeamId" class="form-control" id="inTeamId" />
<input type="submit" value="ADD" onclick="leaguerateCtr.addTeam('inTeamId', 'selectTeams')"/> <select name="selectTeams" class="form-control" id="selectTeams" multiple>
<option></option> </select> // Código HTML da alínea d) </div> <hr /> // leaguerateCtr.js
var leaguerateCtr = function(teams) { leaguerateCtr = {
'addTeam': addTeam,
'group': group // Alínea d)
}
function addTeam(idOfTeamId, idOfSelectTeams) {
const teamId = document.getElementById(idOfTeamId).value const t = teams.find(team => team.teamId == teamId)
const selectTeams = document.getElementById(idOfSelectTeams) const optionTeam = document.createElement('option')
optionTeam.appendChild(document.createTextNode(t.name)) selectTeams.appendChild(optionTeam)
} }
d. [3]
d.1. Adicione também tudo o que for necessário à view principal, de modo a ter a seguinte UI e, quando clicado o botão GROUP, submeter um pedido AJAX para o URI /group, com a informação preenchida pelo utilizador (ver em seguida o formato da informação a enviar).
Implemente também endpoint para o pedido AJAX: POST /group, que recebe uma lista de identificadores de equipas e um nome a atribuir ao grupo formado por essas equipas (os grupos são mantidos apenas em memória na aplicação Node.js).
// View league.handlebars. Acrescentar à view apresentada em a) e b) e c) // no local marcado com o texto “Código HTML da alínea d)”
<label>Nickname: </label>
<input type="text" name='inNickname' id='inNickname'/>
<input type="submit" value="GROUP" onclick="leaguerateCtr.group('selectTeams', 'inNickname')" // leaguerateCtr.js Acrescentar o seguinte código
var leaguerateCtr = function(teams) { leaguerateCtr = {
'addTeam': addTeam, 'group': group
}
function group(idOfSelectTeams, idOfNickname) {
const selectTeams = document.getElementById(idOfSelectTeams) const nickname = document.getElementById(idOfNickname).value const teamsIds = []
for (var index = 0; index < selectTeams.length; index++) {
const t = teams.find(team => team.name === selectTeams[index].value) teamsIds.push(t.teamId)
}
}
function ajaxPost(path, obj) {
const xhttp = new XMLHttpRequest() xhttp.onreadystatechange = function() {
if (xhttp.readyState == 4 && xhttp.status == 200) { alert('Group created') } } xhttp.open("POST", path); xhttp.setRequestHeader("Content-type", "application/json"); xhttp.send(JSON.stringify(obj)); } }
// acrescentar no final de leaguerate.js const groups = {}
app.post('/group', (req, resp) => {
groups[req.body.nickname] = req.body.teamsIds console.log(groups)
resp.send(true); })
d.2. Implemente o endpoint GET /group/{nickname}, que apresenta as equipas do grupo
nickname, utilizando a view realizada em a).
// acrescentar no final de leaguerate.js app.get('/group/:nickname', (req, resp) =>{ const teams = []
const total = groups[req.params.nickname].length groups[req.params.nickname].forEach(id => { soccerapi.getTeam(id, (err, team) => { teams.push(team)
if(teams.length >= total) { resp.render('league', { 'teams': teams,
'teamsJson': JSON.stringify(teams),
'filters': filters // as we don't have a league id here, lets pass an empty array so no changes are needed in the views
}) } }) }) })
2. [3] Pretende-se qua a aplicação web anterior tenha suporte para browsers em desktops e browsers que estão em dispositivos móveis (smart phones, tablets, etc.). A versão da aplicação para dispositivos móveis tem os mesmos endpoints da aplicação para desktop, só que a path de cada URI é prefixada de /mobile. Exº: Se existir o recurso /home, o mesmo recurso para a versão mobile tem o uri /mobile/home.
a. [1,5] Desenvolva o necessário para que a aplicação detete clientes mobile que estão a tentar aceder a URIs cujas representações são vocacionadas para desktop e os redireciona para o correspondente recurso da versão para dispositivos móveis.
// Assumindo toda a iniciação de uma aplicação express tendo na variável app // o objeto que representa a aplicação
app.use(redirectMobile);
function redirectMobile(req, res, next) {
if(isMobileRequestToDesktopSite(req)) { res.redirect(getMobilePath(req.path)); return;
}
next(); // Otherwise call the next middleware }
// Funções auxiliares
function isMobileRequestToDesktopSite(req) {
function containsMobileWord(str) {
// if the user agent contains the "mobile" word is a mobile device return str.toLocaleLowerCase().indexOf("mobile") != -1;
}
// returns true if the path does not contains the "mobile" word and the user agent header contains // the "mobile" word, indicating the client is a mobile device
return !containsMobileWord(req.path) && containsMobileWord(req.header("User-Agent")) }
b. [1,5] Implemente uma solução alternativa que redireciona o utilizador para uma página onde este é questionado se pretende continuar a aceder à versão para desktop ou pretende ser redirecionado para a versão mobile, redirecionando-o para o recurso correspondente que estava a tentar aceder, consoante a sua resposta.
// Assumindo toda a iniciação de uma aplicação express com o middleware cookie-parser, // view engine handlebars e na variável app o objeto que representa a aplicação // e utilizando as funções auxiliaries de a)
const ASK_VERSION_URI = '/askversion'; app.use(askMobileDevices);
app.get(ASK_VERSION_URI, function(req, res) { let redirectFrom = req.query.redirectFrom; res.cookie("select-version", "selecting")
res.render('ask-version', { desktopUri: redirectFrom, mobileUri: getMobilePath(redirectFrom) }); })
function askMobileDevices(req, res, next) {
// get the state of the select-version cookie (set by this middleware once version is selected) var selectVer = req.cookies['select-version'];
if(!req.path.startsWith(ASK_VERSION_URI) && !selectVer && isMobileRequestToDesktopSite(req)) { res.redirect(ASK_VERSION_URI + "?redirectFrom=" + req.path)
return; }
next(); // Otherwise call the next middleware }
// View: ask-versions.handlebars <h1>
You are in a mobile device and trying to access to the desktop site version. </h1>
<div>
Do you want to proceed to the <a href={{desktopUri}}>desktop</a> optimized site, or be redirected to the <a href="{{mobileUri}}">mobile</a> version
</div>