001package com.credibledoc.substitution.reporting.markdown;
002
003import com.credibledoc.plantuml.exception.PlantumlRuntimeException;
004import com.credibledoc.plantuml.svggenerator.SvgGeneratorService;
005import com.credibledoc.substitution.core.configuration.Configuration;
006import com.credibledoc.substitution.core.context.SubstitutionContext;
007import com.credibledoc.substitution.core.exception.SubstitutionRuntimeException;
008import com.credibledoc.substitution.core.placeholder.Placeholder;
009import com.credibledoc.substitution.core.resource.ResourceService;
010import com.credibledoc.substitution.reporting.placeholder.PlaceholderToReportDocumentService;
011import com.credibledoc.substitution.core.replacement.ReplacementType;
012import com.credibledoc.substitution.reporting.reportdocument.ReportDocument;
013import org.slf4j.Logger;
014import org.slf4j.LoggerFactory;
015
016import java.io.File;
017import java.io.FileOutputStream;
018import java.io.OutputStream;
019
020/**
021 * This singleton helps to parse templates from the {@link Configuration#getTemplatesResource()} folder, extract
022 * {@link Placeholder}s between {@link Configuration#getPlaceholderBegin()} and {@link Configuration#getPlaceholderEnd()}
023 * tags, create {@link ReportDocument}s which represent a content of the placeholders and generate new documents in the
024 * {@link Configuration#getTargetDirectory()} with the new content instead of placeholders.
025 *
026 * @author Kyrylo Semenko
027 */
028public class MarkdownService {
029    private static final Logger logger = LoggerFactory.getLogger(MarkdownService.class);
030    private static final String SLASH = "/";
031    private static final String IMAGE_DIRECTORY_NAME = "img";
032    private static final String SVG_FILE_EXTENSION = ".svg";
033    private static final String SVG_TAG_BEGIN = "![";
034    private static final String SVG_TAG_MIDDLE = "](";
035    private static final String SVG_TAG_END = "?sanitize=true)";
036    private static final String SYNTAX_ERROR_GENERATED_KEYWORD = "Syntax Error?";
037    private static final String IGNORE_SYNTAX_ERROR_PLACEHOLDER_PARAMETER = "ignoreSyntaxError";
038
039    /**
040     * Singleton.
041     */
042    private static final MarkdownService instance = new MarkdownService();
043
044    /**
045     * @return The {@link MarkdownService} singleton.
046     */
047    public static MarkdownService getInstance() {
048        return instance;
049    }
050
051    /**
052     * Generate SVG image file and a link to the file.
053     * <ul>
054     *     <li>Load template from the {@link Placeholder#getResource()} field</li>
055     *     <li>Create a new file from the template in the {@link Configuration#getTargetDirectory()} directory</li>
056     *     <li>Create a new {@link #IMAGE_DIRECTORY_NAME} directory</li>
057     *     <li>Get a {@link ReportDocument} form the {@link PlaceholderToReportDocumentService}</li>
058     *     <li>Join lines from the {@link ReportDocument#getCacheLines()} list</li>
059     *     <li>And return result of the
060     *     {@link #generateSvgFileAndTagForMarkdown(File, File, String, Placeholder, boolean)} method</li>
061     * </ul>
062     *
063     * @param placeholder the state object
064     * @param plantUml PlantUML source notation. If the value is 'null', the value will be obtained from
065     *                 {@link ReportDocument} which is stored in
066     *                 {@link PlaceholderToReportDocumentService#getReportDocument(Placeholder)}.
067     * @param substitutionContext the current state
068     * @return A part of markdown document with link to generated SVG image.
069     */
070    public String generateDiagram(Placeholder placeholder, String plantUml, SubstitutionContext substitutionContext) {
071        ResourceService resourceService = ResourceService.getInstance();
072        String placeholderResourceRelativePath =
073                resourceService.generatePlaceholderResourceRelativePath(placeholder.getResource(), substitutionContext);
074
075        Configuration configuration = substitutionContext.getConfiguration();
076        File mdFile = new File(configuration.getTargetDirectory() + placeholderResourceRelativePath);
077        File directory = mdFile.getParentFile();
078        File imageDirectory = new File(directory, IMAGE_DIRECTORY_NAME);
079        createDirectoryIfNotExists(imageDirectory);
080        if (plantUml == null) {
081            ReportDocument reportDocument = PlaceholderToReportDocumentService.getInstance()
082                .getReportDocument(placeholder);
083            plantUml = String.join(System.lineSeparator(), reportDocument.getCacheLines());
084        }
085        String placeholderDescription = placeholder.getDescription();
086
087        if (plantUml.isEmpty()) {
088            return "Cannot generate diagram because source content not found. " +
089                "PlaceholderDescription: '" + placeholderDescription + "'.";
090        }
091
092        boolean replaceFilterId = "true".equals(substitutionContext.getConfiguration().getReplaceFilterId());
093        return generateSvgFileAndTagForMarkdown(
094                mdFile,
095                imageDirectory,
096                plantUml,
097                placeholder,
098                replaceFilterId
099            );
100    }
101
102    private String generateSvgFileAndTagForMarkdown(File mdFile,
103                                                    File imageDirectory,
104                                                    String plantUml,
105                                                    Placeholder placeholder,
106                                                    boolean replaceFilterId) {
107        try {
108            String svg = SvgGeneratorService.getInstance().generateSvgFromPlantUml(plantUml);
109            
110            if (replaceFilterId) {
111                svg = replaceFilterId(svg);
112            }
113
114            File svgFile = new File(imageDirectory,
115                mdFile.getName() + "_" + placeholder.getId() + SVG_FILE_EXTENSION);
116
117            try (OutputStream outputStream = new FileOutputStream(svgFile)) {
118                outputStream.write(svg.getBytes());
119            }
120            logger.debug("File created: {}", svgFile.getAbsolutePath());
121            
122            boolean ignoreSyntaxError = placeholder.getParameters()
123                .containsKey(IGNORE_SYNTAX_ERROR_PLACEHOLDER_PARAMETER) &&
124                placeholder.getParameters().get(IGNORE_SYNTAX_ERROR_PLACEHOLDER_PARAMETER).equals("false");
125            
126            if (!ignoreSyntaxError && svg.contains(SYNTAX_ERROR_GENERATED_KEYWORD)) {
127                throw new PlantumlRuntimeException("SVG contains '" + SYNTAX_ERROR_GENERATED_KEYWORD
128                    + "' substring. SVG: '" + svg
129                    + "'. " + System.lineSeparator()
130                    + placeholder);
131            }
132            if (placeholder.getParameters().get(ReplacementType.TARGET_FORMAT) != null) {
133                ReplacementType replacementType =
134                    ReplacementType.valueOf(placeholder.getParameters().get(ReplacementType.TARGET_FORMAT));
135                if (ReplacementType.HTML_EMBEDDED == replacementType) {
136                    return svg;
137                }
138                throw new SubstitutionRuntimeException("Unknown " + ReplacementType.class.getSimpleName() + " " +
139                    "value " + replacementType);
140            } else {
141                return SVG_TAG_BEGIN + placeholder.getDescription() + SVG_TAG_MIDDLE + imageDirectory.getName() +
142                    SLASH + svgFile.getName() + SVG_TAG_END;
143            }
144        } catch (Exception e) {
145            throw new SubstitutionRuntimeException(e);
146        }
147    }
148
149    /**
150     * Replace all occurrences of the generated ids with constants. Example of filter line:
151     * <pre>{@code
152     * <filter height="300%" id="f10gnta8ifhhre" width="300%" x="-1" y="-1">
153     * }</pre>
154     * @param svg for replacing
155     * @return The svg with replaced ids, for example id="f10gnta8ifhhre" will be replaced with id="1"
156     */
157    private String replaceFilterId(String svg) {
158        int beginFilter = svg.indexOf("<filter ");
159        if (beginFilter == -1) {
160            return svg;
161        }
162        String beginIdPattern = " id=\"";
163        int beginId = svg.indexOf(beginIdPattern, beginFilter);
164        if (beginId == -1) {
165            return svg;
166        }
167        int endId = svg.indexOf("\" ", beginId + beginIdPattern.length());
168        String oldId = svg.substring(beginId + beginIdPattern.length(), endId);
169
170        return svg.replace(oldId, "1");
171    }
172
173    private void createDirectoryIfNotExists(File directory) {
174        if (!directory.exists()) {
175            boolean created = directory.mkdirs();
176            if (!created) {
177                throw new SubstitutionRuntimeException("Cannot create a new directory '" +
178                    directory.getAbsolutePath() + "'");
179            }
180            logger.info("The new directory created '{}'", directory.getAbsolutePath());
181        }
182    }
183}