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.Collection;
022    import java.util.HashMap;
023    import java.util.Iterator;
024    import java.util.LinkedHashMap;
025    import java.util.LinkedList;
026    import java.util.List;
027    import java.util.Map;
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.FixedLengthRecord;
033    import org.apache.camel.dataformat.bindy.annotation.Link;
034    import org.apache.camel.dataformat.bindy.annotation.OneToMany;
035    import org.apache.camel.dataformat.bindy.annotation.Section;
036    import org.apache.camel.dataformat.bindy.format.FormatException;
037    import org.apache.camel.dataformat.bindy.util.Converter;
038    import org.apache.camel.spi.PackageScanClassResolver;
039    import org.apache.camel.util.ObjectHelper;
040    import org.apache.commons.logging.Log;
041    import org.apache.commons.logging.LogFactory;
042    
043    /**
044     * The BindyCsvFactory is the class who allows to : Generate a model associated
045     * to a fixed length record, bind data from a record to the POJOs, export data of POJOs
046     * to a fixed length record and format data into String, Date, Double, ... according to
047     * the format/pattern defined
048     */
049    public class BindyFixedLengthFactory extends BindyAbstractFactory implements BindyFactory {
050    
051        private static final transient Log LOG = LogFactory.getLog(BindyFixedLengthFactory.class);
052    
053        boolean isOneToMany;
054    
055        private Map<Integer, DataField> dataFields = new LinkedHashMap<Integer, DataField>();
056        private Map<Integer, Field> annotedFields = new LinkedHashMap<Integer, Field>();
057    
058        private Map<Integer, List> results;
059    
060        private int numberOptionalFields;
061        private int numberMandatoryFields;
062        private int totalFields;
063    
064        private boolean hasHeader;
065        private boolean hasFooter;
066        private char paddingChar;
067        private int recordLength;
068    
069        public BindyFixedLengthFactory(PackageScanClassResolver resolver, String... packageNames) throws Exception {
070            super(resolver, packageNames);
071    
072            // initialize specific parameters of the fixed length model
073            initFixedLengthModel();
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
080         * 
081         * @throws Exception
082         */
083        public void initFixedLengthModel() throws Exception {
084    
085            // Find annotated fields declared in the Model classes
086            initAnnotedFields();
087    
088            // initialize Fixed length parameter(s)
089            // from @FixedLengthrecord annotation
090            initFixedLengthRecordParameters();
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        // Will not be used in the case of a Fixed Length record
147        // as we provide the content of the record and 
148        // we don't split it as this is the case for a CSV record
149        @Override
150        public void bind(List<String> data, Map<String, Object> model, int line) throws Exception {
151            // TODO Auto-generated method stub
152    
153        }
154    
155        public void bind(String record, Map<String, Object> model, int line) throws Exception {
156    
157            int pos = 1;
158            int counterMandatoryFields = 0;
159            DataField dataField;
160            StringBuilder result = new StringBuilder();
161            String token;
162            int offset;
163            int length;
164            Field field;
165            String pattern;
166    
167            // Iterate through the list of positions
168            // defined in the @DataFieldf
169            // and grab the data from the line
170            Collection c = dataFields.values();
171            Iterator itr = c.iterator();
172    
173            while (itr.hasNext()) {
174                dataField = (DataField)itr.next();
175                offset = dataField.pos();
176                length = dataField.length();
177    
178                ObjectHelper.notNull(offset, "Position/offset is not defined for  the  field "
179                                             + dataField.toString());
180                ObjectHelper.notNull(offset, "Length is not defined for the  field " + dataField.toString());
181    
182                if (offset - 1 <= -1) {
183                    throw new IllegalArgumentException("Offset / Position of the field " + dataField.toString()
184                                                       + " cannot be negative !");
185                }
186    
187                token = record.substring(offset - 1, offset + length - 1);
188    
189                // Check mandatory field
190                if (dataField.required()) {
191    
192                    // Increment counter of mandatory fields
193                    ++counterMandatoryFields;
194    
195                    // Check if content of the field is empty
196                    // This is not possible for mandatory fields
197                    if (token.equals("")) {
198                        throw new IllegalArgumentException("The mandatory field defined at the position " + pos
199                                                           + " is empty for the line : " + line);
200                    }
201                }
202                
203                // Get Field to be setted
204                field = annotedFields.get(offset);
205                field.setAccessible(true);
206    
207                if (LOG.isDebugEnabled()) {
208                    LOG.debug("Pos/Offset : " + offset + ", Data : " + token + ", Field type : " + field.getType());
209                }
210                
211                Format<?> format;
212    
213                // Get pattern defined for the field
214                pattern = dataField.pattern();
215    
216                // Create format object to format the field
217                format = FormatFactory.getFormat(field.getType(), pattern, dataField.precision());
218    
219                // field object to be set
220                Object modelField = model.get(field.getDeclaringClass().getName());
221    
222                // format the data received
223                Object value = null;
224    
225                if (!token.equals("")) {
226                    try {
227                        value = format.parse(token);
228                    } catch (FormatException ie) {
229                        throw new IllegalArgumentException(ie.getMessage() + ", position : " + offset + ", line : " + line, ie);
230                    } catch (Exception e) {
231                        throw new IllegalArgumentException("Parsing error detected for field defined at the position/offset : " + offset + ", line : " + line, e);
232                    }
233                } else {
234                    value = getDefaultValueForPrimitive(field.getType());
235                }
236    
237                field.set(modelField, value);
238    
239                ++pos;
240            
241            }
242    
243            if (LOG.isDebugEnabled()) {
244                LOG.debug("Counter mandatory fields : " + counterMandatoryFields);
245            }
246    
247            if (pos < totalFields) {
248                throw new IllegalArgumentException("Some fields are missing (optional or mandatory), line : " + line);
249            }
250    
251            if (counterMandatoryFields < numberMandatoryFields) {
252                throw new IllegalArgumentException("Some mandatory fields are missing, line : " + line);
253            }  
254            
255        }
256    
257        public String unbind(Map<String, Object> model) throws Exception {
258    
259            StringBuilder buffer = new StringBuilder();
260            results = new HashMap<Integer, List>();
261    
262            for (Class clazz : models) {
263    
264                if (model.containsKey(clazz.getName())) {
265    
266                    Object obj = model.get(clazz.getName());
267    
268                    if (LOG.isDebugEnabled()) {
269                        LOG.debug("Model object : " + obj + ", class : " + obj.getClass().getName());
270                    }
271    
272                    if (obj != null) {
273    
274                        // Generate Fixed Length table
275                        // containing the positions of the fields
276                        generateFixedLengthPositionMap(clazz, obj);
277    
278                    }
279                }
280            }
281    
282            // Convert Map<Integer, List> into List<List>
283            TreeMap<Integer, List> sortValues = new TreeMap<Integer, List>(results);
284            List<String> temp = new ArrayList<String>();
285    
286            for (Integer key : sortValues.keySet()) {
287    
288                // Get list of values
289                List<String> val = sortValues.get(key);
290                String value = (String)val.get(0);
291                
292                buffer.append(value);
293    
294            }
295            
296            return buffer.toString();
297    
298        }
299    
300        /**
301         * 
302         * Generate a table containing the data formated and sorted with their position/offset
303         * The result is placed in the Map<Integer, List> results
304         * 
305         * @param clazz
306         * @param obj
307         * @throws Exception
308         */
309        private void generateFixedLengthPositionMap(Class clazz, Object obj) throws Exception {
310    
311            String result = "";
312    
313            for (Field field : clazz.getDeclaredFields()) {
314    
315                field.setAccessible(true);
316    
317                DataField datafield = field.getAnnotation(DataField.class);
318    
319                if (datafield != null) {
320    
321                    if (obj != null) {
322    
323                        // Retrieve the format, pattern and precision associated to
324                        // the type
325                        Class type = field.getType();
326                        String pattern = datafield.pattern();
327                        int precision = datafield.precision();
328    
329                        // Create format
330                        Format format = FormatFactory.getFormat(type, pattern, precision);
331    
332                        // Get field value
333                        Object value = field.get(obj);
334    
335                        result = formatString(format, value);
336                        
337                        // Get length of the field, alignment (LEFT or RIGHT), pad
338                        int fieldLength = datafield.length();
339                        String align = datafield.align();
340                        char paddCharField = datafield.paddingChar();
341                        char paddChar;
342                        
343                        if (fieldLength > 0) {
344                           
345                            StringBuilder temp = new StringBuilder();
346    
347                            // Check if we must padd
348                            if (result.length() < fieldLength) {
349    
350                                // No padding defined for the field
351                                if (paddCharField == 0) {
352                                    // We use the padding defined for the Record
353                                    paddChar = paddingChar;
354                                } else {
355                                    paddChar = paddCharField;
356                                }
357    
358                                if (align.contains("R")) {
359                                    temp.append(generatePaddingChars(paddChar, fieldLength, result.length()));
360                                    temp.append(result);
361                                } else if (align.contains("L")) {
362                                    temp.append(result);
363                                    temp.append(generatePaddingChars(paddChar, fieldLength, result.length()));
364                                } else {
365                                    throw new IllegalArgumentException("Alignement for the "
366                                                                           + field.getName()
367                                                                           + " must be equal to R for RIGHT or L for LEFT !");
368                                }
369    
370                                result = temp.toString();
371                            }
372    
373                        } else {
374                            throw new IllegalArgumentException("Lenght of the field : "
375                                                                   + field.getName()
376                                                                   + " is a mandatory field and cannot be equal to zero or to be negative !");
377                        }
378    
379                        if (LOG.isDebugEnabled()) {
380                            LOG.debug("Value to be formatted : " + value + ", position : " + datafield.pos() + ", and its formated value : " + result);
381                        }
382    
383                    } else {
384                        result = "";
385                    }
386    
387                    Integer key;
388                    key = datafield.pos();
389    
390                    if (!results.containsKey(key)) {
391    
392                        List list = new LinkedList();
393                        list.add(result);
394                        results.put(key, list);
395    
396                    } else {
397    
398                        List list = (LinkedList)results.get(key);
399                        list.add(result);
400                    }
401    
402                }
403    
404            }
405    
406        }
407        
408        private String generatePaddingChars(char pad, int lengthField, int lengthString) {
409    
410            StringBuilder buffer = new StringBuilder();
411            int size = lengthField - lengthString;
412    
413            for (int i = 0; i < size; i++) {
414                buffer.append(Character.toString(pad));
415            }
416            return buffer.toString();
417        }
418    
419        /**
420         * Get parameters defined in @FixedLengthRecord annotation
421         */
422        private void initFixedLengthRecordParameters() {
423    
424            for (Class<?> cl : models) {
425    
426                // Get annotation @FixedLengthRecord from the class
427                FixedLengthRecord record = cl.getAnnotation(FixedLengthRecord.class);
428    
429                if (record != null) {
430                    if (LOG.isDebugEnabled()) {
431                        LOG.debug("Fixed length record : " + record.toString());
432                    }
433    
434                    // Get carriage return parameter
435                    crlf = record.crlf();
436                    if (LOG.isDebugEnabled()) {
437                        LOG.debug("Carriage return defined for the CSV : " + crlf);
438                    }
439    
440                    // Get hasHeader parameter
441                    hasHeader = record.hasHeader();
442                    if (LOG.isDebugEnabled()) {
443                        LOG.debug("Has Header :  " + hasHeader);
444                    }
445    
446                    // Get hasFooter parameter
447                    hasFooter = record.hasFooter();
448                    if (LOG.isDebugEnabled()) {
449                        LOG.debug("Has Footer :  " + hasFooter);
450                    }
451    
452                    // Get padding character
453                    paddingChar = record.paddingChar();
454                    if (LOG.isDebugEnabled()) {
455                        LOG.debug("Padding char :  " + paddingChar);
456                    }
457    
458                    // Get length of the record
459                    recordLength = record.length();
460                    if (LOG.isDebugEnabled()) {
461                        LOG.debug("Length of the record :  " + recordLength);
462                    }
463    
464                }
465            }
466        }
467    
468        /**
469         * Flag indicating if we have a header
470         * 
471         * @return boolean
472         */
473        public boolean hasHeader() {
474            return hasHeader;
475        }
476        
477        /**
478         * Flag indicating if we have a footer
479         * 
480         * @return boolean
481         */
482        public boolean hasFooter() {
483            return hasFooter;
484        }
485        
486        /**
487         * Padding char used to fill the field
488         * 
489         * @return char
490         */
491        public char paddingchar() {
492            return paddingChar;
493        }
494    
495        public int recordLength() {
496            return recordLength;
497        }
498    
499    }