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