View Javadoc
1   /*
2    * Copyright 2007 Kasper B. Graversen
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.supercsv.io.dozer;
17  
18  import static org.dozer.loader.api.FieldsMappingOptions.hintB;
19  import static org.dozer.loader.api.TypeMappingOptions.mapNull;
20  import static org.dozer.loader.api.TypeMappingOptions.oneWay;
21  import static org.dozer.loader.api.TypeMappingOptions.wildcard;
22  
23  import java.io.IOException;
24  import java.io.Reader;
25  
26  import org.dozer.DozerBeanMapper;
27  import org.dozer.loader.api.BeanMappingBuilder;
28  import org.dozer.loader.api.TypeMappingBuilder;
29  import org.supercsv.cellprocessor.ift.CellProcessor;
30  import org.supercsv.io.AbstractCsvReader;
31  import org.supercsv.io.CsvBeanReader;
32  import org.supercsv.io.ITokenizer;
33  import org.supercsv.prefs.CsvPreference;
34  
35  /**
36   * CsvDozerBeanReader is a powerful replacement for {@link CsvBeanReader} that uses Dozer to map from CSV to a bean.
37   * 
38   * @author James Bassett
39   * @since 2.0.0
40   */
41  public class CsvDozerBeanReader extends AbstractCsvReader implements ICsvDozerBeanReader {
42  	
43  	private final DozerBeanMapper dozerBeanMapper;
44  	
45  	// source of dozer bean mapping
46  	private final CsvDozerBeanData beanData = new CsvDozerBeanData();
47  	
48  	/**
49  	 * Constructs a new <tt>CsvDozerBeanReader</tt> with the supplied Reader and CSV preferences and creates it's own
50  	 * DozerBeanMapper. Note that the <tt>reader</tt> will be wrapped in a <tt>BufferedReader</tt> before accessed.
51  	 * 
52  	 * @param reader
53  	 *            the reader
54  	 * @param preferences
55  	 *            the CSV preferences
56  	 * @throws NullPointerException
57  	 *             if reader or preferences are null
58  	 */
59  	public CsvDozerBeanReader(final Reader reader, final CsvPreference preferences) {
60  		super(reader, preferences);
61  		this.dozerBeanMapper = new DozerBeanMapper();
62  	}
63  	
64  	/**
65  	 * Constructs a new <tt>CsvDozerBeanReader</tt> with the supplied (custom) Tokenizer and CSV preferences and creates
66  	 * it's own DozerBeanMapper. The tokenizer should be set up with the Reader (CSV input) and CsvPreference
67  	 * beforehand.
68  	 * 
69  	 * @param tokenizer
70  	 *            the tokenizer
71  	 * @param preferences
72  	 *            the CSV preferences
73  	 * @throws NullPointerException
74  	 *             if tokenizer or preferences are null
75  	 */
76  	public CsvDozerBeanReader(final ITokenizer tokenizer, final CsvPreference preferences) {
77  		super(tokenizer, preferences);
78  		this.dozerBeanMapper = new DozerBeanMapper();
79  	}
80  	
81  	/**
82  	 * Constructs a new <tt>CsvDozerBeanReader</tt> with the supplied Reader, CSV preferences and DozerBeanMapper. Note
83  	 * that the <tt>reader</tt> will be wrapped in a <tt>BufferedReader</tt> before accessed.
84  	 * 
85  	 * @param reader
86  	 *            the reader
87  	 * @param preferences
88  	 *            the CSV preferences
89  	 * @param dozerBeanMapper
90  	 *            the dozer bean mapper to use
91  	 * @throws NullPointerException
92  	 *             if reader, preferences or dozerBeanMapper are null
93  	 */
94  	public CsvDozerBeanReader(final Reader reader, final CsvPreference preferences,
95  		final DozerBeanMapper dozerBeanMapper) {
96  		super(reader, preferences);
97  		if( dozerBeanMapper == null ) {
98  			throw new NullPointerException("dozerBeanMapper should not be null");
99  		}
100 		this.dozerBeanMapper = dozerBeanMapper;
101 	}
102 	
103 	/**
104 	 * Constructs a new <tt>CsvDozerBeanReader</tt> with the supplied (custom) Tokenizer, CSV preferences and
105 	 * DozerBeanMapper. The tokenizer should be set up with the Reader (CSV input) and CsvPreference beforehand.
106 	 * 
107 	 * @param tokenizer
108 	 *            the tokenizer
109 	 * @param preferences
110 	 *            the CSV preferences
111 	 * @param dozerBeanMapper
112 	 *            the dozer bean mapper to use
113 	 * @throws NullPointerException
114 	 *             if tokenizer, preferences or dozerBeanMapper are null
115 	 */
116 	public CsvDozerBeanReader(final ITokenizer tokenizer, final CsvPreference preferences,
117 		final DozerBeanMapper dozerBeanMapper) {
118 		super(tokenizer, preferences);
119 		if( dozerBeanMapper == null ) {
120 			throw new NullPointerException("dozerBeanMapper should not be null");
121 		}
122 		this.dozerBeanMapper = dozerBeanMapper;
123 	}
124 	
125 	/**
126 	 * {@inheritDoc}
127 	 */
128 	public void configureBeanMapping(final Class<?> clazz, final String[] fieldMapping) {
129 		dozerBeanMapper.addMapping(new MappingBuilder(clazz, fieldMapping));
130 	}
131 	
132 	/**
133 	 * {@inheritDoc}
134 	 */
135 	public void configureBeanMapping(final Class<?> clazz, final String[] fieldMapping, final Class<?>[] hintTypes) {
136 		dozerBeanMapper.addMapping(new MappingBuilder(clazz, fieldMapping, hintTypes));
137 	}
138 	
139 	/**
140 	 * {@inheritDoc}
141 	 */
142 	public <T> T read(final Class<T> clazz) throws IOException {
143 		if( clazz == null ) {
144 			throw new NullPointerException("clazz should not be null");
145 		}
146 		
147 		return readIntoBean(null, clazz, null);
148 	}
149 	
150 	/**
151 	 * {@inheritDoc}
152 	 */
153 	public <T> T read(final Class<T> clazz, final CellProcessor... processors) throws IOException {
154 		if( clazz == null ) {
155 			throw new NullPointerException("clazz should not be null");
156 		} else if( processors == null ) {
157 			throw new NullPointerException("processors should not be null");
158 		}
159 		
160 		return readIntoBean(null, clazz, processors);
161 	}
162 	
163 	/**
164 	 * {@inheritDoc}
165 	 */
166 	public <T> T read(final T bean) throws IOException {
167 		if( bean == null ) {
168 			throw new NullPointerException("bean should not be null");
169 		}
170 		
171 		return readIntoBean(bean, null, null);
172 	}
173 	
174 	/**
175 	 * {@inheritDoc}
176 	 */
177 	public <T> T read(final T bean, final CellProcessor... processors) throws IOException {
178 		if( bean == null ) {
179 			throw new NullPointerException("bean should not be null");
180 		} else if( processors == null ) {
181 			throw new NullPointerException("processors should not be null");
182 		}
183 		
184 		return readIntoBean(bean, null, processors);
185 	}
186 	
187 	/**
188 	 * Reads a row of a CSV file and populates the a bean, using Dozer to map column values to the appropriate field. If
189 	 * an existing bean is supplied, Dozer will populate that, otherwise Dozer will create an instance of type clazz
190 	 * (only one of bean or clazz should be supplied). If processors are supplied then they are used, otherwise the raw
191 	 * String values will be used.
192 	 * 
193 	 * @param bean
194 	 *            the bean to populate (if null, then clazz will be used instead)
195 	 * @param clazz
196 	 *            the type to instantiate (only required if bean is null)
197 	 * @param processors
198 	 *            the (optional) cell processors
199 	 * @return the populated bean
200 	 * @throws IOException
201 	 *             if an I/O error occurred
202 	 */
203 	private <T> T readIntoBean(final T bean, final Class<T> clazz, final CellProcessor[] processors) throws IOException {
204 		if( readRow() ) {
205 			if( processors == null ) {
206 				// populate bean data with the raw String values
207 				beanData.getColumns().clear();
208 				beanData.getColumns().addAll(getColumns());
209 			} else {
210 				// populate bean data with the processed values
211 				executeProcessors(beanData.getColumns(), processors);
212 			}
213 			
214 			if( bean != null ) {
215 				// populate existing bean
216 				dozerBeanMapper.map(beanData, bean);
217 				return bean;
218 			} else {
219 				// let Dozer create a new bean
220 				return dozerBeanMapper.map(beanData, clazz);
221 			}
222 			
223 		}
224 		
225 		return null; // EOF
226 	}
227 	
228 	/**
229 	 * Assembles the dozer bean mappings required by CsvDozerBeanReader programatically using the Dozer API.
230 	 */
231 	private static class MappingBuilder extends BeanMappingBuilder {
232 		
233 		private final Class<?> clazz;
234 		private final String[] fieldMapping;
235 		private final Class<?>[] hintTypes;
236 		
237 		/**
238 		 * Constructs a new MappingBuilder.
239 		 * 
240 		 * @param clazz
241 		 *            the class to add mapping configuration for (same as the type passed into write methods)
242 		 * @param fieldMapping
243 		 *            the field mapping for for each column (may contain <tt>null</tt> elements to indicate ignored
244 		 *            columns)
245 		 * @throws NullPointerException
246 		 *             if clazz or fieldMapping is null
247 		 */
248 		public MappingBuilder(final Class<?> clazz, final String[] fieldMapping) {
249 			if( clazz == null ) {
250 				throw new NullPointerException("clazz should not be null");
251 			} else if( fieldMapping == null ) {
252 				throw new NullPointerException("fieldMapping should not be null");
253 			}
254 			this.clazz = clazz;
255 			this.fieldMapping = fieldMapping;
256 			this.hintTypes = null;
257 		}
258 		
259 		/**
260 		 * Constructs a new MappingBuilder.
261 		 * 
262 		 * @param clazz
263 		 *            the class to add mapping configuration for (same as the type passed into write methods)
264 		 * @param fieldMapping
265 		 *            the field mapping for for each column (may contain <tt>null</tt> elements to indicate ignored
266 		 *            columns)
267 		 * @throws NullPointerException
268 		 *             if clazz, fieldMapping or hintTypes is null
269 		 * @throws IllegalArgumentException
270 		 *             if fieldMapping.length != hintTypes.length
271 		 */
272 		public MappingBuilder(final Class<?> clazz, final String[] fieldMapping, final Class<?>[] hintTypes) {
273 			if( clazz == null ) {
274 				throw new NullPointerException("clazz should not be null");
275 			} else if( fieldMapping == null ) {
276 				throw new NullPointerException("fieldMapping should not be null");
277 			} else if( hintTypes == null ) {
278 				throw new NullPointerException("fieldMapping should not be null");
279 			} else if( fieldMapping.length != hintTypes.length ) {
280 				throw new IllegalArgumentException(String.format(
281 					"hintTypes length(%d) should match fieldMapping length(%d)", hintTypes.length, fieldMapping.length));
282 			}
283 			this.clazz = clazz;
284 			this.fieldMapping = fieldMapping;
285 			this.hintTypes = hintTypes;
286 		}
287 		
288 		@Override
289 		protected void configure() {
290 			
291 			/*
292 			 * Add the required dozer mappings to map from each column (in the CsvDozerBeanData List) to its associated
293 			 * field in the supplied class. mapNull is enabled so that null CSV values are added to Lists if indexed
294 			 * mapping is used. oneWay is enabled just in case a custom DozerBeanMapper is supplied (so the same
295 			 * DozerBeanMapper can be used by CsvDozerBeanWriter). wildcard is disabled to prevent Dozer from trying to
296 			 * map things automatically.
297 			 */
298 			final TypeMappingBuilder mappingBuilder = mapping(CsvDozerBeanData.class, clazz, oneWay(), wildcard(false),
299 				mapNull(true));
300 			
301 			for( int i = 0; i < fieldMapping.length; i++ ) {
302 				
303 				final String mapping = fieldMapping[i];
304 				
305 				if( mapping == null ) {
306 					continue; // no field mappings required (column will be ignored)
307 				}
308 				
309 				if( hintTypes != null && hintTypes[i] != null ) {
310 					// add a hint on the target field
311 					mappingBuilder.fields("columns[" + i + "]", mapping, hintB(hintTypes[i]));
312 				} else {
313 					mappingBuilder.fields("columns[" + i + "]", mapping);
314 				}
315 				
316 			}
317 		}
318 	}
319 	
320 }