/*
* Copyright (c) 2009-2015
* IT-Consulting Stephan Schloepke (http://www.schloepke.de/)
* klemm software consulting Mirko Klemm (http://www.klemm-scs.com/)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.jbasics.csv;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.text.DecimalFormatSymbols;
import java.util.HashMap;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
import org.jbasics.configuration.properties.BooleanValueTypeFactory;
import org.jbasics.configuration.properties.SystemProperty;
import static org.jbasics.utilities.DataUtilities.coalesce;
@Provider
@Produces({"text/csv","text/plain"})
@Consumes({"text/csv","text/plain"})
public class CSVTableProvider implements MessageBodyReader<CSVTable>,MessageBodyWriter<CSVTable> {
public static final String USE_SEMICOLON_AS_STANDARD_PROPERTY = "org.jbasics.csv.CSVTableProvider.invertAlternateSeparator";
public static final SystemProperty<Boolean> INVERT_ALTERNATE_SEPARATOR = SystemProperty.booleanProperty(CSVTableProvider.USE_SEMICOLON_AS_STANDARD_PROPERTY, Boolean.FALSE);
public static final String GERMAN_GUESS_PROPERTY = "org.jbasics.csv.CSVTableProvider.germanUseSemicolonSeparator";
public static final SystemProperty<Boolean> USE_GERMAN_SEPARATOR_DETECTION = SystemProperty.booleanProperty(CSVTableProvider.GERMAN_GUESS_PROPERTY, Boolean.FALSE);
public static final String AUTO_GUESS_PROPERTY = "org.jbasics.csv.CSVTableProvider.separatorAutoGuessing";
public static final SystemProperty<Boolean> USE_SEPARATOR_AUTO_GUESS = SystemProperty.booleanProperty(CSVTableProvider.AUTO_GUESS_PROPERTY, Boolean.FALSE);
private final Logger logger = Logger.getLogger(CSVTableProvider.class.getName());
@Override
public boolean isReadable(final Class<?> type, final Type genericType, final Annotation[] annotations, final MediaType mediaType) {
return CSVTable.class.isAssignableFrom(type);
}
@Override
public CSVTable readFrom(final Class<CSVTable> type, final Type genericType, final Annotation[] annotations, final MediaType mediaType, final MultivaluedMap<String, String> httpHeaders, final InputStream entityStream) throws IOException, WebApplicationException {
final Charset charset = Charset.forName(coalesce(mediaType.getParameters().get("charset"), "ISO-8859-15"));
final Reader r = new BufferedReader(new InputStreamReader(entityStream, charset), 16384);
final boolean headerPresent = CSVTable.HEADER_PRESENT.right().equalsIgnoreCase(mediaType.getParameters().get(CSVTable.HEADER_PRESENT.first()));
boolean useAlternateSeparator = CSVTableProvider.INVERT_ALTERNATE_SEPARATOR.value().booleanValue();
final String temp = mediaType.getParameters().get("use-alternate-separator"); //$NON-NLS-1$
if (temp != null) {
if (BooleanValueTypeFactory.SHARED_INSTANCE.create(temp).booleanValue()) {
useAlternateSeparator = !useAlternateSeparator;
}
} else if (CSVTableProvider.USE_GERMAN_SEPARATOR_DETECTION.value().booleanValue() && coalesce(httpHeaders.getFirst("Content-Language"), "en").equals("de")) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
useAlternateSeparator = true;
} else if (CSVTableProvider.USE_SEPARATOR_AUTO_GUESS.value().booleanValue() && r.markSupported()) {
r.mark(8192);
try {
final int[] commaCount = getRecordsLength(new CSVRecordReader(r, CSVSeparator.SEMICOLON), 2);
r.reset();
final int[] semicolonCount = getRecordsLength(new CSVRecordReader(r, CSVSeparator.SEMICOLON), 2);
// 1. check if both are equal if not use the equal one
if (commaCount[0] == commaCount[1]) {
if (semicolonCount[0] == semicolonCount[1]) {
if (commaCount[0] < semicolonCount[0]) {
useAlternateSeparator = true;
} else {
useAlternateSeparator = false;
}
} else {
useAlternateSeparator = false;
}
} else if (semicolonCount[0] == semicolonCount[1]) {
useAlternateSeparator = true;
} else if (headerPresent) {
if (commaCount[0] < semicolonCount[0]) {
useAlternateSeparator = true;
} else {
useAlternateSeparator = false;
}
}
} catch (final Exception e) {
this.logger.log(Level.WARNING, "Exception thrown in guessing the separator upon reading the first lines", e); //$NON-NLS-1$
} finally {
r.reset();
}
}
final CSVParser p = new CSVParser(headerPresent, useAlternateSeparator ? ';' : ',', true);
return p.parse(r);
}
private int[] getRecordsLength(final CSVRecordReader reader, final int lines) throws IOException {
assert lines > 0 && reader != null;
final int[] result = new int[lines];
for (int i = 0; i < lines; i++) {
final CSVRecord temp = reader.readNext();
result[i] = temp == null ? -1 : temp.size();
}
return result;
}
@Override public boolean isWriteable(final Class<?> type, final Type genericType, final Annotation[] annotations, final MediaType mediaType) {
return CSVTable.class.isAssignableFrom(type);
}
@Override public long getSize(final CSVTable csvRecords, final Class<?> type, final Type genericType, final Annotation[] annotations, final MediaType mediaType) {
return -1;
}
@Override
public void writeTo(final CSVTable csvTable, final Class<?> type, final Type genericType, final Annotation[] annotations, final MediaType mediaType,
final MultivaluedMap<String, Object> responseHeaders, final OutputStream entityStream) throws IOException, WebApplicationException {
final Charset charset = mediaType.getParameters().get("charset") == null ? coalesce(csvTable.getCharset(), Charset.forName("windows-1252")) : Charset.forName(mediaType.getParameters().get("charset"));
final boolean headerPresent = mediaType.getParameters().get("header") == null ? csvTable.hasHeaders() : mediaType.getParameters().get("header").equals("present");
responseHeaders.putSingle(HttpHeaders.CONTENT_TYPE, new MediaType(mediaType.getType(), mediaType.getSubtype(), new HashMap<String,String>(){{put("charset", charset.name()); put("header", headerPresent ? "present" : "absent");}}));
try(final OutputStreamWriter ow = new OutputStreamWriter(entityStream, charset)) {
csvTable.append(ow, getPreferredSeparator(responseHeaders.getFirst(HttpHeaders.CONTENT_LANGUAGE), mediaType));
}
}
private char getPreferredSeparator(final Object langHeader, final MediaType mediaType) {
final String separator = mediaType.getParameters().get("separator");
if(separator == null) {
final String useAlternateSeparatorString = mediaType.getParameters().get("use-alternate-separator");
final boolean useAlternateSeparator = useAlternateSeparatorString != null && BooleanValueTypeFactory.SHARED_INSTANCE.create(useAlternateSeparatorString);
final Locale locale = findLocale(langHeader);
final DecimalFormatSymbols decimalFormat = DecimalFormatSymbols.getInstance(locale);
return decimalFormat.getDecimalSeparator() == ',' ^ useAlternateSeparator ? ';' : ',';
} else {
return separator.charAt(0);
}
}
private Locale findLocale(final Object headerValue) {
return headerValue == null ? Locale.ROOT : headerValue instanceof Locale ? (Locale)headerValue : Locale.forLanguageTag(headerValue.toString());
}
}