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                    // Get field value
255                    Object value = field.get(modelField);
256                    String strValue = null;
257    
258                    if (this.isMessageOrdered()) {
259    
260                        // Generate a key using the number of the section
261                        // and the position of the field
262                        Integer key1 = sections.get(modelField.getClass().getName());
263                        Integer key2 = dataField.position();
264                        Integer keyGenerated = generateKey(key1, key2);
265                        
266                        if (LOG.isDebugEnabled()) {
267                            LOG.debug("Key generated : " + String.valueOf(keyGenerated) + ", for section : " + key1);
268                        }                    
269                        
270                        // Get field value
271                        //Object value = field.get(modelField);
272                        
273                        if (value != null) {
274                            // Format field value
275                            strValue = format.format(value);
276                        } 
277                        
278                        // Add the content to the TreeMap according to the
279                        // position defined
280                        positions.put(keyGenerated, strValue);
281    
282                        if (LOG.isDebugEnabled()) {
283                            LOG.debug("Positions size : " + positions.size());
284                        }
285                            
286                    } else {
287                        // Get field value
288                        //Object value = field.get(modelField);
289                        //String strValue = null;
290    
291                        // Add value to the list if not null
292                        if (value != null) {
293    
294                            // Format field value
295                            strValue = format.format(value);
296                            
297                        }
298                        
299                        if (LOG.isDebugEnabled()) {
300                            LOG.debug("Data : " + value + ", value : " + strValue);
301                        }
302                        
303                        builder.append(strValue);
304    
305                        if (it.hasNext()) {
306                            builder.append(separator);
307                        }
308                    }
309                }
310            }
311            
312            // Iterate through the list to generate
313            // the message according to the order/position
314            if (this.isMessageOrdered()) {
315    
316                Iterator<Integer> posit = positions.keySet().iterator();
317                
318                while (posit.hasNext()) {
319                    String value = positions.get(posit.next());
320                    
321                    if (LOG.isDebugEnabled()) {
322                        LOG.debug("Value added at the position (" + posit + ") : " + value + separator);
323                    }
324                    
325                    builder.append(value);
326                    if (it.hasNext()) {
327                        builder.append(separator);
328                    }
329                }
330            }
331            
332            return builder.toString();
333        }
334    
335        /**
336         * Find the separator used to delimit the CSV fields
337         */
338        public String getSeparator() {
339            return separator;
340        }
341    
342        /**
343         * Find the separator used to delimit the CSV fields
344         */
345        public boolean getSkipFirstLine() {
346            return skipFirstLine;
347        }
348    
349        /**
350         * Flag indicating if the message must be ordered
351         * 
352         * @return boolean
353         */
354        public boolean isMessageOrdered() {
355            return messageOrdered;
356        }
357    
358        /**
359         * 
360         * Get paramaters defined in @Csvrecord annotation
361         * 
362         */
363        private void initCsvRecordParameters() {
364            if (separator == null) {
365                for (Class<?> cl : models) {
366                    // Get annotation @CsvRecord from the class
367                    CsvRecord record = cl.getAnnotation(CsvRecord.class);
368    
369                    // Get annotation @Section from the class
370                    Section section = cl.getAnnotation(Section.class);
371    
372                    if (record != null) {
373                        if (LOG.isDebugEnabled()) {
374                            LOG.debug("Csv record : " + record.toString());
375                        }
376    
377                        // Get skipFirstLine parameter
378                        skipFirstLine = record.skipFirstLine();
379                        if (LOG.isDebugEnabled()) {
380                            LOG.debug("Skip First Line parameter of the CSV : " + skipFirstLine);
381                        }
382    
383                        // Get Separator parameter
384                        ObjectHelper.notNull(record.separator(),
385                                "No separator has been defined in the @Record annotation !");
386                        separator = record.separator();
387                        if (LOG.isDebugEnabled()) {
388                            LOG.debug("Separator defined for the CSV : " + separator);
389                        }
390    
391                        // Get carriage return parameter
392                        crlf = record.crlf();
393                        if (LOG.isDebugEnabled()) {
394                            LOG.debug("Carriage return defined for the CSV : " + crlf);
395                        }
396                    }
397    
398                    if (section != null) {
399                        // Test if section number is not null
400                        ObjectHelper.notNull(section.number(), "No number has been defined for the section !");
401    
402                        // Get section number and add it to the sections
403                        sections.put(cl.getName(), section.number());
404                    }
405                }
406            }
407        }
408    }