package com.twasyl.slideshowfx.engine.presentation; import com.twasyl.slideshowfx.content.extension.Resource; import com.twasyl.slideshowfx.content.extension.ResourceType; import com.twasyl.slideshowfx.engine.AbstractEngine; import com.twasyl.slideshowfx.engine.EngineException; import com.twasyl.slideshowfx.engine.presentation.configuration.PresentationConfiguration; import com.twasyl.slideshowfx.engine.presentation.configuration.Slide; import com.twasyl.slideshowfx.engine.presentation.configuration.SlideElement; import com.twasyl.slideshowfx.engine.template.DynamicAttribute; import com.twasyl.slideshowfx.engine.template.TemplateEngine; import com.twasyl.slideshowfx.engine.template.configuration.SlideTemplate; import com.twasyl.slideshowfx.engine.template.configuration.TemplateConfiguration; import com.twasyl.slideshowfx.global.configuration.GlobalConfiguration; import com.twasyl.slideshowfx.utils.*; import com.twasyl.slideshowfx.utils.beans.Pair; import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateException; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import javafx.embed.swing.SwingFXUtils; import javafx.scene.image.WritableImage; import org.jsoup.Jsoup; import org.jsoup.nodes.Element; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.*; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; /** * This class manages presentations operation done with SlideshowFX. It is used to open them as well as add, update an * delete slides. * The extension of a presentation is {@code sfx}. * * @author Thierry Wasylczenko * @version 1.2 * @since SlideshowFX 1.0 */ public class PresentationEngine extends AbstractEngine<PresentationConfiguration> { private static final Logger LOGGER = Logger.getLogger(PresentationEngine.class.getName()); /** * The default extension for presentation archives. Value is {@value #DEFAULT_ARCHIVE_EXTENSION}. */ public static final String DEFAULT_ARCHIVE_EXTENSION = "sfx"; /** * The default value, containing the dot, for presentation archives. */ public static final String DEFAULT_DOTTED_ARCHIVE_EXTENSION = ".".concat(DEFAULT_ARCHIVE_EXTENSION); private static final String TEMPLATE_SLIDE_NUMBER_TOKEN = "slideNumber"; private static final String TEMPLATE_SFX_JAVASCRIPT_RESOURCES_TOKEN = "sfxJavascriptResources"; private static final String TEMPLATE_SFX_CALLBACK_TOKEN = "sfxCallback"; private static final String TEMPLATE_SLIDE_ID_PREFIX_TOKEN = "slideIdPrefix"; private static final String TEMPLATE_SFX_CONTENT_DEFINER_SCRIPT = "/com/twasyl/slideshowfx/js/setField.js"; private static final String TEMPLATE_SFX_SNIPPET_EXECUTOR_SCRIPT = "/com/twasyl/slideshowfx/js/snippetExecutor.js"; private static final String TEMPLATE_SFX_CALLBACK_SCRIPT = "/com/twasyl/slideshowfx/js/sendInformationToSlideshowFX.js"; private static final String TEMPLATE_SFX_QUIZ_CALLER_SCRIPT = "/com/twasyl/slideshowfx/js/quizCaller.js"; private static final String TEMPLATE_SFX_CALLBACK_CALL = "sendInformationToSlideshowFX(this);"; private TemplateEngine templateEngine; private boolean modifiedSinceLatestSave; public PresentationEngine() { super(DEFAULT_ARCHIVE_EXTENSION, "presentation-config.json"); this.templateEngine = new TemplateEngine(); Presentations.register(this); } @Override public boolean checkConfiguration() throws EngineException { return false; } @Override public PresentationConfiguration readConfiguration(Reader reader) throws NullPointerException, IllegalArgumentException, IOException { if (reader == null) throw new NullPointerException("The configuration reader can not be null"); final PresentationConfiguration presentationConfiguration = new PresentationConfiguration(); presentationConfiguration.setPresentationFile(new File(this.getWorkingDirectory(), PresentationConfiguration.DEFAULT_PRESENTATION_FILENAME)); JsonObject configurationJson = JSONHelper.readFromReader(reader); JsonObject presentationJson = configurationJson.getJsonObject(PresentationConfiguration.PRESENTATION); presentationConfiguration.setId(presentationJson.getLong(PresentationConfiguration.PRESENTATION_ID, System.currentTimeMillis())); if (presentationJson.getJsonArray(PresentationConfiguration.PRESENTATION_CUSTOM_RESOURCES) != null) { presentationJson.getJsonArray(PresentationConfiguration.PRESENTATION_CUSTOM_RESOURCES) .forEach(customResource -> { final Resource resource = new Resource( ResourceType.valueOf(((JsonObject) customResource).getString(PresentationConfiguration.CUSTOM_RESOURCE_TYPE)), new String(Base64.getDecoder().decode(((JsonObject) customResource).getString(PresentationConfiguration.CUSTOM_RESOURCE_CONTENT))) ); presentationConfiguration.getCustomResources().add(resource); }); } if (presentationJson.getJsonArray(PresentationConfiguration.PRESENTATION_VARIABLES) != null) { presentationJson.getJsonArray(PresentationConfiguration.PRESENTATION_VARIABLES) .forEach(variableJson -> { final Pair<String, String> variable = new Pair<>(); variable.setKey(((JsonObject) variableJson).getString(PresentationConfiguration.VARIABLE_NAME)); variable.setValue(new String(Base64.getDecoder().decode(((JsonObject) variableJson).getString(PresentationConfiguration.VARIABLE_VALUE)))); presentationConfiguration.getVariables().add(variable); }); } presentationJson.getJsonArray(PresentationConfiguration.SLIDES) .forEach(slideJson -> { final Slide slide = new Slide(); slide.setId(((JsonObject) slideJson).getString(PresentationConfiguration.SLIDE_ID)); slide.setSlideNumber(((JsonObject) slideJson).getString(PresentationConfiguration.SLIDE_NUMBER)); slide.setTemplate(this.templateEngine.getConfiguration().getSlideTemplate(((JsonObject) slideJson).getInteger( PresentationConfiguration.SLIDE_TEMPLATE_ID))); try { final File thumbnailFile = this.getThumbnailFile(slide); if (thumbnailFile.exists()) { slide.setThumbnail(this.getThumbnailImage(thumbnailFile)); } } catch (IOException e) { LOGGER.log(Level.INFO, "Error setting the thumbnail", e); } ((JsonObject) slideJson).getJsonArray(PresentationConfiguration.SLIDE_ELEMENTS) .forEach(slideElementJson -> { final SlideElement slideElement = new SlideElement(); slideElement.setTemplate(slide.getTemplate().getSlideElementTemplate(((JsonObject) slideElementJson).getInteger( PresentationConfiguration.SLIDE_ELEMENT_TEMPLATE_ID))); slideElement.setId(((JsonObject) slideElementJson).getString(PresentationConfiguration.SLIDE_ELEMENT_ELEMENT_ID)); slideElement.setOriginalContentCode(((JsonObject) slideElementJson).getString(PresentationConfiguration.SLIDE_ELEMENT_ORIGINAL_CONTENT_CODE)); slideElement.setOriginalContentAsBase64(((JsonObject) slideElementJson).getString(PresentationConfiguration.SLIDE_ELEMENT_ORIGINAL_CONTENT)); slideElement.setHtmlContentAsBase64(((JsonObject) slideElementJson).getString(PresentationConfiguration.SLIDE_ELEMENT_HTML_CONTENT)); slide.getElements().add(slideElement); }); presentationConfiguration.getSlides().add(slide); }); return presentationConfiguration; } /** * Get the thumbnail file for the given slide. This methods only creates a {@link File} without checking its existence. * The file is supposed to be found in the {@link TemplateConfiguration#getSlidesThumbnailDirectory() thumbnail directory} * of the template configuration of this presentation. * * @param slide The slide to get the thumbnail file for. * @return The supposed file corresponding to the thumbnail. */ private File getThumbnailFile(final Slide slide) { final File thumbnailFile = new File(this.templateEngine.getConfiguration().getSlidesThumbnailDirectory(), slide.getSlideNumber().concat(".png")); return thumbnailFile; } /** * Get the {@link WritableImage image} located in the {@link File thumbnail file}. * * @param thumbnailFile The file of the image. * @return The thumbnail image. * @throws IOException If something went wrong. */ private WritableImage getThumbnailImage(final File thumbnailFile) throws IOException { final BufferedImage bufferedImage = ImageIO.read(thumbnailFile); final WritableImage image = SwingFXUtils.toFXImage(bufferedImage, null); return image; } @Override public void writeConfiguration(Writer writer) throws NullPointerException, IOException { if (writer == null) throw new NullPointerException("The configuration to write into can not be null"); if (this.configuration != null) { final JsonObject presentationJson = new JsonObject(); final JsonArray slidesJson = new JsonArray(); final JsonArray customResourcesJson = new JsonArray(); final JsonArray variablesJson = new JsonArray(); presentationJson.put(PresentationConfiguration.PRESENTATION_ID, this.configuration.getId()); this.configuration.getCustomResources() .stream() .forEach(resource -> { final JsonObject resourceJson = new JsonObject() .put(PresentationConfiguration.CUSTOM_RESOURCE_TYPE, resource.getType().name()) .put(PresentationConfiguration.CUSTOM_RESOURCE_CONTENT, Base64.getEncoder().encodeToString(resource.getContent().getBytes())); customResourcesJson.add(resourceJson); }); this.configuration.getVariables() .forEach(variable -> { final JsonObject variableJson = new JsonObject() .put(PresentationConfiguration.VARIABLE_NAME, variable.getKey()) .put(PresentationConfiguration.VARIABLE_VALUE, Base64.getEncoder().encodeToString(variable.getValue().getBytes())); variablesJson.add(variableJson); }); this.configuration.getSlides() .stream() .forEach(slide -> { final JsonArray elementsJson = new JsonArray(); final JsonObject slideJson = new JsonObject(); slideJson.put(PresentationConfiguration.SLIDE_TEMPLATE_ID, slide.getTemplate().getId()) .put(PresentationConfiguration.SLIDE_ID, slide.getId()) .put(PresentationConfiguration.SLIDE_NUMBER, slide.getSlideNumber()); slide.getElements() .stream() .forEach(slideElement -> { final JsonObject elementJson = new JsonObject(); elementJson.put(PresentationConfiguration.SLIDE_ELEMENT_TEMPLATE_ID, slideElement.getTemplate().getId()) .put(PresentationConfiguration.SLIDE_ELEMENT_ELEMENT_ID, slideElement.getId()) .put(PresentationConfiguration.SLIDE_ELEMENT_ORIGINAL_CONTENT_CODE, slideElement.getOriginalContentCode()) .put(PresentationConfiguration.SLIDE_ELEMENT_ORIGINAL_CONTENT, slideElement.getOriginalContentAsBase64()) .put(PresentationConfiguration.SLIDE_ELEMENT_HTML_CONTENT, slideElement.getHtmlContentAsBase64()); elementsJson.add(elementJson); }); slideJson.put(PresentationConfiguration.SLIDE_ELEMENTS, elementsJson); slidesJson.add(slideJson); }); presentationJson.put(PresentationConfiguration.PRESENTATION_CUSTOM_RESOURCES, customResourcesJson); presentationJson.put(PresentationConfiguration.PRESENTATION_VARIABLES, variablesJson); presentationJson.put(PresentationConfiguration.SLIDES, slidesJson); final JsonObject finalObject = new JsonObject(); finalObject.put(PresentationConfiguration.PRESENTATION, presentationJson); JSONHelper.writeObject(finalObject, writer); } } @Override public void loadArchive(File file) throws IllegalArgumentException, NullPointerException, IOException, IllegalAccessException { if (file == null) throw new NullPointerException("The archive file can not be null"); if (!file.exists()) throw new FileNotFoundException("The archive file does not exist"); if (!file.canRead()) throw new IllegalAccessException("The archive file can not be read"); if (!file.getName().endsWith(this.getArchiveExtension())) throw new IllegalArgumentException("The extension of the archive is not valid"); this.setModifiedSinceLatestSave(false); this.setArchive(file); this.setWorkingDirectory(this.generateWorkingDirectory()); ZipUtils.unzip(this.getArchive(), this.getWorkingDirectory()); // The template configuration has to be read and set this.templateEngine = new TemplateEngine(); this.templateEngine.setWorkingDirectory(this.getWorkingDirectory()); this.templateEngine.setConfiguration(this.templateEngine.readConfiguration()); // Configure the PresentationConfiguration final PresentationConfiguration configuration = this.readConfiguration(); configuration.getVariables().addAll(this.getTemplateConfiguration().getDefaultVariables() .stream() .filter(defVariable -> !configuration.getVariables().contains(defVariable)) .collect(Collectors.toList())); this.setConfiguration(configuration); final Configuration templateConfiguration = TemplateProcessor.getDefaultConfiguration(); templateConfiguration.setDirectoryForTemplateLoading(this.templateEngine.getConfiguration().getFile().getParentFile()); final Map tokens = new HashMap<>(); tokens.put(TEMPLATE_SFX_JAVASCRIPT_RESOURCES_TOKEN, this.buildJavaScriptResourcesToInclude()); // Replacing the template tokens try (final StringWriter writer = new StringWriter()) { final Template documentTemplate = templateConfiguration.getTemplate(this.templateEngine.getConfiguration().getFile().getName()); documentTemplate.process(tokens, writer); writer.flush(); this.configuration.setDocument(Jsoup.parse(writer.toString())); this.savePresentationFile(); } catch (TemplateException e) { LOGGER.log(Level.SEVERE, "Can not parse template", e); } LOGGER.fine("Building presentation file"); // Append the custom resources this.configuration.getCustomResources() .stream() .forEach(this::addCustomResource); // Append the slides' content to the presentation tokens.clear(); tokens.put(TEMPLATE_SFX_CALLBACK_TOKEN, TEMPLATE_SFX_CALLBACK_CALL); tokens.put(TEMPLATE_SLIDE_ID_PREFIX_TOKEN, this.templateEngine.getConfiguration().getSlideIdPrefix()); tokens.putAll(this.configuration.getVariables().stream().collect(Collectors.toMap(Pair::getKey, Pair::getValue))); for (Slide s : this.configuration.getSlides()) { templateConfiguration.setDirectoryForTemplateLoading(s.getTemplate().getFile().getParentFile()); try (final StringWriter writer = new StringWriter()) { tokens.put(TEMPLATE_SLIDE_NUMBER_TOKEN, s.getSlideNumber()); final Template slideTemplate = templateConfiguration.getTemplate(s.getTemplate().getFile().getName()); slideTemplate.process(tokens, writer); writer.flush(); this.configuration.getDocument() .getElementById(this.templateEngine.getConfiguration().getSlidesContainer()) .append(writer.toString()); s.getElements() .stream() .forEach(element -> this.configuration.getDocument() .getElementById(element.getId()) .html(element.getClearedHtmlContent(this.getConfiguration().getVariables()))); } catch (IOException | TemplateException e) { LOGGER.log(Level.SEVERE, "Can not read slide's template", e); } } this.savePresentationFile(); } @Override public synchronized void saveArchive(File file) throws IllegalArgumentException, IOException { this.writeConfiguration(); LOGGER.fine("Create slides thumbnails"); if (!this.templateEngine.getConfiguration().getSlidesThumbnailDirectory().exists()) { if (!this.templateEngine.getConfiguration().getSlidesThumbnailDirectory().mkdirs()) { LOGGER.log(Level.SEVERE, "Can not create slides thumbnails directory"); } } else { Arrays.stream(this.templateEngine.getConfiguration().getSlidesThumbnailDirectory() .listFiles(f -> f.isFile())) .forEach(slideFile -> slideFile.delete()); } this.configuration.getSlides() .stream() .filter(slide -> slide != null && slide.getThumbnail() != null) .forEach(slide -> { LOGGER.fine("Creating thumbnail file: " + this.templateEngine.getConfiguration().getSlidesThumbnailDirectory().getAbsolutePath() + File.separator + slide.getSlideNumber() + ".png"); try { ImageIO.write(SwingFXUtils.fromFXImage(slide.getThumbnail(), null), "png", new File(this.templateEngine.getConfiguration().getSlidesThumbnailDirectory(), slide.getSlideNumber().concat(".png"))); } catch (IOException e) { LOGGER.log(Level.WARNING, String.format("Can not create thumbnail for slide number %1$s", slide.getSlideNumber()), e); } }); ZipUtils.zip(this.getWorkingDirectory(), file); this.setModifiedSinceLatestSave(false); LOGGER.fine("Presentation saved"); } /** * Indicates if the presentation has already been saved by testing if the {@link #getArchive()} * method returns {@code null} or not. * * @return {@code true} if {@link #getArchive()} is not {@code null}, {@code false} otherwise. */ public boolean isPresentationAlreadySaved() { return this.getArchive() != null; } /** * Indicates if the presentation has been modified since the latest save. If the presentation has never been saved, * then the presentation is considered modified. * * @return {@code true} if the presentation has been modified since the latest save, {@code false} otherwise. */ public boolean isModifiedSinceLatestSave() { return modifiedSinceLatestSave; } /** * Set if the presentation has been modified since its latest save. * * @param modifiedSinceLatestSave {@code true} to indicate a modification, {@code false} otherwise. */ public void setModifiedSinceLatestSave(boolean modifiedSinceLatestSave) { boolean oldValue = this.modifiedSinceLatestSave; this.modifiedSinceLatestSave = modifiedSinceLatestSave; PlatformHelper.run(() -> this.propertyChangeSupport.firePropertyChange("modifiedSinceLatestSave", oldValue, modifiedSinceLatestSave)); } /** * This method creates a presentation from the given template archive. It prepares all resources * in order this engine to be used to create the new presentation. * * @param templateArchive The template archive file to create the presentation from. * @throws IOException If an error occurred when processing the archive. * @throws IllegalAccessException If an error occurred when processing the archive. */ public void createFromTemplate(File templateArchive) throws IOException, IllegalAccessException { this.setArchive(null); this.setModifiedSinceLatestSave(true); this.templateEngine = new TemplateEngine(); this.templateEngine.loadArchive(templateArchive); this.setWorkingDirectory(this.templateEngine.getWorkingDirectory()); this.configuration = new PresentationConfiguration(); this.configuration.setPresentationFile(new File(this.getWorkingDirectory(), PresentationConfiguration.DEFAULT_PRESENTATION_FILENAME)); this.configuration.getVariables().addAll(this.templateEngine.getConfiguration().getDefaultVariables()); final Configuration templateConfiguration = TemplateProcessor.getDefaultConfiguration(); templateConfiguration.setDirectoryForTemplateLoading(this.templateEngine.getConfiguration().getFile().getParentFile()); final Map tokens = new HashMap<>(); tokens.put(TEMPLATE_SFX_JAVASCRIPT_RESOURCES_TOKEN, this.buildJavaScriptResourcesToInclude()); try (final StringWriter writer = new StringWriter()) { final Template documentTemplate = templateConfiguration.getTemplate(this.templateEngine.getConfiguration().getFile().getName()); documentTemplate.process(tokens, writer); writer.flush(); this.configuration.setDocument(Jsoup.parse(writer.toString())); this.savePresentationFile(); } catch (TemplateException e) { LOGGER.log(Level.SEVERE, "Can not parse the template", e); } } /** * Get the configuration of the template stored in the presentation. * * @return The configuration of the template. */ public TemplateConfiguration getTemplateConfiguration() { return this.templateEngine.getConfiguration(); } /** * Add a slide to the presentation and save the presentation. If {@code afterSlideNumber} is {@code null} or not * found, the slide is added at the end of the presentation, otherwise it is added after the given slide number. * * @param template The template of slide to add. * @param afterSlideNumber The slide number to insert the new slide after. * @return The new added slide. * @throws IOException If an error occurred when saving the presentation. */ public Slide addSlide(SlideTemplate template, String afterSlideNumber) throws IOException { if (template == null) throw new IllegalArgumentException("The templateConfiguration for creating a slide can not be null"); this.setModifiedSinceLatestSave(true); final Pair<Slide, Element> createdSlide = this.createSlide(template); if (afterSlideNumber == null) { this.configuration.getSlides().add(createdSlide.getKey()); } else { ListIterator<Slide> slidesIterator = this.configuration.getSlides().listIterator(); this.configuration.getSlideByNumber(afterSlideNumber); int index = -1; while (slidesIterator.hasNext()) { if (slidesIterator.next().getSlideNumber().equals(afterSlideNumber)) { index = slidesIterator.nextIndex(); break; } } if (index > -1) { this.configuration.getSlides().add(index, createdSlide.getKey()); } else { this.configuration.getSlides().add(createdSlide.getKey()); } } if (afterSlideNumber == null || afterSlideNumber.isEmpty()) { this.configuration.getDocument() .getElementById(this.templateEngine.getConfiguration().getSlidesContainer()) .append(createdSlide.getValue().outerHtml()); } else { this.configuration.getDocument() .getElementById(this.configuration.getSlideByNumber(afterSlideNumber).getId()) .after(createdSlide.getValue().outerHtml()); } this.savePresentationFile(); return createdSlide.getKey(); } /** * Delete the slide with the slideNumber and save the presentation. * * @param slideNumber The slide number to delete. */ public void deleteSlide(String slideNumber) { if (slideNumber == null) throw new IllegalArgumentException("Slide number can not be null"); this.setModifiedSinceLatestSave(true); Slide slideToRemove = this.configuration.getSlideByNumber(slideNumber); if (slideToRemove != null) { this.configuration.getSlides().remove(slideToRemove); this.configuration.getDocument() .getElementById(slideToRemove.getId()).remove(); } this.savePresentationFile(); } /** * Duplicates the given slide and add it to the presentation. The presentation is temporary saved. * * @param slide The slide to duplicate. * @return The duplicated slide. */ public Slide duplicateSlide(Slide slide) throws IOException { if (slide == null) throw new IllegalArgumentException("The slide to duplicate can not be null"); this.setModifiedSinceLatestSave(true); final Pair<Slide, Element> duplicatedSlide = this.createSlide(slide.getTemplate()); // Add the slide to the presentation's slides int index = this.configuration.getSlides().indexOf(slide); if (index != -1) { if (index == this.configuration.getSlides().size() - 1) { this.configuration.getSlides().add(duplicatedSlide.getKey()); } else { this.configuration.getSlides().add(index + 1, duplicatedSlide.getKey()); } } // Update the slide elements duplicatedSlide.getKey().getElements().forEach(copiedElement -> { Optional<SlideElement> optional = slide.getElements().stream() .filter(originalElement -> copiedElement.getTemplate().getId() == originalElement.getTemplate().getId()) .findFirst(); // Update the copy if (optional.isPresent()) { copiedElement.setOriginalContentCode(optional.get().getOriginalContentCode()); copiedElement.setOriginalContent(optional.get().getOriginalContent()); copiedElement.setHtmlContent(optional.get().getHtmlContent()); } }); // Update the document this.getConfiguration().getDocument().getElementById(slide.getId()).after(duplicatedSlide.getValue().outerHtml()); this.getConfiguration().updateSlideInDocument(duplicatedSlide.getKey()); this.savePresentationFile(); return duplicatedSlide.getKey(); } /** * Move a slide and update the presentation's document. If <code>beforeSlide</code> is null, * the slide is moved at the end of the presentation. If <code>slideToMove</code> is equal to <code>beforeSlide</code> * nothing is done. * If an operation has been performed, the presentation is temporary saved. * * @param slideToMove The slide to move * @param beforeSlide The slide before <code>slideToMove</code> is moved * @throws IllegalArgumentException if the slideToMove is null */ public void moveSlide(Slide slideToMove, Slide beforeSlide) { if (slideToMove == null) throw new IllegalArgumentException("The slideToMove to move can not be null"); if (!slideToMove.equals(beforeSlide)) { this.setModifiedSinceLatestSave(true); this.configuration.getSlides().remove(slideToMove); final String slideHtml = this.configuration.getDocument() .getElementById(slideToMove.getId()).outerHtml(); this.configuration.getDocument() .getElementById(slideToMove.getId()) .remove(); if (beforeSlide == null) { this.configuration.getSlides().add(slideToMove); this.configuration.getDocument() .getElementById(this.templateEngine.getConfiguration().getSlidesContainer()) .append(slideHtml); } else { int index = this.configuration.getSlides().indexOf(beforeSlide); this.configuration.getSlides().add(index, slideToMove); this.configuration.getDocument() .getElementById(beforeSlide.getId()) .before(slideHtml); } this.savePresentationFile(); } } /** * This method adds the given resource to the collection of resources present in {@link #getConfiguration()} as well * as in the presentation's document. * * @param resource The resource to add in the collection and the document. */ public void addCustomResource(Resource resource) { if (resource != null && resource.getContent() != null && !resource.getContent().trim().isEmpty()) { this.setModifiedSinceLatestSave(true); this.configuration.getCustomResources().add(resource); /* * All of this ensure formatting using the HTML manipulation library. */ final String location = this.relativizeFromWorkingDirectory(this.getTemplateConfiguration().getResourcesDirectory()); final String htmlString = resource.buildHTMLString(location); final String resourceHtml = Jsoup.parseBodyFragment(htmlString).body().html(); if (!this.configuration.getDocument().head().html().contains(resourceHtml)) { this.configuration.getDocument().head().append(htmlString); } } } public void savePresentationFile() { try (final FileOutputStream fileOutputStream = new FileOutputStream(this.configuration.getPresentationFile()); final OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, GlobalConfiguration.getDefaultCharset()); final Writer writer = new BufferedWriter(outputStreamWriter)) { writer.write(this.configuration.getDocument().html()); writer.flush(); } catch (IOException e) { LOGGER.log(Level.SEVERE, "Can not save presentation file", e); } } /** * This method loads all JavaScript resources that should be inserted in a template and return them in a String. * * @return The String containing the content of all JavaScript resources needed for a template */ private String buildJavaScriptResourcesToInclude() { final StringBuilder builder = new StringBuilder(); builder.append(ResourceHelper.readResource(TEMPLATE_SFX_CONTENT_DEFINER_SCRIPT)).append("\n\n") .append(ResourceHelper.readResource(TEMPLATE_SFX_SNIPPET_EXECUTOR_SCRIPT)).append("\n\n") .append(ResourceHelper.readResource(TEMPLATE_SFX_CALLBACK_SCRIPT)).append("\n\n") .append(ResourceHelper.readResource(TEMPLATE_SFX_QUIZ_CALLER_SCRIPT)).append("\n\n"); return builder.toString(); } /** * Create a {@link Slide slide} from the given {@link SlideTemplate template}. * * @param template The template to create the slide from. * @return A {@link Pair} where the key is the created {@link Slide} object and the value the HTML code get from the * parsed template. * @throws IOException If an error occurs when parsing the template. * @throws NullPointerException If the given {@code template} is {@code null}. */ private Pair<Slide, Element> createSlide(final SlideTemplate template) throws NullPointerException, IOException { if (template == null) throw new NullPointerException("The template can not be null"); this.setModifiedSinceLatestSave(true); final Pair<Slide, Element> result = new Pair<>(); result.setKey(new Slide(template, System.currentTimeMillis() + "")); final Map tokens = new HashMap<>(); tokens.put(TEMPLATE_SLIDE_ID_PREFIX_TOKEN, this.templateEngine.getConfiguration().getSlideIdPrefix()); tokens.put(TEMPLATE_SLIDE_NUMBER_TOKEN, result.getKey().getSlideNumber()); // Process the SlideElements by replacing their ID and setting their content final Configuration defaultConfiguration = TemplateProcessor.getDefaultConfiguration(); if (template.getElements() != null) { Arrays.stream(template.getElements()) .forEach(element -> { try (final StringWriter writer = new StringWriter(); final StringReader reader = new StringReader(element.getHtmlId())) { final Template elementTemplate = new Template("element template", reader, defaultConfiguration); elementTemplate.process(tokens, writer); writer.flush(); result.getKey().updateElement(writer.toString(), "HTML", element.getDefaultContent(), element.getDefaultContent()) .setTemplate(element); } catch (IOException | TemplateException e) { LOGGER.log(Level.WARNING, "Can not parse element", e); } }); } // Add dynamic attributes to the tokens by asking their values to the user // TODO INCUBATING tokens.clear(); if (result.getKey().getTemplate().getDynamicAttributes() != null && result.getKey().getTemplate().getDynamicAttributes().length > 0) { Scanner scanner = new Scanner(System.in); String value; for (DynamicAttribute attribute : result.getKey().getTemplate().getDynamicAttributes()) { System.out.print(attribute.getPromptMessage() + " "); value = scanner.nextLine(); if (value == null || value.trim().isEmpty()) { tokens.put(attribute.getTemplateExpression(), ""); } else { tokens.put(attribute.getTemplateExpression(), String.format("%1$s=\"%2$s\"", attribute.getAttribute(), value.trim())); } } } // Parsing the slide's template file defaultConfiguration.setDirectoryForTemplateLoading(result.getKey().getTemplate().getFile().getParentFile()); tokens.put(TEMPLATE_SLIDE_ID_PREFIX_TOKEN, this.templateEngine.getConfiguration().getSlideIdPrefix()); tokens.put(TEMPLATE_SLIDE_NUMBER_TOKEN, result.getKey().getSlideNumber()); tokens.put(TEMPLATE_SFX_CALLBACK_TOKEN, TEMPLATE_SFX_CALLBACK_CALL); tokens.putAll(this.configuration.getVariables().stream().collect(Collectors.toMap(Pair::getKey, Pair::getValue))); try (final StringWriter writer = new StringWriter()) { final Template slideTemplate = defaultConfiguration.getTemplate(template.getFile().getName()); slideTemplate.process(tokens, writer); writer.flush(); result.setValue(DOMUtils.convertToNode(writer.toString())); result.getKey().setId(result.getValue().id()); } catch (TemplateException e) { LOGGER.log(Level.WARNING, "Error when parsing the slide's template", e); } return result; } }