This is an automated email from the ASF dual-hosted git repository. dmvolod pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/camel.git
commit ba75f65a66c10f05a860620c00531c7f1080f132 Author: Christian Ribeaud <christian.ribe...@novartis.com> AuthorDate: Wed Jan 9 22:08:07 2019 +0100 [CAMEL-13026] Add 'CsvMarshallerFactory' and corresponding tests --- .../apache/camel/dataformat/csv/CsvDataFormat.java | 25 +++- .../apache/camel/dataformat/csv/CsvMarshaller.java | 19 ++- .../camel/dataformat/csv/CsvMarshallerFactory.java | 41 ++++++ .../camel/dataformat/csv/CsvMarshalHeaderTest.java | 109 +++++++++++++++ ...MarshalHeaderWithCustomMarshallFactoryTest.java | 153 +++++++++++++++++++++ 5 files changed, 342 insertions(+), 5 deletions(-) diff --git a/components/camel-csv/src/main/java/org/apache/camel/dataformat/csv/CsvDataFormat.java b/components/camel-csv/src/main/java/org/apache/camel/dataformat/csv/CsvDataFormat.java index e9bee40..db19351 100644 --- a/components/camel-csv/src/main/java/org/apache/camel/dataformat/csv/CsvDataFormat.java +++ b/components/camel-csv/src/main/java/org/apache/camel/dataformat/csv/CsvDataFormat.java @@ -69,6 +69,8 @@ public class CsvDataFormat extends ServiceSupport implements DataFormat, DataFor private boolean useOrderedMaps; private CsvRecordConverter<?> recordConverter; + private CsvMarshallerFactory marshallerFactory = CsvMarshallerFactory.DEFAULT; + private volatile CsvMarshaller marshaller; private volatile CsvUnmarshaller unmarshaller; @@ -94,7 +96,7 @@ public class CsvDataFormat extends ServiceSupport implements DataFormat, DataFor @Override protected void doStart() throws Exception { - marshaller = CsvMarshaller.create(getActiveFormat(), this); + marshaller = marshallerFactory.create(getActiveFormat(), this); unmarshaller = CsvUnmarshaller.create(getActiveFormat(), this); } @@ -208,6 +210,27 @@ public class CsvDataFormat extends ServiceSupport implements DataFormat, DataFor } /** + * Sets the {@link CsvMarshaller} factory. + * If {@code null}, then {@link CsvMarshallerFactory#DEFAULT} is used instead. + * + * @param marshallerFactory + * @return Current {@code CsvDataFormat}, fluent API + */ + public CsvDataFormat setMarshallerFactory(CsvMarshallerFactory marshallerFactory) { + this.marshallerFactory = (marshallerFactory == null) ? CsvMarshallerFactory.DEFAULT : marshallerFactory; + return this; + } + + /** + * Returns the used {@link CsvMarshallerFactory}. + * + * @return never {@code null}. + */ + public CsvMarshallerFactory getMarshallerFactory() { + return marshallerFactory; + } + + /** * Sets the CSV format by name before applying any changes. * * @param name CSV format name diff --git a/components/camel-csv/src/main/java/org/apache/camel/dataformat/csv/CsvMarshaller.java b/components/camel-csv/src/main/java/org/apache/camel/dataformat/csv/CsvMarshaller.java index 5c5ad70..648cb4a 100644 --- a/components/camel-csv/src/main/java/org/apache/camel/dataformat/csv/CsvMarshaller.java +++ b/components/camel-csv/src/main/java/org/apache/camel/dataformat/csv/CsvMarshaller.java @@ -28,18 +28,18 @@ import java.util.Map; import org.apache.camel.Exchange; import org.apache.camel.NoTypeConversionAvailableException; import org.apache.camel.support.ExchangeHelper; -import org.apache.camel.util.IOHelper; import org.apache.camel.support.ObjectHelper; +import org.apache.camel.util.IOHelper; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVPrinter; /** * This class marshal data into a CSV format. */ -abstract class CsvMarshaller { +public abstract class CsvMarshaller { private final CSVFormat format; - private CsvMarshaller(CSVFormat format) { + protected CsvMarshaller(CSVFormat format) { this.format = format; } @@ -73,7 +73,7 @@ abstract class CsvMarshaller { * @throws IOException if we cannot write into the given stream */ public void marshal(Exchange exchange, Object object, OutputStream outputStream) throws NoTypeConversionAvailableException, IOException { - CSVPrinter printer = new CSVPrinter(new OutputStreamWriter(outputStream, ExchangeHelper.getCharsetName(exchange)), format); + CSVPrinter printer = createPrinter(exchange, outputStream); try { Iterator it = ObjectHelper.createIterator(object); while (it.hasNext()) { @@ -85,6 +85,17 @@ abstract class CsvMarshaller { } } + /** + * Creates and returns a {@link CSVPrinter}. + * + * @param exchange Exchange (used for access to type conversion). Could NOT be <code>null</code>. + * @param outputStream Output stream of the CSV. Could NOT be <code>null</code>. + * @return a new {@link CSVPrinter}. Never <code>null</code>. + */ + protected CSVPrinter createPrinter(Exchange exchange, OutputStream outputStream) throws IOException { + return new CSVPrinter(new OutputStreamWriter(outputStream, ExchangeHelper.getCharsetName(exchange)), format); + } + private Iterable<?> getRecordValues(Exchange exchange, Object data) throws NoTypeConversionAvailableException { // each row must be a map or list based Map<?, ?> map = exchange.getContext().getTypeConverter().tryConvertTo(Map.class, exchange, data); diff --git a/components/camel-csv/src/main/java/org/apache/camel/dataformat/csv/CsvMarshallerFactory.java b/components/camel-csv/src/main/java/org/apache/camel/dataformat/csv/CsvMarshallerFactory.java new file mode 100644 index 0000000..e88db04 --- /dev/null +++ b/components/camel-csv/src/main/java/org/apache/camel/dataformat/csv/CsvMarshallerFactory.java @@ -0,0 +1,41 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.dataformat.csv; + +import org.apache.commons.csv.CSVFormat; + +/** + * A {@link CsvMarshaller} factory. + */ +public interface CsvMarshallerFactory { + + CsvMarshallerFactory DEFAULT = new CsvMarshallerFactory() { + @Override + public CsvMarshaller create(CSVFormat format, CsvDataFormat dataFormat) { + return CsvMarshaller.create(format, dataFormat); + } + }; + + /** + * Creates and returns a new {@link CsvMarshaller}. + * + * @param format the <b>CSV</b> format. Can NOT be <code>null</code>. + * @param dataFormat the <b>CSV</b> data format. Can NOT be <code>null</code>. + * @return a new {@link CsvMarshaller}. + */ + CsvMarshaller create(CSVFormat format, CsvDataFormat dataFormat); +} diff --git a/components/camel-csv/src/test/java/org/apache/camel/dataformat/csv/CsvMarshalHeaderTest.java b/components/camel-csv/src/test/java/org/apache/camel/dataformat/csv/CsvMarshalHeaderTest.java new file mode 100644 index 0000000..af7c689 --- /dev/null +++ b/components/camel-csv/src/test/java/org/apache/camel/dataformat/csv/CsvMarshalHeaderTest.java @@ -0,0 +1,109 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.dataformat.csv; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.camel.Exchange; +import org.apache.camel.Produce; +import org.apache.camel.ProducerTemplate; +import org.apache.camel.RoutesBuilder; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.test.junit4.CamelTestSupport; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** + * <b>Camel</b> based test cases for {@link org.apache.camel.dataformat.csv.CsvDataFormat}. + */ +public class CsvMarshalHeaderTest extends CamelTestSupport { + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + @Produce(uri = "direct:start") + private ProducerTemplate producerTemplate; + + private File outputFile; + + @Override + protected void doPreSetup() throws Exception { + outputFile = new File(folder.newFolder(), "output.csv"); + } + + @Test + public void testSendBody() throws IOException { + Map<String, String> body = new LinkedHashMap<>(); + body.put("first_name", "John"); + body.put("last_name", "Doe"); + String fileName = outputFile.getName(); + assertEquals("output.csv", fileName); + producerTemplate.sendBodyAndHeader(body, Exchange.FILE_NAME, fileName); + body = new LinkedHashMap<>(); + body.put("first_name", "Max"); + body.put("last_name", "Mustermann"); + producerTemplate.sendBodyAndHeader(body, Exchange.FILE_NAME, fileName); + List<String> lines = Files.lines(Paths.get(outputFile.toURI())) + .filter(l -> l.trim().length() > 0).collect(Collectors.toList()); + // We got twice the headers... :( + assertEquals(4, lines.size()); + } + + @Test + public void testSendBodyWithList() throws IOException { + List<List<String>> body = Collections.singletonList(Arrays.asList("John", "Doe")); + String fileName = outputFile.getName(); + assertEquals("output.csv", fileName); + producerTemplate.sendBodyAndHeader(body, Exchange.FILE_NAME, fileName); + body = Collections.singletonList(Arrays.asList("Max", "Mustermann")); + producerTemplate.sendBodyAndHeader(body, Exchange.FILE_NAME, fileName); + List<String> lines = Files.lines(Paths.get(outputFile.toURI())) + .filter(l -> l.trim().length() > 0).collect(Collectors.toList()); + // We got twice the headers... :( + assertEquals(4, lines.size()); + } + + @Override + protected RoutesBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + String uri = String.format("file:%s?charset=utf-8&fileExist=Append", outputFile.getParentFile().getAbsolutePath()); + from("direct:start").marshal(createCsvDataFormat()).to(uri); + } + }; + } + + private static CsvDataFormat createCsvDataFormat() { + CsvDataFormat dataFormat = new CsvDataFormat(); + dataFormat.setDelimiter('\t'); + dataFormat.setTrim(true); + dataFormat.setIgnoreSurroundingSpaces(true); + dataFormat.setHeader((String[]) Arrays.asList("first_name", "last_name").toArray()); + return dataFormat; + } +} \ No newline at end of file diff --git a/components/camel-csv/src/test/java/org/apache/camel/dataformat/csv/CsvMarshalHeaderWithCustomMarshallFactoryTest.java b/components/camel-csv/src/test/java/org/apache/camel/dataformat/csv/CsvMarshalHeaderWithCustomMarshallFactoryTest.java new file mode 100644 index 0000000..0916bc4 --- /dev/null +++ b/components/camel-csv/src/test/java/org/apache/camel/dataformat/csv/CsvMarshalHeaderWithCustomMarshallFactoryTest.java @@ -0,0 +1,153 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.dataformat.csv; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.camel.Exchange; +import org.apache.camel.Produce; +import org.apache.camel.ProducerTemplate; +import org.apache.camel.RoutesBuilder; +import org.apache.camel.RuntimeCamelException; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.support.ObjectHelper; +import org.apache.camel.test.junit4.CamelTestSupport; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** + * <b>Camel</b> based test cases for {@link CsvDataFormat}. + */ +public class CsvMarshalHeaderWithCustomMarshallFactoryTest extends CamelTestSupport { + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + @Produce(uri = "direct:start") + private ProducerTemplate producerTemplate; + + private File outputFile; + + @Override + protected void doPreSetup() throws Exception { + outputFile = new File(folder.newFolder(), "output.csv"); + } + + @Test + public void testSendBody() throws IOException { + Map<String, String> body = new LinkedHashMap<>(); + body.put("first_name", "John"); + body.put("last_name", "Doe"); + String fileName = outputFile.getName(); + assertEquals("output.csv", fileName); + producerTemplate.sendBodyAndHeader(body, Exchange.FILE_NAME, fileName); + body = new LinkedHashMap<>(); + body.put("first_name", "Max"); + body.put("last_name", "Mustermann"); + producerTemplate.sendBodyAndHeader(body, Exchange.FILE_NAME, fileName); + List<String> lines = Files.lines(Paths.get(outputFile.toURI())) + .filter(l -> l.trim().length() > 0).collect(Collectors.toList()); + assertEquals(3, lines.size()); + } + + @Override + protected RoutesBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + String uri = String.format("file:%s?charset=utf-8&fileExist=Append", outputFile.getParentFile().getAbsolutePath()); + from("direct:start").marshal(createCsvDataFormat()).to(uri); + } + }; + } + + private static CsvDataFormat createCsvDataFormat() { + CsvDataFormat dataFormat = new CsvDataFormat(); + dataFormat.setDelimiter('\t'); + dataFormat.setTrim(true); + dataFormat.setIgnoreSurroundingSpaces(true); + dataFormat.setHeader((String[]) Arrays.asList("first_name", "last_name").toArray()); + dataFormat.setMarshallerFactory(new CsvMarshallerFactory() { + + @Override + public CsvMarshaller create(CSVFormat format, CsvDataFormat dataFormat) { + return new SinglePrinterCsvMarshaller(format); + } + }); + return dataFormat; + } + + // + // Helper classes + // + + private static final class SinglePrinterCsvMarshaller extends CsvMarshaller { + + private final CSVPrinter printer; + + private SinglePrinterCsvMarshaller(CSVFormat format) { + super(format); + printer = createPrinter(format); + } + + private static CSVPrinter createPrinter(CSVFormat format) { + try { + // Headers and header comments are written out in the constructor already. + return format.print(new StringBuilder()); + } catch (IOException e) { + throw RuntimeCamelException.wrapRuntimeCamelException(e); + } + } + + public void marshal(Exchange exchange, Object object, OutputStream outputStream) throws IOException { + Iterator<Map<String, String>> it = (Iterator<Map<String, String>>) ObjectHelper.createIterator(object); + synchronized (printer) { + while (it.hasNext()) { + printer.printRecord(getMapRecordValues(it.next())); + } + // Access the 'Appendable' + StringBuilder stringBuilder = (StringBuilder) printer.getOut(); + outputStream.write(stringBuilder.toString().getBytes()); + // Reset the 'Appendable' for the next exchange. + stringBuilder.setLength(0); + } + } + + @Override + protected Iterable<?> getMapRecordValues(Map<?, ?> map) { + List<String> result = new ArrayList<>(map.size()); + for (Object key : map.keySet()) { + result.add((String) map.get(key)); + } + return result; + } + } +} \ No newline at end of file