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