Version 3.0.381

This commit is contained in:
Anja 2024-04-09 12:53:23 +02:00
parent f963c0a219
commit 3a363acf3a
1525 changed files with 60589 additions and 125098 deletions

View file

@ -0,0 +1,35 @@
using System;
namespace ShareeBike.Repository
{
/// <summary>
/// Holds info passed to COPRI.
/// </summary>
public class AppContextInfo
{
public AppContextInfo(string merchantId, string name, Version version)
{
Name = !string.IsNullOrEmpty(name)
? name
: throw new ArgumentNullException(nameof(name));
Version = version;
MerchantId = !string.IsNullOrEmpty(merchantId)
? merchantId
: throw new ArgumentNullException(nameof(merchantId));
}
/// <summary> Name of the app. </summary>
public string Name { get; private set; }
/// <summary> Version of the app.</summary>
public Version Version { get; private set; }
/// <summary> Merchang id of the app. </summary>
public string MerchantId { get; private set; }
/// <summary> Returns http- user agent.</summary>
public string UserAgent { get => $"{Name}/{Version}"; }
}
}

View file

@ -0,0 +1,67 @@
using ShareeBike.Repository.Response;
using ShareeBike.Repository;
using System.Linq;
using Newtonsoft.Json;
using System.Text.RegularExpressions;
namespace SharedBusinessLogic.Tests.Framework.Repository
{
public static class CopriCallsHelper
{
/// <summary>
/// Filters bikes available response by station.
/// </summary>
/// <param name="bikes"></param>
/// <param name="stationId"></param>
/// <returns></returns>
public static BikesAvailableResponse FilterByStation(this BikesAvailableResponse bikes, string stationId)
{
if (string.IsNullOrEmpty(stationId))
{
// If filter is off there is nothing to filter.
return bikes;
}
var response = CopriCallsStatic.DeserializeResponse<BikesAvailableResponse>(
$"{{ \"{nameof(VersionindependentResponse.shareejson)}\" : {JsonConvert.SerializeObject(bikes)} }}");
response.bikes.Clear();
var allBikes = bikes.bikes.Values.Where(x => x.station == stationId).ToList();
foreach (var bike in allBikes)
{
response.bikes.Add(bike.bike, bike);
}
return response;
}
/// <summary>
/// Filters bikes available response by station.
/// </summary>
/// <param name="bikes"></param>
/// <param name="stationId"></param>
/// <returns></returns>
public static BikesAvailableResponse FilterByBike(this BikesAvailableResponse bikes, string bikeId)
{
if (string.IsNullOrEmpty(bikeId))
{
// If filter is off there is nothing to filter.
return bikes;
}
var response = CopriCallsStatic.DeserializeResponse<BikesAvailableResponse>(
$"{{ \"{nameof(VersionindependentResponse.shareejson)}\" : {JsonConvert.SerializeObject(bikes)} }}");
response.bikes.Clear();
var allBikes = bikes.bikes.Values.Where(x => Regex.IsMatch(x.bike, bikeId)).ToList();
foreach (var bike in allBikes)
{
response.bikes.Add(bike.bike, bike);
}
return response;
}
}
}

View file

@ -0,0 +1,917 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.Logging;
using ShareeBike.Repository.Exception;
using ShareeBike.Repository.Request;
using ShareeBike.Repository.Response;
using ShareeBike.Repository.Response.Stations;
namespace ShareeBike.Repository
{
/// <summary> Object which manages calls to copri. </summary>
public class CopriCallsHttps : ICopriServer
{
/// <summary> Builds requests.</summary>
private readonly IRequestBuilder requestBuilder;
/// <summary> Initializes a instance of the copri calls https object. </summary>
/// <param name="copriHost">Host to connect to. </param>
/// <param name="appContextInfo">Provides app related info (app name and version, merchant id) to pass to COPRI.</param>
/// <param name="uiIsoLangugageName">Two letter ISO language name.</param>
/// <param name="smartDevice">Holds info about smart device.</param>
/// <param name="sessionCookie">Session cookie if user is logged in, null otherwise.</param>
public CopriCallsHttps(
Uri copriHost,
AppContextInfo appContextInfo,
string uiIsoLangugageName,
string sessionCookie = null,
ISmartDevice smartDevice = null)
{
m_oCopriHost = copriHost
?? throw new System.Exception($"Can not construct {GetType()}- object. Uri of copri host must not be null.");
UserAgent = appContextInfo != null
? appContextInfo.UserAgent
: throw new System.Exception($"Can not construct {GetType()}- object. User agent must not be null or empty.");
requestBuilder = string.IsNullOrEmpty(sessionCookie)
? new RequestBuilder(appContextInfo.MerchantId, uiIsoLangugageName, smartDevice) as IRequestBuilder
: new RequestBuilderLoggedIn(appContextInfo.MerchantId, uiIsoLangugageName, sessionCookie, smartDevice);
}
/// <summary> Holds the URL for rest calls.</summary>
private readonly Uri m_oCopriHost;
/// <summary> Specifies name and version of app. </summary>
private string UserAgent { get; }
/// <summary> Returns true because value requested form copri server are returned. </summary>
public bool IsConnected => true;
/// <summary> Gets the merchant id.</summary>
public string MerchantId => requestBuilder.MerchantId;
/// <summary> Gets the session cookie if user is logged in, an empty string otherwise. </summary>
public string SessionCookie => requestBuilder.SessionCookie;
/// <summary> Logs user in. </summary>
/// <param name="mailAddress">Mail address of user to log in.</param>
/// <param name="password">Password to log in.</param>
/// <param name="deviceId">Id specifying user and hardware.</param>
/// <remarks>Response which holds auth cookie <see cref="ResponseBase.authcookie"/></remarks>
public async Task<AuthorizationResponse> DoAuthorizationAsync(
string mailAddress,
string password,
string deviceId)
=> await DoAuthorizationAsync(
m_oCopriHost.AbsoluteUri,
requestBuilder.DoAuthorization(mailAddress, password, deviceId),
() => requestBuilder.DoAuthorization(mailAddress, "********", deviceId),
UserAgent);
/// <summary> Logs user out. </summary>
/// <remarks>Response which holds auth cookie <see cref="ResponseBase.authcookie"/></remarks>
public async Task<AuthorizationoutResponse> DoAuthoutAsync()
=> await DoAuthoutAsync(m_oCopriHost.AbsoluteUri, requestBuilder.DoAuthout(), UserAgent);
/// <summary>Gets bikes available.</summary>
/// <param name="operatorUri">Uri of the operator host to get bikes from or null if bikes have to be gotten form primary host.</param>
/// <param name="stationId"> Id of station which is used for filtering bikes. Null if no filtering should be applied.</param>
/// <param name="bikeId"> Id of bike which is used for filtering bikes. Null if no filtering should be applied.</param>
/// <returns>Response holding list of bikes.</returns>
public async Task<BikesAvailableResponse> GetBikesAvailableAsync(Uri operatorUri = null, string stationId = null, string bikeId = null)
=> await GetBikesAvailableAsync(
operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri,
requestBuilder.GetBikesAvailable(stationId, bikeId),
UserAgent);
/// <summary> Gets a list of bikes reserved/ booked by active user. </summary>
/// <returns>Response holding list of bikes.</returns>
public async Task<BikesReservedOccupiedResponse> GetBikesOccupiedAsync()
{
try
{
return await GetBikesOccupiedAsync(m_oCopriHost.AbsoluteUri, requestBuilder.GetBikesOccupied(), UserAgent);
}
catch (NotSupportedException)
{
// No user logged in.
await Task.CompletedTask;
return ResponseHelper.GetBikesOccupiedNone();
}
}
/// <summary> Get list of stations. </summary>
/// <returns>List of files.</returns>
public async Task<StationsAvailableResponse> GetStationsAsync()
=> await GetStationsAsync(m_oCopriHost.AbsoluteUri, requestBuilder.GetStations(), UserAgent);
/// <summary> Get authentication keys. </summary>
/// <param name="bikeId">Id of the bike to get keys for.</param>
/// <returns>Response holding authentication keys.</returns>
public async Task<ReservationBookingResponse> GetAuthKeys(string bikeId)
=> await GetAuthKeysAsync(m_oCopriHost.AbsoluteUri, requestBuilder.CalculateAuthParameters(bikeId), UserAgent);
/// <summary> Gets booking request response.</summary>
/// <param name="bikeId">Id of the bike to book.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Booking response.</returns>
public async Task<ReservationBookingResponse> DoReserveAsync(
string bikeId,
Uri operatorUri)
=> await DoReserveAsync(
operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri,
requestBuilder.DoReserve(bikeId),
UserAgent);
/// <summary> Gets cancel booking request response.</summary>
/// <param name="bikeId">Id of the bike to book.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Response on cancel booking request.</returns>
public async Task<BookingActionResponse> DoCancelReservationAsync(
string bikeId,
Uri operatorUri)
=> await DoCancelReservationAsync(
operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri,
requestBuilder.DoCancelReservation(bikeId),
UserAgent);
/// <summary> Get authentication keys. </summary>
/// <param name="bikeId">Id of the bike to get keys for.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Response holding authentication keys.</returns>
public async Task<ReservationBookingResponse> CalculateAuthKeysAsync(
string bikeId,
Uri operatorUri)
=> await GetAuthKeysAsync(
operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri,
requestBuilder.CalculateAuthParameters(bikeId),
UserAgent);
/// <summary> Notifies COPRI about start of returning sequence. </summary>
/// <remarks> Operator specific call.</remarks>
/// <param name="bikeId">Id of the bike to return.</param>+
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Response on notification about start of returning sequence.</returns>
public async Task<ResponseBase> StartReturningBike(
string bikeId,
Uri operatorUri)
=> await DoStartReturningBike(
operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri,
requestBuilder.StartReturningBike(bikeId),
UserAgent);
/// <summary> Updates lock state for a booked bike. </summary>
/// <param name="bikeId">Id of the bike to update locking state for.</param>
/// <param name="location">Geolocation of lock.</param>
/// <param name="state">New locking state.</param>
/// <param name="batteryPercentage">Holds the filling level percentage of the battery.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Response on updating locking state.</returns>
public async Task<ReservationBookingResponse> UpdateLockingStateAsync(
string bikeId,
lock_state state,
Uri operatorUri,
LocationDto location,
double batteryLevel,
IVersionInfo versionInfo) =>
await DoUpdateLockingStateAsync(
operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri,
requestBuilder.UpdateLockingState(bikeId, state, location, batteryLevel, versionInfo),
UserAgent);
/// <summary> Gets booking request. </summary>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <param name="bikeId">Id of the bike to book.</param>
/// <param name="guid">Used to publish GUID from app to copri. Used for initial setup of bike in copri.</param>
/// <param name="batteryPercentage">Holds the filling level percentage of the battery.</param>
/// <param name="nextAction">If not null next locking action which is performed after booking.</param>
/// <returns>Request on booking request.</returns>
public async Task<ReservationBookingResponse> DoBookAsync(
Uri operatorUri,
string bikeId,
Guid guid,
double batteryPercentage,
LockingAction? nextAction = null)
=> await DoBookAsync(
operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri,
requestBuilder.DoBook(bikeId, guid, batteryPercentage, nextAction),
UserAgent);
/// <summary> Books a bike and starts opening bike. </summary>
/// <param name="bikeId">Id of the bike to book.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Response on booking request.</returns>
public async Task<ReservationBookingResponse> BookAvailableAndStartOpeningAsync(
string bikeId,
Uri operatorUri)
=> await DoBookAsync(
operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri,
requestBuilder.BookAvailableAndStartOpening(bikeId),
UserAgent);
/// <summary> Books a bike and starts opening bike. </summary>
/// <param name="bikeId">Id of the bike to book.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Response on booking request.</returns>
public async Task<ReservationBookingResponse> BookReservedAndStartOpeningAsync(
string bikeId,
Uri operatorUri)
=> await DoBookAsync(
operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri,
requestBuilder.BookReservedAndStartOpening(bikeId),
UserAgent);
/// <summary> Returns a bike. </summary>
/// <param name="bikeId">Id of the bike to return.</param>
/// <param name="location">Geolocation of lock.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Response on returning request.</returns>
public async Task<DoReturnResponse> DoReturn(
string bikeId,
LocationDto location,
Uri operatorUri)
=> await DoReturn(
operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri,
requestBuilder.DoReturn(bikeId, location),
UserAgent);
/// <summary> Returns a bike and starts closing. </summary>
/// <param name="bikeId">Id of the bike to return.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Response on returning request.</returns>
public async Task<DoReturnResponse> ReturnAndStartClosingAsync(
string bikeId,
Uri operatorUri)
=> await DoReturn(
operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri,
requestBuilder.ReturnAndStartClosing(bikeId),
UserAgent);
/// <summary> Submits feedback to copri server. </summary>
/// <param name="bikeId">Id of the bike to which the feedback is related to.</param>
/// <param name="currentChargeBars">Null if bike has no engine or charge is unknown. Otherwise the charge filling level of the drive battery.</param>
/// <param name="isBikeBroken">True if bike is broken.</param>
/// <param name="message">General purpose message or error description.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Response on submitting feedback request.</returns>
public async Task<SubmitFeedbackResponse> DoSubmitFeedback(
string bikeId,
int? currentChargeBars,
string message,
bool isBikeBroken,
Uri operatorUri) =>
await DoSubmitFeedback(
operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri,
requestBuilder.DoSubmitFeedback(bikeId, currentChargeBars, message, isBikeBroken),
UserAgent);
/// <summary> Submits mini survey to copri server. </summary>
/// <param name="answers">Collection of answers.</param>
public Task<ResponseBase> DoSubmitMiniSurvey(IDictionary<string, string> answers)
=> DoSubmitMiniSurvey(
m_oCopriHost.AbsoluteUri,
requestBuilder.DoSubmitMiniSurvey(answers),
UserAgent);
/// <summary> Logs user in. </summary>
/// <param name="copriHost">Host to connect to. </param>
/// <param name="command">Command to log user in.</param>
/// <remarks>Response which holds auth cookie <see cref="ResponseBase.authcookie"/></remarks>
public static async Task<AuthorizationResponse> DoAuthorizationAsync(
string copriHost,
string command,
Func<string> displayCommand,
string userAgent = null)
{
#if !WINDOWS_UWP
/// Extract session cookie from response.
string response = string.Empty;
try
{
response = await PostAsync(
copriHost,
command,
userAgent,
displayCommand); // Do not include password into exception output when an error occurs.
}
catch (System.Exception l_oException)
{
if (l_oException.GetIsConnectFailureException())
{
throw new WebConnectFailureException("Login fehlgeschlagen aufgrund eines Netzwerkfehlers.", l_oException);
}
if (l_oException.GetIsForbiddenException())
{
throw new WebForbiddenException("Login fehlgeschlagen aufgrund eines Netzwerkfehlers.", l_oException);
}
throw;
}
return CopriCallsStatic.DeserializeResponse<AuthorizationResponse>(response, (version) => new UnsupportedCopriVersionDetectedException());
#else
return null;
#endif
}
/// <summary> Logs user out. </summary>
/// <param name="copriHost">Host to connect to. </param>
/// <param name="command">Command to log user out.</param>
public static async Task<AuthorizationoutResponse> DoAuthoutAsync(
string copriHost,
string command,
string userAgent = null)
{
#if !WINDOWS_UWP
string logoutResponse;
try
{
logoutResponse = await PostAsync(copriHost, command, userAgent);
}
catch (System.Exception l_oException)
{
if (l_oException.GetIsConnectFailureException())
{
throw new WebConnectFailureException("Login fehlgeschlagen wegen Netzwerkfehler.", l_oException);
}
if (l_oException.GetIsForbiddenException())
{
throw new WebForbiddenException("Login fehlgeschlagen wegen Netzwerkfehler.", l_oException);
}
throw;
}
/// Extract session cookie from response.
return CopriCallsStatic.DeserializeResponse<AuthorizationoutResponse>(logoutResponse, (version) => new UnsupportedCopriVersionDetectedException());
#else
return null;
#endif
}
/// <summary>
/// Get list of stations from file.
/// </summary>
/// <param name="copriHost">URL of the copri host to connect to.</param>
/// <param name="command">Command to get stations.</param>
/// <returns>List of files.</returns>
public static async Task<StationsAvailableResponse> GetStationsAsync(
string copriHost,
string command,
string userAgent = null)
{
#if !WINDOWS_UWP
string response;
try
{
response = await PostAsync(copriHost, command, userAgent);
}
catch (System.Exception l_oException)
{
if (l_oException.GetIsConnectFailureException())
{
throw new WebConnectFailureException("Abfage der verfügbaren Räder fehlgeschlagen wegen Netzwerkfehler.", l_oException);
}
if (l_oException.GetIsForbiddenException())
{
throw new WebForbiddenException("Abfage der verfügbaren Räder fehlgeschlagen wegen Netzwerkfehler.", l_oException);
}
throw;
}
// Extract bikes from response.
return CopriCallsStatic.DeserializeResponse(
response,
(version) => CopriCallsMonkeyStoreHelper.GetEmptyStationsAllResponse(version));
#else
return null;
#endif
}
/// <summary> Gets a list of bikes from Copri. </summary>
/// <param name="copriHost">URL of the copri host to connect to.</param>
/// <param name="command">Command to get bikes.</param>
/// <returns>Response holding list of bikes.</returns>
public static async Task<BikesAvailableResponse> GetBikesAvailableAsync(
string copriHost,
string command,
string userAgent = null)
{
#if !WINDOWS_UWP
string response;
try
{
response = await PostAsync(copriHost, command, userAgent);
}
catch (System.Exception l_oException)
{
if (l_oException.GetIsConnectFailureException())
{
throw new WebConnectFailureException("Abfage der verfügbaren Räder fehlgeschlagen wegen Netzwerkfehler.", l_oException);
}
if (l_oException.GetIsForbiddenException())
{
throw new WebForbiddenException("Abfage der verfügbaren Räder fehlgeschlagen wegen Netzwerkfehler.", l_oException);
}
throw;
}
// Extract bikes from response.
return CopriCallsStatic.DeserializeResponse(
response,
(version) => CopriCallsMonkeyStoreHelper.GetEmptyBikesAvailableResponse(version));
#else
return null;
#endif
}
/// <summary> Gets a list of bikes reserved/ booked by active user from Copri.</summary>
/// <param name="copriHost">URL of the copri host to connect to.</param>
/// <param name="command">Command to post.</param>
/// <returns>Response holding list of bikes.</returns>
public static async Task<BikesReservedOccupiedResponse> GetBikesOccupiedAsync(
string copriHost,
string command,
string userAgent = null)
{
#if !WINDOWS_UWP
string response;
try
{
response = await PostAsync(copriHost, command, userAgent);
}
catch (System.Exception l_oException)
{
if (l_oException.GetIsConnectFailureException())
{
throw new WebConnectFailureException("Abfage der reservierten/ gebuchten Räder fehlgeschlagen wegen Netzwerkfehler.", l_oException);
}
if (l_oException.GetIsForbiddenException())
{
throw new WebForbiddenException("Abfage der reservierten/ gebuchten Räder fehlgeschlagen wegen Netzwerkfehler.", l_oException);
}
throw;
}
// Extract bikes from response.
return CopriCallsStatic.DeserializeResponse(
response,
(version) => CopriCallsMonkeyStoreHelper.GetEmptyBikesReservedOccupiedResponse(version));
#else
return null;
#endif
}
/// <summary> Get auth keys from COPRI. </summary>
/// <param name="copriHost">Host to connect to. </param>
/// <param name="command">Command to log user in.</param>
/// <returns>Response on booking request.</returns>
public static async Task<ReservationBookingResponse> GetAuthKeysAsync(
string copriHost,
string command,
string userAgent = null)
{
#if !WINDOWS_UWP
string bikesAvaialbeResponse;
try
{
bikesAvaialbeResponse = await PostAsync(copriHost, command, userAgent);
}
catch (System.Exception exception)
{
if (exception.GetIsConnectFailureException())
{
throw new WebConnectFailureException("Schlosssuche wegen Netzwerkfehler fehlgeschlagen.", exception);
}
if (exception.GetIsForbiddenException())
{
throw new WebForbiddenException("Schlosssuche wegen Netzwerkfehler fehlgeschlagen.", exception);
}
throw;
}
// Extract bikes from response.
return JsonConvertRethrow.DeserializeObject<ResponseContainer<ReservationBookingResponse>>(bikesAvaialbeResponse)?.shareejson;
#else
return null;
#endif
}
/// <summary> Gets booking request response. </summary>
/// <param name="copriHost">Host to connect to. </param>
/// <param name="command">Command to log user in.</param>
/// <returns>Response on booking request.</returns>
public static async Task<ReservationBookingResponse> DoReserveAsync(
string copriHost,
string command,
string userAgent = null)
{
#if !WINDOWS_UWP
string bikesAvaialbeResponse;
try
{
bikesAvaialbeResponse = await PostAsync(copriHost, command, userAgent);
}
catch (System.Exception exception)
{
if (exception.GetIsConnectFailureException())
{
throw new WebConnectFailureException("Reservierung des Fahrrads wegen Netzwerkfehler fehlgeschlagen.", exception);
}
if (exception.GetIsForbiddenException())
{
throw new WebForbiddenException("Reservierung des Fahrrads wegen Netzwerkfehler fehlgeschlagen.", exception);
}
throw;
}
// Extract bikes from response.
return JsonConvertRethrow.DeserializeObject<ResponseContainer<ReservationBookingResponse>>(bikesAvaialbeResponse)?.shareejson;
#else
return null;
#endif
}
/// <summary> Gets cancel booking request response.</summary>
/// <param name="copriHost">Host to connect to. </param>
/// <param name="command">Command to log user in.</param>
/// <returns>Response on cancel booking request.</returns>
public static async Task<BookingActionResponse> DoCancelReservationAsync(
string copriHost,
string command,
string userAgent = null)
{
#if !WINDOWS_UWP
string l_oBikesAvaialbeResponse;
try
{
l_oBikesAvaialbeResponse = await PostAsync(copriHost, command, userAgent);
}
catch (System.Exception l_oException)
{
if (l_oException.GetIsConnectFailureException())
{
throw new WebConnectFailureException("Reservierung des Fahrrads aufgrund eines Netzwerkfehlers fehlgeschlagen.", l_oException);
}
if (l_oException.GetIsForbiddenException())
{
throw new WebForbiddenException("Reservierung des Fahrrads aufgrund eines Netzwerkfehlers fehlgeschlagen.", l_oException);
}
throw;
}
// Extract bikes from response.
return JsonConvertRethrow.DeserializeObject<ResponseContainer<BookingActionResponse>>(l_oBikesAvaialbeResponse)?.shareejson;
#else
return null;
#endif
}
public static async Task<ResponseBase> DoStartReturningBike(
string copriHost,
string command,
string agent = null)
{
#if !WINDOWS_UWP
string response;
try
{
response = await PostAsync(copriHost, command, agent);
}
catch (System.Exception exception)
{
if (exception.GetIsConnectFailureException())
{
throw new WebConnectFailureException("Benachrichtigung von Start der Rückgabe wegen Netzwerkfehler fehlgeschlagen.", exception);
}
if (exception.GetIsForbiddenException())
{
throw new WebForbiddenException("Benachrichtigung von Start der Rückgabe wegen Netzwerkfehler fehlgeschlagen.", exception);
}
throw;
}
// Extract bikes from response.
return JsonConvertRethrow.DeserializeObject<ResponseContainer<ResponseBase>>(response)?.shareejson;
#else
return null;
#endif
}
public static async Task<ReservationBookingResponse> DoUpdateLockingStateAsync(
string copriHost,
string command,
string agent = null)
{
#if !WINDOWS_UWP
string bikesAvaialbeResponse;
try
{
bikesAvaialbeResponse = await PostAsync(copriHost, command, agent);
}
catch (System.Exception exception)
{
if (exception.GetIsConnectFailureException())
{
throw new WebConnectFailureException("Aktualisierung des Schlossstatuses wegen Netzwerkfehler fehlgeschlagen.", exception);
}
if (exception.GetIsForbiddenException())
{
throw new WebForbiddenException("Aktualisierung des Schlossstatuses wegen Netzwerkfehler fehlgeschlagen.", exception);
}
throw;
}
// Extract bikes from response.
return JsonConvertRethrow.DeserializeObject<ResponseContainer<ReservationBookingResponse>>(bikesAvaialbeResponse)?.shareejson;
#else
return null;
#endif
}
public static async Task<ReservationBookingResponse> DoBookAsync(
string copriHost,
string command,
string agent = null)
{
#if !WINDOWS_UWP
string bikesAvaialbeResponse;
try
{
bikesAvaialbeResponse = await PostAsync(copriHost, command, agent);
}
catch (System.Exception exception)
{
if (exception.GetIsConnectFailureException())
{
throw new WebConnectFailureException("Buchung des Fahrrads wegen Netzwerkfehler fehlgeschlagen.", exception);
}
if (exception.GetIsForbiddenException())
{
throw new WebForbiddenException("Buchung des Fahrrads wegen Netzwerkfehler fehlgeschlagen.", exception);
}
throw;
}
// Extract bikes from response.
return JsonConvertRethrow.DeserializeObject<ResponseContainer<ReservationBookingResponse>>(bikesAvaialbeResponse)?.shareejson;
#else
return null;
#endif
}
public static async Task<DoReturnResponse> DoReturn(
string copriHost,
string command,
string userAgent = null)
{
#if !WINDOWS_UWP
string doReturnResponse;
try
{
doReturnResponse = await PostAsync(copriHost, command, userAgent);
}
catch (System.Exception l_oException)
{
if (l_oException.GetIsConnectFailureException())
{
throw new WebConnectFailureException("Rückgabe des Fahrrads aufgrund eines Netzwerkfehlers fehlgeschlagen.", l_oException);
}
if (l_oException.GetIsForbiddenException())
{
throw new WebForbiddenException("Rückgabe des Fahrrads aufgrund eines Netzwerkfehlers fehlgeschlagen.", l_oException);
}
throw;
}
// Extract bikes from response.
return JsonConvertRethrow.DeserializeObject<ResponseContainer<DoReturnResponse>>(doReturnResponse)?.shareejson;
#else
return null;
#endif
}
public async Task<SubmitFeedbackResponse> DoSubmitFeedback(
string copriHost,
string command,
string userAgent = null)
{
#if !WINDOWS_UWP
string userFeedbackResponse;
try
{
userFeedbackResponse = await PostAsync(copriHost, command, userAgent);
}
catch (System.Exception l_oException)
{
if (l_oException.GetIsConnectFailureException())
{
throw new WebConnectFailureException("Senden der Rückmeldung aufgrund eines Netzwerkfehlers fehlgeschlagen.", l_oException);
}
if (l_oException.GetIsForbiddenException())
{
throw new WebForbiddenException("Senden der Rückmeldung aufgrund eines Netzwerkfehlers fehlgeschlagen.", l_oException);
}
throw;
}
// Extract bikes from response.
return JsonConvertRethrow.DeserializeObject<ResponseContainer<SubmitFeedbackResponse>>(userFeedbackResponse)?.shareejson;
#else
return null;
#endif
}
/// <summary> Submits mini survey to copri server. </summary>
public async Task<ResponseBase> DoSubmitMiniSurvey(
string copriHost,
string command,
string userAgent = null)
{
#if !WINDOWS_UWP
string miniSurveyResponse;
try
{
miniSurveyResponse = await PostAsync(copriHost, command, userAgent);
}
catch (System.Exception l_oException)
{
if (l_oException.GetIsConnectFailureException())
{
throw new WebConnectFailureException("Senden der Miniumfrage aufgrund eines Netzwerkfehlers fehlgeschlagen.", l_oException);
}
if (l_oException.GetIsForbiddenException())
{
throw new WebForbiddenException("Senden der der Miniumfrage aufgrund eines Netzwerkfehlers fehlgeschlagen.", l_oException);
}
throw;
}
// Extract bikes from response.
return JsonConvertRethrow.DeserializeObject<ResponseContainer<ResponseBase>>(miniSurveyResponse)?.shareejson;
#else
return null;
#endif
}
/// <summary> Https get- request.</summary>
/// <param name="Url">Url to get info from.</param>
/// <returns>response from server</returns>
public static async Task<string> Get(string Url)
{
string result = string.Empty;
HttpWebRequest myRequest = (HttpWebRequest)WebRequest.Create(Url);
myRequest.Method = "GET";
using (var myResponse = await myRequest.GetResponseAsync())
{
using (var sr = new StreamReader(myResponse.GetResponseStream(), Encoding.UTF8))
{
result = sr.ReadToEnd();
}
}
return result;
}
/// <summary> Https- post request.</summary>
/// <param name="command">Command to send.</param>
/// <param name="displayCommand">Command to display/ log used for error handling.</param>
/// <param name="uRL">Address of server to communicate with.</param>
/// <returns>Response as text.</returns>
/// <changelog> An unused member PostAsyncHttpClient using HttpClient for posting was removed 2020-04-02.</changelog>
private static async Task<string> PostAsync(
string uRL,
string command,
string userAgent = null,
Func<string> displayCommand = null)
{
if (string.IsNullOrEmpty(command))
{
Log.ForContext<CopriCallsHttps>().Fatal("Can not post command. Command must not be null or empty.");
throw new ArgumentException("Can not post command. Command must not be null or empty.");
}
if (string.IsNullOrEmpty(uRL))
{
Log.ForContext<CopriCallsHttps>().Fatal("Can not post command. Host must not be null or empty.");
throw new ArgumentException("Can not post command. Host must not be null or empty.");
}
// Get display version of command to used for display/ logging (password should never be included in output)
Func<string> displayCommandFunc = displayCommand ?? delegate () { return command; };
try
{
#if !WINDOWS_UWP
var l_strHost = uRL;
// Returns a https request.
var request = WebRequest.CreateHttp(l_strHost);
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
request.UserAgent = userAgent;
// Workaround for issue https://bugzilla.xamarin.com/show_bug.cgi?id=57705
// If not KeepAlive is set to true Stream.Write leads arbitrarily to an object disposed exception.
request.KeepAlive = true;
byte[] postData = Encoding.UTF8.GetBytes(command);
request.ContentLength = postData.Length;
// Get the request stream.
using (Stream dataStream = await request.GetRequestStreamAsync())
{
// Write the data to the request stream.
await dataStream.WriteAsync(postData, 0, postData.Length);
}
// Get the response.
var webResponse = await request.GetResponseAsync() as HttpWebResponse;
if (webResponse == null)
{
throw new System.Exception(string.Format("Reserve request failed. Response form from server was not of expected type."));
}
if (webResponse.StatusCode != HttpStatusCode.OK)
{
throw new CommunicationException(string.Format(
"Posting request {0} failed. Expected status code is {1} but was {2}.",
displayCommandFunc(),
HttpStatusCode.OK,
webResponse.StatusCode));
}
string response = string.Empty;
// Get the request stream.
using (Stream l_oDataStream = webResponse.GetResponseStream())
using (StreamReader l_oReader = new StreamReader(l_oDataStream))
{
// Read the content.
response = l_oReader.ReadToEnd();
// Display the content.
Console.WriteLine(response);
// Clean up the streams.
webResponse.Close();
}
Log.ForContext<CopriCallsHttps>().Verbose("Post command {DisplayCommand} to host {URL} received {ResponseText:j}.", displayCommandFunc(), uRL, response);
return response;
#else
return null;
#endif
}
catch (System.Exception exception)
{
Log.ForContext<CopriCallsHttps>().InformationOrError("Posting command {DisplayCommand} to host {URL} failed. {Exception}.", displayCommandFunc(), uRL, exception);
throw;
}
}
}
}

