diff --git a/src/main/java/com/softwaremill/java_fp_example/contest/hexmind/ExtractionOptions.java b/src/main/java/com/softwaremill/java_fp_example/contest/hexmind/ExtractionOptions.java new file mode 100644 index 0000000..18b6622 --- /dev/null +++ b/src/main/java/com/softwaremill/java_fp_example/contest/hexmind/ExtractionOptions.java @@ -0,0 +1,29 @@ +package com.softwaremill.java_fp_example.contest.hexmind; + +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; + +/** + * The Open Graph protocol enables any web page to become a rich object in a social graph. For instance, this is used on + * Facebook to allow any web page to have the same functionality as any other object on Facebook. + * + * @link http://ogp.me + */ +@Getter +@Builder +public class ExtractionOptions { + + @NonNull + private final String pageUrl; + + @NonNull + private final Integer timeoutMs; + + @NonNull + private final String metaProperty; + + @NonNull + private final String fallbackContent; + +} diff --git a/src/main/java/com/softwaremill/java_fp_example/contest/hexmind/FacebookImage.java b/src/main/java/com/softwaremill/java_fp_example/contest/hexmind/FacebookImage.java new file mode 100644 index 0000000..0135ab6 --- /dev/null +++ b/src/main/java/com/softwaremill/java_fp_example/contest/hexmind/FacebookImage.java @@ -0,0 +1,33 @@ +package com.softwaremill.java_fp_example.contest.hexmind; + +import com.softwaremill.java_fp_example.DefaultImage; + +public class FacebookImage { + + private static final int TIMEOUT_MS = 10_000; + private static final String META_PROPERTY = "og:image"; + + private final OpenGraphPage page; + + public FacebookImage(ExtractionOptions options) { + this.page = new OpenGraphPage(options); + } + + public static FacebookImage fromPage(String pageUrl) { + return new FacebookImage(setupForPage(pageUrl)); + } + + public String extractUrl() { + return page.extractMetaContent(); + } + + private static ExtractionOptions setupForPage(String pageUrl) { + return ExtractionOptions.builder() + .pageUrl(pageUrl) + .timeoutMs(TIMEOUT_MS) + .metaProperty(META_PROPERTY) + .fallbackContent(DefaultImage.DEFAULT_IMAGE) + .build(); + } + +} diff --git a/src/main/java/com/softwaremill/java_fp_example/contest/hexmind/LICENSE b/src/main/java/com/softwaremill/java_fp_example/contest/hexmind/LICENSE new file mode 100644 index 0000000..2a9dc6d --- /dev/null +++ b/src/main/java/com/softwaremill/java_fp_example/contest/hexmind/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Hexmind + +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/hexmind/OpenGraphExtractor.java b/src/main/java/com/softwaremill/java_fp_example/contest/hexmind/OpenGraphExtractor.java new file mode 100644 index 0000000..48d7c4b --- /dev/null +++ b/src/main/java/com/softwaremill/java_fp_example/contest/hexmind/OpenGraphExtractor.java @@ -0,0 +1,47 @@ +package com.softwaremill.java_fp_example.contest.hexmind; + +import java.io.IOException; +import java.net.URL; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import javaslang.collection.List; +import javaslang.control.Try; + +public class OpenGraphExtractor { + + private final ExtractionOptions options; + + public OpenGraphExtractor(ExtractionOptions options) { + this.options = options; + } + + public Try extractContent() { + return Try.of(this::loadWebPage) + .map(this::getMetaElements) + .flatMap(this::findContent); + } + + private Document loadWebPage() throws IOException { + URL url = new URL(options.getPageUrl()); + return Jsoup.parse(url, options.getTimeoutMs()); + } + + private Elements getMetaElements(Document document) { + return document.head().getElementsByTag("meta"); + } + + private Try findContent(Elements elements) { + return List.ofAll(elements) + .filter(this::hasMetaProperty) + .map(element -> element.attr("content")) + .toTry(); + } + + private boolean hasMetaProperty(Element element) { + return options.getMetaProperty().equals(element.attr("property")); + } +} diff --git a/src/main/java/com/softwaremill/java_fp_example/contest/hexmind/OpenGraphPage.java b/src/main/java/com/softwaremill/java_fp_example/contest/hexmind/OpenGraphPage.java new file mode 100644 index 0000000..b797ebe --- /dev/null +++ b/src/main/java/com/softwaremill/java_fp_example/contest/hexmind/OpenGraphPage.java @@ -0,0 +1,34 @@ +package com.softwaremill.java_fp_example.contest.hexmind; + +import java.net.MalformedURLException; +import java.net.UnknownHostException; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class OpenGraphPage { + + private final ExtractionOptions options; + + public OpenGraphPage(ExtractionOptions options) { + this.options = options; + } + + public String extractMetaContent() { + return new OpenGraphExtractor(options) + .extractContent() + .onFailure(this::onFailure) + .getOrElse(options::getFallbackContent); + } + + private void onFailure(Throwable error) { + if (error instanceof MalformedURLException) { + log.error("Invalid URL {}. Problem: {}", options.getPageUrl(), error.getMessage()); + } else if (error instanceof UnknownHostException) { + log.error("Unable to extract content from URL {}. Problem: {}", options.getPageUrl(), error.getMessage()); + } else { + log.warn("No {} found for page {}", options.getMetaProperty(), options.getPageUrl()); + } + } + +} diff --git a/src/main/java/com/softwaremill/java_fp_example/contest/hexmind/README.md b/src/main/java/com/softwaremill/java_fp_example/contest/hexmind/README.md new file mode 100644 index 0000000..3c16cce --- /dev/null +++ b/src/main/java/com/softwaremill/java_fp_example/contest/hexmind/README.md @@ -0,0 +1,45 @@ +# Road to a more functional Java +## Example refactoring + +### Prologue + +> The Open Graph protocol enables any web page to become a rich object in a social graph. +> og:image - An image URL which should represent your object within the graph. +> +> http://ogp.me/ + + +## Who is who? + +### [FacebookImage](FacebookImage.java) + +client-specific *OpenGraphPage* image + +``` +String img = FacebookImage.fromPage(postUrl).extractUrl() +``` + +### [OpenGraphPage](OpenGraphPage.java) + +non-functional OG node: errors handling and *OpenGraphExtractor* as internal implementation + +``` +public String extractMetaContent() { + return new OpenGraphExtractor(options) + .extractContent() + .onFailure(this::onFailure) + .getOrElse(options::getFallbackContent); +} +``` + +### [OpenGraphExtractor](OpenGraphExtractor.java) + +functional OG node metadata: extraction without side effects + +``` +Try extractContent() { + return Try.of(this::loadWebPage) + .map(this::getMetaElements) + .flatMap(this::findContent); +} +``` diff --git a/src/test/groovy/com/softwaremill/java_fp_example/contest/hexmind/FacebookImageSpec.groovy b/src/test/groovy/com/softwaremill/java_fp_example/contest/hexmind/FacebookImageSpec.groovy new file mode 100644 index 0000000..37e4446 --- /dev/null +++ b/src/test/groovy/com/softwaremill/java_fp_example/contest/hexmind/FacebookImageSpec.groovy @@ -0,0 +1,27 @@ +package com.softwaremill.java_fp_example.contest.hexmind + +import spock.lang.Specification +import spock.lang.Unroll + +import static com.softwaremill.java_fp_example.DefaultImage.DEFAULT_IMAGE + +@Unroll +class FacebookImageSpec extends Specification { + + def "should test Hexmind version with address #postAddress"() { + when: + FacebookImage facebookImage = FacebookImage.fromPage(postAddress) + + then: + facebookImage.extractUrl() == 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 + "i-am-not-a-url" || DEFAULT_IMAGE + } + +}