com.hauldata.dbpa.file.book.XlsxTargetSheet.java Source code

Java tutorial

Introduction

Here is the source code for com.hauldata.dbpa.file.book.XlsxTargetSheet.java

Source

/*
 * Copyright (c) 2016, 2018, Ronald DeSantis
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */

package com.hauldata.dbpa.file.book;

import java.awt.HeadlessException;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.IntStream;

import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.FontUnderline;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFColor;
import org.apache.poi.xssf.usermodel.XSSFFont;

import com.hauldata.dbpa.file.PageOptions;
import com.hauldata.dbpa.file.TargetHeaders;
import com.hauldata.dbpa.file.book.BorderStyles.BorderEdge;
import com.hauldata.dbpa.file.book.BorderStyles.BorderWidth;
import com.hauldata.dbpa.file.book.XlsxTargetBook.XlsxCellStyle;
import com.hauldata.dbpa.file.html.HtmlOptions;

public class XlsxTargetSheet extends XlsxSheet {

    private SXSSFSheet sheet;
    private int rowIndex;
    private ArrayList<Object> rowValues;
    private int columnCount;
    private ArrayList<Styles> previousRowCellStyles;

    private ResolvedSheetStyles sheetStyles;

    public XlsxTargetSheet(Book owner, String name, PageOptions options) {
        super(owner, name, options);

        sheet = null;
        rowIndex = 0;
        rowValues = new ArrayList<Object>();
        columnCount = 0;
        previousRowCellStyles = null;
    }

    public static class TargetOptions extends HtmlOptions {

        public static final TargetOptions DEFAULT = new TargetOptions();

        private boolean styled = false;

        public boolean isStyled() {
            return styled;
        }

        public static class Parser extends HtmlOptions.Parser {

            static Map<String, Modifier> modifiers;

            static {
                modifiers = new HashMap<String, Modifier>();
                modifiers.put("STYLED", (parser, options) -> {
                    ((TargetOptions) options).styled = true;
                });
            }

            protected Parser() {
                super(modifiers);
            }

            @Override
            protected PageOptions makeDefaultOptions() {
                return new TargetOptions();
            }
        }
    }

    protected TargetOptions getTargetOptions() {
        return getOptions() != null ? (TargetOptions) getOptions() : TargetOptions.DEFAULT;
    }

    // Node overrides

    @Override
    public void create() throws IOException {

        sheetStyles = new ResolvedSheetStyles(getTargetOptions());

        sheet = getOwner().getBook().createSheet(getName());
        sheet.trackAllColumnsForAutoSizing();

        // The following is duplicated in DsvFile.create() and should probably be moved to common code
        // but TxtFile has a different implementation.

        TargetHeaders headers = getTargetHeaders();
        if (headers.exist() && !headers.fromMetadata()) {
            for (int columnIndex = 1; columnIndex <= headers.getColumnCount(); ++columnIndex) {
                writeColumn(columnIndex, headers.getCaption(columnIndex - 1));
            }
        }
    }

    @Override
    public void append() throws IOException {
        if (!isOpen() || !isWritable()) {
            throw new RuntimeException("Appending a sheet in an existing XLSX book is not supported: " + getName());
        }
    }

    @Override
    public void close() throws IOException {
    }

    // PageNode overrides

    @Override
    public void writeColumn(int columnIndex, Object object) throws IOException {

        if (columnIndex == 1) {
            if (0 < rowIndex) {
                previousRowCellStyles = writeRow(rowValues, rowIndex, getRowPosition(rowIndex),
                        previousRowCellStyles);
            }
            rowIndex++;
        }

        if (rowIndex == 1) {
            rowValues.add(object);
        } else {
            rowValues.set(columnIndex - 1, object);
        }

        // Can't depend on the number of columns in the last row for true column count,
        // because trailing null columns are not written.  Must detect attempted writes.

        if (columnCount < columnIndex) {
            columnCount = columnIndex;
        }
    }

    private RowPosition getRowPosition(int rowIndex) {
        return rowIndex == 1 ? (headers.exist() ? RowPosition.HEADER : RowPosition.TOP)
                : rowIndex == 2 ? (headers.exist() ? RowPosition.NEXT : RowPosition.MIDDLE) : RowPosition.MIDDLE;
    }

    private ArrayList<Styles> writeRow(ArrayList<Object> rowValues, int rowIndex, RowPosition rowPosition,
            ArrayList<Styles> previousRowCellStyles) {

        ArrayList<Styles> rowCellStyles = new ArrayList<Styles>();

        Styles rowStyles = null;
        if (getTargetOptions().isStyled()) {

            ValueStyles valueStyles = ValueStyles.parse(TableTag.TR, rowValues.get(0));
            rowValues.set(0, valueStyles.value);
            rowStyles = valueStyles.styles;
        }

        Styles leftStyles = null;

        Row row = sheet.createRow(rowIndex - 1);

        int columnIndex = 0;
        while (columnIndex < columnCount) {

            Styles aboveStyles = (previousRowCellStyles != null) ? previousRowCellStyles.get(columnIndex) : null;

            Object object = rowValues.get(columnIndex);

            Styles cellStyles = null;
            if (getTargetOptions().isStyled()) {

                TableTag tag = (rowPosition == RowPosition.HEADER) ? TableTag.TH : TableTag.TD;
                ValueStyles valueStyles = ValueStyles.parse(tag, object);
                object = valueStyles.value;
                cellStyles = valueStyles.styles;
            }

            Cell cell = row.createCell(columnIndex++);

            setCellValue(cell, object);

            if (!sheetStyles.areDefault() || (rowStyles != null) || (cellStyles != null)) {
                leftStyles = setCellStyle(cell, cellStyles, rowStyles, sheetStyles, rowPosition,
                        getColumnPosition(columnIndex), leftStyles, aboveStyles);
            } else {
                leftStyles = null;
            }
            rowCellStyles.add(leftStyles);
        }

        return rowCellStyles;
    }

    private ColumnPosition getColumnPosition(int columnIndex) {
        return columnIndex == 1 ? (columnCount == 1 ? ColumnPosition.SINGLE : ColumnPosition.LEFT)
                : columnIndex == columnCount ? ColumnPosition.RIGHT : ColumnPosition.MIDDLE;
    }

    private void setCellValue(Cell cell, Object object) {

        if (object == null) {
            // Leave cell empty
        } else if (object instanceof Short || object instanceof Integer) {
            cell.setCellValue(((Number) object).doubleValue());
            cell.setCellStyle(getOwner().getCellStyle(XlsxCellStyle.INTEGER));
        } else if (object instanceof Long || object instanceof BigInteger) {
            cell.setCellValue(object.toString());
        } else if (object instanceof BigDecimal) {
            cell.setCellValue(((Number) object).doubleValue());

            XlsxCellStyle style;
            switch (((BigDecimal) object).scale()) {
            case 2:
                style = XlsxCellStyle.TWO_DECIMAL;
                break;
            case 4:
                style = XlsxCellStyle.FOUR_DECIMAL;
                break;
            default:
                style = XlsxCellStyle.OTHER_DECIMAL;
                break;
            }
            cell.setCellStyle(getOwner().getCellStyle(style));
        } else if (object instanceof Number) {
            cell.setCellValue(((Number) object).doubleValue());
        } else if (object instanceof String) {

            String string = (String) object;
            if (0 < string.length()) {
                ValueXlsxCellStyle valueStyle = ValueXlsxCellStyle.parse(string);
                if (valueStyle.value instanceof Number) {
                    cell.setCellValue(((Number) valueStyle.value).doubleValue());
                } else {
                    cell.setCellValue((String) valueStyle.value);
                }
                if (valueStyle.style != null) {
                    cell.setCellStyle(getOwner().getCellStyle(valueStyle.style));
                }
            } else {
                cell.setCellValue(string);
            }
        } else if (object instanceof Boolean) {
            cell.setCellValue((Boolean) object);
        } else if (object instanceof Date || object instanceof LocalDateTime) {

            Date date;
            LocalTime time;

            if (object instanceof Date) {
                date = (Date) object;
                Instant instant = Instant.ofEpochMilli(date.getTime());
                time = LocalDateTime.ofInstant(instant, ZoneId.systemDefault()).toLocalTime();
            } else {
                LocalDateTime dateTime = (LocalDateTime) object;
                date = Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
                time = dateTime.toLocalTime();
            }

            cell.setCellValue(date);

            CellStyle style = null;
            if (time.equals(LocalTime.MIDNIGHT)) {
                style = getOwner().getCellStyle(XlsxCellStyle.DATE);
            } else {
                style = getOwner().getCellStyle(XlsxCellStyle.DATETIME);
            }
            cell.setCellStyle(style);
        } else {
            cell.setCellValue(object.toString());
        }
    }