View file

@ -0,0 +1,443 @@
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
{
/// <summary> Prevents concurrent communication. </summary>
private object monkeyLock = new object();
/// <summary> Builds requests.</summary>
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""
}";
/// <summary> Version COPRI 4.0. or earlier</summary>
public const string STATIONSALL = @"{
""apiserver"" : """",
""authcookie"" : """",
""response"" : ""stations_all"",
""copri_version"" : ""4.1.0.0"",
""stations"" : {},
""response_state"" : ""OK"",
""bikes_occupied"" : {}
}";
/// <summary>
/// Holds the seconds after which station and bikes info is considered to be invalid.
/// Default value 1s.
/// </summary>
private TimeSpan ExpiresAfter { get; }
/// <summary> Returns false because cached values are returned. </summary>
public bool IsConnected => false;
/// <summary> Gets the merchant id.</summary>
public string MerchantId => requestBuilder.MerchantId;
/// <summary> Gets the merchant id.</summary>
public string SessionCookie => requestBuilder.SessionCookie;
/// <summary>
/// Holds a cache of copri, i.e. stations and bikes.
/// </summary>
private readonly CopriResponseModel copriModel;
/// <summary> Initializes a instance of the copri monkey store object. </summary>
/// <param name="merchantId">Id of the merchant. Used to access </param>
/// <param name="uiIsoLangugageName">Two letter ISO language name.</param>
/// <param name="sessionCookie">Session cookie if user is logged in, null otherwise.</param>
/// <param name="smartDevice">Holds info about smart device.</param>
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<BikesAvailableResponse>(requestBuilder.GetBikesAvailable())
: JsonConvertRethrow.DeserializeObject<BikesAvailableResponse>(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<BikesReservedOccupiedResponse>(requestBuilder.GetBikesOccupied())
: JsonConvertRethrow.DeserializeObject<BikesReservedOccupiedResponse>(BIKESOCCUPIED);
if (isLoggedIn && !readBikesOccupiedFromCache)
{
AddToCache(bikesOccupied, new TimeSpan(0));
}
var stationsEntryExists = Barrel.Current.Exists(requestBuilder.GetStations());
var stations = stationsEntryExists
? Barrel.Current.Get<StationsAvailableResponse>(requestBuilder.GetStations())
: JsonConvertRethrow.DeserializeObject<StationsAvailableResponse>(STATIONSALL);
if (!stationsEntryExists)
{
AddToCache(stations, new TimeSpan(0));
}
copriModel = new CopriResponseModel(bikesAvailable, bikesOccupied, stations);
}
public Task<ReservationBookingResponse> DoReserveAsync(string bikeId, Uri operatorUri)
{
throw new System.Exception(AppResources.ErrorNoWeb);
}
public Task<BookingActionResponse> DoCancelReservationAsync(string bikeId, Uri operatorUri)
{
throw new System.Exception(AppResources.ErrorNoWeb);
}
public Task<ReservationBookingResponse> CalculateAuthKeysAsync(string bikeId, Uri operatorUri)
=> throw new System.Exception(AppResources.ErrorNoWeb);
public Task<ResponseBase> StartReturningBike(
string bikeId,
Uri operatorUri)
=> throw new System.Exception(AppResources.ErrorNoWeb);
public Task<ReservationBookingResponse> UpdateLockingStateAsync(
string bikeId,
lock_state state,
Uri operatorUri,
LocationDto geolocation,
double batteryLevel,
IVersionInfo versionInfo)
=> throw new System.Exception(AppResources.ErrorNoWeb);
public Task<ReservationBookingResponse> DoBookAsync(Uri operatorUri, string bikeId, Guid guid, double batteryPercentage, LockingAction? nextAction = null)
=> throw new System.Exception(AppResources.ErrorNoWeb);
/// <summary> Books a bike and starts opening bike. </summary>
/// <param name="bikeId">Id of the bike to book.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Response on booking request.</returns>
public Task<ReservationBookingResponse> BookAvailableAndStartOpeningAsync(
string bikeId,
Uri operatorUri)
=> throw new System.Exception(AppResources.ErrorNoWeb);
/// <summary> Books a bike and starts opening bike. </summary>
/// <param name="bikeId">Id of the bike to book.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Response on booking request.</returns>
public Task<ReservationBookingResponse> BookReservedAndStartOpeningAsync(
string bikeId,
Uri operatorUri)
=> throw new System.Exception(AppResources.ErrorNoWeb);
public Task<DoReturnResponse> DoReturn(
string bikeId,
LocationDto geolocation,
Uri operatorUri)
=> throw new System.Exception(AppResources.ErrorNoWeb);
/// <summary> Returns a bike and starts closing. </summary>
/// <param name="bikeId">Id of the bike to return.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Response on returning request.</returns>
public Task<DoReturnResponse> ReturnAndStartClosingAsync(
string bikeId,
Uri operatorUri)
=> throw new System.Exception(AppResources.ErrorNoWeb);
public Task<SubmitFeedbackResponse> DoSubmitFeedback(string bikeId, int? currentChargeBars, string message, bool isBikeBroken, Uri operatorUri)
=> throw new System.Exception(AppResources.ErrorNoWeb);
/// <summary> Submits mini survey to copri server. </summary>
/// <param name="answers">Collection of answers.</param>
public Task<ResponseBase> DoSubmitMiniSurvey(IDictionary<string, string> answers)
=> throw new System.Exception(AppResources.ErrorNoWeb);
public Task<AuthorizationResponse> DoAuthorizationAsync(string p_strMailAddress, string p_strPassword, string p_strDeviceId)
{
throw new System.Exception(AppResources.ErrorNoWeb);
}
public Task<AuthorizationoutResponse> DoAuthoutAsync()
{
throw new System.Exception(AppResources.ErrorNoWeb);
}
/// <summary>Gets bikes available.</summary>
/// <param name="operatorUri">Uri of the operator host to get bikes from or null if bikes have to be gotten form primary host.</param>
/// <param name="stationId"> Id of station which is used for filtering bikes. Null if no filtering should be applied.</param>
/// <param name="bikeId"> Id of bike which is used for filtering bikes. Null if no filtering should be applied.</param>
public async Task<BikesAvailableResponse> GetBikesAvailableAsync(
Uri operatorUri = null,
string stationId = null,
string bikeId = null)
{
var bikesAvailableTask = new TaskCompletionSource<BikesAvailableResponse>();
bikesAvailableTask.SetResult(copriModel.BikesAll
.FilterByStation(stationId)
.FilterByBike(bikeId));
return await bikesAvailableTask.Task;
}
public async Task<BikesReservedOccupiedResponse> GetBikesOccupiedAsync()
{
try
{
var bikesOccupiedTask = new TaskCompletionSource<BikesReservedOccupiedResponse>();
bikesOccupiedTask.SetResult(copriModel.BikesReservedOccupied);
return await bikesOccupiedTask.Task;
}
catch (NotSupportedException)
{
// No user logged in.
await Task.CompletedTask;
return ResponseHelper.GetBikesOccupiedNone();
}
}
public async Task<StationsAvailableResponse> GetStationsAsync()
{
var stationsAllTask = new TaskCompletionSource<StationsAvailableResponse>();
stationsAllTask.SetResult(copriModel.Stations);
return await stationsAllTask.Task;
}
/// <summary> Gets a value indicating whether stations are expired or not.</summary>
public bool IsStationsExpired
{
get
{
lock (monkeyLock)
{
return Barrel.Current.IsExpired(requestBuilder.GetStations());
}
}
}
/// <summary> Adds a stations all response to cache.</summary>
/// <param name="stations">Stations to add.</param>
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);
}
}
/// <summary> Adds a stations all response to cache.</summary>
/// <param name="stations">Stations to add.</param>
/// <param name="expiresAfter">Time after which answer is considered to be expired.</param>
private void AddToCache(StationsAvailableResponse stations, TimeSpan expiresAfter)
{
lock (monkeyLock)
{
Barrel.Current.Add(
requestBuilder.GetStations(),
JsonConvertRethrow.SerializeObject(stations),
expiresAfter);
}
}
/// <summary>
/// Updates cache from bike which changed rental state.
/// </summary>
/// <param name="response">Response to update from.</param>
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);
}
}
/// <summary> Updates cache from bike which changed rental state (reservation/ booking canceled). </summary>
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);
}
}
/// <summary> Gets a value indicating whether stations are expired or not.</summary>
public bool IsBikesAvailableExpired
{
get
{
lock (monkeyLock)
{
return Barrel.Current.IsExpired(requestBuilder.GetBikesAvailable());
}
}
}
/// <summary> Adds a bikes response to cache.</summary>
/// <param name="bikes">Bikes to add.</param>
/// <param name="operatorUri">Uri of the operator host to get bikes from or null if bikes have to be gotten form primary host.</param>
/// <param name="stationId"> Id of station which was used for filtering bikes. Null if no filtering was applied.</param>
/// <param name="bikeId"> Id of bike which was used for filtering bikes. Null if no filtering was applied.</param>
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);
}
}
/// <summary> Adds a bikes response to cache.</summary>
/// <param name="bikes">Bikes to add.</param>
/// <param name="expiresAfter">Time after which answer is considered to be expired.</param>
/// <param name="operatorUri">Uri of the operator host to get bikes from or null if bikes have to be gotten form primary host.</param>
/// <param name="stationId"> Id of station which is used for filtering bikes. Null if no filtering should be applied.</param>
private void AddToCache(
BikesAvailableResponse bikes,
TimeSpan expiresAfter)
{
lock (monkeyLock)
{
Barrel.Current.Add(
$"{requestBuilder.GetBikesAvailable()}",
JsonConvertRethrow.SerializeObject(bikes),
expiresAfter);
}
}
/// <summary> Gets a value indicating whether stations are expired or not.</summary>
public bool IsBikesOccupiedExpired
{
get
{
lock (monkeyLock)
{
return Barrel.Current.IsExpired(requestBuilder.GetBikesOccupied());
}
}
}
/// <summary> Adds a bikes response to cache.</summary>
/// <param name="bikes">Bikes to add.</param>
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);
}
}
/// <summary> Adds a bikes response to cache.</summary>
/// <param name="bikes">Bikes to add.</param>
/// <param name="expiresAfter">Time after which answer is considered to be expired.</param>
private void AddToCache(BikesReservedOccupiedResponse bikes, TimeSpan expiresAfter)
{
lock (monkeyLock)
{
Barrel.Current.Add(
requestBuilder.GetBikesOccupied(),
JsonConvertRethrow.SerializeObject(bikes),
expiresAfter);
}
}
}
}

