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.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 = annotedFields.get(pos);
174                field.setAccessible(true);
175    
176                if (LOG.isDebugEnabled()) {
177                    LOG.debug("Pos : " + pos + ", Data : " + data + ", Field type : " + 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, 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            if (LOG.isDebugEnabled()) {
213                LOG.debug("Counter mandatory fields : " + counterMandatoryFields);
214            }
215    
216            if (pos < totalFields) {
217                throw new IllegalArgumentException("Some fields are missing (optional or mandatory), line : " + line);
218            }
219    
220            if (counterMandatoryFields < numberMandatoryFields) {
221                throw new IllegalArgumentException("Some mandatory fields are missing, line : " + line);
222            }
223    
224        }
225    
226        public String unbind(Map<String, Object> model) throws Exception {
227    
228            StringBuilder buffer = new StringBuilder();
229            results = new HashMap<Integer, List>();
230    
231            // Check if separator exists
232            ObjectHelper.notNull(this.separator, "The separator has not been instantiated or property not defined in the @CsvRecord annotation");
233    
234            char separator = Converter.getCharDelimitor(this.getSeparator());
235    
236            if (LOG.isDebugEnabled()) {
237                LOG.debug("Separator converted : '0x" + Integer.toHexString(separator) + "', from : " + this.getSeparator());
238            }
239    
240            for (Class clazz : models) {
241    
242                if (model.containsKey(clazz.getName())) {
243    
244                    Object obj = model.get(clazz.getName());
245    
246                    if (LOG.isDebugEnabled()) {
247                        LOG.debug("Model object : " + obj + ", class : " + obj.getClass().getName());
248                    }
249    
250                    if (obj != null) {
251    
252                        // Generate Csv table
253                        generateCsvPositionMap(clazz, obj);
254    
255                    }
256                }
257            }
258    
259            // Transpose result
260            List<List> l = new ArrayList<List>();
261    
262            if (isOneToMany) {
263    
264                l = product(results);
265    
266            } else {
267    
268                // Convert Map<Integer, List> into List<List>
269                TreeMap<Integer, List> sortValues = new TreeMap<Integer, List>(results);
270                List<String> temp = new ArrayList<String>();
271    
272                for (Integer key : sortValues.keySet()) {
273    
274                    // Get list of values
275                    List<String> val = sortValues.get(key);
276    
277                    // For one to one relation
278                    // There is only one item in the list
279                    String value = (String)val.get(0);
280    
281                    // Add the value to the temp array
282                    if (value != null) {
283                        temp.add(value);
284                    } else {
285                        temp.add("");
286                    }
287                }
288    
289                l.add(temp);
290            }
291    
292            if (l != null) {
293    
294                Iterator it = l.iterator();
295                while (it.hasNext()) {
296    
297                    List<String> tokens = (ArrayList<String>)it.next();
298                    Iterator itx = tokens.iterator();
299    
300                    while (itx.hasNext()) {
301    
302                        String res = (String)itx.next();
303    
304                        if (res != null) {
305                            buffer.append(res);
306                        } else {
307                            buffer.append("");
308                        }
309    
310                        if (itx.hasNext()) {
311                            buffer.append(separator);
312                        }
313    
314                    }
315    
316                    if (it.hasNext()) {
317                        buffer.append(Converter.getStringCarriageReturn(getCarriageReturn()));
318                    }
319    
320                }
321    
322            }
323    
324            return buffer.toString();
325    
326        }
327    
328        private List<List> product(Map<Integer, List> values) {
329    
330            TreeMap<Integer, List> sortValues = new TreeMap<Integer, List>(values);
331    
332            List<List> product = new ArrayList<List>();
333            Map<Integer, Integer> index = new HashMap<Integer, Integer>();
334    
335            boolean cont = true;
336            int idx = 0;
337            int idxSize;
338    
339            do {
340    
341                idxSize = 0;
342                List v = new ArrayList();
343    
344                for (int ii = 1; ii <= sortValues.lastKey(); ii++) {
345    
346                    List l = values.get(ii);
347    
348                    if (l == null) {
349                        v.add("");
350                        ++idxSize;
351                        continue;
352                    }
353    
354                    if (l.size() >= idx + 1) {
355                        v.add(l.get(idx));
356                        index.put(ii, idx);
357                        if (LOG.isDebugEnabled()) {
358                            LOG.debug("Value : " + l.get(idx) + ", pos : " + ii + ", at :" + idx);
359                        }
360    
361                    } else {
362                        v.add(l.get(0));
363                        index.put(ii, 0);
364                        ++idxSize;
365                        if (LOG.isDebugEnabled()) {
366                            LOG.debug("Value : " + l.get(0) + ", pos : " + ii + ", at index : " + 0);
367                        }
368                    }
369    
370                }
371    
372                if (idxSize != sortValues.lastKey()) {
373                    product.add(v);
374                }
375                ++idx;
376    
377            } while (idxSize != sortValues.lastKey());
378    
379            return product;
380        }
381    
382        /**
383         * 
384         * Generate a table containing the data formated and sorted with their position/offset
385         * If the model is Ordered than a key is created combining the annotation @Section and Position of the field
386         * If a relation @OneToMany is defined, than we iterate recursivelu through this function
387         * The result is placed in the Map<Integer, List> results
388         * 
389         * @param clazz
390         * @param obj
391         * @throws Exception
392         */
393        private void generateCsvPositionMap(Class clazz, Object obj) throws Exception {
394    
395            String result = "";
396    
397            for (Field field : clazz.getDeclaredFields()) {
398    
399                field.setAccessible(true);
400    
401                DataField datafield = field.getAnnotation(DataField.class);
402    
403                if (datafield != null) {
404    
405                    if (obj != null) {
406    
407                        // Retrieve the format, pattern and precision associated to
408                        // the type
409                        Class type = field.getType();
410                        String pattern = datafield.pattern();
411                        int precision = datafield.precision();
412    
413                        // Create format
414                        Format format = FormatFactory.getFormat(type, pattern, precision);
415    
416                        // Get field value
417                        Object value = field.get(obj);
418    
419                        result = formatString(format, value);
420    
421                        if (LOG.isDebugEnabled()) {
422                            LOG.debug("Value to be formatted : " + value + ", position : " + datafield.pos() + ", and its formated value : " + result);
423                        }
424    
425                    } else {
426                        result = "";
427                    }
428    
429                    Integer key;
430    
431                    if (isMessageOrdered()) {
432    
433                        // Generate a key using the number of the section
434                        // and the position of the field
435                        Integer key1 = sections.get(obj.getClass().getName());
436                        Integer key2 = datafield.position();
437                        Integer keyGenerated = generateKey(key1, key2);
438    
439                        if (LOG.isDebugEnabled()) {
440                            LOG.debug("Key generated : " + String.valueOf(keyGenerated) + ", for section : " + key1);
441                        }
442    
443                        key = keyGenerated;
444    
445                    } else {
446    
447                        key = datafield.pos();
448                    }
449    
450                    if (!results.containsKey(key)) {
451    
452                        List list = new LinkedList();
453                        list.add(result);
454                        results.put(key, list);
455    
456                    } else {
457    
458                        List list = (LinkedList)results.get(key);
459                        list.add(result);
460                    }
461    
462                }
463    
464                OneToMany oneToMany = field.getAnnotation(OneToMany.class);
465                if (oneToMany != null) {
466    
467                    // Set global variable
468                    // Will be used during generation of CSV
469                    isOneToMany = true;
470    
471                    ArrayList list = (ArrayList)field.get(obj);
472    
473                    if (list != null) {
474    
475                        Iterator it = list.iterator();
476    
477                        while (it.hasNext()) {
478    
479                            Object target = it.next();
480                            generateCsvPositionMap(target.getClass(), target);
481    
482                        }
483    
484                    } else {
485    
486                        // Call this function to add empty value
487                        // in the table
488                        generateCsvPositionMap(field.getClass(), null);
489                    }
490    
491                }
492            }
493    
494        }
495        
496        /**
497         * Generate for the first line the headers of the columns
498         * 
499         * @return the headers columns
500         */
501        public String generateHeader() {
502    
503            Map<Integer, DataField> dataFieldsSorted = new TreeMap<Integer, DataField>(dataFields);
504            Iterator<Integer> it = dataFieldsSorted.keySet().iterator();
505    
506            StringBuilder builderHeader = new StringBuilder();
507    
508            while (it.hasNext()) {
509    
510                DataField dataField = dataFieldsSorted.get(it.next());
511    
512                // Retrieve the field
513                Field field = annotedFields.get(dataField.pos());
514                // Change accessibility to allow to read protected/private fields
515                field.setAccessible(true);
516    
517                // Get dataField
518                if (!dataField.columnName().equals("")) {
519                    builderHeader.append(dataField.columnName());
520                } else {
521                    builderHeader.append(field.getName());
522                }
523    
524                if (it.hasNext()) {
525                    builderHeader.append(separator);
526                }
527    
528            }
529    
530            return builderHeader.toString();
531        }
532    
533        /**
534         * Get parameters defined in @Csvrecord annotation
535         */
536        private void initCsvRecordParameters() {
537            if (separator == null) {
538                for (Class<?> cl : models) {
539    
540                    // Get annotation @CsvRecord from the class
541                    CsvRecord record = cl.getAnnotation(CsvRecord.class);
542    
543                    // Get annotation @Section from the class
544                    Section section = cl.getAnnotation(Section.class);
545    
546                    if (record != null) {
547                        if (LOG.isDebugEnabled()) {
548                            LOG.debug("Csv record : " + record.toString());
549                        }
550    
551                        // Get skipFirstLine parameter
552                        skipFirstLine = record.skipFirstLine();
553                        if (LOG.isDebugEnabled()) {
554                            LOG.debug("Skip First Line parameter of the CSV : " + skipFirstLine);
555                        }
556    
557                        // Get generateHeaderColumnNames parameter
558                        generateHeaderColumnNames = record.generateHeaderColumns();
559                        if (LOG.isDebugEnabled()) {
560                            LOG.debug("Generate header column names parameter of the CSV : " + generateHeaderColumnNames);
561                        }
562    
563                        // Get Separator parameter
564                        ObjectHelper.notNull(record.separator(), "No separator has been defined in the @Record annotation !");
565                        separator = record.separator();
566                        if (LOG.isDebugEnabled()) {
567                            LOG.debug("Separator defined for the CSV : " + separator);
568                        }
569    
570                        // Get carriage return parameter
571                        crlf = record.crlf();
572                        if (LOG.isDebugEnabled()) {
573                            LOG.debug("Carriage return defined for the CSV : " + crlf);
574                        }
575    
576                        // Get isOrdered parameter
577                        messageOrdered = record.isOrdered();
578                        if (LOG.isDebugEnabled()) {
579                            LOG.debug("Must CSV record be ordered ? " + messageOrdered);
580                        }
581    
582                    }
583    
584                    if (section != null) {
585                        // Test if section number is not null
586                        ObjectHelper.notNull(section.number(), "No number has been defined for the section !");
587    
588                        // Get section number and add it to the sections
589                        sections.put(cl.getName(), section.number());
590                    }
591                }
592            }
593        }
594    
595        /**
596         * Find the separator used to delimit the CSV fields
597         */
598        public String getSeparator() {
599            return separator;
600        }
601    
602        /**
603         * Flag indicating if the first line of the CSV must be skipped
604         */
605        public boolean getGenerateHeaderColumnNames() {
606            return generateHeaderColumnNames;
607        }
608    
609        /**
610         * Find the separator used to delimit the CSV fields
611         */
612        public boolean getSkipFirstLine() {
613            return skipFirstLine;
614        }
615    
616        /**
617         * Flag indicating if the message must be ordered
618         * 
619         * @return boolean
620         */
621        public boolean isMessageOrdered() {
622            return messageOrdered;
623        }
624    }