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