View file

@ -0,0 +1,26 @@
using ShareeBike.Repository.Response;
using ShareeBike.Repository.Response.Stations;
namespace ShareeBike.Repository
{
public static class CopriCallsMonkeyStoreHelper
{
/// <summary> Gets an empty response. </summary>
/// <param name="copriVersion">Version of empty response.</param>
/// <returns>Response.</returns>
public static BikesAvailableResponse GetEmptyBikesAvailableResponse(string copriVersion)
=> JsonConvertRethrow.DeserializeObject<BikesAvailableResponse>(CopriCallsMonkeyStore.BIKESAVAILABLE.Replace("4.1.0.0", copriVersion));
/// <summary> Gets an empty response. </summary>
/// <param name="copriVersion">Version of empty response.</param>
/// <returns>Response.</returns>
public static StationsAvailableResponse GetEmptyStationsAllResponse(string copriVersion)
=> JsonConvertRethrow.DeserializeObject<StationsAvailableResponse>(CopriCallsMonkeyStore.STATIONSALL.Replace("4.1.0.0", copriVersion));
/// <summary> Gets an empty response. </summary>
/// <param name="copriVersion">Version of empty response.</param>
/// <returns>Response.</returns>
public static BikesReservedOccupiedResponse GetEmptyBikesReservedOccupiedResponse(string copriVersion)
=> JsonConvertRethrow.DeserializeObject<BikesReservedOccupiedResponse>(CopriCallsMonkeyStore.BIKESOCCUPIED.Replace("4.1.0.0", copriVersion));
}
}

View file

@ -0,0 +1,67 @@
using System;
using Serilog;
using ShareeBike.Model.Connector;
using ShareeBike.Repository.Response;
namespace ShareeBike.Repository
{
public static class CopriCallsStatic
{
#if !USCSHARP9
private static Version UNSUPPORTEDFUTURECOPRIVERSIONLOWER = new Version(4, 1);
#else
private static Version UNSUPPORTEDFUTURECOPRIVERSIONLOWER = new(4, 1);
#endif
#if !USCSHARP9
private static Version UNSUPPORTEDFUTURECOPRIVERSIONUPPER = new Version(4, 2);
#else
private static Version UNSUPPORTEDFUTURECOPRIVERSIONUPPER = new(4, 2);
#endif
public static Version UnsupportedVersionLower => UNSUPPORTEDFUTURECOPRIVERSIONLOWER;
public static Version UnsupportedVersionUpper => UNSUPPORTEDFUTURECOPRIVERSIONUPPER;
/// <summary> Deserializes response JSON if response is of supported version or provides default response otherwise. </summary>
/// <typeparam name="T">Type of response object.</typeparam>
/// <param name="response">Response JSON.</param>
/// <param name="emptyResponseFactory">Factory providing default delegate.</param>
/// <returns>Response object.</returns>
public static T DeserializeResponse<T>(this string response, Func<string, T> emptyResponseFactory) where T : class
{
// Get COPRI version from response.
var bikeInfoBase = JsonConvertRethrow.DeserializeObject<VersionindependentResponse>(response)?.shareejson;
if (bikeInfoBase.GetCopriVersion() < UNSUPPORTEDFUTURECOPRIVERSIONLOWER
|| bikeInfoBase.GetCopriVersion() >= UNSUPPORTEDFUTURECOPRIVERSIONUPPER)
{
return emptyResponseFactory?.Invoke(bikeInfoBase.copri_version) ?? null;
}
return JsonConvertRethrow.DeserializeObject<ResponseContainer<T>>(response)?.shareejson;
}
/// <summary> Deserializes response JSON if response is of supported version or throws an exception. </summary>
/// <typeparam name="T">Type of response object.</typeparam>
/// <param name="response">Response JSON.</param>
/// <param name="unsupportedVersionExectpion">Exception to fire.</param>
/// <returns>Response object.</returns>
public static T DeserializeResponse<T>(
this string response,
Func<string, System.Exception> unsupportedVersionExectpion = null) where T : class
{
// Get COPRI version from response.
var bikeInfoBase = JsonConvertRethrow.DeserializeObject<VersionindependentResponse>(response)?.shareejson;
if (bikeInfoBase.GetCopriVersion() < UNSUPPORTEDFUTURECOPRIVERSIONLOWER
|| bikeInfoBase.GetCopriVersion() >= UNSUPPORTEDFUTURECOPRIVERSIONUPPER)
{
Log.Error($"Unsupported copri version {bikeInfoBase.copri_version} detected on attempt to log in.");
throw unsupportedVersionExectpion?.Invoke(bikeInfoBase.copri_version) ?? new System.Exception($"Unsupported COPRI version {bikeInfoBase.copri_version} detected.");
}
return JsonConvertRethrow.DeserializeObject<ResponseContainer<T>>(response)?.shareejson;
}
}
}

View file

@ -0,0 +1,591 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Serilog;
using ShareeBike.Model.Connector;
using ShareeBike.Repository.Response;
using ShareeBike.Repository.Response.Stations;
using ShareeBike.Repository.Response.Stations.Station;
namespace ShareeBike.Repository
{
[Flags]
public enum UpdateTarget
{
None = 0,
BikesAvailableResponse = 1,
BikesReservedOccupiedResponse = 2,
StationsAvailableResponse = 4,
All = BikesAvailableResponse | BikesReservedOccupiedResponse | StationsAvailableResponse
}
/// <summary>
/// Manages station and bikes information.
/// Bike is kept in a response- oriented way which is redundant an has to be synchronized for this reason when updated.
/// </summary>
public class CopriResponseModel
{
public CopriResponseModel(
BikesAvailableResponse bikesAll,
BikesReservedOccupiedResponse bikesReservedOccupied,
StationsAvailableResponse stations)
{
BikesAll = bikesAll ?? new BikesAvailableResponse();
BikesReservedOccupied = bikesReservedOccupied ?? new BikesReservedOccupiedResponse();
Stations = stations ?? new StationsAvailableResponse();
}
/// <summary>
/// Holds
/// - part of/ or all available bikes and
/// - part of/ or all reserved bikes of logged in user if user is logged in and
/// - part of/ or all occupied bikes of logged in user if user is logged in.
/// </summary>
/// <remarks>
/// Available bikes: Full information is only contained in <see cref="BikesAll"/>.
/// Available bikes: Count of available bikes per station is kept in <see cref="Stations"/>.
/// Reserved and occupied bikes: Are contained as well in <see cref="BikesReservedOccupied"/>.
/// Reserved and occupied bikes: Are contained as well in <see cref="Stations"/>.
/// </remarks>
public BikesAvailableResponse BikesAll { get; private set; }
/// <summary>
/// Holds all reserved and occupied bikes of logged in user if not used without being logged in.
/// </summary>
/// <remarks>
/// Reserved and occupied bikes: Are contained as well in <see cref="BikesAll"/>.
/// Reserved and occupied bikes: Are contained as well in <see cref="Stations"/>.
/// </remarks>
public BikesReservedOccupiedResponse BikesReservedOccupied { get; private set; }
/// <summary>
/// All bike stations, count of available bikes at each station
/// and all reserved and occupied bikes of logged in user if not used without being logged in.
/// </summary>
/// <remarks>
/// Count of available bikes a each station: Information contained in <see cref="BikesAll"/>.
/// Reserved and occupied bikes: Are contained as well in <see cref="BikesReservedOccupied"/>.
/// </remarks>
public StationsAvailableResponse Stations { get; private set; }
/// <summary>
/// Updates model from response.
/// </summary>
/// <param name="bikesAll">
/// Holds
/// - part of/ or all available bikes and
/// - part of/ or all reserved bikes of logged in user if user is logged in and
/// - part of/ or all occupied bikes of logged in user if user is logged in.
/// </param>
/// <returns>Parts of model which have to be updated.</returns>
public UpdateTarget Update(
BikesAvailableResponse bikesAllResponse,
string stationId = null,
string bikeId = null)
{
try
{
if (!string.IsNullOrEmpty(stationId))
{
return UpdateFromBikesAtStation(
bikesAllResponse,
stationId);
}
if (!string.IsNullOrEmpty(bikeId))
{
return UpdateFromBikesAtStation(
bikesAllResponse,
bikesAllResponse.bikes.Values.FirstOrDefault(x => x.bike.ToUpper() == bikeId.ToUpper())?.station);
}
return UpdateTarget.None;
}
catch (System.Exception ex)
{
Log.ForContext<CopriResponseModel>().Error(
"Updating response model form {@Type} for station id {@StationId} and bike id {BikeId} failed. {@Exception}",
nameof(BikesAvailableResponse),
stationId,
bikeId,
ex);
return UpdateTarget.None;
}
}
/// <summary>
/// Updates cache from bike which changed rental state from available or reserved to booked.
/// </summary>
/// <param name="response"></param>
public UpdateTarget Update(BikeInfoReservedOrBooked response)
{
try
{
var updateTarget = UpdateTarget.None;
if (response == null || string.IsNullOrEmpty(response.bike))
{
// Can nor update station or remove bike
return updateTarget;
}
// Update station if required i.e. decrease count of available bikes if required.
updateTarget = UpdateAvailableBikesCount(
BikesAll.bikes.Values.FirstOrDefault(b => b.bike == response?.bike)?.station,
i => i - 1, // Decrease count of available bikes.
updateTarget);
// A booked bike is no more available.
BikesAll.bikes.Remove(response.bike);
// Update Stations with latest reserved/ rented bike.
Stations.bikes_occupied.Remove(response.bike);
Stations.bikes_occupied.Add(response.bike, response);
// Update BikesAll with latest reserved/ rented bike.
BikesAll.bikes_occupied.Remove(response.bike);
BikesAll.bikes_occupied.Add(response.bike, response);
// Update BikesReservedOccupied with latest reserved/ rented bike.
BikesReservedOccupied.bikes_occupied.Remove(response.bike);
BikesReservedOccupied.bikes_occupied.Add(response.bike, response);
return updateTarget | UpdateTarget.BikesAvailableResponse | UpdateTarget.BikesReservedOccupiedResponse;
} catch (System.Exception ex)
{
Log.ForContext<CopriResponseModel>().Error(
"Updating response model form {@Type} failed. {@Exception}",
nameof(BikeInfoReservedOrBooked),
ex);
return UpdateTarget.None;
}
}
/// <summary> Updates cache from bike which changed rental state (reservation/ booking canceled). </summary>
public UpdateTarget Update(BookingActionResponse response)
{
try
{
if (response == null || string.IsNullOrEmpty(response.bike))
{
// Can nor update station or remove bike
return UpdateTarget.None;
}
// Remove available bike from Stations.
Stations.bikes_occupied.Remove(response.bike);
// Remove available bike from BikesAll.
BikesAll.bikes_occupied.Remove(response.bike);
// Remove available bike from BikesReservedOccupied.
BikesReservedOccupied.bikes_occupied.Remove(response.bike);
return UpdateTarget.StationsAvailableResponse | UpdateTarget.BikesAvailableResponse | UpdateTarget.BikesReservedOccupiedResponse;
}
catch (System.Exception ex)
{
Log.ForContext<CopriResponseModel>().Error(
"Updating response model form {@Type} failed. {@Exception}",
nameof(BookingActionResponse),
ex);
return UpdateTarget.None;
}
}
/// <summary>
/// Updates model from response.
/// </summary>
/// <param name="bikesAll">
/// Holds
/// - all available bikes located at given station and
/// - all reserved bikes of logged in user if user is logged in located at given and
/// - all occupied bikes of logged in user if user is logged in located at given .
/// </param>
/// <returns>Parts of model which have to be updated.</returns>
private UpdateTarget UpdateFromBikesAtStation(
BikesAvailableResponse bikesAllResponse,
string stationId)
{
/// <summary>
/// Synchronizes a dictionary with another, i.e.
/// - adds values which do not yet exist
/// - updates existing values with new ones and
/// - removes bikes which do no more exist
/// </summary>
/// <param name="source">Source dictionary to be read from.</param>
/// <param name="target">Target directory to be written to.</param>
void Synchonize<T>(
ComparableDictionary<T> sourceDict,
ComparableDictionary<T> targetDict,
Action updateRequiredAction) where T : BikeInfoBase, IEquatable<T>
{
if (targetDict == null)
{
// Nothing to do if target dictionary is empty.
return;
}
// Add/ update bikes
foreach (var sourceBike in sourceDict ?? new ComparableDictionary<T>())
{
// Check if bikes already exists.
var targetBike = targetDict.Values.FirstOrDefault(x => x.bike == sourceBike.Value.bike);
if (targetBike != null)
{
// Check if bikes equals
if (sourceBike.Value.Equals(targetBike))
{
// Bikes equal, process next.
continue;
}
// Remove all bikes from target which part of source to be added later.
targetDict.Remove(sourceBike.Key);
}
targetDict.Add(sourceBike.Key, sourceBike.Value);
updateRequiredAction?.Invoke();
}
// Remove bikes from target dictionary which are no more at station
var bikesToRemove = targetDict
.Where(x => x.Value.station == stationId)
.Where(x => sourceDict?.Values?.FirstOrDefault(y => y.station == stationId) == null);
foreach (var bike in bikesToRemove)
{
targetDict.Remove(bike.Key);
updateRequiredAction?.Invoke();
}
}
if (BikesAll == bikesAllResponse
|| bikesAllResponse == null)
{
return UpdateTarget.None;
}
var updateTarget = UpdateTarget.None;
// Update bikes all by updating existing bikes and adding new ones.
Synchonize(
bikesAllResponse.bikes,
BikesAll.bikes,
() => { updateTarget |= UpdateTarget.BikesAvailableResponse; });
Synchonize(
bikesAllResponse.bikes_occupied,
BikesAll.bikes_occupied,
() => { updateTarget |= UpdateTarget.BikesAvailableResponse; });
// Update stations property count of available bikes for selected station.
UpdateAvailableBikesCount(
bikesAllResponse.bikes.Count,
Stations.stations.Values.FirstOrDefault(x => x.station == stationId),
() => updateTarget |= UpdateTarget.StationsAvailableResponse);
// Add/ update stations bikes reserved and stations bikes occupied.
Synchonize(
bikesAllResponse.bikes_occupied,
Stations.bikes_occupied,
() => updateTarget |= UpdateTarget.StationsAvailableResponse);
// Add/ update reserved and occupied bikes.
Synchonize(
bikesAllResponse.bikes_occupied,
BikesReservedOccupied.bikes_occupied,
() => updateTarget |= UpdateTarget.BikesReservedOccupiedResponse);
// Remove available bikes from bikes available response if there are: A bike reserved or occupied can not be available at the same time.
Remove(
bikesAllResponse.bikes_occupied.Values.Select(x => x.bike).ToList(),
BikesAll.bikes,
() => updateTarget |= UpdateTarget.BikesAvailableResponse);
// Remove reserved and occupied bikes from bikes available response if there are: A bike available can not be reserved or occupied at the same time.
Remove(
bikesAllResponse.bikes.Values.Select(x => x.bike).ToList(),
BikesAll.bikes_occupied,
() => updateTarget |= UpdateTarget.BikesAvailableResponse);
// Remove reserved and occupied bikes from stations response if there are: A bike available can not be reserved or occupied at the same time.
Remove(
bikesAllResponse.bikes.Values.Select(x => x.bike).ToList(),
Stations.bikes_occupied,
() => updateTarget |= UpdateTarget.StationsAvailableResponse);
return updateTarget;
}
/// <summary>
/// Updates model from reserved and occupied bikes.
/// </summary>
/// <param name="bikesReservedOccupiedResponse">Bikes to update with.</param>
/// <returns>Parts of model which have to be updated.</returns>
public UpdateTarget Update(BikesReservedOccupiedResponse bikesReservedOccupiedResponse)
{
try
{
if (bikesReservedOccupiedResponse == BikesReservedOccupied
|| bikesReservedOccupiedResponse == null)
{
return UpdateTarget.None;
}
var updateTarget = UpdateTarget.BikesReservedOccupiedResponse;
BikesReservedOccupied = bikesReservedOccupiedResponse; ;
// Update bikes reserved or occupied from bikes reserved or occupied response.
Write(
bikesReservedOccupiedResponse.bikes_occupied,
BikesAll.bikes_occupied,
() => updateTarget |= UpdateTarget.BikesAvailableResponse);
// Update stations from bikes reserved or occupied response.
Write(
bikesReservedOccupiedResponse.bikes_occupied,
Stations.bikes_occupied,
() => updateTarget |= UpdateTarget.StationsAvailableResponse);
// Remove available bikes from bikes available response: A bike reserved/ occupied can not be available at the same time.
Remove(
bikesReservedOccupiedResponse.bikes_occupied.Values.Select(x => x.bike).ToList(),
BikesAll.bikes,
() => updateTarget |= UpdateTarget.BikesAvailableResponse);
// Due to possible change of bikes available from bikes available response (step above), count of bikes from stations might need to be updated.
UpdateAvailableBikesCount(
BikesAll.bikes,
Stations.stations,
() => updateTarget |= UpdateTarget.StationsAvailableResponse);
return updateTarget;
}
catch (System.Exception ex)
{
Log.ForContext<CopriResponseModel>().Error(
"Updating response model form {@Type} failed. {@Exception}",
nameof(BikesReservedOccupiedResponse),
ex);
return UpdateTarget.None;
}
}
/// <summary>
/// Updates model from stations response.
/// </summary>
/// <param name="stationsResponse">Response to update from.</param>
/// <returns></returns>
public UpdateTarget Update(StationsAvailableResponse stationsResponse)
{
try
{
// Remove bike from dictionary if located at a list of stations.
// This can only be done randomly because only the count of bikes without bike ids is known (task #901).
void LimitAvailableBikesCountByStation(
IList<StationInfo> stations,
ComparableBikeDictionary<BikeInfoAvailable> bikes,
Action updateRequiredAction)
{
// Group available bikes by station.
var groupedBikes = bikes.GroupBy(x => x.Value.station).ToList();
foreach (var station in stations)
{
var bikesAtStation = groupedBikes.FirstOrDefault(x => x.Key == station.station);
var toReviseCount = bikesAtStation?.ToList()?.Count ?? 0;
if (string.IsNullOrEmpty(bikesAtStation?.Key)
|| !station.TryGetBikesAvailableCount(out var trueCount)
|| toReviseCount <= trueCount)
{
// Either
// - there is no bikes at current station or
// - invalid format detected
// - count of bikes is ok
continue;
}
var surplus = toReviseCount - trueCount;
// There are move bikes available at current station in bikes available response than the station object reports.
// Randomly remove surplus bikes.
var keys = bikesAtStation.Select(x => x.Key).ToList();
for (var i = 0; i < surplus; i++)
{
bikes.Remove(keys[i]);
}
updateRequiredAction?.Invoke();
}
}
if (Stations == stationsResponse
|| stationsResponse == null)
{
return UpdateTarget.None;
}
var updateTarget = UpdateTarget.StationsAvailableResponse;
Stations = stationsResponse;
// Ensure that bikes which have become occupied are no more in list of available bikes
foreach (var bike in Stations.bikes_occupied.Values)
{
if (!BikesAll.bikes.ContainsKey(bike.bike)) continue;
BikesAll.bikes.Remove(bike.bike);
}
// Ensure that there are not more bikes assigned to one station in bikes available object than there are reported in station response.
// An example for this case is that a bike is brought to garage.
LimitAvailableBikesCountByStation(
Stations.stations.Values.ToList(),
BikesAll.bikes,
() => updateTarget |= UpdateTarget.BikesAvailableResponse);
// Update bikes reserved and bikes occupied from stations response.
Write(
Stations.bikes_occupied,
BikesAll.bikes_occupied,
() => updateTarget |= UpdateTarget.BikesAvailableResponse);
// Update bikes reserved or occupied from stations response.
Write(
Stations.bikes_occupied,
BikesReservedOccupied.bikes_occupied,
() => updateTarget |= UpdateTarget.BikesReservedOccupiedResponse);
return updateTarget;
}
catch (System.Exception ex)
{
Log.ForContext<CopriResponseModel>().Error(
"Updating response model form {@Type} failed. {@Exception}",
nameof(StationsAvailableResponse),
ex);
return UpdateTarget.None;
}
}
/// <summary>
/// Removes bikes from a dictionary.
/// </summary>
/// <typeparam name="T">Type of dictionary values to remove.</typeparam>
/// <param name="bikesToRemove">Bikes to remove.</param>
/// <param name="target">Dictionary to removed bikes from.</param>
/// <param name="updateRequiredAction">Action to be invoked if any bike gets removed from dictionary.</param>
private void Remove<T>(
IList<string> bikesToRemove,
ComparableDictionary<T> target,
Action updateRequiredAction) where T : BikeInfoBase
{
if (target == null)
{
// Nothing to do.
return;
}
foreach (var bike in bikesToRemove)
{
if (!target.Remove(bike))
{
continue;
}
updateRequiredAction?.Invoke();
}
}
/// <summary>
/// Writes a dictionary to another if required.
/// </summary>
/// <param name="source">Source dictionary to be read from.</param>
/// <param name="target">Target directory to be written to.</param>
void Write(
ComparableDictionary<BikeInfoReservedOrBooked> source,
ComparableDictionary<BikeInfoReservedOrBooked> target,
Action updateRequiredAction)
{
if (target == null || source == target)
{
return;
}
target.Clear();
foreach (var bike in source ?? new ComparableBikeDictionary<BikeInfoReservedOrBooked>())
{
target.Add(bike.Key, bike.Value);
}
updateRequiredAction?.Invoke();
}
private void UpdateAvailableBikesCount(
ComparableBikeDictionary<BikeInfoAvailable> bikesAvailable,
ComparableDictionary<StationInfo> stations,
Action updateRequiredAction)
{
foreach (var station in stations.Values)
{
UpdateAvailableBikesCount(
bikesAvailable.Values.Where(x => x.station == station.station).Count(),
station,
updateRequiredAction);
}
}
/// <summary>
/// Updates the count of available bikes of a given station.
/// </summary>
/// <param name="id">Id of the station to update count for.</param>
/// <param name="countDelegate">Delegate to return the new count.</param>
/// <param name="updateRequiredAction">Update action required</param>
private UpdateTarget UpdateAvailableBikesCount(
string id,
Func<int, int> countDelegate,
UpdateTarget currentUpdateTarget)
{
if (string.IsNullOrEmpty(id))
{
// Bike was not available before.
return currentUpdateTarget;
}
var station = Stations.stations.Values.FirstOrDefault(s => s.station == id);
if (station == null)
{
// There is no matching station.
return currentUpdateTarget;
}
// Decrement count of stations.
station.SetBikesAvailableCount(countDelegate(station.GetBikesAvailableCount() ?? 1));
return currentUpdateTarget | UpdateTarget.StationsAvailableResponse;
}
private void UpdateAvailableBikesCount(
int bikesAvailableCount,
StationInfo station,
Action updateRequiredAction)
{
if (station == null
|| !station.TryGetBikesAvailableCount(out var targetBikesCount)
|| targetBikesCount == bikesAvailableCount)
{
// Either
// - there is no matching station or
// - station count if not numeric or
// - count matches
return;
}
station.SetBikesAvailableCount(bikesAvailableCount);
updateRequiredAction?.Invoke();
}
}
}

