001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.camel.dataformat.bindy;
018    
019    import java.lang.reflect.Field;
020    import java.util.ArrayList;
021    import java.util.HashMap;
022    import java.util.Iterator;
023    import java.util.LinkedHashMap;
024    import java.util.LinkedList;
025    import java.util.List;
026    import java.util.Map;
027    import java.util.Map.Entry;
028    import java.util.TreeMap;
029    
030    import org.apache.camel.dataformat.bindy.annotation.CsvRecord;
031    import org.apache.camel.dataformat.bindy.annotation.DataField;
032    import org.apache.camel.dataformat.bindy.annotation.Link;
033    import org.apache.camel.dataformat.bindy.annotation.OneToMany;
034    import org.apache.camel.dataformat.bindy.annotation.Section;
035    import org.apache.camel.dataformat.bindy.format.FormatException;
036    import org.apache.camel.dataformat.bindy.util.Converter;
037    import org.apache.camel.spi.PackageScanClassResolver;
038    import org.apache.camel.util.ObjectHelper;
039    import org.slf4j.Logger;
040    import org.slf4j.LoggerFactory;
041    
042    /**
043     * The BindyCsvFactory is the class who allows to : Generate a model associated
044     * to a CSV record, bind data from a record to the POJOs, export data of POJOs
045     * to a CSV record and format data into String, Date, Double, ... according to
046     * the format/pattern defined
047     */
048    public class BindyCsvFactory extends BindyAbstractFactory implements BindyFactory {
049    
050        private static final transient Logger LOG = LoggerFactory.getLogger(BindyCsvFactory.class);
051    
052        boolean isOneToMany;
053    
054        private Map<Integer, DataField> dataFields = new LinkedHashMap<Integer, DataField>();
055        private Map<Integer, Field> annotatedFields = new LinkedHashMap<Integer, Field>();
056        private Map<String, Integer> sections = new HashMap<String, Integer>();
057    
058        private int numberOptionalFields;
059        private int numberMandatoryFields;
060        private int totalFields;
061    
062        private String separator;
063        private boolean skipFirstLine;
064        private boolean generateHeaderColumnNames;
065        private boolean messageOrdered;
066        private String quote;
067    
068        public BindyCsvFactory(PackageScanClassResolver resolver, String... packageNames) throws Exception {
069            super(resolver, packageNames);
070    
071            // initialize specific parameters of the csv model
072            initCsvModel();
073        }
074    
075        /**
076         * method uses to initialize the model representing the classes who will
077         * bind the data. This process will scan for classes according to the
078         * package name provided, check the annotated classes and fields and
079         * retrieve the separator of the CSV record
080         * 
081         * @throws Exception
082         */
083        public void initCsvModel() throws Exception {
084    
085            // Find annotated Datafields declared in the Model classes
086            initAnnotatedFields();
087    
088            // initialize Csv parameter(s)
089            // separator and skip first line from @CSVrecord annotation
090            initCsvRecordParameters();
091        }
092    
093        public void initAnnotatedFields() {
094    
095            for (Class<?> cl : models) {
096    
097                List<Field> linkFields = new ArrayList<Field>();
098    
099                if (LOG.isDebugEnabled()) {
100                    LOG.debug("Class retrieved: {}", cl.getName());
101                }
102    
103                for (Field field : cl.getDeclaredFields()) {
104                    DataField dataField = field.getAnnotation(DataField.class);
105                    if (dataField != null) {
106                        if (LOG.isDebugEnabled()) {
107                            LOG.debug("Position defined in the class: {}, position: {}, Field: {}",
108                                    new Object[]{cl.getName(), dataField.pos(), dataField});
109                        }
110    
111                        if (dataField.required()) {
112                            ++numberMandatoryFields;
113                        } else {
114                            ++numberOptionalFields;
115                        }
116    
117                        dataFields.put(dataField.pos(), dataField);
118                        annotatedFields.put(dataField.pos(), field);
119                    }
120    
121                    Link linkField = field.getAnnotation(Link.class);
122    
123                    if (linkField != null) {
124                        if (LOG.isDebugEnabled()) {
125                            LOG.debug("Class linked: {}, Field: {}", cl.getName(), field);
126                        }
127                        linkFields.add(field);
128                    }
129    
130                }
131    
132                if (!linkFields.isEmpty()) {
133                    annotatedLinkFields.put(cl.getName(), linkFields);
134                }
135    
136                totalFields = numberMandatoryFields + numberOptionalFields;
137    
138                if (LOG.isDebugEnabled()) {
139                    LOG.debug("Number of optional fields: {}", numberOptionalFields);
140                    LOG.debug("Number of mandatory fields: {}", numberMandatoryFields);
141                    LOG.debug("Total: {}", totalFields);
142                }
143            }
144        }
145    
146        public void bind(List<String> tokens, Map<String, Object> model, int line) throws Exception {
147    
148            int pos = 1;
149            int counterMandatoryFields = 0;
150    
151            for (String data : tokens) {
152    
153                // Get DataField from model
154                DataField dataField = dataFields.get(pos);
155                ObjectHelper.notNull(dataField, "No position " + pos + " defined for the field: " + data + ", line: " + line);
156    
157                if (dataField.trim()) {
158                    data = data.trim();
159                }
160                
161                if (dataField.required()) {
162                    // Increment counter of mandatory fields
163                    ++counterMandatoryFields;
164    
165                    // Check if content of the field is empty
166                    // This is not possible for mandatory fields
167                    if (data.equals("")) {
168                        throw new IllegalArgumentException("The mandatory field defined at the position " + pos + " is empty for the line: " + line);
169                    }
170                }
171    
172                // Get Field to be setted
173                Field field = annotatedFields.get(pos);
174                field.setAccessible(true);
175    
176                if (LOG.isDebugEnabled()) {
177                    LOG.debug("Pos: {}, Data: {}, Field type: {}", new Object[]{pos, data, field.getType()});
178                }
179    
180                Format<?> format;
181    
182                // Get pattern defined for the field
183                String pattern = dataField.pattern();
184    
185                // Create format object to format the field
186                format = FormatFactory.getFormat(field.getType(), pattern, getLocale(), dataField.precision());
187    
188                // field object to be set
189                Object modelField = model.get(field.getDeclaringClass().getName());
190    
191                // format the data received
192                Object value = null;
193    
194                if (!data.equals("")) {
195                    try {
196                        value = format.parse(data);
197                    } catch (FormatException ie) {
198                        throw new IllegalArgumentException(ie.getMessage() + ", position: " + pos + ", line: " + line, ie);
199                    } catch (Exception e) {
200                        throw new IllegalArgumentException("Parsing error detected for field defined at the position: " + pos + ", line: " + line, e);
201                    }
202                } else {
203                    value = getDefaultValueForPrimitive(field.getType());
204                }
205    
206                field.set(modelField, value);
207    
208                ++pos;
209    
210            }
211    
212            LOG.debug("Counter mandatory fields: {}", counterMandatoryFields);
213    
214            if (pos < totalFields) {
215                throw new IllegalArgumentException("Some fields are missing (optional or mandatory), line: " + line);
216            }
217    
218            if (counterMandatoryFields < numberMandatoryFields) {
219                throw new IllegalArgumentException("Some mandatory fields are missing, line: " + line);
220            }
221    
222        }
223    
224        @SuppressWarnings("unchecked")
225        public String unbind(Map<String, Object> model) throws Exception {
226    
227            StringBuilder buffer = new StringBuilder();
228            Map<Integer, List> results = new HashMap<Integer, List>();
229    
230            // Check if separator exists
231            ObjectHelper.notNull(this.separator, "The separator has not been instantiated or property not defined in the @CsvRecord annotation");
232    
233            char separator = Converter.getCharDelimitor(this.getSeparator());
234    
235            if (LOG.isDebugEnabled()) {
236                LOG.debug("Separator converted: '0x{}', from: {}", Integer.toHexString(separator), this.getSeparator());
237            }
238    
239            for (Class clazz : models) {
240                if (model.containsKey(clazz.getName())) {
241    
242                    Object obj = model.get(clazz.getName());
243                    if (LOG.isDebugEnabled()) {
244                        LOG.debug("Model object: {}, class: {}", obj, obj.getClass().getName());
245                    }
246                    if (obj != null) {
247    
248                        // Generate Csv table
249                        generateCsvPositionMap(clazz, obj, results);
250    
251                    }
252                }
253            }
254    
255            // Transpose result
256            List<List> l = new ArrayList<List>();
257            if (isOneToMany) {
258                l = product(results);
259            } else {
260                // Convert Map<Integer, List> into List<List>
261                TreeMap<Integer, List> sortValues = new TreeMap<Integer, List>(results);
262                List<String> temp = new ArrayList<String>();
263    
264                for (Entry<Integer, List> entry : sortValues.entrySet()) {
265                    // Get list of values
266                    List<String> val = (List<String>)entry.getValue();
267    
268                    // For one to one relation
269                    // There is only one item in the list
270                    String value = val.get(0);
271    
272                    // Add the value to the temp array
273                    if (value != null) {
274                        temp.add(value);
275                    } else {
276                        temp.add("");
277                    }
278                }
279    
280                l.add(temp);
281            }
282    
283            if (l != null) {
284                Iterator it = l.iterator();
285                while (it.hasNext()) {
286                    List<String> tokens = (ArrayList<String>)it.next();
287                    Iterator itx = tokens.iterator();
288    
289                    while (itx.hasNext()) {
290                        String res = (String)itx.next();
291                        if (res != null) {
292                            // the field may be enclosed in quotes if a quote was configured
293                            if (quote != null) {
294                                buffer.append(quote);
295                            }
296                            buffer.append(res);
297                            if (quote != null) {
298                                buffer.append(quote);
299                            }
300                        }
301    
302                        if (itx.hasNext()) {
303                            buffer.append(separator);
304                        }
305                    }
306    
307                    if (it.hasNext()) {
308                        buffer.append(Converter.getStringCarriageReturn(getCarriageReturn()));
309                    }
310                }
311            }
312    
313            return buffer.toString();
314        }
315    
316        private List<List> product(Map<Integer, List> values) {
317            TreeMap<Integer, List> sortValues = new TreeMap<Integer, List>(values);
318    
319            List<List> product = new ArrayList<List>();
320            Map<Integer, Integer> index = new HashMap<Integer, Integer>();
321    
322            int idx = 0;
323            int idxSize = 0;
324            do {
325                idxSize = 0;
326                List v = new ArrayList();
327    
328                for (int ii = 1; ii <= sortValues.lastKey(); ii++) {
329                    List l = values.get(ii);
330                    if (l == null) {
331                        v.add("");
332                        ++idxSize;
333                        continue;
334                    }
335    
336                    if (l.size() >= idx + 1) {
337                        v.add(l.get(idx));
338                        index.put(ii, idx);
339                        if (LOG.isDebugEnabled()) {
340                            LOG.debug("Value: {}, pos: {}, at: {}", new Object[]{l.get(idx), ii, idx});
341                        }
342                    } else {
343                        v.add(l.get(0));
344                        index.put(ii, 0);
345                        ++idxSize;
346                        if (LOG.isDebugEnabled()) {
347                            LOG.debug("Value: {}, pos: {}, at index: {}", new Object[]{l.get(0), ii, 0});
348                        }
349                    }
350                }
351    
352                if (idxSize != sortValues.lastKey()) {
353                    product.add(v);
354                }
355                ++idx;
356    
357            } while (idxSize != sortValues.lastKey());
358    
359            return product;
360        }
361    
362        /**
363         * 
364         * Generate a table containing the data formatted and sorted with their position/offset
365         * If the model is Ordered than a key is created combining the annotation @Section and Position of the field
366         * If a relation @OneToMany is defined, than we iterate recursively through this function
367         * The result is placed in the Map<Integer, List> results
368         */
369        private void generateCsvPositionMap(Class clazz, Object obj, Map<Integer, List> results) throws Exception {
370    
371            String result = "";
372    
373            for (Field field : clazz.getDeclaredFields()) {
374    
375                field.setAccessible(true);
376    
377                DataField datafield = field.getAnnotation(DataField.class);
378    
379                if (datafield != null) {
380    
381                    if (obj != null) {
382    
383                        // Retrieve the format, pattern and precision associated to
384                        // the type
385                        Class type = field.getType();
386                        String pattern = datafield.pattern();
387                        int precision = datafield.precision();
388    
389                        // Create format
390                        Format format = FormatFactory.getFormat(type, pattern, getLocale(), precision);
391    
392                        // Get field value
393                        Object value = field.get(obj);
394    
395                        result = formatString(format, value);
396    
397                        if (datafield.trim()) {
398                            result = result.trim();
399                        }
400    
401                        if (datafield.clip() && result.length() > datafield.length()) {
402                            result = result.substring(0, datafield.length());
403                        }
404    
405                        if (LOG.isDebugEnabled()) {
406                            LOG.debug("Value to be formatted: {}, position: {}, and its formatted value: {}", new Object[]{value, datafield.pos(), result});
407                        }
408    
409                    } else {
410                        result = "";
411                    }
412    
413                    Integer key;
414    
415                    if (isMessageOrdered() && obj != null) {
416    
417                        // Generate a key using the number of the section
418                        // and the position of the field
419                        Integer key1 = sections.get(obj.getClass().getName());
420                        Integer key2 = datafield.position();
421                        Integer keyGenerated = generateKey(key1, key2);
422    
423                        if (LOG.isDebugEnabled()) {
424                            LOG.debug("Key generated: {}, for section: {}", String.valueOf(keyGenerated), key1);
425                        }
426    
427                        key = keyGenerated;
428    
429                    } else {
430                        key = datafield.pos();
431                    }
432    
433                    if (!results.containsKey(key)) {
434                        List list = new LinkedList();
435                        list.add(result);
436                        results.put(key, list);
437                    } else {
438                        List list = (LinkedList)results.get(key);
439                        list.add(result);
440                    }
441    
442                }
443    
444                OneToMany oneToMany = field.getAnnotation(OneToMany.class);
445                if (oneToMany != null) {
446    
447                    // Set global variable
448                    // Will be used during generation of CSV
449                    isOneToMany = true;
450    
451                    ArrayList list = (ArrayList)field.get(obj);
452                    if (list != null) {
453    
454                        Iterator it = list.iterator();
455                        while (it.hasNext()) {
456                            Object target = it.next();
457                            generateCsvPositionMap(target.getClass(), target, results);
458                        }
459    
460                    } else {
461    
462                        // Call this function to add empty value
463                        // in the table
464                        generateCsvPositionMap(field.getClass(), null, results);
465                    }
466    
467                }
468            }
469    
470        }
471        
472        /**
473         * Generate for the first line the headers of the columns
474         * 
475         * @return the headers columns
476         */
477        public String generateHeader() {
478    
479            Map<Integer, DataField> dataFieldsSorted = new TreeMap<Integer, DataField>(dataFields);
480            Iterator<Integer> it = dataFieldsSorted.keySet().iterator();
481    
482            StringBuilder builderHeader = new StringBuilder();
483    
484            while (it.hasNext()) {
485    
486                DataField dataField = dataFieldsSorted.get(it.next());
487    
488                // Retrieve the field
489                Field field = annotatedFields.get(dataField.pos());
490                // Change accessibility to allow to read protected/private fields
491                field.setAccessible(true);
492    
493                // Get dataField
494                if (!dataField.columnName().equals("")) {
495                    builderHeader.append(dataField.columnName());
496                } else {
497                    builderHeader.append(field.getName());
498                }
499    
500                if (it.hasNext()) {
501                    builderHeader.append(separator);
502                }
503    
504            }
505    
506            return builderHeader.toString();
507        }
508    
509        /**
510         * Get parameters defined in @Csvrecord annotation
511         */
512        private void initCsvRecordParameters() {
513            if (separator == null) {
514                for (Class<?> cl : models) {
515    
516                    // Get annotation @CsvRecord from the class
517                    CsvRecord record = cl.getAnnotation(CsvRecord.class);
518    
519                    // Get annotation @Section from the class
520                    Section section = cl.getAnnotation(Section.class);
521    
522                    if (record != null) {
523                        LOG.debug("Csv record: {}", record);
524    
525                        // Get skipFirstLine parameter
526                        skipFirstLine = record.skipFirstLine();
527                        LOG.debug("Skip First Line parameter of the CSV: {}" + skipFirstLine);
528    
529                        // Get generateHeaderColumnNames parameter
530                        generateHeaderColumnNames = record.generateHeaderColumns();
531                        LOG.debug("Generate header column names parameter of the CSV: {}", generateHeaderColumnNames);
532    
533                        // Get Separator parameter
534                        ObjectHelper.notNull(record.separator(), "No separator has been defined in the @Record annotation");
535                        separator = record.separator();
536                        LOG.debug("Separator defined for the CSV: {}", separator);
537    
538                        // Get carriage return parameter
539                        crlf = record.crlf();
540                        LOG.debug("Carriage return defined for the CSV: {}", crlf);
541    
542                        // Get isOrdered parameter
543                        messageOrdered = record.isOrdered();
544                        LOG.debug("Must CSV record be ordered: {}", messageOrdered);
545    
546                        if (ObjectHelper.isNotEmpty(record.quote())) {
547                            quote = record.quote();
548                            LOG.debug("Quoting columns with: {}", quote);
549                        }
550                    }
551    
552                    if (section != null) {
553                        // Test if section number is not null
554                        ObjectHelper.notNull(section.number(), "No number has been defined for the section");
555    
556                        // Get section number and add it to the sections
557                        sections.put(cl.getName(), section.number());
558                    }
559                }
560            }
561        }
562    
563        /**
564         * Find the separator used to delimit the CSV fields
565         */
566        public String getSeparator() {
567            return separator;
568        }
569    
570        /**
571         * Flag indicating if the first line of the CSV must be skipped
572         */
573        public boolean getGenerateHeaderColumnNames() {
574            return generateHeaderColumnNames;
575        }
576    
577        /**
578         * Find the separator used to delimit the CSV fields
579         */
580        public boolean getSkipFirstLine() {
581            return skipFirstLine;
582        }
583    
584        /**
585         * Flag indicating if the message must be ordered
586         * 
587         * @return boolean
588         */
589        public boolean isMessageOrdered() {
590            return messageOrdered;
591        }
592    }