When we work with SPAs and web applications we need to handle with the browser’s cache. Sometimes we change our static files but the client’s browser uses a cached version of the file instead of the new one. We can tell the user: Please empty your cache to use the new version. But most of the times the user don’t know what we’re speaking about, and we have a problem. There’s a technique called cache buster used to bypass this issue. It consists on to change the name of the file (or adding an extra parameter), basically to ensure that the browser will send a different request to the server to prevent the browser from reusing the cached version of the file.
When we work with sapui5 application over SCP, we only need to use the cachebuster version of sap-ui-core
With this configuration, our framework will use a “cache buster friendly” version of our files and SCP will serve them properly.
For example, when our framework wants the /dist/Component.js file, the browser will request /dist/~1541685070813~/Component.js to the server. And the server will server the file /dist/Component.js. As I said before when we work with SCP, our standard build process automatically takes care about it. It creates a file called sap-ui-cachebuster-info.json where we can find all our files with one kind of hash that our build process changes each time our file is changed.
It works like a charm but I not always use SCP. Sometimes I use OpenUI5 in one nginx server, for example. So cache buster “doesn’t work”. That’s a problem because I need to handle with browser caches again each time we deploy the new version of the application. I wanted to solve the issue. Let me explain how I did it.
Since I was using one Lumen/PHP server to the backend, my first idea was to create a dynamic route in Lumen to handle cache-buster urls. With this approach I know I can solve the problem but there’s something that I did not like: I’ll use a dynamic server to serve static content. I don’t have a huge traffic. I can use this approach but it isn’t beautiful.
My second approach was: Ok I’ve got a sap-ui-cachebuster-info.json file where I can see all the files that cache buster will use and their hashes. So, Why not I create those files in my build script. With this approach I will create the full static structure each time I deploy de application, without needing any server side scripting language to generate dynamic content. OpenUI5 uses grunt so I can create a simple grunt task to create my files.
'use strict';
var fs = require('fs');
var path = require('path');
var chalk = require('chalk');
module.exports = function(grunt) {
var name = 'cacheBuster';
var info = 'Generates Cache buster files';
var cacheBuster = function() {
var config = grunt.config.get(name);
var data, t, src, dest, dir, prop;
data = grunt.file.readJSON(config.src + '/sap-ui-cachebuster-info.json');
for (prop in data) {
if (data.hasOwnProperty(prop)) {
t = data[prop];
src = config.src + '/' + prop;
dest = config.src + '/~' + t + '~/' + prop;
grunt.verbose.writeln(
name + ': ' + chalk.cyan(path.basename(src)) + ' to ' +
chalk.cyan(dest) + '.');
dir = path.dirname(dest);
grunt.file.mkdir(dir);
fs.copyFileSync(src, dest);
}
}
};
grunt.registerMultiTask(name, info, cacheBuster);
};
I deploy my grunt task to npm so when I need to use it I only need to:
Install the task
npm install gonzalo123-cachebuster
Add the task to my gruntfile
require('gonzalo123-cachebuster')(grunt);
and set up the path where ui5 task generates our dist files
Today I want to create an UI5/OpenUI5 boilerplate that plays with Lumen backends. Simple, isn’t it? We only need to create a Lumen API server and connect our OpenUI5 application with this API server. But today I also want to create a Login also. The typical user/password input form. I don’t want to build it from scratch (a user database, oauth provider or something like that). Since this days I’m involved with Amazon AWS projects I want to try Amazon Cognito.
Cognito has a great javaScript SDK. In fact we can do all the authentication flow (create users, validate passwords, change password, multifactor authentication, …) with Cognito. To create this project first I’ve create the following steps within Amazon AWS Cognito Console: Create a user pool with required attributes (email only in this example), without MFA and only allow administrators to create users. I’ve also created a App client inside this pool, so I’ve got a UserPoolId and a ClientId.
Let’s start with the OpenUI5 application. I’ve created an small application with one route called “home”. To handle the login process I will work in Component.js init function. The idea is check the cognito session. If there’s an active one (that’s means a Json Web Token stored in the local storage) we’ll display to “home” route and if there isn’t we’ll show login one.
To encapsulate the cognito operations I’ve create a model called cognito.js. It’s not perfect, but it allows me to abstract cognito stuff in the OpenUI5 application.
sap.ui.define([
"app/conf/env"
], function (env) {
"use strict";
AWSCognito.config.region = env.region;
var poolData = {
UserPoolId: env.UserPoolId,
ClientId: env.ClientId
};
var userPool = new AWSCognito.CognitoIdentityServiceProvider.CognitoUserPool(poolData);
var jwt;
var cognito = {
getJwt: function () {
return jwt;
},
hasSession: function (cbk) {
var cognitoUser = cognito.getCurrentUser();
if (cognitoUser != null) {
cognitoUser.getSession(function (err, session) {
if (err) {
cbk(err);
return;
}
if (session.isValid()) {
jwt = session.idToken.getJwtToken();
cbk(false, session)
} else {
cbk(true);
}
});
} else {
cbk(true);
}
},
getCurrentUser: function () {
return userPool.getCurrentUser();
},
signOut: function () {
var currentUser = cognito.getCurrentUser();
if (currentUser) {
currentUser.signOut()
}
},
getUsername: function () {
var currentUser = cognito.getCurrentUser();
return (currentUser) ? currentUser.username : undefined;
},
getUserData: function (user) {
return {
Username: user,
Pool: userPool
};
},
getCognitoUser: function (user) {
return new AWSCognito.CognitoIdentityServiceProvider.CognitoUser(cognito.getUserData(user));
},
authenticateUser: function (user, pass, cbk) {
var authenticationData = {
Username: user,
Password: pass
};
var authenticationDetails = new AWSCognito.CognitoIdentityServiceProvider.AuthenticationDetails(authenticationData);
var cognitoUser = new AWSCognito.CognitoIdentityServiceProvider.CognitoUser(cognito.getUserData(user));
cognitoUser.authenticateUser(authenticationDetails, cbk);
return cognitoUser;
}
};
return cognito;
}
);
It has three different stages: “login”, “PasswordReset” and “newPasswordRequired”
“login” is the main one. In this stage the user can input his login credentials. If credentials are OK then we’ll display home route.
The first time a user log in in the application with the password provided by the administrator, Cognito will force to change the password. Then We’ll show newPasswordRequired flow. I’m not going to explain each step. We developers prefer code than texts. That’s the code:
sap.ui.define([
"app/controller/BaseController",
"sap/ui/model/json/JSONModel",
"sap/m/MessageToast",
"app/model/cognito"
], function (BaseController, JSONModel, MessageToast, cognito) {
"use strict";
var cognitoUser;
return BaseController.extend("app.controller.Login", {
model: {
user: "",
pass: "",
flow: "login",
verificationCode: undefined,
newPass: undefined
},
onInit: function () {
this.getView().setModel(new JSONModel(this.model));
},
newPassPressHandle: function () {
var that = this;
var targets = this.getOwnerComponent().getTargets();
var attributesData = {};
sap.ui.core.BusyIndicator.show();
cognitoUser.completeNewPasswordChallenge(this.model.newPass, attributesData, {
onFailure: function (err) {
sap.ui.core.BusyIndicator.hide();
MessageToast.show(err.message);
},
onSuccess: function (data) {
sap.ui.core.BusyIndicator.hide();
that.getModel().setProperty("/flow", "login");
targets.display("home");
}
})
},
newPassVerificationPressHandle: function () {
var that = this;
var targets = this.getOwnerComponent().getTargets();
sap.ui.core.BusyIndicator.show();
cognito.getCognitoUser(this.model.user).confirmPassword(this.model.verificationCode, this.model.newPass, {
onFailure: function (err) {
sap.ui.core.BusyIndicator.hide();
MessageToast.show(err);
},
onSuccess: function (result) {
sap.ui.core.BusyIndicator.hide();
that.getModel().setProperty("/flow", "PasswordReset");
targets.display("home");
}
});
},
loginPressHandle: function () {
var that = this;
var targets = this.getOwnerComponent().getTargets();
sap.ui.core.BusyIndicator.show();
cognitoUser = cognito.authenticateUser(this.model.user, this.model.pass, {
onSuccess: function (result) {
sap.ui.core.BusyIndicator.hide();
targets.display("home");
},
onFailure: function (err) {
sap.ui.core.BusyIndicator.hide();
switch (err.code) {
case "PasswordResetRequiredException":
that.getModel().setProperty("/flow", "PasswordReset");
break;
default:
MessageToast.show(err.message);
}
},
newPasswordRequired: function (userAttributes, requiredAttributes) {
sap.ui.core.BusyIndicator.hide();
that.getModel().setProperty("/flow", "newPasswordRequired");
}
});
}
});
}
);
The home route is the main one. It asumes that there’s an active Cognito session enabled.
<mvc:View
controllerName="app.controller.Home"
xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc"
xmlns:semantic="sap.m.semantic">
<semantic:FullscreenPage
id="page"
semanticRuleSet="Optimized"
showNavButton="false"
title="{i18n>loggedUser}: {/userName}">
<semantic:content>
<Panel width="auto" class="sapUiResponsiveMargin" accessibleRole="Region">
<headerToolbar>
<Toolbar height="3rem">
<Title text="Title"/>
</Toolbar>
</headerToolbar>
<content>
<Text text="Lorem ipsum dolor st amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat"/>
<Button text="{i18n>Hello}" icon="sap-icon://hello-world" press="helloPress"/>
</content>
</Panel>
</semantic:content>
<semantic:customFooterContent>
<Button text="{i18n>LogOff}" icon="sap-icon://visits" press="onLogOffPress"/>
</semantic:customFooterContent>
</semantic:FullscreenPage>
</mvc:View>
It shows the Cognito login name. It alos has a simple logff button and also one button that calls to the backend.
That’s the frontend. Now it’s time to backend. Our Backend will be a simple Lumen server.
use App\Http\Middleware;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Laravel\Lumen\Application;
(new Dotenv\Dotenv(__DIR__ . "/../env/"))->load();
$app = new Application();
$app->singleton(ExceptionHandler::class, App\Exceptions\Handler::class);
$app->routeMiddleware([
'cognito' => Middleware\AuthCognitoMiddleware::class,
]);
$app->register(App\Providers\RedisServiceProvider::class);
$app->group([
'middleware' => 'cognito',
'namespace' => 'App\Http\Controllers',
], function (Application $app) {
$app->get("/api/hi", "DemoController@hi");
});
$app->run();
As you can see I’ve created a middelware to handle the authentication. This middleware will check the jwt provided by the frontend. We will use “spomky-labs/jose” library to validate the token.
That means that we need to fetch this url within every backend request, and that’s not cool. spomky-labs/jose allows us to use a cache to avoid fetch the request again and again. This cache is an instance of something that implementes the interface Psr\Cache\CacheItemPoolInterface. I’m not going to create a Cache from scratch. I’m not crazy. I’ll use symfony/cache here with a Redis adapter
And basically that’s all. Full application in my github
I’ve been playing with MQTT in previous posts. Today I want to build a simple dashboard. Basically because I’ve got a 3.5inch display for my Raspberry Py and I want to use it. The idea is set up my Rasperry Pi as a web kiosk and display the MQTT variables in real time using websockets. Let’s start.
Set up Raspberry Pi as a web kiosk is pretty straightforward. You only need to follow instructions detailed here. Now we will prepare the MQTT inputs. Today we’re going to reuse one example of previous post. A potentiometer controlled by a nodemcu microcontroller connected to our MQTT server via Wifi.
We also will build another circuit using a Arduino board and a ethernet Shield.
With this circuit we’ll register the temperature (using a LM35 temperature sensor), a photo resistor (CDS) to show the light level and a relay to switch on/off a light bulb. The Idea of the circuit is emit the temperature and light level to mosquitto mqtt server and listen to switch status form mqtt server to fire the relay. That’s the arduino code
Now we’re going to work with dashboard. This days I’m working with OpenUI5 within various projects and because of that we’ll use this library to build the dashboard. we’ll build something like this:
Last days I’ve been playing with OpenUI5. OpenUI5 is a web toolkit that SAP people has released as an open source project. I’ve read several good reviews about this framework, and because of that I started to hack a little bit with it. OpenUI5 came with a very complete set of controls. In this small example I want to use the “table” control. It’s just a datagrid. This days I playing a lot with Angular.js so I wanted to use together OpenUI5’s table control and Angularjs.
I’m not quite sure if it’s a good decision to use together both frameworks. In fact we don’t need Angular.js to create web applications using OpenUI5. OpenUI5 uses internally jQuery, but I wanted to hack a little bit and create one Angularjs directive to enclose one OpenUI5 datagrid.
First of all, we create one index.html. It’s just a boilerplate with angular + ui-router + ui-bootstrap. We also start our OpenUi5 environment with de default theme and including the table library
Then we create a directive enclosing the OpenUI5 needed code within a Angular module
(function () {
'use strict';
angular.module('ng.openui5', [])
.directive('openui5Table', function () {
function renderColumns(columns, oTable) {
for (var i = 0; i <= columns.length; i++) {
oTable.addColumn(new sap.ui.table.Column(columns[i]));
}
}
var link = function (scope, element) {
var oData = scope.model.data,
oTable = new sap.ui.table.Table(scope.model.conf),
oModel = new sap.ui.model.json.JSONModel();
oModel.setData({modelData: oData});
renderColumns(scope.model.columns, oTable);
oTable.setModel(oModel);
oTable.bindRows("/modelData");
oTable.sort(oTable.getColumns()[0]);
oTable.placeAt(element);
scope.$watch('model.data', function (data) {
if (data) {
oModel.setData({modelData: data});
oModel.refresh();
}
}, true);
};
return {
restrict: 'E',
scope: {model: '=ngModel'},
link: link
};
});
}());
And now we can create a simple Angular.js using the ng.openui5 module. In this application we configure the table and fetch the data from an externar API server