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