diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ded116a --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +build/ +*.iml +*.ipr +*.ips +.gradle/ +.idea/ +work +.lazybones/ +local.properties +log/ diff --git a/README.adoc b/README.adoc index 54006bb..478428e 100644 --- a/README.adoc +++ b/README.adoc @@ -1,36 +1,5 @@ -== _Learning Spring Boot_ Contest +== Polaromatic -This is a contest based on _Learning Spring Boot_. The idea is to submit a small, pithy, cool Spring Boot application to win a prize. We will soon announce the prizes. +Please read the documentation here: http://ilopmar.github.io/contest/ -=== Calling all Spring Boot apps - -Years ago, the http://www.ioccc.org/years.html[The International Obfuscated C Code Contest] was invented. Seeing http://blog.aerojockey.com/post/iocccsim[Carl Banks' flight simulator] forever impressed me as a neat, pithy little app. - -Well, we aren't looking for obfuscated, complex, impossible to read apps. But we *ARE* looking for slick, cool apps that show off the power/coolness/wicked abilities of Spring Boot mixed with your hackable creativity. Characteristics the judges evaluate include: - -* Stylish -* Short and sweet -* Custom auto-configurations are welcome -* Custom health indicators, metrics, and fancy usage thereof -* Nice on the server side OR cool frontends (You don't have to build a web frontend to have a slick, elegeant, and original UI.) -* Popularity. Tweet things up while you work on your submission. We will definitely look at stars on your forked repo of this contest, volume of traffic your generate, and other evidence of popularity. - -=== How/when to submit - -IMPORANT: Deadline is 11:59pm January 17th CST (UTC-6). (I hate midnight deadlines, since they're so ambiguous.) - -To submit an entery: - -. Fork this repo. -. Code your solution inside your fork. -. Tweet/blog/reddit/facebook your efforts and gather evidence of your apps popularity (stars on your repo, total hits on your blog entry, total number of registered userse for your slick app, etc.) -. Replace this README.adoc with your own documentation (cuteness rewarded!). -. Submit a pull request (before the deadline!!!) -. Wait to see the announcement. -. Collect your prize! - -Those of you that can't wait, use the holiday time as you wish. Those of you that are enjoying time with family and friends, we included enough time so you can still get into the contest. - -NOTE: No member of Pivotal Inc. is permitted to enter this contest. Only one submission per person or team. If there is evidence of multiple "sock puppet" entries coming from the same group of people, the judges reserve the right to disqualify anyone involved. All decisions are final. - -Good luck! +If you like the application please star this Github repository. You can also write some tweets with the *#Polaromatic* hashtag. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..7e610b6 --- /dev/null +++ b/build.gradle @@ -0,0 +1,16 @@ +subprojects { + buildscript { + repositories { + jcenter() + + } + } + + repositories { + jcenter() + } +} + +task wrapper(type: Wrapper) { + gradleVersion = '2.2.1' +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..c97a8bd Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..4024276 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jan 09 00:23:39 CET 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-bin.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..91a7e26 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/logos/logo.svg b/logos/logo.svg new file mode 100644 index 0000000..a78572f --- /dev/null +++ b/logos/logo.svg @@ -0,0 +1,65 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/logos/schema.svg b/logos/schema.svg new file mode 100644 index 0000000..08e4a85 --- /dev/null +++ b/logos/schema.svg @@ -0,0 +1,981 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + Spring Boot + + + + + + + + + + + + + + + + + + + + + + + + + + + Flickr Downloader + + + + Polaromatic + (Spring Boot) + + + + + + FlickrDownloader + (Spring Boot CLI) + + + + + FlickrDownloader + (Spring Boot CLI) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Websockets + + + + + POST + + + + + Dowloadphotos + + + + + Get photos(Spring Int.) + Android App(Groovy) + + diff --git a/polaromatic-back/FlickrDownloader.groovy b/polaromatic-back/FlickrDownloader.groovy new file mode 100644 index 0000000..abbde19 --- /dev/null +++ b/polaromatic-back/FlickrDownloader.groovy @@ -0,0 +1,57 @@ +package polaromatic + +import groovy.util.logging.Slf4j +import org.apache.commons.io.FileUtils +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import org.jsoup.select.Elements +import org.springframework.scheduling.annotation.EnableScheduling +import org.springframework.scheduling.annotation.Scheduled + +import static groovyx.gpars.GParsPool.withPool + +@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" + File workDir = new File(WORK_DIR) + + @Scheduled(fixedRate = 30000L) + void downloadFlickrInteresting() { + def photos = extractPhotosFromFlickr() + + withPool { + photos.eachParallel { photo -> + log.info "Downloading photo ${photo}" + def tempFile = download(photo) + + 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 + } + + private File download(String url) { + def tempFile = File.createTempFile('flickr_downloader', '') + tempFile.withOutputStream { out -> + out << url.toURL().openStream() + } + + tempFile + } +} \ No newline at end of file diff --git a/polaromatic-back/build.gradle b/polaromatic-back/build.gradle new file mode 100644 index 0000000..2a7a88b --- /dev/null +++ b/polaromatic-back/build.gradle @@ -0,0 +1,55 @@ +buildscript { + ext { + springBootVersion = '1.2.1.RELEASE' + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") + classpath "org.grooscript:grooscript-gradle-plugin:0.9" + } +} + +apply plugin: 'groovy' +apply plugin: 'idea' +apply plugin: 'spring-boot' +apply plugin: 'org.grooscript.conversion' + +jar { + baseName = 'polaromatic' + version = '0.0.1-SNAPSHOT' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +String groovyVersion = '2.4.0-rc-2' + +dependencies { + compile 'org.springframework.boot:spring-boot-starter-integration' + compile 'org.springframework.boot:spring-boot-starter-web' + compile 'org.springframework.boot:spring-boot-starter-websocket' + compile 'org.springframework.boot:spring-boot-starter-actuator' + + compile "org.codehaus.groovy:groovy-all:${groovyVersion}" + compile 'org.im4java:im4java:1.4.0' + + compile 'org.webjars:sockjs-client:0.3.4-1' + compile 'org.webjars:stomp-websocket:2.3.1-1' + compile 'org.webjars:jquery:2.1.3' + compile 'org.webjars:handlebars:2.0.0-1' + compile "org.codehaus.groovy:groovy-templates:${groovyVersion}" + compile 'org.grooscript:grooscript:1.0.0-rc-1' + + testCompile 'org.springframework.boot:spring-boot-starter-test' + testCompile 'org.spockframework:spock-core:0.7-groovy-2.0' + testCompile 'cglib:cglib-nodep:3.1' +} + +grooscript { + source = ['polaromatic-back/src/main/grooscript/polaromatic'] + destination = 'polaromatic-back/src/main/resources/static/js' + addGsLib = 'grooscript.min' + mainContextScope = ['$', 'gs'] +} + +bootRun.dependsOn convert +build.dependsOn convert \ No newline at end of file diff --git a/polaromatic-back/src/main/grooscript/polaromatic/Connection.groovy b/polaromatic-back/src/main/grooscript/polaromatic/Connection.groovy new file mode 100644 index 0000000..d1e6617 --- /dev/null +++ b/polaromatic-back/src/main/grooscript/polaromatic/Connection.groovy @@ -0,0 +1,29 @@ +package polaromatic + +import org.grooscript.asts.GsNative + +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() + } + } + } + } +} diff --git a/polaromatic-back/src/main/groovy/polaromatic/PolaromaticApplication.groovy b/polaromatic-back/src/main/groovy/polaromatic/PolaromaticApplication.groovy new file mode 100644 index 0000000..d5365fe --- /dev/null +++ b/polaromatic-back/src/main/groovy/polaromatic/PolaromaticApplication.groovy @@ -0,0 +1,27 @@ +package polaromatic + +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.ImportResource +import org.springframework.web.filter.CharacterEncodingFilter + +import javax.servlet.Filter + +@SpringBootApplication +@ImportResource("classpath:resources.xml") +class PolaromaticApplication { + + static void main(String[] args) { + SpringApplication.run PolaromaticApplication, args + } + + @Bean + public Filter characterEncodingFilter() { + def characterEncodingFilter = new CharacterEncodingFilter() + characterEncodingFilter.encoding = "UTF-8" + characterEncodingFilter.forceEncoding = true + + return characterEncodingFilter + } +} \ No newline at end of file diff --git a/polaromatic-back/src/main/groovy/polaromatic/config/WebsocketConfig.groovy b/polaromatic-back/src/main/groovy/polaromatic/config/WebsocketConfig.groovy new file mode 100644 index 0000000..6f45196 --- /dev/null +++ b/polaromatic-back/src/main/groovy/polaromatic/config/WebsocketConfig.groovy @@ -0,0 +1,22 @@ +package polaromatic.config + +import org.springframework.context.annotation.Configuration +import org.springframework.messaging.simp.config.MessageBrokerRegistry +import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker +import org.springframework.web.socket.config.annotation.StompEndpointRegistry + +@Configuration +@EnableWebSocketMessageBroker +class WebsocketConfig extends AbstractWebSocketMessageBrokerConfigurer { + + @Override + void configureMessageBroker(MessageBrokerRegistry config) { + config.enableSimpleBroker '/notifications' + } + + @Override + void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint('/polaromatic').withSockJS() + } +} \ No newline at end of file diff --git a/polaromatic-back/src/main/groovy/polaromatic/controller/PolaromaticController.groovy b/polaromatic-back/src/main/groovy/polaromatic/controller/PolaromaticController.groovy new file mode 100644 index 0000000..4a35106 --- /dev/null +++ b/polaromatic-back/src/main/groovy/polaromatic/controller/PolaromaticController.groovy @@ -0,0 +1,42 @@ +package polaromatic.controller + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestMethod +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.ResponseBody +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.multipart.MultipartFile +import org.springframework.web.servlet.ModelAndView +import polaromatic.domain.PolaroidRequest +import polaromatic.service.PhotoToPolaromatize + +@Controller +class PolaromaticController { + + @Autowired + PhotoToPolaromatize polaromatizeService + + @RequestMapping("/") + def home() { + new ModelAndView('home') + } + + @ResponseBody + @RequestMapping(value = "/upload-photo", method = RequestMethod.POST) + @ResponseStatus(HttpStatus.CREATED) + void handleFileUpload(@RequestParam("photo") MultipartFile photo, + @RequestParam("text") String text) { + + if (!photo.isEmpty()) { + def tempFile = File.createTempFile("temp_", "") + photo.transferTo(tempFile) + + def polaroidRequest = new PolaroidRequest(tempFile, text) + + polaromatizeService.process(polaroidRequest) + } + } +} \ No newline at end of file diff --git a/polaromatic-back/src/main/groovy/polaromatic/domain/Photo.groovy b/polaromatic-back/src/main/groovy/polaromatic/domain/Photo.groovy new file mode 100644 index 0000000..aa713f7 --- /dev/null +++ b/polaromatic-back/src/main/groovy/polaromatic/domain/Photo.groovy @@ -0,0 +1,10 @@ +package polaromatic.domain + +import groovy.transform.Immutable + +@Immutable +class Photo { + String input + String output + String text +} diff --git a/polaromatic-back/src/main/groovy/polaromatic/domain/PolaroidRequest.groovy b/polaromatic-back/src/main/groovy/polaromatic/domain/PolaroidRequest.groovy new file mode 100644 index 0000000..0b2862d --- /dev/null +++ b/polaromatic-back/src/main/groovy/polaromatic/domain/PolaroidRequest.groovy @@ -0,0 +1,9 @@ +package polaromatic.domain + +import groovy.transform.TupleConstructor + +@TupleConstructor +class PolaroidRequest { + File inputFile + String text +} diff --git a/polaromatic-back/src/main/groovy/polaromatic/service/BrowserPushService.groovy b/polaromatic-back/src/main/groovy/polaromatic/service/BrowserPushService.groovy new file mode 100644 index 0000000..46d95f5 --- /dev/null +++ b/polaromatic-back/src/main/groovy/polaromatic/service/BrowserPushService.groovy @@ -0,0 +1,27 @@ +package polaromatic.service + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.messaging.simp.SimpMessagingTemplate +import org.springframework.stereotype.Service +import polaromatic.domain.Photo + +@Slf4j +@Service +@CompileStatic +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 + } +} \ No newline at end of file diff --git a/polaromatic-back/src/main/groovy/polaromatic/service/FileService.groovy b/polaromatic-back/src/main/groovy/polaromatic/service/FileService.groovy new file mode 100644 index 0000000..a21bafc --- /dev/null +++ b/polaromatic-back/src/main/groovy/polaromatic/service/FileService.groovy @@ -0,0 +1,31 @@ +package polaromatic.service + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.springframework.stereotype.Service +import polaromatic.domain.Photo +import polaromatic.domain.PolaroidRequest + +@Slf4j +@Service +@CompileStatic +class FileService { + + 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) + } + + void deleteTempFiles(Photo photo) { + [photo.input, photo.output].each { file -> + log.debug "Deleting file: ${file}" + new File(file).delete() + } + } +} \ No newline at end of file diff --git a/polaromatic-back/src/main/groovy/polaromatic/service/ImageConverterService.groovy b/polaromatic-back/src/main/groovy/polaromatic/service/ImageConverterService.groovy new file mode 100644 index 0000000..8d99292 --- /dev/null +++ b/polaromatic-back/src/main/groovy/polaromatic/service/ImageConverterService.groovy @@ -0,0 +1,45 @@ +package polaromatic.service + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.im4java.core.ConvertCmd +import org.im4java.core.IMOperation +import org.springframework.stereotype.Service +import polaromatic.domain.Photo + +@Slf4j +@Service +@CompileStatic +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 + } +} + + diff --git a/polaromatic-back/src/main/groovy/polaromatic/service/MetricsService.groovy b/polaromatic-back/src/main/groovy/polaromatic/service/MetricsService.groovy new file mode 100644 index 0000000..217e8f5 --- /dev/null +++ b/polaromatic-back/src/main/groovy/polaromatic/service/MetricsService.groovy @@ -0,0 +1,29 @@ +package polaromatic.service + +import groovy.transform.CompileStatic +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.actuate.metrics.CounterService +import org.springframework.stereotype.Service +import polaromatic.domain.Photo + +@Service +@CompileStatic +class MetricsService { + + static final String PHOTO_COUNTER_METRICS_FLICKR = "polaromatized.photos.flickr" + static final String PHOTO_COUNTER_METRICS_ANDROID = "polaromatized.photos.android" + + @Autowired + CounterService counterService + + Photo updateMetrics(Photo photo) { + + if (photo.text) { + counterService.increment(PHOTO_COUNTER_METRICS_ANDROID) + } else { + counterService.increment(PHOTO_COUNTER_METRICS_FLICKR) + } + + photo + } +} diff --git a/polaromatic-back/src/main/groovy/polaromatic/service/PhotoToPolaromatize.groovy b/polaromatic-back/src/main/groovy/polaromatic/service/PhotoToPolaromatize.groovy new file mode 100644 index 0000000..160867d --- /dev/null +++ b/polaromatic-back/src/main/groovy/polaromatic/service/PhotoToPolaromatize.groovy @@ -0,0 +1,9 @@ +package polaromatic.service + +import polaromatic.domain.PolaroidRequest + +interface PhotoToPolaromatize { + + void process(PolaroidRequest polaroidRequest) + +} \ No newline at end of file diff --git a/polaromatic-back/src/main/resources/application.yml b/polaromatic-back/src/main/resources/application.yml new file mode 100644 index 0000000..7826d46 --- /dev/null +++ b/polaromatic-back/src/main/resources/application.yml @@ -0,0 +1,14 @@ +multipart.maxFileSize: 2048KB + +endpoints: + autoconfig.enabled: false + beans.enabled: false + configprops.enabled: false + dump.enabled: false + env.enabled: false + health.enabled: true + info.enabled: true + metrics.enabled: true + mappings.enabled: false + shutdown.enabled: false + trace.enabled: false diff --git a/polaromatic-back/src/main/resources/favicon.ico b/polaromatic-back/src/main/resources/favicon.ico new file mode 100644 index 0000000..81b6dca Binary files /dev/null and b/polaromatic-back/src/main/resources/favicon.ico differ diff --git a/polaromatic-back/src/main/resources/logback.groovy b/polaromatic-back/src/main/resources/logback.groovy new file mode 100644 index 0000000..64fe0c7 --- /dev/null +++ b/polaromatic-back/src/main/resources/logback.groovy @@ -0,0 +1,31 @@ +import ch.qos.logback.classic.encoder.PatternLayoutEncoder +import ch.qos.logback.core.ConsoleAppender +import ch.qos.logback.core.rolling.RollingFileAppender +import ch.qos.logback.core.rolling.TimeBasedRollingPolicy + +import static ch.qos.logback.classic.Level.DEBUG +import static ch.qos.logback.classic.Level.INFO + +final String PATTERN = '%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %5p %c{1}:%L - %m%n' +final String LOG_NAME = 'log/polaromatic.log' + +appender "CONSOLE", ConsoleAppender, { + encoder PatternLayoutEncoder, { + pattern = PATTERN + } +} + +appender "FILE", RollingFileAppender, { + file = LOG_NAME + rollingPolicy TimeBasedRollingPolicy, { + fileNamePattern = "${LOG_NAME}.%d{yyyy-MM-dd}" + maxHistory = 365 + } + encoder PatternLayoutEncoder, { + pattern = PATTERN + } +} + +root INFO, ["CONSOLE", "FILE"] + +logger "polaromatic", DEBUG, ["CONSOLE", "FILE"], false diff --git a/polaromatic-back/src/main/resources/resources.xml b/polaromatic-back/src/main/resources/resources.xml new file mode 100644 index 0000000..0647428 --- /dev/null +++ b/polaromatic-back/src/main/resources/resources.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/polaromatic-back/src/main/resources/static/css/app.css b/polaromatic-back/src/main/resources/static/css/app.css new file mode 100644 index 0000000..c96586c --- /dev/null +++ b/polaromatic-back/src/main/resources/static/css/app.css @@ -0,0 +1,65 @@ +@import url(http://fonts.googleapis.com/css?family=Montserrat); + +body { + overflow: auto; + margin: 0; + padding: 0; + font-family: 'Montserrat', sans-serif; +} + +.center { + width: 1280px; + margin: 0 auto; +} + +#header { + background-image: none; + background-color: #34302d; + border: 0; + border-top: 4px solid #6db33f; + padding: 0; + position: fixed; + width: 100%; +} + +#header a { + display: inline-block; +} + +#header p { + background-color: #6db33f; + color: #fff; + display: inline-block; + font-family: 'Montserrat', sans-serif; + font-size: 48px; + margin: 26px 0 0 25px; + padding: 0 15px; + vertical-align: top; + letter-spacing: -2px; +} +#header span { + color: #6db33f; + display: inline-block; + font-family: 'Montserrat', sans-serif; + font-size: 24px; + margin-top: 55px; + padding: 0 15px; + vertical-align: top; + letter-spacing: -2px; + font-style: italic; +} + +#header img { + margin: 4px 0 0 10px; +} + +#timeline { + padding: 125px 10px 20px 10px; +} + +.photo-cover { + width: 30%; + height: 350px; + float: left; + margin: 0 0 20px 20px; +} \ No newline at end of file diff --git a/polaromatic-back/src/main/resources/static/css/gh-fork-ribbon.css b/polaromatic-back/src/main/resources/static/css/gh-fork-ribbon.css new file mode 100644 index 0000000..e04e4b8 --- /dev/null +++ b/polaromatic-back/src/main/resources/static/css/gh-fork-ribbon.css @@ -0,0 +1,87 @@ +/*! + * "Fork me on GitHub" CSS ribbon v0.1.1 | MIT License + * https://github.com/simonwhitaker/github-fork-ribbon-css +*/ + +/* Left will inherit from right (so we don't need to duplicate code) */ +.github-fork-ribbon { + /* The right and left classes determine the side we attach our banner to */ + position: absolute; + + /* Add a bit of padding to give some substance outside the "stitching" */ + padding: 2px 0; + + /* Set the base colour */ + background-color: #6db33f; + + /* Set a gradient: transparent black at the top to almost-transparent black at the bottom */ + background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(0, 0, 0, 0)), to(rgba(0, 0, 0, 0.15))); + background-image: -webkit-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15)); + background-image: -moz-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15)); + background-image: -ms-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15)); + background-image: -o-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15)); + background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15)); + + /* Add a drop shadow */ + -webkit-box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.5); + -moz-box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.5); + box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.5); + + /* Set the font */ + font: 700 13px "Helvetica Neue", Helvetica, Arial, sans-serif; + + z-index: 9999; + pointer-events: auto; +} + +.github-fork-ribbon a, +.github-fork-ribbon a:hover { + /* Set the text properties */ + color: #fff; + text-decoration: none; + text-shadow: 0 -1px rgba(0, 0, 0, 0.5); + text-align: center; + + /* Set the geometry. If you fiddle with these you'll also need + to tweak the top and right values in .github-fork-ribbon. */ + width: 200px; + line-height: 20px; + + /* Set the layout properties */ + display: inline-block; + padding: 2px 0; + + /* Add "stitching" effect */ + border-width: 1px 0; + border-style: dotted; + border-color: #fff; + border-color: rgba(255, 255, 255, 0.7); +} + +.github-fork-ribbon-wrapper { + width: 150px; + height: 150px; + position: fixed; + overflow: hidden; + top: 0; + z-index: 9999; + pointer-events: none; +} + +.github-fork-ribbon-wrapper.fixed { + position: fixed; +} + +.github-fork-ribbon-wrapper.right { + right: 0; +} +.github-fork-ribbon-wrapper.right .github-fork-ribbon { + top: 42px; + right: -43px; + + -webkit-transform: rotate(45deg); + -moz-transform: rotate(45deg); + -ms-transform: rotate(45deg); + -o-transform: rotate(45deg); + transform: rotate(45deg); +} \ No newline at end of file diff --git a/polaromatic-back/src/main/resources/static/images/polaromatic-logo.png b/polaromatic-back/src/main/resources/static/images/polaromatic-logo.png new file mode 100644 index 0000000..7046b23 Binary files /dev/null and b/polaromatic-back/src/main/resources/static/images/polaromatic-logo.png differ diff --git a/polaromatic-back/src/main/resources/static/js/Connection.js b/polaromatic-back/src/main/resources/static/js/Connection.js new file mode 100644 index 0000000..6738b2e --- /dev/null +++ b/polaromatic-back/src/main/resources/static/js/Connection.js @@ -0,0 +1,30 @@ +(function(){function isConstructor(t){return t[0]==t[0].toUpperCase()}function applyBaseClassFunctions(t){t.asType=gs.baseClass.asType}function isObjectProperty(t){return["clazz","gSdefaultValue","leftShift","minus","plus","equals","toString","clone","withz","getProperties","getStatic","getClass","getMetaClass","getMethods","invokeMethod","constructor","asType","withTraits"].indexOf(t)>=0}function expandWithMetaClass(t,e){if(globalMetaClass&&globalMetaClass[e]){var r,n=globalMetaClass[e];for(r in n){var i=n.getStatic();if(i){var s;for(s in i)"gSparent"!=s&&(t[r]=i[s])}t[r]=n[r]}}return t}function createClassNames(t,e){var r,n,i=e.length;for(r=0;i>r;r++)0===r&&(n={},t.clazz=n),n.name=e[r],n.simpleName=getSimpleName(e[r]),i>r&&(n.superclass={},n=n.superclass)}function getSimpleName(t){for(var e=t.indexOf(".");e>=0;)t=t.substring(e+1),e=t.indexOf(".");return t}function isMapProperty(t){return isObjectProperty(t)||["any","collect","collectEntries","collectMany","countBy","dropWhile","each","eachWithIndex","every","find","findAll","findResult","findResults","get","getAt","groupBy","inject","intersect","max","min","putAll","putAt","reverseEach","clear","sort","spread","subMap","add","take","takeWhile","withDefault","count","drop","keySet","put","size","isEmpty","remove","containsKey","containsValue","values"].indexOf(t)>=0}function GsGroovyMap(){this.clazz={name:"java.util.LinkedHashMap",simpleName:"LinkedHashMap",superclass:{name:"java.util.HashMap",simpleName:"HashMap"}},this.add=function(t,e){if("spreadMap"==t){var r;for(r in e)isMapProperty(r)||(this[r]=e[r])}else this[t]=e;return this},this.put=function(t,e){return this.add(t,e)},this.leftShift=function(t,e){return 1==arguments.length?this.plus(arguments[0]):this.add(t,e)},this.putAt=function(t,e){this.put(t,e)},this.size=function(){var t,e=0;for(t in this)isMapProperty(t)||e++;return e},this.isEmpty=function(){return 0===this.size()},this.remove=function(t){this[t]&&delete this[t]},this.each=function(t){var e;for(e in this)if(!isMapProperty(e)){var r=arguments[0];1==r.length&&t({key:e,value:this[e]}),2==r.length&&t(e,this[e])}},this.count=function(t){var e,r=0;for(e in this)isMapProperty(e)||(1==t.length&&t({key:e,value:this[e]})&&r++,2==t.length&&t(e,this[e])&&r++);return r},this.any=function(t){var e;for(e in this)if(!isMapProperty(e)){var r=arguments[0];if(1==r.length&&t({key:e,value:this[e]}))return!0;if(2==r.length&&t(e,this[e]))return!0}return!1},this.every=function(t){var e;for(e in this)if(!isMapProperty(e)){var r=arguments[0];if(1==r.length&&!t({key:e,value:this[e]}))return!1;if(2==r.length&&!t(e,this[e]))return!1}return!0},this.find=function(t){var e;for(e in this)if(!isMapProperty(e)){var r=arguments[0];if(1==r.length){var n={key:e,value:this[e]};if(t(n))return n}if(2==r.length&&t(e,this[e]))return{key:e,value:this[e]}}return null},this.dropWhile=function(t){var e,r=gs.map();for(e in this)if(!isMapProperty(e)){var n={key:e,value:this[e]},i=arguments[0];1==i.length&&(t(n)||r.add(n.key,n.value)),2==i.length&&(t(n.key,n.value)||r.add(n.key,n.value))}return r},this.drop=function(t){var e,r=gs.map(),n=0;for(e in this)isMapProperty(e)||(n++,n>t&&r.add(e,this[e]));return r},this.findAll=function(t){var e,r=gs.map();for(e in this)if(!isMapProperty(e)){var n=arguments[0];if(1==n.length){var i={key:e,value:this[e]};t(i)&&r.add(i.key,i.value)}2==n.length&&t(e,this[e])&&r.add(e,this[e])}return r.size()>0?r:null},this.collect=function(t){var e,r=gs.list([]);for(e in this)if(!isMapProperty(e)){var n=arguments[0];1==n.length&&r.add(t({key:e,value:this[e]})),2==n.length&&r.add(t(e,this[e]))}return r.size()>0?r:null},this.containsKey=function(t){return void 0===this[t]||null===this[t]?!1:!0},this.containsValue=function(t){var e,r=!1;for(e in this)if(!isMapProperty(e)&&gs.equals(this[e],t)){r=!0;break}return r},this.get=function(t,e){return this.containsKey(t)||(this[t]=e),this[t]},this.toString=function(){var t="";return this.each(function(e,r){t=t+e+": "+r+" ,"}),"["+t+"]"},this.equals=function(t){var e,r=!0;for(e in this)isMapProperty(e)||gs.equals(this[e],t[e])||(r=!1);return r},this.keySet=function(){var t,e=gs.list([]);for(t in this)isMapProperty(t)||e.add(t);return e},this.values=function(){var t,e=gs.list([]);for(t in this)isMapProperty(t)||e.add(this[t]);return e},this.gSdefaultValue=null,this.withDefault=function(t){return this.gSdefaultValue=t,this},this.inject=function(t,e){var r;for(r in this)if(!isMapProperty(r)){if(2==e.length){var n={key:r,value:this[r]};t=e(t,n)}3==e.length&&(t=e(t,r,this[r]))}return t},this.putAll=function(t){if(t instanceof Array){var e;for(e=0;e1&&""===e[e.length-1]&&e.splice(e.length-1,1),e}function StaticMethods(t){this.gSparent=t}function interceptClosureCall(t,e){return e instanceof Array&&t.length>1?t.apply(t,e):t(e)}function classInfoContainsName(t,e){return t.name==e||t.simpleName==e}function hasFunc(t,e){return null===t||void 0===t||void 0===t[e]||"function"!=typeof t[e]?!1:!0}function findPropertyInDelegates(t,e){for(var r,n=delegates.length,i=!1;n>0&&!i&&e;)n-=1,r=gs.gp(delegates[n],t,!0),void 0!==r&&(i=!0);return r}function exFn(t,e,r,n){return t[e].apply(r,joinParameters(r,n))}function delegatesFunc(t){var e=null;if(delegates.length>0){var r;for(r=delegates.length-1;r>=0&&!e;r--)delegates[r][t]&&(e=delegates[r])}return e}function joinParameters(t,e){var r,n=[t];for(r=0;r=0&&null===r;e--){var n=categories[e];n[t]&&(r=n)}return r}function existAnnotatedCategory(t){return null!==annotatedCategories[t]&&void 0!==annotatedCategories[t]}function mixinSearching(t,e){var r=null,n=null;if("string"==typeof t&&(n="String"),t.clazz&&t.clazz.simpleName&&"object"==typeof t&&(n=t.clazz.simpleName),null!==n){var i,s=null;for(i=mixins.length-1;i>=0&&null===s;i--){var a=mixins[i];a.name==n&&(s=a.items)}if(null!==s)for(i=0;i=0&&null===i;r--){var s=mixinsObjects[r];s.item==t&&(i=s.items)}if(null!==i)for(r=0;r=0?(this.splice(e,1),!0):!1},e},gs.map=function(){var t=new GsGroovyMap;return expandWithMetaClass(t,"LinkedHashMap"),applyBaseClassFunctions(t),1==arguments.length&&arguments[0]instanceof Object&&gs.passMapToObject(arguments[0],t),t},Array.prototype.get=function(t){return 2==arguments.length&&(null===this[t]||void 0===this[t])?arguments[1]:this[t]},Array.prototype.getAt=function(t){return this[t]},Array.prototype.withz=function(t){return interceptClosureCall(t,this)},Array.prototype.size=function(){return this.length},Array.prototype.isEmpty=function(){return 0===this.length},Array.prototype.add=function(t,e){return void 0===e?this[this.length]=t:this[t]=e,this},Array.prototype.addAll=function(t){var e;if(1==arguments.length)if(t instanceof Array)for(e=0;e=0;e--)interceptClosureCall(t,this[e]);return this},Array.prototype.eachWithIndex=function(t,e){for(e=0;e=0&&(e=!0)),r>=0&&this.splice(r,1),e},Array.prototype.removeAll=function(t){if(t instanceof Array){var e=[];if(this.forEach(function(r,n){t.contains(r)&&e.push(n)}),e.length>0){var r=0,n=this;e.forEach(function(t){n.splice(t-r,1),r+=1})}}else if("function"==typeof t){var i;for(i=this.length-1;i>=0;i--)t(this[i])&&this.remove(i)}return this},Array.prototype.collect=function(t){var e,r=gs.list([]);for(e=0;e0&&this[0].plus){var r=this[0];for(t=0;t+1e)&&(e=this[t]);return e},Array.prototype.min=function(){var t,e=null;for(t=0;t0){var t,e="[";for(t=0;t0&&arguments[0]===!1&&(t=!1);var e,r=[],n=null;for(2==arguments.length&&"function"==typeof arguments[1]&&(n=arguments[1]),1==arguments.length&&"function"==typeof arguments[0]&&(n=arguments[0]),e=0;e0&&arguments[0]===!1&&(t=!1);var e,r=[];for(e=0;ee;t--){var r=this[e];this[e++]=this[t],this[t]=r}return this}var n=[];for(t=this.length-1;t>=0;t--)n[e++]=this[t];return gs.list(n)},Array.prototype.take=function(t){var e,r=[];for(e=0;t>e;e++)ee;e++){var n;for(n=0;n0){var r;for(r=0;r0){var i;for(i=0;i0&&gs.flatten(t,e):t.add(e)})},gs.range=function(t,e,r){var n=t,i=e,s="string"==typeof t;s&&(n=n.charCodeAt(0),i=i.charCodeAt(0));var a=!1;if(n>i){var o=n;n=i,i=o,a=!0,r||(n+=1)}else r||(i-=1);var u,l,h;for(u=[],l=n,h=0;i>=l;l++,h++)u[h]=s?String.fromCharCode(l):l;a&&(u=u.reverse());var f=gs.list(u);return f.toList=function(){return gs.list(this.values())},f},gs.date=function(){var t;return t=1==arguments.length?new Date(arguments[0]):new Date,createClassNames(t,["java.util.Date"]),t.withz=gs.baseClass.withz,t.time=t.getTime(),t.setTime=function(e){t.time=e},t.year=t.getFullYear(),t.month=t.getMonth(),t.date=t.getDay(),t.plus=function(e){return"number"==typeof e?gs.date(t.time+144e4*e):t+e},t.minus=function(e){return"number"==typeof e?gs.date(t.time-144e4*e):t-e},t.format=function(e){var r="";return e&&(r=e,r=r.replaceAll("yyyy",t.getFullYear()),r=r.replaceAll("MM",fillZerosLeft(t.getMonth()+1,2)),r=r.replaceAll("dd",fillZerosLeft(t.getUTCDate(),2)),r=r.replaceAll("HH",fillZerosLeft(t.getHours(),2)),r=r.replaceAll("mm",fillZerosLeft(t.getMinutes(),2)),r=r.replaceAll("ss",fillZerosLeft(t.getSeconds(),2)),r=r.replaceAll("yy",lastChars(t.getFullYear(),2))),r},t.parse=function(e,r){var n=e.indexOf("MM");if(n>=0)for(var i=r.substr(n,2)-1;t.getMonth()!=i;)t.setMonth(i,t.getUTCDate());if(n=e.indexOf("dd"),n>=0)for(var s=r.substr(n,2);t.getUTCDate()!=s;)t.setUTCDate(s);return n=e.indexOf("yyyy"),n>=0?t.setFullYear(r.substr(n,4)):(n=e.indexOf("yy"),n>=0&&t.setFullYear(r.substr(n,2))),n=e.indexOf("HH"),n>=0&&t.setHours(r.substr(n,2)),n=e.indexOf("mm"),n>=0&&t.setMinutes(r.substr(n,2)),n=e.indexOf("ss"),n>=0&&t.setSeconds(r.substr(n,2)),t},t.clearTime=function(){return t.setHours(0,0,0,0),t},t.equals=function(e){return t.time==e.time},t.before=function(e){return t.timee.time},t},gs.rangeFromList=function(t,e,r){return t.slice(e,r+1)},gs.exactMatch=function(t,e){var r=t;return r=e instanceof RegExp?r.replace(e,"#"):r.replace(new RegExp(e),"#"),"#"==r},gs.match=function(t,e){var r;return e instanceof RegExp&&(r=t.search(e)),r>=0},gs.regExp=function(t,e){var r;r=e instanceof RegExp?new RegExp(e.source,"g"):new RegExp(e,"g");var n,i=r.exec(t);if(null===i||void 0===i)return null;for(var s=gs.list([]),a=0;i;)s[a]=i instanceof Array&&i.length<2?i[0]:gs.list(i),a+=1,i=r.exec(t);return n=gs.inherit(s,"RegExp"),createClassNames(n,["java.util.regex.Matcher"]),n.pattern=r,n.text=t,n.replaceFirst=function(t){return this.text.replaceFirst(this[0],t)},n.replaceAll=function(t){return this.text.replaceAll(this.pattern,t)},n.reset=function(){return this},n},gs.pattern=function(t){var e=gs.inherit(gs.baseClass,"Pattern");return createClassNames(e,["java.util.regex.Pattern"]),e.value=t,e},gs.matcher=function(t,e){var r=gs.inherit(gs.baseClass,"Matcher");return createClassNames(r,["java.util.regex.Matcher"]),r.data=t,r.regExp=e,r.matches=function(){return gs.exactMatch(this.data,this.regExp)},r},RegExp.prototype.matcher=function(t){return gs.matcher(t,this)},Number.prototype.times=function(t){var e;for(e=0;this>e;e++)t(e)},Number.prototype.upto=function(t,e){var r;for(r=this.value;t>=r;r++)e(r)},Number.prototype.step=function(t,e,r){var n;for(n=this.value;t>n;)r(n),n+=e},Number.prototype.multiply=function(t){return this*t},Number.prototype.power=function(t){return Math.pow(this,t)},Number.prototype.byteValue=Number.prototype.doubleValue=Number.prototype.shortValue=Number.prototype.floatValue=Number.prototype.longValue=function(){return this},Number.prototype.intValue=function(){return Math.floor(this)},String.prototype.contains=function(t){return this.indexOf(t)>=0},String.prototype.startsWith=function(t){return 0===this.indexOf(t)},String.prototype.endsWith=function(t){return this.indexOf(t)==this.length-t.length},String.prototype.count=function(t){var e=new RegExp(t,"g"),r=this.match(e);return r?r.length:0},String.prototype.size=function(){return this.length},String.prototype.replaceAll=function(t,e){var r;return r=t instanceof RegExp?new RegExp(t.source,"g"):new RegExp(t,"g"),this.replace(r,e)},String.prototype.replaceFirst=function(t,e){return this.replace(t,e)},String.prototype.reverse=function(){return this.split("").reverse().join("")},String.prototype.tokenize=function(){var t=" ";1==arguments.length&&arguments[0]&&(t=arguments[0]);var e=this.split(t);return gs.list(e)},String.prototype.multiply=function(t){if("number"==typeof t){var e,r="";for(e=0;(0|t)>e;e++)r+=this;return r}},String.prototype.capitalize=function(){return this.charAt(0).toUpperCase()+this.slice(1)},String.prototype.each=function(t){var e=gs.list(this.split(""));e.each(t)},String.prototype.inject=function(t,e){var r=gs.list(this.split(""));return r.inject(t,e)},String.prototype.eachLine=function(t){var e,r=getItemsMultiline(this);for(e=0;e=0;)name=name.substring(pos+1),pos=name.indexOf(".");result=eval(name)}catch(err){result=obj}return result},gs.metaClass=function(t){var e=typeof t;return"string"==e?t=new String(t):"number"==e?t=new Number(t):"function"===e&&(globalMetaClass[t.name]||(globalMetaClass[t.name]={gSstatic:new StaticMethods(t),getStatic:function(){return this.gSstatic}}),t=globalMetaClass[t.name]),t},gs.passMapToObject=function(t,e){var r;for(r in t)"function"!=typeof t[r]&&(isMapProperty(r)||gs.sp(e,r,t[r]))},gs.equals=function(t,e){return hasFunc(t,"equals")?t.equals(e):hasFunc(e,"equals")?e.equals(t):t==e},gs.is=function(t,e){if(null!==t&&hasFunc(t,"is")){var r,n=gs.list([e]);for(r=2;rt},t},gs.bool=function(t){if(t&&void 0!==t.isEmpty)return!t.isEmpty();if(t){if(t.asBoolean)return t.asBoolean();if("number"==typeof t&&0===t)return!1;if("string"==typeof t)return""!==t}return t},gs.less=function(t,e){return e>t},gs.greater=function(t,e){return t>e},gs.spaceShip=function(t,e){return gs.equals(t,e)?0:gs.less(t,e)?-1:gs.greater(t,e)?1:void 0},gs.instanceOf=function(t,e){var r=!1;if("String"==e)return"string"==typeof t;if("Number"==e)return"number"==typeof t;if(t.clazz){var n;for(n=t.clazz;n&&!r;)classInfoContainsName(n,e)?r=!0:n=n.superclass;if(!r&&t.clazz.interfaces){var i;for(i=0;it+e?(1e3*t+1e3*e)/1e3:t+e},gs.minus=function(t,e){return hasFunc(t,"minus")?t.minus(e):t-e},gs.gSin=function(t,e){return e&&"function"==typeof e.contains?e.contains(t):!1},gs.thisOrObject=function(t,e){return void 0===t.withz&&e?e:t},gs.spread=function(t){t&&t instanceof Array&&(this.values=t)},gs.sp=function(t,e,r){if("setProperty"==e)t[e]=r;else if("getProperty"==e)t[e]=r;else if(null!==t&&t instanceof StaticMethods)t[e]=r,t.gSparent[e]=r;else if(t.setProperty)t.setProperty(e,r);else{var n="set"+e.charAt(0).toUpperCase()+e.slice(1);t[n]?t[n](r):void 0===t[e]&&void 0!==t.setPropertyMissing&&"function"==typeof t.setPropertyMissing?t.setPropertyMissing(e,r):t[e]=r}},gs.gp=function(t,e,r){if(3==arguments.length){if(null===t||void 0===t)return null}else if(null==t||void 0===t)throw"gs.gp Get property: "+e+" on null or undefined object.";if(t.getProperty)return t.getProperty(e);var n="get"+e.charAt(0).toUpperCase()+e.slice(1);if(t[n])return t[n]();if("size"==e&&"function"==typeof t[e])return t[e]();if(void 0!==t[e])return t[e];if(void 0!==t.clazz){var i=mapAddDelegate[t.clazz.simpleName];if(null!==i&&void 0!==i){var s;for(s=0;s0&&void 0===t[e]){var u=categorySearching(n);if(null!==u)return u[n].apply(t,[t])}return void 0!==t.propertyMissing&&"function"==typeof t.propertyMissing?t.propertyMissing(e):!r&&delegates.length>0?findPropertyInDelegates(e,t):t[e]},gs.plusPlus=function(t,e,r,n){var i=gs.gp(t,e),s=i;return r?(gs.sp(t,e,i+1),s++):(gs.sp(t,e,i-1),s--),n?s:i},gs.mc=function(item,methodName,values,objectVar){if(gs.consoleInfo&&console&&console.log("[INFO] gs.mc ("+item+")."+methodName+" params:"+values),null===item||void 0===item)throw"gs.mc Calling method: "+methodName+" on null or undefined object.";if("split"==methodName&&"string"==typeof item)return item.tokenize(values[0]);if("length"==methodName&&"string"==typeof item)return item.length;if("join"==methodName&&item instanceof Array)return values.size()>0?item.gSjoin(values[0]):item.gSjoin();if(objectVar)try{return gs.mc(objectVar,methodName,values)}catch(e){}if(item[methodName]){var f=item[methodName];return f.apply?f.apply(item,values):gs.execCall(f,item,values)}if(methodName.startsWith("get")||methodName.startsWith("set")){var varName=methodName.charAt(3).toLowerCase()+methodName.slice(4);if(void 0!==item[varName]&&!hasFunc(item,varName))return methodName.startsWith("get")?gs.gp(item,varName):gs.sp(item,varName,values[0])}if(methodName.startsWith("is")){var varName=methodName.charAt(2).toLowerCase()+methodName.slice(3);if(void 0!==item[varName]&&!hasFunc(item,varName))return gs.gp(item,varName)}if("newInstance"==methodName)return item();var whereExecutes;if(categories.length>0&&(whereExecutes=categorySearching(methodName),null!==whereExecutes))return exFn(whereExecutes,methodName,item,values);var ob;for(ob in annotatedCategories)if(annotatedCategories[ob]==item.clazz.simpleName){var categoryItem=gs.myCategories[ob]();if(categoryItem[methodName]&&"function"==typeof categoryItem[methodName])return exFn(categoryItem,methodName,item,values)}if(mixins.length>0&&(whereExecutes=mixinSearching(item,methodName),null!==whereExecutes))return exFn(whereExecutes,methodName,item,values);if(mixinsObjects.length>0&&(whereExecutes=mixinObjectsSearching(item,methodName),null!==whereExecutes))return exFn(whereExecutes,methodName,item,values);if(void 0!==item.clazz){var addDelegate=mapAddDelegate[item.clazz.simpleName];if(addDelegate){var i;for(i=0;i0){var delegateFunc=delegatesFunc(methodName);if(delegateFunc)return delegateFunc[methodName].apply(item,values)}if(item.methodMissing)return item.methodMissing(methodName,values);if(delegates.length>0&&delegatesFunc("methodMissing"))return gs.mc(delegatesFunc("methodMissing"),methodName,values);if("function"==typeof eval(methodName))return eval(methodName).apply(this,values);throw"gs.mc Method "+methodName+" not exist in "+item},gs.categoryUse=function(t,e,r){var n,i;if(existAnnotatedCategory(t)){i=gs.myCategories[t]();for(n in i)isObjectProperty(n)||isConstructor(n,i[n])||"function"!=typeof i[n]||addFunctionToClassIfPrototyped(n,i[n],annotatedCategories[t])}else categories[categories.length]=e;if(r(),existAnnotatedCategory(t)){i=gs.myCategories[t]();for(n in i)isObjectProperty(n)||isConstructor(n,i[n])||"function"!=typeof i[n]||removeFunctionToClass(n,i[n],annotatedCategories[t])}else categories.splice(categories.length-1,1)};var annotatedCategories={};gs.addAnnotatedCategory=function(t,e){annotatedCategories[t]=e},gs.mixinClass=function(t,e){var r=!1;if(mixins.length>0){var n;for(n=0;n0){var n;for(n=0;n' + +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 "" + } + } + + body { + div(class: 'github-fork-ribbon-wrapper right') { + div(class: 'github-fork-ribbon') { + a(href: 'https://github.com/lmivan/contest', 'Fork me on GitHub') + } + } + + div(id: 'header') { + div(class: 'center') { + a(href: 'https://github.com/lmivan/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 "" +} \ No newline at end of file diff --git a/polaromatic-back/src/test/groovy/polaromatic/controller/PolaromaticControllerSpec.groovy b/polaromatic-back/src/test/groovy/polaromatic/controller/PolaromaticControllerSpec.groovy new file mode 100644 index 0000000..d74cedb --- /dev/null +++ b/polaromatic-back/src/test/groovy/polaromatic/controller/PolaromaticControllerSpec.groovy @@ -0,0 +1,39 @@ +package polaromatic.controller + +import org.springframework.mock.web.MockMultipartFile +import polaromatic.service.PhotoToPolaromatize +import spock.lang.Specification + +class PolaromaticControllerSpec extends Specification { + + def polaromatizeService = Mock(PhotoToPolaromatize) + + void 'upload a photo'() { + given: 'a photo to upload' + def file = new File('src/test/resources/photo.jpg') + def multipart = new MockMultipartFile('photo', file.bytes) + + and: 'the controller' + def polaromaticController = new PolaromaticController(polaromatizeService: polaromatizeService) + + when: 'uploading the file' + polaromaticController.handleFileUpload(multipart, "text") + + then: 'the file processed' + 1 * polaromatizeService.process(_) + } + + void 'try to upload an empty photo'() { + given: 'a photo to upload' + def multipart = new MockMultipartFile('photo', "".bytes) + + and: 'the controller' + def polaromaticController = new PolaromaticController(polaromatizeService: polaromatizeService) + + when: 'uploading the file' + polaromaticController.handleFileUpload(multipart, "text") + + then: 'the file is not processed' + 0 * polaromatizeService.process(_) + } +} \ No newline at end of file diff --git a/polaromatic-back/src/test/groovy/polaromatic/service/BrowserPushServiceSpec.groovy b/polaromatic-back/src/test/groovy/polaromatic/service/BrowserPushServiceSpec.groovy new file mode 100644 index 0000000..944522c --- /dev/null +++ b/polaromatic-back/src/test/groovy/polaromatic/service/BrowserPushServiceSpec.groovy @@ -0,0 +1,28 @@ +package polaromatic.service + +import org.springframework.messaging.simp.SimpMessagingTemplate +import polaromatic.domain.Photo +import spock.lang.Specification + +class BrowserPushServiceSpec extends Specification { + + void 'push a converted photo to the browser'() { + given: 'a photo' + def input = new File('src/test/resources/photo.jpg') + def output = File.createTempFile("output", "") + + def photo = new Photo(input: input.path, output: output.path) + + and: 'a mocked SimpMessagingTemplate' + def simpMessagingTemplate = Mock(SimpMessagingTemplate) + + and: 'the push service' + def browserPushService = new BrowserPushService(template: simpMessagingTemplate) + + when: 'pushing the photo to the browser' + browserPushService.pushToBrowser(photo) + + then: 'the photo is pushed' + 1 * simpMessagingTemplate.convertAndSend('/notifications/photo', "") + } +} \ No newline at end of file diff --git a/polaromatic-back/src/test/groovy/polaromatic/service/FileServiceSpec.groovy b/polaromatic-back/src/test/groovy/polaromatic/service/FileServiceSpec.groovy new file mode 100644 index 0000000..78b8b5d --- /dev/null +++ b/polaromatic-back/src/test/groovy/polaromatic/service/FileServiceSpec.groovy @@ -0,0 +1,55 @@ +package polaromatic.service + +import polaromatic.domain.Photo +import polaromatic.domain.PolaroidRequest +import spock.lang.Specification + +class FileServiceSpec extends Specification { + + def fileService = new FileService() + + void 'preprocess a polaroid request'() { + given: 'a polaroid request' + def pr = new PolaroidRequest(new File(filePath), text) + + when: 'preprocessing the file' + def photo = fileService.preprocessFile(pr) + + then: 'the photo object is created correctly' + photo.input.contains filePath + photo.output != null + photo.output.endsWith('.png') + photo.text == text + + where: + filePath = 'src/test/resources/photo.jpg' + text = 'Polaromatic #FTW' + } + + void 'preprocess a file without a text'() { + given: 'a file' + def file = new File('src/test/resources/photo.jpg') + + when: 'preprocessing the file' + def photo = fileService.preprocessFile(file) + + then: 'the photo object is created with an empty text' + photo.text == "" + } + + void 'delete photo temporary file'() { + given: 'an input and an output file' + def input = File.createTempFile("input", "") + def output = File.createTempFile("output", "") + + and: 'a photo object' + def photo = new Photo(input: input.path, output: output.path) + + when: 'cleaning the photo' + fileService.deleteTempFiles(photo) + + then: 'the files have been deleted' + !input.exists() + !output.exists() + } +} \ No newline at end of file diff --git a/polaromatic-back/src/test/groovy/polaromatic/service/ImageConverterServiceSpec.groovy b/polaromatic-back/src/test/groovy/polaromatic/service/ImageConverterServiceSpec.groovy new file mode 100644 index 0000000..1c49b89 --- /dev/null +++ b/polaromatic-back/src/test/groovy/polaromatic/service/ImageConverterServiceSpec.groovy @@ -0,0 +1,24 @@ +package polaromatic.service + +import polaromatic.domain.Photo +import spock.lang.Specification + +class ImageConverterServiceSpec extends Specification { + + def imageConverterService = new ImageConverterService() + + void 'apply polaroid effect to an image'() { + given: 'a photo' + def input = new File('src/test/resources/photo.jpg') + def output = File.createTempFile("output", "") + + def photo = new Photo(input: input.path, output: output.path) + + when: 'applying the efect' + imageConverterService.applyEffect(photo) + + then: 'the output file size is greater that zero' + def file = new File(photo.output) + file.size() > 0 + } +} diff --git a/polaromatic-back/src/test/groovy/polaromatic/service/MetricsServiceSpec.groovy b/polaromatic-back/src/test/groovy/polaromatic/service/MetricsServiceSpec.groovy new file mode 100644 index 0000000..5f16dc2 --- /dev/null +++ b/polaromatic-back/src/test/groovy/polaromatic/service/MetricsServiceSpec.groovy @@ -0,0 +1,30 @@ +package polaromatic.service + +import org.springframework.boot.actuate.metrics.CounterService +import polaromatic.domain.Photo +import spock.lang.Specification +import spock.lang.Unroll + +class MetricsServiceSpec extends Specification { + + def counterService = Mock(CounterService) + def metricsService = new MetricsService(counterService: counterService) + + @Unroll + void 'update the metrics for #type photo'() { + given: 'an Android photo' + def photo = new Photo(text: text) + + when: 'updating the metrics' + metricsService.updateMetrics(photo) + + then: 'the Android metrics are updated' + numExecutionsFlickr * counterService.increment(MetricsService.PHOTO_COUNTER_METRICS_FLICKR) + numExecutionsAndroid * counterService.increment(MetricsService.PHOTO_COUNTER_METRICS_ANDROID) + + where: + type | text | numExecutionsFlickr | numExecutionsAndroid + "a Flickr" | "" | 1 | 0 + "an Android" | "my text" | 0 | 1 + } +} \ No newline at end of file diff --git a/polaromatic-back/src/test/resources/photo.jpg b/polaromatic-back/src/test/resources/photo.jpg new file mode 100644 index 0000000..9fc982b Binary files /dev/null and b/polaromatic-back/src/test/resources/photo.jpg differ diff --git a/polaromatic-docs/build.gradle b/polaromatic-docs/build.gradle new file mode 100644 index 0000000..c5c0422 --- /dev/null +++ b/polaromatic-docs/build.gradle @@ -0,0 +1,16 @@ +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' +} diff --git a/polaromatic-docs/src/docs/android-app.ad b/polaromatic-docs/src/docs/android-app.ad new file mode 100644 index 0000000..0b41dac --- /dev/null +++ b/polaromatic-docs/src/docs/android-app.ad @@ -0,0 +1,17 @@ +== Android Application + +While I was developing the backend I thought that it would be great to be able to take a photo with the phone an +_Polaromatize it_. So I made the changes to the backend to be able to receive a photo and then I create an Android +application to send the photo. + +With this application you can take a picture with your phone, add the text you want and then share it with the +Polaromatic web. + +This is my first Android application and it has been fun to create it using Groovy. + +image::polaromatic-android.jpg[align=center, title=Sharing a photo from the Polaromatic Android app with the web] + +TIP: Please be sure to configure in the _Settings_ option the url in which you have deployed your Polaromatic server or +use the default demo environment. + +NOTE: The application is only a _share-app_ so it is not shown in the applications list after you install it. \ No newline at end of file diff --git a/polaromatic-docs/src/docs/images/groovy-logo.png b/polaromatic-docs/src/docs/images/groovy-logo.png new file mode 100644 index 0000000..da54c8f Binary files /dev/null and b/polaromatic-docs/src/docs/images/groovy-logo.png differ diff --git a/polaromatic-docs/src/docs/images/polaromatic-android.jpg b/polaromatic-docs/src/docs/images/polaromatic-android.jpg new file mode 100644 index 0000000..ff4ff7c Binary files /dev/null and b/polaromatic-docs/src/docs/images/polaromatic-android.jpg differ diff --git a/polaromatic-docs/src/docs/images/polaromatic-app.jpg b/polaromatic-docs/src/docs/images/polaromatic-app.jpg new file mode 100644 index 0000000..e4975f3 Binary files /dev/null and b/polaromatic-docs/src/docs/images/polaromatic-app.jpg differ diff --git a/polaromatic-docs/src/docs/images/polaromatic-flow.png b/polaromatic-docs/src/docs/images/polaromatic-flow.png new file mode 100644 index 0000000..6ae8dc6 Binary files /dev/null and b/polaromatic-docs/src/docs/images/polaromatic-flow.png differ diff --git a/polaromatic-docs/src/docs/images/polaromatic-logo.png b/polaromatic-docs/src/docs/images/polaromatic-logo.png new file mode 100644 index 0000000..60c3314 Binary files /dev/null and b/polaromatic-docs/src/docs/images/polaromatic-logo.png differ diff --git a/polaromatic-docs/src/docs/index.ad b/polaromatic-docs/src/docs/index.ad new file mode 100644 index 0000000..5a30298 --- /dev/null +++ b/polaromatic-docs/src/docs/index.ad @@ -0,0 +1,34 @@ += Polaromatic +by Iván López + +:numbered: +:imagesDir: images/ +:polaromaticBackResources: ../../../polaromatic-back/src/main/resources +:stem: + + +image::polaromatic-logo.png[align=center,link=https://github.com/lmivan/contest,width=150] + +include::intro.ad[] + +<<< + +include::live-demo.ad[] + +<<< + +include::spring-boot-app.ad[] + +<<< + +include::android-app.ad[] + +<<< + +include::installation.ad[] + +<<< + +include::tech-stack.ad[] + +<<< diff --git a/polaromatic-docs/src/docs/installation.ad b/polaromatic-docs/src/docs/installation.ad new file mode 100644 index 0000000..6902252 --- /dev/null +++ b/polaromatic-docs/src/docs/installation.ad @@ -0,0 +1,97 @@ +== Build and installation + +If you want to install Polaromatic in your own server, make some changes to the code, test some new things,... you need +to build it. + +First please clone the repository (and if you like the project please star-it on Github): + +[source,bash] +---- +git clone https://github.com/lmivan/contest.git +cd contest +---- + +=== Polaromatic + +Polaromatic back is a standard Spring Boot application so the only thing you need to do is execute it: + +[source,bash] +---- +./gradlew polaromatic-back:bootRun +---- + +Or if your prefer you can build it and create an executable jar file: + +[source,bash] +---- +./gradlew polaromatic-back:build +---- + +WARNING: You need to have _ImageMagick_ installed in your system to pass the tests and to apply the Polaroid effect +(see below). + +Or just build the jar file without execute the tests: + +[source,bash] +---- +./gradlew polaromatic-back:build -x test +---- + +=== FlickDownloader + +You need to install Spring Boot CLI. The easy way to do this is with http://gvmtool.net/[GVM^]. Once you have it +installed you can just run: + +[source,bash] +---- +cd polaromatic-back +spring run FlickrDownloader.groovy +---- + + +=== Android + +If this is the first time you develop for Android or if you haven't installed the Android SDK, the easy way to setup +everything is using http://developer.android.com/sdk/index.html[Android Studio^]. Download, install it and then import +the `polaromatic-grooid/build.gradle` file as a _Non-Android Studio project_. This will download the necessary Sdk +files and Android build tools. + +Once you have everything installed you can just set your android home and then build the .apk file (or just install it +in your phone directly using Android Studio). + +[source,bash] +---- +# Set your own path +export ANDROID_HOME=/home/ivan/Android/Sdk/ + +./gradlew polaromatic-grooid:build +---- + +=== Documentation + +This documentation is created using http://asciidoctor.org/[AsciiDoctor^]. To build the documentation just execute: + +[source,bash] +---- +./gradlew polaromatic-docs:asciidoctor +---- + +The documentation is available in `polaromatic-docs/build/docs/html5/index.html` + + +=== Dependencies + +For the image conversion you need to have _ImageMagick_ installed in your system. If you use Linux Mint/Debian/Ubuntu +you only need to execute: + +[source,bash] +---- +sudo apt-get install imagemagick +---- + + +=== Tips & Tricks + +If you are not interested in developing the Android application and do not want to download and configure Android SDK +you can exclude the project from the Gradle build. +Edit `setting.gradle` and remove (or comment) the project `polaromatic-grooid`. diff --git a/polaromatic-docs/src/docs/intro.ad b/polaromatic-docs/src/docs/intro.ad new file mode 100644 index 0000000..4607a60 --- /dev/null +++ b/polaromatic-docs/src/docs/intro.ad @@ -0,0 +1,36 @@ +== Intro + +*Polaromatic* is a web application to show a photo wall in which photos appear automatically in the browser after +applying a cool _Polaroid_ effect. + +This application has been created for the +http://blog.greglturnquist.com/2014/12/announcing-learningspringboot-contest-cc-packtpub-springcentral.html[#LearningSpringBoot^] +contest by https://twitter.com/gregturn[Greg Turnquist^] and *Polaromatic* source code is available at my Github +account: https://github.com/lmivan/contest[https://github.com/lmivan/contest^]. + +TIP: If you like the application please star-it on Github :-) + +image::polaromatic-app.jpg[align=center, title=Polaromatic with some photos Polaromatized] + + +=== Application flow + +There are two different ways to send a photo to the wall: + +- The first one is to copy some photo files (*.jpg, *.png,...) in the _work_ directory in the same level as the +application. +- The second one is to send a photo using the Android _Share image with Polaromatic_ Application. If you use this +method you can also customize the text in the Polaroid. + +This flow is explained in detail in the following sections of the documentation. + +image::polaromatic-flow.png[align=center, title=Application flow] + +. From a browser you connect to _Polaromatic_ and just wait. +. Then, if you just copy some photos into the work directory, they are processed by the Spring Integration flow +inside the _Polaromatic_ application, converted to a cool Polaroid photo with a text and finally pushed to the +browser using Websockets. +. A _Flickr Interesing_ photos downloader as been created as a _Spring Boot CLI Application_ to download automatically +photos every 30 seconds. +. The other way to publish photos is using the _Polaromatic Android Application_ and share a photo using the application. + diff --git a/polaromatic-docs/src/docs/live-demo.ad b/polaromatic-docs/src/docs/live-demo.ad new file mode 100644 index 0000000..7a666e1 --- /dev/null +++ b/polaromatic-docs/src/docs/live-demo.ad @@ -0,0 +1,7 @@ +== Live Demo + +The application is deployed at http://polaromatic.noip.me/[http://polaromatic.noip.me/^] and every 30 seconds +new Flickr photos will appear automatically, so you only have to wait and enjoy :-) + +You can also download the Android application and publish some photos from your mobile phone: +https://www.dropbox.com/s/xholnkj5oz5tgx8/polaromatic-grooid.apk?dl=1 \ No newline at end of file diff --git a/polaromatic-docs/src/docs/spring-boot-app.ad b/polaromatic-docs/src/docs/spring-boot-app.ad new file mode 100644 index 0000000..9b4e3ba --- /dev/null +++ b/polaromatic-docs/src/docs/spring-boot-app.ad @@ -0,0 +1,56 @@ +== Backend + +=== Spring Boot Application + +The central part of the application is the *Polaromatic* App built with _Spring Boot_. We also use _Spring Integration_ +to decouple the application from all the unnecessary boilerplate code needed to read files from the file system and to +define the flow of the information in the system. +From our application's point of view we define a POGO that is going to be called by the _infrastructure_ +(Spring Integration) and we are just going to receive a `File`. + +To achieve this we use a `File Inbound Channel Adapter` from Spring Integration that is going to monitor a directory +(`work`) and for every file copied to that directory it's going to send a message to the channel `incommingFilesChannel` +in which we define an _integration flow_. + +[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 + + +==== Custom metrics + +Another cool thing about Spring Boot is that you can define you own custom metrics inside your application. As you can +see in the previous application flow, the step 5 is to update the metrics. We count the number of photos processed from +Flickr and from the Android application and expose this numbers in the `/metrics` endpoint. + +You can check it using the demo environment: http://polaromatic.noip.me/metrics[http://polaromatic.noip.me/metrics^] + +[source,json,indent=0] +---- +{ + ... + counter.polaromatized.photos.flickr: 152, + counter.polaromatized.photos.android: 42, + ... +} +---- + + +=== Spring Boot CLI + +We need to feed the system with nice photos to apply the _Polaroid_ effect to them. We've chosen +https://www.flickr.com/explore/interesting/7days[Flickr Interesing page^] to get the photos. + +*FlickrDownloader* is a simple _Spring Boot CLI_ application that downloads the photos from the page every 30 seconds +and copy them to the `work` directory to be processed by *Polaromatic*. + +We use http://jsoup.org/[jsoup^] to parse the page and extract the links of the photos and +http://gpars.codehaus.org/[GPars^] to download all of them concurrently. \ No newline at end of file diff --git a/polaromatic-docs/src/docs/tech-stack.ad b/polaromatic-docs/src/docs/tech-stack.ad new file mode 100644 index 0000000..c12b7fb --- /dev/null +++ b/polaromatic-docs/src/docs/tech-stack.ad @@ -0,0 +1,20 @@ +== Technological stack + +The technological stack of the *Polaromatic* applications has one thing in common: http://groovy-lang.org/[Groovy^]. + +image::groovy-logo.png[align=center,link=http://groovy-lang.org/,width=300] + +I'm a very big fan of Groovy so I used it for the whole stack :-) + +- *Polaromatic*: All the Spring Boot code is Groovy. +- *FlickrDownloader:* A Spring Boot CLI Application can only be written with Groovy. +- *Android app*: This is my first Android app, so the best way to built it is using Groovy now that it's finally +possible. +- *Tests*: I use http://www.spockframework.org[Spock^] for the Polaromatic tests. +- *HTML*: The html of Polaromatic web is built with the new +http://beta.groovy-lang.org/docs/latest/html/documentation/markup-template-engine.html[MarkupTemplateEngine^] +introduced in Groovy 2.3. +- *Javascript*: All the javascript is written in Groovy. Then the Groovy code is converted to Javascript using the +amazing http://grooscript.org/[Grooscript^] project. +- *Build Tool*: I've created a http://www.gradle.org/[Gradle^] multi-project to build the different projects: back, +grooid and documentation. diff --git a/polaromatic-grooid/build.gradle b/polaromatic-grooid/build.gradle new file mode 100644 index 0000000..6257eb2 --- /dev/null +++ b/polaromatic-grooid/build.gradle @@ -0,0 +1,67 @@ +buildscript { + dependencies { + classpath 'com.android.tools.build:gradle:1.0.0' + classpath 'me.champeau.gradle:gradle-groovy-android-plugin:0.3.4' + } +} + +apply plugin: 'com.android.application' +apply plugin: 'me.champeau.gradle.groovy-android' + +android { + + compileSdkVersion 21 + buildToolsVersion "21.1.2" + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 21 + versionCode 1 + versionName "1.0" + } + + lintOptions { + abortOnError false + } + + buildTypes { + debug { + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } + +} + +repositories { + jcenter() + maven { url = 'https://oss.jfrog.org/oss-snapshot-local/' } + maven { url = 'http://repository.codehaus.org' } +} + +dependencies { + + // Android tools + compile 'com.android.support:support-v4:21.0.3' + // Groovy version + compile 'org.codehaus.groovy:groovy:2.4.0-rc-1:grooid' + // Groovy Json handling + compile ('org.codehaus.groovy:groovy-json:2.4.0-rc-1') { + transitive = false + } + + // Rest handling + compile 'com.squareup.retrofit:retrofit:1.8.0' + // Getting rid of boiler plate code + compile 'com.arasthel:swissknife:1.1.4' + // Imaging + compile 'com.squareup.picasso:picasso:2.4.0' + // Tasks + compile 'com.parse.bolts:bolts-android:1.1.4' + // Event bus + compile 'de.greenrobot:eventbus:2.4.0' +} diff --git a/polaromatic-grooid/gradle/wrapper/gradle-wrapper.jar b/polaromatic-grooid/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..3d0dee6 Binary files /dev/null and b/polaromatic-grooid/gradle/wrapper/gradle-wrapper.jar differ diff --git a/polaromatic-grooid/gradle/wrapper/gradle-wrapper.properties b/polaromatic-grooid/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..cdb23cd --- /dev/null +++ b/polaromatic-grooid/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Dec 13 17:43:34 GMT 2014 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip diff --git a/polaromatic-grooid/gradlew b/polaromatic-grooid/gradlew new file mode 100755 index 0000000..91a7e26 --- /dev/null +++ b/polaromatic-grooid/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/polaromatic-grooid/gradlew.bat b/polaromatic-grooid/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/polaromatic-grooid/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/polaromatic-grooid/proguard-rules.txt b/polaromatic-grooid/proguard-rules.txt new file mode 100644 index 0000000..580758c --- /dev/null +++ b/polaromatic-grooid/proguard-rules.txt @@ -0,0 +1,30 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /home/cchampeau/DEV/ANDROID/android/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +-dontobfuscate +-keep class org.codehaus.groovy.vmplugin.** +-keep class org.codehaus.groovy.runtime.dgm* +-keepclassmembers class org.codehaus.groovy.runtime.dgm* { + *; +} +-keepclassmembers class ** implements org.codehaus.groovy.runtime.GeneratedClosure { + *; +} +-dontwarn org.codehaus.groovy.** +-dontwarn groovy** +-dontwarn com.squareup.okhttp.** \ No newline at end of file diff --git a/polaromatic-grooid/src/main/AndroidManifest.xml b/polaromatic-grooid/src/main/AndroidManifest.xml new file mode 100644 index 0000000..72d4110 --- /dev/null +++ b/polaromatic-grooid/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + diff --git a/polaromatic-grooid/src/main/groovy/polaromatic/app/activity/SettingsActivity.groovy b/polaromatic-grooid/src/main/groovy/polaromatic/app/activity/SettingsActivity.groovy new file mode 100644 index 0000000..9896db9 --- /dev/null +++ b/polaromatic-grooid/src/main/groovy/polaromatic/app/activity/SettingsActivity.groovy @@ -0,0 +1,55 @@ +package polaromatic.app.activity + +import android.app.Activity +import android.content.SharedPreferences +import android.os.Bundle +import android.widget.EditText +import com.arasthel.swissknife.SwissKnife +import com.arasthel.swissknife.annotations.InjectView +import com.arasthel.swissknife.annotations.OnClick +import groovy.transform.CompileStatic +import polaromatic.app.R +import polaromatic.app.util.Toastable + +@CompileStatic +class SettingsActivity extends Activity implements Toastable { + + static final String PREFS_NAME = "settings_pref" + static final String BACKEND_URL_KEY = "backend_url" + static final String DEFAULT_BACKEND_URL = "http://polaromatic.noip.me" + + @InjectView(R.id.backendUrl) + EditText backendUrlEditText + + @Override + void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_settings) + SwissKnife.inject(this) + + SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0) + String backendUrl = settings.getString(BACKEND_URL_KEY, "") + + if (!backendUrl) { + storeBackendUrl(DEFAULT_BACKEND_URL) + } + + backendUrlEditText.text = backendUrl ?: DEFAULT_BACKEND_URL + } + + @OnClick(R.id.settings_save_button) + void saveSettings() { + storeBackendUrl(backendUrlEditText.text.toString()) + + showToastMessage(getString(R.string.settings_saved_ok)) + finish() + } + + private void storeBackendUrl(String backendUrl) { + SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0) + SharedPreferences.Editor editor = settings.edit() + + editor.putString(BACKEND_URL_KEY, backendUrl) + editor.commit() + } +} \ No newline at end of file diff --git a/polaromatic-grooid/src/main/groovy/polaromatic/app/activity/ShareActivity.groovy b/polaromatic-grooid/src/main/groovy/polaromatic/app/activity/ShareActivity.groovy new file mode 100644 index 0000000..ec5d3eb --- /dev/null +++ b/polaromatic-grooid/src/main/groovy/polaromatic/app/activity/ShareActivity.groovy @@ -0,0 +1,150 @@ +package polaromatic.app.activity + +import android.app.Activity +import android.app.AlertDialog +import android.app.ProgressDialog +import android.content.Intent +import android.content.SharedPreferences +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.net.Uri +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.widget.EditText +import android.widget.ImageView +import bolts.Task +import com.arasthel.swissknife.SwissKnife +import com.arasthel.swissknife.annotations.InjectView +import com.arasthel.swissknife.annotations.OnClick +import com.arasthel.swissknife.annotations.OnUIThread +import com.squareup.picasso.Picasso +import de.greenrobot.event.EventBus +import groovy.transform.CompileStatic +import polaromatic.app.R +import polaromatic.app.service.PolaromaticRest +import polaromatic.app.service.RestServiceFactory +import polaromatic.app.service.events.BackendEvent +import polaromatic.app.util.Toastable +import retrofit.mime.TypedFile +import retrofit.mime.TypedString + +@CompileStatic +public class ShareActivity extends Activity implements Toastable { + + private static final int IMAGE_DIMENSION = 1000 + + @InjectView(R.id.share_image) + ImageView shareImageView + + @InjectView(R.id.share_text) + EditText shareEditText + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_share) + SwissKnife.inject(this) + EventBus.default.register(this) + + Intent intent = intent + String action = intent.action + String type = intent.type + + if (Intent.ACTION_SEND == action && type?.startsWith("image/")) { + Uri imageUri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM) + + if (imageUri) { + Picasso.with(applicationContext) + .load(imageUri) + .resize(IMAGE_DIMENSION, IMAGE_DIMENSION) + .centerInside() + .into(shareImageView) + } + } + } + + @Override + boolean onCreateOptionsMenu(Menu menu) { + menuInflater.inflate(R.menu.menu_share, menu) + return true + } + + @Override + boolean onOptionsItemSelected(MenuItem item) { + int id = item.itemId + + if (id == R.id.action_about) { + showAbout() + return true + } else if (id == R.id.action_settings) { + showSettings() + } + + return super.onOptionsItemSelected(item) + } + + @Override + protected void onDestroy() { + super.onDestroy() + EventBus.default.unregister(this) + } + + @OnClick(R.id.share_polaromatize_button) + void polaromatize() { + ProgressDialog dialog = ProgressDialog.show(this, "", getString(R.string.share_waiting_spinner)) + + Task.callInBackground { + String textToShare = shareEditText.text.toString() + File imageToShare = extractImage(shareImageView) + + TypedFile typedPhoto = new TypedFile("image/*", imageToShare) + TypedString typedText = new TypedString(textToShare) + + polaromaticRest.uploadPhoto(typedPhoto, typedText) + + imageToShare.delete() + showToastMessage(getString(R.string.share_ok_msg)) + dialog.dismiss() + finish() + } + } + + PolaromaticRest getPolaromaticRest() { + SharedPreferences preferences = getSharedPreferences(SettingsActivity.PREFS_NAME, 0) + RestServiceFactory.getService(preferences.getString(SettingsActivity.BACKEND_URL_KEY, ""), PolaromaticRest) + } + + private File extractImage(ImageView imageView) { + File tempFile = File.createTempFile("temp_", "") + FileOutputStream output = new FileOutputStream(tempFile) + + Bitmap bitmap = ((BitmapDrawable) imageView.drawable).bitmap + bitmap.compress(Bitmap.CompressFormat.JPEG, 95, output) + + output?.flush() + output?.close() + + return tempFile + } + + void onEventBackgroundThread(BackendEvent backendError) { + showToastMessage(getString(R.string.share_backend_error)) + finish() + } + + @OnUIThread + void showAbout() { + new AlertDialog.Builder(this) + .setIcon(R.drawable.polaromatic_logo) + .setTitle(R.string.app_name) + .setView(layoutInflater.inflate(R.layout.about, null, false)) + .create() + .show() + } + + @OnUIThread + void showSettings() { + startActivity(new Intent(applicationContext, SettingsActivity)) + } +} \ No newline at end of file diff --git a/polaromatic-grooid/src/main/groovy/polaromatic/app/service/PolaromaticRest.groovy b/polaromatic-grooid/src/main/groovy/polaromatic/app/service/PolaromaticRest.groovy new file mode 100644 index 0000000..6bc24e0 --- /dev/null +++ b/polaromatic-grooid/src/main/groovy/polaromatic/app/service/PolaromaticRest.groovy @@ -0,0 +1,14 @@ +package polaromatic.app.service + +import retrofit.http.Multipart +import retrofit.http.POST +import retrofit.http.Part +import retrofit.mime.TypedFile +import retrofit.mime.TypedString + +interface PolaromaticRest { + + @Multipart + @POST("/upload-photo") + Map uploadPhoto(@Part("photo") TypedFile photo, @Part("text") TypedString text) +} diff --git a/polaromatic-grooid/src/main/groovy/polaromatic/app/service/RestServiceFactory.groovy b/polaromatic-grooid/src/main/groovy/polaromatic/app/service/RestServiceFactory.groovy new file mode 100644 index 0000000..8ec986c --- /dev/null +++ b/polaromatic-grooid/src/main/groovy/polaromatic/app/service/RestServiceFactory.groovy @@ -0,0 +1,24 @@ +package polaromatic.app.service + +import de.greenrobot.event.EventBus +import polaromatic.app.activity.SettingsActivity +import polaromatic.app.service.events.BackendEvent +import retrofit.ErrorHandler +import retrofit.RestAdapter +import retrofit.RetrofitError + +class RestServiceFactory { + + static T getService(String baseUrl, Class clazz) { + ErrorHandler eh = { RetrofitError cause -> + EventBus.default.post(new BackendEvent()) + cause.response + } + + return new RestAdapter.Builder() + .setEndpoint(baseUrl ?: SettingsActivity.DEFAULT_BACKEND_URL) + .setErrorHandler(eh) + .build() + .create(clazz) + } +} \ No newline at end of file diff --git a/polaromatic-grooid/src/main/groovy/polaromatic/app/service/events/BackendEvent.groovy b/polaromatic-grooid/src/main/groovy/polaromatic/app/service/events/BackendEvent.groovy new file mode 100644 index 0000000..cd9c017 --- /dev/null +++ b/polaromatic-grooid/src/main/groovy/polaromatic/app/service/events/BackendEvent.groovy @@ -0,0 +1,4 @@ +package polaromatic.app.service.events + +class BackendEvent { +} diff --git a/polaromatic-grooid/src/main/groovy/polaromatic/app/util/Toastable.groovy b/polaromatic-grooid/src/main/groovy/polaromatic/app/util/Toastable.groovy new file mode 100644 index 0000000..497618d --- /dev/null +++ b/polaromatic-grooid/src/main/groovy/polaromatic/app/util/Toastable.groovy @@ -0,0 +1,12 @@ +package polaromatic.app.util + +import android.widget.Toast +import com.arasthel.swissknife.annotations.OnUIThread + +trait Toastable { + @OnUIThread + void showToastMessage(String message) { + Toast toast = Toast.makeText(applicationContext, message, Toast.LENGTH_SHORT) + toast.show() + } +} diff --git a/polaromatic-grooid/src/main/polaromatic_logo-web.png b/polaromatic-grooid/src/main/polaromatic_logo-web.png new file mode 100644 index 0000000..ba2113f Binary files /dev/null and b/polaromatic-grooid/src/main/polaromatic_logo-web.png differ diff --git a/polaromatic-grooid/src/main/res/drawable-hdpi/polaromatic_logo.png b/polaromatic-grooid/src/main/res/drawable-hdpi/polaromatic_logo.png new file mode 100644 index 0000000..7f76e8d Binary files /dev/null and b/polaromatic-grooid/src/main/res/drawable-hdpi/polaromatic_logo.png differ diff --git a/polaromatic-grooid/src/main/res/drawable-mdpi/polaromatic_logo.png b/polaromatic-grooid/src/main/res/drawable-mdpi/polaromatic_logo.png new file mode 100644 index 0000000..77f78d2 Binary files /dev/null and b/polaromatic-grooid/src/main/res/drawable-mdpi/polaromatic_logo.png differ diff --git a/polaromatic-grooid/src/main/res/drawable-xhdpi/polaromatic_logo.png b/polaromatic-grooid/src/main/res/drawable-xhdpi/polaromatic_logo.png new file mode 100644 index 0000000..de464d9 Binary files /dev/null and b/polaromatic-grooid/src/main/res/drawable-xhdpi/polaromatic_logo.png differ diff --git a/polaromatic-grooid/src/main/res/drawable-xxhdpi/polaromatic_logo.png b/polaromatic-grooid/src/main/res/drawable-xxhdpi/polaromatic_logo.png new file mode 100644 index 0000000..c8cc937 Binary files /dev/null and b/polaromatic-grooid/src/main/res/drawable-xxhdpi/polaromatic_logo.png differ diff --git a/polaromatic-grooid/src/main/res/layout/about.xml b/polaromatic-grooid/src/main/res/layout/about.xml new file mode 100644 index 0000000..1be3079 --- /dev/null +++ b/polaromatic-grooid/src/main/res/layout/about.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/polaromatic-grooid/src/main/res/layout/activity_settings.xml b/polaromatic-grooid/src/main/res/layout/activity_settings.xml new file mode 100644 index 0000000..4fb4daf --- /dev/null +++ b/polaromatic-grooid/src/main/res/layout/activity_settings.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + +