offishell.word.Word.java Source code

Java tutorial

Introduction

Here is the source code for offishell.word.Word.java

Source

/*
 * Copyright (C) 2019 offishell Development Team
 *
 * Licensed under the MIT License (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *          https://opensource.org/licenses/MIT
 */
package offishell.word;

import java.awt.Desktop;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
import java.util.List;
import java.util.function.Consumer;

import org.apache.poi.xwpf.model.XWPFHeaderFooterPolicy;
import org.apache.poi.xwpf.usermodel.BodyType;
import org.apache.poi.xwpf.usermodel.IBodyElement;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFFooter;
import org.apache.poi.xwpf.usermodel.XWPFHeader;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
import org.apache.xmlbeans.XmlCursor;
import org.apache.xmlbeans.impl.values.XmlValueDisconnectedException;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTMarkupRange;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageMar;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageSz;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSectPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTextDirection;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STPageOrientation;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STTextDirection;

import kiss.Disposable;
import kiss.I;
import kiss.Observer;
import kiss.Signal;
import officeman.model.FileType;
import offishell.Date;
import offishell.UI;
import offishell.expression.VariableContext;
import psychopath.Directory;
import psychopath.Locator;

/**
 * @version 2016/05/28 9:53:59
 */
public class Word {

    static {
        I.load(Date.class);
    }

    /** The template file. */
    final Path path;

    /** The culculated document. */
    XWPFDocument calculated;

    /**
     * <p>
     * Collect all paragraphs in this document.
     * </p>
     * 
     * @return A list of {@link XWPFParagraph}.
     */
    public final Signal<XWPFParagraph> paragraphs = new Signal<XWPFParagraph>((observer, disposer) -> {
        for (IBodyElement element : calculated.getBodyElements()) {
            collectParagraph(element, observer);
        }
        return Disposable.empty();
    });

    /** The context. */
    private CalculationContext context = new CalculationContext();

    /** The text direction. */
    boolean textIsVerticalAlign;

    /** The next break type. */
    private BreakType breakType = BreakType.None;

    private Section section = new Section();

    /**
     * 
     */
    private Word() {
        this.path = Locator.temporaryFile().asJavaPath();
        this.calculated = new XWPFDocument();

        CTSectPr sect = calculated.getDocument().getBody().getSectPr();
        section.getSize(sect);
        section.getMargin(sect);
    }

    /**
     * @param path
     */
    protected Word(Path path) {
        if (Files.notExists(path)) {
            throw new Error(" " + path.toAbsolutePath() + " ?????");
        }

        try {
            this.path = path;
            this.calculated = new XWPFDocument(Files.newInputStream(path));

            CTTextDirection direction = calculated.getDocument().getBody().getSectPr().getTextDirection();

            if (direction != null) {
                this.textIsVerticalAlign = direction.getVal() == STTextDirection.TB_RL;
            } else {
                this.textIsVerticalAlign = false;
            }
        } catch (IOException e) {
            throw I.quiet(e);
        }
    }

    /**
     * <p>
     * Expose POI API.
     * </p>
     * 
     * @return POI document.
     */
    public XWPFDocument docment() {
        return calculated;
    }

    /**
     * <p>
     * Helper method to find {@link XWPFParagraph} which contains the specified text.
     * </p>
     * 
     * @param text A text to search.
     * @return A serach result.
     */
    public XWPFParagraph findParagraphWith(String text) {
        List<XWPFParagraph> list = paragraphs.take(p -> p.getText().contains(text)).toList();

        return list.isEmpty() ? null : list.get(0);
    }

    /**
     * <p>
     * Helper method to find {@link XWPFParagraph} which contains the specified text.
     * </p>
     * 
     * @param text A text to search.
     * @param start A start position to search.
     * @return A serach result.
     */
    public XWPFParagraph findParagraphWith(String text, XWPFParagraph start) {
        List<XWPFParagraph> list = paragraphs.skipUntil(start).take(p -> p.getText().contains(text)).toList();

        return list.isEmpty() ? null : list.get(0);
    }