    private Styles setCellStyle(Cell cell, Styles cellStyles, Styles rowStyles, ResolvedSheetStyles sheetStyles,
            RowPosition rowPosition, ColumnPosition columnPosition, Styles leftStyles, Styles aboveStyles) {

        Styles styles = sheetStyles.resolve(cellStyles, rowStyles, rowPosition, columnPosition, leftStyles,
                aboveStyles);

        adjustAdjacentCellStyle(cell, styles, rowPosition, columnPosition, leftStyles, aboveStyles);

        CellStyle originalStyle = cell.getCellStyle();

        CellStyle finalStyle = composeCellStyle(cell, styles);

        if (finalStyle != originalStyle) {
            cell.setCellStyle(finalStyle);
        }

        return styles;
    }

    private void adjustAdjacentCellStyle(Cell cell, Styles styles, RowPosition rowPosition,
            ColumnPosition columnPosition, Styles leftStyles, Styles aboveStyles) {

        if (styles == null) {
            return;
        }

        if ((leftStyles != null) && (columnPosition != ColumnPosition.LEFT)
                && (columnPosition != ColumnPosition.SINGLE) && !leftStyles.rightBorder.equals(styles.leftBorder)) {

            leftStyles.rightBorder = styles.leftBorder;

            Cell leftCell = cell.getRow().getCell(cell.getColumnIndex() - 1);

            leftCell.setCellStyle(composeCellStyle(leftCell, leftStyles));
        }

        if ((aboveStyles != null) && (rowPosition != RowPosition.HEADER) && (rowPosition != RowPosition.TOP)
                && !aboveStyles.bottomBorder.equals(styles.topBorder)) {

            aboveStyles.bottomBorder = styles.topBorder;

            Cell aboveCell = sheet.getRow(cell.getRowIndex() - 1).getCell(cell.getColumnIndex());

            aboveCell.setCellStyle(composeCellStyle(aboveCell, aboveStyles));
        }
    }

    private CellStyle composeCellStyle(Cell cell, Styles styles) {

        CellStyle originalStyle = cell.getCellStyle();

        short formatIndex = originalStyle.getIndex();

        StylesWithFormatting stylesWithFormatting = new StylesWithFormatting(styles, formatIndex);

        return stylesWithFormatting.getCellStyle(getOwner().getBook(), getOwner().stylesUsed, getOwner().fontsUsed,
                getOwner().colorsUsed);
    }

    private XlsxTargetBook getOwner() {
        return (XlsxTargetBook) owner;
    }

    @Override
    public void flush() throws IOException {

        if (0 < rowIndex) {
            // Write the bottom row but prepare for the possibility that this sheet may be appended.
            // If it is, the bottom row needs to be re-written without styling for RowPosition.BOTTOM.
            // But note that writeRow() will alter the contents of rowValues when inline styling is
            // used. So make a deep copy of rowValues, the restore it.

            ArrayList<Object> originalRowValues = new ArrayList<Object>(rowValues);

            writeRow(rowValues, rowIndex, RowPosition.BOTTOM, previousRowCellStyles);

            rowValues = originalRowValues;
        }

        try {
            for (int columnIndex = 0; columnIndex < columnCount; columnIndex++) {
                sheet.autoSizeColumn(columnIndex);
            }
        } catch (HeadlessException ex) {
            // Per https://poi.apache.org/spreadsheet/quick-guide.html:
            //
            // Warning:
            // To calculate column width Sheet.autoSizeColumn uses Java2D classes that throw exception if graphical environment is not available.
            // In case if graphical environment is not available, you must tell Java that you are running in headless mode and set the following system property:
            // java.awt.headless=true . You should also ensure that the fonts you use in your workbook are available to Java.
            //
            // Per http://www.oracle.com/technetwork/articles/javase/headless-136834.html#headlessexception:
            //
            // You can also use the following command line if you plan to run the same application in both a headless and a traditional environment:
            // java -Djava.awt.headless=true
        }

        if (headers.exist()) {
            sheet.createFreezePane(0, 1);
        }
    }

    // Never called.
    @Override
    public void open() {
    }

    @Override
    public void load() {
    }

    @Override
    public Object readColumn(int columnIndex) {
        return null;
    }

    @Override
    public boolean hasRow() {
        return false;
    }
}

class ValueXlsxCellStyle {

    public Object value;
    public XlsxCellStyle style;

    final static Pattern pattern = Pattern
            .compile("\\A(?:(-)?(?:(0|[1-9]\\d*)|([1-9]\\d{0,2}(?:,\\d{3})+))?)(?:\\.(\\d+))?\\z");

    private ValueXlsxCellStyle(Object value, XlsxCellStyle style) {
        this.value = value;
        this.style = style;
    }

    /**
     * If a string can be converted to a numeric type, convert it;
     * return the numeric type and the enumerator for the xlsx cell style that will
     * render the number to reasonably match the appearance of the original string;
     * if the string cannot be converted, return the original string
     * with a null enumerator
     *
     * Note that a value starting with a leading zero always returns the original value,
     * unless the zero is the only character or is followed immediately by a decimal point.
     *
     * This function does not correctly handle a zero-length string.  Do not pass it.
     */
    public static ValueXlsxCellStyle parse(String value) {

        final int minusGroup = 1;
        final int wholeGroup = 2;
        final int commasGroup = 3;
        final int decimalGroup = 4;

        Object valueObject = null;
        XlsxCellStyle style = null;

        Matcher matcher = pattern.matcher(value);
        if (matcher.find()) {
            boolean hasWhole = (matcher.group(wholeGroup) != null) || (matcher.group(commasGroup) != null);

            if (hasWhole) {
                boolean hasCommas = (matcher.group(commasGroup) != null);
                boolean hasDecimals = (matcher.group(decimalGroup) != null);

                if (!hasCommas) {
                    if (!hasDecimals) {
                        String wholePart = matcher.group(wholeGroup);
                        if (wholePart.length() <= 9) {
                            valueObject = new Integer(value);
                            style = XlsxCellStyle.INTEGER;
                        } else /* 9 < wholePart.length() */ {
                            valueObject = value;
                        }
                    } else /* hasDecimals */ {
                        valueObject = new BigDecimal(value);

                        String decimalPart = matcher.group(decimalGroup);
                        switch (decimalPart.length()) {
                        case 2:
                            style = XlsxCellStyle.TWO_DECIMAL;
                            break;
                        case 4:
                            style = XlsxCellStyle.FOUR_DECIMAL;
                            break;
                        default:
                            style = XlsxCellStyle.OTHER_DECIMAL;
                            break;
                        }
                    }
                } else /* hasCommas */ {
                    String valueNoCommas = value.replace(",", "");
                    if (!hasDecimals) {
                        String wholePart = matcher.group(commasGroup).replace(",", "");
                        if (wholePart.length() <= 9) {
                            valueObject = new Integer(valueNoCommas);
                            style = XlsxCellStyle.BIG_INTEGER;
                        } else /* 9 < wholePart.length() */ {
                            valueObject = value;
                        }
                    } else /* hasDecimals */ {
                        valueObject = new BigDecimal(valueNoCommas);

                        String decimalPart = matcher.group(decimalGroup);
                        switch (decimalPart.length()) {
                        case 2:
                            style = XlsxCellStyle.BIG_TWO_DECIMAL;
                            break;
                        case 4:
                            style = XlsxCellStyle.BIG_FOUR_DECIMAL;
                            break;
                        default:
                            style = XlsxCellStyle.BIG_OTHER_DECIMAL;
                            break;
                        }
                    }
                }
            } else /* !hasWhole */ {
                boolean hasMinus = (matcher.group(minusGroup) != null);
                String decimalPart = matcher.group(decimalGroup);

                String valueWithZero = (hasMinus ? "-" : "") + "0." + decimalPart;
                valueObject = new BigDecimal(valueWithZero);

                switch (decimalPart.length()) {
                case 2:
                    style = XlsxCellStyle.TWO_DECIMAL_ONLY;
                    break;
                case 4:
                    style = XlsxCellStyle.FOUR_DECIMAL_ONLY;
                    break;
                default:
                    style = XlsxCellStyle.OTHER_DECIMAL_ONLY;
                    break;
                }
            }
        } else /* !matcher.find() */ {
            valueObject = value;
        }

        return new ValueXlsxCellStyle(valueObject, style);
    }
}

class StylesWithFormatting {

    public Styles styles;
    public short formatIndex;

    StylesWithFormatting(Styles styles, short formatIndex) {
        this.styles = styles;
        this.formatIndex = formatIndex;
    }

    @Override
    public int hashCode() {
        return styles.hashCode() ^ formatIndex << 12;
    }

    @Override
    public boolean equals(Object obj) {

        if (!(obj instanceof StylesWithFormatting)) {
            return false;
        }

        StylesWithFormatting other = (StylesWithFormatting) obj;
        return styles.equals(other.styles) && (formatIndex == other.formatIndex);
    }

