| | 1 | | // Copyright (c) Microsoft Corporation. All rights reserved. |
| | 2 | | // Licensed under the MIT License. |
| | 3 | |
|
| | 4 | | using System; |
| | 5 | | using System.Collections.Generic; |
| | 6 | | using System.Globalization; |
| | 7 | | using System.Linq; |
| | 8 | | using System.Text; |
| | 9 | | using Azure.Storage.Blobs.Models; |
| | 10 | | using Azure.Storage.Files.DataLake.Models; |
| | 11 | |
|
| | 12 | | namespace Azure.Storage.Files.DataLake |
| | 13 | | { |
| | 14 | | internal static partial class DataLakeExtensions |
| | 15 | | { |
| | 16 | | internal static FileSystemItem ToFileSystemItem(this BlobContainerItem containerItem) => |
| 19858 | 17 | | new FileSystemItem() |
| 19858 | 18 | | { |
| 19858 | 19 | | Name = containerItem.Name, |
| 19858 | 20 | | Properties = containerItem.Properties.ToFileSystemProperties() |
| 19858 | 21 | | }; |
| | 22 | |
|
| | 23 | | internal static FileSystemProperties ToFileSystemProperties(this BlobContainerProperties containerProperties) => |
| 19922 | 24 | | new FileSystemProperties() |
| 19922 | 25 | | { |
| 19922 | 26 | | LastModified = containerProperties.LastModified, |
| 19922 | 27 | | LeaseStatus = containerProperties.LeaseStatus.HasValue |
| 19922 | 28 | | ? (Models.DataLakeLeaseStatus)containerProperties.LeaseStatus : default, |
| 19922 | 29 | | LeaseState = containerProperties.LeaseState.HasValue |
| 19922 | 30 | | ? (Models.DataLakeLeaseState)containerProperties.LeaseState : default, |
| 19922 | 31 | | LeaseDuration = containerProperties.LeaseDuration.HasValue |
| 19922 | 32 | | ? (Models.DataLakeLeaseDuration)containerProperties.LeaseDuration : default, |
| 19922 | 33 | | PublicAccess = containerProperties.PublicAccess.HasValue |
| 19922 | 34 | | ? (Models.PublicAccessType)containerProperties.PublicAccess : default, |
| 19922 | 35 | | HasImmutabilityPolicy = containerProperties.HasImmutabilityPolicy, |
| 19922 | 36 | | HasLegalHold = containerProperties.HasLegalHold, |
| 19922 | 37 | | ETag = containerProperties.ETag, |
| 19922 | 38 | | Metadata = containerProperties.Metadata |
| 19922 | 39 | | }; |
| | 40 | |
|
| | 41 | | internal static FileDownloadDetails ToFileDownloadDetails(this BlobDownloadDetails blobDownloadProperties) => |
| 80 | 42 | | new FileDownloadDetails() |
| 80 | 43 | | { |
| 80 | 44 | | LastModified = blobDownloadProperties.LastModified, |
| 80 | 45 | | Metadata = blobDownloadProperties.Metadata, |
| 80 | 46 | | ContentRange = blobDownloadProperties.ContentRange, |
| 80 | 47 | | ETag = blobDownloadProperties.ETag, |
| 80 | 48 | | ContentEncoding = blobDownloadProperties.ContentEncoding, |
| 80 | 49 | | ContentDisposition = blobDownloadProperties.ContentDisposition, |
| 80 | 50 | | ContentLanguage = blobDownloadProperties.ContentLanguage, |
| 80 | 51 | | CopyCompletedOn = blobDownloadProperties.CopyCompletedOn, |
| 80 | 52 | | CopyStatusDescription = blobDownloadProperties.CopyStatusDescription, |
| 80 | 53 | | CopyId = blobDownloadProperties.CopyId, |
| 80 | 54 | | CopyProgress = blobDownloadProperties.CopyProgress, |
| 80 | 55 | | CopyStatus = (Models.CopyStatus)blobDownloadProperties.CopyStatus, |
| 80 | 56 | | LeaseDuration = (Models.DataLakeLeaseDuration)blobDownloadProperties.LeaseDuration, |
| 80 | 57 | | LeaseState = (Models.DataLakeLeaseState)blobDownloadProperties.LeaseState, |
| 80 | 58 | | LeaseStatus = (Models.DataLakeLeaseStatus)blobDownloadProperties.LeaseStatus, |
| 80 | 59 | | AcceptRanges = blobDownloadProperties.AcceptRanges, |
| 80 | 60 | | IsServerEncrypted = blobDownloadProperties.IsServerEncrypted, |
| 80 | 61 | | EncryptionKeySha256 = blobDownloadProperties.EncryptionKeySha256, |
| 80 | 62 | | ContentHash = blobDownloadProperties.BlobContentHash |
| 80 | 63 | | }; |
| | 64 | |
|
| | 65 | | internal static FileDownloadInfo ToFileDownloadInfo(this BlobDownloadInfo blobDownloadInfo) => |
| 80 | 66 | | new FileDownloadInfo() |
| 80 | 67 | | { |
| 80 | 68 | | ContentLength = blobDownloadInfo.ContentLength, |
| 80 | 69 | | Content = blobDownloadInfo.Content, |
| 80 | 70 | | ContentHash = blobDownloadInfo.ContentHash, |
| 80 | 71 | | Properties = blobDownloadInfo.Details.ToFileDownloadDetails() |
| 80 | 72 | | }; |
| | 73 | |
|
| | 74 | | internal static PathProperties ToPathProperties(this BlobProperties blobProperties) => |
| 636 | 75 | | new PathProperties() |
| 636 | 76 | | { |
| 636 | 77 | | LastModified = blobProperties.LastModified, |
| 636 | 78 | | CreatedOn = blobProperties.CreatedOn, |
| 636 | 79 | | Metadata = blobProperties.Metadata, |
| 636 | 80 | | CopyCompletedOn = blobProperties.CopyCompletedOn, |
| 636 | 81 | | CopyStatusDescription = blobProperties.CopyStatusDescription, |
| 636 | 82 | | CopyId = blobProperties.CopyId, |
| 636 | 83 | | CopyProgress = blobProperties.CopyProgress, |
| 636 | 84 | | CopySource = blobProperties.CopySource, |
| 636 | 85 | | IsIncrementalCopy = blobProperties.IsIncrementalCopy, |
| 636 | 86 | | LeaseDuration = (Models.DataLakeLeaseDuration)blobProperties.LeaseDuration, |
| 636 | 87 | | LeaseStatus = (Models.DataLakeLeaseStatus)blobProperties.LeaseStatus, |
| 636 | 88 | | LeaseState = (Models.DataLakeLeaseState)blobProperties.LeaseState, |
| 636 | 89 | | ContentLength = blobProperties.ContentLength, |
| 636 | 90 | | ContentType = blobProperties.ContentType, |
| 636 | 91 | | ETag = blobProperties.ETag, |
| 636 | 92 | | ContentHash = blobProperties.ContentHash, |
| 636 | 93 | | ContentEncoding = blobProperties.ContentEncoding, |
| 636 | 94 | | ContentDisposition = blobProperties.ContentDisposition, |
| 636 | 95 | | ContentLanguage = blobProperties.ContentLanguage, |
| 636 | 96 | | CacheControl = blobProperties.CacheControl, |
| 636 | 97 | | AcceptRanges = blobProperties.AcceptRanges, |
| 636 | 98 | | IsServerEncrypted = blobProperties.IsServerEncrypted, |
| 636 | 99 | | EncryptionKeySha256 = blobProperties.EncryptionKeySha256, |
| 636 | 100 | | AccessTier = blobProperties.AccessTier, |
| 636 | 101 | | ArchiveStatus = blobProperties.ArchiveStatus, |
| 636 | 102 | | AccessTierChangedOn = blobProperties.AccessTierChangedOn, |
| 636 | 103 | | ExpiresOn = blobProperties.ExpiresOn |
| 636 | 104 | | }; |
| | 105 | |
|
| | 106 | | internal static PathInfo ToPathInfo(this BlobInfo blobInfo) => |
| 112 | 107 | | new PathInfo |
| 112 | 108 | | { |
| 112 | 109 | | ETag = blobInfo.ETag, |
| 112 | 110 | | LastModified = blobInfo.LastModified |
| 112 | 111 | | }; |
| | 112 | |
|
| | 113 | | internal static DataLakeLease ToDataLakeLease(this BlobLease blobLease) => |
| 792 | 114 | | new DataLakeLease() |
| 792 | 115 | | { |
| 792 | 116 | | ETag = blobLease.ETag, |
| 792 | 117 | | LastModified = blobLease.LastModified, |
| 792 | 118 | | LeaseId = blobLease.LeaseId, |
| 792 | 119 | | LeaseTime = blobLease.LeaseTime |
| 792 | 120 | | }; |
| | 121 | |
|
| | 122 | | internal static BlobHttpHeaders ToBlobHttpHeaders(this PathHttpHeaders pathHttpHeaders) |
| | 123 | | { |
| 104 | 124 | | if (pathHttpHeaders == null) |
| | 125 | | { |
| 0 | 126 | | return null; |
| | 127 | | } |
| | 128 | |
|
| 104 | 129 | | return new BlobHttpHeaders() |
| 104 | 130 | | { |
| 104 | 131 | | ContentType = pathHttpHeaders.ContentType, |
| 104 | 132 | | ContentHash = pathHttpHeaders.ContentHash, |
| 104 | 133 | | ContentEncoding = pathHttpHeaders.ContentEncoding, |
| 104 | 134 | | ContentLanguage = pathHttpHeaders.ContentLanguage, |
| 104 | 135 | | ContentDisposition = pathHttpHeaders.ContentDisposition, |
| 104 | 136 | | CacheControl = pathHttpHeaders.CacheControl |
| 104 | 137 | | }; |
| | 138 | | } |
| | 139 | |
|
| | 140 | | internal static BlobRequestConditions ToBlobRequestConditions(this DataLakeRequestConditions dataLakeRequestCond |
| | 141 | | { |
| 3994 | 142 | | if (dataLakeRequestConditions == null) |
| | 143 | | { |
| 3394 | 144 | | return null; |
| | 145 | | } |
| | 146 | |
|
| 600 | 147 | | return new BlobRequestConditions() |
| 600 | 148 | | { |
| 600 | 149 | | IfMatch = dataLakeRequestConditions.IfMatch, |
| 600 | 150 | | IfNoneMatch = dataLakeRequestConditions.IfNoneMatch, |
| 600 | 151 | | IfModifiedSince = dataLakeRequestConditions.IfModifiedSince, |
| 600 | 152 | | IfUnmodifiedSince = dataLakeRequestConditions.IfUnmodifiedSince, |
| 600 | 153 | | LeaseId = dataLakeRequestConditions.LeaseId |
| 600 | 154 | | }; |
| | 155 | | } |
| | 156 | |
|
| | 157 | | internal static PathItem ToPathItem(this Dictionary<string, string> dictionary) |
| | 158 | | { |
| 276 | 159 | | dictionary.TryGetValue("name", out string name); |
| 276 | 160 | | dictionary.TryGetValue("isDirectory", out string isDirectoryString); |
| 276 | 161 | | dictionary.TryGetValue("lastModified", out string lastModifiedString); |
| 276 | 162 | | dictionary.TryGetValue("etag", out string etagString); |
| 276 | 163 | | dictionary.TryGetValue("contentLength", out string contentLengthString); |
| 276 | 164 | | dictionary.TryGetValue("owner", out string owner); |
| 276 | 165 | | dictionary.TryGetValue("group", out string group); |
| 276 | 166 | | dictionary.TryGetValue("permissions", out string permissions); |
| | 167 | |
|
| 276 | 168 | | bool isDirectory = false; |
| 276 | 169 | | if (isDirectoryString != null) |
| | 170 | | { |
| 216 | 171 | | isDirectory = bool.Parse(isDirectoryString); |
| | 172 | | } |
| | 173 | |
|
| 276 | 174 | | DateTimeOffset lastModified = new DateTimeOffset(); |
| 276 | 175 | | if (lastModifiedString != null) |
| | 176 | | { |
| 276 | 177 | | lastModified = DateTimeOffset.Parse(lastModifiedString, CultureInfo.InvariantCulture); |
| | 178 | | } |
| | 179 | |
|
| 276 | 180 | | ETag eTag = new ETag(); |
| 276 | 181 | | if (etagString != null) |
| | 182 | | { |
| 276 | 183 | | eTag = new ETag(etagString); |
| | 184 | | } |
| | 185 | |
|
| 276 | 186 | | long contentLength = 0; |
| 276 | 187 | | if (contentLengthString != null) |
| | 188 | | { |
| 268 | 189 | | contentLength = long.Parse(contentLengthString, CultureInfo.InvariantCulture); |
| | 190 | | } |
| | 191 | |
|
| 276 | 192 | | PathItem pathItem = new PathItem() |
| 276 | 193 | | { |
| 276 | 194 | | Name = name, |
| 276 | 195 | | IsDirectory = isDirectory, |
| 276 | 196 | | LastModified = lastModified, |
| 276 | 197 | | ETag = eTag, |
| 276 | 198 | | ContentLength = contentLength, |
| 276 | 199 | | Owner = owner, |
| 276 | 200 | | Group = group, |
| 276 | 201 | | Permissions = permissions |
| 276 | 202 | | }; |
| 276 | 203 | | return pathItem; |
| | 204 | | } |
| | 205 | |
|
| | 206 | | internal static PathContentInfo ToPathContentInfo(this PathUpdateResult pathUpdateResult) => |
| 0 | 207 | | new PathContentInfo() |
| 0 | 208 | | { |
| 0 | 209 | | ContentHash = pathUpdateResult.ContentMD5, |
| 0 | 210 | | ETag = pathUpdateResult.ETag, |
| 0 | 211 | | LastModified = pathUpdateResult.LastModified, |
| 0 | 212 | | AcceptRanges = pathUpdateResult.AcceptRanges, |
| 0 | 213 | | CacheControl = pathUpdateResult.CacheControl, |
| 0 | 214 | | ContentDisposition = pathUpdateResult.ContentDisposition, |
| 0 | 215 | | ContentEncoding = pathUpdateResult.ContentEncoding, |
| 0 | 216 | | ContentLanguage = pathUpdateResult.ContentLanguage, |
| 0 | 217 | | ContentLength = pathUpdateResult.ContentLength, |
| 0 | 218 | | ContentRange = pathUpdateResult.ContentRange, |
| 0 | 219 | | ContentType = pathUpdateResult.ContentType, |
| 0 | 220 | | Metadata = ToMetadata(pathUpdateResult.Properties) |
| 0 | 221 | | }; |
| | 222 | |
|
| | 223 | | private static IDictionary<string, string> ToMetadata(string rawMetdata) |
| | 224 | | { |
| 0 | 225 | | if (rawMetdata == null) |
| | 226 | | { |
| 0 | 227 | | return null; |
| | 228 | | } |
| | 229 | |
|
| 0 | 230 | | IDictionary<string, string> metadataDictionary = new Dictionary<string, string>(StringComparer.InvariantCult |
| 0 | 231 | | string[] metadataArray = rawMetdata.Split(','); |
| 0 | 232 | | foreach (string entry in metadataArray) |
| | 233 | | { |
| 0 | 234 | | string[] entryArray = entry.Split('='); |
| 0 | 235 | | if (entryArray.Length == 2) |
| | 236 | | { |
| 0 | 237 | | byte[] valueArray = Convert.FromBase64String(entryArray[1]); |
| 0 | 238 | | metadataDictionary.Add(entryArray[0], Encoding.UTF8.GetString(valueArray)); |
| | 239 | | } |
| | 240 | | } |
| 0 | 241 | | return metadataDictionary; |
| | 242 | | } |
| | 243 | |
|
| | 244 | | internal static FileSystemAccessPolicy ToFileSystemAccessPolicy(this BlobContainerAccessPolicy blobContainerAcce |
| | 245 | | { |
| 24 | 246 | | if (blobContainerAccessPolicy == null) |
| | 247 | | { |
| 0 | 248 | | return null; |
| | 249 | | } |
| | 250 | |
|
| 24 | 251 | | return new FileSystemAccessPolicy() |
| 24 | 252 | | { |
| 24 | 253 | | DataLakePublicAccess = (Models.PublicAccessType)blobContainerAccessPolicy.BlobPublicAccess, |
| 24 | 254 | | ETag = blobContainerAccessPolicy.ETag, |
| 24 | 255 | | LastModified = blobContainerAccessPolicy.LastModified, |
| 24 | 256 | | SignedIdentifiers = blobContainerAccessPolicy.SignedIdentifiers.ToDataLakeSignedIdentifiers() |
| 24 | 257 | | }; |
| | 258 | | } |
| | 259 | |
|
| | 260 | | internal static IEnumerable<DataLakeSignedIdentifier> ToDataLakeSignedIdentifiers(this IEnumerable<BlobSignedIde |
| | 261 | | { |
| 24 | 262 | | if (blobSignedIdentifiers == null) |
| | 263 | | { |
| 0 | 264 | | return null; |
| | 265 | | } |
| | 266 | |
|
| 56 | 267 | | return blobSignedIdentifiers.ToList().Select(r => r.ToDataLakeSignedIdentifier()); |
| | 268 | | } |
| | 269 | |
|
| | 270 | | internal static DataLakeSignedIdentifier ToDataLakeSignedIdentifier(this BlobSignedIdentifier blobSignedIdentifi |
| | 271 | | { |
| 32 | 272 | | if (blobSignedIdentifier == null) |
| | 273 | | { |
| 0 | 274 | | return null; |
| | 275 | | } |
| | 276 | |
|
| 32 | 277 | | return new DataLakeSignedIdentifier() |
| 32 | 278 | | { |
| 32 | 279 | | AccessPolicy = blobSignedIdentifier.AccessPolicy.ToDataLakeAccessPolicy(), |
| 32 | 280 | | Id = blobSignedIdentifier.Id |
| 32 | 281 | | }; |
| | 282 | | } |
| | 283 | |
|
| | 284 | | internal static DataLakeAccessPolicy ToDataLakeAccessPolicy(this BlobAccessPolicy blobAccessPolicy) |
| | 285 | | { |
| 32 | 286 | | if (blobAccessPolicy == null) |
| | 287 | | { |
| 0 | 288 | | return null; |
| | 289 | | } |
| | 290 | |
|
| 32 | 291 | | return new DataLakeAccessPolicy() |
| 32 | 292 | | { |
| 32 | 293 | | PolicyStartsOn = blobAccessPolicy.PolicyStartsOn, |
| 32 | 294 | | PolicyExpiresOn = blobAccessPolicy.PolicyExpiresOn, |
| 32 | 295 | | Permissions = blobAccessPolicy.Permissions |
| 32 | 296 | | }; |
| | 297 | | } |
| | 298 | |
|
| | 299 | | internal static IEnumerable<BlobSignedIdentifier> ToBlobSignedIdentifiers(this IEnumerable<DataLakeSignedIdentif |
| | 300 | | { |
| 56 | 301 | | if (dataLakeSignedIdentifiers == null) |
| | 302 | | { |
| 4 | 303 | | return null; |
| | 304 | | } |
| | 305 | |
|
| 104 | 306 | | return dataLakeSignedIdentifiers.ToList().Select(r => r.ToBlobSignedIdentifier()); |
| | 307 | | } |
| | 308 | |
|
| | 309 | | internal static BlobSignedIdentifier ToBlobSignedIdentifier(this DataLakeSignedIdentifier dataLakeSignedIdentifi |
| | 310 | | { |
| 52 | 311 | | if (dataLakeSignedIdentifier == null) |
| | 312 | | { |
| 0 | 313 | | return null; |
| | 314 | | } |
| | 315 | |
|
| 52 | 316 | | return new BlobSignedIdentifier() |
| 52 | 317 | | { |
| 52 | 318 | | AccessPolicy = dataLakeSignedIdentifier.AccessPolicy.ToBlobAccessPolicy(), |
| 52 | 319 | | Id = dataLakeSignedIdentifier.Id |
| 52 | 320 | | }; |
| | 321 | | } |
| | 322 | |
|
| | 323 | | internal static BlobAccessPolicy ToBlobAccessPolicy(this DataLakeAccessPolicy dataLakeAccessPolicy) |
| | 324 | | { |
| 52 | 325 | | if (dataLakeAccessPolicy == null) |
| | 326 | | { |
| 0 | 327 | | return null; |
| | 328 | | } |
| | 329 | |
|
| 52 | 330 | | return new BlobAccessPolicy() |
| 52 | 331 | | { |
| 52 | 332 | | PolicyStartsOn = dataLakeAccessPolicy.PolicyStartsOn, |
| 52 | 333 | | PolicyExpiresOn = dataLakeAccessPolicy.PolicyExpiresOn, |
| 52 | 334 | | Permissions = dataLakeAccessPolicy.Permissions |
| 52 | 335 | | }; |
| | 336 | | } |
| | 337 | |
|
| | 338 | | internal static BlobQueryOptions ToBlobQueryOptions(this DataLakeQueryOptions options) |
| | 339 | | { |
| 64 | 340 | | if (options == null) |
| | 341 | | { |
| 12 | 342 | | return null; |
| | 343 | | } |
| | 344 | |
|
| 52 | 345 | | BlobQueryOptions blobQueryOptions = new BlobQueryOptions |
| 52 | 346 | | { |
| 52 | 347 | | InputTextConfiguration = options.InputTextConfiguration.ToBlobQueryTextConfiguration(), |
| 52 | 348 | | OutputTextConfiguration = options.OutputTextConfiguration.ToBlobQueryTextConfiguration(), |
| 52 | 349 | | Conditions = options.Conditions.ToBlobRequestConditions(), |
| 52 | 350 | | ProgressHandler = options.ProgressHandler |
| 52 | 351 | | }; |
| | 352 | |
|
| 52 | 353 | | if (options._errorHandler != null) |
| | 354 | | { |
| 12 | 355 | | blobQueryOptions.ErrorHandler += (BlobQueryError error) => { options._errorHandler(error.ToDataLakeQuery |
| | 356 | | } |
| | 357 | |
|
| 52 | 358 | | return blobQueryOptions; |
| | 359 | | } |
| | 360 | |
|
| | 361 | | internal static IBlobQueryTextOptions ToBlobQueryTextConfiguration(this IDataLakeQueryTextOptions textConfigurat |
| | 362 | | { |
| 104 | 363 | | if (textConfiguration == null) |
| | 364 | | { |
| 104 | 365 | | return null; |
| | 366 | | } |
| | 367 | |
|
| 0 | 368 | | if (textConfiguration.GetType() == typeof(DataLakeQueryJsonTextOptions)) |
| | 369 | | { |
| 0 | 370 | | return ((DataLakeQueryJsonTextOptions)textConfiguration).ToBlobQueryJsonTextConfiguration(); |
| | 371 | | } |
| | 372 | |
|
| 0 | 373 | | if (textConfiguration.GetType() == typeof(DataLakeQueryCsvTextOptions)) |
| | 374 | | { |
| 0 | 375 | | return ((DataLakeQueryCsvTextOptions)textConfiguration).ToBlobQueryCsvTextConfiguration(); |
| | 376 | | } |
| | 377 | |
|
| 0 | 378 | | throw new ArgumentException("Invalid text configuration type"); |
| | 379 | | } |
| | 380 | |
|
| | 381 | | internal static BlobQueryJsonTextOptions ToBlobQueryJsonTextConfiguration(this DataLakeQueryJsonTextOptions text |
| 0 | 382 | | => new BlobQueryJsonTextOptions |
| 0 | 383 | | { |
| 0 | 384 | | RecordSeparator = textConfiguration.RecordSeparator |
| 0 | 385 | | }; |
| | 386 | |
|
| | 387 | | internal static BlobQueryCsvTextOptions ToBlobQueryCsvTextConfiguration(this DataLakeQueryCsvTextOptions textCon |
| 0 | 388 | | => new BlobQueryCsvTextOptions |
| 0 | 389 | | { |
| 0 | 390 | | ColumnSeparator = textConfiguration.ColumnSeparator, |
| 0 | 391 | | QuotationCharacter = textConfiguration.QuotationCharacter, |
| 0 | 392 | | EscapeCharacter = textConfiguration.EscapeCharacter, |
| 0 | 393 | | HasHeaders = textConfiguration.HasHeaders |
| 0 | 394 | | }; |
| | 395 | |
|
| | 396 | | internal static DataLakeQueryError ToDataLakeQueryError(this BlobQueryError error) |
| | 397 | | { |
| 4 | 398 | | if (error == null) |
| | 399 | | { |
| 0 | 400 | | return null; |
| | 401 | | } |
| | 402 | |
|
| 4 | 403 | | return new DataLakeQueryError |
| 4 | 404 | | { |
| 4 | 405 | | Name = error.Name, |
| 4 | 406 | | Description = error.Description, |
| 4 | 407 | | IsFatal = error.IsFatal, |
| 4 | 408 | | Position = error.Position |
| 4 | 409 | | }; |
| | 410 | | } |
| | 411 | | } |
| | 412 | | } |