1 | package fr.sii.ogham.html.translator; | |
2 | ||
3 | import static fr.sii.ogham.core.util.HtmlUtils.getDistinctCssImageUrls; | |
4 | import static fr.sii.ogham.core.util.HtmlUtils.getDistinctImageUrls; | |
5 | import static fr.sii.ogham.core.util.HtmlUtils.skipExternalUrls; | |
6 | import static fr.sii.ogham.html.inliner.impl.jsoup.ImageInlineUtils.removeOghamAttributes; | |
7 | import static fr.sii.ogham.html.inliner.impl.regexp.CssImageInlineUtils.removeOghamProperties; | |
8 | ||
9 | import java.io.ByteArrayInputStream; | |
10 | import java.io.File; | |
11 | import java.io.IOException; | |
12 | import java.util.ArrayList; | |
13 | import java.util.List; | |
14 | ||
15 | import org.slf4j.Logger; | |
16 | import org.slf4j.LoggerFactory; | |
17 | ||
18 | import fr.sii.ogham.core.exception.handler.ContentTranslatorException; | |
19 | import fr.sii.ogham.core.exception.handler.ImageInliningException; | |
20 | import fr.sii.ogham.core.exception.mimetype.MimeTypeDetectionException; | |
21 | import fr.sii.ogham.core.exception.resource.ResourceResolutionException; | |
22 | import fr.sii.ogham.core.message.content.Content; | |
23 | import fr.sii.ogham.core.message.content.HasResourcePath; | |
24 | import fr.sii.ogham.core.message.content.MayHaveStringContent; | |
25 | import fr.sii.ogham.core.message.content.StringContent; | |
26 | import fr.sii.ogham.core.message.content.UpdatableStringContent; | |
27 | import fr.sii.ogham.core.mimetype.MimeTypeProvider; | |
28 | import fr.sii.ogham.core.resource.path.RelativePath; | |
29 | import fr.sii.ogham.core.resource.path.RelativePathResolver; | |
30 | import fr.sii.ogham.core.resource.path.ResourcePath; | |
31 | import fr.sii.ogham.core.resource.path.UnresolvedPath; | |
32 | import fr.sii.ogham.core.resource.resolver.ResourceResolver; | |
33 | import fr.sii.ogham.core.translator.content.ContentTranslator; | |
34 | import fr.sii.ogham.core.util.IOUtils; | |
35 | import fr.sii.ogham.email.message.content.ContentWithAttachments; | |
36 | import fr.sii.ogham.html.inliner.ContentWithImages; | |
37 | import fr.sii.ogham.html.inliner.ImageInliner; | |
38 | import fr.sii.ogham.html.inliner.ImageResource; | |
39 | ||
40 | /** | |
41 | * Translator that transforms HTML content. If not HTML, the translator has no | |
42 | * effect. The HTML is analyzed in order to find images. For each found image, | |
43 | * it uses the resource resolver in order to find the image file. Once all | |
44 | * images are found, the HTML is transformed in order to inline the images. The | |
45 | * images can be inlined using several methods: | |
46 | * <ul> | |
47 | * <li>Base64 inliner to convert images to base64 equivalent</li> | |
48 | * <li>Extract images and generate attachments to join to the email</li> | |
49 | * <li>Maybe anything else</li> | |
50 | * </ul> | |
51 | * | |
52 | * @author Aurélien Baudet | |
53 | * | |
54 | */ | |
55 | public class InlineImageTranslator implements ContentTranslator { | |
56 | private static final Logger LOG = LoggerFactory.getLogger(InlineImageTranslator.class); | |
57 | | |
58 | /** | |
59 | * The image inliner | |
60 | */ | |
61 | private final ImageInliner inliner; | |
62 | ||
63 | /** | |
64 | * The resource resolver used to find images | |
65 | */ | |
66 | private final ResourceResolver resourceResolver; | |
67 | ||
68 | /** | |
69 | * The provider that detects the mimetype for each image | |
70 | */ | |
71 | private final MimeTypeProvider mimetypeProvider; | |
72 | | |
73 | /** | |
74 | * Provides an instance used to resolve relative path from source path and relative path | |
75 | */ | |
76 | private final RelativePathResolver relativePathProvider; | |
77 | ||
78 | public InlineImageTranslator(ImageInliner inliner, ResourceResolver resourceResolver, MimeTypeProvider mimetypeProvider, RelativePathResolver relativePathProvider) { | |
79 | super(); | |
80 | this.inliner = inliner; | |
81 | this.resourceResolver = resourceResolver; | |
82 | this.mimetypeProvider = mimetypeProvider; | |
83 | this.relativePathProvider = relativePathProvider; | |
84 | } | |
85 | ||
86 | @Override | |
87 | public Content translate(Content content) throws ContentTranslatorException { | |
88 |
10
1. translate : negated conditional → SURVIVED 2. translate : negated conditional → NO_COVERAGE 3. translate : negated conditional → SURVIVED 4. translate : negated conditional → NO_COVERAGE 5. translate : negated conditional → TIMED_OUT 6. translate : negated conditional → TIMED_OUT 7. translate : negated conditional → KILLED 8. translate : negated conditional → KILLED 9. translate : negated conditional → KILLED 10. translate : negated conditional → KILLED |
if (content instanceof MayHaveStringContent && ((MayHaveStringContent) content).canProvideString()) { |
89 | String stringContent = ((MayHaveStringContent) content).asString(); | |
90 | List<String> images = skipExternalUrls(merge(getDistinctImageUrls(stringContent), getDistinctCssImageUrls(stringContent))); | |
91 |
5
1. translate : negated conditional → NO_COVERAGE 2. translate : negated conditional → SURVIVED 3. translate : negated conditional → TIMED_OUT 4. translate : negated conditional → KILLED 5. translate : negated conditional → KILLED |
if (!images.isEmpty()) { |
92 | LOG.debug("inlining {} images", images.size()); | |
93 | // prepare list of images paths/urls with their content | |
94 | List<ImageResource> imageResources = load(getSourcePath(content), images); | |
95 | // generate new HTML with inlined images | |
96 | ContentWithImages contentWithImages = inliner.inline(stringContent, imageResources); | |
97 | // remove ogham attributes | |
98 | ContentWithImages cleaned = clean(contentWithImages); | |
99 | // update the HTML content | |
100 | Content inlinedContent = updateHtmlContent(content, cleaned); | |
101 | LOG.debug("{} images inlined", contentWithImages.getAttachments().size()); | |
102 | // if it was already a content with attachments then update it otherwise create a new one | |
103 |
3
1. translate : replaced return value with null for fr/sii/ogham/html/translator/InlineImageTranslator::translate → NO_COVERAGE 2. translate : replaced return value with null for fr/sii/ogham/html/translator/InlineImageTranslator::translate → KILLED 3. translate : replaced return value with null for fr/sii/ogham/html/translator/InlineImageTranslator::translate → KILLED |
return generateFinalContent(content, cleaned, inlinedContent); |
104 | } | |
105 | } else { | |
106 | LOG.debug("Neither content usable as string nor HTML. Skip image inlining"); | |
107 | LOG.trace("content: {}", content); | |
108 | } | |
109 |
5
1. translate : replaced return value with null for fr/sii/ogham/html/translator/InlineImageTranslator::translate → SURVIVED 2. translate : replaced return value with null for fr/sii/ogham/html/translator/InlineImageTranslator::translate → NO_COVERAGE 3. translate : replaced return value with null for fr/sii/ogham/html/translator/InlineImageTranslator::translate → TIMED_OUT 4. translate : replaced return value with null for fr/sii/ogham/html/translator/InlineImageTranslator::translate → KILLED 5. translate : replaced return value with null for fr/sii/ogham/html/translator/InlineImageTranslator::translate → KILLED |
return content; |
110 | } | |
111 | ||
112 | private static List<String> merge(List<String> distinctImageUrls, List<String> distinctCssImageUrls) { | |
113 | List<String> merged = new ArrayList<>(distinctImageUrls); | |
114 | merged.addAll(distinctCssImageUrls); | |
115 |
5
1. merge : replaced return value with Collections.emptyList for fr/sii/ogham/html/translator/InlineImageTranslator::merge → SURVIVED 2. merge : replaced return value with Collections.emptyList for fr/sii/ogham/html/translator/InlineImageTranslator::merge → NO_COVERAGE 3. merge : replaced return value with Collections.emptyList for fr/sii/ogham/html/translator/InlineImageTranslator::merge → TIMED_OUT 4. merge : replaced return value with Collections.emptyList for fr/sii/ogham/html/translator/InlineImageTranslator::merge → KILLED 5. merge : replaced return value with Collections.emptyList for fr/sii/ogham/html/translator/InlineImageTranslator::merge → KILLED |
return merged; |
116 | } | |
117 | ||
118 | private static ContentWithImages clean(ContentWithImages contentWithImages) { | |
119 | String html = contentWithImages.getContent(); | |
120 | html = removeOghamAttributes(html); | |
121 | html = removeOghamProperties(html); | |
122 |
3
1. clean : removed call to fr/sii/ogham/html/inliner/ContentWithImages::setContent → NO_COVERAGE 2. clean : removed call to fr/sii/ogham/html/inliner/ContentWithImages::setContent → KILLED 3. clean : removed call to fr/sii/ogham/html/inliner/ContentWithImages::setContent → KILLED |
contentWithImages.setContent(html); |
123 |
3
1. clean : replaced return value with null for fr/sii/ogham/html/translator/InlineImageTranslator::clean → NO_COVERAGE 2. clean : replaced return value with null for fr/sii/ogham/html/translator/InlineImageTranslator::clean → KILLED 3. clean : replaced return value with null for fr/sii/ogham/html/translator/InlineImageTranslator::clean → KILLED |
return contentWithImages; |
124 | } | |
125 | ||
126 | private static ResourcePath getSourcePath(Content content) { | |
127 |
3
1. getSourcePath : negated conditional → NO_COVERAGE 2. getSourcePath : negated conditional → KILLED 3. getSourcePath : negated conditional → KILLED |
if(content instanceof HasResourcePath) { |
128 |
2
1. getSourcePath : replaced return value with null for fr/sii/ogham/html/translator/InlineImageTranslator::getSourcePath → NO_COVERAGE 2. getSourcePath : replaced return value with null for fr/sii/ogham/html/translator/InlineImageTranslator::getSourcePath → KILLED |
return ((HasResourcePath) content).getPath(); |
129 | } | |
130 |
3
1. getSourcePath : replaced return value with null for fr/sii/ogham/html/translator/InlineImageTranslator::getSourcePath → NO_COVERAGE 2. getSourcePath : replaced return value with null for fr/sii/ogham/html/translator/InlineImageTranslator::getSourcePath → KILLED 3. getSourcePath : replaced return value with null for fr/sii/ogham/html/translator/InlineImageTranslator::getSourcePath → KILLED |
return new UnresolvedPath(""); |
131 | } | |
132 | | |
133 | private List<ImageResource> load(ResourcePath sourcePath, List<String> images) throws ContentTranslatorException { | |
134 | List<ImageResource> imageResources = new ArrayList<>(images.size()); | |
135 | for (String path : images) { | |
136 |
3
1. load : removed call to fr/sii/ogham/html/translator/InlineImageTranslator::load → NO_COVERAGE 2. load : removed call to fr/sii/ogham/html/translator/InlineImageTranslator::load → KILLED 3. load : removed call to fr/sii/ogham/html/translator/InlineImageTranslator::load → KILLED |
load(imageResources, relativePathProvider.resolve(sourcePath, path)); |
137 | } | |
138 |
3
1. load : replaced return value with Collections.emptyList for fr/sii/ogham/html/translator/InlineImageTranslator::load → NO_COVERAGE 2. load : replaced return value with Collections.emptyList for fr/sii/ogham/html/translator/InlineImageTranslator::load → KILLED 3. load : replaced return value with Collections.emptyList for fr/sii/ogham/html/translator/InlineImageTranslator::load → KILLED |
return imageResources; |
139 | } | |
140 | ||
141 | @SuppressWarnings("squid:S1192") | |
142 | private void load(List<ImageResource> imageResources, RelativePath path) throws ContentTranslatorException { | |
143 | try { | |
144 | byte[] imgContent = IOUtils.toByteArray(resourceResolver.getResource(path).getInputStream()); | |
145 | String mimetype = mimetypeProvider.detect(new ByteArrayInputStream(imgContent)).toString(); | |
146 | String imgName = new File(path.getOriginalPath()).getName(); | |
147 | imageResources.add(new ImageResource(imgName, path.getRelativePath().getOriginalPath(), path, imgContent, mimetype)); | |
148 | } catch (IOException e) { | |
149 | throw new ImageInliningException("Failed to inline image file " + path + " because it can't be read", e); | |
150 | } catch (ResourceResolutionException e) { | |
151 | throw new ImageInliningException("Failed to inline image file " + path + " because it can't be resolved", e); | |
152 | } catch (MimeTypeDetectionException e) { | |
153 | throw new ImageInliningException("Failed to inline image file " + path + " because mimetype can't be detected", e); | |
154 | } | |
155 | } | |
156 | ||
157 | private static Content updateHtmlContent(Content content, ContentWithImages contentWithImages) { | |
158 |
2
1. updateHtmlContent : negated conditional → NO_COVERAGE 2. updateHtmlContent : negated conditional → SURVIVED |
if(content instanceof UpdatableStringContent) { |
159 | LOG.debug("Content is updatable => update it with inlined images"); | |
160 |
3
1. updateHtmlContent : removed call to fr/sii/ogham/core/message/content/UpdatableStringContent::setStringContent → NO_COVERAGE 2. updateHtmlContent : removed call to fr/sii/ogham/core/message/content/UpdatableStringContent::setStringContent → KILLED 3. updateHtmlContent : removed call to fr/sii/ogham/core/message/content/UpdatableStringContent::setStringContent → KILLED |
((UpdatableStringContent) content).setStringContent(contentWithImages.getContent()); |
161 |
3
1. updateHtmlContent : replaced return value with null for fr/sii/ogham/html/translator/InlineImageTranslator::updateHtmlContent → NO_COVERAGE 2. updateHtmlContent : replaced return value with null for fr/sii/ogham/html/translator/InlineImageTranslator::updateHtmlContent → KILLED 3. updateHtmlContent : replaced return value with null for fr/sii/ogham/html/translator/InlineImageTranslator::updateHtmlContent → KILLED |
return content; |
162 | } | |
163 | LOG.info("Content is not updatable => create a new StringContent for image inlining result"); | |
164 |
1
1. updateHtmlContent : replaced return value with null for fr/sii/ogham/html/translator/InlineImageTranslator::updateHtmlContent → NO_COVERAGE |
return new StringContent(contentWithImages.getContent()); |
165 | } | |
166 | ||
167 | private static Content generateFinalContent(Content content, ContentWithImages contentWithImages, Content inlinedContent) { | |
168 |
3
1. generateFinalContent : negated conditional → NO_COVERAGE 2. generateFinalContent : negated conditional → KILLED 3. generateFinalContent : negated conditional → KILLED |
if(content instanceof ContentWithAttachments) { |
169 | ContentWithAttachments finalContent = (ContentWithAttachments) content; | |
170 |
1
1. generateFinalContent : removed call to fr/sii/ogham/email/message/content/ContentWithAttachments::addAttachments → NO_COVERAGE |
finalContent.addAttachments(contentWithImages.getAttachments()); |
171 |
1
1. generateFinalContent : removed call to fr/sii/ogham/email/message/content/ContentWithAttachments::setContent → NO_COVERAGE |
finalContent.setContent(inlinedContent); |
172 |
1
1. generateFinalContent : replaced return value with null for fr/sii/ogham/html/translator/InlineImageTranslator::generateFinalContent → NO_COVERAGE |
return finalContent; |
173 | } | |
174 |
3
1. generateFinalContent : replaced return value with null for fr/sii/ogham/html/translator/InlineImageTranslator::generateFinalContent → NO_COVERAGE 2. generateFinalContent : replaced return value with null for fr/sii/ogham/html/translator/InlineImageTranslator::generateFinalContent → KILLED 3. generateFinalContent : replaced return value with null for fr/sii/ogham/html/translator/InlineImageTranslator::generateFinalContent → KILLED |
return new ContentWithAttachments(inlinedContent, contentWithImages.getAttachments()); |
175 | } | |
176 | ||
177 | @Override | |
178 | public String toString() { | |
179 |
3
1. toString : replaced return value with "" for fr/sii/ogham/html/translator/InlineImageTranslator::toString → NO_COVERAGE 2. toString : replaced return value with "" for fr/sii/ogham/html/translator/InlineImageTranslator::toString → SURVIVED 3. toString : replaced return value with "" for fr/sii/ogham/html/translator/InlineImageTranslator::toString → TIMED_OUT |
return "InlineImageTranslator"; |
180 | } | |
181 | | |
182 | } | |
Mutations | ||
88 |
1.1 2.2 3.3 4.4 5.5 6.6 7.7 8.8 9.9 10.10 |
|
91 |
1.1 2.2 3.3 4.4 5.5 |
|
103 |
1.1 2.2 3.3 |
|
109 |
1.1 2.2 3.3 4.4 5.5 |
|
115 |
1.1 2.2 3.3 4.4 5.5 |
|
122 |
1.1 2.2 3.3 |
|
123 |
1.1 2.2 3.3 |
|
127 |
1.1 2.2 3.3 |
|
128 |
1.1 2.2 |
|
130 |
1.1 2.2 3.3 |
|
136 |
1.1 2.2 3.3 |
|
138 |
1.1 2.2 3.3 |
|
158 |
1.1 2.2 |
|
160 |
1.1 2.2 3.3 |
|
161 |
1.1 2.2 3.3 |
|
164 |
1.1 |
|
168 |
1.1 2.2 3.3 |
|
170 |
1.1 |
|
171 |
1.1 |
|
172 |
1.1 |
|
174 |
1.1 2.2 3.3 |
|
179 |
1.1 2.2 3.3 |