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