From 12cbb945583162e794296d1dab6842f281e20c0c Mon Sep 17 00:00:00 2001 From: Bartosz Belcik Date: Wed, 12 Apr 2017 22:24:34 +0200 Subject: [PATCH] Refactoring towards OO design. --- .../FacebookImageVersionBarti271.java | 22 ++++++++ .../contest/barti271/LICENSE.txt | 21 ++++++++ .../contest/barti271/page/CachedPage.java | 51 +++++++++++++++++++ .../contest/barti271/page/Page.java | 7 +++ .../contest/barti271/page/SimplePage.java | 46 +++++++++++++++++ .../barti271/page/content/Content.java | 8 +++ .../contest/barti271/page/content/Image.java | 7 +++ .../barti271/page/content/SimpleContent.java | 34 +++++++++++++ .../page/content/jspup/JsoupMetaElement.java | 26 ++++++++++ .../page/content/jspup/JsoupSource.java | 51 +++++++++++++++++++ .../page/content/source/MetaElement.java | 9 ++++ .../barti271/page/content/source/Source.java | 9 ++++ .../java_fp_example/FacebookImageSpec.groovy | 5 +- .../contest/barti271/FacebookImageSpec.groovy | 25 +++++++++ .../contest/barti271/LICENSE.txt | 21 ++++++++ 15 files changed, 339 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/softwaremill/java_fp_example/contest/barti271/FacebookImageVersionBarti271.java create mode 100644 src/main/java/com/softwaremill/java_fp_example/contest/barti271/LICENSE.txt create mode 100644 src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/CachedPage.java create mode 100644 src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/Page.java create mode 100644 src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/SimplePage.java create mode 100644 src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/Content.java create mode 100644 src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/Image.java create mode 100644 src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/SimpleContent.java create mode 100644 src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/jspup/JsoupMetaElement.java create mode 100644 src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/jspup/JsoupSource.java create mode 100644 src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/source/MetaElement.java create mode 100644 src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/source/Source.java create mode 100644 src/test/groovy/com/softwaremill/java_fp_example/contest/barti271/FacebookImageSpec.groovy create mode 100644 src/test/groovy/com/softwaremill/java_fp_example/contest/barti271/LICENSE.txt diff --git a/src/main/java/com/softwaremill/java_fp_example/contest/barti271/FacebookImageVersionBarti271.java b/src/main/java/com/softwaremill/java_fp_example/contest/barti271/FacebookImageVersionBarti271.java new file mode 100644 index 0000000..81d20d5 --- /dev/null +++ b/src/main/java/com/softwaremill/java_fp_example/contest/barti271/FacebookImageVersionBarti271.java @@ -0,0 +1,22 @@ +package com.softwaremill.java_fp_example.contest.barti271; + +import com.softwaremill.java_fp_example.contest.barti271.page.CachedPage; +import com.softwaremill.java_fp_example.contest.barti271.page.Page; +import com.softwaremill.java_fp_example.contest.barti271.page.SimplePage; +import com.softwaremill.java_fp_example.contest.barti271.page.content.SimpleContent; +import com.softwaremill.java_fp_example.contest.barti271.page.content.jspup.JsoupSource; + +public class FacebookImageVersionBarti271 { + + private final Page page; + + public FacebookImageVersionBarti271(String url) { + this.page = new CachedPage(new SimplePage(new SimpleContent(new JsoupSource(url)))); + } + + @Override + public String toString() { + return page.extractImageAddress(); + } + +} diff --git a/src/main/java/com/softwaremill/java_fp_example/contest/barti271/LICENSE.txt b/src/main/java/com/softwaremill/java_fp_example/contest/barti271/LICENSE.txt new file mode 100644 index 0000000..5b6aa08 --- /dev/null +++ b/src/main/java/com/softwaremill/java_fp_example/contest/barti271/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 barti271 + +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/barti271/page/CachedPage.java b/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/CachedPage.java new file mode 100644 index 0000000..7359abb --- /dev/null +++ b/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/CachedPage.java @@ -0,0 +1,51 @@ +package com.softwaremill.java_fp_example.contest.barti271.page; + +import java.util.function.Supplier; + +/** + * Caches results produced by supplied page. + * This class is thread-safe. + */ +public class CachedPage implements Page { + + private final Page page; + private Supplier imageAddress = this::retrieveImageAddress; + + public CachedPage(Page page) { + this.page = page; + } + + @Override + public String extractImageAddress() { + return imageAddress.get(); + } + + private synchronized String retrieveImageAddress() { + return isInitialized() ? imageAddress.get() : initialize(); + } + + private boolean isInitialized() { + return imageAddress instanceof Holder; + } + + private String initialize() { + imageAddress = new Holder(page.extractImageAddress()); + return imageAddress.get(); + } + + private static class Holder implements Supplier { + + private final String url; + + private Holder(String url) { + this.url = url; + } + + @Override + public String get() { + return url; + } + + } + +} diff --git a/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/Page.java b/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/Page.java new file mode 100644 index 0000000..bfea48f --- /dev/null +++ b/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/Page.java @@ -0,0 +1,7 @@ +package com.softwaremill.java_fp_example.contest.barti271.page; + +public interface Page { + + String extractImageAddress(); + +} diff --git a/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/SimplePage.java b/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/SimplePage.java new file mode 100644 index 0000000..5a20393 --- /dev/null +++ b/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/SimplePage.java @@ -0,0 +1,46 @@ +package com.softwaremill.java_fp_example.contest.barti271.page; + +import static com.softwaremill.java_fp_example.DefaultImage.DEFAULT_IMAGE; + +import com.softwaremill.java_fp_example.contest.barti271.page.content.Content; +import com.softwaremill.java_fp_example.contest.barti271.page.content.Image; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SimplePage implements Page { + + private final Content content; + + public SimplePage(Content content) { + this.content = content; + } + + /** + * @return URL of first og:image read from the supplied {@link Content} or + * {@link com.softwaremill.java_fp_example.DefaultImage#DEFAULT_IMAGE} URL when + * no images can be read. + */ + @Override + public String extractImageAddress() { + try { + return tryExtractImageAddress(); + } catch (RuntimeException e) { + log.error("Unable to extract og:image from url {}. Problem: {}", + e.getMessage(), e.getCause().getMessage()); + return DEFAULT_IMAGE; + } + } + + private String tryExtractImageAddress() { + return content.getOpenGraphImages() + .findFirst() + .map(Image::getUrl) + .orElseGet(this::defaultUrl); + } + + private String defaultUrl() { + log.warn("No og:image found for blog post {}", content); + return DEFAULT_IMAGE; + } +} diff --git a/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/Content.java b/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/Content.java new file mode 100644 index 0000000..6cf1a35 --- /dev/null +++ b/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/Content.java @@ -0,0 +1,8 @@ +package com.softwaremill.java_fp_example.contest.barti271.page.content; + +import java.util.stream.Stream; + +public interface Content { + + Stream getOpenGraphImages(); +} diff --git a/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/Image.java b/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/Image.java new file mode 100644 index 0000000..51eea5b --- /dev/null +++ b/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/Image.java @@ -0,0 +1,7 @@ +package com.softwaremill.java_fp_example.contest.barti271.page.content; + +public interface Image { + + String getUrl(); + +} diff --git a/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/SimpleContent.java b/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/SimpleContent.java new file mode 100644 index 0000000..ddef890 --- /dev/null +++ b/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/SimpleContent.java @@ -0,0 +1,34 @@ +package com.softwaremill.java_fp_example.contest.barti271.page.content; + +import java.util.stream.Stream; + +import com.softwaremill.java_fp_example.contest.barti271.page.content.source.Source; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SimpleContent implements Content { + + private static final String OG_IMAGE = "og:image"; + + private final Source source; + + public SimpleContent(Source source) { + this.source = source; + } + + /** + * @return stream of Open Graph {@link Image}s read from supplied {@link Source} + */ + @Override + public Stream getOpenGraphImages() { + return source.getMetaElements() + .filter(m -> OG_IMAGE.equals(m.getProperty())) + .map(m -> m::getContent); + } + + @Override + public String toString() { + return source.toString(); + } +} diff --git a/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/jspup/JsoupMetaElement.java b/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/jspup/JsoupMetaElement.java new file mode 100644 index 0000000..e1d3424 --- /dev/null +++ b/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/jspup/JsoupMetaElement.java @@ -0,0 +1,26 @@ +package com.softwaremill.java_fp_example.contest.barti271.page.content.jspup; + +import org.jsoup.nodes.Element; + +import com.softwaremill.java_fp_example.contest.barti271.page.content.source.MetaElement; + + +class JsoupMetaElement implements MetaElement { + + private final Element element; + + JsoupMetaElement(Element element) { + this.element = element; + } + + @Override + public String getContent() { + return element.attr("content"); + } + + @Override + public String getProperty() { + return element.attr("property"); + } + +} diff --git a/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/jspup/JsoupSource.java b/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/jspup/JsoupSource.java new file mode 100644 index 0000000..9f36052 --- /dev/null +++ b/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/jspup/JsoupSource.java @@ -0,0 +1,51 @@ +package com.softwaremill.java_fp_example.contest.barti271.page.content.jspup; + +import java.io.IOException; +import java.net.URL; +import java.util.stream.Stream; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Element; + +import com.softwaremill.java_fp_example.contest.barti271.page.content.source.MetaElement; +import com.softwaremill.java_fp_example.contest.barti271.page.content.source.Source; + + +/** + * {@link Jsoup} based implementation of {@link Source}. + */ +public class JsoupSource implements Source { + + private static final int TIMEOUT_TEN_SECONDS = 10_000; + + private final String url; + + public JsoupSource(String url) { + this.url = url; + } + + @Override + public Stream getMetaElements() { + Element head = parseHead(); + return head == null + ? Stream.empty() + : head.getElementsByTag("meta").stream().map(JsoupMetaElement::new); + } + + private Element parseHead() { + try { + return tryParseHead(); + } catch (IOException e) { + throw new IllegalStateException(url, e); + } + } + + private Element tryParseHead() throws IOException { + return Jsoup.parse(new URL(url), TIMEOUT_TEN_SECONDS).head(); + } + + @Override + public String toString() { + return url; + } +} diff --git a/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/source/MetaElement.java b/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/source/MetaElement.java new file mode 100644 index 0000000..0b5ebf1 --- /dev/null +++ b/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/source/MetaElement.java @@ -0,0 +1,9 @@ +package com.softwaremill.java_fp_example.contest.barti271.page.content.source; + +public interface MetaElement { + + String getProperty(); + + String getContent(); + +} diff --git a/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/source/Source.java b/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/source/Source.java new file mode 100644 index 0000000..e33253f --- /dev/null +++ b/src/main/java/com/softwaremill/java_fp_example/contest/barti271/page/content/source/Source.java @@ -0,0 +1,9 @@ +package com.softwaremill.java_fp_example.contest.barti271.page.content.source; + +import java.util.stream.Stream; + +public interface Source { + + Stream getMetaElements(); + +} diff --git a/src/test/groovy/com/softwaremill/java_fp_example/FacebookImageSpec.groovy b/src/test/groovy/com/softwaremill/java_fp_example/FacebookImageSpec.groovy index 8e8be37..674ba68 100644 --- a/src/test/groovy/com/softwaremill/java_fp_example/FacebookImageSpec.groovy +++ b/src/test/groovy/com/softwaremill/java_fp_example/FacebookImageSpec.groovy @@ -5,7 +5,6 @@ import spock.lang.Unroll import static com.softwaremill.java_fp_example.DefaultImage.DEFAULT_IMAGE - class FacebookImageSpec extends Specification { @Unroll @@ -62,7 +61,7 @@ class FacebookImageSpec extends Specification { @Unroll def "should test Better Javaslang version with address #postAddress"() { when: - FacebookImageVersion2Javaslang facebookImage = new FacebookImageVersion2Javaslang(postAddress) + FacebookImageVersion3BetterJavaslang facebookImage = new FacebookImageVersion3BetterJavaslang(postAddress) then: facebookImage.getUrl() == expectedImageUrl @@ -74,5 +73,5 @@ class FacebookImageSpec extends Specification { "https://twitter.com/softwaremill" || DEFAULT_IMAGE "http://i-do-not-exist.pl" || DEFAULT_IMAGE } - + } diff --git a/src/test/groovy/com/softwaremill/java_fp_example/contest/barti271/FacebookImageSpec.groovy b/src/test/groovy/com/softwaremill/java_fp_example/contest/barti271/FacebookImageSpec.groovy new file mode 100644 index 0000000..5f3e1cf --- /dev/null +++ b/src/test/groovy/com/softwaremill/java_fp_example/contest/barti271/FacebookImageSpec.groovy @@ -0,0 +1,25 @@ +package com.softwaremill.java_fp_example.contest.barti271 + +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 Barti271 version with address #postAddress"() { + when: + FacebookImageVersionBarti271 facebookImage = new FacebookImageVersionBarti271(postAddress) + + then: + facebookImage.toString() == 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 + } +} diff --git a/src/test/groovy/com/softwaremill/java_fp_example/contest/barti271/LICENSE.txt b/src/test/groovy/com/softwaremill/java_fp_example/contest/barti271/LICENSE.txt new file mode 100644 index 0000000..5b6aa08 --- /dev/null +++ b/src/test/groovy/com/softwaremill/java_fp_example/contest/barti271/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 barti271 + +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.