View file

@ -0,0 +1,62 @@
namespace ShareeBike.Repository.Exception
{
/// <summary>
/// Is fired with request used a cookie which is not defined.
/// Reasons for cookie to be not defined might be
/// - user used more that 8 different devices (copri invalidates cookies in this case)
/// - user account has been deleted?
/// </summary>
public class AuthcookieNotDefinedException : InvalidResponseException<Response.ResponseBase>
{
/// <summary>Constructs a authorization exceptions. </summary>
/// <param name="p_strTextOfAction">Text describing request which is shown if validation fails.</param>
public AuthcookieNotDefinedException(string p_strTextOfAction, Response.ResponseBase response) :
base($"{p_strTextOfAction}\r\nDie Sitzung ist abgelaufen. Bitte neu anmelden.", response)
{
}
/// <summary>
/// Gets whether authcookie is defined or not.
/// </summary>
/// <param name="reponse">Response to check</param>
/// <param name="actionText">Text holding context in which authcookie is checked.</param>
/// <param name="exception">Exception thrown if cookie is not defined.</param>
/// <returns></returns>
public static bool IsAuthcookieNotDefined(
Response.ResponseBase reponse,
string actionText,
out AuthcookieNotDefinedException exception)
{
if (reponse == null || reponse.response_state == null)
{
// Empty response or response without response state is no authcookie not defined exception.
exception = null;
return false;
}
if (!reponse.response_state.ToUpper().Contains(AUTH_FAILURE_QUERY_AUTHCOOKIENOTDEFIED.ToUpper())
&& !reponse.response_state.ToUpper().Contains(AUTH_FAILURE_BOOK_AUTICOOKIENOTDEFIED.ToUpper())
&& !reponse.response_state.ToUpper().Contains(AUTH_FAILURE_BIKESOCCUPIED_AUTICOOKIENOTDEFIED.ToUpper())
&& !reponse.response_state.ToUpper().Contains(AUTH_FAILURE_LOGOUT_AUTHCOOKIENOTDEFIED.ToUpper()))
{
exception = null;
return false;
}
exception = new AuthcookieNotDefinedException(actionText, reponse);
return true;
}
/// <summary> Holds error description if session expired. From COPRI 4.0.0.0 1001 is the only authcookie not defined error. </summary>
private const string AUTH_FAILURE_QUERY_AUTHCOOKIENOTDEFIED = "Failure 1001: authcookie not defined";
/// <summary> Holds error description if session expired (Applies to COPRI < 4.0.0.0) </summary>
private const string AUTH_FAILURE_BOOK_AUTICOOKIENOTDEFIED = "Failure 1002: authcookie not defined";
/// <summary> Holds error description if session expired (Applies to COPRI < 4.0.0.0) </summary>
private const string AUTH_FAILURE_BIKESOCCUPIED_AUTICOOKIENOTDEFIED = "Failure 1003: authcookie not defined";
/// <summary> Holds error description if session expired. (Applies to COPRI < 4.0.0.0)</summary>
private const string AUTH_FAILURE_LOGOUT_AUTHCOOKIENOTDEFIED = "Failure 1004: authcookie not defined";
}
}

View file

@ -0,0 +1,17 @@
using ShareeBike.MultilingualResources;
namespace ShareeBike.Repository.Exception
{
public class InvalidAuthorizationResponseException : InvalidResponseException<Response.ResponseBase>
{
/// <summary>Constructs a authorization exceptions. </summary>
/// <param name="mail">Mail address to create a detailed error message.</param>
public InvalidAuthorizationResponseException(string mail, Response.ResponseBase response) :
base(string.Format(AppResources.ErrorAccountInvalidAuthorization, mail), response)
{
}
/// <summary> Holds error description if user/ password combination is not valid. </summary>
public const string AUTH_FAILURE_STATUS_MESSAGE_UPPERCASE = "FAILURE: CANNOT GENERATE AUTHCOOKIE";
}
}

View file

@ -0,0 +1,42 @@
using System.Text.RegularExpressions;
namespace ShareeBike.Repository.Exception
{
/// <summary> Handles booking request which fail due to too many bikes requested/ booked.</summary>
public class BookingDeclinedException : InvalidResponseException
{
/// <summary> Holds error description if user/ password combination is not valid. </summary>
public const string BOOKING_FAILURE_STATUS_MESSAGE_UPPERCASE = "(OK: BOOKING_REQUEST DECLINED\\. MAX COUNT OF )([0-9]+)( OCCUPIED BIKES HAS BEEN REACHED)";
/// <summary> Prevents invalid use of exception. </summary>
private BookingDeclinedException() : base(typeof(BookingDeclinedException).Name)
{
}
/// <summary> Prevents invalid use of exception. </summary>
public BookingDeclinedException(int maxBikesCount) : base(typeof(BookingDeclinedException).Name)
{
MaxBikesCount = maxBikesCount;
}
public static bool IsBookingDeclined(string responseState, out BookingDeclinedException exception)
{
// Check if there are too many bikes requested/ booked.
var match = Regex.Match(
responseState.ToUpper(),
BOOKING_FAILURE_STATUS_MESSAGE_UPPERCASE);
if (match.Groups.Count != 4
|| !int.TryParse(match.Groups[2].ToString(), out int maxBikesCount))
{
exception = null;
return false;
}
exception = new BookingDeclinedException(maxBikesCount);
return true;
}
/// <summary> Holds the maximum count of bikes allowed to reserve/ book.</summary>
public int MaxBikesCount { get; private set; }
}
}

View file

@ -0,0 +1,6 @@
namespace ShareeBike.Repository.Exception
{
public class CallNotRequiredException : System.Exception
{
}
}

View file

@ -0,0 +1,28 @@
namespace ShareeBike.Repository.Exception
{
public class CommunicationException : System.Exception
{
/// <summary>
/// Constructs a communication exception object.
/// </summary>
public CommunicationException()
{ }
/// <summary>
/// Constructs a communication exeption object.
/// </summary>
/// <param name="p_strMessage">Error message.</param>
public CommunicationException(string p_strMessage) : base(p_strMessage)
{
}
/// <summary>
/// Constructs a communication exeption object.
/// </summary>
/// <param name="p_strMessage">Error message.</param>
/// <param name="p_oException">Inner exceptions.</param>
public CommunicationException(string p_strMessage, System.Exception p_oException) : base(p_strMessage, p_oException)
{
}
}
}

View file

@ -0,0 +1,13 @@
using ShareeBike.MultilingualResources;
namespace ShareeBike.Repository.Exception
{
public class DeserializationException : CommunicationException
{
public DeserializationException(System.Exception ex) : base(
string.Format(AppResources.ErrorDeserializingServerResponse, ex.Message),
ex)
{
}
}
}

View file

@ -0,0 +1,34 @@
namespace ShareeBike.Repository.Exception
{
public class InvalidResponseException<T> : InvalidResponseException
{
/// <summary> Constructs an invalid response Exception. </summary>
/// <param name="actionWhichFailed">Describes the action which failed.</param>
/// <param name="response">Response from copri.</param>
public InvalidResponseException(string actionWhichFailed, T response)
: base(string.Format(
"{0}{1}",
actionWhichFailed,
response == null ? string.Format(" Response of type {0} is null.", typeof(T).Name.ToString()) : string.Empty))
{
Response = response;
}
public T Response { get; private set; }
}
/// <summary>
/// Base exception for all generic invalid response exceptions.
/// </summary>
public class InvalidResponseException : CommunicationException
{
/// <summary> Prevents an invalid instance to be created. </summary>
private InvalidResponseException()
{ }
/// <summary> Constructs a invalid response execption.</summary>
/// <param name="message">Exception.</param>
public InvalidResponseException(string message) : base(message)
{ }
}
}

View file

@ -0,0 +1,27 @@

namespace ShareeBike.Repository.Exception
{
public class NoGPSDataException : InvalidResponseException
{
/// <summary> COPRI response status. </summary>
public const string RETURNBIKE_FAILURE_STATUS_MESSAGE_UPPERCASE = "FAILURE 2245: NO GPS DATA, STATE CHANGE FORBIDDEN.";
/// <summary> Prevents invalid use of exception. </summary>
private NoGPSDataException() : base(typeof(NoGPSDataException).Name)
{
}
public static bool IsNoGPSData(string responseState, out NoGPSDataException exception)
{
// Check if there are too many bikes requested/ booked.
if (!responseState.Trim().ToUpper().StartsWith(RETURNBIKE_FAILURE_STATUS_MESSAGE_UPPERCASE))
{
exception = null;
return false;
}
exception = new NoGPSDataException();
return true;
}
}
}

View file

@ -0,0 +1,52 @@
using System.Text.RegularExpressions;
namespace ShareeBike.Repository.Exception
{
public class NotAtStationException : InvalidResponseException
{
public const string RETURNBIKE_FAILURE_STATUS_MESSAGE_CODE = "FAILURE 2178:";
/// <summary> COPRI response status regular expression to extract detail information. </summary>
public const string RETURNBIKE_FAILURE_STATUS_MESSAGE_UPPERCASE = "(BIKE [A-Za-z0-9_]+ OUT OF GEO FENCING\\. )([0-9]+)( METER DISTANCE TO NEXT STATION )([A-Za-z0-9_]+)";
/// <summary> Prevents invalid use of exception. </summary>
private NotAtStationException() : base(typeof(NotAtStationException).Name)
{
}
public static bool IsNotAtStation(string responseState, out NotAtStationException exception)
{
// Check if there are too many bikes requested/ booked.
var response = responseState.Trim().ToUpper();
if (!response.StartsWith(RETURNBIKE_FAILURE_STATUS_MESSAGE_CODE))
{
exception = null;
return false;
}
var match = Regex.Match(
responseState.ToUpper(),
RETURNBIKE_FAILURE_STATUS_MESSAGE_UPPERCASE);
if (match.Groups.Count != 5
|| !int.TryParse(match.Groups[2].ToString(), out int meters))
{
exception = new NotAtStationException();
return true;
}
exception = new NotAtStationException {
Distance = meters,
StationNr = match.Groups[4].ToString()
};
return true;
}
/// <summary> Holds the distance to next station.</summary>
public int? Distance { get; private set; } = null;
/// <summary> Holds the id of next station.</summary>
public string StationNr { get; private set; } = string.Empty;
}
}

View file

@ -0,0 +1,15 @@
using ShareeBike.Repository.Response;
namespace ShareeBike.Repository.Exception
{
public class ResponseException : System.Exception
{
private readonly ResponseBase _response;
public ResponseException(ResponseBase response, string message) : base(message)
{
_response = response;
}
public string Response => _response.response_text;
}
}

View file

@ -0,0 +1,10 @@
using ShareeBike.Repository.Response;
namespace ShareeBike.Repository.Exception
{
public class ReturnBikeException : ResponseException
{
public ReturnBikeException(BikesReservedOccupiedResponse response, string message) : base(response, message)
{ }
}
}

View file

@ -0,0 +1,8 @@
namespace ShareeBike.Repository.Exception
{
public class UnsupportedCopriVersionDetectedException : System.Exception
{
public UnsupportedCopriVersionDetectedException() : base("Unsupported app version detected.")
{ }
}
}

View file

@ -0,0 +1,16 @@
using ShareeBike.MultilingualResources;
namespace ShareeBike.Repository.Exception
{
public class WebConnectFailureException : CommunicationException
{
/// <summary>
/// Constructs a communication exception object.
/// </summary>
/// <param name="message"></param>
/// <param name="exception"></param>
public WebConnectFailureException(string message, System.Exception exception) : base(message, exception)
{
}
}
}

View file