    /**
     * <p>
     * Copy all pages.
     * </p>
     */
    public Word copy(int number) {
        List<IBodyElement> copy = copy(calculated.getBodyElements());

        for (int i = 0; i < number - 1; i++) {
            merge(copy);
        }
        return this;
    }

    /**
     * <p>
     * Helper method to get cusor.
     * </p>
     * 
     * @param e
     * @return
     */
    private XmlCursor cursorAfter(IBodyElement e) {
        if (e instanceof XWPFParagraph) {
            XmlCursor cursor = ((XWPFParagraph) e).getCTP().newCursor();
            cursor.toNextSibling();
            return cursor;
        } else if (e instanceof XWPFTable) {
            XmlCursor cursor = ((XWPFTable) e).getCTTbl().newCursor();
            cursor.toNextSibling();
            return cursor;
        } else {
            // If this exception will be thrown, it is bug of this program. So we must rethrow the
            // wrapped error in here.
            throw new Error();
        }
    }

    /**
     * <p>
     * Helper method to get cusor.
     * </p>
     * 
     * @param e
     * @return
     */
    private XmlCursor cursorLast() {
        List<IBodyElement> elements = calculated.getBodyElements();

        if (elements.isEmpty()) {
            XmlCursor cursor = calculated.getDocument().newCursor();
            cursor.toStartDoc();
            return cursor;
        } else {
            return cursorAfter(elements.get(elements.size() - 1));
        }
    }

    /**
     * <p>
     * Calculate variables by the given model.
     * </p>
     * 
     * @param models
     * @return
     */
    public Word calculate(Object model, Object... models) {
        ArrayList list = new ArrayList();
        list.add(model);
        list.addAll(Arrays.asList(models));

        return calculate(list);
    }

    /**
     * <p>
     * Calculate variables by the given model.
     * </p>
     * 
     * @param models
     * @return
     */
    public Word calculate(List models) {
        if (models == null) {
            throw new Error("??????");
        }

        try {
            // calculate variables
            context.variable = new VariableContext(path, textIsVerticalAlign, models);
            replace(calculated);

            // clear all comments
            WordHeleper.clearComment(calculated);

            // API definition
            return this;
        } catch (Exception e) {
            throw I.quiet(e);
        }
    }

    /**
     * <p>
     * Calculate variables by the given model list and merge them all.
     * </p>
     * 
     * @param models
     * @return
     */
    public Word calculateAndMerge(Signal models, Object... additions) {
        return calculateAndMerge(models.toList(), additions);
    }

    /**
     * <p>
     * Calculate variables by the given model list and merge them all.
     * </p>
     * 
     * @param models
     * @return
     */
    public Word calculateAndMerge(List models, Object... additions) {
        if (models == null || models.isEmpty()) {
            return this;
        }

        if (models.size() == 1) {
            return calculate(models.get(0), additions);
        }

        calculate(models.get(0), additions);

        for (int i = 1; i < models.size(); i++) {
            merge(new Word(path).calculate(models.get(i), additions));
        }
        return this;
    }

    /**
     * <p>
     * Merge the specified {@link Word} to this document.
     * </p>
     * 
     * @param after
     * @return
     */
    public Word merge(Word after) {
        merge(after.calculated.getBodyElements());
        return this;
    }

    /**
     * <p>
     * Merge the specified {@link Word} to this document.
     * </p>
     * 
     * @param after
     * @return
     */
    private Word merge(List<IBodyElement> elements) {
        XmlCursor cursor = cursorLast();

        // copy children
        for (int i = 0; i < elements.size(); i++) {
            IBodyElement element = elements.get(i);

            if (element instanceof XWPFParagraph) {
                XWPFParagraph para = (XWPFParagraph) element;
                XWPFParagraph created = createParagraph(cursor);
                WordHeleper.copy(para, created, v -> v);
                cursor = cursorAfter(created);

                if (i == 0) {
                    created.setPageBreak(true);

                    CTSectPr inSec = WordHeleper.section(para.getDocument());
                    CTSectPr outSec = WordHeleper.section(created.getDocument());
                    CTPageMar inMargin = inSec.getPgMar();
                    CTPageMar outMargin = outSec.addNewPgMar();
                    outMargin.setBottom(inMargin.getBottom());
                    outMargin.setLeft(inMargin.getLeft());
                    outMargin.setRight(inMargin.getRight());
                    outMargin.setTop(inMargin.getTop());

                    // List<XWPFFooter> inFooter = para.getDocument().getFooterList();
                    // List<XWPFFooter> outFooter = created.getDocument().getFooterList();
                    // XWPFHeaderFooterPolicy headerFooterPolicy =
                    // created.getDocument().getHeaderFooterPolicy();
                }
            } else if (element instanceof XWPFTable) {
                XWPFTable table = (XWPFTable) element;
                XWPFTable created = calculated.createTable();
                created.removeRow(0); // new table has one row and one column, so we must remove it
                WordHeleper.copy(table, created, v -> v);
                cursor = cursorAfter(created);
            }
        }
        return this;
    }

