Line data Source code
1 : #region Copyright
2 : // // -----------------------------------------------------------------------
3 : // // <copyright company="Chinchilla Software Limited">
4 : // // Copyright Chinchilla Software Limited. All rights reserved.
5 : // // </copyright>
6 : // // -----------------------------------------------------------------------
7 : #endregion
8 :
9 : using System;
10 : using System.Collections.Generic;
11 : using System.Linq;
12 : using System.Net.Http.Formatting;
13 : using System.Reflection;
14 : using Cqrs.WebApi.Formatters.FormMultipart.Infrastructure;
15 : using Cqrs.WebApi.Formatters.FormMultipart.Infrastructure.Extensions;
16 : using Cqrs.WebApi.Formatters.FormMultipart.Infrastructure.Logger;
17 :
18 : namespace Cqrs.WebApi.Formatters.FormMultipart.Converters
19 : {
20 : /// <summary>
21 : /// Converts multi-part form-data to <see cref="object">objects</see>.
22 : /// </summary>
23 : public class FormDataToObjectConverter
24 1 : {
25 : private readonly FormData _sourceData;
26 :
27 : private readonly IFormDataConverterLogger _logger;
28 :
29 : private readonly MultipartFormatterSettings _settings;
30 :
31 : /// <summary>
32 : /// Instantiate and initialise a new instance of <see cref="FormDataToObjectConverter"/>
33 : /// </summary>
34 : /// <param name="sourceData">The <see cref="FormData"/> to convert.</param>
35 : /// <param name="logger">The <see cref="IFormatterLogger"/> to log events to.</param>
36 : /// <param name="settings">The <see cref="MultipartFormatterSettings"/> to use.</param>
37 1 : public FormDataToObjectConverter(FormData sourceData, IFormDataConverterLogger logger, MultipartFormatterSettings settings)
38 : {
39 : if (sourceData == null)
40 : throw new ArgumentNullException("sourceData");
41 : if (logger == null)
42 : throw new ArgumentNullException("logger");
43 : if (settings == null)
44 : throw new ArgumentNullException("settings");
45 :
46 : _settings = settings;
47 : _sourceData = sourceData;
48 : _logger = logger;
49 : }
50 :
51 : /// <summary>
52 : /// Converts the multi-part form-data to the provided <paramref name="destinationType"/>
53 : /// </summary>
54 : /// <param name="destinationType">The <see cref="Type"/> to convert the multi-part form-data to.</param>
55 1 : public object Convert(Type destinationType)
56 : {
57 : if (destinationType == null)
58 : throw new ArgumentNullException("destinationType");
59 :
60 : if (destinationType == typeof(FormData))
61 : return _sourceData;
62 :
63 : var objResult = CreateObject(destinationType);
64 : return objResult;
65 : }
66 :
67 : private object CreateObject(Type destinationType, string propertyName = "")
68 : {
69 : object propValue = null;
70 :
71 : if (propertyName == null)
72 : {
73 : propertyName = "";
74 : }
75 :
76 : object buf;
77 : if (TryGetAsNotIndexedListOrArray(destinationType, propertyName, out buf)
78 : || TryGetFromFormData(destinationType, propertyName, out buf)
79 : || TryGetAsGenericDictionary(destinationType, propertyName, out buf)
80 : || TryGetAsIndexedGenericListOrArray(destinationType, propertyName, out buf)
81 : || TryGetAsCustomType(destinationType, propertyName, out buf))
82 : {
83 : propValue = buf;
84 : }
85 : else if (IsNotNullableValueType(destinationType)
86 : && IsNeedValidateMissedProperty(propertyName))
87 : {
88 : _logger.LogError(propertyName, "The value is required.");
89 : }
90 : else if (!IsFileOrConvertableFromString(destinationType))
91 : {
92 : _logger.LogError(propertyName, String.Format("Cannot parse type \"{0}\".", destinationType.FullName));
93 : }
94 :
95 : return propValue;
96 : }
97 :
98 : private bool TryGetAsNotIndexedListOrArray(Type destinationType, string propertyName, out object propValue)
99 : {
100 : propValue = null;
101 :
102 : Type genericListItemType;
103 : bool isGenericList = IsGenericListOrArray(destinationType, out genericListItemType);
104 : if (isGenericList)
105 : {
106 : var items = GetNotIndexedListItems(propertyName, genericListItemType);
107 : propValue = MakeList(genericListItemType, destinationType, items, propertyName);
108 : }
109 :
110 : return propValue != null;
111 : }
112 :
113 : private List<object> GetNotIndexedListItems(string propertyName, Type genericListItemType)
114 : {
115 : List<object> res;
116 : if (!TryGetListFromFormData(genericListItemType, propertyName, out res))
117 : {
118 : TryGetListFromFormData(genericListItemType, propertyName + "[]", out res);
119 : }
120 :
121 : return res ?? new List<object>();
122 : }
123 :
124 : private bool TryGetFromFormData(Type destinationType, string propertyName, out object propValue)
125 : {
126 : propValue = null;
127 : List<object> values;
128 : if (TryGetListFromFormData(destinationType, propertyName, out values))
129 : {
130 : propValue = values.FirstOrDefault();
131 : return true;
132 : }
133 : return false;
134 : }
135 :
136 : private bool TryGetListFromFormData(Type destinationType, string propertyName, out List<object> propValue)
137 : {
138 : bool existsInFormData = false;
139 : propValue = null;
140 :
141 : if (destinationType == typeof(HttpFile) || destinationType == typeof(byte[]))
142 : {
143 : var files = _sourceData.GetFiles(propertyName, _settings.CultureInfo);
144 : if (files.Any())
145 : {
146 : existsInFormData = true;
147 : propValue = new List<object>();
148 :
149 : foreach (var httpFile in files)
150 : {
151 : var item = destinationType == typeof(byte[])
152 : ? httpFile.Buffer
153 : : (object)httpFile;
154 :
155 : propValue.Add(item);
156 : }
157 : }
158 : }
159 : else
160 : {
161 : var values = _sourceData.GetValues(propertyName, _settings.CultureInfo);
162 : if (values.Any())
163 : {
164 : existsInFormData = true;
165 : propValue = new List<object>();
166 :
167 : foreach (var value in values)
168 : {
169 : object val;
170 : if(TryConvertFromString(destinationType, propertyName, value, out val))
171 : {
172 : propValue.Add(val);
173 : }
174 : }
175 : }
176 : }
177 :
178 : return existsInFormData;
179 : }
180 :
181 : private bool TryConvertFromString(Type destinationType, string propertyName, string val, out object propValue)
182 : {
183 : propValue = null;
184 : var typeConverter = destinationType.GetFromStringConverter();
185 : if (typeConverter == null)
186 : {
187 : _logger.LogError(propertyName, "Cannot find type converter for field - " + propertyName);
188 : }
189 : else
190 : {
191 : try
192 : {
193 : propValue = typeConverter.ConvertFromString(val, _settings.CultureInfo);
194 : return true;
195 : }
196 : catch (Exception ex)
197 : {
198 : _logger.LogError(propertyName, String.Format("Error parsing field \"{0}\": {1}", propertyName, ex.Message));
199 : }
200 : }
201 : return false;
202 : }
203 :
204 : private bool TryGetAsGenericDictionary(Type destinationType, string propertyName, out object propValue)
205 : {
206 : propValue = null;
207 : Type keyType, valueType;
208 : bool isGenericDictionary = IsGenericDictionary(destinationType, out keyType, out valueType);
209 : if (isGenericDictionary)
210 : {
211 : var dictType = typeof(Dictionary<,>).MakeGenericType(new[] { keyType, valueType });
212 : var add = dictType.GetMethod("Add");
213 :
214 : var pValue = Activator.CreateInstance(dictType);
215 :
216 : int index = 0;
217 : string origPropName = propertyName;
218 : bool isFilled = false;
219 : while (true)
220 : {
221 : string propertyKeyName = String.Format("{0}[{1}].Key", origPropName, index);
222 : var objKey = CreateObject(keyType, propertyKeyName);
223 : if (objKey != null)
224 : {
225 : string propertyValueName = String.Format("{0}[{1}].Value", origPropName, index);
226 : var objValue = CreateObject(valueType, propertyValueName);
227 :
228 : if (objValue != null)
229 : {
230 : add.Invoke(pValue, new[] { objKey, objValue });
231 : isFilled = true;
232 : }
233 : }
234 : else
235 : {
236 : break;
237 : }
238 : index++;
239 : }
240 :
241 : if (isFilled || IsRootProperty(propertyName))
242 : {
243 : propValue = pValue;
244 : }
245 : }
246 :
247 : return isGenericDictionary;
248 : }
249 :
250 : private bool TryGetAsIndexedGenericListOrArray(Type destinationType, string propertyName, out object propValue)
251 : {
252 : propValue = null;
253 : Type genericListItemType;
254 : bool isGenericList = IsGenericListOrArray(destinationType, out genericListItemType);
255 : if (isGenericList)
256 : {
257 : var items = GetIndexedListItems(propertyName, genericListItemType);
258 : propValue = MakeList(genericListItemType, destinationType, items, propertyName);
259 : }
260 :
261 : return isGenericList;
262 : }
263 :
264 : private object MakeList(Type genericListItemType, Type destinationType, List<object> listItems, string propertyName)
265 : {
266 : object result = null;
267 :
268 : if (listItems.Any() || IsRootProperty(propertyName))
269 : {
270 : var listType = typeof(List<>).MakeGenericType(genericListItemType);
271 :
272 : var add = listType.GetMethod("Add");
273 : var pValue = Activator.CreateInstance(listType);
274 :
275 : foreach (var listItem in listItems)
276 : {
277 : add.Invoke(pValue, new[] { listItem });
278 : }
279 :
280 : if (destinationType.IsArray)
281 : {
282 : var toArrayMethod = listType.GetMethod("ToArray");
283 : result = toArrayMethod.Invoke(pValue, new object[0]);
284 : }
285 : else
286 : {
287 : result = pValue;
288 : }
289 : }
290 :
291 : return result;
292 : }
293 :
294 : private List<object> GetIndexedListItems(string origPropName, Type genericListItemType)
295 : {
296 : var res = new List<object>();
297 : int index = 0;
298 : while (true)
299 : {
300 : var propertyName = String.Format("{0}[{1}]", origPropName, index);
301 : var objValue = CreateObject(genericListItemType, propertyName);
302 : if (objValue != null)
303 : {
304 : res.Add(objValue);
305 : }
306 : else
307 : {
308 : break;
309 : }
310 :
311 : index++;
312 : }
313 : return res;
314 : }
315 :
316 : private bool TryGetAsCustomType(Type destinationType, string propertyName, out object propValue)
317 : {
318 : propValue = null;
319 : bool isCustomNonEnumerableType = destinationType.IsCustomNonEnumerableType();
320 : if (isCustomNonEnumerableType && IsRootPropertyOrAnyChildPropertiesExistsInFormData(propertyName))
321 : {
322 : propValue = Activator.CreateInstance(destinationType);
323 : foreach (PropertyInfo propertyInfo in destinationType.GetProperties().Where(m => m.SetMethod != null))
324 : {
325 : var propName = (!String.IsNullOrEmpty(propertyName) ? propertyName + "." : "") + propertyInfo.Name;
326 :
327 : var objValue = CreateObject(propertyInfo.PropertyType, propName);
328 : if (objValue != null)
329 : {
330 : propertyInfo.SetValue(propValue, objValue);
331 : }
332 : }
333 : }
334 : return isCustomNonEnumerableType;
335 : }
336 :
337 : private bool IsGenericDictionary(Type type, out Type keyType, out Type valueType)
338 : {
339 : Type iDictType = type.GetInterface(typeof (IDictionary<,>).Name);
340 : if (iDictType != null)
341 : {
342 : var types = iDictType.GetGenericArguments();
343 : if (types.Length == 2)
344 : {
345 : keyType = types[0];
346 : valueType = types[1];
347 : return true;
348 : }
349 : }
350 :
351 : keyType = null;
352 : valueType = null;
353 : return false;
354 : }
355 :
356 : private bool IsGenericListOrArray(Type type, out Type itemType)
357 : {
358 : if (type.GetInterface(typeof(IDictionary<,>).Name) == null) //not a dictionary
359 : {
360 : if (type.IsArray)
361 : {
362 : itemType = type.GetElementType();
363 : return true;
364 : }
365 :
366 : Type iListType = type.GetInterface(typeof(ICollection<>).Name);
367 : if (iListType != null)
368 : {
369 : Type[] genericArguments = iListType.GetGenericArguments();
370 : if (genericArguments.Length == 1)
371 : {
372 : itemType = genericArguments[0];
373 : return true;
374 : }
375 : }
376 : }
377 :
378 : itemType = null;
379 : return false;
380 : }
381 :
382 : private bool IsFileOrConvertableFromString(Type type)
383 : {
384 : if (type == typeof (HttpFile))
385 : return true;
386 :
387 : return type.GetFromStringConverter() != null;
388 : }
389 :
390 : private bool IsNotNullableValueType(Type type)
391 : {
392 : if (!type.IsValueType)
393 : return false;
394 :
395 : return Nullable.GetUnderlyingType(type) == null;
396 : }
397 :
398 : private bool IsNeedValidateMissedProperty(string propertyName)
399 : {
400 : return _settings.ValidateNonNullableMissedProperty
401 : && !IsIndexedProperty(propertyName)
402 : && IsRootPropertyOrAnyParentsPropertyExistsInFormData(propertyName);
403 : }
404 :
405 : private bool IsRootPropertyOrAnyParentsPropertyExistsInFormData(string propertyName)
406 : {
407 : string parentName = "";
408 : if (propertyName != null)
409 : {
410 : int lastDotIndex = propertyName.LastIndexOf('.');
411 : if (lastDotIndex >= 0)
412 : {
413 : parentName = propertyName.Substring(0, lastDotIndex);
414 : }
415 : }
416 :
417 : bool result = IsRootPropertyOrAnyChildPropertiesExistsInFormData(parentName);
418 : return result;
419 : }
420 :
421 : private bool IsRootPropertyOrAnyChildPropertiesExistsInFormData(string propertyName)
422 : {
423 : if (IsRootProperty(propertyName))
424 : return true;
425 :
426 : string prefixWithDot = propertyName + ".";
427 : bool result = _sourceData.GetAllKeys().Any(m => m.StartsWith(prefixWithDot, true, _settings.CultureInfo));
428 : return result;
429 : }
430 :
431 : private bool IsRootProperty(string propertyName)
432 : {
433 : return propertyName == "";
434 : }
435 :
436 : private bool IsIndexedProperty(string propName)
437 : {
438 : return propName != null && propName.EndsWith("]");
439 : }
440 : }
441 : }
|