Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Decoupling Image Thumbnail Generation with the Bridge Pattern

Tech May 14 1

To generate and store image thumbnails, the system must support multiple input sources—such as local files or remote URLs—and various output destinations, including local sttorage, MinIO, Alibaba Cloud OSS, or Qiniu Cloud. Given that both input and output strategies may evolve independently, a flexible design is essential.

The solution leverages the Bridge pattern to decouple abstraction from implementation. This allows new loading or saving mechanisms to be added without modifying existing application logic. The core idea is to define two parallel hierarchies: one for loading images (ThumbLoad) and another for saving them (ThumbSave), with composition linking the two at runtime.

Abstraction: Image Loading

The base ThumbLoad class defines the contract for loading an image and delegates saving to a ThumbSave instance:

public abstract class ThumbLoad {
    protected ThumbSave saver;

    public ThumbLoad(ThumbSave saver) {
        this.saver = saver;
    }

    protected boolean success = false;
    protected String errorMessage = null;

    public abstract ImageActionVo process(String source);

    protected boolean isUrlAccessible(String url, boolean requireImageType) {
        try {
            HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
            conn.setConnectTimeout(2000);
            int status = conn.getResponseCode();
            if (status != 200) return false;
            if (requireImageType) {
                String contentType = conn.getContentType();
                if (!contentType.startsWith("image/")) return false;
            }
            return true;
        } catch (Exception e) {
            return false;
        } finally {
            // disconnect logic omitted for brevity
        }
    }
}

Implementation: Image Saving

The ThumbSave hierarchy handles where and how the processed thumbnail is stored:

public abstract class ThumbSave {
    protected final String outputPath;
    protected final int targetWidth;
    protected final int targetHeight;

    public ThumbSave(String outputPath, int width, int height) {
        this.outputPath = outputPath;
        this.targetWidth = width;
        this.targetHeight = height;
    }

    public abstract SaveResult persist(InputStream originalStream);

    public static class SaveResult {
        public final boolean success;
        public final String uri;
        public final String message;

        public SaveResult(boolean success, String uri, String message) {
            this.success = success;
            this.uri = uri;
            this.message = message;
        }
    }
}

Concrete Loaders

Local file loader:

public class LocalImageLoader extends ThumbLoad {
    public LocalImageLoader(ThumbSave saver) {
        super(saver);
    }

    @Override
    public ImageActionVo process(String filePath) {
        File file = new File(filePath);
        if (!file.exists()) {
            return new ImageActionVo(filePath, false, null, "File not found");
        }
        try (InputStream in = new FileInputStream(file)) {
            SaveResult result = saver.persist(in);
            return new ImageActionVo(filePath, result.success, result.uri, result.message);
        } catch (IOException e) {
            return new ImageActionVo(filePath, false, null, e.getMessage());
        }
    }
}

Remote URL loader:

public class RemoteImageLoader extends ThumbLoad {
    public RemoteImageLoader(ThumbSave saver) {
        super(saver);
    }

    @Override
    public ImageActionVo process(String imageUrl) {
        if (!isUrlAccessible(imageUrl, true)) {
            return new ImageActionVo(imageUrl, false, null, "URL inaccessible or not an image");
        }
        try {
            URL url = new URL(imageUrl);
            try (InputStream in = url.openStream()) {
                SaveResult result = saver.persist(in);
                return new ImageActionVo(imageUrl, result.success, result.uri, result.message);
            }
        } catch (IOException e) {
            return new ImageActionVo(imageUrl, false, null, e.getMessage());
        }
    }
}

Concrete Savers

Local disk saver:

public class LocalThumbnailSaver extends ThumbSave {
    public LocalThumbnailSaver(String path, int w, int h) {
        super(path, w, h);
    }

    @Override
    public SaveResult persist(InputStream in) {
        try {
            Thumbnails.of(in).size(targetWidth, targetHeight).toFile(outputPath);
            return new SaveResult(true, outputPath, null);
        } catch (IOException e) {
            return new SaveResult(false, null, e.getMessage());
        }
    }
}

Cloud storage saver (via internal file service):

public class CloudThumbnailSaver extends ThumbSave {
    public CloudThumbnailSaver(String hintPath, int w, int h) {
        super(hintPath, w, h);
    }

    @Override
    public SaveResult persist(InputStream in) {
        try (ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
            Thumbnails.of(in).size(targetWidth, targetHeight).toOutputStream(buffer);
            byte[] data = buffer.toByteArray();
            try (InputStream uploadStream = new ByteArrayInputStream(data)) {
                MockMultipartFile file = new MockMultipartFile("thumb.jpg", "thumb.jpg",
                    MediaType.IMAGE_JPEG_VALUE, uploadStream);
                R response = fileFeignClient.upload("thumb_images", file);
                if (response.isSuccess()) {
                    String cloudUrl = (String) response.get("data");
                    return new SaveResult(true, cloudUrl, null);
                } else {
                    return new SaveResult(false, null, response.getMessage());
                }
            }
        } catch (IOException e) {
            return new SaveResult(false, null, e.getMessage());
        }
    }
}

This architecture cleanly separates concerns: loaders handle source acquiistion, savers manage destination logic, and the bridge between them enables any loader to work with any saver. New storage backends or input types can be introduced without cascadnig changes across the system.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.