    /**
     * <p>
     * Print document with the given variables.
     * </p>
     * 
     * @return Chainable API
     */
    public Word print() {
        try {
            return print(Files.createTempFile("calculated", ".docx"));
        } catch (IOException e) {
            throw I.quiet(e);
        }
    }

    /**
     * <p>
     * Print document with the given variables.
     * </p>
     * 
     * @return Chainable API
     */
    public Word print(Path path) {
        save(path);

        UI.print(path);

        // API definition
        return this;
    }

    /**
     * <p>
     * Open document with the given variables.
     * </p>
     * 
     * @return
     */
    public Word open() {
        try {
            Path temp = Files.createTempFile("calculated", ".docx");
            save(temp);
            Desktop.getDesktop().open(temp.toFile());
        } catch (IOException e) {
            throw I.quiet(e);
        }
        return this;
    }

    /**
     * <p>
     * Save this document to the specified {@link Path}.
     * </p>
     * 
     * @param output
     * @return Chainable API.
     */
    public Word save(Path output) {
        return save(output, true);
    }

    /**
     * <p>
     * Save this document to the specified {@link Path}.
     * </p>
     * 
     * @param output
     * @return Chainable API.
     */
    public Word save(Path output, boolean overwrite) {
        if (overwrite == true || Files.notExists(output)) {
            try (OutputStream stream = Files.newOutputStream(output)) {
                calculated.write(stream);
            } catch (IOException e) {
                throw I.quiet(e);
            }
        }

        // API definition
        return this;
    }

    /**
     * <p>
     * Replace variable text.
     * </p>
     * 
     * @param doc
     * @param object
     */
    private void replace(XWPFDocument doc) {
        // for paragraph
        for (XWPFParagraph para : copy(doc.getParagraphs())) {
            replace(para);
        }

        // for table
        for (XWPFTable table : copy(doc.getTables())) {
            for (XWPFTableRow row : copy(table.getRows())) {
                for (XWPFTableCell cell : copy(row.getTableCells())) {
                    for (XWPFParagraph para : copy(cell.getParagraphs())) {
                        replace(para);
                    }
                }
            }
        }

        // for header
        for (XWPFHeader header : copy(doc.getHeaderList())) {
            for (XWPFParagraph para : copy(header.getParagraphs())) {
                replace(para);
            }
        }

        // for footer
        for (XWPFFooter footer : copy(doc.getFooterList())) {
            for (XWPFParagraph para : copy(footer.getParagraphs())) {
                replace(para);
            }
        }
    }

    /**
     * @param para
     * @param object
     */
    private void replace(XWPFParagraph para) {
        context.isStartConditinalBlock(para);
        context.block.process(para);
        context.isEndConditionalBlock(para);
    }

    /**
     * <p>
     * Helper method to collect all paragraphs in this document.
     * </p>
     * 
     * @param observer A list of {@link XWPFParagraph}.
     */
    private void collectParagraph(IBodyElement element, Observer<? super XWPFParagraph> observer) {
        switch (element.getElementType()) {
        case PARAGRAPH:
            observer.accept((XWPFParagraph) element);
            break;

        case TABLE:
            XWPFTable table = (XWPFTable) element;
            for (XWPFTableRow row : table.getRows()) {
                for (XWPFTableCell cell : row.getTableCells()) {
                    for (XWPFParagraph para : cell.getParagraphs()) {
                        collectParagraph(para, observer);
                    }
                }
            }
            break;

        default:
            break;
        }
    }

