/** * 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.bindy.fixed; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Scanner; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import org.apache.camel.Exchange; import org.apache.camel.dataformat.bindy.BindyAbstractDataFormat; import org.apache.camel.dataformat.bindy.BindyAbstractFactory; import org.apache.camel.dataformat.bindy.BindyFixedLengthFactory; import org.apache.camel.dataformat.bindy.FormatFactory; import org.apache.camel.dataformat.bindy.util.ConverterUtils; import org.apache.camel.spi.DataFormat; import org.apache.camel.util.IOHelper; import org.apache.camel.util.ObjectHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A <a href="http://camel.apache.org/data-format.html">data format</a> ( * {@link DataFormat}) using Bindy to marshal to and from Fixed Length */ public class BindyFixedLengthDataFormat extends BindyAbstractDataFormat { public static final String CAMEL_BINDY_FIXED_LENGTH_HEADER = "CamelBindyFixedLengthHeader"; public static final String CAMEL_BINDY_FIXED_LENGTH_FOOTER = "CamelBindyFixedLengthFooter"; private static final Logger LOG = LoggerFactory.getLogger(BindyFixedLengthDataFormat.class); private BindyFixedLengthFactory headerFactory; private BindyFixedLengthFactory footerFactory; public BindyFixedLengthDataFormat() { } public BindyFixedLengthDataFormat(Class<?> type) { super(type); } @Override public String getDataFormatName() { return "bindy-fixed"; } @SuppressWarnings("unchecked") public void marshal(Exchange exchange, Object body, OutputStream outputStream) throws Exception { BindyFixedLengthFactory factory = (BindyFixedLengthFactory) getFactory(); ObjectHelper.notNull(factory, "not instantiated"); // Get CRLF byte[] bytesCRLF = ConverterUtils.getByteReturn(factory.getCarriageReturn()); List<Map<String, Object>> models; // the body is not a prepared list so help a bit here and create one for us if (!isPreparedList(body)) { models = new ArrayList<Map<String, Object>>(); Iterator<?> it = ObjectHelper.createIterator(body); while (it.hasNext()) { Object model = it.next(); String name = model.getClass().getName(); Map<String, Object> row = new HashMap<String, Object>(); row.put(name, model); row.putAll(createLinkedFieldsModel(model)); models.add(row); } } else { // cast to the expected type models = (List<Map<String, Object>>) body; } // add the header if it is in the exchange header Map<String, Object> headerRow = (Map<String, Object>) exchange.getIn().getHeader(CAMEL_BINDY_FIXED_LENGTH_HEADER); if (headerRow != null) { models.add(0, headerRow); } // add the footer if it is in the exchange header Map<String, Object> footerRow = (Map<String, Object>) exchange.getIn().getHeader(CAMEL_BINDY_FIXED_LENGTH_FOOTER); if (footerRow != null) { models.add(models.size(), footerRow); } int row = 0; for (Map<String, Object> model : models) { row++; String result = null; if (row == 1 && headerFactory != null) { // marshal the first row as a header if the models match Set<String> modelClassNames = model.keySet(); // only use the header factory if the row is the header if (headerFactory.supportsModel(modelClassNames)) { if (factory.skipHeader()) { LOG.info("Skipping marshal of header row; 'skipHeader=true'"); continue; } else { result = headerFactory.unbind(model); } } } else if (row == models.size() && footerFactory != null) { // marshal the last row as a footer if the models match Set<String> modelClassNames = model.keySet(); // only use the header factory if the row is the header if (footerFactory.supportsModel(modelClassNames)) { if (factory.skipFooter()) { LOG.info("Skipping marshal of footer row; 'skipFooter=true'"); continue; } else { result = footerFactory.unbind(model); } } } if (result == null) { // marshal as a normal / default row result = factory.unbind(model); } byte[] bytes = exchange.getContext().getTypeConverter().convertTo(byte[].class, exchange, result); outputStream.write(bytes); // Add a carriage return outputStream.write(bytesCRLF); } } /* * Check if the body is already parsed. * Bindy expects a list containing Map<String, Object> entries * where each Map contains only one entry where the key is the class * name of the object to be marshalled, and the value is the * object to be marshalled. */ private boolean isPreparedList(Object object) { if (List.class.isAssignableFrom(object.getClass())) { List<?> list = (List<?>) object; if (list.size() > 0) { // Check first entry, should be enough Object entry = list.get(0); if (Map.class.isAssignableFrom(entry.getClass())) { Map<?, ?> map = (Map<?, ?>) entry; if (map.size() == 1) { if (map.keySet().toArray()[0] instanceof String) { return true; } } } } } return false; } public Object unmarshal(Exchange exchange, InputStream inputStream) throws Exception { BindyFixedLengthFactory factory = (BindyFixedLengthFactory) getFactory(); ObjectHelper.notNull(factory, "not instantiated"); // List of Pojos List<Map<String, Object>> models = new ArrayList<Map<String, Object>>(); // Pojos of the model Map<String, Object> model; InputStreamReader in = new InputStreamReader(inputStream, IOHelper.getCharsetName(exchange)); // Scanner is used to read big file Scanner scanner = new Scanner(in); AtomicInteger count = new AtomicInteger(0); try { // Parse the header if it exists if (scanner.hasNextLine() && factory.hasHeader()) { // Read the line (should not trim as its fixed length) String line = getNextNonEmptyLine(scanner, count); if (!factory.skipHeader()) { Map<String, Object> headerObjMap = createModel(headerFactory, line, count.intValue()); exchange.getOut().setHeader(CAMEL_BINDY_FIXED_LENGTH_HEADER, headerObjMap); } } String thisLine = getNextNonEmptyLine(scanner, count); String nextLine = null; if (thisLine != null) { nextLine = getNextNonEmptyLine(scanner, count); } // Parse the main file content while (thisLine != null && nextLine != null) { model = createModel(factory, thisLine, count.intValue()); // Add objects graph to the list models.add(model); thisLine = nextLine; nextLine = getNextNonEmptyLine(scanner, count); } // this line should be the last non-empty line from the file // optionally parse the line as a footer if (thisLine != null) { if (factory.hasFooter()) { if (!factory.skipFooter()) { Map<String, Object> footerObjMap = createModel(footerFactory, thisLine, count.intValue()); exchange.getOut().setHeader(CAMEL_BINDY_FIXED_LENGTH_FOOTER, footerObjMap); } } else { model = createModel(factory, thisLine, count.intValue()); models.add(model); } } // BigIntegerFormatFactory if models list is empty or not // If this is the case (correspond to an empty stream, ...) if (models.size() == 0) { throw new java.lang.IllegalArgumentException("No records have been defined in the the file"); } else { return extractUnmarshalResult(models); } } finally { scanner.close(); IOHelper.close(in, "in", LOG); } } private String getNextNonEmptyLine(Scanner scanner, AtomicInteger count) { String line = ""; while (ObjectHelper.isEmpty(line) && scanner.hasNextLine()) { count.incrementAndGet(); line = scanner.nextLine(); } if (ObjectHelper.isEmpty(line)) { return null; } else { return line; } } protected Map<String, Object> createModel(BindyFixedLengthFactory factory, String line, int count) throws Exception { String myLine = line; // Check if the record length corresponds to the parameter // provided in the @FixedLengthRecord if (factory.recordLength() > 0) { if (isPaddingNeededAndEnable(factory, myLine)) { //myLine = rightPad(myLine, factory.recordLength()); } if (isTrimmingNeededAndEnabled(factory, myLine)) { myLine = myLine.substring(0, factory.recordLength()); } if ((myLine.length() < factory.recordLength() && !factory.isIgnoreMissingChars()) || (myLine.length() > factory.recordLength())) { throw new java.lang.IllegalArgumentException("Size of the record: " + myLine.length() + " is not equal to the value provided in the model: " + factory.recordLength()); } } // Create POJO where Fixed data will be stored Map<String, Object> model = factory.factory(); // Bind data from Fixed record with model classes factory.bind(myLine, model, count); // Link objects together factory.link(model); LOG.debug("Graph of objects created: {}", model); return model; } private boolean isTrimmingNeededAndEnabled(BindyFixedLengthFactory factory, String myLine) { return factory.isIgnoreTrailingChars() && myLine.length() > factory.recordLength(); } private String rightPad(String myLine, int length) { return String.format("%1$-" + length + "s", myLine); } private boolean isPaddingNeededAndEnable(BindyFixedLengthFactory factory, String myLine) { return myLine.length() < factory.recordLength() && factory.isIgnoreMissingChars(); } @Override protected BindyAbstractFactory createModelFactory(FormatFactory formatFactory) throws Exception { BindyFixedLengthFactory factory = new BindyFixedLengthFactory(getClassType()); factory.setFormatFactory(formatFactory); // Optionally initialize the header factory... using header model classes if (factory.hasHeader()) { this.headerFactory = new BindyFixedLengthFactory(factory.header()); this.headerFactory.setFormatFactory(formatFactory); } // Optionally initialize the footer factory... using footer model classes if (factory.hasFooter()) { this.footerFactory = new BindyFixedLengthFactory(factory.footer()); this.footerFactory.setFormatFactory(formatFactory); } return factory; } }