diff --git a/build.gradle b/build.gradle index 23c6e1d..9b60a52 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,7 @@ targetCompatibility = 1.8 repositories { mavenCentral() + jcenter() } dependencies { @@ -20,6 +21,12 @@ dependencies { compile 'ch.qos.logback:logback-classic:1.0.1' compile 'ch.qos.logback:logback-core:1.0.1' + compile 'com.github.florent37:retrojsoup:1.0.3' + compile 'com.github.florent37:rxjsoup:1.0.3' + compile 'org.jsoup:jsoup:1.10.2' + compile 'io.reactivex.rxjava2:rxjava:2.0.8' + compileOnly 'com.github.florent37:retrojsoup-compiler:1.0.3' + compileOnly "org.projectlombok:lombok:1.16.10" testCompile 'org.spockframework:spock-core:1.0-groovy-2.4' diff --git a/src/main/java/com/softwaremill/java_fp_example/contest/maciejdobrowolski/FacebookImage.java b/src/main/java/com/softwaremill/java_fp_example/contest/maciejdobrowolski/FacebookImage.java new file mode 100644 index 0000000..637c744 --- /dev/null +++ b/src/main/java/com/softwaremill/java_fp_example/contest/maciejdobrowolski/FacebookImage.java @@ -0,0 +1,59 @@ +package com.softwaremill.java_fp_example.contest.maciejdobrowolski; + +import static com.softwaremill.java_fp_example.DefaultImage.DEFAULT_IMAGE; +import static io.reactivex.schedulers.Schedulers.io; +import static java.util.concurrent.TimeUnit.SECONDS; + +import com.github.florent37.retrojsoup.RetroJsoup; +import io.reactivex.Maybe; +import io.reactivex.ObservableTransformer; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class FacebookImage { + + private final static String FACEBOOK_IMAGE_TAG = "og:image"; + private final static MetaEntry FALLBACK_META_ENTRY = new MetaEntry(FACEBOOK_IMAGE_TAG, DEFAULT_IMAGE); + private final static int TIMEOUT_IN_SECONDS = 10; + + private final String url; + + public FacebookImage(String url) { + this.url = url; + } + + public Maybe imageAddress() { + return pageModel().metaTags() + .subscribeOn(io()) + .compose(defaultOnTimeout()) + .filter(FacebookImage::isFacebookImageTag) + .compose(firstOrDefault()) + .singleElement() + .map(meta -> meta.content); + } + + private PageModel pageModel() { + return new RetroJsoup.Builder() + .url(url) + .build() + .create(PageModel.class); + } + + private ObservableTransformer defaultOnTimeout() { + return upstream -> upstream.timeout(TIMEOUT_IN_SECONDS, SECONDS) + .doOnError(e -> log.error("Unable to extract og:image from url {}. Problem: {}", url, e.getMessage())) + .onErrorReturnItem(FALLBACK_META_ENTRY); + } + + private static boolean isFacebookImageTag(MetaEntry metaEntry) { + return FACEBOOK_IMAGE_TAG.equals(metaEntry.attr); + } + + private ObservableTransformer firstOrDefault() { + return upstream -> upstream.firstOrError() + .doOnError(e -> log.warn("No {} found for blog post {}", FACEBOOK_IMAGE_TAG, url)) + .onErrorReturnItem(FALLBACK_META_ENTRY) + .toObservable(); + } + +} diff --git a/src/main/java/com/softwaremill/java_fp_example/contest/maciejdobrowolski/LICENSE.txt b/src/main/java/com/softwaremill/java_fp_example/contest/maciejdobrowolski/LICENSE.txt new file mode 100644 index 0000000..9cb5672 --- /dev/null +++ b/src/main/java/com/softwaremill/java_fp_example/contest/maciejdobrowolski/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 MaciejDobrowolski + +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/main/java/com/softwaremill/java_fp_example/contest/maciejdobrowolski/MetaEntry.java b/src/main/java/com/softwaremill/java_fp_example/contest/maciejdobrowolski/MetaEntry.java new file mode 100644 index 0000000..e648060 --- /dev/null +++ b/src/main/java/com/softwaremill/java_fp_example/contest/maciejdobrowolski/MetaEntry.java @@ -0,0 +1,20 @@ +package com.softwaremill.java_fp_example.contest.maciejdobrowolski; + +import com.github.florent37.retrojsoup.annotations.JsoupAttr; + +class MetaEntry { + + @JsoupAttr(value = "meta", attr = "property") + String attr; + + @JsoupAttr(value = "meta", attr = "content") + String content; + + MetaEntry() { + } + + MetaEntry(String attr, String content) { + this.attr = attr; + this.content = content; + } +} diff --git a/src/main/java/com/softwaremill/java_fp_example/contest/maciejdobrowolski/PageModel.java b/src/main/java/com/softwaremill/java_fp_example/contest/maciejdobrowolski/PageModel.java new file mode 100644 index 0000000..ce7dc4e --- /dev/null +++ b/src/main/java/com/softwaremill/java_fp_example/contest/maciejdobrowolski/PageModel.java @@ -0,0 +1,11 @@ +package com.softwaremill.java_fp_example.contest.maciejdobrowolski; + +import com.github.florent37.retrojsoup.annotations.Select; +import io.reactivex.Observable; + +interface PageModel { + + @Select("head meta") + Observable metaTags(); + +} diff --git a/src/test/groovy/com/softwaremill/java_fp_example/contest/maciejdobrowolski/FacebookImageSpec.groovy b/src/test/groovy/com/softwaremill/java_fp_example/contest/maciejdobrowolski/FacebookImageSpec.groovy new file mode 100644 index 0000000..e860893 --- /dev/null +++ b/src/test/groovy/com/softwaremill/java_fp_example/contest/maciejdobrowolski/FacebookImageSpec.groovy @@ -0,0 +1,26 @@ +package com.softwaremill.java_fp_example.contest.maciejdobrowolski + +import spock.lang.Specification +import spock.lang.Unroll + +import static com.softwaremill.java_fp_example.DefaultImage.DEFAULT_IMAGE + +class FacebookImageSpec extends Specification { + + @Unroll + def "should test Maciej's version with address #postAddress"() { + when: + FacebookImage facebookImage = new FacebookImage(postAddress) + + then: + facebookImage.imageAddress().blockingGet() == 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 + } + +}