59
Mum, I want to be a Groovy full-stack developer IVÁN LÓPEZ @ilopmar

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

Embed Size (px)

Citation preview

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

IVÁN LÓPEZ

@ilopmar

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 servicePhoto 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 Polaromatic\\n"

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-02-17 21:56:11.139 INFO 16447 --- [111617-worker-1] polaromatic.FlickrDownloader2016-02-17 21:56:11.139 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader2016-02-17 21:56:11.139 INFO 16447 --- [111617-worker-3] polaromatic.FlickrDownloader2016-02-17 21:56:11.354 INFO 16447 --- [111617-worker-1] polaromatic.FlickrDownloader2016-02-17 21:56:11.375 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader2016-02-17 21:56:11.527 INFO 16447 --- [111617-worker-3] polaromatic.FlickrDownloader2016-02-17 21:56:11.537 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader2016-02-17 21:56:11.612 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader2016-02-17 21:56:11.693 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader

2016-02-17 22:02:17.019 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader2016-02-17 22:02:19.451 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader2016-02-17 22:02:21.661 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader2016-02-17 22:02:22.079 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader2016-02-17 22:02:22.877 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader2016-02-17 22:02:23.392 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader2016-02-17 22:02:23.749 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader2016-02-17 22:02:24.250 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader2016-02-17 22:02:24.695 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader

FlickrDownloader

2016-02-17 21:56:11.139 INFO 16447 --- [111617-worker-1] polaromatic.FlickrDownloader2016-02-17 21:56:11.139 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader2016-02-17 21:56:11.139 INFO 16447 --- [111617-worker-3] polaromatic.FlickrDownloader2016-02-17 21:56:11.354 INFO 16447 --- [111617-worker-1] polaromatic.FlickrDownloader2016-02-17 21:56:11.375 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader2016-02-17 21:56:11.527 INFO 16447 --- [111617-worker-3] polaromatic.FlickrDownloader2016-02-17 21:56:11.537 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader2016-02-17 21:56:11.612 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader2016-02-17 21:56:11.693 INFO 16447 --- [111617-worker-2] polaromatic.FlickrDownloader

2016-02-17 22:02:17.019 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader2016-02-17 22:02:19.451 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader2016-02-17 22:02:21.661 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader2016-02-17 22:02:22.079 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader2016-02-17 22:02:22.877 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader2016-02-17 22:02:23.392 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader2016-02-17 22:02:23.749 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader2016-02-17 22:02:24.250 INFO 9872 --- [pool-1-thread-1] polaromatic.FlickrDownloader2016-02-17 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>" } }}

HTMLbody { ...

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@EnableWebSocketMessageBrokerclass 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() }}

@CompileStaticpublic class ShareActivity extends Activity implements Toastable { ...

showToastMessage(getString(R.string.share_ok_msg))

...

}

6.Tests

Tests

▷ Spock Framework

▷ 0.7 for more than 4 years

▷ Now 1.0 (more than 1.5 years now)

▷ JUnit compatible (but way better)

Spockclass 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

[email protected]

https://github.com/ilopmar

Iván López

http://bit.ly/confoo-groovy