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