0
\$\begingroup\$

I have a service that gets an URL from the user input, extracts the body content of this URL, apply CSS, and finally returns the result as a stream.

The tricky part is that I have different implementations depending on the URL, if the URL is not recognized, then a "default" implementation is used. To do so, I used a BeanFactory to choose the correct implementation on Runtime.

@Service
@Qualifier("defaultHtmlToHtmlService")
public class DefaultHtmlToHtmlImpl implements HtmlToHtmlService {

    @Override
    public InputStream htmlToHtml(String url) throws IOException {
        Document document = Jsoup.connect(url).get();
        Element content = document.body();

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        outputStream.write(content.outerHtml().getBytes());
        outputStream.close();

        return new ByteArrayInputStream(outputStream.toByteArray());
    }
}

@Component
public class BeanFactory {


private final HtmlToHtmlService defaultHtmlToHtmlService;

private final HtmlToHtmlService impl1;

private final HtmlToHtmlService impl2;

@Autowired
public BeanFactory(@Qualifier("defaultHtmlToHtmlImpl") HtmlToHtmlService defaultHtmlToHtmlService,
                   @Qualifier("impl1") HtmlToHtmlService impl1,
                   @Qualifier("impl2") HtmlToHtmlService impl2) {
    this.defaultHtmlToHtmlService = defaultHtmlToHtmlService;
    this.impl1 = impl1;
    this.impl2 = impl2;
}

public HtmlToHtmlService getHtmlToHtmlImpl(String url) {
    if (url.contains("some pattern")) {
        return impl1;
    } else if (url.contains("some other pattern")) {
        return impl2;
    } else {
        return defaultHtmlToHtmlService;
    }
}

This is working just fine. However, my issue is that I don't want my BeanFactory to share any information with the rest, but I do not know how to decouple it. Basically, I want to remove the @Qualifier annotation, and not to have to manually enter the new URLs patterns in the Bean Factory in the future when I will get more implementations.

I saw that maybe @PostConstruct annotation or to use static blocks could be the solution, but I am really not sure how to apply it in this case.

\$\endgroup\$
4
  • \$\begingroup\$ Welcome to Code Review, when I saw your code doing different actions corresponding to different url patterns I thought about a filter class mapping redirection of different url patterns to different services. Have you considered this type of approach to your problem ? \$\endgroup\$ Feb 2, 2021 at 11:09
  • \$\begingroup\$ Thank you. Indeed, I did not consider it but it seems like a good idea. Would you recommend some documentation or old post in particular? \$\endgroup\$
    – LaChope
    Feb 2, 2021 at 11:14
  • \$\begingroup\$ You are welcome. A possible approach can be found in webfilter-mapping, or in alternative a java servlet httpfilter. My suggested approach is to map services to different urls to I can redirect to them basing on the original url submitted. \$\endgroup\$ Feb 2, 2021 at 11:51
  • \$\begingroup\$ @dariosicily Thank you for your answer and sorry for this late reply. I had a look, however I think I did not explain well my issue and therefore there is a misunderstanding. If I understood well, a filter is useful for intercepting HTTP requests (so at the controller level). However, my issue is that my I have only one controller that receives a multipartfile that contains a String (url of a website). It is this string that I need to analyse and therefore use the correct service. \$\endgroup\$
    – LaChope
    Feb 4, 2021 at 14:26

1 Answer 1

2
\$\begingroup\$

The problem with your approach is, if you decide to add/remove implementations, you have to modify the BeanFactory. Also, if there are numerous implementations, then you have to add them all in BeanFactory, which causes it be verbose and will be difficult to maintain.

You can achieve this in multiple ways. One of those and simplest one is as following:

Your interface:

interface HtmlToHtmlService {
    InputStream htmlToHtml(String url);
    boolean canProcess(String url);
}

Your implementations.

@Service
@Order(1)
public class SomeHtmlToHtmlImpl implements HtmlToHtmlService {
    InputStream htmlToHtml(String url) {
        //do your stuff;
    }

    boolean canProcess(String url) {
        return url.contains("");
    }
}


@Service
@Order(2)
public class SomeHtmlToHtmlImpl implements HtmlToHtmlService {
    InputStream htmlToHtml(String url) {
        //do your stuff;
    }

    boolean canProcess(String url) {
        return url.contains("");
    }
}

Default

@Service
@Order(3) // Give lowest precendence.
public class SomeHtmlToHtmlImpl implements HtmlToHtmlService {
    InputStream htmlToHtml(String url) {
        //do your stuff;
    }
    boolean contains(String url) {
        return true;
    }
}

So, whenever you need to process the url, you do as following:

@Autowired
List<HtmlToHtmlService> htmlToHtmlServices;

void toHtml(String url) {
    for (HtmlToHtmlService service: htmlToHtmlServices) {
        if(service.canProcess(url) {
            this.service.process(url);
            break;
        }
    }
}

Basically, you have multiple implementations for processing different patterns of url. Each implementations tells you whether it can process the url or not by canProcess(String url) method. If none can process, then default implementation will process the url. I set the orders so that the default will be in the last of the collection. You get flexibility of adding/removing different implementations and don't need to declare anywhere.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.