/* * Copyright 2012 Michael Bischoff * * 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 de.jpaw.bonaparte.poi; import java.io.ByteArrayOutputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.math.BigDecimal; import java.math.BigInteger; import java.util.List; import java.util.Map; import java.util.UUID; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.DataFormat; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.joda.time.Instant; import org.joda.time.LocalDate; import org.joda.time.LocalDateTime; import org.joda.time.LocalTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import de.jpaw.bonaparte.core.AbstractMessageComposer; import de.jpaw.bonaparte.core.BonaCustom; import de.jpaw.bonaparte.core.BonaparteJsonEscaper; import de.jpaw.bonaparte.enums.BonaNonTokenizableEnum; import de.jpaw.bonaparte.enums.BonaTokenizableEnum; import de.jpaw.bonaparte.pojos.meta.AlphanumericElementaryDataItem; import de.jpaw.bonaparte.pojos.meta.BasicNumericElementaryDataItem; import de.jpaw.bonaparte.pojos.meta.BinaryElementaryDataItem; import de.jpaw.bonaparte.pojos.meta.EnumDataItem; import de.jpaw.bonaparte.pojos.meta.FieldDefinition; import de.jpaw.bonaparte.pojos.meta.MiscElementaryDataItem; import de.jpaw.bonaparte.pojos.meta.NumericElementaryDataItem; import de.jpaw.bonaparte.pojos.meta.ObjectReference; import de.jpaw.bonaparte.pojos.meta.TemporalElementaryDataItem; import de.jpaw.bonaparte.pojos.meta.XEnumDataItem; import de.jpaw.bonaparte.util.DayTime; import de.jpaw.enums.XEnum; import de.jpaw.util.Base64; import de.jpaw.util.ByteArray; import de.jpaw.util.ByteBuilder; import de.jpaw.util.IntegralLimits; // according to http://stackoverflow.com/questions/469695/decode-base64-data-in-java , xml.bind is included in Java 6 SE //import javax.xml.bind.DatatypeConverter; /** * Implements the output of Bonaparte objects into Excel formats. * * For a description of the codes MS Excel wants, please see here: * http://office.microsoft.com/assistance/hfws.aspx?AssetID=HA010346351033#BMnumeralshape * * @author Michael Bischoff * @version $Revision$ */ public class BaseExcelComposer extends AbstractMessageComposer<RuntimeException> implements ExcelWriter { private static final Logger LOGGER = LoggerFactory.getLogger(BaseExcelComposer.class); protected static final int MAX_DECIMALS = 18; protected final Workbook xls; protected final DataFormat xlsDataFormat; protected final CellStyle csLong; protected final CellStyle [] csBigDecimal; // one per number of decimals, cache protected final CellStyle csDay; protected final CellStyle csTime; protected final CellStyle csTimestamp; private final String [] BIGDECIMAL_FORMATS = { "#0", "#0.#", "#0.##", "#0.###", "#0.####", "#0.#####", "#0.######", "#0.#######", "#0.########", "#0.#########", "#0.##########", "#0.###########", "#0.############", "#0.#############", "#0.##############", "#0.###############", "#0.################", "#0.#################", "#0.##################" }; private Sheet sheet = null; private Row row; private int rownum = -1; private int column = 0; private int sheetNum = 0; public BaseExcelComposer(Workbook xls) { this.xls = xls; // create a few data formats xlsDataFormat = xls.createDataFormat(); csLong = xls.createCellStyle(); csLong.setDataFormat(xlsDataFormat.getFormat("#,###,###,###,###,###,###,###,###,##0")); csBigDecimal = new CellStyle[1 + MAX_DECIMALS]; csDay = xls.createCellStyle(); csDay.setDataFormat(xlsDataFormat.getFormat("yyyy-mm-dd")); csTime = xls.createCellStyle(); csTime.setDataFormat(xlsDataFormat.getFormat("hh:mm:ss")); csTimestamp = xls.createCellStyle(); csTimestamp.setDataFormat(xlsDataFormat.getFormat("yyyy-mm-dd hh:mm:ss")); } public void newSheet(String name) { sheet = xls.createSheet(); xls.setSheetName(sheetNum, name); rownum = -1; ++sheetNum; } public void closeSheet() { } private CellStyle getCachedCellStyle(int decimals) { if (decimals < 0 || decimals > MAX_DECIMALS) return null; // no application format if (csBigDecimal[decimals] == null) { CellStyle newStyle = xls.createCellStyle(); newStyle.setDataFormat(xlsDataFormat.getFormat(BIGDECIMAL_FORMATS[decimals])); csBigDecimal[decimals] = newStyle; return newStyle; } else { return csBigDecimal[decimals]; } } /** Write the current state of the Workbook onto a stream. */ @Override public void write(OutputStream os) throws IOException { xls.write(os); } @Override public void writeToFile(String filename) throws IOException { try (FileOutputStream out = new FileOutputStream(filename)) { write(out); } } @Override public byte[] getBytes() throws IOException { byte [] result = null; try (ByteArrayOutputStream out = new ByteArrayOutputStream(50000)) { write(out); out.flush(); result = out.toByteArray(); } return result; } protected void setFieldWidth(FieldDefinition di) { int gap = 3; // addon for graphical reasons (border dist) int width = 8; // this is the xls default switch (di.getDataCategory()) { case BASICNUMERIC: case NUMERIC: BasicNumericElementaryDataItem ni = (BasicNumericElementaryDataItem)di; width = ni.getTotalDigits() + (ni.getIsSigned() ? 1 : 0) + (ni.getDecimalDigits() > 0 ? 1 : 0); // allow 1 for sign and decimal point) break; case BINARY: break; case ENUM: width = 3; // small number break; case ENUMALPHA: width = ((XEnumDataItem)di).getBaseXEnum().getBaseEnum().getMaxTokenLength(); break; case ENUMSET: break; case ENUMSETALPHA: break; case MISC: break; case OBJECT: break; case STRING: int len = ((AlphanumericElementaryDataItem)di).getLength(); width = len > 32 ? 32 : len; break; case TEMPORAL: width = 20; // 10 for date, 8 for time break; case XENUM: width = ((XEnumDataItem)di).getBaseXEnum().getBaseEnum().getMaxTokenLength(); break; case XENUMSET: break; default: break; } LOGGER.debug("Setting width of column {} ({}); to {}", column, di.getName(), width); sheet.setColumnWidth(column, (width + gap) * 256); } /************************************************************************************************** * Serialization goes here **************************************************************************************************/ // protected void writeNull() { // ++column; // no output for empty cells, but ensure that everything goes nicely into the correct column // } @Override public void writeNull(FieldDefinition di) { ++column; // no output for empty cells, but ensure that everything goes nicely into the correct column if (rownum == 0) setFieldWidth(di); } @Override public void writeNullCollection(FieldDefinition di) { ++column; // no output for empty cells, but ensure that everything goes nicely into the correct column } @Override public void startTransmission() { newSheet("Sheet " + (sheetNum+1)); } @Override public void terminateTransmission() { closeSheet(); } @Override public void terminateRecord() { } @Override public void writeSuperclassSeparator() { } @Override public void startRecord() { ++rownum; column = -1; row = sheet.createRow(rownum); } private Cell newCell(FieldDefinition di) { ++column; if (rownum == 0) setFieldWidth(di); return row.createCell(column); } // create a new cell and apply an existng cell style to it private Cell newCell(FieldDefinition di, CellStyle cs) { Cell cell = newCell(di); if (cs != null) cell.setCellStyle(cs); return cell; } // field type specific output functions // character @Override public void addField(MiscElementaryDataItem di, char c) { newCell(di).setCellValue(String.valueOf(c)); } // ascii only (unicode uses different method) @Override public void addField(AlphanumericElementaryDataItem di, String s) { if (s != null) newCell(di).setCellValue(s); else writeNull(di); } // decimal @Override public void addField(NumericElementaryDataItem di, BigDecimal n) { if (n != null) { newCell(di, getCachedCellStyle(n.scale())).setCellValue(n.doubleValue()); } else { writeNull(di); } } // output a non-null number which was stored with possibly implicit fixed point private void addScaledNumber(BasicNumericElementaryDataItem di, double n) { int fractionalDigits = di.getDecimalDigits(); if (fractionalDigits > 0) newCell(di, getCachedCellStyle(fractionalDigits)).setCellValue(n * IntegralLimits.IMPLICIT_SCALES[fractionalDigits]); else newCell(di, csLong).setCellValue(n); } // byte @Override public void addField(BasicNumericElementaryDataItem di, byte n) { addScaledNumber(di, n); } // short @Override public void addField(BasicNumericElementaryDataItem di, short n) { addScaledNumber(di, n); } // integer @Override public void addField(BasicNumericElementaryDataItem di, int n) { addScaledNumber(di, n); } // int(n) @Override public void addField(BasicNumericElementaryDataItem di, BigInteger n) { if (n != null) { newCell(di).setCellValue(n.doubleValue()); } else { writeNull(di); } } // long @Override public void addField(BasicNumericElementaryDataItem di, long n) { addScaledNumber(di, n); } // boolean @Override public void addField(MiscElementaryDataItem di, boolean b) { newCell(di).setCellValue(b); } // float @Override public void addField(BasicNumericElementaryDataItem di, float f) { newCell(di).setCellValue(f); } // double @Override public void addField(BasicNumericElementaryDataItem di, double d) { newCell(di).setCellValue(d); } // UUID @Override public void addField(MiscElementaryDataItem di, UUID n) { if (n != null) { newCell(di).setCellValue(n.toString()); } else { writeNull(di); } } // ByteArray: initial quick & dirty implementation @Override public void addField(BinaryElementaryDataItem di, ByteArray b) { if (b != null) { ByteBuilder tmp = new ByteBuilder((b.length() * 2) + 4, null); Base64.encodeToByte(tmp, b.getBytes(), 0, b.length()); newCell(di).setCellValue(new String(tmp.getCurrentBuffer(), 0, tmp.length())); } else { writeNull(di); } } // raw @Override public void addField(BinaryElementaryDataItem di, byte[] b) { if (b != null) { ByteBuilder tmp = new ByteBuilder((b.length * 2) + 4, null); Base64.encodeToByte(tmp, b, 0, b.length); newCell(di).setCellValue(new String(tmp.getCurrentBuffer(), 0, tmp.length())); } else { writeNull(di); } } // converters for DAY und TIMESTAMP @Override public void addField(TemporalElementaryDataItem di, LocalDate t) { if (t != null) { newCell(di, csDay).setCellValue(t.toDate()); } else { writeNull(di); } } @Override public void addField(TemporalElementaryDataItem di, LocalDateTime t) { if (t != null) { newCell(di, csTimestamp).setCellValue(t.toDate()); } else { writeNull(di); } } @Override public void addField(TemporalElementaryDataItem di, LocalTime t) { if (t != null) { newCell(di, csTime).setCellValue(DayTime.toDate(t)); } else { writeNull(di); } } @Override public void addField(TemporalElementaryDataItem di, Instant t) { if (t != null) { newCell(di, csTimestamp).setCellValue(t.toDate()); } else { writeNull(di); } } @Override public void startMap(FieldDefinition di, int currentMembers) { } @Override public void startArray(FieldDefinition di, int currentMembers, int sizeOfElement) { } @Override public void terminateArray() { } @Override public void terminateMap() { } @Override public void startObject(ObjectReference di, BonaCustom obj) { } @Override public void terminateObject(ObjectReference di, BonaCustom obj) { } /** Adding objects will lead to column misalignment if the objects itself are null. */ @Override public void addField(ObjectReference di, BonaCustom obj) { if (obj != null) { obj.serializeSub(this); // no start and stop for now... } } // enum with numeric expansion: delegate to Null/Int @Override public void addEnum(EnumDataItem di, BasicNumericElementaryDataItem ord, BonaNonTokenizableEnum n) { if (n == null) writeNull(ord); else addField(ord, n.ordinal()); } // enum with alphanumeric expansion: delegate to Null/String @Override public void addEnum(EnumDataItem di, AlphanumericElementaryDataItem token, BonaTokenizableEnum n) { if (n == null) writeNull(token); else addField(token, n.getToken()); } @Override public void addEnum(XEnumDataItem di, AlphanumericElementaryDataItem token, XEnum<?> n) { if (n == null) writeNull(token); else addField(token, n.getToken()); } @Override public boolean addExternal(ObjectReference di, Object obj) { return false; // perform conversion by default } @Override public void addField(ObjectReference di, Map<String, Object> obj) { if (obj == null) writeNull(di); else newCell(di).setCellValue(BonaparteJsonEscaper.asJson(obj)); } @Override public void addField(ObjectReference di, List<Object> obj) throws RuntimeException { if (obj == null) writeNull(di); else newCell(di).setCellValue(BonaparteJsonEscaper.asJson(obj)); } @Override public void addField(ObjectReference di, Object obj) { if (obj == null) writeNull(di); else newCell(di).setCellValue(BonaparteJsonEscaper.asJson(obj)); } }