    /**
     * Translate styling to workbook CellStyle.
     *
     * @param stylesUsed tracks the styles that have been used in the workbook; it will be updated
     * @param fontsUsed tracks the fonts that have been used in the workbook; it may be updated
     * @param colorsUsed tracks the colors that have been used in the workbook; it may be updated
     */
    public CellStyle getCellStyle(SXSSFWorkbook book, Map<StylesWithFormatting, XSSFCellStyle> stylesUsed,
            Map<FontStyles, XSSFFont> fontsUsed, Map<Integer, XSSFColor> colorsUsed) {

        XSSFCellStyle cellStyle = stylesUsed.get(this);
        if (cellStyle != null) {
            return cellStyle;
        }

        cellStyle = (XSSFCellStyle) book.createCellStyle();
        cellStyle.cloneStyleFrom(book.getCellStyleAt(formatIndex));

        if (styles.bottomBorder.style != null) {
            cellStyle.setBorderBottom(resolveBorderStyle(styles.bottomBorder));
        }
        if (styles.leftBorder.style != null) {
            cellStyle.setBorderLeft(resolveBorderStyle(styles.leftBorder));
        }
        if (styles.rightBorder.style != null) {
            cellStyle.setBorderRight(resolveBorderStyle(styles.rightBorder));
        }
        if (styles.topBorder.style != null) {
            cellStyle.setBorderTop(resolveBorderStyle(styles.topBorder));
        }

        if (styles.bottomBorder.color != null) {
            cellStyle.setBottomBorderColor(getColor(styles.bottomBorder.color, book, colorsUsed));
        }
        if (styles.leftBorder.color != null) {
            cellStyle.setLeftBorderColor(getColor(styles.leftBorder.color, book, colorsUsed));
        }
        if (styles.rightBorder.color != null) {
            cellStyle.setRightBorderColor(getColor(styles.rightBorder.color, book, colorsUsed));
        }
        if (styles.topBorder.color != null) {
            cellStyle.setTopBorderColor(getColor(styles.topBorder.color, book, colorsUsed));
        }

        if (styles.backgroundColor != null) {
            cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
            cellStyle.setFillForegroundColor(getColor(styles.backgroundColor, book, colorsUsed));
        }

        if (styles.textAlign != null) {
            cellStyle.setAlignment(styles.textAlign);
        }

        if (!styles.font.areDefault()) {
            cellStyle.setFont(getFont(styles.font, book, fontsUsed, colorsUsed));
        }

        stylesUsed.put(this, cellStyle);

        return cellStyle;
    }

    private static BorderStyle resolveBorderStyle(BorderStyles border) {

        BorderWidth borderWidth = (border.width != null) ? border.width : BorderWidth.MEDIUM;
        BorderEdge borderStyle = (border.style != null) ? border.style : BorderEdge.NONE;

        if (borderWidth == BorderWidth.ZERO) {
            return BorderStyle.NONE;
        }

        switch (borderStyle) {
        case NONE:
        case HIDDEN:
            return BorderStyle.NONE;
        default:
        case SOLID:
            switch (borderWidth) {
            case THIN:
                return BorderStyle.THIN;
            default:
            case MEDIUM:
                return BorderStyle.MEDIUM;
            case THICK:
                return BorderStyle.THICK;
            }
        case DOUBLE:
            return BorderStyle.DOUBLE;
        case DASHED:
            switch (borderWidth) {
            case THIN:
                return BorderStyle.DASHED;
            default:
            case MEDIUM:
            case THICK:
                return BorderStyle.MEDIUM_DASHED;
            }
        case DOTTED:
            return BorderStyle.DOTTED;
        }
    }

    private static XSSFColor getColor(Integer rgb, SXSSFWorkbook book, Map<Integer, XSSFColor> colorsUsed) {

        XSSFColor color = colorsUsed.get(rgb);
        if (color != null) {
            return color;
        }

        color = new XSSFColor(new java.awt.Color(rgb));

        colorsUsed.put(rgb, color);

        return color;
    }

    private static Font getFont(FontStyles fontStyles, SXSSFWorkbook book, Map<FontStyles, XSSFFont> fontsUsed,
            Map<Integer, XSSFColor> colorsUsed) {

        XSSFFont font = fontsUsed.get(fontStyles);
        if (font != null) {
            return font;
        }

        font = (XSSFFont) book.createFont();

        if (fontStyles.color != null) {
            font.setColor(getColor(fontStyles.color, book, colorsUsed));
        }

        if (fontStyles.fontStyle != null) {
            switch (fontStyles.fontStyle) {
            case NORMAL:
                break;
            case ITALIC:
                font.setItalic(true);
                break;
            }
        }

        if (fontStyles.fontWeight != null) {
            switch (fontStyles.fontWeight) {
            case NORMAL:
                break;
            case BOLD:
                font.setBold(true);
                break;
            }
        }

        if (fontStyles.textDecorationLine != null) {
            switch (fontStyles.textDecorationLine) {
            case NONE:
                break;
            case LINE_THROUGH:
                font.setStrikeout(true);
                break;
            case UNDERLINE:
                font.setUnderline((fontStyles.textDecorationStyle == FontStyles.TextDecorationStyle.DOUBLE)
                        ? FontUnderline.DOUBLE
                        : FontUnderline.SINGLE);
                break;
            }
        }

        fontsUsed.put(fontStyles, font);

        return font;
    }
}

enum TableTag {
    TR, TH, TD
};

class ValueStyles {

    public Object value;
    public Styles styles;

    private static Pattern[] patterns;

    static {
        final String regex = "\\A<%s(?: +style *= *\"(.*?)\")? *>(.*)\\z";

        patterns = new Pattern[TableTag.values().length];
        patterns[TableTag.TR.ordinal()] = Pattern.compile(String.format(regex, "tr"));
        patterns[TableTag.TH.ordinal()] = Pattern.compile(String.format(regex, "th"));
        patterns[TableTag.TD.ordinal()] = Pattern.compile(String.format(regex, "td"));
    }

    private ValueStyles(Object value, Styles styles) {
        this.value = value;
        this.styles = styles;
    }

    /**
     * @param tag identifies an HTML tag
     * @param object is potentially a string with HTML styling
     * @return an always non-NULL object with fields set as follows.
     * <p>
     * If object is a string that starts with the indicated HTML tag optionally
     * including a style attribute, value returns the string with
     * the tag removed and styles returns a non-null Styles object reflecting the styling.
     * <p>
     * Otherwise, value returns the original object and styles returns null.
     */
    public static ValueStyles parse(TableTag tag, Object object) {

        Styles styles = null;
        if (object instanceof String) {
            Matcher matcher = patterns[tag.ordinal()].matcher((String) object);

            if (matcher.find()) {
                object = matcher.group(2);
                styles = Styles.parse(matcher.group(1));
            }
        }
        return new ValueStyles(object, styles);
    }

}

enum RowPosition {
    HEADER, NEXT /* after the header row */, TOP /* if there is no header row */, MIDDLE, BOTTOM
};

enum ColumnPosition {
    SINGLE /* if there is only one column */, LEFT, MIDDLE, RIGHT
};

class ResolvedSheetStyles extends SheetStyles {

    private RowStyles[] rowStyles;

    public RowStyles getStyles(RowPosition rowPosition) {
        return rowStyles[rowPosition.ordinal()];
    }

    private boolean areDefault = true;

    public boolean areDefault() {
        return areDefault;
    }

    public ResolvedSheetStyles(XlsxTargetSheet.TargetOptions options) {

        super(options);

        rowStyles = new RowStyles[RowPosition.values().length];

        rowStyles[RowPosition.HEADER.ordinal()] = new RowStyles();
        rowStyles[RowPosition.NEXT.ordinal()] = new RowStyles();
        rowStyles[RowPosition.TOP.ordinal()] = new RowStyles();
        rowStyles[RowPosition.MIDDLE.ordinal()] = new RowStyles();
        rowStyles[RowPosition.BOTTOM.ordinal()] = new RowStyles();

        RowStyles previousRowStyles = null;

        for (RowPosition rowPosition : RowPosition.values()) {

            RowStyles thisRowStyles = getStyles(rowPosition);
            Styles leftStyles = null;

            for (ColumnPosition columnPosition : ColumnPosition.values()) {

                Styles aboveStyles = (previousRowStyles != null) ? previousRowStyles.getStyles(columnPosition)
                        : null;

                Styles positionStyles = super.resolve(null, null, rowPosition, columnPosition, leftStyles,
                        aboveStyles);
                thisRowStyles.setStyles(columnPosition, positionStyles);

                if (!positionStyles.areDefault()) {
                    areDefault = false;
                }

                leftStyles = positionStyles;
            }

            previousRowStyles = thisRowStyles;
        }
    }

    public Styles resolve(Styles cellStyles, Styles rowStyles, RowPosition rowPosition,
            ColumnPosition columnPosition, Styles leftStyles, Styles aboveStyles) {

        if ((cellStyles == null) && (rowStyles == null) && (leftStyles == null) && (aboveStyles == null)) {
            return getStyles(rowPosition).getStyles(columnPosition);
        } else {
            return super.resolve(cellStyles, rowStyles, rowPosition, columnPosition, leftStyles, aboveStyles);
        }
    }
}

class SheetStyles {