@ -0,0 +1,53 @@
using System.Net;
namespace ShareeBike.Repository.Exception
{
public static class WebExceptionHelper
{
/// <summary> Gets if a exception is caused by an error connecting to copri (LAN or mobile data off/ not reachable, proxy, ...).</summary>
/// <param name="exception">Expection to check.</param>
/// <returns>True if exception if caused by an connection error. </returns>
public static bool GetIsConnectFailureException(this System.Exception exception)
{
#if !USCSHARP9
if (!(exception is WebException webException))
#else
if (exception is not WebException webException)
#endif
{
return false;
}
return webException.Status == WebExceptionStatus.ConnectFailure // Happens if WLAN and mobile data is off/ Router denies internet access/ ...
|| webException.Status == WebExceptionStatus.NameResolutionFailure // Happens sometimes when not WLAN and no mobil connection are available (bad connection in lift).
|| webException.Status == WebExceptionStatus.ReceiveFailure; // Happened when mobile was connected to WLAN
}
/// <summary> Gets if a exception is caused by clicking too fast.</summary>
/// <param name="exception">Expection to check.</param>
/// <returns>True if exception if caused by a fast click sequence. </returns>
public static bool GetIsForbiddenException(this System.Exception exception)
{
#if !USCSHARP9
if (!(exception is WebException webException))
#else
if (exception is not WebException webException)
#endif
{
return false;
}
#if !USCSHARP9
if (!(webException?.Response is HttpWebResponse response))
#else
if (webException?.Response is not HttpWebResponse response)
#endif
{
return false;
}
return webException.Status == WebExceptionStatus.ProtocolError
&& response.StatusCode == HttpStatusCode.Forbidden;
}
}
}

View file

@ -0,0 +1,14 @@
namespace ShareeBike.Repository.Exception
{
public class WebForbiddenException : CommunicationException
{
/// <summary>
/// Constructs a communication exeption object.
/// </summary>
/// <param name="p_strMessage"></param>
/// <param name="p_oException"></param>
public WebForbiddenException(string p_strMessage, System.Exception p_oException) : base($"{p_strMessage}\r\nSchnell getippt?\r\nBitte die App etwas langsamer bedienen...", p_oException)
{
}
}
}

View file

@ -0,0 +1,172 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
using ShareeBike.Model.Connector;
using ShareeBike.Repository.Request;
using ShareeBike.Repository.Response;
using ShareeBike.Repository.Response.Stations;
namespace ShareeBike.Repository
{
/// <summary> Interface to communicate with copri server.</summary>
public interface ICopriServerBase
{
/// <summary> Logs user in. </summary>
/// <param name="mailAddress">Mail address of user to log in.</param>
/// <param name="password">Password to log in.</param>
/// <param name="deviceId">Id specifying user and hardware.</param>
/// <remarks>Response which holds auth cookie <see cref="ResponseBase.authcookie"/></remarks>
Task<AuthorizationResponse> DoAuthorizationAsync(
string mailAddress,
string password,
string deviceId);
/// <summary> Logs user out. </summary>
/// <remarks>Response which holds auth cookie <see cref="ResponseBase.authcookie"/></remarks>
Task<AuthorizationoutResponse> DoAuthoutAsync();
/// <summary> Reserves bike. </summary>
/// <param name="bikeId">Id of the bike to reserve.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Response on reserving request.</returns>
Task<ReservationBookingResponse> DoReserveAsync(
string bikeId,
Uri operatorUri);
/// <summary> Cancels reservation of bike. </summary>
/// <param name="bikeId">Id of the bike to reserve.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Response on cancel reservation request.</returns>
Task<BookingActionResponse> DoCancelReservationAsync(
string bikeId,
Uri operatorUri);
/// <summary> Get authentication keys. </summary>
/// <param name="bikeId">Id of the bike to get keys for.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Response holding authentication keys.</returns>
Task<ReservationBookingResponse> CalculateAuthKeysAsync(
string bikeId,
Uri operatorUri);
/// <summary> Notifies COPRI about start of returning sequence. </summary>
/// <remarks> Operator specific call.</remarks>
/// <param name="bikeId">Id of the bike to return.</param>+
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Response on notification about start of returning sequence.</returns>
Task<ResponseBase> StartReturningBike(
string bikeId,
Uri operatorUri);
/// <summary> Updates COPRI lock state for a booked bike. </summary>
/// <param name="bikeId">Id of the bike to update locking state for.</param>
/// <param name="location">Geolocation of lock.</param>
/// <param name="state">New locking state.</param>
/// <param name="batteryPercentage">Holds the filling level percentage of the battery.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Response on updating locking state.</returns>
Task<ReservationBookingResponse> UpdateLockingStateAsync(
string bikeId,
lock_state state,
Uri operatorUri,
LocationDto location = null,
double batteryPercentage = double.NaN,
IVersionInfo versionInfo = null);
/// <summary> Books a bluetooth bike. </summary>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <param name="bikeId">Id of the bike to book.</param>
/// <param name="guid">Used to publish GUID from app to copri. Used for initial setup of bike in copri.</param>
/// <param name="batteryPercentage">Holds the filling level percentage of the battery.</param>
/// <param name="nextAction">If not null next locking action which is performed after booking.</param>
/// <returns>Response on booking request.</returns>
Task<ReservationBookingResponse> DoBookAsync(
Uri operatorUri,
string bikeId,
Guid guid,
double batteryPercentage,
LockingAction? nextAction = null);
/// <summary> Books a bike and starts opening bike. </summary>
/// <param name="bikeId">Id of the bike to book.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Response on booking request.</returns>
Task<ReservationBookingResponse> BookAvailableAndStartOpeningAsync(
string bikeId,
Uri operatorUri);
/// <summary> Books a bike and starts opening bike. </summary>
/// <param name="bikeId">Id of the bike to book.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Response on booking request.</returns>
Task<ReservationBookingResponse> BookReservedAndStartOpeningAsync(
string bikeId,
Uri operatorUri);
/// <summary> Returns a bike. </summary>
/// <param name="bikeId">Id of the bike to return.</param>
/// <param name="location">Geolocation of lock.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Response on returning request.</returns>
Task<DoReturnResponse> DoReturn(
string bikeId,
LocationDto location,
Uri operatorUri);
/// <summary> Returns a bike and starts closing. </summary>
/// <param name="bikeId">Id of the bike to return.</param>
/// <param name="smartDevice">Provides info about hard and software.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Response on returning request.</returns>
Task<DoReturnResponse> ReturnAndStartClosingAsync(
string bikeId,
Uri operatorUri);
/// <summary>
/// Submits feedback to copri server.
/// </summary>
/// <param name="bikeId">Id of the bike to submit feedback for.</param>
/// <param name="currentChargeBars">Null if bike has no engine or charge is unknown. Otherwise the charge filling level of the drive battery.</param>
/// <param name="isBikeBroken">True if bike is broken.</param>
/// <param name="message">General purpose message or error description.</param>
Task<SubmitFeedbackResponse> DoSubmitFeedback(
string bikeId,
int? currentChargeBars,
string message,
bool isBikeBroken,
Uri operatorUri);
/// <summary> Submits mini survey to copri server. </summary>
/// <param name="answers">Collection of answers.</param>
Task<ResponseBase> DoSubmitMiniSurvey(IDictionary<string, string> answers);
/// <summary> True if connector has access to copri server, false if cached values are used. </summary>
bool IsConnected { get; }
/// <summary> Gets the session cookie if user is logged in, an empty string otherwise. </summary>
string SessionCookie { get; }
/// <summary> Holds the id of the merchant. </summary>
string MerchantId { get; }
}
/// <summary> Interface to communicate with copri server.</summary>
public interface ICopriServer : ICopriServerBase
{
/// <summary> Get list of stations. </summary>
/// <returns>List of all stations.</returns>
Task<StationsAvailableResponse> GetStationsAsync();
/// <summary> Gets a list of bikes from Copri. </summary>
/// <param name="operatorUri">Uri of the operator host to get bikes from or null if bikes have to be gotten form primary host.</param>
/// <param name="stationId"> Id of station which is used for filtering bikes. Null if no filtering should be applied.</param>
/// <param name="bikeId"> Id of bike which is used for filtering bikes. Null if no filtering should be applied.</param>
/// <returns>Response holding list of bikes.</returns>
Task<BikesAvailableResponse> GetBikesAvailableAsync(Uri operatorUri = null, string stationId = null, string bikeId = null);
/// <summary> Gets a list of bikes reserved/ booked by active user from Copri.</summary>
/// <returns>Response holding list of bikes.</returns>
Task<BikesReservedOccupiedResponse> GetBikesOccupiedAsync();
}
}

View file

@ -0,0 +1,177 @@
using System;
using System.Collections.Generic;
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
using ShareeBike.Model.Connector;
namespace ShareeBike.Repository.Request
{
/// <summary> Defines members to create requests.</summary>
public interface IRequestBuilder
{
/// <summary> Holds the id denoting the merchant (ShareeBike app). </summary>
string MerchantId { get; }
/// <summary> Gets the session cookie if user is logged in, an empty string otherwise. </summary>
string SessionCookie { get; }
/// <summary> Gets request to log user in. </summary>
/// <param name="mailAddress">Mail address of user to log in.</param>
/// <param name="password">Password to log in.</param>
/// <param name="deviceId">Id specifying user and hardware.</param>
/// <remarks>Request which holds auth cookie <see cref="RequstBase.authcookie"/></remarks>
string DoAuthorization(
string mailAddress,
string password,
string deviceId);
/// <summary> Logs user out. </summary>
/// <param name="merchantId">Id of the merchant.</param>
/// <param name="p_strSessionCookie"> Cookie which identifies user.</param>
string DoAuthout();
/// <summary> Get list of stations from file. </summary>
/// <returns>Request to query list of station.</returns>
string GetStations();
/// <summary>Gets bikes available.</summary>
/// <param name="stationId"> Id of station which is used for filtering bikes. Null if no filtering should be applied.</param>
/// <param name="bikeId"> Id of bike to get.</param>
/// <returns>Request to query list of bikes available.</returns>
string GetBikesAvailable(string stationId = null, string bikeId = null);
/// <summary> Gets a list of bikes reserved/ booked by active user from Copri.</summary>
/// <returns>Request to query list of bikes occupied.</returns>
string GetBikesOccupied();
/// <summary> Gets reservation request (synonym: reservation == request == reservieren). </summary>
/// <param name="bikeId">Id of the bike to reserve.</param>
/// <returns>Request to reserve bike.</returns>
string DoReserve(string bikeId);
/// <summary> Gets request to cancel reservation. </summary>
/// <param name="bikeId">Id of the bike to cancel reservation for.</param>
/// <returns>Request on cancel booking request.</returns>
string DoCancelReservation(string bikeId);
/// <summary> Request to get keys. </summary>
/// <param name="bikeId">Id of the bike to get keys for.</param>
/// <returns>Request to get keys.</returns>
string CalculateAuthParameters(string bikeId);
/// <summary> Gets the request for notifying about start of returning sequence. </summary>
/// <remarks> Operator specific call.</remarks>
/// <param name="bikeId">Id of the bike to return.</param>
/// <returns>Request to notify about start of returning sequence.</returns>
string StartReturningBike(string bikeId);
/// <summary> Gets the request for updating lock state for a booked bike. </summary>
/// <param name="bikeId">Id of the bike to update locking state for.</param>
/// <param name="state">New locking state.</param>
/// <param name="location">Geolocation of lock when state change occurred.</param>
/// <param name="versionInfo">Information about lock (firmware version, hardware version, ...).</param>
/// <returns>Request to update locking state.</returns>
string UpdateLockingState(
string bikeId,
lock_state state,
LocationDto location = null,
double batteryPercentage = double.NaN,
IVersionInfo versionInfo = null);
/// <summary> Gets the booking request (synonym: booking == renting == mieten). </summary>
/// <param name="bikeId">Id of the bike to book.</param>
/// <param name="guid">Used to publish GUID from app to copri. Used for initial setup of bike in copri.</param>
/// <param name="batteryPercentage">Holds the filling level percentage of the battery.</param>
/// <param name="nextAction">If not null next locking action which is performed after booking.</param>
/// <returns>Request to booking bike.</returns>
string DoBook(string bikeId, Guid guid, double batteryPercentage, LockingAction? nextAction = null);
/// <summary> Gets the request to book and start opening the bike (synonym: booking == renting == mieten). </summary>
/// <param name="bikeId">Id of the bike to book.</param>
/// <returns>Request to booking bike.</returns>
string BookAvailableAndStartOpening(string bikeId);
/// <summary> Gets the request to book and start opening the bike (synonym: booking == renting == mieten). </summary>
/// <param name="bikeId">Id of the bike to book.</param>
/// <returns>Request to booking bike.</returns>
string BookReservedAndStartOpening(string bikeId);
/// <summary> Gets request for returning the bike. </summary>
/// <param name="bikeId">Id of the bike to return.</param>
/// <param name="location">Geolocation of lock when returning bike.</param>
/// <returns>Request on returning request.</returns>
string DoReturn(string bikeId, LocationDto location);
/// <summary> Returns a bike and starts closing. </summary>
/// <param name="bikeId">Id of the bike to return.</param>
/// <param name="smartDevice">Provides info about hard and software.</param>
/// <returns>Response to send to copri.</returns>
string ReturnAndStartClosing(string bikeId);
/// <summary>
/// Gets request for submitting feedback to copri server.
/// </summary>
/// <param name="bikeId">Id of the bike to which the feedback is related to.</param>
/// <param name="currentChargeBars">Null if bike has no engine or charge is unknown. Otherwise the charge filling level of the drive battery.</param>
/// <param name="message">General purpose message or error description.</param>
/// <param name="isBikeBroken">True if bike is broken.</param>
string DoSubmitFeedback(
string bikeId,
int? currentChargeBars,
string message = null,
bool isBikeBroken = false);
/// <summary>
/// Gets request for submitting mini survey to copri server.
/// </summary>
/// <param name="answers">Collection of answers.</param>
string DoSubmitMiniSurvey(IDictionary<string, string> answers);
}
/// <summary> Copri locking states</summary>
public enum lock_state
{
/// <summary> Request to backend to close lock in context of pausing ride.</summary>
locking,
/// <summary> Lock is closed.</summary>
locked,
/// <summary> Request to backend to close lock either in context of resuming ride or starting a rental.</summary>
unlocking,
/// <summary> Lock is open.</summary>
unlocked,
/// <summary> Lock is unknown state.</summary>
unspecific,
}
/// <summary> Holds location info.</summary>
public class LocationDto
{
public double Latitude { get; private set; }
public double Longitude { get; private set; }
/// <summary> Accuracy of location in meters.</summary>
public double? Accuracy { get; private set; }
public TimeSpan Age { get; private set; }
public class Builder
{
public double Latitude { get; set; }
public double Longitude { get; set; }
public double? Accuracy { get; set; }
public TimeSpan Age { get; set; }
public LocationDto Build()
{
return new LocationDto { Latitude = Latitude, Longitude = Longitude, Accuracy = Accuracy, Age = Age };
}
}
}
}

View file

@ -0,0 +1,28 @@

namespace ShareeBike.Repository.Request
{
public static class QueryBuilderHelper
{
/// <summary>
/// Gets the htlm query string element for session id.
/// </summary>
/// <returns>Query string.</returns>
public static string GetSessionIdQueryElement(
string op,
string merchantId,
string sessionCookie = null)
=> !string.IsNullOrEmpty(merchantId) || !string.IsNullOrEmpty(sessionCookie)
? $"{op}sessionid={(!string.IsNullOrEmpty(sessionCookie) ? sessionCookie : string.Empty)}{(!string.IsNullOrEmpty(merchantId) ? merchantId : string.Empty)}"
: string.Empty;
/// <summary>
/// Gets the htlm query string element for the language.
/// </summary>
/// <param name="uiIsoLangugageName">Two letter ISO language name.</param>
/// <returns>Query string.</returns>
public static string GetLanguageQueryElement(string op, string uiIsoLangugageName)
=> !string.IsNullOrEmpty(uiIsoLangugageName)
? $"{op}lang={uiIsoLangugageName}"
: string.Empty;
}
}

View file

@ -0,0 +1,165 @@
using System;
using System.Collections.Generic;
using System.Net;
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Repository.Exception;
namespace ShareeBike.Repository.Request
{
/// <summary> Creates requests if no user is logged in.</summary>
public class RequestBuilder : IRequestBuilder
{
/// <summary> Constructs a object for building requests. </summary>
/// <param name="merchantId">Holds the id denoting the merchant.</param>
/// <param name="uiIsoLangugageName">Two letter ISO language name.</param>
/// <param name="smartDevice">Holds info about smart device.</param>
public RequestBuilder(
string merchantId,
string uiIsoLangugageName,
ISmartDevice smartDevice = null)
{
MerchantId = !string.IsNullOrEmpty(merchantId)
? merchantId
: throw new ArgumentException("Merchant id must not be null.", nameof(merchantId));
UiIsoLanguageNameParameter = RequestBuilderHelper.GetLanguageParameter(WebUtility.UrlEncode(uiIsoLangugageName));
AuthCookieParameter = $"&authcookie={WebUtility.UrlEncode(MerchantId)}";
SmartDevice = smartDevice;
}
/// <summary>Holds the id denoting the merchant.</summary>
public string MerchantId { get; }
/// <summary> Holds the session cookie if a user is logged in. </summary>
public string SessionCookie => string.Empty;
/// <summary> Holds the current ui two letter ISO language name. </summary>
private string UiIsoLanguageNameParameter { get; }
/// <summary> Auth cookie parameter. </summary>
private string AuthCookieParameter { get; }
/// <summary>Holds info about smart device.</summary>
private ISmartDevice SmartDevice { get; }
/// <summary> Gets request to log user in. </summary>
/// <param name="mailAddress">Mailaddress of user to log in.</param>
/// <param name="password">Password to log in.</param>
/// <param name="deviceId">Id specifying user and hardware.</param>
/// <remarks>Response which holds auth cookie <see cref="ResponseBase.authcookie"/></remarks>
public string DoAuthorization(
string mailAddress,
string password,
string deviceId)
=> "request=authorization" +
$"&merchant_id={MerchantId}" +
$"&user_id={WebUtility.UrlEncode(mailAddress)}" +
$"&user_pw={WebUtility.UrlEncode(password)}" +
$"&hw_id={WebUtility.UrlEncode(deviceId)}" +
UiIsoLanguageNameParameter;
/// <summary> Logs user out. </summary>
public string DoAuthout()
=> throw new CallNotRequiredException();
/// <summary>Gets bikes available.</summary>
/// <param name="stationId"> Id of station which is used for filtering bikes. Null if no filtering should be applied.</param>
/// <param name="bikeId"> Id of bike to get.</param>
/// <returns>Request to query list of bikes available.</returns>
public string GetBikesAvailable(string stationId = null, string bikeId = null)
=> "request=bikes_available&system=all" +
stationId.GetStationId() +
bikeId.GetBikeId() +
AuthCookieParameter +
UiIsoLanguageNameParameter;
/// <summary> Get list of stations from file. </summary>
/// <returns>Request to query list of station.</returns>
public string GetStations()
=> "request=stations_available" +
AuthCookieParameter +
SmartDevice.GetSmartDeviceParameters() +
UiIsoLanguageNameParameter;
/// <summary> Gets a list of bikes reserved/ booked by active user from Copri.</summary>
/// <returns>Request to query list of bikes occupied.</returns>
public string GetBikesOccupied()
=> throw new NotSupportedException();
/// <summary> Gets booking request response. </summary>
/// <param name="bikeId">Id of the bike to book.</param>
/// <returns>Response on booking request.</returns>
public string DoReserve(string bikeId)
=> throw new NotSupportedException();
/// <summary> Gets cancel booking request response. </summary>
/// <param name="p_iBikeId">Id of the bike to book.</param>
/// <returns>Response on cancel booking request.</returns>
public string DoCancelReservation(string p_iBikeId)
=> throw new NotSupportedException();
/// <summary> Request to calculate authentication keys. </summary>
/// <param name="bikeId">Id of the bike to get keys for.</param>
/// <returns>Response on request.</returns>
public string CalculateAuthParameters(string bikeId)
=> throw new NotSupportedException();
/// <summary> Gets the request for notifying about start of returning sequence. </summary>
/// <remarks> Operator specific call.</remarks>
/// <param name="bikeId">Id of the bike to return.</param>
/// <returns>Request to notify about start of returning sequence.</returns>
public string StartReturningBike(string bikeId)
=> throw new NotSupportedException();
/// <summary>
/// Not supported if user is not logged in. Lock state is only updated after open/ close which is only possible if user is logged in.
/// </summary>
/// <exception cref="NotSupportedException"></exception>
public string UpdateLockingState(string bikeId, lock_state state, LocationDto geolocation, double batteryPercentage, IVersionInfo versionInfo)
=> throw new NotSupportedException();
public string DoBook(string bikeId, Guid guid, double batteryPercentage, LockingAction? nextAction = null)
=> throw new NotSupportedException();
/// <summary> Gets the request to book and start opening the bike (synonym: booking == renting == mieten). </summary>
/// <param name="bikeId">Id of the bike to book.</param>
/// <returns>Request to booking bike.</returns>
public string BookAvailableAndStartOpening(string bikeId)
=> throw new NotSupportedException();
/// <summary> Gets the request to book and start opening the bike (synonym: booking == renting == mieten). </summary>
/// <param name="bikeId">Id of the bike to book.</param>
/// <returns>Request to booking bike.</returns>
public string BookReservedAndStartOpening(string bikeId)
=> throw new NotSupportedException();
public string DoReturn(string bikeId, LocationDto geolocation)
=> throw new NotSupportedException();
/// <summary> Returns a bike and starts closing. </summary>
/// <param name="bikeId">Id of the bike to return.</param>
/// <param name="smartDevice">Provides info about hard and software.</param>
/// <returns>Response to send to copri.</returns>
public string ReturnAndStartClosing(string bikeId)
=> throw new NotSupportedException();
/// <summary> Gets submit feedback request. </summary>
/// <param name="bikeId">Id of the bike to which the feedback is related to.</param>
/// <param name="message">General purpose message or error description.</param>
/// <param name="isBikeBroken">True if bike is broken.</param>
/// <returns>Submit feedback request.</returns>
public string DoSubmitFeedback(string bikeId, int? currentChargeBars, string message = null, bool isBikeBroken = false)
=> throw new NotSupportedException();
/// <summary>
/// Gets request for submitting mini survey to copri server.
/// </summary>
/// <param name="answers">Collection of answers.</param>
public string DoSubmitMiniSurvey(IDictionary<string, string> answers) =>
throw new NotSupportedException();
}
}

