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.List;
025    import java.util.Map;
026    import java.util.TreeMap;
027    import java.util.jar.JarException;
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.Section;
033    import org.apache.camel.dataformat.bindy.util.Converter;
034    import org.apache.camel.spi.PackageScanClassResolver;
035    import org.apache.camel.util.ObjectHelper;
036    import org.apache.commons.logging.Log;
037    import org.apache.commons.logging.LogFactory;
038    
039    /**
040     * The BindyCsvFactory is the class who allows to : Generate a model associated
041     * to a CSV record, bind data from a record to the POJOs, export data of POJOs
042     * to a CSV record and format data into String, Date, Double, ... according to
043     * the format/pattern defined
044     */
045    public class BindyCsvFactory extends BindyAbstractFactory implements BindyFactory {
046    
047        private static final transient Log LOG = LogFactory.getLog(BindyCsvFactory.class);
048    
049        private Map<Integer, DataField> dataFields = new LinkedHashMap<Integer, DataField>();
050        private Map<Integer, Field> annotedFields = new LinkedHashMap<Integer, Field>();
051        private Map<String, Integer> sections = new HashMap<String, Integer>();
052        private int numberOptionalFields;
053        private int numberMandatoryFields;
054        private int totalFields;
055    
056        private String separator;
057        private boolean skipFirstLine;
058        private boolean messageOrdered;
059    
060        public BindyCsvFactory(PackageScanClassResolver resolver, String... packageNames) throws Exception {
061            super(resolver, packageNames);
062    
063            // initialize specific parameters of the csv model
064            initCsvModel();
065        }
066    
067        /**
068         * method uses to initialize the model representing the classes who will
069         * bind the data. This process will scan for classes according to the package
070         * name provided, check the annotated classes and fields and retrieve the
071         * separator of the CSV record
072         * 
073         * @throws Exception
074         */
075        public void initCsvModel() throws Exception {
076    
077            // Find annotated Datafields declared in the Model classes
078            initAnnotedFields();
079    
080            // initialize Csv parameter(s)
081            // separator and skip first line from @CSVrecord annotation
082            initCsvRecordParameters();
083        }
084    
085        public void initAnnotedFields() {
086    
087            for (Class<?> cl : models) {
088    
089                List<Field> linkFields = new ArrayList<Field>();
090    
091                if (LOG.isDebugEnabled()) {
092                    LOG.debug("Class retrieved : " + cl.getName());
093                }
094    
095                for (Field field : cl.getDeclaredFields()) {
096                    DataField dataField = field.getAnnotation(DataField.class);
097                    if (dataField != null) {
098                        if (LOG.isDebugEnabled()) {
099                            LOG.debug("Position defined in the class : " + cl.getName() + ", position : "
100                                      + dataField.pos() + ", Field : " + dataField.toString());
101                        }
102                        
103                        if (dataField.required()) {
104                            ++numberMandatoryFields;
105                        } else {
106                            ++numberOptionalFields;
107                        }
108                        
109                        dataFields.put(dataField.pos(), dataField);
110                        annotedFields.put(dataField.pos(), field);
111                    }
112    
113                    Link linkField = field.getAnnotation(Link.class);
114    
115                    if (linkField != null) {
116                        if (LOG.isDebugEnabled()) {
117                            LOG.debug("Class linked  : " + cl.getName() + ", Field" + field.toString());
118                        }
119                        linkFields.add(field);
120                    }
121    
122                }
123    
124                if (!linkFields.isEmpty()) {
125                    annotedLinkFields.put(cl.getName(), linkFields);
126                }
127                
128                totalFields = numberMandatoryFields + numberOptionalFields;
129                
130                if (LOG.isDebugEnabled()) {
131                    LOG.debug("Number of optional fields : " + numberOptionalFields);
132                    LOG.debug("Number of mandatory fields : " + numberMandatoryFields);
133                    LOG.debug("Total : " + totalFields);
134                }  
135                
136            }
137        }
138    
139        public void bind(List<String> tokens, Map<String, Object> model) throws Exception {
140    
141            int pos = 0;
142            int counterMandatoryFields = 0;
143     
144            for (String data : tokens) {
145            
146                // Get DataField from model
147                DataField dataField = dataFields.get(pos);
148                ObjectHelper.notNull(dataField, "No position " + pos + " defined for the field : " + data);
149                
150                if (dataField.required()) {
151                    // Increment counter of mandatory fields
152                    ++counterMandatoryFields;
153    
154                    // Check if content of the field is empty
155                    // This is not possible for mandatory fields
156                    if (data.equals("")) {
157                        throw new IllegalArgumentException("The mandatory field defined at the position " + pos
158                                                           + " is empty !");
159                    }
160                }
161                
162                // Get Field to be setted
163                Field field = annotedFields.get(pos);
164                field.setAccessible(true);
165                
166                if (LOG.isDebugEnabled()) {
167                    LOG.debug("Pos : " + pos + ", Data : " + data + ", Field type : " + field.getType());
168                }
169                
170                Format<?> format;
171                
172                // Get pattern defined for the field
173                String pattern = dataField.pattern();
174                
175                // Create format object to format the field 
176                format = FormatFactory.getFormat(field.getType(), pattern, dataField.precision());
177                
178                // field object to be set
179                Object modelField = model.get(field.getDeclaringClass().getName());
180                
181                // format the data received
182                Object value = null;
183                
184                if (!data.equals("")) {
185                    value = format.parse(data);
186                } else {
187                    value = getDefaultValueforPrimitive(field.getType());
188                }
189                
190                field.set(modelField, value);
191                
192                ++pos;            
193                
194            }
195            
196            if (LOG.isDebugEnabled()) {
197                LOG.debug("Counter mandatory fields : " + counterMandatoryFields);
198            }
199            
200         
201            if (pos < totalFields) {
202                throw new IllegalArgumentException("Some fields are missing (optional or mandatory) !!");
203            }
204    
205            if (counterMandatoryFields < numberMandatoryFields) {
206                throw new IllegalArgumentException("Some mandatory fields are missing !!");
207            }
208            
209        }
210    
211        public String unbind(Map<String, Object> model) throws Exception {
212    
213            StringBuilder builder = new StringBuilder();
214    
215            Map<Integer, DataField> dataFieldsSorted = new TreeMap<Integer, DataField>(dataFields);
216            Iterator<Integer> it = dataFieldsSorted.keySet().iterator();
217            
218            // Map containing the OUT position of the field
219            // The key is double and is created using the position of the field and 
220            // location of the class in the message (using section)
221            Map<Integer, String> positions = new TreeMap<Integer, String>();
222    
223            // Check if separator exists
224            ObjectHelper.notNull(this.separator, "The separator has not been instantiated or property not defined in the @CsvRecord annotation");
225            
226            char separator = Converter.getCharDelimitor(this.getSeparator());
227    
228            if (LOG.isDebugEnabled()) {
229                LOG.debug("Separator converted : '0x" + Integer.toHexString(separator) + "', from : "
230                        + this.getSeparator());
231            }
232    
233            while (it.hasNext()) {
234    
235                DataField dataField = dataFieldsSorted.get(it.next());
236    
237                // Retrieve the field
238                Field field = annotedFields.get(dataField.pos());
239                // Change accessibility to allow to read protected/private fields
240                field.setAccessible(true);
241    
242                // Retrieve the format, pattern and precision associated to the type
243                Class type = field.getType();
244                String pattern = dataField.pattern();
245                int precision = dataField.precision();
246                
247                // Create format
248                Format format = FormatFactory.getFormat(type, pattern, precision);
249                
250                // Get field from model
251                Object modelField = model.get(field.getDeclaringClass().getName());
252                
253                if (modelField != null) {
254    
255                    if (this.isMessageOrdered()) {
256    
257                        // Generate a key using the number of the section
258                        // and the position of the field
259                        Integer key1 = sections.get(modelField.getClass().getName());
260                        Integer key2 = dataField.position();
261                        Integer keyGenerated = generateKey(key1, key2);
262                        
263                        if (LOG.isDebugEnabled()) {
264                            LOG.debug("Key generated : " + String.valueOf(keyGenerated) + ", for section : " + key1);
265                        }                    
266                        
267                        // Get field value
268                        Object value = field.get(modelField);
269                        
270                        // Add value to the list if not null
271                        if (value != null) {
272    
273                            // Format field value
274                            String valueFormated = format.format(value);
275    
276                            // Add the content to the TreeMap according to the
277                            // position defined
278                            positions.put(keyGenerated, valueFormated);
279    
280                            if (LOG.isDebugEnabled()) {
281                                LOG.debug("Positions size : " + positions.size());
282                            }
283                        }
284                    } else {
285                        // Get field value
286                        Object value = field.get(modelField);
287    
288                        // Add value to the list if not null
289                        if (value != null) {
290    
291                            // Format field value
292                            String valueFormated = format.format(value);
293                            builder.append(valueFormated);
294                        }
295    
296                        if (it.hasNext()) {
297                            builder.append(separator);
298                        }
299                    }
300                }
301            }
302            
303            // Iterate through the list to generate
304            // the message according to the order/position
305            if (this.isMessageOrdered()) {
306    
307                Iterator<Integer> posit = positions.keySet().iterator();
308                
309                while (posit.hasNext()) {
310                    String value = positions.get(posit.next());
311                    
312                    if (LOG.isDebugEnabled()) {
313                        LOG.debug("Value added at the position (" + posit + ") : " + value + separator);
314                    }
315                    
316                    builder.append(value);
317                    if (it.hasNext()) {
318                        builder.append(separator);
319                    }
320                }
321            }
322            
323            return builder.toString();
324        }
325    
326        /**
327         * Find the separator used to delimit the CSV fields
328         */
329        public String getSeparator() {
330            return separator;
331        }
332    
333        /**
334         * Find the separator used to delimit the CSV fields
335         */
336        public boolean getSkipFirstLine() {
337            return skipFirstLine;
338        }
339    
340        /**
341         * Flag indicating if the message must be ordered
342         * 
343         * @return boolean
344         */
345        public boolean isMessageOrdered() {
346            return messageOrdered;
347        }
348    
349        /**
350         * 
351         * Get paramaters defined in @Csvrecord annotation
352         * 
353         */
354        private void initCsvRecordParameters() {
355            if (separator == null) {
356                for (Class<?> cl : models) {
357                    // Get annotation @CsvRecord from the class
358                    CsvRecord record = cl.getAnnotation(CsvRecord.class);
359    
360                    // Get annotation @Section from the class
361                    Section section = cl.getAnnotation(Section.class);
362    
363                    if (record != null) {
364                        if (LOG.isDebugEnabled()) {
365                            LOG.debug("Csv record : " + record.toString());
366                        }
367    
368                        // Get skipFirstLine parameter
369                        skipFirstLine = record.skipFirstLine();
370                        if (LOG.isDebugEnabled()) {
371                            LOG.debug("Skip First Line parameter of the CSV : " + skipFirstLine);
372                        }
373    
374                        // Get Separator parameter
375                        ObjectHelper.notNull(record.separator(),
376                                "No separator has been defined in the @Record annotation !");
377                        separator = record.separator();
378                        if (LOG.isDebugEnabled()) {
379                            LOG.debug("Separator defined for the CSV : " + separator);
380                        }
381    
382                        // Get carriage return parameter
383                        crlf = record.crlf();
384                        if (LOG.isDebugEnabled()) {
385                            LOG.debug("Carriage return defined for the CSV : " + crlf);
386                        }
387                    }
388    
389                    if (section != null) {
390                        // Test if section number is not null
391                        ObjectHelper.notNull(section.number(), "No number has been defined for the section !");
392    
393                        // Get section number and add it to the sections
394                        sections.put(cl.getName(), section.number());
395                    }
396                }
397            }
398        }
399    }