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.List;
025 import java.util.Map;
026 import java.util.TreeMap;
027 import java.util.jar.JarException;
028
029 import org.apache.camel.dataformat.bindy.annotation.CsvRecord;
030 import org.apache.camel.dataformat.bindy.annotation.DataField;
031 import org.apache.camel.dataformat.bindy.annotation.Link;
032 import org.apache.camel.dataformat.bindy.annotation.Section;
033 import org.apache.camel.dataformat.bindy.util.Converter;
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 CSV record, bind data from a record to the POJOs, export data of POJOs
042 * to a CSV record and format data into String, Date, Double, ... according to
043 * the format/pattern defined
044 */
045 public class BindyCsvFactory extends BindyAbstractFactory implements BindyFactory {
046
047 private static final transient Log LOG = LogFactory.getLog(BindyCsvFactory.class);
048
049 private Map<Integer, DataField> dataFields = new LinkedHashMap<Integer, DataField>();
050 private Map<Integer, Field> annotedFields = new LinkedHashMap<Integer, Field>();
051 private Map<String, Integer> sections = new HashMap<String, Integer>();
052 private int numberOptionalFields;
053 private int numberMandatoryFields;
054 private int totalFields;
055
056 private String separator;
057 private boolean skipFirstLine;
058 private boolean messageOrdered;
059
060 public BindyCsvFactory(PackageScanClassResolver resolver, String... packageNames) throws Exception {
061 super(resolver, packageNames);
062
063 // initialize specific parameters of the csv model
064 initCsvModel();
065 }
066
067 /**
068 * method uses to initialize the model representing the classes who will
069 * bind the data. This process will scan for classes according to the package
070 * name provided, check the annotated classes and fields and retrieve the
071 * separator of the CSV record
072 *
073 * @throws Exception
074 */
075 public void initCsvModel() throws Exception {
076
077 // Find annotated Datafields declared in the Model classes
078 initAnnotedFields();
079
080 // initialize Csv parameter(s)
081 // separator and skip first line from @CSVrecord annotation
082 initCsvRecordParameters();
083 }
084
085 public void initAnnotedFields() {
086
087 for (Class<?> cl : models) {
088
089 List<Field> linkFields = new ArrayList<Field>();
090
091 if (LOG.isDebugEnabled()) {
092 LOG.debug("Class retrieved : " + cl.getName());
093 }
094
095 for (Field field : cl.getDeclaredFields()) {
096 DataField dataField = field.getAnnotation(DataField.class);
097 if (dataField != null) {
098 if (LOG.isDebugEnabled()) {
099 LOG.debug("Position defined in the class : " + cl.getName() + ", position : "
100 + dataField.pos() + ", Field : " + dataField.toString());
101 }
102
103 if (dataField.required()) {
104 ++numberMandatoryFields;
105 } else {
106 ++numberOptionalFields;
107 }
108
109 dataFields.put(dataField.pos(), dataField);
110 annotedFields.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 : " + cl.getName() + ", Field" + field.toString());
118 }
119 linkFields.add(field);
120 }
121
122 }
123
124 if (!linkFields.isEmpty()) {
125 annotedLinkFields.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 public void bind(List<String> tokens, Map<String, Object> model) throws Exception {
140
141 int pos = 0;
142 int counterMandatoryFields = 0;
143
144 for (String data : tokens) {
145
146 // Get DataField from model
147 DataField dataField = dataFields.get(pos);
148 ObjectHelper.notNull(dataField, "No position " + pos + " defined for the field : " + data);
149
150 if (dataField.required()) {
151 // Increment counter of mandatory fields
152 ++counterMandatoryFields;
153
154 // Check if content of the field is empty
155 // This is not possible for mandatory fields
156 if (data.equals("")) {
157 throw new IllegalArgumentException("The mandatory field defined at the position " + pos
158 + " is empty !");
159 }
160 }
161
162 // Get Field to be setted
163 Field field = annotedFields.get(pos);
164 field.setAccessible(true);
165
166 if (LOG.isDebugEnabled()) {
167 LOG.debug("Pos : " + pos + ", Data : " + data + ", Field type : " + field.getType());
168 }
169
170 Format<?> format;
171
172 // Get pattern defined for the field
173 String pattern = dataField.pattern();
174
175 // Create format object to format the field
176 format = FormatFactory.getFormat(field.getType(), pattern, dataField.precision());
177
178 // field object to be set
179 Object modelField = model.get(field.getDeclaringClass().getName());
180
181 // format the data received
182 Object value = null;
183
184 if (!data.equals("")) {
185 value = format.parse(data);
186 } else {
187 value = getDefaultValueforPrimitive(field.getType());
188 }
189
190 field.set(modelField, value);
191
192 ++pos;
193
194 }
195
196 if (LOG.isDebugEnabled()) {
197 LOG.debug("Counter mandatory fields : " + counterMandatoryFields);
198 }
199
200
201 if (pos < totalFields) {
202 throw new IllegalArgumentException("Some fields are missing (optional or mandatory) !!");
203 }
204
205 if (counterMandatoryFields < numberMandatoryFields) {
206 throw new IllegalArgumentException("Some mandatory fields are missing !!");
207 }
208
209 }
210
211 public String unbind(Map<String, Object> model) throws Exception {
212
213 StringBuilder builder = new StringBuilder();
214
215 Map<Integer, DataField> dataFieldsSorted = new TreeMap<Integer, DataField>(dataFields);
216 Iterator<Integer> it = dataFieldsSorted.keySet().iterator();
217
218 // Map containing the OUT position of the field
219 // The key is double and is created using the position of the field and
220 // location of the class in the message (using section)
221 Map<Integer, String> positions = new TreeMap<Integer, String>();
222
223 // Check if separator exists
224 ObjectHelper.notNull(this.separator, "The separator has not been instantiated or property not defined in the @CsvRecord annotation");
225
226 char separator = Converter.getCharDelimitor(this.getSeparator());
227
228 if (LOG.isDebugEnabled()) {
229 LOG.debug("Separator converted : '0x" + Integer.toHexString(separator) + "', from : "
230 + this.getSeparator());
231 }
232
233 while (it.hasNext()) {
234
235 DataField dataField = dataFieldsSorted.get(it.next());
236
237 // Retrieve the field
238 Field field = annotedFields.get(dataField.pos());
239 // Change accessibility to allow to read protected/private fields
240 field.setAccessible(true);
241
242 // Retrieve the format, pattern and precision associated to the type
243 Class type = field.getType();
244 String pattern = dataField.pattern();
245 int precision = dataField.precision();
246
247 // Create format
248 Format format = FormatFactory.getFormat(type, pattern, precision);
249
250 // Get field from model
251 Object modelField = model.get(field.getDeclaringClass().getName());
252
253 if (modelField != null) {
254 // Get field value
255 Object value = field.get(modelField);
256 String strValue = null;
257
258 if (this.isMessageOrdered()) {
259
260 // Generate a key using the number of the section
261 // and the position of the field
262 Integer key1 = sections.get(modelField.getClass().getName());
263 Integer key2 = dataField.position();
264 Integer keyGenerated = generateKey(key1, key2);
265
266 if (LOG.isDebugEnabled()) {
267 LOG.debug("Key generated : " + String.valueOf(keyGenerated) + ", for section : " + key1);
268 }
269
270 // Get field value
271 //Object value = field.get(modelField);
272
273 if (value != null) {
274 // Format field value
275 strValue = format.format(value);
276 }
277
278 // Add the content to the TreeMap according to the
279 // position defined
280 positions.put(keyGenerated, strValue);
281
282 if (LOG.isDebugEnabled()) {
283 LOG.debug("Positions size : " + positions.size());
284 }
285
286 } else {
287 // Get field value
288 //Object value = field.get(modelField);
289 //String strValue = null;
290
291 // Add value to the list if not null
292 if (value != null) {
293
294 // Format field value
295 strValue = format.format(value);
296
297 }
298
299 if (LOG.isDebugEnabled()) {
300 LOG.debug("Data : " + value + ", value : " + strValue);
301 }
302
303 builder.append(strValue);
304
305 if (it.hasNext()) {
306 builder.append(separator);
307 }
308 }
309 }
310 }
311
312 // Iterate through the list to generate
313 // the message according to the order/position
314 if (this.isMessageOrdered()) {
315
316 Iterator<Integer> posit = positions.keySet().iterator();
317
318 while (posit.hasNext()) {
319 String value = positions.get(posit.next());
320
321 if (LOG.isDebugEnabled()) {
322 LOG.debug("Value added at the position (" + posit + ") : " + value + separator);
323 }
324
325 builder.append(value);
326 if (it.hasNext()) {
327 builder.append(separator);
328 }
329 }
330 }
331
332 return builder.toString();
333 }
334
335 /**
336 * Find the separator used to delimit the CSV fields
337 */
338 public String getSeparator() {
339 return separator;
340 }
341
342 /**
343 * Find the separator used to delimit the CSV fields
344 */
345 public boolean getSkipFirstLine() {
346 return skipFirstLine;
347 }
348
349 /**
350 * Flag indicating if the message must be ordered
351 *
352 * @return boolean
353 */
354 public boolean isMessageOrdered() {
355 return messageOrdered;
356 }
357
358 /**
359 *
360 * Get paramaters defined in @Csvrecord annotation
361 *
362 */
363 private void initCsvRecordParameters() {
364 if (separator == null) {
365 for (Class<?> cl : models) {
366 // Get annotation @CsvRecord from the class
367 CsvRecord record = cl.getAnnotation(CsvRecord.class);
368
369 // Get annotation @Section from the class
370 Section section = cl.getAnnotation(Section.class);
371
372 if (record != null) {
373 if (LOG.isDebugEnabled()) {
374 LOG.debug("Csv record : " + record.toString());
375 }
376
377 // Get skipFirstLine parameter
378 skipFirstLine = record.skipFirstLine();
379 if (LOG.isDebugEnabled()) {
380 LOG.debug("Skip First Line parameter of the CSV : " + skipFirstLine);
381 }
382
383 // Get Separator parameter
384 ObjectHelper.notNull(record.separator(),
385 "No separator has been defined in the @Record annotation !");
386 separator = record.separator();
387 if (LOG.isDebugEnabled()) {
388 LOG.debug("Separator defined for the CSV : " + separator);
389 }
390
391 // Get carriage return parameter
392 crlf = record.crlf();
393 if (LOG.isDebugEnabled()) {
394 LOG.debug("Carriage return defined for the CSV : " + crlf);
395 }
396 }
397
398 if (section != null) {
399 // Test if section number is not null
400 ObjectHelper.notNull(section.number(), "No number has been defined for the section !");
401
402 // Get section number and add it to the sections
403 sections.put(cl.getName(), section.number());
404 }
405 }
406 }
407 }
408 }