    public Styles tableStyles;
    public Styles headStyles;
    public Styles bodyStyles;
    public Styles headCellStyles;
    public Styles bodyCellStyles;

    public SheetStyles(XlsxTargetSheet.TargetOptions options) {

        tableStyles = Styles.parse(options.getTableStyle());
        headStyles = Styles.parse(options.getHeadStyle());
        bodyStyles = Styles.parse(options.getBodyStyle());
        headCellStyles = Styles.parse(options.getHeadCellStyle());
        bodyCellStyles = Styles.parse(options.getBodyCellStyle());
    }

    public Styles resolve(Styles cellStyles, Styles rowStyles, RowPosition rowPosition,
            ColumnPosition columnPosition, Styles leftStyles, Styles aboveStyles) {

        Styles result = new Styles();

        // Top borders

        switch (rowPosition) {
        case HEADER: {
            resolveTopBorder(result, null, cellStyles, headCellStyles, rowStyles, headStyles, tableStyles);
            break;
        }
        case NEXT: {
            resolveTopBorder(result, aboveStyles, cellStyles, bodyCellStyles, rowStyles, bodyStyles);
            break;
        }
        case TOP: {
            resolveTopBorder(result, null, cellStyles, bodyCellStyles, rowStyles, bodyStyles, tableStyles);
            break;
        }
        case MIDDLE:
        case BOTTOM: {
            resolveTopBorder(result, aboveStyles, cellStyles, bodyCellStyles, rowStyles);
            break;
        }
        }

        // Left and right borders, backgrounds, fonts

        switch (rowPosition) {
        case HEADER: {
            switch (columnPosition) {
            case SINGLE:
            case LEFT: {
                resolveLeftBorder(result, null, cellStyles, headCellStyles, rowStyles, headStyles, tableStyles);
                break;
            }
            case MIDDLE:
            case RIGHT: {
                resolveLeftBorder(result, leftStyles, cellStyles, headCellStyles);
                break;
            }
            }

            switch (columnPosition) {
            case LEFT:
            case MIDDLE: {
                resolveRightBorder(result, cellStyles, headCellStyles);
                break;
            }
            case SINGLE:
            case RIGHT: {
                resolveRightBorder(result, cellStyles, headCellStyles, rowStyles, headStyles, tableStyles);
                break;
            }
            }

            resolveNonBorders(result, cellStyles, headCellStyles, rowStyles, headStyles, tableStyles);
            break;
        }
        case NEXT:
        case TOP:
        case MIDDLE:
        case BOTTOM: {
            switch (columnPosition) {
            case SINGLE:
            case LEFT: {
                resolveLeftBorder(result, null, cellStyles, bodyCellStyles, rowStyles, bodyStyles, tableStyles);
                break;
            }
            case MIDDLE:
            case RIGHT: {
                resolveLeftBorder(result, leftStyles, cellStyles, bodyCellStyles);
                break;
            }
            }

            switch (columnPosition) {
            case LEFT:
            case MIDDLE: {
                resolveRightBorder(result, cellStyles, bodyCellStyles);
                break;
            }
            case SINGLE:
            case RIGHT: {
                resolveRightBorder(result, cellStyles, bodyCellStyles, rowStyles, bodyStyles, tableStyles);
                break;
            }
            }

            resolveNonBorders(result, cellStyles, bodyCellStyles, rowStyles, bodyStyles, tableStyles);
            break;
        }
        }

        // Bottom borders

        switch (rowPosition) {
        case HEADER: {
            resolveBottomBorder(result, cellStyles, headCellStyles, rowStyles, headStyles);
            break;
        }
        case NEXT:
        case TOP:
        case MIDDLE: {
            resolveBottomBorder(result, cellStyles, bodyCellStyles, rowStyles);
            break;
        }
        case BOTTOM: {
            resolveBottomBorder(result, cellStyles, bodyCellStyles, rowStyles, bodyStyles, tableStyles);
            break;
        }
        }

        return result;
    }

    // Regarding shared borders between cells, in the CSS model border properties (width, style, color)
    // are not inherited and in the collapsing border model they are not resolved property by property.
    // Instead each shared border takes on all the properties of one of the competing borders.
    // See http://www.w3.org/TR/CSS21/tables.html#border-conflict-resolution.
    // "The rule of thumb is that at each edge the most "eye catching" border style is chosen."

    // Also, while border properties are not inherited, the first two elements of each stylesArray
    // are always essentially an inline style followed by a style defined in a <style> element,
    // so we must resolve between those two and we can do that property by property.

    private void resolveTopBorder(Styles result, Styles aboveStyles, Styles... stylesArray) {

        ArrayList<BorderStyles> competingStyles = new ArrayList<BorderStyles>();

        if (aboveStyles != null) {
            competingStyles.add(aboveStyles.bottomBorder);
        }

        competingStyles.add(new BorderStyles(resolve(Styles.topBorderWidthGetter, stylesArray[0], stylesArray[1]),
                resolve(Styles.topBorderStyleGetter, stylesArray[0], stylesArray[1]),
                resolve(Styles.topBorderColorGetter, stylesArray[0], stylesArray[1])));

        for (int i = 2; i < stylesArray.length; ++i) {
            if (stylesArray[i] != null) {
                competingStyles.add(stylesArray[i].topBorder);
            }
        }

        BorderStyles mostEyeCatchingStyles = BorderStyles.mostEyeCatching(competingStyles);

        result.topBorder = mostEyeCatchingStyles;
    }

    private void resolveLeftBorder(Styles result, Styles leftStyles, Styles... stylesArray) {

        ArrayList<BorderStyles> competingStyles = new ArrayList<BorderStyles>();

        if (leftStyles != null) {
            competingStyles.add(leftStyles.rightBorder);
        }

        competingStyles.add(new BorderStyles(resolve(Styles.leftBorderWidthGetter, stylesArray[0], stylesArray[1]),
                resolve(Styles.leftBorderStyleGetter, stylesArray[0], stylesArray[1]),
                resolve(Styles.leftBorderColorGetter, stylesArray[0], stylesArray[1])));

        for (int i = 2; i < stylesArray.length; ++i) {
            if (stylesArray[i] != null) {
                competingStyles.add(stylesArray[i].leftBorder);
            }
        }

        BorderStyles mostEyeCatchingStyles = BorderStyles.mostEyeCatching(competingStyles);

        result.leftBorder = mostEyeCatchingStyles;
    }

    private void resolveRightBorder(Styles result, Styles... stylesArray) {

        ArrayList<BorderStyles> competingStyles = new ArrayList<BorderStyles>();

        competingStyles.add(new BorderStyles(resolve(Styles.rightBorderWidthGetter, stylesArray[0], stylesArray[1]),
                resolve(Styles.rightBorderStyleGetter, stylesArray[0], stylesArray[1]),
                resolve(Styles.rightBorderColorGetter, stylesArray[0], stylesArray[1])));

        for (int i = 2; i < stylesArray.length; ++i) {
            if (stylesArray[i] != null) {
                competingStyles.add(stylesArray[i].rightBorder);
            }
        }

        BorderStyles mostEyeCatchingStyles = BorderStyles.mostEyeCatching(competingStyles);

        result.rightBorder = mostEyeCatchingStyles;
    }

    private void resolveBottomBorder(Styles result, Styles... stylesArray) {

        ArrayList<BorderStyles> competingStyles = new ArrayList<BorderStyles>();

        competingStyles
                .add(new BorderStyles(resolve(Styles.bottomBorderWidthGetter, stylesArray[0], stylesArray[1]),
                        resolve(Styles.bottomBorderStyleGetter, stylesArray[0], stylesArray[1]),
                        resolve(Styles.bottomBorderColorGetter, stylesArray[0], stylesArray[1])));

        for (int i = 2; i < stylesArray.length; ++i) {
            if (stylesArray[i] != null) {
                competingStyles.add(stylesArray[i].bottomBorder);
            }
        }

        BorderStyles mostEyeCatchingStyles = BorderStyles.mostEyeCatching(competingStyles);

        result.bottomBorder = mostEyeCatchingStyles;
    }

    private void resolveNonBorders(Styles result, Styles... stylesArray) {

        result.backgroundColor = resolve(Styles.backgroundColorGetter, stylesArray);
        result.textAlign = resolve(Styles.textAlignGetter, stylesArray);

        result.font.color = resolve(FontStyles.colorGetter, stylesArray);
        result.font.fontStyle = resolve(FontStyles.fontStyleGetter, stylesArray);
        result.font.fontWeight = resolve(FontStyles.fontWeightGetter, stylesArray);

        // In the CSS model, text_decoration-line and text-decoration-style are not inherited.
        // However, the first two arguments to this function are always essentially an inline style
        // followed by a style defined in a <style> element, so we must resolve between those two.

        result.font.textDecorationLine = resolve(FontStyles.textDecorationLineGetter, stylesArray[0],
                stylesArray[1]);
        result.font.textDecorationStyle = resolve(FontStyles.textDecorationStyleGetter, stylesArray[0],
                stylesArray[1]);
    }

