Skip to content

Commit

Permalink
Merge pull request #81 in MML/infobip-mobile-messaging-ios from akado…
Browse files Browse the repository at this point in the history
…chnikov-MM-2283-DynamicBaseUrl to master

Squashed commit of the following:

commit 113025057d21c33f361e73d8ce35b0a26bc198e6
Author: Andrey K <[email protected]>
Date:   Mon Nov 27 17:12:44 2017 +0100

    refinements for a test

commit 24edc8cff55106ecc01c1c4ec769b13bf4dcdc58
Author: Andrey K <[email protected]>
Date:   Mon Nov 27 17:10:58 2017 +0100

    additional comments

commit 0f5f8f91eba096830325a188c32ca2a43dba8716
Author: Andrey K <[email protected]>
Date:   Mon Nov 27 17:05:22 2017 +0100

    refinements

commit 5d8cfe04a6774a357ec250503aed747a6bf1fd02
Author: Andrey K <[email protected]>
Date:   Mon Nov 27 16:56:35 2017 +0100

    now we will retry if host not found for any request (now, by default, retry limit is 1 for all requests and only allowed for "cannot find host" error if other rules not specified)

commit eb3675e2d6afb1a9df26747a5f2b2f3a734044ef
Author: Andrey K <[email protected]>
Date:   Mon Nov 27 16:23:08 2017 +0100

    tests for retries when baseurl changing

commit b223ac6620366c477540e2f107899e9d2ea87387
Author: Andrey K <[email protected]>
Date:   Mon Nov 27 09:46:57 2017 +0100

    code refinements

commit 17d3beca47fb3a2438302e7432bcb7d3b56d6854
Author: Andrey K <[email protected]>
Date:   Fri Nov 24 18:24:12 2017 +0300

    shared dynamic base url storage

commit b2d9cba0e7ce88984eacfb4ddb31bbfb243a389d
Author: Andrey K <[email protected]>
Date:   Fri Nov 24 13:17:54 2017 +0300

    error handling

commit c98d242330b28b3d51463b98939d8b9d3f2b4d1a
Author: Andrey K <[email protected]>
Date:   Fri Nov 24 12:13:30 2017 +0300

    tests

commit 5fddd25fbd61dd3444cb84e4d73928583e47fdc1
Author: Andrey K <[email protected]>
Date:   Thu Nov 23 18:01:48 2017 +0300

    extension supports dynamic base url

commit 08590535bc80c0cd283a8623841aa40915cbc420
Author: Andrey K <[email protected]>
Date:   Thu Nov 23 15:14:53 2017 +0300

    initial implementation
  • Loading branch information
Andrey Kadochnikov authored and Andrey Kadochnikov committed Nov 29, 2017
1 parent 9ebf775 commit f6e59e0
Show file tree
Hide file tree
Showing 33 changed files with 417 additions and 200 deletions.
125 changes: 125 additions & 0 deletions Classes/Core/HTTP/DynamicBaseUrlHTTPSessionManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
//
// DynamicBaseUrlHTTPSessionManager.swift
// MobileMessaging
//
// Created by Andrey Kadochnikov on 23/11/2017.
//

import Foundation

struct DynamicBaseUrlConsts {
static let newBaseUrlHeader = "newbaseurl"
static let storedDynamicBaseUrlKey = "com.mobile-messaging.dynamic-base-url"
}

protocol DynamicBaseUrlStorage {
func get() -> URL?

func cleanUp()

func set(_ url: URL)
}

extension UserDefaults: DynamicBaseUrlStorage {
func get() -> URL? {
return object(forKey: DynamicBaseUrlConsts.storedDynamicBaseUrlKey) as? URL
}

func cleanUp() {
removeObject(forKey: DynamicBaseUrlConsts.storedDynamicBaseUrlKey)
synchronize()
}

func set(_ url: URL) {
set(url, forKey: DynamicBaseUrlConsts.storedDynamicBaseUrlKey)
synchronize()
}
}

