InlineCssTranslator.java

package fr.sii.ogham.html.translator;

import static fr.sii.ogham.core.util.HtmlUtils.getDistinctCssUrls;
import static fr.sii.ogham.core.util.HtmlUtils.isHtml;
import static fr.sii.ogham.core.util.HtmlUtils.skipExternalUrls;
import static fr.sii.ogham.html.inliner.impl.jsoup.CssInlineUtils.removeOghamAttributes;

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.CssInliningException;
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.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.html.inliner.CssInliner;
import fr.sii.ogham.html.inliner.ExternalCss;

/**
 * Translator that transforms HTML content. If not HTML, the translator has no
 * effect. The HTML is analyzed in order to find external css files. For each
 * found image, it uses the resource resolver in order to find the css file.
 * Once all css files are found, the HTML is transformed in order to inline the
 * styles.
 * 
 * @author Aurélien Baudet
 *
 */
public class InlineCssTranslator implements ContentTranslator {
	private static final Logger LOG = LoggerFactory.getLogger(InlineCssTranslator.class);
	
	/**
	 * The CSS inliner
	 */
	private final CssInliner cssInliner;

	/**
	 * The resource resolver to find the CSS files
	 */
	private final ResourceResolver resourceResolver;
	
	/**
	 * Provides an instance used to resolve relative path from source path and relative path
	 */
	private final RelativePathResolver relativePathProvider;

	public InlineCssTranslator(CssInliner cssInliner, ResourceResolver resourceResolver, RelativePathResolver relativePathProvider) {
		super();
		this.cssInliner = cssInliner;
		this.resourceResolver = resourceResolver;
		this.relativePathProvider = relativePathProvider;
	}

	@Override
	public Content translate(Content content) throws ContentTranslatorException {
		if (content instanceof MayHaveStringContent && ((MayHaveStringContent) content).canProvideString()) {
			String stringContent = ((MayHaveStringContent) content).asString();
			if (isHtml(stringContent)) {
				List<String> cssFiles = skipExternalUrls(getDistinctCssUrls(stringContent));
				if (!cssFiles.isEmpty()) {
					// prepare list of css files/urls with their content
					List<ExternalCss> cssResources = load(getSourcePath(content), cssFiles);
					// generate the content with inlined css
					String inlinedContentStr = cssInliner.inline(stringContent, cssResources);
					// remove ogham attributes
					String cleaned = clean(inlinedContentStr);
					// update the HTML content
					return updateHtmlContent(content, cleaned);
				}
			}
		} else {
			LOG.debug("Neither content as string nor HTML. Skip CSS inlining");
			LOG.trace("content: {}", content);
		}
		return content;
	}

	private static ResourcePath getSourcePath(Content content) {
		if(content instanceof HasResourcePath) {
			return ((HasResourcePath) content).getPath();
		}
		return new UnresolvedPath("");
	}

	private List<ExternalCss> load(ResourcePath sourcePath, List<String> cssFiles) throws ContentTranslatorException {
		List<ExternalCss> cssResources = new ArrayList<>(cssFiles.size());
		for (String path : cssFiles) {
			load(cssResources, relativePathProvider.resolve(sourcePath, path));
		}
		return cssResources;
	}

	private void load(List<ExternalCss> cssResources, ResourcePath path) throws ContentTranslatorException {
		try {
			cssResources.add(new ExternalCss(path, IOUtils.toString(resourceResolver.getResource(path).getInputStream())));
		} catch (IOException e) {
			throw new CssInliningException("Failed to inline CSS file " + path + " because it can't be read", e);
		} catch (ResourceResolutionException e) {
			throw new CssInliningException("Failed to inline CSS file " + path + " because it can't be resolved", e);
		}
	}

	private static Content updateHtmlContent(Content content, String inlinedContentStr) {
		Content inlinedContent = content;
		if(content instanceof UpdatableStringContent) {
			LOG.debug("Content is updatable => update it with inlined CSS");
			((UpdatableStringContent) inlinedContent).setStringContent(inlinedContentStr);
		} else {
			LOG.info("Content is not updatable => create a new StringContent for CSS inlining result");
			inlinedContent = new StringContent(inlinedContentStr);
		}
		return inlinedContent;
	}
	
	private static String clean(String content) {
		return removeOghamAttributes(content);
	}


	@Override
	public String toString() {
		return "InlineCssTranslator";
	}
	
}