| /** |
| * 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; |
| |
| import java.lang.reflect.Field; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.TreeMap; |
| |
| import org.apache.camel.dataformat.bindy.annotation.CsvRecord; |
| import org.apache.camel.dataformat.bindy.annotation.DataField; |
| import org.apache.camel.dataformat.bindy.annotation.Link; |
| import org.apache.camel.dataformat.bindy.annotation.OneToMany; |
| import org.apache.camel.dataformat.bindy.annotation.Section; |
| import org.apache.camel.dataformat.bindy.format.FormatException; |
| import org.apache.camel.dataformat.bindy.util.Converter; |
| import org.apache.camel.spi.PackageScanClassResolver; |
| import org.apache.camel.util.ObjectHelper; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * The BindyCsvFactory is the class who allows to : Generate a model associated |
| * to a CSV record, bind data from a record to the POJOs, export data of POJOs |
| * to a CSV record and format data into String, Date, Double, ... according to |
| * the format/pattern defined |
| */ |
| public class BindyCsvFactory extends BindyAbstractFactory implements BindyFactory { |
| |
| private static final transient Logger LOG = LoggerFactory.getLogger(BindyCsvFactory.class); |
| |
| boolean isOneToMany; |
| |
| private Map<Integer, DataField> dataFields = new LinkedHashMap<Integer, DataField>(); |
| private Map<Integer, Field> annotatedFields = new LinkedHashMap<Integer, Field>(); |
| private Map<String, Integer> sections = new HashMap<String, Integer>(); |
| |
| private int numberOptionalFields; |
| private int numberMandatoryFields; |
| private int totalFields; |
| |
| private String separator; |
| private boolean skipFirstLine; |
| private boolean generateHeaderColumnNames; |
| private boolean messageOrdered; |
| private String quote; |
| |
| public BindyCsvFactory(PackageScanClassResolver resolver, String... packageNames) throws Exception { |
| super(resolver, packageNames); |
| |
| // initialize specific parameters of the csv model |
| initCsvModel(); |
| } |
| |
| /** |
| * method uses to initialize the model representing the classes who will |
| * bind the data. This process will scan for classes according to the |
| * package name provided, check the annotated classes and fields and |
| * retrieve the separator of the CSV record |
| * |
| * @throws Exception |
| */ |
| public void initCsvModel() throws Exception { |
| |
| // Find annotated Datafields declared in the Model classes |
| initAnnotatedFields(); |
| |
| // initialize Csv parameter(s) |
| // separator and skip first line from @CSVrecord annotation |
| initCsvRecordParameters(); |
| } |
| |
| public void initAnnotatedFields() { |
| |
| for (Class<?> cl : models) { |
| |
| List<Field> linkFields = new ArrayList<Field>(); |
| |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Class retrieved: {}", cl.getName()); |
| } |
| |
| for (Field field : cl.getDeclaredFields()) { |
| DataField dataField = field.getAnnotation(DataField.class); |
| if (dataField != null) { |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Position defined in the class: {}, position: {}, Field: {}", |
| new Object[]{cl.getName(), dataField.pos(), dataField}); |
| } |
| |
| if (dataField.required()) { |
| ++numberMandatoryFields; |
| } else { |
| ++numberOptionalFields; |
| } |
| |
| dataFields.put(dataField.pos(), dataField); |
| annotatedFields.put(dataField.pos(), field); |
| } |
| |
| Link linkField = field.getAnnotation(Link.class); |
| |
| if (linkField != null) { |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Class linked: {}, Field: {}", cl.getName(), field); |
| } |
| linkFields.add(field); |
| } |
| |
| } |
| |
| if (!linkFields.isEmpty()) { |
| annotatedLinkFields.put(cl.getName(), linkFields); |
| } |
| |
| totalFields = numberMandatoryFields + numberOptionalFields; |
| |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Number of optional fields: {}", numberOptionalFields); |
| LOG.debug("Number of mandatory fields: {}", numberMandatoryFields); |
| LOG.debug("Total: {}", totalFields); |
| } |
| } |
| } |
| |
| public void bind(List<String> tokens, Map<String, Object> model, int line) throws Exception { |
| |
| int pos = 1; |
| int counterMandatoryFields = 0; |
| |
| for (String data : tokens) { |
| |
| // Get DataField from model |
| DataField dataField = dataFields.get(pos); |
| ObjectHelper.notNull(dataField, "No position " + pos + " defined for the field: " + data + ", line: " + line); |
| |
| if (dataField.trim()) { |
| data = data.trim(); |
| } |
| |
| if (dataField.required()) { |
| // Increment counter of mandatory fields |
| ++counterMandatoryFields; |
| |
| // Check if content of the field is empty |
| // This is not possible for mandatory fields |
| if (data.equals("")) { |
| throw new IllegalArgumentException("The mandatory field defined at the position " + pos + " is empty for the line: " + line); |
| } |
| } |
| |
| // Get Field to be setted |
| Field field = annotatedFields.get(pos); |
| field.setAccessible(true); |
| |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Pos: {}, Data: {}, Field type: {}", new Object[]{pos, data, field.getType()}); |
| } |
| |
| Format<?> format; |
| |
| // Get pattern defined for the field |
| String pattern = dataField.pattern(); |
| |
| // Create format object to format the field |
| format = FormatFactory.getFormat(field.getType(), pattern, getLocale(), dataField.precision()); |
| |
| // field object to be set |
| Object modelField = model.get(field.getDeclaringClass().getName()); |
| |
| // format the data received |
| Object value = null; |
| |
| if (!data.equals("")) { |
| try { |
| value = format.parse(data); |
| } catch (FormatException ie) { |
| throw new IllegalArgumentException(ie.getMessage() + ", position: " + pos + ", line: " + line, ie); |
| } catch (Exception e) { |
| throw new IllegalArgumentException("Parsing error detected for field defined at the position: " + pos + ", line: " + line, e); |
| } |
| } else { |
| value = getDefaultValueForPrimitive(field.getType()); |
| } |
| |
| field.set(modelField, value); |
| |
| ++pos; |
| |
| } |
| |
| LOG.debug("Counter mandatory fields: {}", counterMandatoryFields); |
| |
| if (pos < totalFields) { |
| throw new IllegalArgumentException("Some fields are missing (optional or mandatory), line: " + line); |
| } |
| |
| if (counterMandatoryFields < numberMandatoryFields) { |
| throw new IllegalArgumentException("Some mandatory fields are missing, line: " + line); |
| } |
| |
| } |
| |
| @SuppressWarnings("unchecked") |
| public String unbind(Map<String, Object> model) throws Exception { |
| |
| StringBuilder buffer = new StringBuilder(); |
| Map<Integer, List> results = new HashMap<Integer, List>(); |
| |
| // Check if separator exists |
| ObjectHelper.notNull(this.separator, "The separator has not been instantiated or property not defined in the @CsvRecord annotation"); |
| |
| char separator = Converter.getCharDelimitor(this.getSeparator()); |
| |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Separator converted: '0x{}', from: {}", Integer.toHexString(separator), this.getSeparator()); |
| } |
| |
| for (Class clazz : models) { |
| if (model.containsKey(clazz.getName())) { |
| |
| Object obj = model.get(clazz.getName()); |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Model object: {}, class: {}", obj, obj.getClass().getName()); |
| } |
| if (obj != null) { |
| |
| // Generate Csv table |
| generateCsvPositionMap(clazz, obj, results); |
| |
| } |
| } |
| } |
| |
| // Transpose result |
| List<List> l = new ArrayList<List>(); |
| if (isOneToMany) { |
| l = product(results); |
| } else { |
| // Convert Map<Integer, List> into List<List> |
| TreeMap<Integer, List> sortValues = new TreeMap<Integer, List>(results); |
| List<String> temp = new ArrayList<String>(); |
| |
| for (Entry<Integer, List> entry : sortValues.entrySet()) { |
| // Get list of values |
| List<String> val = (List<String>)entry.getValue(); |
| |
| // For one to one relation |
| // There is only one item in the list |
| String value = val.get(0); |
| |
| // Add the value to the temp array |
| if (value != null) { |
| temp.add(value); |
| } else { |
| temp.add(""); |
| } |
| } |
| |
| l.add(temp); |
| } |
| |
| if (l != null) { |
| Iterator it = l.iterator(); |
| while (it.hasNext()) { |
| List<String> tokens = (ArrayList<String>)it.next(); |
| Iterator itx = tokens.iterator(); |
| |
| while (itx.hasNext()) { |
| String res = (String)itx.next(); |
| if (res != null) { |
| // the field may be enclosed in quotes if a quote was configured |
| if (quote != null) { |
| buffer.append(quote); |
| } |
| buffer.append(res); |
| if (quote != null) { |
| buffer.append(quote); |
| } |
| } |
| |
| if (itx.hasNext()) { |
| buffer.append(separator); |
| } |
| } |
| |
| if (it.hasNext()) { |
| buffer.append(Converter.getStringCarriageReturn(getCarriageReturn())); |
| } |
| } |
| } |
| |
| return buffer.toString(); |
| } |
| |
| private List<List> product(Map<Integer, List> values) { |
| TreeMap<Integer, List> sortValues = new TreeMap<Integer, List>(values); |
| |
| List<List> product = new ArrayList<List>(); |
| Map<Integer, Integer> index = new HashMap<Integer, Integer>(); |
| |
| int idx = 0; |
| int idxSize = 0; |
| do { |
| idxSize = 0; |
| List v = new ArrayList(); |
| |
| for (int ii = 1; ii <= sortValues.lastKey(); ii++) { |
| List l = values.get(ii); |
| if (l == null) { |
| v.add(""); |
| ++idxSize; |
| continue; |
| } |
| |
| if (l.size() >= idx + 1) { |
| v.add(l.get(idx)); |
| index.put(ii, idx); |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Value: {}, pos: {}, at: {}", new Object[]{l.get(idx), ii, idx}); |
| } |
| } else { |
| v.add(l.get(0)); |
| index.put(ii, 0); |
| ++idxSize; |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Value: {}, pos: {}, at index: {}", new Object[]{l.get(0), ii, 0}); |
| } |
| } |
| } |
| |
| if (idxSize != sortValues.lastKey()) { |
| product.add(v); |
| } |
| ++idx; |
| |
| } while (idxSize != sortValues.lastKey()); |
| |
| return product; |
| } |
| |
| /** |
| * |
| * Generate a table containing the data formatted and sorted with their position/offset |
| * If the model is Ordered than a key is created combining the annotation @Section and Position of the field |
| * If a relation @OneToMany is defined, than we iterate recursively through this function |
| * The result is placed in the Map<Integer, List> results |
| */ |
| private void generateCsvPositionMap(Class clazz, Object obj, Map<Integer, List> results) throws Exception { |
| |
| String result = ""; |
| |
| for (Field field : clazz.getDeclaredFields()) { |
| |
| field.setAccessible(true); |
| |
| DataField datafield = field.getAnnotation(DataField.class); |
| |
| if (datafield != null) { |
| |
| if (obj != null) { |
| |
| // Retrieve the format, pattern and precision associated to |
| // the type |
| Class type = field.getType(); |
| String pattern = datafield.pattern(); |
| int precision = datafield.precision(); |
| |
| // Create format |
| Format format = FormatFactory.getFormat(type, pattern, getLocale(), precision); |
| |
| // Get field value |
| Object value = field.get(obj); |
| |
| result = formatString(format, value); |
| |
| if (datafield.trim()) { |
| result = result.trim(); |
| } |
| |
| if (datafield.clip() && result.length() > datafield.length()) { |
| result = result.substring(0, datafield.length()); |
| } |
| |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Value to be formatted: {}, position: {}, and its formatted value: {}", new Object[]{value, datafield.pos(), result}); |
| } |
| |
| } else { |
| result = ""; |
| } |
| |
| Integer key; |
| |
| if (isMessageOrdered() && obj != null) { |
| |
| // Generate a key using the number of the section |
| // and the position of the field |
| Integer key1 = sections.get(obj.getClass().getName()); |
| Integer key2 = datafield.position(); |
| Integer keyGenerated = generateKey(key1, key2); |
| |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Key generated: {}, for section: {}", String.valueOf(keyGenerated), key1); |
| } |
| |
| key = keyGenerated; |
| |
| } else { |
| key = datafield.pos(); |
| } |
| |
| if (!results.containsKey(key)) { |
| List list = new LinkedList(); |
| list.add(result); |
| results.put(key, list); |
| } else { |
| List list = (LinkedList)results.get(key); |
| list.add(result); |
| } |
| |
| } |
| |
| OneToMany oneToMany = field.getAnnotation(OneToMany.class); |
| if (oneToMany != null) { |
| |
| // Set global variable |
| // Will be used during generation of CSV |
| isOneToMany = true; |
| |
| ArrayList list = (ArrayList)field.get(obj); |
| if (list != null) { |
| |
| Iterator it = list.iterator(); |
| while (it.hasNext()) { |
| Object target = it.next(); |
| generateCsvPositionMap(target.getClass(), target, results); |
| } |
| |
| } else { |
| |
| // Call this function to add empty value |
| // in the table |
| generateCsvPositionMap(field.getClass(), null, results); |
| } |
| |
| } |
| } |
| |
| } |
| |
| /** |
| * Generate for the first line the headers of the columns |
| * |
| * @return the headers columns |
| */ |
| public String generateHeader() { |
| |
| Map<Integer, DataField> dataFieldsSorted = new TreeMap<Integer, DataField>(dataFields); |
| Iterator<Integer> it = dataFieldsSorted.keySet().iterator(); |
| |
| StringBuilder builderHeader = new StringBuilder(); |
| |
| while (it.hasNext()) { |
| |
| DataField dataField = dataFieldsSorted.get(it.next()); |
| |
| // Retrieve the field |
| Field field = annotatedFields.get(dataField.pos()); |
| // Change accessibility to allow to read protected/private fields |
| field.setAccessible(true); |
| |
| // Get dataField |
| if (!dataField.columnName().equals("")) { |
| builderHeader.append(dataField.columnName()); |
| } else { |
| builderHeader.append(field.getName()); |
| } |
| |
| if (it.hasNext()) { |
| builderHeader.append(separator); |
| } |
| |
| } |
| |
| return builderHeader.toString(); |
| } |
| |
| /** |
| * Get parameters defined in @Csvrecord annotation |
| */ |
| private void initCsvRecordParameters() { |
| if (separator == null) { |
| for (Class<?> cl : models) { |
| |
| // Get annotation @CsvRecord from the class |
| CsvRecord record = cl.getAnnotation(CsvRecord.class); |
| |
| // Get annotation @Section from the class |
| Section section = cl.getAnnotation(Section.class); |
| |
| if (record != null) { |
| LOG.debug("Csv record: {}", record); |
| |
| // Get skipFirstLine parameter |
| skipFirstLine = record.skipFirstLine(); |
| LOG.debug("Skip First Line parameter of the CSV: {}" + skipFirstLine); |
| |
| // Get generateHeaderColumnNames parameter |
| generateHeaderColumnNames = record.generateHeaderColumns(); |
| LOG.debug("Generate header column names parameter of the CSV: {}", generateHeaderColumnNames); |
| |
| // Get Separator parameter |
| ObjectHelper.notNull(record.separator(), "No separator has been defined in the @Record annotation"); |
| separator = record.separator(); |
| LOG.debug("Separator defined for the CSV: {}", separator); |
| |
| // Get carriage return parameter |
| crlf = record.crlf(); |
| LOG.debug("Carriage return defined for the CSV: {}", crlf); |
| |
| // Get isOrdered parameter |
| messageOrdered = record.isOrdered(); |
| LOG.debug("Must CSV record be ordered: {}", messageOrdered); |
| |
| if (ObjectHelper.isNotEmpty(record.quote())) { |
| quote = record.quote(); |
| LOG.debug("Quoting columns with: {}", quote); |
| } |
| } |
| |
| if (section != null) { |
| // Test if section number is not null |
| ObjectHelper.notNull(section.number(), "No number has been defined for the section"); |
| |
| // Get section number and add it to the sections |
| sections.put(cl.getName(), section.number()); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Find the separator used to delimit the CSV fields |
| */ |
| public String getSeparator() { |
| return separator; |
| } |
| |
| /** |
| * Flag indicating if the first line of the CSV must be skipped |
| */ |
| public boolean getGenerateHeaderColumnNames() { |
| return generateHeaderColumnNames; |
| } |
| |
| /** |
| * Find the separator used to delimit the CSV fields |
| */ |
| public boolean getSkipFirstLine() { |
| return skipFirstLine; |
| } |
| |
| /** |
| * Flag indicating if the message must be ordered |
| * |
| * @return boolean |
| */ |
| public boolean isMessageOrdered() { |
| return messageOrdered; |
| } |
| } |