    /**
     * <p>
     * Create header with the specified text.
     * </p>
     * 
     * @param headerText A header text.
     */
    public Word header(String headerText) {
        try {
            CTSectPr section = calculated.getDocument().getBody().addNewSectPr();
            XWPFHeaderFooterPolicy policy = new XWPFHeaderFooterPolicy(calculated, section);
            XWPFHeader header = policy.createHeader(XWPFHeaderFooterPolicy.DEFAULT);
            XWPFParagraph para = header.createParagraph();
            XWPFRun run = para.createRun();
            run.setText(headerText);
            styles().base().apply(run);
        } catch (Exception e) {
            throw I.quiet(e);
        }
        return this;
    }

    /**
     * <p>
     * Add new paragraph.
     * </p>
     * 
     * @param text A paragraph text.
     * @return
     */
    public Word text(String text) {
        XWPFParagraph para = createParagraph();
        XWPFRun run = para.createRun();
        run.setText(text, 0);
        styles().base().apply(run);

        return this;
    }

    /**
     * <p>
     * Add new page without section break.
     * </p>
     * 
     * @return
     */
    public Word page() {
        return page(false);
    }

    /**
     * <p>
     * Add new page.
     * </p>
     * 
     * @return
     */
    public Word page(boolean withNewSection) {
        if (withNewSection) {
            breakType = BreakType.Section;
        } else {
            breakType = BreakType.Page;
        }
        return this;
    }

    /**
     * <p>
     * Add new page.
     * </p>
     * 
     * @return
     */
    public Word section(Consumer<Section> configurator) {
        breakType = BreakType.Section;

        if (configurator != null) {
            configurator.accept(section = new Section());
        }
        return this;
    }

    /**
     * <p>
     * Search {@link WordStyleManager}.
     * </p>
     * 
     * @return
     */
    private WordStyleManager styles() {
        List<WordStyleManager> list = I.find(WordStyleManager.class);

        return list.get(list.size() - 1);
    }

    /**
     * <p>
     * Create new paragraph.
     * </p>
     * 
     * @return
     */
    private final XWPFParagraph createParagraph() {
        return createParagraph(null);
    }

    /**
     * <p>
     * Create new paragraph at the specified location.
     * </p>
     * 
     * @param cursor A location.
     * @return
     */
    private final XWPFParagraph createParagraph(XmlCursor cursor) {
        XWPFParagraph created = null;

        if (cursor != null) {
            created = calculated.insertNewParagraph(cursor);
        }

        if (created == null) {
            created = calculated.createParagraph();
        }

        switch (breakType) {
        case Page:
            breakType = BreakType.None;
            created.setPageBreak(true);
            break;

        case Section:
            breakType = BreakType.None;
            XWPFParagraph before = calculated.getParagraphArray(calculated.getParagraphs().size() - 2);

            if (before != null) {
                CTSectPr section = calculated.getDocument().getBody().addNewSectPr();
                WordHeleper.ppr(before).setSectPr(section);

                this.section.setSize(section);
                this.section.setMargin(section);
            }
            break;

        default:
            break;
        }
        return created;
    }

    /**
     * <p>
     * Avoid {@link ConcurrentModificationException}.
     * </p>
     * 
     * @param list A copy list items.
     * @return A copied list.
     */
    private static <T> List<T> copy(List<T> list) {
        return new ArrayList(list);
    }

    /**
     * <p>
     * Avoid {@link ConcurrentModificationException}.
     * </p>
     * 
     * @param list A copy list items.
     * @return A copied list.
     */
    private static <T> List<T> copy(List<T> list, T start, T end) {
        boolean take = false;
        List<T> copy = new ArrayList();

        for (T item : list) {
            if (!take && start == item) {
                take = true;
            }

            if (take) {
                copy.add(item);
            }

            if (take && end == item) {
                break;
            }
        }
        return copy;
    }

