Iván López - @ilopmar
Mum, I want to be a Groovy
full-stack developer
Hello!
I am Iván López
@ilopmar
http://greachconf.com@madridgug
Thank you very much!
Q&A
Just kidding!
What's a full-stack developer?
Full-stack developer
Backend language
Javascript
HTML
Mobile App
Polaromatic
1.
Demo
2.
Application flow
3.
Backend
Polaromatic
▷ Spring Boot
▷ Core App
▷ Spring MVC
▷ Spring Integration Flow
<file:inbound-channel-adapter directory="work" channel="incommingFilesChannel"/>
<chain input-channel="incommingFilesChannel">
<service-activator ref="fileService" method="preprocessFile"/>
<service-activator ref="imageConverterService" method="applyEffect"/>
<service-activator ref="browserPushService" method="pushToBrowser"/>
<service-activator ref="metricsService" method="updateMetrics"/>
<service-activator ref="fileService" method="deleteTempFiles"/>
</chain>
Spring Integration Flow
<file:inbound-channel-adapter directory="work" channel="incommingFilesChannel"/>
<chain input-channel="incommingFilesChannel">
<service-activator ref="fileService" method="preprocessFile"/>
<service-activator ref="imageConverterService" method="applyEffect"/>
<service-activator ref="browserPushService" method="pushToBrowser"/>
<service-activator ref="metricsService" method="updateMetrics"/>
<service-activator ref="fileService" method="deleteTempFiles"/>
</chain>
Spring Integration Flow
Photo preprocessFile(File file) {
def pr = new PolaroidRequest(file)
this.preprocessFile(pr)
}
File service
<file:inbound-channel-adapter directory="work" channel="incommingFilesChannel"/>
<chain input-channel="incommingFilesChannel">
<service-activator ref="fileService" method="preprocessFile"/>
<service-activator ref="imageConverterService" method="applyEffect"/>
<service-activator ref="browserPushService" method="pushToBrowser"/>
<service-activator ref="metricsService" method="updateMetrics"/>
<service-activator ref="fileService" method="deleteTempFiles"/>
</chain>
Spring Integration Flow
File service
Photo preprocessFile(File file) {
def pr = new PolaroidRequest(file)
this.preprocessFile(pr)
} Photo preprocessFile(PolaroidRequest polaroidRequest) {
String outputFile = File.createTempFile("output", ".png").path
return new Photo(input: polaroidRequest.inputFile.absolutePath,
output: outputFile, text: polaroidRequest.text)
}
class ImageConverterService {
private static final String DEFAULT_CAPTION = "#LearningSpringBoot with Polaromaticn"
Random rnd = new Random()
Photo applyEffect(Photo photo) {
log.debug "Applying effect to file: ${photo.input}..."
def inputFile = photo.input
def outputFile = photo.output
double polaroidRotation = rnd.nextInt(6).toDouble()
String caption = photo.text ?: DEFAULT_CAPTION
def op = new IMOperation()
op.addImage(inputFile)
op.thumbnail(300, 300)
.set("caption", caption)
.gravity("center")
.pointsize(20)
.background("black")
.polaroid(rnd.nextBoolean() ? polaroidRotation : -polaroidRotation)
.addImage(outputFile)
def command = new ConvertCmd()
command.run(op)
photo
}
}
Image converter
FlickrDownloader
▷ Spring Boot CLI
▷ Download Flickr Interesting pictures
▷ Jsoup, GPars
▷ 55 lines of Groovy code
(microservice?)
@Slf4j
@EnableScheduling
@Grab('org.jsoup:jsoup:1.8.1')
@Grab('commons-io:commons-io:2.4')
@Grab('org.codehaus.gpars:gpars:1.2.1')
class FlickrDownloader {
static final String FLICKER_INTERESTING_URL =
"https://www.flickr.com/explore/interesting/7days"
static final String WORK_DIR = "./work"
final File workDir = new File(WORK_DIR)
@Scheduled(fixedRate = 30000L)
void downloadFlickrInteresting() {
def photos = extractPhotosFromFlickr()
withPool {
photos.eachParallel { photoUrl ->
log.info "Downloading photo ${photoUrl}"
def tempFile = download(photoUrl)
FileUtils.moveFileToDirectory(tempFile, workDir, true)
}
}
}
}
FlickrDownloader
@Slf4j
@EnableScheduling
@Grab('org.jsoup:jsoup:1.8.1')
@Grab('commons-io:commons-io:2.4')
@Grab('org.codehaus.gpars:gpars:1.2.1')
class FlickrDownloader {
static final String FLICKER_INTERESTING_URL =
"https://www.flickr.com/explore/interesting/7days"
static final String WORK_DIR = "./work"
final File workDir = new File(WORK_DIR)
@Scheduled(fixedRate = 30000L)
void downloadFlickrInteresting() {
def photos = extractPhotosFromFlickr()
withPool {
photos.eachParallel { photoUrl ->
log.info "Downloading photo ${photoUrl}"
def tempFile = download(photoUrl)
FileUtils.moveFileToDirectory(tempFile, workDir, true)
}
}
}
}
FlickrDownloader
FlickrDownloader
@Slf4j
@EnableScheduling
@Grab('org.jsoup:jsoup:1.8.1')
@Grab('commons-io:commons-io:2.4')
@Grab('org.codehaus.gpars:gpars:1.2.1')
class FlickrDownloader {
static final String FLICKER_INTERESTING_URL =
"https://www.flickr.com/explore/interesting/7days"
static final String WORK_DIR = "./work"
final File workDir = new File(WORK_DIR)
@Scheduled(fixedRate = 30000L)
void downloadFlickrInteresting() {
def photos = extractPhotosFromFlickr()
withPool {
photos.eachParallel { photoUrl ->
log.info "Downloading photo ${photoUrl}"
def tempFile = download(photoUrl)
FileUtils.moveFileToDirectory(tempFile, workDir, true)
}
}
}
}
private List extractPhotosFromFlickr() {
Document doc = Jsoup.connect(FLICKER_INTERESTING_URL).get()
Elements images = doc.select("img.pc_img")
def photos = images
.listIterator()
.collect { it.attr('src').replace('_m.jpg', '_b.jpg') }
photos
}
FlickrDownloader
@Slf4j
@EnableScheduling
@Grab('org.jsoup:jsoup:1.8.1')
@Grab('commons-io:commons-io:2.4')
@Grab('org.codehaus.gpars:gpars:1.2.1')
class FlickrDownloader {
static final String FLICKER_INTERESTING_URL =
"https://www.flickr.com/explore/interesting/7days"
static final String WORK_DIR = "./work"
final File workDir = new File(WORK_DIR)
@Scheduled(fixedRate = 30000L)
void downloadFlickrInteresting() {
def photos = extractPhotosFromFlickr()
withPool {
photos.eachParallel { photoUrl ->
log.info "Downloading photo ${photoUrl}"
def tempFile = download(photoUrl)
FileUtils.moveFileToDirectory(tempFile, workDir, true)
}
}
}
}
private File download(String url) {
def tempFile = File.createTempFile('flickr_downloader', '')
tempFile << url.toURL().bytes
tempFile
}
FlickrDownloader
@Slf4j
@EnableScheduling
@Grab('org.jsoup:jsoup:1.8.1')
@Grab('commons-io:commons-io:2.4')
@Grab('org.codehaus.gpars:gpars:1.2.1')
class FlickrDownloader {
static final String FLICKER_INTERESTING_URL =
"https://www.flickr.com/explore/interesting/7days"
static final String WORK_DIR = "./work"
final File workDir = new File(WORK_DIR)
@Scheduled(fixedRate = 30000L)
void downloadFlickrInteresting() {
def photos = extractPhotosFromFlickr()
withPool {
photos.eachParallel { photoUrl ->
log.info "Downloading photo ${photoUrl}"
def tempFile = download(photoUrl)
FileUtils.moveFileToDirectory(tempFile, workDir, true)
}
}
}
}
FlickrDownloader
2016-05-27 21:56:11.139 INFO 16447 --- [111617-worker-1] polaromatic.FlickrDownloader
2016-05-27 21:56:11.139 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader
2016-05-27 21:56:11.139 INFO 16447 --- [111617-worker-3] polaromatic.FlickrDownloader
2016-05-27 21:56:11.354 INFO 16447 --- [111617-worker-1] polaromatic.FlickrDownloader
2016-05-27 21:56:11.375 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader
2016-05-27 21:56:11.527 INFO 16447 --- [111617-worker-3] polaromatic.FlickrDownloader
2016-05-27 21:56:11.537 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader
2016-05-27 21:56:11.612 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader
2016-05-27 21:56:11.693 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader
2016-05-27 22:02:17.019 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader
2016-05-27 22:02:19.451 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader
2016-05-27 22:02:21.661 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader
2016-05-27 22:02:22.079 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader
2016-05-27 22:02:22.877 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader
2016-05-27 22:02:23.392 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader
2016-05-27 22:02:23.749 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader
2016-05-27 22:02:24.250 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader
2016-05-27 22:02:24.695 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader
FlickrDownloader
2016-05-27 21:56:11.139 INFO 16447 --- [111617-worker-1] polaromatic.FlickrDownloader
2016-05-27 21:56:11.139 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader
2016-05-27 21:56:11.139 INFO 16447 --- [111617-worker-3] polaromatic.FlickrDownloader
2016-05-27 21:56:11.354 INFO 16447 --- [111617-worker-1] polaromatic.FlickrDownloader
2016-05-27 21:56:11.375 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader
2016-05-27 21:56:11.527 INFO 16447 --- [111617-worker-3] polaromatic.FlickrDownloader
2016-05-27 21:56:11.537 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader
2016-05-27 21:56:11.612 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader
2016-05-27 21:56:11.693 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader
2016-05-27 22:02:17.019 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader
2016-05-27 22:02:19.451 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader
2016-05-27 22:02:21.661 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader
2016-05-27 22:02:22.079 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader
2016-05-27 22:02:22.877 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader
2016-05-27 22:02:23.392 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader
2016-05-27 22:02:23.749 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader
2016-05-27 22:02:24.250 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader
2016-05-27 22:02:24.695 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader
4.
Frontend
Frontend
▷ MarkupTemplateEngine (HTML)
▷ Websockets
▷ Grooscript
HTML
yieldUnescaped '<!DOCTYPE html>'
html {
head {
title "Polaromatic"
link(rel: 'stylesheet', href: '/css/app.css')
link(rel: 'stylesheet', href: '/css/gh-fork-ribbon.css')
['webjars/sockjs-client/0.3.4-1/sockjs.min.js',
'webjars/stomp-websocket/2.3.1-1/stomp.min.js',
'webjars/jquery/2.1.3/jquery.min.js',
'webjars/handlebars/2.0.0-1/handlebars.min.js',
'js/Connection.js']
.each {
yieldUnescaped "<script src='$it'></script>"
}
}
}
HTML
body {
...
div(id: 'header') {
div(class: 'center') {
a(href: 'https://github.com/ilopmar/contest', target: 'blank') {
img(src: 'images/polaromatic-logo.png')
}
p('Polaromatic')
span('Powered by Spring Boot')
}
}
div(id: 'timeline', class: 'center')
}
script(id: 'photo-template', type: 'text/x-handlebars-template') {
div(class: 'photo-cover') {
div(class: 'photo', style: 'visibility:hidden; height:0') {
img(src: '{{image}}')
}
}
}
yieldUnescaped "<script>Connection().start()</script>"
}
Websockets
@Configuration
@EnableWebSocketMessageBroker
class WebsocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker '/notifications'
}
@Override
void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint('/polaromatic').withSockJS()
}
}
Websockets
class BrowserPushService {
@Autowired
SimpMessagingTemplate template
Photo pushToBrowser(Photo photo) {
log.debug "Pushing file to browser: ${photo.output}"
String imageB64 = new File(photo.output).bytes.encodeBase64().toString()
template.convertAndSend "/notifications/photo", imageB64
return photo
}
}
Websockets
class BrowserPushService {
@Autowired
SimpMessagingTemplate template
Photo pushToBrowser(Photo photo) {
log.debug "Pushing file to browser: ${photo.output}"
String imageB64 = new File(photo.output).bytes.encodeBase64().toString()
template.convertAndSend "/notifications/photo", imageB64
return photo
}
}
<chain input-channel="incommingFilesChannel">
<service-activator ref="fileService" method="preprocessFile"/>
<service-activator ref="imageConverterService" method="applyEffect"/>
<service-activator ref="browserPushService" method="pushToBrowser"/>
<service-activator ref="metricsService" method="updateMetrics"/>
<service-activator ref="fileService" method="deleteTempFiles"/>
</chain>
class Connection {
@GsNative
def initOn(source, path) {/*
var socket = new SockJS(path);
return [Handlebars.compile(source), Stomp.over(socket)];
*/}
def start() {
def source = $("#photo-template").html()
def (template, client) = initOn(source, '/polaromatic')
client.debug = null
client.connect(gs.toJavascript([:])) { ->
client.subscribe('/notifications/photo') { message ->
def context = [image: 'data:image/png;base64,' + message.body]
def html = template(context)
$("#timeline").prepend(html)
$("#timeline .photo:first-child img").on("load") {
$(this).parent().css(gs.toJavascript(display: 'none', visibility: 'visible', height: 'auto'))
$(this).parent().slideDown()
}
}
}
}
}
Grooscript (Javascript)
5.
Android
Android App
▷ Disclaimer: I'm not an Android developer
▷ Lazybones template (@marioggar)
▷ Traits, @CompileStatic
▷ SwissKnife
Android
trait Toastable {
@OnUIThread
void showToastMessage(String message) {
Toast toast = Toast.makeText(applicationContext, message, Toast.LENGTH_SHORT)
toast.show()
}
}
Android
trait Toastable {
@OnUIThread
void showToastMessage(String message) {
Toast toast = Toast.makeText(applicationContext, message, Toast.LENGTH_SHORT)
toast.show()
}
}
@CompileStatic
public class ShareActivity extends Activity implements Toastable {
...
showToastMessage(getString(R.string.share_ok_msg))
...
}
6.
Tests
Tests
▷ Spock Framework
▷ Version 1.0 (more than 2 years now)
▷ JUnit compatible (but way better)
Spock
class BrowserPushServiceSpec extends Specification {
void 'should push a converted photo to the browser'() {
given: 'a photo'
def output = File.createTempFile("output", "")
def photo = new Photo(output: output.path)
and: 'a mocked SimpMessagingTemplate'
def mockSimpMessagingTemplate = Mock(SimpMessagingTemplate)
and: 'the push service'
def browserPushService = new BrowserPushService(template: mockSimpMessagingTemplate)
when: 'pushing the photo to the browser'
browserPushService.pushToBrowser(photo)
then: 'the photo is pushed'
1 * mockSimpMessagingTemplate.convertAndSend('/notifications/photo', "")
}
}
7.
Build tool
Build tool
▷ Gradle
▷ Multiproject to build backend, documentation
and android
Gradle
subprojects {
buildscript {
repositories {
jcenter()
}
}
repositories {
jcenter()
}
}
task wrapper(type: Wrapper) {
gradleVersion = '2.2.1'
}
include 'polaromatic-back'
include 'polaromatic-groid'
include 'polaromatic-docs'
build.gradle settings.gradle
8.
Documentation
Documentation
▷ Asciidoctor (FTW!)
▷ Gradle plugin
▷ Backends: html, epub, pdf,...
Asciidoctor
buildscript {
dependencies {
classpath 'org.asciidoctor:asciidoctor-gradle-plugin:1.5.2'
}
}
apply plugin: 'org.asciidoctor.convert'
asciidoctor {
sourceDir 'src/docs'
outputDir "${buildDir}/docs"
attributes 'source-highlighter': 'coderay',
toc : 'left',
icons : 'font'
}
Asciidoctor
[source,xml,indent=0]
.src/main/resources/resources.xml
----
include::{polaromaticBackResources}/resources.xml[tags=appFlow]
----
<1> Define the integration with the file system
<2> Preprocess the file received
<3> Apply the Polaroid effect
<4> Send the new photo to the browser using Websockets
<5> Update the metrics
<6> Delete all temporary files
Asciidoctor
[source,xml,indent=0]
.src/main/resources/resources.xml
----
include::{polaromaticBackResources}/resources.xml[tags=appFlow]
----
<1> Define the integration with the file system
<2> Preprocess the file received
<3> Apply the Polaroid effect
<4> Send the new photo to the browser using Websockets
<5> Update the metrics
<6> Delete all temporary files
<!-- tag::appFlow[] -->
<file:inbound-channel-adapter directory="work" channel="incommingFilesChannel"/> <!--1-->
<chain input-channel="incommingFilesChannel">
<service-activator ref="fileService" method="preprocessFile"/> <!--2-->
<service-activator ref="imageConverterService" method="applyEffect"/> <!--3-->
<service-activator ref="browserPushService" method="pushToBrowser"/> <!--4-->
<service-activator ref="metricsService" method="updateMetrics"/> <!--5-->
<service-activator ref="fileService" method="deleteTempFiles"/> <!--6-->
</chain>
<!-- end::appFlow[]-->
Asciidoctor
9.
Summary
“
Groovy, groovy everywhere...
Thanks!
Any questions?
@ilopmar
lopez.ivan@gmail.com
https://github.com/ilopmar
Iván López
http://bit.ly/fullstack-groovy