    @SafeVarargs
    private static <Type> Type resolve(AnyStyles.StylesGetter<Type> getter, Styles... stylesArray) {
        for (Styles styles : stylesArray) {
            if (styles != null) {
                Type result = getter.get(styles);
                if (result != null) {
                    return result;
                }
            }
        }
        return null;
    }
}

class RowStyles {

    private Styles[] styles;

    public RowStyles() {
        styles = new Styles[ColumnPosition.values().length];

        styles[ColumnPosition.SINGLE.ordinal()] = new Styles();
        styles[ColumnPosition.LEFT.ordinal()] = new Styles();
        styles[ColumnPosition.MIDDLE.ordinal()] = new Styles();
        styles[ColumnPosition.RIGHT.ordinal()] = new Styles();
    }

    public Styles getStyles(ColumnPosition columnPosition) {
        return styles[columnPosition.ordinal()];
    }

    public void setStyles(ColumnPosition columnPosition, Styles styles) {
        this.styles[columnPosition.ordinal()] = styles;
    }
}

abstract class AnyStyles {

    public static <Type> boolean areSame(Type one, Type other) {
        return (one == null) ? (other == null) : one.equals(other);
    }

    @FunctionalInterface
    public static interface StylesGetter<Type> {
        Type get(Styles styles);
    }
}

class BorderStyles extends AnyStyles {

    public BorderWidth width = null;
    public BorderEdge style = null;
    public Integer color = null;

    public BorderStyles() {
    }

    public BorderStyles(BorderWidth width, BorderEdge style, Integer color) {
        this.width = width;
        this.style = style;
        this.color = color;
    }

    @Override
    public int hashCode() {
        return
        // 30 bits total
        ((width != null) ? width.ordinal() : BorderWidth.values().length)
                ^ ((style != null) ? style.ordinal() : BorderEdge.values().length) << 2
                ^ ((color != null) ? color.intValue() : 0x1000000) << 6;
    }

    @Override
    public boolean equals(Object obj) {

        if (!(obj instanceof BorderStyles)) {
            return false;
        }

        BorderStyles other = (BorderStyles) obj;
        return areSame(width, other.width) && areSame(style, other.style) && areSame(color, other.color);
    }

    public boolean areDefault() {
        return width == null && style == null && color == null;
    }

    public enum BorderWidth {
        ZERO, MEDIUM, THIN, THICK
    };

    public static boolean isBorderWidth(String borderProperty) {
        return getBorderWidth(borderProperty) != null;
    }

    public static BorderWidth getBorderWidth(String borderProperty) {
        switch (borderProperty) {
        case "thin":
            return BorderWidth.THIN;
        case "medium":
            return BorderWidth.MEDIUM;
        case "thick":
            return BorderWidth.THICK;
        default: {
            final Pattern pattern = Pattern.compile("\\A(\\d{1,4})px\\z");

            Matcher matcher = pattern.matcher(borderProperty);
            if (matcher.find()) {
                int px = Integer.valueOf(matcher.group(1));
                if (px == 0) {
                    return BorderWidth.ZERO;
                } else if (px == 1 || px == 2) {
                    return BorderWidth.THIN;
                } else if (px == 3 || px == 4) {
                    return BorderWidth.MEDIUM;
                } else {
                    return BorderWidth.THICK;
                }
            }
            return null;
        }
        }
    }

    public enum BorderEdge {
        NONE, HIDDEN, DOTTED, DASHED, SOLID, DOUBLE, GROOVE, RIDGE, INSET, OUTSET
    };

    public static boolean isBorderStyle(String borderProperty) {
        return getBorderStyle(borderProperty) != null;
    }

    public static BorderEdge getBorderStyle(String borderProperty) {
        switch (borderProperty) {
        case "none":
            return BorderEdge.NONE;
        case "hidden":
            return BorderEdge.HIDDEN;
        case "dotted":
            return BorderEdge.DOTTED;
        case "dashed":
            return BorderEdge.DASHED;
        case "solid":
            return BorderEdge.SOLID;
        case "double":
            return BorderEdge.DOUBLE;
        case "groove":
            return BorderEdge.GROOVE;
        case "ridge":
            return BorderEdge.RIDGE;
        case "inset":
            return BorderEdge.INSET;
        case "outset":
            return BorderEdge.OUTSET;
        default:
            return null;
        }
    }

    static private Map<BorderWidth, Integer> widthPriority;
    static {
        widthPriority = new HashMap<BorderWidth, Integer>();
        widthPriority.put(BorderWidth.THICK, 0);
        widthPriority.put(BorderWidth.MEDIUM, 1);
        widthPriority.put(null, 1); // Default width = medium
        widthPriority.put(BorderWidth.THIN, 2);
        widthPriority.put(BorderWidth.ZERO, 3);

        widthScale = widthPriority.values().stream().max(Integer::compare).get() + 1;
    }
    static private int widthScale;

    private static int widthRank(BorderStyles styles) {
        return ((styles.style == null) || (styles.style == BorderEdge.NONE)) ? widthScale
                : widthPriority.get(styles.width);
    }

    static private Map<BorderEdge, Integer> stylePriority;
    static {
        stylePriority = new HashMap<BorderEdge, Integer>();
        stylePriority.put(BorderEdge.HIDDEN, 0);
        stylePriority.put(BorderEdge.DOUBLE, 1);
        stylePriority.put(BorderEdge.SOLID, 2);
        stylePriority.put(BorderEdge.DASHED, 3);
        stylePriority.put(BorderEdge.DOTTED, 4);
        stylePriority.put(BorderEdge.RIDGE, 5);
        stylePriority.put(BorderEdge.OUTSET, 6);
        stylePriority.put(BorderEdge.GROOVE, 7);
        stylePriority.put(BorderEdge.INSET, 8);
        stylePriority.put(BorderEdge.NONE, 9);
        stylePriority.put(null, 9); // Default style = none

        styleScale = stylePriority.values().stream().max(Integer::compare).get() + 1;
    }
    static private int styleScale;

    private static int styleRank(BorderStyles styles) {
        return stylePriority.get(styles.style);
    }

    /**
     * Return the relative ranking of a border styling.  A lower rank takes precedence over a higher rank.
     *
     * @param styles is the border styling.
     * @param order is the precedence of this styling over others, all other styling considerations being equal.
     * @param length is the total number of stylings that will be ranked.
     * @return
     */
    static private int rank(BorderStyles styles, int order, int length) {
        /*
         * From http://www.w3.org/TR/CSS21/tables.html#border-conflict-resolution:
         *
         * 1. Borders with the 'border-style' of 'hidden' take precedence over all other conflicting borders.
         *    Any border with this value suppresses all borders at this location.
         *
         * 2. Borders with a style of 'none' have the lowest priority. Only if the border properties of all the elements
         *    meeting at this edge are 'none' will the border be omitted (but note that 'none' is the default value for the border style.)
         *
         * 3. If none of the styles are 'hidden' and at least one of them is not 'none', then narrow borders are discarded in favor of wider ones.
         *
         *    If several have the same 'border-width' then styles are preferred in this order:
         *    'double', 'solid', 'dashed', 'dotted', 'ridge', 'outset', 'groove', and the lowest: 'inset'.
         *
         * 4. If border styles differ only in color, then a style set on a cell wins over one on a row, which wins over a row group,
         *    column, column group and, lastly, table. When two elements of the same type conflict, then the one further to the left
         *    (if the table's 'direction' is 'ltr'; right, if it is 'rtl') and further to the top wins.
         */

        if (styles.style == BorderEdge.HIDDEN) {
            return order;
        }

        return (widthRank(styles) * styleScale + styleRank(styles)) * length + order;
    }

    static public BorderStyles mostEyeCatching(ArrayList<BorderStyles> stylesList) {

        Comparator<Integer> stylesComparator = Comparator
                .comparing(i -> rank(stylesList.get(i), i, stylesList.size()));

        int indexOfMostEyeCatching = IntStream.range(0, stylesList.size()).boxed().min(stylesComparator).get();

        return stylesList.get(indexOfMostEyeCatching);
    }
}

class FontStyles extends AnyStyles {

    public Integer color = null;
    public FontStyle fontStyle = null;
    public FontWeight fontWeight = null;
    public TextDecorationLine textDecorationLine = null;
    public TextDecorationStyle textDecorationStyle = null;

    public static final StylesGetter<Integer> colorGetter = new StylesGetter<Integer>() {
        public Integer get(Styles styles) {
            return styles.font.color;
        }
    };
    public static final StylesGetter<FontStyle> fontStyleGetter = new StylesGetter<FontStyle>() {
        public FontStyle get(Styles styles) {
            return styles.font.fontStyle;
        }
    };
    public static final StylesGetter<FontWeight> fontWeightGetter = new StylesGetter<FontWeight>() {
        public FontWeight get(Styles styles) {
            return styles.font.fontWeight;
        }
    };
    public static final StylesGetter<TextDecorationLine> textDecorationLineGetter = new StylesGetter<TextDecorationLine>() {
        public TextDecorationLine get(Styles styles) {
            return styles.font.textDecorationLine;
        }
    };
    public static final StylesGetter<TextDecorationStyle> textDecorationStyleGetter = new StylesGetter<TextDecorationStyle>() {
        public TextDecorationStyle get(Styles styles) {
            return styles.font.textDecorationStyle;
        }
    };