class DynamicBaseUrlHTTPSessionManager {
let applicationCode: String
var dynamicBaseUrl: URL?
let originalBaseUrl: URL?
let configuration: URLSessionConfiguration?
let appGroupId: String?
var storage: DynamicBaseUrlStorage

init(applicationCode: String, baseURL url: URL?, sessionConfiguration configuration: URLSessionConfiguration?, appGroupId: String?) {
self.applicationCode = applicationCode
self.configuration = configuration
self.originalBaseUrl = url
self.appGroupId = appGroupId
if let appGroupId = appGroupId, let sharedUserDefaults = UserDefaults(suiteName: appGroupId) {
self.storage = sharedUserDefaults
} else {
self.storage = UserDefaults.standard
}
self.dynamicBaseUrl = getStoredDynamicBaseUrl() ?? url
}

private func storeDynamicBaseUrl(_ url: URL?) {
if let url = url {
storage.set(url)
} else {
storage.cleanUp()
}
}

private func getStoredDynamicBaseUrl() -> URL? {
return storage.get()
}

func sendRequest<R: RequestData>(_ request: R, completion: @escaping (Result<R.ResponseType>) -> Void) {
let sessionManager = makeSessionManager(for: request)

let successBlock = { (task: URLSessionDataTask, obj: Any?) -> Void in
self.handleDynamicBaseUrl(response: task.response, error: nil)
if let obj = obj as? R.ResponseType {
completion(Result.Success(obj))
} else {
let error = NSError(domain: AFURLResponseSerializationErrorDomain, code: NSURLErrorCannotDecodeContentData, userInfo:[NSLocalizedFailureReasonErrorKey : "Request succeeded with no return value or return value wasn't a ResponseType value."])
completion(Result.Failure(error))
}
}

let failureBlock = { (task: URLSessionDataTask?, error: Error) -> Void in
self.handleDynamicBaseUrl(response: task?.response, error: error as NSError?)
completion(Result<R.ResponseType>.Failure(error as NSError?))
}

performRequest(request, sessionManager: sessionManager, successBlock: successBlock, failureBlock: failureBlock)
}

func makeSessionManager<R: RequestData>(for request: R) -> MM_AFHTTPSessionManager {
let sessionManager = MM_AFHTTPSessionManager(baseURL: dynamicBaseUrl, sessionConfiguration: configuration)
sessionManager.responseSerializer = ResponseSerializer<R.ResponseType>()
sessionManager.requestSerializer = RequestSerializer(applicationCode: applicationCode, jsonBody: request.body, headers: request.headers)
return sessionManager
}

func performRequest<R: RequestData>(_ request: R, sessionManager: MM_AFHTTPSessionManager, successBlock: @escaping (URLSessionDataTask, Any?) -> Void, failureBlock: @escaping (URLSessionDataTask?, Error) -> Void) {

MMLogDebug("Sending request \(type(of: self))\nparameters: \(String(describing: request.parameters))\nbody: \(String(describing: request.body))\nto \(dynamicBaseUrl?.absoluteString ?? "empty-host" + request.path.rawValue)")

switch request.method {
case .POST:
sessionManager.post(request.path.rawValue, parameters: request.parameters, progress: nil, success: successBlock, failure: failureBlock)
case .PUT:
sessionManager.put(request.path.rawValue, parameters: request.parameters, success: successBlock, failure: failureBlock)
case .GET:
sessionManager.get(request.path.rawValue, parameters: request.parameters, progress: nil, success: successBlock, failure: failureBlock)
}
}

func handleDynamicBaseUrl(response: URLResponse?, error: NSError?) {
if let error = error, error.mm_isCannotFindHost {
storeDynamicBaseUrl(nil)
dynamicBaseUrl = originalBaseUrl
} else {
if let httpResponse = response as? HTTPURLResponse, let newBaseUrlString = httpResponse.allHeaderFields[DynamicBaseUrlConsts.newBaseUrlHeader] as? String, let newDynamicBaseUrl = URL(string: newBaseUrlString) {
storeDynamicBaseUrl(newDynamicBaseUrl)
dynamicBaseUrl = newDynamicBaseUrl
}
}
}
}
3 changes: 2 additions & 1 deletion Classes/Core/HTTP/MMAPIKeysAndValues.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ struct APNSPayloadKeys {
}

struct APIValues {
static let prodBaseURLString = "https://oneapi2.infobip.com"
static let prodDynamicBaseURLString = "https://mobile.infobip.com"
static let prodBaseURLString = "https://oneapi2.infobip.com" // not in use afte migration on https://mobile.infobip.com
static let platformType = "APNS"
}
9 changes: 3 additions & 6 deletions Classes/Core/HTTP/MMRemoteAPIQueue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,19 @@ enum Result<ValueType> {
}

