InlineImageTranslator.java
package fr.sii.ogham.html.translator;
import static fr.sii.ogham.core.util.HtmlUtils.getDistinctCssImageUrls;
import static fr.sii.ogham.core.util.HtmlUtils.getDistinctImageUrls;
import static fr.sii.ogham.core.util.HtmlUtils.skipExternalUrls;
import static fr.sii.ogham.html.inliner.impl.jsoup.ImageInlineUtils.removeOghamAttributes;
import static fr.sii.ogham.html.inliner.impl.regexp.CssImageInlineUtils.removeOghamProperties;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import fr.sii.ogham.core.exception.handler.ContentTranslatorException;
import fr.sii.ogham.core.exception.handler.ImageInliningException;
import fr.sii.ogham.core.exception.mimetype.MimeTypeDetectionException;
import fr.sii.ogham.core.exception.resource.ResourceResolutionException;
import fr.sii.ogham.core.message.content.Content;
import fr.sii.ogham.core.message.content.HasResourcePath;
import fr.sii.ogham.core.message.content.MayHaveStringContent;
import fr.sii.ogham.core.message.content.StringContent;
import fr.sii.ogham.core.message.content.UpdatableStringContent;
import fr.sii.ogham.core.mimetype.MimeTypeProvider;
import fr.sii.ogham.core.resource.path.RelativePath;
import fr.sii.ogham.core.resource.path.RelativePathResolver;
import fr.sii.ogham.core.resource.path.ResourcePath;
import fr.sii.ogham.core.resource.path.UnresolvedPath;
import fr.sii.ogham.core.resource.resolver.ResourceResolver;
import fr.sii.ogham.core.translator.content.ContentTranslator;
import fr.sii.ogham.core.util.IOUtils;
import fr.sii.ogham.email.message.content.ContentWithAttachments;
import fr.sii.ogham.html.inliner.ContentWithImages;
import fr.sii.ogham.html.inliner.ImageInliner;
import fr.sii.ogham.html.inliner.ImageResource;
/**
* Translator that transforms HTML content. If not HTML, the translator has no
* effect. The HTML is analyzed in order to find images. For each found image,
* it uses the resource resolver in order to find the image file. Once all
* images are found, the HTML is transformed in order to inline the images. The
* images can be inlined using several methods:
* <ul>
* <li>Base64 inliner to convert images to base64 equivalent</li>
* <li>Extract images and generate attachments to join to the email</li>
* <li>Maybe anything else</li>
* </ul>
*
* @author Aurélien Baudet
*
*/
public class InlineImageTranslator implements ContentTranslator {
private static final Logger LOG = LoggerFactory.getLogger(InlineImageTranslator.class);
/**
* The image inliner
*/
private final ImageInliner inliner;
/**
* The resource resolver used to find images
*/
private final ResourceResolver resourceResolver;
/**
* The provider that detects the mimetype for each image
*/
private final MimeTypeProvider mimetypeProvider;
/**
* Provides an instance used to resolve relative path from source path and relative path
*/
private final RelativePathResolver relativePathProvider;
public InlineImageTranslator(ImageInliner inliner, ResourceResolver resourceResolver, MimeTypeProvider mimetypeProvider, RelativePathResolver relativePathProvider) {
super();
this.inliner = inliner;
this.resourceResolver = resourceResolver;
this.mimetypeProvider = mimetypeProvider;
this.relativePathProvider = relativePathProvider;
}
@Override
public Content translate(Content content) throws ContentTranslatorException {
if (content instanceof MayHaveStringContent && ((MayHaveStringContent) content).canProvideString()) {
String stringContent = ((MayHaveStringContent) content).asString();
List<String> images = skipExternalUrls(merge(getDistinctImageUrls(stringContent), getDistinctCssImageUrls(stringContent)));
if (!images.isEmpty()) {
LOG.debug("inlining {} images", images.size());
// prepare list of images paths/urls with their content
List<ImageResource> imageResources = load(getSourcePath(content), images);
// generate new HTML with inlined images
ContentWithImages contentWithImages = inliner.inline(stringContent, imageResources);
// remove ogham attributes
ContentWithImages cleaned = clean(contentWithImages);
// update the HTML content
Content inlinedContent = updateHtmlContent(content, cleaned);
LOG.debug("{} images inlined", contentWithImages.getAttachments().size());
// if it was already a content with attachments then update it otherwise create a new one
return generateFinalContent(content, cleaned, inlinedContent);
}
} else {
LOG.debug("Neither content usable as string nor HTML. Skip image inlining");
LOG.trace("content: {}", content);
}
return content;
}
private static List<String> merge(List<String> distinctImageUrls, List<String> distinctCssImageUrls) {
List<String> merged = new ArrayList<>(distinctImageUrls);
merged.addAll(distinctCssImageUrls);
return merged;
}
private static ContentWithImages clean(ContentWithImages contentWithImages) {
String html = contentWithImages.getContent();
html = removeOghamAttributes(html);
html = removeOghamProperties(html);
contentWithImages.setContent(html);
return contentWithImages;
}
private static ResourcePath getSourcePath(Content content) {
if(content instanceof HasResourcePath) {
return ((HasResourcePath) content).getPath();
}
return new UnresolvedPath("");
}
private List<ImageResource> load(ResourcePath sourcePath, List<String> images) throws ContentTranslatorException {
List<ImageResource> imageResources = new ArrayList<>(images.size());
for (String path : images) {
load(imageResources, relativePathProvider.resolve(sourcePath, path));
}
return imageResources;
}
@SuppressWarnings("squid:S1192")
private void load(List<ImageResource> imageResources, RelativePath path) throws ContentTranslatorException {
try {
byte[] imgContent = IOUtils.toByteArray(resourceResolver.getResource(path).getInputStream());
String mimetype = mimetypeProvider.detect(new ByteArrayInputStream(imgContent)).toString();
String imgName = new File(path.getOriginalPath()).getName();
imageResources.add(new ImageResource(imgName, path.getRelativePath().getOriginalPath(), path, imgContent, mimetype));
} catch (IOException e) {
throw new ImageInliningException("Failed to inline image file " + path + " because it can't be read", e);
} catch (ResourceResolutionException e) {
throw new ImageInliningException("Failed to inline image file " + path + " because it can't be resolved", e);
} catch (MimeTypeDetectionException e) {
throw new ImageInliningException("Failed to inline image file " + path + " because mimetype can't be detected", e);
}
}
private static Content updateHtmlContent(Content content, ContentWithImages contentWithImages) {
if(content instanceof UpdatableStringContent) {
LOG.debug("Content is updatable => update it with inlined images");
((UpdatableStringContent) content).setStringContent(contentWithImages.getContent());
return content;
}
LOG.info("Content is not updatable => create a new StringContent for image inlining result");
return new StringContent(contentWithImages.getContent());
}
private static Content generateFinalContent(Content content, ContentWithImages contentWithImages, Content inlinedContent) {
if(content instanceof ContentWithAttachments) {
ContentWithAttachments finalContent = (ContentWithAttachments) content;
finalContent.addAttachments(contentWithImages.getAttachments());
finalContent.setContent(inlinedContent);
return finalContent;
}
return new ContentWithAttachments(inlinedContent, contentWithImages.getAttachments());
}
@Override
public String toString() {
return "InlineImageTranslator";
}
}