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 }