View file

@ -0,0 +1,83 @@
using System.Net;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
namespace ShareeBike.Repository.Request
{
public static class RequestBuilderHelper
{
/// <summary>
/// Gets the REST language parameter.
/// </summary>
/// <param name="uiIsoLangugageName">Two letter ISO language name.</param>
/// <returns></returns>
public static string GetLanguageParameter(string uiIsoLangugageName)
=> !string.IsNullOrEmpty(uiIsoLangugageName)
? $"&lang={uiIsoLangugageName}"
: string.Empty;
public static lock_state? GetLockState(this LockingAction? action)
{
switch (action)
{
case LockingAction.Open:
return lock_state.unlocking;
case LockingAction.Close:
return lock_state.locking;
default:
return null;
}
}
/// <summary> Gets the smart device parameters. </summary>
/// <returns>in a format which is url encode invariant.</returns>
public static string GetSmartDeviceParameters(this ISmartDevice smartDevice)
=> smartDevice != null
? $"{(!string.IsNullOrEmpty(smartDevice.Manufacturer) ? $"&user_device_manufacturer={WebUtility.UrlEncode(smartDevice.Manufacturer)}" : string.Empty)}" +
$"{(!string.IsNullOrEmpty(smartDevice.Model) ? $"&user_device_model={WebUtility.UrlEncode(smartDevice.Model)}" : string.Empty)}" +
$"{(!string.IsNullOrEmpty(smartDevice.Platform.ToString()) ? $"&user_device_platform={WebUtility.UrlEncode(smartDevice.Platform.ToString())}" : string.Empty)}" +
$"{(!string.IsNullOrEmpty(smartDevice.VersionText) ? $"&user_device_version={WebUtility.UrlEncode(smartDevice.VersionText)}" : string.Empty)}" +
$"{(!string.IsNullOrEmpty(smartDevice.Identifier) ? $"&user_device_id={WebUtility.UrlEncode(smartDevice.Identifier)}" : string.Empty)}"
: string.Empty;
/// <summary>
/// Converts from one locking state enum to another.
/// </summary>
/// <param name="state">Locking state to convert.</param>
/// <returns>Target state.</returns>
public static lock_state? GetLockState(this Model.Bikes.BikeInfoNS.BluetoothLock.LockingState state)
{
switch (state)
{
case Model.Bikes.BikeInfoNS.BluetoothLock.LockingState.Open:
return lock_state.unlocked;
case Model.Bikes.BikeInfoNS.BluetoothLock.LockingState.Closed:
return lock_state.locked;
case Model.Bikes.BikeInfoNS.BluetoothLock.LockingState.UnknownFromHardwareError:
return lock_state.unspecific;
default:
return null;
}
}
/// <summary> Gets the station id filter. </summary>
/// <returns>Station id filter.</returns>
public static string GetStationId(this string stationId)
=> !string.IsNullOrEmpty(stationId)
? $"&station={WebUtility.UrlEncode(stationId)}"
: string.Empty;
/// <summary> Gets the bike id filter. </summary>
/// <returns>Bike id filter.</returns>
public static string GetBikeId(this string bikeId)
=> !string.IsNullOrEmpty(bikeId)
? $"&bike={WebUtility.UrlEncode(bikeId)}"
: string.Empty;
}
}

View file

@ -0,0 +1,344 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Repository.Exception;
using ShareeBike.Services.Logging;
namespace ShareeBike.Repository.Request
{
/// <summary> Creates requests if a user is logged in.</summary>
public class RequestBuilderLoggedIn : IRequestBuilder
{
/// <summary> Constructs a object for building requests. </summary>
/// <param name="merchantId">Holds the id denoting the merchant.</param>
/// <param name="uiIsoLangugageName">Two letter ISO language name.</param>
/// <param name="smartDevice">Holds info about smart device.</param>
public RequestBuilderLoggedIn(
string merchantId,
string uiIsoLangugageName,
string sessionCookie,
ISmartDevice smartDevice = null)
{
MerchantId = !string.IsNullOrEmpty(merchantId)
? merchantId
: throw new ArgumentException("Merchant id must not be null.", nameof(merchantId));
SessionCookie = !string.IsNullOrEmpty(sessionCookie)
? sessionCookie
: throw new ArgumentException("Session cookie must not be null.", nameof(sessionCookie));
UiIsoLanguageNameParameter = RequestBuilderHelper.GetLanguageParameter(WebUtility.UrlEncode(uiIsoLangugageName));
AuthCookieParameter = $"&authcookie={WebUtility.UrlEncode(SessionCookie)}{WebUtility.UrlEncode(MerchantId)}";
SmartDevice = smartDevice;
}
/// <summary> Holds the id denoting the merchant. </summary>
public string MerchantId { get; }
/// <summary> Holds the session cookie if a user is logged in. </summary>
public string SessionCookie { get; }
/// <summary> Holds the current ui two letter ISO language name. </summary>
public string UiIsoLanguageNameParameter { get; }
/// <summary> Auth cookie parameter. </summary>
private string AuthCookieParameter { get; }
/// <summary>Holds info about smart device.</summary>
private ISmartDevice SmartDevice { get; }
/// <summary> Gets request to log user in. </summary>
/// <param name="mailAddress">Mail address of user to log in.</param>
/// <param name="password">Password to log in.</param>
/// <param name="deviceId">Id specifying user and hardware.</param>
/// <remarks>Response which holds auth cookie <see cref="ResponseBase.authcookie"/></remarks>
public string DoAuthorization(
string mailAddress,
string password,
string deviceId)
=> throw new CallNotRequiredException();
/// <summary> Logs user out. </summary>
public string DoAuthout()
=> "request=authout" +
AuthCookieParameter +
UiIsoLanguageNameParameter;
/// <summary>Gets bikes available.</summary>
/// <param name="stationId"> Id of station which is used for filtering bikes. Null if no filtering should be applied.</param>
/// <param name="bikeId"> Id of bike to get.</param>
/// <returns>Request to query list of bikes available.</returns>
public string GetBikesAvailable(string stationId = null, string bikeId = null)
=> "request=bikes_available&system=all" +
stationId.GetStationId() +
bikeId.GetBikeId() +
AuthCookieParameter +
UiIsoLanguageNameParameter;
/// <summary> Gets a list of bikes reserved/ booked by active user from Copri.</summary>
/// <returns>Request to query list of bikes occupied.</returns>
public string GetBikesOccupied()
=> "request=user_bikes_occupied&system=all&genkey=1" +
AuthCookieParameter +
UiIsoLanguageNameParameter;
/// <summary> Get list of stations from file. </summary>
/// <returns>Request to query list of station.</returns>
public string GetStations()
=> "request=stations_available" +
AuthCookieParameter +
SmartDevice.GetSmartDeviceParameters() +
UiIsoLanguageNameParameter;
/// <summary> Gets reservation request (synonym: reservation == request == reservieren). </summary>
/// <remarks> Operator specific call.</remarks>
/// <param name="bikeId">Id of the bike to reserve.</param>
/// <returns>Request to reserve bike.</returns>
public string DoReserve(string bikeId)
=> "request=booking_request" +
GetBikeIdParameter(bikeId) +
AuthCookieParameter +
SmartDevice.GetSmartDeviceParameters() +
UiIsoLanguageNameParameter;
/// <summary> Gets request to cancel reservation. </summary>
/// <remarks> Operator specific call.</remarks>
/// <param name="bikeId">Id of the bike to cancel reservation for.</param>
/// <returns>Request on cancel booking request.</returns>
public string DoCancelReservation(string bikeId)
=> "request=booking_cancel" +
GetBikeIdParameter(bikeId) +
AuthCookieParameter +
UiIsoLanguageNameParameter;
/// <summary> Request to get keys. </summary>
/// <remarks> Operator specific call.</remarks>
/// <param name="bikeId">Id of the bike to get keys for.</param>
/// <returns>Request to get keys.</returns>
public string CalculateAuthParameters(string bikeId)
=> "request=booking_update" +
GetBikeIdParameter(bikeId) +
AuthCookieParameter +
"&genkey=1" +
UiIsoLanguageNameParameter;
/// <summary> Gets the request for notifying about start of returning sequence. </summary>
/// <remarks> Operator specific call.</remarks>
/// <param name="bikeId">Id of the bike to return.</param>
/// <returns>Request to notify about start of returning sequence.</returns>
public string StartReturningBike(string bikeId)
=> "request=booking_update" +
GetBikeIdParameter(bikeId) +
GetLockStateParameter(lock_state.locking) +
AuthCookieParameter +
UiIsoLanguageNameParameter;
/// <summary> Gets the request for updating lock state for a booked bike. </summary>
/// <remarks> Operator specific call.</remarks>
/// <param name="bikeId">Id of the bike to update locking state for.</param>
/// <param name="state">New locking state.</param>
/// <param name="versionInfo">Information about lock (firmware version, hardware version, ...).</param>
/// <returns>Request to update locking state.</returns>
public string UpdateLockingState(
string bikeId,
lock_state state,
LocationDto geolocation,
double batteryPercentage,
IVersionInfo versionInfo)
=> "request=booking_update" +
GetBikeIdParameter(bikeId) +
GetLocationParameters(geolocation) +
GetLockStateParameter(state) +
GetBatteryPercentageParameters(batteryPercentage) +
GetVersionInfoParameter(versionInfo) +
AuthCookieParameter +
UiIsoLanguageNameParameter;
/// <summary> Gets booking request (synonym: booking == renting == mieten). </summary>
/// <remarks> Operator specific call.</remarks>
/// <param name="bikeId">Id of the bike to book.</param>
/// <param name="guid">Used to publish GUID from app to copri. Used for initial setup of bike in copri.</param>
/// <param name="batteryPercentage">Holds the filling level percentage of the battery.</param>
/// <param name="nextAction">If not null next locking action which is performed after booking.</param>
/// <returns>Request to booking bike.</returns>
public string DoBook(string bikeId, Guid guid, double batteryPercentage, LockingAction? nextAction = null)
=> "request=booking_update" +
GetBikeIdParameter(bikeId) +
AuthCookieParameter +
$"&Ilockit_GUID={guid}" +
"&state=occupied" +
GetLockStateParameter(nextAction.GetLockState()) +
GetBatteryPercentageParameters(batteryPercentage) +
UiIsoLanguageNameParameter;
/// <summary> Gets the request to book and start opening the bike (synonym: booking == renting == mieten). </summary>
/// <param name="bikeId">Id of the bike to book.</param>
/// <returns>Request to booking bike.</returns>
public string BookAvailableAndStartOpening(string bikeId)
=> "request=booking_request" +
GetBikeIdParameter(bikeId) +
AuthCookieParameter +
"&state=occupied" +
GetLockStateParameter(lock_state.unlocking) +
SmartDevice.GetSmartDeviceParameters() +
UiIsoLanguageNameParameter;
/// <summary> Gets the request to book and start opening the bike (synonym: booking == renting == mieten). </summary>
/// <param name="bikeId">Id of the bike to book.</param>
/// <returns>Request to booking bike.</returns>
public string BookReservedAndStartOpening(string bikeId)
=> "request=booking_update" +
GetBikeIdParameter(bikeId) +
AuthCookieParameter +
"&state=occupied" +
GetLockStateParameter(lock_state.unlocking) +
UiIsoLanguageNameParameter;
/// <summary> Gets request for returning the bike. </summary>
/// <remarks> Operator specific call.</remarks>
/// <param name="bikeId">Id of bike to return.</param>
/// <param name="geolocation">Geolocation of lock when returning bike.</param>
/// <returns>Request on returning request.</returns>
public string DoReturn(string bikeId, LocationDto geolocation)
=> "request=booking_update" +
GetBikeIdParameter(bikeId) +
AuthCookieParameter +
"&state=available" +
GetLocationParameters(geolocation) +
GetLockStateParameter(lock_state.locked) +
SmartDevice.GetSmartDeviceParameters() +
GetLog() +
UiIsoLanguageNameParameter;
/// <summary> Returns a bike and starts closing. </summary>
/// <param name="bikeId">Id of the bike to return.</param>
/// <param name="smartDevice">Provides info about hard and software.</param>
/// <returns>Response to send to copri.</returns>
public string ReturnAndStartClosing(string bikeId)
=> "request=booking_update" +
GetBikeIdParameter(bikeId) +
AuthCookieParameter +
"&state=available" +
GetLockStateParameter(lock_state.locking) +
SmartDevice.GetSmartDeviceParameters() +
UiIsoLanguageNameParameter;
/// <summary> Gets submit feedback request. </summary>
/// <param name="bikeId">Id of the bike to return.</param>
/// <param name="message">General purpose message or error description.</param>
/// <param name="isBikeBroken">True if bike is broken.</param>
/// <returns>Submit feedback request.</returns>
public string DoSubmitFeedback(
string bikeId,
int? currentChargeBars = null,
string message = null,
bool isBikeBroken = false)
{
string GetIsBikeBroken()
=> isBikeBroken ? "&bike_broken=1" : string.Empty;
string GetMessage()
=> !string.IsNullOrEmpty(message) ? $"&message={WebUtility.UrlEncode(message)}" : string.Empty;
string GetCurrentChargeBars()
=> currentChargeBars != null ? $"&charge_current_bars={currentChargeBars.Value}" : string.Empty;
return "request=user_feedback" +
GetBikeIdParameter(bikeId) +
GetCurrentChargeBars() +
GetIsBikeBroken() +
GetMessage() +
AuthCookieParameter +
UiIsoLanguageNameParameter;
}
/// <summary>
/// Gets request for submitting mini survey to copri server.
/// </summary>
/// <param name="answers">Collection of answers.</param>
public string DoSubmitMiniSurvey(IDictionary<string, string> answers)
{
// Remove entires which invalid keys or values.
var validAnsers = answers?.Where(x => !string.IsNullOrEmpty(x.Key?.Trim()) && !string.IsNullOrEmpty(x.Value?.Trim()));
// Create quersy
if (validAnsers == null || validAnsers.Count() <= 0)
return "request=user_minianswer" +
AuthCookieParameter +
UiIsoLanguageNameParameter;
return "request=user_minianswer" +
$"&{string.Join("&", validAnsers.Select(x => $"{x.Key}={WebUtility.UrlEncode(x.Value)}"))}" +
AuthCookieParameter +
UiIsoLanguageNameParameter;
}
/// <summary>
/// Gets bike id parameter.
/// </summary>
/// <param name="bikeId">Id of bike.</param>
/// <returns>bike id parameter in a format which is urlencode invariant.</returns>
private static string GetBikeIdParameter(string bikeId)
=> $"&bike={WebUtility.UrlEncode(bikeId)}";
/// <summary>
/// Gets parameter holding lock state or null if state is unknown.
/// </summary>
/// <param name="lockState">Null or state of lock.</param>
/// <returns>Lock state in a format which is urlencode invariant.</returns>
private static string GetLockStateParameter(lock_state? lockState)
=> lockState.HasValue ? $"&lock_state={lockState}" : string.Empty;
/// <summary>
/// Gets the battery level percentage parameter if percentage is not NaN an empty string otherwise.
/// </summary>
/// <returns>Gets battery percentage parameters in a format which is urlencode invariant or an empty string if percentage percentge is NaN.</returns>
private static string GetBatteryPercentageParameters(double batteryPercentage) => !double.IsNaN(batteryPercentage)
? $"&voltage={batteryPercentage.ToString(CultureInfo.InvariantCulture)}"
: string.Empty;
/// <summary>
/// Gets the version info parameter or an empty string if version info is not available.
/// </summary>
/// <param name="versionInfo">Lock version info.</param>
/// <returns>Version info in a format which is urlencode invariant or empty. </returns>
private static string GetVersionInfoParameter(IVersionInfo versionInfo) => versionInfo?.FirmwareVersion > 0 || versionInfo?.HardwareVersion > 0 || versionInfo?.LockVersion > 0
? $"&firmware=HW%20{versionInfo.HardwareVersion}%3BFW%20{versionInfo.FirmwareVersion}%3BLock%20{versionInfo.LockVersion}"
: string.Empty;
/// <summary> Gets the geolocation parameter. </summary>
/// <param name="geolocation">Geolocation or null.</param>
/// <returns>Empty string if geolocation is null otherwise parameter including latitude, longitude and age in a format which is urlencode invariant.</returns>
private static string GetLocationParameters(LocationDto geolocation)
{
if (geolocation == null)
return string.Empty;
if (geolocation.Accuracy == null)
return $"&gps={geolocation.Latitude.ToString(CultureInfo.InvariantCulture)},{geolocation.Longitude.ToString(CultureInfo.InvariantCulture)}&gps_age={geolocation.Age.TotalSeconds}";
return $"&gps={geolocation.Latitude.ToString(CultureInfo.InvariantCulture)},{geolocation.Longitude.ToString(CultureInfo.InvariantCulture)}&gps_accuracy={geolocation.Accuracy.Value.ToString(CultureInfo.InvariantCulture)}&gps_age={geolocation.Age.TotalSeconds}";
}
/// <summary>
/// Gets logging entries from serilog.
/// </summary>
/// <returns></returns>
private static string GetLog()
{
var messages = string.Join("\n", MemoryStackSink.PopAllMessages());
return !string.IsNullOrEmpty(messages)
? "&app_debug=" + WebUtility.UrlEncode(string.Join("\n", messages))
: string.Empty;
}
}
}