    @Override
    public int hashCode() {
        return
        // 20 bits total
        ((color != null) ? color.intValue() : 0x1000000)
                ^ ((fontStyle != null) ? fontStyle.ordinal() : FontStyle.values().length) << 12
                ^ ((fontWeight != null) ? fontWeight.ordinal() : FontWeight.values().length) << 14
                ^ ((textDecorationLine != null) ? textDecorationLine.ordinal()
                        : TextDecorationLine.values().length) << 16
                ^ ((textDecorationStyle != null) ? textDecorationStyle.ordinal()
                        : TextDecorationStyle.values().length) << 18;
    }

    @Override
    public boolean equals(Object obj) {

        if (!(obj instanceof FontStyles)) {
            return false;
        }

        FontStyles other = (FontStyles) obj;
        return areSame(color, other.color) && areSame(fontStyle, other.fontStyle)
                && areSame(fontWeight, other.fontWeight) && areSame(textDecorationLine, other.textDecorationLine)
                && areSame(textDecorationStyle, other.textDecorationStyle);
    }

    public boolean areDefault() {
        return color == null && fontStyle == null && fontWeight == null && textDecorationLine == null
                && textDecorationStyle == null;
    }

    public enum FontStyle {
        NORMAL, ITALIC
    };

    public static boolean isFontStyle(String fontProperty) {
        return getFontStyle(fontProperty) != null;
    }

    public static FontStyle getFontStyle(String fontProperty) {
        switch (fontProperty) {
        case "normal":
            return FontStyle.NORMAL;
        case "italic":
            return FontStyle.ITALIC;
        default:
            return null;
        }
    }

    public enum FontWeight {
        NORMAL, BOLD
    };

    public static boolean isFontWeight(String fontProperty) {
        return getFontWeight(fontProperty) != null;
    }

    public static FontWeight getFontWeight(String fontProperty) {
        switch (fontProperty) {
        case "normal":
            return FontWeight.NORMAL;
        case "bold":
            return FontWeight.BOLD;
        default:
            return null;
        }
    }

    public enum TextDecorationLine {
        NONE, LINE_THROUGH, UNDERLINE
    };

    public static boolean isTextDecorationLine(String textProperty) {
        return getTextDecorationLine(textProperty) != null;
    }

    public static TextDecorationLine getTextDecorationLine(String textProperty) {
        switch (textProperty) {
        case "none":
            return TextDecorationLine.NONE;
        case "line-through":
            return TextDecorationLine.LINE_THROUGH;
        case "underline":
            return TextDecorationLine.UNDERLINE;
        default:
            return null;
        }
    }

    public enum TextDecorationStyle {
        SINGLE, DOUBLE
    };

    public static boolean isTextDecorationStyle(String textProperty) {
        return getTextDecorationStyle(textProperty) != null;
    }

    public static TextDecorationStyle getTextDecorationStyle(String textProperty) {
        switch (textProperty) {
        case "solid":
            return TextDecorationStyle.SINGLE;
        case "double":
            return TextDecorationStyle.DOUBLE;
        default:
            return null;
        }
    }
}

class Styles extends AnyStyles {

    public BorderStyles bottomBorder = new BorderStyles();
    public BorderStyles leftBorder = new BorderStyles();
    public BorderStyles rightBorder = new BorderStyles();
    public BorderStyles topBorder = new BorderStyles();

    public Integer backgroundColor = null;
    public HorizontalAlignment textAlign = null;

    public FontStyles font = new FontStyles();

    public static final StylesGetter<BorderWidth> bottomBorderWidthGetter = new StylesGetter<BorderWidth>() {
        public BorderWidth get(Styles styles) {
            return styles.bottomBorder.width;
        }
    };
    public static final StylesGetter<BorderWidth> leftBorderWidthGetter = new StylesGetter<BorderWidth>() {
        public BorderWidth get(Styles styles) {
            return styles.leftBorder.width;
        }
    };
    public static final StylesGetter<BorderWidth> rightBorderWidthGetter = new StylesGetter<BorderWidth>() {
        public BorderWidth get(Styles styles) {
            return styles.rightBorder.width;
        }
    };
    public static final StylesGetter<BorderWidth> topBorderWidthGetter = new StylesGetter<BorderWidth>() {
        public BorderWidth get(Styles styles) {
            return styles.topBorder.width;
        }
    };

    public static final StylesGetter<BorderEdge> bottomBorderStyleGetter = new StylesGetter<BorderEdge>() {
        public BorderEdge get(Styles styles) {
            return styles.bottomBorder.style;
        }
    };
    public static final StylesGetter<BorderEdge> leftBorderStyleGetter = new StylesGetter<BorderEdge>() {
        public BorderEdge get(Styles styles) {
            return styles.leftBorder.style;
        }
    };
    public static final StylesGetter<BorderEdge> rightBorderStyleGetter = new StylesGetter<BorderEdge>() {
        public BorderEdge get(Styles styles) {
            return styles.rightBorder.style;
        }
    };
    public static final StylesGetter<BorderEdge> topBorderStyleGetter = new StylesGetter<BorderEdge>() {
        public BorderEdge get(Styles styles) {
            return styles.topBorder.style;
        }
    };

    public static final StylesGetter<Integer> bottomBorderColorGetter = new StylesGetter<Integer>() {
        public Integer get(Styles styles) {
            return styles.bottomBorder.color;
        }
    };
    public static final StylesGetter<Integer> leftBorderColorGetter = new StylesGetter<Integer>() {
        public Integer get(Styles styles) {
            return styles.leftBorder.color;
        }
    };
    public static final StylesGetter<Integer> rightBorderColorGetter = new StylesGetter<Integer>() {
        public Integer get(Styles styles) {
            return styles.rightBorder.color;
        }
    };
    public static final StylesGetter<Integer> topBorderColorGetter = new StylesGetter<Integer>() {
        public Integer get(Styles styles) {
            return styles.topBorder.color;
        }
    };

    public static final StylesGetter<Integer> backgroundColorGetter = new StylesGetter<Integer>() {
        public Integer get(Styles styles) {
            return styles.backgroundColor;
        }
    };
    public static final StylesGetter<HorizontalAlignment> textAlignGetter = new StylesGetter<HorizontalAlignment>() {
        public HorizontalAlignment get(Styles styles) {
            return styles.textAlign;
        }
    };

    @Override
    public int hashCode() {
        return ((bottomBorder.width != null) ? bottomBorder.width.ordinal() : BorderWidth.values().length)
                ^ ((leftBorder.width != null) ? leftBorder.width.ordinal() : BorderWidth.values().length) << 2
                ^ ((rightBorder.width != null) ? rightBorder.width.ordinal() : BorderWidth.values().length) << 4
                ^ ((topBorder.width != null) ? topBorder.width.ordinal() : BorderWidth.values().length) << 6 ^

                ((bottomBorder.style != null) ? bottomBorder.style.ordinal() : BorderEdge.values().length) << 8
                ^ ((leftBorder.style != null) ? leftBorder.style.ordinal() : BorderEdge.values().length) << 12
                ^ ((rightBorder.style != null) ? rightBorder.style.ordinal() : BorderEdge.values().length) << 16
                ^ ((topBorder.style != null) ? topBorder.style.ordinal() : BorderEdge.values().length) << 20 ^

                ((bottomBorder.color != null) ? bottomBorder.color.intValue() : 0x1000000)
                ^ ((leftBorder.color != null) ? leftBorder.color.intValue() : 0x1000000) << 1
                ^ ((rightBorder.color != null) ? rightBorder.color.intValue() : 0x1000000) << 2
                ^ ((topBorder.color != null) ? topBorder.color.intValue() : 0x1000000) << 3 ^

                ((backgroundColor != null) ? backgroundColor.intValue() : 0x1000000) << 5
                ^ ((textAlign != null) ? textAlign.ordinal() : HorizontalAlignment.values().length) << 28 ^

                font.hashCode() << 8;
    }

    @Override
    public boolean equals(Object obj) {

        if (!(obj instanceof Styles)) {
            return false;
        }

        Styles other = (Styles) obj;
        return bottomBorder.equals(other.bottomBorder) && leftBorder.equals(other.leftBorder)
                && rightBorder.equals(other.rightBorder) && topBorder.equals(other.topBorder) &&

                areSame(backgroundColor, other.backgroundColor) && areSame(textAlign, other.textAlign) &&

                font.equals(other.font);
    }

    public boolean areDefault() {
        return bottomBorder.areDefault() && leftBorder.areDefault() && rightBorder.areDefault()
                && topBorder.areDefault() &&

                backgroundColor == null && textAlign == null &&

                font.areDefault();
    }