    /**
     * @param path
     * @return
     */
    public static Word of(Path path) {
        return new Word(path);
    }

    /**
     * @param string
     * @param fileName
     */
    public static Word of(String directoryPath, String fileName) {
        return of(Locator.directory(directoryPath), fileName);
    }

    /**
     * @param root
     * @param string
     */
    public static Word of(Directory directory, String fileName) {
        return of(directory.walkFile(fileName + "." + FileType.Word).first().to().v.asJavaPath());
    }

    /**
     * <p>
     * Create empty file.
     * </p>
     * 
     * @return
     */
    public static Word blank() {
        return new Word() {

            /**
             * {@inheritDoc}
             */
            @Override
            public Word merge(Word after) {
                if (calculated.getParagraphs().isEmpty()) {
                    calculated = after.calculated;
                } else {
                    super.merge(after);
                }
                return this;
            }
        };
    }

    /**
     * @version 2016/09/06 18:37:44
     */
    private static enum BreakType {
        Page, Section, None;
    }

    /**
     * <p>
     * Section configurator.
     * </p>
     * 
     * @version 2016/09/06 19:12:18
     */
    public class Section {

        private STPageOrientation.Enum orientation;

        private int height;

        private int width;

        private int marginTop;

        private int marginBottom;

        private int marginLeft;

        private int marginRight;

        /**
         * Set margin on all side.
         * 
         * @param size
         * @return
         */
        public Section marginHorizontal(int size) {
            return marginLeft(size).marginRight(size);
        }

        /**
         * Set margin on all side.
         * 
         * @param size
         * @return
         */
        public Section marginVertical(int size) {
            return marginTop(size).marginBottom(size);
        }

        /**
         * Set margin on all side.
         * 
         * @param size
         * @return
         */
        public Section margin(int size) {
            return marginHorizontal(size).marginVertical(size);
        }

        /**
         * <p>
         * Set page orientation.
         * </p>
         * 
         * @return Chainable API.
         */
        public Section landscape() {
            orientation = STPageOrientation.LANDSCAPE;

            return this;
        }

        /**
         * <p>
         * Set page orientation.
         * </p>
         * 
         * @return Chainable API.
         */
        public Section portrait() {
            orientation = STPageOrientation.PORTRAIT;

            return this;
        }

        public void head(String text) {

        }

        /**
         * Set page size.
         * 
         * @param section
         */
        private void setSize(CTSectPr section) {
            if (section != null) {
                CTPageSz size = section.isSetPgSz() ? section.getPgSz() : section.addNewPgSz();
                size.setOrient(orientation);
                size.setW(BigInteger.valueOf(width * 20));
                size.setH(BigInteger.valueOf(height * 20));
            }
        }

        /**
         * Get page size.
         * 
         * @param section
         */
        private void getSize(CTSectPr section) {
            if (section != null) {
                CTPageSz size = section.isSetPgSz() ? section.getPgSz() : section.addNewPgSz();
                orientation = size.getOrient();
                width = size.getW().divide(BigInteger.valueOf(20)).intValue();
                height = size.getH().divide(BigInteger.valueOf(20)).intValue();
            }
        }

        /**
         * Set page margin.
         * 
         * @param section
         */
        private void setMargin(CTSectPr section) {
            if (section != null) {
                CTPageMar margin = section.isSetPgMar() ? section.getPgMar() : section.addNewPgMar();
                margin.setBottom(BigInteger.valueOf(marginBottom * 20));
                margin.setTop(BigInteger.valueOf(marginTop * 20));
                margin.setRight(BigInteger.valueOf(marginRight * 20));
                margin.setLeft(BigInteger.valueOf(marginLeft * 20));
            }
        }

        /**
         * Get page margin.
         * 
         * @param section
         */
        private void getMargin(CTSectPr section) {
            if (section != null) {
                CTPageMar margin = section.isSetPgMar() ? section.getPgMar() : section.addNewPgMar();
                marginTop = margin.getTop().divide(BigInteger.valueOf(20)).intValue();
                marginBottom = margin.getBottom().divide(BigInteger.valueOf(20)).intValue();
                marginLeft = margin.getLeft().divide(BigInteger.valueOf(20)).intValue();
                marginRight = margin.getRight().divide(BigInteger.valueOf(20)).intValue();
            }
        }

