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.HashMap;
022 import java.util.Iterator;
023 import java.util.LinkedHashMap;
024 import java.util.LinkedList;
025 import java.util.List;
026 import java.util.Map;
027 import java.util.TreeMap;
028
029 import org.apache.camel.dataformat.bindy.annotation.CsvRecord;
030 import org.apache.camel.dataformat.bindy.annotation.DataField;
031 import org.apache.camel.dataformat.bindy.annotation.Link;
032 import org.apache.camel.dataformat.bindy.annotation.OneToMany;
033 import org.apache.camel.dataformat.bindy.annotation.Section;
034 import org.apache.camel.dataformat.bindy.format.FormatException;
035 import org.apache.camel.dataformat.bindy.util.Converter;
036 import org.apache.camel.spi.PackageScanClassResolver;
037 import org.apache.camel.util.ObjectHelper;
038 import org.apache.commons.logging.Log;
039 import org.apache.commons.logging.LogFactory;
040
041 /**
042 * The BindyCsvFactory is the class who allows to : Generate a model associated
043 * to a CSV record, bind data from a record to the POJOs, export data of POJOs
044 * to a CSV record and format data into String, Date, Double, ... according to
045 * the format/pattern defined
046 */
047 public class BindyCsvFactory extends BindyAbstractFactory implements BindyFactory {
048
049 private static final transient Log LOG = LogFactory.getLog(BindyCsvFactory.class);
050
051 boolean isOneToMany;
052
053 private Map<Integer, DataField> dataFields = new LinkedHashMap<Integer, DataField>();
054 private Map<Integer, Field> annotedFields = new LinkedHashMap<Integer, Field>();
055 private Map<String, Integer> sections = new HashMap<String, Integer>();
056
057 private Map<Integer, List> results;
058
059 private int numberOptionalFields;
060 private int numberMandatoryFields;
061 private int totalFields;
062
063 private String separator;
064 private boolean skipFirstLine;
065 private boolean generateHeaderColumnNames;
066 private boolean messageOrdered;
067
068 public BindyCsvFactory(PackageScanClassResolver resolver, String... packageNames) throws Exception {
069 super(resolver, packageNames);
070
071 // initialize specific parameters of the csv model
072 initCsvModel();
073 }
074
075 /**
076 * method uses to initialize the model representing the classes who will
077 * bind the data. This process will scan for classes according to the
078 * package name provided, check the annotated classes and fields and
079 * retrieve the separator of the CSV record
080 *
081 * @throws Exception
082 */
083 public void initCsvModel() throws Exception {
084
085 // Find annotated Datafields declared in the Model classes
086 initAnnotedFields();
087
088 // initialize Csv parameter(s)
089 // separator and skip first line from @CSVrecord annotation
090 initCsvRecordParameters();
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 public void bind(List<String> tokens, Map<String, Object> model, int line) throws Exception {
147
148 int pos = 1;
149 int counterMandatoryFields = 0;
150
151 for (String data : tokens) {
152
153 // Get DataField from model
154 DataField dataField = dataFields.get(pos);
155 ObjectHelper.notNull(dataField, "No position " + pos + " defined for the field : " + data + ", line : " + line);
156
157 if (dataField.trim()) {
158 data = data.trim();
159 }
160
161 if (dataField.required()) {
162 // Increment counter of mandatory fields
163 ++counterMandatoryFields;
164
165 // Check if content of the field is empty
166 // This is not possible for mandatory fields
167 if (data.equals("")) {
168 throw new IllegalArgumentException("The mandatory field defined at the position " + pos + " is empty for the line : " + line);
169 }
170 }
171
172 // Get Field to be setted
173 Field field = annotedFields.get(pos);
174 field.setAccessible(true);
175
176 if (LOG.isDebugEnabled()) {
177 LOG.debug("Pos : " + pos + ", Data : " + data + ", Field type : " + field.getType());
178 }
179
180 Format<?> format;
181
182 // Get pattern defined for the field
183 String pattern = dataField.pattern();
184
185 // Create format object to format the field
186 format = FormatFactory.getFormat(field.getType(), pattern, dataField.precision());
187
188 // field object to be set
189 Object modelField = model.get(field.getDeclaringClass().getName());
190
191 // format the data received
192 Object value = null;
193
194 if (!data.equals("")) {
195 try {
196 value = format.parse(data);
197 } catch (FormatException ie) {
198 throw new IllegalArgumentException(ie.getMessage() + ", position : " + pos + ", line : " + line, ie);
199 } catch (Exception e) {
200 throw new IllegalArgumentException("Parsing error detected for field defined at the position : " + pos + ", line : " + line, e);
201 }
202 } else {
203 value = getDefaultValueForPrimitive(field.getType());
204 }
205
206 field.set(modelField, value);
207
208 ++pos;
209
210 }
211
212 if (LOG.isDebugEnabled()) {
213 LOG.debug("Counter mandatory fields : " + counterMandatoryFields);
214 }
215
216 if (pos < totalFields) {
217 throw new IllegalArgumentException("Some fields are missing (optional or mandatory), line : " + line);
218 }
219
220 if (counterMandatoryFields < numberMandatoryFields) {
221 throw new IllegalArgumentException("Some mandatory fields are missing, line : " + line);
222 }
223
224 }
225
226 public String unbind(Map<String, Object> model) throws Exception {
227
228 StringBuilder buffer = new StringBuilder();
229 results = new HashMap<Integer, List>();
230
231 // Check if separator exists
232 ObjectHelper.notNull(this.separator, "The separator has not been instantiated or property not defined in the @CsvRecord annotation");
233
234 char separator = Converter.getCharDelimitor(this.getSeparator());
235
236 if (LOG.isDebugEnabled()) {
237 LOG.debug("Separator converted : '0x" + Integer.toHexString(separator) + "', from : " + this.getSeparator());
238 }
239
240 for (Class clazz : models) {
241
242 if (model.containsKey(clazz.getName())) {
243
244 Object obj = model.get(clazz.getName());
245
246 if (LOG.isDebugEnabled()) {
247 LOG.debug("Model object : " + obj + ", class : " + obj.getClass().getName());
248 }
249
250 if (obj != null) {
251
252 // Generate Csv table
253 generateCsvPositionMap(clazz, obj);
254
255 }
256 }
257 }
258
259 // Transpose result
260 List<List> l = new ArrayList<List>();
261
262 if (isOneToMany) {
263
264 l = product(results);
265
266 } else {
267
268 // Convert Map<Integer, List> into List<List>
269 TreeMap<Integer, List> sortValues = new TreeMap<Integer, List>(results);
270 List<String> temp = new ArrayList<String>();
271
272 for (Integer key : sortValues.keySet()) {
273
274 // Get list of values
275 List<String> val = sortValues.get(key);
276
277 // For one to one relation
278 // There is only one item in the list
279 String value = (String)val.get(0);
280
281 // Add the value to the temp array
282 if (value != null) {
283 temp.add(value);
284 } else {
285 temp.add("");
286 }
287 }
288
289 l.add(temp);
290 }
291
292 if (l != null) {
293
294 Iterator it = l.iterator();
295 while (it.hasNext()) {
296
297 List<String> tokens = (ArrayList<String>)it.next();
298 Iterator itx = tokens.iterator();
299
300 while (itx.hasNext()) {
301
302 String res = (String)itx.next();
303
304 if (res != null) {
305 buffer.append(res);
306 } else {
307 buffer.append("");
308 }
309
310 if (itx.hasNext()) {
311 buffer.append(separator);
312 }
313
314 }
315
316 if (it.hasNext()) {
317 buffer.append(Converter.getStringCarriageReturn(getCarriageReturn()));
318 }
319
320 }
321
322 }
323
324 return buffer.toString();
325
326 }
327
328 private List<List> product(Map<Integer, List> values) {
329
330 TreeMap<Integer, List> sortValues = new TreeMap<Integer, List>(values);
331
332 List<List> product = new ArrayList<List>();
333 Map<Integer, Integer> index = new HashMap<Integer, Integer>();
334
335 boolean cont = true;
336 int idx = 0;
337 int idxSize;
338
339 do {
340
341 idxSize = 0;
342 List v = new ArrayList();
343
344 for (int ii = 1; ii <= sortValues.lastKey(); ii++) {
345
346 List l = values.get(ii);
347
348 if (l == null) {
349 v.add("");
350 ++idxSize;
351 continue;
352 }
353
354 if (l.size() >= idx + 1) {
355 v.add(l.get(idx));
356 index.put(ii, idx);
357 if (LOG.isDebugEnabled()) {
358 LOG.debug("Value : " + l.get(idx) + ", pos : " + ii + ", at :" + idx);
359 }
360
361 } else {
362 v.add(l.get(0));
363 index.put(ii, 0);
364 ++idxSize;
365 if (LOG.isDebugEnabled()) {
366 LOG.debug("Value : " + l.get(0) + ", pos : " + ii + ", at index : " + 0);
367 }
368 }
369
370 }
371
372 if (idxSize != sortValues.lastKey()) {
373 product.add(v);
374 }
375 ++idx;
376
377 } while (idxSize != sortValues.lastKey());
378
379 return product;
380 }
381
382 /**
383 *
384 * Generate a table containing the data formated and sorted with their position/offset
385 * If the model is Ordered than a key is created combining the annotation @Section and Position of the field
386 * If a relation @OneToMany is defined, than we iterate recursivelu through this function
387 * The result is placed in the Map<Integer, List> results
388 *
389 * @param clazz
390 * @param obj
391 * @throws Exception
392 */
393 private void generateCsvPositionMap(Class clazz, Object obj) throws Exception {
394
395 String result = "";
396
397 for (Field field : clazz.getDeclaredFields()) {
398
399 field.setAccessible(true);
400
401 DataField datafield = field.getAnnotation(DataField.class);
402
403 if (datafield != null) {
404
405 if (obj != null) {
406
407 // Retrieve the format, pattern and precision associated to
408 // the type
409 Class type = field.getType();
410 String pattern = datafield.pattern();
411 int precision = datafield.precision();
412
413 // Create format
414 Format format = FormatFactory.getFormat(type, pattern, precision);
415
416 // Get field value
417 Object value = field.get(obj);
418
419 result = formatString(format, value);
420
421 if (LOG.isDebugEnabled()) {
422 LOG.debug("Value to be formatted : " + value + ", position : " + datafield.pos() + ", and its formated value : " + result);
423 }
424
425 } else {
426 result = "";
427 }
428
429 Integer key;
430
431 if (isMessageOrdered()) {
432
433 // Generate a key using the number of the section
434 // and the position of the field
435 Integer key1 = sections.get(obj.getClass().getName());
436 Integer key2 = datafield.position();
437 Integer keyGenerated = generateKey(key1, key2);
438
439 if (LOG.isDebugEnabled()) {
440 LOG.debug("Key generated : " + String.valueOf(keyGenerated) + ", for section : " + key1);
441 }
442
443 key = keyGenerated;
444
445 } else {
446
447 key = datafield.pos();
448 }
449
450 if (!results.containsKey(key)) {
451
452 List list = new LinkedList();
453 list.add(result);
454 results.put(key, list);
455
456 } else {
457
458 List list = (LinkedList)results.get(key);
459 list.add(result);
460 }
461
462 }
463
464 OneToMany oneToMany = field.getAnnotation(OneToMany.class);
465 if (oneToMany != null) {
466
467 // Set global variable
468 // Will be used during generation of CSV
469 isOneToMany = true;
470
471 ArrayList list = (ArrayList)field.get(obj);
472
473 if (list != null) {
474
475 Iterator it = list.iterator();
476
477 while (it.hasNext()) {
478
479 Object target = it.next();
480 generateCsvPositionMap(target.getClass(), target);
481
482 }
483
484 } else {
485
486 // Call this function to add empty value
487 // in the table
488 generateCsvPositionMap(field.getClass(), null);
489 }
490
491 }
492 }
493
494 }
495
496 /**
497 * Generate for the first line the headers of the columns
498 *
499 * @return the headers columns
500 */
501 public String generateHeader() {
502
503 Map<Integer, DataField> dataFieldsSorted = new TreeMap<Integer, DataField>(dataFields);
504 Iterator<Integer> it = dataFieldsSorted.keySet().iterator();
505
506 StringBuilder builderHeader = new StringBuilder();
507
508 while (it.hasNext()) {
509
510 DataField dataField = dataFieldsSorted.get(it.next());
511
512 // Retrieve the field
513 Field field = annotedFields.get(dataField.pos());
514 // Change accessibility to allow to read protected/private fields
515 field.setAccessible(true);
516
517 // Get dataField
518 if (!dataField.columnName().equals("")) {
519 builderHeader.append(dataField.columnName());
520 } else {
521 builderHeader.append(field.getName());
522 }
523
524 if (it.hasNext()) {
525 builderHeader.append(separator);
526 }
527
528 }
529
530 return builderHeader.toString();
531 }
532
533 /**
534 * Get parameters defined in @Csvrecord annotation
535 */
536 private void initCsvRecordParameters() {
537 if (separator == null) {
538 for (Class<?> cl : models) {
539
540 // Get annotation @CsvRecord from the class
541 CsvRecord record = cl.getAnnotation(CsvRecord.class);
542
543 // Get annotation @Section from the class
544 Section section = cl.getAnnotation(Section.class);
545
546 if (record != null) {
547 if (LOG.isDebugEnabled()) {
548 LOG.debug("Csv record : " + record.toString());
549 }
550
551 // Get skipFirstLine parameter
552 skipFirstLine = record.skipFirstLine();
553 if (LOG.isDebugEnabled()) {
554 LOG.debug("Skip First Line parameter of the CSV : " + skipFirstLine);
555 }
556
557 // Get generateHeaderColumnNames parameter
558 generateHeaderColumnNames = record.generateHeaderColumns();
559 if (LOG.isDebugEnabled()) {
560 LOG.debug("Generate header column names parameter of the CSV : " + generateHeaderColumnNames);
561 }
562
563 // Get Separator parameter
564 ObjectHelper.notNull(record.separator(), "No separator has been defined in the @Record annotation !");
565 separator = record.separator();
566 if (LOG.isDebugEnabled()) {
567 LOG.debug("Separator defined for the CSV : " + separator);
568 }
569
570 // Get carriage return parameter
571 crlf = record.crlf();
572 if (LOG.isDebugEnabled()) {
573 LOG.debug("Carriage return defined for the CSV : " + crlf);
574 }
575
576 // Get isOrdered parameter
577 messageOrdered = record.isOrdered();
578 if (LOG.isDebugEnabled()) {
579 LOG.debug("Must CSV record be ordered ? " + messageOrdered);
580 }
581
582 }
583
584 if (section != null) {
585 // Test if section number is not null
586 ObjectHelper.notNull(section.number(), "No number has been defined for the section !");
587
588 // Get section number and add it to the sections
589 sections.put(cl.getName(), section.number());
590 }
591 }
592 }
593 }
594
595 /**
596 * Find the separator used to delimit the CSV fields
597 */
598 public String getSeparator() {
599 return separator;
600 }
601
602 /**
603 * Flag indicating if the first line of the CSV must be skipped
604 */
605 public boolean getGenerateHeaderColumnNames() {
606 return generateHeaderColumnNames;
607 }
608
609 /**
610 * Find the separator used to delimit the CSV fields
611 */
612 public boolean getSkipFirstLine() {
613 return skipFirstLine;
614 }
615
616 /**
617 * Flag indicating if the message must be ordered
618 *
619 * @return boolean
620 */
621 public boolean isMessageOrdered() {
622 return messageOrdered;
623 }
624 }