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> annotedFields = new LinkedHashMap<Integer, Field>();
056        private Map<String, Integer> sections = new HashMap<String, Integer>();
057    
058        private Map<Integer, List> results;
059    
060        private int numberOptionalFields;
061        private int numberMandatoryFields;
062        private int totalFields;
063    
064        private String separator;
065        private boolean skipFirstLine;
066        private boolean generateHeaderColumnNames;
067        private boolean messageOrdered;
068    
069        public BindyCsvFactory(PackageScanClassResolver resolver, String... packageNames) throws Exception {
070            super(resolver, packageNames);
071    
072            // initialize specific parameters of the csv model
073            initCsvModel();
074        }
075    
076        /**
077         * method uses to initialize the model representing the classes who will
078         * bind the data. This process will scan for classes according to the
079         * package name provided, check the annotated classes and fields and
080         * retrieve the separator of the CSV record
081         * 
082         * @throws Exception
083         */
084        public void initCsvModel() throws Exception {
085    
086            // Find annotated Datafields declared in the Model classes
087            initAnnotatedFields();
088    
089            // initialize Csv parameter(s)
090            // separator and skip first line from @CSVrecord annotation
091            initCsvRecordParameters();
092        }
093    
094        public void initAnnotatedFields() {
095    
096            for (Class<?> cl : models) {
097    
098                List<Field> linkFields = new ArrayList<Field>();
099    
100                if (LOG.isDebugEnabled()) {
101                    LOG.debug("Class retrieved : " + cl.getName());
102                }
103    
104                for (Field field : cl.getDeclaredFields()) {
105                    DataField dataField = field.getAnnotation(DataField.class);
106                    if (dataField != null) {
107                        if (LOG.isDebugEnabled()) {
108                            LOG.debug("Position defined in the class : " + cl.getName() + ", position : " + dataField.pos() + ", Field : " + dataField.toString());
109                        }
110    
111                        if (dataField.required()) {
112                            ++numberMandatoryFields;
113                        } else {
114                            ++numberOptionalFields;
115                        }
116    
117                        dataFields.put(dataField.pos(), dataField);
118                        annotedFields.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  : " + cl.getName() + ", Field" + field.toString());
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    
147        public void bind(List<String> tokens, Map<String, Object> model, int line) throws Exception {
148    
149            int pos = 1;
150            int counterMandatoryFields = 0;
151    
152            for (String data : tokens) {
153    
154                // Get DataField from model
155                DataField dataField = dataFields.get(pos);
156                ObjectHelper.notNull(dataField, "No position " + pos + " defined for the field : " + data + ", line : " + line);
157    
158                if (dataField.trim()) {
159                    data = data.trim();
160                }
161                
162                if (dataField.required()) {
163                    // Increment counter of mandatory fields
164                    ++counterMandatoryFields;
165    
166                    // Check if content of the field is empty
167                    // This is not possible for mandatory fields
168                    if (data.equals("")) {
169                        throw new IllegalArgumentException("The mandatory field defined at the position " + pos + " is empty for the line : " + line);
170                    }
171                }
172    
173                // Get Field to be setted
174                Field field = annotedFields.get(pos);
175                field.setAccessible(true);
176    
177                if (LOG.isDebugEnabled()) {
178                    LOG.debug("Pos : " + pos + ", Data : " + data + ", Field type : " + field.getType());
179                }
180    
181                Format<?> format;
182    
183                // Get pattern defined for the field
184                String pattern = dataField.pattern();
185    
186                // Create format object to format the field
187                format = FormatFactory.getFormat(field.getType(), pattern, getLocale(), dataField.precision());
188    
189                // field object to be set
190                Object modelField = model.get(field.getDeclaringClass().getName());
191    
192                // format the data received
193                Object value = null;
194    
195                if (!data.equals("")) {
196                    try {
197                        value = format.parse(data);
198                    } catch (FormatException ie) {
199                        throw new IllegalArgumentException(ie.getMessage() + ", position : " + pos + ", line : " + line, ie);
200                    } catch (Exception e) {
201                        throw new IllegalArgumentException("Parsing error detected for field defined at the position : " + pos + ", line : " + line, e);
202                    }
203                } else {
204                    value = getDefaultValueForPrimitive(field.getType());
205                }
206    
207                field.set(modelField, value);
208    
209                ++pos;
210    
211            }
212    
213            if (LOG.isDebugEnabled()) {
214                LOG.debug("Counter mandatory fields : " + counterMandatoryFields);
215            }
216    
217            if (pos < totalFields) {
218                throw new IllegalArgumentException("Some fields are missing (optional or mandatory), line : " + line);
219            }
220    
221            if (counterMandatoryFields < numberMandatoryFields) {
222                throw new IllegalArgumentException("Some mandatory fields are missing, line : " + line);
223            }
224    
225        }
226    
227        public String unbind(Map<String, Object> model) throws Exception {
228    
229            StringBuilder buffer = new StringBuilder();
230            results = new HashMap<Integer, List>();
231    
232            // Check if separator exists
233            ObjectHelper.notNull(this.separator, "The separator has not been instantiated or property not defined in the @CsvRecord annotation");
234    
235            char separator = Converter.getCharDelimitor(this.getSeparator());
236    
237            if (LOG.isDebugEnabled()) {
238                LOG.debug("Separator converted : '0x" + Integer.toHexString(separator) + "', from : " + this.getSeparator());
239            }
240    
241            for (Class clazz : models) {
242    
243                if (model.containsKey(clazz.getName())) {
244    
245                    Object obj = model.get(clazz.getName());
246    
247                    if (LOG.isDebugEnabled()) {
248                        LOG.debug("Model object : " + obj + ", class : " + obj.getClass().getName());
249                    }
250    
251                    if (obj != null) {
252    
253                        // Generate Csv table
254                        generateCsvPositionMap(clazz, obj);
255    
256                    }
257                }
258            }
259    
260            // Transpose result
261            List<List> l = new ArrayList<List>();
262    
263            if (isOneToMany) {
264    
265                l = product(results);
266    
267            } else {
268    
269                // Convert Map<Integer, List> into List<List>
270                TreeMap<Integer, List> sortValues = new TreeMap<Integer, List>(results);
271                List<String> temp = new ArrayList<String>();
272    
273                for (Entry<Integer, List> entry : sortValues.entrySet()) {
274    
275                    // Get list of values
276                    List<String> val = entry.getValue();
277    
278                    // For one to one relation
279                    // There is only one item in the list
280                    String value = (String)val.get(0);
281    
282                    // Add the value to the temp array
283                    if (value != null) {
284                        temp.add(value);
285                    } else {
286                        temp.add("");
287                    }
288                }
289    
290                l.add(temp);
291            }
292    
293            if (l != null) {
294    
295                Iterator it = l.iterator();
296                while (it.hasNext()) {
297    
298                    List<String> tokens = (ArrayList<String>)it.next();
299                    Iterator itx = tokens.iterator();
300    
301                    while (itx.hasNext()) {
302    
303                        String res = (String)itx.next();
304    
305                        if (res != null) {
306                            buffer.append(res);
307                        } else {
308                            buffer.append("");
309                        }
310    
311                        if (itx.hasNext()) {
312                            buffer.append(separator);
313                        }
314    
315                    }
316    
317                    if (it.hasNext()) {
318                        buffer.append(Converter.getStringCarriageReturn(getCarriageReturn()));
319                    }
320    
321                }
322    
323            }
324    
325            return buffer.toString();
326    
327        }
328    
329        private List<List> product(Map<Integer, List> values) {
330    
331            TreeMap<Integer, List> sortValues = new TreeMap<Integer, List>(values);
332    
333            List<List> product = new ArrayList<List>();
334            Map<Integer, Integer> index = new HashMap<Integer, Integer>();
335    
336            boolean cont = true;
337            int idx = 0;
338            int idxSize;
339    
340            do {
341    
342                idxSize = 0;
343                List v = new ArrayList();
344    
345                for (int ii = 1; ii <= sortValues.lastKey(); ii++) {
346    
347                    List l = values.get(ii);
348    
349                    if (l == null) {
350                        v.add("");
351                        ++idxSize;
352                        continue;
353                    }
354    
355                    if (l.size() >= idx + 1) {
356                        v.add(l.get(idx));
357                        index.put(ii, idx);
358                        if (LOG.isDebugEnabled()) {
359                            LOG.debug("Value : " + l.get(idx) + ", pos : " + ii + ", at :" + idx);
360                        }
361    
362                    } else {
363                        v.add(l.get(0));
364                        index.put(ii, 0);
365                        ++idxSize;
366                        if (LOG.isDebugEnabled()) {
367                            LOG.debug("Value : " + l.get(0) + ", pos : " + ii + ", at index : " + 0);
368                        }
369                    }
370    
371                }
372    
373                if (idxSize != sortValues.lastKey()) {
374                    product.add(v);
375                }
376                ++idx;
377    
378            } while (idxSize != sortValues.lastKey());
379    
380            return product;
381        }
382    
383        /**
384         * 
385         * Generate a table containing the data formated and sorted with their position/offset
386         * If the model is Ordered than a key is created combining the annotation @Section and Position of the field
387         * If a relation @OneToMany is defined, than we iterate recursivelu through this function
388         * The result is placed in the Map<Integer, List> results
389         * 
390         * @param clazz
391         * @param obj
392         * @throws Exception
393         */
394        private void generateCsvPositionMap(Class clazz, Object obj) throws Exception {
395    
396            String result = "";
397    
398            for (Field field : clazz.getDeclaredFields()) {
399    
400                field.setAccessible(true);
401    
402                DataField datafield = field.getAnnotation(DataField.class);
403    
404                if (datafield != null) {
405    
406                    if (obj != null) {
407    
408                        // Retrieve the format, pattern and precision associated to
409                        // the type
410                        Class type = field.getType();
411                        String pattern = datafield.pattern();
412                        int precision = datafield.precision();
413    
414                        // Create format
415                        Format format = FormatFactory.getFormat(type, pattern, getLocale(), precision);
416    
417                        // Get field value
418                        Object value = field.get(obj);
419    
420                        result = formatString(format, value);
421    
422                        if (LOG.isDebugEnabled()) {
423                            LOG.debug("Value to be formatted : " + value + ", position : " + datafield.pos() + ", and its formated value : " + result);
424                        }
425    
426                    } else {
427                        result = "";
428                    }
429    
430                    Integer key;
431    
432                    if (isMessageOrdered()) {
433    
434                        // Generate a key using the number of the section
435                        // and the position of the field
436                        Integer key1 = sections.get(obj.getClass().getName());
437                        Integer key2 = datafield.position();
438                        Integer keyGenerated = generateKey(key1, key2);
439    
440                        if (LOG.isDebugEnabled()) {
441                            LOG.debug("Key generated : " + String.valueOf(keyGenerated) + ", for section : " + key1);
442                        }
443    
444                        key = keyGenerated;
445    
446                    } else {
447    
448                        key = datafield.pos();
449                    }
450    
451                    if (!results.containsKey(key)) {
452    
453                        List list = new LinkedList();
454                        list.add(result);
455                        results.put(key, list);
456    
457                    } else {
458    
459                        List list = (LinkedList)results.get(key);
460                        list.add(result);
461                    }
462    
463                }
464    
465                OneToMany oneToMany = field.getAnnotation(OneToMany.class);
466                if (oneToMany != null) {
467    
468                    // Set global variable
469                    // Will be used during generation of CSV
470                    isOneToMany = true;
471    
472                    ArrayList list = (ArrayList)field.get(obj);
473    
474                    if (list != null) {
475    
476                        Iterator it = list.iterator();
477    
478                        while (it.hasNext()) {
479    
480                            Object target = it.next();
481                            generateCsvPositionMap(target.getClass(), target);
482    
483                        }
484    
485                    } else {
486    
487                        // Call this function to add empty value
488                        // in the table
489                        generateCsvPositionMap(field.getClass(), null);
490                    }
491    
492                }
493            }
494    
495        }
496        
497        /**
498         * Generate for the first line the headers of the columns
499         * 
500         * @return the headers columns
501         */
502        public String generateHeader() {
503    
504            Map<Integer, DataField> dataFieldsSorted = new TreeMap<Integer, DataField>(dataFields);
505            Iterator<Integer> it = dataFieldsSorted.keySet().iterator();
506    
507            StringBuilder builderHeader = new StringBuilder();
508    
509            while (it.hasNext()) {
510    
511                DataField dataField = dataFieldsSorted.get(it.next());
512    
513                // Retrieve the field
514                Field field = annotedFields.get(dataField.pos());
515                // Change accessibility to allow to read protected/private fields
516                field.setAccessible(true);
517    
518                // Get dataField
519                if (!dataField.columnName().equals("")) {
520                    builderHeader.append(dataField.columnName());
521                } else {
522                    builderHeader.append(field.getName());
523                }
524    
525                if (it.hasNext()) {
526                    builderHeader.append(separator);
527                }
528    
529            }
530    
531            return builderHeader.toString();
532        }
533    
534        /**
535         * Get parameters defined in @Csvrecord annotation
536         */
537        private void initCsvRecordParameters() {
538            if (separator == null) {
539                for (Class<?> cl : models) {
540    
541                    // Get annotation @CsvRecord from the class
542                    CsvRecord record = cl.getAnnotation(CsvRecord.class);
543    
544                    // Get annotation @Section from the class
545                    Section section = cl.getAnnotation(Section.class);
546    
547                    if (record != null) {
548                        if (LOG.isDebugEnabled()) {
549                            LOG.debug("Csv record : " + record.toString());
550                        }
551    
552                        // Get skipFirstLine parameter
553                        skipFirstLine = record.skipFirstLine();
554                        if (LOG.isDebugEnabled()) {
555                            LOG.debug("Skip First Line parameter of the CSV : " + skipFirstLine);
556                        }
557    
558                        // Get generateHeaderColumnNames parameter
559                        generateHeaderColumnNames = record.generateHeaderColumns();
560                        if (LOG.isDebugEnabled()) {
561                            LOG.debug("Generate header column names parameter of the CSV : " + generateHeaderColumnNames);
562                        }
563    
564                        // Get Separator parameter
565                        ObjectHelper.notNull(record.separator(), "No separator has been defined in the @Record annotation !");
566                        separator = record.separator();
567                        if (LOG.isDebugEnabled()) {
568                            LOG.debug("Separator defined for the CSV : " + separator);
569                        }
570    
571                        // Get carriage return parameter
572                        crlf = record.crlf();
573                        if (LOG.isDebugEnabled()) {
574                            LOG.debug("Carriage return defined for the CSV : " + crlf);
575                        }
576    
577                        // Get isOrdered parameter
578                        messageOrdered = record.isOrdered();
579                        if (LOG.isDebugEnabled()) {
580                            LOG.debug("Must CSV record be ordered ? " + messageOrdered);
581                        }
582    
583                    }
584    
585                    if (section != null) {
586                        // Test if section number is not null
587                        ObjectHelper.notNull(section.number(), "No number has been defined for the section !");
588    
589                        // Get section number and add it to the sections
590                        sections.put(cl.getName(), section.number());
591                    }
592                }
593            }
594        }
595    
596        /**
597         * Find the separator used to delimit the CSV fields
598         */
599        public String getSeparator() {
600            return separator;
601        }
602    
603        /**
604         * Flag indicating if the first line of the CSV must be skipped
605         */
606        public boolean getGenerateHeaderColumnNames() {
607            return generateHeaderColumnNames;
608        }
609    
610        /**
611         * Find the separator used to delimit the CSV fields
612         */
613        public boolean getSkipFirstLine() {
614            return skipFirstLine;
615        }
616    
617        /**
618         * Flag indicating if the message must be ordered
619         * 
620         * @return boolean
621         */
622        public boolean isMessageOrdered() {
623            return messageOrdered;
624        }
625    }