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> annotatedFields = new LinkedHashMap<Integer, Field>();
056 private Map<String, Integer> sections = new HashMap<String, Integer>();
057
058 private int numberOptionalFields;
059 private int numberMandatoryFields;
060 private int totalFields;
061
062 private String separator;
063 private boolean skipFirstLine;
064 private boolean generateHeaderColumnNames;
065 private boolean messageOrdered;
066 private String quote;
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 initAnnotatedFields();
087
088 // initialize Csv parameter(s)
089 // separator and skip first line from @CSVrecord annotation
090 initCsvRecordParameters();
091 }
092
093 public void initAnnotatedFields() {
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: {}, position: {}, Field: {}",
108 new Object[]{cl.getName(), dataField.pos(), dataField});
109 }
110
111 if (dataField.required()) {
112 ++numberMandatoryFields;
113 } else {
114 ++numberOptionalFields;
115 }
116
117 dataFields.put(dataField.pos(), dataField);
118 annotatedFields.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: {}, Field: {}", cl.getName(), field);
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 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 = annotatedFields.get(pos);
174 field.setAccessible(true);
175
176 if (LOG.isDebugEnabled()) {
177 LOG.debug("Pos: {}, Data: {}, Field type: {}", new Object[]{pos, data, 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, getLocale(), 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 LOG.debug("Counter mandatory fields: {}", counterMandatoryFields);
213
214 if (pos < totalFields) {
215 throw new IllegalArgumentException("Some fields are missing (optional or mandatory), line: " + line);
216 }
217
218 if (counterMandatoryFields < numberMandatoryFields) {
219 throw new IllegalArgumentException("Some mandatory fields are missing, line: " + line);
220 }
221
222 }
223
224 @SuppressWarnings("unchecked")
225 public String unbind(Map<String, Object> model) throws Exception {
226
227 StringBuilder buffer = new StringBuilder();
228 Map<Integer, List> results = new HashMap<Integer, List>();
229
230 // Check if separator exists
231 ObjectHelper.notNull(this.separator, "The separator has not been instantiated or property not defined in the @CsvRecord annotation");
232
233 char separator = Converter.getCharDelimitor(this.getSeparator());
234
235 if (LOG.isDebugEnabled()) {
236 LOG.debug("Separator converted: '0x{}', from: {}", Integer.toHexString(separator), this.getSeparator());
237 }
238
239 for (Class clazz : models) {
240 if (model.containsKey(clazz.getName())) {
241
242 Object obj = model.get(clazz.getName());
243 if (LOG.isDebugEnabled()) {
244 LOG.debug("Model object: {}, class: {}", obj, obj.getClass().getName());
245 }
246 if (obj != null) {
247
248 // Generate Csv table
249 generateCsvPositionMap(clazz, obj, results);
250
251 }
252 }
253 }
254
255 // Transpose result
256 List<List> l = new ArrayList<List>();
257 if (isOneToMany) {
258 l = product(results);
259 } else {
260 // Convert Map<Integer, List> into List<List>
261 TreeMap<Integer, List> sortValues = new TreeMap<Integer, List>(results);
262 List<String> temp = new ArrayList<String>();
263
264 for (Entry<Integer, List> entry : sortValues.entrySet()) {
265 // Get list of values
266 List<String> val = (List<String>)entry.getValue();
267
268 // For one to one relation
269 // There is only one item in the list
270 String value = val.get(0);
271
272 // Add the value to the temp array
273 if (value != null) {
274 temp.add(value);
275 } else {
276 temp.add("");
277 }
278 }
279
280 l.add(temp);
281 }
282
283 if (l != null) {
284 Iterator it = l.iterator();
285 while (it.hasNext()) {
286 List<String> tokens = (ArrayList<String>)it.next();
287 Iterator itx = tokens.iterator();
288
289 while (itx.hasNext()) {
290 String res = (String)itx.next();
291 if (res != null) {
292 // the field may be enclosed in quotes if a quote was configured
293 if (quote != null) {
294 buffer.append(quote);
295 }
296 buffer.append(res);
297 if (quote != null) {
298 buffer.append(quote);
299 }
300 }
301
302 if (itx.hasNext()) {
303 buffer.append(separator);
304 }
305 }
306
307 if (it.hasNext()) {
308 buffer.append(Converter.getStringCarriageReturn(getCarriageReturn()));
309 }
310 }
311 }
312
313 return buffer.toString();
314 }
315
316 private List<List> product(Map<Integer, List> values) {
317 TreeMap<Integer, List> sortValues = new TreeMap<Integer, List>(values);
318
319 List<List> product = new ArrayList<List>();
320 Map<Integer, Integer> index = new HashMap<Integer, Integer>();
321
322 int idx = 0;
323 int idxSize = 0;
324 do {
325 idxSize = 0;
326 List v = new ArrayList();
327
328 for (int ii = 1; ii <= sortValues.lastKey(); ii++) {
329 List l = values.get(ii);
330 if (l == null) {
331 v.add("");
332 ++idxSize;
333 continue;
334 }
335
336 if (l.size() >= idx + 1) {
337 v.add(l.get(idx));
338 index.put(ii, idx);
339 if (LOG.isDebugEnabled()) {
340 LOG.debug("Value: {}, pos: {}, at: {}", new Object[]{l.get(idx), ii, idx});
341 }
342 } else {
343 v.add(l.get(0));
344 index.put(ii, 0);
345 ++idxSize;
346 if (LOG.isDebugEnabled()) {
347 LOG.debug("Value: {}, pos: {}, at index: {}", new Object[]{l.get(0), ii, 0});
348 }
349 }
350 }
351
352 if (idxSize != sortValues.lastKey()) {
353 product.add(v);
354 }
355 ++idx;
356
357 } while (idxSize != sortValues.lastKey());
358
359 return product;
360 }
361
362 /**
363 *
364 * Generate a table containing the data formatted and sorted with their position/offset
365 * If the model is Ordered than a key is created combining the annotation @Section and Position of the field
366 * If a relation @OneToMany is defined, than we iterate recursively through this function
367 * The result is placed in the Map<Integer, List> results
368 */
369 private void generateCsvPositionMap(Class clazz, Object obj, Map<Integer, List> results) throws Exception {
370
371 String result = "";
372
373 for (Field field : clazz.getDeclaredFields()) {
374
375 field.setAccessible(true);
376
377 DataField datafield = field.getAnnotation(DataField.class);
378
379 if (datafield != null) {
380
381 if (obj != null) {
382
383 // Retrieve the format, pattern and precision associated to
384 // the type
385 Class type = field.getType();
386 String pattern = datafield.pattern();
387 int precision = datafield.precision();
388
389 // Create format
390 Format format = FormatFactory.getFormat(type, pattern, getLocale(), precision);
391
392 // Get field value
393 Object value = field.get(obj);
394
395 result = formatString(format, value);
396
397 if (datafield.trim()) {
398 result = result.trim();
399 }
400
401 if (datafield.clip() && result.length() > datafield.length()) {
402 result = result.substring(0, datafield.length());
403 }
404
405 if (LOG.isDebugEnabled()) {
406 LOG.debug("Value to be formatted: {}, position: {}, and its formatted value: {}", new Object[]{value, datafield.pos(), result});
407 }
408
409 } else {
410 result = "";
411 }
412
413 Integer key;
414
415 if (isMessageOrdered() && obj != null) {
416
417 // Generate a key using the number of the section
418 // and the position of the field
419 Integer key1 = sections.get(obj.getClass().getName());
420 Integer key2 = datafield.position();
421 Integer keyGenerated = generateKey(key1, key2);
422
423 if (LOG.isDebugEnabled()) {
424 LOG.debug("Key generated: {}, for section: {}", String.valueOf(keyGenerated), key1);
425 }
426
427 key = keyGenerated;
428
429 } else {
430 key = datafield.pos();
431 }
432
433 if (!results.containsKey(key)) {
434 List list = new LinkedList();
435 list.add(result);
436 results.put(key, list);
437 } else {
438 List list = (LinkedList)results.get(key);
439 list.add(result);
440 }
441
442 }
443
444 OneToMany oneToMany = field.getAnnotation(OneToMany.class);
445 if (oneToMany != null) {
446
447 // Set global variable
448 // Will be used during generation of CSV
449 isOneToMany = true;
450
451 ArrayList list = (ArrayList)field.get(obj);
452 if (list != null) {
453
454 Iterator it = list.iterator();
455 while (it.hasNext()) {
456 Object target = it.next();
457 generateCsvPositionMap(target.getClass(), target, results);
458 }
459
460 } else {
461
462 // Call this function to add empty value
463 // in the table
464 generateCsvPositionMap(field.getClass(), null, results);
465 }
466
467 }
468 }
469
470 }
471
472 /**
473 * Generate for the first line the headers of the columns
474 *
475 * @return the headers columns
476 */
477 public String generateHeader() {
478
479 Map<Integer, DataField> dataFieldsSorted = new TreeMap<Integer, DataField>(dataFields);
480 Iterator<Integer> it = dataFieldsSorted.keySet().iterator();
481
482 StringBuilder builderHeader = new StringBuilder();
483
484 while (it.hasNext()) {
485
486 DataField dataField = dataFieldsSorted.get(it.next());
487
488 // Retrieve the field
489 Field field = annotatedFields.get(dataField.pos());
490 // Change accessibility to allow to read protected/private fields
491 field.setAccessible(true);
492
493 // Get dataField
494 if (!dataField.columnName().equals("")) {
495 builderHeader.append(dataField.columnName());
496 } else {
497 builderHeader.append(field.getName());
498 }
499
500 if (it.hasNext()) {
501 builderHeader.append(separator);
502 }
503
504 }
505
506 return builderHeader.toString();
507 }
508
509 /**
510 * Get parameters defined in @Csvrecord annotation
511 */
512 private void initCsvRecordParameters() {
513 if (separator == null) {
514 for (Class<?> cl : models) {
515
516 // Get annotation @CsvRecord from the class
517 CsvRecord record = cl.getAnnotation(CsvRecord.class);
518
519 // Get annotation @Section from the class
520 Section section = cl.getAnnotation(Section.class);
521
522 if (record != null) {
523 LOG.debug("Csv record: {}", record);
524
525 // Get skipFirstLine parameter
526 skipFirstLine = record.skipFirstLine();
527 LOG.debug("Skip First Line parameter of the CSV: {}" + skipFirstLine);
528
529 // Get generateHeaderColumnNames parameter
530 generateHeaderColumnNames = record.generateHeaderColumns();
531 LOG.debug("Generate header column names parameter of the CSV: {}", generateHeaderColumnNames);
532
533 // Get Separator parameter
534 ObjectHelper.notNull(record.separator(), "No separator has been defined in the @Record annotation");
535 separator = record.separator();
536 LOG.debug("Separator defined for the CSV: {}", separator);
537
538 // Get carriage return parameter
539 crlf = record.crlf();
540 LOG.debug("Carriage return defined for the CSV: {}", crlf);
541
542 // Get isOrdered parameter
543 messageOrdered = record.isOrdered();
544 LOG.debug("Must CSV record be ordered: {}", messageOrdered);
545
546 if (ObjectHelper.isNotEmpty(record.quote())) {
547 quote = record.quote();
548 LOG.debug("Quoting columns with: {}", quote);
549 }
550 }
551
552 if (section != null) {
553 // Test if section number is not null
554 ObjectHelper.notNull(section.number(), "No number has been defined for the section");
555
556 // Get section number and add it to the sections
557 sections.put(cl.getName(), section.number());
558 }
559 }
560 }
561 }
562
563 /**
564 * Find the separator used to delimit the CSV fields
565 */
566 public String getSeparator() {
567 return separator;
568 }
569
570 /**
571 * Flag indicating if the first line of the CSV must be skipped
572 */
573 public boolean getGenerateHeaderColumnNames() {
574 return generateHeaderColumnNames;
575 }
576
577 /**
578 * Find the separator used to delimit the CSV fields
579 */
580 public boolean getSkipFirstLine() {
581 return skipFirstLine;
582 }
583
584 /**
585 * Flag indicating if the message must be ordered
586 *
587 * @return boolean
588 */
589 public boolean isMessageOrdered() {
590 return messageOrdered;
591 }
592 }