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