Mum, I want to be a Groovy full-stack developer

  • 1.
    Iván López -@ilopmar Mum, I want to be a Groovy full-stack developer
  • 2.
    Hello! I am IvánLópez @ilopmar http://greachconf.com@madridgug
  • 3.
    Thank you verymuch! Q&A
  • 4.
  • 6.
  • 7.
  • 9.
  • 11.
  • 12.
  • 14.
  • 16.
    Polaromatic ▷ Spring Boot ▷Core App ▷ Spring MVC ▷ Spring Integration Flow
  • 17.
    <file:inbound-channel-adapter directory="work" channel="incommingFilesChannel"/> <chaininput-channel="incommingFilesChannel"> <service-activator ref="fileService" method="preprocessFile"/> <service-activator ref="imageConverterService" method="applyEffect"/> <service-activator ref="browserPushService" method="pushToBrowser"/> <service-activator ref="metricsService" method="updateMetrics"/> <service-activator ref="fileService" method="deleteTempFiles"/> </chain> Spring Integration Flow
  • 18.
    <file:inbound-channel-adapter directory="work" channel="incommingFilesChannel"/> <chaininput-channel="incommingFilesChannel"> <service-activator ref="fileService" method="preprocessFile"/> <service-activator ref="imageConverterService" method="applyEffect"/> <service-activator ref="browserPushService" method="pushToBrowser"/> <service-activator ref="metricsService" method="updateMetrics"/> <service-activator ref="fileService" method="deleteTempFiles"/> </chain> Spring Integration Flow Photo preprocessFile(File file) { def pr = new PolaroidRequest(file) this.preprocessFile(pr) } File service
  • 19.
    <file:inbound-channel-adapter directory="work" channel="incommingFilesChannel"/> <chaininput-channel="incommingFilesChannel"> <service-activator ref="fileService" method="preprocessFile"/> <service-activator ref="imageConverterService" method="applyEffect"/> <service-activator ref="browserPushService" method="pushToBrowser"/> <service-activator ref="metricsService" method="updateMetrics"/> <service-activator ref="fileService" method="deleteTempFiles"/> </chain> Spring Integration Flow File service Photo preprocessFile(File file) { def pr = new PolaroidRequest(file) this.preprocessFile(pr) } Photo preprocessFile(PolaroidRequest polaroidRequest) { String outputFile = File.createTempFile("output", ".png").path return new Photo(input: polaroidRequest.inputFile.absolutePath, output: outputFile, text: polaroidRequest.text) }
  • 20.
    class ImageConverterService { privatestatic final String DEFAULT_CAPTION = "#LearningSpringBoot with Polaromaticn" Random rnd = new Random() Photo applyEffect(Photo photo) { log.debug "Applying effect to file: ${photo.input}..." def inputFile = photo.input def outputFile = photo.output double polaroidRotation = rnd.nextInt(6).toDouble() String caption = photo.text ?: DEFAULT_CAPTION def op = new IMOperation() op.addImage(inputFile) op.thumbnail(300, 300) .set("caption", caption) .gravity("center") .pointsize(20) .background("black") .polaroid(rnd.nextBoolean() ? polaroidRotation : -polaroidRotation) .addImage(outputFile) def command = new ConvertCmd() command.run(op) photo } } Image converter
  • 22.
    FlickrDownloader ▷ Spring BootCLI ▷ Download Flickr Interesting pictures ▷ Jsoup, GPars ▷ 55 lines of Groovy code (microservice?)
  • 23.
    @Slf4j @EnableScheduling @Grab('org.jsoup:jsoup:1.8.1') @Grab('commons-io:commons-io:2.4') @Grab('org.codehaus.gpars:gpars:1.2.1') class FlickrDownloader { staticfinal String FLICKER_INTERESTING_URL = "https://www.flickr.com/explore/interesting/7days" static final String WORK_DIR = "./work" final File workDir = new File(WORK_DIR) @Scheduled(fixedRate = 30000L) void downloadFlickrInteresting() { def photos = extractPhotosFromFlickr() withPool { photos.eachParallel { photoUrl -> log.info "Downloading photo ${photoUrl}" def tempFile = download(photoUrl) FileUtils.moveFileToDirectory(tempFile, workDir, true) } } } } FlickrDownloader
  • 24.
    @Slf4j @EnableScheduling @Grab('org.jsoup:jsoup:1.8.1') @Grab('commons-io:commons-io:2.4') @Grab('org.codehaus.gpars:gpars:1.2.1') class FlickrDownloader { staticfinal String FLICKER_INTERESTING_URL = "https://www.flickr.com/explore/interesting/7days" static final String WORK_DIR = "./work" final File workDir = new File(WORK_DIR) @Scheduled(fixedRate = 30000L) void downloadFlickrInteresting() { def photos = extractPhotosFromFlickr() withPool { photos.eachParallel { photoUrl -> log.info "Downloading photo ${photoUrl}" def tempFile = download(photoUrl) FileUtils.moveFileToDirectory(tempFile, workDir, true) } } } } FlickrDownloader
  • 25.
    FlickrDownloader @Slf4j @EnableScheduling @Grab('org.jsoup:jsoup:1.8.1') @Grab('commons-io:commons-io:2.4') @Grab('org.codehaus.gpars:gpars:1.2.1') class FlickrDownloader { staticfinal String FLICKER_INTERESTING_URL = "https://www.flickr.com/explore/interesting/7days" static final String WORK_DIR = "./work" final File workDir = new File(WORK_DIR) @Scheduled(fixedRate = 30000L) void downloadFlickrInteresting() { def photos = extractPhotosFromFlickr() withPool { photos.eachParallel { photoUrl -> log.info "Downloading photo ${photoUrl}" def tempFile = download(photoUrl) FileUtils.moveFileToDirectory(tempFile, workDir, true) } } } } private List extractPhotosFromFlickr() { Document doc = Jsoup.connect(FLICKER_INTERESTING_URL).get() Elements images = doc.select("img.pc_img") def photos = images .listIterator() .collect { it.attr('src').replace('_m.jpg', '_b.jpg') } photos }
  • 26.
    FlickrDownloader @Slf4j @EnableScheduling @Grab('org.jsoup:jsoup:1.8.1') @Grab('commons-io:commons-io:2.4') @Grab('org.codehaus.gpars:gpars:1.2.1') class FlickrDownloader { staticfinal String FLICKER_INTERESTING_URL = "https://www.flickr.com/explore/interesting/7days" static final String WORK_DIR = "./work" final File workDir = new File(WORK_DIR) @Scheduled(fixedRate = 30000L) void downloadFlickrInteresting() { def photos = extractPhotosFromFlickr() withPool { photos.eachParallel { photoUrl -> log.info "Downloading photo ${photoUrl}" def tempFile = download(photoUrl) FileUtils.moveFileToDirectory(tempFile, workDir, true) } } } } private File download(String url) { def tempFile = File.createTempFile('flickr_downloader', '') tempFile << url.toURL().bytes tempFile }
  • 27.
    FlickrDownloader @Slf4j @EnableScheduling @Grab('org.jsoup:jsoup:1.8.1') @Grab('commons-io:commons-io:2.4') @Grab('org.codehaus.gpars:gpars:1.2.1') class FlickrDownloader { staticfinal String FLICKER_INTERESTING_URL = "https://www.flickr.com/explore/interesting/7days" static final String WORK_DIR = "./work" final File workDir = new File(WORK_DIR) @Scheduled(fixedRate = 30000L) void downloadFlickrInteresting() { def photos = extractPhotosFromFlickr() withPool { photos.eachParallel { photoUrl -> log.info "Downloading photo ${photoUrl}" def tempFile = download(photoUrl) FileUtils.moveFileToDirectory(tempFile, workDir, true) } } } }
  • 28.
    FlickrDownloader 2016-05-27 21:56:11.139 INFO16447 --- [111617-worker-1] polaromatic.FlickrDownloader 2016-05-27 21:56:11.139 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader 2016-05-27 21:56:11.139 INFO 16447 --- [111617-worker-3] polaromatic.FlickrDownloader 2016-05-27 21:56:11.354 INFO 16447 --- [111617-worker-1] polaromatic.FlickrDownloader 2016-05-27 21:56:11.375 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader 2016-05-27 21:56:11.527 INFO 16447 --- [111617-worker-3] polaromatic.FlickrDownloader 2016-05-27 21:56:11.537 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader 2016-05-27 21:56:11.612 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader 2016-05-27 21:56:11.693 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader 2016-05-27 22:02:17.019 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader 2016-05-27 22:02:19.451 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader 2016-05-27 22:02:21.661 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader 2016-05-27 22:02:22.079 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader 2016-05-27 22:02:22.877 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader 2016-05-27 22:02:23.392 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader 2016-05-27 22:02:23.749 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader 2016-05-27 22:02:24.250 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader 2016-05-27 22:02:24.695 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader
  • 29.
    FlickrDownloader 2016-05-27 21:56:11.139 INFO16447 --- [111617-worker-1] polaromatic.FlickrDownloader 2016-05-27 21:56:11.139 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader 2016-05-27 21:56:11.139 INFO 16447 --- [111617-worker-3] polaromatic.FlickrDownloader 2016-05-27 21:56:11.354 INFO 16447 --- [111617-worker-1] polaromatic.FlickrDownloader 2016-05-27 21:56:11.375 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader 2016-05-27 21:56:11.527 INFO 16447 --- [111617-worker-3] polaromatic.FlickrDownloader 2016-05-27 21:56:11.537 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader 2016-05-27 21:56:11.612 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader 2016-05-27 21:56:11.693 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader 2016-05-27 22:02:17.019 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader 2016-05-27 22:02:19.451 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader 2016-05-27 22:02:21.661 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader 2016-05-27 22:02:22.079 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader 2016-05-27 22:02:22.877 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader 2016-05-27 22:02:23.392 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader 2016-05-27 22:02:23.749 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader 2016-05-27 22:02:24.250 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader 2016-05-27 22:02:24.695 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader
  • 30.
  • 32.
  • 33.
    HTML yieldUnescaped '<!DOCTYPE html>' html{ head { title "Polaromatic" link(rel: 'stylesheet', href: '/css/app.css') link(rel: 'stylesheet', href: '/css/gh-fork-ribbon.css') ['webjars/sockjs-client/0.3.4-1/sockjs.min.js', 'webjars/stomp-websocket/2.3.1-1/stomp.min.js', 'webjars/jquery/2.1.3/jquery.min.js', 'webjars/handlebars/2.0.0-1/handlebars.min.js', 'js/Connection.js'] .each { yieldUnescaped "<script src='$it'></script>" } } }
  • 34.
    HTML body { ... div(id: 'header'){ div(class: 'center') { a(href: 'https://github.com/ilopmar/contest', target: 'blank') { img(src: 'images/polaromatic-logo.png') } p('Polaromatic') span('Powered by Spring Boot') } } div(id: 'timeline', class: 'center') } script(id: 'photo-template', type: 'text/x-handlebars-template') { div(class: 'photo-cover') { div(class: 'photo', style: 'visibility:hidden; height:0') { img(src: '{{image}}') } } } yieldUnescaped "<script>Connection().start()</script>" }
  • 35.
    Websockets @Configuration @EnableWebSocketMessageBroker class WebsocketConfig extendsAbstractWebSocketMessageBrokerConfigurer { @Override void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker '/notifications' } @Override void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint('/polaromatic').withSockJS() } }
  • 36.
    Websockets class BrowserPushService { @Autowired SimpMessagingTemplatetemplate Photo pushToBrowser(Photo photo) { log.debug "Pushing file to browser: ${photo.output}" String imageB64 = new File(photo.output).bytes.encodeBase64().toString() template.convertAndSend "/notifications/photo", imageB64 return photo } }
  • 37.
    Websockets class BrowserPushService { @Autowired SimpMessagingTemplatetemplate Photo pushToBrowser(Photo photo) { log.debug "Pushing file to browser: ${photo.output}" String imageB64 = new File(photo.output).bytes.encodeBase64().toString() template.convertAndSend "/notifications/photo", imageB64 return photo } } <chain input-channel="incommingFilesChannel"> <service-activator ref="fileService" method="preprocessFile"/> <service-activator ref="imageConverterService" method="applyEffect"/> <service-activator ref="browserPushService" method="pushToBrowser"/> <service-activator ref="metricsService" method="updateMetrics"/> <service-activator ref="fileService" method="deleteTempFiles"/> </chain>
  • 38.
    class Connection { @GsNative definitOn(source, path) {/* var socket = new SockJS(path); return [Handlebars.compile(source), Stomp.over(socket)]; */} def start() { def source = $("#photo-template").html() def (template, client) = initOn(source, '/polaromatic') client.debug = null client.connect(gs.toJavascript([:])) { -> client.subscribe('/notifications/photo') { message -> def context = [image: 'data:image/png;base64,' + message.body] def html = template(context) $("#timeline").prepend(html) $("#timeline .photo:first-child img").on("load") { $(this).parent().css(gs.toJavascript(display: 'none', visibility: 'visible', height: 'auto')) $(this).parent().slideDown() } } } } } Grooscript (Javascript)
  • 39.
  • 41.
    Android App ▷ Disclaimer:I'm not an Android developer ▷ Lazybones template (@marioggar) ▷ Traits, @CompileStatic ▷ SwissKnife
  • 43.
    Android trait Toastable { @OnUIThread voidshowToastMessage(String message) { Toast toast = Toast.makeText(applicationContext, message, Toast.LENGTH_SHORT) toast.show() } }
  • 44.
    Android trait Toastable { @OnUIThread voidshowToastMessage(String message) { Toast toast = Toast.makeText(applicationContext, message, Toast.LENGTH_SHORT) toast.show() } } @CompileStatic public class ShareActivity extends Activity implements Toastable { ... showToastMessage(getString(R.string.share_ok_msg)) ... }
  • 45.
  • 46.
    Tests ▷ Spock Framework ▷Version 1.0 (more than 2 years now) ▷ JUnit compatible (but way better)
  • 47.
    Spock class BrowserPushServiceSpec extendsSpecification { void 'should push a converted photo to the browser'() { given: 'a photo' def output = File.createTempFile("output", "") def photo = new Photo(output: output.path) and: 'a mocked SimpMessagingTemplate' def mockSimpMessagingTemplate = Mock(SimpMessagingTemplate) and: 'the push service' def browserPushService = new BrowserPushService(template: mockSimpMessagingTemplate) when: 'pushing the photo to the browser' browserPushService.pushToBrowser(photo) then: 'the photo is pushed' 1 * mockSimpMessagingTemplate.convertAndSend('/notifications/photo', "") } }
  • 48.
  • 49.
    Build tool ▷ Gradle ▷Multiproject to build backend, documentation and android
  • 50.
    Gradle subprojects { buildscript { repositories{ jcenter() } } repositories { jcenter() } } task wrapper(type: Wrapper) { gradleVersion = '2.2.1' } include 'polaromatic-back' include 'polaromatic-groid' include 'polaromatic-docs' build.gradle settings.gradle
  • 51.
  • 52.
    Documentation ▷ Asciidoctor (FTW!) ▷Gradle plugin ▷ Backends: html, epub, pdf,...
  • 53.
    Asciidoctor buildscript { dependencies { classpath'org.asciidoctor:asciidoctor-gradle-plugin:1.5.2' } } apply plugin: 'org.asciidoctor.convert' asciidoctor { sourceDir 'src/docs' outputDir "${buildDir}/docs" attributes 'source-highlighter': 'coderay', toc : 'left', icons : 'font' }
  • 54.
    Asciidoctor [source,xml,indent=0] .src/main/resources/resources.xml ---- include::{polaromaticBackResources}/resources.xml[tags=appFlow] ---- <1> Define theintegration with the file system <2> Preprocess the file received <3> Apply the Polaroid effect <4> Send the new photo to the browser using Websockets <5> Update the metrics <6> Delete all temporary files
  • 55.
    Asciidoctor [source,xml,indent=0] .src/main/resources/resources.xml ---- include::{polaromaticBackResources}/resources.xml[tags=appFlow] ---- <1> Define theintegration with the file system <2> Preprocess the file received <3> Apply the Polaroid effect <4> Send the new photo to the browser using Websockets <5> Update the metrics <6> Delete all temporary files <!-- tag::appFlow[] --> <file:inbound-channel-adapter directory="work" channel="incommingFilesChannel"/> <!--1--> <chain input-channel="incommingFilesChannel"> <service-activator ref="fileService" method="preprocessFile"/> <!--2--> <service-activator ref="imageConverterService" method="applyEffect"/> <!--3--> <service-activator ref="browserPushService" method="pushToBrowser"/> <!--4--> <service-activator ref="metricsService" method="updateMetrics"/> <!--5--> <service-activator ref="fileService" method="deleteTempFiles"/> <!--6--> </chain> <!-- end::appFlow[]-->
  • 56.
  • 57.
  • 58.
  • 59.