I refactored this class as far as I'm capable of at the time but I wonder if it can't be better. One thing that I'm not sure of, is that I take parameters from a method, which is not a constructor, and put them in fields. I wonder if it's bad practice.
Also, I decided to pull parameters away to fields to reduce the passing around of arguments. It made the method calls cleaner but initializing a field in a for loop looks awkward.
I suspect further improvements are possible so if you can see any, feel free to chop away at the code.
Some context: JsoupParser
is just a wrapper around Jsoup with some retry functionality and to make it testable. SelectorContainer
is a container that holds Selector
objects that are used to define what has to be scraped. Response
is a returned value from Jsoup when you connect to a URL, in this case the URL of an img src attribute.
@Component
public class ImageScraper {
private JsoupParser jsoupParser;
private GenericArticleScraper genericArticleScraper;
private SelectorContainer selector;
private Document document;
private Elements elements;
private Element element;
@Autowired
public ImageScraper(JsoupParser jsoupParser, GenericArticleScraper genericArticleScraper) {
this.jsoupParser = jsoupParser;
this.genericArticleScraper = genericArticleScraper;
}
public List<Image> scrape(Document document, SelectorContainer selector) {
intializeFields(document, selector, scrapeImageElements());
if (hasNoImageElements()) {
return new ArrayList<>();
}
return scrapeImages(elements);
}
private void intializeFields(Document document, SelectorContainer selector, Elements elements) {
this.document = document;
this.selector = selector;
this.elements = elements;
}
private Elements scrapeImageElements() {
return genericArticleScraper.scrapeElementsUsingSelector(document, selector.forImage());
}
private boolean hasNoImageElements() {
return elements == null || elements.isEmpty();
}
private List<Image> scrapeImages(Elements elements) {
int orderOfAppearance = 0;
List<Image> images = new ArrayList<>();
for (Element element : elements) {
try {
++orderOfAppearance;
this.element = element;
images.add(
createImage(
getResponse(scrapeUrl(this.element)),
orderOfAppearance,
createName(createBaseName(), orderOfAppearance)));
} catch (IOException e) {
--orderOfAppearance;
e.printStackTrace();
}
}
return images;
}
private Image createImage(Response response, int orderOfAppearance, String name) throws IOException {
return new Image(
scrapeUrl(element),
name,
orderOfAppearance,
scrapeImageCaption(element),
scrapeImageCredit(element),
createExtension(response),
responseBodyAsBytes(response));
}
private String scrapeUrl(Element element) {
return genericArticleScraper.scrapeTextUsingSelector(element, selector.forImageUrl());
}
private String scrapeImageCaption(Element element) {
return genericArticleScraper.scrapeTextUsingSelector(element, selector.forImageCaption());
}
private String scrapeImageCredit(Element element) {
return genericArticleScraper.scrapeTextUsingSelector(element, selector.forImageCredit());
}
private Extension createExtension(Response response) {
return Extension.from(response.contentType().replace("image/", "").replace(";charset=UTF-8", ""));
}
private byte[] responseBodyAsBytes(Response response) {
return response.bodyAsBytes();
}
private Response getResponse(String url) throws IOException {
return jsoupParser.connect(url);
}
private String createName(String name, int count) {
return name + "_" + count;
}
private String createBaseName() {
return replaceIllegalFileNameCharacters(scrapeHeadline());
}
private String replaceIllegalFileNameCharacters(String text) {
return text.replaceAll("[^a-zA-Z0-9.-]", "_");
}
private String scrapeHeadline() {
return genericArticleScraper.scrapeTextUsingSelector(document, selector.forHeadline());
}
}
Refactored after ndm13's answer:
package org.observer.media.scrape.article;
import org.jsoup.Connection;
import org.jsoup.Connection.Response;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.observer.media.model.Image;
import org.observer.media.model.Image.Extension;
import org.observer.media.parse.JsoupParser;
import org.observer.media.scrape.SelectorContainer;
import org.observer.media.scrape.component.GenericArticleScraper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@Component
public class ImageScraper {
private JsoupParser jsoupParser;
private GenericArticleScraper scraper;
@Autowired
public ImageScraper(JsoupParser jsoupParser, GenericArticleScraper scraper) {
this.jsoupParser = jsoupParser;
this.scraper = scraper;
}
public List<Image> scrape(Document document, SelectorContainer selector) {
List<Image> images = new ArrayList<>();
Elements elements = scraper.scrapeElements(document, selector.forImage());
if (elements.isEmpty()) {
return images;
}
int index = 0;
for (Element element : elements) {
try {
images.add(createImage(++index, selector, element, createBaseName(document, selector)));
} catch (IOException e) {
--index;
e.printStackTrace();
}
}
return images;
}
private Image createImage(int index, SelectorContainer selector, Element element, String baseName) throws IOException {
Connection.Response response = getResponse(scraper.scrape(element, selector.forImageUrl()));
return new Image(
scraper.scrape(element, selector.forImageUrl()),
createName(baseName, index),
index,
scraper.scrape(element, selector.forImageCaption()),
scraper.scrape(element, selector.forImageCredit()),
createExtension(response),
response.bodyAsBytes());
}
private Response getResponse(String url) throws IOException {
return jsoupParser.connect(url);
}
private String createName(String name, int count) {
return name + "_" + count;
}
private String createBaseName(Document document, SelectorContainer selector) {
return replaceIllegalFileNameCharacters(scraper.scrape(document, selector.forHeadline()));
}
private String replaceIllegalFileNameCharacters(String text) {
return text.replaceAll("[^a-zA-Z0-9.-]", "_");
}
private Extension createExtension(Response response) {
return Extension.from(response.contentType().replace("image/", "").replace(";charset=UTF-8", ""));
}
}