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