make it subtree
[fanfix.git] / output / BasicOutput.java
diff --git a/output/BasicOutput.java b/output/BasicOutput.java
new file mode 100644 (file)
index 0000000..15d8cc1
--- /dev/null
@@ -0,0 +1,553 @@
+package be.nikiroo.fanfix.output;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import be.nikiroo.fanfix.Instance;
+import be.nikiroo.fanfix.bundles.StringId;
+import be.nikiroo.fanfix.data.Chapter;
+import be.nikiroo.fanfix.data.Paragraph;
+import be.nikiroo.fanfix.data.Paragraph.ParagraphType;
+import be.nikiroo.fanfix.data.Story;
+import be.nikiroo.utils.Progress;
+import be.nikiroo.utils.Version;
+
+/**
+ * This class is the base class used by the other output classes. It can be used
+ * outside of this package, and have static method that you can use to get
+ * access to the correct support class.
+ * 
+ * @author niki
+ */
+public abstract class BasicOutput {
+       /**
+        * The supported output types for which we can get a {@link BasicOutput}
+        * object.
+        * 
+        * @author niki
+        */
+       public enum OutputType {
+               /** EPUB files created with this program */
+               EPUB,
+               /** Pure text file with some rules */
+               TEXT,
+               /** TEXT but with associated .info file */
+               INFO_TEXT,
+               /** DEBUG output to console */
+               SYSOUT,
+               /** ZIP with (PNG) images */
+               CBZ,
+               /** LaTeX file with "book" template */
+               LATEX,
+               /** HTML files in a dedicated directory */
+               HTML,
+
+               ;
+
+               @Override
+               public String toString() {
+                       return super.toString().toLowerCase();
+               }
+
+               /**
+                * A description of this output type.
+                * 
+                * @param longDesc
+                *            TRUE for the long description, FALSE for the short one
+                * 
+                * @return the description
+                */
+               public String getDesc(boolean longDesc) {
+                       StringId id = longDesc ? StringId.OUTPUT_DESC
+                                       : StringId.OUTPUT_DESC_SHORT;
+
+                       String desc = Instance.getTrans().getStringX(id, this.name());
+
+                       if (desc == null) {
+                               desc = Instance.getTrans().getString(id, this.toString());
+                       }
+
+                       if (desc == null || desc.isEmpty()) {
+                               desc = this.toString();
+                       }
+
+                       return desc;
+               }
+
+               /**
+                * The default extension to add to the output files.
+                * 
+                * @param readerTarget
+                *            TRUE to point to the main {@link Story} entry point for a
+                *            reader (for instance, the main entry point if this
+                *            {@link Story} is in a directory bundle), FALSE to point to
+                *            the main file even if it is a directory for instance
+                * 
+                * @return the extension
+                */
+               public String getDefaultExtension(boolean readerTarget) {
+                       BasicOutput output = BasicOutput.getOutput(this, false, false);
+                       if (output != null) {
+                               return output.getDefaultExtension(readerTarget);
+                       }
+
+                       return null;
+               }
+
+               /**
+                * Call {@link OutputType#valueOf(String)} after conversion to upper
+                * case.
+                * 
+                * @param typeName
+                *            the possible type name
+                * 
+                * @return NULL or the type
+                */
+               public static OutputType valueOfUC(String typeName) {
+                       return OutputType.valueOf(typeName == null ? null : typeName
+                                       .toUpperCase());
+               }
+
+               /**
+                * Call {@link OutputType#valueOf(String)} after conversion to upper
+                * case but return def for NULL and empty instead of raising an
+                * exception.
+                * 
+                * @param typeName
+                *            the possible type name
+                * @param def
+                *            the default value
+                * 
+                * @return NULL or the type
+                */
+               public static OutputType valueOfNullOkUC(String typeName, OutputType def) {
+                       if (typeName == null || typeName.isEmpty()) {
+                               return def;
+                       }
+
+                       return OutputType.valueOfUC(typeName);
+               }
+
+               /**
+                * Call {@link OutputType#valueOf(String)} after conversion to upper
+                * case but return def in case of error instead of raising an exception.
+                * 
+                * @param typeName
+                *            the possible type name
+                * @param def
+                *            the default value
+                * 
+                * @return NULL or the type
+                */
+               public static OutputType valueOfAllOkUC(String typeName, OutputType def) {
+                       try {
+                               return OutputType.valueOfUC(typeName);
+                       } catch (Exception e) {
+                               return def;
+                       }
+               }
+       }
+
+       /** The creator name (this program, by me!) */
+       static protected final String EPUB_CREATOR = "Fanfix "
+                       + Version.getCurrentVersion() + " (by Niki)";
+
+       /** The current best name for an image */
+       private String imageName;
+       private File targetDir;
+       private String targetName;
+       private OutputType type;
+       private boolean writeCover;
+       private boolean writeInfo;
+       private Progress storyPg;
+       private Progress chapPg;
+
+       /**
+        * Process the {@link Story} into the given target.
+        * 
+        * @param story
+        *            the {@link Story} to export
+        * @param target
+        *            the target where to save to (will not necessary be taken as is
+        *            by the processor, for instance an extension can be added)
+        * @param pg
+        *            the optional progress reporter
+        * 
+        * @return the actual main target saved, which can be slightly different
+        *         that the input one
+        * 
+        * @throws IOException
+        *             in case of I/O error
+        */
+       public File process(Story story, String target, Progress pg)
+                       throws IOException {
+               storyPg = pg;
+
+               File targetDir = null;
+               String targetName = null;
+               if (target != null) {
+                       target = new File(target).getAbsolutePath();
+                       targetDir = new File(target).getParentFile();
+                       targetName = new File(target).getName();
+
+                       String ext = getDefaultExtension(false);
+                       if (ext != null && !ext.isEmpty()) {
+                               if (targetName.toLowerCase().endsWith(ext)) {
+                                       targetName = targetName.substring(0, targetName.length()
+                                                       - ext.length());
+                               }
+                       }
+               }
+
+               return process(story, targetDir, targetName);
+       }
+
+       /**
+        * Process the {@link Story} into the given target.
+        * <p>
+        * This method is expected to be overridden in most cases.
+        * 
+        * @param story
+        *            the {@link Story} to export
+        * @param targetDir
+        *            the target dir where to save to
+        * @param targetName
+        *            the target filename (will not necessary be taken as is by the
+        *            processor, for instance an extension can be added)
+        * 
+        * 
+        * @return the actual main target saved, which can be slightly different
+        *         that the input one
+        * 
+        * @throws IOException
+        *             in case of I/O error
+        */
+       protected File process(Story story, File targetDir, String targetName)
+                       throws IOException {
+               this.targetDir = targetDir;
+               this.targetName = targetName;
+
+               writeStory(story);
+
+               return null;
+       }
+
+       /**
+        * The output type.
+        * 
+        * @return the type
+        */
+       public OutputType getType() {
+               return type;
+       }
+
+       /**
+        * Enable the creation of a .info file next to the resulting processed file.
+        * 
+        * @return TRUE to enable it
+        */
+       protected boolean isWriteInfo() {
+               return writeInfo;
+       }
+
+       /**
+        * Enable the creation of a cover file next to the resulting processed file
+        * if possible.
+        * 
+        * @return TRUE to enable it
+        */
+       protected boolean isWriteCover() {
+               return writeCover;
+       }
+
+       /**
+        * The output type.
+        * 
+        * @param type
+        *            the new type
+        * @param writeCover
+        *            TRUE to enable the creation of a cover if possible
+        * @param writeInfo
+        *            TRUE to enable the creation of a .info file
+        * 
+        * @return this
+        */
+       protected BasicOutput setType(OutputType type, boolean writeInfo,
+                       boolean writeCover) {
+               this.type = type;
+               this.writeInfo = writeInfo;
+               this.writeCover = writeCover;
+
+               return this;
+       }
+
+       /**
+        * The default extension to add to the output files.
+        * 
+        * @param readerTarget
+        *            TRUE to point to the main {@link Story} entry point for a
+        *            reader (for instance, the main entry point if this
+        *            {@link Story} is in a directory bundle), FALSE to point to the
+        *            main file even if it is a directory for instance
+        * 
+        * @return the extension
+        */
+       public String getDefaultExtension(
+                       @SuppressWarnings("unused") boolean readerTarget) {
+               return "";
+       }
+
+       @SuppressWarnings("unused")
+       protected void writeStoryHeader(Story story) throws IOException {
+       }
+
+       @SuppressWarnings("unused")
+       protected void writeChapterHeader(Chapter chap) throws IOException {
+       }
+
+       @SuppressWarnings("unused")
+       protected void writeParagraphHeader(Paragraph para) throws IOException {
+       }
+
+       @SuppressWarnings("unused")
+       protected void writeStoryFooter(Story story) throws IOException {
+       }
+
+       @SuppressWarnings("unused")
+       protected void writeChapterFooter(Chapter chap) throws IOException {
+       }
+
+       @SuppressWarnings("unused")
+       protected void writeParagraphFooter(Paragraph para) throws IOException {
+       }
+
+       protected void writeStory(Story story) throws IOException {
+               if (storyPg == null) {
+                       storyPg = new Progress(0, story.getChapters().size() + 2);
+               } else {
+                       storyPg.setMinMax(0, story.getChapters().size() + 2);
+               }
+
+               String chapterNameNum = String.format("%03d", 0);
+               String paragraphNumber = String.format("%04d", 0);
+               imageName = paragraphNumber + "_" + chapterNameNum;
+
+               if (story.getMeta() != null) {
+                       story.getMeta().setType("" + getType());
+               }
+
+               if (isWriteCover()) {
+                       InfoCover.writeCover(targetDir, targetName, story.getMeta());
+               }
+               if (isWriteInfo()) {
+                       InfoCover.writeInfo(targetDir, targetName, story.getMeta());
+               }
+
+               storyPg.setProgress(1);
+
+               List<Progress> chapPgs = new ArrayList<Progress>(story.getChapters()
+                               .size());
+               for (Chapter chap : story) {
+                       chapPg = new Progress(0, chap.getParagraphs().size());
+                       storyPg.addProgress(chapPg, 1);
+                       chapPgs.add(chapPg);
+                       chapPg = null;
+               }
+
+               writeStoryHeader(story);
+               for (int i = 0; i < story.getChapters().size(); i++) {
+                       chapPg = chapPgs.get(i);
+                       writeChapter(story.getChapters().get(i));
+                       chapPg.setProgress(chapPg.getMax());
+                       chapPg = null;
+               }
+               writeStoryFooter(story);
+
+               storyPg.setProgress(storyPg.getMax());
+               storyPg = null;
+       }
+
+       protected void writeChapter(Chapter chap) throws IOException {
+               String chapterNameNum;
+               if (chap.getName() == null || chap.getName().isEmpty()) {
+                       chapterNameNum = String.format("%03d", chap.getNumber());
+               } else {
+                       chapterNameNum = String.format("%03d", chap.getNumber()) + "_"
+                                       + chap.getName().replace(" ", "_");
+               }
+
+               int num = 0;
+               String paragraphNumber = String.format("%04d", num++);
+               imageName = chapterNameNum + "_" + paragraphNumber;
+
+               writeChapterHeader(chap);
+               int i = 1;
+               for (Paragraph para : chap) {
+                       paragraphNumber = String.format("%04d", num++);
+                       imageName = chapterNameNum + "_" + paragraphNumber;
+                       writeParagraph(para);
+                       if (chapPg != null) {
+                               chapPg.setProgress(i++);
+                       }
+               }
+               writeChapterFooter(chap);
+       }
+
+       protected void writeParagraph(Paragraph para) throws IOException {
+               writeParagraphHeader(para);
+               writeTextLine(para.getType(), para.getContent());
+               writeParagraphFooter(para);
+       }
+
+       @SuppressWarnings("unused")
+       protected void writeTextLine(ParagraphType type, String line)
+                       throws IOException {
+       }
+
+       /**
+        * Return the current best guess for an image name, based upon the current
+        * {@link Chapter} and {@link Paragraph}.
+        * 
+        * @param prefix
+        *            add the original target name as a prefix
+        * 
+        * @return the guessed name
+        */
+       protected String getCurrentImageBestName(boolean prefix) {
+               if (prefix) {
+                       return targetName + "_" + imageName;
+               }
+
+               return imageName;
+       }
+
+       /**
+        * Return the given word or sentence as <b>bold</b>.
+        * 
+        * @param word
+        *            the input
+        * 
+        * @return the bold output
+        */
+       protected String enbold(String word) {
+               return word;
+       }
+
+       /**
+        * Return the given word or sentence as <i>italic</i>.
+        * 
+        * @param word
+        *            the input
+        * 
+        * @return the italic output
+        */
+       protected String italize(String word) {
+               return word;
+       }
+
+       /**
+        * Decorate the given text with <b>bold</b> and <i>italic</i> words,
+        * according to {@link BasicOutput#enbold(String)} and
+        * {@link BasicOutput#italize(String)}.
+        * 
+        * @param text
+        *            the input
+        * 
+        * @return the decorated output
+        */
+       protected String decorateText(String text) {
+               StringBuilder builder = new StringBuilder();
+
+               int bold = -1;
+               int italic = -1;
+               char prev = '\0';
+               for (char car : text.toCharArray()) {
+                       switch (car) {
+                       case '*':
+                               if (bold >= 0 && prev != ' ') {
+                                       String data = builder.substring(bold);
+                                       builder.setLength(bold);
+                                       builder.append(enbold(data));
+                                       bold = -1;
+                               } else if (bold < 0
+                                               && (prev == ' ' || prev == '\0' || prev == '\n')) {
+                                       bold = builder.length();
+                               } else {
+                                       builder.append(car);
+                               }
+
+                               break;
+                       case '_':
+                               if (italic >= 0 && prev != ' ') {
+                                       String data = builder.substring(italic);
+                                       builder.setLength(italic);
+                                       builder.append(enbold(data));
+                                       italic = -1;
+                               } else if (italic < 0
+                                               && (prev == ' ' || prev == '\0' || prev == '\n')) {
+                                       italic = builder.length();
+                               } else {
+                                       builder.append(car);
+                               }
+
+                               break;
+                       default:
+                               builder.append(car);
+                               break;
+                       }
+
+                       prev = car;
+               }
+
+               if (bold >= 0) {
+                       builder.insert(bold, '*');
+               }
+
+               if (italic >= 0) {
+                       builder.insert(italic, '_');
+               }
+
+               return builder.toString();
+       }
+
+       /**
+        * Return a {@link BasicOutput} object compatible with the given
+        * {@link OutputType}.
+        * 
+        * @param type
+        *            the type
+        * @param writeCover
+        *            TRUE to enable the creation of a cover if possible to be saved
+        *            next to the main target file
+        * @param writeInfo
+        *            TRUE to enable the creation of a .info file to be saved next
+        *            to the main target file
+        * 
+        * @return the {@link BasicOutput}
+        */
+       public static BasicOutput getOutput(OutputType type, boolean writeInfo,
+                       boolean writeCover) {
+               if (type != null) {
+                       switch (type) {
+                       case EPUB:
+                               return new Epub().setType(type, writeInfo, writeCover);
+                       case TEXT:
+                               return new Text().setType(type, writeInfo, true);
+                       case INFO_TEXT:
+                               return new InfoText().setType(type, true, true);
+                       case SYSOUT:
+                               return new Sysout().setType(type, false, false);
+                       case CBZ:
+                               return new Cbz().setType(type, writeInfo, writeCover);
+                       case LATEX:
+                               return new LaTeX().setType(type, writeInfo, writeCover);
+                       case HTML:
+                               return new Html().setType(type, writeInfo, writeCover);
+                       }
+               }
+
+               return null;
+       }
+}