View file

@ -0,0 +1,21 @@
using System.Runtime.Serialization;
using Newtonsoft.Json;
namespace ShareeBike.Repository.Response
{
[DataContract]
[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
public class AuthorizationResponse : ResponseBase
{
[DataMember]
public int debuglevel { get; private set; }
/// <summary> Holds the group of the bike (ShareeBike, Citybike, ...).</summary>
[DataMember]
public string[] user_group { get; private set; }
/// <summary> Holds value of 0 if agb were not acknowledged.</summary>
[DataMember]
public string agb_checked { get; private set; }
}
}

View file

@ -0,0 +1,11 @@
using System.Runtime.Serialization;
using Newtonsoft.Json;
namespace ShareeBike.Repository.Response
{
[DataContract]
[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
public class AuthorizationoutResponse : ResponseBase
{
}
}

View file

@ -0,0 +1,31 @@
using System;
using System.Runtime.Serialization;
using Newtonsoft.Json;
namespace ShareeBike.Repository.Response
{
[DataContract]
[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
public class BikeInfoAvailable : BikeInfoBase, IEquatable<BikeInfoAvailable>
{
/// <summary>Mini survey for bikes which were rented before and for which feedback is pending.</summary>
[DataMember]
public MiniSurveyResponse user_miniquery { get; private set; }
/// <summary> Information about Co2- saving for bikes which were rented before and for which feedback is pending.</summary>
[DataMember]
public string co2saving { get; private set; }
public static bool operator ==(BikeInfoAvailable first, BikeInfoAvailable second)
=> JsonConvert.SerializeObject(first) == JsonConvert.SerializeObject(second);
public static bool operator !=(BikeInfoAvailable first, BikeInfoAvailable second)
=> !(first == second);
public override bool Equals(object obj) => obj is BikeInfoAvailable target && target == this;
public override int GetHashCode() => JsonConvert.SerializeObject(this).GetHashCode();
public bool Equals(BikeInfoAvailable other) => other == this;
}
}

View file

@ -0,0 +1,113 @@
using System.Runtime.Serialization;
using Newtonsoft.Json;
namespace ShareeBike.Repository.Response
{
/// <summary>
/// Holds info about a single bike.
/// </summary>
[DataContract]
[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
public class BikeInfoBase
{
/// <summary>
/// Id of the bike.
/// </summary>
[DataMember]
public string bike { get; private set; }
/// <summary>
/// Position of the bike.
/// </summary>
[DataMember]
public Position gps { get; private set; }
/// <summary>
/// Id of the station.
/// </summary>
[DataMember]
public string station { get; private set; }
/// <summary>
/// Holds the localized (German) description of the bike.
/// </summary>
[DataMember]
public string description { get; private set; }
/// <summary> Holds the group of the bike.</summary>
/// <remarks>
/// Copri returns values "ShareeBike", "Citybike".
/// </remarks>
[DataMember]
public string[] bike_group { get; private set; }
/// <summary>
/// Rental state.
/// </summary>
[DataMember]
public string state { get; private set; }
/// <summary>
/// Holds the uri where to reserve/ rent the bike.
/// </summary>
[DataMember]
public string uri_operator { get; private set; }
/// <summary> Holds whether bike is equipped with computer or if bike is a lock bike.</summary>
/// <remarks>
/// <table>
/// <tr><th>Value </th><th>Type of bike </th><th>Member to extract info.</th></tr>
/// <tr><td>LOCK </td><td>Bike with manualHtml lock. </td><td>TextToTypeHelper.GetIsNonBikeComputerBike</td></tr>
/// <tr><td>BC </td><td>Bike with a bord computer. </td><td></td></tr>
/// <tr><td>Ilockit </td><td>Bike with a bluetooth lock.</td><td></td></tr>
/// <tr><td>sigo </td><td>Sigo bike.</td><td></td></tr>
/// </table>
/// </remarks>
[DataMember]
public string system { get; private set; }
/// <summary> Holds the tariff information for a bike. </summary>
/// <remarks> This member is obsolete. Use <cref="rental_description"> instead.</cref></remarks>
[DataMember]
public TariffDescription tariff_description { get; private set; }
/// <summary> Holds the rental information for a bike. </summary>
[DataMember]
public RentalDescription rental_description { get; private set; }
[DataMember]
/// <summary> Describes type of the bike.</summary>
public BikeType bike_type { get; private set; }
/// <summary>
/// Holds whether bike is a AA bike (bike must be always returned a the same station) or AB bike (start and end stations can be different stations).
/// </summary>
[DataMember]
public string aa_ride { get; private set; }
/// <summary> Loading state of motor battery in % ]0..100[. </summary>
[DataMember]
public string bike_charge { get; private set; }
/// <summary> Locking state. </summary>
[DataMember]
public string lock_state { get; private set; }
[DataMember]
/// <summary> Full advertisement name.</summary>
public string Ilockit_ID { get; private set; }
[DataMember]
/// <summary> Full advertisement name.</summary>
public string Ilockit_GUID { get; private set; }
/// <summary>
/// Textual description of response.
/// </summary>
/// <returns>Object as text.</returns>
public override string ToString()
{
return $"Bike {bike}{(station != null ? $", at station {station}" : string.Empty)}{(!string.IsNullOrEmpty(description) ? $", {description}" : string.Empty)}{(!string.IsNullOrEmpty(state) ? $", status={state}" : string.Empty)}.";
}
}
}

View file

@ -0,0 +1,46 @@
using System;
using System.Runtime.Serialization;
using Newtonsoft.Json;
namespace ShareeBike.Repository.Response
{
[DataContract]
[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
public class BikeInfoReservedOrBooked : BikeInfoBase, IEquatable<BikeInfoReservedOrBooked>
{
/// <summary>
/// Date from when bike was reserved from/ booked from.
/// Format: 2017-11-28 11:01:51.637747+01
/// </summary>
[DataMember]
public string start_time { get; private set; }
/// <summary> Booking code if bike is BC-bike.</summary>
[DataMember]
public string timeCode { get; private set; }
[DataMember]
/// <summary> Seed used to generate key for connecting to bluetooth lock.</summary>
public string K_seed { get; private set; }
[DataMember]
/// <summary> Key for connect to bluetooth lock as user.</summary>
public string K_u { get; private set; }
[DataMember]
/// <summary> Key for connect to bluetooth lock as admin.</summary>
public string K_a { get; private set; }
public static bool operator ==(BikeInfoReservedOrBooked first, BikeInfoReservedOrBooked second)
=> JsonConvert.SerializeObject(first) == JsonConvert.SerializeObject(second);
public static bool operator !=(BikeInfoReservedOrBooked first, BikeInfoReservedOrBooked second)
=> !(first == second);
public override bool Equals(object obj) => obj is BikeInfoReservedOrBooked target && target == this;
public override int GetHashCode() => JsonConvert.SerializeObject(this).GetHashCode();
public bool Equals(BikeInfoReservedOrBooked other) => other == this;
}
}

View file

@ -0,0 +1,89 @@
using System.Runtime.Serialization;
using Newtonsoft.Json;
namespace ShareeBike.Repository.Response
{
/// <summary>
/// Holds info about a single bike.
/// </summary>
[DataContract]
[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
public class BikeType
{
/// <summary>
/// Holds the engine.
/// </summary>
[DataContract]
[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
public class Engine
{
/// <summary>
/// Manufacturer: ...
/// </summary>
[DataMember]
public string manufacturer { get; private set; }
}
/// <summary>
/// Holds the engine.
/// </summary>
[DataContract]
[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
public class Battery
{
/// <summary>
/// Holds the current charging level in bars.
/// </summary>
[DataMember]
public string charge_current_bars { get; private set; }
/// <summary>
/// Holds the current charging level of the battery in percent.
/// </summary>
[DataMember]
public string charge_current_percent { get; private set; }
/// <summary>
/// Holds the maximum charging level of the battery in bars.
/// </summary>
[DataMember]
public string charge_max_bars { get; private set; }
/// <summary>
/// Holds whether backend is aware of battery charging level.
/// </summary>
[DataMember]
public string backend_accessible { get; private set; }
/// <summary>
/// Holds whether to display battery level or not.
/// </summary>
[DataMember]
public string hidden { get; private set; }
}
/// <summary>
/// Category of the bike. Possible entries: "city", "cargo", ...
/// </summary>
[DataMember]
public string category { get; private set; }
/// <summary>
/// Count of wheels. There are trikes (3 wheels) and two wheeled bikes.
/// </summary>
[DataMember]
public string wheels { get; private set; }
/// <summary>
/// Holds engine information. .
/// </summary>
[DataMember]
public Engine engine { get; private set; }
/// <summary>
/// Holds battery information .
/// </summary>
[DataMember]
public Battery battery { get; private set; }
}
}

View file

@ -0,0 +1,37 @@
using System.Runtime.Serialization;
using Newtonsoft.Json;
namespace ShareeBike.Repository.Response
{
/// <summary>
/// Holds the information about all bikes and is used for deserialization of copri answer.
/// </summary>
[DataContract]
[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
public class BikesAvailableResponse : ResponseBase
{
public BikesAvailableResponse()
{
bikes = new ComparableBikeDictionary<BikeInfoAvailable> ();
bikes_occupied = new ComparableBikeDictionary<BikeInfoReservedOrBooked> ();
}
/// <summary> Dictionary of bikes available.</summary>
[DataMember]
public ComparableBikeDictionary<BikeInfoAvailable> bikes { get; private set; }
/// <summary> Dictionary of bikes reserved or booked.</summary>
[DataMember]
public ComparableBikeDictionary<BikeInfoReservedOrBooked> bikes_occupied { get; private set; }
public static bool operator== (BikesAvailableResponse first, BikesAvailableResponse second)
=> JsonConvert.SerializeObject(first) == JsonConvert.SerializeObject(second);
public static bool operator !=(BikesAvailableResponse first, BikesAvailableResponse second)
=> !(first == second);
public override bool Equals(object obj) => obj is BikesAvailableResponse target && target == this;
public override int GetHashCode() => JsonConvert.SerializeObject(this).GetHashCode();
}
}

View file

@ -0,0 +1,34 @@
using System.Runtime.Serialization;
using Newtonsoft.Json;
namespace ShareeBike.Repository.Response
{
/// <summary>
/// Holds the information about a booking request (reserve, cancel reservation, book or cancel booking) and is used for deserialization of copri answer.
/// </summary>
[DataContract]
[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
public class BikesReservedOccupiedResponse : BookingActionResponse
{
public BikesReservedOccupiedResponse()
{
bikes_occupied = new ComparableBikeDictionary<BikeInfoReservedOrBooked> ();
}
/// <summary>
/// Dictionary of bikes.
/// </summary>
[DataMember]
public ComparableBikeDictionary<BikeInfoReservedOrBooked> bikes_occupied { get; private set; }
public static bool operator ==(BikesReservedOccupiedResponse first, BikesReservedOccupiedResponse second)
=> JsonConvert.SerializeObject(first) == JsonConvert.SerializeObject(second);
public static bool operator !=(BikesReservedOccupiedResponse first, BikesReservedOccupiedResponse second)
=> !(first == second);
public override bool Equals(object obj) => obj is BikesReservedOccupiedResponse target && target == this;
public override int GetHashCode() => JsonConvert.SerializeObject(this).GetHashCode();
}
}

View file

@ -0,0 +1,21 @@
using System.Runtime.Serialization;
using Newtonsoft.Json;
namespace ShareeBike.Repository.Response
{
/// <summary>
/// Holds the information about a booking request (reserve, cancel reservation, book or cancel booking) and is used for deserialization of copri answer.
/// </summary>
[DataContract]
[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
public class BookingActionResponse : ResponseBase
{
/// <summary>
/// Id of the bike which was target of the booking request.
/// </summary>
[DataMember]
public string bike { get; private set; }
}
}

View file

@ -0,0 +1,64 @@
using System.Linq;
using System.Runtime.Serialization;
namespace ShareeBike.Repository.Response
{
public class ComparableBikeDictionary<S> : ComparableDictionary<S> where S : BikeInfoBase
{
[OnDeserialized]
internal void OnDeserializedMethod(StreamingContext context)
{
var toCorrectDictionary = this.Where( x =>
!string.IsNullOrEmpty(x.Value.bike)
&& x.Key != x.Value.bike).ToArray();
foreach (var element in toCorrectDictionary)
{
Remove(element.Key);
if (ContainsKey(element.Value.bike))
{
// Remove duplicates.
Remove(element.Value.bike);
continue;
}
Add(element.Value.bike, element.Value);
}
}
/// <summary>
/// Removes a bike from dictionary.
/// </summary>
/// <typeparam name="T">Type of dictionary values to remove.</typeparam>
/// <param name="bikeId">If of bike to remove.</param>
/// <returns>True if at least one bike was removed, false if no bike was removed.</returns>
public bool RemoveByBikeId<T>(
string bikeId)
{
if (string.IsNullOrEmpty(bikeId))
{
// Nothing to do.
return false;
}
var bikeToRemove = this.FirstOrDefault(x => bikeId == x.Value.bike);
if (string.IsNullOrEmpty(bikeToRemove.Key))
{
// Bike is not contained in dictionary.
return false;
}
return Remove(bikeToRemove.Key);
}
/// <summary>
/// Gets a bike by bike id from dictionary.
/// </summary>
/// <typeparam name="T">Type of dictionary values to remove.</typeparam>
/// <param name="bikeId">If of bike to get.</param>
/// <returns>True if at least one bike was removed, false if no bike was removed.</returns>
public S GetByBikeId(
string bikeId)
=> this.FirstOrDefault(x => bikeId == x.Value.bike).Value;
}
}

View file

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using Newtonsoft.Json;
namespace ShareeBike.Repository.Response
{
/// <summary>
/// Holds a dictionary of comparable elements.
/// </summary>
/// <typeparam name="S">Type of the element.</typeparam>
[DataContract]
public class ComparableDictionary<S> : Dictionary<string, S>, IEquatable<ComparableDictionary<S>>
{
public bool Equals(ComparableDictionary<S> other)
{
return this.OrderBy(x => x.Key).SequenceEqual(other.OrderBy(x => x.Key));
}
public static bool operator ==(ComparableDictionary<S> first, ComparableDictionary<S> second)
=> JsonConvert.SerializeObject(first) == JsonConvert.SerializeObject(second);
public static bool operator !=(ComparableDictionary<S> first, ComparableDictionary<S> second)
=> !(first == second);
public override bool Equals(object obj) => obj is ComparableDictionary<S> target && target == this;
public override int GetHashCode() => JsonConvert.SerializeObject(this).GetHashCode();
}
}

View file

@ -0,0 +1,13 @@
using System.Runtime.Serialization;
using Newtonsoft.Json;
namespace ShareeBike.Repository.Response
{
[DataContract]
[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
public class CopriVersion
{
[DataMember]
public string copri_version { get; private set; }
}
}

View file

@ -0,0 +1,77 @@
using System.Runtime.Serialization;
using Newtonsoft.Json;
namespace ShareeBike.Repository.Response
{
[DataContract]
[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
public class DoReturnResponse : BikesReservedOccupiedResponse
{
/// <summary>
/// Holds information about the returned bike.
/// </summary>
[DataContract]
public class BikeReturned
{
/// <summary>
/// Rental state.
/// </summary>
[DataMember]
public string state { get; private set; }
/// <summary>
/// Id of the bike.
/// </summary>
[DataMember]
public string bike { get; private set; }
/// <summary> Locking state. </summary>
[DataMember]
public string lock_state { get; private set; }
/// <summary>
/// Id of the station.
/// </summary>
[DataMember]
public string station { get; private set; }
/// <summary>
/// Amount of Co2Saving.
/// </summary>
[DataMember]
public string co2saving { get; private set; }
/// <summary>
/// Driven Distance.
/// </summary>
[DataMember]
public string distance { get; private set; }
/// <summary>
/// Duration of finished rental.
/// </summary>
[DataMember]
public string real_clock { get; private set; }
/// <summary>
/// Accruing costs for rental.
/// </summary>
[DataMember]
public string total_price { get; private set; }
}
/// <summary> Mini survey.</summary>
[DataMember]
public MiniSurveyResponse user_miniquery { get; private set; }
[DataMember]
public string co2saving { get; private set; }
/// <summary>
/// Holds information about the returned bike.
/// </summary>
[DataMember]
public BikeReturned bike_returned { get; private set; }
}
}

View file

@ -0,0 +1,29 @@
using Serilog;
using ShareeBike.Repository.Exception;
namespace ShareeBike.Repository.Response
{
public static class JsonConvertRethrow
{
/// <summary>
/// Deserializes COPRI responses in a consitent way for entire app.
/// </summary>
/// <typeparam name="T">Type of object to serialize to.</typeparam>
/// <param name="response">JSON to deserialize.</param>
/// <returns>Deserialized object.</returns>
public static T DeserializeObject<T>(string response)
{
try
{
return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(response);
}
catch (System.Exception ex)
{
Log.Error("Deserializing response failed. {@Exception}",ex);
throw new DeserializationException(ex);
}
}
public static string SerializeObject(object value) => Newtonsoft.Json.JsonConvert.SerializeObject(value);
}
}

View file

@ -0,0 +1,19 @@
using System.Runtime.Serialization;
using Newtonsoft.Json;
namespace ShareeBike.Repository.Response
{
/// <summary> Holds information about map area to display.</summary>
[DataContract]
[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
public class MapSpan
{
/// <summary> Center position of the map. </summary>
[DataMember]
public Position center { get; private set; }
/// <summary> Radius to the map area. </summary>
[DataMember]
public string radius { get; private set; }
}
}

View file

@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
namespace ShareeBike.Repository.Response
{
[DataContract]
public class MiniSurveyResponse
{
[DataContract]
public class Question
{
[DataMember]
public string quest_text { get; private set; }
[DataMember]
public string type { get; private set; }
[DataMember]
public Dictionary<string, string> query { get; private set; }
}
[DataMember]
public string title { get; private set; }
[DataMember]
public string subtitle { get; private set; }
[DataMember]
public string footer { get; private set; }
[DataMember]
public Dictionary<string, Question> questions { get; private set; }
}
}

View file

@ -0,0 +1,19 @@
using System.Runtime.Serialization;
using Newtonsoft.Json;
namespace ShareeBike.Repository.Response
{
/// <summary> Holds position info. </summary>
[DataContract]
[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
public class Position
{
/// <summary> Latitude position (bike, station, map center...). </summary>
[DataMember]
public string latitude { get; private set; }
/// <summary> Longitude position (bike, station, map center...). </summary>
[DataMember]
public string longitude { get; private set; }
}
}

View file

@ -0,0 +1,49 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
using Newtonsoft.Json;
namespace ShareeBike.Repository.Response
{
/// <summary>
/// Successor of TarifDescription- object.
/// Manages tariff- and rental info.
/// </summary>
[DataContract]
[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
public class RentalDescription
{
/// <summary>
/// Name of the tariff.
/// </summary>
[DataMember]
public string name { get; private set; }
/// <summary>
/// Id of the tariff.
/// </summary>
[DataMember]
public string id { get; private set; }
/// <summary>
/// Holds the time span in minutes for which a bike can be reserved.
/// </summary>
[DataMember]
public string reserve_timerange { get; private set; }
/// <summary> Holds tariff entires to show to user.</summary>
[DataMember]
public Dictionary<
string /* Key of tariff object for sorting purposes*/,
string[] /* Holds two Elements: first element is the description of the element (example: "Max Gebühr"), second is the value (example: "9.00 € / Tag")*/>
tarif_elements
{ get; private set; }
/// <summary> Holds tariff entires to show to user.</summary>
[DataMember]
public Dictionary<
string /* Key of info object for sorting purposes*/,
string[] /* Holds two Elements: first element is the key of the element (example: "Tracking"), second is the value (example: "Ich stimme der Speicherung (Tracking) meiner Fahrstrecke ....")*/>
rental_info
{ get; private set; }
}
}

View file

@ -0,0 +1,17 @@
using System.Runtime.Serialization;
using Newtonsoft.Json;
namespace ShareeBike.Repository.Response
{
/// <summary>
/// Holds the information about a booking request and is used for deserialization of copri answer.
/// </summary>
[DataContract]
[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
public class ReservationBookingResponse : BikesReservedOccupiedResponse
{
/// <summary> Booking code for BC- bikes. </summary>
[DataMember]
public string timeCode { get; private set; }
}
}

View file

@ -0,0 +1,59 @@
using System.Runtime.Serialization;
using Newtonsoft.Json;
namespace ShareeBike.Repository.Response
{
[DataContract]
[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
public class ResponseBase : CopriVersion
{
[DataMember]
public string response_state { get; private set; }
[DataMember]
public string response { get; private set; }
[DataMember]
public string response_text { get; private set; }
[DataMember]
public string authcookie { get; private set; }
/// <summary> Message shown to user.</summary>
[DataMember]
public string merchant_message { get; private set; }
/// <summary> Initial map display area.</summary>
[DataMember]
public MapSpan init_map { get; private set; }
/// <summary> Url of page holding agb info. </summary>
[DataMember]
public string agb_html { get; private set; }
/// <summary> Url of page holding instructions how to rent bikes. </summary>
[DataMember]
public string bike_info_html { get; private set; }
/// <summary> Url of page holding privacy info. </summary>
[DataMember]
public string privacy_html { get; private set; }
/// <summary> Url of page holding impress info. </summary>
[DataMember]
public string impress_html { get; private set; }
/// <summary> Url of page holding tariff info. </summary>
[DataMember]
public string tariff_info_html { get; private set; }
/// <summary> Textual description of response. </summary>
public override string ToString()
{
return $"Response state is \"{response_state ?? string.Empty}\", " +
$"auth cookie is \"{authcookie ?? string.Empty}\" and response is \"{response_text ?? string.Empty}\", " +
$"code \"{response ?? string.Empty}\"" +
$"response text \"{response_text ?? string.Empty}\".";
}
}
}

View file

@ -0,0 +1,27 @@
using System.Runtime.Serialization;
using Newtonsoft.Json;
namespace ShareeBike.Repository.Response
{
[DataContract]
[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
public class ResponseContainer<T>
{
[DataMember]
public T shareejson { get; private set; }
/// <summary>
/// Serializes object to string.
/// </summary>
/// <returns></returns>
public override string ToString()
{
if (shareejson == null)
{
return "Response container does not hold no entry.";
}
return shareejson.ToString();
}
}
}

View file

@ -0,0 +1,230 @@
using System.Linq;
using ShareeBike.MultilingualResources;
using ShareeBike.Repository.Exception;
namespace ShareeBike.Repository.Response
{
public static class ResponseHelper
{
public const string RESPONSE_OK = "OK";
/// <summary> Holds the description of the action return bike. </summary>
public const string RESPONSE_AUTHCOOKIE_EXPRIED = "Failure 1001:";
/// <summary> Holds the description of the action logout. </summary>
public const string BIKES_LOGOUT_ACTIONTEXT = "Abmeldung fehlgeschlagen.";
/// <summary> Holds the description of the action get stations available. </summary>
public const string STATIONS_AVAILABLE_ACTIONTEXT = "Abfrage der verfügbaren Stationen fehlgeschlagen.";
/// <summary> Holds the description of the action get bikes available. </summary>
public const string BIKES_AVAILABLE_ACTIONTEXT = "Abfrage der verfügbaren Fahrräder fehlgeschlagen.";
/// <summary> Holds the description of the action get bikes occupied. </summary>
public const string BIKES_OCCUPIED_ACTIONTEXT = "Abfrage der reservierten/ gebuchten Fahrräder fehlgeschlagen.";
/// <summary> Holds the description of the action return bike. </summary>
public const string BIKES_RETURNBIKE_ACTIONTEXT = "Rückgabe des Rads fehlgeschlagen.";
/// <summary>
/// Checks if log in response is ok.
/// </summary>
/// <param name="response">Response to check whether it is valid.</param>
/// <param name="mail">Mail address used to create details error message in case log in response is invalid.</param>
/// <returns></returns>
public static AuthorizationResponse GetIsResponseOk(this AuthorizationResponse response, string mail)
{
if (response == null)
{
throw new InvalidResponseException<AuthorizationResponse>("Anmeldung fehlgeschlagen.", null);
}
if (response.response_state.ToUpper() == InvalidAuthorizationResponseException.AUTH_FAILURE_STATUS_MESSAGE_UPPERCASE)
{
throw new InvalidAuthorizationResponseException(mail, response);
}
else if (!response.response_state.Trim().ToUpper().StartsWith(RESPONSE_OK))
{
throw new InvalidResponseException<AuthorizationResponse>($"Anmeldung {mail} fehlgeschlagen.\r\nServer Antwort: {response.response_text}", response);
}
return response;
}
/// <summary>
/// Checks if log out response is ok.
/// </summary>
/// <param name="p_oResponse">Response to check whether it is valid.</param>
/// <returns></returns>
public static AuthorizationoutResponse GetIsResponseOk(this AuthorizationoutResponse p_oResponse)
{
if (AuthcookieNotDefinedException.IsAuthcookieNotDefined(p_oResponse, BIKES_LOGOUT_ACTIONTEXT, out AuthcookieNotDefinedException exception))
{
throw exception;
}
GetIsResponseOk(p_oResponse, BIKES_LOGOUT_ACTIONTEXT);
if (p_oResponse.authcookie != "1")
{
throw new InvalidResponseException<AuthorizationoutResponse>(
BIKES_LOGOUT_ACTIONTEXT,
p_oResponse);
}
return p_oResponse;
}
/// <summary>Gets if a call to reserve bike succeeded or not by checking a booking response.</summary>
/// <param name="bikeId">Id of bike which should be booked.</param>
/// <param name="sessionCookie">Session cookie of logged in user.</param>
/// <param name="bookingResponse">Response to check.</param>
/// <returns></returns>
public static BikeInfoReservedOrBooked GetIsReserveResponseOk(
this ReservationBookingResponse bookingResponse,
string bikeId)
{
GetIsResponseOk(bookingResponse, string.Format(AppResources.ErrorReservingBike, bikeId));
if (BookingDeclinedException.IsBookingDeclined(bookingResponse.response_state, out BookingDeclinedException exception))
{
throw exception;
}
// Get bike which has to be booked.
var bikeInfoRequestedOccupied = bookingResponse?.bikes_occupied?.Values?.FirstOrDefault(x => x.bike == bikeId);
if (bikeInfoRequestedOccupied == null)
{
throw new System.Exception(string.Format(
AppResources.ErrorReservingBikeUnavailalbe,
bikeId,
!string.IsNullOrWhiteSpace(bookingResponse?.response_text) ? $"\r\n{bookingResponse.response_text}" : string.Empty));
}
return bikeInfoRequestedOccupied;
}
/// <summary> Gets if a booking call succeeded or not by checking a booking response. </summary>
/// <param name="bikeId">Id of bike which should be booked.</param>
/// <param name="bookingResponse">Response to check.</param>
/// <returns></returns>
public static BikeInfoReservedOrBooked GetIsBookingResponseOk(
this ReservationBookingResponse bookingResponse,
string bikeId)
{
GetIsResponseOk(bookingResponse, string.Format(AppResources.ErrorRentingBike, bikeId));
// Get bike which has to be booked.
var bikeInfoRequestedOccupied = bookingResponse?.bikes_occupied?.Values?.FirstOrDefault(x => x.bike == bikeId);
if (bikeInfoRequestedOccupied == null)
{
throw new System.Exception(string.Format(
AppResources.ErrorRentingBikeUnavailalbe,
bikeId,
!string.IsNullOrWhiteSpace(bookingResponse?.response_text) ? $"\r\n{bookingResponse.response_text}" : string.Empty));
}
return bikeInfoRequestedOccupied;
}
/// <summary> Gets if request is ok.</summary>
/// <param name="response">Response to verify.</param>
/// <param name="textOfAction">Text describing request which is shown if validation fails.</param>
/// <returns>Verified response.</returns>
public static BikesReservedOccupiedResponse GetIsResponseOk(this BikesReservedOccupiedResponse response, string textOfAction)
{
if (response == null || response.response_state == null)
{
throw new InvalidResponseException<BikesReservedOccupiedResponse>(textOfAction, null);
}
if (AuthcookieNotDefinedException.IsAuthcookieNotDefined(response, textOfAction, out AuthcookieNotDefinedException exception))
{
throw exception;
}
if (response.response_state.Trim().ToUpper().StartsWith(RESPONSE_AUTHCOOKIE_EXPRIED.ToUpper()))
{
throw new AuthcookieNotDefinedException(
$"{textOfAction}\r\n{AppResources.ErrorAccountInvalidAuthorization}",
response);
}
GetIsResponseOk((ResponseBase)response, textOfAction);
return response;
}
/// <summary> Gets if request is ok.</summary>
/// <param name="response">Response to verify.</param>
/// <param name="textOfAction">Text describing request which is shown if validation fails.</param>
/// <returns>Verified response.</returns>
public static T GetIsResponseOk<T>(this T response, string textOfAction) where T : ResponseBase
{
if (response == null || response.response_state == null)
{
throw new InvalidResponseException<T>(textOfAction, null);
}
if (AuthcookieNotDefinedException.IsAuthcookieNotDefined(response, textOfAction, out AuthcookieNotDefinedException exception))
{
throw exception;
}
if (!response.response_state.Trim().ToUpper().StartsWith(RESPONSE_OK))
{
throw new InvalidResponseException<T>(
$"{textOfAction}\r\nServer Antwort: {response.response_text}",
response);
}
return response;
}
/// <summary> Gets if return bike request is ok.</summary>
/// <param name="returnBikeResponse">Response to verify.</param>
/// <param name="textOfAction">Text describing request which is shown if validation fails.</param>
/// <param name="bikeId">Id of bike.</param>
/// <returns>Verified response.</returns>
public static DoReturnResponse GetIsReturnBikeResponseOk(
this DoReturnResponse returnBikeResponse,
string bikeId)
{
// Check if bike is at station.
if (NotAtStationException.IsNotAtStation(returnBikeResponse.response_state.ToUpper(), out NotAtStationException notAtStationException))
{
throw notAtStationException;
}
// Check if GPS data was send to copri.
if (NoGPSDataException.IsNoGPSData(returnBikeResponse.response_state.ToUpper(), out NoGPSDataException noGPSDataException))
{
throw noGPSDataException;
}
GetIsResponseOk<ResponseBase>(returnBikeResponse, BIKES_RETURNBIKE_ACTIONTEXT);
// Get bike which has to be booked.
var l_oBikeInfo = returnBikeResponse?.bikes_occupied?.Values?.FirstOrDefault(x => x.bike == bikeId);
if (l_oBikeInfo != null)
{
throw new ReturnBikeException(
returnBikeResponse,
$"{BIKES_RETURNBIKE_ACTIONTEXT} Aufruf wurde erfolgreich ausgeführt, aber Rad ist noch in Liste der gemieteten Räder enthalten.");
}
return returnBikeResponse;
}
/// <summary>
/// Gets the response for bikes occupied request with no bikes reserved.
/// </summary>
/// <param name="p_strSesstionCookie"></param>
/// <returns></returns>
public static BikesReservedOccupiedResponse GetBikesOccupiedNone(string p_strSesstionCookie = null)
{
var l_oJson = CopriCallsMonkeyStore.BIKESOCCUPIED.Replace(@"""authcookie"": """"", @"""authcookie"": """ + (p_strSesstionCookie ?? string.Empty) + @"""");
return CopriCallsStatic.DeserializeResponse(@"{ ""shareejson"" : " + l_oJson + "}", (version) => new BikesReservedOccupiedResponse());
}
}
}

View file

@ -0,0 +1,21 @@
using System.Runtime.Serialization;
namespace ShareeBike.Repository.Response.Stations.Station
{
/// <summary>
/// Holds a single bike group.
/// </summary>
[DataContract]
public class BikeGroup
{
/// <summary> Holds the count of bikes for the group. </summary>
[DataMember]
public string bike_count { get; private set;}
/// <summary>
/// Holds the group identifying the bike group type.
/// </summary>
[DataMember]
public string bike_group { get; private set; }
}
}

View file

@ -0,0 +1,27 @@
using System.Runtime.Serialization;
namespace ShareeBike.Repository.Response.Stations.Station
{
/// <summary>
/// Holds info about operator data.
/// </summary>
[DataContract]
public class OperatorData
{
[DataMember]
public string operator_name { get; private set; }
[DataMember]
public string operator_phone { get; private set; }
[DataMember]
public string operator_hours { get; private set; }
[DataMember]
public string operator_email { get; private set; }
[DataMember]
public string operator_color { get; private set; }
}
}

View file

@ -0,0 +1,62 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
using Newtonsoft.Json;
namespace ShareeBike.Repository.Response.Stations.Station
{
/// <summary>
/// Holds info about a single station.
/// </summary>
[DataContract]
public class StationInfo
{
/// <summary>
/// Unique id of the station.
/// </summary>
[DataMember]
public string station { get; private set; }
[DataMember]
public string[] station_group { get; private set; }
[DataMember]
public string description { get; private set; }
/// <summary>
/// Position of the station.
/// </summary>
[DataMember]
public Position gps { get; private set; }
[DataMember]
public OperatorData operator_data { get; private set; }
/// <summary>
/// Holds the count of available bikes at the station.
/// </summary>
[DataMember]
public string bike_count { get; set; }
/// <summary>
/// Holds the count type of station, i.e. which bikes are located at station.
/// </summary>
[DataMember]
public Dictionary<string, BikeGroup> station_type { get; private set; }
/// <summary>
/// Holds the uri of the operator which manages the bikes at station/ the station.
/// </summary>
[DataMember]
public string uri_operator { get; private set; }
public static bool operator ==(StationInfo first, StationInfo second)
=> JsonConvert.SerializeObject(first) == JsonConvert.SerializeObject(second);
public static bool operator !=(StationInfo first, StationInfo second)
=> !(first == second);
public override bool Equals(object obj) => obj is StationInfo target && target == this;
public override int GetHashCode() => JsonConvert.SerializeObject(this).GetHashCode();
}
}

View file

@ -0,0 +1,41 @@
using System.Runtime.Serialization;
using Newtonsoft.Json;
using ShareeBike.Repository.Response.Stations.Station;
namespace ShareeBike.Repository.Response.Stations
{
/// <summary>
/// Holds the information about all stations and is used for deserialization of copri answer.
/// </summary>
[DataContract]
public class StationsAvailableResponse : ResponseBase
{
public StationsAvailableResponse()
{
stations = new ComparableDictionary< StationInfo>();
bikes_occupied = new ComparableBikeDictionary<BikeInfoReservedOrBooked> { };
}
/// <summary>
/// Dictionary of bikes.
/// </summary>
[DataMember]
public ComparableDictionary< StationInfo> stations { get; private set; }
/// <summary>
/// Dictionary of bikes reserved (requested) and rented (occupied) by current user if user is logged in and has reserved or rented bikes.
/// </summary>
[DataMember]
public ComparableBikeDictionary<BikeInfoReservedOrBooked> bikes_occupied { get; private set; }
public static bool operator ==(StationsAvailableResponse first, StationsAvailableResponse second)
=> JsonConvert.SerializeObject(first) == JsonConvert.SerializeObject(second);
public static bool operator !=(StationsAvailableResponse first, StationsAvailableResponse second)
=> !(first == second);
public override bool Equals(object obj) => obj is StationsAvailableResponse target && target == this;
public override int GetHashCode() => JsonConvert.SerializeObject(this).GetHashCode();
}
}

View file

@ -0,0 +1,11 @@
using Newtonsoft.Json;
using System.Runtime.Serialization;
namespace ShareeBike.Repository.Response
{
[DataContract]
[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
public class SubmitFeedbackResponse : ResponseBase
{
}
}

View file

@ -0,0 +1,57 @@
using System.Runtime.Serialization;
using Newtonsoft.Json;
namespace ShareeBike.Repository.Response
{
/// <summary>
/// Holds tariff info for a single bike.
/// </summary>
[DataContract]
[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
#if USCSHARP9
public record TariffDescription
#else
public class TariffDescription
#endif
{
/// <summary>
/// Name of the tariff.
/// </summary>
[DataMember]
public string name { get; private set; }
/// <summary>
/// Number of the tariff.
/// </summary>
[DataMember]
public string number { get; private set; }
/// <summary>
/// Costs per hour in euro.
/// </summary>
[DataMember]
public string eur_per_hour { get; private set; }
/// <summary>
/// Costs of the abo per month.
/// </summary>
[DataMember]
public string abo_eur_per_month { get; private set; }
/// <summary>
/// Costs per hour in euro.
/// </summary>
[DataMember]
public string free_hours { get; private set; }
/// <summary>
/// Maximum fee per day.
/// </summary>
[DataMember]
public string max_eur_per_day { get; private set; }
/// <summary> Text which informs users about GPS tracking if tracking is on. </summary>
[DataMember]
public string track_info { get; private set; }
}
}

View file

@ -0,0 +1,24 @@
using System.Runtime.Serialization;
using Newtonsoft.Json;
namespace ShareeBike.Repository.Response
{
[DataContract]
[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
public class VersionindependentResponse
{
private CopriVersion _shareejson;
/// <summary> Root element for versions 4.0 and older. </summary>
[DataMember]
public CopriVersion tinkjson { get; private set; }
/// <summary> Root element from 4.1 and later. </summary>
[DataMember]
public CopriVersion shareejson
{
get => _shareejson ?? tinkjson;
private set { _shareejson = value; }
}
}
}