In questo articolo vedremo come trasformare un progetto universitario in Java in una web application in Node.js.

Il progetto ha come tema il pagamento del pedaggio autostradale. L'utente dopo aver inserito i dati del suo veicolo e scelti i caselli di partenza e di arrivo, viene informato sull'importo da pagare e provvede al pagamento.

L'amministratore del sistema ha la visione complessiva dei caselli e delle autostrade disponibili. Nella versione Java i dati del veicolo vengono inseriti tramite un file CSV, scelta non replicabile in una web application in quanto l'utente finale potrebbe avere problemi a generare il file nel formato corretto.

In Express.js l'unica similitudine con l'applicativo Java (un applicativo desktop basato sul design pattern MVC e su Java Swing) da tenere come riferimento è l'implementazione del design pattern Factory per il calcolo del pedaggio.

Uno dei requisiti del progetto infatti prevede che nel futuro il sistema debba considerare anche la classe ambientale del veicolo per il calcolo finale del pedaggio per venire incontro alle direttive Europee.

In Node implementeremo il pattern Factory in questo modo:

'use strict';

const PedaggioBase = require('./PedaggioBase');
const Pedaggio = require('./Pedaggio');

class PedaggioFactory {
    static getInstance( type, tariffaUnitaria, arrotondamento, classeVeicolo, classeAmbientale) {
        switch(type) {
            case 'default':
                return new PedaggioBase(tariffaUnitaria, arrotondamento,classeVeicolo);
            case 'tax':
                return new Pedaggio(tariffaUnitaria, arrotondamento, classeVeicolo, classeAmbientale);
            default:
                return null;
                
        }
    }
}

module.exports = PedaggioFactory;

In pratica il calcolo è condizionato dalla scelta della classe che implementa tale calcolo: PedaggioBase effettua il calcolo senza tenere conto della classe ambientale mentre Pedaggio ne tiene conto.

Per la prima avremo:

'use strict';

class PedaggioBase {
    constructor(tariffaUnitaria, arrotondamento, classeVeicolo) {
        this.tariffaUnitaria = tariffaUnitaria;
        this.arrotondamento = arrotondamento;
        this.classeVeicolo = classeVeicolo;
    }

    calculate(km) {
        let amount = 0;
        let amtByKm = this.tariffaUnitaria * km;
        
        amount += amtByKm;
        
        const vat = (amount * 22) / 100;
        
        amount += vat;
        
        let afterDecimal = amount - amount;
        
        if(afterDecimal > this.arrotondamento) {
            amount = amount + 1;
        } else {
            amount = amount + 0;
        }
        
        switch(this.classeVeicolo) {
            case 'A':
                amount += 1;
                break;
            case 'B':
                amount += 2;
                break;
            case '3':
                amount += 3;
                break;
            case '4':
                amount += 4;
                break;
            case '5':
                amount += 5;
                break;
            default:
                break;
        }
        
        
        
        return amount;
    }
}

module.exports = PedaggioBase;

Nel caso della seconda, invece, il calcolo sui chilometri percorsi dal casello di partenza al casello di arrivo, terrà conto della classe ambientale del veicolo.

'use strict';

class Pedaggio {
    constructor(tariffaUnitaria, arrotondamento, classeVeicolo, classeAmbientale) {
        this.tariffaUnitaria = tariffaUnitaria;
        this.arrotondamento = arrotondamento;
        this.classeVeicolo = classeVeicolo;
        this.classeAmbientale = classeAmbientale;
    }

