diff --git a/Storage/Storage.Test/ObjectControllerTests.cs b/Storage/Storage.Test/ObjectControllerTests.cs new file mode 100644 index 0000000..fc99e8f --- /dev/null +++ b/Storage/Storage.Test/ObjectControllerTests.cs @@ -0,0 +1,31 @@ +using NUnit.Framework; +using System.Threading; +using System.Threading.Tasks; +using System.Runtime.CompilerServices; +using LeanCloud; + +namespace LeanCloudTests { + public class ObjectControllerTests { + [SetUp] + public void SetUp() { + AVClient.Initialize(new AVClient.Configuration { + ApplicationId = "BMYV4RKSTwo8WSqt8q9ezcWF-gzGzoHsz", + ApplicationKey = "pbf6Nk5seyjilexdpyrPwjSp", + }); + AVClient.HttpLog(TestContext.Out.WriteLine); + } + + [Test] + [AsyncStateMachine(typeof(ObjectControllerTests))] + public async Task TestSave() { + TestContext.Out.WriteLine($"before at {Thread.CurrentThread.ManagedThreadId}"); + var obj = AVObject.Create("Foo"); + obj["content"] = "hello, world"; + await obj.SaveAsync(); + TestContext.Out.WriteLine($"saved at {Thread.CurrentThread.ManagedThreadId}"); + Assert.NotNull(obj.ObjectId); + Assert.NotNull(obj.CreatedAt); + Assert.NotNull(obj.UpdatedAt); + } + } +} diff --git a/Storage/Storage.Test/ObjectTest.cs b/Storage/Storage.Test/ObjectTest.cs new file mode 100644 index 0000000..49bb1ca --- /dev/null +++ b/Storage/Storage.Test/ObjectTest.cs @@ -0,0 +1,58 @@ +using NUnit.Framework; +using LeanCloud; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Runtime.CompilerServices; + +namespace LeanCloudTests { + public class ObjectTests { + [SetUp] + public void SetUp() { + AVClient.Initialize(new AVClient.Configuration { + ApplicationId = "BMYV4RKSTwo8WSqt8q9ezcWF-gzGzoHsz", + ApplicationKey = "pbf6Nk5seyjilexdpyrPwjSp", + RTMServer = "https://router-g0-push.avoscloud.com", + }); + AVClient.HttpLog(TestContext.Out.WriteLine); + } + + [Test] + public void TestAVObjectConstructor() { + AVObject obj = new AVObject("Foo"); + Assert.AreEqual("Foo", obj.ClassName); + Assert.Null(obj.CreatedAt); + Assert.True(obj.IsDataAvailable); + Assert.True(obj.IsDirty); + } + + [Test] + public void TestAVObjectCreate() { + AVObject obj = AVObject.CreateWithoutData("Foo", "5d356b1cd5de2b00837162ca"); + Assert.AreEqual("Foo", obj.ClassName); + Assert.AreEqual("5d356b1cd5de2b00837162ca", obj.ObjectId); + Assert.Null(obj.CreatedAt); + Assert.False(obj.IsDataAvailable); + Assert.False(obj.IsDirty); + } + + [Test] + public async Task TestHttp() { + if (SynchronizationContext.Current == null) { + TestContext.Out.WriteLine("is null"); + } + TestContext.Out.WriteLine($"current {SynchronizationContext.Current}"); + var client = new HttpClient(); + TestContext.Out.WriteLine($"request at {Thread.CurrentThread.ManagedThreadId}"); + string url = $"{AVClient.CurrentConfiguration.RTMServer}/v1/route?appId={AVClient.CurrentConfiguration.ApplicationId}&secure=1"; + var res = await client.GetAsync(url); + TestContext.Out.WriteLine($"get at {Thread.CurrentThread.ManagedThreadId}"); + var data = await res.Content.ReadAsStringAsync(); + res.Dispose(); + TestContext.Out.WriteLine($"response at {Thread.CurrentThread.ManagedThreadId}"); + TestContext.Out.WriteLine(data); + Assert.Pass(); + } + + } +} \ No newline at end of file diff --git a/Storage/Storage.Test/Storage.Test.csproj b/Storage/Storage.Test/Storage.Test.csproj index fcae868..b44ec62 100644 --- a/Storage/Storage.Test/Storage.Test.csproj +++ b/Storage/Storage.Test/Storage.Test.csproj @@ -1,48 +1,18 @@ - - - + + - Debug - AnyCPU - {04DA35BB-6473-4D99-8A33-F499D40047E6} - Library - Storage.Test - Storage.Test - v4.7 - 0.1.0 - - - true - full - false - bin\Debug - DEBUG; - prompt - 4 - - - true - bin\Release - prompt - 4 + netcoreapp2.2 + + false + - - - ..\..\packages\NUnit.3.12.0\lib\net45\nunit.framework.dll - + + + + - + - - - - - - {659D19F0-9A40-42C0-886C-555E64F16848} - Storage.PCL - - - - \ No newline at end of file + diff --git a/Storage/Storage.Test/Test.cs b/Storage/Storage.Test/Test.cs deleted file mode 100644 index e2edb6b..0000000 --- a/Storage/Storage.Test/Test.cs +++ /dev/null @@ -1,21 +0,0 @@ -using NUnit.Framework; -using System; -using System.Reflection; -using System.Diagnostics; -using LeanCloud; - -namespace Storage.Test { - [TestFixture()] - public class Test { - [Test()] - public void TestCase() { - Assembly assembly = Assembly.GetEntryAssembly(); - var attr = assembly.GetCustomAttribute(); - Console.WriteLine(attr.InformationalVersion); - - FileVersionInfo versionInfo = FileVersionInfo.GetVersionInfo(assembly.Location); - String version = versionInfo.FileVersion; - Console.WriteLine(version); - } - } -} diff --git a/Storage/Storage.Test/packages.config b/Storage/Storage.Test/packages.config deleted file mode 100644 index ded08cf..0000000 --- a/Storage/Storage.Test/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/Storage/Storage/Internal/AVCorePlugins.cs b/Storage/Storage/Internal/AVCorePlugins.cs new file mode 100644 index 0000000..3b2fd06 --- /dev/null +++ b/Storage/Storage/Internal/AVCorePlugins.cs @@ -0,0 +1,384 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal +{ + public class AVPlugins : IAVCorePlugins + { + private static readonly object instanceMutex = new object(); + private static IAVCorePlugins instance; + public static IAVCorePlugins Instance + { + get + { + lock (instanceMutex) + { + instance = instance ?? new AVPlugins(); + return instance; + } + } + set + { + lock (instanceMutex) + { + instance = value; + } + } + } + + private readonly object mutex = new object(); + + #region Server Controllers + + private IHttpClient httpClient; + private IAppRouterController appRouterController; + private IAVCommandRunner commandRunner; + private IStorageController storageController; + + private IAVCloudCodeController cloudCodeController; + private IAVConfigController configController; + private IAVFileController fileController; + private IAVObjectController objectController; + private IAVQueryController queryController; + private IAVSessionController sessionController; + private IAVUserController userController; + private IObjectSubclassingController subclassingController; + + #endregion + + #region Current Instance Controller + + private IAVCurrentUserController currentUserController; + private IInstallationIdController installationIdController; + + #endregion + + public void Reset() + { + lock (mutex) + { + HttpClient = null; + AppRouterController = null; + CommandRunner = null; + StorageController = null; + + CloudCodeController = null; + FileController = null; + ObjectController = null; + SessionController = null; + UserController = null; + SubclassingController = null; + + CurrentUserController = null; + InstallationIdController = null; + } + } + + public IHttpClient HttpClient + { + get + { + lock (mutex) + { + httpClient = httpClient ?? new HttpClient(); + return httpClient; + } + } + set + { + lock (mutex) + { + httpClient = value; + } + } + } + + public IAppRouterController AppRouterController + { + get + { + lock (mutex) + { + appRouterController = appRouterController ?? new AppRouterController(); + return appRouterController; + } + } + set + { + lock (mutex) + { + appRouterController = value; + } + } + } + + public IAVCommandRunner CommandRunner + { + get + { + lock (mutex) + { + commandRunner = commandRunner ?? new AVCommandRunner(HttpClient, InstallationIdController); + return commandRunner; + } + } + set + { + lock (mutex) + { + commandRunner = value; + } + } + } + +#if !UNITY + public IStorageController StorageController + { + get + { + lock (mutex) + { + storageController = storageController ?? new StorageController(AVClient.CurrentConfiguration.ApplicationId); + return storageController; + } + } + set + { + lock (mutex) + { + storageController = value; + } + } + } +#endif +#if UNITY + public IStorageController StorageController + { + get + { + lock (mutex) + { + storageController = storageController ?? new StorageController(AVInitializeBehaviour.IsWebPlayer, AVClient.CurrentConfiguration.ApplicationId); + return storageController; + } + } + set + { + lock (mutex) + { + storageController = value; + } + } + } +#endif + + public IAVCloudCodeController CloudCodeController + { + get + { + lock (mutex) + { + cloudCodeController = cloudCodeController ?? new AVCloudCodeController(CommandRunner); + return cloudCodeController; + } + } + set + { + lock (mutex) + { + cloudCodeController = value; + } + } + } + + public IAVFileController FileController + { + get + { + lock (mutex) + { + if (AVClient.CurrentConfiguration.RegionValue == 0) + fileController = fileController ?? new QiniuFileController(CommandRunner); + else if (AVClient.CurrentConfiguration.RegionValue == 2) + fileController = fileController ?? new QCloudCosFileController(CommandRunner); + else if (AVClient.CurrentConfiguration.RegionValue == 1) + fileController = fileController ?? new AWSS3FileController(CommandRunner); + + return fileController; + } + } + set + { + lock (mutex) + { + fileController = value; + } + } + } + + public IAVConfigController ConfigController + { + get + { + lock (mutex) + { + if (configController == null) + { + configController = new AVConfigController(CommandRunner, StorageController); + } + return configController; + } + } + set + { + lock (mutex) + { + configController = value; + } + } + } + + public IAVObjectController ObjectController + { + get + { + lock (mutex) + { + objectController = objectController ?? new AVObjectController(CommandRunner); + return objectController; + } + } + set + { + lock (mutex) + { + objectController = value; + } + } + } + + public IAVQueryController QueryController + { + get + { + lock (mutex) + { + if (queryController == null) + { + queryController = new AVQueryController(CommandRunner); + } + return queryController; + } + } + set + { + lock (mutex) + { + queryController = value; + } + } + } + + public IAVSessionController SessionController + { + get + { + lock (mutex) + { + sessionController = sessionController ?? new AVSessionController(CommandRunner); + return sessionController; + } + } + set + { + lock (mutex) + { + sessionController = value; + } + } + } + + public IAVUserController UserController + { + get + { + lock (mutex) + { + userController = userController ?? new AVUserController(CommandRunner); + return userController; + } + } + set + { + lock (mutex) + { + userController = value; + } + } + } + + public IAVCurrentUserController CurrentUserController + { + get + { + lock (mutex) + { + currentUserController = currentUserController ?? new AVCurrentUserController(StorageController); + return currentUserController; + } + } + set + { + lock (mutex) + { + currentUserController = value; + } + } + } + + public IObjectSubclassingController SubclassingController + { + get + { + lock (mutex) + { + if (subclassingController == null) + { + subclassingController = new ObjectSubclassingController(); + subclassingController.AddRegisterHook(typeof(AVUser), () => CurrentUserController.ClearFromMemory()); + } + return subclassingController; + } + } + set + { + lock (mutex) + { + subclassingController = value; + } + } + } + + public IInstallationIdController InstallationIdController + { + get + { + lock (mutex) + { + installationIdController = installationIdController ?? new InstallationIdController(StorageController); + return installationIdController; + } + } + set + { + lock (mutex) + { + installationIdController = value; + } + } + } + } +} diff --git a/Storage/Storage/Internal/AppRouter/AppRouterController.cs b/Storage/Storage/Internal/AppRouter/AppRouterController.cs new file mode 100644 index 0000000..1e0c73d --- /dev/null +++ b/Storage/Storage/Internal/AppRouter/AppRouterController.cs @@ -0,0 +1,103 @@ +using System; +using System.Net; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal +{ + public class AppRouterController : IAppRouterController + { + private AppRouterState currentState; + private object mutex = new object(); + + /// + /// Get current app's router state + /// + /// + public AppRouterState Get() + { + if (string.IsNullOrEmpty(AVClient.CurrentConfiguration.ApplicationId)) + { + throw new AVException(AVException.ErrorCode.NotInitialized, "ApplicationId can not be null."); + } + AppRouterState state; + state = AppRouterState.GetInitial(AVClient.CurrentConfiguration.ApplicationId, AVClient.CurrentConfiguration.Region); + + lock (mutex) + { + if (currentState != null) + { + if (!currentState.isExpired()) + state = currentState; + } + } + + if (state.isExpired()) + { + lock (mutex) + { + state.FetchedAt = DateTime.Now + TimeSpan.FromMinutes(10); + } + Task.Factory.StartNew(RefreshAsync); + } + return state; + } + + public Task RefreshAsync() + { + return QueryAsync(CancellationToken.None).ContinueWith(t => + { + if (!t.IsFaulted && !t.IsCanceled) + { + lock (mutex) + { + currentState = t.Result; + } + } + }); + } + + public Task QueryAsync(CancellationToken cancellationToken) + { + string appId = AVClient.CurrentConfiguration.ApplicationId; + string url = string.Format("https://app-router.leancloud.cn/2/route?appId={0}", appId); + + return AVClient.HttpGetAsync(new Uri(url)).ContinueWith(t => + { + var tcs = new TaskCompletionSource(); + if (t.Result.Item1 != HttpStatusCode.OK) + { + tcs.SetException(new AVException(AVException.ErrorCode.ConnectionFailed, "can not reach router.", null)); + } + else + { + var result = Json.Parse(t.Result.Item2) as IDictionary; + tcs.SetResult(ParseAppRouterState(result)); + } + return tcs.Task; + }).Unwrap(); + } + + private static AppRouterState ParseAppRouterState(IDictionary jsonObj) + { + var state = new AppRouterState() + { + TTL = (int)jsonObj["ttl"], + StatsServer = jsonObj["stats_server"] as string, + RealtimeRouterServer = jsonObj["rtm_router_server"] as string, + PushServer = jsonObj["push_server"] as string, + EngineServer = jsonObj["engine_server"] as string, + ApiServer = jsonObj["api_server"] as string, + Source = "network", + }; + return state; + } + + public void Clear() + { + currentState = null; + } + } +} diff --git a/Storage/Storage/Internal/AppRouter/AppRouterState.cs b/Storage/Storage/Internal/AppRouter/AppRouterState.cs new file mode 100644 index 0000000..b2e61f9 --- /dev/null +++ b/Storage/Storage/Internal/AppRouter/AppRouterState.cs @@ -0,0 +1,85 @@ +using System; + +namespace LeanCloud.Storage.Internal +{ + public class AppRouterState + { + public long TTL { get; internal set; } + public string ApiServer { get; internal set; } + public string EngineServer { get; internal set; } + public string PushServer { get; internal set; } + public string RealtimeRouterServer { get; internal set; } + public string StatsServer { get; internal set; } + public string Source { get; internal set; } + + public DateTime FetchedAt { get; internal set; } + + private static object mutex = new object(); + + public AppRouterState() + { + FetchedAt = DateTime.Now; + } + + /// + /// Is this app router state expired. + /// + public bool isExpired() + { + return DateTime.Now > FetchedAt + TimeSpan.FromSeconds(TTL); + } + + /// + /// Get the initial usable router state + /// + /// Current app's appId + /// Current app's region + /// Initial app router state + public static AppRouterState GetInitial(string appId, AVClient.Configuration.AVRegion region) + { + var regionValue = (int)region; + var prefix = appId.Substring(0, 8).ToLower(); + switch (regionValue) + { + case 0: + // 华北 + return new AppRouterState() + { + TTL = -1, + ApiServer = String.Format("{0}.api.lncld.net", prefix), + EngineServer = String.Format("{0}.engine.lncld.net", prefix), + PushServer = String.Format("{0}.push.lncld.net", prefix), + RealtimeRouterServer = String.Format("{0}.rtm.lncld.net", prefix), + StatsServer = String.Format("{0}.stats.lncld.net", prefix), + Source = "initial", + }; + case 1: + // 美国 + return new AppRouterState() + { + TTL = -1, + ApiServer = string.Format("{0}.api.lncldglobal.com", prefix), + EngineServer = string.Format("{0}.engine.lncldglobal.com", prefix), + PushServer = string.Format("{0}.push.lncldglobal.com", prefix), + RealtimeRouterServer = string.Format("{0}.rtm.lncldglobal.com", prefix), + StatsServer = string.Format("{0}.stats.lncldglobal.com", prefix), + Source = "initial", + }; + case 2: + // 华东 + return new AppRouterState() { + TTL = -1, + ApiServer = string.Format("{0}.api.lncldapi.com", prefix), + EngineServer = string.Format("{0}.engine.lncldapi.com", prefix), + PushServer = string.Format("{0}.push.lncldapi.com", prefix), + RealtimeRouterServer = string.Format("{0}.rtm.lncldapi.com", prefix), + StatsServer = string.Format("{0}.stats.lncldapi.com", prefix), + Source = "initial", + }; + default: + throw new AVException(AVException.ErrorCode.OtherCause, "invalid region"); + } + } + + } +} \ No newline at end of file diff --git a/Storage/Storage/Internal/AppRouter/IAppRouterController.cs b/Storage/Storage/Internal/AppRouter/IAppRouterController.cs new file mode 100644 index 0000000..771ee18 --- /dev/null +++ b/Storage/Storage/Internal/AppRouter/IAppRouterController.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal +{ + public interface IAppRouterController + { + AppRouterState Get(); + /// + /// Start refresh the app router. + /// + /// + Task RefreshAsync(); + void Clear(); + /// + /// Query the app router. + /// + /// New AppRouterState + Task QueryAsync(CancellationToken cancellationToken); + } +} diff --git a/Storage/Storage/Internal/Authentication/IAVAuthenticationProvider.cs b/Storage/Storage/Internal/Authentication/IAVAuthenticationProvider.cs new file mode 100644 index 0000000..406e37c --- /dev/null +++ b/Storage/Storage/Internal/Authentication/IAVAuthenticationProvider.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + public interface IAVAuthenticationProvider { + /// + /// Authenticates with the service. + /// + /// The cancellation token. + Task> AuthenticateAsync(CancellationToken cancellationToken); + + /// + /// Deauthenticates (logs out) the user associated with this provider. This + /// call may block. + /// + void Deauthenticate(); + + /// + /// Restores authentication that has been serialized, such as session keys, + /// etc. + /// + /// The auth data for the provider. This value may be null + /// when unlinking an account. + /// true iff the authData was successfully synchronized. A false return + /// value indicates that the user should no longer be associated because of bad auth + /// data. + bool RestoreAuthentication(IDictionary authData); + + /// + /// Provides a unique name for the type of authentication the provider does. + /// For example, the FacebookAuthenticationProvider would return "facebook". + /// + string AuthType { get; } + } +} diff --git a/Storage/Storage/Internal/Cloud/Controller/AVCloudCodeController.cs b/Storage/Storage/Internal/Cloud/Controller/AVCloudCodeController.cs new file mode 100644 index 0000000..c6fa216 --- /dev/null +++ b/Storage/Storage/Internal/Cloud/Controller/AVCloudCodeController.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using LeanCloud.Utilities; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal +{ + public class AVCloudCodeController : IAVCloudCodeController + { + private readonly IAVCommandRunner commandRunner; + + public AVCloudCodeController(IAVCommandRunner commandRunner) + { + this.commandRunner = commandRunner; + } + + public Task CallFunctionAsync(String name, + IDictionary parameters, + string sessionToken, + CancellationToken cancellationToken) + { + var command = new AVCommand(string.Format("functions/{0}", Uri.EscapeUriString(name)), + method: "POST", + sessionToken: sessionToken, + data: NoObjectsEncoder.Instance.Encode(parameters) as IDictionary); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + { + var decoded = AVDecoder.Instance.Decode(t.Result.Item2) as IDictionary; + if (!decoded.ContainsKey("result")) + { + return default(T); + } + return Conversion.To(decoded["result"]); + }); + } + + public Task RPCFunction(string name, IDictionary parameters, string sessionToken, CancellationToken cancellationToken) + { + var command = new AVCommand(string.Format("call/{0}", Uri.EscapeUriString(name)), + method: "POST", + sessionToken: sessionToken, + data: PointerOrLocalIdEncoder.Instance.Encode(parameters) as IDictionary); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + { + var decoded = AVDecoder.Instance.Decode(t.Result.Item2) as IDictionary; + if (!decoded.ContainsKey("result")) + { + return default(T); + } + return Conversion.To(decoded["result"]); + }); + } + } +} diff --git a/Storage/Storage/Internal/Cloud/Controller/IAVCloudCodeController.cs b/Storage/Storage/Internal/Cloud/Controller/IAVCloudCodeController.cs new file mode 100644 index 0000000..ad6138f --- /dev/null +++ b/Storage/Storage/Internal/Cloud/Controller/IAVCloudCodeController.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal +{ + public interface IAVCloudCodeController + { + Task CallFunctionAsync(String name, + IDictionary parameters, + string sessionToken, + CancellationToken cancellationToken); + + Task RPCFunction(string name, IDictionary parameters, + string sessionToken, + CancellationToken cancellationToken); + } +} diff --git a/Storage/Storage/Internal/Command/AVCommand.cs b/Storage/Storage/Internal/Command/AVCommand.cs new file mode 100644 index 0000000..0d038bc --- /dev/null +++ b/Storage/Storage/Internal/Command/AVCommand.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using LeanCloud.Storage.Internal; +using System.Linq; + +namespace LeanCloud.Storage.Internal +{ + /// + /// AVCommand is an with pre-populated + /// headers. + /// + public class AVCommand : HttpRequest + { + public IDictionary DataObject { get; private set; } + public override Stream Data + { + get + { + if (base.Data != null) + { + return base.Data; + } + + return base.Data = (DataObject != null + ? new MemoryStream(Encoding.UTF8.GetBytes(Json.Encode(DataObject))) + : null); + } + set { base.Data = value; } + } + + public AVCommand(string relativeUri, + string method, + string sessionToken = null, + IList> headers = null, + IDictionary data = null) : this(relativeUri: relativeUri, + method: method, + sessionToken: sessionToken, + headers: headers, + stream: null, + contentType: data != null ? "application/json" : null) + { + DataObject = data; + } + + public AVCommand(string relativeUri, + string method, + string sessionToken = null, + IList> headers = null, + Stream stream = null, + string contentType = null) + { + var state = AVPlugins.Instance.AppRouterController.Get(); + var urlTemplate = "https://{0}/{1}/{2}"; + AVClient.Configuration configuration = AVClient.CurrentConfiguration; + var apiVersion = "1.1"; + if (relativeUri.StartsWith("push") || relativeUri.StartsWith("installations")) + { + Uri = new Uri(string.Format(urlTemplate, state.PushServer, apiVersion, relativeUri)); + if (configuration.PushServer != null) + { + Uri = new Uri(string.Format("{0}{1}/{2}", configuration.PushServer, apiVersion, relativeUri)); + } + } + else if (relativeUri.StartsWith("stats") || relativeUri.StartsWith("always_collect") || relativeUri.StartsWith("statistics")) + { + Uri = new Uri(string.Format(urlTemplate, state.StatsServer, apiVersion, relativeUri)); + if (configuration.StatsServer != null) + { + Uri = new Uri(string.Format("{0}{1}/{2}", configuration.StatsServer, apiVersion, relativeUri)); + } + } + else if (relativeUri.StartsWith("functions") || relativeUri.StartsWith("call")) + { + Uri = new Uri(string.Format(urlTemplate, state.EngineServer, apiVersion, relativeUri)); + + if (configuration.EngineServer != null) + { + Uri = new Uri(string.Format("{0}{1}/{2}", configuration.EngineServer, apiVersion, relativeUri)); + } + } + else + { + Uri = new Uri(string.Format(urlTemplate, state.ApiServer, apiVersion, relativeUri)); + + if (configuration.ApiServer != null) + { + Uri = new Uri(string.Format("{0}{1}/{2}", configuration.ApiServer, apiVersion, relativeUri)); + } + } + Method = method; + Data = stream; + Headers = new List>(headers ?? Enumerable.Empty>()); + + string useProduction = AVClient.UseProduction ? "1" : "0"; + Headers.Add(new KeyValuePair("X-LC-Prod", useProduction)); + + if (!string.IsNullOrEmpty(sessionToken)) + { + Headers.Add(new KeyValuePair("X-LC-Session", sessionToken)); + } + if (!string.IsNullOrEmpty(contentType)) + { + Headers.Add(new KeyValuePair("Content-Type", contentType)); + } + } + + public AVCommand(AVCommand other) + { + this.Uri = other.Uri; + this.Method = other.Method; + this.DataObject = other.DataObject; + this.Headers = new List>(other.Headers); + this.Data = other.Data; + } + } +} diff --git a/Storage/Storage/Internal/Command/AVCommandRunner.cs b/Storage/Storage/Internal/Command/AVCommandRunner.cs new file mode 100644 index 0000000..cfbfc18 --- /dev/null +++ b/Storage/Storage/Internal/Command/AVCommandRunner.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal +{ + /// + /// Command Runner. + /// + public class AVCommandRunner : IAVCommandRunner + { + private readonly IHttpClient httpClient; + private readonly IInstallationIdController installationIdController; + + /// + /// + /// + /// + /// + public AVCommandRunner(IHttpClient httpClient, IInstallationIdController installationIdController) + { + this.httpClient = httpClient; + this.installationIdController = installationIdController; + } + + /// + /// + /// + /// + /// + /// + /// + /// + public Task>> RunCommandAsync(AVCommand command, + IProgress uploadProgress = null, + IProgress downloadProgress = null, + CancellationToken cancellationToken = default(CancellationToken)) + { + return PrepareCommand(command).ContinueWith(commandTask => + { + var requestLog = commandTask.Result.ToLog(); + AVClient.PrintLog("http=>" + requestLog); + + return httpClient.ExecuteAsync(commandTask.Result, uploadProgress, downloadProgress, cancellationToken).OnSuccess(t => + { + cancellationToken.ThrowIfCancellationRequested(); + + var response = t.Result; + var contentString = response.Item2; + int responseCode = (int)response.Item1; + + var responseLog = responseCode + ";" + contentString; + AVClient.PrintLog("http<=" + responseLog); + + if (responseCode >= 500) + { + // Server error, return InternalServerError. + throw new AVException(AVException.ErrorCode.InternalServerError, response.Item2); + } + else if (contentString != null) + { + IDictionary contentJson = null; + try + { + if (contentString.StartsWith("[")) + { + var arrayJson = Json.Parse(contentString); + contentJson = new Dictionary { { "results", arrayJson } }; + } + else + { + contentJson = Json.Parse(contentString) as IDictionary; + } + } + catch (Exception e) + { + throw new AVException(AVException.ErrorCode.OtherCause, + "Invalid response from server", e); + } + if (responseCode < 200 || responseCode > 299) + { + AVClient.PrintLog("error response code:" + responseCode); + int code = (int)(contentJson.ContainsKey("code") ? (int)contentJson["code"] : (int)AVException.ErrorCode.OtherCause); + string error = contentJson.ContainsKey("error") ? + contentJson["error"] as string : contentString; + AVException.ErrorCode ec = (AVException.ErrorCode)code; + throw new AVException(ec, error); + } + return new Tuple>(response.Item1, + contentJson); + } + return new Tuple>(response.Item1, null); + }); + }).Unwrap(); + } + + private const string revocableSessionTokenTrueValue = "1"; + private Task PrepareCommand(AVCommand command) + { + AVCommand newCommand = new AVCommand(command); + + Task installationIdTask = installationIdController.GetAsync().ContinueWith(t => + { + newCommand.Headers.Add(new KeyValuePair("X-LC-Installation-Id", t.Result.ToString())); + return newCommand; + }); + + AVClient.Configuration configuration = AVClient.CurrentConfiguration; + newCommand.Headers.Add(new KeyValuePair("X-LC-Id", configuration.ApplicationId)); + + long timestamp = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalMilliseconds; + if (!string.IsNullOrEmpty(configuration.MasterKey) && AVClient.UseMasterKey) + { + string sign = MD5.GetMd5String(timestamp + configuration.MasterKey); + newCommand.Headers.Add(new KeyValuePair("X-LC-Sign", string.Format("{0},{1},master", sign, timestamp))); + } + else + { + string sign = MD5.GetMd5String(timestamp + configuration.ApplicationKey); + newCommand.Headers.Add(new KeyValuePair("X-LC-Sign", string.Format("{0},{1}", sign, timestamp))); + } + + newCommand.Headers.Add(new KeyValuePair("X-LC-Client-Version", AVClient.VersionString)); + + if (!string.IsNullOrEmpty(configuration.VersionInfo.BuildVersion)) + { + newCommand.Headers.Add(new KeyValuePair("X-LC-App-Build-Version", configuration.VersionInfo.BuildVersion)); + } + if (!string.IsNullOrEmpty(configuration.VersionInfo.DisplayVersion)) + { + newCommand.Headers.Add(new KeyValuePair("X-LC-App-Display-Version", configuration.VersionInfo.DisplayVersion)); + } + if (!string.IsNullOrEmpty(configuration.VersionInfo.OSVersion)) + { + newCommand.Headers.Add(new KeyValuePair("X-LC-OS-Version", configuration.VersionInfo.OSVersion)); + } + + if (AVUser.IsRevocableSessionEnabled) + { + newCommand.Headers.Add(new KeyValuePair("X-LeanCloud-Revocable-Session", revocableSessionTokenTrueValue)); + } + + if (configuration.AdditionalHTTPHeaders != null) + { + var headersDictionary = newCommand.Headers.ToDictionary(kv => kv.Key, kv => kv.Value); + foreach (var header in configuration.AdditionalHTTPHeaders) + { + if (headersDictionary.ContainsKey(header.Key)) + { + headersDictionary[header.Key] = header.Value; + } + else + { + newCommand.Headers.Add(header); + } + } + newCommand.Headers = headersDictionary.ToList(); + } + + return installationIdTask; + } + } +} diff --git a/Storage/Storage/Internal/Command/IAVCommandRunner.cs b/Storage/Storage/Internal/Command/IAVCommandRunner.cs new file mode 100644 index 0000000..081623b --- /dev/null +++ b/Storage/Storage/Internal/Command/IAVCommandRunner.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal +{ + public interface IAVCommandRunner + { + /// + /// Executes and convert the result into Dictionary. + /// + /// The command to be run. + /// Upload progress callback. + /// Download progress callback. + /// The cancellation token for the request. + /// + Task>> RunCommandAsync(AVCommand command, + IProgress uploadProgress = null, + IProgress downloadProgress = null, + CancellationToken cancellationToken = default(CancellationToken)); + } +} diff --git a/Storage/Storage/Internal/Config/Controller/AVConfigController.cs b/Storage/Storage/Internal/Config/Controller/AVConfigController.cs new file mode 100644 index 0000000..15d34ff --- /dev/null +++ b/Storage/Storage/Internal/Config/Controller/AVConfigController.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading.Tasks; +using System.Threading; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal { + /// + /// Config controller. + /// + internal class AVConfigController : IAVConfigController { + private readonly IAVCommandRunner commandRunner; + + /// + /// Initializes a new instance of the class. + /// + public AVConfigController(IAVCommandRunner commandRunner, IStorageController storageController) { + this.commandRunner = commandRunner; + CurrentConfigController = new AVCurrentConfigController(storageController); + } + + public IAVCommandRunner CommandRunner { get; internal set; } + public IAVCurrentConfigController CurrentConfigController { get; internal set; } + + public Task FetchConfigAsync(String sessionToken, CancellationToken cancellationToken) { + var command = new AVCommand("config", + method: "GET", + sessionToken: sessionToken, + data: null); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(task => { + cancellationToken.ThrowIfCancellationRequested(); + return new AVConfig(task.Result.Item2); + }).OnSuccess(task => { + cancellationToken.ThrowIfCancellationRequested(); + CurrentConfigController.SetCurrentConfigAsync(task.Result); + return task; + }).Unwrap(); + } + } +} diff --git a/Storage/Storage/Internal/Config/Controller/AVCurrentConfigController.cs b/Storage/Storage/Internal/Config/Controller/AVCurrentConfigController.cs new file mode 100644 index 0000000..41ba8d8 --- /dev/null +++ b/Storage/Storage/Internal/Config/Controller/AVCurrentConfigController.cs @@ -0,0 +1,76 @@ +using System; +using System.Threading.Tasks; +using System.Threading; +using System.Collections.Generic; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal { + /// + /// LeanCloud current config controller. + /// + internal class AVCurrentConfigController : IAVCurrentConfigController { + private const string CurrentConfigKey = "CurrentConfig"; + + private readonly TaskQueue taskQueue; + private AVConfig currentConfig; + + private IStorageController storageController; + + /// + /// Initializes a new instance of the class. + /// + public AVCurrentConfigController(IStorageController storageController) { + this.storageController = storageController; + + taskQueue = new TaskQueue(); + } + + public Task GetCurrentConfigAsync() { + return taskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => { + if (currentConfig == null) { + return storageController.LoadAsync().OnSuccess(t => { + object tmp; + t.Result.TryGetValue(CurrentConfigKey, out tmp); + + string propertiesString = tmp as string; + if (propertiesString != null) { + var dictionary = AVClient.DeserializeJsonString(propertiesString); + currentConfig = new AVConfig(dictionary); + } else { + currentConfig = new AVConfig(); + } + + return currentConfig; + }); + } + + return Task.FromResult(currentConfig); + }), CancellationToken.None).Unwrap(); + } + + public Task SetCurrentConfigAsync(AVConfig config) { + return taskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => { + currentConfig = config; + + var jsonObject = ((IJsonConvertible)config).ToJSON(); + var jsonString = AVClient.SerializeJsonString(jsonObject); + + return storageController.LoadAsync().OnSuccess(t => t.Result.AddAsync(CurrentConfigKey, jsonString)); + }).Unwrap().Unwrap(), CancellationToken.None); + } + + public Task ClearCurrentConfigAsync() { + return taskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => { + currentConfig = null; + + return storageController.LoadAsync().OnSuccess(t => t.Result.RemoveAsync(CurrentConfigKey)); + }).Unwrap().Unwrap(), CancellationToken.None); + } + + public Task ClearCurrentConfigInMemoryAsync() { + return taskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => { + currentConfig = null; + }), CancellationToken.None); + } + } +} diff --git a/Storage/Storage/Internal/Config/Controller/IAVConfigController.cs b/Storage/Storage/Internal/Config/Controller/IAVConfigController.cs new file mode 100644 index 0000000..5cef0ac --- /dev/null +++ b/Storage/Storage/Internal/Config/Controller/IAVConfigController.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading.Tasks; +using System.Threading; + +namespace LeanCloud.Storage.Internal { + public interface IAVConfigController { + /// + /// Gets the current config controller. + /// + /// The current config controller. + IAVCurrentConfigController CurrentConfigController { get; } + + /// + /// Fetches the config from the server asynchronously. + /// + /// The config async. + /// Session token. + /// Cancellation token. + Task FetchConfigAsync(String sessionToken, CancellationToken cancellationToken); + } +} diff --git a/Storage/Storage/Internal/Config/Controller/IAVCurrentConfigController.cs b/Storage/Storage/Internal/Config/Controller/IAVCurrentConfigController.cs new file mode 100644 index 0000000..759329c --- /dev/null +++ b/Storage/Storage/Internal/Config/Controller/IAVCurrentConfigController.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + public interface IAVCurrentConfigController { + /// + /// Gets the current config async. + /// + /// The current config async. + Task GetCurrentConfigAsync(); + + /// + /// Sets the current config async. + /// + /// The current config async. + /// Config. + Task SetCurrentConfigAsync(AVConfig config); + + /// + /// Clears the current config async. + /// + /// The current config async. + Task ClearCurrentConfigAsync(); + + /// + /// Clears the current config in memory async. + /// + /// The current config in memory async. + Task ClearCurrentConfigInMemoryAsync(); + } +} diff --git a/Storage/Storage/Internal/Encoding/AVDecoder.cs b/Storage/Storage/Internal/Encoding/AVDecoder.cs new file mode 100644 index 0000000..fdb4303 --- /dev/null +++ b/Storage/Storage/Internal/Encoding/AVDecoder.cs @@ -0,0 +1,164 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Globalization; +using LeanCloud.Utilities; + +namespace LeanCloud.Storage.Internal +{ + public class AVDecoder + { + // This class isn't really a Singleton, but since it has no state, it's more efficient to get + // the default instance. + private static readonly AVDecoder instance = new AVDecoder(); + public static AVDecoder Instance + { + get + { + return instance; + } + } + + // Prevent default constructor. + private AVDecoder() { } + + public object Decode(object data) + { + if (data == null) + { + return null; + } + + var dict = data as IDictionary; + if (dict != null) + { + if (dict.ContainsKey("__op")) + { + return AVFieldOperations.Decode(dict); + } + + object type; + dict.TryGetValue("__type", out type); + var typeString = type as string; + + if (typeString == null) + { + var newDict = new Dictionary(); + foreach (var pair in dict) + { + newDict[pair.Key] = Decode(pair.Value); + } + return newDict; + } + + if (typeString == "Date") + { + return ParseDate(dict["iso"] as string); + } + + if (typeString == "Bytes") + { + return Convert.FromBase64String(dict["base64"] as string); + } + + if (typeString == "Pointer") + { + //set a include key to fetch or query. + if (dict.Keys.Count > 3) + { + return DecodeAVObject(dict); + } + return DecodePointer(dict["className"] as string, dict["objectId"] as string); + } + + if (typeString == "File") + { + return DecodeAVFile(dict); + } + + if (typeString == "GeoPoint") + { + return new AVGeoPoint(Conversion.To(dict["latitude"]), + Conversion.To(dict["longitude"])); + } + + if (typeString == "Object") + { + return DecodeAVObject(dict); + } + + if (typeString == "Relation") + { + return AVRelationBase.CreateRelation(null, null, dict["className"] as string); + } + + var converted = new Dictionary(); + foreach (var pair in dict) + { + converted[pair.Key] = Decode(pair.Value); + } + return converted; + } + + var list = data as IList; + if (list != null) + { + return (from item in list + select Decode(item)).ToList(); + } + + return data; + } + + protected virtual object DecodePointer(string className, string objectId) + { + if (className == "_File") + { + return AVFile.CreateWithoutData(objectId); + } + return AVObject.CreateWithoutData(className, objectId); + } + protected virtual object DecodeAVObject(IDictionary dict) + { + var className = dict["className"] as string; + if (className == "_File") + { + return DecodeAVFile(dict); + } + var state = AVObjectCoder.Instance.Decode(dict, this); + return AVObject.FromState(state, dict["className"] as string); + } + protected virtual object DecodeAVFile(IDictionary dict) + { + var objectId = dict["objectId"] as string; + var file = AVFile.CreateWithoutData(objectId); + file.MergeFromJSON(dict); + return file; + } + + + public virtual IList DecodeList(object data) + { + IList rtn = null; + var list = (IList)data; + if (list != null) + { + rtn = new List(); + foreach (var item in list) + { + rtn.Add((T)item); + } + } + return rtn; + } + + public static DateTime ParseDate(string input) + { + var rtn = DateTime.ParseExact(input, + AVClient.DateFormatStrings, + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal); + return rtn; + } + } +} diff --git a/Storage/Storage/Internal/Encoding/AVEncoder.cs b/Storage/Storage/Internal/Encoding/AVEncoder.cs new file mode 100644 index 0000000..401edc1 --- /dev/null +++ b/Storage/Storage/Internal/Encoding/AVEncoder.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using LeanCloud.Utilities; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal +{ + /// + /// A AVEncoder can be used to transform objects such as into JSON + /// data structures. + /// + /// + public abstract class AVEncoder + { +#if UNITY + private static readonly bool isCompiledByIL2CPP = AppDomain.CurrentDomain.FriendlyName.Equals("IL2CPP Root Domain"); +#else + private static readonly bool isCompiledByIL2CPP = false; +#endif + + public static bool IsValidType(object value) + { + return value == null || + ReflectionHelpers.IsPrimitive(value.GetType()) || + value is string || + value is AVObject || + value is AVACL || + value is AVFile || + value is AVGeoPoint || + value is AVRelationBase || + value is DateTime || + value is byte[] || + Conversion.As>(value) != null || + Conversion.As>(value) != null; + } + + public object Encode(object value) + { + // If this object has a special encoding, encode it and return the + // encoded object. Otherwise, just return the original object. + if (value is DateTime) + { + return new Dictionary + { + { + "iso", ((DateTime)value).ToUniversalTime().ToString(AVClient.DateFormatStrings.First(), CultureInfo.InvariantCulture) + }, + { + "__type", "Date" + } + }; + } + + if (value is AVFile) + { + var file = value as AVFile; + return new Dictionary + { + {"__type", "Pointer"}, + { "className", "_File"}, + { "objectId", file.ObjectId} + }; + } + + var bytes = value as byte[]; + if (bytes != null) + { + return new Dictionary + { + { "__type", "Bytes"}, + { "base64", Convert.ToBase64String(bytes)} + }; + } + + var obj = value as AVObject; + if (obj != null) + { + return EncodeAVObject(obj); + } + + var jsonConvertible = value as IJsonConvertible; + if (jsonConvertible != null) + { + return jsonConvertible.ToJSON(); + } + + var dict = Conversion.As>(value); + if (dict != null) + { + var json = new Dictionary(); + foreach (var pair in dict) + { + json[pair.Key] = Encode(pair.Value); + } + return json; + } + + var list = Conversion.As>(value); + if (list != null) + { + return EncodeList(list); + } + + // TODO (hallucinogen): convert IAVFieldOperation to IJsonConvertible + var operation = value as IAVFieldOperation; + if (operation != null) + { + return operation.Encode(); + } + + return value; + } + + protected abstract IDictionary EncodeAVObject(AVObject value); + + private object EncodeList(IList list) + { + var newArray = new List(); + // We need to explicitly cast `list` to `List` rather than + // `IList` because IL2CPP is stricter than the usual Unity AOT compiler pipeline. + if (isCompiledByIL2CPP && list.GetType().IsArray) + { + list = new List(list); + } + foreach (var item in list) + { + if (!IsValidType(item)) + { + throw new ArgumentException("Invalid type for value in an array"); + } + newArray.Add(Encode(item)); + } + return newArray; + } + } +} diff --git a/Storage/Storage/Internal/Encoding/AVObjectCoder.cs b/Storage/Storage/Internal/Encoding/AVObjectCoder.cs new file mode 100644 index 0000000..05a6509 --- /dev/null +++ b/Storage/Storage/Internal/Encoding/AVObjectCoder.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; + +namespace LeanCloud.Storage.Internal +{ + // TODO: (richardross) refactor entire LeanCloud coder interfaces. + public class AVObjectCoder + { + private static readonly AVObjectCoder instance = new AVObjectCoder(); + public static AVObjectCoder Instance + { + get + { + return instance; + } + } + + // Prevent default constructor. + private AVObjectCoder() { } + + public IDictionary Encode(T state, + IDictionary operations, + AVEncoder encoder) where T : IObjectState + { + var result = new Dictionary(); + foreach (var pair in operations) + { + // AVRPCSerialize the data + var operation = pair.Value; + + result[pair.Key] = encoder.Encode(operation); + } + + return result; + } + + public IObjectState Decode(IDictionary data, + AVDecoder decoder) + { + IDictionary serverData = new Dictionary(); + var mutableData = new Dictionary(data); + string objectId = extractFromDictionary(mutableData, "objectId", (obj) => + { + return obj as string; + }); + DateTime? createdAt = extractFromDictionary(mutableData, "createdAt", (obj) => + { + return AVDecoder.ParseDate(obj as string); + }); + DateTime? updatedAt = extractFromDictionary(mutableData, "updatedAt", (obj) => + { + return AVDecoder.ParseDate(obj as string); + }); + + if (mutableData.ContainsKey("ACL")) + { + serverData["ACL"] = extractFromDictionary(mutableData, "ACL", (obj) => + { + return new AVACL(obj as IDictionary); + }); + } + string className = extractFromDictionary(mutableData, "className", obj => + { + return obj as string; + }); + if (createdAt != null && updatedAt == null) + { + updatedAt = createdAt; + } + + // Bring in the new server data. + foreach (var pair in mutableData) + { + if (pair.Key == "__type" || pair.Key == "className") + { + continue; + } + + var value = pair.Value; + serverData[pair.Key] = decoder.Decode(value); + } + + return new MutableObjectState + { + ObjectId = objectId, + CreatedAt = createdAt, + UpdatedAt = updatedAt, + ServerData = serverData, + ClassName = className + }; + } + + private T extractFromDictionary(IDictionary data, string key, Func action) + { + T result = default(T); + if (data.ContainsKey(key)) + { + result = action(data[key]); + data.Remove(key); + } + + return result; + } + } +} diff --git a/Storage/Storage/Internal/Encoding/NoObjectsEncoder.cs b/Storage/Storage/Internal/Encoding/NoObjectsEncoder.cs new file mode 100644 index 0000000..6a1e7e1 --- /dev/null +++ b/Storage/Storage/Internal/Encoding/NoObjectsEncoder.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; + +namespace LeanCloud.Storage.Internal { + /// + /// A that throws an exception if it attempts to encode + /// a + /// + public class NoObjectsEncoder : AVEncoder { + // This class isn't really a Singleton, but since it has no state, it's more efficient to get + // the default instance. + private static readonly NoObjectsEncoder instance = new NoObjectsEncoder(); + public static NoObjectsEncoder Instance { + get { + return instance; + } + } + + protected override IDictionary EncodeAVObject(AVObject value) { + throw new ArgumentException("AVObjects not allowed here."); + } + } +} diff --git a/Storage/Storage/Internal/Encoding/PointerOrLocalIdEncoder.cs b/Storage/Storage/Internal/Encoding/PointerOrLocalIdEncoder.cs new file mode 100644 index 0000000..2394cf7 --- /dev/null +++ b/Storage/Storage/Internal/Encoding/PointerOrLocalIdEncoder.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace LeanCloud.Storage.Internal +{ + /// + /// A that encode as pointers. If the object + /// does not have an , uses a local id. + /// + public class PointerOrLocalIdEncoder : AVEncoder + { + // This class isn't really a Singleton, but since it has no state, it's more efficient to get + // the default instance. + private static readonly PointerOrLocalIdEncoder instance = new PointerOrLocalIdEncoder(); + public static PointerOrLocalIdEncoder Instance + { + get + { + return instance; + } + } + + protected override IDictionary EncodeAVObject(AVObject value) + { + if (value.ObjectId == null) + { + // TODO (hallucinogen): handle local id. For now we throw. + throw new ArgumentException("Cannot create a pointer to an object without an objectId"); + } + + return new Dictionary { + {"__type", "Pointer"}, + { "className", value.ClassName}, + { "objectId", value.ObjectId} + }; + } + + public IDictionary EncodeAVObject(AVObject value, bool isPointer) + { + if (isPointer) + { + return EncodeAVObject(value); + } + var operations = value.GetCurrentOperations(); + var operationJSON = AVObject.ToJSONObjectForSaving(operations); + var objectJSON = value.ToDictionary(kvp => kvp.Key, kvp => PointerOrLocalIdEncoder.Instance.Encode(kvp.Value)); + foreach (var kvp in operationJSON) + { + objectJSON[kvp.Key] = kvp.Value; + } + if (value.CreatedAt.HasValue) + { + objectJSON["createdAt"] = value.CreatedAt.Value.ToString(AVClient.DateFormatStrings.First(), + CultureInfo.InvariantCulture); + } + if (value.UpdatedAt.HasValue) + { + objectJSON["updatedAt"] = value.UpdatedAt.Value.ToString(AVClient.DateFormatStrings.First(), + CultureInfo.InvariantCulture); + } + if(!string.IsNullOrEmpty(value.ObjectId)) + { + objectJSON["objectId"] = value.ObjectId; + } + objectJSON["className"] = value.ClassName; + objectJSON["__type"] = "Object"; + return objectJSON; + } + } +} diff --git a/Storage/Storage/Internal/File/Controller/AVFileController.cs b/Storage/Storage/Internal/File/Controller/AVFileController.cs new file mode 100644 index 0000000..688689b --- /dev/null +++ b/Storage/Storage/Internal/File/Controller/AVFileController.cs @@ -0,0 +1,151 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using LeanCloud.Storage.Internal; +using System.Net; +using System.Collections.Generic; +using System.Linq; + +namespace LeanCloud.Storage.Internal +{ + /// + /// AVF ile controller. + /// + public class AVFileController : IAVFileController + { + private readonly IAVCommandRunner commandRunner; + /// + /// Initializes a new instance of the class. + /// + /// Command runner. + public AVFileController(IAVCommandRunner commandRunner) + { + this.commandRunner = commandRunner; + } + /// + /// Saves the async. + /// + /// The async. + /// State. + /// Data stream. + /// Session token. + /// Progress. + /// Cancellation token. + public virtual Task SaveAsync(FileState state, + Stream dataStream, + String sessionToken, + IProgress progress, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (state.Url != null) + { + // !isDirty + return Task.FromResult(state); + } + + if (cancellationToken.IsCancellationRequested) + { + var tcs = new TaskCompletionSource(); + tcs.TrySetCanceled(); + return tcs.Task; + } + + var oldPosition = dataStream.Position; + var command = new AVCommand("files/" + state.Name, + method: "POST", + sessionToken: sessionToken, + contentType: state.MimeType, + stream: dataStream); + + return commandRunner.RunCommandAsync(command, + uploadProgress: progress, + cancellationToken: cancellationToken).OnSuccess(uploadTask => + { + var result = uploadTask.Result; + var jsonData = result.Item2; + cancellationToken.ThrowIfCancellationRequested(); + + return new FileState + { + Name = jsonData["name"] as string, + Url = new Uri(jsonData["url"] as string, UriKind.Absolute), + MimeType = state.MimeType + }; + }).ContinueWith(t => + { + // Rewind the stream on failure or cancellation (if possible) + if ((t.IsFaulted || t.IsCanceled) && dataStream.CanSeek) + { + dataStream.Seek(oldPosition, SeekOrigin.Begin); + } + return t; + }).Unwrap(); + } + public Task DeleteAsync(FileState state, string sessionToken, CancellationToken cancellationToken) + { + var command = new AVCommand("files/" + state.ObjectId, + method: "DELETE", + sessionToken: sessionToken, + data: null); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken); + } + internal static Task>> GetFileToken(FileState fileState, CancellationToken cancellationToken) + { + Task>> rtn; + string currentSessionToken = AVUser.CurrentSessionToken; + string str = fileState.Name; + IDictionary parameters = new Dictionary(); + parameters.Add("name", str); + parameters.Add("key", GetUniqueName(fileState)); + parameters.Add("__type", "File"); + parameters.Add("mime_type", AVFile.GetMIMEType(str)); + parameters.Add("metaData", fileState.MetaData); + + rtn = AVClient.RequestAsync("POST", new Uri("fileTokens", UriKind.Relative), currentSessionToken, parameters, cancellationToken); + + return rtn; + } + public Task GetAsync(string objectId, string sessionToken, CancellationToken cancellationToken) + { + var command = new AVCommand("files/" + objectId, + method: "GET", + sessionToken: sessionToken, + data: null); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(_ => + { + var result = _.Result; + var jsonData = result.Item2; + cancellationToken.ThrowIfCancellationRequested(); + return new FileState + { + ObjectId = jsonData["objectId"] as string, + Name = jsonData["name"] as string, + Url = new Uri(jsonData["url"] as string, UriKind.Absolute), + }; + }); + } + internal static string GetUniqueName(FileState fileState) + { + string key = Random(12); + string extension = Path.GetExtension(fileState.Name); + key += extension; + fileState.CloudName = key; + return key; + } + internal static string Random(int length) + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz"; + var random = new Random(); + return new string(Enumerable.Repeat(chars, length) + .Select(s => s[random.Next(s.Length)]).ToArray()); + } + internal static double CalcProgress(double already, double total) + { + var pv = (1.0 * already / total); + return Math.Round(pv, 3); + } + } +} diff --git a/Storage/Storage/Internal/File/Controller/AWSS3FileController.cs b/Storage/Storage/Internal/File/Controller/AWSS3FileController.cs new file mode 100644 index 0000000..d812f20 --- /dev/null +++ b/Storage/Storage/Internal/File/Controller/AWSS3FileController.cs @@ -0,0 +1,54 @@ +using System; +using System.Threading.Tasks; +using System.Threading; +using System.IO; +using LeanCloud.Storage.Internal; +using System.Collections.Generic; + +namespace LeanCloud.Storage.Internal +{ + internal class AWSS3FileController : AVFileController + { + + private object mutex = new object(); + + + public AWSS3FileController(IAVCommandRunner commandRunner) : base(commandRunner) + { + + } + + public override Task SaveAsync(FileState state, Stream dataStream, string sessionToken, IProgress progress, CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (state.Url != null) + { + return Task.FromResult(state); + } + + return GetFileToken(state, cancellationToken).OnSuccess(t => + { + var fileToken = t.Result.Item2; + var uploadUrl = fileToken["upload_url"].ToString(); + state.ObjectId = fileToken["objectId"].ToString(); + string url = fileToken["url"] as string; + state.Url = new Uri(url, UriKind.Absolute); + return PutFile(state, uploadUrl, dataStream); + + }).Unwrap().OnSuccess(s => + { + return s.Result; + }); + } + + internal Task PutFile(FileState state, string uploadUrl, Stream dataStream) + { + IList> makeBlockHeaders = new List>(); + makeBlockHeaders.Add(new KeyValuePair("Content-Type", state.MimeType)); + + return AVClient.RequestAsync(new Uri(uploadUrl), "PUT", makeBlockHeaders, dataStream, state.MimeType, CancellationToken.None).OnSuccess(t => + { + return state; + }); + } + } +} diff --git a/Storage/Storage/Internal/File/Controller/IAVFileController.cs b/Storage/Storage/Internal/File/Controller/IAVFileController.cs new file mode 100644 index 0000000..2dea784 --- /dev/null +++ b/Storage/Storage/Internal/File/Controller/IAVFileController.cs @@ -0,0 +1,24 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal +{ + public interface IAVFileController + { + Task SaveAsync(FileState state, + Stream dataStream, + String sessionToken, + IProgress progress, + CancellationToken cancellationToken); + + Task DeleteAsync(FileState state, + string sessionToken, + CancellationToken cancellationToken); + + Task GetAsync(string objectId, + string sessionToken, + CancellationToken cancellationToken); + } +} diff --git a/Storage/Storage/Internal/File/Controller/QCloudCosFileController.cs b/Storage/Storage/Internal/File/Controller/QCloudCosFileController.cs new file mode 100644 index 0000000..9eee34a --- /dev/null +++ b/Storage/Storage/Internal/File/Controller/QCloudCosFileController.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Linq; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal +{ + internal class QCloudCosFileController : AVFileController + { + private object mutex = new object(); + + FileState fileState; + Stream data; + string bucket; + string token; + string uploadUrl; + bool done; + private long sliceSize = (long)CommonSize.KB512; + + public QCloudCosFileController(IAVCommandRunner commandRunner) : base(commandRunner) + { + } + + public Task SaveAsync(FileState state, + Stream dataStream, + string sessionToken, + IProgress progress, + CancellationToken cancellationToken) + { + if (state.Url != null) + { + return Task.FromResult(state); + } + fileState = state; + data = dataStream; + return GetFileToken(fileState, cancellationToken).OnSuccess(_ => + { + var fileToken = _.Result.Item2; + uploadUrl = fileToken["upload_url"].ToString(); + token = fileToken["token"].ToString(); + fileState.ObjectId = fileToken["objectId"].ToString(); + bucket = fileToken["bucket"].ToString(); + + return FileSlice(cancellationToken).OnSuccess(t => + { + if (done) return Task.FromResult(state); + var response = t.Result.Item2; + var resumeData = response["data"] as IDictionary; + if (resumeData.ContainsKey("access_url")) return Task.FromResult(state); + var sliceSession = resumeData["session"].ToString(); + var sliceOffset = long.Parse(resumeData["offset"].ToString()); + return UploadSlice(sliceSession, sliceOffset, dataStream, progress, cancellationToken); + }).Unwrap(); + + }).Unwrap(); + } + + Task UploadSlice( + string sessionId, + long offset, + Stream dataStream, + IProgress progress, + CancellationToken cancellationToken) + { + + long dataLength = dataStream.Length; + if (progress != null) + { + lock (mutex) + { + progress.Report(new AVUploadProgressEventArgs() + { + Progress = AVFileController.CalcProgress(offset, dataLength) + }); + } + } + + if (offset == dataLength) + { + return Task.FromResult(fileState); + } + + var sliceFile = GetNextBinary(offset, dataStream); + return ExcuteUpload(sessionId, offset, sliceFile, cancellationToken).OnSuccess(_ => + { + offset += sliceFile.Length; + if (offset == dataLength) + { + done = true; + return Task.FromResult(fileState); + } + var response = _.Result.Item2; + var resumeData = response["data"] as IDictionary; + var sliceSession = resumeData["session"].ToString(); + return UploadSlice(sliceSession, offset, dataStream, progress, cancellationToken); + }).Unwrap(); + } + + Task>> ExcuteUpload(string sessionId, long offset, byte[] sliceFile, CancellationToken cancellationToken) + { + var body = new Dictionary(); + body.Add("op", "upload_slice"); + body.Add("session", sessionId); + body.Add("offset", offset.ToString()); + + return PostToQCloud(body, sliceFile, cancellationToken); + } + + Task>> FileSlice(CancellationToken cancellationToken) + { + SHA1CryptoServiceProvider sha1 = new SHA1CryptoServiceProvider(); + var body = new Dictionary(); + if (data.Length <= (long)CommonSize.KB512) + { + body.Add("op", "upload"); + body.Add("sha", HexStringFromBytes(sha1.ComputeHash(data))); + var wholeFile = GetNextBinary(0, data); + return PostToQCloud(body, wholeFile, cancellationToken).OnSuccess(_ => + { + if (_.Result.Item1 == HttpStatusCode.OK) + { + done = true; + } + return _.Result; + }); + } + else + { + body.Add("op", "upload_slice"); + body.Add("filesize", data.Length); + body.Add("sha", HexStringFromBytes(sha1.ComputeHash(data))); + body.Add("slice_size", (long)CommonSize.KB512); + } + + return PostToQCloud(body, null, cancellationToken); + } + public static string HexStringFromBytes(byte[] bytes) + { + var sb = new StringBuilder(); + foreach (byte b in bytes) + { + var hex = b.ToString("x2"); + sb.Append(hex); + } + return sb.ToString(); + } + + public static string SHA1HashStringForUTF8String(string s) + { + byte[] bytes = Encoding.UTF8.GetBytes(s); + + SHA1CryptoServiceProvider sha1 = new SHA1CryptoServiceProvider(); + byte[] hashBytes = sha1.ComputeHash(bytes); + + return HexStringFromBytes(hashBytes); + } + Task>> PostToQCloud( + Dictionary body, + byte[] sliceFile, + CancellationToken cancellationToken) + { + IList> sliceHeaders = new List>(); + sliceHeaders.Add(new KeyValuePair("Authorization", this.token)); + + string contentType; + long contentLength; + + var tempStream = HttpUploadFile(sliceFile, fileState.CloudName, out contentType, out contentLength, body); + + sliceHeaders.Add(new KeyValuePair("Content-Type", contentType)); + + var rtn = AVClient.RequestAsync(new Uri(this.uploadUrl), "POST", sliceHeaders, tempStream, null, cancellationToken).OnSuccess(_ => + { + var dic = AVClient.ReponseResolve(_.Result, CancellationToken.None); + + return dic; + }); + + return rtn; + } + public static Stream HttpUploadFile(byte[] file, string fileName, out string contentType, out long contentLength, IDictionary nvc) + { + string boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x"); + byte[] boundarybytes = StringToAscii("\r\n--" + boundary + "\r\n"); + contentType = "multipart/form-data; boundary=" + boundary; + + MemoryStream rs = new MemoryStream(); + + string formdataTemplate = "Content-Disposition: form-data; name=\"{0}\"\r\n\r\n{1}"; + foreach (string key in nvc.Keys) + { + rs.Write(boundarybytes, 0, boundarybytes.Length); + string formitem = string.Format(formdataTemplate, key, nvc[key]); + byte[] formitembytes = System.Text.Encoding.UTF8.GetBytes(formitem); + rs.Write(formitembytes, 0, formitembytes.Length); + } + rs.Write(boundarybytes, 0, boundarybytes.Length); + + if (file != null) + { + string headerTemplate = "Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"\r\nContent-Type: {2}\r\n\r\n"; + string header = string.Format(headerTemplate, "fileContent", fileName, "application/octet-stream"); + byte[] headerbytes = System.Text.Encoding.UTF8.GetBytes(header); + rs.Write(headerbytes, 0, headerbytes.Length); + + rs.Write(file, 0, file.Length); + } + + byte[] trailer = StringToAscii("\r\n--" + boundary + "--\r\n"); + rs.Write(trailer, 0, trailer.Length); + contentLength = rs.Length; + + rs.Position = 0; + var tempBuffer = new byte[rs.Length]; + rs.Read(tempBuffer, 0, tempBuffer.Length); + + return new MemoryStream(tempBuffer); + } + + public static byte[] StringToAscii(string s) + { + byte[] retval = new byte[s.Length]; + for (int ix = 0; ix < s.Length; ++ix) + { + char ch = s[ix]; + if (ch <= 0x7f) retval[ix] = (byte)ch; + else retval[ix] = (byte)'?'; + } + return retval; + } + + byte[] GetNextBinary(long completed, Stream dataStream) + { + if (completed + sliceSize > dataStream.Length) + { + sliceSize = dataStream.Length - completed; + } + + byte[] chunkBinary = new byte[sliceSize]; + dataStream.Seek(completed, SeekOrigin.Begin); + dataStream.Read(chunkBinary, 0, (int)sliceSize); + return chunkBinary; + } + } +} diff --git a/Storage/Storage/Internal/File/Controller/QiniuFileController.cs b/Storage/Storage/Internal/File/Controller/QiniuFileController.cs new file mode 100644 index 0000000..72a8c5b --- /dev/null +++ b/Storage/Storage/Internal/File/Controller/QiniuFileController.cs @@ -0,0 +1,332 @@ +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal +{ + internal enum CommonSize : long + { + MB4 = 1024 * 1024 * 4, + MB1 = 1024 * 1024, + KB512 = 1024 * 1024 / 2, + KB256 = 1024 * 1024 / 4 + } + + internal class QiniuFileController : AVFileController + { + private static int BLOCKSIZE = 1024 * 1024 * 4; + private const int blockMashk = (1 << blockBits) - 1; + private const int blockBits = 22; + private int CalcBlockCount(long fsize) + { + return (int)((fsize + blockMashk) >> blockBits); + } + internal static string UP_HOST = "https://up.qbox.me"; + private object mutex = new object(); + + public QiniuFileController(IAVCommandRunner commandRunner) : base(commandRunner) + { + + } + + public override Task SaveAsync(FileState state, + Stream dataStream, + String sessionToken, + IProgress progress, + CancellationToken cancellationToken) + { + if (state.Url != null) + { + return Task.FromResult(state); + } + state.frozenData = dataStream; + state.CloudName = GetUniqueName(state); + return GetQiniuToken(state, CancellationToken.None).ContinueWith(t => + { + MergeFromJSON(state, t.Result.Item2); + return UploadNextChunk(state, dataStream, string.Empty, 0, progress); + }).Unwrap().OnSuccess(s => + { + return state; + }); + } + Task UploadNextChunk(FileState state, Stream dataStream, string context, long offset, IProgress progress) + { + var totalSize = dataStream.Length; + var remainingSize = totalSize - state.completed; + + if (progress != null) + { + lock (mutex) + { + progress.Report(new AVUploadProgressEventArgs() + { + Progress = AVFileController.CalcProgress(state.completed, totalSize) + }); + } + } + if (state.completed == totalSize) + { + return QiniuMakeFile(state, state.frozenData, state.token, state.CloudName, totalSize, state.block_ctxes.ToArray(), CancellationToken.None); + + } + else if (state.completed % BLOCKSIZE == 0) + { + var firstChunkBinary = GetChunkBinary(state.completed, dataStream); + + var blockSize = remainingSize > BLOCKSIZE ? BLOCKSIZE : remainingSize; + return MakeBlock(state, firstChunkBinary, blockSize).ContinueWith(t => + { + + var dic = AVClient.ReponseResolve(t.Result, CancellationToken.None); + var ctx = dic.Item2["ctx"].ToString(); + + offset = long.Parse(dic.Item2["offset"].ToString()); + var host = dic.Item2["host"].ToString(); + + state.completed += firstChunkBinary.Length; + if (state.completed % BLOCKSIZE == 0 || state.completed == totalSize) + { + state.block_ctxes.Add(ctx); + } + + return UploadNextChunk(state, dataStream, ctx, offset, progress); + }).Unwrap(); + + } + else + { + var chunkBinary = GetChunkBinary(state.completed, dataStream); + return PutChunk(state, chunkBinary, context, offset).ContinueWith(t => + { + var dic = AVClient.ReponseResolve(t.Result, CancellationToken.None); + var ctx = dic.Item2["ctx"].ToString(); + + offset = long.Parse(dic.Item2["offset"].ToString()); + var host = dic.Item2["host"].ToString(); + state.completed += chunkBinary.Length; + if (state.completed % BLOCKSIZE == 0 || state.completed == totalSize) + { + state.block_ctxes.Add(ctx); + } + //if (AVClient.fileUploaderDebugLog) + //{ + // AVClient.LogTracker(state.counter + "|completed=" + state.completed + "stream:position=" + dataStream.Position + "|"); + //} + + return UploadNextChunk(state, dataStream, ctx, offset, progress); + }).Unwrap(); + } + } + + byte[] GetChunkBinary(long completed, Stream dataStream) + { + long chunkSize = (long)CommonSize.MB1; + if (completed + chunkSize > dataStream.Length) + { + chunkSize = dataStream.Length - completed; + } + byte[] chunkBinary = new byte[chunkSize]; + dataStream.Seek(completed, SeekOrigin.Begin); + dataStream.Read(chunkBinary, 0, (int)chunkSize); + return chunkBinary; + } + + internal string GetUniqueName(FileState state) + { + string key = Guid.NewGuid().ToString();//file Key in Qiniu. + string extension = Path.GetExtension(state.Name); + key += extension; + return key; + } + internal Task>> GetQiniuToken(FileState state, CancellationToken cancellationToken) + { + Task>> rtn; + string currentSessionToken = AVUser.CurrentSessionToken; + string str = state.Name; + + IDictionary parameters = new Dictionary(); + parameters.Add("name", str); + parameters.Add("key", state.CloudName); + parameters.Add("__type", "File"); + parameters.Add("mime_type", AVFile.GetMIMEType(str)); + + state.MetaData = GetMetaData(state, state.frozenData); + + parameters.Add("metaData", state.MetaData); + + rtn = AVClient.RequestAsync("POST", new Uri("qiniu", UriKind.Relative), currentSessionToken, parameters, cancellationToken); + + return rtn; + } + IList> GetQiniuRequestHeaders(FileState state) + { + IList> makeBlockHeaders = new List>(); + + string authHead = "UpToken " + state.token; + makeBlockHeaders.Add(new KeyValuePair("Authorization", authHead)); + return makeBlockHeaders; + } + + Task> MakeBlock(FileState state, byte[] firstChunkBinary, long blcokSize = 4194304) + { + MemoryStream firstChunkData = new MemoryStream(firstChunkBinary, 0, firstChunkBinary.Length); + return AVClient.RequestAsync(new Uri(new Uri(UP_HOST) + string.Format("mkblk/{0}", blcokSize)), "POST", GetQiniuRequestHeaders(state), firstChunkData, "application/octet-stream", CancellationToken.None); + } + Task> PutChunk(FileState state, byte[] chunkBinary, string LastChunkctx, long currentChunkOffsetInBlock) + { + MemoryStream chunkData = new MemoryStream(chunkBinary, 0, chunkBinary.Length); + return AVClient.RequestAsync(new Uri(new Uri(UP_HOST) + string.Format("bput/{0}/{1}", LastChunkctx, + currentChunkOffsetInBlock)), "POST", + GetQiniuRequestHeaders(state), chunkData, + "application/octet-stream", CancellationToken.None); + } + internal Task> QiniuMakeFile(FileState state, Stream dataStream, string upToken, string key, long fsize, string[] ctxes, CancellationToken cancellationToken) + { + StringBuilder urlBuilder = new StringBuilder(); + urlBuilder.AppendFormat("{0}/mkfile/{1}", UP_HOST, fsize); + if (key != null) + { + urlBuilder.AppendFormat("/key/{0}", ToBase64URLSafe(key)); + } + var metaData = GetMetaData(state, dataStream); + + StringBuilder sb = new StringBuilder(); + foreach (string _key in metaData.Keys) + { + sb.AppendFormat("/{0}/{1}", _key, ToBase64URLSafe(metaData[_key].ToString())); + } + urlBuilder.Append(sb.ToString()); + + IList> headers = new List>(); + //makeBlockDic.Add("Content-Type", "application/octet-stream"); + + string authHead = "UpToken " + upToken; + headers.Add(new KeyValuePair("Authorization", authHead)); + int proCount = ctxes.Length; + Stream body = new MemoryStream(); + + for (int i = 0; i < proCount; i++) + { + byte[] bctx = StringToAscii(ctxes[i]); + body.Write(bctx, 0, bctx.Length); + if (i != proCount - 1) + { + body.WriteByte((byte)','); + } + } + body.Seek(0, SeekOrigin.Begin); + + var rtn = AVClient.RequestAsync(new Uri(urlBuilder.ToString()), "POST", headers, body, "text/plain", cancellationToken).OnSuccess(_ => + { + var dic = AVClient.ReponseResolve(_.Result, CancellationToken.None); + return _.Result; + }); + return rtn; + } + internal void MergeFromJSON(FileState state, IDictionary jsonData) + { + lock (this.mutex) + { + string url = jsonData["url"] as string; + state.Url = new Uri(url, UriKind.Absolute); + state.bucketId = FetchBucketId(url); + state.token = jsonData["token"] as string; + state.bucket = jsonData["bucket"] as string; + state.ObjectId = jsonData["objectId"] as string; + } + } + + string FetchBucketId(string url) + { + var elements = url.Split('/'); + + return elements[elements.Length - 1]; + } + public static byte[] StringToAscii(string s) + { + byte[] retval = new byte[s.Length]; + for (int ix = 0; ix < s.Length; ++ix) + { + char ch = s[ix]; + if (ch <= 0x7f) + retval[ix] = (byte)ch; + else + retval[ix] = (byte)'?'; + } + return retval; + } + public static string ToBase64URLSafe(string str) + { + return Encode(str); + } + public static string Encode(byte[] bs) + { + if (bs == null || bs.Length == 0) + return ""; + string encodedStr = Convert.ToBase64String(bs); + encodedStr = encodedStr.Replace('+', '-').Replace('/', '_'); + return encodedStr; + } + public static string Encode(string text) + { + if (String.IsNullOrEmpty(text)) + return ""; + byte[] bs = Encoding.UTF8.GetBytes(text); + string encodedStr = Convert.ToBase64String(bs); + encodedStr = encodedStr.Replace('+', '-').Replace('/', '_'); + return encodedStr; + } + + internal static string GetMD5Code(Stream data) + { + MD5 md5 = new MD5CryptoServiceProvider(); + byte[] retVal = md5.ComputeHash(data); + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < retVal.Length; i++) + { + sb.Append(retVal[i].ToString("x2")); + } + return sb.ToString(); + + } + + internal IDictionary GetMetaData(FileState state, Stream data) + { + IDictionary rtn = new Dictionary(); + + if (state.MetaData != null) + { + foreach (var meta in state.MetaData) + { + rtn.Add(meta.Key, meta.Value); + } + } + MergeDic(rtn, "mime_type", AVFile.GetMIMEType(state.Name)); + MergeDic(rtn, "size", data.Length); + MergeDic(rtn, "_checksum", GetMD5Code(data)); + if (AVUser.CurrentUser != null) + if (AVUser.CurrentUser.ObjectId != null) + MergeDic(rtn, "owner", AVUser.CurrentUser.ObjectId); + + return rtn; + } + internal void MergeDic(IDictionary dic, string key, object value) + { + if (dic.ContainsKey(key)) + { + dic[key] = value; + } + else + { + dic.Add(key, value); + } + } + } +} diff --git a/Storage/Storage/Internal/File/Cryptography/MD5/MD5.cs b/Storage/Storage/Internal/File/Cryptography/MD5/MD5.cs new file mode 100644 index 0000000..aa3eb2b --- /dev/null +++ b/Storage/Storage/Internal/File/Cryptography/MD5/MD5.cs @@ -0,0 +1,566 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.IO; + +namespace LeanCloud.Storage.Internal +{ + /// + /// 弥补Windows Phone 8 API没有自带MD5加密的拓展方法。 + /// + internal class MD5CryptoServiceProvider : MD5 + { + public MD5CryptoServiceProvider() + : base() + { + } + } + /// + /// Summary description for MD5. + /// + internal class MD5 : IDisposable + { + static public MD5 Create(string hashName) + { + if (hashName == "MD5") + return new MD5(); + else + throw new NotSupportedException(); + } + + static public string GetMd5String(String source) + { + MD5 md = MD5CryptoServiceProvider.Create(); + byte[] hash; + + //Create a new instance of ASCIIEncoding to + //convert the string into an array of Unicode bytes. + UTF8Encoding enc = new UTF8Encoding(); + // ASCIIEncoding enc = new ASCIIEncoding(); + + //Convert the string into an array of bytes. + byte[] buffer = enc.GetBytes(source); + + //Create the hash value from the array of bytes. + hash = md.ComputeHash(buffer); + + StringBuilder sb = new StringBuilder(); + foreach (byte b in hash) + sb.Append(b.ToString("x2")); + return sb.ToString(); + } + + static public MD5 Create() + { + return new MD5(); + } + + #region base implementation of the MD5 + #region constants + private const byte S11 = 7; + private const byte S12 = 12; + private const byte S13 = 17; + private const byte S14 = 22; + private const byte S21 = 5; + private const byte S22 = 9; + private const byte S23 = 14; + private const byte S24 = 20; + private const byte S31 = 4; + private const byte S32 = 11; + private const byte S33 = 16; + private const byte S34 = 23; + private const byte S41 = 6; + private const byte S42 = 10; + private const byte S43 = 15; + private const byte S44 = 21; + static private byte[] PADDING = new byte[] { + 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }; + #endregion + + #region F, G, H and I are basic MD5 functions. + static private uint F(uint x, uint y, uint z) + { + return (((x) & (y)) | ((~x) & (z))); + } + static private uint G(uint x, uint y, uint z) + { + return (((x) & (z)) | ((y) & (~z))); + } + static private uint H(uint x, uint y, uint z) + { + return ((x) ^ (y) ^ (z)); + } + static private uint I(uint x, uint y, uint z) + { + return ((y) ^ ((x) | (~z))); + } + #endregion + + #region rotates x left n bits. + /// + /// rotates x left n bits. + /// + /// + /// + /// + static private uint ROTATE_LEFT(uint x, byte n) + { + return (((x) << (n)) | ((x) >> (32 - (n)))); + } + #endregion + + #region FF, GG, HH, and II transformations + /// FF, GG, HH, and II transformations + /// for rounds 1, 2, 3, and 4. + /// Rotation is separate from addition to prevent recomputation. + static private void FF(ref uint a, uint b, uint c, uint d, uint x, byte s, uint ac) + { + (a) += F((b), (c), (d)) + (x) + (uint)(ac); + (a) = ROTATE_LEFT((a), (s)); + (a) += (b); + } + static private void GG(ref uint a, uint b, uint c, uint d, uint x, byte s, uint ac) + { + (a) += G((b), (c), (d)) + (x) + (uint)(ac); + (a) = ROTATE_LEFT((a), (s)); + (a) += (b); + } + static private void HH(ref uint a, uint b, uint c, uint d, uint x, byte s, uint ac) + { + (a) += H((b), (c), (d)) + (x) + (uint)(ac); + (a) = ROTATE_LEFT((a), (s)); + (a) += (b); + } + static private void II(ref uint a, uint b, uint c, uint d, uint x, byte s, uint ac) + { + (a) += I((b), (c), (d)) + (x) + (uint)(ac); + (a) = ROTATE_LEFT((a), (s)); + (a) += (b); + } + #endregion + + #region context info + /// + /// state (ABCD) + /// + uint[] state = new uint[4]; + + /// + /// number of bits, modulo 2^64 (lsb first) + /// + uint[] count = new uint[2]; + + /// + /// input buffer + /// + byte[] buffer = new byte[64]; + #endregion + + internal MD5() + { + Initialize(); + } + + /// + /// MD5 initialization. Begins an MD5 operation, writing a new context. + /// + /// + /// The RFC named it "MD5Init" + /// + public virtual void Initialize() + { + count[0] = count[1] = 0; + + // Load magic initialization constants. + state[0] = 0x67452301; + state[1] = 0xefcdab89; + state[2] = 0x98badcfe; + state[3] = 0x10325476; + } + + /// + /// MD5 block update operation. Continues an MD5 message-digest + /// operation, processing another message block, and updating the + /// context. + /// + /// + /// + /// + /// The RFC Named it MD5Update + protected virtual void HashCore(byte[] input, int offset, int count) + { + int i; + int index; + int partLen; + + // Compute number of bytes mod 64 + index = (int)((this.count[0] >> 3) & 0x3F); + + // Update number of bits + if ((this.count[0] += (uint)((uint)count << 3)) < ((uint)count << 3)) + this.count[1]++; + this.count[1] += ((uint)count >> 29); + + partLen = 64 - index; + + // Transform as many times as possible. + if (count >= partLen) + { + Buffer.BlockCopy(input, offset, this.buffer, index, partLen); + Transform(this.buffer, 0); + + for (i = partLen; i + 63 < count; i += 64) + Transform(input, offset + i); + + index = 0; + } + else + i = 0; + + // Buffer remaining input + Buffer.BlockCopy(input, offset + i, this.buffer, index, count - i); + } + + /// + /// MD5 finalization. Ends an MD5 message-digest operation, writing the + /// the message digest and zeroizing the context. + /// + /// message digest + /// The RFC named it MD5Final + protected virtual byte[] HashFinal() + { + byte[] digest = new byte[16]; + byte[] bits = new byte[8]; + int index, padLen; + + // Save number of bits + Encode(bits, 0, this.count, 0, 8); + + // Pad out to 56 mod 64. + index = (int)((uint)(this.count[0] >> 3) & 0x3f); + padLen = (index < 56) ? (56 - index) : (120 - index); + HashCore(PADDING, 0, padLen); + + // Append length (before padding) + HashCore(bits, 0, 8); + + // Store state in digest + Encode(digest, 0, state, 0, 16); + + // Zeroize sensitive information. + count[0] = count[1] = 0; + state[0] = 0; + state[1] = 0; + state[2] = 0; + state[3] = 0; + + // initialize again, to be ready to use + Initialize(); + + return digest; + } + + /// + /// MD5 basic transformation. Transforms state based on 64 bytes block. + /// + /// + /// + private void Transform(byte[] block, int offset) + { + uint a = state[0], b = state[1], c = state[2], d = state[3]; + uint[] x = new uint[16]; + Decode(x, 0, block, offset, 64); + + // Round 1 + FF(ref a, b, c, d, x[0], S11, 0xd76aa478); /* 1 */ + FF(ref d, a, b, c, x[1], S12, 0xe8c7b756); /* 2 */ + FF(ref c, d, a, b, x[2], S13, 0x242070db); /* 3 */ + FF(ref b, c, d, a, x[3], S14, 0xc1bdceee); /* 4 */ + FF(ref a, b, c, d, x[4], S11, 0xf57c0faf); /* 5 */ + FF(ref d, a, b, c, x[5], S12, 0x4787c62a); /* 6 */ + FF(ref c, d, a, b, x[6], S13, 0xa8304613); /* 7 */ + FF(ref b, c, d, a, x[7], S14, 0xfd469501); /* 8 */ + FF(ref a, b, c, d, x[8], S11, 0x698098d8); /* 9 */ + FF(ref d, a, b, c, x[9], S12, 0x8b44f7af); /* 10 */ + FF(ref c, d, a, b, x[10], S13, 0xffff5bb1); /* 11 */ + FF(ref b, c, d, a, x[11], S14, 0x895cd7be); /* 12 */ + FF(ref a, b, c, d, x[12], S11, 0x6b901122); /* 13 */ + FF(ref d, a, b, c, x[13], S12, 0xfd987193); /* 14 */ + FF(ref c, d, a, b, x[14], S13, 0xa679438e); /* 15 */ + FF(ref b, c, d, a, x[15], S14, 0x49b40821); /* 16 */ + + // Round 2 + GG(ref a, b, c, d, x[1], S21, 0xf61e2562); /* 17 */ + GG(ref d, a, b, c, x[6], S22, 0xc040b340); /* 18 */ + GG(ref c, d, a, b, x[11], S23, 0x265e5a51); /* 19 */ + GG(ref b, c, d, a, x[0], S24, 0xe9b6c7aa); /* 20 */ + GG(ref a, b, c, d, x[5], S21, 0xd62f105d); /* 21 */ + GG(ref d, a, b, c, x[10], S22, 0x2441453); /* 22 */ + GG(ref c, d, a, b, x[15], S23, 0xd8a1e681); /* 23 */ + GG(ref b, c, d, a, x[4], S24, 0xe7d3fbc8); /* 24 */ + GG(ref a, b, c, d, x[9], S21, 0x21e1cde6); /* 25 */ + GG(ref d, a, b, c, x[14], S22, 0xc33707d6); /* 26 */ + GG(ref c, d, a, b, x[3], S23, 0xf4d50d87); /* 27 */ + GG(ref b, c, d, a, x[8], S24, 0x455a14ed); /* 28 */ + GG(ref a, b, c, d, x[13], S21, 0xa9e3e905); /* 29 */ + GG(ref d, a, b, c, x[2], S22, 0xfcefa3f8); /* 30 */ + GG(ref c, d, a, b, x[7], S23, 0x676f02d9); /* 31 */ + GG(ref b, c, d, a, x[12], S24, 0x8d2a4c8a); /* 32 */ + + // Round 3 + HH(ref a, b, c, d, x[5], S31, 0xfffa3942); /* 33 */ + HH(ref d, a, b, c, x[8], S32, 0x8771f681); /* 34 */ + HH(ref c, d, a, b, x[11], S33, 0x6d9d6122); /* 35 */ + HH(ref b, c, d, a, x[14], S34, 0xfde5380c); /* 36 */ + HH(ref a, b, c, d, x[1], S31, 0xa4beea44); /* 37 */ + HH(ref d, a, b, c, x[4], S32, 0x4bdecfa9); /* 38 */ + HH(ref c, d, a, b, x[7], S33, 0xf6bb4b60); /* 39 */ + HH(ref b, c, d, a, x[10], S34, 0xbebfbc70); /* 40 */ + HH(ref a, b, c, d, x[13], S31, 0x289b7ec6); /* 41 */ + HH(ref d, a, b, c, x[0], S32, 0xeaa127fa); /* 42 */ + HH(ref c, d, a, b, x[3], S33, 0xd4ef3085); /* 43 */ + HH(ref b, c, d, a, x[6], S34, 0x4881d05); /* 44 */ + HH(ref a, b, c, d, x[9], S31, 0xd9d4d039); /* 45 */ + HH(ref d, a, b, c, x[12], S32, 0xe6db99e5); /* 46 */ + HH(ref c, d, a, b, x[15], S33, 0x1fa27cf8); /* 47 */ + HH(ref b, c, d, a, x[2], S34, 0xc4ac5665); /* 48 */ + + // Round 4 + II(ref a, b, c, d, x[0], S41, 0xf4292244); /* 49 */ + II(ref d, a, b, c, x[7], S42, 0x432aff97); /* 50 */ + II(ref c, d, a, b, x[14], S43, 0xab9423a7); /* 51 */ + II(ref b, c, d, a, x[5], S44, 0xfc93a039); /* 52 */ + II(ref a, b, c, d, x[12], S41, 0x655b59c3); /* 53 */ + II(ref d, a, b, c, x[3], S42, 0x8f0ccc92); /* 54 */ + II(ref c, d, a, b, x[10], S43, 0xffeff47d); /* 55 */ + II(ref b, c, d, a, x[1], S44, 0x85845dd1); /* 56 */ + II(ref a, b, c, d, x[8], S41, 0x6fa87e4f); /* 57 */ + II(ref d, a, b, c, x[15], S42, 0xfe2ce6e0); /* 58 */ + II(ref c, d, a, b, x[6], S43, 0xa3014314); /* 59 */ + II(ref b, c, d, a, x[13], S44, 0x4e0811a1); /* 60 */ + II(ref a, b, c, d, x[4], S41, 0xf7537e82); /* 61 */ + II(ref d, a, b, c, x[11], S42, 0xbd3af235); /* 62 */ + II(ref c, d, a, b, x[2], S43, 0x2ad7d2bb); /* 63 */ + II(ref b, c, d, a, x[9], S44, 0xeb86d391); /* 64 */ + + state[0] += a; + state[1] += b; + state[2] += c; + state[3] += d; + + // Zeroize sensitive information. + for (int i = 0; i < x.Length; i++) + x[i] = 0; + } + + /// + /// Encodes input (uint) into output (byte). Assumes len is + /// multiple of 4. + /// + /// + /// + /// + /// + /// + private static void Encode(byte[] output, int outputOffset, uint[] input, int inputOffset, int count) + { + int i, j; + int end = outputOffset + count; + for (i = inputOffset, j = outputOffset; j < end; i++, j += 4) + { + output[j] = (byte)(input[i] & 0xff); + output[j + 1] = (byte)((input[i] >> 8) & 0xff); + output[j + 2] = (byte)((input[i] >> 16) & 0xff); + output[j + 3] = (byte)((input[i] >> 24) & 0xff); + } + } + + /// + /// Decodes input (byte) into output (uint). Assumes len is + /// a multiple of 4. + /// + /// + /// + /// + /// + /// + static private void Decode(uint[] output, int outputOffset, byte[] input, int inputOffset, int count) + { + int i, j; + int end = inputOffset + count; + for (i = outputOffset, j = inputOffset; j < end; i++, j += 4) + output[i] = ((uint)input[j]) | (((uint)input[j + 1]) << 8) | (((uint)input[j + 2]) << 16) | (((uint)input[j + 3]) << 24); + } + #endregion + + #region expose the same interface as the regular MD5 object + + protected byte[] HashValue; + protected int State; + public virtual bool CanReuseTransform + { + get + { + return true; + } + } + + public virtual bool CanTransformMultipleBlocks + { + get + { + return true; + } + } + public virtual byte[] Hash + { + get + { + if (this.State != 0) + throw new InvalidOperationException(); + return (byte[])HashValue.Clone(); + } + } + public virtual int HashSize + { + get + { + return HashSizeValue; + } + } + protected int HashSizeValue = 128; + + public virtual int InputBlockSize + { + get + { + return 1; + } + } + public virtual int OutputBlockSize + { + get + { + return 1; + } + } + + public void Clear() + { + Dispose(true); + } + + public byte[] ComputeHash(byte[] buffer) + { + return ComputeHash(buffer, 0, buffer.Length); + } + public byte[] ComputeHash(byte[] buffer, int offset, int count) + { + Initialize(); + HashCore(buffer, offset, count); + HashValue = HashFinal(); + return (byte[])HashValue.Clone(); + } + + public byte[] ComputeHash(Stream inputStream) + { + Initialize(); + int count; + byte[] buffer = new byte[4096]; + while (0 < (count = inputStream.Read(buffer, 0, 4096))) + { + HashCore(buffer, 0, count); + } + HashValue = HashFinal(); + return (byte[])HashValue.Clone(); + } + + public int TransformBlock( + byte[] inputBuffer, + int inputOffset, + int inputCount, + byte[] outputBuffer, + int outputOffset + ) + { + if (inputBuffer == null) + { + throw new ArgumentNullException("inputBuffer"); + } + if (inputOffset < 0) + { + throw new ArgumentOutOfRangeException("inputOffset"); + } + if ((inputCount < 0) || (inputCount > inputBuffer.Length)) + { + throw new ArgumentException("inputCount"); + } + if ((inputBuffer.Length - inputCount) < inputOffset) + { + throw new ArgumentOutOfRangeException("inputOffset"); + } + if (this.State == 0) + { + Initialize(); + this.State = 1; + } + + HashCore(inputBuffer, inputOffset, inputCount); + if ((inputBuffer != outputBuffer) || (inputOffset != outputOffset)) + { + Buffer.BlockCopy(inputBuffer, inputOffset, outputBuffer, outputOffset, inputCount); + } + return inputCount; + } + public byte[] TransformFinalBlock( + byte[] inputBuffer, + int inputOffset, + int inputCount + ) + { + if (inputBuffer == null) + { + throw new ArgumentNullException("inputBuffer"); + } + if (inputOffset < 0) + { + throw new ArgumentOutOfRangeException("inputOffset"); + } + if ((inputCount < 0) || (inputCount > inputBuffer.Length)) + { + throw new ArgumentException("inputCount"); + } + if ((inputBuffer.Length - inputCount) < inputOffset) + { + throw new ArgumentOutOfRangeException("inputOffset"); + } + if (this.State == 0) + { + Initialize(); + } + HashCore(inputBuffer, inputOffset, inputCount); + HashValue = HashFinal(); + byte[] buffer = new byte[inputCount]; + Buffer.BlockCopy(inputBuffer, inputOffset, buffer, 0, inputCount); + this.State = 0; + return buffer; + } + #endregion + + protected virtual void Dispose(bool disposing) + { + if (!disposing) + Initialize(); + } + public void Dispose() + { + Dispose(true); + } + } +} + diff --git a/Storage/Storage/Internal/File/Cryptography/SHA1/SHA1CryptoServiceProvider.cs b/Storage/Storage/Internal/File/Cryptography/SHA1/SHA1CryptoServiceProvider.cs new file mode 100644 index 0000000..74f6e09 --- /dev/null +++ b/Storage/Storage/Internal/File/Cryptography/SHA1/SHA1CryptoServiceProvider.cs @@ -0,0 +1,495 @@ +// +// System.Security.Cryptography.SHA1CryptoServiceProvider.cs +// +// Authors: +// Matthew S. Ford (Matthew.S.Ford@Rose-Hulman.Edu) +// Sebastien Pouliot (sebastien@ximian.com) +// +// Copyright 2001 by Matthew S. Ford. +// Copyright (C) 2004, 2005, 2008 Novell, Inc (http://www.novell.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +// Note: +// The MS Framework includes two (almost) identical class for SHA1. +// SHA1Managed is a 100% managed implementation. +// SHA1CryptoServiceProvider (this file) is a wrapper on CryptoAPI. +// Mono must provide those two class for binary compatibility. +// In our case both class are wrappers around a managed internal class SHA1Internal. + +using System.IO; +using System; +using System.Runtime.InteropServices; + +namespace LeanCloud.Storage.Internal +{ + + internal class SHA1Internal + { + + private const int BLOCK_SIZE_BYTES = 64; + private uint[] _H; // these are my chaining variables + private ulong count; + private byte[] _ProcessingBuffer; // Used to start data when passed less than a block worth. + private int _ProcessingBufferCount; // Counts how much data we have stored that still needs processed. + private uint[] buff; + + public SHA1Internal() + { + _H = new uint[5]; + _ProcessingBuffer = new byte[BLOCK_SIZE_BYTES]; + buff = new uint[80]; + + Initialize(); + } + + public void HashCore(byte[] rgb, int ibStart, int cbSize) + { + int i; + + if (_ProcessingBufferCount != 0) + { + if (cbSize < (BLOCK_SIZE_BYTES - _ProcessingBufferCount)) + { + System.Buffer.BlockCopy(rgb, ibStart, _ProcessingBuffer, _ProcessingBufferCount, cbSize); + _ProcessingBufferCount += cbSize; + return; + } + else + { + i = (BLOCK_SIZE_BYTES - _ProcessingBufferCount); + System.Buffer.BlockCopy(rgb, ibStart, _ProcessingBuffer, _ProcessingBufferCount, i); + ProcessBlock(_ProcessingBuffer, 0); + _ProcessingBufferCount = 0; + ibStart += i; + cbSize -= i; + } + } + + for (i = 0; i < cbSize - cbSize % BLOCK_SIZE_BYTES; i += BLOCK_SIZE_BYTES) + { + ProcessBlock(rgb, (uint)(ibStart + i)); + } + + if (cbSize % BLOCK_SIZE_BYTES != 0) + { + System.Buffer.BlockCopy(rgb, cbSize - cbSize % BLOCK_SIZE_BYTES + ibStart, _ProcessingBuffer, 0, cbSize % BLOCK_SIZE_BYTES); + _ProcessingBufferCount = cbSize % BLOCK_SIZE_BYTES; + } + } + + public byte[] HashFinal() + { + byte[] hash = new byte[20]; + + ProcessFinalBlock(_ProcessingBuffer, 0, _ProcessingBufferCount); + + for (int i = 0; i < 5; i++) + { + for (int j = 0; j < 4; j++) + { + hash[i * 4 + j] = (byte)(_H[i] >> (8 * (3 - j))); + } + } + + return hash; + } + + public void Initialize() + { + count = 0; + _ProcessingBufferCount = 0; + + _H[0] = 0x67452301; + _H[1] = 0xefcdab89; + _H[2] = 0x98badcfe; + _H[3] = 0x10325476; + _H[4] = 0xC3D2E1F0; + } + + private void ProcessBlock(byte[] inputBuffer, uint inputOffset) + { + uint a, b, c, d, e; + + count += BLOCK_SIZE_BYTES; + + // abc removal would not work on the fields + uint[] _H = this._H; + uint[] buff = this.buff; + InitialiseBuff(buff, inputBuffer, inputOffset); + FillBuff(buff); + + a = _H[0]; + b = _H[1]; + c = _H[2]; + d = _H[3]; + e = _H[4]; + + // This function was unrolled because it seems to be doubling our performance with current compiler/VM. + // Possibly roll up if this changes. + + // ---- Round 1 -------- + int i = 0; + while (i < 20) + { + e += ((a << 5) | (a >> 27)) + (((c ^ d) & b) ^ d) + 0x5A827999 + buff[i]; + b = (b << 30) | (b >> 2); + + d += ((e << 5) | (e >> 27)) + (((b ^ c) & a) ^ c) + 0x5A827999 + buff[i + 1]; + a = (a << 30) | (a >> 2); + + c += ((d << 5) | (d >> 27)) + (((a ^ b) & e) ^ b) + 0x5A827999 + buff[i + 2]; + e = (e << 30) | (e >> 2); + + b += ((c << 5) | (c >> 27)) + (((e ^ a) & d) ^ a) + 0x5A827999 + buff[i + 3]; + d = (d << 30) | (d >> 2); + + a += ((b << 5) | (b >> 27)) + (((d ^ e) & c) ^ e) + 0x5A827999 + buff[i + 4]; + c = (c << 30) | (c >> 2); + i += 5; + } + + // ---- Round 2 -------- + while (i < 40) + { + e += ((a << 5) | (a >> 27)) + (b ^ c ^ d) + 0x6ED9EBA1 + buff[i]; + b = (b << 30) | (b >> 2); + + d += ((e << 5) | (e >> 27)) + (a ^ b ^ c) + 0x6ED9EBA1 + buff[i + 1]; + a = (a << 30) | (a >> 2); + + c += ((d << 5) | (d >> 27)) + (e ^ a ^ b) + 0x6ED9EBA1 + buff[i + 2]; + e = (e << 30) | (e >> 2); + + b += ((c << 5) | (c >> 27)) + (d ^ e ^ a) + 0x6ED9EBA1 + buff[i + 3]; + d = (d << 30) | (d >> 2); + + a += ((b << 5) | (b >> 27)) + (c ^ d ^ e) + 0x6ED9EBA1 + buff[i + 4]; + c = (c << 30) | (c >> 2); + i += 5; + } + + // ---- Round 3 -------- + while (i < 60) + { + e += ((a << 5) | (a >> 27)) + ((b & c) | (b & d) | (c & d)) + 0x8F1BBCDC + buff[i]; + b = (b << 30) | (b >> 2); + + d += ((e << 5) | (e >> 27)) + ((a & b) | (a & c) | (b & c)) + 0x8F1BBCDC + buff[i + 1]; + a = (a << 30) | (a >> 2); + + c += ((d << 5) | (d >> 27)) + ((e & a) | (e & b) | (a & b)) + 0x8F1BBCDC + buff[i + 2]; + e = (e << 30) | (e >> 2); + + b += ((c << 5) | (c >> 27)) + ((d & e) | (d & a) | (e & a)) + 0x8F1BBCDC + buff[i + 3]; + d = (d << 30) | (d >> 2); + + a += ((b << 5) | (b >> 27)) + ((c & d) | (c & e) | (d & e)) + 0x8F1BBCDC + buff[i + 4]; + c = (c << 30) | (c >> 2); + i += 5; + } + + // ---- Round 4 -------- + while (i < 80) + { + e += ((a << 5) | (a >> 27)) + (b ^ c ^ d) + 0xCA62C1D6 + buff[i]; + b = (b << 30) | (b >> 2); + + d += ((e << 5) | (e >> 27)) + (a ^ b ^ c) + 0xCA62C1D6 + buff[i + 1]; + a = (a << 30) | (a >> 2); + + c += ((d << 5) | (d >> 27)) + (e ^ a ^ b) + 0xCA62C1D6 + buff[i + 2]; + e = (e << 30) | (e >> 2); + + b += ((c << 5) | (c >> 27)) + (d ^ e ^ a) + 0xCA62C1D6 + buff[i + 3]; + d = (d << 30) | (d >> 2); + + a += ((b << 5) | (b >> 27)) + (c ^ d ^ e) + 0xCA62C1D6 + buff[i + 4]; + c = (c << 30) | (c >> 2); + i += 5; + } + + _H[0] += a; + _H[1] += b; + _H[2] += c; + _H[3] += d; + _H[4] += e; + } + + private static void InitialiseBuff(uint[] buff, byte[] input, uint inputOffset) + { + buff[0] = (uint)((input[inputOffset + 0] << 24) | (input[inputOffset + 1] << 16) | (input[inputOffset + 2] << 8) | (input[inputOffset + 3])); + buff[1] = (uint)((input[inputOffset + 4] << 24) | (input[inputOffset + 5] << 16) | (input[inputOffset + 6] << 8) | (input[inputOffset + 7])); + buff[2] = (uint)((input[inputOffset + 8] << 24) | (input[inputOffset + 9] << 16) | (input[inputOffset + 10] << 8) | (input[inputOffset + 11])); + buff[3] = (uint)((input[inputOffset + 12] << 24) | (input[inputOffset + 13] << 16) | (input[inputOffset + 14] << 8) | (input[inputOffset + 15])); + buff[4] = (uint)((input[inputOffset + 16] << 24) | (input[inputOffset + 17] << 16) | (input[inputOffset + 18] << 8) | (input[inputOffset + 19])); + buff[5] = (uint)((input[inputOffset + 20] << 24) | (input[inputOffset + 21] << 16) | (input[inputOffset + 22] << 8) | (input[inputOffset + 23])); + buff[6] = (uint)((input[inputOffset + 24] << 24) | (input[inputOffset + 25] << 16) | (input[inputOffset + 26] << 8) | (input[inputOffset + 27])); + buff[7] = (uint)((input[inputOffset + 28] << 24) | (input[inputOffset + 29] << 16) | (input[inputOffset + 30] << 8) | (input[inputOffset + 31])); + buff[8] = (uint)((input[inputOffset + 32] << 24) | (input[inputOffset + 33] << 16) | (input[inputOffset + 34] << 8) | (input[inputOffset + 35])); + buff[9] = (uint)((input[inputOffset + 36] << 24) | (input[inputOffset + 37] << 16) | (input[inputOffset + 38] << 8) | (input[inputOffset + 39])); + buff[10] = (uint)((input[inputOffset + 40] << 24) | (input[inputOffset + 41] << 16) | (input[inputOffset + 42] << 8) | (input[inputOffset + 43])); + buff[11] = (uint)((input[inputOffset + 44] << 24) | (input[inputOffset + 45] << 16) | (input[inputOffset + 46] << 8) | (input[inputOffset + 47])); + buff[12] = (uint)((input[inputOffset + 48] << 24) | (input[inputOffset + 49] << 16) | (input[inputOffset + 50] << 8) | (input[inputOffset + 51])); + buff[13] = (uint)((input[inputOffset + 52] << 24) | (input[inputOffset + 53] << 16) | (input[inputOffset + 54] << 8) | (input[inputOffset + 55])); + buff[14] = (uint)((input[inputOffset + 56] << 24) | (input[inputOffset + 57] << 16) | (input[inputOffset + 58] << 8) | (input[inputOffset + 59])); + buff[15] = (uint)((input[inputOffset + 60] << 24) | (input[inputOffset + 61] << 16) | (input[inputOffset + 62] << 8) | (input[inputOffset + 63])); + } + + private static void FillBuff(uint[] buff) + { + uint val; + for (int i = 16; i < 80; i += 8) + { + val = buff[i - 3] ^ buff[i - 8] ^ buff[i - 14] ^ buff[i - 16]; + buff[i] = (val << 1) | (val >> 31); + + val = buff[i - 2] ^ buff[i - 7] ^ buff[i - 13] ^ buff[i - 15]; + buff[i + 1] = (val << 1) | (val >> 31); + + val = buff[i - 1] ^ buff[i - 6] ^ buff[i - 12] ^ buff[i - 14]; + buff[i + 2] = (val << 1) | (val >> 31); + + val = buff[i + 0] ^ buff[i - 5] ^ buff[i - 11] ^ buff[i - 13]; + buff[i + 3] = (val << 1) | (val >> 31); + + val = buff[i + 1] ^ buff[i - 4] ^ buff[i - 10] ^ buff[i - 12]; + buff[i + 4] = (val << 1) | (val >> 31); + + val = buff[i + 2] ^ buff[i - 3] ^ buff[i - 9] ^ buff[i - 11]; + buff[i + 5] = (val << 1) | (val >> 31); + + val = buff[i + 3] ^ buff[i - 2] ^ buff[i - 8] ^ buff[i - 10]; + buff[i + 6] = (val << 1) | (val >> 31); + + val = buff[i + 4] ^ buff[i - 1] ^ buff[i - 7] ^ buff[i - 9]; + buff[i + 7] = (val << 1) | (val >> 31); + } + } + + private void ProcessFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount) + { + ulong total = count + (ulong)inputCount; + int paddingSize = (56 - (int)(total % BLOCK_SIZE_BYTES)); + + if (paddingSize < 1) + paddingSize += BLOCK_SIZE_BYTES; + + int length = inputCount + paddingSize + 8; + byte[] fooBuffer = (length == 64) ? _ProcessingBuffer : new byte[length]; + + for (int i = 0; i < inputCount; i++) + { + fooBuffer[i] = inputBuffer[i + inputOffset]; + } + + fooBuffer[inputCount] = 0x80; + for (int i = inputCount + 1; i < inputCount + paddingSize; i++) + { + fooBuffer[i] = 0x00; + } + + // I deal in bytes. The algorithm deals in bits. + ulong size = total << 3; + AddLength(size, fooBuffer, inputCount + paddingSize); + ProcessBlock(fooBuffer, 0); + + if (length == 128) + ProcessBlock(fooBuffer, 64); + } + + internal void AddLength(ulong length, byte[] buffer, int position) + { + buffer[position++] = (byte)(length >> 56); + buffer[position++] = (byte)(length >> 48); + buffer[position++] = (byte)(length >> 40); + buffer[position++] = (byte)(length >> 32); + buffer[position++] = (byte)(length >> 24); + buffer[position++] = (byte)(length >> 16); + buffer[position++] = (byte)(length >> 8); + buffer[position] = (byte)(length); + } + } + + public sealed class SHA1CryptoServiceProvider : SHA1 + { + + private SHA1Internal sha; + + public SHA1CryptoServiceProvider() + { + sha = new SHA1Internal(); + } + + ~SHA1CryptoServiceProvider() + { + Dispose(false); + } + + protected override void Dispose(bool disposing) + { + // nothing new to do (managed implementation) + base.Dispose(disposing); + } + + protected override void HashCore(byte[] rgb, int ibStart, int cbSize) + { + State = 1; + sha.HashCore(rgb, ibStart, cbSize); + } + + protected override byte[] HashFinal() + { + State = 0; + return sha.HashFinal(); + } + + public override void Initialize() + { + sha.Initialize(); + } + } + + public abstract class SHA1 : HashAlgorithm + { + protected SHA1() + { + HashSizeValue = 160; + } + } + + public abstract class HashAlgorithm : IDisposable + { + protected int HashSizeValue; + protected internal byte[] HashValue; + protected int State = 0; + + private bool m_bDisposed = false; + + protected HashAlgorithm() { } + + // + // public properties + // + + public virtual int HashSize + { + get { return HashSizeValue; } + } + + // + // public methods + // + + public byte[] ComputeHash(Stream inputStream) + { + if (m_bDisposed) + throw new ObjectDisposedException(null); + + // Default the buffer size to 4K. + byte[] buffer = new byte[4096]; + int bytesRead; + do + { + bytesRead = inputStream.Read(buffer, 0, 4096); + if (bytesRead > 0) + { + HashCore(buffer, 0, bytesRead); + } + } while (bytesRead > 0); + + HashValue = HashFinal(); + byte[] Tmp = (byte[])HashValue.Clone(); + Initialize(); + return (Tmp); + } + + public byte[] ComputeHash(byte[] buffer) + { + if (m_bDisposed) + throw new ObjectDisposedException(null); + + // Do some validation + if (buffer == null) throw new ArgumentNullException("buffer"); + + HashCore(buffer, 0, buffer.Length); + HashValue = HashFinal(); + byte[] Tmp = (byte[])HashValue.Clone(); + Initialize(); + return (Tmp); + } + + // ICryptoTransform methods + + // we assume any HashAlgorithm can take input a byte at a time + public virtual int InputBlockSize + { + get { return (1); } + } + + public virtual int OutputBlockSize + { + get { return (1); } + } + + public virtual bool CanTransformMultipleBlocks + { + get { return (true); } + } + + public virtual bool CanReuseTransform + { + get { return (true); } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public void Clear() + { + (this as IDisposable).Dispose(); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (HashValue != null) + Array.Clear(HashValue, 0, HashValue.Length); + HashValue = null; + m_bDisposed = true; + } + } + + // + // abstract public methods + // + + public abstract void Initialize(); + + protected abstract void HashCore(byte[] array, int ibStart, int cbSize); + + protected abstract byte[] HashFinal(); + } +} diff --git a/Storage/Storage/Internal/File/State/FileState.cs b/Storage/Storage/Internal/File/State/FileState.cs new file mode 100644 index 0000000..667646a --- /dev/null +++ b/Storage/Storage/Internal/File/State/FileState.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace LeanCloud.Storage.Internal +{ + public class FileState + { + public string ObjectId { get; internal set; } + public string Name { get; internal set; } + public string CloudName { get; set; } + public string MimeType { get; internal set; } + public Uri Url { get; internal set; } + public IDictionary MetaData { get; internal set; } + public long Size { get; internal set; } + public long FixedChunkSize { get; internal set; } + + public int counter; + public Stream frozenData; + public string bucketId; + public string bucket; + public string token; + public long completed; + public List block_ctxes = new List(); + + } +} diff --git a/Storage/Storage/Internal/HttpClient/HttpClient.cs b/Storage/Storage/Internal/HttpClient/HttpClient.cs new file mode 100644 index 0000000..29ef446 --- /dev/null +++ b/Storage/Storage/Internal/HttpClient/HttpClient.cs @@ -0,0 +1,86 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using System.Text; +using System.IO; +using NetHttpClient = System.Net.Http.HttpClient; + +namespace LeanCloud.Storage.Internal { + public class HttpClient : IHttpClient { + static readonly HashSet HttpContentHeaders = new HashSet { + { "Allow" }, + { "Content-Disposition" }, + { "Content-Encoding" }, + { "Content-Language" }, + { "Content-Length" }, + { "Content-Location" }, + { "Content-MD5" }, + { "Content-Range" }, + { "Content-Type" }, + { "Expires" }, + { "Last-Modified" } + }; + + readonly NetHttpClient client; + + public HttpClient() { + client = new NetHttpClient(); + // TODO 设置版本号 + client.DefaultRequestHeaders.Add("User-Agent", "LeanCloud-dotNet-SDK/" + "2.0.0"); + } + + public HttpClient(NetHttpClient client) { + this.client = client; + } + + public async Task> ExecuteAsync(HttpRequest httpRequest, + IProgress uploadProgress, + IProgress downloadProgress, + CancellationToken cancellationToken) { + + HttpMethod httpMethod = new HttpMethod(httpRequest.Method); + HttpRequestMessage message = new HttpRequestMessage(httpMethod, httpRequest.Uri); + + // Fill in zero-length data if method is post. + Stream data = httpRequest.Data; + if (httpRequest.Data == null && httpRequest.Method.ToLower().Equals("post")) { + data = new MemoryStream(new byte[0]); + } + + if (data != null) { + message.Content = new StreamContent(data); + } + + if (httpRequest.Headers != null) { + foreach (var header in httpRequest.Headers) { + if (!string.IsNullOrEmpty(header.Value)) { + if (HttpContentHeaders.Contains(header.Key)) { + message.Content.Headers.Add(header.Key, header.Value); + } else { + message.Headers.Add(header.Key, header.Value); + } + } + } + } + + // Avoid aggressive caching on Windows Phone 8.1. + message.Headers.Add("Cache-Control", "no-cache"); + message.Headers.IfModifiedSince = DateTimeOffset.UtcNow; + + uploadProgress?.Report(new AVUploadProgressEventArgs { Progress = 0 }); + var response = await client.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + uploadProgress?.Report(new AVUploadProgressEventArgs { Progress = 1 }); + + var resultString = await response.Content.ReadAsStringAsync(); + response.Dispose(); + + downloadProgress?.Report(new AVDownloadProgressEventArgs { Progress = 1 }); + + return new Tuple(response.StatusCode, resultString); + } + } +} diff --git a/Storage/Storage/Internal/HttpClient/HttpRequest.cs b/Storage/Storage/Internal/HttpClient/HttpRequest.cs new file mode 100644 index 0000000..f3e1444 --- /dev/null +++ b/Storage/Storage/Internal/HttpClient/HttpRequest.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace LeanCloud.Storage.Internal +{ + /// + /// IHttpRequest is an interface that provides an API to execute HTTP request data. + /// + public class HttpRequest + { + public Uri Uri { get; set; } + public IList> Headers { get; set; } + + /// + /// Data stream to be uploaded. + /// + public virtual Stream Data { get; set; } + + /// + /// HTTP method. One of DELETE, GET, HEAD, POST or PUT + /// + public string Method { get; set; } + } +} diff --git a/Storage/Storage/Internal/HttpClient/IHttpClient.cs b/Storage/Storage/Internal/HttpClient/IHttpClient.cs new file mode 100644 index 0000000..6bbe9d2 --- /dev/null +++ b/Storage/Storage/Internal/HttpClient/IHttpClient.cs @@ -0,0 +1,24 @@ +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal +{ + public interface IHttpClient + { + /// + /// Executes HTTP request to a with HTTP verb + /// and . + /// + /// The HTTP request to be executed. + /// Upload progress callback. + /// Download progress callback. + /// The cancellation token. + /// A task that resolves to Htt + Task> ExecuteAsync(HttpRequest httpRequest, + IProgress uploadProgress, + IProgress downloadProgress, + CancellationToken cancellationToken); + } +} diff --git a/Storage/Storage/Internal/IAVCorePlugins.cs b/Storage/Storage/Internal/IAVCorePlugins.cs new file mode 100644 index 0000000..51060bd --- /dev/null +++ b/Storage/Storage/Internal/IAVCorePlugins.cs @@ -0,0 +1,26 @@ +using LeanCloud.Storage.Internal; +using System; + +namespace LeanCloud.Storage.Internal +{ + public interface IAVCorePlugins + { + void Reset(); + + IHttpClient HttpClient { get; } + IAppRouterController AppRouterController { get; } + IAVCommandRunner CommandRunner { get; } + IStorageController StorageController { get; } + + IAVCloudCodeController CloudCodeController { get; } + IAVConfigController ConfigController { get; } + IAVFileController FileController { get; } + IAVObjectController ObjectController { get; } + IAVQueryController QueryController { get; } + IAVSessionController SessionController { get; } + IAVUserController UserController { get; } + IObjectSubclassingController SubclassingController { get; } + IAVCurrentUserController CurrentUserController { get; } + IInstallationIdController InstallationIdController { get; } + } +} \ No newline at end of file diff --git a/Storage/Storage/Internal/InstallationId/Controller/IInstallationIdController.cs b/Storage/Storage/Internal/InstallationId/Controller/IInstallationIdController.cs new file mode 100644 index 0000000..1fb9af4 --- /dev/null +++ b/Storage/Storage/Internal/InstallationId/Controller/IInstallationIdController.cs @@ -0,0 +1,24 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + public interface IInstallationIdController { + /// + /// Sets current installationId and saves it to local storage. + /// + /// The installationId to be saved. + Task SetAsync(Guid? installationId); + + /// + /// Gets current installationId from local storage. Generates a none exists. + /// + /// Current installationId. + Task GetAsync(); + + /// + /// Clears current installationId from memory and local storage. + /// + Task ClearAsync(); + } +} diff --git a/Storage/Storage/Internal/InstallationId/Controller/InstallationIdController.cs b/Storage/Storage/Internal/InstallationId/Controller/InstallationIdController.cs new file mode 100644 index 0000000..d28b015 --- /dev/null +++ b/Storage/Storage/Internal/InstallationId/Controller/InstallationIdController.cs @@ -0,0 +1,66 @@ +using LeanCloud.Storage.Internal; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + public class InstallationIdController : IInstallationIdController { + private const string InstallationIdKey = "InstallationId"; + private readonly object mutex = new object(); + private Guid? installationId; + + private readonly IStorageController storageController; + public InstallationIdController(IStorageController storageController) { + this.storageController = storageController; + } + + public Task SetAsync(Guid? installationId) { + lock (mutex) { + Task saveTask; + + if (installationId == null) { + saveTask = storageController + .LoadAsync() + .OnSuccess(storage => storage.Result.RemoveAsync(InstallationIdKey)) + .Unwrap(); + } else { + saveTask = storageController + .LoadAsync() + .OnSuccess(storage => storage.Result.AddAsync(InstallationIdKey, installationId.ToString())) + .Unwrap(); + } + this.installationId = installationId; + return saveTask; + } + } + + public Task GetAsync() { + lock (mutex) { + if (installationId != null) { + return Task.FromResult(installationId); + } + } + + return storageController + .LoadAsync() + .OnSuccess, Task>(s => { + object id; + s.Result.TryGetValue(InstallationIdKey, out id); + try { + lock (mutex) { + installationId = new Guid((string)id); + return Task.FromResult(installationId); + } + } catch (Exception) { + var newInstallationId = Guid.NewGuid(); + return SetAsync(newInstallationId).OnSuccess(_ => newInstallationId); + } + }) + .Unwrap(); + } + + public Task ClearAsync() { + return SetAsync(null); + } + } +} diff --git a/Storage/Storage/Internal/Object/Controller/AVObjectController.cs b/Storage/Storage/Internal/Object/Controller/AVObjectController.cs new file mode 100644 index 0000000..63e5a30 --- /dev/null +++ b/Storage/Storage/Internal/Object/Controller/AVObjectController.cs @@ -0,0 +1,248 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using LeanCloud.Utilities; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal +{ + public class AVObjectController : IAVObjectController + { + private readonly IAVCommandRunner commandRunner; + + public AVObjectController(IAVCommandRunner commandRunner) + { + this.commandRunner = commandRunner; + } + + public Task FetchAsync(IObjectState state, + string sessionToken, + CancellationToken cancellationToken) + { + var command = new AVCommand(string.Format("classes/{0}/{1}", + Uri.EscapeDataString(state.ClassName), + Uri.EscapeDataString(state.ObjectId)), + method: "GET", + sessionToken: sessionToken, + data: null); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + { + return AVObjectCoder.Instance.Decode(t.Result.Item2, AVDecoder.Instance); + }); + } + + public Task FetchAsync(IObjectState state, + IDictionary queryString, + string sessionToken, + CancellationToken cancellationToken) + { + var command = new AVCommand(string.Format("classes/{0}/{1}?{2}", + Uri.EscapeDataString(state.ClassName), + Uri.EscapeDataString(state.ObjectId), + AVClient.BuildQueryString(queryString)), + method: "GET", + sessionToken: sessionToken, + data: null); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + { + return AVObjectCoder.Instance.Decode(t.Result.Item2, AVDecoder.Instance); + }); + } + + public Task SaveAsync(IObjectState state, + IDictionary operations, + string sessionToken, + CancellationToken cancellationToken) + { + var objectJSON = AVObject.ToJSONObjectForSaving(operations); + + var command = new AVCommand((state.ObjectId == null ? + string.Format("classes/{0}", Uri.EscapeDataString(state.ClassName)) : + string.Format("classes/{0}/{1}", Uri.EscapeDataString(state.ClassName), state.ObjectId)), + method: (state.ObjectId == null ? "POST" : "PUT"), + sessionToken: sessionToken, + data: objectJSON); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + { + var serverState = AVObjectCoder.Instance.Decode(t.Result.Item2, AVDecoder.Instance); + serverState = serverState.MutatedClone(mutableClone => + { + mutableClone.IsNew = t.Result.Item1 == System.Net.HttpStatusCode.Created; + }); + return serverState; + }); + } + + public IList> SaveAllAsync(IList states, + IList> operationsList, + string sessionToken, + CancellationToken cancellationToken) + { + + var requests = states + .Zip(operationsList, (item, ops) => new AVCommand( + item.ObjectId == null + ? string.Format("classes/{0}", Uri.EscapeDataString(item.ClassName)) + : string.Format("classes/{0}/{1}", Uri.EscapeDataString(item.ClassName), Uri.EscapeDataString(item.ObjectId)), + method: item.ObjectId == null ? "POST" : "PUT", + data: AVObject.ToJSONObjectForSaving(ops))) + .ToList(); + + var batchTasks = ExecuteBatchRequests(requests, sessionToken, cancellationToken); + var stateTasks = new List>(); + foreach (var task in batchTasks) + { + stateTasks.Add(task.OnSuccess(t => + { + return AVObjectCoder.Instance.Decode(t.Result, AVDecoder.Instance); + })); + } + + return stateTasks; + } + + public Task DeleteAsync(IObjectState state, + string sessionToken, + CancellationToken cancellationToken) + { + var command = new AVCommand(string.Format("classes/{0}/{1}", + state.ClassName, state.ObjectId), + method: "DELETE", + sessionToken: sessionToken, + data: null); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken); + } + + public IList DeleteAllAsync(IList states, + string sessionToken, + CancellationToken cancellationToken) + { + var requests = states + .Where(item => item.ObjectId != null) + .Select(item => new AVCommand( + string.Format("classes/{0}/{1}", Uri.EscapeDataString(item.ClassName), Uri.EscapeDataString(item.ObjectId)), + method: "DELETE", + data: null)) + .ToList(); + return ExecuteBatchRequests(requests, sessionToken, cancellationToken).Cast().ToList(); + } + + // TODO (hallucinogen): move this out to a class to be used by Analytics + private const int MaximumBatchSize = 50; + internal IList>> ExecuteBatchRequests(IList requests, + string sessionToken, + CancellationToken cancellationToken) + { + var tasks = new List>>(); + int batchSize = requests.Count; + + IEnumerable remaining = requests; + while (batchSize > MaximumBatchSize) + { + var process = remaining.Take(MaximumBatchSize).ToList(); + remaining = remaining.Skip(MaximumBatchSize); + + tasks.AddRange(ExecuteBatchRequest(process, sessionToken, cancellationToken)); + + batchSize = remaining.Count(); + } + tasks.AddRange(ExecuteBatchRequest(remaining.ToList(), sessionToken, cancellationToken)); + + return tasks; + } + + private IList>> ExecuteBatchRequest(IList requests, + string sessionToken, + CancellationToken cancellationToken) + { + var tasks = new List>>(); + int batchSize = requests.Count; + var tcss = new List>>(); + for (int i = 0; i < batchSize; ++i) + { + var tcs = new TaskCompletionSource>(); + tcss.Add(tcs); + tasks.Add(tcs.Task); + } + + var encodedRequests = requests.Select(r => + { + var results = new Dictionary { + { "method", r.Method }, + { "path", r.Uri.AbsolutePath }, + }; + + if (r.DataObject != null) + { + results["body"] = r.DataObject; + } + return results; + }).Cast().ToList(); + var command = new AVCommand("batch", + method: "POST", + sessionToken: sessionToken, + data: new Dictionary { { "requests", encodedRequests } }); + + commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).ContinueWith(t => + { + if (t.IsFaulted || t.IsCanceled) + { + foreach (var tcs in tcss) + { + if (t.IsFaulted) + { + tcs.TrySetException(t.Exception); + } + else if (t.IsCanceled) + { + tcs.TrySetCanceled(); + } + } + return; + } + + var resultsArray = Conversion.As>(t.Result.Item2["results"]); + int resultLength = resultsArray.Count; + if (resultLength != batchSize) + { + foreach (var tcs in tcss) + { + tcs.TrySetException(new InvalidOperationException( + "Batch command result count expected: " + batchSize + " but was: " + resultLength + ".")); + } + return; + } + + for (int i = 0; i < batchSize; ++i) + { + var result = resultsArray[i] as Dictionary; + var tcs = tcss[i]; + + if (result.ContainsKey("success")) + { + tcs.TrySetResult(result["success"] as IDictionary); + } + else if (result.ContainsKey("error")) + { + var error = result["error"] as IDictionary; + long errorCode = long.Parse(error["code"].ToString()); + tcs.TrySetException(new AVException((AVException.ErrorCode)errorCode, error["error"] as string)); + } + else + { + tcs.TrySetException(new InvalidOperationException( + "Invalid batch command response.")); + } + } + }); + + return tasks; + } + } +} diff --git a/Storage/Storage/Internal/Object/Controller/IAVObjectController.cs b/Storage/Storage/Internal/Object/Controller/IAVObjectController.cs new file mode 100644 index 0000000..9230eb5 --- /dev/null +++ b/Storage/Storage/Internal/Object/Controller/IAVObjectController.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal +{ + public interface IAVObjectController + { + //Task FetchAsync(IObjectState state, + // string sessionToken, + // CancellationToken cancellationToken); + + Task FetchAsync(IObjectState state, + IDictionary queryString, + string sessionToken, + CancellationToken cancellationToken); + + Task SaveAsync(IObjectState state, + IDictionary operations, + string sessionToken, + CancellationToken cancellationToken); + + IList> SaveAllAsync(IList states, + IList> operationsList, + string sessionToken, + CancellationToken cancellationToken); + + Task DeleteAsync(IObjectState state, + string sessionToken, + CancellationToken cancellationToken); + + IList DeleteAllAsync(IList states, + string sessionToken, + CancellationToken cancellationToken); + } +} diff --git a/Storage/Storage/Internal/Object/Controller/IAVObjectCurrentController.cs b/Storage/Storage/Internal/Object/Controller/IAVObjectCurrentController.cs new file mode 100644 index 0000000..e9b6f40 --- /dev/null +++ b/Storage/Storage/Internal/Object/Controller/IAVObjectCurrentController.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + /// + /// IAVObjectCurrentController controls the single-instance + /// persistence used throughout the code-base. Sample usages are and + /// . + /// + /// Type of object being persisted. + public interface IAVObjectCurrentController where T : AVObject { + /// + /// Persists current . + /// + /// to be persisted. + /// The cancellation token. + Task SetAsync(T obj, CancellationToken cancellationToken); + + /// + /// Gets the persisted current . + /// + /// The cancellation token. + Task GetAsync(CancellationToken cancellationToken); + + /// + /// Returns a that resolves to true if current + /// exists. + /// + /// The cancellation token. + Task ExistsAsync(CancellationToken cancellationToken); + + /// + /// Returns true if the given is the persisted current + /// . + /// + /// The object to check. + /// True if obj is the current persisted . + bool IsCurrent(T obj); + + /// + /// Nullifies the current from memory. + /// + void ClearFromMemory(); + + /// + /// Clears current from disk. + /// + void ClearFromDisk(); + } +} diff --git a/Storage/Storage/Internal/Object/State/IObjectState.cs b/Storage/Storage/Internal/Object/State/IObjectState.cs new file mode 100644 index 0000000..ab7b074 --- /dev/null +++ b/Storage/Storage/Internal/Object/State/IObjectState.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +namespace LeanCloud.Storage.Internal +{ + public interface IObjectState : IEnumerable> + { + bool IsNew { get; } + string ClassName { get; } + string ObjectId { get; } + DateTime? UpdatedAt { get; } + DateTime? CreatedAt { get; } + object this[string key] { get; } + + bool ContainsKey(string key); + + IObjectState MutatedClone(Action func); + } +} diff --git a/Storage/Storage/Internal/Object/State/MutableObjectState.cs b/Storage/Storage/Internal/Object/State/MutableObjectState.cs new file mode 100644 index 0000000..ff9be3a --- /dev/null +++ b/Storage/Storage/Internal/Object/State/MutableObjectState.cs @@ -0,0 +1,114 @@ +using System; +using System.Linq; +using System.Collections.Generic; + +namespace LeanCloud.Storage.Internal +{ + public class MutableObjectState : IObjectState + { + public bool IsNew { get; set; } + public string ClassName { get; set; } + public string ObjectId { get; set; } + public DateTime? UpdatedAt { get; set; } + public DateTime? CreatedAt { get; set; } + + // Initialize serverData to avoid further null checking. + private IDictionary serverData = new Dictionary(); + public IDictionary ServerData + { + get + { + return serverData; + } + + set + { + serverData = value; + } + } + + public object this[string key] + { + get + { + return ServerData[key]; + } + } + + public bool ContainsKey(string key) + { + return ServerData.ContainsKey(key); + } + + public void Apply(IDictionary operationSet) + { + // Apply operationSet + foreach (var pair in operationSet) + { + object oldValue; + ServerData.TryGetValue(pair.Key, out oldValue); + var newValue = pair.Value.Apply(oldValue, pair.Key); + if (newValue != AVDeleteOperation.DeleteToken) + { + ServerData[pair.Key] = newValue; + } + else + { + ServerData.Remove(pair.Key); + } + } + } + + public void Apply(IObjectState other) + { + IsNew = other.IsNew; + if (other.ObjectId != null) + { + ObjectId = other.ObjectId; + } + if (other.UpdatedAt != null) + { + UpdatedAt = other.UpdatedAt; + } + if (other.CreatedAt != null) + { + CreatedAt = other.CreatedAt; + } + + foreach (var pair in other) + { + ServerData[pair.Key] = pair.Value; + } + } + + public IObjectState MutatedClone(Action func) + { + var clone = MutableClone(); + func(clone); + return clone; + } + + protected virtual MutableObjectState MutableClone() + { + return new MutableObjectState + { + IsNew = IsNew, + ClassName = ClassName, + ObjectId = ObjectId, + CreatedAt = CreatedAt, + UpdatedAt = UpdatedAt, + ServerData = this.ToDictionary(t => t.Key, t => t.Value) + }; + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + return ServerData.GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return ((IEnumerable>)this).GetEnumerator(); + } + } +} diff --git a/Storage/Storage/Internal/Object/Subclassing/IObjectSubclassingController.cs b/Storage/Storage/Internal/Object/Subclassing/IObjectSubclassingController.cs new file mode 100644 index 0000000..38322fc --- /dev/null +++ b/Storage/Storage/Internal/Object/Subclassing/IObjectSubclassingController.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal +{ + public interface IObjectSubclassingController + { + String GetClassName(Type type); + Type GetType(String className); + + bool IsTypeValid(String className, Type type); + + void RegisterSubclass(Type t); + void UnregisterSubclass(Type t); + + void AddRegisterHook(Type t, Action action); + + AVObject Instantiate(String className); + IDictionary GetPropertyMappings(String className); + } +} diff --git a/Storage/Storage/Internal/Object/Subclassing/ObjectSubclassInfo.cs b/Storage/Storage/Internal/Object/Subclassing/ObjectSubclassInfo.cs new file mode 100644 index 0000000..080fe6c --- /dev/null +++ b/Storage/Storage/Internal/Object/Subclassing/ObjectSubclassInfo.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Reflection; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal +{ + internal class ObjectSubclassInfo + { + public ObjectSubclassInfo(Type type, ConstructorInfo constructor) + { + TypeInfo = type.GetTypeInfo(); + ClassName = GetClassName(TypeInfo); + Constructor = constructor; + PropertyMappings = ReflectionHelpers.GetProperties(type) + .Select(prop => Tuple.Create(prop, prop.GetCustomAttribute(true))) + .Where(t => t.Item2 != null) + .Select(t => Tuple.Create(t.Item1, t.Item2.FieldName)) + .ToDictionary(t => t.Item1.Name, t => t.Item2); + } + + public TypeInfo TypeInfo { get; private set; } + public String ClassName { get; private set; } + public IDictionary PropertyMappings { get; private set; } + private ConstructorInfo Constructor { get; set; } + + public AVObject Instantiate() + { + return (AVObject)Constructor.Invoke(null); + } + + internal static String GetClassName(TypeInfo type) + { + var attribute = type.GetCustomAttribute(); + return attribute != null ? attribute.ClassName : null; + } + } +} diff --git a/Storage/Storage/Internal/Object/Subclassing/ObjectSubclassingController.cs b/Storage/Storage/Internal/Object/Subclassing/ObjectSubclassingController.cs new file mode 100644 index 0000000..b1f913e --- /dev/null +++ b/Storage/Storage/Internal/Object/Subclassing/ObjectSubclassingController.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal +{ + internal class ObjectSubclassingController : IObjectSubclassingController + { + // Class names starting with _ are documented to be reserved. Use this one + // here to allow us to 'inherit' certain properties. + private static readonly string avObjectClassName = "_AVObject"; + + private readonly ReaderWriterLockSlim mutex; + private readonly IDictionary registeredSubclasses; + private Dictionary registerActions; + + public ObjectSubclassingController() + { + mutex = new ReaderWriterLockSlim(); + registeredSubclasses = new Dictionary(); + registerActions = new Dictionary(); + + // Register the AVObject subclass, so we get access to the ACL, + // objectId, and other AVFieldName properties. + RegisterSubclass(typeof(AVObject)); + } + + public String GetClassName(Type type) + { + return type == typeof(AVObject) + ? avObjectClassName + : ObjectSubclassInfo.GetClassName(type.GetTypeInfo()); + } + + public Type GetType(String className) + { + ObjectSubclassInfo info = null; + mutex.EnterReadLock(); + registeredSubclasses.TryGetValue(className, out info); + mutex.ExitReadLock(); + + return info != null + ? info.TypeInfo.AsType() + : null; + } + + public bool IsTypeValid(String className, Type type) + { + ObjectSubclassInfo subclassInfo = null; + + mutex.EnterReadLock(); + registeredSubclasses.TryGetValue(className, out subclassInfo); + mutex.ExitReadLock(); + + return subclassInfo == null + ? type == typeof(AVObject) + : subclassInfo.TypeInfo == type.GetTypeInfo(); + } + + public void RegisterSubclass(Type type) + { + TypeInfo typeInfo = type.GetTypeInfo(); + if (!typeof(AVObject).GetTypeInfo().IsAssignableFrom(typeInfo)) + { + throw new ArgumentException("Cannot register a type that is not a subclass of AVObject"); + } + + String className = GetClassName(type); + + try + { + // Perform this as a single independent transaction, so we can never get into an + // intermediate state where we *theoretically* register the wrong class due to a + // TOCTTOU bug. + mutex.EnterWriteLock(); + + ObjectSubclassInfo previousInfo = null; + if (registeredSubclasses.TryGetValue(className, out previousInfo)) + { + if (typeInfo.IsAssignableFrom(previousInfo.TypeInfo)) + { + // Previous subclass is more specific or equal to the current type, do nothing. + return; + } + else if (previousInfo.TypeInfo.IsAssignableFrom(typeInfo)) + { + // Previous subclass is parent of new child, fallthrough and actually register + // this class. + /* Do nothing */ + } + else + { + throw new ArgumentException( + "Tried to register both " + previousInfo.TypeInfo.FullName + " and " + typeInfo.FullName + + " as the AVObject subclass of " + className + ". Cannot determine the right class " + + "to use because neither inherits from the other." + ); + } + } + + ConstructorInfo constructor = type.FindConstructor(); + if (constructor == null) + { + throw new ArgumentException("Cannot register a type that does not implement the default constructor!"); + } + + registeredSubclasses[className] = new ObjectSubclassInfo(type, constructor); + } + finally + { + mutex.ExitWriteLock(); + } + + Action toPerform; + + mutex.EnterReadLock(); + registerActions.TryGetValue(className, out toPerform); + mutex.ExitReadLock(); + + if (toPerform != null) + { + toPerform(); + } + } + + public void UnregisterSubclass(Type type) + { + mutex.EnterWriteLock(); + registeredSubclasses.Remove(GetClassName(type)); + mutex.ExitWriteLock(); + } + + public void AddRegisterHook(Type t, Action action) + { + mutex.EnterWriteLock(); + registerActions.Add(GetClassName(t), action); + mutex.ExitWriteLock(); + } + + public AVObject Instantiate(String className) + { + ObjectSubclassInfo info = null; + + mutex.EnterReadLock(); + registeredSubclasses.TryGetValue(className, out info); + mutex.ExitReadLock(); + + return info != null + ? info.Instantiate() + : new AVObject(className); + } + + public IDictionary GetPropertyMappings(String className) + { + ObjectSubclassInfo info = null; + mutex.EnterReadLock(); + registeredSubclasses.TryGetValue(className, out info); + if (info == null) + { + registeredSubclasses.TryGetValue(avObjectClassName, out info); + } + mutex.ExitReadLock(); + + return info.PropertyMappings; + } + + } +} diff --git a/Storage/Storage/Internal/Operation/AVAddOperation.cs b/Storage/Storage/Internal/Operation/AVAddOperation.cs new file mode 100644 index 0000000..ed0a83e --- /dev/null +++ b/Storage/Storage/Internal/Operation/AVAddOperation.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using LeanCloud.Utilities; + +namespace LeanCloud.Storage.Internal { + public class AVAddOperation : IAVFieldOperation { + private ReadOnlyCollection objects; + public AVAddOperation(IEnumerable objects) { + this.objects = new ReadOnlyCollection(objects.ToList()); + } + + public object Encode() { + return new Dictionary { + {"__op", "Add"}, + {"objects", PointerOrLocalIdEncoder.Instance.Encode(objects)} + }; + } + + public IAVFieldOperation MergeWithPrevious(IAVFieldOperation previous) { + if (previous == null) { + return this; + } + if (previous is AVDeleteOperation) { + return new AVSetOperation(objects.ToList()); + } + if (previous is AVSetOperation) { + var setOp = (AVSetOperation)previous; + var oldList = Conversion.To>(setOp.Value); + return new AVSetOperation(oldList.Concat(objects).ToList()); + } + if (previous is AVAddOperation) { + return new AVAddOperation(((AVAddOperation)previous).Objects.Concat(objects)); + } + throw new InvalidOperationException("Operation is invalid after previous operation."); + } + + public object Apply(object oldValue, string key) { + if (oldValue == null) { + return objects.ToList(); + } + var oldList = Conversion.To>(oldValue); + return oldList.Concat(objects).ToList(); + } + + public IEnumerable Objects { + get { + return objects; + } + } + } +} diff --git a/Storage/Storage/Internal/Operation/AVAddUniqueOperation.cs b/Storage/Storage/Internal/Operation/AVAddUniqueOperation.cs new file mode 100644 index 0000000..8e0a957 --- /dev/null +++ b/Storage/Storage/Internal/Operation/AVAddUniqueOperation.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using LeanCloud.Utilities; + +namespace LeanCloud.Storage.Internal { + public class AVAddUniqueOperation : IAVFieldOperation { + private ReadOnlyCollection objects; + public AVAddUniqueOperation(IEnumerable objects) { + this.objects = new ReadOnlyCollection(objects.Distinct().ToList()); + } + + public object Encode() { + return new Dictionary { + {"__op", "AddUnique"}, + {"objects", PointerOrLocalIdEncoder.Instance.Encode(objects)} + }; + } + + public IAVFieldOperation MergeWithPrevious(IAVFieldOperation previous) { + if (previous == null) { + return this; + } + if (previous is AVDeleteOperation) { + return new AVSetOperation(objects.ToList()); + } + if (previous is AVSetOperation) { + var setOp = (AVSetOperation)previous; + var oldList = Conversion.To>(setOp.Value); + var result = this.Apply(oldList, null); + return new AVSetOperation(result); + } + if (previous is AVAddUniqueOperation) { + var oldList = ((AVAddUniqueOperation)previous).Objects; + return new AVAddUniqueOperation((IList)this.Apply(oldList, null)); + } + throw new InvalidOperationException("Operation is invalid after previous operation."); + } + + public object Apply(object oldValue, string key) { + if (oldValue == null) { + return objects.ToList(); + } + var newList = Conversion.To>(oldValue).ToList(); + var comparer = AVFieldOperations.AVObjectComparer; + foreach (var objToAdd in objects) { + if (objToAdd is AVObject) { + var matchedObj = newList.FirstOrDefault(listObj => comparer.Equals(objToAdd, listObj)); + if (matchedObj == null) { + newList.Add(objToAdd); + } else { + var index = newList.IndexOf(matchedObj); + newList[index] = objToAdd; + } + } else if (!newList.Contains(objToAdd, comparer)) { + newList.Add(objToAdd); + } + } + return newList; + } + + public IEnumerable Objects { + get { + return objects; + } + } + } +} diff --git a/Storage/Storage/Internal/Operation/AVDeleteOperation.cs b/Storage/Storage/Internal/Operation/AVDeleteOperation.cs new file mode 100644 index 0000000..7b77b94 --- /dev/null +++ b/Storage/Storage/Internal/Operation/AVDeleteOperation.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; + +namespace LeanCloud.Storage.Internal +{ + /// + /// An operation where a field is deleted from the object. + /// + public class AVDeleteOperation : IAVFieldOperation + { + internal static readonly object DeleteToken = new object(); + private static AVDeleteOperation _Instance = new AVDeleteOperation(); + public static AVDeleteOperation Instance + { + get + { + return _Instance; + } + } + + private AVDeleteOperation() { } + public object Encode() + { + return new Dictionary { + {"__op", "Delete"} + }; + } + + public IAVFieldOperation MergeWithPrevious(IAVFieldOperation previous) + { + return this; + } + + public object Apply(object oldValue, string key) + { + return DeleteToken; + } + } +} diff --git a/Storage/Storage/Internal/Operation/AVFieldOperations.cs b/Storage/Storage/Internal/Operation/AVFieldOperations.cs new file mode 100644 index 0000000..9c07ffa --- /dev/null +++ b/Storage/Storage/Internal/Operation/AVFieldOperations.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; + +namespace LeanCloud.Storage.Internal { + public class AVObjectIdComparer : IEqualityComparer { + bool IEqualityComparer.Equals(object p1, object p2) { + var avObj1 = p1 as AVObject; + var avObj2 = p2 as AVObject; + if (avObj1 != null && avObj2 != null) { + return object.Equals(avObj1.ObjectId, avObj2.ObjectId); + } + return object.Equals(p1, p2); + } + + public int GetHashCode(object p) { + var avObject = p as AVObject; + if (avObject != null) { + return avObject.ObjectId.GetHashCode(); + } + return p.GetHashCode(); + } + } + + static class AVFieldOperations { + private static AVObjectIdComparer comparer; + + public static IAVFieldOperation Decode(IDictionary json) { + throw new NotImplementedException(); + } + + public static IEqualityComparer AVObjectComparer { + get { + if (comparer == null) { + comparer = new AVObjectIdComparer(); + } + return comparer; + } + } + } +} diff --git a/Storage/Storage/Internal/Operation/AVIncrementOperation.cs b/Storage/Storage/Internal/Operation/AVIncrementOperation.cs new file mode 100644 index 0000000..1606e5f --- /dev/null +++ b/Storage/Storage/Internal/Operation/AVIncrementOperation.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace LeanCloud.Storage.Internal +{ + public class AVIncrementOperation : IAVFieldOperation + { + private static readonly IDictionary, Func> adders; + + static AVIncrementOperation() + { + // Defines adders for all of the implicit conversions: http://msdn.microsoft.com/en-US/library/y5b434w4(v=vs.80).aspx + adders = new Dictionary, Func> { + {new Tuple(typeof(sbyte), typeof(sbyte)), (left, right) => (sbyte)left + (sbyte)right}, + {new Tuple(typeof(sbyte), typeof(short)), (left, right) => (sbyte)left + (short)right}, + {new Tuple(typeof(sbyte), typeof(int)), (left, right) => (sbyte)left + (int)right}, + {new Tuple(typeof(sbyte), typeof(long)), (left, right) => (sbyte)left + (long)right}, + {new Tuple(typeof(sbyte), typeof(float)), (left, right) => (sbyte)left + (float)right}, + {new Tuple(typeof(sbyte), typeof(double)), (left, right) => (sbyte)left + (double)right}, + {new Tuple(typeof(sbyte), typeof(decimal)), (left, right) => (sbyte)left + (decimal)right}, + {new Tuple(typeof(byte), typeof(byte)), (left, right) => (byte)left + (byte)right}, + {new Tuple(typeof(byte), typeof(short)), (left, right) => (byte)left + (short)right}, + {new Tuple(typeof(byte), typeof(ushort)), (left, right) => (byte)left + (ushort)right}, + {new Tuple(typeof(byte), typeof(int)), (left, right) => (byte)left + (int)right}, + {new Tuple(typeof(byte), typeof(uint)), (left, right) => (byte)left + (uint)right}, + {new Tuple(typeof(byte), typeof(long)), (left, right) => (byte)left + (long)right}, + {new Tuple(typeof(byte), typeof(ulong)), (left, right) => (byte)left + (ulong)right}, + {new Tuple(typeof(byte), typeof(float)), (left, right) => (byte)left + (float)right}, + {new Tuple(typeof(byte), typeof(double)), (left, right) => (byte)left + (double)right}, + {new Tuple(typeof(byte), typeof(decimal)), (left, right) => (byte)left + (decimal)right}, + {new Tuple(typeof(short), typeof(short)), (left, right) => (short)left + (short)right}, + {new Tuple(typeof(short), typeof(int)), (left, right) => (short)left + (int)right}, + {new Tuple(typeof(short), typeof(long)), (left, right) => (short)left + (long)right}, + {new Tuple(typeof(short), typeof(float)), (left, right) => (short)left + (float)right}, + {new Tuple(typeof(short), typeof(double)), (left, right) => (short)left + (double)right}, + {new Tuple(typeof(short), typeof(decimal)), (left, right) => (short)left + (decimal)right}, + {new Tuple(typeof(ushort), typeof(ushort)), (left, right) => (ushort)left + (ushort)right}, + {new Tuple(typeof(ushort), typeof(int)), (left, right) => (ushort)left + (int)right}, + {new Tuple(typeof(ushort), typeof(uint)), (left, right) => (ushort)left + (uint)right}, + {new Tuple(typeof(ushort), typeof(long)), (left, right) => (ushort)left + (long)right}, + {new Tuple(typeof(ushort), typeof(ulong)), (left, right) => (ushort)left + (ulong)right}, + {new Tuple(typeof(ushort), typeof(float)), (left, right) => (ushort)left + (float)right}, + {new Tuple(typeof(ushort), typeof(double)), (left, right) => (ushort)left + (double)right}, + {new Tuple(typeof(ushort), typeof(decimal)), (left, right) => (ushort)left + (decimal)right}, + {new Tuple(typeof(int), typeof(int)), (left, right) => (int)left + (int)right}, + {new Tuple(typeof(int), typeof(long)), (left, right) => (int)left + (long)right}, + {new Tuple(typeof(int), typeof(float)), (left, right) => (int)left + (float)right}, + {new Tuple(typeof(int), typeof(double)), (left, right) => (int)left + (double)right}, + {new Tuple(typeof(int), typeof(decimal)), (left, right) => (int)left + (decimal)right}, + {new Tuple(typeof(uint), typeof(uint)), (left, right) => (uint)left + (uint)right}, + {new Tuple(typeof(uint), typeof(long)), (left, right) => (uint)left + (long)right}, + {new Tuple(typeof(uint), typeof(ulong)), (left, right) => (uint)left + (ulong)right}, + {new Tuple(typeof(uint), typeof(float)), (left, right) => (uint)left + (float)right}, + {new Tuple(typeof(uint), typeof(double)), (left, right) => (uint)left + (double)right}, + {new Tuple(typeof(uint), typeof(decimal)), (left, right) => (uint)left + (decimal)right}, + {new Tuple(typeof(long), typeof(long)), (left, right) => (long)left + (long)right}, + {new Tuple(typeof(long), typeof(float)), (left, right) => (long)left + (float)right}, + {new Tuple(typeof(long), typeof(double)), (left, right) => (long)left + (double)right}, + {new Tuple(typeof(long), typeof(decimal)), (left, right) => (long)left + (decimal)right}, + {new Tuple(typeof(char), typeof(char)), (left, right) => (char)left + (char)right}, + {new Tuple(typeof(char), typeof(ushort)), (left, right) => (char)left + (ushort)right}, + {new Tuple(typeof(char), typeof(int)), (left, right) => (char)left + (int)right}, + {new Tuple(typeof(char), typeof(uint)), (left, right) => (char)left + (uint)right}, + {new Tuple(typeof(char), typeof(long)), (left, right) => (char)left + (long)right}, + {new Tuple(typeof(char), typeof(ulong)), (left, right) => (char)left + (ulong)right}, + {new Tuple(typeof(char), typeof(float)), (left, right) => (char)left + (float)right}, + {new Tuple(typeof(char), typeof(double)), (left, right) => (char)left + (double)right}, + {new Tuple(typeof(char), typeof(decimal)), (left, right) => (char)left + (decimal)right}, + {new Tuple(typeof(float), typeof(float)), (left, right) => (float)left + (float)right}, + {new Tuple(typeof(float), typeof(double)), (left, right) => (float)left + (double)right}, + {new Tuple(typeof(ulong), typeof(ulong)), (left, right) => (ulong)left + (ulong)right}, + {new Tuple(typeof(ulong), typeof(float)), (left, right) => (ulong)left + (float)right}, + {new Tuple(typeof(ulong), typeof(double)), (left, right) => (ulong)left + (double)right}, + {new Tuple(typeof(ulong), typeof(decimal)), (left, right) => (ulong)left + (decimal)right}, + {new Tuple(typeof(double), typeof(double)), (left, right) => (double)left + (double)right}, + {new Tuple(typeof(decimal), typeof(decimal)), (left, right) => (decimal)left + (decimal)right} + }; + // Generate the adders in the other direction + foreach (var pair in adders.Keys.ToList()) + { + if (pair.Item1.Equals(pair.Item2)) + { + continue; + } + var reversePair = new Tuple(pair.Item2, pair.Item1); + var func = adders[pair]; + adders[reversePair] = (left, right) => func(right, left); + } + } + + private object amount; + + public AVIncrementOperation(object amount) + { + this.amount = amount; + } + + public object Encode() + { + return new Dictionary + { + {"__op", "Increment"}, + {"amount", amount} + }; + } + + private static object Add(object obj1, object obj2) + { + Func adder; + if (adders.TryGetValue(new Tuple(obj1.GetType(), obj2.GetType()), out adder)) + { + return adder(obj1, obj2); + } + throw new InvalidCastException("Cannot add " + obj1.GetType() + " to " + obj2.GetType()); + } + + public IAVFieldOperation MergeWithPrevious(IAVFieldOperation previous) + { + if (previous == null) + { + return this; + } + if (previous is AVDeleteOperation) + { + return new AVSetOperation(amount); + } + if (previous is AVSetOperation) + { + var otherAmount = ((AVSetOperation)previous).Value; + if (otherAmount is string) + { + throw new InvalidOperationException("Cannot increment a non-number type."); + } + var myAmount = amount; + return new AVSetOperation(Add(otherAmount, myAmount)); + } + if (previous is AVIncrementOperation) + { + object otherAmount = ((AVIncrementOperation)previous).Amount; + object myAmount = amount; + return new AVIncrementOperation(Add(otherAmount, myAmount)); + } + throw new InvalidOperationException("Operation is invalid after previous operation."); + } + + public object Apply(object oldValue, string key) + { + if (oldValue is string) + { + throw new InvalidOperationException("Cannot increment a non-number type."); + } + object otherAmount = oldValue ?? 0; + object myAmount = amount; + return Add(otherAmount, myAmount); + } + + public object Amount + { + get + { + return amount; + } + } + } +} diff --git a/Storage/Storage/Internal/Operation/AVRelationOperation.cs b/Storage/Storage/Internal/Operation/AVRelationOperation.cs new file mode 100644 index 0000000..512b5f5 --- /dev/null +++ b/Storage/Storage/Internal/Operation/AVRelationOperation.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + public class AVRelationOperation : IAVFieldOperation { + private readonly IList adds; + private readonly IList removes; + private readonly string targetClassName; + + private AVRelationOperation(IEnumerable adds, + IEnumerable removes, + string targetClassName) { + this.targetClassName = targetClassName; + this.adds = new ReadOnlyCollection(adds.ToList()); + this.removes = new ReadOnlyCollection(removes.ToList()); + } + + public AVRelationOperation(IEnumerable adds, + IEnumerable removes) { + adds = adds ?? new AVObject[0]; + removes = removes ?? new AVObject[0]; + this.targetClassName = adds.Concat(removes).Select(o => o.ClassName).FirstOrDefault(); + this.adds = new ReadOnlyCollection(IdsFromObjects(adds).ToList()); + this.removes = new ReadOnlyCollection(IdsFromObjects(removes).ToList()); + } + + public object Encode() { + var adds = this.adds + .Select(id => PointerOrLocalIdEncoder.Instance.Encode( + AVObject.CreateWithoutData(targetClassName, id))) + .ToList(); + var removes = this.removes + .Select(id => PointerOrLocalIdEncoder.Instance.Encode( + AVObject.CreateWithoutData(targetClassName, id))) + .ToList(); + var addDict = adds.Count == 0 ? null : new Dictionary { + {"__op", "AddRelation"}, + {"objects", adds} + }; + var removeDict = removes.Count == 0 ? null : new Dictionary { + {"__op", "RemoveRelation"}, + {"objects", removes} + }; + + if (addDict != null && removeDict != null) { + return new Dictionary { + {"__op", "Batch"}, + {"ops", new[] {addDict, removeDict}} + }; + } + return addDict ?? removeDict; + } + + public IAVFieldOperation MergeWithPrevious(IAVFieldOperation previous) { + if (previous == null) { + return this; + } + if (previous is AVDeleteOperation) { + throw new InvalidOperationException("You can't modify a relation after deleting it."); + } + var other = previous as AVRelationOperation; + if (other != null) { + if (other.TargetClassName != TargetClassName) { + throw new InvalidOperationException( + string.Format("Related object must be of class {0}, but {1} was passed in.", + other.TargetClassName, + TargetClassName)); + } + var newAdd = adds.Union(other.adds.Except(removes)).ToList(); + var newRemove = removes.Union(other.removes.Except(adds)).ToList(); + return new AVRelationOperation(newAdd, newRemove, TargetClassName); + } + throw new InvalidOperationException("Operation is invalid after previous operation."); + } + + public object Apply(object oldValue, string key) { + if (adds.Count == 0 && removes.Count == 0) { + return null; + } + if (oldValue == null) { + return AVRelationBase.CreateRelation(null, key, targetClassName); + } + if (oldValue is AVRelationBase) { + var oldRelation = (AVRelationBase)oldValue; + var oldClassName = oldRelation.TargetClassName; + if (oldClassName != null && oldClassName != targetClassName) { + throw new InvalidOperationException("Related object must be a " + oldClassName + + ", but a " + targetClassName + " was passed in."); + } + oldRelation.TargetClassName = targetClassName; + return oldRelation; + } + throw new InvalidOperationException("Operation is invalid after previous operation."); + } + + public string TargetClassName { get { return targetClassName; } } + + private IEnumerable IdsFromObjects(IEnumerable objects) { + foreach (var obj in objects) { + if (obj.ObjectId == null) { + throw new ArgumentException( + "You can't add an unsaved AVObject to a relation."); + } + if (obj.ClassName != targetClassName) { + throw new ArgumentException(string.Format( + "Tried to create a AVRelation with 2 different types: {0} and {1}", + targetClassName, + obj.ClassName)); + } + } + return objects.Select(o => o.ObjectId).Distinct(); + } + } +} diff --git a/Storage/Storage/Internal/Operation/AVRemoveOperation.cs b/Storage/Storage/Internal/Operation/AVRemoveOperation.cs new file mode 100644 index 0000000..c9d14a4 --- /dev/null +++ b/Storage/Storage/Internal/Operation/AVRemoveOperation.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +using LeanCloud.Utilities; + +namespace LeanCloud.Storage.Internal +{ + public class AVRemoveOperation : IAVFieldOperation + { + private ReadOnlyCollection objects; + public AVRemoveOperation(IEnumerable objects) + { + this.objects = new ReadOnlyCollection(objects.Distinct().ToList()); + } + + public object Encode() + { + return new Dictionary { + {"__op", "Remove"}, + {"objects", PointerOrLocalIdEncoder.Instance.Encode(objects)} + }; + } + + public IAVFieldOperation MergeWithPrevious(IAVFieldOperation previous) + { + if (previous == null) + { + return this; + } + if (previous is AVDeleteOperation) + { + return previous; + } + if (previous is AVSetOperation) + { + var setOp = (AVSetOperation)previous; + var oldList = Conversion.As>(setOp.Value); + return new AVSetOperation(this.Apply(oldList, null)); + } + if (previous is AVRemoveOperation) + { + var oldOp = (AVRemoveOperation)previous; + return new AVRemoveOperation(oldOp.Objects.Concat(objects)); + } + throw new InvalidOperationException("Operation is invalid after previous operation."); + } + + public object Apply(object oldValue, string key) + { + if (oldValue == null) + { + return new List(); + } + var oldList = Conversion.As>(oldValue); + return oldList.Except(objects, AVFieldOperations.AVObjectComparer).ToList(); + } + + public IEnumerable Objects + { + get + { + return objects; + } + } + } +} diff --git a/Storage/Storage/Internal/Operation/AVSetOperation.cs b/Storage/Storage/Internal/Operation/AVSetOperation.cs new file mode 100644 index 0000000..1c3a412 --- /dev/null +++ b/Storage/Storage/Internal/Operation/AVSetOperation.cs @@ -0,0 +1,21 @@ +namespace LeanCloud.Storage.Internal { + public class AVSetOperation : IAVFieldOperation { + public AVSetOperation(object value) { + Value = value; + } + + public object Encode() { + return PointerOrLocalIdEncoder.Instance.Encode(Value); + } + + public IAVFieldOperation MergeWithPrevious(IAVFieldOperation previous) { + return this; + } + + public object Apply(object oldValue, string key) { + return Value; + } + + public object Value { get; private set; } + } +} diff --git a/Storage/Storage/Internal/Operation/IAVFieldOperation.cs b/Storage/Storage/Internal/Operation/IAVFieldOperation.cs new file mode 100644 index 0000000..70b2930 --- /dev/null +++ b/Storage/Storage/Internal/Operation/IAVFieldOperation.cs @@ -0,0 +1,42 @@ +namespace LeanCloud.Storage.Internal { + /// + /// A AVFieldOperation represents a modification to a value in a AVObject. + /// For example, setting, deleting, or incrementing a value are all different kinds of + /// AVFieldOperations. AVFieldOperations themselves can be considered to be + /// immutable. + /// + public interface IAVFieldOperation { + /// + /// Converts the AVFieldOperation to a data structure that can be converted to JSON and sent to + /// LeanCloud as part of a save operation. + /// + /// An object to be JSONified. + object Encode(); + + /// + /// Returns a field operation that is composed of a previous operation followed by + /// this operation. This will not mutate either operation. However, it may return + /// this if the current operation is not affected by previous changes. + /// For example: + /// {increment by 2}.MergeWithPrevious({set to 5}) -> {set to 7} + /// {set to 5}.MergeWithPrevious({increment by 2}) -> {set to 5} + /// {add "foo"}.MergeWithPrevious({delete}) -> {set to ["foo"]} + /// {delete}.MergeWithPrevious({add "foo"}) -> {delete} /// + /// The most recent operation on the field, or null if none. + /// A new AVFieldOperation or this. + IAVFieldOperation MergeWithPrevious(IAVFieldOperation previous); + + /// + /// Returns a new estimated value based on a previous value and this operation. This + /// value is not intended to be sent to LeanCloud, but it is used locally on the client to + /// inspect the most likely current value for a field. + /// + /// The key and object are used solely for AVRelation to be able to construct objects + /// that refer back to their parents. + /// + /// The previous value for the field. + /// The key that this value is for. + /// The new value for the field. + object Apply(object oldValue, string key); + } +} diff --git a/Storage/Storage/Internal/Query/Controller/AVQueryController.cs b/Storage/Storage/Internal/Query/Controller/AVQueryController.cs new file mode 100644 index 0000000..42c1869 --- /dev/null +++ b/Storage/Storage/Internal/Query/Controller/AVQueryController.cs @@ -0,0 +1,110 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Threading; +using System.Threading.Tasks; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal +{ + internal class AVQueryController : IAVQueryController + { + private readonly IAVCommandRunner commandRunner; + + public AVQueryController(IAVCommandRunner commandRunner) + { + this.commandRunner = commandRunner; + } + + public Task> FindAsync(AVQuery query, + AVUser user, + CancellationToken cancellationToken) where T : AVObject + { + string sessionToken = user != null ? user.SessionToken : null; + + return FindAsync(query.RelativeUri, query.BuildParameters(), sessionToken, cancellationToken).OnSuccess(t => + { + var items = t.Result["results"] as IList; + + return (from item in items + select AVObjectCoder.Instance.Decode(item as IDictionary, AVDecoder.Instance)); + }); + } + + public Task CountAsync(AVQuery query, + AVUser user, + CancellationToken cancellationToken) where T : AVObject + { + string sessionToken = user != null ? user.SessionToken : null; + var parameters = query.BuildParameters(); + parameters["limit"] = 0; + parameters["count"] = 1; + + return FindAsync(query.RelativeUri, parameters, sessionToken, cancellationToken).OnSuccess(t => + { + return Convert.ToInt32(t.Result["count"]); + }); + } + + public Task FirstAsync(AVQuery query, + AVUser user, + CancellationToken cancellationToken) where T : AVObject + { + string sessionToken = user != null ? user.SessionToken : null; + var parameters = query.BuildParameters(); + parameters["limit"] = 1; + + return FindAsync(query.RelativeUri, parameters, sessionToken, cancellationToken).OnSuccess(t => + { + var items = t.Result["results"] as IList; + var item = items.FirstOrDefault() as IDictionary; + + // Not found. Return empty state. + if (item == null) + { + return (IObjectState)null; + } + + return AVObjectCoder.Instance.Decode(item, AVDecoder.Instance); + }); + } + + private Task> FindAsync(string relativeUri, + IDictionary parameters, + string sessionToken, + CancellationToken cancellationToken) + { + + var command = new AVCommand(string.Format("{0}?{1}", + relativeUri, + AVClient.BuildQueryString(parameters)), + method: "GET", + sessionToken: sessionToken, + data: null); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + { + return t.Result.Item2; + }); + } + + //private Task> FindAsync(string className, + // IDictionary parameters, + // string sessionToken, + // CancellationToken cancellationToken) + //{ + // var command = new AVCommand(string.Format("classes/{0}?{1}", + // Uri.EscapeDataString(className), + // AVClient.BuildQueryString(parameters)), + // method: "GET", + // sessionToken: sessionToken, + // data: null); + + // return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + // { + // return t.Result.Item2; + // }); + //} + } +} diff --git a/Storage/Storage/Internal/Query/Controller/IAVQueryController.cs b/Storage/Storage/Internal/Query/Controller/IAVQueryController.cs new file mode 100644 index 0000000..f5d1494 --- /dev/null +++ b/Storage/Storage/Internal/Query/Controller/IAVQueryController.cs @@ -0,0 +1,21 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + public interface IAVQueryController { + Task> FindAsync(AVQuery query, + AVUser user, + CancellationToken cancellationToken) where T : AVObject; + + Task CountAsync(AVQuery query, + AVUser user, + CancellationToken cancellationToken) where T : AVObject; + + Task FirstAsync(AVQuery query, + AVUser user, + CancellationToken cancellationToken) where T : AVObject; + } +} diff --git a/Storage/Storage/Internal/Session/Controller/AVSessionController.cs b/Storage/Storage/Internal/Session/Controller/AVSessionController.cs new file mode 100644 index 0000000..fd40711 --- /dev/null +++ b/Storage/Storage/Internal/Session/Controller/AVSessionController.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal { + public class AVSessionController : IAVSessionController { + private readonly IAVCommandRunner commandRunner; + + public AVSessionController(IAVCommandRunner commandRunner) { + this.commandRunner = commandRunner; + } + + public Task GetSessionAsync(string sessionToken, CancellationToken cancellationToken) { + var command = new AVCommand("sessions/me", + method: "GET", + sessionToken: sessionToken, + data: null); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => { + return AVObjectCoder.Instance.Decode(t.Result.Item2, AVDecoder.Instance); + }); + } + + public Task RevokeAsync(string sessionToken, CancellationToken cancellationToken) { + var command = new AVCommand("logout", + method: "POST", + sessionToken: sessionToken, + data: new Dictionary()); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken); + } + + public Task UpgradeToRevocableSessionAsync(string sessionToken, CancellationToken cancellationToken) { + var command = new AVCommand("upgradeToRevocableSession", + method: "POST", + sessionToken: sessionToken, + data: new Dictionary()); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => { + return AVObjectCoder.Instance.Decode(t.Result.Item2, AVDecoder.Instance); + }); + } + + public bool IsRevocableSessionToken(string sessionToken) { + return sessionToken.Contains("r:"); + } + } +} diff --git a/Storage/Storage/Internal/Session/Controller/IAVSessionController.cs b/Storage/Storage/Internal/Session/Controller/IAVSessionController.cs new file mode 100644 index 0000000..72e7e95 --- /dev/null +++ b/Storage/Storage/Internal/Session/Controller/IAVSessionController.cs @@ -0,0 +1,15 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + public interface IAVSessionController { + Task GetSessionAsync(string sessionToken, CancellationToken cancellationToken); + + Task RevokeAsync(string sessionToken, CancellationToken cancellationToken); + + Task UpgradeToRevocableSessionAsync(string sessionToken, CancellationToken cancellationToken); + + bool IsRevocableSessionToken(string sessionToken); + } +} diff --git a/Storage/Storage/Internal/Storage/IStorageController.cs b/Storage/Storage/Internal/Storage/IStorageController.cs new file mode 100644 index 0000000..b334c92 --- /dev/null +++ b/Storage/Storage/Internal/Storage/IStorageController.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + /// + /// An abstraction for accessing persistent storage in the LeanCloud SDK. + /// + public interface IStorageController { + /// + /// Load the contents of this storage controller asynchronously. + /// + /// + Task> LoadAsync(); + + /// + /// Overwrites the contents of this storage controller asynchronously. + /// + /// + /// + Task> SaveAsync(IDictionary contents); + } + + /// + /// An interface for a dictionary that is persisted to disk asynchronously. + /// + /// They key type of the dictionary. + /// The value type of the dictionary. + public interface IStorageDictionary : IEnumerable> { + int Count { get; } + TValue this[TKey key] { get; } + + IEnumerable Keys { get; } + IEnumerable Values { get; } + + bool ContainsKey(TKey key); + bool TryGetValue(TKey key, out TValue value); + + /// + /// Adds a key to this dictionary, and saves it asynchronously. + /// + /// The key to insert. + /// The value to insert. + /// + Task AddAsync(TKey key, TValue value); + + /// + /// Removes a key from this dictionary, and saves it asynchronously. + /// + /// + /// + Task RemoveAsync(TKey key); + } +} \ No newline at end of file diff --git a/Storage/Storage/Internal/Storage/Portable/StorageController.cs b/Storage/Storage/Internal/Storage/Portable/StorageController.cs new file mode 100644 index 0000000..04b96ed --- /dev/null +++ b/Storage/Storage/Internal/Storage/Portable/StorageController.cs @@ -0,0 +1,217 @@ +using System; +using System.Threading.Tasks; +using System.Linq; +using System.IO; +using System.Collections.Generic; +using System.Threading; + +namespace LeanCloud.Storage.Internal +{ + /// + /// Implements `IStorageController` for PCL targets, based off of PCLStorage. + /// + public class StorageController : IStorageController + { + private class StorageDictionary : IStorageDictionary + { + private readonly string filePath; + + private Dictionary dictionary; + readonly ReaderWriterLockSlim locker = new ReaderWriterLockSlim(); + + public StorageDictionary(string filePath) + { + this.filePath = filePath; + dictionary = new Dictionary(); + } + + internal Task SaveAsync() + { + string json; + locker.EnterReadLock(); + json = Json.Encode(dictionary); + locker.ExitReadLock(); + using (var sw = new StreamWriter(filePath)) { + return sw.WriteAsync(json); + } + } + + internal async Task LoadAsync() + { + using (var sr = new StreamReader(filePath)) { + var text = await sr.ReadToEndAsync(); + Dictionary result = null; + try { + result = Json.Parse(text) as Dictionary; + } catch (Exception e) { + AVClient.PrintLog(e.Message); + } + + locker.EnterWriteLock(); + dictionary = result ?? new Dictionary(); + locker.ExitWriteLock(); + } + } + + internal void Update(IDictionary contents) + { + locker.EnterWriteLock(); + dictionary = contents.ToDictionary(p => p.Key, p => p.Value); + locker.ExitWriteLock(); + } + + public Task AddAsync(string key, object value) + { + locker.EnterWriteLock(); + dictionary[key] = value; + locker.ExitWriteLock(); + return SaveAsync(); + } + + public Task RemoveAsync(string key) + { + locker.EnterWriteLock(); + dictionary.Remove(key); + locker.ExitWriteLock(); + return SaveAsync(); + } + + public bool ContainsKey(string key) + { + try { + locker.EnterReadLock(); + return dictionary.ContainsKey(key); + } finally { + locker.ExitReadLock(); + } + } + + public IEnumerable Keys + { + get { + try { + locker.EnterReadLock(); + return dictionary.Keys; + } finally { + locker.ExitReadLock(); + } + } + } + + public bool TryGetValue(string key, out object value) + { + try { + locker.EnterReadLock(); + return dictionary.TryGetValue(key, out value); + } finally { + locker.ExitReadLock(); + } + } + + public IEnumerable Values + { + get { + try { + locker.EnterReadLock(); + return dictionary.Values; + } finally { + locker.ExitReadLock(); + } + } + } + + public object this[string key] + { + get { + try { + locker.EnterReadLock(); + return dictionary[key]; + } finally { + locker.ExitReadLock(); + } + } + } + + public int Count + { + get { + try { + locker.EnterReadLock(); + return dictionary.Count; + } finally { + locker.ExitReadLock(); + } + } + } + + public IEnumerator> GetEnumerator() + { + try { + locker.EnterReadLock(); + return dictionary.GetEnumerator(); + } finally { + locker.ExitReadLock(); + } + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + try { + locker.EnterReadLock(); + return dictionary.GetEnumerator(); + } finally { + locker.ExitReadLock(); + } + } + } + + private const string LeanCloudStorageFileName = "ApplicationSettings"; + private readonly TaskQueue taskQueue = new TaskQueue(); + private readonly Task fileTask; + private StorageDictionary storageDictionary; + + public StorageController(string fileNamePrefix) + { + fileTask = taskQueue.Enqueue(t => t.ContinueWith(_ => + { + string path = $"{fileNamePrefix}_{LeanCloudStorageFileName}"; + File.CreateText(path); + return path; + }), CancellationToken.None); + } + + public Task> LoadAsync() + { + return taskQueue.Enqueue(toAwait => + { + return toAwait.ContinueWith(_ => + { + if (storageDictionary != null) + { + return Task.FromResult>(storageDictionary); + } + + storageDictionary = new StorageDictionary(fileTask.Result); + return storageDictionary.LoadAsync().OnSuccess(__ => storageDictionary as IStorageDictionary); + }).Unwrap(); + }, CancellationToken.None); + } + + public Task> SaveAsync(IDictionary contents) + { + return taskQueue.Enqueue(toAwait => + { + return toAwait.ContinueWith(_ => + { + if (storageDictionary == null) + { + storageDictionary = new StorageDictionary(fileTask.Result); + } + + storageDictionary.Update(contents); + return storageDictionary.SaveAsync().OnSuccess(__ => storageDictionary as IStorageDictionary); + }).Unwrap(); + }, CancellationToken.None); + } + } +} diff --git a/Storage/Storage/Internal/User/Controller/AVCurrentUserController.cs b/Storage/Storage/Internal/User/Controller/AVCurrentUserController.cs new file mode 100644 index 0000000..eb78961 --- /dev/null +++ b/Storage/Storage/Internal/User/Controller/AVCurrentUserController.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal +{ + public class AVCurrentUserController : IAVCurrentUserController + { + private readonly object mutex = new object(); + private readonly TaskQueue taskQueue = new TaskQueue(); + + private IStorageController storageController; + + public AVCurrentUserController(IStorageController storageController) + { + this.storageController = storageController; + } + + private AVUser currentUser; + public AVUser CurrentUser + { + get + { + lock (mutex) + { + return currentUser; + } + } + set + { + lock (mutex) + { + currentUser = value; + } + } + } + + public Task SetAsync(AVUser user, CancellationToken cancellationToken) + { + Task saveTask = null; + if (user == null) + { + saveTask = storageController + .LoadAsync() + .OnSuccess(t => t.Result.RemoveAsync("CurrentUser")) + .Unwrap(); + } + else + { + var data = user.ServerDataToJSONObjectForSerialization(); + data["objectId"] = user.ObjectId; + if (user.CreatedAt != null) + { + data["createdAt"] = user.CreatedAt.Value.ToString(AVClient.DateFormatStrings.First(), + CultureInfo.InvariantCulture); + } + if (user.UpdatedAt != null) + { + data["updatedAt"] = user.UpdatedAt.Value.ToString(AVClient.DateFormatStrings.First(), + CultureInfo.InvariantCulture); + } + + saveTask = storageController + .LoadAsync() + .OnSuccess(t => t.Result.AddAsync("CurrentUser", Json.Encode(data))) + .Unwrap(); + } + CurrentUser = user; + + return saveTask; + } + + public Task GetAsync(CancellationToken cancellationToken) + { + AVUser cachedCurrent; + + lock (mutex) + { + cachedCurrent = CurrentUser; + } + + if (cachedCurrent != null) + { + return Task.FromResult(cachedCurrent); + } + + return storageController.LoadAsync().OnSuccess(t => + { + object temp; + t.Result.TryGetValue("CurrentUser", out temp); + var userDataString = temp as string; + AVUser user = null; + if (userDataString != null) + { + var userData = Json.Parse(userDataString) as IDictionary; + var state = AVObjectCoder.Instance.Decode(userData, AVDecoder.Instance); + user = AVObject.FromState(state, "_User"); + } + + CurrentUser = user; + return user; + }); + } + + public Task ExistsAsync(CancellationToken cancellationToken) + { + if (CurrentUser != null) + { + return Task.FromResult(true); + } + + return storageController.LoadAsync().OnSuccess(t => t.Result.ContainsKey("CurrentUser")); + } + + public bool IsCurrent(AVUser user) + { + lock (mutex) + { + return CurrentUser == user; + } + } + + public void ClearFromMemory() + { + CurrentUser = null; + } + + public void ClearFromDisk() + { + lock (mutex) + { + ClearFromMemory(); + + storageController.LoadAsync().OnSuccess(t => t.Result.RemoveAsync("CurrentUser")); + } + } + + public Task GetCurrentSessionTokenAsync(CancellationToken cancellationToken) + { + return GetAsync(cancellationToken).OnSuccess(t => + { + var user = t.Result; + return user == null ? null : user.SessionToken; + }); + } + + public Task LogOutAsync(CancellationToken cancellationToken) + { + return GetAsync(cancellationToken).OnSuccess(t => + { + ClearFromDisk(); + }); + } + } +} diff --git a/Storage/Storage/Internal/User/Controller/AVUserController.cs b/Storage/Storage/Internal/User/Controller/AVUserController.cs new file mode 100644 index 0000000..c74f602 --- /dev/null +++ b/Storage/Storage/Internal/User/Controller/AVUserController.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Storage.Internal +{ + public class AVUserController : IAVUserController + { + private readonly IAVCommandRunner commandRunner; + + public AVUserController(IAVCommandRunner commandRunner) + { + this.commandRunner = commandRunner; + } + + public Task SignUpAsync(IObjectState state, + IDictionary operations, + CancellationToken cancellationToken) + { + var objectJSON = AVObject.ToJSONObjectForSaving(operations); + + var command = new AVCommand("classes/_User", + method: "POST", + data: objectJSON); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + { + var serverState = AVObjectCoder.Instance.Decode(t.Result.Item2, AVDecoder.Instance); + serverState = serverState.MutatedClone(mutableClone => + { + mutableClone.IsNew = true; + }); + return serverState; + }); + } + + public Task LogInAsync(string username, string email, + string password, + CancellationToken cancellationToken) + { + var data = new Dictionary{ + { "password", password} + }; + if (username != null) { + data.Add("username", username); + } + if (email != null) { + data.Add("email", email); + } + + var command = new AVCommand("login", + method: "POST", + data: data); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + { + var serverState = AVObjectCoder.Instance.Decode(t.Result.Item2, AVDecoder.Instance); + serverState = serverState.MutatedClone(mutableClone => + { + mutableClone.IsNew = t.Result.Item1 == System.Net.HttpStatusCode.Created; + }); + return serverState; + }); + } + + public Task LogInAsync(string authType, + IDictionary data, + bool failOnNotExist, + CancellationToken cancellationToken) + { + var authData = new Dictionary(); + authData[authType] = data; + var path = failOnNotExist ? "users?failOnNotExist=true" : "users"; + var command = new AVCommand(path, + method: "POST", + data: new Dictionary { + { "authData", authData} + }); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + { + var serverState = AVObjectCoder.Instance.Decode(t.Result.Item2, AVDecoder.Instance); + serverState = serverState.MutatedClone(mutableClone => + { + mutableClone.IsNew = t.Result.Item1 == System.Net.HttpStatusCode.Created; + }); + return serverState; + }); + } + + public Task GetUserAsync(string sessionToken, CancellationToken cancellationToken) + { + var command = new AVCommand("users/me", + method: "GET", + sessionToken: sessionToken, + data: null); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + { + return AVObjectCoder.Instance.Decode(t.Result.Item2, AVDecoder.Instance); + }); + } + + public Task RequestPasswordResetAsync(string email, CancellationToken cancellationToken) + { + var command = new AVCommand("requestPasswordReset", + method: "POST", + data: new Dictionary { + { "email", email} + }); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken); + } + + public Task LogInWithParametersAsync(string relativeUrl, IDictionary data, + CancellationToken cancellationToken) + { + var command = new AVCommand(string.Format("{0}", relativeUrl), + method: "POST", + data: data); + + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + { + var serverState = AVObjectCoder.Instance.Decode(t.Result.Item2, AVDecoder.Instance); + serverState = serverState.MutatedClone(mutableClone => + { + mutableClone.IsNew = t.Result.Item1 == System.Net.HttpStatusCode.Created; + }); + return serverState; + }); + } + + public Task UpdatePasswordAsync(string userId, string sessionToken, string oldPassword, string newPassword, CancellationToken cancellationToken) + { + var command = new AVCommand(String.Format("users/{0}/updatePassword", userId), + method: "PUT", + sessionToken: sessionToken, + data: new Dictionary { + {"old_password", oldPassword}, + {"new_password", newPassword}, + }); + return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken); + } + + public Task RefreshSessionTokenAsync(string userId, string sessionToken, + CancellationToken cancellationToken) + { + var command = new AVCommand(String.Format("users/{0}/refreshSessionToken", userId), + method: "PUT", + sessionToken: sessionToken, + data: null); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).OnSuccess(t => + { + var serverState = AVObjectCoder.Instance.Decode(t.Result.Item2, AVDecoder.Instance); + return serverState; + }); + } + } +} diff --git a/Storage/Storage/Internal/User/Controller/IAVCurrentUserController.cs b/Storage/Storage/Internal/User/Controller/IAVCurrentUserController.cs new file mode 100644 index 0000000..c7664aa --- /dev/null +++ b/Storage/Storage/Internal/User/Controller/IAVCurrentUserController.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + public interface IAVCurrentUserController : IAVObjectCurrentController { + Task GetCurrentSessionTokenAsync(CancellationToken cancellationToken); + + Task LogOutAsync(CancellationToken cancellationToken); + } +} diff --git a/Storage/Storage/Internal/User/Controller/IAVUserController.cs b/Storage/Storage/Internal/User/Controller/IAVUserController.cs new file mode 100644 index 0000000..58966a7 --- /dev/null +++ b/Storage/Storage/Internal/User/Controller/IAVUserController.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal +{ + public interface IAVUserController + { + Task SignUpAsync(IObjectState state, + IDictionary operations, + CancellationToken cancellationToken); + + Task LogInAsync(string username, + string email, + string password, + CancellationToken cancellationToken); + + Task LogInWithParametersAsync(string relativeUrl, + IDictionary data, + CancellationToken cancellationToken); + + Task LogInAsync(string authType, + IDictionary data, + bool failOnNotExist, + CancellationToken cancellationToken); + + Task GetUserAsync(string sessionToken, + CancellationToken cancellationToken); + + Task RequestPasswordResetAsync(string email, + CancellationToken cancellationToken); + + Task UpdatePasswordAsync(string usedId, string sessionToken, + string oldPassword, string newPassword, + CancellationToken cancellationToken); + + Task RefreshSessionTokenAsync(string userId, + string sessionToken, + CancellationToken cancellationToken); + } +} diff --git a/Storage/Storage/Internal/Utilities/AVConfigExtensions.cs b/Storage/Storage/Internal/Utilities/AVConfigExtensions.cs new file mode 100644 index 0000000..0df396d --- /dev/null +++ b/Storage/Storage/Internal/Utilities/AVConfigExtensions.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; + +namespace LeanCloud.Storage.Internal { + /// + /// So here's the deal. We have a lot of internal APIs for AVObject, AVUser, etc. + /// + /// These cannot be 'internal' anymore if we are fully modularizing things out, because + /// they are no longer a part of the same library, especially as we create things like + /// Installation inside push library. + /// + /// So this class contains a bunch of extension methods that can live inside another + /// namespace, which 'wrap' the intenral APIs that already exist. + /// + public static class AVConfigExtensions { + public static AVConfig Create(IDictionary fetchedConfig) { + return new AVConfig(fetchedConfig); + } + } +} diff --git a/Storage/Storage/Internal/Utilities/AVFileExtensions.cs b/Storage/Storage/Internal/Utilities/AVFileExtensions.cs new file mode 100644 index 0000000..ae502d5 --- /dev/null +++ b/Storage/Storage/Internal/Utilities/AVFileExtensions.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; + +namespace LeanCloud.Storage.Internal { + /// + /// So here's the deal. We have a lot of internal APIs for AVObject, AVUser, etc. + /// + /// These cannot be 'internal' anymore if we are fully modularizing things out, because + /// they are no longer a part of the same library, especially as we create things like + /// Installation inside push library. + /// + /// So this class contains a bunch of extension methods that can live inside another + /// namespace, which 'wrap' the intenral APIs that already exist. + /// + public static class AVFileExtensions { + public static AVFile Create(string name, Uri uri, string mimeType = null) { + return new AVFile(name, uri, mimeType); + } + } +} diff --git a/Storage/Storage/Internal/Utilities/AVObjectExtensions.cs b/Storage/Storage/Internal/Utilities/AVObjectExtensions.cs new file mode 100644 index 0000000..2147049 --- /dev/null +++ b/Storage/Storage/Internal/Utilities/AVObjectExtensions.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Linq; +using System.Globalization; +using System.ComponentModel; +using System.Collections; + +namespace LeanCloud.Storage.Internal +{ + /// + /// So here's the deal. We have a lot of internal APIs for AVObject, AVUser, etc. + /// + /// These cannot be 'internal' anymore if we are fully modularizing things out, because + /// they are no longer a part of the same library, especially as we create things like + /// Installation inside push library. + /// + /// So this class contains a bunch of extension methods that can live inside another + /// namespace, which 'wrap' the intenral APIs that already exist. + /// + public static class AVObjectExtensions + { + public static T FromState(IObjectState state, string defaultClassName) where T : AVObject + { + return AVObject.FromState(state, defaultClassName); + } + + public static IObjectState GetState(this AVObject obj) + { + return obj.State; + } + + public static void HandleFetchResult(this AVObject obj, IObjectState serverState) + { + obj.HandleFetchResult(serverState); + } + + public static IDictionary GetCurrentOperations(this AVObject obj) + { + return obj.CurrentOperations; + } + + public static IDictionary Encode(this AVObject obj) + { + return PointerOrLocalIdEncoder.Instance.EncodeAVObject(obj, false); + } + + public static IEnumerable DeepTraversal(object root, bool traverseAVObjects = false, bool yieldRoot = false) + { + return AVObject.DeepTraversal(root, traverseAVObjects, yieldRoot); + } + + public static void SetIfDifferent(this AVObject obj, string key, T value) + { + obj.SetIfDifferent(key, value); + } + + public static IDictionary ServerDataToJSONObjectForSerialization(this AVObject obj) + { + return obj.ServerDataToJSONObjectForSerialization(); + } + + public static void Set(this AVObject obj, string key, object value) + { + obj.Set(key, value); + } + + public static void DisableHooks(this AVObject obj, IEnumerable hookKeys) + { + obj.Set("__ignore_hooks", hookKeys); + } + public static void DisableHook(this AVObject obj, string hookKey) + { + var newList = new List(); + if (obj.ContainsKey("__ignore_hooks")) + { + var hookKeys = obj.Get>("__ignore_hooks"); + newList = hookKeys.ToList(); + } + newList.Add(hookKey); + obj.DisableHooks(newList); + } + + public static void DisableAfterHook(this AVObject obj) + { + obj.DisableAfterSave(); + obj.DisableAfterUpdate(); + obj.DisableAfterDelete(); + } + + public static void DisableBeforeHook(this AVObject obj) + { + obj.DisableBeforeSave(); + obj.DisableBeforeDelete(); + obj.DisableBeforeUpdate(); + } + + public static void DisableBeforeSave(this AVObject obj) + { + obj.DisableHook("beforeSave"); + } + public static void DisableAfterSave(this AVObject obj) + { + obj.DisableHook("afterSave"); + } + public static void DisableBeforeUpdate(this AVObject obj) + { + obj.DisableHook("beforeUpdate"); + } + public static void DisableAfterUpdate(this AVObject obj) + { + obj.DisableHook("afterUpdate"); + } + public static void DisableBeforeDelete(this AVObject obj) + { + obj.DisableHook("beforeDelete"); + } + public static void DisableAfterDelete(this AVObject obj) + { + obj.DisableHook("afterDelete"); + } + + #region on property updated or changed or collection updated + + /// + /// On the property changed. + /// + /// Av object. + /// Property name. + /// Handler. + public static void OnPropertyChanged(this AVObject avObj, string propertyName, PropertyChangedEventHandler handler) + { + avObj.PropertyChanged += (sender, e) => + { + if (e.PropertyName == propertyName) + { + handler(sender, e); + } + }; + } + + /// + /// On the property updated. + /// + /// Av object. + /// Property name. + /// Handler. + public static void OnPropertyUpdated(this AVObject avObj, string propertyName, PropertyUpdatedEventHandler handler) + { + avObj.PropertyUpdated += (sender, e) => + { + if (e.PropertyName == propertyName) + { + handler(sender, e); + } + }; + } + + /// + /// On the property updated. + /// + /// Av object. + /// Property name. + /// Handler. + public static void OnPropertyUpdated(this AVObject avObj, string propertyName, Action handler) + { + avObj.OnPropertyUpdated(propertyName,(object sender, PropertyUpdatedEventArgs e) => + { + handler(e.OldValue, e.NewValue); + }); + } + + /// + /// On the collection property updated. + /// + /// Av object. + /// Property name. + /// Handler. + public static void OnCollectionPropertyUpdated(this AVObject avObj, string propertyName, CollectionPropertyUpdatedEventHandler handler) + { + avObj.CollectionPropertyUpdated += (sender, e) => + { + if (e.PropertyName == propertyName) + { + handler(sender, e); + } + }; + } + + /// + /// On the collection property added. + /// + /// Av object. + /// Property name. + /// Handler. + public static void OnCollectionPropertyAdded(this AVObject avObj, string propertyName, Action handler) + { + avObj.OnCollectionPropertyUpdated(propertyName, (sender, e) => + { + if (e.CollectionAction == NotifyCollectionUpdatedAction.Add) + { + handler(e.NewValues); + } + }); + } + + /// + /// On the collection property removed. + /// + /// Av object. + /// Property name. + /// Handler. + public static void OnCollectionPropertyRemoved(this AVObject avObj, string propertyName, Action handler) + { + avObj.OnCollectionPropertyUpdated(propertyName, (sender, e) => + { + if (e.CollectionAction == NotifyCollectionUpdatedAction.Remove) + { + handler(e.NewValues); + } + }); + } + #endregion + } +} diff --git a/Storage/Storage/Internal/Utilities/AVQueryExtensions.cs b/Storage/Storage/Internal/Utilities/AVQueryExtensions.cs new file mode 100644 index 0000000..059650a --- /dev/null +++ b/Storage/Storage/Internal/Utilities/AVQueryExtensions.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; + +namespace LeanCloud.Storage.Internal { + /// + /// So here's the deal. We have a lot of internal APIs for AVObject, AVUser, etc. + /// + /// These cannot be 'internal' anymore if we are fully modularizing things out, because + /// they are no longer a part of the same library, especially as we create things like + /// Installation inside push library. + /// + /// So this class contains a bunch of extension methods that can live inside another + /// namespace, which 'wrap' the intenral APIs that already exist. + /// + public static class AVQueryExtensions { + public static string GetClassName(this AVQuery query) where T: AVObject { + return query.ClassName; + } + + public static IDictionary BuildParameters(this AVQuery query) where T: AVObject { + return query.BuildParameters(false); + } + + public static object GetConstraint(this AVQuery query, string key) where T : AVObject { + return query.GetConstraint(key); + } + } +} diff --git a/Storage/Storage/Internal/Utilities/AVRelationExtensions.cs b/Storage/Storage/Internal/Utilities/AVRelationExtensions.cs new file mode 100644 index 0000000..c2d2c85 --- /dev/null +++ b/Storage/Storage/Internal/Utilities/AVRelationExtensions.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; + +namespace LeanCloud.Storage.Internal { + /// + /// So here's the deal. We have a lot of internal APIs for AVObject, AVUser, etc. + /// + /// These cannot be 'internal' anymore if we are fully modularizing things out, because + /// they are no longer a part of the same library, especially as we create things like + /// Installation inside push library. + /// + /// So this class contains a bunch of extension methods that can live inside another + /// namespace, which 'wrap' the intenral APIs that already exist. + /// + public static class AVRelationExtensions { + public static AVRelation Create(AVObject parent, string childKey) where T : AVObject { + return new AVRelation(parent, childKey); + } + + public static AVRelation Create(AVObject parent, string childKey, string targetClassName) where T: AVObject { + return new AVRelation(parent, childKey, targetClassName); + } + + public static string GetTargetClassName(this AVRelation relation) where T : AVObject { + return relation.TargetClassName; + } + } +} diff --git a/Storage/Storage/Internal/Utilities/AVSessionExtensions.cs b/Storage/Storage/Internal/Utilities/AVSessionExtensions.cs new file mode 100644 index 0000000..40a3c3d --- /dev/null +++ b/Storage/Storage/Internal/Utilities/AVSessionExtensions.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + /// + /// So here's the deal. We have a lot of internal APIs for AVObject, AVUser, etc. + /// + /// These cannot be 'internal' anymore if we are fully modularizing things out, because + /// they are no longer a part of the same library, especially as we create things like + /// Installation inside push library. + /// + /// So this class contains a bunch of extension methods that can live inside another + /// namespace, which 'wrap' the intenral APIs that already exist. + /// + public static class AVSessionExtensions { + public static Task UpgradeToRevocableSessionAsync(string sessionToken, CancellationToken cancellationToken) { + return AVSession.UpgradeToRevocableSessionAsync(sessionToken, cancellationToken); + } + + public static Task RevokeAsync(string sessionToken, CancellationToken cancellationToken) { + return AVSession.RevokeAsync(sessionToken, cancellationToken); + } + } +} diff --git a/Storage/Storage/Internal/Utilities/AVUserExtensions.cs b/Storage/Storage/Internal/Utilities/AVUserExtensions.cs new file mode 100644 index 0000000..936dc37 --- /dev/null +++ b/Storage/Storage/Internal/Utilities/AVUserExtensions.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal +{ + /// + /// So here's the deal. We have a lot of internal APIs for AVObject, AVUser, etc. + /// + /// These cannot be 'internal' anymore if we are fully modularizing things out, because + /// they are no longer a part of the same library, especially as we create things like + /// Installation inside push library. + /// + /// So this class contains a bunch of extension methods that can live inside another + /// namespace, which 'wrap' the intenral APIs that already exist. + /// + public static class AVUserExtensions + { + + } +} diff --git a/Storage/Storage/Internal/Utilities/FlexibleDictionaryWrapper.cs b/Storage/Storage/Internal/Utilities/FlexibleDictionaryWrapper.cs new file mode 100644 index 0000000..5ee7d2c --- /dev/null +++ b/Storage/Storage/Internal/Utilities/FlexibleDictionaryWrapper.cs @@ -0,0 +1,104 @@ +using System.Collections.Generic; +using System.Linq; +using LeanCloud.Utilities; + +namespace LeanCloud.Storage.Internal { + /// + /// Provides a Dictionary implementation that can delegate to any other + /// dictionary, regardless of its value type. Used for coercion of + /// dictionaries when returning them to users. + /// + /// The resulting type of value in the dictionary. + /// The original type of value in the dictionary. + [Preserve(AllMembers = true, Conditional = false)] + public class FlexibleDictionaryWrapper : IDictionary { + private readonly IDictionary toWrap; + public FlexibleDictionaryWrapper(IDictionary toWrap) { + this.toWrap = toWrap; + } + + public void Add(string key, TOut value) { + toWrap.Add(key, (TIn)Conversion.ConvertTo(value)); + } + + public bool ContainsKey(string key) { + return toWrap.ContainsKey(key); + } + + public ICollection Keys { + get { return toWrap.Keys; } + } + + public bool Remove(string key) { + return toWrap.Remove(key); + } + + public bool TryGetValue(string key, out TOut value) { + TIn outValue; + bool result = toWrap.TryGetValue(key, out outValue); + value = (TOut)Conversion.ConvertTo(outValue); + return result; + } + + public ICollection Values { + get { + return toWrap.Values + .Select(item => (TOut)Conversion.ConvertTo(item)).ToList(); + } + } + + public TOut this[string key] { + get { + return (TOut)Conversion.ConvertTo(toWrap[key]); + } + set { + toWrap[key] = (TIn)Conversion.ConvertTo(value); + } + } + + public void Add(KeyValuePair item) { + toWrap.Add(new KeyValuePair(item.Key, + (TIn)Conversion.ConvertTo(item.Value))); + } + + public void Clear() { + toWrap.Clear(); + } + + public bool Contains(KeyValuePair item) { + return toWrap.Contains(new KeyValuePair(item.Key, + (TIn)Conversion.ConvertTo(item.Value))); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) { + var converted = from pair in toWrap + select new KeyValuePair(pair.Key, + (TOut)Conversion.ConvertTo(pair.Value)); + converted.ToList().CopyTo(array, arrayIndex); + } + + public int Count { + get { return toWrap.Count; } + } + + public bool IsReadOnly { + get { return toWrap.IsReadOnly; } + } + + public bool Remove(KeyValuePair item) { + return toWrap.Remove(new KeyValuePair(item.Key, + (TIn)Conversion.ConvertTo(item.Value))); + } + + public IEnumerator> GetEnumerator() { + foreach (var pair in toWrap) { + yield return new KeyValuePair(pair.Key, + (TOut)Conversion.ConvertTo(pair.Value)); + } + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { + return this.GetEnumerator(); + } + } +} diff --git a/Storage/Storage/Internal/Utilities/FlexibleListWrapper.cs b/Storage/Storage/Internal/Utilities/FlexibleListWrapper.cs new file mode 100644 index 0000000..3db78d6 --- /dev/null +++ b/Storage/Storage/Internal/Utilities/FlexibleListWrapper.cs @@ -0,0 +1,81 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using LeanCloud.Utilities; + +namespace LeanCloud.Storage.Internal { + /// + /// Provides a List implementation that can delegate to any other + /// list, regardless of its value type. Used for coercion of + /// lists when returning them to users. + /// + /// The resulting type of value in the list. + /// The original type of value in the list. + [Preserve(AllMembers = true, Conditional = false)] + public class FlexibleListWrapper : IList { + private IList toWrap; + public FlexibleListWrapper(IList toWrap) { + this.toWrap = toWrap; + } + + public int IndexOf(TOut item) { + return toWrap.IndexOf((TIn)Conversion.ConvertTo(item)); + } + + public void Insert(int index, TOut item) { + toWrap.Insert(index, (TIn)Conversion.ConvertTo(item)); + } + + public void RemoveAt(int index) { + toWrap.RemoveAt(index); + } + + public TOut this[int index] { + get { + return (TOut)Conversion.ConvertTo(toWrap[index]); + } + set { + toWrap[index] = (TIn)Conversion.ConvertTo(value); + } + } + + public void Add(TOut item) { + toWrap.Add((TIn)Conversion.ConvertTo(item)); + } + + public void Clear() { + toWrap.Clear(); + } + + public bool Contains(TOut item) { + return toWrap.Contains((TIn)Conversion.ConvertTo(item)); + } + + public void CopyTo(TOut[] array, int arrayIndex) { + toWrap.Select(item => (TOut)Conversion.ConvertTo(item)) + .ToList().CopyTo(array, arrayIndex); + } + + public int Count { + get { return toWrap.Count; } + } + + public bool IsReadOnly { + get { return toWrap.IsReadOnly; } + } + + public bool Remove(TOut item) { + return toWrap.Remove((TIn)Conversion.ConvertTo(item)); + } + + public IEnumerator GetEnumerator() { + foreach (var item in (IEnumerable)toWrap) { + yield return (TOut)Conversion.ConvertTo(item); + } + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { + return this.GetEnumerator(); + } + } +} diff --git a/Storage/Storage/Internal/Utilities/IJsonConvertible.cs b/Storage/Storage/Internal/Utilities/IJsonConvertible.cs new file mode 100644 index 0000000..af29ef1 --- /dev/null +++ b/Storage/Storage/Internal/Utilities/IJsonConvertible.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace LeanCloud.Storage.Internal { + /// + /// Represents an object that can be converted into JSON. + /// + public interface IJsonConvertible { + /// + /// Converts the object to a data structure that can be converted to JSON. + /// + /// An object to be JSONified. + IDictionary ToJSON(); + } +} diff --git a/Storage/Storage/Internal/Utilities/IdentityEqualityComparer.cs b/Storage/Storage/Internal/Utilities/IdentityEqualityComparer.cs new file mode 100644 index 0000000..e25f818 --- /dev/null +++ b/Storage/Storage/Internal/Utilities/IdentityEqualityComparer.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace LeanCloud.Storage.Internal +{ + /// + /// An equality comparer that uses the object identity (i.e. ReferenceEquals) + /// rather than .Equals, allowing identity to be used for checking equality in + /// ISets and IDictionaries. + /// + public class IdentityEqualityComparer : IEqualityComparer + { + public bool Equals(T x, T y) + { + return object.ReferenceEquals(x, y); + } + + public int GetHashCode(T obj) + { + return RuntimeHelpers.GetHashCode(obj); + } + } +} diff --git a/Storage/Storage/Internal/Utilities/InternalExtensions.cs b/Storage/Storage/Internal/Utilities/InternalExtensions.cs new file mode 100644 index 0000000..e0228a5 --- /dev/null +++ b/Storage/Storage/Internal/Utilities/InternalExtensions.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + /// + /// Provides helper methods that allow us to use terser code elsewhere. + /// + public static class InternalExtensions { + /// + /// Ensures a task (even null) is awaitable. + /// + /// + /// + /// + public static Task Safe(this Task task) { + return task ?? Task.FromResult(default(T)); + } + + /// + /// Ensures a task (even null) is awaitable. + /// + /// + /// + public static Task Safe(this Task task) { + return task ?? Task.FromResult(null); + } + + public delegate void PartialAccessor(ref T arg); + + public static TValue GetOrDefault(this IDictionary self, + TKey key, + TValue defaultValue) { + TValue value; + if (self.TryGetValue(key, out value)) { + return value; + } + return defaultValue; + } + + public static bool CollectionsEqual(this IEnumerable a, IEnumerable b) { + return Object.Equals(a, b) || + (a != null && b != null && + a.SequenceEqual(b)); + } + + public static Task OnSuccess(this Task task, + Func, TResult> continuation) { + return ((Task)task).OnSuccess(t => continuation((Task)t)); + } + + public static Task OnSuccess(this Task task, Action> continuation) { + return task.OnSuccess((Func, object>)(t => { + continuation(t); + return null; + })); + } + + public static Task OnSuccess(this Task task, + Func continuation) { + return task.ContinueWith(t => { + if (t.IsFaulted) { + var ex = t.Exception.Flatten(); + if (ex.InnerExceptions.Count == 1) { + ExceptionDispatchInfo.Capture(ex.InnerExceptions[0]).Throw(); + } else { + ExceptionDispatchInfo.Capture(ex).Throw(); + } + // Unreachable + return Task.FromResult(default(TResult)); + } else if (t.IsCanceled) { + var tcs = new TaskCompletionSource(); + tcs.SetCanceled(); + return tcs.Task; + } else { + return Task.FromResult(continuation(t)); + } + }).Unwrap(); + } + + public static Task OnSuccess(this Task task, Action continuation) { + return task.OnSuccess((Func)(t => { + continuation(t); + return null; + })); + } + + public static Task WhileAsync(Func> predicate, Func body) { + Func iterate = null; + iterate = () => { + return predicate().OnSuccess(t => { + if (!t.Result) { + return Task.FromResult(0); + } + return body().OnSuccess(_ => iterate()).Unwrap(); + }).Unwrap(); + }; + return iterate(); + } + } +} diff --git a/Storage/Storage/Internal/Utilities/Json.cs b/Storage/Storage/Internal/Utilities/Json.cs new file mode 100644 index 0000000..8babc7f --- /dev/null +++ b/Storage/Storage/Internal/Utilities/Json.cs @@ -0,0 +1,554 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal +{ + /// + /// A simple recursive-descent JSON Parser based on the grammar defined at http://www.json.org + /// and http://tools.ietf.org/html/rfc4627 + /// + public class Json + { + /// + /// Place at the start of a regex to force the match to begin wherever the search starts (i.e. + /// anchored at the index of the first character of the search, even when that search starts + /// in the middle of the string). + /// + private static readonly string startOfString = "\\G"; + private static readonly char startObject = '{'; + private static readonly char endObject = '}'; + private static readonly char startArray = '['; + private static readonly char endArray = ']'; + private static readonly char valueSeparator = ','; + private static readonly char nameSeparator = ':'; + private static readonly char[] falseValue = "false".ToCharArray(); + private static readonly char[] trueValue = "true".ToCharArray(); + private static readonly char[] nullValue = "null".ToCharArray(); + private static readonly Regex numberValue = new Regex(startOfString + + @"-?(?:0|[1-9]\d*)(?\.\d+)?(?(?:e|E)(?:-|\+)?\d+)?"); + private static readonly Regex stringValue = new Regex(startOfString + + "\"(?(?:[^\\\\\"]|(?\\\\(?:[\\\\\"/bfnrt]|u[0-9a-fA-F]{4})))*)\"", + RegexOptions.Multiline); + + private static readonly Regex escapePattern = new Regex("\\\\|\"|[\u0000-\u001F]"); + + private class JsonStringParser + { + public string Input { get; private set; } + + public char[] InputAsArray { get; private set; } + + private int currentIndex; + public int CurrentIndex + { + get + { + return currentIndex; + } + } + + public void Skip(int skip) + { + currentIndex += skip; + } + + public JsonStringParser(string input) + { + Input = input; + InputAsArray = input.ToCharArray(); + } + + /// + /// Parses JSON object syntax (e.g. '{}') + /// + internal bool AVObject(out object output) + { + output = null; + int initialCurrentIndex = CurrentIndex; + if (!Accept(startObject)) + { + return false; + } + var dict = new Dictionary(); + while (true) + { + object pairValue; + if (!ParseMember(out pairValue)) + { + break; + } + var pair = pairValue as Tuple; + dict[pair.Item1] = pair.Item2; + if (!Accept(valueSeparator)) + { + break; + } + } + if (!Accept(endObject)) + { + return false; + } + output = dict; + return true; + } + + /// + /// Parses JSON member syntax (e.g. '"keyname" : null') + /// + private bool ParseMember(out object output) + { + output = null; + object key; + if (!ParseString(out key)) + { + return false; + } + if (!Accept(nameSeparator)) + { + return false; + } + object value; + if (!ParseValue(out value)) + { + return false; + } + output = new Tuple((string)key, value); + return true; + } + + /// + /// Parses JSON array syntax (e.g. '[]') + /// + internal bool ParseArray(out object output) + { + output = null; + if (!Accept(startArray)) + { + return false; + } + var list = new List(); + while (true) + { + object value; + if (!ParseValue(out value)) + { + break; + } + list.Add(value); + if (!Accept(valueSeparator)) + { + break; + } + } + if (!Accept(endArray)) + { + return false; + } + output = list; + return true; + } + + /// + /// Parses a value (i.e. the right-hand side of an object member assignment or + /// an element in an array) + /// + private bool ParseValue(out object output) + { + if (Accept(falseValue)) + { + output = false; + return true; + } + else if (Accept(nullValue)) + { + output = null; + return true; + } + else if (Accept(trueValue)) + { + output = true; + return true; + } + return AVObject(out output) || + ParseArray(out output) || + ParseNumber(out output) || + ParseString(out output); + } + + /// + /// Parses a JSON string (e.g. '"foo\u1234bar\n"') + /// + private bool ParseString(out object output) + { + output = null; + Match m; + if (!Accept(stringValue, out m)) + { + return false; + } + // handle escapes: + int offset = 0; + var contentCapture = m.Groups["content"]; + var builder = new StringBuilder(contentCapture.Value); + foreach (Capture escape in m.Groups["escape"].Captures) + { + int index = (escape.Index - contentCapture.Index) - offset; + offset += escape.Length - 1; + builder.Remove(index + 1, escape.Length - 1); + switch (escape.Value[1]) + { + case '\"': + builder[index] = '\"'; + break; + case '\\': + builder[index] = '\\'; + break; + case '/': + builder[index] = '/'; + break; + case 'b': + builder[index] = '\b'; + break; + case 'f': + builder[index] = '\f'; + break; + case 'n': + builder[index] = '\n'; + break; + case 'r': + builder[index] = '\r'; + break; + case 't': + builder[index] = '\t'; + break; + case 'u': + builder[index] = (char)ushort.Parse(escape.Value.Substring(2), NumberStyles.AllowHexSpecifier); + break; + default: + throw new ArgumentException("Unexpected escape character in string: " + escape.Value); + } + } + output = builder.ToString(); + return true; + } + + /// + /// Parses a number. Returns a long if the number is an integer or has an exponent, + /// otherwise returns a double. + /// + private bool ParseNumber(out object output) + { + output = null; + Match m; + if (!Accept(numberValue, out m)) + { + return false; + } + if (m.Groups["frac"].Length > 0 || m.Groups["exp"].Length > 0) + { + // It's a double. + output = double.Parse(m.Value, CultureInfo.InvariantCulture); + return true; + } + else + { + int temp = 0; + if (int.TryParse(m.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out temp)) + { + output = temp; + return true; + } + output = long.Parse(m.Value, CultureInfo.InvariantCulture); + return true; + } + } + + /// + /// Matches the string to a regex, consuming part of the string and returning the match. + /// + private bool Accept(Regex matcher, out Match match) + { + match = matcher.Match(Input, CurrentIndex); + if (match.Success) + { + Skip(match.Length); + } + return match.Success; + } + + /// + /// Find the first occurrences of a character, consuming part of the string. + /// + private bool Accept(char condition) + { + int step = 0; + int strLen = InputAsArray.Length; + int currentStep = currentIndex; + char currentChar; + + // Remove whitespace + while (currentStep < strLen && + ((currentChar = InputAsArray[currentStep]) == ' ' || + currentChar == '\r' || + currentChar == '\t' || + currentChar == '\n')) + { + ++step; + ++currentStep; + } + + bool match = (currentStep < strLen) && (InputAsArray[currentStep] == condition); + if (match) + { + ++step; + ++currentStep; + + // Remove whitespace + while (currentStep < strLen && + ((currentChar = InputAsArray[currentStep]) == ' ' || + currentChar == '\r' || + currentChar == '\t' || + currentChar == '\n')) + { + ++step; + ++currentStep; + } + + Skip(step); + } + return match; + } + + /// + /// Find the first occurrences of a string, consuming part of the string. + /// + private bool Accept(char[] condition) + { + int step = 0; + int strLen = InputAsArray.Length; + int currentStep = currentIndex; + char currentChar; + + // Remove whitespace + while (currentStep < strLen && + ((currentChar = InputAsArray[currentStep]) == ' ' || + currentChar == '\r' || + currentChar == '\t' || + currentChar == '\n')) + { + ++step; + ++currentStep; + } + + bool strMatch = true; + for (int i = 0; currentStep < strLen && i < condition.Length; ++i, ++currentStep) + { + if (InputAsArray[currentStep] != condition[i]) + { + strMatch = false; + break; + } + } + + bool match = (currentStep < strLen) && strMatch; + if (match) + { + Skip(step + condition.Length); + } + return match; + } + } + + /// + /// Parses a JSON-text as defined in http://tools.ietf.org/html/rfc4627, returning an + /// IDictionary<string, object> or an IList<object> depending on whether + /// the value was an array or dictionary. Nested objects also match these types. + /// + public static object Parse(string input) + { + object output; + input = input.Trim(); + JsonStringParser parser = new JsonStringParser(input); + + if ((parser.AVObject(out output) || + parser.ParseArray(out output)) && + parser.CurrentIndex == input.Length) + { + return output; + } + throw new ArgumentException("Input JSON was invalid."); + } + + /// + /// Encodes a dictionary into a JSON string. Supports values that are + /// IDictionary<string, object>, IList<object>, strings, + /// nulls, and any of the primitive types. + /// + public static string Encode(IDictionary dict) + { + if (dict == null) + { + throw new ArgumentNullException(); + } + if (dict.Count == 0) + { + return "{}"; + } + var builder = new StringBuilder("{"); + foreach (var pair in dict) + { + builder.Append(Encode(pair.Key)); + builder.Append(":"); + builder.Append(Encode(pair.Value)); + builder.Append(","); + } + builder[builder.Length - 1] = '}'; + return builder.ToString(); + } + + /// + /// Encodes a list into a JSON string. Supports values that are + /// IDictionary<string, object>, IList<object>, strings, + /// nulls, and any of the primitive types. + /// + public static string Encode(IList list) + { + if (list == null) + { + throw new ArgumentNullException(); + } + if (list.Count == 0) + { + return "[]"; + } + var builder = new StringBuilder("["); + foreach (var item in list) + { + builder.Append(Encode(item)); + builder.Append(","); + } + builder[builder.Length - 1] = ']'; + return builder.ToString(); + } + + public static string Encode(IList strList) + { + if (strList == null) + { + throw new ArgumentNullException(); + } + if (strList.Count == 0) + { + return "[]"; + } + StringBuilder stringBuilder = new StringBuilder("["); + foreach (object obj in strList) + { + stringBuilder.Append(Json.Encode(obj)); + stringBuilder.Append(","); + } + stringBuilder[stringBuilder.Length - 1] = ']'; + return stringBuilder.ToString(); + } + + public static string Encode(IList> dicList) + { + if (dicList == null) + { + throw new ArgumentNullException(); + } + if (dicList.Count == 0) + { + return "[]"; + } + StringBuilder stringBuilder = new StringBuilder("["); + foreach (object obj in dicList) + { + stringBuilder.Append(Json.Encode(obj)); + stringBuilder.Append(","); + } + stringBuilder[stringBuilder.Length - 1] = ']'; + return stringBuilder.ToString(); + } + + /// + /// Encodes an object into a JSON string. + /// + public static string Encode(object obj) + { + var dict = obj as IDictionary; + if (dict != null) + { + return Encode(dict); + } + var list = obj as IList; + if (list != null) + { + return Encode(list); + } + var dicList = obj as IList>; + if (dicList != null) + { + return Encode(dicList); + } + var strLists = obj as IList; + if (strLists != null) + { + return Encode(strLists); + } + var str = obj as string; + if (str != null) + { + str = escapePattern.Replace(str, m => + { + switch (m.Value[0]) + { + case '\\': + return "\\\\"; + case '\"': + return "\\\""; + case '\b': + return "\\b"; + case '\f': + return "\\f"; + case '\n': + return "\\n"; + case '\r': + return "\\r"; + case '\t': + return "\\t"; + default: + return "\\u" + ((ushort)m.Value[0]).ToString("x4"); + } + }); + return "\"" + str + "\""; + } + if (obj == null) + { + return "null"; + } + if (obj is bool) + { + if ((bool)obj) + { + return "true"; + } + else + { + return "false"; + } + } + if (!obj.GetType().GetTypeInfo().IsPrimitive) + { + throw new ArgumentException("Unable to encode objects of type " + obj.GetType()); + } + return Convert.ToString(obj, CultureInfo.InvariantCulture); + } + } +} diff --git a/Storage/Storage/Internal/Utilities/LockSet.cs b/Storage/Storage/Internal/Utilities/LockSet.cs new file mode 100644 index 0000000..65e188c --- /dev/null +++ b/Storage/Storage/Internal/Utilities/LockSet.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + public class LockSet { + private static readonly ConditionalWeakTable stableIds = + new ConditionalWeakTable(); + private static long nextStableId = 0; + + private readonly IEnumerable mutexes; + + public LockSet(IEnumerable mutexes) { + this.mutexes = (from mutex in mutexes + orderby GetStableId(mutex) + select mutex).ToList(); + } + + public void Enter() { + foreach (var mutex in mutexes) { + Monitor.Enter(mutex); + } + } + + public void Exit() { + foreach (var mutex in mutexes) { + Monitor.Exit(mutex); + } + } + + private static IComparable GetStableId(object mutex) { + lock (stableIds) { + return stableIds.GetValue(mutex, k => nextStableId++); + } + } + } +} diff --git a/Storage/Storage/Internal/Utilities/ReflectionHelpers.cs b/Storage/Storage/Internal/Utilities/ReflectionHelpers.cs new file mode 100644 index 0000000..55c4833 --- /dev/null +++ b/Storage/Storage/Internal/Utilities/ReflectionHelpers.cs @@ -0,0 +1,123 @@ +using System; +using System.Reflection; +using System.Collections.Generic; +using System.Linq; + +namespace LeanCloud.Storage.Internal +{ + public static class ReflectionHelpers + { + public static IEnumerable GetProperties(Type type) + { +#if MONO || UNITY + return type.GetProperties(); +#else + return type.GetRuntimeProperties(); +#endif + } + + public static MethodInfo GetMethod(Type type, string name, Type[] parameters) + { +#if MONO || UNITY + return type.GetMethod(name, parameters); +#else + return type.GetRuntimeMethod(name, parameters); +#endif + } + + public static bool IsPrimitive(Type type) + { +#if MONO || UNITY + return type.IsPrimitive; +#else + return type.GetTypeInfo().IsPrimitive; +#endif + } + + public static IEnumerable GetInterfaces(Type type) + { +#if MONO || UNITY + return type.GetInterfaces(); +#else + return type.GetTypeInfo().ImplementedInterfaces; +#endif + } + + public static bool IsConstructedGenericType(Type type) + { +#if UNITY + return type.IsGenericType && !type.IsGenericTypeDefinition; +#else + return type.IsConstructedGenericType; +#endif + } + + public static IEnumerable GetConstructors(Type type) + { +#if UNITY + BindingFlags searchFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + return type.GetConstructors(searchFlags); +#else + return type.GetTypeInfo().DeclaredConstructors + .Where(c => (c.Attributes & MethodAttributes.Static) == 0); +#endif + } + + public static Type[] GetGenericTypeArguments(Type type) + { +#if UNITY + return type.GetGenericArguments(); +#else + return type.GenericTypeArguments; +#endif + } + + public static PropertyInfo GetProperty(Type type, string name) + { +#if MONO || UNITY + return type.GetProperty(name); +#else + return type.GetRuntimeProperty(name); +#endif + } + + /// + /// This method helps simplify the process of getting a constructor for a type. + /// A method like this exists in .NET but is not allowed in a Portable Class Library, + /// so we've built our own. + /// + /// + /// + /// + public static ConstructorInfo FindConstructor(this Type self, params Type[] parameterTypes) + { + var constructors = + from constructor in GetConstructors(self) + let parameters = constructor.GetParameters() + let types = from p in parameters select p.ParameterType + where types.SequenceEqual(parameterTypes) + select constructor; + return constructors.SingleOrDefault(); + } + + public static bool IsNullable(Type t) + { + bool isGeneric; +#if UNITY + isGeneric = t.IsGenericType && !t.IsGenericTypeDefinition; +#else + isGeneric = t.IsConstructedGenericType; +#endif + return isGeneric && t.GetGenericTypeDefinition().Equals(typeof(Nullable<>)); + } + + public static IEnumerable GetCustomAttributes(this Assembly assembly) where T : Attribute + { +#if UNITY + return assembly.GetCustomAttributes(typeof(T), false).Select(attr => attr as T); +#else + return CustomAttributeExtensions.GetCustomAttributes(assembly); +#endif + } + } +} diff --git a/Storage/Storage/Internal/Utilities/SynchronizedEventHandler.cs b/Storage/Storage/Internal/Utilities/SynchronizedEventHandler.cs new file mode 100644 index 0000000..d94f12a --- /dev/null +++ b/Storage/Storage/Internal/Utilities/SynchronizedEventHandler.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + /// + /// Represents an event handler that calls back from the synchronization context + /// that subscribed. + /// Should look like an EventArgs, but may not inherit EventArgs if T is implemented by the Windows team. + /// + public class SynchronizedEventHandler { + private LinkedList> delegates = + new LinkedList>(); + public void Add(Delegate del) { + lock (delegates) { + TaskFactory factory; + if (SynchronizationContext.Current != null) { + factory = + new TaskFactory(CancellationToken.None, + TaskCreationOptions.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.FromCurrentSynchronizationContext()); + } else { + factory = Task.Factory; + } + foreach (var d in del.GetInvocationList()) { + delegates.AddLast(new Tuple(d, factory)); + } + } + } + + public void Remove(Delegate del) { + lock (delegates) { + if (delegates.Count == 0) { + return; + } + foreach (var d in del.GetInvocationList()) { + var node = delegates.First; + while (node != null) { + if (node.Value.Item1 == d) { + delegates.Remove(node); + break; + } + node = node.Next; + } + } + } + } + + public Task Invoke(object sender, T args) { + IEnumerable> toInvoke; + var toContinue = new[] { Task.FromResult(0) }; + lock (delegates) { + toInvoke = delegates.ToList(); + } + var invocations = toInvoke + .Select(p => p.Item2.ContinueWhenAll(toContinue, + _ => p.Item1.DynamicInvoke(sender, args))) + .ToList(); + return Task.WhenAll(invocations); + } + } +} diff --git a/Storage/Storage/Internal/Utilities/TaskQueue.cs b/Storage/Storage/Internal/Utilities/TaskQueue.cs new file mode 100644 index 0000000..4f1ee25 --- /dev/null +++ b/Storage/Storage/Internal/Utilities/TaskQueue.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud.Storage.Internal { + /// + /// A helper class for enqueuing tasks + /// + public class TaskQueue { + /// + /// We only need to keep the tail of the queue. Cancelled tasks will + /// just complete normally/immediately when their turn arrives. + /// + private Task tail; + private readonly object mutex = new object(); + + /// + /// Gets a cancellable task that can be safely awaited and is dependent + /// on the current tail of the queue. This essentially gives us a proxy + /// for the tail end of the queue whose awaiting can be cancelled. + /// + /// A cancellation token that cancels + /// the task even if the task is still in the queue. This allows the + /// running task to return immediately without breaking the dependency + /// chain. It also ensures that errors do not propagate. + /// A new task that should be awaited by enqueued tasks. + private Task GetTaskToAwait(CancellationToken cancellationToken) { + lock (mutex) { + Task toAwait = tail ?? Task.FromResult(true); + return toAwait.ContinueWith(task => { }, cancellationToken); + } + } + + /// + /// Enqueues a task created by . If the task is + /// cancellable (or should be able to be cancelled while it is waiting in the + /// queue), pass a cancellationToken. + /// + /// The type of task. + /// A function given a task to await once state is + /// snapshotted (e.g. after capturing session tokens at the time of the save call). + /// Awaiting this task will wait for the created task's turn in the queue. + /// A cancellation token that can be used to + /// cancel waiting in the queue. + /// The task created by the taskStart function. + public T Enqueue(Func taskStart, CancellationToken cancellationToken) + where T : Task { + Task oldTail; + T task; + lock (mutex) { + oldTail = this.tail ?? Task.FromResult(true); + // The task created by taskStart is responsible for waiting the + // task passed to it before doing its work (this gives it an opportunity + // to do startup work or save state before waiting for its turn in the queue + task = taskStart(GetTaskToAwait(cancellationToken)); + + // The tail task should be dependent on the old tail as well as the newly-created + // task. This prevents cancellation of the new task from causing the queue to run + // out of order. + this.tail = Task.WhenAll(oldTail, task); + } + return task; + } + + public object Mutex { get { return mutex; } } + } +} diff --git a/Storage/Storage/Internal/Utilities/XamarinAttributes.cs b/Storage/Storage/Internal/Utilities/XamarinAttributes.cs new file mode 100644 index 0000000..219cb1a --- /dev/null +++ b/Storage/Storage/Internal/Utilities/XamarinAttributes.cs @@ -0,0 +1,426 @@ +using System; +using System.Collections.Generic; +using System.Threading; + +namespace LeanCloud.Storage.Internal { + /// + /// A reimplementation of Xamarin's PreserveAttribute. + /// This allows us to support AOT and linking for Xamarin platforms. + /// + [AttributeUsage(AttributeTargets.All)] + internal class PreserveAttribute : Attribute { + public bool AllMembers; + public bool Conditional; + } + + [AttributeUsage(AttributeTargets.All)] + internal class LinkerSafeAttribute : Attribute { + public LinkerSafeAttribute() { } + } + + [Preserve(AllMembers = true)] + internal class PreserveWrapperTypes { + /// + /// Exists to ensure that generic types are AOT-compiled for the conversions we support. + /// Any new value types that we add support for will need to be registered here. + /// The method itself is never called, but by virtue of the Preserve attribute being set + /// on the class, these types will be AOT-compiled. + /// + /// This also applies to Unity. + /// + private static List CreateWrapperTypes() { + return new List { + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + (Action)(() => AVCloud.CallFunctionAsync(null, null, null,CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync(null, null,null, CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync(null, null,null, CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync(null, null,null ,CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync(null, null,null, CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync(null, null,null, CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync(null, null,null, CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync(null, null,null, CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync(null, null, null,CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync(null, null,null ,CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync(null, null,null, CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync(null, null,null, CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync(null, null,null, CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync>(null, null,null, CancellationToken.None)), + (Action)(() => AVCloud.CallFunctionAsync>(null, null,null, CancellationToken.None)), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + }; + } + } +} diff --git a/Storage/Storage/Public/AVACL.cs b/Storage/Storage/Public/AVACL.cs new file mode 100644 index 0000000..22544c4 --- /dev/null +++ b/Storage/Storage/Public/AVACL.cs @@ -0,0 +1,284 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using LeanCloud.Storage.Internal; + +namespace LeanCloud { + /// + /// A AVACL is used to control which users and roles can access or modify a particular object. Each + /// can have its own AVACL. You can grant read and write permissions + /// separately to specific users, to groups of users that belong to roles, or you can grant permissions + /// to "the public" so that, for example, any user could read a particular object but only a particular + /// set of users could write to that object. + /// + public class AVACL : IJsonConvertible { + private enum AccessKind { + Read, + Write + } + private const string publicName = "*"; + private readonly ICollection readers = new HashSet(); + private readonly ICollection writers = new HashSet(); + + internal AVACL(IDictionary jsonObject) { + readers = new HashSet(from pair in jsonObject + where ((IDictionary)pair.Value).ContainsKey("read") + select pair.Key); + writers = new HashSet(from pair in jsonObject + where ((IDictionary)pair.Value).ContainsKey("write") + select pair.Key); + } + + /// + /// Creates an ACL with no permissions granted. + /// + public AVACL() { + } + + /// + /// Creates an ACL where only the provided user has access. + /// + /// The only user that can read or write objects governed by this ACL. + public AVACL(AVUser owner) { + SetReadAccess(owner, true); + SetWriteAccess(owner, true); + } + + IDictionary IJsonConvertible.ToJSON() { + var result = new Dictionary(); + foreach (var user in readers.Union(writers)) { + var userPermissions = new Dictionary(); + if (readers.Contains(user)) { + userPermissions["read"] = true; + } + if (writers.Contains(user)) { + userPermissions["write"] = true; + } + result[user] = userPermissions; + } + return result; + } + + private void SetAccess(AccessKind kind, string userId, bool allowed) { + if (userId == null) { + throw new ArgumentException("Cannot set access for an unsaved user or role."); + } + ICollection target = null; + switch (kind) { + case AccessKind.Read: + target = readers; + break; + case AccessKind.Write: + target = writers; + break; + default: + throw new NotImplementedException("Unknown AccessKind"); + } + if (allowed) { + target.Add(userId); + } else { + target.Remove(userId); + } + } + + private bool GetAccess(AccessKind kind, string userId) { + if (userId == null) { + throw new ArgumentException("Cannot get access for an unsaved user or role."); + } + switch (kind) { + case AccessKind.Read: + return readers.Contains(userId); + case AccessKind.Write: + return writers.Contains(userId); + default: + throw new NotImplementedException("Unknown AccessKind"); + } + } + + /// + /// Gets or sets whether the public is allowed to read this object. + /// + public bool PublicReadAccess { + get { + return GetAccess(AccessKind.Read, publicName); + } + set { + SetAccess(AccessKind.Read, publicName, value); + } + } + + /// + /// Gets or sets whether the public is allowed to write this object. + /// + public bool PublicWriteAccess { + get { + return GetAccess(AccessKind.Write, publicName); + } + set { + SetAccess(AccessKind.Write, publicName, value); + } + } + + /// + /// Sets whether the given user id is allowed to read this object. + /// + /// The objectId of the user. + /// Whether the user has permission. + public void SetReadAccess(string userId, bool allowed) { + SetAccess(AccessKind.Read, userId, allowed); + } + + /// + /// Sets whether the given user is allowed to read this object. + /// + /// The user. + /// Whether the user has permission. + public void SetReadAccess(AVUser user, bool allowed) { + SetReadAccess(user.ObjectId, allowed); + } + + /// + /// Sets whether the given user id is allowed to write this object. + /// + /// The objectId of the user. + /// Whether the user has permission. + public void SetWriteAccess(string userId, bool allowed) { + SetAccess(AccessKind.Write, userId, allowed); + } + + /// + /// Sets whether the given user is allowed to write this object. + /// + /// The user. + /// Whether the user has permission. + public void SetWriteAccess(AVUser user, bool allowed) { + SetWriteAccess(user.ObjectId, allowed); + } + + /// + /// Gets whether the given user id is *explicitly* allowed to read this object. + /// Even if this returns false, the user may still be able to read it if + /// PublicReadAccess is true or a role that the user belongs to has read access. + /// + /// The user objectId to check. + /// Whether the user has access. + public bool GetReadAccess(string userId) { + return GetAccess(AccessKind.Read, userId); + } + + /// + /// Gets whether the given user is *explicitly* allowed to read this object. + /// Even if this returns false, the user may still be able to read it if + /// PublicReadAccess is true or a role that the user belongs to has read access. + /// + /// The user to check. + /// Whether the user has access. + public bool GetReadAccess(AVUser user) { + return GetReadAccess(user.ObjectId); + } + + /// + /// Gets whether the given user id is *explicitly* allowed to write this object. + /// Even if this returns false, the user may still be able to write it if + /// PublicReadAccess is true or a role that the user belongs to has write access. + /// + /// The user objectId to check. + /// Whether the user has access. + public bool GetWriteAccess(string userId) { + return GetAccess(AccessKind.Write, userId); + } + + /// + /// Gets whether the given user is *explicitly* allowed to write this object. + /// Even if this returns false, the user may still be able to write it if + /// PublicReadAccess is true or a role that the user belongs to has write access. + /// + /// The user to check. + /// Whether the user has access. + public bool GetWriteAccess(AVUser user) { + return GetWriteAccess(user.ObjectId); + } + + /// + /// Sets whether users belonging to the role with the given + /// are allowed to read this object. + /// + /// The name of the role. + /// Whether the role has access. + public void SetRoleReadAccess(string roleName, bool allowed) { + SetAccess(AccessKind.Read, "role:" + roleName, allowed); + } + + /// + /// Sets whether users belonging to the given role are allowed to read this object. + /// + /// The role. + /// Whether the role has access. + public void SetRoleReadAccess(AVRole role, bool allowed) { + SetRoleReadAccess(role.Name, allowed); + } + + /// + /// Gets whether users belonging to the role with the given + /// are allowed to read this object. Even if this returns false, the role may still be + /// able to read it if a parent role has read access. + /// + /// The name of the role. + /// Whether the role has access. + public bool GetRoleReadAccess(string roleName) { + return GetAccess(AccessKind.Read, "role:" + roleName); + } + + /// + /// Gets whether users belonging to the role are allowed to read this object. + /// Even if this returns false, the role may still be able to read it if a + /// parent role has read access. + /// + /// The name of the role. + /// Whether the role has access. + public bool GetRoleReadAccess(AVRole role) { + return GetRoleReadAccess(role.Name); + } + + /// + /// Sets whether users belonging to the role with the given + /// are allowed to write this object. + /// + /// The name of the role. + /// Whether the role has access. + public void SetRoleWriteAccess(string roleName, bool allowed) { + SetAccess(AccessKind.Write, "role:" + roleName, allowed); + } + + /// + /// Sets whether users belonging to the given role are allowed to write this object. + /// + /// The role. + /// Whether the role has access. + public void SetRoleWriteAccess(AVRole role, bool allowed) { + SetRoleWriteAccess(role.Name, allowed); + } + + /// + /// Gets whether users belonging to the role with the given + /// are allowed to write this object. Even if this returns false, the role may still be + /// able to write it if a parent role has write access. + /// + /// The name of the role. + /// Whether the role has access. + public bool GetRoleWriteAccess(string roleName) { + return GetAccess(AccessKind.Write, "role:" + roleName); + } + + /// + /// Gets whether users belonging to the role are allowed to write this object. + /// Even if this returns false, the role may still be able to write it if a + /// parent role has write access. + /// + /// The name of the role. + /// Whether the role has access. + public bool GetRoleWriteAccess(AVRole role) { + return GetRoleWriteAccess(role.Name); + } + } +} diff --git a/Storage/Storage/Public/AVClassNameAttribute.cs b/Storage/Storage/Public/AVClassNameAttribute.cs new file mode 100644 index 0000000..aa82d0b --- /dev/null +++ b/Storage/Storage/Public/AVClassNameAttribute.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud +{ + /// + /// Defines the class name for a subclass of AVObject. + /// + [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)] + public sealed class AVClassNameAttribute : Attribute + { + /// + /// Constructs a new AVClassName attribute. + /// + /// The class name to associate with the AVObject subclass. + public AVClassNameAttribute(string className) + { + this.ClassName = className; + } + + /// + /// Gets the class name to associate with the AVObject subclass. + /// + public string ClassName { get; private set; } + } +} diff --git a/Storage/Storage/Public/AVClient.cs b/Storage/Storage/Public/AVClient.cs new file mode 100644 index 0000000..a4308c9 --- /dev/null +++ b/Storage/Storage/Public/AVClient.cs @@ -0,0 +1,506 @@ +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud +{ + /// + /// AVClient contains static functions that handle global + /// configuration for the LeanCloud library. + /// + public static partial class AVClient + { + public static readonly string[] DateFormatStrings = { + // Official ISO format + "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'", + // It's possible that the string converter server-side may trim trailing zeroes, + // so these two formats cover ourselves from that. + "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'ff'Z'", + "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'f'Z'", + }; + + /// + /// Represents the configuration of the LeanCloud SDK. + /// + public struct Configuration + { + /// + /// 与 SDK 通讯的云端节点 + /// + public enum AVRegion + { + /// + /// 默认值,LeanCloud 华北节点,同 Public_North_China + /// + [Obsolete("please use Configuration.AVRegion.Public_North_China")] + Public_CN = 0, + + /// + /// 默认值,华北公有云节点,同 Public_CN + /// + Public_North_China = 0, + + /// + /// LeanCloud 北美区公有云节点,同 Public_North_America + /// + [Obsolete("please use Configuration.AVRegion.Public_North_America")] + Public_US = 1, + /// + /// LeanCloud 北美区公有云节点,同 Public_US + /// + Public_North_America = 1, + + /// + /// 华东公有云节点,同 Public_East_China + /// + [Obsolete("please use Configuration.AVRegion.Public_East_China")] + Vendor_Tencent = 2, + + /// + /// 华东公有云节点,同 Vendor_Tencent + /// + Public_East_China = 2, + } + + /// + /// In the event that you would like to use the LeanCloud SDK + /// from a completely portable project, with no platform-specific library required, + /// to get full access to all of our features available on LeanCloud.com + /// (A/B testing, slow queries, etc.), you must set the values of this struct + /// to be appropriate for your platform. + /// + /// Any values set here will overwrite those that are automatically configured by + /// any platform-specific migration library your app includes. + /// + public struct VersionInformation + { + /// + /// The build number of your app. + /// + public String BuildVersion { get; set; } + + /// + /// The human friendly version number of your happ. + /// + public String DisplayVersion { get; set; } + + /// + /// The operating system version of the platform the SDK is operating in.. + /// + public String OSVersion { get; set; } + + } + + /// + /// The LeanCloud application ID of your app. + /// + public string ApplicationId { get; set; } + + /// + /// LeanCloud C# SDK 支持的服务节点,目前支持华北,华东和北美公有云节点和私有节点,以及专属节点 + /// + public AVRegion Region { get; set; } + + internal int RegionValue + { + get + { + return (int)Region; + } + } + + /// + /// The LeanCloud application key for your app. + /// + public string ApplicationKey { get; set; } + + /// + /// The LeanCloud master key for your app. + /// + /// The master key. + public string MasterKey { get; set; } + + /// + /// Gets or sets additional HTTP headers to be sent with network requests from the SDK. + /// + public IDictionary AdditionalHTTPHeaders { get; set; } + + /// + /// The version information of your application environment. + /// + public VersionInformation VersionInfo { get; set; } + + /// + /// 存储服务器地址 + /// + /// The API server. + public string ApiServer { get; set; } + + /// + /// 云引擎服务器地址 + /// + /// The engine server uri. + public string EngineServer { get; set; } + + /// + /// 即时通信服务器地址 + /// + /// The RTMR outer. + public string RTMServer { get; set; } + + /// + /// 直连即时通信服务器地址 + /// + /// The realtime server. + public string RealtimeServer { get; set; } + + public Uri PushServer { get; set; } + + public Uri StatsServer { get; set; } + } + + private static readonly object mutex = new object(); + + static AVClient() + { + versionString = "net-portable-" + Version; + + //AVModuleController.Instance.ScanForModules(); + } + + /// + /// The current configuration that LeanCloud has been initialized with. + /// + public static Configuration CurrentConfiguration { get; internal set; } + + internal static Version Version + { + get + { + var assemblyName = new AssemblyName(typeof(AVClient).GetTypeInfo().Assembly.FullName); + return assemblyName.Version; + } + } + + private static readonly string versionString; + /// + /// 当前 SDK 版本号 + /// + public static string VersionString + { + get + { + return versionString; + } + } + + /// + /// Authenticates this client as belonging to your application. This must be + /// called before your application can use the LeanCloud library. The recommended + /// way is to put a call to AVClient.Initialize in your + /// Application startup. + /// + /// The Application ID provided in the LeanCloud dashboard. + /// + /// The .NET API Key provided in the LeanCloud dashboard. + /// + public static void Initialize(string applicationId, string applicationKey) + { + Initialize(new Configuration + { + ApplicationId = applicationId, + ApplicationKey = applicationKey + }); + } + + internal static Action LogTracker { get; private set; } + + /// + /// 启动日志打印 + /// + /// + public static void HttpLog(Action trace) + { + LogTracker = trace; + } + /// + /// 打印 HTTP 访问日志 + /// + /// + public static void PrintLog(string log) + { + if (AVClient.LogTracker != null) + { + AVClient.LogTracker(log); + } + } + + static bool useProduction = true; + /// + /// Gets or sets a value indicating whether send the request to production server or staging server. + /// + /// true if use production; otherwise, false. + public static bool UseProduction + { + get + { + return useProduction; + } + set + { + useProduction = value; + } + } + + static bool useMasterKey = false; + public static bool UseMasterKey + { + get + { + return useMasterKey; + } + set + { + useMasterKey = value; + } + } + + /// + /// Authenticates this client as belonging to your application. This must be + /// called before your application can use the LeanCloud library. The recommended + /// way is to put a call to AVClient.Initialize in your + /// Application startup. + /// + /// The configuration to initialize LeanCloud with. + /// + public static void Initialize(Configuration configuration) + { + Config(configuration); + + AVObject.RegisterSubclass(); + AVObject.RegisterSubclass(); + AVObject.RegisterSubclass(); + } + + internal static void Config(Configuration configuration) + { + lock (mutex) + { + var nodeHash = configuration.ApplicationId.Split('-'); + if (nodeHash.Length > 1) + { + if (nodeHash[1].Trim() == "9Nh9j0Va") + { + configuration.Region = Configuration.AVRegion.Public_East_China; + } + } + + CurrentConfiguration = configuration; + } + } + + internal static void Clear() + { + AVPlugins.Instance.AppRouterController.Clear(); + AVPlugins.Instance.Reset(); + AVUser.ClearInMemoryUser(); + } + + /// + /// Switch app. + /// + /// Configuration. + public static void Switch(Configuration configuration) + { + Clear(); + Initialize(configuration); + } + + public static void Switch(string applicationId, string applicationKey, Configuration.AVRegion region = Configuration.AVRegion.Public_North_China) + { + var configuration = new Configuration + { + ApplicationId = applicationId, + ApplicationKey = applicationKey, + Region = region + }; + Switch(configuration); + } + + public static string BuildQueryString(IDictionary parameters) + { + return string.Join("&", (from pair in parameters + let valueString = pair.Value as string + select string.Format("{0}={1}", + Uri.EscapeDataString(pair.Key), + Uri.EscapeDataString(string.IsNullOrEmpty(valueString) ? + Json.Encode(pair.Value) : valueString))) + .ToArray()); + } + + internal static IDictionary DecodeQueryString(string queryString) + { + var dict = new Dictionary(); + foreach (var pair in queryString.Split('&')) + { + var parts = pair.Split(new char[] { '=' }, 2); + dict[parts[0]] = parts.Length == 2 ? Uri.UnescapeDataString(parts[1].Replace("+", " ")) : null; + } + return dict; + } + + internal static IDictionary DeserializeJsonString(string jsonData) + { + return Json.Parse(jsonData) as IDictionary; + } + + internal static string SerializeJsonString(IDictionary jsonData) + { + return Json.Encode(jsonData); + } + + public static Task> HttpGetAsync(Uri uri) + { + return RequestAsync(uri, "GET", null, body: null, contentType: null, cancellationToken: CancellationToken.None); + } + + public static Task> RequestAsync(Uri uri, string method, IList> headers, IDictionary body, string contentType, CancellationToken cancellationToken) + { + var dataStream = body != null ? new MemoryStream(Encoding.UTF8.GetBytes(Json.Encode(body))) : null; + return AVClient.RequestAsync(uri, method, headers, dataStream, contentType, cancellationToken); + //return AVPlugins.Instance.HttpClient.ExecuteAsync(request, null, null, cancellationToken); + } + + public static Task> RequestAsync(Uri uri, string method, IList> headers, Stream data, string contentType, CancellationToken cancellationToken) + { + HttpRequest request = new HttpRequest() + { + Data = data != null ? data : null, + Headers = headers, + Method = method, + Uri = uri + }; + return AVPlugins.Instance.HttpClient.ExecuteAsync(request, null, null, cancellationToken).OnSuccess(t => + { + var response = t.Result; + var contentString = response.Item2; + int responseCode = (int)response.Item1; + var responseLog = responseCode + ";" + contentString; + PrintLog(responseLog); + return response; + }); + } + + internal static Tuple> ReponseResolve(Tuple response, CancellationToken cancellationToken) + { + Tuple result = response; + HttpStatusCode code = result.Item1; + string item2 = result.Item2; + + if (item2 == null) + { + cancellationToken.ThrowIfCancellationRequested(); + return new Tuple>(code, null); + } + IDictionary strs = null; + try + { + strs = (!item2.StartsWith("[", StringComparison.Ordinal) ? AVClient.DeserializeJsonString(item2) : new Dictionary() + { + { "results", Json.Parse(item2) } + }); + } + catch (Exception exception) + { + throw new AVException(AVException.ErrorCode.OtherCause, "Invalid response from server", exception); + } + var codeValue = (int)code; + if (codeValue > 203 || codeValue < 200) + { + throw new AVException((AVException.ErrorCode)((int)((strs.ContainsKey("code") ? (long)strs["code"] : (long)-1))), (strs.ContainsKey("error") ? strs["error"] as string : item2), null); + } + + cancellationToken.ThrowIfCancellationRequested(); + return new Tuple>(code, strs); + } + internal static Task>> RequestAsync(string method, Uri relativeUri, string sessionToken, IDictionary data, CancellationToken cancellationToken) + { + + var command = new AVCommand(relativeUri.ToString(), + method: method, + sessionToken: sessionToken, + data: data); + + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command, cancellationToken: cancellationToken); + } + + internal static Task>> RunCommandAsync(AVCommand command) + { + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command); + } + + internal static bool IsSuccessStatusCode(HttpStatusCode responseStatus) + { + var codeValue = (int)responseStatus; + return (codeValue > 199) && (codeValue < 204); + } + + public static string ToLog(this HttpRequest request) + { + StringBuilder sb = new StringBuilder(); + var start = "===HTTP Request Start==="; + sb.AppendLine(start); + var urlLog = "Url: " + request.Uri; + sb.AppendLine(urlLog); + + var methodLog = "Method: " + request.Method; + sb.AppendLine(methodLog); + + try + { + var headers = request.Headers.ToDictionary(x => x.Key, x => x.Value as object); + var headersLog = "Headers: " + Json.Encode(headers); + sb.AppendLine(headersLog); + } + catch (Exception) + { + + } + + try + { + if (request is AVCommand) + { + var command = (AVCommand)request; + if (command.DataObject != null) + { + var bodyLog = "Body:" + Json.Encode(command.DataObject); + sb.AppendLine(bodyLog); + } + } + else + { + StreamReader reader = new StreamReader(request.Data); + string bodyLog = reader.ReadToEnd(); + sb.AppendLine(bodyLog); + } + } + catch (Exception) + { + + } + + var end = "===HTTP Request End==="; + sb.AppendLine(end); + return sb.ToString(); + } + } +} diff --git a/Storage/Storage/Public/AVCloud.cs b/Storage/Storage/Public/AVCloud.cs new file mode 100644 index 0000000..76b3a13 --- /dev/null +++ b/Storage/Storage/Public/AVCloud.cs @@ -0,0 +1,599 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using LeanCloud.Storage.Internal; + +namespace LeanCloud { + /// + /// The AVCloud class provides methods for interacting with LeanCloud Cloud Functions. + /// + /// + /// For example, this sample code calls the + /// "validateGame" Cloud Function and calls processResponse if the call succeeded + /// and handleError if it failed. + /// + /// + /// var result = + /// await AVCloud.CallFunctionAsync<IDictionary<string, object>>("validateGame", parameters); + /// + /// + public static class AVCloud + { + internal static IAVCloudCodeController CloudCodeController + { + get + { + return AVPlugins.Instance.CloudCodeController; + } + } + + /// + /// Calls a cloud function. + /// + /// The type of data you will receive from the cloud function. This + /// can be an IDictionary, string, IList, AVObject, or any other type supported by + /// AVObject. + /// The cloud function to call. + /// The parameters to send to the cloud function. This + /// dictionary can contain anything that could be passed into a AVObject except for + /// AVObjects themselves. + /// + /// The cancellation token. + /// The result of the cloud call. + public static Task CallFunctionAsync(String name, IDictionary parameters = null, string sesstionToken = null, CancellationToken cancellationToken = default(CancellationToken)) + { + var sessionTokenTask = AVUser.TakeSessionToken(sesstionToken); + + return sessionTokenTask.OnSuccess(s => + { + return CloudCodeController.CallFunctionAsync(name, + parameters, s.Result, + cancellationToken); + + }).Unwrap(); + } + + /// + /// 远程调用云函数,返回结果会反序列化为 . + /// + /// + /// + /// + /// + /// + /// + public static Task RPCFunctionAsync(String name, IDictionary parameters = null, string sesstionToken = null, CancellationToken cancellationToken = default(CancellationToken)) + { + var sessionTokenTask = AVUser.TakeSessionToken(sesstionToken); + + return sessionTokenTask.OnSuccess(s => + { + return CloudCodeController.RPCFunction(name, + parameters, + s.Result, + cancellationToken); + }).Unwrap(); + } + + /// + /// 获取 LeanCloud 服务器的时间 + /// + /// 如果获取失败,将返回 DateTime.MinValue + /// + /// + /// 服务器的时间 + public static Task GetServerDateTimeAsync() + { + var command = new AVCommand(relativeUri: "date", + method: "GET", + sessionToken: null, + data: null); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).ContinueWith(t => + { + DateTime rtn = DateTime.MinValue; + if (AVClient.IsSuccessStatusCode(t.Result.Item1)) + { + var date = AVDecoder.Instance.Decode(t.Result.Item2); + if (date != null) + { + if (date is DateTime) + { + rtn = (DateTime)date; + } + } + } + return rtn; + }); + } + + /// + /// 请求短信认证。 + /// + /// 手机号。 + /// 应用名称。 + /// 进行的操作名称。 + /// 验证码失效时间。 + /// + public static Task RequestSMSCodeAsync(string mobilePhoneNumber, string name, string op, int ttl = 10) + { + return RequestSMSCodeAsync(mobilePhoneNumber, name, op, ttl, CancellationToken.None); + } + + + /// + /// 请求发送验证码。 + /// + /// 是否发送成功。 + /// 手机号。 + /// 应用名称。 + /// 进行的操作名称。 + /// 验证码失效时间。 + /// Cancellation token。 + public static Task RequestSMSCodeAsync(string mobilePhoneNumber, string name, string op, int ttl = 10, CancellationToken cancellationToken = default(CancellationToken)) + { + if (string.IsNullOrEmpty(mobilePhoneNumber)) + { + throw new AVException(AVException.ErrorCode.MobilePhoneInvalid, "Moblie Phone number is invalid.", null); + } + + Dictionary strs = new Dictionary() + { + { "mobilePhoneNumber", mobilePhoneNumber }, + }; + if (!string.IsNullOrEmpty(name)) + { + strs.Add("name", name); + } + if (!string.IsNullOrEmpty(op)) + { + strs.Add("op", op); + } + if (ttl > 0) + { + strs.Add("TTL", ttl); + } + var command = new AVCommand("requestSmsCode", + method: "POST", + sessionToken: null, + data: strs); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + /// + /// 请求发送验证码。 + /// + /// 是否发送成功。 + /// 手机号。 + public static Task RequestSMSCodeAsync(string mobilePhoneNumber) + { + return AVCloud.RequestSMSCodeAsync(mobilePhoneNumber, CancellationToken.None); + } + + + /// + /// 请求发送验证码。 + /// + /// 是否发送成功。 + /// 手机号。 + /// Cancellation Token. + public static Task RequestSMSCodeAsync(string mobilePhoneNumber, CancellationToken cancellationToken) + { + return AVCloud.RequestSMSCodeAsync(mobilePhoneNumber, null, null, 0, cancellationToken); + } + + /// + /// 发送手机短信,并指定模板以及传入模板所需的参数。 + /// Exceptions: + /// AVOSCloud.AVException: + /// 手机号为空。 + /// + /// Sms's template + /// Template variables env. + /// Sms's sign. + /// Cancellation token. + /// + public static Task RequestSMSCodeAsync( + string mobilePhoneNumber, + string template, + IDictionary env, + string sign = "", + string validateToken = "", + CancellationToken cancellationToken = default(CancellationToken)) + { + + if (string.IsNullOrEmpty(mobilePhoneNumber)) + { + throw new AVException(AVException.ErrorCode.MobilePhoneInvalid, "Moblie Phone number is invalid.", null); + } + Dictionary strs = new Dictionary() + { + { "mobilePhoneNumber", mobilePhoneNumber }, + }; + strs.Add("template", template); + if (String.IsNullOrEmpty(sign)) + { + strs.Add("sign", sign); + } + if (String.IsNullOrEmpty(validateToken)) + { + strs.Add("validate_token", validateToken); + } + foreach (var key in env.Keys) + { + strs.Add(key, env[key]); + } + var command = new AVCommand("requestSmsCode", + method: "POST", + sessionToken: null, + data: strs); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + /// + /// + /// + /// + /// + public static Task RequestVoiceCodeAsync(string mobilePhoneNumber) + { + if (string.IsNullOrEmpty(mobilePhoneNumber)) + { + throw new AVException(AVException.ErrorCode.MobilePhoneInvalid, "Moblie Phone number is invalid.", null); + } + Dictionary strs = new Dictionary() + { + { "mobilePhoneNumber", mobilePhoneNumber }, + { "smsType", "voice" }, + { "IDD","+86" } + }; + + var command = new AVCommand("requestSmsCode", + method: "POST", + sessionToken: null, + data: strs); + + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + /// + /// 验证是否是有效短信验证码。 + /// + /// 是否验证通过。 + /// 手机号 + /// 验证码。 + public static Task VerifySmsCodeAsync(string code, string mobilePhoneNumber) + { + return AVCloud.VerifySmsCodeAsync(code, mobilePhoneNumber, CancellationToken.None); + } + + /// + /// 验证是否是有效短信验证码。 + /// + /// 是否验证通过。 + /// 验证码。 + /// 手机号 + /// Cancellation token. + public static Task VerifySmsCodeAsync(string code, string mobilePhoneNumber, CancellationToken cancellationToken) + { + var command = new AVCommand("verifySmsCode/" + code.Trim() + "?mobilePhoneNumber=" + mobilePhoneNumber.Trim(), + method: "POST", + sessionToken: null, + data: null); + + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + /// + /// Stands for a captcha result. + /// + public class Captcha + { + /// + /// Used for captcha verify. + /// + public string Token { internal set; get; } + + /// + /// Captcha image URL. + /// + public string Url { internal set; get; } + + /// + /// Verify the user's input of catpcha. + /// + /// User's input of this captcha. + /// CancellationToken. + /// + public Task VerifyAsync(string code, CancellationToken cancellationToken = default(CancellationToken)) + { + return AVCloud.VerifyCaptchaAsync(code, Token); + } + } + + /// + /// Get a captcha image. + /// + /// captcha image width. + /// captcha image height. + /// CancellationToken. + /// an instance of Captcha. + public static Task RequestCaptchaAsync(int width = 85, int height = 30, CancellationToken cancellationToken = default(CancellationToken)) + { + var path = String.Format("requestCaptcha?width={0}&height={1}", width, height); + var command = new AVCommand(path, method: "GET", sessionToken: null, data: null); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + { + var decoded = AVDecoder.Instance.Decode(t.Result.Item2) as IDictionary; + return new Captcha() + { + Token = decoded["captcha_token"] as string, + Url = decoded["captcha_url"] as string, + }; + }); + } + + /// + /// Verify the user's input of catpcha. + /// + /// The captcha's token, from server. + /// User's input of this captcha. + /// CancellationToken. + /// + public static Task VerifyCaptchaAsync(string code, string token, CancellationToken cancellationToken = default(CancellationToken)) + { + var data = new Dictionary + { + { "captcha_token", token }, + { "captcha_code", code }, + }; + var command = new AVCommand("verifyCaptcha", method: "POST", sessionToken: null, data: data); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).ContinueWith(t => + { + if (!t.Result.Item2.ContainsKey("validate_token")) + throw new KeyNotFoundException("validate_token"); + return t.Result.Item2["validate_token"] as string; + }); + } + + /// + /// Get the custom cloud parameters, you can set them at console https://leancloud.cn/dashboard/devcomponent.html?appid={your_app_Id}#/component/custom_param + /// + /// + /// + public static Task> GetCustomParametersAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + var command = new AVCommand(string.Format("statistics/apps/{0}/sendPolicy", AVClient.CurrentConfiguration.ApplicationId), + method: "GET", + sessionToken: null, + data: null); + + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => + { + var settings = t.Result.Item2; + var CloudParameters = settings["parameters"] as IDictionary; + return CloudParameters; + }); + } + + public class RealtimeSignature + { + public string Nonce { internal set; get; } + public long Timestamp { internal set; get; } + public string ClientId { internal set; get; } + public string Signature { internal set; get; } + } + + public static Task RequestRealtimeSignatureAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + return AVUser.GetCurrentUserAsync(cancellationToken).OnSuccess(t => + { + return RequestRealtimeSignatureAsync(t.Result, cancellationToken); + }).Unwrap(); + } + + public static Task RequestRealtimeSignatureAsync(AVUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + var command = new AVCommand(string.Format("rtm/sign"), + method: "POST", + sessionToken: null, + data: new Dictionary + { + { "session_token", user.SessionToken }, + } + ); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).ContinueWith(t => + { + var body = t.Result.Item2; + return new RealtimeSignature() + { + Nonce = body["nonce"] as string, + Timestamp = (long)body["timestamp"], + ClientId = body["client_id"] as string, + Signature = body["signature"] as string, + }; + }); + } + + /// + /// Gets the LeanEngine hosting URL for current app async. + /// + /// The lean engine hosting URL async. + public static Task GetLeanEngineHostingUrlAsync() + { + return CallFunctionAsync("_internal_extensions_get_domain"); + } + + } + + + /// + /// AVRPCC loud function base. + /// + public class AVRPCCloudFunctionBase + { + /// + /// AVRPCD eserialize. + /// + public delegate R AVRPCDeserialize(IDictionary result); + /// + /// AVRPCS erialize. + /// + public delegate IDictionary AVRPCSerialize

(P parameters); + + public AVRPCCloudFunctionBase() + : this(true) + { + + } + + public AVRPCCloudFunctionBase(bool noneParameters) + { + if (noneParameters) + { + this.Encode = n => + { + return null; + }; + } + } + + + + private AVRPCDeserialize _decode; + public AVRPCDeserialize Decode + { + get + { + return _decode; + } + set + { + _decode = value; + } + } + + + private AVRPCSerialize

_encode; + public AVRPCSerialize

Encode + { + get + { + if (_encode == null) + { + _encode = n => + { + if (n != null) + return Json.Parse(n.ToString()) as IDictionary; + return null; + }; + } + return _encode; + } + set + { + _encode = value; + } + + } + public string FunctionName { get; set; } + + public Task ExecuteAsync(P parameters) + { + return AVUser.GetCurrentAsync().OnSuccess(t => + { + var user = t.Result; + var encodedParameters = Encode(parameters); + var command = new AVCommand( + string.Format("call/{0}", Uri.EscapeUriString(this.FunctionName)), + method: "POST", + sessionToken: user != null ? user.SessionToken : null, + data: encodedParameters); + + return AVClient.RunCommandAsync(command); + + }).Unwrap().OnSuccess(s => + { + var responseBody = s.Result.Item2; + if (!responseBody.ContainsKey("result")) + { + return default(R); + } + + return Decode(responseBody); + }); + + } + } + + + public class AVObjectRPCCloudFunction : AVObjectRPCCloudFunction + { + + } + public class AVObjectListRPCCloudFunction : AVObjectListRPCCloudFunction + { + + } + + public class AVObjectListRPCCloudFunction : AVRPCCloudFunctionBase> where R : AVObject + { + public AVObjectListRPCCloudFunction() + : base(true) + { + this.Decode = this.AVObjectListDeserializer(); + } + + public AVRPCDeserialize> AVObjectListDeserializer() + { + AVRPCDeserialize> del = data => + { + var items = data["result"] as IList; + + return items.Select(item => + { + var state = AVObjectCoder.Instance.Decode(item as IDictionary, AVDecoder.Instance); + return AVObject.FromState(state, state.ClassName); + }).ToList() as IList; + + }; + return del; + } + } + + public class AVObjectRPCCloudFunction : AVRPCCloudFunctionBase where R : AVObject + { + public AVObjectRPCCloudFunction() + : base(true) + { + this.Decode = this.AVObjectDeserializer(); + } + + + public AVRPCDeserialize AVObjectDeserializer() + { + AVRPCDeserialize del = data => + { + var item = data["result"] as object; + var state = AVObjectCoder.Instance.Decode(item as IDictionary, AVDecoder.Instance); + + return AVObject.FromState(state, state.ClassName); + }; + return del; + + } + } +} diff --git a/Storage/Storage/Public/AVConfig.cs b/Storage/Storage/Public/AVConfig.cs new file mode 100644 index 0000000..8e3b041 --- /dev/null +++ b/Storage/Storage/Public/AVConfig.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using LeanCloud.Storage.Internal; +using LeanCloud.Utilities; + +namespace LeanCloud +{ + /// + /// The AVConfig is a representation of the remote configuration object, + /// that enables you to add things like feature gating, a/b testing or simple "Message of the day". + /// + public class AVConfig : IJsonConvertible + { + private IDictionary properties = new Dictionary(); + + /// + /// Gets the latest fetched AVConfig. + /// + /// AVConfig object + public static AVConfig CurrentConfig + { + get + { + Task task = ConfigController.CurrentConfigController.GetCurrentConfigAsync(); + task.Wait(); + return task.Result; + } + } + + internal static void ClearCurrentConfig() + { + ConfigController.CurrentConfigController.ClearCurrentConfigAsync().Wait(); + } + + internal static void ClearCurrentConfigInMemory() + { + ConfigController.CurrentConfigController.ClearCurrentConfigInMemoryAsync().Wait(); + } + + private static IAVConfigController ConfigController + { + get { return AVPlugins.Instance.ConfigController; } + } + + internal AVConfig() + : base() + { + } + + internal AVConfig(IDictionary fetchedConfig) + { + var props = AVDecoder.Instance.Decode(fetchedConfig["params"]) as IDictionary; + properties = props; + } + + /// + /// Retrieves the AVConfig asynchronously from the server. + /// + /// AVConfig object that was fetched + public static Task GetAsync() + { + return GetAsync(CancellationToken.None); + } + + /// + /// Retrieves the AVConfig asynchronously from the server. + /// + /// The cancellation token. + /// AVConfig object that was fetched + public static Task GetAsync(CancellationToken cancellationToken) + { + return ConfigController.FetchConfigAsync(AVUser.CurrentSessionToken, cancellationToken); + } + + /// + /// Gets a value for the key of a particular type. + /// + /// The type to convert the value to. Supported types are + /// AVObject and its descendents, LeanCloud types such as AVRelation and AVGeopoint, + /// primitive types,IList<T>, IDictionary<string, T> and strings. + /// The key of the element to get. + /// The property is retrieved + /// and is not found. + /// The property under this + /// key was found, but of a different type. + public T Get(string key) + { + return Conversion.To(this.properties[key]); + } + + /// + /// Populates result with the value for the key, if possible. + /// + /// The desired type for the value. + /// The key to retrieve a value for. + /// The value for the given key, converted to the + /// requested type, or null if unsuccessful. + /// true if the lookup and conversion succeeded, otherwise false. + public bool TryGetValue(string key, out T result) + { + if (this.properties.ContainsKey(key)) + { + try + { + var temp = Conversion.To(this.properties[key]); + result = temp; + return true; + } + catch (Exception) + { + // Could not convert, do nothing + } + } + result = default(T); + return false; + } + + /// + /// Gets a value on the config. + /// + /// The key for the parameter. + /// The property is + /// retrieved and is not found. + /// The value for the key. + virtual public object this[string key] + { + get + { + return this.properties[key]; + } + } + + IDictionary IJsonConvertible.ToJSON() + { + return new Dictionary { + { "params", NoObjectsEncoder.Instance.Encode(properties) } + }; + } + } +} diff --git a/Storage/Storage/Public/AVDownloadProgressEventArgs.cs b/Storage/Storage/Public/AVDownloadProgressEventArgs.cs new file mode 100644 index 0000000..8858eaa --- /dev/null +++ b/Storage/Storage/Public/AVDownloadProgressEventArgs.cs @@ -0,0 +1,15 @@ +using System; + +namespace LeanCloud { + /// + /// Represents download progress. + /// + public class AVDownloadProgressEventArgs : EventArgs { + public AVDownloadProgressEventArgs() { } + + /// + /// Gets the progress (a number between 0.0 and 1.0) of a download. + /// + public double Progress { get; set; } + } +} diff --git a/Storage/Storage/Public/AVException.cs b/Storage/Storage/Public/AVException.cs new file mode 100644 index 0000000..4fedef0 --- /dev/null +++ b/Storage/Storage/Public/AVException.cs @@ -0,0 +1,275 @@ +using System; + +namespace LeanCloud +{ + /// + /// Exceptions that may occur when sending requests to LeanCloud. + /// + public class AVException : Exception + { + /// + /// Error codes that may be delivered in response to requests to LeanCloud. + /// + public enum ErrorCode + { + /// + /// Error code indicating that an unknown error or an error unrelated to LeanCloud + /// occurred. + /// + OtherCause = -1, + + /// + /// Error code indicating that something has gone wrong with the server. + /// If you get this error code, it is LeanCloud's fault. + /// + InternalServerError = 1, + + /// + /// Error code indicating the connection to the LeanCloud servers failed. + /// + ConnectionFailed = 100, + + /// + /// Error code indicating the specified object doesn't exist. + /// + ObjectNotFound = 101, + + /// + /// Error code indicating you tried to query with a datatype that doesn't + /// support it, like exact matching an array or object. + /// + InvalidQuery = 102, + + /// + /// Error code indicating a missing or invalid classname. Classnames are + /// case-sensitive. They must start with a letter, and a-zA-Z0-9_ are the + /// only valid characters. + /// + InvalidClassName = 103, + + /// + /// Error code indicating an unspecified object id. + /// + MissingObjectId = 104, + + /// + /// Error code indicating an invalid key name. Keys are case-sensitive. They + /// must start with a letter, and a-zA-Z0-9_ are the only valid characters. + /// + InvalidKeyName = 105, + + /// + /// Error code indicating a malformed pointer. You should not see this unless + /// you have been mucking about changing internal LeanCloud code. + /// + InvalidPointer = 106, + + /// + /// Error code indicating that badly formed JSON was received upstream. This + /// either indicates you have done something unusual with modifying how + /// things encode to JSON, or the network is failing badly. + /// + InvalidJSON = 107, + + /// + /// Error code indicating that the feature you tried to access is only + /// available internally for testing purposes. + /// + CommandUnavailable = 108, + + /// + /// You must call LeanCloud.initialize before using the LeanCloud library. + /// + NotInitialized = 109, + + /// + /// Error code indicating that a field was set to an inconsistent type. + /// + IncorrectType = 111, + + /// + /// Error code indicating an invalid channel name. A channel name is either + /// an empty string (the broadcast channel) or contains only a-zA-Z0-9_ + /// characters and starts with a letter. + /// + InvalidChannelName = 112, + + /// + /// Error code indicating that push is misconfigured. + /// + PushMisconfigured = 115, + + /// + /// Error code indicating that the object is too large. + /// + ObjectTooLarge = 116, + + /// + /// Error code indicating that the operation isn't allowed for clients. + /// + OperationForbidden = 119, + + /// + /// Error code indicating the result was not found in the cache. + /// + CacheMiss = 120, + + /// + /// Error code indicating that an invalid key was used in a nested + /// JSONObject. + /// + InvalidNestedKey = 121, + + /// + /// Error code indicating that an invalid filename was used for AVFile. + /// A valid file name contains only a-zA-Z0-9_. characters and is between 1 + /// and 128 characters. + /// + InvalidFileName = 122, + + /// + /// Error code indicating an invalid ACL was provided. + /// + InvalidACL = 123, + + /// + /// Error code indicating that the request timed out on the server. Typically + /// this indicates that the request is too expensive to run. + /// + Timeout = 124, + + /// + /// Error code indicating that the email address was invalid. + /// + InvalidEmailAddress = 125, + + /// + /// Error code indicating that a unique field was given a value that is + /// already taken. + /// + DuplicateValue = 137, + + /// + /// Error code indicating that a role's name is invalid. + /// + InvalidRoleName = 139, + + /// + /// Error code indicating that an application quota was exceeded. Upgrade to + /// resolve. + /// + ExceededQuota = 140, + + /// + /// Error code indicating that a Cloud Code script failed. + /// + ScriptFailed = 141, + + /// + /// Error code indicating that a Cloud Code validation failed. + /// + ValidationFailed = 142, + + /// + /// Error code indicating that deleting a file failed. + /// + FileDeleteFailed = 153, + + /// + /// Error code indicating that the application has exceeded its request limit. + /// + RequestLimitExceeded = 155, + + /// + /// Error code indicating that the provided event name is invalid. + /// + InvalidEventName = 160, + + /// + /// Error code indicating that the username is missing or empty. + /// + UsernameMissing = 200, + + /// + /// Error code indicating that the password is missing or empty. + /// + PasswordMissing = 201, + + /// + /// Error code indicating that the username has already been taken. + /// + UsernameTaken = 202, + + /// + /// Error code indicating that the email has already been taken. + /// + EmailTaken = 203, + + /// + /// Error code indicating that the email is missing, but must be specified. + /// + EmailMissing = 204, + + /// + /// Error code indicating that a user with the specified email was not found. + /// + EmailNotFound = 205, + + /// + /// Error code indicating that a user object without a valid session could + /// not be altered. + /// + SessionMissing = 206, + + /// + /// Error code indicating that a user can only be created through signup. + /// + MustCreateUserThroughSignup = 207, + + /// + /// Error code indicating that an an account being linked is already linked + /// to another user. + /// + AccountAlreadyLinked = 208, + + /// + /// Error code indicating that the current session token is invalid. + /// + InvalidSessionToken = 209, + + /// + /// Error code indicating that a user cannot be linked to an account because + /// that account's id could not be found. + /// + LinkedIdMissing = 250, + + /// + /// Error code indicating that a user with a linked (e.g. Facebook) account + /// has an invalid session. + /// + InvalidLinkedSession = 251, + + /// + /// Error code indicating that a service being linked (e.g. Facebook or + /// Twitter) is unsupported. + /// + UnsupportedService = 252, + + /// + /// 手机号不合法 + /// + MobilePhoneInvalid = 253 + } + + internal AVException(ErrorCode code, string message, Exception cause = null) + : base(message, cause) + { + this.Code = code; + } + + /// + /// The LeanCloud error code associated with the exception. + /// + public ErrorCode Code { get; private set; } + } +} diff --git a/Storage/Storage/Public/AVExtensions.cs b/Storage/Storage/Public/AVExtensions.cs new file mode 100644 index 0000000..973ff5c --- /dev/null +++ b/Storage/Storage/Public/AVExtensions.cs @@ -0,0 +1,160 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using System.Linq; +using LeanCloud.Storage.Internal; + +namespace LeanCloud +{ + /// + /// Provides convenience extension methods for working with collections + /// of AVObjects so that you can easily save and fetch them in batches. + /// + public static class AVExtensions + { + /// + /// Saves all of the AVObjects in the enumeration. Equivalent to + /// calling . + /// + /// The objects to save. + public static Task SaveAllAsync(this IEnumerable objects) where T : AVObject + { + return AVObject.SaveAllAsync(objects); + } + + /// + /// Saves all of the AVObjects in the enumeration. Equivalent to + /// calling + /// . + /// + /// The objects to save. + /// The cancellation token. + public static Task SaveAllAsync( + this IEnumerable objects, CancellationToken cancellationToken) where T : AVObject + { + return AVObject.SaveAllAsync(objects, cancellationToken); + } + + /// + /// Fetches all of the objects in the enumeration. Equivalent to + /// calling . + /// + /// The objects to save. + public static Task> FetchAllAsync(this IEnumerable objects) + where T : AVObject + { + return AVObject.FetchAllAsync(objects); + } + + /// + /// Fetches all of the objects in the enumeration. Equivalent to + /// calling + /// . + /// + /// The objects to fetch. + /// The cancellation token. + public static Task> FetchAllAsync( + this IEnumerable objects, CancellationToken cancellationToken) + where T : AVObject + { + return AVObject.FetchAllAsync(objects, cancellationToken); + } + + /// + /// Fetches all of the objects in the enumeration that don't already have + /// data. Equivalent to calling + /// . + /// + /// The objects to fetch. + public static Task> FetchAllIfNeededAsync( + this IEnumerable objects) + where T : AVObject + { + return AVObject.FetchAllIfNeededAsync(objects); + } + + /// + /// Fetches all of the objects in the enumeration that don't already have + /// data. Equivalent to calling + /// . + /// + /// The objects to fetch. + /// The cancellation token. + public static Task> FetchAllIfNeededAsync( + this IEnumerable objects, CancellationToken cancellationToken) + where T : AVObject + { + return AVObject.FetchAllIfNeededAsync(objects, cancellationToken); + } + + /// + /// Constructs a query that is the or of the given queries. + /// + /// The type of AVObject being queried. + /// An initial query to 'or' with additional queries. + /// The list of AVQueries to 'or' together. + /// A query that is the or of the given queries. + public static AVQuery Or(this AVQuery source, params AVQuery[] queries) + where T : AVObject + { + return AVQuery.Or(queries.Concat(new[] { source })); + } + + /// + /// Fetches this object with the data from the server. + /// + public static Task FetchAsync(this T obj) where T : AVObject + { + return obj.FetchAsyncInternal(CancellationToken.None).OnSuccess(t => (T)t.Result); + } + + /// + /// Fetches this object with the data from the server. + /// + /// The AVObject to fetch. + /// The cancellation token. + public static Task FetchAsync(this T obj, CancellationToken cancellationToken) + where T : AVObject + { + return FetchAsync(obj, null, cancellationToken); + } + public static Task FetchAsync(this T obj, IEnumerable includeKeys) where T : AVObject + { + return FetchAsync(obj, includeKeys, CancellationToken.None).OnSuccess(t => (T)t.Result); + } + public static Task FetchAsync(this T obj, IEnumerable includeKeys, CancellationToken cancellationToken) + where T : AVObject + { + var queryString = new Dictionary(); + if (includeKeys != null) + { + + var encode = string.Join(",", includeKeys.ToArray()); + queryString.Add("include", encode); + } + return obj.FetchAsyncInternal(queryString, cancellationToken).OnSuccess(t => (T)t.Result); + } + + /// + /// If this AVObject has not been fetched (i.e. returns + /// false), fetches this object with the data from the server. + /// + /// The AVObject to fetch. + public static Task FetchIfNeededAsync(this T obj) where T : AVObject + { + return obj.FetchIfNeededAsyncInternal(CancellationToken.None).OnSuccess(t => (T)t.Result); + } + + /// + /// If this AVObject has not been fetched (i.e. returns + /// false), fetches this object with the data from the server. + /// + /// The AVObject to fetch. + /// The cancellation token. + public static Task FetchIfNeededAsync(this T obj, CancellationToken cancellationToken) + where T : AVObject + { + return obj.FetchIfNeededAsyncInternal(cancellationToken).OnSuccess(t => (T)t.Result); + } + } +} diff --git a/Storage/Storage/Public/AVFieldNameAttribute.cs b/Storage/Storage/Public/AVFieldNameAttribute.cs new file mode 100644 index 0000000..8ee91a7 --- /dev/null +++ b/Storage/Storage/Public/AVFieldNameAttribute.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud +{ + /// + /// Specifies a field name for a property on a AVObject subclass. + /// + [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)] + public sealed class AVFieldNameAttribute : Attribute + { + /// + /// Constructs a new AVFieldName attribute. + /// + /// The name of the field on the AVObject that the + /// property represents. + public AVFieldNameAttribute(string fieldName) + { + FieldName = fieldName; + } + + /// + /// Gets the name of the field represented by this property. + /// + public string FieldName { get; private set; } + } +} diff --git a/Storage/Storage/Public/AVFile.cs b/Storage/Storage/Public/AVFile.cs new file mode 100644 index 0000000..6d05995 --- /dev/null +++ b/Storage/Storage/Public/AVFile.cs @@ -0,0 +1,728 @@ +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud +{ + /// + /// AVFile is a local representation of a file that is saved to the LeanCloud. + /// + /// + /// The workflow is to construct a with data and a filename, + /// then save it and set it as a field on a AVObject: + /// + /// + /// var file = new AVFile("hello.txt", + /// new MemoryStream(Encoding.UTF8.GetBytes("hello"))); + /// await file.SaveAsync(); + /// var obj = new AVObject("TestObject"); + /// obj["file"] = file; + /// await obj.SaveAsync(); + /// + /// + public partial class AVFile : IJsonConvertible + { + internal static int objectCounter = 0; + internal static readonly object Mutex = new object(); + private FileState state; + private readonly Stream dataStream; + private readonly TaskQueue taskQueue = new TaskQueue(); + + #region Constructor + /// + /// 通过文件名,数据流,文件类型,文件源信息构建一个 AVFile + /// + /// 文件名 + /// 数据流 + /// 文件类型 + /// 文件源信息 + public AVFile(string name, Stream data, string mimeType = null, IDictionary metaData = null) + { + mimeType = mimeType == null ? GetMIMEType(name) : mimeType; + state = new FileState + { + Name = name, + MimeType = mimeType, + MetaData = metaData + }; + this.dataStream = data; + lock (Mutex) + { + objectCounter++; + state.counter = objectCounter; + } + } + + /// + /// 根据文件名,文件 Byte 数组,以及文件类型构建 AVFile + /// + /// 文件名 + /// 文件 Byte 数组 + /// 文件类型 + public AVFile(string name, byte[] data, string mimeType = null) + : this(name, new MemoryStream(data), mimeType) { } + + /// + /// 根据文件名,文件流数据,文件类型构建 AVFile + /// + /// 文件名 + /// 文件流数据 + /// 文件类型 + public AVFile(string name, Stream data, string mimeType = null) + : this(name, data, mimeType, new Dictionary()) + { + } + + /// + /// 根据 byte 数组以及文件名创建文件 + /// + /// 文件名 + /// 文件的 byte[] 数据 + public AVFile(string name, byte[] data) + : this(name, new MemoryStream(data), new Dictionary()) + { + + } + + /// + /// 根据文件名,数据 byte[] 数组以及元数据创建文件 + /// + /// 文件名 + /// 文件的 byte[] 数据 + /// 元数据 + public AVFile(string name, byte[] data, IDictionary metaData) + : this(name, new MemoryStream(data), metaData) + { + + } + + /// + /// 根据文件名,数据流以及元数据创建文件 + /// + /// 文件名 + /// 文件的数据流 + /// 元数据 + public AVFile(string name, Stream data, IDictionary metaData) + : this(name, data, GetMIMEType(name), metaData) + { + } + + /// + /// 根据文件名,数据流以及元数据创建文件 + /// + /// 文件名 + /// 文件的数据流 + public AVFile(string name, Stream data) + : this(name, data, new Dictionary()) + { + + } + + #region created by url or uri + /// + /// 根据文件名,Uri,文件类型以及文件源信息 + /// + /// 文件名 + /// 文件Uri + /// 文件类型 + /// 文件源信息 + public AVFile(string name, Uri uri, string mimeType = null, IDictionary metaData = null) + { + mimeType = mimeType == null ? GetMIMEType(name) : mimeType; + state = new FileState + { + Name = name, + Url = uri, + MetaData = metaData, + MimeType = mimeType + }; + lock (Mutex) + { + objectCounter++; + state.counter = objectCounter; + } + this.isExternal = true; + } + + /// + /// 根据文件名,文件 Url,文件类型,文件源信息构建 AVFile + /// + /// 文件名 + /// 文件 Url + /// 文件类型 + /// 文件源信息 + public AVFile(string name, string url, string mimeType = null, IDictionary metaData = null) + : this(name, new Uri(url), mimeType, metaData) + { + + } + + /// + /// 根据文件名,文件 Url以及文件的源信息构建 AVFile + /// + /// 文件名 + /// 文件 Url + /// 文件源信息 + public AVFile(string name, string url, IDictionary metaData) + : this(name, url, null, metaData) + { + } + + /// + /// 根据文件名,文件 Uri,以及文件类型构建 AVFile + /// + /// 文件名 + /// 文件 Uri + /// 文件类型 + public AVFile(string name, Uri uri, string mimeType = null) + : this(name, uri, mimeType, new Dictionary()) + { + + } + + /// + /// 根据文件名以及文件 Uri 构建 AVFile + /// + /// 文件名 + /// 文件 Uri + public AVFile(string name, Uri uri) + : this(name, uri, null, new Dictionary()) + { + + } + /// + /// 根据文件名和 Url 创建文件 + /// + /// 文件名 + /// 文件的 Url + public AVFile(string name, string url) + : this(name, new Uri(url)) + { + } + + internal AVFile(FileState filestate) + { + this.state = filestate; + } + internal AVFile(string objectId) + : this(new FileState() + { + ObjectId = objectId + }) + { + + } + #endregion + + #endregion + + #region Properties + + /// + /// Gets whether the file still needs to be saved. + /// + public bool IsDirty + { + get + { + return state.Url == null; + } + } + + /// + /// Gets the name of the file. Before save is called, this is the filename given by + /// the user. After save is called, that name gets prefixed with a unique identifier. + /// + [AVFieldName("name")] + public string Name + { + get + { + return state.Name; + } + } + + /// + /// Gets the MIME type of the file. This is either passed in to the constructor or + /// inferred from the file extension. "unknown/unknown" will be used if neither is + /// available. + /// + public string MimeType + { + get + { + return state.MimeType; + } + } + + /// + /// Gets the url of the file. It is only available after you save the file or after + /// you get the file from a . + /// + [AVFieldName("url")] + public Uri Url + { + get + { + return state.Url; + } + } + + internal static IAVFileController FileController + { + get + { + return AVPlugins.Instance.FileController; + } + } + + #endregion + + IDictionary IJsonConvertible.ToJSON() + { + if (this.IsDirty) + { + throw new InvalidOperationException( + "AVFile must be saved before it can be serialized."); + } + return new Dictionary { + {"__type", "File"}, + { "id", ObjectId }, + {"name", Name}, + {"url", Url.AbsoluteUri} + }; + } + + #region Save + + /// + /// Saves the file to the LeanCloud cloud. + /// + public Task SaveAsync() + { + return SaveAsync(null, CancellationToken.None); + } + + /// + /// Saves the file to the LeanCloud cloud. + /// + /// The cancellation token. + public Task SaveAsync(CancellationToken cancellationToken) + { + return SaveAsync(null, cancellationToken); + } + + /// + /// Saves the file to the LeanCloud cloud. + /// + /// The progress callback. + public Task SaveAsync(IProgress progress) + { + return SaveAsync(progress, CancellationToken.None); + } + + /// + /// Saves the file to the LeanCloud cloud. + /// + /// The progress callback. + /// The cancellation token. + public Task SaveAsync(IProgress progress, + CancellationToken cancellationToken) + { + if (this.isExternal) + return this.SaveExternal(); + + return taskQueue.Enqueue( + toAwait => FileController.SaveAsync(state, dataStream, AVUser.CurrentSessionToken, progress, cancellationToken), cancellationToken) + .OnSuccess(t => + { + state = t.Result; + }); + } + + internal Task SaveExternal() + { + Dictionary strs = new Dictionary() + { + { "url", this.Url.ToString() }, + { "name",this.Name }, + { "mime_type",this.MimeType}, + { "metaData",this.MetaData} + }; + AVCommand cmd = null; + + if (!string.IsNullOrEmpty(this.ObjectId)) + { + cmd = new AVCommand("files/" + this.ObjectId, + method: "PUT", sessionToken: AVUser.CurrentSessionToken, data: strs); + } + else + { + cmd = new AVCommand("files", method: "POST", sessionToken: AVUser.CurrentSessionToken, data: strs); + } + + return AVPlugins.Instance.CommandRunner.RunCommandAsync(cmd).ContinueWith(t => + { + var result = t.Result.Item2; + this.state.ObjectId = result["objectId"].ToString(); + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + #endregion + + #region Compatible + private readonly static Dictionary MIMETypesDictionary; + private object mutex = new object(); + private bool isExternal; + /// + /// 文件在 LeanCloud 的唯一Id 标识 + /// + public string ObjectId + { + get + { + String str; + lock (this.mutex) + { + str = state.ObjectId; + } + return str; + } + } + + /// + /// 文件的元数据。 + /// + public IDictionary MetaData + { + get + { + return state.MetaData; + } + } + + /// + /// 文件是否为外链文件。 + /// + /// + /// + public bool IsExternal + { + get + { + return isExternal; + } + } + + static AVFile() + { + Dictionary strs = new Dictionary() + { + { "ai", "application/postscript" }, + { "aif", "audio/x-aiff" }, + { "aifc", "audio/x-aiff" }, + { "aiff", "audio/x-aiff" }, + { "asc", "text/plain" }, + { "atom", "application/atom+xml" }, + { "au", "audio/basic" }, + { "avi", "video/x-msvideo" }, + { "bcpio", "application/x-bcpio" }, + { "bin", "application/octet-stream" }, + { "bmp", "image/bmp" }, + { "cdf", "application/x-netcdf" }, + { "cgm", "image/cgm" }, + { "class", "application/octet-stream" }, + { "cpio", "application/x-cpio" }, + { "cpt", "application/mac-compactpro" }, + { "csh", "application/x-csh" }, + { "css", "text/css" }, + { "dcr", "application/x-director" }, + { "dif", "video/x-dv" }, + { "dir", "application/x-director" }, + { "djv", "image/vnd.djvu" }, + { "djvu", "image/vnd.djvu" }, + { "dll", "application/octet-stream" }, + { "dmg", "application/octet-stream" }, + { "dms", "application/octet-stream" }, + { "doc", "application/msword" }, + { "docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }, + { "dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template" }, + { "docm", "application/vnd.ms-word.document.macroEnabled.12" }, + { "dotm", "application/vnd.ms-word.template.macroEnabled.12" }, + { "dtd", "application/xml-dtd" }, + { "dv", "video/x-dv" }, + { "dvi", "application/x-dvi" }, + { "dxr", "application/x-director" }, + { "eps", "application/postscript" }, + { "etx", "text/x-setext" }, + { "exe", "application/octet-stream" }, + { "ez", "application/andrew-inset" }, + { "gif", "image/gif" }, + { "gram", "application/srgs" }, + { "grxml", "application/srgs+xml" }, + { "gtar", "application/x-gtar" }, + { "hdf", "application/x-hdf" }, + { "hqx", "application/mac-binhex40" }, + { "htm", "text/html" }, + { "html", "text/html" }, + { "ice", "x-conference/x-cooltalk" }, + { "ico", "image/x-icon" }, + { "ics", "text/calendar" }, + { "ief", "image/ief" }, + { "ifb", "text/calendar" }, + { "iges", "model/iges" }, + { "igs", "model/iges" }, + { "jnlp", "application/x-java-jnlp-file" }, + { "jp2", "image/jp2" }, + { "jpe", "image/jpeg" }, + { "jpeg", "image/jpeg" }, + { "jpg", "image/jpeg" }, + { "js", "application/x-javascript" }, + { "kar", "audio/midi" }, + { "latex", "application/x-latex" }, + { "lha", "application/octet-stream" }, + { "lzh", "application/octet-stream" }, + { "m3u", "audio/x-mpegurl" }, + { "m4a", "audio/mp4a-latm" }, + { "m4b", "audio/mp4a-latm" }, + { "m4p", "audio/mp4a-latm" }, + { "m4u", "video/vnd.mpegurl" }, + { "m4v", "video/x-m4v" }, + { "mac", "image/x-macpaint" }, + { "man", "application/x-troff-man" }, + { "mathml", "application/mathml+xml" }, + { "me", "application/x-troff-me" }, + { "mesh", "model/mesh" }, + { "mid", "audio/midi" }, + { "midi", "audio/midi" }, + { "mif", "application/vnd.mif" }, + { "mov", "video/quicktime" }, + { "movie", "video/x-sgi-movie" }, + { "mp2", "audio/mpeg" }, + { "mp3", "audio/mpeg" }, + { "mp4", "video/mp4" }, + { "mpe", "video/mpeg" }, + { "mpeg", "video/mpeg" }, + { "mpg", "video/mpeg" }, + { "mpga", "audio/mpeg" }, + { "ms", "application/x-troff-ms" }, + { "msh", "model/mesh" }, + { "mxu", "video/vnd.mpegurl" }, + { "nc", "application/x-netcdf" }, + { "oda", "application/oda" }, + { "ogg", "application/ogg" }, + { "pbm", "image/x-portable-bitmap" }, + { "pct", "image/pict" }, + { "pdb", "chemical/x-pdb" }, + { "pdf", "application/pdf" }, + { "pgm", "image/x-portable-graymap" }, + { "pgn", "application/x-chess-pgn" }, + { "pic", "image/pict" }, + { "pict", "image/pict" }, + { "png", "image/png" }, + { "pnm", "image/x-portable-anymap" }, + { "pnt", "image/x-macpaint" }, + { "pntg", "image/x-macpaint" }, + { "ppm", "image/x-portable-pixmap" }, + { "ppt", "application/vnd.ms-powerpoint" }, + { "pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" }, + { "potx", "application/vnd.openxmlformats-officedocument.presentationml.template" }, + { "ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow" }, + { "ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12" }, + { "pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12" }, + { "potm", "application/vnd.ms-powerpoint.template.macroEnabled.12" }, + { "ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12" }, + { "ps", "application/postscript" }, + { "qt", "video/quicktime" }, + { "qti", "image/x-quicktime" }, + { "qtif", "image/x-quicktime" }, + { "ra", "audio/x-pn-realaudio" }, + { "ram", "audio/x-pn-realaudio" }, + { "ras", "image/x-cmu-raster" }, + { "rdf", "application/rdf+xml" }, + { "rgb", "image/x-rgb" }, + { "rm", "application/vnd.rn-realmedia" }, + { "roff", "application/x-troff" }, + { "rtf", "text/rtf" }, + { "rtx", "text/richtext" }, + { "sgm", "text/sgml" }, + { "sgml", "text/sgml" }, + { "sh", "application/x-sh" }, + { "shar", "application/x-shar" }, + { "silo", "model/mesh" }, + { "sit", "application/x-stuffit" }, + { "skd", "application/x-koan" }, + { "skm", "application/x-koan" }, + { "skp", "application/x-koan" }, + { "skt", "application/x-koan" }, + { "smi", "application/smil" }, + { "smil", "application/smil" }, + { "snd", "audio/basic" }, + { "so", "application/octet-stream" }, + { "spl", "application/x-futuresplash" }, + { "src", "application/x-wais-Source" }, + { "sv4cpio", "application/x-sv4cpio" }, + { "sv4crc", "application/x-sv4crc" }, + { "svg", "image/svg+xml" }, + { "swf", "application/x-shockwave-flash" }, + { "t", "application/x-troff" }, + { "tar", "application/x-tar" }, + { "tcl", "application/x-tcl" }, + { "tex", "application/x-tex" }, + { "texi", "application/x-texinfo" }, + { "texinfo", "application/x-texinfo" }, + { "tif", "image/tiff" }, + { "tiff", "image/tiff" }, + { "tr", "application/x-troff" }, + { "tsv", "text/tab-separated-values" }, + { "txt", "text/plain" }, + { "ustar", "application/x-ustar" }, + { "vcd", "application/x-cdlink" }, + { "vrml", "model/vrml" }, + { "vxml", "application/voicexml+xml" }, + { "wav", "audio/x-wav" }, + { "wbmp", "image/vnd.wap.wbmp" }, + { "wbmxl", "application/vnd.wap.wbxml" }, + { "wml", "text/vnd.wap.wml" }, + { "wmlc", "application/vnd.wap.wmlc" }, + { "wmls", "text/vnd.wap.wmlscript" }, + { "wmlsc", "application/vnd.wap.wmlscriptc" }, + { "wrl", "model/vrml" }, + { "xbm", "image/x-xbitmap" }, + { "xht", "application/xhtml+xml" }, + { "xhtml", "application/xhtml+xml" }, + { "xls", "application/vnd.ms-excel" }, + { "xml", "application/xml" }, + { "xpm", "image/x-xpixmap" }, + { "xsl", "application/xml" }, + { "xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }, + { "xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template" }, + { "xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12" }, + { "xltm", "application/vnd.ms-excel.template.macroEnabled.12" }, + { "xlam", "application/vnd.ms-excel.addin.macroEnabled.12" }, + { "xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12" }, + { "xslt", "application/xslt+xml" }, + { "xul", "application/vnd.mozilla.xul+xml" }, + { "xwd", "image/x-xwindowdump" }, + { "xyz", "chemical/x-xyz" }, + { "zip", "application/zip" }, + }; + AVFile.MIMETypesDictionary = strs; + } + internal static string GetMIMEType(string fileName) + { + try + { + string str = Path.GetExtension(fileName).Remove(0, 1); + if (!AVFile.MIMETypesDictionary.ContainsKey(str)) + { + return "unknown/unknown"; + } + return AVFile.MIMETypesDictionary[str]; + } + catch + { + return "unknown/unknown"; + } + } + + /// + /// 根据 ObjectId 获取文件 + /// + /// 获取之后并没有实际执行下载,只是加载了文件的元信息以及物理地址(Url) + /// + public static Task GetFileWithObjectIdAsync(string objectId, CancellationToken cancellationToken) + { + string currentSessionToken = AVUser.CurrentSessionToken; + return FileController.GetAsync(objectId, currentSessionToken, cancellationToken).OnSuccess(_ => + { + var filestate = _.Result; + return new AVFile(filestate); + }); + } + + public static AVFile CreateWithoutData(string objectId) + { + return new AVFile(objectId); + } + + public static AVFile CreateWithState(FileState state) + { + return new AVFile(state); + } + + public static AVFile CreateWithData(string objectId,string name, string url,IDictionary metaData) + { + var fileState = new FileState(); + fileState.Name = name; + fileState.ObjectId = objectId; + fileState.Url = new Uri(url); + fileState.MetaData = metaData; + return CreateWithState(fileState); + } + /// + /// 根据 ObjectId 获取文件 + /// + /// 获取之后并没有实际执行下载,只是加载了文件的元信息以及物理地址(Url) + /// + public static Task GetFileWithObjectIdAsync(string objectId) + { + return GetFileWithObjectIdAsync(objectId, CancellationToken.None); + } + + internal void MergeFromJSON(IDictionary jsonData) + { + lock (this.mutex) + { + state.ObjectId = jsonData["objectId"] as string; + state.Url = new Uri(jsonData["url"] as string, UriKind.Absolute); + if (jsonData.ContainsKey("name")) + { + state.Name = jsonData["name"] as string; + } + if (jsonData.ContainsKey("metaData")) + { + state.MetaData = jsonData["metaData"] as Dictionary; + } + + } + } + + /// + /// 删除文件 + /// + /// Task + public Task DeleteAsync() + { + return DeleteAsync(CancellationToken.None); + } + internal Task DeleteAsync(CancellationToken cancellationToken) + { + return taskQueue.Enqueue(toAwait => DeleteAsync(toAwait, cancellationToken), + cancellationToken); + + } + internal Task DeleteAsync(Task toAwait, CancellationToken cancellationToken) + { + if (ObjectId == null) + { + return Task.FromResult(0); + } + + string sessionToken = AVUser.CurrentSessionToken; + + return toAwait.OnSuccess(_ => + { + return FileController.DeleteAsync(state, sessionToken, cancellationToken); + }).Unwrap().OnSuccess(_ => { }); + } + #endregion + } + + +} diff --git a/Storage/Storage/Public/AVGeoDistance.cs b/Storage/Storage/Public/AVGeoDistance.cs new file mode 100644 index 0000000..e761658 --- /dev/null +++ b/Storage/Storage/Public/AVGeoDistance.cs @@ -0,0 +1,78 @@ +namespace LeanCloud +{ + /// + /// Represents a distance between two AVGeoPoints. + /// + public struct AVGeoDistance + { + private const double EarthMeanRadiusKilometers = 6371.0; + private const double EarthMeanRadiusMiles = 3958.8; + + /// + /// Creates a AVGeoDistance. + /// + /// The distance in radians. + public AVGeoDistance(double radians) + : this() + { + Radians = radians; + } + + /// + /// Gets the distance in radians. + /// + public double Radians { get; private set; } + + /// + /// Gets the distance in miles. + /// + public double Miles + { + get + { + return Radians * EarthMeanRadiusMiles; + } + } + + /// + /// Gets the distance in kilometers. + /// + public double Kilometers + { + get + { + return Radians * EarthMeanRadiusKilometers; + } + } + + /// + /// Gets a AVGeoDistance from a number of miles. + /// + /// The number of miles. + /// A AVGeoDistance for the given number of miles. + public static AVGeoDistance FromMiles(double miles) + { + return new AVGeoDistance(miles / EarthMeanRadiusMiles); + } + + /// + /// Gets a AVGeoDistance from a number of kilometers. + /// + /// The number of kilometers. + /// A AVGeoDistance for the given number of kilometers. + public static AVGeoDistance FromKilometers(double kilometers) + { + return new AVGeoDistance(kilometers / EarthMeanRadiusKilometers); + } + + /// + /// Gets a AVGeoDistance from a number of radians. + /// + /// The number of radians. + /// A AVGeoDistance for the given number of radians. + public static AVGeoDistance FromRadians(double radians) + { + return new AVGeoDistance(radians); + } + } +} diff --git a/Storage/Storage/Public/AVGeoPoint.cs b/Storage/Storage/Public/AVGeoPoint.cs new file mode 100644 index 0000000..7ca0f57 --- /dev/null +++ b/Storage/Storage/Public/AVGeoPoint.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using LeanCloud.Storage.Internal; + +namespace LeanCloud +{ + /// + /// AVGeoPoint represents a latitude / longitude point that may be associated + /// with a key in a AVObject or used as a reference point for geo queries. + /// This allows proximity-based queries on the key. + /// + /// Only one key in a class may contain a GeoPoint. + /// + public struct AVGeoPoint : IJsonConvertible + { + /// + /// Constructs a AVGeoPoint with the specified latitude and longitude. + /// + /// The point's latitude. + /// The point's longitude. + public AVGeoPoint(double latitude, double longitude) + : this() + { + Latitude = latitude; + Longitude = longitude; + } + + private double latitude; + /// + /// Gets or sets the latitude of the GeoPoint. Valid range is [-90, 90]. + /// Extremes should not be used. + /// + public double Latitude + { + get + { + return latitude; + } + set + { + if (value > 90 || value < -90) + { + throw new ArgumentOutOfRangeException("value", + "Latitude must be within the range [-90, 90]"); + } + latitude = value; + } + } + + private double longitude; + /// + /// Gets or sets the longitude. Valid range is [-180, 180]. + /// Extremes should not be used. + /// + public double Longitude + { + get + { + return longitude; + } + set + { + if (value > 180 || value < -180) + { + throw new ArgumentOutOfRangeException("value", + "Longitude must be within the range [-180, 180]"); + } + longitude = value; + } + } + + /// + /// Get the distance in radians between this point and another GeoPoint. This is the smallest angular + /// distance between the two points. + /// + /// GeoPoint describing the other point being measured against. + /// The distance in between the two points. + public AVGeoDistance DistanceTo(AVGeoPoint point) + { + double d2r = Math.PI / 180; // radian conversion factor + double lat1rad = Latitude * d2r; + double long1rad = longitude * d2r; + double lat2rad = point.Latitude * d2r; + double long2rad = point.Longitude * d2r; + double deltaLat = lat1rad - lat2rad; + double deltaLong = long1rad - long2rad; + double sinDeltaLatDiv2 = Math.Sin(deltaLat / 2); + double sinDeltaLongDiv2 = Math.Sin(deltaLong / 2); + // Square of half the straight line chord distance between both points. + // [0.0, 1.0] + double a = sinDeltaLatDiv2 * sinDeltaLatDiv2 + + Math.Cos(lat1rad) * Math.Cos(lat2rad) * sinDeltaLongDiv2 * sinDeltaLongDiv2; + a = Math.Min(1.0, a); + return new AVGeoDistance(2 * Math.Asin(Math.Sqrt(a))); + } + + IDictionary IJsonConvertible.ToJSON() + { + return new Dictionary + { + { "__type", "GeoPoint" }, + { "latitude", Latitude }, + { "longitude", Longitude } + }; + } + } +} diff --git a/Storage/Storage/Public/AVObject.cs b/Storage/Storage/Public/AVObject.cs new file mode 100644 index 0000000..61ec98f --- /dev/null +++ b/Storage/Storage/Public/AVObject.cs @@ -0,0 +1,2100 @@ +using LeanCloud.Storage.Internal; +using LeanCloud.Utilities; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using System.Collections; + +namespace LeanCloud +{ + /// + /// The AVObject is a local representation of data that can be saved and + /// retrieved from the LeanCloud cloud. + /// + /// + /// The basic workflow for creating new data is to construct a new AVObject, + /// use the indexer to fill it with data, and then use SaveAsync() to persist to the + /// database. + /// + /// + /// The basic workflow for accessing existing data is to use a AVQuery + /// to specify which existing data to retrieve. + /// + /// + public class AVObject : IEnumerable>, INotifyPropertyChanged, INotifyPropertyUpdated, INotifyCollectionPropertyUpdated, IAVObject + { + private static readonly string AutoClassName = "_Automatic"; + +#if UNITY + private static readonly bool isCompiledByIL2CPP = AppDomain.CurrentDomain.FriendlyName.Equals("IL2CPP Root Domain"); +#else + private static readonly bool isCompiledByIL2CPP = false; +#endif + + + internal readonly object mutex = new object(); + + private readonly LinkedList> operationSetQueue = + new LinkedList>(); + private readonly IDictionary estimatedData = new Dictionary(); + + private static readonly ThreadLocal isCreatingPointer = new ThreadLocal(() => false); + + private bool hasBeenFetched; + private bool dirty; + internal TaskQueue taskQueue = new TaskQueue(); + + private IObjectState state; + internal void MutateState(Action func) + { + lock (mutex) + { + state = state.MutatedClone(func); + + // Refresh the estimated data. + RebuildEstimatedData(); + } + } + + public IObjectState State + { + get + { + return state; + } + } + + internal static IAVObjectController ObjectController + { + get + { + return AVPlugins.Instance.ObjectController; + } + } + + internal static IObjectSubclassingController SubclassingController + { + get + { + return AVPlugins.Instance.SubclassingController; + } + } + + public static string GetSubClassName() + { + return SubclassingController.GetClassName(typeof(TAVObject)); + } + + #region AVObject Creation + + /// + /// Constructor for use in AVObject subclasses. Subclasses must specify a AVClassName attribute. + /// + protected AVObject() + : this(AutoClassName) + { + } + + /// + /// Constructs a new AVObject with no data in it. A AVObject constructed in this way will + /// not have an ObjectId and will not persist to the database until + /// is called. + /// + /// + /// Class names must be alphanumerical plus underscore, and start with a letter. It is recommended + /// to name classes in CamelCaseLikeThis. + /// + /// The className for this AVObject. + public AVObject(string className) + { + // We use a ThreadLocal rather than passing a parameter so that createWithoutData can do the + // right thing with subclasses. It's ugly and terrible, but it does provide the development + // experience we generally want, so... yeah. Sorry to whomever has to deal with this in the + // future. I pinky-swear we won't make a habit of this -- you believe me, don't you? + var isPointer = isCreatingPointer.Value; + isCreatingPointer.Value = false; + + if (className == null) + { + throw new ArgumentException("You must specify a LeanCloud class name when creating a new AVObject."); + } + if (AutoClassName.Equals(className)) + { + className = SubclassingController.GetClassName(GetType()); + } + // If this is supposed to be created by a factory but wasn't, throw an exception + if (!SubclassingController.IsTypeValid(className, GetType())) + { + throw new ArgumentException( + "You must create this type of AVObject using AVObject.Create() or the proper subclass."); + } + state = new MutableObjectState + { + ClassName = className + }; + OnPropertyChanged("ClassName"); + + operationSetQueue.AddLast(new Dictionary()); + if (!isPointer) + { + hasBeenFetched = true; + IsDirty = true; + SetDefaultValues(); + } + else + { + IsDirty = false; + hasBeenFetched = false; + } + } + + /// + /// Creates a new AVObject based upon a class name. If the class name is a special type (e.g. + /// for ), then the appropriate type of AVObject is returned. + /// + /// The class of object to create. + /// A new AVObject for the given class name. + public static AVObject Create(string className) + { + return SubclassingController.Instantiate(className); + } + + /// + /// Creates a reference to an existing AVObject for use in creating associations between + /// AVObjects. Calling on this object will return + /// false until has been called. + /// No network request will be made. + /// + /// The object's class. + /// The object id for the referenced object. + /// A AVObject without data. + public static AVObject CreateWithoutData(string className, string objectId) + { + isCreatingPointer.Value = true; + try + { + var result = SubclassingController.Instantiate(className); + result.ObjectId = objectId; + result.IsDirty = false; + if (result.IsDirty) + { + throw new InvalidOperationException( + "A AVObject subclass default constructor must not make changes to the object that cause it to be dirty."); + } + return result; + } + finally + { + isCreatingPointer.Value = false; + } + } + + /// + /// Creates a new AVObject based upon a given subclass type. + /// + /// A new AVObject for the given class name. + public static T Create() where T : AVObject + { + return (T)SubclassingController.Instantiate(SubclassingController.GetClassName(typeof(T))); + } + + /// + /// Creates a reference to an existing AVObject for use in creating associations between + /// AVObjects. Calling on this object will return + /// false until has been called. + /// No network request will be made. + /// + /// The object id for the referenced object. + /// A AVObject without data. + public static T CreateWithoutData(string objectId) where T : AVObject + { + return (T)CreateWithoutData(SubclassingController.GetClassName(typeof(T)), objectId); + } + + /// + /// restore a AVObject of subclass instance from IObjectState. + /// + /// IObjectState after encode from Dictionary. + /// The name of the subclass. + public static T FromState(IObjectState state, string defaultClassName) where T : AVObject + { + string className = state.ClassName ?? defaultClassName; + + T obj = (T)CreateWithoutData(className, state.ObjectId); + obj.HandleFetchResult(state); + + return obj; + } + + #endregion + + public static IDictionary GetPropertyMappings(string className) + { + return SubclassingController.GetPropertyMappings(className); + } + + private static string GetFieldForPropertyName(string className, string propertyName) + { + String fieldName = null; + SubclassingController.GetPropertyMappings(className).TryGetValue(propertyName, out fieldName); + return fieldName; + } + + /// + /// Sets the value of a property based upon its associated AVFieldName attribute. + /// + /// The new value. + /// The name of the property. + /// The type for the property. + protected virtual void SetProperty(T value, +#if !UNITY + [CallerMemberName] string propertyName = null +#else + string propertyName +#endif +) + { + this[GetFieldForPropertyName(ClassName, propertyName)] = value; + } + + /// + /// Gets a relation for a property based upon its associated AVFieldName attribute. + /// + /// The AVRelation for the property. + /// The name of the property. + /// The AVObject subclass type of the AVRelation. + protected AVRelation GetRelationProperty( +#if !UNITY +[CallerMemberName] string propertyName = null +#else +string propertyName +#endif +) where T : AVObject + { + return GetRelation(GetFieldForPropertyName(ClassName, propertyName)); + } + + /// + /// Gets the value of a property based upon its associated AVFieldName attribute. + /// + /// The value of the property. + /// The name of the property. + /// The return type of the property. + protected virtual T GetProperty( +#if !UNITY +[CallerMemberName] string propertyName = null +#else +string propertyName +#endif +) + { + return GetProperty(default(T), propertyName); + } + + /// + /// Gets the value of a property based upon its associated AVFieldName attribute. + /// + /// The value of the property. + /// The value to return if the property is not present on the AVObject. + /// The name of the property. + /// The return type of the property. + protected virtual T GetProperty(T defaultValue, +#if !UNITY + [CallerMemberName] string propertyName = null +#else + string propertyName +#endif +) + { + T result; + if (TryGetValue(GetFieldForPropertyName(ClassName, propertyName), out result)) + { + return result; + } + return defaultValue; + } + + /// + /// Allows subclasses to set values for non-pointer construction. + /// + internal virtual void SetDefaultValues() + { + } + + /// + /// Registers a custom subclass type with the LeanCloud SDK, enabling strong-typing of those AVObjects whenever + /// they appear. Subclasses must specify the AVClassName attribute, have a default constructor, and properties + /// backed by AVObject fields should have AVFieldName attributes supplied. + /// + /// The AVObject subclass type to register. + public static void RegisterSubclass() where T : AVObject, new() + { + SubclassingController.RegisterSubclass(typeof(T)); + } + + internal static void UnregisterSubclass() where T : AVObject, new() + { + SubclassingController.UnregisterSubclass(typeof(T)); + } + + /// + /// Clears any changes to this object made since the last call to . + /// + public void Revert() + { + lock (mutex) + { + bool wasDirty = CurrentOperations.Count > 0; + if (wasDirty) + { + CurrentOperations.Clear(); + RebuildEstimatedData(); + OnPropertyChanged("IsDirty"); + } + } + } + + internal virtual void HandleFetchResult(IObjectState serverState) + { + lock (mutex) + { + MergeFromServer(serverState); + } + } + + internal void HandleFailedSave( + IDictionary operationsBeforeSave) + { + lock (mutex) + { + var opNode = operationSetQueue.Find(operationsBeforeSave); + var nextOperations = opNode.Next.Value; + bool wasDirty = nextOperations.Count > 0; + operationSetQueue.Remove(opNode); + // Merge the data from the failed save into the next save. + foreach (var pair in operationsBeforeSave) + { + var operation1 = pair.Value; + IAVFieldOperation operation2 = null; + nextOperations.TryGetValue(pair.Key, out operation2); + if (operation2 != null) + { + operation2 = operation2.MergeWithPrevious(operation1); + } + else + { + operation2 = operation1; + } + nextOperations[pair.Key] = operation2; + } + if (!wasDirty && nextOperations == CurrentOperations && operationsBeforeSave.Count > 0) + { + OnPropertyChanged("IsDirty"); + } + } + } + + internal virtual void HandleSave(IObjectState serverState) + { + lock (mutex) + { + var operationsBeforeSave = operationSetQueue.First.Value; + operationSetQueue.RemoveFirst(); + + // Merge the data from the save and the data from the server into serverData. + //MutateState(mutableClone => + //{ + // mutableClone.Apply(operationsBeforeSave); + //}); + state = state.MutatedClone((objectState) => objectState.Apply(operationsBeforeSave)); + MergeFromServer(serverState); + } + } + + public virtual void MergeFromServer(IObjectState serverState) + { + // Make a new serverData with fetched values. + var newServerData = serverState.ToDictionary(t => t.Key, t => t.Value); + + lock (mutex) + { + // Trigger handler based on serverState + if (serverState.ObjectId != null) + { + // If the objectId is being merged in, consider this object to be fetched. + hasBeenFetched = true; + OnPropertyChanged("IsDataAvailable"); + } + + if (serverState.UpdatedAt != null) + { + OnPropertyChanged("UpdatedAt"); + } + + if (serverState.CreatedAt != null) + { + OnPropertyChanged("CreatedAt"); + } + + // We cache the fetched object because subsequent Save operation might flush + // the fetched objects into Pointers. + IDictionary fetchedObject = CollectFetchedObjects(); + + foreach (var pair in serverState) + { + var value = pair.Value; + if (value is AVObject) + { + // Resolve fetched object. + var avObject = value as AVObject; + if (fetchedObject.ContainsKey(avObject.ObjectId)) + { + value = fetchedObject[avObject.ObjectId]; + } + } + newServerData[pair.Key] = value; + } + + IsDirty = false; + serverState = serverState.MutatedClone(mutableClone => + { + mutableClone.ServerData = newServerData; + }); + MutateState(mutableClone => + { + mutableClone.Apply(serverState); + }); + } + } + + internal void MergeFromObject(AVObject other) + { + lock (mutex) + { + // If they point to the same instance, we don't need to merge + if (this == other) + { + return; + } + } + + // Clear out any changes on this object. + if (operationSetQueue.Count != 1) + { + throw new InvalidOperationException("Attempt to MergeFromObject during save."); + } + operationSetQueue.Clear(); + foreach (var operationSet in other.operationSetQueue) + { + operationSetQueue.AddLast(operationSet.ToDictionary(entry => entry.Key, + entry => entry.Value)); + } + + lock (mutex) + { + state = other.State; + } + RebuildEstimatedData(); + } + + private bool HasDirtyChildren + { + get + { + lock (mutex) + { + return FindUnsavedChildren().FirstOrDefault() != null; + } + } + } + + /// + /// Flattens dictionaries and lists into a single enumerable of all contained objects + /// that can then be queried over. + /// + /// The root of the traversal + /// Whether to traverse into AVObjects' children + /// Whether to include the root in the result + /// + internal static IEnumerable DeepTraversal( + object root, bool traverseAVObjects = false, bool yieldRoot = false) + { + var items = DeepTraversalInternal(root, + traverseAVObjects, + new HashSet(new IdentityEqualityComparer())); + if (yieldRoot) + { + return new[] { root }.Concat(items); + } + else + { + return items; + } + } + + private static IEnumerable DeepTraversalInternal( + object root, bool traverseAVObjects, ICollection seen) + { + seen.Add(root); + var itemsToVisit = isCompiledByIL2CPP ? (System.Collections.IEnumerable)null : (IEnumerable)null; + var dict = Conversion.As>(root); + if (dict != null) + { + itemsToVisit = dict.Values; + } + else + { + var list = Conversion.As>(root); + if (list != null) + { + itemsToVisit = list; + } + else if (traverseAVObjects) + { + var obj = root as AVObject; + if (obj != null) + { + itemsToVisit = obj.Keys.ToList().Select(k => obj[k]); + } + } + } + if (itemsToVisit != null) + { + foreach (var i in itemsToVisit) + { + if (!seen.Contains(i)) + { + yield return i; + var children = DeepTraversalInternal(i, traverseAVObjects, seen); + foreach (var child in children) + { + yield return child; + } + } + } + } + } + + private IEnumerable FindUnsavedChildren() + { + return DeepTraversal(estimatedData) + .OfType() + .Where(o => o.IsDirty); + } + + /// + /// Deep traversal of this object to grab a copy of any object referenced by this object. + /// These instances may have already been fetched, and we don't want to lose their data when + /// refreshing or saving. + /// + /// Map of objectId to AVObject which have been fetched. + private IDictionary CollectFetchedObjects() + { + return DeepTraversal(estimatedData) + .OfType() + .Where(o => o.ObjectId != null && o.IsDataAvailable) + .GroupBy(o => o.ObjectId) + .ToDictionary(group => group.Key, group => group.Last()); + } + + public static IDictionary ToJSONObjectForSaving( + IDictionary operations) + { + var result = new Dictionary(); + foreach (var pair in operations) + { + // AVRPCSerialize the data + var operation = pair.Value; + + result[pair.Key] = PointerOrLocalIdEncoder.Instance.Encode(operation); + } + return result; + } + + internal IDictionary EncodeForSaving(IDictionary data) + { + var result = new Dictionary(); + lock (this.mutex) + { + foreach (var key in data.Keys) + { + var value = data[key]; + result.Add(key, PointerOrLocalIdEncoder.Instance.Encode(value)); + } + } + + return result; + } + + + internal IDictionary ServerDataToJSONObjectForSerialization() + { + return PointerOrLocalIdEncoder.Instance.Encode(state.ToDictionary(t => t.Key, t => t.Value)) + as IDictionary; + } + + #region Save Object(s) + + /// + /// Pushes new operations onto the queue and returns the current set of operations. + /// + public IDictionary StartSave() + { + lock (mutex) + { + var currentOperations = CurrentOperations; + operationSetQueue.AddLast(new Dictionary()); + OnPropertyChanged("IsDirty"); + return currentOperations; + } + } + + protected virtual Task SaveAsync(Task toAwait, + CancellationToken cancellationToken) + { + IDictionary currentOperations = null; + if (!IsDirty) + { + return Task.FromResult(0); + } + + Task deepSaveTask; + string sessionToken; + lock (mutex) + { + // Get the JSON representation of the object. + currentOperations = StartSave(); + + sessionToken = AVUser.CurrentSessionToken; + + deepSaveTask = DeepSaveAsync(estimatedData, sessionToken, cancellationToken); + } + + return deepSaveTask.OnSuccess(_ => + { + return toAwait; + }).Unwrap().OnSuccess(_ => + { + return ObjectController.SaveAsync(state, + currentOperations, + sessionToken, + cancellationToken); + }).Unwrap().ContinueWith(t => + { + if (t.IsFaulted || t.IsCanceled) + { + HandleFailedSave(currentOperations); + } + else + { + var serverState = t.Result; + HandleSave(serverState); + } + return t; + }).Unwrap(); + } + + /// + /// Saves this object to the server. + /// + public virtual Task SaveAsync() + { + return SaveAsync(CancellationToken.None); + } + + /// + /// Saves this object to the server. + /// + /// The cancellation token. + public virtual Task SaveAsync(CancellationToken cancellationToken) + { + return taskQueue.Enqueue(toAwait => SaveAsync(toAwait, cancellationToken), + cancellationToken); + } + + internal virtual Task FetchAsyncInternal( + Task toAwait, + IDictionary queryString, + CancellationToken cancellationToken) + { + return toAwait.OnSuccess(_ => + { + if (ObjectId == null) + { + throw new InvalidOperationException("Cannot refresh an object that hasn't been saved to the server."); + } + if (queryString == null) + { + queryString = new Dictionary(); + } + + return ObjectController.FetchAsync(state, queryString, AVUser.CurrentSessionToken, cancellationToken); + }).Unwrap().OnSuccess(t => + { + HandleFetchResult(t.Result); + return this; + }); + } + + private static Task DeepSaveAsync(object obj, string sessionToken, CancellationToken cancellationToken) + { + var objects = new List(); + CollectDirtyChildren(obj, objects); + + var uniqueObjects = new HashSet(objects, + new IdentityEqualityComparer()); + + var saveDirtyFileTasks = DeepTraversal(obj, true) + .OfType() + .Where(f => f.IsDirty) + .Select(f => f.SaveAsync(cancellationToken)).ToList(); + + return Task.WhenAll(saveDirtyFileTasks).OnSuccess(_ => + { + IEnumerable remaining = new List(uniqueObjects); + return InternalExtensions.WhileAsync(() => Task.FromResult(remaining.Any()), () => + { + // Partition the objects into two sets: those that can be saved immediately, + // and those that rely on other objects to be created first. + var current = (from item in remaining + where item.CanBeSerialized + select item).ToList(); + var nextBatch = (from item in remaining + where !item.CanBeSerialized + select item).ToList(); + remaining = nextBatch; + + if (current.Count == 0) + { + // We do cycle-detection when building the list of objects passed to this + // function, so this should never get called. But we should check for it + // anyway, so that we get an exception instead of an infinite loop. + throw new InvalidOperationException( + "Unable to save a AVObject with a relation to a cycle."); + } + + // Save all of the objects in current. + return AVObject.EnqueueForAll(current, toAwait => + { + return toAwait.OnSuccess(__ => + { + var states = (from item in current + select item.state).ToList(); + var operationsList = (from item in current + select item.StartSave()).ToList(); + + var saveTasks = ObjectController.SaveAllAsync(states, + operationsList, + sessionToken, + cancellationToken); + + return Task.WhenAll(saveTasks).ContinueWith(t => + { + if (t.IsFaulted || t.IsCanceled) + { + foreach (var pair in current.Zip(operationsList, (item, ops) => new { item, ops })) + { + pair.item.HandleFailedSave(pair.ops); + } + } + else + { + var serverStates = t.Result; + foreach (var pair in current.Zip(serverStates, (item, state) => new { item, state })) + { + pair.item.HandleSave(pair.state); + } + } + cancellationToken.ThrowIfCancellationRequested(); + return t; + }).Unwrap(); + }).Unwrap().OnSuccess(t => (object)null); + }, cancellationToken); + }); + }).Unwrap(); + } + + /// + /// Saves each object in the provided list. + /// + /// The objects to save. + public static Task SaveAllAsync(IEnumerable objects) where T : AVObject + { + return SaveAllAsync(objects, CancellationToken.None); + } + + /// + /// Saves each object in the provided list. + /// + /// The objects to save. + /// The cancellation token. + public static Task SaveAllAsync( + IEnumerable objects, CancellationToken cancellationToken) where T : AVObject + { + return DeepSaveAsync(objects.ToList(), AVUser.CurrentSessionToken, cancellationToken); + } + + #endregion + + #region Fetch Object(s) + + /// + /// Fetches this object with the data from the server. + /// + /// The cancellation token. + internal Task FetchAsyncInternal(CancellationToken cancellationToken) + { + return FetchAsyncInternal(null, cancellationToken); + } + + internal Task FetchAsyncInternal(IDictionary queryString, CancellationToken cancellationToken) + { + return taskQueue.Enqueue(toAwait => FetchAsyncInternal(toAwait, queryString, cancellationToken), + cancellationToken); + } + + internal Task FetchIfNeededAsyncInternal( + Task toAwait, CancellationToken cancellationToken) + { + if (!IsDataAvailable) + { + return FetchAsyncInternal(toAwait, null, cancellationToken); + } + return Task.FromResult(this); + } + + /// + /// If this AVObject has not been fetched (i.e. returns + /// false), fetches this object with the data from the server. + /// + /// The cancellation token. + internal Task FetchIfNeededAsyncInternal(CancellationToken cancellationToken) + { + return taskQueue.Enqueue(toAwait => FetchIfNeededAsyncInternal(toAwait, cancellationToken), + cancellationToken); + } + + /// + /// Fetches all of the objects that don't have data in the provided list. + /// + /// The list passed in for convenience. + public static Task> FetchAllIfNeededAsync( + IEnumerable objects) where T : AVObject + { + return FetchAllIfNeededAsync(objects, CancellationToken.None); + } + + /// + /// Fetches all of the objects that don't have data in the provided list. + /// + /// The objects to fetch. + /// The cancellation token. + /// The list passed in for convenience. + public static Task> FetchAllIfNeededAsync( + IEnumerable objects, CancellationToken cancellationToken) where T : AVObject + { + return AVObject.EnqueueForAll(objects.Cast(), (Task toAwait) => + { + return FetchAllInternalAsync(objects, false, toAwait, cancellationToken); + }, cancellationToken); + } + + /// + /// Fetches all of the objects in the provided list. + /// + /// The objects to fetch. + /// The list passed in for convenience. + public static Task> FetchAllAsync( + IEnumerable objects) where T : AVObject + { + return FetchAllAsync(objects, CancellationToken.None); + } + + /// + /// Fetches all of the objects in the provided list. + /// + /// The objects to fetch. + /// The cancellation token. + /// The list passed in for convenience. + public static Task> FetchAllAsync( + IEnumerable objects, CancellationToken cancellationToken) where T : AVObject + { + return AVObject.EnqueueForAll(objects.Cast(), (Task toAwait) => + { + return FetchAllInternalAsync(objects, true, toAwait, cancellationToken); + }, cancellationToken); + } + + /// + /// Fetches all of the objects in the list. + /// + /// The objects to fetch. + /// If false, only objects without data will be fetched. + /// A task to await before starting. + /// The cancellation token. + /// The list passed in for convenience. + private static Task> FetchAllInternalAsync( + IEnumerable objects, bool force, Task toAwait, CancellationToken cancellationToken) where T : AVObject + { + return toAwait.OnSuccess(_ => + { + if (objects.Any(obj => { return obj.state.ObjectId == null; })) + { + throw new InvalidOperationException("You cannot fetch objects that haven't already been saved."); + } + + var objectsToFetch = (from obj in objects + where force || !obj.IsDataAvailable + select obj).ToList(); + + if (objectsToFetch.Count == 0) + { + return Task.FromResult(objects); + } + + // Do one Find for each class. + var findsByClass = + (from obj in objectsToFetch + group obj.ObjectId by obj.ClassName into classGroup + where classGroup.Count() > 0 + select new + { + ClassName = classGroup.Key, + FindTask = new AVQuery(classGroup.Key) + .WhereContainedIn("objectId", classGroup) + .FindAsync(cancellationToken) + }).ToDictionary(pair => pair.ClassName, pair => pair.FindTask); + + // Wait for all the Finds to complete. + return Task.WhenAll(findsByClass.Values.ToList()).OnSuccess(__ => + { + if (cancellationToken.IsCancellationRequested) + { + return objects; + } + + // Merge the data from the Finds into the input objects. + var pairs = from obj in objectsToFetch + from result in findsByClass[obj.ClassName].Result + where result.ObjectId == obj.ObjectId + select new { obj, result }; + foreach (var pair in pairs) + { + pair.obj.MergeFromObject(pair.result); + pair.obj.hasBeenFetched = true; + } + + return objects; + }); + }).Unwrap(); + } + + #endregion + + #region Delete Object + + internal Task DeleteAsync(Task toAwait, CancellationToken cancellationToken) + { + if (ObjectId == null) + { + return Task.FromResult(0); + } + + string sessionToken = AVUser.CurrentSessionToken; + + return toAwait.OnSuccess(_ => + { + return ObjectController.DeleteAsync(State, sessionToken, cancellationToken); + }).Unwrap().OnSuccess(_ => IsDirty = true); + } + + /// + /// Deletes this object on the server. + /// + public Task DeleteAsync() + { + return DeleteAsync(CancellationToken.None); + } + + /// + /// Deletes this object on the server. + /// + /// The cancellation token. + public Task DeleteAsync(CancellationToken cancellationToken) + { + return taskQueue.Enqueue(toAwait => DeleteAsync(toAwait, cancellationToken), + cancellationToken); + } + + /// + /// Deletes each object in the provided list. + /// + /// The objects to delete. + public static Task DeleteAllAsync(IEnumerable objects) where T : AVObject + { + return DeleteAllAsync(objects, CancellationToken.None); + } + + /// + /// Deletes each object in the provided list. + /// + /// The objects to delete. + /// The cancellation token. + public static Task DeleteAllAsync( + IEnumerable objects, CancellationToken cancellationToken) where T : AVObject + { + var uniqueObjects = new HashSet(objects.OfType().ToList(), + new IdentityEqualityComparer()); + + return AVObject.EnqueueForAll(uniqueObjects, toAwait => + { + var states = uniqueObjects.Select(t => t.state).ToList(); + return toAwait.OnSuccess(_ => + { + var deleteTasks = ObjectController.DeleteAllAsync(states, + AVUser.CurrentSessionToken, + cancellationToken); + + return Task.WhenAll(deleteTasks); + }).Unwrap().OnSuccess(t => + { + // Dirty all objects in memory. + foreach (var obj in uniqueObjects) + { + obj.IsDirty = true; + } + + return (object)null; + }); + }, cancellationToken); + } + + #endregion + + private static void CollectDirtyChildren(object node, + IList dirtyChildren, + ICollection seen, + ICollection seenNew) + { + foreach (var obj in DeepTraversal(node).OfType()) + { + ICollection scopedSeenNew; + // Check for cycles of new objects. Any such cycle means it will be impossible to save + // this collection of objects, so throw an exception. + if (obj.ObjectId != null) + { + scopedSeenNew = new HashSet(new IdentityEqualityComparer()); + } + else + { + if (seenNew.Contains(obj)) + { + throw new InvalidOperationException("Found a circular dependency while saving"); + } + scopedSeenNew = new HashSet(seenNew, new IdentityEqualityComparer()); + scopedSeenNew.Add(obj); + } + + // Check for cycles of any object. If this occurs, then there's no problem, but + // we shouldn't recurse any deeper, because it would be an infinite recursion. + if (seen.Contains(obj)) + { + return; + } + seen.Add(obj); + + // Recurse into this object's children looking for dirty children. + // We only need to look at the child object's current estimated data, + // because that's the only data that might need to be saved now. + CollectDirtyChildren(obj.estimatedData, dirtyChildren, seen, scopedSeenNew); + + if (obj.CheckIsDirty(false)) + { + dirtyChildren.Add(obj); + } + } + } + + /// + /// Helper version of CollectDirtyChildren so that callers don't have to add the internally + /// used parameters. + /// + private static void CollectDirtyChildren(object node, IList dirtyChildren) + { + CollectDirtyChildren(node, + dirtyChildren, + new HashSet(new IdentityEqualityComparer()), + new HashSet(new IdentityEqualityComparer())); + } + + /// + /// Returns true if the given object can be serialized for saving as a value + /// that is pointed to by a AVObject. + /// + private static bool CanBeSerializedAsValue(object value) + { + return DeepTraversal(value, yieldRoot: true) + .OfType() + .All(o => o.ObjectId != null); + } + + private bool CanBeSerialized + { + get + { + // This method is only used for batching sets of objects for saveAll + // and when saving children automatically. Since it's only used to + // determine whether or not save should be called on them, it only + // needs to examine their current values, so we use estimatedData. + lock (mutex) + { + return CanBeSerializedAsValue(estimatedData); + } + } + } + + /// + /// Adds a task to the queue for all of the given objects. + /// + private static Task EnqueueForAll(IEnumerable objects, + Func> taskStart, CancellationToken cancellationToken) + { + // The task that will be complete when all of the child queues indicate they're ready to start. + var readyToStart = new TaskCompletionSource(); + + // First, we need to lock the mutex for the queue for every object. We have to hold this + // from at least when taskStart() is called to when obj.taskQueue enqueue is called, so + // that saves actually get executed in the order they were setup by taskStart(). + // The locks have to be sorted so that we always acquire them in the same order. + // Otherwise, there's some risk of deadlock. + var lockSet = new LockSet(objects.Select(o => o.taskQueue.Mutex)); + + lockSet.Enter(); + try + { + + // The task produced by taskStart. By running this immediately, we allow everything prior + // to toAwait to run before waiting for all of the queues on all of the objects. + Task fullTask = taskStart(readyToStart.Task); + + // Add fullTask to each of the objects' queues. + var childTasks = new List(); + foreach (AVObject obj in objects) + { + obj.taskQueue.Enqueue((Task task) => + { + childTasks.Add(task); + return fullTask; + }, cancellationToken); + } + + // When all of the objects' queues are ready, signal fullTask that it's ready to go on. + Task.WhenAll(childTasks.ToArray()).ContinueWith((Task task) => + { + readyToStart.SetResult(null); + }); + + return fullTask; + } + finally + { + lockSet.Exit(); + } + } + + /// + /// Removes a key from the object's data if it exists. + /// + /// The key to remove. + public virtual void Remove(string key) + { + lock (mutex) + { + CheckKeyIsMutable(key); + + PerformOperation(key, AVDeleteOperation.Instance); + } + } + + + private IEnumerable ApplyOperations(IDictionary operations, + IDictionary map) + { + List appliedKeys = new List(); + lock (mutex) + { + foreach (var pair in operations) + { + object oldValue; + map.TryGetValue(pair.Key, out oldValue); + var newValue = pair.Value.Apply(oldValue, pair.Key); + if (newValue != AVDeleteOperation.DeleteToken) + { + map[pair.Key] = newValue; + } + else + { + map.Remove(pair.Key); + } + appliedKeys.Add(pair.Key); + } + } + return appliedKeys; + } + + /// + /// Regenerates the estimatedData map from the serverData and operations. + /// + internal void RebuildEstimatedData() + { + IEnumerable changedKeys = null; + + lock (mutex) + { + //estimatedData.Clear(); + List converdKeys = new List(); + foreach (var item in state) + { + var key = item.Key; + var value = item.Value; + if (!estimatedData.ContainsKey(key)) + { + converdKeys.Add(key); + } + else + { + var oldValue = estimatedData[key]; + if (oldValue != value) + { + converdKeys.Add(key); + } + estimatedData.Remove(key); + } + estimatedData.Add(item); + } + changedKeys = converdKeys; + foreach (var operations in operationSetQueue) + { + var appliedKeys = ApplyOperations(operations, estimatedData); + changedKeys = converdKeys.Concat(appliedKeys); + } + // We've just applied a bunch of operations to estimatedData which + // may have changed all of its keys. Notify of all keys and properties + // mapped to keys being changed. + OnFieldsChanged(changedKeys); + } + } + + /// + /// PerformOperation is like setting a value at an index, but instead of + /// just taking a new value, it takes a AVFieldOperation that modifies the value. + /// + internal void PerformOperation(string key, IAVFieldOperation operation) + { + lock (mutex) + { + var ifDirtyBeforePerform = this.IsDirty; + object oldValue; + estimatedData.TryGetValue(key, out oldValue); + object newValue = operation.Apply(oldValue, key); + if (newValue != AVDeleteOperation.DeleteToken) + { + estimatedData[key] = newValue; + } + else + { + estimatedData.Remove(key); + } + + IAVFieldOperation oldOperation; + bool wasDirty = CurrentOperations.Count > 0; + CurrentOperations.TryGetValue(key, out oldOperation); + var newOperation = operation.MergeWithPrevious(oldOperation); + CurrentOperations[key] = newOperation; + if (!wasDirty) + { + OnPropertyChanged("IsDirty"); + if (ifDirtyBeforePerform != wasDirty) + { + OnPropertyUpdated("IsDirty", ifDirtyBeforePerform, wasDirty); + } + } + + OnFieldsChanged(new[] { key }); + OnPropertyUpdated(key, oldValue, newValue); + } + } + + /// + /// Override to run validations on key/value pairs. Make sure to still + /// call the base version. + /// + internal virtual void OnSettingValue(ref string key, ref object value) + { + if (key == null) + { + throw new ArgumentNullException("key"); + } + } + + /// + /// Gets or sets a value on the object. It is recommended to name + /// keys in partialCamelCaseLikeThis. + /// + /// The key for the object. Keys must be alphanumeric plus underscore + /// and start with a letter. + /// The property is + /// retrieved and is not found. + /// The value for the key. + virtual public object this[string key] + { + get + { + lock (mutex) + { + CheckGetAccess(key); + + var value = estimatedData[key]; + + // A relation may be deserialized without a parent or key. Either way, + // make sure it's consistent. + var relation = value as AVRelationBase; + if (relation != null) + { + relation.EnsureParentAndKey(this, key); + } + + return value; + } + } + set + { + lock (mutex) + { + CheckKeyIsMutable(key); + + Set(key, value); + } + } + } + + /// + /// Perform Set internally which is not gated by mutability check. + /// + /// key for the object. + /// the value for the key. + internal void Set(string key, object value) + { + lock (mutex) + { + OnSettingValue(ref key, ref value); + + if (!AVEncoder.IsValidType(value)) + { + throw new ArgumentException("Invalid type for value: " + value.GetType().ToString()); + } + + PerformOperation(key, new AVSetOperation(value)); + } + } + + internal void SetIfDifferent(string key, T value) + { + T current; + bool hasCurrent = TryGetValue(key, out current); + if (value == null) + { + if (hasCurrent) + { + PerformOperation(key, AVDeleteOperation.Instance); + } + return; + } + if (!hasCurrent || !value.Equals(current)) + { + Set(key, value); + } + } + + #region Atomic Increment + + /// + /// Atomically increments the given key by 1. + /// + /// The key to increment. + public void Increment(string key) + { + Increment(key, 1); + } + + /// + /// Atomically increments the given key by the given number. + /// + /// The key to increment. + /// The amount to increment by. + public void Increment(string key, long amount) + { + lock (mutex) + { + CheckKeyIsMutable(key); + + PerformOperation(key, new AVIncrementOperation(amount)); + } + } + + /// + /// Atomically increments the given key by the given number. + /// + /// The key to increment. + /// The amount to increment by. + public void Increment(string key, double amount) + { + lock (mutex) + { + CheckKeyIsMutable(key); + + PerformOperation(key, new AVIncrementOperation(amount)); + } + } + + #endregion + + /// + /// Atomically adds an object to the end of the list associated with the given key. + /// + /// The key. + /// The object to add. + public void AddToList(string key, object value) + { + AddRangeToList(key, new[] { value }); + } + + /// + /// Atomically adds objects to the end of the list associated with the given key. + /// + /// The key. + /// The objects to add. + public void AddRangeToList(string key, IEnumerable values) + { + lock (mutex) + { + CheckKeyIsMutable(key); + + PerformOperation(key, new AVAddOperation(values.Cast())); + + OnCollectionPropertyUpdated(key, NotifyCollectionUpdatedAction.Add, null, values); + } + } + + /// + /// Atomically adds an object to the end of the list associated with the given key, + /// only if it is not already present in the list. The position of the insert is not + /// guaranteed. + /// + /// The key. + /// The object to add. + public void AddUniqueToList(string key, object value) + { + AddRangeUniqueToList(key, new object[] { value }); + } + + /// + /// Atomically adds objects to the end of the list associated with the given key, + /// only if they are not already present in the list. The position of the inserts are not + /// guaranteed. + /// + /// The key. + /// The objects to add. + public void AddRangeUniqueToList(string key, IEnumerable values) + { + lock (mutex) + { + CheckKeyIsMutable(key); + + PerformOperation(key, new AVAddUniqueOperation(values.Cast())); + } + } + + /// + /// Atomically removes all instances of the objects in + /// from the list associated with the given key. + /// + /// The key. + /// The objects to remove. + public void RemoveAllFromList(string key, IEnumerable values) + { + lock (mutex) + { + CheckKeyIsMutable(key); + + PerformOperation(key, new AVRemoveOperation(values.Cast())); + + OnCollectionPropertyUpdated(key, NotifyCollectionUpdatedAction.Remove, values, null); + } + } + + /// + /// Returns whether this object has a particular key. + /// + /// The key to check for + public bool ContainsKey(string key) + { + lock (mutex) + { + return estimatedData.ContainsKey(key); + } + } + + /// + /// Gets a value for the key of a particular type. + /// The type to convert the value to. Supported types are + /// AVObject and its descendents, LeanCloud types such as AVRelation and AVGeopoint, + /// primitive types,IList<T>, IDictionary<string, T>, and strings. + /// The key of the element to get. + /// The property is + /// retrieved and is not found. + /// + public T Get(string key) + { + return Conversion.To(this[key]); + } + + /// + /// Access or create a Relation value for a key. + /// + /// The type of object to create a relation for. + /// The key for the relation field. + /// A AVRelation for the key. + public AVRelation GetRelation(string key) where T : AVObject + { + // All the sanity checking is done when add or remove is called. + AVRelation relation = null; + TryGetValue(key, out relation); + return relation ?? new AVRelation(this, key); + } + + /// + /// Get relation revserse query. + /// + /// AVObject + /// parent className + /// key + /// + public AVQuery GetRelationRevserseQuery(string parentClassName, string key) where T : AVObject + { + if (string.IsNullOrEmpty(parentClassName)) + { + throw new ArgumentNullException("parentClassName", "can not query a relation without parentClassName."); + } + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentNullException("key", "can not query a relation without key."); + } + return new AVQuery(parentClassName).WhereEqualTo(key, this); + } + + /// + /// Populates result with the value for the key, if possible. + /// + /// The desired type for the value. + /// The key to retrieve a value for. + /// The value for the given key, converted to the + /// requested type, or null if unsuccessful. + /// true if the lookup and conversion succeeded, otherwise + /// false. + public virtual bool TryGetValue(string key, out T result) + { + lock (mutex) + { + if (ContainsKey(key)) + { + try + { + var temp = Conversion.To(this[key]); + result = temp; + return true; + } + catch (InvalidCastException) + { + result = default(T); + return false; + } + } + result = default(T); + return false; + } + } + + /// + /// Gets whether the AVObject has been fetched. + /// + public bool IsDataAvailable + { + get + { + lock (mutex) + { + return hasBeenFetched; + } + } + } + + private bool CheckIsDataAvailable(string key) + { + lock (mutex) + { + return IsDataAvailable || estimatedData.ContainsKey(key); + } + } + + private void CheckGetAccess(string key) + { + lock (mutex) + { + if (!CheckIsDataAvailable(key)) + { + throw new InvalidOperationException( + "AVObject has no data for this key. Call FetchIfNeededAsync() to get the data."); + } + } + } + + private void CheckKeyIsMutable(string key) + { + if (!IsKeyMutable(key)) + { + throw new InvalidOperationException( + "Cannot change the `" + key + "` property of a `" + ClassName + "` object."); + } + } + + protected virtual bool IsKeyMutable(string key) + { + return true; + } + + /// + /// A helper function for checking whether two AVObjects point to + /// the same object in the cloud. + /// + public bool HasSameId(AVObject other) + { + lock (mutex) + { + return other != null && + object.Equals(ClassName, other.ClassName) && + object.Equals(ObjectId, other.ObjectId); + } + } + + internal IDictionary CurrentOperations + { + get + { + lock (mutex) + { + return operationSetQueue.Last.Value; + } + } + } + + /// + /// Gets a set view of the keys contained in this object. This does not include createdAt, + /// updatedAt, or objectId. It does include things like username and ACL. + /// + public ICollection Keys + { + get + { + lock (mutex) + { + return estimatedData.Keys; + } + } + } + + /// + /// Gets or sets the AVACL governing this object. + /// + [AVFieldName("ACL")] + public AVACL ACL + { + get { return GetProperty(null, "ACL"); } + set { SetProperty(value, "ACL"); } + } + + /// + /// Returns true if this object was created by the LeanCloud server when the + /// object might have already been there (e.g. in the case of a Facebook + /// login) + /// +#if !UNITY + public +#else + internal +#endif + bool IsNew + { + get + { + return state.IsNew; + } +#if !UNITY + internal +#endif + set + { + MutateState(mutableClone => + { + mutableClone.IsNew = value; + }); + OnPropertyChanged("IsNew"); + } + } + + /// + /// Gets the last time this object was updated as the server sees it, so that if you make changes + /// to a AVObject, then wait a while, and then call , the updated time + /// will be the time of the call rather than the time the object was + /// changed locally. + /// + [AVFieldName("updatedAt")] + public DateTime? UpdatedAt + { + get + { + return state.UpdatedAt; + } + } + + /// + /// Gets the first time this object was saved as the server sees it, so that if you create a + /// AVObject, then wait a while, and then call , the + /// creation time will be the time of the first call rather than + /// the time the object was created locally. + /// + [AVFieldName("createdAt")] + public DateTime? CreatedAt + { + get + { + return state.CreatedAt; + } + } + + /// + /// Indicates whether this AVObject has unsaved changes. + /// + public bool IsDirty + { + get + { + lock (mutex) { return CheckIsDirty(true); } + } + internal set + { + lock (mutex) + { + dirty = value; + OnPropertyChanged("IsDirty"); + } + } + } + + /// + /// Indicates whether key is unsaved for this AVObject. + /// + /// The key to check for. + /// true if the key has been altered and not saved yet, otherwise + /// false. + public bool IsKeyDirty(string key) + { + lock (mutex) + { + return CurrentOperations.ContainsKey(key); + } + } + + private bool CheckIsDirty(bool considerChildren) + { + lock (mutex) + { + return (dirty || CurrentOperations.Count > 0 || (considerChildren && HasDirtyChildren)); + } + } + + /// + /// Gets or sets the object id. An object id is assigned as soon as an object is + /// saved to the server. The combination of a and an + /// uniquely identifies an object in your application. + /// + [AVFieldName("objectId")] + public string ObjectId + { + get + { + return state.ObjectId; + } + set + { + IsDirty = true; + SetObjectIdInternal(value); + } + } + /// + /// Sets the objectId without marking dirty. + /// + /// The new objectId + private void SetObjectIdInternal(string objectId) + { + lock (mutex) + { + MutateState(mutableClone => + { + mutableClone.ObjectId = objectId; + }); + OnPropertyChanged("ObjectId"); + } + } + + /// + /// Gets the class name for the AVObject. + /// + public string ClassName + { + get + { + return state.ClassName; + } + } + + /// + /// Adds a value for the given key, throwing an Exception if the key + /// already has a value. + /// + /// + /// This allows you to use collection initialization syntax when creating AVObjects, + /// such as: + /// + /// var obj = new AVObject("MyType") + /// { + /// {"name", "foo"}, + /// {"count", 10}, + /// {"found", false} + /// }; + /// + /// + /// The key for which a value should be set. + /// The value for the key. + public void Add(string key, object value) + { + lock (mutex) + { + if (this.ContainsKey(key)) + { + throw new ArgumentException("Key already exists", key); + } + this[key] = value; + } + } + + IEnumerator> IEnumerable> + .GetEnumerator() + { + lock (mutex) + { + return estimatedData.GetEnumerator(); + } + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + lock (mutex) + { + return ((IEnumerable>)this).GetEnumerator(); + } + } + + /// + /// Gets a for the type of object specified by + /// + /// + /// The class name of the object. + /// A new . + public static AVQuery GetQuery(string className) + { + // Since we can't return a AVQuery (due to strong-typing with + // generics), we'll require you to go through subclasses. This is a better + // experience anyway, especially with LINQ integration, since you'll get + // strongly-typed queries and compile-time checking of property names and + // types. + if (SubclassingController.GetType(className) != null) + { + throw new ArgumentException( + "Use the class-specific query properties for class " + className, "className"); + } + return new AVQuery(className); + } + + /// + /// Raises change notifications for all properties associated with the given + /// field names. If fieldNames is null, this will notify for all known field-linked + /// properties (e.g. this happens when we recalculate all estimated data from scratch) + /// + protected virtual void OnFieldsChanged(IEnumerable fieldNames) + { + var mappings = SubclassingController.GetPropertyMappings(ClassName); + IEnumerable properties; + + if (fieldNames != null && mappings != null) + { + properties = from m in mappings + join f in fieldNames on m.Value equals f + select m.Key; + } + else if (mappings != null) + { + properties = mappings.Keys; + } + else + { + properties = Enumerable.Empty(); + } + + foreach (var property in properties) + { + OnPropertyChanged(property); + } + OnPropertyChanged("Item[]"); + } + + /// + /// Raises change notifications for a property. Passing null or the empty string + /// notifies the binding framework that all properties/indexes have changed. + /// Passing "Item[]" tells the binding framework that all indexed values + /// have changed (but not all properties) + /// + protected virtual void OnPropertyChanged( +#if !UNITY +[CallerMemberName] string propertyName = null +#else +string propertyName +#endif +) + { + propertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + private SynchronizedEventHandler propertyChanged = + new SynchronizedEventHandler(); + /// + /// Occurs when a property value changes. + /// + public event PropertyChangedEventHandler PropertyChanged + { + add + { + propertyChanged.Add(value); + } + remove + { + propertyChanged.Remove(value); + } + } + + private SynchronizedEventHandler propertyUpdated = + new SynchronizedEventHandler(); + + public event PropertyUpdatedEventHandler PropertyUpdated + { + add + { + propertyUpdated.Add(value); + } + remove + { + propertyUpdated.Remove(value); + } + } + + protected virtual void OnPropertyUpdated(string propertyName, object newValue, object oldValue) + { + propertyUpdated.Invoke(this, new PropertyUpdatedEventArgs(propertyName, oldValue, newValue)); + } + + private SynchronizedEventHandler collectionUpdated = +new SynchronizedEventHandler(); + + public event CollectionPropertyUpdatedEventHandler CollectionPropertyUpdated + { + add + { + collectionUpdated.Add(value); + } + remove + { + collectionUpdated.Remove(value); + } + } + + protected virtual void OnCollectionPropertyUpdated(string propertyName, NotifyCollectionUpdatedAction action, IEnumerable oldValues, IEnumerable newValues) + { + collectionUpdated?.Invoke(this, new CollectionPropertyUpdatedEventArgs(propertyName, action, oldValues, newValues)); + } + } + + public interface INotifyPropertyUpdated + { + event PropertyUpdatedEventHandler PropertyUpdated; + } + + public interface INotifyCollectionPropertyUpdated + { + event CollectionPropertyUpdatedEventHandler CollectionPropertyUpdated; + } + + public enum NotifyCollectionUpdatedAction + { + Add, + Remove + } + + public class CollectionPropertyUpdatedEventArgs : PropertyChangedEventArgs + { + public CollectionPropertyUpdatedEventArgs(string propertyName, NotifyCollectionUpdatedAction collectionAction, IEnumerable oldValues, IEnumerable newValues) : base(propertyName) + { + CollectionAction = collectionAction; + OldValues = oldValues; + NewValues = newValues; + } + + public IEnumerable OldValues { get; set; } + + public IEnumerable NewValues { get; set; } + + public NotifyCollectionUpdatedAction CollectionAction { get; set; } + } + + public class PropertyUpdatedEventArgs : PropertyChangedEventArgs + { + public PropertyUpdatedEventArgs(string propertyName, object oldValue, object newValue) : base(propertyName) + { + OldValue = oldValue; + NewValue = newValue; + } + + public object OldValue { get; private set; } + public object NewValue { get; private set; } + } + + public delegate void PropertyUpdatedEventHandler(object sender, PropertyUpdatedEventArgs args); + + public delegate void CollectionPropertyUpdatedEventHandler(object sender, CollectionPropertyUpdatedEventArgs args); +} diff --git a/Storage/Storage/Public/AVQuery.cs b/Storage/Storage/Public/AVQuery.cs new file mode 100644 index 0000000..5a0b341 --- /dev/null +++ b/Storage/Storage/Public/AVQuery.cs @@ -0,0 +1,360 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using LeanCloud.Storage.Internal; + +namespace LeanCloud +{ + + /// + /// The AVQuery class defines a query that is used to fetch AVObjects. The + /// most common use case is finding all objects that match a query through the + /// method. + /// + /// + /// This sample code fetches all objects of + /// class "MyClass": + /// + /// + /// AVQuery query = new AVQuery("MyClass"); + /// IEnumerable<AVObject> result = await query.FindAsync(); + /// + /// + /// A AVQuery can also be used to retrieve a single object whose id is known, + /// through the method. For example, this sample code + /// fetches an object of class "MyClass" and id myId. + /// + /// + /// AVQuery query = new AVQuery("MyClass"); + /// AVObject result = await query.GetAsync(myId); + /// + /// + /// A AVQuery can also be used to count the number of objects that match the + /// query without retrieving all of those objects. For example, this sample code + /// counts the number of objects of the class "MyClass". + /// + /// + /// AVQuery query = new AVQuery("MyClass"); + /// int count = await query.CountAsync(); + /// + /// + public class AVQuery : AVQueryPair, T>, IAVQuery + where T : AVObject + { + internal static IAVQueryController QueryController + { + get + { + return AVPlugins.Instance.QueryController; + } + } + + internal static IObjectSubclassingController SubclassingController + { + get + { + return AVPlugins.Instance.SubclassingController; + } + } + + /// + /// 调试时可以用来查看最终的发送的查询语句 + /// + private string JsonString + { + get + { + return AVClient.SerializeJsonString(this.BuildParameters(true)); + } + } + + /// + /// Private constructor for composition of queries. A Source query is required, + /// but the remaining values can be null if they won't be changed in this + /// composition. + /// + private AVQuery(AVQuery source, + IDictionary where = null, + IEnumerable replacementOrderBy = null, + IEnumerable thenBy = null, + int? skip = null, + int? limit = null, + IEnumerable includes = null, + IEnumerable selectedKeys = null, + String redirectClassNameForKey = null) + : base(source, where, replacementOrderBy, thenBy, skip, limit, includes, selectedKeys, redirectClassNameForKey) + { + + } + + //internal override AVQuery CreateInstance( + // AVQuery source, + // IDictionary where = null, + // IEnumerable replacementOrderBy = null, + // IEnumerable thenBy = null, + // int? skip = null, + // int? limit = null, + // IEnumerable includes = null, + // IEnumerable selectedKeys = null, + // String redirectClassNameForKey = null) + //{ + // return new AVQuery(this, where, replacementOrderBy, thenBy, skip, limit, includes, selectedKeys, redirectClassNameForKey); + //} + + public override AVQuery CreateInstance( + IDictionary where = null, + IEnumerable replacementOrderBy = null, + IEnumerable thenBy = null, + int? skip = null, + int? limit = null, + IEnumerable includes = null, + IEnumerable selectedKeys = null, + String redirectClassNameForKey = null) + { + return new AVQuery(this, where, replacementOrderBy, thenBy, skip, limit, includes, selectedKeys, redirectClassNameForKey); + } + + + /// + /// Constructs a query based upon the AVObject subclass used as the generic parameter for the AVQuery. + /// + public AVQuery() + : this(SubclassingController.GetClassName(typeof(T))) + { + } + + /// + /// Constructs a query. A default query with no further parameters will retrieve + /// all s of the provided class. + /// + /// The name of the class to retrieve AVObjects for. + public AVQuery(string className) + : base(className) + { + + } + + /// + /// Constructs a query that is the or of the given queries. + /// + /// The list of AVQueries to 'or' together. + /// A AVQquery that is the 'or' of the passed in queries. + public static AVQuery Or(IEnumerable> queries) + { + string className = null; + var orValue = new List>(); + // We need to cast it to non-generic IEnumerable because of AOT-limitation + var nonGenericQueries = (IEnumerable)queries; + foreach (var obj in nonGenericQueries) + { + var q = (AVQuery)obj; + if (className != null && q.className != className) + { + throw new ArgumentException( + "All of the queries in an or query must be on the same class."); + } + className = q.className; + var parameters = q.BuildParameters(); + if (parameters.Count == 0) + { + continue; + } + object where; + if (!parameters.TryGetValue("where", out where) || parameters.Count > 1) + { + throw new ArgumentException( + "None of the queries in an or query can have non-filtering clauses"); + } + orValue.Add(where as IDictionary); + } + return new AVQuery(new AVQuery(className), + where: new Dictionary { + { "$or", orValue} + }); + } + + /// + /// Retrieves a list of AVObjects that satisfy this query from LeanCloud. + /// + /// The cancellation token. + /// The list of AVObjects that match this query. + public override Task> FindAsync(CancellationToken cancellationToken) + { + return AVUser.GetCurrentUserAsync().OnSuccess(t => + { + return QueryController.FindAsync(this, t.Result, cancellationToken); + }).Unwrap().OnSuccess(t => + { + IEnumerable states = t.Result; + return (from state in states + select AVObject.FromState(state, ClassName)); + }); + } + + /// + /// Retrieves at most one AVObject that satisfies this query. + /// + /// The cancellation token. + /// A single AVObject that satisfies this query, or else null. + public override Task FirstOrDefaultAsync(CancellationToken cancellationToken) + { + return AVUser.GetCurrentUserAsync().OnSuccess(t => + { + return QueryController.FirstAsync(this, t.Result, cancellationToken); + }).Unwrap().OnSuccess(t => + { + IObjectState state = t.Result; + return state == null ? default(T) : AVObject.FromState(state, ClassName); + }); + } + + /// + /// Retrieves at most one AVObject that satisfies this query. + /// + /// The cancellation token. + /// A single AVObject that satisfies this query. + /// If no results match the query. + public override Task FirstAsync(CancellationToken cancellationToken) + { + return FirstOrDefaultAsync(cancellationToken).OnSuccess(t => + { + if (t.Result == null) + { + throw new AVException(AVException.ErrorCode.ObjectNotFound, + "No results matched the query."); + } + return t.Result; + }); + } + + /// + /// Counts the number of objects that match this query. + /// + /// The cancellation token. + /// The number of objects that match this query. + public override Task CountAsync(CancellationToken cancellationToken) + { + return AVUser.GetCurrentUserAsync().OnSuccess(t => + { + return QueryController.CountAsync(this, t.Result, cancellationToken); + }).Unwrap(); + } + + /// + /// Constructs a AVObject whose id is already known by fetching data + /// from the server. + /// + /// ObjectId of the AVObject to fetch. + /// The cancellation token. + /// The AVObject for the given objectId. + public override Task GetAsync(string objectId, CancellationToken cancellationToken) + { + AVQuery singleItemQuery = new AVQuery(className) + .WhereEqualTo("objectId", objectId); + singleItemQuery = new AVQuery(singleItemQuery, includes: this.includes, selectedKeys: this.selectedKeys, limit: 1); + return singleItemQuery.FindAsync(cancellationToken).OnSuccess(t => + { + var result = t.Result.FirstOrDefault(); + if (result == null) + { + throw new AVException(AVException.ErrorCode.ObjectNotFound, + "Object with the given objectId not found."); + } + return result; + }); + } + + #region CQL + /// + /// 执行 CQL 查询 + /// + /// CQL 语句 + /// CancellationToken + /// 返回符合条件的对象集合 + public static Task> DoCloudQueryAsync(string cql, CancellationToken cancellationToken) + { + var queryString = string.Format("cloudQuery?cql={0}", Uri.EscapeDataString(cql)); + + return rebuildObjectFromCloudQueryResult(queryString); + } + + /// + /// 执行 CQL 查询 + /// + /// + /// + public static Task> DoCloudQueryAsync(string cql) + { + return DoCloudQueryAsync(cql, CancellationToken.None); + } + + /// + /// 执行 CQL 查询 + /// + /// 带有占位符的模板 cql 语句 + /// 占位符对应的参数数组 + /// + public static Task> DoCloudQueryAsync(string cqlTeamplate, params object[] pvalues) + { + string queryStringTemplate = "cloudQuery?cql={0}&pvalues={1}"; + string pSrting = Json.Encode(pvalues); + string queryString = string.Format(queryStringTemplate, Uri.EscapeDataString(cqlTeamplate), Uri.EscapeDataString(pSrting)); + + return rebuildObjectFromCloudQueryResult(queryString); + } + + internal static Task> rebuildObjectFromCloudQueryResult(string queryString) + { + var command = new AVCommand(queryString, + method: "GET", + sessionToken: AVUser.CurrentSessionToken, + data: null); + + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command, cancellationToken: CancellationToken.None).OnSuccess(t => + { + var items = t.Result.Item2["results"] as IList; + var className = t.Result.Item2["className"].ToString(); + + IEnumerable states = (from item in items + select AVObjectCoder.Instance.Decode(item as IDictionary, AVDecoder.Instance)); + + return (from state in states + select AVObject.FromState(state, className)); + }); + } + + #endregion + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The object to compare with the current object. + /// true if the specified object is equal to the current object; otherwise, false + public override bool Equals(object obj) + { + if (obj == null || !(obj is AVQuery)) + { + return false; + } + + var other = obj as AVQuery; + return Object.Equals(this.className, other.ClassName) && + this.where.CollectionsEqual(other.where) && + this.orderBy.CollectionsEqual(other.orderBy) && + this.includes.CollectionsEqual(other.includes) && + this.selectedKeys.CollectionsEqual(other.selectedKeys) && + Object.Equals(this.skip, other.skip) && + Object.Equals(this.limit, other.limit); + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + } +} diff --git a/Storage/Storage/Public/AVQueryExtensions.cs b/Storage/Storage/Public/AVQueryExtensions.cs new file mode 100644 index 0000000..c726b41 --- /dev/null +++ b/Storage/Storage/Public/AVQueryExtensions.cs @@ -0,0 +1,818 @@ +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace LeanCloud +{ + /// + /// Provides extension methods for to support + /// Linq-style queries. + /// + public static class AVQueryExtensions + { + private static readonly MethodInfo getMethod; + private static readonly MethodInfo stringContains; + private static readonly MethodInfo stringStartsWith; + private static readonly MethodInfo stringEndsWith; + private static readonly MethodInfo containsMethod; + private static readonly MethodInfo notContainsMethod; + private static readonly MethodInfo containsKeyMethod; + private static readonly MethodInfo notContainsKeyMethod; + private static readonly Dictionary functionMappings; + static AVQueryExtensions() + { + getMethod = GetMethod(obj => obj.Get(null)).GetGenericMethodDefinition(); + stringContains = GetMethod(str => str.Contains(null)); + stringStartsWith = GetMethod(str => str.StartsWith(null)); + stringEndsWith = GetMethod(str => str.EndsWith(null)); + functionMappings = new Dictionary { + { + stringContains, + GetMethod>(q => q.WhereContains(null, null)) + }, + { + stringStartsWith, + GetMethod>(q => q.WhereStartsWith(null, null)) + }, + { + stringEndsWith, + GetMethod>(q => q.WhereEndsWith(null,null)) + }, + }; + containsMethod = GetMethod( + o => AVQueryExtensions.ContainsStub(null, null)).GetGenericMethodDefinition(); + notContainsMethod = GetMethod( + o => AVQueryExtensions.NotContainsStub(null, null)) + .GetGenericMethodDefinition(); + + containsKeyMethod = GetMethod(o => AVQueryExtensions.ContainsKeyStub(null, null)); + notContainsKeyMethod = GetMethod( + o => AVQueryExtensions.NotContainsKeyStub(null, null)); + } + + /// + /// Gets a MethodInfo for a top-level method call. + /// + private static MethodInfo GetMethod(Expression> expression) + { + return (expression.Body as MethodCallExpression).Method; + } + + /// + /// When a query is normalized, this is a placeholder to indicate we should + /// add a WhereContainedIn() clause. + /// + private static bool ContainsStub(object collection, T value) + { + throw new NotImplementedException( + "Exists only for expression translation as a placeholder."); + } + + /// + /// When a query is normalized, this is a placeholder to indicate we should + /// add a WhereNotContainedIn() clause. + /// + private static bool NotContainsStub(object collection, T value) + { + throw new NotImplementedException( + "Exists only for expression translation as a placeholder."); + } + + /// + /// When a query is normalized, this is a placeholder to indicate that we should + /// add a WhereExists() clause. + /// + private static bool ContainsKeyStub(AVObject obj, string key) + { + throw new NotImplementedException( + "Exists only for expression translation as a placeholder."); + } + + /// + /// When a query is normalized, this is a placeholder to indicate that we should + /// add a WhereDoesNotExist() clause. + /// + private static bool NotContainsKeyStub(AVObject obj, string key) + { + throw new NotImplementedException( + "Exists only for expression translation as a placeholder."); + } + + /// + /// Evaluates an expression and throws if the expression has components that can't be + /// evaluated (e.g. uses the parameter that's only represented by an object on the server). + /// + private static object GetValue(Expression exp) + { + try + { + return Expression.Lambda( + typeof(Func<>).MakeGenericType(exp.Type), exp).Compile().DynamicInvoke(); + } + catch (Exception e) + { + throw new InvalidOperationException("Unable to evaluate expression: " + exp, e); + } + } + + /// + /// Checks whether the MethodCallExpression is a call to AVObject.Get(), + /// which is the call we normalize all indexing into the AVObject to. + /// + private static bool IsAVObjectGet(MethodCallExpression node) + { + if (node == null || node.Object == null) + { + return false; + } + if (!typeof(AVObject).GetTypeInfo().IsAssignableFrom(node.Object.Type.GetTypeInfo())) + { + return false; + } + return node.Method.IsGenericMethod && node.Method.GetGenericMethodDefinition() == getMethod; + } + + + /// + /// Visits an Expression, converting AVObject.Get/AVObject[]/AVObject.Property, + /// and nested indices into a single call to AVObject.Get() with a "field path" like + /// "foo.bar.baz" + /// + private class ObjectNormalizer : ExpressionVisitor + { + protected override Expression VisitIndex(IndexExpression node) + { + var visitedObject = Visit(node.Object); + var indexer = visitedObject as MethodCallExpression; + if (IsAVObjectGet(indexer)) + { + var indexValue = GetValue(node.Arguments[0]) as string; + if (indexValue == null) + { + throw new InvalidOperationException("Index must be a string"); + } + var newPath = GetValue(indexer.Arguments[0]) + "." + indexValue; + return Expression.Call(indexer.Object, + getMethod.MakeGenericMethod(node.Type), + Expression.Constant(newPath, typeof(string))); + } + return base.VisitIndex(node); + } + + /// + /// Check for a AVFieldName attribute and use that as the path component, turning + /// properties like foo.ObjectId into foo.Get("objectId") + /// + protected override Expression VisitMember(MemberExpression node) + { + var fieldName = node.Member.GetCustomAttribute(); + if (fieldName != null && + typeof(AVObject).GetTypeInfo().IsAssignableFrom(node.Expression.Type.GetTypeInfo())) + { + var newPath = fieldName.FieldName; + return Expression.Call(node.Expression, + getMethod.MakeGenericMethod(node.Type), + Expression.Constant(newPath, typeof(string))); + } + return base.VisitMember(node); + } + + /// + /// If a AVObject.Get() call has been cast, just change the generic parameter. + /// + protected override Expression VisitUnary(UnaryExpression node) + { + var methodCall = Visit(node.Operand) as MethodCallExpression; + if ((node.NodeType == ExpressionType.Convert || + node.NodeType == ExpressionType.ConvertChecked) && + IsAVObjectGet(methodCall)) + { + return Expression.Call(methodCall.Object, + getMethod.MakeGenericMethod(node.Type), + methodCall.Arguments); + } + return base.VisitUnary(node); + } + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.Name == "get_Item" && node.Object is ParameterExpression) + { + var indexPath = GetValue(node.Arguments[0]) as string; + return Expression.Call(node.Object, + getMethod.MakeGenericMethod(typeof(object)), + Expression.Constant(indexPath, typeof(string))); + } + + if (node.Method.Name == "get_Item" || IsAVObjectGet(node)) + { + var visitedObject = Visit(node.Object); + var indexer = visitedObject as MethodCallExpression; + if (IsAVObjectGet(indexer)) + { + var indexValue = GetValue(node.Arguments[0]) as string; + if (indexValue == null) + { + throw new InvalidOperationException("Index must be a string"); + } + var newPath = GetValue(indexer.Arguments[0]) + "." + indexValue; + return Expression.Call(indexer.Object, + getMethod.MakeGenericMethod(node.Type), + Expression.Constant(newPath, typeof(string))); + } + } + return base.VisitMethodCall(node); + } + } + + /// + /// Normalizes Where expressions. + /// + private class WhereNormalizer : ExpressionVisitor + { + + /// + /// Normalizes binary operators. <, >, <=, >= !=, and == + /// This puts the AVObject.Get() on the left side of the operation + /// (reversing it if necessary), and normalizes the AVObject.Get() + /// + protected override Expression VisitBinary(BinaryExpression node) + { + var leftTransformed = new ObjectNormalizer().Visit(node.Left) as MethodCallExpression; + var rightTransformed = new ObjectNormalizer().Visit(node.Right) as MethodCallExpression; + + MethodCallExpression objectExpression; + Expression filterExpression; + bool inverted; + if (leftTransformed != null) + { + objectExpression = leftTransformed; + filterExpression = node.Right; + inverted = false; + } + else + { + objectExpression = rightTransformed; + filterExpression = node.Left; + inverted = true; + } + + try + { + switch (node.NodeType) + { + case ExpressionType.GreaterThan: + if (inverted) + { + return Expression.LessThan(objectExpression, filterExpression); + } + else + { + return Expression.GreaterThan(objectExpression, filterExpression); + } + case ExpressionType.GreaterThanOrEqual: + if (inverted) + { + return Expression.LessThanOrEqual(objectExpression, filterExpression); + } + else + { + return Expression.GreaterThanOrEqual(objectExpression, filterExpression); + } + case ExpressionType.LessThan: + if (inverted) + { + return Expression.GreaterThan(objectExpression, filterExpression); + } + else + { + return Expression.LessThan(objectExpression, filterExpression); + } + case ExpressionType.LessThanOrEqual: + if (inverted) + { + return Expression.GreaterThanOrEqual(objectExpression, filterExpression); + } + else + { + return Expression.LessThanOrEqual(objectExpression, filterExpression); + } + case ExpressionType.Equal: + return Expression.Equal(objectExpression, filterExpression); + case ExpressionType.NotEqual: + return Expression.NotEqual(objectExpression, filterExpression); + } + } + catch (ArgumentException) + { + throw new InvalidOperationException("Operation not supported: " + node); + } + return base.VisitBinary(node); + } + + /// + /// If a ! operator is used, this removes the ! and instead calls the equivalent + /// function (so e.g. == becomes !=, < becomes >=, Contains becomes NotContains) + /// + protected override Expression VisitUnary(UnaryExpression node) + { + // Normalizes inversion + if (node.NodeType == ExpressionType.Not) + { + var visitedOperand = Visit(node.Operand); + var binaryOperand = visitedOperand as BinaryExpression; + if (binaryOperand != null) + { + switch (binaryOperand.NodeType) + { + case ExpressionType.GreaterThan: + return Expression.LessThanOrEqual(binaryOperand.Left, binaryOperand.Right); + case ExpressionType.GreaterThanOrEqual: + return Expression.LessThan(binaryOperand.Left, binaryOperand.Right); + case ExpressionType.LessThan: + return Expression.GreaterThanOrEqual(binaryOperand.Left, binaryOperand.Right); + case ExpressionType.LessThanOrEqual: + return Expression.GreaterThan(binaryOperand.Left, binaryOperand.Right); + case ExpressionType.Equal: + return Expression.NotEqual(binaryOperand.Left, binaryOperand.Right); + case ExpressionType.NotEqual: + return Expression.Equal(binaryOperand.Left, binaryOperand.Right); + } + } + + var methodCallOperand = visitedOperand as MethodCallExpression; + if (methodCallOperand != null) + { + if (methodCallOperand.Method.IsGenericMethod) + { + if (methodCallOperand.Method.GetGenericMethodDefinition() == containsMethod) + { + var genericNotContains = notContainsMethod.MakeGenericMethod( + methodCallOperand.Method.GetGenericArguments()); + return Expression.Call(genericNotContains, methodCallOperand.Arguments.ToArray()); + } + if (methodCallOperand.Method.GetGenericMethodDefinition() == notContainsMethod) + { + var genericContains = containsMethod.MakeGenericMethod( + methodCallOperand.Method.GetGenericArguments()); + return Expression.Call(genericContains, methodCallOperand.Arguments.ToArray()); + } + } + if (methodCallOperand.Method == containsKeyMethod) + { + return Expression.Call(notContainsKeyMethod, methodCallOperand.Arguments.ToArray()); + } + if (methodCallOperand.Method == notContainsKeyMethod) + { + return Expression.Call(containsKeyMethod, methodCallOperand.Arguments.ToArray()); + } + } + } + return base.VisitUnary(node); + } + + /// + /// Normalizes .Equals into == and Contains() into the appropriate stub. + /// + protected override Expression VisitMethodCall(MethodCallExpression node) + { + // Convert .Equals() into == + if (node.Method.Name == "Equals" && + node.Method.ReturnType == typeof(bool) && + node.Method.GetParameters().Length == 1) + { + var obj = new ObjectNormalizer().Visit(node.Object) as MethodCallExpression; + var parameter = new ObjectNormalizer().Visit(node.Arguments[0]) as MethodCallExpression; + if ((IsAVObjectGet(obj) && (obj.Object is ParameterExpression)) || + (IsAVObjectGet(parameter) && (parameter.Object is ParameterExpression))) + { + return Expression.Equal(node.Object, node.Arguments[0]); + } + } + + // Convert the .Contains() into a ContainsStub + if (node.Method != stringContains && + node.Method.Name == "Contains" && + node.Method.ReturnType == typeof(bool) && + node.Method.GetParameters().Length <= 2) + { + var collection = node.Method.GetParameters().Length == 1 ? + node.Object : + node.Arguments[0]; + var parameterIndex = node.Method.GetParameters().Length - 1; + var parameter = new ObjectNormalizer().Visit(node.Arguments[parameterIndex]) + as MethodCallExpression; + if (IsAVObjectGet(parameter) && (parameter.Object is ParameterExpression)) + { + var genericContains = containsMethod.MakeGenericMethod(parameter.Type); + return Expression.Call(genericContains, collection, parameter); + } + var target = new ObjectNormalizer().Visit(collection) as MethodCallExpression; + var element = node.Arguments[parameterIndex]; + if (IsAVObjectGet(target) && (target.Object is ParameterExpression)) + { + var genericContains = containsMethod.MakeGenericMethod(element.Type); + return Expression.Call(genericContains, target, element); + } + } + + // Convert obj["foo.bar"].ContainsKey("baz") into obj.ContainsKey("foo.bar.baz") + if (node.Method.Name == "ContainsKey" && + node.Method.ReturnType == typeof(bool) && + node.Method.GetParameters().Length == 1) + { + var getter = new ObjectNormalizer().Visit(node.Object) as MethodCallExpression; + Expression target = null; + string path = null; + if (IsAVObjectGet(getter) && getter.Object is ParameterExpression) + { + target = getter.Object; + path = GetValue(getter.Arguments[0]) + "." + GetValue(node.Arguments[0]); + return Expression.Call(containsKeyMethod, target, Expression.Constant(path)); + } + else if (node.Object is ParameterExpression) + { + target = node.Object; + path = GetValue(node.Arguments[0]) as string; + } + if (target != null && path != null) + { + return Expression.Call(containsKeyMethod, target, Expression.Constant(path)); + } + } + return base.VisitMethodCall(node); + } + } + + /// + /// Converts a normalized method call expression into the appropriate AVQuery clause. + /// + private static AVQuery WhereMethodCall( + this AVQuery source, Expression> expression, MethodCallExpression node) + where T : AVObject + { + if (IsAVObjectGet(node) && (node.Type == typeof(bool) || node.Type == typeof(bool?))) + { + // This is a raw boolean field access like 'where obj.Get("foo")' + return source.WhereEqualTo(GetValue(node.Arguments[0]) as string, true); + } + + MethodInfo translatedMethod; + if (functionMappings.TryGetValue(node.Method, out translatedMethod)) + { + var objTransformed = new ObjectNormalizer().Visit(node.Object) as MethodCallExpression; + if (!(IsAVObjectGet(objTransformed) && + objTransformed.Object == expression.Parameters[0])) + { + throw new InvalidOperationException( + "The left-hand side of a supported function call must be a AVObject field access."); + } + var fieldPath = GetValue(objTransformed.Arguments[0]); + var containedIn = GetValue(node.Arguments[0]); + var queryType = translatedMethod.DeclaringType.GetGenericTypeDefinition() + .MakeGenericType(typeof(T)); + translatedMethod = ReflectionHelpers.GetMethod(queryType, + translatedMethod.Name, + translatedMethod.GetParameters().Select(p => p.ParameterType).ToArray()); + return translatedMethod.Invoke(source, new[] { fieldPath, containedIn }) as AVQuery; + } + + if (node.Arguments[0] == expression.Parameters[0]) + { + // obj.ContainsKey("foo") --> query.WhereExists("foo") + if (node.Method == containsKeyMethod) + { + return source.WhereExists(GetValue(node.Arguments[1]) as string); + } + // !obj.ContainsKey("foo") --> query.WhereDoesNotExist("foo") + if (node.Method == notContainsKeyMethod) + { + return source.WhereDoesNotExist(GetValue(node.Arguments[1]) as string); + } + } + + if (node.Method.IsGenericMethod) + { + if (node.Method.GetGenericMethodDefinition() == containsMethod) + { + // obj.Get>("path").Contains(someValue) + if (IsAVObjectGet(node.Arguments[0] as MethodCallExpression)) + { + return source.WhereEqualTo( + GetValue(((MethodCallExpression)node.Arguments[0]).Arguments[0]) as string, + GetValue(node.Arguments[1])); + } + // someList.Contains(obj.Get("path")) + if (IsAVObjectGet(node.Arguments[1] as MethodCallExpression)) + { + var collection = GetValue(node.Arguments[0]) as System.Collections.IEnumerable; + return source.WhereContainedIn( + GetValue(((MethodCallExpression)node.Arguments[1]).Arguments[0]) as string, + collection.Cast()); + } + } + + if (node.Method.GetGenericMethodDefinition() == notContainsMethod) + { + // !obj.Get>("path").Contains(someValue) + if (IsAVObjectGet(node.Arguments[0] as MethodCallExpression)) + { + return source.WhereNotEqualTo( + GetValue(((MethodCallExpression)node.Arguments[0]).Arguments[0]) as string, + GetValue(node.Arguments[1])); + } + // !someList.Contains(obj.Get("path")) + if (IsAVObjectGet(node.Arguments[1] as MethodCallExpression)) + { + var collection = GetValue(node.Arguments[0]) as System.Collections.IEnumerable; + return source.WhereNotContainedIn( + GetValue(((MethodCallExpression)node.Arguments[1]).Arguments[0]) as string, + collection.Cast()); + } + } + } + throw new InvalidOperationException(node.Method + " is not a supported method call in a where expression."); + } + + /// + /// Converts a normalized binary expression into the appropriate AVQuery clause. + /// + private static AVQuery WhereBinaryExpression( + this AVQuery source, Expression> expression, BinaryExpression node) + where T : AVObject + { + var leftTransformed = new ObjectNormalizer().Visit(node.Left) as MethodCallExpression; + + if (!(IsAVObjectGet(leftTransformed) && + leftTransformed.Object == expression.Parameters[0])) + { + throw new InvalidOperationException( + "Where expressions must have one side be a field operation on a AVObject."); + } + + var fieldPath = GetValue(leftTransformed.Arguments[0]) as string; + var filterValue = GetValue(node.Right); + + if (filterValue != null && !AVEncoder.IsValidType(filterValue)) + { + throw new InvalidOperationException( + "Where clauses must use types compatible with AVObjects."); + } + + switch (node.NodeType) + { + case ExpressionType.GreaterThan: + return source.WhereGreaterThan(fieldPath, filterValue); + case ExpressionType.GreaterThanOrEqual: + return source.WhereGreaterThanOrEqualTo(fieldPath, filterValue); + case ExpressionType.LessThan: + return source.WhereLessThan(fieldPath, filterValue); + case ExpressionType.LessThanOrEqual: + return source.WhereLessThanOrEqualTo(fieldPath, filterValue); + case ExpressionType.Equal: + return source.WhereEqualTo(fieldPath, filterValue); + case ExpressionType.NotEqual: + return source.WhereNotEqualTo(fieldPath, filterValue); + default: + throw new InvalidOperationException( + "Where expressions do not support this operator."); + } + } + + /// + /// Filters a query based upon the predicate provided. + /// + /// The type of AVObject being queried for. + /// The base to which + /// the predicate will be added. + /// A function to test each AVObject for a condition. + /// The predicate must be able to be represented by one of the standard Where + /// functions on AVQuery + /// A new AVQuery whose results will match the given predicate as + /// well as the Source's filters. + public static AVQuery Where( + this AVQuery source, Expression> predicate) + where TSource : AVObject + { + // Handle top-level logic operators && and || + var binaryExpression = predicate.Body as BinaryExpression; + if (binaryExpression != null) + { + if (binaryExpression.NodeType == ExpressionType.AndAlso) + { + return source + .Where(Expression.Lambda>( + binaryExpression.Left, predicate.Parameters)) + .Where(Expression.Lambda>( + binaryExpression.Right, predicate.Parameters)); + } + if (binaryExpression.NodeType == ExpressionType.OrElse) + { + var left = source.Where(Expression.Lambda>( + binaryExpression.Left, predicate.Parameters)); + var right = source.Where(Expression.Lambda>( + binaryExpression.Right, predicate.Parameters)); + return left.Or(right); + } + } + + var normalized = new WhereNormalizer().Visit(predicate.Body); + + var methodCallExpr = normalized as MethodCallExpression; + if (methodCallExpr != null) + { + return source.WhereMethodCall(predicate, methodCallExpr); + } + + var binaryExpr = normalized as BinaryExpression; + if (binaryExpr != null) + { + return source.WhereBinaryExpression(predicate, binaryExpr); + } + + var unaryExpr = normalized as UnaryExpression; + if (unaryExpr != null && unaryExpr.NodeType == ExpressionType.Not) + { + var node = unaryExpr.Operand as MethodCallExpression; + if (IsAVObjectGet(node) && (node.Type == typeof(bool) || node.Type == typeof(bool?))) + { + // This is a raw boolean field access like 'where !obj.Get("foo")' + return source.WhereNotEqualTo(GetValue(node.Arguments[0]) as string, true); + } + } + + throw new InvalidOperationException( + "Encountered an unsupported expression for AVQueries."); + } + + /// + /// Normalizes an OrderBy's keySelector expression and then extracts the path + /// from the AVObject.Get() call. + /// + private static string GetOrderByPath( + Expression> keySelector) + { + string result = null; + var normalized = new ObjectNormalizer().Visit(keySelector.Body); + var callExpr = normalized as MethodCallExpression; + if (IsAVObjectGet(callExpr) && callExpr.Object == keySelector.Parameters[0]) + { + // We're operating on the parameter + result = GetValue(callExpr.Arguments[0]) as string; + } + if (result == null) + { + throw new InvalidOperationException( + "OrderBy expression must be a field access on a AVObject."); + } + return result; + } + + /// + /// Orders a query based upon the key selector provided. + /// + /// The type of AVObject being queried for. + /// The type of key returned by keySelector. + /// The query to order. + /// A function to extract a key from the AVObject. + /// A new AVQuery based on Source whose results will be ordered by + /// the key specified in the keySelector. + public static AVQuery OrderBy( + this AVQuery source, Expression> keySelector) + where TSource : AVObject + { + return source.OrderBy(GetOrderByPath(keySelector)); + } + + /// + /// Orders a query based upon the key selector provided. + /// + /// The type of AVObject being queried for. + /// The type of key returned by keySelector. + /// The query to order. + /// A function to extract a key from the AVObject. + /// A new AVQuery based on Source whose results will be ordered by + /// the key specified in the keySelector. + public static AVQuery OrderByDescending( + this AVQuery source, Expression> keySelector) + where TSource : AVObject + { + return source.OrderByDescending(GetOrderByPath(keySelector)); + } + + /// + /// Performs a subsequent ordering of a query based upon the key selector provided. + /// + /// The type of AVObject being queried for. + /// The type of key returned by keySelector. + /// The query to order. + /// A function to extract a key from the AVObject. + /// A new AVQuery based on Source whose results will be ordered by + /// the key specified in the keySelector. + public static AVQuery ThenBy( + this AVQuery source, Expression> keySelector) + where TSource : AVObject + { + return source.ThenBy(GetOrderByPath(keySelector)); + } + + /// + /// Performs a subsequent ordering of a query based upon the key selector provided. + /// + /// The type of AVObject being queried for. + /// The type of key returned by keySelector. + /// The query to order. + /// A function to extract a key from the AVObject. + /// A new AVQuery based on Source whose results will be ordered by + /// the key specified in the keySelector. + public static AVQuery ThenByDescending( + this AVQuery source, Expression> keySelector) + where TSource : AVObject + { + return source.ThenByDescending(GetOrderByPath(keySelector)); + } + + /// + /// Correlates the elements of two queries based on matching keys. + /// + /// The type of AVObjects of the first query. + /// The type of AVObjects of the second query. + /// The type of the keys returned by the key selector + /// functions. + /// The type of the result. This must match either + /// TOuter or TInner + /// The first query to join. + /// The query to join to the first query. + /// A function to extract a join key from the results of + /// the first query. + /// A function to extract a join key from the results of + /// the second query. + /// A function to select either the outer or inner query + /// result to determine which query is the base query. + /// A new AVQuery with a WhereMatchesQuery or WhereMatchesKeyInQuery + /// clause based upon the query indicated in the . + public static AVQuery Join( + this AVQuery outer, + AVQuery inner, + Expression> outerKeySelector, + Expression> innerKeySelector, + Expression> resultSelector) + where TOuter : AVObject + where TInner : AVObject + where TResult : AVObject + { + // resultSelector must select either the inner object or the outer object. If it's the inner + // object, reverse the query. + if (resultSelector.Body == resultSelector.Parameters[1]) + { + // The inner object was selected. + return inner.Join( + outer, + innerKeySelector, + outerKeySelector, + (i, o) => i) as AVQuery; + } + if (resultSelector.Body != resultSelector.Parameters[0]) + { + throw new InvalidOperationException("Joins must select either the outer or inner object."); + } + + // Normalize both selectors + Expression outerNormalized = new ObjectNormalizer().Visit(outerKeySelector.Body); + Expression innerNormalized = new ObjectNormalizer().Visit(innerKeySelector.Body); + MethodCallExpression outerAsGet = outerNormalized as MethodCallExpression; + MethodCallExpression innerAsGet = innerNormalized as MethodCallExpression; + if (IsAVObjectGet(outerAsGet) && outerAsGet.Object == outerKeySelector.Parameters[0]) + { + var outerKey = GetValue(outerAsGet.Arguments[0]) as string; + + if (IsAVObjectGet(innerAsGet) && innerAsGet.Object == innerKeySelector.Parameters[0]) + { + // Both are key accesses, so treat this as a WhereMatchesKeyInQuery + var innerKey = GetValue(innerAsGet.Arguments[0]) as string; + return outer.WhereMatchesKeyInQuery(outerKey, innerKey, inner) as AVQuery; + } + + if (innerKeySelector.Body == innerKeySelector.Parameters[0]) + { + // The inner selector is on the result of the query itself, so treat this as a + // WhereMatchesQuery + return outer.WhereMatchesQuery(outerKey, inner) as AVQuery; + } + throw new InvalidOperationException( + "The key for the joined object must be a AVObject or a field access " + + "on the AVObject."); + } + + // TODO (hallucinogen): If we ever support "and" queries fully and/or support a "where this object + // matches some key in some other query" (as opposed to requiring a key on this query), we + // can add support for even more types of joins. + + throw new InvalidOperationException( + "The key for the selected object must be a field access on the AVObject."); + } + } +} diff --git a/Storage/Storage/Public/AVRelation.cs b/Storage/Storage/Public/AVRelation.cs new file mode 100644 index 0000000..fa7f13e --- /dev/null +++ b/Storage/Storage/Public/AVRelation.cs @@ -0,0 +1,175 @@ +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; + +namespace LeanCloud +{ + /// + /// A common base class for AVRelations. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract class AVRelationBase : IJsonConvertible + { + private AVObject parent; + private string key; + private string targetClassName; + + internal AVRelationBase(AVObject parent, string key) + { + EnsureParentAndKey(parent, key); + } + + internal AVRelationBase(AVObject parent, string key, string targetClassName) + : this(parent, key) + { + this.targetClassName = targetClassName; + } + + internal static IObjectSubclassingController SubclassingController + { + get + { + return AVPlugins.Instance.SubclassingController; + } + } + + internal void EnsureParentAndKey(AVObject parent, string key) + { + this.parent = this.parent ?? parent; + this.key = this.key ?? key; + Debug.Assert(this.parent == parent, "Relation retrieved from two different objects"); + Debug.Assert(this.key == key, "Relation retrieved from two different keys"); + } + + internal void Add(AVObject obj) + { + var change = new AVRelationOperation(new[] { obj }, null); + parent.PerformOperation(key, change); + targetClassName = change.TargetClassName; + } + + internal void Remove(AVObject obj) + { + var change = new AVRelationOperation(null, new[] { obj }); + parent.PerformOperation(key, change); + targetClassName = change.TargetClassName; + } + + IDictionary IJsonConvertible.ToJSON() + { + return new Dictionary { + { "__type", "Relation"}, + { "className", targetClassName} + }; + } + + internal AVQuery GetQuery() where T : AVObject + { + if (targetClassName != null) + { + return new AVQuery(targetClassName) + .WhereRelatedTo(parent, key); + } + + return new AVQuery(parent.ClassName) + .RedirectClassName(key) + .WhereRelatedTo(parent, key); + } + + internal AVQuery GetReverseQuery(T target) where T : AVObject + { + if (target.ObjectId == null) + { + throw new ArgumentNullException("target.ObjectId", "can not query a relation without target ObjectId."); + } + + return new AVQuery(parent.ClassName).WhereEqualTo(key, target); + } + + internal string TargetClassName + { + get + { + return targetClassName; + } + set + { + targetClassName = value; + } + } + + /// + /// Produces the proper AVRelation<T> instance for the given classname. + /// + internal static AVRelationBase CreateRelation(AVObject parent, + string key, + string targetClassName) + { + var targetType = SubclassingController.GetType(targetClassName) ?? typeof(AVObject); + + Expression>> createRelationExpr = + () => CreateRelation(parent, key, targetClassName); + var createRelationMethod = + ((MethodCallExpression)createRelationExpr.Body) + .Method + .GetGenericMethodDefinition() + .MakeGenericMethod(targetType); + return (AVRelationBase)createRelationMethod.Invoke(null, new object[] { parent, key, targetClassName }); + } + + private static AVRelation CreateRelation(AVObject parent, string key, string targetClassName) + where T : AVObject + { + return new AVRelation(parent, key, targetClassName); + } + } + + /// + /// Provides access to all of the children of a many-to-many relationship. Each instance of + /// AVRelation is associated with a particular parent and key. + /// + /// The type of the child objects. + public sealed class AVRelation : AVRelationBase where T : AVObject + { + + internal AVRelation(AVObject parent, string key) : base(parent, key) { } + + internal AVRelation(AVObject parent, string key, string targetClassName) + : base(parent, key, targetClassName) { } + + /// + /// Adds an object to this relation. The object must already have been saved. + /// + /// The object to add. + public void Add(T obj) + { + base.Add(obj); + } + + /// + /// Removes an object from this relation. The object must already have been saved. + /// + /// The object to remove. + public void Remove(T obj) + { + base.Remove(obj); + } + + /// + /// Gets a query that can be used to query the objects in this relation. + /// + public AVQuery Query + { + get + { + return base.GetQuery(); + } + } + } +} diff --git a/Storage/Storage/Public/AVRole.cs b/Storage/Storage/Public/AVRole.cs new file mode 100644 index 0000000..848832b --- /dev/null +++ b/Storage/Storage/Public/AVRole.cs @@ -0,0 +1,111 @@ +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace LeanCloud +{ + /// + /// Represents a Role on the LeanCloud server. AVRoles represent groupings + /// of s for the purposes of granting permissions (e.g. + /// specifying a for a . Roles + /// are specified by their sets of child users and child roles, all of which are granted + /// any permissions that the parent role has. + /// + /// Roles must have a name (that cannot be changed after creation of the role), + /// and must specify an ACL. + /// + [AVClassName("_Role")] + public class AVRole : AVObject + { + private static readonly Regex namePattern = new Regex("^[0-9a-zA-Z_\\- ]+$"); + + /// + /// Constructs a new AVRole. You must assign a name and ACL to the role. + /// + public AVRole() : base() { } + + /// + /// Constructs a new AVRole with the given name. + /// + /// The name of the role to create. + /// The ACL for this role. Roles must have an ACL. + public AVRole(string name, AVACL acl) + : this() + { + Name = name; + ACL = acl; + } + + /// + /// Gets the name of the role. + /// + [AVFieldName("name")] + public string Name + { + get { return GetProperty("Name"); } + set { SetProperty(value, "Name"); } + } + + /// + /// Gets the for the s that are + /// direct children of this role. These users are granted any privileges that + /// this role has been granted (e.g. read or write access through ACLs). You can + /// add or remove child users from the role through this relation. + /// + [AVFieldName("users")] + public AVRelation Users + { + get { return GetRelationProperty("Users"); } + } + + /// + /// Gets the for the s that are + /// direct children of this role. These roles' users are granted any privileges that + /// this role has been granted (e.g. read or write access through ACLs). You can + /// add or remove child roles from the role through this relation. + /// + [AVFieldName("roles")] + public AVRelation Roles + { + get { return GetRelationProperty("Roles"); } + } + + internal override void OnSettingValue(ref string key, ref object value) + { + base.OnSettingValue(ref key, ref value); + if (key == "name") + { + if (ObjectId != null) + { + throw new InvalidOperationException( + "A role's name can only be set before it has been saved."); + } + if (!(value is string)) + { + throw new ArgumentException("A role's name must be a string.", "value"); + } + if (!namePattern.IsMatch((string)value)) + { + throw new ArgumentException( + "A role's name can only contain alphanumeric characters, _, -, and spaces.", + "value"); + } + } + } + + /// + /// Gets a over the Role collection. + /// + public static AVQuery Query + { + get + { + return new AVQuery(); + } + } + } +} diff --git a/Storage/Storage/Public/AVSession.cs b/Storage/Storage/Public/AVSession.cs new file mode 100644 index 0000000..e915b40 --- /dev/null +++ b/Storage/Storage/Public/AVSession.cs @@ -0,0 +1,111 @@ +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud +{ + /// + /// Represents a session of a user for a LeanCloud application. + /// + [AVClassName("_Session")] + public class AVSession : AVObject + { + private static readonly HashSet readOnlyKeys = new HashSet { + "sessionToken", "createdWith", "restricted", "user", "expiresAt", "installationId" + }; + + protected override bool IsKeyMutable(string key) + { + return !readOnlyKeys.Contains(key); + } + + /// + /// Gets the session token for a user, if they are logged in. + /// + [AVFieldName("sessionToken")] + public string SessionToken + { + get { return GetProperty(null, "SessionToken"); } + } + + /// + /// Constructs a for AVSession. + /// + public static AVQuery Query + { + get + { + return new AVQuery(); + } + } + + internal static IAVSessionController SessionController + { + get + { + return AVPlugins.Instance.SessionController; + } + } + + /// + /// Gets the current object related to the current user. + /// + public static Task GetCurrentSessionAsync() + { + return GetCurrentSessionAsync(CancellationToken.None); + } + + /// + /// Gets the current object related to the current user. + /// + /// The cancellation token + public static Task GetCurrentSessionAsync(CancellationToken cancellationToken) + { + return AVUser.GetCurrentUserAsync().OnSuccess(t1 => + { + AVUser user = t1.Result; + if (user == null) + { + return Task.FromResult((AVSession)null); + } + + string sessionToken = user.SessionToken; + if (sessionToken == null) + { + return Task.FromResult((AVSession)null); + } + + return SessionController.GetSessionAsync(sessionToken, cancellationToken).OnSuccess(t => + { + AVSession session = AVObject.FromState(t.Result, "_Session"); + return session; + }); + }).Unwrap(); + } + + internal static Task RevokeAsync(string sessionToken, CancellationToken cancellationToken) + { + if (sessionToken == null || !SessionController.IsRevocableSessionToken(sessionToken)) + { + return Task.FromResult(0); + } + return SessionController.RevokeAsync(sessionToken, cancellationToken); + } + + internal static Task UpgradeToRevocableSessionAsync(string sessionToken, CancellationToken cancellationToken) + { + if (sessionToken == null || SessionController.IsRevocableSessionToken(sessionToken)) + { + return Task.FromResult(sessionToken); + } + + return SessionController.UpgradeToRevocableSessionAsync(sessionToken, cancellationToken).OnSuccess(t => + { + AVSession session = AVObject.FromState(t.Result, "_Session"); + return session.SessionToken; + }); + } + } +} diff --git a/Storage/Storage/Public/AVStatus.cs b/Storage/Storage/Public/AVStatus.cs new file mode 100644 index 0000000..7a7bc65 --- /dev/null +++ b/Storage/Storage/Public/AVStatus.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LeanCloud +{ + /// + /// 事件流系统中的一条状态 + /// + [AVClassName("_Status")] + public class AVStatus : AVObject + { + private static readonly HashSet readOnlyKeys = new HashSet { + "messageId", "inboxType", "data","Source" + }; + + protected override bool IsKeyMutable(string key) + { + return !readOnlyKeys.Contains(key); + } + } +} diff --git a/Storage/Storage/Public/AVUploadProgressEventArgs.cs b/Storage/Storage/Public/AVUploadProgressEventArgs.cs new file mode 100644 index 0000000..dfa3112 --- /dev/null +++ b/Storage/Storage/Public/AVUploadProgressEventArgs.cs @@ -0,0 +1,15 @@ +using System; + +namespace LeanCloud { + /// + /// Represents upload progress. + /// + public class AVUploadProgressEventArgs : EventArgs { + public AVUploadProgressEventArgs() { } + + /// + /// Gets the progress (a number between 0.0 and 1.0) of an upload. + /// + public double Progress { get; set; } + } +} diff --git a/Storage/Storage/Public/AVUser.cs b/Storage/Storage/Public/AVUser.cs new file mode 100644 index 0000000..62aee39 --- /dev/null +++ b/Storage/Storage/Public/AVUser.cs @@ -0,0 +1,1544 @@ +using LeanCloud.Storage.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace LeanCloud +{ + /// + /// Represents a user for a LeanCloud application. + /// + [AVClassName("_User")] + public class AVUser : AVObject + { + private static readonly IDictionary authProviders = + new Dictionary(); + + private static readonly HashSet readOnlyKeys = new HashSet { + "sessionToken", "isNew" + }; + + internal static IAVUserController UserController + { + get + { + return AVPlugins.Instance.UserController; + } + } + + internal static IAVCurrentUserController CurrentUserController + { + get + { + return AVPlugins.Instance.CurrentUserController; + } + } + + /// + /// Whether the AVUser has been authenticated on this device. Only an authenticated + /// AVUser can be saved and deleted. + /// + [Obsolete("This property is deprecated, please use IsAuthenticatedAsync instead.")] + public bool IsAuthenticated + { + get + { + lock (mutex) + { + return SessionToken != null && + CurrentUser != null && + CurrentUser.ObjectId == ObjectId; + } + } + } + + /// + /// Whether the AVUser has been authenticated on this device, and the AVUser's session token is expired. + /// Only an authenticated AVUser can be saved and deleted. + /// + public Task IsAuthenticatedAsync() + { + lock (mutex) + { + if (SessionToken == null || CurrentUser == null || CurrentUser.ObjectId != ObjectId) + { + return Task.FromResult(false); + } + } + var command = new AVCommand(String.Format("users/me?session_token={0}", CurrentSessionToken), + method: "GET", + data: null); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + /// + /// Refresh this user's session token, and current session token will be invalid. + /// + public Task RefreshSessionTokenAsync(CancellationToken cancellationToken) + { + return UserController.RefreshSessionTokenAsync(ObjectId, SessionToken, cancellationToken).OnSuccess(t => + { + var serverState = t.Result; + HandleSave(serverState); + }); + } + + /// + /// Removes a key from the object's data if it exists. + /// + /// The key to remove. + /// Cannot remove the username key. + public override void Remove(string key) + { + if (key == "username") + { + throw new ArgumentException("Cannot remove the username key."); + } + base.Remove(key); + } + + protected override bool IsKeyMutable(string key) + { + return !readOnlyKeys.Contains(key); + } + + internal override void HandleSave(IObjectState serverState) + { + base.HandleSave(serverState); + + SynchronizeAllAuthData(); + CleanupAuthData(); + + MutateState(mutableClone => + { + mutableClone.ServerData.Remove("password"); + }); + } + + /// + /// authenticated token. + /// + public string SessionToken + { + get + { + if (State.ContainsKey("sessionToken")) + { + return State["sessionToken"] as string; + } + return null; + } + } + + internal static string CurrentSessionToken + { + get + { + Task sessionTokenTask = GetCurrentSessionTokenAsync(); + sessionTokenTask.Wait(); + return sessionTokenTask.Result; + } + } + + internal static Task GetCurrentSessionTokenAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + return CurrentUserController.GetCurrentSessionTokenAsync(cancellationToken); + } + + internal Task SetSessionTokenAsync(string newSessionToken) + { + return SetSessionTokenAsync(newSessionToken, CancellationToken.None); + } + + internal Task SetSessionTokenAsync(string newSessionToken, CancellationToken cancellationToken) + { + MutateState(mutableClone => + { + mutableClone.ServerData["sessionToken"] = newSessionToken; + }); + + return SaveCurrentUserAsync(this); + } + + /// + /// Gets or sets the username. + /// + [AVFieldName("username")] + public string Username + { + get { return GetProperty(null, "Username"); } + set { SetProperty(value, "Username"); } + } + + /// + /// Sets the password. + /// + [AVFieldName("password")] + public string Password + { + private get { return GetProperty(null, "Password"); } + set { SetProperty(value, "Password"); } + } + + /// + /// Sets the email address. + /// + [AVFieldName("email")] + public string Email + { + get { return GetProperty(null, "Email"); } + set { SetProperty(value, "Email"); } + } + + /// + /// 用户手机号。 + /// + [AVFieldName("mobilePhoneNumber")] + public string MobilePhoneNumber + { + get + { + return GetProperty(null, "MobilePhoneNumber"); + } + set + { + SetProperty(value, "MobilePhoneNumber"); + } + } + + /// + /// 用户手机号是否已经验证 + /// + /// true if mobile phone verified; otherwise, false. + [AVFieldName("mobilePhoneVerified")] + public bool MobilePhoneVerified + { + get + { + return GetProperty(false, "MobilePhoneVerified"); + } + set + { + SetProperty(value, "MobilePhoneVerified"); + } + } + + /// + /// 判断用户是否为匿名用户 + /// + public bool IsAnonymous + { + get + { + bool rtn = false; + if (this.AuthData != null) + { + rtn = this.AuthData.Keys.Contains("anonymous"); + } + return rtn; + } + } + + internal Task SignUpAsync(Task toAwait, CancellationToken cancellationToken) + { + return this.Create(toAwait, cancellationToken).OnSuccess(_ => SaveCurrentUserAsync(this)).Unwrap(); + } + + /// + /// Signs up a new user. This will create a new AVUser on the server and will also persist the + /// session on disk so that you can access the user using . A username and + /// password must be set before calling SignUpAsync. + /// + public Task SignUpAsync() + { + return SignUpAsync(CancellationToken.None); + } + + /// + /// Signs up a new user. This will create a new AVUser on the server and will also persist the + /// session on disk so that you can access the user using . A username and + /// password must be set before calling SignUpAsync. + /// + /// The cancellation token. + public Task SignUpAsync(CancellationToken cancellationToken) + { + return taskQueue.Enqueue(toAwait => SignUpAsync(toAwait, cancellationToken), + cancellationToken); + } + + #region 事件流系统相关 API + + /// + /// 关注某个用户 + /// + /// 被关注的用户 + /// + public Task FollowAsync(string userObjectId) + { + return this.FollowAsync(userObjectId, null); + } + + /// + /// 关注某个用户 + /// + /// 被关注的用户Id + /// 关注的时候附加属性 + /// + public Task FollowAsync(string userObjectId, IDictionary data) + { + if (data != null) + { + data = this.EncodeForSaving(data); + } + var command = new AVCommand(string.Format("users/{0}/friendship/{1}", this.ObjectId, userObjectId), + method: "POST", + sessionToken: CurrentSessionToken, + data: data); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + /// + /// 取关某一个用户 + /// + /// + /// + public Task UnfollowAsync(string userObjectId) + { + var command = new AVCommand(string.Format("users/{0}/friendship/{1}", this.ObjectId, userObjectId), + method: "DELETE", + sessionToken: CurrentSessionToken, + data: null); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + /// + /// 获取当前用户的关注者的查询 + /// + /// + public AVQuery GetFollowerQuery() + { + AVQuery query = new AVQuery(); + query.RelativeUri = string.Format("users/{0}/followers", this.ObjectId); + return query; + } + + /// + /// 获取当前用户所关注的用户的查询 + /// + /// + public AVQuery GetFolloweeQuery() + { + AVQuery query = new AVQuery(); + query.RelativeUri = string.Format("users/{0}/followees", this.ObjectId); + return query; + } + + /// + /// 同时查询关注了当前用户的关注者和当前用户所关注的用户 + /// + /// + public AVQuery GetFollowersAndFolloweesQuery() + { + AVQuery query = new AVQuery(); + query.RelativeUri = string.Format("users/{0}/followersAndFollowees", this.ObjectId); + return query; + } + + /// + /// 获取当前用户的关注者 + /// + /// + public Task> GetFollowersAsync() + { + return this.GetFollowerQuery().FindAsync(); + } + + /// + /// 获取当前用户所关注的用户 + /// + /// + public Task> GetFolloweesAsync() + { + return this.GetFolloweeQuery().FindAsync(); + } + + + //public Task SendStatusAsync() + //{ + + //} + + #endregion + + /// + /// Logs in a user with a username and password. On success, this saves the session to disk so you + /// can retrieve the currently logged in user using . + /// + /// The username to log in with. + /// The password to log in with. + /// The newly logged-in user. + public static Task LogInAsync(string username, string password) + { + return LogInAsync(username, password, CancellationToken.None); + } + + /// + /// Logs in a user with a username and password. On success, this saves the session to disk so you + /// can retrieve the currently logged in user using . + /// + /// The username to log in with. + /// The password to log in with. + /// The cancellation token. + /// The newly logged-in user. + public static Task LogInAsync(string username, + string password, + CancellationToken cancellationToken) + { + return UserController.LogInAsync(username, null, password, cancellationToken).OnSuccess(t => + { + AVUser user = AVObject.FromState(t.Result, "_User"); + return SaveCurrentUserAsync(user).OnSuccess(_ => user); + }).Unwrap(); + } + + /// + /// Logs in a user with a username and password. On success, this saves the session to disk so you + /// can retrieve the currently logged in user using . + /// + /// The session token to authorize with + /// The user if authorization was successful + public static Task BecomeAsync(string sessionToken) + { + return BecomeAsync(sessionToken, CancellationToken.None); + } + + /// + /// Logs in a user with a username and password. On success, this saves the session to disk so you + /// can retrieve the currently logged in user using . + /// + /// The session token to authorize with + /// The cancellation token. + /// The user if authorization was successful + public static Task BecomeAsync(string sessionToken, CancellationToken cancellationToken) + { + return UserController.GetUserAsync(sessionToken, cancellationToken).OnSuccess(t => + { + AVUser user = AVObject.FromState(t.Result, "_User"); + return SaveCurrentUserAsync(user).OnSuccess(_ => user); + }).Unwrap(); + } + + protected override Task SaveAsync(Task toAwait, CancellationToken cancellationToken) + { + lock (mutex) + { + if (ObjectId == null) + { + throw new InvalidOperationException("You must call SignUpAsync before calling SaveAsync."); + } + return base.SaveAsync(toAwait, cancellationToken).OnSuccess(_ => + { + if (!CurrentUserController.IsCurrent(this)) + { + return Task.FromResult(0); + } + return SaveCurrentUserAsync(this); + }).Unwrap(); + } + } + + internal override Task FetchAsyncInternal(Task toAwait, IDictionary queryString, CancellationToken cancellationToken) + { + return base.FetchAsyncInternal(toAwait, queryString, cancellationToken).OnSuccess(t => + { + if (!CurrentUserController.IsCurrent(this)) + { + return Task.FromResult(t.Result); + } + // If this is already the current user, refresh its state on disk. + return SaveCurrentUserAsync(this).OnSuccess(_ => t.Result); + }).Unwrap(); + } + + /// + /// Logs out the currently logged in user session. This will remove the session from disk, log out of + /// linked services, and future calls to will return null. + /// + /// + /// Typically, you should use , unless you are managing your own threading. + /// + public static void LogOut() + { + // TODO (hallucinogen): this will without a doubt fail in Unity. But what else can we do? + LogOutAsync().Wait(); + } + + /// + /// Logs out the currently logged in user session. This will remove the session from disk, log out of + /// linked services, and future calls to will return null. + /// + /// + /// This is preferable to using , unless your code is already running from a + /// background thread. + /// + public static Task LogOutAsync() + { + return LogOutAsync(CancellationToken.None); + } + + /// + /// Logs out the currently logged in user session. This will remove the session from disk, log out of + /// linked services, and future calls to will return null. + /// + /// This is preferable to using , unless your code is already running from a + /// background thread. + /// + public static Task LogOutAsync(CancellationToken cancellationToken) + { + return GetCurrentUserAsync().OnSuccess(t => + { + LogOutWithProviders(); + + AVUser user = t.Result; + if (user == null) + { + return Task.FromResult(0); + } + + return user.taskQueue.Enqueue(toAwait => user.LogOutAsync(toAwait, cancellationToken), cancellationToken); + }).Unwrap(); + } + + internal Task LogOutAsync(Task toAwait, CancellationToken cancellationToken) + { + string oldSessionToken = SessionToken; + if (oldSessionToken == null) + { + return Task.FromResult(0); + } + + // Cleanup in-memory session. + MutateState(mutableClone => + { + mutableClone.ServerData.Remove("sessionToken"); + }); + var revokeSessionTask = AVSession.RevokeAsync(oldSessionToken, cancellationToken); + return Task.WhenAll(revokeSessionTask, CurrentUserController.LogOutAsync(cancellationToken)); + } + + private static void LogOutWithProviders() + { + foreach (var provider in authProviders.Values) + { + provider.Deauthenticate(); + } + } + + /// + /// Gets the currently logged in AVUser with a valid session, either from memory or disk + /// if necessary. + /// + public static AVUser CurrentUser + { + get + { + var userTask = GetCurrentUserAsync(); + // TODO (hallucinogen): this will without a doubt fail in Unity. How should we fix it? + userTask.Wait(); + return userTask.Result; + } + } + + public static Task GetCurrentAsync() + { + var userTask = GetCurrentUserAsync(); + return userTask; + } + + /// + /// Gets the currently logged in AVUser with a valid session, either from memory or disk + /// if necessary, asynchronously. + /// + public static Task GetCurrentUserAsync() + { + return GetCurrentUserAsync(CancellationToken.None); + } + + /// + /// Gets the currently logged in AVUser with a valid session, either from memory or disk + /// if necessary, asynchronously. + /// + internal static Task GetCurrentUserAsync(CancellationToken cancellationToken) + { + return CurrentUserController.GetAsync(cancellationToken); + } + + private static Task SaveCurrentUserAsync(AVUser user) + { + return SaveCurrentUserAsync(user, CancellationToken.None); + } + + private static Task SaveCurrentUserAsync(AVUser user, CancellationToken cancellationToken) + { + return CurrentUserController.SetAsync(user, cancellationToken); + } + + internal static void ClearInMemoryUser() + { + CurrentUserController.ClearFromMemory(); + } + + /// + /// Constructs a for AVUsers. + /// + public static AVQuery Query + { + get + { + return new AVQuery(); + } + } + + #region Legacy / Revocable Session Tokens + + private static readonly object isRevocableSessionEnabledMutex = new object(); + private static bool isRevocableSessionEnabled; + + /// + /// Tells server to use revocable session on LogIn and SignUp, even when App's Settings + /// has "Require Revocable Session" turned off. Issues network request in background to + /// migrate the sessionToken on disk to revocable session. + /// + /// The Task that upgrades the session. + public static Task EnableRevocableSessionAsync() + { + return EnableRevocableSessionAsync(CancellationToken.None); + } + + /// + /// Tells server to use revocable session on LogIn and SignUp, even when App's Settings + /// has "Require Revocable Session" turned off. Issues network request in background to + /// migrate the sessionToken on disk to revocable session. + /// + /// The Task that upgrades the session. + public static Task EnableRevocableSessionAsync(CancellationToken cancellationToken) + { + lock (isRevocableSessionEnabledMutex) + { + isRevocableSessionEnabled = true; + } + + return GetCurrentUserAsync(cancellationToken).OnSuccess(t => + { + var user = t.Result; + return user.UpgradeToRevocableSessionAsync(cancellationToken); + }); + } + + internal static void DisableRevocableSession() + { + lock (isRevocableSessionEnabledMutex) + { + isRevocableSessionEnabled = false; + } + } + + internal static bool IsRevocableSessionEnabled + { + get + { + lock (isRevocableSessionEnabledMutex) + { + return isRevocableSessionEnabled; + } + } + } + + internal Task UpgradeToRevocableSessionAsync() + { + return UpgradeToRevocableSessionAsync(CancellationToken.None); + } + + public Task UpgradeToRevocableSessionAsync(CancellationToken cancellationToken) + { + return taskQueue.Enqueue(toAwait => UpgradeToRevocableSessionAsync(toAwait, cancellationToken), + cancellationToken); + } + + internal Task UpgradeToRevocableSessionAsync(Task toAwait, CancellationToken cancellationToken) + { + string sessionToken = SessionToken; + + return toAwait.OnSuccess(_ => + { + return AVSession.UpgradeToRevocableSessionAsync(sessionToken, cancellationToken); + }).Unwrap().OnSuccess(t => + { + return SetSessionTokenAsync(t.Result); + }).Unwrap(); + } + + #endregion + + /// + /// Requests a password reset email to be sent to the specified email address associated with the + /// user account. This email allows the user to securely reset their password on the LeanCloud site. + /// + /// The email address associated with the user that forgot their password. + public static Task RequestPasswordResetAsync(string email) + { + return RequestPasswordResetAsync(email, CancellationToken.None); + } + + /// + /// Requests a password reset email to be sent to the specified email address associated with the + /// user account. This email allows the user to securely reset their password on the LeanCloud site. + /// + /// The email address associated with the user that forgot their password. + /// The cancellation token. + public static Task RequestPasswordResetAsync(string email, + CancellationToken cancellationToken) + { + return UserController.RequestPasswordResetAsync(email, cancellationToken); + } + + /// + /// Updates current user's password. Need the user's old password, + /// + /// The password. + /// Old password. + /// New password. + /// Cancellation token. + public Task UpdatePassword(string oldPassword, string newPassword, CancellationToken cancellationToken) + { + return UserController.UpdatePasswordAsync(ObjectId, SessionToken, oldPassword, newPassword, cancellationToken); + } + + /// + /// Gets the authData for this user. + /// + internal IDictionary> AuthData + { + get + { + IDictionary> authData; + if (this.TryGetValue>>( + "authData", out authData)) + { + return authData; + } + return null; + } + private set + { + this["authData"] = value; + } + } + + private static IAVAuthenticationProvider GetProvider(string providerName) + { + IAVAuthenticationProvider provider; + if (authProviders.TryGetValue(providerName, out provider)) + { + return provider; + } + return null; + } + + /// + /// Removes null values from authData (which exist temporarily for unlinking) + /// + private void CleanupAuthData() + { + lock (mutex) + { + if (!CurrentUserController.IsCurrent(this)) + { + return; + } + var authData = AuthData; + + if (authData == null) + { + return; + } + + foreach (var pair in new Dictionary>(authData)) + { + if (pair.Value == null) + { + authData.Remove(pair.Key); + } + } + } + } + + /// + /// Synchronizes authData for all providers. + /// + private void SynchronizeAllAuthData() + { + lock (mutex) + { + var authData = AuthData; + + if (authData == null) + { + return; + } + + foreach (var pair in authData) + { + SynchronizeAuthData(GetProvider(pair.Key)); + } + } + } + + private void SynchronizeAuthData(IAVAuthenticationProvider provider) + { + bool restorationSuccess = false; + lock (mutex) + { + var authData = AuthData; + if (authData == null || provider == null) + { + return; + } + IDictionary data; + if (authData.TryGetValue(provider.AuthType, out data)) + { + restorationSuccess = provider.RestoreAuthentication(data); + } + } + + if (!restorationSuccess) + { + this.UnlinkFromAsync(provider.AuthType, CancellationToken.None); + } + } + + public Task LinkWithAsync(string authType, IDictionary data, CancellationToken cancellationToken) + { + return taskQueue.Enqueue(toAwait => + { + AuthData = new Dictionary>(); + AuthData[authType] = data; + return SaveAsync(cancellationToken); + }, cancellationToken); + } + + public Task LinkWithAsync(string authType, CancellationToken cancellationToken) + { + var provider = GetProvider(authType); + return provider.AuthenticateAsync(cancellationToken) + .OnSuccess(t => LinkWithAsync(authType, t.Result, cancellationToken)) + .Unwrap(); + } + + /// + /// Unlinks a user from a service. + /// + public Task UnlinkFromAsync(string authType, CancellationToken cancellationToken) + { + return LinkWithAsync(authType, null, cancellationToken); + } + + /// + /// Checks whether a user is linked to a service. + /// + internal bool IsLinked(string authType) + { + lock (mutex) + { + return AuthData != null && AuthData.ContainsKey(authType) && AuthData[authType] != null; + } + } + + internal static Task LogInWithAsync(string authType, + IDictionary data, + bool failOnNotExist, + CancellationToken cancellationToken) + { + AVUser user = null; + + return UserController.LogInAsync(authType, data, failOnNotExist, cancellationToken).OnSuccess(t => + { + user = AVObject.FromState(t.Result, "_User"); + + lock (user.mutex) + { + if (user.AuthData == null) + { + user.AuthData = new Dictionary>(); + } + user.AuthData[authType] = data; + user.SynchronizeAllAuthData(); + } + + return SaveCurrentUserAsync(user); + }).Unwrap().OnSuccess(t => user); + } + + internal static Task LogInWithAsync(string authType, + CancellationToken cancellationToken) + { + var provider = GetProvider(authType); + return provider.AuthenticateAsync(cancellationToken) + .OnSuccess(authData => LogInWithAsync(authType, authData.Result, false, cancellationToken)) + .Unwrap(); + } + + internal static void RegisterProvider(IAVAuthenticationProvider provider) + { + authProviders[provider.AuthType] = provider; + var curUser = AVUser.CurrentUser; + if (curUser != null) + { + curUser.SynchronizeAuthData(provider); + } + } + + #region 手机号登录 + + internal static Task LogInWithParametersAsync(Dictionary strs, CancellationToken cancellationToken) + { + AVUser avUser = AVObject.CreateWithoutData(null); + + return UserController.LogInWithParametersAsync("login", strs, cancellationToken).OnSuccess(t => + { + var user = (AVUser)AVObject.CreateWithoutData(null); + user.HandleFetchResult(t.Result); + return SaveCurrentUserAsync(user).OnSuccess(_ => user); + }).Unwrap(); + } + + /// + /// 以手机号和密码实现登陆。 + /// + /// 手机号 + /// 密码 + /// + public static Task LogInByMobilePhoneNumberAsync(string mobilePhoneNumber, string password) + { + return AVUser.LogInByMobilePhoneNumberAsync(mobilePhoneNumber, password, CancellationToken.None); + } + + /// + /// 以手机号和验证码匹配登陆 + /// + /// 手机号 + /// 短信验证码 + /// + public static Task LogInBySmsCodeAsync(string mobilePhoneNumber, string smsCode) + { + return AVUser.LogInBySmsCodeAsync(mobilePhoneNumber, smsCode, CancellationToken.None); + } + + /// + /// 用邮箱作和密码匹配登录 + /// + /// 邮箱 + /// 密码 + /// + public static Task LogInByEmailAsync(string email, string password, CancellationToken cancellationToken = default(CancellationToken)) + { + return UserController.LogInAsync(null, email, password, cancellationToken).OnSuccess(t => { + AVUser user = AVObject.FromState(t.Result, "_User"); + return SaveCurrentUserAsync(user).OnSuccess(_ => user); + }).Unwrap(); + } + + + /// + /// 以手机号和密码匹配登陆 + /// + /// 手机号 + /// 密码 + /// + /// + public static Task LogInByMobilePhoneNumberAsync(string mobilePhoneNumber, string password, CancellationToken cancellationToken) + { + Dictionary strs = new Dictionary() + { + { "mobilePhoneNumber", mobilePhoneNumber }, + { "password", password } + }; + return AVUser.LogInWithParametersAsync(strs, cancellationToken); + } + + /// + /// 以手机号和验证码登陆 + /// + /// 手机号 + /// 短信验证码 + /// + /// + public static Task LogInBySmsCodeAsync(string mobilePhoneNumber, string smsCode, CancellationToken cancellationToken = default(CancellationToken)) + { + Dictionary strs = new Dictionary() + { + { "mobilePhoneNumber", mobilePhoneNumber }, + { "smsCode", smsCode } + }; + return AVUser.LogInWithParametersAsync(strs, cancellationToken); + } + + /// + /// Requests the login SMS code asynchronous. + /// + /// The mobile phone number. + /// + public static Task RequestLogInSmsCodeAsync(string mobilePhoneNumber) + { + return AVUser.RequestLogInSmsCodeAsync(mobilePhoneNumber, CancellationToken.None); + } + + /// + /// Requests the login SMS code asynchronous. + /// + /// The mobile phone number. + /// Validate token. + /// + public static Task RequestLogInSmsCodeAsync(string mobilePhoneNumber, string validateToken) + { + return AVUser.RequestLogInSmsCodeAsync(mobilePhoneNumber, null, CancellationToken.None); + } + + /// + /// Requests the login SMS code asynchronous. + /// + /// The mobile phone number. + /// The cancellation token. + /// + public static Task RequestLogInSmsCodeAsync(string mobilePhoneNumber, CancellationToken cancellationToken) + { + return RequestLogInSmsCodeAsync(mobilePhoneNumber, null, cancellationToken); + } + + /// + /// Requests the login SMS code asynchronous. + /// + /// The mobile phone number. + /// Validate token. + /// The cancellation token. + /// + public static Task RequestLogInSmsCodeAsync(string mobilePhoneNumber, string validateToken, CancellationToken cancellationToken) + { + Dictionary strs = new Dictionary() + { + { "mobilePhoneNumber", mobilePhoneNumber }, + }; + if (String.IsNullOrEmpty(validateToken)) + { + strs.Add("validate_token", validateToken); + } + var command = new AVCommand("requestLoginSmsCode", + method: "POST", + sessionToken: CurrentSessionToken, + data: strs); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + /// + /// 手机号一键登录 + /// + /// 手机号 + /// 短信验证码 + /// + public static Task SignUpOrLogInByMobilePhoneAsync(string mobilePhoneNumber, string smsCode, CancellationToken cancellationToken) + { + Dictionary strs = new Dictionary() + { + { "mobilePhoneNumber", mobilePhoneNumber }, + { "smsCode", smsCode } + }; + return UserController.LogInWithParametersAsync("usersByMobilePhone", strs, cancellationToken).OnSuccess(t => + { + var user = (AVUser)AVObject.CreateWithoutData(null); + user.HandleFetchResult(t.Result); + return SaveCurrentUserAsync(user).OnSuccess(_ => user); + }).Unwrap(); + } + + /// + /// 手机号一键登录 + /// + /// signup or login by mobile phone async. + /// 手机号 + /// 短信验证码 + public static Task SignUpOrLogInByMobilePhoneAsync(string mobilePhoneNumber, string smsCode) + { + return AVUser.SignUpOrLogInByMobilePhoneAsync(mobilePhoneNumber, smsCode, CancellationToken.None); + } + + #region mobile sms shortcode sign up & log in. + /// + /// Send sign up sms code async. + /// + /// The sign up sms code async. + /// Mobile phone number. + public static Task SendSignUpSmsCodeAsync(string mobilePhoneNumber) + { + return AVCloud.RequestSMSCodeAsync(mobilePhoneNumber); + } + + /// + /// Sign up by mobile phone async. + /// + /// The up by mobile phone async. + /// Mobile phone number. + /// Sms code. + public static Task SignUpByMobilePhoneAsync(string mobilePhoneNumber, string smsCode) + { + return AVUser.SignUpOrLogInByMobilePhoneAsync(mobilePhoneNumber, smsCode); + } + + /// + /// Send log in sms code async. + /// + /// The log in sms code async. + /// Mobile phone number. + public static Task SendLogInSmsCodeAsync(string mobilePhoneNumber) + { + return AVUser.RequestLogInSmsCodeAsync(mobilePhoneNumber); + } + + /// + /// Log in by mobile phone async. + /// + /// The in by mobile phone async. + /// Mobile phone number. + /// Sms code. + public static Task LogInByMobilePhoneAsync(string mobilePhoneNumber, string smsCode) + { + return AVUser.LogInBySmsCodeAsync(mobilePhoneNumber, smsCode); + } + #endregion + #endregion + + #region 重置密码 + /// + /// 请求重置密码,需要传入注册时使用的手机号。 + /// + /// 注册时使用的手机号 + /// + public static Task RequestPasswordResetBySmsCode(string mobilePhoneNumber) + { + return AVUser.RequestPasswordResetBySmsCode(mobilePhoneNumber, null, CancellationToken.None); + } + + /// + /// 请求重置密码,需要传入注册时使用的手机号。 + /// + /// 注册时使用的手机号 + /// cancellationToken + /// + public static Task RequestPasswordResetBySmsCode(string mobilePhoneNumber, CancellationToken cancellationToken) + { + return RequestPasswordResetBySmsCode(mobilePhoneNumber, null, cancellationToken); + } + + /// + /// 请求重置密码,需要传入注册时使用的手机号。 + /// + /// 注册时使用的手机号 + /// Validate token. + /// + public static Task RequestPasswordResetBySmsCode(string mobilePhoneNumber, string validateToken) + { + return AVUser.RequestPasswordResetBySmsCode(mobilePhoneNumber, validateToken, CancellationToken.None); + } + + /// + /// 请求重置密码,需要传入注册时使用的手机号。 + /// + /// 注册时使用的手机号 + /// Validate token. + /// cancellationToken + /// + public static Task RequestPasswordResetBySmsCode(string mobilePhoneNumber, string validateToken, CancellationToken cancellationToken) + { + string currentSessionToken = AVUser.CurrentSessionToken; + Dictionary strs = new Dictionary() + { + { "mobilePhoneNumber", mobilePhoneNumber }, + }; + if (String.IsNullOrEmpty(validateToken)) + { + strs.Add("validate_token", validateToken); + } + var command = new AVCommand("requestPasswordResetBySmsCode", + method: "POST", + sessionToken: currentSessionToken, + data: strs); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + /// + /// 通过验证码重置密码。 + /// + /// 新密码 + /// 6位数验证码 + /// + public static Task ResetPasswordBySmsCodeAsync(string newPassword, string smsCode) + { + return AVUser.ResetPasswordBySmsCodeAsync(newPassword, smsCode, CancellationToken.None); + } + + /// + /// 通过验证码重置密码。 + /// + /// 新密码 + /// 6位数验证码 + /// cancellationToken + /// + public static Task ResetPasswordBySmsCodeAsync(string newPassword, string smsCode, CancellationToken cancellationToken) + { + string currentSessionToken = AVUser.CurrentSessionToken; + Dictionary strs = new Dictionary() + { + { "password", newPassword } + }; + var command = new AVCommand("resetPasswordBySmsCode/" + smsCode, + method: "PUT", + sessionToken: currentSessionToken, + data: strs); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + /// + /// 发送认证码到需要认证的手机上 + /// + /// 手机号 + /// + public static Task RequestMobilePhoneVerifyAsync(string mobilePhoneNumber) + { + return AVUser.RequestMobilePhoneVerifyAsync(mobilePhoneNumber, null, CancellationToken.None); + } + + /// + /// 发送认证码到需要认证的手机上 + /// + /// 手机号 + /// Validate token. + /// + public static Task RequestMobilePhoneVerifyAsync(string mobilePhoneNumber, string validateToken) + { + return AVUser.RequestMobilePhoneVerifyAsync(mobilePhoneNumber, validateToken, CancellationToken.None); + } + + /// + /// 发送认证码到需要认证的手机上 + /// + /// 手机号 + /// CancellationToken + /// + public static Task RequestMobilePhoneVerifyAsync(string mobilePhoneNumber, CancellationToken cancellationToken) + { + return RequestMobilePhoneVerifyAsync(mobilePhoneNumber, null, cancellationToken); + } + + /// + /// 发送认证码到需要认证的手机上 + /// + /// 手机号 + /// Validate token. + /// CancellationToken + /// + public static Task RequestMobilePhoneVerifyAsync(string mobilePhoneNumber, string validateToken, CancellationToken cancellationToken) + { + string currentSessionToken = AVUser.CurrentSessionToken; + Dictionary strs = new Dictionary() + { + { "mobilePhoneNumber", mobilePhoneNumber } + }; + if (String.IsNullOrEmpty(validateToken)) + { + strs.Add("validate_token", validateToken); + } + var command = new AVCommand("requestMobilePhoneVerify", + method: "POST", + sessionToken: currentSessionToken, + data: strs); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + /// + /// 验证手机验证码是否为有效值 + /// + /// 手机收到的验证码 + /// 手机号 + /// + public static Task VerifyMobilePhoneAsync(string code, string mobilePhoneNumber) + { + return AVUser.VerifyMobilePhoneAsync(code, mobilePhoneNumber, CancellationToken.None); + } + + /// + /// 验证手机验证码是否为有效值 + /// + /// 手机收到的验证码 + /// 手机号,可选 + /// + /// + public static Task VerifyMobilePhoneAsync(string code, string mobilePhoneNumber, CancellationToken cancellationToken) + { + var command = new AVCommand("verifyMobilePhone/" + code.Trim() + "?mobilePhoneNumber=" + mobilePhoneNumber.Trim(), + method: "POST", + sessionToken: null, + data: null); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + /// + /// 验证手机验证码是否为有效值 + /// + /// 手机收到的验证码 + /// + public static Task VerifyMobilePhoneAsync(string code) + { + var command = new AVCommand("verifyMobilePhone/" + code.Trim(), + method: "POST", + sessionToken: null, + data: null); + + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + + /// + /// 验证手机验证码是否为有效值 + /// + /// 手机收到的验证码 + /// cancellationToken + /// + public static Task VerifyMobilePhoneAsync(string code, CancellationToken cancellationToken) + { + return AVUser.VerifyMobilePhoneAsync(code, CancellationToken.None); + } + + #endregion + + #region 邮箱验证 + /// + /// 申请发送验证邮箱的邮件,一周之内有效 + /// 如果该邮箱已经验证通过,会直接返回 True,并不会真正发送邮件 + /// 注意,不能频繁的调用此接口,一天之内只允许向同一个邮箱发送验证邮件 3 次,超过调用次数,会直接返回错误 + /// + /// 邮箱地址 + /// + public static Task RequestEmailVerifyAsync(string email) + { + Dictionary strs = new Dictionary() + { + { "email", email } + }; + var command = new AVCommand("requestEmailVerify", + method: "POST", + sessionToken: null, + data: strs); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).ContinueWith(t => + { + return AVClient.IsSuccessStatusCode(t.Result.Item1); + }); + } + #endregion + + #region in no-local-storage enviroment + + internal Task Create() + { + return this.Create(CancellationToken.None); + } + internal Task Create(CancellationToken cancellationToken) + { + return taskQueue.Enqueue(toAwait => Create(toAwait, cancellationToken), + cancellationToken); + } + + internal Task Create(Task toAwait, CancellationToken cancellationToken) + { + if (AuthData == null) + { + // TODO (hallucinogen): make an Extension of Task to create Task with exception/canceled. + if (string.IsNullOrEmpty(Username)) + { + TaskCompletionSource tcs = new TaskCompletionSource(); + tcs.TrySetException(new InvalidOperationException("Cannot sign up user with an empty name.")); + return tcs.Task; + } + if (string.IsNullOrEmpty(Password)) + { + TaskCompletionSource tcs = new TaskCompletionSource(); + tcs.TrySetException(new InvalidOperationException("Cannot sign up user with an empty password.")); + return tcs.Task; + } + } + if (!string.IsNullOrEmpty(ObjectId)) + { + TaskCompletionSource tcs = new TaskCompletionSource(); + tcs.TrySetException(new InvalidOperationException("Cannot sign up a user that already exists.")); + return tcs.Task; + } + + IDictionary currentOperations = StartSave(); + + return toAwait.OnSuccess(_ => + { + return UserController.SignUpAsync(State, currentOperations, cancellationToken); + }).Unwrap().ContinueWith(t => + { + if (t.IsFaulted || t.IsCanceled) + { + HandleFailedSave(currentOperations); + } + else + { + var serverState = t.Result; + HandleSave(serverState); + } + return t; + }).Unwrap(); + } + #endregion + + + #region task session token for http request + internal static Task TakeSessionToken(string sesstionToken = null) + { + var sessionTokenTask = Task.FromResult(sesstionToken); + if (sesstionToken == null) + sessionTokenTask = AVUser.GetCurrentAsync().OnSuccess(u => + { + if (u.Result != null) + return u.Result.SessionToken; + return null; + }); + return sessionTokenTask; + } + #endregion + + + #region AVUser Extension + public IDictionary> GetAuthData() { + return AuthData; + } + + /// + /// use 3rd auth data to sign up or log in.if user with the same auth data exits,it will transfer as log in. + /// + /// OAuth data, like {"accessToken":"xxxxxx"} + /// auth platform,maybe "facebook"/"twiiter"/"weibo"/"weixin" .etc + /// + /// + public static Task LogInWithAuthDataAsync(IDictionary data, + string platform, + AVUserAuthDataLogInOption options = null, + CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { + if (options == null) { + options = new AVUserAuthDataLogInOption(); + } + return AVUser.LogInWithAsync(platform, data, options.FailOnNotExist, cancellationToken); + } + + public static Task LogInWithAuthDataAndUnionIdAsync( + IDictionary authData, + string platform, + string unionId, + AVUserAuthDataLogInOption options = null, + CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { + if (options == null) { + options = new AVUserAuthDataLogInOption(); + } + MergeAuthData(authData, unionId, options); + return AVUser.LogInWithAsync(platform, authData, options.FailOnNotExist, cancellationToken); + } + + public static Task LogInAnonymouslyAsync(CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { + var data = new Dictionary { + { "id", Guid.NewGuid().ToString() } + }; + var options = new AVUserAuthDataLogInOption(); + return LogInWithAuthDataAsync(data, "anonymous", options, cancellationToken); + } + + [Obsolete("please use LogInWithAuthDataAsync instead.")] + public static Task LogInWithAsync(string authType, IDictionary data, CancellationToken cancellationToken) { + return AVUser.LogInWithAsync(authType, data, false, cancellationToken); + } + + /// + /// link a 3rd auth account to the user. + /// + /// AVUser instance + /// OAuth data, like {"accessToken":"xxxxxx"} + /// auth platform,maybe "facebook"/"twiiter"/"weibo"/"weixin" .etc + /// + /// + public Task AssociateAuthDataAsync(IDictionary data, string platform, CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { + return LinkWithAsync(platform, data, cancellationToken); + } + + public Task AssociateAuthDataAndUnionIdAsync( + IDictionary authData, + string platform, + string unionId, + AVUserAuthDataLogInOption options = null, + CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { + if (options == null) { + options = new AVUserAuthDataLogInOption(); + } + MergeAuthData(authData, unionId, options); + return LinkWithAsync(platform, authData, cancellationToken); + } + + /// + /// unlink a 3rd auth account from the user. + /// + /// AVUser instance + /// auth platform,maybe "facebook"/"twiiter"/"weibo"/"weixin" .etc + /// + /// + public Task DisassociateWithAuthDataAsync(string platform, CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { + return UnlinkFromAsync(platform, cancellationToken); + } + + /// 合并为支持 AuthData 的格式 + static void MergeAuthData(IDictionary authData, string unionId, AVUserAuthDataLogInOption options) { + authData["platform"] = options.UnionIdPlatform; + authData["main_account"] = options.AsMainAccount; + authData["unionid"] = unionId; + } + #endregion + } +} diff --git a/Storage/Storage/Public/AVUserAuthDataLogInOption.cs b/Storage/Storage/Public/AVUserAuthDataLogInOption.cs new file mode 100644 index 0000000..1918c2b --- /dev/null +++ b/Storage/Storage/Public/AVUserAuthDataLogInOption.cs @@ -0,0 +1,33 @@ +using System; + +namespace LeanCloud { + /// + /// AuthData 登陆选项 + /// + public class AVUserAuthDataLogInOption { + + /// + /// unionId platform + /// + /// unionId platform. + public string UnionIdPlatform; + + /// + /// If true, the unionId will be associated with the user. + /// + /// true If true, the unionId will be associated with the user. false. + public bool AsMainAccount; + + /// + /// If true, the login request will fail when no user matches this authData exists. + /// + /// true If true, the login request will fail when no user matches this authData exists. false. + public bool FailOnNotExist; + + public AVUserAuthDataLogInOption() { + UnionIdPlatform = "weixin"; + AsMainAccount = false; + FailOnNotExist = false; + } + } +} diff --git a/Storage/Storage/Public/IAVQuery.cs b/Storage/Storage/Public/IAVQuery.cs new file mode 100644 index 0000000..a4ee243 --- /dev/null +++ b/Storage/Storage/Public/IAVQuery.cs @@ -0,0 +1,2068 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using LeanCloud.Storage.Internal; + +namespace LeanCloud +{ + /// + /// Query 对象的基础接口 + /// + public interface IAVQuery + { + + } + + /// + /// LeanCloud 存储对象的接触接口 + /// + public interface IAVObject + { + + } + + public abstract class AVQueryBase : IAVQuery + where T : IAVObject + { + internal string className; + internal Dictionary where; + internal ReadOnlyCollection orderBy; + internal ReadOnlyCollection includes; + internal ReadOnlyCollection selectedKeys; + internal String redirectClassNameForKey; + internal int? skip; + internal int? limit; + + /// + /// 构建查询字符串 + /// + /// 是否包含 ClassName + /// + public IDictionary BuildParameters(bool includeClassName = false) + { + Dictionary result = new Dictionary(); + if (where != null) + { + result["where"] = PointerOrLocalIdEncoder.Instance.Encode(where); + } + if (orderBy != null) + { + result["order"] = string.Join(",", orderBy.ToArray()); + } + if (skip != null) + { + result["skip"] = skip.Value; + } + if (limit != null) + { + result["limit"] = limit.Value; + } + if (includes != null) + { + result["include"] = string.Join(",", includes.ToArray()); + } + if (selectedKeys != null) + { + result["keys"] = string.Join(",", selectedKeys.ToArray()); + } + if (includeClassName) + { + result["className"] = className; + } + if (redirectClassNameForKey != null) + { + result["redirectClassNameForKey"] = redirectClassNameForKey; + } + return result; + } + + public virtual Dictionary Where + { + get + { + return this.where; + } + set + { + this.where = value; + } + } + + public virtual IDictionary MergeWhere(IDictionary primary, IDictionary secondary) + { + if (secondary == null) + { + return primary; + } + var newWhere = new Dictionary(primary); + foreach (var pair in secondary) + { + var condition = pair.Value as IDictionary; + if (newWhere.ContainsKey(pair.Key)) + { + var oldCondition = newWhere[pair.Key] as IDictionary; + if (oldCondition == null || condition == null) + { + throw new ArgumentException("More than one where clause for the given key provided."); + } + var newCondition = new Dictionary(oldCondition); + foreach (var conditionPair in condition) + { + if (newCondition.ContainsKey(conditionPair.Key)) + { + throw new ArgumentException("More than one condition for the given key provided."); + } + newCondition[conditionPair.Key] = conditionPair.Value; + } + newWhere[pair.Key] = newCondition; + } + else + { + newWhere[pair.Key] = pair.Value; + } + } + return newWhere; + } + } + + public abstract class AVQueryPair + where S : IAVQuery + where T : IAVObject + + { + protected readonly string className; + protected readonly Dictionary where; + protected readonly ReadOnlyCollection orderBy; + protected readonly ReadOnlyCollection includes; + protected readonly ReadOnlyCollection selectedKeys; + protected readonly String redirectClassNameForKey; + protected readonly int? skip; + protected readonly int? limit; + + internal string ClassName { get { return className; } } + + private string relativeUri; + internal string RelativeUri + { + get + { + string rtn = string.Empty; + if (string.IsNullOrEmpty(relativeUri)) + { + rtn = "classes/" + Uri.EscapeDataString(this.className); + } + else + { + rtn = relativeUri; + } + return rtn; + } + set + { + relativeUri = value; + } + } + public Dictionary Condition + { + get { return this.where; } + } + + protected AVQueryPair() + { + + } + + public abstract S CreateInstance(IDictionary where = null, + IEnumerable replacementOrderBy = null, + IEnumerable thenBy = null, + int? skip = null, + int? limit = null, + IEnumerable includes = null, + IEnumerable selectedKeys = null, + String redirectClassNameForKey = null); + + /// + /// Private constructor for composition of queries. A Source query is required, + /// but the remaining values can be null if they won't be changed in this + /// composition. + /// + protected AVQueryPair(AVQueryPair source, + IDictionary where = null, + IEnumerable replacementOrderBy = null, + IEnumerable thenBy = null, + int? skip = null, + int? limit = null, + IEnumerable includes = null, + IEnumerable selectedKeys = null, + String redirectClassNameForKey = null) + { + if (source == null) + { + throw new ArgumentNullException("Source"); + } + + className = source.className; + this.where = source.where; + this.orderBy = source.orderBy; + this.skip = source.skip; + this.limit = source.limit; + this.includes = source.includes; + this.selectedKeys = source.selectedKeys; + this.redirectClassNameForKey = source.redirectClassNameForKey; + + if (where != null) + { + var newWhere = MergeWhereClauses(where); + this.where = new Dictionary(newWhere); + } + + if (replacementOrderBy != null) + { + this.orderBy = new ReadOnlyCollection(replacementOrderBy.ToList()); + } + + if (thenBy != null) + { + if (this.orderBy == null) + { + throw new ArgumentException("You must call OrderBy before calling ThenBy."); + } + var newOrderBy = new List(this.orderBy); + newOrderBy.AddRange(thenBy); + this.orderBy = new ReadOnlyCollection(newOrderBy); + } + + // Remove duplicates. + if (this.orderBy != null) + { + var newOrderBy = new HashSet(this.orderBy); + this.orderBy = new ReadOnlyCollection(newOrderBy.ToList()); + } + + if (skip != null) + { + this.skip = (this.skip ?? 0) + skip; + } + + if (limit != null) + { + this.limit = limit; + } + + if (includes != null) + { + var newIncludes = MergeIncludes(includes); + this.includes = new ReadOnlyCollection(newIncludes.ToList()); + } + + if (selectedKeys != null) + { + var newSelectedKeys = MergeSelectedKeys(selectedKeys); + this.selectedKeys = new ReadOnlyCollection(newSelectedKeys.ToList()); + } + + if (redirectClassNameForKey != null) + { + this.redirectClassNameForKey = redirectClassNameForKey; + } + } + + public AVQueryPair(string className) + { + if (string.IsNullOrEmpty(className)) + { + throw new ArgumentNullException("className", "Must specify a AVObject class name when creating a AVQuery."); + } + this.className = className; + } + + private HashSet MergeIncludes(IEnumerable includes) + { + if (this.includes == null) + { + return new HashSet(includes); + } + var newIncludes = new HashSet(this.includes); + foreach (var item in includes) + { + newIncludes.Add(item); + } + return newIncludes; + } + + private HashSet MergeSelectedKeys(IEnumerable selectedKeys) + { + if (this.selectedKeys == null) + { + return new HashSet(selectedKeys); + } + var newSelectedKeys = new HashSet(this.selectedKeys); + foreach (var item in selectedKeys) + { + newSelectedKeys.Add(item); + } + return newSelectedKeys; + } + + private IDictionary MergeWhereClauses(IDictionary where) + { + return MergeWhere(this.where, where); + } + + public virtual IDictionary MergeWhere(IDictionary primary, IDictionary secondary) + { + if (secondary == null) + { + return primary; + } + if (primary == null) + { + return secondary; + } + var newWhere = new Dictionary(primary); + foreach (var pair in secondary) + { + var condition = pair.Value as IDictionary; + if (newWhere.ContainsKey(pair.Key)) + { + var oldCondition = newWhere[pair.Key] as IDictionary; + if (oldCondition == null || condition == null) + { + throw new ArgumentException("More than one where clause for the given key provided."); + } + var newCondition = new Dictionary(oldCondition); + foreach (var conditionPair in condition) + { + if (newCondition.ContainsKey(conditionPair.Key)) + { + throw new ArgumentException("More than one condition for the given key provided."); + } + newCondition[conditionPair.Key] = conditionPair.Value; + } + newWhere[pair.Key] = newCondition; + } + else + { + newWhere[pair.Key] = pair.Value; + } + } + return newWhere; + } + + /// + /// Constructs a query that is the or of the given queries. + /// + /// The list of AVQueries to 'or' together. + /// A AVeQquery that is the 'or' of the passed in queries. + public static Q Or(IEnumerable queries) + where Q : AVQueryBase + where O : IAVObject + { + string className = null; + var orValue = new List>(); + // We need to cast it to non-generic IEnumerable because of AOT-limitation + var nonGenericQueries = (IEnumerable)queries; + Q current = null; + foreach (var obj in nonGenericQueries) + { + var q = (Q)obj; + current = q; + if (className != null && q.className != className) + { + throw new ArgumentException( + "All of the queries in an or query must be on the same class."); + } + className = q.className; + var parameters = q.BuildParameters(); + if (parameters.Count == 0) + { + continue; + } + object where; + if (!parameters.TryGetValue("where", out where) || parameters.Count > 1) + { + throw new ArgumentException( + "None of the queries in an or query can have non-filtering clauses"); + } + orValue.Add(where as IDictionary); + } + current.Where = new Dictionary() + { + {"$or", orValue} + }; + return current; + } + + #region Order By + + /// + /// Sorts the results in ascending order by the given key. + /// This will override any existing ordering for the query. + /// + /// The key to order by. + /// A new query with the additional constraint. + public virtual S OrderBy(string key) + { + return CreateInstance(replacementOrderBy: new List { key }); + } + + /// + /// Sorts the results in descending order by the given key. + /// This will override any existing ordering for the query. + /// + /// The key to order by. + /// A new query with the additional constraint. + public virtual S OrderByDescending(string key) + { + return CreateInstance(replacementOrderBy: new List { "-" + key }); + } + + /// + /// Sorts the results in ascending order by the given key, after previous + /// ordering has been applied. + /// + /// This method can only be called if there is already an + /// or + /// on this query. + /// + /// The key to order by. + /// A new query with the additional constraint. + public virtual S ThenBy(string key) + { + return CreateInstance(thenBy: new List { key }); + } + + /// + /// Sorts the results in descending order by the given key, after previous + /// ordering has been applied. + /// + /// This method can only be called if there is already an + /// or on this query. + /// + /// The key to order by. + /// A new query with the additional constraint. + public virtual S ThenByDescending(string key) + { + return CreateInstance(thenBy: new List { "-" + key }); + } + + #endregion + + /// + /// Include nested AVObjects for the provided key. You can use dot notation + /// to specify which fields in the included objects should also be fetched. + /// + /// The key that should be included. + /// A new query with the additional constraint. + public virtual S Include(string key) + { + return CreateInstance(includes: new List { key }); + } + + /// + /// Restrict the fields of returned AVObjects to only include the provided key. + /// If this is called multiple times, then all of the keys specified in each of + /// the calls will be included. + /// + /// The key that should be included. + /// A new query with the additional constraint. + public virtual S Select(string key) + { + return CreateInstance(selectedKeys: new List { key }); + } + + /// + /// Skips a number of results before returning. This is useful for pagination + /// of large queries. Chaining multiple skips together will cause more results + /// to be skipped. + /// + /// The number of results to skip. + /// A new query with the additional constraint. + public virtual S Skip(int count) + { + return CreateInstance(skip: count); + } + + /// + /// Controls the maximum number of results that are returned. Setting a negative + /// limit denotes retrieval without a limit. Chaining multiple limits + /// results in the last limit specified being used. The default limit is + /// 100, with a maximum of 1000 results being returned at a time. + /// + /// The maximum number of results to return. + /// A new query with the additional constraint. + public virtual S Limit(int count) + { + return CreateInstance(limit: count); + } + + internal virtual S RedirectClassName(String key) + { + return CreateInstance(redirectClassNameForKey: key); + } + + #region Where + + /// + /// Adds a constraint to the query that requires a particular key's value to be + /// contained in the provided list of values. + /// + /// The key to check. + /// The values that will match. + /// A new query with the additional constraint. + public virtual S WhereContainedIn(string key, IEnumerable values) + { + return CreateInstance(where: new Dictionary { + { key, new Dictionary{{"$in", values.ToList()}}} + }); + } + + /// + /// Add a constraint to the querey that requires a particular key's value to be + /// a list containing all of the elements in the provided list of values. + /// + /// The key to check. + /// The values that will match. + /// A new query with the additional constraint. + public virtual S WhereContainsAll(string key, IEnumerable values) + { + return CreateInstance(where: new Dictionary { + { key, new Dictionary{{"$all", values.ToList()}}} + }); + } + + /// + /// Adds a constraint for finding string values that contain a provided string. + /// This will be slow for large data sets. + /// + /// The key that the string to match is stored in. + /// The substring that the value must contain. + /// A new query with the additional constraint. + public virtual S WhereContains(string key, string substring) + { + return CreateInstance(where: new Dictionary { + { key, new Dictionary{{"$regex", RegexQuote(substring)}}} + }); + } + + /// + /// Adds a constraint for finding objects that do not contain a given key. + /// + /// The key that should not exist. + /// A new query with the additional constraint. + public virtual S WhereDoesNotExist(string key) + { + return CreateInstance(where: new Dictionary{ + { key, new Dictionary{{"$exists", false}}} + }); + } + + /// + /// Adds a constraint to the query that requires that a particular key's value + /// does not match another AVQuery. This only works on keys whose values are + /// AVObjects or lists of AVObjects. + /// + /// The key to check. + /// The query that the value should not match. + /// A new query with the additional constraint. + public virtual S WhereDoesNotMatchQuery(string key, AVQuery query) + where TOther : AVObject + { + return CreateInstance(where: new Dictionary { + { key, new Dictionary{{"$notInQuery", query.BuildParameters(true)}}} + }); + } + + /// + /// Adds a constraint for finding string values that end with a provided string. + /// This will be slow for large data sets. + /// + /// The key that the string to match is stored in. + /// The substring that the value must end with. + /// A new query with the additional constraint. + public virtual S WhereEndsWith(string key, string suffix) + { + return CreateInstance(where: new Dictionary { + { key, new Dictionary{{"$regex", RegexQuote(suffix) + "$"}}} + }); + } + + /// + /// Adds a constraint to the query that requires a particular key's value to be + /// equal to the provided value. + /// + /// The key to check. + /// The value that the AVObject must contain. + /// A new query with the additional constraint. + public virtual S WhereEqualTo(string key, object value) + { + return CreateInstance(where: new Dictionary { + { key, value} + }); + } + + /// + /// Adds a constraint to the query that requires a particular key's size to be + /// equal to the provided size. + /// + /// The size equal to. + /// The key to check. + /// The value that the size must be. + /// A new query with the additional constraint. + public virtual S WhereSizeEqualTo(string key, uint size) + { + return CreateInstance(where: new Dictionary { + { key, new Dictionary{{"$size", size}}} + }); + } + + /// + /// Adds a constraint for finding objects that contain a given key. + /// + /// The key that should exist. + /// A new query with the additional constraint. + public virtual S WhereExists(string key) + { + return CreateInstance(where: new Dictionary{ + { key, new Dictionary{{"$exists", true}}} + }); + } + + /// + /// Adds a constraint to the query that requires a particular key's value to be + /// greater than the provided value. + /// + /// The key to check. + /// The value that provides a lower bound. + /// A new query with the additional constraint. + public virtual S WhereGreaterThan(string key, object value) + { + return CreateInstance(where: new Dictionary{ + { key, new Dictionary{{"$gt", value}}} + }); + } + + /// + /// Adds a constraint to the query that requires a particular key's value to be + /// greater or equal to than the provided value. + /// + /// The key to check. + /// The value that provides a lower bound. + /// A new query with the additional constraint. + public virtual S WhereGreaterThanOrEqualTo(string key, object value) + { + return CreateInstance(where: new Dictionary{ + { key, new Dictionary{{"$gte", value}}} + }); + } + + /// + /// Adds a constraint to the query that requires a particular key's value to be + /// less than the provided value. + /// + /// The key to check. + /// The value that provides an upper bound. + /// A new query with the additional constraint. + public virtual S WhereLessThan(string key, object value) + { + return CreateInstance(where: new Dictionary{ + { key, new Dictionary{{"$lt", value}}} + }); + } + + /// + /// Adds a constraint to the query that requires a particular key's value to be + /// less than or equal to the provided value. + /// + /// The key to check. + /// The value that provides a lower bound. + /// A new query with the additional constraint. + public virtual S WhereLessThanOrEqualTo(string key, object value) + { + return CreateInstance(where: new Dictionary{ + { key, new Dictionary{{"$lte", value}}} + }); + } + + /// + /// Adds a regular expression constraint for finding string values that match the provided + /// regular expression. This may be slow for large data sets. + /// + /// The key that the string to match is stored in. + /// The regular expression pattern to match. The Regex must + /// have the options flag set. + /// Any of the following supported PCRE modifiers: + /// i - Case insensitive search + /// m Search across multiple lines of input + /// A new query with the additional constraint. + public virtual S WhereMatches(string key, Regex regex, string modifiers) + { + if (!regex.Options.HasFlag(RegexOptions.ECMAScript)) + { + throw new ArgumentException( + "Only ECMAScript-compatible regexes are supported. Please use the ECMAScript RegexOptions flag when creating your regex."); + } + return CreateInstance(where: new Dictionary { + { key, EncodeRegex(regex, modifiers)} + }); + } + + /// + /// Adds a regular expression constraint for finding string values that match the provided + /// regular expression. This may be slow for large data sets. + /// + /// The key that the string to match is stored in. + /// The regular expression pattern to match. The Regex must + /// have the options flag set. + /// A new query with the additional constraint. + public virtual S WhereMatches(string key, Regex regex) + { + return WhereMatches(key, regex, null); + } + + /// + /// Adds a regular expression constraint for finding string values that match the provided + /// regular expression. This may be slow for large data sets. + /// + /// The key that the string to match is stored in. + /// The PCRE regular expression pattern to match. + /// Any of the following supported PCRE modifiers: + /// i - Case insensitive search + /// m Search across multiple lines of input + /// A new query with the additional constraint. + public virtual S WhereMatches(string key, string pattern, string modifiers = null) + { + return WhereMatches(key, new Regex(pattern, RegexOptions.ECMAScript), modifiers); + } + + /// + /// Adds a regular expression constraint for finding string values that match the provided + /// regular expression. This may be slow for large data sets. + /// + /// The key that the string to match is stored in. + /// The PCRE regular expression pattern to match. + /// A new query with the additional constraint. + public virtual S WhereMatches(string key, string pattern) + { + return WhereMatches(key, pattern, null); + } + + /// + /// Adds a constraint to the query that requires a particular key's value + /// to match a value for a key in the results of another AVQuery. + /// + /// The key whose value is being checked. + /// The key in the objects from the subquery to look in. + /// The subquery to run + /// A new query with the additional constraint. + public virtual S WhereMatchesKeyInQuery(string key, + string keyInQuery, + AVQuery query) where TOther : AVObject + { + var parameters = new Dictionary { + { "query", query.BuildParameters(true)}, + { "key", keyInQuery} + }; + return CreateInstance(where: new Dictionary { + { key, new Dictionary{{"$select", parameters}}} + }); + } + + /// + /// Adds a constraint to the query that requires a particular key's value + /// does not match any value for a key in the results of another AVQuery. + /// + /// The key whose value is being checked. + /// The key in the objects from the subquery to look in. + /// The subquery to run + /// A new query with the additional constraint. + public virtual S WhereDoesNotMatchesKeyInQuery(string key, + string keyInQuery, + AVQuery query) where TOther : AVObject + { + var parameters = new Dictionary { + { "query", query.BuildParameters(true)}, + { "key", keyInQuery} + }; + return CreateInstance(where: new Dictionary { + { key, new Dictionary{{"$dontSelect", parameters}}} + }); + } + + /// + /// Adds a constraint to the query that requires that a particular key's value + /// matches another AVQuery. This only works on keys whose values are + /// AVObjects or lists of AVObjects. + /// + /// The key to check. + /// The query that the value should match. + /// A new query with the additional constraint. + public virtual S WhereMatchesQuery(string key, AVQuery query) + where TOther : AVObject + { + return CreateInstance(where: new Dictionary { + { key, new Dictionary{{"$inQuery", query.BuildParameters(true)}}} + }); + } + + /// + /// Adds a proximity-based constraint for finding objects with keys whose GeoPoint + /// values are near the given point. + /// + /// The key that the AVGeoPoint is stored in. + /// The reference AVGeoPoint. + /// A new query with the additional constraint. + public virtual S WhereNear(string key, AVGeoPoint point) + { + return CreateInstance(where: new Dictionary { + { key, new Dictionary{{"$nearSphere", point}}} + }); + } + + /// + /// Adds a constraint to the query that requires a particular key's value to be + /// contained in the provided list of values. + /// + /// The key to check. + /// The values that will match. + /// A new query with the additional constraint. + public virtual S WhereNotContainedIn(string key, IEnumerable values) + { + return CreateInstance(where: new Dictionary { + { key, new Dictionary{{"$nin", values.ToList()}}} + }); + } + + /// + /// Adds a constraint to the query that requires a particular key's value not + /// to be equal to the provided value. + /// + /// The key to check. + /// The value that that must not be equalled. + /// A new query with the additional constraint. + public virtual S WhereNotEqualTo(string key, object value) + { + return CreateInstance(where: new Dictionary { + { key, new Dictionary{{"$ne", value}}} + }); + } + + /// + /// Adds a constraint for finding string values that start with the provided string. + /// This query will use the backend index, so it will be fast even with large data sets. + /// + /// The key that the string to match is stored in. + /// The substring that the value must start with. + /// A new query with the additional constraint. + public virtual S WhereStartsWith(string key, string suffix) + { + return CreateInstance(where: new Dictionary { + { key, new Dictionary{{"$regex", "^" + RegexQuote(suffix)}}} + }); + } + + /// + /// Add a constraint to the query that requires a particular key's coordinates to be + /// contained within a given rectangular geographic bounding box. + /// + /// The key to be constrained. + /// The lower-left inclusive corner of the box. + /// The upper-right inclusive corner of the box. + /// A new query with the additional constraint. + public virtual S WhereWithinGeoBox(string key, + AVGeoPoint southwest, + AVGeoPoint northeast) + { + + return this.CreateInstance(where: new Dictionary + { + { + key, + new Dictionary + { + { + "$within", + new Dictionary { + { "$box", new[] {southwest, northeast}} + } + } + } + } + }); + } + + /// + /// Adds a proximity-based constraint for finding objects with keys whose GeoPoint + /// values are near the given point and within the maximum distance given. + /// + /// The key that the AVGeoPoint is stored in. + /// The reference AVGeoPoint. + /// The maximum distance (in radians) of results to return. + /// A new query with the additional constraint. + public virtual S WhereWithinDistance( + string key, AVGeoPoint point, AVGeoDistance maxDistance) + { + var nearWhere = new Dictionary { + { key, new Dictionary{{"$nearSphere", point}}} + }; + var mergedWhere = MergeWhere(nearWhere, new Dictionary { + { key, new Dictionary{{"$maxDistance", maxDistance.Radians}}} + }); + return CreateInstance(where: mergedWhere); + } + + internal virtual S WhereRelatedTo(AVObject parent, string key) + { + return CreateInstance(where: new Dictionary { + { + "$relatedTo", + new Dictionary { + { "object", parent}, + { "key", key} + } + } + }); + } + + #endregion + + /// + /// Retrieves a list of AVObjects that satisfy this query from LeanCloud. + /// + /// The list of AVObjects that match this query. + public virtual Task> FindAsync() + { + return FindAsync(CancellationToken.None); + } + + /// + /// Retrieves a list of AVObjects that satisfy this query from LeanCloud. + /// + /// The cancellation token. + /// The list of AVObjects that match this query. + public abstract Task> FindAsync(CancellationToken cancellationToken); + + + /// + /// Retrieves at most one AVObject that satisfies this query. + /// + /// A single AVObject that satisfies this query, or else null. + public virtual Task FirstOrDefaultAsync() + { + return FirstOrDefaultAsync(CancellationToken.None); + } + + /// + /// Retrieves at most one AVObject that satisfies this query. + /// + /// The cancellation token. + /// A single AVObject that satisfies this query, or else null. + public abstract Task FirstOrDefaultAsync(CancellationToken cancellationToken); + + /// + /// Retrieves at most one AVObject that satisfies this query. + /// + /// A single AVObject that satisfies this query. + /// If no results match the query. + public virtual Task FirstAsync() + { + return FirstAsync(CancellationToken.None); + } + + /// + /// Retrieves at most one AVObject that satisfies this query. + /// + /// The cancellation token. + /// A single AVObject that satisfies this query. + /// If no results match the query. + public abstract Task FirstAsync(CancellationToken cancellationToken); + + /// + /// Counts the number of objects that match this query. + /// + /// The number of objects that match this query. + public virtual Task CountAsync() + { + return CountAsync(CancellationToken.None); + } + + /// + /// Counts the number of objects that match this query. + /// + /// The cancellation token. + /// The number of objects that match this query. + public abstract Task CountAsync(CancellationToken cancellationToken); + + /// + /// Constructs a AVObject whose id is already known by fetching data + /// from the server. + /// + /// ObjectId of the AVObject to fetch. + /// The AVObject for the given objectId. + public virtual Task GetAsync(string objectId) + { + return GetAsync(objectId, CancellationToken.None); + } + + /// + /// Constructs a AVObject whose id is already known by fetching data + /// from the server. + /// + /// ObjectId of the AVObject to fetch. + /// The cancellation token. + /// The AVObject for the given objectId. + public abstract Task GetAsync(string objectId, CancellationToken cancellationToken); + + internal object GetConstraint(string key) + { + return where == null ? null : where.GetOrDefault(key, null); + } + + /// + /// 构建查询字符串 + /// + /// 是否包含 ClassName + /// + public IDictionary BuildParameters(bool includeClassName = false) + { + Dictionary result = new Dictionary(); + if (where != null) + { + result["where"] = PointerOrLocalIdEncoder.Instance.Encode(where); + } + if (orderBy != null) + { + result["order"] = string.Join(",", orderBy.ToArray()); + } + if (skip != null) + { + result["skip"] = skip.Value; + } + if (limit != null) + { + result["limit"] = limit.Value; + } + if (includes != null) + { + result["include"] = string.Join(",", includes.ToArray()); + } + if (selectedKeys != null) + { + result["keys"] = string.Join(",", selectedKeys.ToArray()); + } + if (includeClassName) + { + result["className"] = className; + } + if (redirectClassNameForKey != null) + { + result["redirectClassNameForKey"] = redirectClassNameForKey; + } + return result; + } + + private string RegexQuote(string input) + { + return "\\Q" + input.Replace("\\E", "\\E\\\\E\\Q") + "\\E"; + } + + private string GetRegexOptions(Regex regex, string modifiers) + { + string result = modifiers ?? ""; + if (regex.Options.HasFlag(RegexOptions.IgnoreCase) && !modifiers.Contains("i")) + { + result += "i"; + } + if (regex.Options.HasFlag(RegexOptions.Multiline) && !modifiers.Contains("m")) + { + result += "m"; + } + return result; + } + + private IDictionary EncodeRegex(Regex regex, string modifiers) + { + var options = GetRegexOptions(regex, modifiers); + var dict = new Dictionary(); + dict["$regex"] = regex.ToString(); + if (!string.IsNullOrEmpty(options)) + { + dict["$options"] = options; + } + return dict; + } + } + + //public abstract class AVQueryBase : IAVQueryTuple + // where T : IAVObject + //{ + // protected readonly string className; + // protected readonly Dictionary where; + // protected readonly ReadOnlyCollection orderBy; + // protected readonly ReadOnlyCollection includes; + // protected readonly ReadOnlyCollection selectedKeys; + // protected readonly String redirectClassNameForKey; + // protected readonly int? skip; + // protected readonly int? limit; + + // internal string ClassName { get { return className; } } + + // private string relativeUri; + // internal string RelativeUri + // { + // get + // { + // string rtn = string.Empty; + // if (string.IsNullOrEmpty(relativeUri)) + // { + // rtn = "classes/" + Uri.EscapeDataString(this.className); + // } + // else + // { + // rtn = relativeUri; + // } + // return rtn; + // } + // set + // { + // relativeUri = value; + // } + // } + // public Dictionary Condition + // { + // get { return this.where; } + // } + + // protected AVQueryBase() + // { + + // } + + // internal abstract S CreateInstance(AVQueryBase source, + // IDictionary where = null, + // IEnumerable replacementOrderBy = null, + // IEnumerable thenBy = null, + // int? skip = null, + // int? limit = null, + // IEnumerable includes = null, + // IEnumerable selectedKeys = null, + // String redirectClassNameForKey = null); + + // /// + // /// Private constructor for composition of queries. A Source query is required, + // /// but the remaining values can be null if they won't be changed in this + // /// composition. + // /// + // protected AVQueryBase(AVQueryBase source, + // IDictionary where = null, + // IEnumerable replacementOrderBy = null, + // IEnumerable thenBy = null, + // int? skip = null, + // int? limit = null, + // IEnumerable includes = null, + // IEnumerable selectedKeys = null, + // String redirectClassNameForKey = null) + // { + // if (source == null) + // { + // throw new ArgumentNullException("Source"); + // } + + // className = source.className; + // this.where = source.where; + // this.orderBy = source.orderBy; + // this.skip = source.skip; + // this.limit = source.limit; + // this.includes = source.includes; + // this.selectedKeys = source.selectedKeys; + // this.redirectClassNameForKey = source.redirectClassNameForKey; + + // if (where != null) + // { + // var newWhere = MergeWhereClauses(where); + // this.where = new Dictionary(newWhere); + // } + + // if (replacementOrderBy != null) + // { + // this.orderBy = new ReadOnlyCollection(replacementOrderBy.ToList()); + // } + + // if (thenBy != null) + // { + // if (this.orderBy == null) + // { + // throw new ArgumentException("You must call OrderBy before calling ThenBy."); + // } + // var newOrderBy = new List(this.orderBy); + // newOrderBy.AddRange(thenBy); + // this.orderBy = new ReadOnlyCollection(newOrderBy); + // } + + // // Remove duplicates. + // if (this.orderBy != null) + // { + // var newOrderBy = new HashSet(this.orderBy); + // this.orderBy = new ReadOnlyCollection(newOrderBy.ToList()); + // } + + // if (skip != null) + // { + // this.skip = (this.skip ?? 0) + skip; + // } + + // if (limit != null) + // { + // this.limit = limit; + // } + + // if (includes != null) + // { + // var newIncludes = MergeIncludes(includes); + // this.includes = new ReadOnlyCollection(newIncludes.ToList()); + // } + + // if (selectedKeys != null) + // { + // var newSelectedKeys = MergeSelectedKeys(selectedKeys); + // this.selectedKeys = new ReadOnlyCollection(newSelectedKeys.ToList()); + // } + + // if (redirectClassNameForKey != null) + // { + // this.redirectClassNameForKey = redirectClassNameForKey; + // } + // } + + // public AVQueryBase(string className) + // { + // if (string.IsNullOrEmpty(className)) + // { + // throw new ArgumentNullException("className", "Must specify a AVObject class name when creating a AVQuery."); + // } + // this.className = className; + // } + + // private HashSet MergeIncludes(IEnumerable includes) + // { + // if (this.includes == null) + // { + // return new HashSet(includes); + // } + // var newIncludes = new HashSet(this.includes); + // foreach (var item in includes) + // { + // newIncludes.Add(item); + // } + // return newIncludes; + // } + + // private HashSet MergeSelectedKeys(IEnumerable selectedKeys) + // { + // if (this.selectedKeys == null) + // { + // return new HashSet(selectedKeys); + // } + // var newSelectedKeys = new HashSet(this.selectedKeys); + // foreach (var item in selectedKeys) + // { + // newSelectedKeys.Add(item); + // } + // return newSelectedKeys; + // } + + // private IDictionary MergeWhereClauses(IDictionary where) + // { + // return MergeWhere(this.where, where); + // } + + // public virtual IDictionary MergeWhere(IDictionary primary, IDictionary secondary) + // { + // if (secondary == null) + // { + // return primary; + // } + // var newWhere = new Dictionary(primary); + // foreach (var pair in secondary) + // { + // var condition = pair.Value as IDictionary; + // if (newWhere.ContainsKey(pair.Key)) + // { + // var oldCondition = newWhere[pair.Key] as IDictionary; + // if (oldCondition == null || condition == null) + // { + // throw new ArgumentException("More than one where clause for the given key provided."); + // } + // var newCondition = new Dictionary(oldCondition); + // foreach (var conditionPair in condition) + // { + // if (newCondition.ContainsKey(conditionPair.Key)) + // { + // throw new ArgumentException("More than one condition for the given key provided."); + // } + // newCondition[conditionPair.Key] = conditionPair.Value; + // } + // newWhere[pair.Key] = newCondition; + // } + // else + // { + // newWhere[pair.Key] = pair.Value; + // } + // } + // return newWhere; + // } + + // ///// + // ///// Constructs a query that is the or of the given queries. + // ///// + // ///// The list of AVQueries to 'or' together. + // ///// A AVQquery that is the 'or' of the passed in queries. + // //public static AVQuery Or(IEnumerable> queries) + // //{ + // // string className = null; + // // var orValue = new List>(); + // // // We need to cast it to non-generic IEnumerable because of AOT-limitation + // // var nonGenericQueries = (IEnumerable)queries; + // // foreach (var obj in nonGenericQueries) + // // { + // // var q = (AVQuery)obj; + // // if (className != null && q.className != className) + // // { + // // throw new ArgumentException( + // // "All of the queries in an or query must be on the same class."); + // // } + // // className = q.className; + // // var parameters = q.BuildParameters(); + // // if (parameters.Count == 0) + // // { + // // continue; + // // } + // // object where; + // // if (!parameters.TryGetValue("where", out where) || parameters.Count > 1) + // // { + // // throw new ArgumentException( + // // "None of the queries in an or query can have non-filtering clauses"); + // // } + // // orValue.Add(where as IDictionary); + // // } + // // return new AVQuery(new AVQuery(className), + // // where: new Dictionary { + // // {"$or", orValue} + // // }); + // //} + + // #region Order By + + // /// + // /// Sorts the results in ascending order by the given key. + // /// This will override any existing ordering for the query. + // /// + // /// The key to order by. + // /// A new query with the additional constraint. + // public virtual S OrderBy(string key) + // { + // return CreateInstance( replacementOrderBy: new List { key }); + // } + + // /// + // /// Sorts the results in descending order by the given key. + // /// This will override any existing ordering for the query. + // /// + // /// The key to order by. + // /// A new query with the additional constraint. + // public virtual S OrderByDescending(string key) + // { + // return CreateInstance( replacementOrderBy: new List { "-" + key }); + // } + + // /// + // /// Sorts the results in ascending order by the given key, after previous + // /// ordering has been applied. + // /// + // /// This method can only be called if there is already an + // /// or + // /// on this query. + // /// + // /// The key to order by. + // /// A new query with the additional constraint. + // public virtual S ThenBy(string key) + // { + // return CreateInstance( thenBy: new List { key }); + // } + + // /// + // /// Sorts the results in descending order by the given key, after previous + // /// ordering has been applied. + // /// + // /// This method can only be called if there is already an + // /// or on this query. + // /// + // /// The key to order by. + // /// A new query with the additional constraint. + // public virtual S ThenByDescending(string key) + // { + // return CreateInstance( thenBy: new List { "-" + key }); + // } + + // #endregion + + // /// + // /// Include nested AVObjects for the provided key. You can use dot notation + // /// to specify which fields in the included objects should also be fetched. + // /// + // /// The key that should be included. + // /// A new query with the additional constraint. + // public virtual S Include(string key) + // { + // return CreateInstance( includes: new List { key }); + // } + + // /// + // /// Restrict the fields of returned AVObjects to only include the provided key. + // /// If this is called multiple times, then all of the keys specified in each of + // /// the calls will be included. + // /// + // /// The key that should be included. + // /// A new query with the additional constraint. + // public virtual S Select(string key) + // { + // return CreateInstance( selectedKeys: new List { key }); + // } + + // /// + // /// Skips a number of results before returning. This is useful for pagination + // /// of large queries. Chaining multiple skips together will cause more results + // /// to be skipped. + // /// + // /// The number of results to skip. + // /// A new query with the additional constraint. + // public virtual S Skip(int count) + // { + // return CreateInstance( skip: count); + // } + + // /// + // /// Controls the maximum number of results that are returned. Setting a negative + // /// limit denotes retrieval without a limit. Chaining multiple limits + // /// results in the last limit specified being used. The default limit is + // /// 100, with a maximum of 1000 results being returned at a time. + // /// + // /// The maximum number of results to return. + // /// A new query with the additional constraint. + // public virtual S Limit(int count) + // { + // return CreateInstance( limit: count); + // } + + // internal virtual S RedirectClassName(String key) + // { + // return CreateInstance( redirectClassNameForKey: key); + // } + + // #region Where + + // /// + // /// Adds a constraint to the query that requires a particular key's value to be + // /// contained in the provided list of values. + // /// + // /// The key to check. + // /// The values that will match. + // /// A new query with the additional constraint. + // public virtual S WhereContainedIn(string key, IEnumerable values) + // { + // return CreateInstance( where: new Dictionary { + // { key, new Dictionary{{"$in", values.ToList()}}} + // }); + // } + + // /// + // /// Add a constraint to the querey that requires a particular key's value to be + // /// a list containing all of the elements in the provided list of values. + // /// + // /// The key to check. + // /// The values that will match. + // /// A new query with the additional constraint. + // public virtual S WhereContainsAll(string key, IEnumerable values) + // { + // return CreateInstance( where: new Dictionary { + // { key, new Dictionary{{"$all", values.ToList()}}} + // }); + // } + + // /// + // /// Adds a constraint for finding string values that contain a provided string. + // /// This will be slow for large data sets. + // /// + // /// The key that the string to match is stored in. + // /// The substring that the value must contain. + // /// A new query with the additional constraint. + // public virtual S WhereContains(string key, string substring) + // { + // return CreateInstance( where: new Dictionary { + // { key, new Dictionary{{"$regex", RegexQuote(substring)}}} + // }); + // } + + // /// + // /// Adds a constraint for finding objects that do not contain a given key. + // /// + // /// The key that should not exist. + // /// A new query with the additional constraint. + // public virtual S WhereDoesNotExist(string key) + // { + // return CreateInstance( where: new Dictionary{ + // { key, new Dictionary{{"$exists", false}}} + // }); + // } + + // /// + // /// Adds a constraint to the query that requires that a particular key's value + // /// does not match another AVQuery. This only works on keys whose values are + // /// AVObjects or lists of AVObjects. + // /// + // /// The key to check. + // /// The query that the value should not match. + // /// A new query with the additional constraint. + // public virtual S WhereDoesNotMatchQuery(string key, AVQuery query) + // where TOther : AVObject + // { + // return CreateInstance( where: new Dictionary { + // { key, new Dictionary{{"$notInQuery", query.BuildParameters(true)}}} + // }); + // } + + // /// + // /// Adds a constraint for finding string values that end with a provided string. + // /// This will be slow for large data sets. + // /// + // /// The key that the string to match is stored in. + // /// The substring that the value must end with. + // /// A new query with the additional constraint. + // public virtual S WhereEndsWith(string key, string suffix) + // { + // return CreateInstance( where: new Dictionary { + // { key, new Dictionary{{"$regex", RegexQuote(suffix) + "$"}}} + // }); + // } + + // /// + // /// Adds a constraint to the query that requires a particular key's value to be + // /// equal to the provided value. + // /// + // /// The key to check. + // /// The value that the AVObject must contain. + // /// A new query with the additional constraint. + // public virtual S WhereEqualTo(string key, object value) + // { + // return CreateInstance( where: new Dictionary { + // { key, value} + // }); + // } + + // /// + // /// Adds a constraint to the query that requires a particular key's size to be + // /// equal to the provided size. + // /// + // /// The size equal to. + // /// The key to check. + // /// The value that the size must be. + // /// A new query with the additional constraint. + // public virtual S WhereSizeEqualTo(string key, uint size) + // { + // return CreateInstance( where: new Dictionary { + // { key, new Dictionary{{"$size", size}}} + // }); + // } + + // /// + // /// Adds a constraint for finding objects that contain a given key. + // /// + // /// The key that should exist. + // /// A new query with the additional constraint. + // public virtual S WhereExists(string key) + // { + // return CreateInstance( where: new Dictionary{ + // { key, new Dictionary{{"$exists", true}}} + // }); + // } + + // /// + // /// Adds a constraint to the query that requires a particular key's value to be + // /// greater than the provided value. + // /// + // /// The key to check. + // /// The value that provides a lower bound. + // /// A new query with the additional constraint. + // public virtual S WhereGreaterThan(string key, object value) + // { + // return CreateInstance( where: new Dictionary{ + // { key, new Dictionary{{"$gt", value}}} + // }); + // } + + // /// + // /// Adds a constraint to the query that requires a particular key's value to be + // /// greater or equal to than the provided value. + // /// + // /// The key to check. + // /// The value that provides a lower bound. + // /// A new query with the additional constraint. + // public virtual S WhereGreaterThanOrEqualTo(string key, object value) + // { + // return CreateInstance( where: new Dictionary{ + // { key, new Dictionary{{"$gte", value}}} + // }); + // } + + // /// + // /// Adds a constraint to the query that requires a particular key's value to be + // /// less than the provided value. + // /// + // /// The key to check. + // /// The value that provides an upper bound. + // /// A new query with the additional constraint. + // public virtual S WhereLessThan(string key, object value) + // { + // return CreateInstance( where: new Dictionary{ + // { key, new Dictionary{{"$lt", value}}} + // }); + // } + + // /// + // /// Adds a constraint to the query that requires a particular key's value to be + // /// less than or equal to the provided value. + // /// + // /// The key to check. + // /// The value that provides a lower bound. + // /// A new query with the additional constraint. + // public virtual S WhereLessThanOrEqualTo(string key, object value) + // { + // return CreateInstance( where: new Dictionary{ + // { key, new Dictionary{{"$lte", value}}} + // }); + // } + + // /// + // /// Adds a regular expression constraint for finding string values that match the provided + // /// regular expression. This may be slow for large data sets. + // /// + // /// The key that the string to match is stored in. + // /// The regular expression pattern to match. The Regex must + // /// have the options flag set. + // /// Any of the following supported PCRE modifiers: + // /// i - Case insensitive search + // /// m Search across multiple lines of input + // /// A new query with the additional constraint. + // public virtual S WhereMatches(string key, Regex regex, string modifiers) + // { + // if (!regex.Options.HasFlag(RegexOptions.ECMAScript)) + // { + // throw new ArgumentException( + // "Only ECMAScript-compatible regexes are supported. Please use the ECMAScript RegexOptions flag when creating your regex."); + // } + // return CreateInstance( where: new Dictionary { + // { key, EncodeRegex(regex, modifiers)} + // }); + // } + + // /// + // /// Adds a regular expression constraint for finding string values that match the provided + // /// regular expression. This may be slow for large data sets. + // /// + // /// The key that the string to match is stored in. + // /// The regular expression pattern to match. The Regex must + // /// have the options flag set. + // /// A new query with the additional constraint. + // public virtual S WhereMatches(string key, Regex regex) + // { + // return WhereMatches(key, regex, null); + // } + + // /// + // /// Adds a regular expression constraint for finding string values that match the provided + // /// regular expression. This may be slow for large data sets. + // /// + // /// The key that the string to match is stored in. + // /// The PCRE regular expression pattern to match. + // /// Any of the following supported PCRE modifiers: + // /// i - Case insensitive search + // /// m Search across multiple lines of input + // /// A new query with the additional constraint. + // public virtual S WhereMatches(string key, string pattern, string modifiers = null) + // { + // return WhereMatches(key, new Regex(pattern, RegexOptions.ECMAScript), modifiers); + // } + + // /// + // /// Adds a regular expression constraint for finding string values that match the provided + // /// regular expression. This may be slow for large data sets. + // /// + // /// The key that the string to match is stored in. + // /// The PCRE regular expression pattern to match. + // /// A new query with the additional constraint. + // public virtual S WhereMatches(string key, string pattern) + // { + // return WhereMatches(key, pattern, null); + // } + + // /// + // /// Adds a constraint to the query that requires a particular key's value + // /// to match a value for a key in the results of another AVQuery. + // /// + // /// The key whose value is being checked. + // /// The key in the objects from the subquery to look in. + // /// The subquery to run + // /// A new query with the additional constraint. + // public virtual S WhereMatchesKeyInQuery(string key, + // string keyInQuery, + // AVQuery query) where TOther : AVObject + // { + // var parameters = new Dictionary { + // { "query", query.BuildParameters(true)}, + // { "key", keyInQuery} + // }; + // return CreateInstance( where: new Dictionary { + // { key, new Dictionary{{"$select", parameters}}} + // }); + // } + + // /// + // /// Adds a constraint to the query that requires a particular key's value + // /// does not match any value for a key in the results of another AVQuery. + // /// + // /// The key whose value is being checked. + // /// The key in the objects from the subquery to look in. + // /// The subquery to run + // /// A new query with the additional constraint. + // public virtual S WhereDoesNotMatchesKeyInQuery(string key, + // string keyInQuery, + // AVQuery query) where TOther : AVObject + // { + // var parameters = new Dictionary { + // { "query", query.BuildParameters(true)}, + // { "key", keyInQuery} + // }; + // return CreateInstance( where: new Dictionary { + // { key, new Dictionary{{"$dontSelect", parameters}}} + // }); + // } + + // /// + // /// Adds a constraint to the query that requires that a particular key's value + // /// matches another AVQuery. This only works on keys whose values are + // /// AVObjects or lists of AVObjects. + // /// + // /// The key to check. + // /// The query that the value should match. + // /// A new query with the additional constraint. + // public virtual S WhereMatchesQuery(string key, AVQuery query) + // where TOther : AVObject + // { + // return CreateInstance( where: new Dictionary { + // { key, new Dictionary{{"$inQuery", query.BuildParameters(true)}}} + // }); + // } + + // /// + // /// Adds a proximity-based constraint for finding objects with keys whose GeoPoint + // /// values are near the given point. + // /// + // /// The key that the AVGeoPoint is stored in. + // /// The reference AVGeoPoint. + // /// A new query with the additional constraint. + // public virtual S WhereNear(string key, AVGeoPoint point) + // { + // return CreateInstance( where: new Dictionary { + // { key, new Dictionary{{"$nearSphere", point}}} + // }); + // } + + // /// + // /// Adds a constraint to the query that requires a particular key's value to be + // /// contained in the provided list of values. + // /// + // /// The key to check. + // /// The values that will match. + // /// A new query with the additional constraint. + // public virtual S WhereNotContainedIn(string key, IEnumerable values) + // { + // return CreateInstance( where: new Dictionary { + // { key, new Dictionary{{"$nin", values.ToList()}}} + // }); + // } + + // /// + // /// Adds a constraint to the query that requires a particular key's value not + // /// to be equal to the provided value. + // /// + // /// The key to check. + // /// The value that that must not be equalled. + // /// A new query with the additional constraint. + // public virtual S WhereNotEqualTo(string key, object value) + // { + // return CreateInstance( where: new Dictionary { + // { key, new Dictionary{{"$ne", value}}} + // }); + // } + + // /// + // /// Adds a constraint for finding string values that start with the provided string. + // /// This query will use the backend index, so it will be fast even with large data sets. + // /// + // /// The key that the string to match is stored in. + // /// The substring that the value must start with. + // /// A new query with the additional constraint. + // public virtual S WhereStartsWith(string key, string suffix) + // { + // return CreateInstance( where: new Dictionary { + // { key, new Dictionary{{"$regex", "^" + RegexQuote(suffix)}}} + // }); + // } + + // /// + // /// Add a constraint to the query that requires a particular key's coordinates to be + // /// contained within a given rectangular geographic bounding box. + // /// + // /// The key to be constrained. + // /// The lower-left inclusive corner of the box. + // /// The upper-right inclusive corner of the box. + // /// A new query with the additional constraint. + // public virtual S WhereWithinGeoBox(string key, + // AVGeoPoint southwest, + // AVGeoPoint northeast) + // { + + // return this.CreateInstance( where: new Dictionary + // { + // { + // key, + // new Dictionary + // { + // { + // "$within", + // new Dictionary { + // { "$box", new[] {southwest, northeast}} + // } + // } + // } + // } + // }); + // } + + // /// + // /// Adds a proximity-based constraint for finding objects with keys whose GeoPoint + // /// values are near the given point and within the maximum distance given. + // /// + // /// The key that the AVGeoPoint is stored in. + // /// The reference AVGeoPoint. + // /// The maximum distance (in radians) of results to return. + // /// A new query with the additional constraint. + // public virtual S WhereWithinDistance( + // string key, AVGeoPoint point, AVGeoDistance maxDistance) + // { + // var nearWhere = new Dictionary { + // { key, new Dictionary{{"$nearSphere", point}}} + // }; + // var mergedWhere = MergeWhere(nearWhere, new Dictionary { + // { key, new Dictionary{{"$maxDistance", maxDistance.Radians}}} + // }); + // return CreateInstance( where: mergedWhere); + // } + + // internal virtual S WhereRelatedTo(AVObject parent, string key) + // { + // return CreateInstance( where: new Dictionary { + // { + // "$relatedTo", + // new Dictionary { + // { "object", parent}, + // { "key", key} + // } + // } + // }); + // } + + // #endregion + + // /// + // /// Retrieves a list of AVObjects that satisfy this query from LeanCloud. + // /// + // /// The list of AVObjects that match this query. + // public virtual Task> FindAsync() + // { + // return FindAsync(CancellationToken.None); + // } + + // /// + // /// Retrieves a list of AVObjects that satisfy this query from LeanCloud. + // /// + // /// The cancellation token. + // /// The list of AVObjects that match this query. + // public abstract Task> FindAsync(CancellationToken cancellationToken); + + + // /// + // /// Retrieves at most one AVObject that satisfies this query. + // /// + // /// A single AVObject that satisfies this query, or else null. + // public virtual Task FirstOrDefaultAsync() + // { + // return FirstOrDefaultAsync(CancellationToken.None); + // } + + // /// + // /// Retrieves at most one AVObject that satisfies this query. + // /// + // /// The cancellation token. + // /// A single AVObject that satisfies this query, or else null. + // public abstract Task FirstOrDefaultAsync(CancellationToken cancellationToken); + + // /// + // /// Retrieves at most one AVObject that satisfies this query. + // /// + // /// A single AVObject that satisfies this query. + // /// If no results match the query. + // public virtual Task FirstAsync() + // { + // return FirstAsync(CancellationToken.None); + // } + + // /// + // /// Retrieves at most one AVObject that satisfies this query. + // /// + // /// The cancellation token. + // /// A single AVObject that satisfies this query. + // /// If no results match the query. + // public abstract Task FirstAsync(CancellationToken cancellationToken); + + // /// + // /// Counts the number of objects that match this query. + // /// + // /// The number of objects that match this query. + // public virtual Task CountAsync() + // { + // return CountAsync(CancellationToken.None); + // } + + // /// + // /// Counts the number of objects that match this query. + // /// + // /// The cancellation token. + // /// The number of objects that match this query. + // public abstract Task CountAsync(CancellationToken cancellationToken); + + // /// + // /// Constructs a AVObject whose id is already known by fetching data + // /// from the server. + // /// + // /// ObjectId of the AVObject to fetch. + // /// The AVObject for the given objectId. + // public virtual Task GetAsync(string objectId) + // { + // return GetAsync(objectId, CancellationToken.None); + // } + + // /// + // /// Constructs a AVObject whose id is already known by fetching data + // /// from the server. + // /// + // /// ObjectId of the AVObject to fetch. + // /// The cancellation token. + // /// The AVObject for the given objectId. + // public abstract Task GetAsync(string objectId, CancellationToken cancellationToken); + + // internal object GetConstraint(string key) + // { + // return where == null ? null : where.GetOrDefault(key, null); + // } + + // /// + // /// 构建查询字符串 + // /// + // /// 是否包含 ClassName + // /// + // public IDictionary BuildParameters(bool includeClassName = false) + // { + // Dictionary result = new Dictionary(); + // if (where != null) + // { + // result["where"] = PointerOrLocalIdEncoder.Instance.Encode(where); + // } + // if (orderBy != null) + // { + // result["order"] = string.Join(",", orderBy.ToArray()); + // } + // if (skip != null) + // { + // result["skip"] = skip.Value; + // } + // if (limit != null) + // { + // result["limit"] = limit.Value; + // } + // if (includes != null) + // { + // result["include"] = string.Join(",", includes.ToArray()); + // } + // if (selectedKeys != null) + // { + // result["keys"] = string.Join(",", selectedKeys.ToArray()); + // } + // if (includeClassName) + // { + // result["className"] = className; + // } + // if (redirectClassNameForKey != null) + // { + // result["redirectClassNameForKey"] = redirectClassNameForKey; + // } + // return result; + // } + + // private string RegexQuote(string input) + // { + // return "\\Q" + input.Replace("\\E", "\\E\\\\E\\Q") + "\\E"; + // } + + // private string GetRegexOptions(Regex regex, string modifiers) + // { + // string result = modifiers ?? ""; + // if (regex.Options.HasFlag(RegexOptions.IgnoreCase) && !modifiers.Contains("i")) + // { + // result += "i"; + // } + // if (regex.Options.HasFlag(RegexOptions.Multiline) && !modifiers.Contains("m")) + // { + // result += "m"; + // } + // return result; + // } + + // private IDictionary EncodeRegex(Regex regex, string modifiers) + // { + // var options = GetRegexOptions(regex, modifiers); + // var dict = new Dictionary(); + // dict["$regex"] = regex.ToString(); + // if (!string.IsNullOrEmpty(options)) + // { + // dict["$options"] = options; + // } + // return dict; + // } + + // /// + // /// Serves as the default hash function. + // /// + // /// A hash code for the current object. + // public override int GetHashCode() + // { + // // TODO (richardross): Implement this. + // return 0; + // } + //} +} diff --git a/Storage/Storage/Public/LeaderBoard/AVLeaderboard.cs b/Storage/Storage/Public/LeaderBoard/AVLeaderboard.cs new file mode 100644 index 0000000..8713c1b --- /dev/null +++ b/Storage/Storage/Public/LeaderBoard/AVLeaderboard.cs @@ -0,0 +1,479 @@ +using System; +using System.Threading.Tasks; +using System.Collections.Generic; +using LeanCloud.Storage.Internal; +using System.IO; +using System.Text; + +namespace LeanCloud { + /// + /// 排行榜顺序 + /// + public enum AVLeaderboardOrder { + /// + /// 升序 + /// + ASCENDING, + /// + /// 降序 + /// + DESCENDING + } + + /// + /// 排行榜更新策略 + /// + public enum AVLeaderboardUpdateStrategy { + /// + /// 更好地 + /// + BETTER, + /// + /// 最近的 + /// + LAST, + /// + /// 总和 + /// + SUM, + } + + /// + /// 排行榜刷新频率 + /// + public enum AVLeaderboardVersionChangeInterval { + /// + /// 从不 + /// + NEVER, + /// + /// 每天 + /// + DAY, + /// + /// 每周 + /// + WEEK, + /// + /// 每月 + /// + MONTH + } + + /// + /// 排行榜类 + /// + public class AVLeaderboard { + /// + /// 成绩名字 + /// + /// The name of the statistic. + public string StatisticName { + get; private set; + } + + /// + /// 排行榜顺序 + /// + /// The order. + public AVLeaderboardOrder Order { + get; private set; + } + + /// + /// 排行榜更新策略 + /// + /// The update strategy. + public AVLeaderboardUpdateStrategy UpdateStrategy { + get; private set; + } + + /// + /// 排行榜版本更新频率 + /// + /// The version change intervak. + public AVLeaderboardVersionChangeInterval VersionChangeInterval { + get; private set; + } + + /// + /// 版本号 + /// + /// The version. + public int Version { + get; private set; + } + + /// + /// 下次重置时间 + /// + /// The next reset at. + public DateTime NextResetAt { + get; private set; + } + + /// + /// 创建时间 + /// + /// The created at. + public DateTime CreatedAt { + get; private set; + } + + /// + /// Leaderboard 构造方法 + /// + /// 成绩名称 + AVLeaderboard(string statisticName) { + StatisticName = statisticName; + } + + AVLeaderboard() { + } + + /// + /// 创建排行榜对象 + /// + /// 排行榜对象 + /// 名称 + /// 排序方式 + /// 版本更新频率 + /// 成绩更新策略 + public static Task CreateLeaderboard(string statisticName, + AVLeaderboardOrder order = AVLeaderboardOrder.DESCENDING, + AVLeaderboardUpdateStrategy updateStrategy = AVLeaderboardUpdateStrategy.BETTER, + AVLeaderboardVersionChangeInterval versionChangeInterval = AVLeaderboardVersionChangeInterval.WEEK) { + + if (string.IsNullOrEmpty(statisticName)) { + throw new ArgumentNullException(nameof(statisticName)); + } + var data = new Dictionary { + { "statisticName", statisticName }, + { "order", order.ToString().ToLower() }, + { "versionChangeInterval", versionChangeInterval.ToString().ToLower() }, + { "updateStrategy", updateStrategy.ToString().ToLower() }, + }; + var command = new AVCommand("leaderboard/leaderboards", "POST", data: data); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).OnSuccess(t => { + try { + var leaderboard = Parse(t.Result.Item2); + return leaderboard; + } catch (Exception e) { + throw new AVException(AVException.ErrorCode.InvalidJSON, e.Message); + } + }); + } + + /// + /// 创建排行榜对象 + /// + /// 排行榜对象 + /// 名称 + public static AVLeaderboard CreateWithoutData(string statisticName) { + if (string.IsNullOrEmpty(statisticName)) { + throw new ArgumentNullException(nameof(statisticName)); + } + return new AVLeaderboard(statisticName); + } + + /// + /// 获取排行榜对象 + /// + /// 排行榜对象 + /// 名称 + public static Task GetLeaderboard(string statisticName) { + return CreateWithoutData(statisticName).Fetch(); + } + + /// + /// 更新用户成绩 + /// + /// 更新的成绩 + /// 用户 + /// 成绩 + /// 是否强行覆盖 + public static Task> UpdateStatistics(AVUser user, Dictionary statistics, bool overwrite = false) { + if (user == null) { + throw new ArgumentNullException(nameof(user)); + } + if (statistics == null || statistics.Count == 0) { + throw new ArgumentNullException(nameof(statistics)); + } + var data = new List(); + foreach (var statistic in statistics) { + var kv = new Dictionary { + { "statisticName", statistic.Key }, + { "statisticValue", statistic.Value }, + }; + data.Add(kv); + } + var path = string.Format("leaderboard/users/{0}/statistics", user.ObjectId); + if (overwrite) { + path = string.Format("{0}?overwrite=1", path); + } + var dataStr = Json.Encode(data); + var dataStream = new MemoryStream(Encoding.UTF8.GetBytes(dataStr)); + var command = new AVCommand(path, "POST", contentType: "application/json", sessionToken: user.SessionToken, stream: dataStream); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).OnSuccess(t => { + try { + List statisticList = new List(); + List list = t.Result.Item2["results"] as List; + foreach (object obj in list) { + statisticList.Add(AVStatistic.Parse(obj as IDictionary)); + } + return statisticList; + } catch (Exception e) { + throw new AVException(AVException.ErrorCode.InvalidJSON, e.Message); + } + }); + } + + /// + /// 获取用户成绩 + /// + /// 成绩列表 + /// 用户 + /// 名称列表 + public static Task> GetStatistics(AVUser user, List statisticNames = null) { + if (user == null) { + throw new ArgumentNullException(nameof(user)); + } + var path = string.Format("leaderboard/users/{0}/statistics", user.ObjectId); + if (statisticNames != null && statisticNames.Count > 0) { + var names = string.Join(",", statisticNames.ToArray()); + path = string.Format("{0}?statistics={1}", path, names); + } + var sessionToken = AVUser.CurrentUser?.SessionToken; + var command = new AVCommand(path, "GET", sessionToken, data: null); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).OnSuccess(t => { + try { + List statisticList = new List(); + List list = t.Result.Item2["results"] as List; + foreach (object obj in list) { + statisticList.Add(AVStatistic.Parse(obj as IDictionary)); + } + return statisticList; + } catch (Exception e) { + throw new AVException(AVException.ErrorCode.InvalidJSON, e.Message); + } + }); + } + + /// + /// 删除用户成绩 + /// + /// 用户 + /// 名称列表 + public static Task DeleteStatistics(AVUser user, List statisticNames) { + if (user == null) { + throw new ArgumentNullException(nameof(user)); + } + if (statisticNames == null || statisticNames.Count == 0) { + throw new ArgumentNullException(nameof(statisticNames)); + } + var path = string.Format("leaderboard/users/{0}/statistics", user.ObjectId); + var names = string.Join(",", statisticNames.ToArray()); + path = string.Format("{0}?statistics={1}", path, names); + var command = new AVCommand(path, "DELETE", sessionToken: user.SessionToken, data: null); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command); + } + + /// + /// 获取排行榜历史数据 + /// + /// 排行榜归档列表 + /// 跳过数量 + /// 分页数量 + public Task> GetArchives(int skip = 0, int limit = 10) { + var path = string.Format("leaderboard/leaderboards/{0}/archives", StatisticName); + path = string.Format("{0}?skip={1}&limit={2}", path, skip, limit); + var command = new AVCommand(path, "GET", data: null); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).OnSuccess(t => { + List archives = new List(); + List list = t.Result.Item2["results"] as List; + foreach (object obj in list) { + archives.Add(AVLeaderboardArchive.Parse(obj as IDictionary)); + } + return archives; + }); + } + + /// + /// 获取排行榜结果 + /// + /// 排名列表 + public Task> GetResults(int version = -1, int skip = 0, int limit = 10, List selectUserKeys = null, + List includeStatistics = null) { + return GetResults(null, version, skip, limit, selectUserKeys, includeStatistics); + } + + /// + /// 获取用户及附近的排名 + /// + /// 排名列表 + /// 用户 + /// 版本号 + /// 跳过数量 + /// 分页数量 + /// 包含的玩家的字段列表 + /// 包含的其他排行榜名称 + public Task> GetResultsAroundUser(int version = -1, int skip = 0, int limit = 10, + List selectUserKeys = null, + List includeStatistics = null) { + return GetResults(AVUser.CurrentUser, version, skip, limit, selectUserKeys, includeStatistics); + } + + Task> GetResults(AVUser user, + int version, int skip, int limit, + List selectUserKeys, + List includeStatistics) { + + var path = string.Format("leaderboard/leaderboards/{0}/ranks", StatisticName); + if (user != null) { + path = string.Format("{0}/{1}", path, user.ObjectId); + } + path = string.Format("{0}?skip={1}&limit={2}", path, skip, limit); + if (version != -1) { + path = string.Format("{0}&version={1}", path, version); + } + if (selectUserKeys != null) { + var keys = string.Join(",", selectUserKeys.ToArray()); + path = string.Format("{0}&includeUser={1}", path, keys); + } + if (includeStatistics != null) { + var statistics = string.Join(",", includeStatistics.ToArray()); + path = string.Format("{0}&includeStatistics={1}", path, statistics); + } + var command = new AVCommand(path, "GET", data: null); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).OnSuccess(t => { + try { + List rankingList = new List(); + List list = t.Result.Item2["results"] as List; + foreach (object obj in list) { + rankingList.Add(AVRanking.Parse(obj as IDictionary)); + } + return rankingList; + } catch (Exception e) { + throw new AVException(AVException.ErrorCode.InvalidJSON, e.Message); + } + }); + } + + /// + /// 设置更新策略 + /// + /// 排行榜对象 + /// 更新策略 + public Task UpdateUpdateStrategy(AVLeaderboardUpdateStrategy updateStrategy) { + var data = new Dictionary { + { "updateStrategy", updateStrategy.ToString().ToLower() } + }; + return Update(data).OnSuccess(t => { + UpdateStrategy = (AVLeaderboardUpdateStrategy)Enum.Parse(typeof(AVLeaderboardUpdateStrategy), t.Result["updateStrategy"].ToString().ToUpper()); + return this; + }); + } + + /// + /// 设置版本更新频率 + /// + /// 排行榜对象 + /// 版本更新频率 + public Task UpdateVersionChangeInterval(AVLeaderboardVersionChangeInterval versionChangeInterval) { + var data = new Dictionary { + { "versionChangeInterval", versionChangeInterval.ToString().ToLower() } + }; + return Update(data).OnSuccess(t => { + VersionChangeInterval = (AVLeaderboardVersionChangeInterval)Enum.Parse(typeof(AVLeaderboardVersionChangeInterval), t.Result["versionChangeInterval"].ToString().ToUpper()); + return this; + }); + } + + Task> Update(Dictionary data) { + var path = string.Format("leaderboard/leaderboards/{0}", StatisticName); + var command = new AVCommand(path, "PUT", data: data); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).OnSuccess(t => { + return t.Result.Item2; + }); + } + + /// + /// 拉取排行榜数据 + /// + /// 排行榜对象 + public Task Fetch() { + var path = string.Format("leaderboard/leaderboards/{0}", StatisticName); + var command = new AVCommand(path, "GET", data: null); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).OnSuccess(t => { + try { + // 反序列化 Leaderboard 对象 + var leaderboard = Parse(t.Result.Item2); + return leaderboard; + } catch (Exception e) { + throw new AVException(AVException.ErrorCode.InvalidJSON, e.Message); + } + }); + } + + /// + /// 重置排行榜 + /// + /// 排行榜对象 + public Task Reset() { + var path = string.Format("leaderboard/leaderboards/{0}/incrementVersion", StatisticName); + var command = new AVCommand(path, "PUT", data: null); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command).OnSuccess(t => { + try { + Init(t.Result.Item2); + return this; + } catch (Exception e) { + throw new AVException(AVException.ErrorCode.InvalidJSON, e.Message); + } + }); + } + + /// + /// 销毁排行榜 + /// + public Task Destroy() { + var path = string.Format("leaderboard/leaderboards/{0}", StatisticName); + var command = new AVCommand(path, "DELETE", data: null); + return AVPlugins.Instance.CommandRunner.RunCommandAsync(command); + } + + static AVLeaderboard Parse(IDictionary data) { + if (data == null) { + throw new ArgumentNullException(nameof(data)); + } + var leaderboard = new AVLeaderboard(); + leaderboard.Init(data); + return leaderboard; + } + + void Init(IDictionary data) { + if (data == null) { + throw new ArgumentNullException(nameof(data)); + } + object nameObj; + if (data.TryGetValue("statisticName", out nameObj)) { + StatisticName = nameObj.ToString(); + } + object orderObj; + if (data.TryGetValue("order", out orderObj)) { + Order = (AVLeaderboardOrder)Enum.Parse(typeof(AVLeaderboardOrder), orderObj.ToString().ToUpper()); + } + object strategyObj; + if (data.TryGetValue("updateStrategy", out strategyObj)) { + UpdateStrategy = (AVLeaderboardUpdateStrategy)Enum.Parse(typeof(AVLeaderboardUpdateStrategy), strategyObj.ToString().ToUpper()); + } + object intervalObj; + if (data.TryGetValue("versionChangeInterval", out intervalObj)) { + VersionChangeInterval = (AVLeaderboardVersionChangeInterval)Enum.Parse(typeof(AVLeaderboardVersionChangeInterval), intervalObj.ToString().ToUpper()); + } + object versionObj; + if (data.TryGetValue("version", out versionObj)) { + Version = int.Parse(versionObj.ToString()); + } + } + } +} diff --git a/Storage/Storage/Public/LeaderBoard/AVLeaderboardArchive.cs b/Storage/Storage/Public/LeaderBoard/AVLeaderboardArchive.cs new file mode 100644 index 0000000..a9df41f --- /dev/null +++ b/Storage/Storage/Public/LeaderBoard/AVLeaderboardArchive.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using LeanCloud.Storage.Internal; + +namespace LeanCloud { + /// + /// 归档的排行榜 + /// + public class AVLeaderboardArchive { + /// + /// 名称 + /// + /// The name of the statistic. + public string StatisticName { + get; internal set; + } + + /// + /// 版本号 + /// + /// The version. + public int Version { + get; internal set; + } + + /// + /// 状态 + /// + /// The status. + public string Status { + get; internal set; + } + + /// + /// 下载地址 + /// + /// The URL. + public string Url { + get; internal set; + } + + /// + /// 激活时间 + /// + /// The activated at. + public DateTime ActivatedAt { + get; internal set; + } + + /// + /// 归档时间 + /// + /// The deactivated at. + public DateTime DeactivatedAt { + get; internal set; + } + + AVLeaderboardArchive() { + } + + internal static AVLeaderboardArchive Parse(IDictionary data) { + if (data == null) { + throw new ArgumentNullException(nameof(data)); + } + AVLeaderboardArchive archive = new AVLeaderboardArchive { + StatisticName = data["statisticName"].ToString(), + Version = int.Parse(data["version"].ToString()), + Status = data["status"].ToString(), + Url = data["url"].ToString(), + ActivatedAt = (DateTime)AVDecoder.Instance.Decode(data["activatedAt"]), + DeactivatedAt = (DateTime)AVDecoder.Instance.Decode(data["activatedAt"]) + }; + return archive; + } + } +} diff --git a/Storage/Storage/Public/LeaderBoard/AVRanking.cs b/Storage/Storage/Public/LeaderBoard/AVRanking.cs new file mode 100644 index 0000000..6d5fa90 --- /dev/null +++ b/Storage/Storage/Public/LeaderBoard/AVRanking.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using LeanCloud.Storage.Internal; + +namespace LeanCloud { + /// + /// 排名类 + /// + public class AVRanking { + /// + /// 名次 + /// + /// The rank. + public int Rank { + get; private set; + } + + /// + /// 用户 + /// + /// The user. + public AVUser User { + get; private set; + } + + public string StatisticName { + get; private set; + } + + /// + /// 分数 + /// + /// The value. + public double Value { + get; private set; + } + + /// + /// 成绩 + /// + /// The included statistics. + public List IncludedStatistics { + get; private set; + } + + AVRanking() { + } + + internal static AVRanking Parse(IDictionary data) { + if (data == null) { + throw new ArgumentNullException(nameof(data)); + } + var ranking = new AVRanking { + Rank = int.Parse(data["rank"].ToString()), + User = AVDecoder.Instance.Decode(data["user"]) as AVUser, + StatisticName = data["statisticName"].ToString(), + Value = double.Parse(data["statisticValue"].ToString()) + }; + object statisticsObj; + if (data.TryGetValue("statistics", out statisticsObj)) { + ranking.IncludedStatistics = new List(); + var statisticsObjList = statisticsObj as List; + foreach (object statisticObj in statisticsObjList) { + var statistic = AVStatistic.Parse(statisticObj as IDictionary); + ranking.IncludedStatistics.Add(statistic); + } + } + + return ranking; + } + } +} diff --git a/Storage/Storage/Public/LeaderBoard/AVStatistic.cs b/Storage/Storage/Public/LeaderBoard/AVStatistic.cs new file mode 100644 index 0000000..6d4fdcb --- /dev/null +++ b/Storage/Storage/Public/LeaderBoard/AVStatistic.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; + +namespace LeanCloud { + /// + /// 成绩类 + /// + public class AVStatistic { + /// + /// 排行榜名称 + /// + /// The name. + public string Name { + get; private set; + } + + /// + /// 成绩值 + /// + /// The value. + public double Value { + get; private set; + } + + /// + /// 排行榜版本 + /// + /// The version. + public int Version { + get; internal set; + } + + public AVStatistic(string name, double value) { + Name = name; + Value = value; + } + + AVStatistic() { } + + internal static AVStatistic Parse(IDictionary data) { + if (data == null) { + throw new ArgumentNullException(nameof(data)); + } + AVStatistic statistic = new AVStatistic { + Name = data["statisticName"].ToString(), + Value = double.Parse(data["statisticValue"].ToString()), + Version = int.Parse(data["version"].ToString()) + }; + return statistic; + } + } +} diff --git a/Storage/Storage/Public/Utilities/Conversion.cs b/Storage/Storage/Public/Utilities/Conversion.cs new file mode 100644 index 0000000..f802f5f --- /dev/null +++ b/Storage/Storage/Public/Utilities/Conversion.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using LeanCloud.Storage.Internal; + +namespace LeanCloud.Utilities +{ + /// + /// A set of utilities for converting generic types between each other. + /// + public static class Conversion + { + /// + /// Converts a value to the requested type -- coercing primitives to + /// the desired type, wrapping lists and dictionaries appropriately, + /// or else returning null. + /// + /// This should be used on any containers that might be coming from a + /// user to normalize the collection types. Collection types coming from + /// JSON deserialization can be safely assumed to be lists or dictionaries of + /// objects. + /// + public static T As(object value) where T : class + { + return ConvertTo(value) as T; + } + + /// + /// Converts a value to the requested type -- coercing primitives to + /// the desired type, wrapping lists and dictionaries appropriately, + /// or else throwing an exception. + /// + /// This should be used on any containers that might be coming from a + /// user to normalize the collection types. Collection types coming from + /// JSON deserialization can be safely assumed to be lists or dictionaries of + /// objects. + /// + public static T To(object value) + { + return (T)ConvertTo(value); + } + + /// + /// Converts a value to the requested type -- coercing primitives to + /// the desired type, wrapping lists and dictionaries appropriately, + /// or else passing the object along to the caller unchanged. + /// + /// This should be used on any containers that might be coming from a + /// user to normalize the collection types. Collection types coming from + /// JSON deserialization can be safely assumed to be lists or dictionaries of + /// objects. + /// + internal static object ConvertTo(object value) + { + if (value is T || value == null) + { + return value; + } + + if (ReflectionHelpers.IsPrimitive(typeof(T))) + { + return (T)Convert.ChangeType(value, typeof(T)); + } + + if (ReflectionHelpers.IsConstructedGenericType(typeof(T))) + { + // Add lifting for nullables. Only supports conversions between primitives. + if (ReflectionHelpers.IsNullable(typeof(T))) + { + var innerType = ReflectionHelpers.GetGenericTypeArguments(typeof(T))[0]; + if (ReflectionHelpers.IsPrimitive(innerType)) + { + return (T)Convert.ChangeType(value, innerType); + } + } + Type listType = GetInterfaceType(value.GetType(), typeof(IList<>)); + var la = typeof(T).GetGenericTypeDefinition(); + var ilb = typeof(IList<>); + var lb = typeof(List<>); + if (listType != null && + (la == ilb || la == lb)) + { + var wrapperType = typeof(FlexibleListWrapper<,>) + .MakeGenericType(ReflectionHelpers.GetGenericTypeArguments(typeof(T))[0], + ReflectionHelpers.GetGenericTypeArguments(listType)[0]); + return Activator.CreateInstance(wrapperType, value); + } + Type dictType = GetInterfaceType(value.GetType(), typeof(IDictionary<,>)); + var da = typeof(T).GetGenericTypeDefinition(); + var db = typeof(IDictionary<,>); + if (dictType != null && + da == db) + { + var wrapperType = typeof(FlexibleDictionaryWrapper<,>) + .MakeGenericType(ReflectionHelpers.GetGenericTypeArguments(typeof(T))[1], + ReflectionHelpers.GetGenericTypeArguments(dictType)[1]); + return Activator.CreateInstance(wrapperType, value); + } + } + + return value; + } + + /// + /// Holds a dictionary that maps a cache of interface types for related concrete types. + /// The lookup is slow the first time for each type because it has to enumerate all interface + /// on the object type, but made fast by the cache. + /// + /// The map is: + /// (object type, generic interface type) => constructed generic type + /// + private static readonly Dictionary, Type> interfaceLookupCache = + new Dictionary, Type>(); + private static Type GetInterfaceType(Type objType, Type genericInterfaceType) + { + // Side note: It so sucks to have to do this. What a piece of crap bit of code + // Unfortunately, .NET doesn't provide any of the right hooks to do this for you + // *sigh* + if (ReflectionHelpers.IsConstructedGenericType(genericInterfaceType)) + { + genericInterfaceType = genericInterfaceType.GetGenericTypeDefinition(); + } + var cacheKey = new Tuple(objType, genericInterfaceType); + if (interfaceLookupCache.ContainsKey(cacheKey)) + { + return interfaceLookupCache[cacheKey]; + } + foreach (var type in ReflectionHelpers.GetInterfaces(objType)) + { + if (ReflectionHelpers.IsConstructedGenericType(type) && + type.GetGenericTypeDefinition() == genericInterfaceType) + { + return interfaceLookupCache[cacheKey] = type; + } + } + return null; + } + } +} diff --git a/Storage/Storage/Storage.csproj b/Storage/Storage/Storage.csproj new file mode 100644 index 0000000..6de1371 --- /dev/null +++ b/Storage/Storage/Storage.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + 0.1.0 + + + + + + diff --git a/csharp-sdk.sln b/csharp-sdk.sln index 26bcd87..4eaadbf 100644 --- a/csharp-sdk.sln +++ b/csharp-sdk.sln @@ -13,8 +13,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Storage.PCL", "Storage\Stor EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Storage.Unity", "Storage\Storage.Unity\Storage.Unity.csproj", "{A0D50BCB-E50E-4AAE-8E7D-24BF5AE33DAC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Storage.Test", "Storage\Storage.Test\Storage.Test.csproj", "{04DA35BB-6473-4D99-8A33-F499D40047E6}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RTM.PCL", "RTM\RTM.PCL\RTM.PCL.csproj", "{92B2B40E-A3CD-4672-AC84-2E894E1A6CE5}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RTM.Unity", "RTM\RTM.Unity\RTM.Unity.csproj", "{1E608FCD-9039-4FF7-8EE7-BA8B00E15D1C}" @@ -27,6 +25,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiveQuery.Unity", "LiveQuer EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiveQuery.Test", "LiveQuery\LiveQuery.Test\LiveQuery.Test.csproj", "{F907012C-74DF-4575-AFE6-E8DAACC26D24}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Storage", "Storage\Storage\Storage.csproj", "{B6D2D6A4-6F02-4AA0-916C-CB78238D9634}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Storage.Test", "Storage\Storage.Test\Storage.Test.csproj", "{BE05B492-78CD-47CA-9F48-C3E9B4813AFF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -41,10 +43,6 @@ Global {A0D50BCB-E50E-4AAE-8E7D-24BF5AE33DAC}.Debug|Any CPU.Build.0 = Debug|Any CPU {A0D50BCB-E50E-4AAE-8E7D-24BF5AE33DAC}.Release|Any CPU.ActiveCfg = Release|Any CPU {A0D50BCB-E50E-4AAE-8E7D-24BF5AE33DAC}.Release|Any CPU.Build.0 = Release|Any CPU - {04DA35BB-6473-4D99-8A33-F499D40047E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {04DA35BB-6473-4D99-8A33-F499D40047E6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {04DA35BB-6473-4D99-8A33-F499D40047E6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {04DA35BB-6473-4D99-8A33-F499D40047E6}.Release|Any CPU.Build.0 = Release|Any CPU {92B2B40E-A3CD-4672-AC84-2E894E1A6CE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {92B2B40E-A3CD-4672-AC84-2E894E1A6CE5}.Debug|Any CPU.Build.0 = Debug|Any CPU {92B2B40E-A3CD-4672-AC84-2E894E1A6CE5}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -69,17 +67,26 @@ Global {F907012C-74DF-4575-AFE6-E8DAACC26D24}.Debug|Any CPU.Build.0 = Debug|Any CPU {F907012C-74DF-4575-AFE6-E8DAACC26D24}.Release|Any CPU.ActiveCfg = Release|Any CPU {F907012C-74DF-4575-AFE6-E8DAACC26D24}.Release|Any CPU.Build.0 = Release|Any CPU + {B6D2D6A4-6F02-4AA0-916C-CB78238D9634}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B6D2D6A4-6F02-4AA0-916C-CB78238D9634}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6D2D6A4-6F02-4AA0-916C-CB78238D9634}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B6D2D6A4-6F02-4AA0-916C-CB78238D9634}.Release|Any CPU.Build.0 = Release|Any CPU + {BE05B492-78CD-47CA-9F48-C3E9B4813AFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE05B492-78CD-47CA-9F48-C3E9B4813AFF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE05B492-78CD-47CA-9F48-C3E9B4813AFF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE05B492-78CD-47CA-9F48-C3E9B4813AFF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {659D19F0-9A40-42C0-886C-555E64F16848} = {CD6B6669-1A56-437A-932E-BCE7F5D4CD18} {A0D50BCB-E50E-4AAE-8E7D-24BF5AE33DAC} = {CD6B6669-1A56-437A-932E-BCE7F5D4CD18} - {04DA35BB-6473-4D99-8A33-F499D40047E6} = {CD6B6669-1A56-437A-932E-BCE7F5D4CD18} {92B2B40E-A3CD-4672-AC84-2E894E1A6CE5} = {64D8F9A1-BA44-459C-817C-788B4EBC0B9F} {1E608FCD-9039-4FF7-8EE7-BA8B00E15D1C} = {64D8F9A1-BA44-459C-817C-788B4EBC0B9F} {A1BBD0B5-41C6-4579-B9A3-5EF778BE7F95} = {64D8F9A1-BA44-459C-817C-788B4EBC0B9F} {EA1C601E-D853-41F7-B9EB-276CBF7D1FA5} = {5B895B7A-1F6E-40A5-8081-43B334D2C076} {3251B4D8-D11A-4D90-8626-27FEE266B066} = {5B895B7A-1F6E-40A5-8081-43B334D2C076} {F907012C-74DF-4575-AFE6-E8DAACC26D24} = {5B895B7A-1F6E-40A5-8081-43B334D2C076} + {B6D2D6A4-6F02-4AA0-916C-CB78238D9634} = {CD6B6669-1A56-437A-932E-BCE7F5D4CD18} + {BE05B492-78CD-47CA-9F48-C3E9B4813AFF} = {CD6B6669-1A56-437A-932E-BCE7F5D4CD18} EndGlobalSection GlobalSection(MonoDevelopProperties) = preSolution version = 0.1.0