using System; using System.Collections.Generic; using System.Threading.Tasks; using MonkeyCache.FileStore; using SharedBusinessLogic.Tests.Framework.Repository; using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock; using ShareeBike.Model.Connector; using ShareeBike.Model.Device; using ShareeBike.Model.Services.CopriApi; using ShareeBike.MultilingualResources; using ShareeBike.Repository.Request; using ShareeBike.Repository.Response; using ShareeBike.Repository.Response.Stations; namespace ShareeBike.Repository { public class CopriCallsMonkeyStore : ICopriCache { /// Prevents concurrent communication. private object monkeyLock = new object(); /// Builds requests. private IRequestBuilder requestBuilder; public const string BIKESAVAILABLE = @"{ ""copri_version"" : ""4.1.0.0"", ""bikes"" : {}, ""response_state"" : ""OK"", ""apiserver"" : ""https://app.tink-konstanz.de"", ""authcookie"" : """", ""response"" : ""bikes_available"" }"; public const string BIKESOCCUPIED = @"{ ""debuglevel"" : ""1"", ""user_id"" : """", ""response"" : ""user_bikes_occupied"", ""user_group"" : [ ""Citybike"", ""ShareeBike"" ], ""authcookie"" : """", ""response_state"" : ""OK"", ""bikes_occupied"" : {}, ""copri_version"" : ""4.1.0.0"", ""apiserver"" : ""https://app.tink-konstanz.de"" }"; /// Version COPRI 4.0. or earlier public const string STATIONSALL = @"{ ""apiserver"" : """", ""authcookie"" : """", ""response"" : ""stations_all"", ""copri_version"" : ""4.1.0.0"", ""stations"" : {}, ""response_state"" : ""OK"", ""bikes_occupied"" : {} }"; /// /// Holds the seconds after which station and bikes info is considered to be invalid. /// Default value 1s. /// private TimeSpan ExpiresAfter { get; } /// Returns false because cached values are returned. public bool IsConnected => false; /// Gets the merchant id. public string MerchantId => requestBuilder.MerchantId; /// Gets the merchant id. public string SessionCookie => requestBuilder.SessionCookie; /// /// Holds a cache of copri, i.e. stations and bikes. /// private readonly CopriResponseModel copriModel; /// Initializes a instance of the copri monkey store object. /// Id of the merchant. Used to access /// Two letter ISO language name. /// Session cookie if user is logged in, null otherwise. /// Holds info about smart device. public CopriCallsMonkeyStore( string merchantId, string uiIsoLangugageName, string sessionCookie = null, ISmartDevice smartDevice = null, TimeSpan? expiresAfter = null) { ExpiresAfter = expiresAfter ?? TimeSpan.FromSeconds(1); requestBuilder = string.IsNullOrEmpty(sessionCookie) ? new RequestBuilder(merchantId, uiIsoLangugageName, smartDevice) as IRequestBuilder : new RequestBuilderLoggedIn(merchantId, uiIsoLangugageName, sessionCookie, smartDevice); var bikesAvailableEntryExists = Barrel.Current.Exists(requestBuilder.GetBikesAvailable()); var bikesAvailable = bikesAvailableEntryExists ? Barrel.Current.Get(requestBuilder.GetBikesAvailable()) : JsonConvertRethrow.DeserializeObject(BIKESAVAILABLE); // Ensure that store holds valid entries. if (!bikesAvailableEntryExists) { AddToCache(bikesAvailable, new TimeSpan(0)); } // Do not query bikes occupied if no user is logged in (leads to not implemented exception) var isLoggedIn = !string.IsNullOrEmpty(sessionCookie); var readBikesOccupiedFromCache = isLoggedIn && Barrel.Current.Exists(requestBuilder.GetBikesOccupied()); var bikesOccupied = readBikesOccupiedFromCache ? Barrel.Current.Get(requestBuilder.GetBikesOccupied()) : JsonConvertRethrow.DeserializeObject(BIKESOCCUPIED); if (isLoggedIn && !readBikesOccupiedFromCache) { AddToCache(bikesOccupied, new TimeSpan(0)); } var stationsEntryExists = Barrel.Current.Exists(requestBuilder.GetStations()); var stations = stationsEntryExists ? Barrel.Current.Get(requestBuilder.GetStations()) : JsonConvertRethrow.DeserializeObject(STATIONSALL); if (!stationsEntryExists) { AddToCache(stations, new TimeSpan(0)); } copriModel = new CopriResponseModel(bikesAvailable, bikesOccupied, stations); } public Task DoReserveAsync(string bikeId, Uri operatorUri) { throw new System.Exception(AppResources.ErrorNoWeb); } public Task DoCancelReservationAsync(string bikeId, Uri operatorUri) { throw new System.Exception(AppResources.ErrorNoWeb); } public Task CalculateAuthKeysAsync(string bikeId, Uri operatorUri) => throw new System.Exception(AppResources.ErrorNoWeb); public Task StartReturningBike( string bikeId, Uri operatorUri) => throw new System.Exception(AppResources.ErrorNoWeb); public Task UpdateLockingStateAsync( string bikeId, lock_state state, Uri operatorUri, LocationDto geolocation, double batteryLevel, IVersionInfo versionInfo) => throw new System.Exception(AppResources.ErrorNoWeb); public Task DoBookAsync(Uri operatorUri, string bikeId, Guid guid, double batteryPercentage, LockingAction? nextAction = null) => throw new System.Exception(AppResources.ErrorNoWeb); /// Books a bike and starts opening bike. /// Id of the bike to book. /// Holds the uri of the operator or null, in case of single operator setup. /// Response on booking request. public Task BookAvailableAndStartOpeningAsync( string bikeId, Uri operatorUri) => throw new System.Exception(AppResources.ErrorNoWeb); /// Books a bike and starts opening bike. /// Id of the bike to book. /// Holds the uri of the operator or null, in case of single operator setup. /// Response on booking request. public Task BookReservedAndStartOpeningAsync( string bikeId, Uri operatorUri) => throw new System.Exception(AppResources.ErrorNoWeb); public Task DoReturn( string bikeId, LocationDto geolocation, Uri operatorUri) => throw new System.Exception(AppResources.ErrorNoWeb); /// Returns a bike and starts closing. /// Id of the bike to return. /// Holds the uri of the operator or null, in case of single operator setup. /// Response on returning request. public Task ReturnAndStartClosingAsync( string bikeId, Uri operatorUri) => throw new System.Exception(AppResources.ErrorNoWeb); public Task DoSubmitFeedback(string bikeId, int? currentChargeBars, string message, bool isBikeBroken, Uri operatorUri) => throw new System.Exception(AppResources.ErrorNoWeb); /// Submits mini survey to copri server. /// Collection of answers. public Task DoSubmitMiniSurvey(IDictionary answers) => throw new System.Exception(AppResources.ErrorNoWeb); public Task DoAuthorizationAsync(string p_strMailAddress, string p_strPassword, string p_strDeviceId) { throw new System.Exception(AppResources.ErrorNoWeb); } public Task DoAuthoutAsync() { throw new System.Exception(AppResources.ErrorNoWeb); } /// Gets bikes available. /// Uri of the operator host to get bikes from or null if bikes have to be gotten form primary host. /// Id of station which is used for filtering bikes. Null if no filtering should be applied. /// Id of bike which is used for filtering bikes. Null if no filtering should be applied. public async Task GetBikesAvailableAsync( Uri operatorUri = null, string stationId = null, string bikeId = null) { var bikesAvailableTask = new TaskCompletionSource(); bikesAvailableTask.SetResult(copriModel.BikesAll .FilterByStation(stationId) .FilterByBike(bikeId)); return await bikesAvailableTask.Task; } public async Task GetBikesOccupiedAsync() { try { var bikesOccupiedTask = new TaskCompletionSource(); bikesOccupiedTask.SetResult(copriModel.BikesReservedOccupied); return await bikesOccupiedTask.Task; } catch (NotSupportedException) { // No user logged in. await Task.CompletedTask; return ResponseHelper.GetBikesOccupiedNone(); } } public async Task GetStationsAsync() { var stationsAllTask = new TaskCompletionSource(); stationsAllTask.SetResult(copriModel.Stations); return await stationsAllTask.Task; } /// Gets a value indicating whether stations are expired or not. public bool IsStationsExpired { get { lock (monkeyLock) { return Barrel.Current.IsExpired(requestBuilder.GetStations()); } } } /// Adds a stations all response to cache. /// Stations to add. public void AddToCache(StationsAvailableResponse stations) { var updateTarget = copriModel.Update(stations); AddToCache(copriModel.Stations, ExpiresAfter); if (updateTarget.HasFlag(UpdateTarget.BikesAvailableResponse)) { AddToCache(copriModel.BikesAll, ExpiresAfter); } if (updateTarget.HasFlag(UpdateTarget.BikesReservedOccupiedResponse)) { AddToCache(copriModel.BikesReservedOccupied, ExpiresAfter); } } /// Adds a stations all response to cache. /// Stations to add. /// Time after which answer is considered to be expired. private void AddToCache(StationsAvailableResponse stations, TimeSpan expiresAfter) { lock (monkeyLock) { Barrel.Current.Add( requestBuilder.GetStations(), JsonConvertRethrow.SerializeObject(stations), expiresAfter); } } /// /// Updates cache from bike which changed rental state. /// /// Response to update from. public void Update(BikeInfoReservedOrBooked response) { var updateTarget = copriModel.Update(response); if (updateTarget.HasFlag(UpdateTarget.StationsAvailableResponse)) { AddToCache(copriModel.Stations, ExpiresAfter); } if (updateTarget.HasFlag(UpdateTarget.BikesAvailableResponse)) { AddToCache(copriModel.BikesAll, ExpiresAfter); } if (updateTarget.HasFlag(UpdateTarget.BikesReservedOccupiedResponse)) { AddToCache(copriModel.BikesReservedOccupied, ExpiresAfter); } } /// Updates cache from bike which changed rental state (reservation/ booking canceled). public void Update(BookingActionResponse response) { var updateTarget = copriModel.Update(response); if (updateTarget.HasFlag(UpdateTarget.StationsAvailableResponse)) { AddToCache(copriModel.Stations, ExpiresAfter); } if (updateTarget.HasFlag(UpdateTarget.BikesAvailableResponse)) { AddToCache(copriModel.BikesAll, ExpiresAfter); } if (updateTarget.HasFlag(UpdateTarget.BikesReservedOccupiedResponse)) { AddToCache(copriModel.BikesReservedOccupied, ExpiresAfter); } } /// Gets a value indicating whether stations are expired or not. public bool IsBikesAvailableExpired { get { lock (monkeyLock) { return Barrel.Current.IsExpired(requestBuilder.GetBikesAvailable()); } } } /// Adds a bikes response to cache. /// Bikes to add. /// Uri of the operator host to get bikes from or null if bikes have to be gotten form primary host. /// Id of station which was used for filtering bikes. Null if no filtering was applied. /// Id of bike which was used for filtering bikes. Null if no filtering was applied. public void AddToCache( BikesAvailableResponse bikes, Uri operatorUri = null, string stationId = null, string bikeId = null) { var updateTarget = copriModel.Update(bikes, stationId, bikeId); AddToCache(copriModel.BikesAll, ExpiresAfter); if (updateTarget.HasFlag(UpdateTarget.BikesReservedOccupiedResponse)) { AddToCache(copriModel.BikesReservedOccupied, ExpiresAfter); } if (updateTarget.HasFlag(UpdateTarget.StationsAvailableResponse)) { AddToCache(copriModel.Stations, ExpiresAfter); } } /// Adds a bikes response to cache. /// Bikes to add. /// Time after which answer is considered to be expired. /// Uri of the operator host to get bikes from or null if bikes have to be gotten form primary host. /// Id of station which is used for filtering bikes. Null if no filtering should be applied. private void AddToCache( BikesAvailableResponse bikes, TimeSpan expiresAfter) { lock (monkeyLock) { Barrel.Current.Add( $"{requestBuilder.GetBikesAvailable()}", JsonConvertRethrow.SerializeObject(bikes), expiresAfter); } } /// Gets a value indicating whether stations are expired or not. public bool IsBikesOccupiedExpired { get { lock (monkeyLock) { return Barrel.Current.IsExpired(requestBuilder.GetBikesOccupied()); } } } /// Adds a bikes response to cache. /// Bikes to add. public void AddToCache(BikesReservedOccupiedResponse bikes) { // Update model in order to ensure a consistent state. var updateTarget = copriModel.Update(bikes); // Update cache. AddToCache(copriModel.BikesReservedOccupied, ExpiresAfter); if (updateTarget.HasFlag(UpdateTarget.BikesAvailableResponse)) { AddToCache(copriModel.BikesAll, ExpiresAfter); } if (updateTarget.HasFlag(UpdateTarget.StationsAvailableResponse)) { AddToCache(copriModel.Stations, ExpiresAfter); } } /// Adds a bikes response to cache. /// Bikes to add. /// Time after which answer is considered to be expired. private void AddToCache(BikesReservedOccupiedResponse bikes, TimeSpan expiresAfter) { lock (monkeyLock) { Barrel.Current.Add( requestBuilder.GetBikesOccupied(), JsonConvertRethrow.SerializeObject(bikes), expiresAfter); } } } }