        /**
         * Get the orientation property of this {@link Word.Section}.
         * 
         * @return The orientation property.
         */
        public STPageOrientation.Enum orientation() {
            return orientation;
        }

        /**
         * Set the orientation property of this {@link Word.Section}.
         * 
         * @param orientation The orientation value to set.
         */
        public Section orientation(STPageOrientation.Enum orientation) {
            this.orientation = orientation;
            return this;
        }

        /**
         * Get the height property of this {@link Word.Section}.
         * 
         * @return The height property.
         */
        public int height() {
            return height;
        }

        /**
         * Set the height property of this {@link Word.Section}.
         * 
         * @param height The height value to set.
         */
        public Section height(int height) {
            this.height = height;
            return this;
        }

        /**
         * Get the width property of this {@link Word.Section}.
         * 
         * @return The width property.
         */
        public int width() {
            return width;
        }

        /**
         * Set the width property of this {@link Word.Section}.
         * 
         * @param width The width value to set.
         */
        public Section width(int width) {
            this.width = width;
            return this;
        }

        /**
         * Get the marginTop property of this {@link Word.Section}.
         * 
         * @return The marginTop property.
         */
        public int marginTop() {
            return marginTop;
        }

        /**
         * Set the marginTop property of this {@link Word.Section}.
         * 
         * @param marginTop The marginTop value to set.
         */
        public Section marginTop(int marginTop) {
            this.marginTop = marginTop;
            return this;
        }

        /**
         * Get the marginBottom property of this {@link Word.Section}.
         * 
         * @return The marginBottom property.
         */
        public int marginBottom() {
            return marginBottom;
        }

        /**
         * Set the marginBottom property of this {@link Word.Section}.
         * 
         * @param marginBottom The marginBottom value to set.
         */
        public Section marginBottom(int marginBottom) {
            this.marginBottom = marginBottom;
            return this;
        }

        /**
         * Get the marginLeft property of this {@link Word.Section}.
         * 
         * @return The marginLeft property.
         */
        public int marginLeft() {
            return marginLeft;
        }

        /**
         * Set the marginLeft property of this {@link Word.Section}.
         * 
         * @param marginLeft The marginLeft value to set.
         */
        public Section marginLeft(int marginLeft) {
            this.marginLeft = marginLeft;
            return this;
        }

        /**
         * Get the marginRight property of this {@link Word.Section}.
         * 
         * @return The marginRight property.
         */
        public int marginRight() {
            return marginRight;
        }

        /**
         * Set the marginRight property of this {@link Word.Section}.
         * 
         * @param marginRight The marginRight value to set.
         */
        public Section marginRight(int marginRight) {
            this.marginRight = marginRight;
            return this;
        }
    }

    /**
     * @version 2016/06/04 15:29:53
     */
    private interface Block {

        default void start(XWPFParagraph paragraph) {
        };

        void process(XWPFParagraph paragraph);

        default void end(XWPFParagraph paragraph) {
        }
    }

    /**
     * @version 2016/06/04 14:33:19
     */
    private class CalculationContext {

        /** The block processor. */
        private Block block = new Normal();

        /** The variable context. */
        private VariableContext variable;