    /**
     * @param styling is an HTML style attribute string
     * @return an always non-null Styles object reflecting the styling
     */
    public static Styles parse(String styling) {

        Styles result = new Styles();

        if (styling != null) {

            String[] properties = styling.split(";");
            for (String property : properties) {

                String[] keywordValue = property.split(":");
                if (keywordValue.length == 2) {

                    String keyword = keywordValue[0].toLowerCase().trim();
                    String value = keywordValue[1].toLowerCase().trim();

                    if (keyword.equals("background-color")) {
                        result.backgroundColor = getColor(value);
                    } else if (keyword.equals("border")) {
                        String[] borderProperties = value.split(" +");
                        for (String borderProperty : borderProperties) {
                            if (BorderStyles.isBorderWidth(borderProperty)) {
                                result.bottomBorder.width = BorderStyles.getBorderWidth(borderProperty);
                                result.leftBorder.width = BorderStyles.getBorderWidth(borderProperty);
                                result.rightBorder.width = BorderStyles.getBorderWidth(borderProperty);
                                result.topBorder.width = BorderStyles.getBorderWidth(borderProperty);
                            } else if (BorderStyles.isBorderStyle(borderProperty)) {
                                result.bottomBorder.style = BorderStyles.getBorderStyle(borderProperty);
                                result.leftBorder.style = BorderStyles.getBorderStyle(borderProperty);
                                result.rightBorder.style = BorderStyles.getBorderStyle(borderProperty);
                                result.topBorder.style = BorderStyles.getBorderStyle(borderProperty);
                            } else if (isColor(borderProperty)) {
                                result.bottomBorder.color = getColor(borderProperty);
                                result.leftBorder.color = getColor(borderProperty);
                                result.rightBorder.color = getColor(borderProperty);
                                result.topBorder.color = getColor(borderProperty);
                            }
                        }
                    } else if (keyword.equals("border-bottom")) {
                        String[] borderBottomProperties = value.split(" +");
                        for (String borderProperty : borderBottomProperties) {
                            if (BorderStyles.isBorderWidth(borderProperty)) {
                                result.bottomBorder.width = BorderStyles.getBorderWidth(borderProperty);
                            } else if (BorderStyles.isBorderStyle(borderProperty)) {
                                result.bottomBorder.style = BorderStyles.getBorderStyle(borderProperty);
                            } else if (isColor(borderProperty)) {
                                result.bottomBorder.color = getColor(borderProperty);
                            }
                        }
                    } else if (keyword.equals("border-bottom-color")) {
                        result.bottomBorder.color = getColor(value);
                    } else if (keyword.equals("border-bottom-style")) {
                        result.bottomBorder.style = BorderStyles.getBorderStyle(value);
                    } else if (keyword.equals("border-bottom-width")) {
                        result.bottomBorder.width = BorderStyles.getBorderWidth(value);
                    } else if (keyword.equals("border-color")) {
                        result.bottomBorder.color = getColor(value);
                        result.leftBorder.color = getColor(value);
                        result.rightBorder.color = getColor(value);
                        result.topBorder.color = getColor(value);
                    } else if (keyword.equals("border-left")) {
                        String[] borderLeftProperties = value.split(" +");
                        for (String borderProperty : borderLeftProperties) {
                            if (BorderStyles.isBorderWidth(borderProperty)) {
                                result.leftBorder.width = BorderStyles.getBorderWidth(borderProperty);
                            } else if (BorderStyles.isBorderStyle(borderProperty)) {
                                result.leftBorder.style = BorderStyles.getBorderStyle(borderProperty);
                            } else if (isColor(borderProperty)) {
                                result.leftBorder.color = getColor(borderProperty);
                            }
                        }
                    } else if (keyword.equals("border-left-color")) {
                        result.leftBorder.color = getColor(value);
                    } else if (keyword.equals("border-left-style")) {
                        result.leftBorder.style = BorderStyles.getBorderStyle(value);
                    } else if (keyword.equals("border-left-width")) {
                        result.leftBorder.width = BorderStyles.getBorderWidth(value);
                    } else if (keyword.equals("border-right")) {
                        String[] borderRightProperties = value.split(" +");
                        for (String borderProperty : borderRightProperties) {
                            if (BorderStyles.isBorderWidth(borderProperty)) {
                                result.rightBorder.width = BorderStyles.getBorderWidth(borderProperty);
                            } else if (BorderStyles.isBorderStyle(borderProperty)) {
                                result.rightBorder.style = BorderStyles.getBorderStyle(borderProperty);
                            } else if (isColor(borderProperty)) {
                                result.rightBorder.color = getColor(borderProperty);
                            }
                        }
                    } else if (keyword.equals("border-right-color")) {
                        result.rightBorder.color = getColor(value);
                    } else if (keyword.equals("border-right-style")) {
                        result.rightBorder.style = BorderStyles.getBorderStyle(value);
                    } else if (keyword.equals("border-right-width")) {
                        result.rightBorder.width = BorderStyles.getBorderWidth(value);
                    } else if (keyword.equals("border-style")) {
                        result.bottomBorder.style = BorderStyles.getBorderStyle(value);
                        result.leftBorder.style = BorderStyles.getBorderStyle(value);
                        result.rightBorder.style = BorderStyles.getBorderStyle(value);
                        result.topBorder.style = BorderStyles.getBorderStyle(value);
                    } else if (keyword.equals("border-top")) {
                        String[] borderTopProperties = value.split(" ");
                        for (String borderProperty : borderTopProperties) {
                            if (BorderStyles.isBorderWidth(borderProperty)) {
                                result.topBorder.width = BorderStyles.getBorderWidth(borderProperty);
                            } else if (BorderStyles.isBorderStyle(borderProperty)) {
                                result.topBorder.style = BorderStyles.getBorderStyle(borderProperty);
                            } else if (isColor(borderProperty)) {
                                result.topBorder.color = getColor(borderProperty);
                            }
                        }
                    } else if (keyword.equals("border-top-color")) {
                        result.topBorder.color = getColor(value);
                    } else if (keyword.equals("border-top-style")) {
                        result.topBorder.style = BorderStyles.getBorderStyle(value);
                    } else if (keyword.equals("border-top-width")) {
                        result.topBorder.width = BorderStyles.getBorderWidth(value);
                    } else if (keyword.equals("border-width")) {
                        result.bottomBorder.width = BorderStyles.getBorderWidth(value);
                        result.leftBorder.width = BorderStyles.getBorderWidth(value);
                        result.rightBorder.width = BorderStyles.getBorderWidth(value);
                        result.topBorder.width = BorderStyles.getBorderWidth(value);
                    } else if (keyword.equals("color")) {
                        result.font.color = getColor(value);
                    } else if (keyword.equals("font")) {
                        String[] fontProperties = value.split(" +");
                        for (String fontProperty : fontProperties) {
                            if (FontStyles.isFontStyle(fontProperty)) {
                                result.font.fontStyle = FontStyles.getFontStyle(fontProperty);
                            } else if (FontStyles.isFontWeight(fontProperty)) {
                                result.font.fontWeight = FontStyles.getFontWeight(fontProperty);
                            }
                        }
                    } else if (keyword.equals("font-style")) {
                        result.font.fontStyle = FontStyles.getFontStyle(value);
                    } else if (keyword.equals("font-weight")) {
                        result.font.fontWeight = FontStyles.getFontWeight(value);
                    } else if (keyword.equals("text-align")) {
                        result.textAlign = Styles.getTextAlign(value);
                    } else if (keyword.equals("text-decoration")) {
                        String[] textProperties = value.split(" +");
                        for (String textProperty : textProperties) {
                            if (FontStyles.isTextDecorationLine(textProperty)) {
                                result.font.textDecorationLine = FontStyles.getTextDecorationLine(textProperty);
                            } else if (FontStyles.isTextDecorationStyle(textProperty)) {
                                result.font.textDecorationStyle = FontStyles.getTextDecorationStyle(textProperty);
                            }
                        }
                    } else if (keyword.equals("text-decoration-line")) {
                        result.font.textDecorationLine = FontStyles.getTextDecorationLine(value);
                    } else if (keyword.equals("text-decoration-style")) {
                        result.font.textDecorationStyle = FontStyles.getTextDecorationStyle(value);
                    }
                }
            }
        }

        return result;
    }

    private static boolean isColor(String property) {
        return getColor(property) != null;
    }

    private static Integer getColor(String property) {
        Integer result = cssColor.get(property);
        if (result == null) {
            final Pattern pattern = Pattern.compile("\\#(\\p{XDigit}{6})\\z");
            Matcher matcher = pattern.matcher(property);
            if (matcher.find()) {
                result = Integer.valueOf(matcher.group(1), 16);
            }
        }
        return result;
    }