    calculate(km) {
        let amount = 0;
        let amtByKm = this.tariffaUnitaria * km;
        
        amount += amtByKm;
        
        const vat = (amount * 22) / 100;
        
        amount += vat;
        
        let afterDecimal = amount - amount;
        
        if(afterDecimal > this.arrotondamento) {
            amount = amount + 1;
        } else {
            amount = amount + 0;
        }
        
        switch(this.classeVeicolo) {
            case 'A':
                amount += 1;
                break;
            case 'B':
                amount += 2;
                break;
            case '3':
                amount += 3;
                break;
            case '4':
                amount += 4;
                break;
            case '5':
                amount += 5;
                break;
            default:
                break;
        }
        
        
            switch(this.classeAmbientale) {
                case '6':
                    amount += 1;
                    break;
                case '5':
                    amount += 2;
                    break;
                case '4':
                    amount += 3;
                    break;
                case '3':
                    amount += 4;
                    break;
                case '2':
                    amount += 5;
                    break;
                case '1':
                    amount += 6;
                    break;
                    default:
                        break;
            }
        
        return amount;
    }
}

module.exports = Pedaggio;

Il database originale MySQL è stato sostituito con MongoDB. A livello utente abbiamo un problema nella view di inserimento dati: essendo il numero dei caselli superiore a 500, usare delle select box costituirebbe un serio rischio per l'usabilità della pagina.

Per questo motivo possiamo sostituirle con due campi muniti di autocomplete AJAX che fanno riferimento a questa route delle nostre API JSON.

'use strict';

const express = require('express');
const validator = require('validator');
const router = express.Router();
const { db } = require('../utils');
const { name } = require('../config').database;
const { Veicolo, Casello, PedaggioFactory, Percorso } = require('../classes');


router.get('/caselli', async (req, res, next) => {
    try {
        const { term } = req.query;
        const client = await db();
        const database = client.db(name);
        const collection = database.collection('caselli');
        const results = await collection.find({ nome: { $regex: term, $options: 'i' } } ).toArray();
        res.json({ results });
    } catch(err) {
        res.sendStatus(500);
    }
});

//...

module.exports = router;

Usiamo le espressioni regolari di MongoDB per trovare le corrispondenze nel campo nome dei caselli. Notate come l'espressione regolare sia case insensitive.

Sempre nelle nostri API JSON, implementiamo la route per il calcolo del pedaggio che salverà il risultato nella sessione e permetterà al codice lato client di effettuare un redirect sulla pagina del pagamento.

router.post('/calculate', (req, res, next) => {
    const { start, arrival, model, brand, plate, year, axles, weight, height, class_veic, class_env } = req.body;
    const errors = [];

    if(validator.isEmpty(start)) {
        errors.push({ start: 'Valore non valido.' });
    }
    if(validator.isEmpty(arrival)) {
        errors.push({ arrival: 'Valore non valido.' });
    }
    if(validator.isEmpty(model)) {
        errors.push({ model: 'Valore non valido.' });
    }
    if(validator.isEmpty(brand)) {
        errors.push({ brand: 'Valore non valido.' });
    }
    if(validator.isEmpty(plate)) {
        errors.push({ plate: 'Valore non valido.' });
    }
    if(!validator.isInt(year)) {
        errors.push({ year: 'Valore non valido.' });
    }
    if(!validator.isInt(axles)) {
        errors.push({ axles: 'Valore non valido.' });
    }
    if(!validator.isInt(weight)) {
        errors.push({ weight: 'Valore non valido.' });
    }
    if(!validator.isInt(height)) {
        errors.push({ height: 'Valore non valido.' });
    }
    if(validator.isEmpty(class_veic)) {
        errors.push({ class_veic: 'Valore non valido.' });
    }
    if(validator.isEmpty(class_env)) {
        errors.push({ class_env: 'Valore non valido.' });
    }

    if(errors.length > 0) {
        res.json({ errors });
    } else {
        const veicolo = new Veicolo(model, brand, parseInt(year, 10), plate, parseInt(axles, 10), parseInt(weight, 10), parseInt(height, 10), class_veic, class_env);
        const pedaggio = PedaggioFactory.getInstance('default', 2, 0.50, veicolo.classe, veicolo.classeAmbientale);
        const casello = new Casello(Number(start), Number(arrival));
        const percorso = new Percorso(casello.startKm, casello.endKm);

        percorso.calculateTravelKm();

        const amt = pedaggio.calculate(percorso.travelKm);
        const total = amt.toFixed(2);

        req.session.total = parseFloat(total);

        res.json({ done: true });
    }
});