        /**
         * @param paragraph
         * @param model
         */
        public void isStartConditinalBlock(XWPFParagraph paragraph) {
            try {
                CTP context = paragraph.getCTP();
                List<CTMarkupRange> starts = context.getCommentRangeStartList();

                if (starts.size() != 0) {
                    String special = "";
                    String condition = paragraph.getDocument().getCommentByID(starts.get(0).getId().toString())
                            .getText();

                    int index = condition.indexOf("#");

                    if (index != -1) {
                        special = condition.substring(index + 1);
                        condition = condition.substring(0, index);
                    }

                    Object value = variable.resolve(condition);

                    if (value instanceof Signal) {
                        value = ((Signal) value).toList();
                    }

                    if (value instanceof Boolean) {
                        block = new If((Boolean) value);
                    } else if (value instanceof List) {
                        switch (paragraph.getPartType()) {
                        case DOCUMENT:
                            block = new Loop((List) value);
                            break;

                        case TABLECELL:
                            XWPFTableCell cell = (XWPFTableCell) paragraph.getBody();

                            if (cell.getParagraphs().indexOf(paragraph) != 0) {
                                block = new LoopInCell((List) value, special);
                            } else {
                                block = new TableRowLoop((List) value);
                            }
                            break;

                        default:
                            // If this exception will be thrown, it is bug of this program. So we
                            // must
                            // rethrow the wrapped error in here.
                            throw new Error();
                        }
                    } else {
                        String text = paragraph.getText();

                        if (text.equals("$") || text.equals("{$}")) {
                            block = new Replace(value);
                        } else {
                            block = new If(value != null && !value.toString().trim().isEmpty());
                        }
                    }
                    block.start(paragraph);
                }
            } catch (XmlValueDisconnectedException e) {
                // ignore
            }
        }

        /**
         * @param para
         */
        public void isEndConditionalBlock(XWPFParagraph paragraph) {
            try {
                CTP context = paragraph.getCTP();

                if (context.getCommentRangeEndList().size() != 0) {
                    block.end(paragraph);
                    block = new Normal();
                }
            } catch (XmlValueDisconnectedException e) {
                // ignore
            }
        }

        /**
         * @version 2016/06/04 16:08:00
         */
        private class Normal implements Block {

            /**
             * {@inheritDoc}
             */
            @Override
            public void process(XWPFParagraph paragraph) {
                for (XWPFRun run : paragraph.getRuns()) {
                    try {
                        WordHeleper.write(run, variable.apply(run.getText(0)));
                    } catch (XmlValueDisconnectedException e) {
                        // ignore
                    }
                }
            }
        }

        /**
         * @version 2016/06/22 15:11:14
         */
        private class Replace implements Block {

            /** The replaced value. */
            private final Object value;

            /**
             * @param value
             */
            private Replace(Object value) {
                this.value = value;
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public void process(XWPFParagraph paragraph) {
                List<XWPFRun> runs = paragraph.getRuns();

                for (int i = runs.size() - 1; 0 <= i; i--) {
                    String text = runs.get(i).text();

                    if (!text.equals("$")) {
                        paragraph.removeRun(i);
                    }
                }

                if (!paragraph.getIRuns().isEmpty()) {
                    paragraph.getRuns().get(0).setText(I.transform(value, String.class), 0);
                }
            }
        }

        /**
         * @version 2016/06/04 17:55:05
         */
        private class If implements Block {

            /** The actual condition. */
            private final boolean condition;

            /** The start index. */
            private XWPFParagraph start;

            /** The end index. */
            private XWPFParagraph end;

