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.TreeMap;
028    
029    import org.apache.camel.dataformat.bindy.annotation.CsvRecord;
030    import org.apache.camel.dataformat.bindy.annotation.DataField;
031    import org.apache.camel.dataformat.bindy.annotation.Link;
032    import org.apache.camel.dataformat.bindy.annotation.OneToMany;
033    import org.apache.camel.dataformat.bindy.annotation.Section;
034    import org.apache.camel.dataformat.bindy.format.FormatException;
035    import org.apache.camel.dataformat.bindy.util.Converter;
036    import org.apache.camel.spi.PackageScanClassResolver;
037    import org.apache.camel.util.ObjectHelper;
038    import org.apache.commons.logging.Log;
039    import org.apache.commons.logging.LogFactory;
040    
041    /**
042     * The BindyCsvFactory is the class who allows to : Generate a model associated
043     * to a CSV record, bind data from a record to the POJOs, export data of POJOs
044     * to a CSV record and format data into String, Date, Double, ... according to
045     * the format/pattern defined
046     */
047    public class BindyCsvFactory extends BindyAbstractFactory implements BindyFactory {
048    
049        private static final transient Log LOG = LogFactory.getLog(BindyCsvFactory.class);
050    
051        boolean isOneToMany;
052    
053        private Map<Integer, DataField> dataFields = new LinkedHashMap<Integer, DataField>();
054        private Map<Integer, Field> annotedFields = new LinkedHashMap<Integer, Field>();
055        private Map<String, Integer> sections = new HashMap<String, Integer>();
056    
057        private Map<Integer, List> results;
058    
059        private int numberOptionalFields;
060        private int numberMandatoryFields;
061        private int totalFields;
062    
063        private String separator;
064        private boolean skipFirstLine;
065        private boolean generateHeaderColumnNames;
066        private boolean messageOrdered;
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            initAnnotedFields();
087    
088            // initialize Csv parameter(s)
089            // separator and skip first line from @CSVrecord annotation
090            initCsvRecordParameters();
091        }
092    
093        public void initAnnotedFields() {
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 : " + cl.getName() + ", position : " + dataField.pos() + ", Field : " + dataField.toString());
108                        }
109    
110                        if (dataField.required()) {
111                            ++numberMandatoryFields;
112                        } else {
113                            ++numberOptionalFields;
114                        }
115    
116                        dataFields.put(dataField.pos(), dataField);
117                        annotedFields.put(dataField.pos(), field);
118                    }
119    
120                    Link linkField = field.getAnnotation(Link.class);
121    
122                    if (linkField != null) {
123                        if (LOG.isDebugEnabled()) {
124                            LOG.debug("Class linked  : " + cl.getName() + ", Field" + field.toString());
125                        }
126                        linkFields.add(field);
127                    }
128    
129                }
130    
131                if (!linkFields.isEmpty()) {
132                    annotedLinkFields.put(cl.getName(), linkFields);
133                }
134    
135                totalFields = numberMandatoryFields + numberOptionalFields;
136    
137                if (LOG.isDebugEnabled()) {
138                    LOG.debug("Number of optional fields : " + numberOptionalFields);
139                    LOG.debug("Number of mandatory fields : " + numberMandatoryFields);
140                    LOG.debug("Total : " + totalFields);
141                }
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.required()) {
158                    // Increment counter of mandatory fields
159                    ++counterMandatoryFields;
160    
161                    // Check if content of the field is empty
162                    // This is not possible for mandatory fields
163                    if (data.equals("")) {
164                        throw new IllegalArgumentException("The mandatory field defined at the position " + pos + " is empty for the line : " + line);
165                    }
166                }
167    
168                // Get Field to be setted
169                Field field = annotedFields.get(pos);
170                field.setAccessible(true);
171    
172                if (LOG.isDebugEnabled()) {
173                    LOG.debug("Pos : " + pos + ", Data : " + data + ", Field type : " + field.getType());
174                }
175    
176                Format<?> format;
177    
178                // Get pattern defined for the field
179                String pattern = dataField.pattern();
180    
181                // Create format object to format the field
182                format = FormatFactory.getFormat(field.getType(), pattern, dataField.precision());
183    
184                // field object to be set
185                Object modelField = model.get(field.getDeclaringClass().getName());
186    
187                // format the data received
188                Object value = null;
189    
190                if (!data.equals("")) {
191                    try {
192                        value = format.parse(data);
193                    } catch (FormatException ie) {
194                        throw new IllegalArgumentException(ie.getMessage() + ", position : " + pos + ", line : " + line, ie);
195                    } catch (Exception e) {
196                        throw new IllegalArgumentException("Parsing error detected for field defined at the position : " + pos + ", line : " + line, e);
197                    }
198                } else {
199                    value = getDefaultValueforPrimitive(field.getType());
200                }
201    
202                field.set(modelField, value);
203    
204                ++pos;
205    
206            }
207    
208            if (LOG.isDebugEnabled()) {
209                LOG.debug("Counter mandatory fields : " + counterMandatoryFields);
210            }
211    
212            if (pos < totalFields) {
213                throw new IllegalArgumentException("Some fields are missing (optional or mandatory), line : " + line);
214            }
215    
216            if (counterMandatoryFields < numberMandatoryFields) {
217                throw new IllegalArgumentException("Some mandatory fields are missing, line : " + line);
218            }
219    
220        }
221    
222        /*
223         * public String unbind(Map<String, Object> model) throws Exception {
224         * StringBuilder builder = new StringBuilder(); Map<Integer, DataField>
225         * dataFieldsSorted = new TreeMap<Integer, DataField>(dataFields);
226         * Iterator<Integer> it = dataFieldsSorted.keySet().iterator(); // Map
227         * containing the OUT position of the field // The key is double and is
228         * created using the position of the field and // location of the class in
229         * the message (using section) Map<Integer, String> positions = new
230         * TreeMap<Integer, String>(); // Check if separator exists ObjectHelper
231         * .notNull(this.separator,
232         * "The separator has not been instantiated or property not defined in the @CsvRecord annotation"
233         * ); char separator = Converter.getCharDelimitor(this.getSeparator()); if
234         * (LOG.isDebugEnabled()) { LOG.debug("Separator converted : '0x" +
235         * Integer.toHexString(separator) + "', from : " + this.getSeparator()); }
236         * while (it.hasNext()) { DataField dataField =
237         * dataFieldsSorted.get(it.next()); // Retrieve the field Field field =
238         * annotedFields.get(dataField.pos()); // Change accessibility to allow to
239         * read protected/private fields field.setAccessible(true); // Retrieve the
240         * format, pattern and precision associated to the type Class type =
241         * field.getType(); String pattern = dataField.pattern(); int precision =
242         * dataField.precision(); // Create format Format format =
243         * FormatFactory.getFormat(type, pattern, precision); // Get field from
244         * model Object modelField = model.get(field.getDeclaringClass().getName());
245         * if (modelField != null) { // Get field value Object value =
246         * field.get(modelField); String strValue = ""; if (this.isMessageOrdered())
247         * { // Generate a key using the number of the section // and the position
248         * of the field Integer key1 =
249         * sections.get(modelField.getClass().getName()); Integer key2 =
250         * dataField.position(); Integer keyGenerated = generateKey(key1, key2); if
251         * (LOG.isDebugEnabled()) { LOG.debug("Key generated : " +
252         * String.valueOf(keyGenerated) + ", for section : " + key1); } if (value !=
253         * null) { // Format field value try { strValue = format.format(value); }
254         * catch (Exception e) { throw new
255         * IllegalArgumentException("Formating error detected for the value : " +
256         * value, e); } } // Add the content to the TreeMap according to the //
257         * position defined positions.put(keyGenerated, strValue); if
258         * (LOG.isDebugEnabled()) { LOG.debug("Positions size : " +
259         * positions.size()); } } else { // Add value to the appender if not null if
260         * (value != null) { // Format field value try { strValue =
261         * format.format(value); } catch (Exception e) { throw new
262         * IllegalArgumentException("Formating error detected for the value : " +
263         * value, e); } } if (LOG.isDebugEnabled()) {
264         * LOG.debug("Value to be formatted : " + value + ", position : " +
265         * dataField.pos() + ", and its formated value : " + strValue); }
266         * builder.append(strValue); if (it.hasNext()) { builder.append(separator);
267         * } } } } // Iterate through the list to generate // the message according
268         * to the order/position if (this.isMessageOrdered()) { Iterator<Integer>
269         * posit = positions.keySet().iterator(); while (posit.hasNext()) { String
270         * value = positions.get(posit.next()); if (LOG.isDebugEnabled()) {
271         * LOG.debug("Value added at the position (" + posit + ") : " + value +
272         * separator); } builder.append(value); if (it.hasNext()) {
273         * builder.append(separator); } } } return builder.toString(); }
274         */
275    
276        public String unbind(Map<String, Object> model) throws Exception {
277    
278            StringBuffer buffer = new StringBuffer();
279            results = new HashMap<Integer, List>();
280    
281            // Check if separator exists
282            ObjectHelper.notNull(this.separator, "The separator has not been instantiated or property not defined in the @CsvRecord annotation");
283    
284            char separator = Converter.getCharDelimitor(this.getSeparator());
285    
286            if (LOG.isDebugEnabled()) {
287                LOG.debug("Separator converted : '0x" + Integer.toHexString(separator) + "', from : " + this.getSeparator());
288            }
289    
290            for (Class clazz : models) {
291    
292                if (model.containsKey(clazz.getName())) {
293    
294                    Object obj = model.get(clazz.getName());
295    
296                    if (LOG.isDebugEnabled()) {
297                        LOG.debug("Model object : " + obj + ", class : " + obj.getClass().getName());
298                    }
299    
300                    if (obj != null) {
301    
302                        // Generate Csv table
303                        generateCsvPositionMap(clazz, obj);
304    
305                    }
306                }
307            }
308    
309            // Transpose result
310            List<List> l = new ArrayList<List>();
311    
312            if (isOneToMany) {
313    
314                l = product(results);
315    
316            } else {
317    
318                // Convert Map<Integer, List> into List<List>
319                TreeMap<Integer, List> sortValues = new TreeMap<Integer, List>(results);
320                List<String> temp = new ArrayList<String>();
321    
322                for (Integer key : sortValues.keySet()) {
323    
324                    // Get list of values
325                    List<String> val = sortValues.get(key);
326    
327                    // For one to one relation
328                    // There is only one item in the list
329                    String value = (String)val.get(0);
330    
331                    // Add the value to the temp array
332                    if (value != null) {
333                        temp.add(value);
334                    } else {
335                        temp.add("");
336                    }
337                }
338    
339                l.add(temp);
340            }
341    
342            if (l != null) {
343    
344                Iterator it = l.iterator();
345                while (it.hasNext()) {
346    
347                    List<String> tokens = (ArrayList<String>)it.next();
348                    Iterator itx = tokens.iterator();
349    
350                    while (itx.hasNext()) {
351    
352                        String res = (String)itx.next();
353    
354                        if (res != null) {
355                            buffer.append(res);
356                        } else {
357                            buffer.append("");
358                        }
359    
360                        if (itx.hasNext()) {
361                            buffer.append(separator);
362                        }
363    
364                    }
365    
366                    if (it.hasNext()) {
367                        buffer.append(Converter.getStringCarriageReturn(getCarriageReturn()));
368                    }
369    
370                }
371    
372            }
373    
374            return buffer.toString();
375    
376        }
377    
378        private List<List> product(Map<Integer, List> values) {
379    
380            TreeMap<Integer, List> sortValues = new TreeMap<Integer, List>(values);
381    
382            List<List> product = new ArrayList<List>();
383            Map<Integer, Integer> index = new HashMap<Integer, Integer>();
384    
385            boolean cont = true;
386            int idx = 0;
387            int idxSize;
388    
389            do {
390    
391                idxSize = 0;
392                List v = new ArrayList();
393    
394                for (int ii = 1; ii <= sortValues.lastKey(); ii++) {
395    
396                    List l = values.get(ii);
397    
398                    if (l == null) {
399                        v.add("");
400                        ++idxSize;
401                        continue;
402                    }
403    
404                    if (l.size() >= idx + 1) {
405                        v.add(l.get(idx));
406                        index.put(ii, idx);
407                        if (LOG.isDebugEnabled()) {
408                            LOG.debug("Value : " + l.get(idx) + ", pos : " + ii + ", at :" + idx);
409                        }
410    
411                    } else {
412                        v.add(l.get(0));
413                        index.put(ii, 0);
414                        ++idxSize;
415                        if (LOG.isDebugEnabled()) {
416                            LOG.debug("Value : " + l.get(0) + ", pos : " + ii + ", at index : " + 0);
417                        }
418                    }
419    
420                }
421    
422                if (idxSize != sortValues.lastKey()) {
423                    product.add(v);
424                }
425                ++idx;
426    
427            } while (idxSize != sortValues.lastKey());
428    
429            return product;
430        }
431    
432        private void generateCsvPositionMap(Class clazz, Object obj) throws Exception {
433    
434            String result = "";
435    
436            for (Field field : clazz.getDeclaredFields()) {
437    
438                field.setAccessible(true);
439    
440                DataField datafield = field.getAnnotation(DataField.class);
441    
442                if (datafield != null) {
443    
444                    if (obj != null) {
445    
446                        // Retrieve the format, pattern and precision associated to
447                        // the type
448                        Class type = field.getType();
449                        String pattern = datafield.pattern();
450                        int precision = datafield.precision();
451    
452                        // Create format
453                        Format format = FormatFactory.getFormat(type, pattern, precision);
454    
455                        // Get field value
456                        Object value = field.get(obj);
457    
458                        result = formatString(format, value);
459    
460                        if (LOG.isDebugEnabled()) {
461                            LOG.debug("Value to be formatted : " + value + ", position : " + datafield.pos() + ", and its formated value : " + result);
462                        }
463    
464                    } else {
465                        result = "";
466                    }
467    
468                    Integer key;
469    
470                    if (isMessageOrdered()) {
471    
472                        // Generate a key using the number of the section
473                        // and the position of the field
474                        Integer key1 = sections.get(obj.getClass().getName());
475                        Integer key2 = datafield.position();
476                        Integer keyGenerated = generateKey(key1, key2);
477    
478                        if (LOG.isDebugEnabled()) {
479                            LOG.debug("Key generated : " + String.valueOf(keyGenerated) + ", for section : " + key1);
480                        }
481    
482                        key = keyGenerated;
483    
484                    } else {
485    
486                        key = datafield.pos();
487                    }
488    
489                    if (!results.containsKey(key)) {
490    
491                        List list = new LinkedList();
492                        list.add(result);
493                        results.put(key, list);
494    
495                    } else {
496    
497                        List list = (LinkedList)results.get(key);
498                        list.add(result);
499                    }
500    
501                }
502    
503                OneToMany oneToMany = field.getAnnotation(OneToMany.class);
504                if (oneToMany != null) {
505    
506                    // Set global variable
507                    // Will be used during generation of CSV
508                    isOneToMany = true;
509    
510                    ArrayList list = (ArrayList)field.get(obj);
511    
512                    if (list != null) {
513    
514                        Iterator it = list.iterator();
515    
516                        while (it.hasNext()) {
517    
518                            Object target = it.next();
519                            generateCsvPositionMap(target.getClass(), target);
520    
521                        }
522    
523                    } else {
524    
525                        // Call this function to add empty value
526                        // in the table
527                        generateCsvPositionMap(field.getClass(), null);
528                    }
529    
530                }
531            }
532    
533        }
534    
535        private String formatString(Format format, Object value) throws Exception {
536    
537            String strValue = "";
538    
539            if (value != null) {
540    
541                // Format field value
542                try {
543                    strValue = format.format(value);
544                } catch (Exception e) {
545                    throw new IllegalArgumentException("Formating error detected for the value : " + value, e);
546                }
547    
548            }
549    
550            return strValue;
551    
552        }
553    
554        public String generateHeader() {
555    
556            Map<Integer, DataField> dataFieldsSorted = new TreeMap<Integer, DataField>(dataFields);
557            Iterator<Integer> it = dataFieldsSorted.keySet().iterator();
558    
559            StringBuilder builderHeader = new StringBuilder();
560    
561            while (it.hasNext()) {
562    
563                DataField dataField = dataFieldsSorted.get(it.next());
564    
565                // Retrieve the field
566                Field field = annotedFields.get(dataField.pos());
567                // Change accessibility to allow to read protected/private fields
568                field.setAccessible(true);
569    
570                // Get dataField
571                if (!dataField.columnName().equals("")) {
572                    builderHeader.append(dataField.columnName());
573                } else {
574                    builderHeader.append(field.getName());
575                }
576    
577                if (it.hasNext()) {
578                    builderHeader.append(separator);
579                }
580    
581            }
582    
583            return builderHeader.toString();
584        }
585    
586        /**
587         * Get paramaters defined in @Csvrecord annotation
588         */
589        private void initCsvRecordParameters() {
590            if (separator == null) {
591                for (Class<?> cl : models) {
592    
593                    // Get annotation @CsvRecord from the class
594                    CsvRecord record = cl.getAnnotation(CsvRecord.class);
595    
596                    // Get annotation @Section from the class
597                    Section section = cl.getAnnotation(Section.class);
598    
599                    if (record != null) {
600                        if (LOG.isDebugEnabled()) {
601                            LOG.debug("Csv record : " + record.toString());
602                        }
603    
604                        // Get skipFirstLine parameter
605                        skipFirstLine = record.skipFirstLine();
606                        if (LOG.isDebugEnabled()) {
607                            LOG.debug("Skip First Line parameter of the CSV : " + skipFirstLine);
608                        }
609    
610                        // Get generateHeaderColumnNames parameter
611                        generateHeaderColumnNames = record.generateHeaderColumns();
612                        if (LOG.isDebugEnabled()) {
613                            LOG.debug("Generate header column names parameter of the CSV : " + generateHeaderColumnNames);
614                        }
615    
616                        // Get Separator parameter
617                        ObjectHelper.notNull(record.separator(), "No separator has been defined in the @Record annotation !");
618                        separator = record.separator();
619                        if (LOG.isDebugEnabled()) {
620                            LOG.debug("Separator defined for the CSV : " + separator);
621                        }
622    
623                        // Get carriage return parameter
624                        crlf = record.crlf();
625                        if (LOG.isDebugEnabled()) {
626                            LOG.debug("Carriage return defined for the CSV : " + crlf);
627                        }
628    
629                        // Get isOrdered parameter
630                        messageOrdered = record.isOrdered();
631                        if (LOG.isDebugEnabled()) {
632                            LOG.debug("Must CSV record be ordered ? " + messageOrdered);
633                        }
634    
635                    }
636    
637                    if (section != null) {
638                        // Test if section number is not null
639                        ObjectHelper.notNull(section.number(), "No number has been defined for the section !");
640    
641                        // Get section number and add it to the sections
642                        sections.put(cl.getName(), section.number());
643                    }
644                }
645            }
646        }
647    
648        /**
649         * Find the separator used to delimit the CSV fields
650         */
651        public String getSeparator() {
652            return separator;
653        }
654    
655        /**
656         * Flag indicating if the first line of the CSV must be skipped
657         */
658        public boolean getGenerateHeaderColumnNames() {
659            return generateHeaderColumnNames;
660        }
661    
662        /**
663         * Find the separator used to delimit the CSV fields
664         */
665        public boolean getSkipFirstLine() {
666            return skipFirstLine;
667        }
668    
669        /**
670         * Flag indicating if the message must be ordered
671         * 
672         * @return boolean
673         */
674        public boolean isMessageOrdered() {
675            return messageOrdered;
676        }
677    }