001package com.credibledoc.substitution.reporting.replacement;
002
003import com.credibledoc.combiner.file.FileService;
004import com.credibledoc.substitution.core.configuration.Configuration;
005import com.credibledoc.substitution.core.content.Content;
006import com.credibledoc.substitution.core.content.ContentGenerator;
007import com.credibledoc.substitution.core.content.ContentGeneratorService;
008import com.credibledoc.substitution.core.context.SubstitutionContext;
009import com.credibledoc.substitution.core.exception.SubstitutionRuntimeException;
010import com.credibledoc.substitution.core.pair.Pair;
011import com.credibledoc.substitution.core.placeholder.Placeholder;
012import com.credibledoc.substitution.core.placeholder.PlaceholderService;
013import com.credibledoc.substitution.core.resource.ResourceService;
014import com.credibledoc.substitution.core.resource.ResourceType;
015import com.credibledoc.substitution.core.resource.TemplateResource;
016import com.credibledoc.substitution.core.template.TemplateService;
017import com.credibledoc.substitution.core.tracking.Trackable;
018import com.credibledoc.substitution.reporting.markdown.MarkdownService;
019import com.credibledoc.substitution.reporting.reportdocument.creator.ReportDocumentCreator;
020import org.slf4j.Logger;
021import org.slf4j.LoggerFactory;
022
023import java.io.File;
024import java.io.FileOutputStream;
025import java.io.OutputStream;
026import java.nio.charset.StandardCharsets;
027import java.nio.file.Files;
028import java.nio.file.Path;
029import java.nio.file.Paths;
030import java.nio.file.StandardCopyOption;
031import java.util.ArrayList;
032import java.util.List;
033
034public class ReplacementService {
035    private static final Logger logger = LoggerFactory.getLogger(ReplacementService.class);
036    
037    public static final String CONTENT_REPLACED = "Content replaced. ";
038
039    /**
040     * Singleton.
041     */
042    private static final ReplacementService instance = new ReplacementService();
043
044    /**
045     * @return The {@link ReplacementService} singleton.
046     */
047    public static ReplacementService getInstance() {
048        return instance;
049    }
050
051    /**
052     * Load template from the {@link TemplateResource}, collect {@link Placeholder}s from the template, replace the
053     * {@link Placeholder}s with generated content and write the template with generated content to a target file.
054     *
055     * @param templateResource source of a template, for example <i>/template/markdown/doc/diagrams.md</i>
056     * @param substitutionContext the current state
057     */
058    public void insertContentIntoTemplate(TemplateResource templateResource, SubstitutionContext substitutionContext) {
059        try {
060            List<String> templatePlaceholders =
061                PlaceholderService.getInstance().parsePlaceholders(templateResource, substitutionContext);
062
063            File generatedFile = getTargetFile(templateResource, substitutionContext);
064
065            if (templatePlaceholders.isEmpty()) {
066                Files.copy(templateResource.getFile().toPath(), generatedFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
067                return;
068            }
069
070            String replacedContent =
071                replaceContent(templateResource, templatePlaceholders, substitutionContext);
072
073            try (OutputStream outputStream = new FileOutputStream(generatedFile)){
074                outputStream.write(replacedContent.getBytes());
075            }
076            logger.trace("File generated. '{}'", generatedFile.getAbsolutePath());
077        } catch (Exception exception) {
078            throw new SubstitutionRuntimeException(exception);
079        }
080    }
081
082    public File getTargetFile(TemplateResource templateResource, SubstitutionContext substitutionContext) {
083        ResourceService resourceService = ResourceService.getInstance();
084        String placeholderResourceRelativePath =
085            resourceService.generatePlaceholderResourceRelativePath(templateResource, substitutionContext);
086        Configuration configuration = substitutionContext.getConfiguration();
087        File generatedFile = new File(configuration.getTargetDirectory() + placeholderResourceRelativePath);
088        File generatedFileDirectory = generatedFile.getParentFile();
089        createDirectoryIfNotExists(generatedFileDirectory);
090        return generatedFile;
091    }
092
093    private String replaceContent(TemplateResource templateResource,
094                                  List<String> templatePlaceholders,
095                                  SubstitutionContext substitutionContext) {
096        String replacedContent =
097            TemplateService.getInstance().getTemplateContent(templateResource, StandardCharsets.UTF_8.name());
098        String lineEnding = FileService.findLineEnding(replacedContent);
099        int position = 1;
100        for (String templatePlaceholder : templatePlaceholders) {
101            Placeholder placeholder = PlaceholderService.getInstance()
102                .parseJsonFromPlaceholder(templatePlaceholder, templateResource, substitutionContext);
103            placeholder.setId(Integer.toString(position++));
104            String contentForReplacement = generateContent(placeholder, substitutionContext);
105            String normalizedContent = contentForReplacement.replaceAll(FileService.ANY_LINE_ENDING, lineEnding);
106            replacedContent = replacedContent.replace(templatePlaceholder, normalizedContent);
107            String json = PlaceholderService.getInstance().writePlaceholderToJson(placeholder);
108            logger.trace("{}{}", CONTENT_REPLACED, json);
109        }
110        return replacedContent;
111    }
112
113    /**
114     * Depends on {@link Placeholder#getClassName()} use its instance for generation
115     * of placeholder content and replace it.
116     *
117     * @param placeholder contains a {@link Placeholder#getClassName()}
118     * @param substitutionContext the current state
119     * @return in case of {@link ReportDocumentCreator} return for example
120     * <pre>![Diagram generated by this application](img/launching.md_1.svg?sanitize=true)</pre>
121     * tag.
122     * <p>
123     * In case of {@link ContentGenerator} return a generated content.
124     */
125    @SuppressWarnings("unchecked")
126    private String generateContent(Placeholder placeholder, SubstitutionContext substitutionContext) {
127        try {
128            Class<?> placeholderClass = Class.forName(placeholder.getClassName());
129            if (ReportDocumentCreator.class.isAssignableFrom(placeholderClass)) {
130                String generatedTag = findPlaceholderAndGenerateDiagram(placeholder, substitutionContext);
131                if (generatedTag != null) {
132                    return generatedTag;
133                }
134            } else if (ContentGenerator.class.isAssignableFrom(placeholderClass)) {
135                return processContentGenerator(placeholder,
136                    substitutionContext,
137                    (Class<? extends ContentGenerator>) placeholderClass);
138            }
139        } catch (ClassNotFoundException classNotFoundException) {
140            throw new SubstitutionRuntimeException("PlaceholderClass cannot be found." +
141                " Placeholder className: '" + placeholder.getClassName() +
142                "', placeholder: " + placeholder, classNotFoundException);
143        }
144        throw new SubstitutionRuntimeException("Cannot generate a content " +
145            "for the placeholder: " + placeholder + "'");
146    }
147
148    private String processContentGenerator(Placeholder placeholder, SubstitutionContext substitutionContext,
149                                           Class<? extends ContentGenerator> placeholderClass) {
150        ContentGenerator contentGenerator =
151            ContentGeneratorService.getInstance().getContentGenerator(placeholderClass);
152
153        Content content = contentGenerator.generate(placeholder, substitutionContext);
154
155        if (contentGenerator instanceof Trackable && !ResourceService.getInstance().isLocatedInJar()) {
156            Trackable trackable = (Trackable) contentGenerator;
157            List<Path> paths = trackable.getFragmentPaths();
158            for (Path path : paths) {
159                Pair<Path, Path> pair = new Pair<>(path, placeholder.getResource().getFile().toPath());
160                List<Pair<Path, Path>> pairs = substitutionContext.getTrackableRepository().getPairs();
161                if (!pairs.contains(pair)) {
162                    pairs.add(pair);
163                }
164            }
165        }
166
167        if (content.getPlantUmlContent() != null) {
168            String svg = MarkdownService.getInstance()
169                .generateDiagram(placeholder, content.getPlantUmlContent(), substitutionContext);
170            if (content.getMarkdownContent() != null) {
171                return svg + content.getMarkdownContent();
172            }
173            return svg;
174        } else {
175            return content.getMarkdownContent();
176        }
177    }
178
179    private String findPlaceholderAndGenerateDiagram(Placeholder placeholder, SubstitutionContext substitutionContext) {
180        for (Placeholder nextPlaceholder : substitutionContext.getPlaceholderRepository().getPlaceholders()) {
181            if (nextPlaceholder.getResource().equals(placeholder.getResource()) &&
182                nextPlaceholder.getId().equals(placeholder.getId())) {
183
184                return MarkdownService.getInstance()
185                    .generateDiagram(nextPlaceholder, null, substitutionContext);
186            }
187        }
188        return null;
189    }
190
191    private void createDirectoryIfNotExists(File directory) {
192        if (!directory.exists()) {
193            boolean created = directory.mkdirs();
194            if (!created) {
195                throw new SubstitutionRuntimeException("Cannot create a new directory '" +
196                    directory.getAbsolutePath() + "'");
197            }
198            logger.info("The new directory created '{}'", directory.getAbsolutePath());
199        }
200    }
201
202    /**
203     * Call the {@link #copyResourcesToTargetDirectory(SubstitutionContext)} method and then for every
204     * {@link TemplateResource} call the {@link #insertContentIntoTemplate(TemplateResource, SubstitutionContext)} method.
205     * @param substitutionContext the current state.
206     */
207    public void replace(SubstitutionContext substitutionContext) {
208        List<TemplateResource> templateResources = copyResourcesToTargetDirectory(substitutionContext);
209        for (TemplateResource templateResource : templateResources) {
210            insertContentIntoTemplate(templateResource, substitutionContext);
211        }
212    }
213
214    public List<TemplateResource> copyResourcesToTargetDirectory(SubstitutionContext substitutionContext) {
215        try {
216            List<TemplateResource> result = new ArrayList<>();
217            Configuration configuration = substitutionContext.getConfiguration();
218            ResourceService resourceService = ResourceService.getInstance();
219            List<TemplateResource> allResources =
220                resourceService.getResources(null, configuration.getTemplatesResource());
221            TemplateService templateService = TemplateService.getInstance();
222            PlaceholderService placeholderService = PlaceholderService.getInstance();
223            logger.info("Templates will be copied to the target directory. Templates number: {}. " +
224                "Target directory: '{}'.", allResources.size(), configuration.getTargetDirectory());
225            for (TemplateResource templateResource : allResources) {
226                List<String> placeholders = placeholderService.parsePlaceholders(templateResource, substitutionContext);
227                if (!placeholders.isEmpty()) {
228                    result.add(templateResource);
229                }
230                if (templateResource.getType() == ResourceType.FILE) {
231                    String targetFileRelativePath =
232                        resourceService.generatePlaceholderResourceRelativePath(templateResource, substitutionContext);
233                    String targetFileAbsolutePath = configuration.getTargetDirectory() + targetFileRelativePath;
234                    logger.trace("Resource will be copied to file. Resource: '{}'. TargetFileAbsolutePath: '{}'",
235                        templateResource, targetFileAbsolutePath);
236                    Path targetPath = Paths.get(targetFileAbsolutePath);
237                    Files.createDirectories(targetPath.getParent());
238                    Files.copy(templateResource.getFile().toPath(), targetPath, StandardCopyOption.REPLACE_EXISTING);
239                } else if (templateResource.getType() == ResourceType.CLASSPATH) {
240                    if (containsDotInName(templateResource.getPath())) {
241                        String targetFileRelativePath =
242                            resourceService.generatePlaceholderResourceRelativePath(templateResource,
243                                substitutionContext);
244                        String targetFileAbsolutePath = configuration.getTargetDirectory() + targetFileRelativePath;
245                        logger.trace("Resource will be copied to file. Resource: '{}'. TargetFileAbsolutePath: '{}'",
246                            templateResource, targetFileAbsolutePath);
247                        File file = templateService.exportResource(templateResource.getPath(), targetFileAbsolutePath);
248                        logger.trace("Resource copied to file: '{}'", file.getAbsolutePath());
249                    }
250                } else {
251                    throw new IllegalArgumentException("Unknown ResourceType " + templateResource.getType());
252                }
253            }
254            return result;
255        } catch (Exception e) {
256            throw new SubstitutionRuntimeException(e);
257        }
258    }
259
260    /**
261     * @param resource for example '/template/markdown/' is a directory, and '/template/markdown/README.md' is a file.
262     * @return 'False' if this resource is directory
263     */
264    private boolean containsDotInName(String resource) {
265        int index = resource.lastIndexOf('/');
266        if (index == -1) {
267            index = 0;
268        }
269        String fileName = resource.substring(index);
270        return fileName.contains(".");
271    }
272}