            /**
             * @param condition
             */
            private If(boolean condition) {
                this.condition = condition;
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public void start(XWPFParagraph paragraph) {
                start = paragraph;
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public void process(XWPFParagraph paragraph) {
                // do nothing
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public void end(XWPFParagraph paragraph) {
                end = paragraph;

                if (!condition) {
                    BodyType type = paragraph.getPartType();

                    if (type == BodyType.DOCUMENT) {
                        XWPFDocument doc = paragraph.getDocument();

                        for (XWPFParagraph para : copy(doc.getParagraphs(), start, end)) {
                            doc.removeBodyElement(doc.getBodyElements().indexOf(para));
                        }
                    } else if (type == BodyType.TABLECELL) {
                        XWPFTableCell cell = (XWPFTableCell) paragraph.getBody();

                        for (XWPFParagraph para : copy(cell.getParagraphs(), start, end)) {
                            WordHeleper.clearText(para);
                        }
                    }
                } else {
                    XWPFDocument doc = paragraph.getDocument();

                    for (XWPFParagraph para : copy(doc.getParagraphs(), start, end)) {
                        for (XWPFRun run : para.getRuns()) {
                            WordHeleper.write(run, variable.apply(run.getText(0)));
                        }
                    }
                }
            }
        }

        /**
         * @version 2016/06/04 16:12:34
         */
        private class Loop implements Block {

            /** The loop items. */
            private final List items;

            /** The start index. */
            private XWPFParagraph start;

            /** The end index. */
            private XWPFParagraph end;

            /**
             * @param items
             */
            private Loop(List items) {
                this.items = items;
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public void start(XWPFParagraph paragraph) {
                start = paragraph;
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public void process(XWPFParagraph paragraph) {
                // do nothing
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public void end(XWPFParagraph paragraph) {
                end = paragraph;

                XWPFDocument doc = paragraph.getDocument();
                List<XWPFParagraph> paragraphs = copy(doc.getParagraphs(), start, end);
                XmlCursor index = paragraph.getCTP().newCursor();

                for (Object item : items) {
                    for (XWPFParagraph para : paragraphs) {
                        WordHeleper.copy(para, doc.insertNewParagraph(index.newCursor()),
                                new VariableContext(path, textIsVerticalAlign, item));
                    }
                }

                for (XWPFParagraph para : paragraphs) {
                    doc.removeBodyElement(doc.getPosOfParagraph(para));
                }
            }
        }

        /**
         * @version 2016/06/27 9:17:30
         */
        private class LoopInCell implements Block {

            /** The loop items. */
            private final List items;

            /** The special command. */
            private final boolean keep;

            /** The start index. */
            private XWPFParagraph start;

            /** The end index. */
            private XWPFParagraph end;

            /**
             * @param items
             */
            private LoopInCell(List items, String command) {
                this.items = items;
                this.keep = command.equals("keepLine");
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public void start(XWPFParagraph paragraph) {
                start = paragraph;
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public void process(XWPFParagraph paragraph) {
                // do nothing
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public void end(XWPFParagraph paragraph) {
                end = paragraph;

                XWPFTableCell cell = (XWPFTableCell) paragraph.getBody();
                List<XWPFParagraph> paragraphs = copy(cell.getParagraphs(), start, end);
                XmlCursor index = paragraph.getCTP().newCursor();

                for (Object item : items) {
                    for (XWPFParagraph para : paragraphs) {
                        WordHeleper.copy(para, cell.insertNewParagraph(index.newCursor()),
                                new VariableContext(path, textIsVerticalAlign, item));
                    }
                }

                for (XWPFParagraph para : paragraphs) {
                    cell.removeParagraph(cell.getParagraphs().indexOf(para));
                }

                if (keep) {
                    for (int i = 0; i < items.size() - 1; i++) {
                        List<XWPFParagraph> list = cell.getParagraphs();
                        int pos = list.size() - 1;
                        XWPFParagraph last = list.get(pos);
                        String text = last.getText();

                        if (text.equals("")) {
                            cell.removeParagraph(pos);
                        }
                    }
                }
            }
        }

        /**
         * 
         */
        private class TableRowLoop implements Block {

            /** The loop items. */
            private final List items;

            /** The start row. */
            private XWPFTableRow start;

            /** The end row. */
            private XWPFTableRow end;

            /**
             * @param element
             * @param items
             */
            private TableRowLoop(List items) {
                this.items = items;
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public void start(XWPFParagraph paragraph) {
                start = ((XWPFTableCell) paragraph.getBody()).getTableRow();
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public void process(XWPFParagraph paragraph) {
                // do nothing
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public void end(XWPFParagraph paragraph) {
                end = ((XWPFTableCell) paragraph.getBody()).getTableRow();

                XWPFTable table = start.getTable();
                List<XWPFTableRow> rows = copy(table.getRows(), start, end);
                int start = table.getRows().indexOf(end);

                for (int count = 0; count < items.size(); count++) {
                    for (int offset = 0; offset < rows.size(); offset++) {
                        WordHeleper.copy(rows.get(offset),
                                table.insertNewTableRow(start + count * rows.size() + offset),
                                new VariableContext(path, textIsVerticalAlign, items.get(count)));
                    }
                }

                for (XWPFTableRow row : rows) {
                    table.removeRow(table.getRows().indexOf(row));
                }
            }
        }
    }
}