// Copyright (c) Service Stack LLC. All Rights Reserved. // License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; using ServiceStack.Logging; using ServiceStack.Serialization; using ServiceStack.Text; using ServiceStack.Web; namespace ServiceStack { public class JsonHttpClient : IServiceClient, IJsonServiceClient, IHasCookieContainer { public static ILog log = LogManager.GetLogger(typeof(JsonHttpClient)); public static Func GlobalHttpMessageHandlerFactory { get; set; } public HttpMessageHandler HttpMessageHandler { get; set; } public HttpClient HttpClient { get; set; } public CookieContainer CookieContainer { get; set; } public ResultsFilterHttpDelegate ResultsFilter { get; set; } public ResultsFilterHttpResponseDelegate ResultsFilterResponse { get; set; } public const string DefaultHttpMethod = HttpMethods.Post; public static string DefaultUserAgent = "ServiceStack .NET HttpClient " + Env.ServiceStackVersion; public string BaseUri { get; set; } public string Format = "json"; public string ContentType = MimeTypes.Json; public string SyncReplyBaseUri { get; set; } public string AsyncOneWayBaseUri { get; set; } public int Version { get; set; } public string SessionId { get; set; } public string UserName { get; set; } public string Password { get; set; } public bool AlwaysSendBasicAuthHeader { get; set; } public CancellationTokenSource CancelTokenSource { get; set; } /// /// Gets the collection of headers to be added to outgoing requests. /// public INameValueCollection Headers { get; private set; } public void SetBaseUri(string baseUri) { this.BaseUri = baseUri; this.SyncReplyBaseUri = baseUri.WithTrailingSlash() + Format + "/reply/"; this.AsyncOneWayBaseUri = baseUri.WithTrailingSlash() + Format + "/oneway/"; } public JsonHttpClient(string baseUri) : this() { SetBaseUri(baseUri); } public JsonHttpClient() { this.Headers = PclExportClient.Instance.NewNameValueCollection(); this.CookieContainer = new CookieContainer(); JsConfig.InitStatics(); } public void SetCredentials(string userName, string password) { this.UserName = userName; this.Password = password; } public virtual string GetBaseUrl(string relativeOrAbsoluteUrl) { return relativeOrAbsoluteUrl.StartsWith("http:") || relativeOrAbsoluteUrl.StartsWith("https:") ? relativeOrAbsoluteUrl : this.BaseUri.CombineWith(relativeOrAbsoluteUrl); } public HttpClient GetHttpClient() { //Should reuse same instance: http://social.msdn.microsoft.com/Forums/en-US/netfxnetcom/thread/4e12d8e2-e0bf-4654-ac85-3d49b07b50af/ if (HttpClient != null) return HttpClient; if (HttpMessageHandler == null && GlobalHttpMessageHandlerFactory != null) HttpMessageHandler = GlobalHttpMessageHandlerFactory(); var baseUri = BaseUri != null ? new Uri(BaseUri) : null; return HttpClient = HttpMessageHandler != null ? new HttpClient(HttpMessageHandler) { BaseAddress = baseUri } : new HttpClient(new HttpClientHandler { UseCookies = true, CookieContainer = CookieContainer, UseDefaultCredentials = true }) { BaseAddress = baseUri }; } public void AddHeader(string name, string value) { Headers[name] = value; } private int activeAsyncRequests = 0; public Task SendAsync(string httpMethod, string absoluteUrl, object request) { if (!httpMethod.HasRequestBody() && request != null) { var queryString = QueryStringSerializer.SerializeToString(request); if (!string.IsNullOrEmpty(queryString)) { absoluteUrl += "?" + queryString; } } if (ResultsFilter != null) { var response = ResultsFilter(typeof(TResponse), httpMethod, absoluteUrl, request); if (response is TResponse) { var tcs = new TaskCompletionSource(); tcs.SetResult((TResponse)response); return tcs.Task; } } var client = GetHttpClient(); if (AlwaysSendBasicAuthHeader) AddBasicAuth(client); this.PopulateRequestMetadata(request); var httpReq = new HttpRequestMessage(new HttpMethod(httpMethod), absoluteUrl); foreach (var name in Headers.AllKeys) { httpReq.Headers.Add(name, Headers[name]); } httpReq.Headers.Add(HttpHeaders.Accept, ContentType); if (httpMethod.HasRequestBody() && request != null) { var httpContent = request as HttpContent; if (httpContent != null) { httpReq.Content = httpContent; } else { var str = request as string; var bytes = request as byte[]; var stream = request as Stream; if (str != null) httpReq.Content = new StringContent(str); else if (bytes != null) httpReq.Content = new ByteArrayContent(bytes); else if (stream != null) httpReq.Content = new StreamContent(stream); else { using (__requestAccess()) { httpReq.Content = new StringContent(request.ToJson(), Encoding.UTF8, ContentType); } } } } ApplyWebRequestFilters(httpReq); Interlocked.Increment(ref activeAsyncRequests); if (CancelTokenSource == null) CancelTokenSource = new CancellationTokenSource(); var sendAsyncTask = client.SendAsync(httpReq, CancelTokenSource.Token); if (typeof(TResponse) == typeof(HttpResponseMessage)) { return (Task)(object)sendAsyncTask; } return sendAsyncTask .ContinueWith(responseTask => { var httpRes = responseTask.Result; ApplyWebResponseFilters(httpRes); if (typeof(TResponse) == typeof(byte[])) { return httpRes.Content.ReadAsByteArrayAsync().ContinueWith(task => { ThrowIfError(task, httpRes, request, absoluteUrl, task.Result); var response = (TResponse)(object)task.Result; if (ResultsFilterResponse != null) ResultsFilterResponse(httpRes, response, httpMethod, absoluteUrl, request); return response; }); } if (typeof(TResponse) == typeof(Stream)) { return httpRes.Content.ReadAsStreamAsync().ContinueWith(task => { ThrowIfError(task, httpRes, request, absoluteUrl, task.Result); var response = (TResponse)(object)task.Result; if (ResultsFilterResponse != null) ResultsFilterResponse(httpRes, response, httpMethod, absoluteUrl, request); return response; }); } return httpRes.Content.ReadAsStringAsync().ContinueWith(task => { ThrowIfError(task, httpRes, request, absoluteUrl, task.Result); var body = task.Result; using (__requestAccess()) { var response = body.FromJson(); if (ResultsFilterResponse != null) ResultsFilterResponse(httpRes, response, httpMethod, absoluteUrl, request); return response; } }); }).Unwrap(); } private void DisposeCancelToken() { if (Interlocked.Decrement(ref activeAsyncRequests) > 0) return; if (CancelTokenSource == null) return; CancelTokenSource.Dispose(); CancelTokenSource = null; } private class AccessToken { private string token; internal static readonly AccessToken __accessToken = new AccessToken("lUjBZNG56eE9yd3FQdVFSTy9qeGl5dlI5RmZwamc4U05udl000"); private AccessToken(string token) { this.token = token; } } protected static IDisposable __requestAccess() { return LicenseUtils.RequestAccess(AccessToken.__accessToken, LicenseFeature.Client, LicenseFeature.Text); } public Action RequestFilter { get; set; } public static Action GlobalRequestFilter { get; set; } private void ApplyWebRequestFilters(HttpRequestMessage httpReq) { if (RequestFilter != null) RequestFilter(httpReq); if (GlobalRequestFilter != null) GlobalRequestFilter(httpReq); } public Action ResponseFilter { get; set; } public static Action GlobalResponseFilter { get; set; } private void ApplyWebResponseFilters(HttpResponseMessage httpRes) { if (ResponseFilter != null) ResponseFilter(httpRes); if (GlobalResponseFilter != null) GlobalResponseFilter(httpRes); } private void ThrowIfError(Task task, HttpResponseMessage httpRes, object request, string requestUri, object response) { DisposeCancelToken(); if (task.IsFaulted) throw CreateException(httpRes, task.Exception); if (!httpRes.IsSuccessStatusCode) ThrowResponseTypeException(httpRes, request, requestUri, response); } private void AddBasicAuth(HttpClient client) { if (string.IsNullOrEmpty(UserName) || string.IsNullOrEmpty(Password)) return; var byteArray = Encoding.UTF8.GetBytes("{0}:{1}".Fmt(UserName, Password)); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray)); } protected T ResultFilter(T response, HttpResponseMessage httpRes, string httpMethod, string requestUri, object request) { if (ResultsFilterResponse != null) { ResultsFilterResponse(httpRes, response, httpMethod, requestUri, request); } return response; } private static WebServiceException CreateException(HttpResponseMessage httpRes, Exception ex) { return new WebServiceException(); } readonly ConcurrentDictionary> ResponseHandlers = new ConcurrentDictionary>(); private void ThrowResponseTypeException(HttpResponseMessage httpRes, object request, string requestUri, object response) { var responseType = WebRequestUtils.GetErrorResponseDtoType(request); Action responseHandler; if (!ResponseHandlers.TryGetValue(responseType, out responseHandler)) { var mi = GetType().GetInstanceMethod("ThrowWebServiceException") .MakeGenericMethod(new[] { responseType }); responseHandler = (Action)mi.CreateDelegate( typeof(Action), this); ResponseHandlers[responseType] = responseHandler; } responseHandler(httpRes, request, requestUri, response); } public byte[] GetResponseBytes(object response) { var stream = response as Stream; if (stream != null) return stream.ReadFully(); var bytes = response as byte[]; if (bytes != null) return bytes; var str = response as string; if (str != null) return str.ToUtf8Bytes(); return null; } public void ThrowWebServiceException(HttpResponseMessage httpRes, object request, string requestUri, object response) { if (log.IsDebugEnabled) { log.DebugFormat("Status Code : {0}", httpRes.StatusCode); log.DebugFormat("Status Description : {0}", httpRes.ReasonPhrase); } var serviceEx = new WebServiceException(httpRes.ReasonPhrase) { StatusCode = (int)httpRes.StatusCode, StatusDescription = httpRes.ReasonPhrase, ResponseHeaders = httpRes.Headers.ToWebHeaderCollection() }; try { var contentType = httpRes.GetContentType(); var bytes = GetResponseBytes(response); if (bytes != null) { if (string.IsNullOrEmpty(contentType) || contentType.MatchesContentType(ContentType)) { using (__requestAccess()) { var stream = MemoryStreamFactory.GetStream(bytes); serviceEx.ResponseBody = bytes.FromUtf8Bytes(); serviceEx.ResponseDto = JsonSerializer.DeserializeFromStream(stream); if (stream.CanRead) stream.Dispose(); //alt ms throws when you dispose twice } } else { serviceEx.ResponseBody = bytes.FromUtf8Bytes(); } } } catch (Exception innerEx) { // Oh, well, we tried throw new WebServiceException(httpRes.ReasonPhrase, innerEx) { StatusCode = (int)httpRes.StatusCode, StatusDescription = httpRes.ReasonPhrase, ResponseBody = serviceEx.ResponseBody }; } //Escape deserialize exception handling and throw here throw serviceEx; //var authEx = ex as AuthenticationException; //if (authEx != null) //{ // throw WebRequestUtils.CreateCustomException(requestUri, authEx); //} } public T GetSyncResponse(Task task) { try { return task.Result; } catch (Exception ex) { throw ex.UnwrapIfSingleException(); } } public void WaitSyncResponse(Task task) { try { task.Wait(); } catch (Exception ex) { throw ex.UnwrapIfSingleException(); } } public virtual Task SendAsync(IReturn requestDto) { return SendAsync((object)requestDto); } public virtual Task SendAsync(object request) { var requestUri = this.SyncReplyBaseUri.WithTrailingSlash() + request.GetType().Name; var httpMethod = ServiceClientBase.GetExplicitMethod(request) ?? DefaultHttpMethod; return SendAsync(httpMethod, requestUri, request); } public virtual Task SendAsync(IReturnVoid requestDto) { return SendAsync(requestDto); } public virtual Task> SendAllAsync(IEnumerable> requests) { var elType = requests.GetType().GetCollectionType(); var requestUri = this.SyncReplyBaseUri.WithTrailingSlash() + elType.Name + "[]"; return SendAsync>(HttpMethods.Post, requestUri, requests); } public Task GetAsync(IReturn requestDto) { return GetAsync(requestDto.ToUrl(HttpMethods.Get, Format)); } public Task GetAsync(object requestDto) { return GetAsync(requestDto.ToUrl(HttpMethods.Get, Format)); } public Task GetAsync(string relativeOrAbsoluteUrl) { return SendAsync(HttpMethods.Get, GetBaseUrl(relativeOrAbsoluteUrl), null); } public Task GetAsync(IReturnVoid requestDto) { return GetAsync(requestDto.ToUrl(HttpMethods.Get, Format)); } public Task DeleteAsync(IReturn requestDto) { return DeleteAsync(requestDto.ToUrl(HttpMethods.Delete, Format)); } public Task DeleteAsync(object requestDto) { return DeleteAsync(requestDto.ToUrl(HttpMethods.Delete, Format)); } public Task DeleteAsync(string relativeOrAbsoluteUrl) { return SendAsync(HttpMethods.Delete, GetBaseUrl(relativeOrAbsoluteUrl), null); } public Task DeleteAsync(IReturnVoid requestDto) { return DeleteAsync(requestDto.ToUrl(HttpMethods.Delete, Format)); } public Task PostAsync(IReturn requestDto) { return PostAsync(requestDto.ToUrl(HttpMethods.Post, Format), requestDto); } public Task PostAsync(object requestDto) { return PostAsync(requestDto.ToUrl(HttpMethods.Post, Format), requestDto); } public Task PostAsync(string relativeOrAbsoluteUrl, object request) { return SendAsync(HttpMethods.Post, GetBaseUrl(relativeOrAbsoluteUrl), request); } public Task PostAsync(IReturnVoid requestDto) { return PostAsync(requestDto.ToUrl(HttpMethods.Post, Format), requestDto); } public Task PutAsync(IReturn requestDto) { return PutAsync(requestDto.ToUrl(HttpMethods.Put, Format), requestDto); } public Task PutAsync(object requestDto) { return PutAsync(requestDto.ToUrl(HttpMethods.Put, Format), requestDto); } public Task PutAsync(string relativeOrAbsoluteUrl, object request) { return SendAsync(HttpMethods.Put, GetBaseUrl(relativeOrAbsoluteUrl), request); } public Task PutAsync(IReturnVoid requestDto) { return PutAsync(requestDto.ToUrl(HttpMethods.Put, Format), requestDto); } public Task CustomMethodAsync(string httpVerb, IReturn requestDto) { if (!HttpMethods.HasVerb(httpVerb)) throw new NotSupportedException("Unknown HTTP Method is not supported: " + httpVerb); var requestBody = httpVerb.HasRequestBody() ? requestDto : null; return SendAsync(httpVerb, GetBaseUrl(requestDto.ToUrl(httpVerb, Format)), requestBody); } public Task CustomMethodAsync(string httpVerb, object requestDto) { if (!HttpMethods.HasVerb(httpVerb)) throw new NotSupportedException("Unknown HTTP Method is not supported: " + httpVerb); var requestBody = httpVerb.HasRequestBody() ? requestDto : null; return SendAsync(httpVerb, GetBaseUrl(requestDto.ToUrl(httpVerb, Format)), requestBody); } public Task CustomMethodAsync(string httpVerb, IReturnVoid requestDto) { if (!HttpMethods.HasVerb(httpVerb)) throw new NotSupportedException("Unknown HTTP Method is not supported: " + httpVerb); var requestBody = httpVerb.HasRequestBody() ? requestDto : null; return SendAsync(httpVerb, GetBaseUrl(requestDto.ToUrl(httpVerb, Format)), requestBody); } public Task CustomMethodAsync(string httpVerb, string relativeOrAbsoluteUrl, object request) { if (!HttpMethods.HasVerb(httpVerb)) throw new NotSupportedException("Unknown HTTP Method is not supported: " + httpVerb); var requestBody = httpVerb.HasRequestBody() ? request : null; return SendAsync(httpVerb, GetBaseUrl(relativeOrAbsoluteUrl), requestBody); } public void SendOneWay(object request) { var requestUri = this.AsyncOneWayBaseUri.WithTrailingSlash() + request.GetType().Name; var httpMethod = ServiceClientBase.GetExplicitMethod(request) ?? DefaultHttpMethod; SendOneWay(httpMethod, requestUri, request); } public void SendOneWay(string relativeOrAbsoluteUri, object request) { SendOneWay(HttpMethods.Post, relativeOrAbsoluteUri, request); } public virtual void SendOneWay(string httpMethod, string relativeOrAbsoluteUrl, object requestDto) { var requestUri = GetBaseUrl(relativeOrAbsoluteUrl); SendAsync(httpMethod, requestUri, requestDto).Wait(); } public void SendAllOneWay(IEnumerable requests) { var elType = requests.GetType().GetCollectionType(); var requestUri = this.AsyncOneWayBaseUri.WithTrailingSlash() + elType.Name + "[]"; SendOneWay(HttpMethods.Post, requestUri, requests); } public void ClearCookies() { CookieContainer = new CookieContainer(); HttpClient = null; GetHttpClient(); } public Dictionary GetCookieValues() { return CookieContainer.ToDictionary(BaseUri); } public void SetCookie(string name, string value, TimeSpan? expiresIn = null) { this.SetCookie(HttpClient.BaseAddress, name, value, expiresIn != null ? DateTime.UtcNow.Add(expiresIn.Value) : (DateTime?)null); } public void Get(IReturnVoid request) { WaitSyncResponse(GetAsync(request)); } public TResponse Get(IReturn request) { return GetSyncResponse(GetAsync(request)); } public TResponse Get(object request) { return GetSyncResponse(GetAsync(request)); } public TResponse Get(string relativeOrAbsoluteUrl) { return GetSyncResponse(GetAsync(relativeOrAbsoluteUrl)); } public IEnumerable GetLazy(IReturn> queryDto) { throw new NotImplementedException(); } public void Delete(IReturnVoid requestDto) { WaitSyncResponse(DeleteAsync(requestDto)); } public TResponse Delete(IReturn request) { return GetSyncResponse(DeleteAsync(request)); } public TResponse Delete(object request) { return GetSyncResponse(DeleteAsync(request)); } public TResponse Delete(string relativeOrAbsoluteUrl) { return GetSyncResponse(DeleteAsync(relativeOrAbsoluteUrl)); } public void Post(IReturnVoid requestDto) { WaitSyncResponse(PostAsync(requestDto)); } public TResponse Post(IReturn request) { return GetSyncResponse(PostAsync(request)); } public TResponse Post(object request) { return GetSyncResponse(PostAsync(request)); } public TResponse Post(string relativeOrAbsoluteUrl, object request) { return GetSyncResponse(PostAsync(relativeOrAbsoluteUrl, request)); } public void Put(IReturnVoid requestDto) { WaitSyncResponse(PutAsync(requestDto)); } public TResponse Put(IReturn request) { return GetSyncResponse(PutAsync(request)); } public TResponse Put(object request) { return GetSyncResponse(PutAsync(request)); } public TResponse Put(string relativeOrAbsoluteUrl, object request) { return GetSyncResponse(PutAsync(relativeOrAbsoluteUrl, request)); } public void Patch(IReturnVoid request) { WaitSyncResponse(SendAsync(HttpMethods.Patch, request.ToUrl(HttpMethods.Patch, Format), null)); } public TResponse Patch(IReturn request) { return GetSyncResponse(SendAsync(HttpMethods.Patch, request.ToUrl(HttpMethods.Patch, Format), request)); } public TResponse Patch(object request) { return GetSyncResponse(SendAsync(HttpMethods.Patch, request.ToUrl(HttpMethods.Patch, Format), request)); } public TResponse Patch(string relativeOrAbsoluteUrl, object request) { return GetSyncResponse(SendAsync(HttpMethods.Patch, relativeOrAbsoluteUrl, request)); } public void CustomMethod(string httpVerb, IReturnVoid request) { WaitSyncResponse(SendAsync(httpVerb, request.ToUrl(httpVerb, Format), request)); } public TResponse CustomMethod(string httpVerb, IReturn request) { return GetSyncResponse(SendAsync(httpVerb, request.ToUrl(httpVerb, Format), request)); } public TResponse CustomMethod(string httpVerb, object request) { return GetSyncResponse(SendAsync(httpVerb, request.ToUrl(httpVerb, Format), null)); } public Task PostFileAsync(string relativeOrAbsoluteUrl, Stream fileToUpload, string fileName, string mimeType = null) { var content = new MultipartFormDataContent(); var fileBytes = fileToUpload.ReadFully(); var fileContent = new ByteArrayContent(fileBytes, 0, fileBytes.Length); fileContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") { Name = "file", FileName = fileName }; fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(mimeType ?? MimeTypes.GetMimeType(fileName)); content.Add(fileContent, "file", fileName); return SendAsync(HttpMethods.Post, GetBaseUrl(relativeOrAbsoluteUrl), content) .ContinueWith(t => { content.Dispose(); fileContent.Dispose(); return t.Result; }, TaskContinuationOptions.ExecuteSynchronously); } public TResponse PostFile(string relativeOrAbsoluteUrl, Stream fileToUpload, string fileName, string mimeType) { return GetSyncResponse(PostFileAsync(relativeOrAbsoluteUrl, fileToUpload, fileName, mimeType)); } public Task PostFileWithRequestAsync(Stream fileToUpload, string fileName, object request, string fieldName = "upload") { return PostFileWithRequestAsync(request.ToPostUrl(), fileToUpload, fileName, request, fieldName); } public TResponse PostFileWithRequest(Stream fileToUpload, string fileName, object request, string fieldName = "upload") { return GetSyncResponse(PostFileWithRequestAsync(fileToUpload, fileName, request, fileName)); } public Task PostFileWithRequestAsync(string relativeOrAbsoluteUrl, Stream fileToUpload, string fileName, object request, string fieldName = "upload") { var queryString = QueryStringSerializer.SerializeToString(request); var nameValueCollection = PclExportClient.Instance.ParseQueryString(queryString); var content = new MultipartFormDataContent(); foreach (string key in nameValueCollection) { var value = nameValueCollection[key]; content.Add(new StringContent(value), "\"{0}\"".Fmt(key)); } var fileBytes = fileToUpload.ReadFully(); var fileContent = new ByteArrayContent(fileBytes, 0, fileBytes.Length); fileContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") { Name = "file", FileName = fileName }; fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MimeTypes.GetMimeType(fileName)); content.Add(fileContent, "file", fileName); return SendAsync(HttpMethods.Post, GetBaseUrl(relativeOrAbsoluteUrl), content) .ContinueWith(t => { content.Dispose(); fileContent.Dispose(); return t.Result; }, TaskContinuationOptions.ExecuteSynchronously); } public TResponse PostFileWithRequest(string relativeOrAbsoluteUrl, Stream fileToUpload, string fileName, object request, string fieldName = "upload") { return GetSyncResponse(PostFileWithRequestAsync(relativeOrAbsoluteUrl, fileToUpload, fileName, request, fileName)); } public TResponse Send(object request) { return GetSyncResponse(SendAsync(request)); } public virtual TResponse Send(IReturn request) { return Send((object)request); } public virtual void Send(IReturnVoid request) { SendOneWay(request); } public List SendAll(IEnumerable> requests) { return GetSyncResponse(SendAllAsync(requests)); } public void CancelAsync() { CancelTokenSource.Cancel(); } public void Dispose() { } } public delegate object ResultsFilterHttpDelegate(Type responseType, string httpMethod, string requestUri, object request); public delegate void ResultsFilterHttpResponseDelegate(HttpResponseMessage webResponse, object response, string httpMethod, string requestUri, object request); public static class JsonHttpClientUtils { public static Dictionary ToDictionary(this HttpResponseHeaders headers) { var to = new Dictionary(); foreach (var header in headers) { to[header.Key] = string.Join(", ", header.Value); } return to; } public static WebHeaderCollection ToWebHeaderCollection(this HttpResponseHeaders headers) { var to = new WebHeaderCollection(); foreach (var header in headers) { to[header.Key] = string.Join(", ", header.Value); } return to; } public static string GetContentType(this HttpResponseMessage httpRes) { IEnumerable values; return httpRes.Headers.TryGetValues(HttpHeaders.ContentType, out values) ? values.FirstOrDefault() : null; } } }