class RemoteAPIQueue {
private(set) var baseURL: String
private(set) var applicationCode: String
let mmContext: MobileMessaging

lazy var queue: MMRetryOperationQueue = {
return MMRetryOperationQueue.newSerialQueue
}()

init(mmContext: MobileMessaging, baseURL: String, applicationCode: String) {
init(mmContext: MobileMessaging) {
self.mmContext = mmContext
self.baseURL = baseURL
self.applicationCode = applicationCode
}

func perform<R: RequestData>(request: R, exclusively: Bool = false, completion: @escaping (Result<R.ResponseType>) -> Void) {
let requestOperation = MMRetryableRequestOperation<R>(request: request, reachabilityManager: mmContext.reachabilityManager, applicationCode: applicationCode, baseURL: baseURL) { responseResult in
let requestOperation = MMRetryableRequestOperation<R>(request: request, reachabilityManager: mmContext.reachabilityManager, sessionManager: mmContext.httpSessionManager) { responseResult in

completion(responseResult)
self.postErrorNotificationIfNeeded(error: responseResult.error)
}
Expand Down
41 changes: 8 additions & 33 deletions Classes/Core/HTTP/MMRequests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ enum APIPath: String {
struct RegistrationRequest: PostRequest {
typealias ResponseType = RegistrationResponse
var retryLimit: Int { return 3 }
func mustRetryOnResponseError(_ error: NSError) -> Bool {
return retryLimit > 0 && error.mm_isRetryable
}
var path: APIPath { return .Registration }
var parameters: RequestParameters? {
var params: RequestParameters = [PushRegistration.deviceToken: deviceToken,
Expand Down Expand Up @@ -62,9 +65,11 @@ struct LibraryVersionRequest: GetRequest {
}

struct MessagesSyncRequest: PostRequest {

typealias ResponseType = MessagesSyncResponse
var retryLimit: Int = 2
func mustRetryOnResponseError(_ error: NSError) -> Bool {
return retryLimit > 0 && error.mm_isRetryable
}
var path: APIPath { return .SyncMessages }
var parameters: RequestParameters? {
var params = RequestParameters()
Expand Down Expand Up @@ -195,7 +200,6 @@ enum Method: String {

protocol RequestResponsable {
associatedtype ResponseType: JSONDecodable
func responseObject(applicationCode: String, baseURL: String, completion: @escaping (Result<ResponseType>) -> Void)
}

protocol RequestData: RequestResponsable {
Expand All @@ -220,41 +224,12 @@ extension PostRequest {

extension RequestData {
func mustRetryOnResponseError(_ error: NSError) -> Bool {
return retryLimit > 0 && error.mm_isRetryable
return retryLimit > 0 && error.mm_isCannotFindHost
}
var retryLimit: Int { return 0 }
var retryLimit: Int { return 1 }
var headers: RequestHeaders? { return nil }
var body: RequestBody? { return nil }
var parameters: RequestParameters? { return nil }
func responseObject(applicationCode: String, baseURL: String, completion: @escaping (Result<ResponseType>) -> Void) {
let manager = MM_AFHTTPSessionManager(baseURL: URL(string: baseURL), sessionConfiguration: MobileMessaging.urlSessionConfiguration)
manager.requestSerializer = RequestSerializer(applicationCode: applicationCode, jsonBody: body, headers: headers)
manager.responseSerializer = ResponseSerializer<ResponseType>()
MMLogDebug("Sending request \(type(of: self))\nparameters: \(String(describing: parameters))\nbody: \(String(describing: body))\nto \(baseURL + path.rawValue)")

let successBlock = { (task: URLSessionDataTask, obj: Any?) -> Void in
if let obj = obj as? ResponseType {
completion(Result.Success(obj))
} else {
let error = NSError(domain: AFURLResponseSerializationErrorDomain, code: NSURLErrorCannotDecodeContentData, userInfo:[NSLocalizedFailureReasonErrorKey : "Request succeeded with no return value or return value wasn't a ResponseType value."])
completion(Result.Failure(error))
}
}

let failureBlock = { (task: URLSessionDataTask?, error: Error) -> Void in
completion(Result<ResponseType>.Failure(error as NSError?))
}

let urlString = manager.baseURL!.absoluteString + self.path.rawValue
switch self.method {
case .POST:
manager.post(urlString, parameters: parameters, progress: nil, success: successBlock, failure: failureBlock)
case .PUT:
manager.put(urlString, parameters: parameters, success: successBlock, failure: failureBlock)
case .GET:
manager.get(urlString, parameters: parameters, progress: nil, success: successBlock, failure: failureBlock)
}
}
}

struct SeenData: DictionaryRepresentable {
Expand Down
17 changes: 8 additions & 9 deletions Classes/Core/HTTP/MMResponseSerializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
//
//


final class ResponseSerializer<T: JSONDecodable> : MM_AFHTTPResponseSerializer {
override init() {
super.init()
Expand All @@ -19,28 +18,28 @@ final class ResponseSerializer<T: JSONDecodable> : MM_AFHTTPResponseSerializer {
override func responseObject(for response: URLResponse?, data: Data?, error: NSErrorPointer) -> Any? {
super.responseObject(for: response, data: data, error: error)

guard let response = response,
let data = data else {
return nil
guard let response = response, let data = data else {
return nil
}

let dataString = String(data: data, encoding: String.Encoding.utf8)

MMLogDebug("Response received: \(response)\n\(String(describing: dataString))")

let json = JSON(data: data)
if let requestError = RequestError(json: json) ,response.isFailureHTTPREsponse {
if let requestError = RequestError(json: json), response.isFailureHTTPResponse {
error?.pointee = requestError.foundationError
}
return T(json: json)
}
}

extension URLResponse {
var isFailureHTTPREsponse: Bool {
var statusCodeIsError = false
var isFailureHTTPResponse: Bool {
if let httpResponse = self as? HTTPURLResponse {
statusCodeIsError = IndexSet(integersIn: 200..<300).contains(httpResponse.statusCode) == false
return IndexSet(integersIn: 200..<300).contains(httpResponse.statusCode) == false
} else {
return false
}
return statusCodeIsError
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
//
// RemoteAPIManager.swift
// RemoteAPIProvider.swift
//
// Created by Andrey K. on 26/11/2016.
//
//

import Foundation

class RemoteAPIManager {
class RemoteAPIProvider {
internal(set) var versionFetchingQueue: RemoteAPIQueue
internal(set) var registrationQueue: RemoteAPIQueue
internal(set) var messageSyncQueue: RemoteAPIQueue
internal(set) var seenStatusQueue: RemoteAPIQueue

init(baseUrl: String, applicationCode: String, mmContext: MobileMessaging) {
registrationQueue = RemoteAPIQueue(mmContext: mmContext, baseURL: baseUrl, applicationCode: applicationCode)
seenStatusQueue = RemoteAPIQueue(mmContext: mmContext, baseURL: baseUrl, applicationCode: applicationCode)
messageSyncQueue = RemoteAPIQueue(mmContext: mmContext, baseURL: baseUrl, applicationCode: applicationCode)
versionFetchingQueue = RemoteAPIQueue(mmContext: mmContext, baseURL: baseUrl, applicationCode: applicationCode)
init(mmContext: MobileMessaging) {
registrationQueue = RemoteAPIQueue(mmContext: mmContext)
seenStatusQueue = RemoteAPIQueue(mmContext: mmContext)
messageSyncQueue = RemoteAPIQueue(mmContext: mmContext)
versionFetchingQueue = RemoteAPIQueue(mmContext: mmContext)
}

func syncRegistration(deviceToken: String, isEnabled: Bool?, expiredInternalId: String?, completion: @escaping (RegistrationResult) -> Void) {
Expand Down
16 changes: 10 additions & 6 deletions Classes/Core/MobileMessaging.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public final class MobileMessaging: NSObject {
/// - parameter code: The application code of your Application from Push Portal website.
/// - parameter notificationType: Preferable notification types that indicating how the app alerts the user when a push notification arrives.
public class func withApplicationCode(_ code: String, notificationType: UserNotificationType) -> MobileMessaging? {
return MobileMessaging.withApplicationCode(code, notificationType: notificationType, backendBaseURL: APIValues.prodBaseURLString)
return MobileMessaging.withApplicationCode(code, notificationType: notificationType, backendBaseURL: APIValues.prodDynamicBaseURLString)
}

/// Fabric method for Mobile Messaging session.
Expand All @@ -25,13 +25,13 @@ public final class MobileMessaging: NSObject {
/// - parameter forceCleanup: Defines whether the SDK must be cleaned up on startup.
/// - warning: The cleanup (parameter `forceCleanup = true`) must be performed manually if you changed the application code while `PrivacySettings.applicationCodePersistingDisabled` is set to `true`.
public class func withApplicationCode(_ code: String, notificationType: UserNotificationType, forceCleanup: Bool) -> MobileMessaging? {
return MobileMessaging.withApplicationCode(code, notificationType: notificationType, backendBaseURL: APIValues.prodBaseURLString, forceCleanup: forceCleanup)
return MobileMessaging.withApplicationCode(code, notificationType: notificationType, backendBaseURL: APIValues.prodDynamicBaseURLString, forceCleanup: forceCleanup)
}

/// Fabric method for Mobile Messaging session.
/// - parameter notificationType: Preferable notification types that indicating how the app alerts the user when a push notification arrives.
/// - parameter code: The application code of your Application from Push Portal website.
/// - parameter backendBaseURL: Your backend server base URL, optional parameter. Default is http://oneapi.infobip.com.
/// - parameter backendBaseURL: Your backend server base URL, optional parameter. Default is https://oneapi.infobip.com.
public class func withApplicationCode(_ code: String, notificationType: UserNotificationType, backendBaseURL: String) -> MobileMessaging? {
return MobileMessaging.withApplicationCode(code, notificationType: notificationType, backendBaseURL: backendBaseURL, forceCleanup: false)
}
Expand Down Expand Up @@ -278,7 +278,7 @@ public final class MobileMessaging: NSObject {
currentUser = nil
appListener = nil
messageHandler = nil
remoteApiManager = nil
remoteApiProvider = nil
reachabilityManager = nil
keychain = nil
sharedNotificationExtensionStorage = nil
Expand Down Expand Up @@ -375,6 +375,7 @@ public final class MobileMessaging: NSObject {
self.applicationCode = appCode
self.userNotificationType = notificationType
self.remoteAPIBaseURL = backendBaseURL
self.httpSessionManager = DynamicBaseUrlHTTPSessionManager(applicationCode: appCode, baseURL: URL(string: backendBaseURL), sessionConfiguration: MobileMessaging.urlSessionConfiguration, appGroupId: appGroupId)

MMLogInfo("SDK successfully initialized!")
}
Expand Down Expand Up @@ -424,7 +425,7 @@ public final class MobileMessaging: NSObject {

if !isTestingProcessRunning {
#if DEBUG
VersionManager(remoteApiManager: self.remoteApiManager).validateVersion()
VersionManager(remoteApiProvider: self.remoteApiProvider).validateVersion()
#endif
}
}
Expand All @@ -444,15 +445,18 @@ public final class MobileMessaging: NSObject {
var appListener: MMApplicationListener!
//TODO: continue decoupling. Move messageHandler to a subservice completely. (as GeofencingService)
lazy var messageHandler: MMMessageHandler! = MMMessageHandler(storage: self.internalStorage, mmContext: self)
lazy var remoteApiManager: RemoteAPIManager! = RemoteAPIManager(baseUrl: self.remoteAPIBaseURL, applicationCode: self.applicationCode, mmContext: self)
lazy var remoteApiProvider: RemoteAPIProvider! = RemoteAPIProvider(mmContext: self)
lazy var application: MMApplication! = UIApplication.shared
lazy var reachabilityManager: ReachabilityManagerProtocol! = MMNetworkReachabilityManager.sharedInstance
lazy var keychain: MMKeychain! = MMKeychain()

static var date: MMDate = MMDate() // testability

var appGroupId: String?
var sharedNotificationExtensionStorage: AppGroupMessageStorage?
lazy var userNotificationCenterStorage: UserNotificationCenterStorage = DefaultUserNotificationCenterStorage()

var httpSessionManager: DynamicBaseUrlHTTPSessionManager
}

extension UIApplication: MMApplication {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ final class MessageFetchingOperation: Operation {

MMLogDebug("[Message fetching] Found \(String(describing: nonReportedMessageIds?.count)) not reported messages. \(String(describing: archveMessageIds?.count)) archive messages.")

self.mmContext.remoteApiManager.syncMessages(archiveMsgIds: archveMessageIds, dlrMsgIds: nonReportedMessageIds) { result in
self.mmContext.remoteApiProvider.syncMessages(archiveMsgIds: archveMessageIds, dlrMsgIds: nonReportedMessageIds) { result in
self.result = result
self.handleRequestResponse(result: result, nonReportedMessageIds: nonReportedMessageIds) {
self.finish()
Expand Down
Loading

0 comments on commit f6e59e0

Please sign in to comment.