    private static Map<String, Integer> cssColor;
    static {
        // From https://www.w3schools.com/cssref/css_colors.asp
        cssColor = new HashMap<String, Integer>();
        putCssColor("AliceBlue", 0xF0F8FF);
        putCssColor("AntiqueWhite", 0xFAEBD7);
        putCssColor("Aqua", 0x00FFFF);
        putCssColor("Aquamarine", 0x7FFFD4);
        putCssColor("Azure", 0xF0FFFF);
        putCssColor("Beige", 0xF5F5DC);
        putCssColor("Bisque", 0xFFE4C4);
        putCssColor("Black", 0x000000);
        putCssColor("BlanchedAlmond", 0xFFEBCD);
        putCssColor("Blue", 0x0000FF);
        putCssColor("BlueViolet", 0x8A2BE2);
        putCssColor("Brown", 0xA52A2A);
        putCssColor("BurlyWood", 0xDEB887);
        putCssColor("CadetBlue", 0x5F9EA0);
        putCssColor("Chartreuse", 0x7FFF00);
        putCssColor("Chocolate", 0xD2691E);
        putCssColor("Coral", 0xFF7F50);
        putCssColor("CornflowerBlue", 0x6495ED);
        putCssColor("Cornsilk", 0xFFF8DC);
        putCssColor("Crimson", 0xDC143C);
        putCssColor("Cyan", 0x00FFFF);
        putCssColor("DarkBlue", 0x00008B);
        putCssColor("DarkCyan", 0x008B8B);
        putCssColor("DarkGoldenRod", 0xB8860B);
        putCssColor("DarkGray", 0xA9A9A9);
        putCssColor("DarkGrey", 0xA9A9A9);
        putCssColor("DarkGreen", 0x006400);
        putCssColor("DarkKhaki", 0xBDB76B);
        putCssColor("DarkMagenta", 0x8B008B);
        putCssColor("DarkOliveGreen", 0x556B2F);
        putCssColor("DarkOrange", 0xFF8C00);
        putCssColor("DarkOrchid", 0x9932CC);
        putCssColor("DarkRed", 0x8B0000);
        putCssColor("DarkSalmon", 0xE9967A);
        putCssColor("DarkSeaGreen", 0x8FBC8F);
        putCssColor("DarkSlateBlue", 0x483D8B);
        putCssColor("DarkSlateGray", 0x2F4F4F);
        putCssColor("DarkSlateGrey", 0x2F4F4F);
        putCssColor("DarkTurquoise", 0x00CED1);
        putCssColor("DarkViolet", 0x9400D3);
        putCssColor("DeepPink", 0xFF1493);
        putCssColor("DeepSkyBlue", 0x00BFFF);
        putCssColor("DimGray", 0x696969);
        putCssColor("DimGrey", 0x696969);
        putCssColor("DodgerBlue", 0x1E90FF);
        putCssColor("FireBrick", 0xB22222);
        putCssColor("FloralWhite", 0xFFFAF0);
        putCssColor("ForestGreen", 0x228B22);
        putCssColor("Fuchsia", 0xFF00FF);
        putCssColor("Gainsboro", 0xDCDCDC);
        putCssColor("GhostWhite", 0xF8F8FF);
        putCssColor("Gold", 0xFFD700);
        putCssColor("GoldenRod", 0xDAA520);
        putCssColor("Gray", 0x808080);
        putCssColor("Grey", 0x808080);
        putCssColor("Green", 0x008000);
        putCssColor("GreenYellow", 0xADFF2F);
        putCssColor("HoneyDew", 0xF0FFF0);
        putCssColor("HotPink", 0xFF69B4);
        putCssColor("IndianRed ", 0xCD5C5C);
        putCssColor("Indigo ", 0x4B0082);
        putCssColor("Ivory", 0xFFFFF0);
        putCssColor("Khaki", 0xF0E68C);
        putCssColor("Lavender", 0xE6E6FA);
        putCssColor("LavenderBlush", 0xFFF0F5);
        putCssColor("LawnGreen", 0x7CFC00);
        putCssColor("LemonChiffon", 0xFFFACD);
        putCssColor("LightBlue", 0xADD8E6);
        putCssColor("LightCoral", 0xF08080);
        putCssColor("LightCyan", 0xE0FFFF);
        putCssColor("LightGoldenRodYellow", 0xFAFAD2);
        putCssColor("LightGray", 0xD3D3D3);
        putCssColor("LightGrey", 0xD3D3D3);
        putCssColor("LightGreen", 0x90EE90);
        putCssColor("LightPink", 0xFFB6C1);
        putCssColor("LightSalmon", 0xFFA07A);
        putCssColor("LightSeaGreen", 0x20B2AA);
        putCssColor("LightSkyBlue", 0x87CEFA);
        putCssColor("LightSlateGray", 0x778899);
        putCssColor("LightSlateGrey", 0x778899);
        putCssColor("LightSteelBlue", 0xB0C4DE);
        putCssColor("LightYellow", 0xFFFFE0);
        putCssColor("Lime", 0x00FF00);
        putCssColor("LimeGreen", 0x32CD32);
        putCssColor("Linen", 0xFAF0E6);
        putCssColor("Magenta", 0xFF00FF);
        putCssColor("Maroon", 0x800000);
        putCssColor("MediumAquaMarine", 0x66CDAA);
        putCssColor("MediumBlue", 0x0000CD);
        putCssColor("MediumOrchid", 0xBA55D3);
        putCssColor("MediumPurple", 0x9370DB);
        putCssColor("MediumSeaGreen", 0x3CB371);
        putCssColor("MediumSlateBlue", 0x7B68EE);
        putCssColor("MediumSpringGreen", 0x00FA9A);
        putCssColor("MediumTurquoise", 0x48D1CC);
        putCssColor("MediumVioletRed", 0xC71585);
        putCssColor("MidnightBlue", 0x191970);
        putCssColor("MintCream", 0xF5FFFA);
        putCssColor("MistyRose", 0xFFE4E1);
        putCssColor("Moccasin", 0xFFE4B5);
        putCssColor("NavajoWhite", 0xFFDEAD);
        putCssColor("Navy", 0x000080);
        putCssColor("OldLace", 0xFDF5E6);
        putCssColor("Olive", 0x808000);
        putCssColor("OliveDrab", 0x6B8E23);
        putCssColor("Orange", 0xFFA500);
        putCssColor("OrangeRed", 0xFF4500);
        putCssColor("Orchid", 0xDA70D6);
        putCssColor("PaleGoldenRod", 0xEEE8AA);
        putCssColor("PaleGreen", 0x98FB98);
        putCssColor("PaleTurquoise", 0xAFEEEE);
        putCssColor("PaleVioletRed", 0xDB7093);
        putCssColor("PapayaWhip", 0xFFEFD5);
        putCssColor("PeachPuff", 0xFFDAB9);
        putCssColor("Peru", 0xCD853F);
        putCssColor("Pink", 0xFFC0CB);
        putCssColor("Plum", 0xDDA0DD);
        putCssColor("PowderBlue", 0xB0E0E6);
        putCssColor("Purple", 0x800080);
        putCssColor("RebeccaPurple", 0x663399);
        putCssColor("Red", 0xFF0000);
        putCssColor("RosyBrown", 0xBC8F8F);
        putCssColor("RoyalBlue", 0x4169E1);
        putCssColor("SaddleBrown", 0x8B4513);
        putCssColor("Salmon", 0xFA8072);
        putCssColor("SandyBrown", 0xF4A460);
        putCssColor("SeaGreen", 0x2E8B57);
        putCssColor("SeaShell", 0xFFF5EE);
        putCssColor("Sienna", 0xA0522D);
        putCssColor("Silver", 0xC0C0C0);
        putCssColor("SkyBlue", 0x87CEEB);
        putCssColor("SlateBlue", 0x6A5ACD);
        putCssColor("SlateGray", 0x708090);
        putCssColor("SlateGrey", 0x708090);
        putCssColor("Snow", 0xFFFAFA);
        putCssColor("SpringGreen", 0x00FF7F);
        putCssColor("SteelBlue", 0x4682B4);
        putCssColor("Tan", 0xD2B48C);
        putCssColor("Teal", 0x008080);
        putCssColor("Thistle", 0xD8BFD8);
        putCssColor("Tomato", 0xFF6347);
        putCssColor("Turquoise", 0x40E0D0);
        putCssColor("Violet", 0xEE82EE);
        putCssColor("Wheat", 0xF5DEB3);
        putCssColor("White", 0xFFFFFF);
        putCssColor("WhiteSmoke", 0xF5F5F5);
        putCssColor("Yellow", 0xFFFF00);
        putCssColor("YellowGreen", 0x9ACD32);
    }

    private static void putCssColor(String name, Integer rgb) {
        cssColor.put(name.toLowerCase(), rgb);
    }

    public static HorizontalAlignment getTextAlign(String textProperty) {
        switch (textProperty) {
        case "center":
            return HorizontalAlignment.CENTER;
        case "left":
            return HorizontalAlignment.LEFT;
        case "right":
            return HorizontalAlignment.RIGHT;
        default:
            return null;
        }
    }
}