/*
* 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.text.NumberFormat;
import java.util.Map;
import java.util.UUID;
import org.joda.time.Instant;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.joda.time.LocalTime;
import org.joda.time.format.DateTimeFormatter;
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.ObjectReference;
import de.jpaw.bonaparte.pojos.meta.TemporalElementaryDataItem;
import de.jpaw.util.ByteArray;
/**
* The CSVComposer class.
*
* @author Michael Bischoff
* @version $Revision$
*
* Implements the serialization for the bonaparte format using a character Appendable, for CSV output
*/
public class CSVComposer extends AppendableComposer {
protected boolean recordStart = true;
//protected boolean shouldWarnWhenUsingFloat;
protected final CSVConfiguration cfg;
// derived data from CSVConfiguration
protected final boolean replaceSeparator; // true is a separator is specified and we use unquoted strings and the quote is null, but a quote replacement defined
protected final String stringQuote; // quote character for strings, as a string
protected final DateTimeFormatter dayFormat; // day without time (Joda)
protected final DateTimeFormatter timeFormat; // time on second precision (Joda)
protected final DateTimeFormatter time3Format; // time on millisecond precision (Joda)
protected final DateTimeFormatter timestampFormat; // day and time on second precision (Joda)
protected final DateTimeFormatter timestamp3Format; // day and time on millisecond precision (Joda)
protected final NumberFormat numberFormat; // locale's default format for formatting float and double, covers decimal point and sign
protected final NumberFormat bigDecimalFormat; // locale's default format for formatting BigDecimal, covers decimal point and sign
protected final DateTimeFormatter doDateTimeFormatter(DateTimeFormatter input) {
return cfg.timeZone == null ? input.withLocale(cfg.locale).withZoneUTC() : input.withLocale(cfg.locale).withZone(cfg.timeZone);
}
public CSVComposer(Appendable work, CSVConfiguration cfg) {
super(work, ObjectReuseStrategy.NONE); // CSV does not know about object backreferences...
this.cfg = cfg;
this.stringQuote = (cfg.quote != null) ? String.valueOf(cfg.quote) : ""; // use this for cases where a String is required
this.replaceSeparator = (cfg.quote == null) && cfg.quoteReplacement != null && cfg.separator != null && cfg.separator.length() > 0;
//this.usesDefaultDecimalPoint = cfg.decimalPoint.equals(".");
//this.shouldWarnWhenUsingFloat = cfg.decimalPoint.length() == 0; // removing decimal points from float or double is a bad idea, because no scale is defined
this.dayFormat = doDateTimeFormatter(cfg.determineDayFormatter());
this.timeFormat = doDateTimeFormatter(cfg.determineTimeFormatter());
this.time3Format = doDateTimeFormatter(cfg.determineTime3Formatter());
this.timestampFormat = doDateTimeFormatter(cfg.determineTimestampFormatter());
this.timestamp3Format = doDateTimeFormatter(cfg.determineTimestamp3Formatter());
this.numberFormat = NumberFormat.getInstance(cfg.locale);
this.numberFormat.setGroupingUsed(cfg.useGrouping);
this.bigDecimalFormat = cfg.removePoint4BD ? null : (NumberFormat)this.numberFormat.clone(); // make a copy for BigDecimal, where we set fractional digits as required
}
protected void writeSeparator() throws IOException { // nothing to do in the standard bonaparte format
if (recordStart)
recordStart = false;
else
addRawData(cfg.separator);
}
@Override
protected void terminateField() {
}
@Override
public void writeNull(FieldDefinition di) throws IOException {
writeSeparator();
}
@Override
public void writeNullCollection(FieldDefinition di) throws IOException {
writeSeparator();
}
@Override
public void startTransmission() {
}
@Override
public void terminateTransmission() {
}
@Override
public void writeSuperclassSeparator() {
}
@Override
public void startRecord() {
recordStart = true;
}
private void addCharSub(char c) throws IOException {
addRawData(cfg.quote != null && c == cfg.quote ? stringQuote : c < 0x20 ? cfg.ctrlReplacement : String.valueOf(c));
}
// field type specific output functions
// character
@Override
public void addField(MiscElementaryDataItem di, char c) throws IOException {
writeSeparator();
addCharSub(c);
}
protected void writeString(String s) throws IOException {
writeSeparator();
if (s != null) {
addRawData(stringQuote);
for (int i = 0; i < s.length(); ++i) {
addCharSub(s.charAt(i));
}
addRawData(stringQuote);
}
}
@Override
public void addField(AlphanumericElementaryDataItem di, String s) throws IOException {
writeString(replaceSeparator && s != null ? s.replace(cfg.separator, cfg.quoteReplacement): s);
}
// int(n)
@Override
public void addField(BasicNumericElementaryDataItem di, BigInteger n) throws IOException {
writeSeparator();
super.addField(di, n);
}
// decimal
@Override
public void addField(NumericElementaryDataItem di, BigDecimal n) throws IOException {
writeSeparator();
if (n != null) {
if (cfg.removePoint4BD) {
// use standard BigDecimal formatter, and remove the "." from the output
addRawData(n.setScale(di.getDecimalDigits()).toPlainString().replace(".", ""));
} else {
// use standard locale formatter to get the localized . or ,
bigDecimalFormat.setMaximumFractionDigits(n.scale());
bigDecimalFormat.setMinimumFractionDigits(n.scale());
addRawData(bigDecimalFormat.format(n));
}
}
}
// byte
@Override
public void addField(BasicNumericElementaryDataItem di, byte n) throws IOException {
writeSeparator();
super.addField(di, n);
}
// short
@Override
public void addField(BasicNumericElementaryDataItem di, short n) throws IOException {
writeSeparator();
super.addField(di, n);
}
// integer
@Override
public void addField(BasicNumericElementaryDataItem di, int n) throws IOException {
writeSeparator();
super.addField(di, n);
}
// long
@Override
public void addField(BasicNumericElementaryDataItem di, long n) throws IOException {
writeSeparator();
super.addField(di, n);
}
// boolean
@Override
public void addField(MiscElementaryDataItem di, boolean b) throws IOException {
writeSeparator();
super.addRawData(b ? cfg.booleanTrue : cfg.booleanFalse);
}
// float
@Override
public void addField(BasicNumericElementaryDataItem di, float f) throws IOException {
writeSeparator();
addRawData(numberFormat.format(f)); // format using the locale's approach
/*
String defaultFormat = Float.toString(f);
addRawData(usesDefaultDecimalPoint ? defaultFormat : defaultFormat.replace(".", cfg.decimalPoint));
if (shouldWarnWhenUsingFloat) {
shouldWarnWhenUsingFloat = false; // only warn once per record
LOGGER.warn("Using float or double and removal of decimal point may result in undefined output");
} */
}
// double
@Override
public void addField(BasicNumericElementaryDataItem di, double d) throws IOException {
writeSeparator();
addRawData(numberFormat.format(d)); // format using the locale's approach
/*
String defaultFormat = Double.toString(d);
addRawData(usesDefaultDecimalPoint ? defaultFormat : defaultFormat.replace(".", cfg.decimalPoint));
if (shouldWarnWhenUsingFloat) {
shouldWarnWhenUsingFloat = false; // only warn once per record
LOGGER.warn("Using float or double and removal of decimal point may result in undefined output");
} */
}
// UUID
@Override
public void addField(MiscElementaryDataItem di, UUID n) throws IOException {
writeSeparator();
if (n != null) {
addRawData(stringQuote);
super.addField(di, n);
addRawData(stringQuote);
}
}
// ByteArray: initial quick & dirty implementation
@Override
public void addField(BinaryElementaryDataItem di, ByteArray b) throws IOException {
writeSeparator();
if (b != null) {
addRawData(stringQuote);
super.addField(di, b);
addRawData(stringQuote);
}
}
// raw
@Override
public void addField(BinaryElementaryDataItem di, byte[] b) throws IOException {
writeSeparator();
if (b != null) {
addRawData(stringQuote);
super.addField(di, b);
addRawData(stringQuote);
}
}
// converters for DAY und TIMESTAMP
@Override
public void addField(TemporalElementaryDataItem di, LocalDate t) throws IOException {
writeSeparator();
if (t != null) {
if (cfg.datesQuoted)
addRawData(stringQuote);
addRawData(dayFormat.print(t));
if (cfg.datesQuoted)
addRawData(stringQuote);
}
}
@Override
public void addField(TemporalElementaryDataItem di, LocalTime t) throws IOException {
writeSeparator();
if (t != null) {
if (cfg.datesQuoted)
addRawData(stringQuote);
if (di.getFractionalSeconds() <= 0)
addRawData(timeFormat.print(t)); // second precision
else
addRawData(time3Format.print(t)); // millisecond precision
if (cfg.datesQuoted)
addRawData(stringQuote);
}
}
@Override
public void addField(TemporalElementaryDataItem di, LocalDateTime t) throws IOException {
writeSeparator();
if (t != null) {
if (cfg.datesQuoted)
addRawData(stringQuote);
if (di.getFractionalSeconds() <= 0)
addRawData(timestampFormat.print(t)); // second precision
else
addRawData(timestamp3Format.print(t)); // millisecond precision
if (cfg.datesQuoted)
addRawData(stringQuote);
}
}
@Override
public void addField(TemporalElementaryDataItem di, Instant t) throws IOException {
writeSeparator();
if (t != null) {
addRawData(Long.toString(t.getMillis()));
}
}
@Override
public void startMap(FieldDefinition di, int currentMembers) throws IOException {
if (cfg.mapStart != null && cfg.mapStart.length() > 0) {
super.addRawData(cfg.mapStart);
recordStart = true;
}
}
@Override
public void startArray(FieldDefinition di, int currentMembers, int sizeOfElement) throws IOException {
if (cfg.arrayStart != null && cfg.arrayStart.length() > 0) {
super.addRawData(cfg.arrayStart);
recordStart = true;
}
}
@Override
public void terminateArray() throws IOException {
if (cfg.arrayEnd != null && cfg.arrayEnd.length() > 0) {
super.addRawData(cfg.arrayEnd);
recordStart = true;
}
}
@Override
public void terminateMap() throws IOException {
if (cfg.mapEnd != null && cfg.mapEnd.length() > 0) {
super.addRawData(cfg.mapEnd);
recordStart = true;
}
}
@Override
public void startObject(ObjectReference di, BonaCustom obj) throws IOException {
if (cfg.objectStart != null && cfg.objectStart.length() > 0) {
super.addRawData(cfg.objectStart);
recordStart = true;
}
}
@Override
public void terminateObject(ObjectReference di, BonaCustom obj) throws IOException {
if (cfg.objectEnd != null && cfg.objectEnd.length() > 0) {
super.addRawData(cfg.objectEnd);
recordStart = true;
}
}
@Override
public void addField(ObjectReference di, BonaCustom obj) throws IOException {
if (obj != null) {
startObject(di, obj);
// do all fields
obj.serializeSub(this);
terminateObject(di, obj);
}
}
@Override
public void addField(ObjectReference di, Map<String, Object> obj) throws IOException {
writeSeparator();
if (obj != null) {
addRawData(stringQuote);
super.addField(di, obj);
addRawData(stringQuote);
}
}
@Override
public void addField(ObjectReference di, Object obj) throws IOException {
writeSeparator();
if (obj != null) {
addRawData(stringQuote);
super.addField(di, obj);
addRawData(stringQuote);
}
}
}