Dopo la validazione dei dati inseriti dall'utente, avviene il calcolo dell'importo da pagare. L'uso delle classi Casello e Percorso è puramente simbolico: JavaScript al momento non supporta gli operatori di visibilità dei membri di una classe, quindi tutti i benefici dell'incapsulamento originario di Java vengono persi. L'unica classe che aggiunge una logica ulteriore al calcolo del percorso tra caselli è appunto Percorso.

'use strict';

class Percorso {
    constructor(startKm, endKm) {
        this.startKm = startKm;
        this.endKm = endKm;
        this.travelKm = 0;
    }

    calculateTravelKm() {
        if(this.endKm > this.startKm) {
            this.travelKm = this.endKm - this.startKm;
        } else {
            this.travelKm = this.startKm - this.endKm;
        }
    }
}

module.exports = Percorso;

Una volta che l'utente viene reindirizzato alla pagina del pagamento, abbiamo nell'oggetto sessione la proprietà total che possiamo mostrare nella view della pagina.

Quindi creiamo un form di PayPal usando i dati di configurazione e il totale della sessione.

<form action="<%= paypal.url %>" method="post" id="form-payment" novalidate>
        <input name="cmd" value="_cart" type="hidden">
        <input name="upload" value="1" type="hidden">
        <input name="no_note" value="0" type="hidden">
        <input name="bn" value="<%= paypal.formId %>" type="hidden">
        <input name="tax_cart" value="0.00" type="hidden">
        <input name="rm" value="2" type="hidden">
        <input name="business" value="<%= paypal.email %>" type="hidden">
        <input name="handling_cart" value="0" type="hidden">
        <input name="currency_code" value="EUR" type="hidden">
        <input name="lc" value="IT" type="hidden">
        <input name="return" value="<%= paypal.return %>" type="hidden">
        <input name="cbt" value="Ritorna al negozio" type="hidden">
        <input name="cancel_return" value="<%= paypal.cancel %>" type="hidden">
        <input name="item_name_1" value="Pedaggio Autostradale" type="hidden">
        <input name="quantity_1" value="1" type="hidden">
        <input name="amount_1" value="<%= total.toFixed(2) %>" type="hidden">
        <input name="shippping_1" value="0.00" type="hidden">
        <p class="mt-5">
            <input type="submit" value="Paga con PayPal" class="btn btn-primary btn-lg">
        </p>
    </form>

PayPal ci impone di creare una route per l'esito della transazione ed una per l'annullamento dell'operazione.

router.get('/annulla', (req, res, next) => {
    res.redirect('/pagamento');
});

router.get('/grazie', (req, res, next) => {
    if(req.session.total) {
        delete req.session.total;
    }
    res.render('thank-you', {
        title: 'Grazie!'
    });
});

Una volta completato l'ordine il totale dell'importo non è più necessario. Volendo è possibile esaminare il parametro st restituito da PayPal per verificare che sia effettivamente impostato su Completed ma questo requisito è necessario solo in un ambiente di produzione.

Conclusione

Gran parte dei benefici di un linguaggio che implementa l'OOP in modo completo come Java vengono persi nella trasformazione in una web application in Node.js. Tuttavia questi benefici possono essere riacquistati se si utilizza un metalinguaggio come TypeScript. Il vero punto di forza di Node sta nel permettere effettivamente all'utente di pagare l'importo calcolato, dettaglio questo che era stato solo simulato nel progetto originario in Java. Inoltre ora l'utente dispone di un'interfaccia notevolmente più ricca e usabile di quella di partenza.

Risorse collegate

Java

  1. Repository del progetto
  2. Documentazione del codice
  3. Specifiche del progetto

Node.js

  1. Repository del progetto
  2. Demo

Accessi area demo

  1. Amministratore: username admin, password admin.
  2. Utente: username user, password user.