confoo 2016 - mum, i want to be a groovy full-stack developer
TRANSCRIPT
<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
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)
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))
...
}
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', "") }}
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
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[]-->
Thanks!Any questions?
@ilopmar
https://github.com/ilopmar
Iván López
http://bit.ly/confoo-groovy