diff --git a/build.gradle b/build.gradle index 23c6e1d..573a79a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,29 +1,34 @@ +plugins { + id 'idea' + id 'java' + id 'groovy' +} + +task wrapper(type: Wrapper) { + gradleVersion = '3.4.1' +} + group 'com.softwaremill' version '1.0-SNAPSHOT' -apply plugin: 'idea' -apply plugin: 'java' -apply plugin: 'groovy' - -sourceCompatibility = 1.8 -targetCompatibility = 1.8 +sourceCompatibility = JavaVersion.VERSION_1_8 +targetCompatibility = JavaVersion.VERSION_1_8 repositories { mavenCentral() } dependencies { - compile 'io.javaslang:javaslang:2.0.2' + compile 'io.javaslang:javaslang:2.0.6' compile 'org.jsoup:jsoup:1.10.2' - compile 'org.slf4j:slf4j-api:1.6.4' - compile 'ch.qos.logback:logback-classic:1.0.1' - compile 'ch.qos.logback:logback-core:1.0.1' + compile 'org.slf4j:slf4j-api:1.7.25' + compile 'ch.qos.logback:logback-classic:1.2.2' + compile 'ch.qos.logback:logback-core:1.2.2' - compileOnly "org.projectlombok:lombok:1.16.10" + compileOnly 'org.projectlombok:lombok:1.16.16' - testCompile 'org.spockframework:spock-core:1.0-groovy-2.4' - testCompile 'org.codehaus.groovy:groovy:2.4.7' - testCompile 'cglib:cglib:3.2.2' - + testCompile 'org.spockframework:spock-core:1.1-groovy-2.4-rc-4' + testCompile 'org.codehaus.groovy:groovy:2.4.10' + testCompile 'cglib:cglib:3.2.5' } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 6ffa237..6d4d2e1 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e490051..4776d2e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Mar 03 23:12:26 CET 2017 +#Fri Apr 07 20:59:07 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-all.zip diff --git a/gradlew b/gradlew index 9aa616c..4453cce 100755 --- a/gradlew +++ b/gradlew @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh ############################################################################## ## @@ -154,16 +154,19 @@ if $cygwin ; then 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=("$@") +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then cd "$(dirname "$0")" fi -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/src/main/java/com/softwaremill/java_fp_example/contest/aplotnikov/FacebookImageAddressExtractor.java b/src/main/java/com/softwaremill/java_fp_example/contest/aplotnikov/FacebookImageAddressExtractor.java new file mode 100644 index 0000000..3f8243e --- /dev/null +++ b/src/main/java/com/softwaremill/java_fp_example/contest/aplotnikov/FacebookImageAddressExtractor.java @@ -0,0 +1,55 @@ +package com.softwaremill.java_fp_example.contest.aplotnikov; + +import javaslang.control.Try; +import lombok.extern.slf4j.Slf4j; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Stream; + +import static com.softwaremill.java_fp_example.DefaultImage.DEFAULT_IMAGE; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.jsoup.Jsoup.connect; + +@Slf4j +public class FacebookImageAddressExtractor { + private static final String FACEBOOK_IMAGE_TAG = "og:image"; + + private static final int TEN_SECONDS = (int) SECONDS.toMillis(10); + + private static final Function toContentAttribute = FacebookImageAddressExtractor::getContentAttributeFrom; + + private static final Function> toFacebookImageTags = FacebookImageAddressExtractor::findFacebookImageTags; + + public String extractFrom(String url) { + return extractFacebookImageTagsFrom(url) + .findFirst() + .map(toContentAttribute) + .orElseGet(() -> { + log.warn("No {} found for blog post {}", FACEBOOK_IMAGE_TAG, url); + return DEFAULT_IMAGE; + }); + } + + private static Stream extractFacebookImageTagsFrom(String url) { + return parseContentFrom(url).map(toFacebookImageTags).getOrElse(Stream::empty); + } + + private static Stream findFacebookImageTags(Document document) { + return document.head().select("meta[property=" + FACEBOOK_IMAGE_TAG + ']').stream(); + } + + private static String getContentAttributeFrom(Element element) { + return element.attr("content"); + } + + private static Try parseContentFrom(String url) { + return Try.of(() -> connect(url).timeout(TEN_SECONDS).get()).onFailure(logErrorFor(url)); + } + + private static Consumer logErrorFor(String url) { + return exception -> log.error("Unable to extract og:image from url {}. Problem: {}", url, exception.getMessage()); + } +} diff --git a/src/main/java/com/softwaremill/java_fp_example/contest/aplotnikov/LICENSE-template.txt b/src/main/java/com/softwaremill/java_fp_example/contest/aplotnikov/LICENSE-template.txt new file mode 100644 index 0000000..61294d4 --- /dev/null +++ b/src/main/java/com/softwaremill/java_fp_example/contest/aplotnikov/LICENSE-template.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 aplotnikov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/test/groovy/com/softwaremill/java_fp_example/contest/aplotnikov/FacebookImageAddressExtractorSpec.groovy b/src/test/groovy/com/softwaremill/java_fp_example/contest/aplotnikov/FacebookImageAddressExtractorSpec.groovy new file mode 100644 index 0000000..ed56f70 --- /dev/null +++ b/src/test/groovy/com/softwaremill/java_fp_example/contest/aplotnikov/FacebookImageAddressExtractorSpec.groovy @@ -0,0 +1,24 @@ +package com.softwaremill.java_fp_example.contest.aplotnikov + +import static com.softwaremill.java_fp_example.DefaultImage.DEFAULT_IMAGE + +import spock.lang.Specification +import spock.lang.Subject +import spock.lang.Unroll + +@Unroll +class FacebookImageAddressExtractorSpec extends Specification { + @Subject + FacebookImageAddressExtractor extractor = new FacebookImageAddressExtractor() + + void 'should find image url - #expectedImageUrl into #postAddress'() { + expect: + extractor.extractFrom(postAddress) == expectedImageUrl + where: + postAddress || expectedImageUrl + 'https://softwaremill.com/the-wrong-abstraction-recap/' || 'https://softwaremill.com/images/uploads/2017/02/street-shoe-chewing-gum.0526d557.jpg' + 'https://softwaremill.com/using-kafka-as-a-message-queue/' || 'https://softwaremill.com/images/uploads/2017/02/kmq.93f842cf.png' + 'https://twitter.com/softwaremill' || DEFAULT_IMAGE + 'http://i-do-not-exist.pl' || DEFAULT_IMAGE + } +}