Skip to content
This repository was archived by the owner on Apr 27, 2023. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ dependencies {
compile 'ch.qos.logback:logback-classic:1.0.1'
compile 'ch.qos.logback:logback-core:1.0.1'

compile 'com.google.guava:guava:21.0'

compileOnly "org.projectlombok:lombok:1.16.10"

testCompile 'org.spockframework:spock-core:1.0-groovy-2.4'
testCompile 'org.codehaus.groovy:groovy:2.4.7'
testCompile 'cglib:cglib:3.2.2'

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2017 <twój login na GitHubie>
Copyright (c) 2017 mszarlinski

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.softwaremill.java_fp_example.contest.mszarlinski.adapter.jsoup;

import com.google.common.base.Strings;
import com.softwaremill.java_fp_example.contest.mszarlinski.domain.web.Element;
import com.softwaremill.java_fp_example.contest.mszarlinski.domain.web.Elements;
import javaslang.collection.List;
import javaslang.control.Option;

class JsoupElement implements Element {

private final org.jsoup.nodes.Element wrapped;

static Element from(org.jsoup.nodes.Element element) {
return new JsoupElement(element);
}

private JsoupElement(org.jsoup.nodes.Element element) {
this.wrapped = element;
}

@Override
public boolean hasAttr(String attrName, String attrValue) {
return attr(attrName)
.filter(val -> val.equals(attrValue))
.isDefined();
}

@Override
public Option<String> attr(String attrName) {
return Option.of(wrapped.attr(attrName))
.filter(s -> !Strings.isNullOrEmpty(s));
}

@Override
public Elements childrenByTag(String tagName) {
return JsoupElements.from(wrapped.getElementsByTag(tagName)
.stream()
.map(JsoupElement::new)
.collect(List.collector()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.softwaremill.java_fp_example.contest.mszarlinski.adapter.jsoup;

import com.softwaremill.java_fp_example.contest.mszarlinski.domain.web.DummyElement;
import com.softwaremill.java_fp_example.contest.mszarlinski.domain.web.DummyElements;
import com.softwaremill.java_fp_example.contest.mszarlinski.domain.web.Element;
import com.softwaremill.java_fp_example.contest.mszarlinski.domain.web.Elements;
import javaslang.collection.List;

class JsoupElements implements Elements {

private final List<JsoupElement> wrapped;

static Elements from(List<JsoupElement> elements) {
if (elements.isEmpty()) {
return DummyElements.INSTANCE;
} else {
return new JsoupElements(elements);
}
}

private JsoupElements(List<JsoupElement> wrapped) {
this.wrapped = wrapped;
}

@Override
public Element first() {
return wrapped.headOption()
.map(h -> (Element) h)
.getOrElse(DummyElement.INSTANCE);
}

@Override
public Elements filterByAttribute(String attrName, String attrValue) {
return JsoupElements.from(wrapped.filter(e -> e.hasAttr(attrName, attrValue)));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.softwaremill.java_fp_example.contest.mszarlinski.adapter.jsoup;

import com.softwaremill.java_fp_example.contest.mszarlinski.domain.web.DummyElement;
import com.softwaremill.java_fp_example.contest.mszarlinski.domain.web.Element;
import com.softwaremill.java_fp_example.contest.mszarlinski.domain.web.WebPage;
import javaslang.control.Option;
import org.jsoup.nodes.Document;

class JsoupWebPage implements WebPage {

private final Document wrapped;

JsoupWebPage(Document document) {
this.wrapped = document;
}

@Override
public Element head() {
return Option.of(wrapped.head())
.map(JsoupElement::from)
.getOrElse(DummyElement.INSTANCE);
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.softwaremill.java_fp_example.contest.mszarlinski.adapter.jsoup;

import com.softwaremill.java_fp_example.contest.mszarlinski.domain.WebPageLoader;
import com.softwaremill.java_fp_example.contest.mszarlinski.domain.web.WebPage;
import javaslang.control.Try;
import org.jsoup.Jsoup;

import java.net.URL;
import java.time.Duration;

class JsoupWebPageLoader implements WebPageLoader {

private final int webPageLoadingTimeoutMillis;

JsoupWebPageLoader(Duration webPageLoadingTimeout) {
this.webPageLoadingTimeoutMillis = (int) webPageLoadingTimeout.toMillis();
}

@Override
public Try<WebPage> tryLoadWebPage(String url) {
return Try.of(() -> new JsoupWebPage(Jsoup.parse(new URL(url), webPageLoadingTimeoutMillis)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.softwaremill.java_fp_example.contest.mszarlinski.domain;

import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;

/**
* Immutable value object representing a FB image.
*/
public class FacebookImage {

public final static String FACEBOOK_IMAGE_TAG = "og:image";

public static final FacebookImage DEFAULT = new FacebookImage("https://softwaremill.com/images/logo-vertical.023d8496.png");

private final String url;

public FacebookImage(String url) {
this.url = url;
}

public String getUrl() {
return url;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FacebookImage that = (FacebookImage) o;
return Objects.equal(url, that.url);
}

@Override
public int hashCode() {
return Objects.hashCode(url);
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("url", url)
.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.softwaremill.java_fp_example.contest.mszarlinski.domain;

import com.softwaremill.java_fp_example.contest.mszarlinski.domain.web.Element;
import com.softwaremill.java_fp_example.contest.mszarlinski.domain.web.Elements;
import com.softwaremill.java_fp_example.contest.mszarlinski.domain.web.WebPage;
import lombok.extern.slf4j.Slf4j;

import java.util.function.Consumer;
import java.util.function.Function;

import static java.lang.String.format;

/**
* Component responsible for extracting FB image from given page.
*/
@Slf4j
public class FacebookImageProvider {

private final WebPageLoader webPageLoader;

public FacebookImageProvider(WebPageLoader webPageLoader) {
this.webPageLoader = webPageLoader;
}

public FacebookImage getImageUrlFromPage(String pageUrl) {
return webPageLoader.tryLoadWebPage(pageUrl)
.map(WebPage::head)
.map(metasWithImage())
.map(Elements::first)
.map(fbImage())
.onFailure(logError(pageUrl))
.getOrElse(FacebookImage.DEFAULT);
}

private Function<Element, Elements> metasWithImage() {
return h -> h.childrenByTag("meta")
.filterByAttribute("property", FacebookImage.FACEBOOK_IMAGE_TAG);
}

private Function<Element, FacebookImage> fbImage() {
return e -> e.attr("content")
.map(FacebookImage::new)
.getOrElse(FacebookImage.DEFAULT);
}

private Consumer<Throwable> logError(String pageUrl) {
return t -> log.warn(format("Failed to extract image from %s", pageUrl), t);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.softwaremill.java_fp_example.contest.mszarlinski.domain;

import com.softwaremill.java_fp_example.contest.mszarlinski.domain.web.WebPage;
import javaslang.control.Try;

public interface WebPageLoader {
Try<WebPage> tryLoadWebPage(String url);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.softwaremill.java_fp_example.contest.mszarlinski.domain.web;

import javaslang.control.Option;

public class DummyElement implements Element {

public static final Element INSTANCE = new DummyElement();

@Override
public boolean hasAttr(String attrName, String attrValue) {
return false;
}

@Override
public Option<String> attr(String attrName) {
return Option.none();
}

@Override
public Elements childrenByTag(String tagName) {
return DummyElements.INSTANCE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.softwaremill.java_fp_example.contest.mszarlinski.domain.web;

public class DummyElements implements Elements {

public static final Elements INSTANCE = new DummyElements();

@Override
public Element first() {
return DummyElement.INSTANCE;
}

@Override
public Elements filterByAttribute(String attrName, String attrValue) {
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.softwaremill.java_fp_example.contest.mszarlinski.domain.web;

import javaslang.control.Option;

public interface Element {
boolean hasAttr(String attrName, String attrValue);

Option<String> attr(String attrName);

Elements childrenByTag(String tagName);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.softwaremill.java_fp_example.contest.mszarlinski.domain.web;

public interface Elements {

Element first();

Elements filterByAttribute(String attrName, String attrValue);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.softwaremill.java_fp_example.contest.mszarlinski.domain.web;

public interface WebPage {

Element head();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.softwaremill.java_fp_example.contest.mszarlinski.domain

import com.softwaremill.java_fp_example.contest.mszarlinski.adapter.jsoup.JsoupWebPageLoader
import spock.lang.Specification
import spock.lang.Unroll

import java.time.Duration

import static com.softwaremill.java_fp_example.DefaultImage.DEFAULT_IMAGE

class FacebookImageProviderSpec extends Specification {

@Unroll
def "should test Provider version with address #postAddress"() {
given:
FacebookImageProvider provider = new FacebookImageProvider(
new JsoupWebPageLoader(Duration.ofSeconds(10)))

when:
def image = provider.getImageUrlFromPage(postAddress)

then:
image.url == 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
}
}