/*
* 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.core;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.UUID;
import org.joda.time.Instant;
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.FieldDefinition;
import de.jpaw.bonaparte.pojos.meta.MiscElementaryDataItem;
import de.jpaw.bonaparte.pojos.meta.NumericElementaryDataItem;
import de.jpaw.bonaparte.pojos.meta.TemporalElementaryDataItem;
/**
* The CSVComposer class.
*
* @author Michael Bischoff
* @version $Revision$
*
* Fixed width composer, suitable for the SAP IDOC format or COBOL formats.
* Numeric values if value NULL with be written as blanks.
* There is no specific way to enforce writing nulls as zero or to use the COBOL "BLANK WHEN ZERO", if either of these is required,
* the caller must preprocess the data and either convert 0 to null values or null to 0 to achieve the desired effect.
* For configuration, the CSV configuration is used.
*
* Not all data types are supported. The supported types are:
* - all String types (Ascii/Unicode/Upper/Lower) (nullable)
* - Day/Timestamp (not null)
* - boolean (not null)
* - Number (nullable)
* - long (not null, unsigned, assuming a length of 18 characters)
* - int (not null, signed, assuming a length of 9 digits)
*
* As we currently don't have all information about the fields passed as parameters, we have to make some assumptions.
*/
public class FixedWidthComposer extends CSVComposer {
private static final int MAX_SPACE_PADDING = 80;
// some cached static paddings (for speed)
private static final String SPACES = " "; // 80 characters
private static final String ZEROES = "000000000000000000"; // 18 characters
private static final String [] SPACE_PADDINGS = new String [MAX_SPACE_PADDING + 1];
private static final String [] ZERO_PADDINGS = new String [19];
static {
for (int i = 0; i <= MAX_SPACE_PADDING; ++i)
SPACE_PADDINGS[i] = SPACES.substring(0, i);
for (int i = 0; i <= 18; ++i)
ZERO_PADDINGS[i] = ZEROES.substring(0, i);
}
// utility method to provide a space padding of suitable length
private static String getPadding(int length) {
if (length <= MAX_SPACE_PADDING)
return SPACE_PADDINGS[length]; // use a cached padding
// construct a new padding
StringBuilder b = new StringBuilder(length);
while (length >= MAX_SPACE_PADDING) {
b.append(SPACE_PADDINGS[MAX_SPACE_PADDING]);
length -= MAX_SPACE_PADDING;
}
if (length > 0)
b.append(SPACE_PADDINGS[length]);
return b.toString();
}
private void numericPad(int digits) throws IOException {
if (digits > 0)
addRawData(cfg.zeroPadNumbers ? ZERO_PADDINGS[digits] : getPadding(digits));
}
public FixedWidthComposer(Appendable work, CSVConfiguration cfg) {
super(work, cfg);
}
@Override
public void writeNull(FieldDefinition di) throws IOException {
// examine the type of field in order to write the correct number of spaces
// we have to cover at least all Wrapper types
switch (di.getDataCategory()) {
case BINARY:
int paddingBytes = getFieldWidth((BinaryElementaryDataItem)di);
while (paddingBytes >= MAX_SPACE_PADDING) {
paddingBytes -= MAX_SPACE_PADDING;
addRawData(SPACE_PADDINGS[MAX_SPACE_PADDING]);
}
if (paddingBytes > 0)
addRawData(SPACE_PADDINGS[paddingBytes]);
break;
case TEMPORAL:
if (di.getDataType().equals("LocalDate")) {
addRawData(SPACE_PADDINGS[10]); // FIXME - this length could vary with the selected format
return;
}
if (di.getDataType().equals("LocalTime")) {
addRawData(SPACE_PADDINGS[8]); // FIXME - this length could vary with the selected format
return;
}
if (di.getDataType().equals("Instant")) {
addRawData(SPACE_PADDINGS[18]);
return;
}
if (di.getDataType().equals("LocalDateTime")) {
addRawData(SPACE_PADDINGS[19]); // FIXME - this length could vary with the selected format
return;
}
break;
case BASICNUMERIC:
addRawData(SPACE_PADDINGS[getFieldWidth((BasicNumericElementaryDataItem)di)]);
break;
case MISC:
if (di.getDataType().equals("Boolean")) {
addRawData(" ");
return;
}
if (di.getDataType().equals("UUID")) {
addRawData(SPACE_PADDINGS[36]);
return;
}
break;
default:
throw new RuntimeException("writeNull() for category " + di.getDataCategory() + " should be converted by specific methods such as addEnum() etc.");
}
}
@Override
public void addField(TemporalElementaryDataItem di, Instant t) throws IOException {
if (t != null) {
outputPaddedNumber(Long.toString(t.getMillis()), 18);
} else {
addRawData(SPACE_PADDINGS[18]);
}
}
@Override
public void addField(MiscElementaryDataItem di, UUID n) throws IOException {
if (n != null) {
addRawData(n.toString());
} else {
addRawData(SPACE_PADDINGS[36]);
}
}
@Override
public void addField(AlphanumericElementaryDataItem di, String s) throws IOException {
writeSeparator();
if (s != null) {
addRawData(s);
if (s.length() < di.getLength())
addRawData(getPadding(di.getLength() - s.length()));
} else {
addRawData(getPadding(di.getLength()));
}
}
protected void outputPaddedNumber(String pattern, int totalLength) throws IOException {
if (cfg.rightPadNumbers) {
addRawData(pattern);
numericPad(totalLength - pattern.length());
} else {
numericPad(totalLength - pattern.length());
addRawData(pattern);
}
}
// decimal using TRAILING SIGN
@Override
public void addField(NumericElementaryDataItem di, BigDecimal n) throws IOException {
writeSeparator();
int decimals = di.getDecimalDigits();
if (n != null) {
// space for the field is length + (1 if decimals > 0 && removePoint4BD == false) + (1 if isSigned)
boolean isNegative = n.signum() == -1;
BigDecimal absVal = isNegative ? n.negate() : n;
if (cfg.removePoint4BD) {
// use standard BigDecimal formatter, and remove the "." from the output
outputPaddedNumber(absVal.setScale(decimals).toPlainString().replace(".", ""), di.getTotalDigits());
} else {
// use standard locale formatter to get the localized . or ,
bigDecimalFormat.setMaximumFractionDigits(decimals);
bigDecimalFormat.setMinimumFractionDigits(decimals);
outputPaddedNumber(bigDecimalFormat.format(absVal), di.getTotalDigits() + (decimals > 0 ? 1 : 0));
}
if (di.getIsSigned()) {
addRawData(isNegative ? "-" : " ");
}
} else {
// write an appropriate number of spaces
addRawData(getPadding(getFieldWidth(di)));
}
}
// generic method for all integral types, also supports a fixed decimal point.
// The point consumes another byte of space, unless it is at position zero.
private void paddedFixedWidthString(BasicNumericElementaryDataItem di, String s) throws IOException {
if (di.getDecimalDigits() == 0 || cfg.removePoint4BD) {
// no decimal point at all
numericPad(di.getTotalDigits() - s.length());
addRawData(s);
} else {
if (s.length() >= di.getDecimalDigits()) {
// padding without interruption
numericPad(di.getTotalDigits() - s.length());
if (s.length() == di.getDecimalDigits()) {
addRawData(".");
addRawData(s);
} else {
// 2 real substrings
addRawData(s.substring(0, s.length() - di.getTotalDigits()));
addRawData(".");
addRawData(s.substring(s.length() - di.getTotalDigits()));
}
} else {
// decimal point is somewhere within the padding: split it to make sure second part is definitely zeros!
numericPad(di.getTotalDigits() - di.getDecimalDigits());
addRawData(".");
addRawData(ZERO_PADDINGS[di.getDecimalDigits() - s.length()]);
addRawData(s);
}
}
}
// long (SIGNED, LEADING SIGN)
@Override
public void addField(BasicNumericElementaryDataItem di, long n) throws IOException {
writeSeparator();
if (di.getIsSigned()) {
addRawData(n < 0 ? "-" : " ");
if (n < 0L)
n = -n;
if (n < 0L) {
// must have been MINVAL => special treatment here
paddedFixedWidthString(di, "9223372036854775808");
return;
}
}
paddedFixedWidthString(di, Long.toString(n));
}
// int (SIGNED, LEADING SIGN)
@Override
public void addField(BasicNumericElementaryDataItem di, int n) throws IOException {
writeSeparator();
if (di.getIsSigned()) {
addRawData(n < 0 ? "-" : " ");
if (n < 0)
n = -n;
if (n < 0) {
// must have been MINVAL => special treatment here
paddedFixedWidthString(di, "2147483648");
return;
}
}
paddedFixedWidthString(di, Integer.toString(n));
}
// int (SIGNED, LEADING SIGN)
@Override
public void addField(BasicNumericElementaryDataItem di, short n) throws IOException {
writeSeparator();
if (di.getIsSigned()) {
addRawData(n < 0 ? "-" : " ");
}
paddedFixedWidthString(di, Integer.toString(n < 0 ? -(int)n : n));
}
// int (SIGNED, LEADING SIGN)
@Override
public void addField(BasicNumericElementaryDataItem di, byte n) throws IOException {
writeSeparator();
if (di.getIsSigned()) {
addRawData(n < 0 ? "-" : " ");
}
paddedFixedWidthString(di, Integer.toString(n < 0 ? -(int)n : n));
}
// int(n) (SIGNED AND UNSIGNED; specific length, LEADING SIGN), null possible
@Override
public void addField(BasicNumericElementaryDataItem di, BigInteger n) throws IOException {
writeSeparator();
if (n == null) {
addRawData(SPACE_PADDINGS[getFieldWidth(di)]);
} else {
if (cfg.rightPadNumbers) {
String val = n.toString();
addRawData(val);
addRawData(SPACE_PADDINGS[di.getTotalDigits() - val.length()]);
} else {
if (di.getIsSigned()) {
if (n.signum() < 0) {
addRawData("-");
n = n.negate();
} else {
addRawData(" ");
}
}
String val = n.toString();
numericPad(di.getTotalDigits() - val.length());
addRawData(val);
}
}
}
protected int getFieldWidth(BasicNumericElementaryDataItem di) {
return di.getTotalDigits() + (di.getIsSigned() ? 1 : 0) + (di.getDecimalDigits() > 0 && !cfg.removePoint4BD ? 1 : 0);
}
protected int getFieldWidth(BinaryElementaryDataItem di) {
return (di.getLength() + 2) / 3 * 4;
}
}