diff --git a/app/build.gradle b/app/build.gradle index 2727513c69..b40169405d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -338,7 +338,7 @@ dependencies { def dnsjava_version = "2.1.9" def openpgp_version = "12.0" def badge_version = "1.1.22" - def bugsnag_version = "5.14.0" + def bugsnag_version = "5.19.2" def biweekly_version = "0.6.6" def vcard_version = "0.11.3" def relinker_version = "1.4.3" diff --git a/app/src/main/java/com/bugsnag/android/Breadcrumb.java b/app/src/main/java/com/bugsnag/android/Breadcrumb.java index 79e2ef1538..7e6114e464 100644 --- a/app/src/main/java/com/bugsnag/android/Breadcrumb.java +++ b/app/src/main/java/com/bugsnag/android/Breadcrumb.java @@ -16,6 +16,11 @@ public class Breadcrumb implements JsonStream.Streamable { final BreadcrumbInternal impl; private final Logger logger; + Breadcrumb(@NonNull BreadcrumbInternal impl, @NonNull Logger logger) { + this.impl = impl; + this.logger = logger; + } + Breadcrumb(@NonNull String message, @NonNull Logger logger) { this.impl = new BreadcrumbInternal(message); this.logger = logger; diff --git a/app/src/main/java/com/bugsnag/android/BreadcrumbState.kt b/app/src/main/java/com/bugsnag/android/BreadcrumbState.kt index 20192ebea9..e2d392b6b8 100644 --- a/app/src/main/java/com/bugsnag/android/BreadcrumbState.kt +++ b/app/src/main/java/com/bugsnag/android/BreadcrumbState.kt @@ -1,6 +1,5 @@ package com.bugsnag.android -import com.bugsnag.android.internal.DateUtils import java.io.IOException import java.util.concurrent.atomic.AtomicInteger @@ -43,7 +42,8 @@ internal class BreadcrumbState( StateEvent.AddBreadcrumb( breadcrumb.impl.message, breadcrumb.impl.type, - DateUtils.toIso8601(breadcrumb.impl.timestamp), + // an encoding of milliseconds since the epoch + "t${breadcrumb.impl.timestamp.time}", breadcrumb.impl.metadata ?: mutableMapOf() ) } diff --git a/app/src/main/java/com/bugsnag/android/BreadcrumbType.kt b/app/src/main/java/com/bugsnag/android/BreadcrumbType.kt index f1c05910a4..10ef85de12 100644 --- a/app/src/main/java/com/bugsnag/android/BreadcrumbType.kt +++ b/app/src/main/java/com/bugsnag/android/BreadcrumbType.kt @@ -38,4 +38,8 @@ enum class BreadcrumbType(private val type: String) { USER("user"); override fun toString() = type + + internal companion object { + internal fun fromDescriptor(type: String) = values().singleOrNull { it.type == type } + } } diff --git a/app/src/main/java/com/bugsnag/android/Bugsnag.java b/app/src/main/java/com/bugsnag/android/Bugsnag.java index 74926ecd8b..f32b49d6ae 100644 --- a/app/src/main/java/com/bugsnag/android/Bugsnag.java +++ b/app/src/main/java/com/bugsnag/android/Bugsnag.java @@ -6,7 +6,6 @@ import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; @@ -404,6 +403,59 @@ public final class Bugsnag { getClient().markLaunchCompleted(); } + /** + * Add a single feature flag with no variant. If there is an existing feature flag with the + * same name, it will be overwritten to have no variant. + * + * @param name the name of the feature flag to add + * @see #addFeatureFlag(String, String) + */ + public static void addFeatureFlag(@NonNull String name) { + getClient().addFeatureFlag(name); + } + + /** + * Add a single feature flag with an optional variant. If there is an existing feature + * flag with the same name, it will be overwritten with the new variant. If the variant is + * {@code null} this method has the same behaviour as {@link #addFeatureFlag(String)}. + * + * @param name the name of the feature flag to add + * @param variant the variant to set the feature flag to, or {@code null} to specify a feature + * flag with no variant + */ + public static void addFeatureFlag(@NonNull String name, @Nullable String variant) { + getClient().addFeatureFlag(name, variant); + } + + /** + * Add a collection of feature flags. This method behaves exactly the same as calling + * {@link #addFeatureFlag(String, String)} for each of the {@code FeatureFlag} objects. + * + * @param featureFlags the feature flags to add + * @see #addFeatureFlag(String, String) + */ + public static void addFeatureFlags(@NonNull Iterable featureFlags) { + getClient().addFeatureFlags(featureFlags); + } + + /** + * Remove a single feature flag regardless of its current status. This will stop the specified + * feature flag from being reported. If the named feature flag does not exist this will + * have no effect. + * + * @param name the name of the feature flag to remove + */ + public static void clearFeatureFlag(@NonNull String name) { + getClient().clearFeatureFlag(name); + } + + /** + * Clear all of the feature flags. This will stop all feature flags from being reported. + */ + public static void clearFeatureFlags() { + getClient().clearFeatureFlags(); + } + /** * Get the current Bugsnag Client instance. */ diff --git a/app/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt b/app/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt new file mode 100644 index 0000000000..8f17f4049d --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt @@ -0,0 +1,264 @@ +package com.bugsnag.android + +import com.bugsnag.android.internal.DateUtils +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone + +internal class BugsnagEventMapper( + private val logger: Logger +) { + + @Suppress("UNCHECKED_CAST") + internal fun convertToEventImpl(map: Map, apiKey: String): EventInternal { + val event = EventInternal(apiKey) + + // populate exceptions. check this early to avoid unnecessary serialization if + // no stacktrace was gathered. + val exceptions = map["exceptions"] as? List> + exceptions?.mapTo(event.errors) { Error(convertErrorInternal(it), this.logger) } + + // populate user + event.userImpl = convertUser(map.readEntry("user")) + + // populate metadata + val metadataMap: Map> = map.readEntry("metaData") + metadataMap.forEach { (key, value) -> + event.addMetadata(key, value) + } + + val featureFlagsList: List> = map.readEntry("featureFlags") + featureFlagsList.forEach { featureFlagMap -> + event.addFeatureFlag( + featureFlagMap.readEntry("featureFlag"), + featureFlagMap["variant"] as? String + ) + } + + // populate breadcrumbs + val breadcrumbList: List> = map.readEntry("breadcrumbs") + breadcrumbList.mapTo(event.breadcrumbs) { + Breadcrumb( + convertBreadcrumbInternal(it), + logger + ) + } + + // populate context + event.context = map["context"] as? String + + // populate groupingHash + event.groupingHash = map["groupingHash"] as? String + + // populate app + event.app = convertAppWithState(map.readEntry("app")) + + // populate device + event.device = convertDeviceWithState(map.readEntry("device")) + + // populate session + val sessionMap = map["session"] as? Map + sessionMap?.let { + event.session = Session(it, logger) + } + + // populate threads + val threads = map["threads"] as? List> + threads?.mapTo(event.threads) { Thread(convertThread(it), logger) } + + // populate projectPackages + val projectPackages = map["projectPackages"] as? List + projectPackages?.let { + event.projectPackages = projectPackages + } + + // populate severity + val severityStr: String = map.readEntry("severity") + val severity = Severity.fromDescriptor(severityStr) + val unhandled: Boolean = map.readEntry("unhandled") + val reason = deserializeSeverityReason(map, unhandled, severity) + event.updateSeverityReasonInternal(reason) + event.normalizeStackframeErrorTypes() + + return event + } + + internal fun convertErrorInternal(error: Map): ErrorInternal { + return ErrorInternal( + error.readEntry("errorClass"), + error["message"] as? String, + type = error.readEntry("type").let { type -> + ErrorType.fromDescriptor(type) + ?: throw IllegalArgumentException("unknown ErrorType: '$type'") + }, + stacktrace = convertStacktrace(error.readEntry("stacktrace")) + ) + } + + internal fun convertUser(user: Map): User { + return User( + user["id"] as? String, + user["email"] as? String, + user["name"] as? String + ) + } + + @Suppress("UNCHECKED_CAST") + internal fun convertBreadcrumbInternal(breadcrumb: Map): BreadcrumbInternal { + return BreadcrumbInternal( + breadcrumb.readEntry("name"), + breadcrumb.readEntry("type").let { type -> + BreadcrumbType.fromDescriptor(type) + ?: BreadcrumbType.MANUAL + }, + breadcrumb["metaData"] as? MutableMap, + breadcrumb.readEntry("timestamp").toDate() + ) + } + + internal fun convertAppWithState(app: Map): AppWithState { + return AppWithState( + app["binaryArch"] as? String, + app["id"] as? String, + app["releaseStage"] as? String, + app["version"] as? String, + app["codeBundleId"] as? String, + app["buildUUID"] as? String, + app["type"] as? String, + (app["versionCode"] as? Number)?.toInt(), + (app["duration"] as? Number)?.toLong(), + (app["durationInForeground"] as? Number)?.toLong(), + app["inForeground"] as? Boolean, + app["isLaunching"] as? Boolean + ) + } + + @Suppress("UNCHECKED_CAST") + internal fun convertDeviceWithState(device: Map): DeviceWithState { + return DeviceWithState( + DeviceBuildInfo( + device["manufacturer"] as? String, + device["model"] as? String, + device["osVersion"] as? String, + null, + null, + null, + null, + null, + (device["cpuAbi"] as? List)?.toTypedArray() + ), + device["jailbroken"] as? Boolean, + device["id"] as? String, + device["locale"] as? String, + (device["totalMemory"] as? Number)?.toLong(), + (device["runtimeVersions"] as? Map)?.toMutableMap() + ?: mutableMapOf(), + (device["freeDisk"] as? Number)?.toLong(), + (device["freeMemory"] as? Number)?.toLong(), + device["orientation"] as? String, + (device["time"] as? String)?.toDate() + ) + } + + @Suppress("UNCHECKED_CAST") + internal fun convertThread(thread: Map): ThreadInternal { + return ThreadInternal( + (thread["id"] as? Number)?.toLong() ?: 0, + thread.readEntry("name"), + ThreadType.fromDescriptor(thread.readEntry("type")) ?: ThreadType.ANDROID, + thread["errorReportingThread"] == true, + thread.readEntry("state"), + (thread["stacktrace"] as? List>)?.let { convertStacktrace(it) } + ?: Stacktrace(emptyList()) + ) + } + + internal fun convertStacktrace(trace: List>): Stacktrace { + return Stacktrace(trace.map { convertStackframe(it) }) + } + + internal fun convertStackframe(frame: Map): Stackframe { + val copy: MutableMap = frame.toMutableMap() + val lineNumber = frame["lineNumber"] as? Number + copy["lineNumber"] = lineNumber?.toLong() + + (frame["frameAddress"] as? String)?.let { + copy["frameAddress"] = java.lang.Long.decode(it) + } + + (frame["symbolAddress"] as? String)?.let { + copy["symbolAddress"] = java.lang.Long.decode(it) + } + + (frame["loadAddress"] as? String)?.let { + copy["loadAddress"] = java.lang.Long.decode(it) + } + + (frame["isPC"] as? Boolean)?.let { + copy["isPC"] = it + } + + return Stackframe(copy) + } + + internal fun deserializeSeverityReason( + map: Map, + unhandled: Boolean, + severity: Severity? + ): SeverityReason { + val severityReason: Map = map.readEntry("severityReason") + val unhandledOverridden: Boolean = + severityReason.readEntry("unhandledOverridden") + val type: String = severityReason.readEntry("type") + val originalUnhandled = when { + unhandledOverridden -> !unhandled + else -> unhandled + } + + val attrMap: Map? = severityReason.readEntry("attributes") + val entry = attrMap?.entries?.singleOrNull() + return SeverityReason( + type, + severity, + unhandled, + originalUnhandled, + entry?.value, + entry?.key + ) + } + + /** + * Convenience method for getting an entry from a Map in the expected type, which + * throws useful error messages if the expected type is not there. + */ + private inline fun Map<*, *>.readEntry(key: String): T { + when (val value = get(key)) { + is T -> return value + null -> throw IllegalStateException("cannot find json property '$key'") + else -> throw IllegalArgumentException( + "json property '$key' not " + + "of expected type, found ${value.javaClass.name}" + ) + } + } + + // SimpleDateFormat isn't thread safe, cache one instance per thread as needed. + private val ndkDateFormatHolder = object : ThreadLocal() { + override fun initialValue(): DateFormat { + return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { + timeZone = TimeZone.getTimeZone("UTC") + } + } + } + + private fun String.toDate(): Date { + return try { + DateUtils.fromIso8601(this) + } catch (pe: IllegalArgumentException) { + ndkDateFormatHolder.get()!!.parse(this) + ?: throw IllegalArgumentException("cannot parse date $this") + } + } +} diff --git a/app/src/main/java/com/bugsnag/android/BugsnagStateModule.kt b/app/src/main/java/com/bugsnag/android/BugsnagStateModule.kt index 50377ad59c..ae8db30b52 100644 --- a/app/src/main/java/com/bugsnag/android/BugsnagStateModule.kt +++ b/app/src/main/java/com/bugsnag/android/BugsnagStateModule.kt @@ -28,6 +28,8 @@ internal class BugsnagStateModule( val metadataState = copyMetadataState(configuration) + val featureFlagState = configuration.impl.featureFlagState.copy() + private fun copyMetadataState(configuration: Configuration): MetadataState { // performs deep copy of metadata to preserve immutability of Configuration interface val orig = configuration.impl.metadataState.metadata diff --git a/app/src/main/java/com/bugsnag/android/CallbackState.kt b/app/src/main/java/com/bugsnag/android/CallbackState.kt index 734d26b18d..39218f7d5c 100644 --- a/app/src/main/java/com/bugsnag/android/CallbackState.kt +++ b/app/src/main/java/com/bugsnag/android/CallbackState.kt @@ -1,11 +1,12 @@ package com.bugsnag.android -import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.CopyOnWriteArrayList internal data class CallbackState( - val onErrorTasks: MutableCollection = ConcurrentLinkedQueue(), - val onBreadcrumbTasks: MutableCollection = ConcurrentLinkedQueue(), - val onSessionTasks: MutableCollection = ConcurrentLinkedQueue() + val onErrorTasks: MutableCollection = CopyOnWriteArrayList(), + val onBreadcrumbTasks: MutableCollection = CopyOnWriteArrayList(), + val onSessionTasks: MutableCollection = CopyOnWriteArrayList(), + val onSendTasks: MutableCollection = CopyOnWriteArrayList() ) : CallbackAware { override fun addOnError(onError: OnErrorCallback) { @@ -32,6 +33,14 @@ internal data class CallbackState( onSessionTasks.remove(onSession) } + fun addOnSend(onSend: OnSendCallback) { + onSendTasks.add(onSend) + } + + fun removeOnSend(onSend: OnSendCallback) { + onSendTasks.remove(onSend) + } + fun runOnErrorTasks(event: Event, logger: Logger): Boolean { // optimization to avoid construction of iterator when no callbacks set if (onErrorTasks.isEmpty()) { @@ -83,9 +92,32 @@ internal data class CallbackState( return true } + fun runOnSendTasks(event: Event, logger: Logger): Boolean { + onSendTasks.forEach { + try { + if (!it.onSend(event)) { + return false + } + } catch (ex: Throwable) { + logger.w("OnSendCallback threw an Exception", ex) + } + } + return true + } + + fun runOnSendTasks(eventSource: () -> Event, logger: Logger): Boolean { + if (onSendTasks.isEmpty()) { + // avoid constructing event from eventSource if not needed + return true + } + + return this.runOnSendTasks(eventSource(), logger) + } + fun copy() = this.copy( onErrorTasks = onErrorTasks, onBreadcrumbTasks = onBreadcrumbTasks, - onSessionTasks = onSessionTasks + onSessionTasks = onSessionTasks, + onSendTasks = onSendTasks ) } diff --git a/app/src/main/java/com/bugsnag/android/Client.java b/app/src/main/java/com/bugsnag/android/Client.java index e31df68f9f..a6cdf8d932 100644 --- a/app/src/main/java/com/bugsnag/android/Client.java +++ b/app/src/main/java/com/bugsnag/android/Client.java @@ -20,6 +20,7 @@ import kotlin.jvm.functions.Function1; import kotlin.jvm.functions.Function2; import java.io.File; +import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -41,11 +42,12 @@ import java.util.concurrent.RejectedExecutionException; * @see Bugsnag */ @SuppressWarnings({"checkstyle:JavadocTagContinuationIndentation", "ConstantConditions"}) -public class Client implements MetadataAware, CallbackAware, UserAware { +public class Client implements MetadataAware, CallbackAware, UserAware, FeatureFlagAware { final ImmutableConfig immutableConfig; final MetadataState metadataState; + final FeatureFlagState featureFlagState; private final ContextState contextState; private final CallbackState callbackState; @@ -152,6 +154,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware { breadcrumbState = bugsnagStateModule.getBreadcrumbState(); contextState = bugsnagStateModule.getContextState(); metadataState = bugsnagStateModule.getMetadataState(); + featureFlagState = bugsnagStateModule.getFeatureFlagState(); // lookup system services final SystemServiceModule systemServiceModule = new SystemServiceModule(contextModule); @@ -179,12 +182,13 @@ public class Client implements MetadataAware, CallbackAware, UserAware { registerLifecycleCallbacks(); EventStorageModule eventStorageModule = new EventStorageModule(contextModule, configModule, - dataCollectionModule, bgTaskService, trackerModule, systemServiceModule, notifier); + dataCollectionModule, bgTaskService, trackerModule, systemServiceModule, notifier, + callbackState); eventStorageModule.resolveDependencies(bgTaskService, TaskType.IO); eventStore = eventStorageModule.getEventStore(); deliveryDelegate = new DeliveryDelegate(logger, eventStore, - immutableConfig, breadcrumbState, notifier, bgTaskService); + immutableConfig, callbackState, notifier, bgTaskService); // Install a default exception handler with this client exceptionHandler = new ExceptionHandler(this, logger); @@ -222,6 +226,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware { ContextState contextState, CallbackState callbackState, UserState userState, + FeatureFlagState featureFlagState, ClientObservable clientObservable, Context appContext, @NonNull DeviceDataCollector deviceDataCollector, @@ -243,6 +248,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware { this.contextState = contextState; this.callbackState = callbackState; this.userState = userState; + this.featureFlagState = featureFlagState; this.clientObservable = clientObservable; this.appContext = appContext; this.deviceDataCollector = deviceDataCollector; @@ -402,6 +408,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware { deliveryDelegate.addObserver(observer); launchCrashTracker.addObserver(observer); memoryTrimState.addObserver(observer); + featureFlagState.addObserver(observer); } void removeObserver(StateObserver observer) { @@ -414,6 +421,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware { deliveryDelegate.removeObserver(observer); launchCrashTracker.removeObserver(observer); memoryTrimState.removeObserver(observer); + featureFlagState.removeObserver(observer); } /** @@ -424,6 +432,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware { contextState.emitObservableEvent(); userState.emitObservableEvent(); memoryTrimState.emitObservableEvent(); + featureFlagState.emitObservableEvent(); } /** @@ -677,7 +686,9 @@ public class Client implements MetadataAware, CallbackAware, UserAware { } SeverityReason severityReason = SeverityReason.newInstance(REASON_HANDLED_EXCEPTION); Metadata metadata = metadataState.getMetadata(); - Event event = new Event(exc, immutableConfig, severityReason, metadata, logger); + FeatureFlags featureFlags = featureFlagState.getFeatureFlags(); + Event event = new Event(exc, immutableConfig, severityReason, metadata, featureFlags, + logger); populateAndNotifyAndroidEvent(event, onError); } else { logNull("notify"); @@ -695,7 +706,8 @@ public class Client implements MetadataAware, CallbackAware, UserAware { SeverityReason handledState = SeverityReason.newInstance(severityReason, Severity.ERROR, attributeValue); Metadata data = Metadata.Companion.merge(metadataState.getMetadata(), metadata); - Event event = new Event(exc, immutableConfig, handledState, data, logger); + Event event = new Event(exc, immutableConfig, handledState, + data, featureFlagState.getFeatureFlags(), logger); populateAndNotifyAndroidEvent(event, null); // persist LastRunInfo so that on relaunch users can check the app crashed @@ -740,9 +752,8 @@ public class Client implements MetadataAware, CallbackAware, UserAware { @Nullable OnErrorCallback onError) { // set the redacted keys on the event as this // will not have been set for RN/Unity events - Set redactedKeys = metadataState.getMetadata().getRedactedKeys(); - Metadata eventMetadata = event.getImpl().getMetadata(); - eventMetadata.setRedactedKeys(redactedKeys); + Collection redactedKeys = metadataState.getMetadata().getRedactedKeys(); + event.setRedactedKeys(redactedKeys); // get session for event Session currentSession = sessionTracker.getCurrentSession(); @@ -754,11 +765,15 @@ public class Client implements MetadataAware, CallbackAware, UserAware { // Run on error tasks, don't notify if any return false if (!callbackState.runOnErrorTasks(event, logger) - || (onError != null && !onError.onError(event))) { + || (onError != null + && !onError.onError(event))) { logger.d("Skipping notification - onError task returned false"); return; } + // leave an error breadcrumb of this event - for the next event + leaveErrorBreadcrumb(event); + deliveryDelegate.deliver(event); } @@ -922,6 +937,80 @@ public class Client implements MetadataAware, CallbackAware, UserAware { } } + private void leaveErrorBreadcrumb(@NonNull Event event) { + // Add a breadcrumb for this event occurring + List errors = event.getErrors(); + + if (errors.size() > 0) { + String errorClass = errors.get(0).getErrorClass(); + String message = errors.get(0).getErrorMessage(); + + Map data = new HashMap<>(); + data.put("errorClass", errorClass); + data.put("message", message); + data.put("unhandled", String.valueOf(event.isUnhandled())); + data.put("severity", event.getSeverity().toString()); + breadcrumbState.add(new Breadcrumb(errorClass, + BreadcrumbType.ERROR, data, new Date(), logger)); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void addFeatureFlag(@NonNull String name) { + if (name != null) { + featureFlagState.addFeatureFlag(name); + } else { + logNull("addFeatureFlag"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void addFeatureFlag(@NonNull String name, @Nullable String variant) { + if (name != null) { + featureFlagState.addFeatureFlag(name, variant); + } else { + logNull("addFeatureFlag"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void addFeatureFlags(@NonNull Iterable featureFlags) { + if (featureFlags != null) { + featureFlagState.addFeatureFlags(featureFlags); + } else { + logNull("addFeatureFlags"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void clearFeatureFlag(@NonNull String name) { + if (name != null) { + featureFlagState.clearFeatureFlag(name); + } else { + logNull("clearFeatureFlag"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void clearFeatureFlags() { + featureFlagState.clearFeatureFlags(); + } + /** * Retrieves information about the last launch of the application, if it has been run before. * @@ -976,8 +1065,15 @@ public class Client implements MetadataAware, CallbackAware, UserAware { private void warnIfNotAppContext(Context androidContext) { if (!(androidContext instanceof Application)) { - logger.w("Warning - Non-Application context detected! Please ensure that you are " - + "initializing Bugsnag from a custom Application class."); + logger.w("You should initialize Bugsnag from the onCreate() callback of your " + + "Application subclass, as this guarantees errors are captured as early " + + "as possible. " + + "If a custom Application subclass is not possible in your app then you " + + "should suppress this warning by passing the Application context instead: " + + "Bugsnag.start(context.getApplicationContext()). " + + "For further info see: " + + "https://docs.bugsnag.com/platforms/android/#basic-configuration"); + } } @@ -1039,6 +1135,10 @@ public class Client implements MetadataAware, CallbackAware, UserAware { return metadataState; } + FeatureFlagState getFeatureFlagState() { + return featureFlagState; + } + ContextState getContextState() { return contextState; } diff --git a/app/src/main/java/com/bugsnag/android/ConfigInternal.kt b/app/src/main/java/com/bugsnag/android/ConfigInternal.kt index 15e4526236..f3ec4939f9 100644 --- a/app/src/main/java/com/bugsnag/android/ConfigInternal.kt +++ b/app/src/main/java/com/bugsnag/android/ConfigInternal.kt @@ -3,7 +3,9 @@ package com.bugsnag.android import android.content.Context import java.io.File -internal class ConfigInternal(var apiKey: String) : CallbackAware, MetadataAware, UserAware { +internal class ConfigInternal( + var apiKey: String +) : CallbackAware, MetadataAware, UserAware, FeatureFlagAware { private var user = User() @@ -13,6 +15,9 @@ internal class ConfigInternal(var apiKey: String) : CallbackAware, MetadataAware @JvmField internal val metadataState: MetadataState = MetadataState() + @JvmField + internal val featureFlagState: FeatureFlagState = FeatureFlagState() + var appVersion: String? = null var versionCode: Int? = 0 var releaseStage: String? = null @@ -61,6 +66,8 @@ internal class ConfigInternal(var apiKey: String) : CallbackAware, MetadataAware callbackState.removeOnBreadcrumb(onBreadcrumb) override fun addOnSession(onSession: OnSessionCallback) = callbackState.addOnSession(onSession) override fun removeOnSession(onSession: OnSessionCallback) = callbackState.removeOnSession(onSession) + fun addOnSend(onSend: OnSendCallback) = callbackState.addOnSend(onSend) + fun removeOnSend(onSend: OnSendCallback) = callbackState.removeOnSend(onSend) override fun addMetadata(section: String, value: Map) = metadataState.addMetadata(section, value) @@ -71,6 +78,14 @@ internal class ConfigInternal(var apiKey: String) : CallbackAware, MetadataAware override fun getMetadata(section: String) = metadataState.getMetadata(section) override fun getMetadata(section: String, key: String) = metadataState.getMetadata(section, key) + override fun addFeatureFlag(name: String) = featureFlagState.addFeatureFlag(name) + override fun addFeatureFlag(name: String, variant: String?) = + featureFlagState.addFeatureFlag(name, variant) + override fun addFeatureFlags(featureFlags: Iterable) = + featureFlagState.addFeatureFlags(featureFlags) + override fun clearFeatureFlag(name: String) = featureFlagState.clearFeatureFlag(name) + override fun clearFeatureFlags() = featureFlagState.clearFeatureFlags() + override fun getUser(): User = user override fun setUser(id: String?, email: String?, name: String?) { user = User(id, email, name) diff --git a/app/src/main/java/com/bugsnag/android/Configuration.java b/app/src/main/java/com/bugsnag/android/Configuration.java index 13c2daef93..3b9e6dc2a2 100644 --- a/app/src/main/java/com/bugsnag/android/Configuration.java +++ b/app/src/main/java/com/bugsnag/android/Configuration.java @@ -16,7 +16,7 @@ import java.util.Set; * specified at the client level, api-key and endpoint configuration. */ @SuppressWarnings("ConstantConditions") // suppress warning about making redundant null checks -public class Configuration implements CallbackAware, MetadataAware, UserAware { +public class Configuration implements CallbackAware, MetadataAware, UserAware, FeatureFlagAware { private static final int MIN_BREADCRUMBS = 0; private static final int MAX_BREADCRUMBS = 100; @@ -873,6 +873,38 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware { } } + /** + * Add a callback which will be invoked prior to an event being delivered + * to Bugsnag. The callback can be used to modify events or cancel + * delivering the event altogether by returning false. Note + * that the callback may be invoked in the current or a subsequent app + * launch depending on whether the app terminated prior to delivering the + * event. + * + * @param onSend the callback to add + * @see OnSendCallback + */ + public void addOnSend(@NonNull OnSendCallback onSend) { + if (onSend != null) { + impl.addOnSend(onSend); + } else { + logNull("addOnSend"); + } + } + + /** + * Remove a callback previously added with {@link Configuration#addOnSend(OnSendCallback)} + * + * @param onSend the callback to remove + */ + public void removeOnSend(@NonNull OnSendCallback onSend) { + if (onSend != null) { + impl.removeOnSend(onSend); + } else { + logNull("removeOnSend"); + } + } + /** * Adds a map of multiple metadata key-value pairs to the specified section. */ @@ -950,6 +982,62 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware { } } + /** + * {@inheritDoc} + */ + @Override + public void addFeatureFlag(@NonNull String name) { + if (name != null) { + impl.addFeatureFlag(name); + } else { + logNull("addFeatureFlag"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void addFeatureFlag(@NonNull String name, @Nullable String variant) { + if (name != null) { + impl.addFeatureFlag(name, variant); + } else { + logNull("addFeatureFlag"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void addFeatureFlags(@NonNull Iterable featureFlags) { + if (featureFlags != null) { + impl.addFeatureFlags(featureFlags); + } else { + logNull("addFeatureFlags"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void clearFeatureFlag(@NonNull String name) { + if (name != null) { + impl.clearFeatureFlag(name); + } else { + logNull("clearFeatureFlag"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void clearFeatureFlags() { + impl.clearFeatureFlags(); + } + /** * Returns the currently set User information. */ diff --git a/app/src/main/java/com/bugsnag/android/ConnectivityCompat.kt b/app/src/main/java/com/bugsnag/android/ConnectivityCompat.kt index ad34e0b423..97e87553c1 100644 --- a/app/src/main/java/com/bugsnag/android/ConnectivityCompat.kt +++ b/app/src/main/java/com/bugsnag/android/ConnectivityCompat.kt @@ -9,6 +9,9 @@ import android.net.Network import android.net.NetworkCapabilities import android.os.Build import androidx.annotation.RequiresApi +import androidx.annotation.VisibleForTesting +import com.bugsnag.android.UnknownConnectivity.retrieveNetworkAccessState +import java.util.concurrent.atomic.AtomicBoolean internal typealias NetworkChangeCallback = (hasConnection: Boolean, networkState: String) -> Unit @@ -89,10 +92,16 @@ internal class ConnectivityLegacy( } } - private inner class ConnectivityChangeReceiver(private val cb: NetworkChangeCallback?) : - BroadcastReceiver() { + private inner class ConnectivityChangeReceiver( + private val cb: NetworkChangeCallback? + ) : BroadcastReceiver() { + + private val receivedFirstCallback = AtomicBoolean(false) + override fun onReceive(context: Context, intent: Intent) { - cb?.invoke(hasNetworkConnection(), retrieveNetworkAccessState()) + if (receivedFirstCallback.getAndSet(true)) { + cb?.invoke(hasNetworkConnection(), retrieveNetworkAccessState()) + } } } } @@ -122,22 +131,38 @@ internal class ConnectivityApi24( } } - private inner class ConnectivityTrackerCallback(private val cb: NetworkChangeCallback?) : - ConnectivityManager.NetworkCallback() { + @VisibleForTesting + internal class ConnectivityTrackerCallback( + private val cb: NetworkChangeCallback? + ) : ConnectivityManager.NetworkCallback() { + + private val receivedFirstCallback = AtomicBoolean(false) + override fun onUnavailable() { super.onUnavailable() - cb?.invoke(false, retrieveNetworkAccessState()) + invokeNetworkCallback(false) } override fun onAvailable(network: Network) { super.onAvailable(network) - cb?.invoke(true, retrieveNetworkAccessState()) + invokeNetworkCallback(true) + } + + /** + * Invokes the network callback, as long as the ConnectivityManager callback has been + * triggered at least once before (when setting a NetworkCallback Android always + * invokes the callback with the current network state). + */ + private fun invokeNetworkCallback(hasConnection: Boolean) { + if (receivedFirstCallback.getAndSet(true)) { + cb?.invoke(hasConnection, retrieveNetworkAccessState()) + } } } } /** - * Connectivity used in cases where we cannot access the system ConnectivityManager. + * Connectivity used in cases where we cannot access the system ConnectivityManager. * We assume that there is some sort of network and do not attempt to report any network changes. */ internal object UnknownConnectivity : Connectivity { diff --git a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt index 2e2f2d2fd4..e1bac196e2 100644 --- a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt +++ b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt @@ -105,32 +105,38 @@ internal class DefaultDelivery( } private fun logRequestInfo(code: Int, conn: HttpURLConnection, status: DeliveryStatus) { - logger.i( - "Request completed with code $code, " + - "message: ${conn.responseMessage}, " + - "headers: ${conn.headerFields}" - ) - - conn.inputStream.bufferedReader().use { - logger.d("Received request response: ${it.readText()}") + runCatching { + logger.i( + "Request completed with code $code, " + + "message: ${conn.responseMessage}, " + + "headers: ${conn.headerFields}" + ) + } + runCatching { + conn.inputStream.bufferedReader().use { + logger.d("Received request response: ${it.readText()}") + } } - if (status != DeliveryStatus.DELIVERED) { - conn.errorStream.bufferedReader().use { - logger.w("Request error details: ${it.readText()}") + runCatching { + if (status != DeliveryStatus.DELIVERED) { + conn.errorStream.bufferedReader().use { + logger.w("Request error details: ${it.readText()}") + } } } } internal fun getDeliveryStatus(responseCode: Int): DeliveryStatus { - val unrecoverableCodes = IntRange(HTTP_BAD_REQUEST, 499).filter { - it != HTTP_CLIENT_TIMEOUT && it != 429 - } - - return when (responseCode) { - in HTTP_OK..299 -> DeliveryStatus.DELIVERED - in unrecoverableCodes -> DeliveryStatus.FAILURE + return when { + responseCode in HTTP_OK..299 -> DeliveryStatus.DELIVERED + isUnrecoverableStatusCode(responseCode) -> DeliveryStatus.FAILURE else -> DeliveryStatus.UNDELIVERED } } + + private fun isUnrecoverableStatusCode(responseCode: Int) = + responseCode in HTTP_BAD_REQUEST..499 && // 400-499 are considered unrecoverable + responseCode != HTTP_CLIENT_TIMEOUT && // except for 408 + responseCode != 429 // and 429 } diff --git a/app/src/main/java/com/bugsnag/android/DeliveryDelegate.java b/app/src/main/java/com/bugsnag/android/DeliveryDelegate.java index bf2e5bf95a..d9dd1aa168 100644 --- a/app/src/main/java/com/bugsnag/android/DeliveryDelegate.java +++ b/app/src/main/java/com/bugsnag/android/DeliveryDelegate.java @@ -18,29 +18,26 @@ class DeliveryDelegate extends BaseObservable { final Logger logger; private final EventStore eventStore; private final ImmutableConfig immutableConfig; - final BreadcrumbState breadcrumbState; private final Notifier notifier; + private final CallbackState callbackState; final BackgroundTaskService backgroundTaskService; DeliveryDelegate(Logger logger, EventStore eventStore, ImmutableConfig immutableConfig, - BreadcrumbState breadcrumbState, + CallbackState callbackState, Notifier notifier, BackgroundTaskService backgroundTaskService) { this.logger = logger; this.eventStore = eventStore; this.immutableConfig = immutableConfig; - this.breadcrumbState = breadcrumbState; + this.callbackState = callbackState; this.notifier = notifier; this.backgroundTaskService = backgroundTaskService; } void deliver(@NonNull Event event) { logger.d("DeliveryDelegate#deliver() - event being stored/delivered by Client"); - // Build the eventPayload - String apiKey = event.getApiKey(); - EventPayload eventPayload = new EventPayload(apiKey, event, notifier, immutableConfig); Session session = event.getSession(); if (session != null) { @@ -59,7 +56,10 @@ class DeliveryDelegate extends BaseObservable { boolean promiseRejection = REASON_PROMISE_REJECTION.equals(severityReasonType); boolean anr = event.getImpl().isAnr(event); cacheEvent(event, anr || promiseRejection); - } else { + } else if (callbackState.runOnSendTasks(event, logger)) { + // Build the eventPayload + String apiKey = event.getApiKey(); + EventPayload eventPayload = new EventPayload(apiKey, event, notifier, immutableConfig); deliverPayloadAsync(event, eventPayload); } } @@ -92,13 +92,11 @@ class DeliveryDelegate extends BaseObservable { switch (deliveryStatus) { case DELIVERED: logger.i("Sent 1 new event to Bugsnag"); - leaveErrorBreadcrumb(event); break; case UNDELIVERED: logger.w("Could not send event(s) to Bugsnag," + " saving to disk to send later"); cacheEvent(event, false); - leaveErrorBreadcrumb(event); break; case FAILURE: logger.w("Problem sending event to Bugsnag"); @@ -115,22 +113,4 @@ class DeliveryDelegate extends BaseObservable { eventStore.flushAsync(); } } - - private void leaveErrorBreadcrumb(@NonNull Event event) { - // Add a breadcrumb for this event occurring - List errors = event.getErrors(); - - if (errors.size() > 0) { - String errorClass = errors.get(0).getErrorClass(); - String message = errors.get(0).getErrorMessage(); - - Map data = new HashMap<>(); - data.put("errorClass", errorClass); - data.put("message", message); - data.put("unhandled", String.valueOf(event.isUnhandled())); - data.put("severity", event.getSeverity().toString()); - breadcrumbState.add(new Breadcrumb(errorClass, - BreadcrumbType.ERROR, data, new Date(), logger)); - } - } } diff --git a/app/src/main/java/com/bugsnag/android/Device.kt b/app/src/main/java/com/bugsnag/android/Device.kt index 75478b27d5..34d740de8d 100644 --- a/app/src/main/java/com/bugsnag/android/Device.kt +++ b/app/src/main/java/com/bugsnag/android/Device.kt @@ -36,7 +36,7 @@ open class Device internal constructor( * A collection of names and their versions of the primary languages, frameworks or * runtimes that the application is running on */ - var runtimeVersions: MutableMap? + runtimeVersions: MutableMap? ) : JsonStream.Streamable { /** @@ -59,6 +59,11 @@ open class Device internal constructor( */ var osVersion: String? = buildInfo.osVersion + var runtimeVersions: MutableMap? = sanitizeRuntimeVersions(runtimeVersions) + set(value) { + field = sanitizeRuntimeVersions(value) + } + internal open fun serializeFields(writer: JsonStream) { writer.name("cpuAbi").value(cpuAbi) writer.name("jailbroken").value(jailbroken) @@ -77,4 +82,7 @@ open class Device internal constructor( serializeFields(writer) writer.endObject() } + + private fun sanitizeRuntimeVersions(value: MutableMap?): MutableMap? = + value?.mapValuesTo(mutableMapOf()) { (_, value) -> value.toString() } } diff --git a/app/src/main/java/com/bugsnag/android/ErrorType.kt b/app/src/main/java/com/bugsnag/android/ErrorType.kt index b569b8a807..6ab5f98a56 100644 --- a/app/src/main/java/com/bugsnag/android/ErrorType.kt +++ b/app/src/main/java/com/bugsnag/android/ErrorType.kt @@ -18,5 +18,9 @@ enum class ErrorType(internal val desc: String) { /** * An error captured from Android's C layer */ - C("c") + C("c"); + + internal companion object { + internal fun fromDescriptor(desc: String) = values().find { it.desc == desc } + } } diff --git a/app/src/main/java/com/bugsnag/android/Event.java b/app/src/main/java/com/bugsnag/android/Event.java index fe68e04585..e3c9fa7d4e 100644 --- a/app/src/main/java/com/bugsnag/android/Event.java +++ b/app/src/main/java/com/bugsnag/android/Event.java @@ -6,8 +6,10 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.IOException; +import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Set; /** * An Event object represents a Throwable captured by Bugsnag and is available as a parameter on @@ -15,7 +17,7 @@ import java.util.Map; * sent to Bugsnag's API. */ @SuppressWarnings("ConstantConditions") -public class Event implements JsonStream.Streamable, MetadataAware, UserAware { +public class Event implements JsonStream.Streamable, MetadataAware, UserAware, FeatureFlagAware { private final EventInternal impl; private final Logger logger; @@ -24,15 +26,17 @@ public class Event implements JsonStream.Streamable, MetadataAware, UserAware { @NonNull ImmutableConfig config, @NonNull SeverityReason severityReason, @NonNull Logger logger) { - this(originalError, config, severityReason, new Metadata(), logger); + this(originalError, config, severityReason, new Metadata(), new FeatureFlags(), logger); } Event(@Nullable Throwable originalError, @NonNull ImmutableConfig config, @NonNull SeverityReason severityReason, @NonNull Metadata metadata, + @NonNull FeatureFlags featureFlags, @NonNull Logger logger) { - this(new EventInternal(originalError, config, severityReason, metadata), logger); + this(new EventInternal(originalError, config, severityReason, metadata, featureFlags), + logger); } Event(@NonNull EventInternal impl, @NonNull Logger logger) { @@ -283,6 +287,62 @@ public class Event implements JsonStream.Streamable, MetadataAware, UserAware { } } + /** + * {@inheritDoc} + */ + @Override + public void addFeatureFlag(@NonNull String name) { + if (name != null) { + impl.addFeatureFlag(name); + } else { + logNull("addFeatureFlag"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void addFeatureFlag(@NonNull String name, @Nullable String variant) { + if (name != null) { + impl.addFeatureFlag(name, variant); + } else { + logNull("addFeatureFlag"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void addFeatureFlags(@NonNull Iterable featureFlags) { + if (featureFlags != null) { + impl.addFeatureFlags(featureFlags); + } else { + logNull("addFeatureFlags"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void clearFeatureFlag(@NonNull String name) { + if (name != null) { + impl.clearFeatureFlag(name); + } else { + logNull("clearFeatureFlag"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void clearFeatureFlags() { + impl.clearFeatureFlags(); + } + @Override public void toStream(@NonNull JsonStream stream) throws IOException { impl.toStream(stream); @@ -348,4 +408,8 @@ public class Event implements JsonStream.Streamable, MetadataAware, UserAware { EventInternal getImpl() { return impl; } + + void setRedactedKeys(Collection redactedKeys) { + impl.setRedactedKeys(redactedKeys); + } } diff --git a/app/src/main/java/com/bugsnag/android/EventInternal.kt b/app/src/main/java/com/bugsnag/android/EventInternal.kt index 06ad16bc84..7c10ee61d9 100644 --- a/app/src/main/java/com/bugsnag/android/EventInternal.kt +++ b/app/src/main/java/com/bugsnag/android/EventInternal.kt @@ -3,16 +3,75 @@ package com.bugsnag.android import com.bugsnag.android.internal.ImmutableConfig import java.io.IOException -internal class EventInternal @JvmOverloads internal constructor( - val originalError: Throwable? = null, - config: ImmutableConfig, - private var severityReason: SeverityReason, - data: Metadata = Metadata() -) : JsonStream.Streamable, MetadataAware, UserAware { +internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, MetadataAware, UserAware { - val metadata: Metadata = data.copy() - private val discardClasses: Set = config.discardClasses.toSet() - private val projectPackages = config.projectPackages + @JvmOverloads + internal constructor( + originalError: Throwable? = null, + config: ImmutableConfig, + severityReason: SeverityReason, + data: Metadata = Metadata(), + featureFlags: FeatureFlags = FeatureFlags() + ) : this( + config.apiKey, + mutableListOf(), + config.discardClasses.toSet(), + when (originalError) { + null -> mutableListOf() + else -> Error.createError(originalError, config.projectPackages, config.logger) + }, + data.copy(), + featureFlags.copy(), + originalError, + config.projectPackages, + severityReason, + ThreadState(originalError, severityReason.unhandled, config).threads, + User(), + config.redactedKeys.toSet() + ) + + internal constructor( + apiKey: String, + breadcrumbs: MutableList = mutableListOf(), + discardClasses: Set = setOf(), + errors: MutableList = mutableListOf(), + metadata: Metadata = Metadata(), + featureFlags: FeatureFlags = FeatureFlags(), + originalError: Throwable? = null, + projectPackages: Collection = setOf(), + severityReason: SeverityReason = SeverityReason.newInstance(SeverityReason.REASON_HANDLED_EXCEPTION), + threads: MutableList = mutableListOf(), + user: User = User(), + redactionKeys: Set? = null + ) { + this.apiKey = apiKey + this.breadcrumbs = breadcrumbs + this.discardClasses = discardClasses + this.errors = errors + this.metadata = metadata + this.featureFlags = featureFlags + this.originalError = originalError + this.projectPackages = projectPackages + this.severityReason = severityReason + this.threads = threads + this.userImpl = user + + redactionKeys?.let { + this.redactedKeys = it + } + } + + val originalError: Throwable? + internal var severityReason: SeverityReason + + val metadata: Metadata + val featureFlags: FeatureFlags + private val discardClasses: Set + internal var projectPackages: Collection + + private val jsonStreamer: ObjectJsonStreamer = ObjectJsonStreamer().apply { + redactedKeys = redactedKeys.toSet() + } @JvmField internal var session: Session? = null @@ -23,34 +82,36 @@ internal class EventInternal @JvmOverloads internal constructor( severityReason.currentSeverity = value } - var apiKey: String = config.apiKey + var apiKey: String lateinit var app: AppWithState lateinit var device: DeviceWithState - var breadcrumbs: MutableList = mutableListOf() var unhandled: Boolean get() = severityReason.unhandled set(value) { severityReason.unhandled = value } - val unhandledOverridden: Boolean - get() = severityReason.unhandledOverridden - val originalUnhandled: Boolean - get() = severityReason.originalUnhandled - - var errors: MutableList = when (originalError) { - null -> mutableListOf() - else -> Error.createError(originalError, config.projectPackages, config.logger) - } - - var threads: MutableList = ThreadState(originalError, unhandled, config).threads + var breadcrumbs: MutableList + var errors: MutableList + var threads: MutableList var groupingHash: String? = null var context: String? = null + var redactedKeys: Collection + get() = jsonStreamer.redactedKeys + set(value) { + jsonStreamer.redactedKeys = value.toSet() + metadata.redactedKeys = value.toSet() + } + /** * @return user information associated with this Event */ - internal var _user = User(null, null, null) + internal var userImpl: User + + fun getUnhandledOverridden(): Boolean = severityReason.unhandledOverridden + + fun getOriginalUnhandled(): Boolean = severityReason.originalUnhandled protected fun shouldDiscardClass(): Boolean { return when { @@ -70,7 +131,8 @@ internal class EventInternal @JvmOverloads internal constructor( } @Throws(IOException::class) - override fun toStream(writer: JsonStream) { + override fun toStream(parentWriter: JsonStream) { + val writer = JsonStream(parentWriter, jsonStreamer) // Write error basics writer.beginObject() writer.name("context").value(context) @@ -93,7 +155,7 @@ internal class EventInternal @JvmOverloads internal constructor( writer.endArray() // Write user info - writer.name("user").value(_user) + writer.name("user").value(userImpl) // Write diagnostics writer.name("app").value(app) @@ -106,6 +168,8 @@ internal class EventInternal @JvmOverloads internal constructor( threads.forEach { writer.value(it) } writer.endArray() + writer.name("featureFlags").value(featureFlags) + if (session != null) { val copy = Session.copySession(session) writer.name("session").beginObject() @@ -129,12 +193,26 @@ internal class EventInternal @JvmOverloads internal constructor( return errorTypes.plus(frameOverrideTypes) } + internal fun normalizeStackframeErrorTypes() { + if (getErrorTypesFromStackframes().size == 1) { + errors.flatMap { it.stacktrace }.forEach { + it.type = null + } + } + } + + internal fun updateSeverityReasonInternal(severityReason: SeverityReason) { + this.severityReason = severityReason + } + protected fun updateSeverityInternal(severity: Severity) { severityReason = SeverityReason( severityReason.severityReasonType, severity, severityReason.unhandled, - severityReason.attributeValue + severityReason.unhandledOverridden, + severityReason.attributeValue, + severityReason.attributeKey ) } @@ -143,19 +221,22 @@ internal class EventInternal @JvmOverloads internal constructor( reason, severityReason.currentSeverity, severityReason.unhandled, - severityReason.attributeValue + severityReason.unhandledOverridden, + severityReason.attributeValue, + severityReason.attributeKey ) } fun getSeverityReasonType(): String = severityReason.severityReasonType override fun setUser(id: String?, email: String?, name: String?) { - _user = User(id, email, name) + userImpl = User(id, email, name) } - override fun getUser() = _user + override fun getUser() = userImpl - override fun addMetadata(section: String, value: Map) = metadata.addMetadata(section, value) + override fun addMetadata(section: String, value: Map) = + metadata.addMetadata(section, value) override fun addMetadata(section: String, key: String, value: Any?) = metadata.addMetadata(section, key, value) @@ -167,4 +248,15 @@ internal class EventInternal @JvmOverloads internal constructor( override fun getMetadata(section: String) = metadata.getMetadata(section) override fun getMetadata(section: String, key: String) = metadata.getMetadata(section, key) + + override fun addFeatureFlag(name: String) = featureFlags.addFeatureFlag(name) + + override fun addFeatureFlag(name: String, variant: String?) = featureFlags.addFeatureFlag(name, variant) + + override fun addFeatureFlags(featureFlags: MutableIterable) = + this.featureFlags.addFeatureFlags(featureFlags) + + override fun clearFeatureFlag(name: String) = featureFlags.clearFeatureFlag(name) + + override fun clearFeatureFlags() = featureFlags.clearFeatureFlags() } diff --git a/app/src/main/java/com/bugsnag/android/EventStorageModule.kt b/app/src/main/java/com/bugsnag/android/EventStorageModule.kt index 790fd9e45b..78980d1458 100644 --- a/app/src/main/java/com/bugsnag/android/EventStorageModule.kt +++ b/app/src/main/java/com/bugsnag/android/EventStorageModule.kt @@ -15,7 +15,8 @@ internal class EventStorageModule( bgTaskService: BackgroundTaskService, trackerModule: TrackerModule, systemServiceModule: SystemServiceModule, - notifier: Notifier + notifier: Notifier, + callbackState: CallbackState ) : DependencyModule() { private val cfg = configModule.config @@ -34,5 +35,5 @@ internal class EventStorageModule( ) } - val eventStore by future { EventStore(cfg, cfg.logger, notifier, bgTaskService, delegate) } + val eventStore by future { EventStore(cfg, cfg.logger, notifier, bgTaskService, delegate, callbackState) } } diff --git a/app/src/main/java/com/bugsnag/android/EventStore.java b/app/src/main/java/com/bugsnag/android/EventStore.java index e95fc6b1d4..e1365644ae 100644 --- a/app/src/main/java/com/bugsnag/android/EventStore.java +++ b/app/src/main/java/com/bugsnag/android/EventStore.java @@ -29,6 +29,7 @@ class EventStore extends FileStore { private final Delegate delegate; private final Notifier notifier; private final BackgroundTaskService bgTaskSevice; + private final CallbackState callbackState; final Logger logger; static final Comparator EVENT_COMPARATOR = new Comparator() { @@ -51,7 +52,8 @@ class EventStore extends FileStore { @NonNull Logger logger, Notifier notifier, BackgroundTaskService bgTaskSevice, - Delegate delegate) { + Delegate delegate, + CallbackState callbackState) { super(new File(config.getPersistenceDirectory().getValue(), "bugsnag-errors"), config.getMaxPersistedEvents(), EVENT_COMPARATOR, @@ -62,6 +64,7 @@ class EventStore extends FileStore { this.delegate = delegate; this.notifier = notifier; this.bgTaskSevice = bgTaskSevice; + this.callbackState = callbackState; } /** @@ -162,33 +165,64 @@ class EventStore extends FileStore { try { EventFilenameInfo eventInfo = EventFilenameInfo.Companion.fromFile(eventFile, config); String apiKey = eventInfo.getApiKey(); - EventPayload payload = new EventPayload(apiKey, null, eventFile, notifier, config); - DeliveryParams deliveryParams = config.getErrorApiDeliveryParams(payload); - Delivery delivery = config.getDelivery(); - DeliveryStatus deliveryStatus = delivery.deliver(payload, deliveryParams); + EventPayload payload = createEventPayload(eventFile, apiKey); - switch (deliveryStatus) { - case DELIVERED: - deleteStoredFiles(Collections.singleton(eventFile)); - logger.i("Deleting sent error file " + eventFile.getName()); - break; - case UNDELIVERED: - cancelQueuedFiles(Collections.singleton(eventFile)); - logger.w("Could not send previously saved error(s)" - + " to Bugsnag, will try again later"); - break; - case FAILURE: - Exception exc = new RuntimeException("Failed to deliver event payload"); - handleEventFlushFailure(exc, eventFile); - break; - default: - break; + if (payload == null) { + deleteStoredFiles(Collections.singleton(eventFile)); + } else { + deliverEventPayload(eventFile, payload); } } catch (Exception exception) { handleEventFlushFailure(exception, eventFile); } } + private void deliverEventPayload(File eventFile, EventPayload payload) { + DeliveryParams deliveryParams = config.getErrorApiDeliveryParams(payload); + Delivery delivery = config.getDelivery(); + DeliveryStatus deliveryStatus = delivery.deliver(payload, deliveryParams); + + switch (deliveryStatus) { + case DELIVERED: + deleteStoredFiles(Collections.singleton(eventFile)); + logger.i("Deleting sent error file " + eventFile.getName()); + break; + case UNDELIVERED: + cancelQueuedFiles(Collections.singleton(eventFile)); + logger.w("Could not send previously saved error(s)" + + " to Bugsnag, will try again later"); + break; + case FAILURE: + Exception exc = new RuntimeException("Failed to deliver event payload"); + handleEventFlushFailure(exc, eventFile); + break; + default: + break; + } + } + + @Nullable + private EventPayload createEventPayload(File eventFile, String apiKey) { + MarshalledEventSource eventSource = new MarshalledEventSource(eventFile, apiKey, logger); + + try { + if (!callbackState.runOnSendTasks(eventSource, logger)) { + // do not send the payload at all, we must block sending + return null; + } + } catch (Exception ioe) { + eventSource.clear(); + } + + Event processedEvent = eventSource.getEvent(); + if (processedEvent != null) { + apiKey = processedEvent.getApiKey(); + return new EventPayload(apiKey, processedEvent, null, notifier, config); + } else { + return new EventPayload(apiKey, null, eventFile, notifier, config); + } + } + private void handleEventFlushFailure(Exception exc, File eventFile) { if (delegate != null) { delegate.onErrorIOFailure(exc, eventFile, "Crash Report Deserialization"); diff --git a/app/src/main/java/com/bugsnag/android/ExceptionHandler.java b/app/src/main/java/com/bugsnag/android/ExceptionHandler.java index 057f69b72c..37648a3ca8 100644 --- a/app/src/main/java/com/bugsnag/android/ExceptionHandler.java +++ b/app/src/main/java/com/bugsnag/android/ExceptionHandler.java @@ -1,6 +1,7 @@ package com.bugsnag.android; import android.os.StrictMode; + import androidx.annotation.NonNull; import java.lang.Thread; @@ -35,37 +36,47 @@ class ExceptionHandler implements UncaughtExceptionHandler { @Override public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) { - if (client.getConfig().shouldDiscardError(throwable)) { - return; - } - boolean strictModeThrowable = strictModeHandler.isStrictModeThrowable(throwable); - - // Notify any subscribed clients of the uncaught exception - Metadata metadata = new Metadata(); - String violationDesc = null; - - if (strictModeThrowable) { // add strictmode policy violation to metadata - violationDesc = strictModeHandler.getViolationDescription(throwable.getMessage()); - metadata = new Metadata(); - metadata.addMetadata(STRICT_MODE_TAB, STRICT_MODE_KEY, violationDesc); - } - - String severityReason = strictModeThrowable - ? SeverityReason.REASON_STRICT_MODE : SeverityReason.REASON_UNHANDLED_EXCEPTION; - - if (strictModeThrowable) { // writes to disk on main thread - StrictMode.ThreadPolicy originalThreadPolicy = StrictMode.getThreadPolicy(); - StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.LAX); - - client.notifyUnhandledException(throwable, - metadata, severityReason, violationDesc); - - StrictMode.setThreadPolicy(originalThreadPolicy); - } else { - client.notifyUnhandledException(throwable, - metadata, severityReason, null); + try { + if (client.getConfig().shouldDiscardError(throwable)) { + return; + } + + boolean strictModeThrowable = strictModeHandler.isStrictModeThrowable(throwable); + + // Notify any subscribed clients of the uncaught exception + Metadata metadata = new Metadata(); + String violationDesc = null; + + if (strictModeThrowable) { // add strictmode policy violation to metadata + violationDesc = strictModeHandler.getViolationDescription(throwable.getMessage()); + metadata = new Metadata(); + metadata.addMetadata(STRICT_MODE_TAB, STRICT_MODE_KEY, violationDesc); + } + + String severityReason = strictModeThrowable + ? SeverityReason.REASON_STRICT_MODE : SeverityReason.REASON_UNHANDLED_EXCEPTION; + + if (strictModeThrowable) { // writes to disk on main thread + StrictMode.ThreadPolicy originalThreadPolicy = StrictMode.getThreadPolicy(); + StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.LAX); + + client.notifyUnhandledException(throwable, + metadata, severityReason, violationDesc); + + StrictMode.setThreadPolicy(originalThreadPolicy); + } else { + client.notifyUnhandledException(throwable, + metadata, severityReason, null); + } + } catch (Throwable ignored) { + // the runtime would ignore any exceptions here, we make that absolutely clear + // to avoid any possible unhandled-exception loops + } finally { + forwardToOriginalHandler(thread, throwable); } + } + private void forwardToOriginalHandler(@NonNull Thread thread, @NonNull Throwable throwable) { // Pass exception on to original exception handler if (originalHandler != null) { originalHandler.uncaughtException(thread, throwable); diff --git a/app/src/main/java/com/bugsnag/android/FeatureFlag.java b/app/src/main/java/com/bugsnag/android/FeatureFlag.java new file mode 100644 index 0000000000..39bef789b8 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/FeatureFlag.java @@ -0,0 +1,134 @@ +package com.bugsnag.android; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Map; + +/** + * Represents a single feature-flag / experiment marker within Bugsnag. Each {@code FeatureFlag} + * object has a {@link #getName() name} and an optional {@link #getVariant() variant} which can be + * used to identify runtime experiments and groups when reporting errors. + * + * @see Bugsnag#addFeatureFlag(String, String) + * @see Event#addFeatureFlag(String, String) + */ +public final class FeatureFlag implements Map.Entry { + private final String name; + + private final String variant; + + /** + * Create a named {@code FeatureFlag} with no variant + * + * @param name the identifying name of the new {@code FeatureFlag} (not {@code null}) + * @see Bugsnag#addFeatureFlag(String) + * @see Event#addFeatureFlag(String) + */ + public FeatureFlag(@NonNull String name) { + this(name, null); + } + + /** + * Create a new {@code FeatureFlag} with a name and (optionally) a variant. + * + * @param name the identifying name of the new {@code FeatureFlag} (not {@code null}) + * @param variant the feature variant + */ + public FeatureFlag(@NonNull String name, @Nullable String variant) { + if (name == null) { + throw new NullPointerException("FeatureFlags cannot have null name"); + } + + this.name = name; + this.variant = variant; + } + + /** + * Create a new {@code FeatureFlag} based on an existing {@code Map.Entry}. This is the same + * as {@code new FeatureFlag(mapEntry.getKey(), mapEntry.getValue())}. + * + * @param mapEntry an existing {@code Map.Entry} to copy the feature flag from + */ + public FeatureFlag(@NonNull Map.Entry mapEntry) { + this(mapEntry.getKey(), mapEntry.getValue()); + } + + @NonNull + public String getName() { + return name; + } + + @Nullable + public String getVariant() { + return variant; + } + + /** + * Same as {@link #getName()}. + * + * @return the name of this {@code FeatureFlag} + * @see #getName() + */ + @NonNull + @Override + public String getKey() { + return name; + } + + /** + * Same as {@link #getVariant()}. + * + * @return the variant of this {@code FeatureFlag} (may be {@code null}) + * @see #getVariant() + */ + @Nullable + @Override + public String getValue() { + return variant; + } + + /** + * Throws {@code UnsupportedOperationException} as {@code FeatureFlag} is considered immutable. + * + * @param value ignored + * @return nothing + */ + @Override + @Nullable + public String setValue(@Nullable String value) { + throw new UnsupportedOperationException("FeatureFlag is immutable"); + } + + @Override + public int hashCode() { + // Follows the Map.Entry contract + return getKey().hashCode() ^ (getValue() == null ? 0 : getValue().hashCode()); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + // This follows the contract defined in Map.Entry exactly + if (!(other instanceof Map.Entry)) { + return false; + } + + Map.Entry e2 = + (Map.Entry) other; + + return getKey().equals(e2.getKey()) + && (getValue() == null ? e2.getValue() == null : getValue().equals(e2.getValue())); + } + + @Override + public String toString() { + return "FeatureFlag{" + + "name='" + name + '\'' + + ", variant='" + variant + '\'' + + '}'; + } +} diff --git a/app/src/main/java/com/bugsnag/android/FeatureFlagAware.java b/app/src/main/java/com/bugsnag/android/FeatureFlagAware.java new file mode 100644 index 0000000000..b2571da1c9 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/FeatureFlagAware.java @@ -0,0 +1,49 @@ +package com.bugsnag.android; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +interface FeatureFlagAware { + /** + * Add a single feature flag with no variant. If there is an existing feature flag with the + * same name, it will be overwritten to have no variant. + * + * @param name the name of the feature flag to add + * @see #addFeatureFlag(String, String) + */ + void addFeatureFlag(@NonNull String name); + + /** + * Add a single feature flag with an optional variant. If there is an existing feature + * flag with the same name, it will be overwritten with the new variant. If the variant is + * {@code null} this method has the same behaviour as {@link #addFeatureFlag(String)}. + * + * @param name the name of the feature flag to add + * @param variant the variant to set the feature flag to, or {@code null} to specify a feature + * flag with no variant + */ + void addFeatureFlag(@NonNull String name, @Nullable String variant); + + /** + * Add a collection of feature flags. This method behaves exactly the same as calling + * {@link #addFeatureFlag(String, String)} for each of the {@code FeatureFlag} objects. + * + * @param featureFlags the feature flags to add + * @see #addFeatureFlag(String, String) + */ + void addFeatureFlags(@NonNull Iterable featureFlags); + + /** + * Remove a single feature flag regardless of its current status. This will stop the specified + * feature flag from being reported. If the named feature flag does not exist this will + * have no effect. + * + * @param name the name of the feature flag to remove + */ + void clearFeatureFlag(@NonNull String name); + + /** + * Clear all of the feature flags. This will stop all feature flags from being reported. + */ + void clearFeatureFlags(); +} \ No newline at end of file diff --git a/app/src/main/java/com/bugsnag/android/FeatureFlagState.kt b/app/src/main/java/com/bugsnag/android/FeatureFlagState.kt new file mode 100644 index 0000000000..ba2b78bd9b --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/FeatureFlagState.kt @@ -0,0 +1,50 @@ +package com.bugsnag.android + +internal data class FeatureFlagState( + val featureFlags: FeatureFlags = FeatureFlags() +) : BaseObservable(), FeatureFlagAware { + override fun addFeatureFlag(name: String) { + this.featureFlags.addFeatureFlag(name) + updateState { + StateEvent.AddFeatureFlag(name) + } + } + + override fun addFeatureFlag(name: String, variant: String?) { + this.featureFlags.addFeatureFlag(name, variant) + updateState { + StateEvent.AddFeatureFlag(name, variant) + } + } + + override fun addFeatureFlags(featureFlags: Iterable) { + featureFlags.forEach { (name, variant) -> + addFeatureFlag(name, variant) + } + } + + override fun clearFeatureFlag(name: String) { + this.featureFlags.clearFeatureFlag(name) + updateState { + StateEvent.ClearFeatureFlag(name) + } + } + + override fun clearFeatureFlags() { + this.featureFlags.clearFeatureFlags() + updateState { + StateEvent.ClearFeatureFlags + } + } + + fun emitObservableEvent() { + val flags = toList() + + flags.forEach { (name, variant) -> + updateState { StateEvent.AddFeatureFlag(name, variant) } + } + } + + fun toList(): List = featureFlags.toList() + fun copy() = FeatureFlagState(featureFlags.copy()) +} diff --git a/app/src/main/java/com/bugsnag/android/FeatureFlags.kt b/app/src/main/java/com/bugsnag/android/FeatureFlags.kt new file mode 100644 index 0000000000..bce01bff5d --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/FeatureFlags.kt @@ -0,0 +1,52 @@ +package com.bugsnag.android + +import java.io.IOException +import java.util.concurrent.ConcurrentHashMap + +internal class FeatureFlags( + internal val store: MutableMap = ConcurrentHashMap() +) : JsonStream.Streamable, FeatureFlagAware { + private val emptyVariant = "__EMPTY_VARIANT_SENTINEL__" + + override fun addFeatureFlag(name: String) { + store[name] = emptyVariant + } + + override fun addFeatureFlag(name: String, variant: String?) { + store[name] = variant ?: emptyVariant + } + + override fun addFeatureFlags(featureFlags: Iterable) { + featureFlags.forEach { (name, variant) -> + addFeatureFlag(name, variant) + } + } + + override fun clearFeatureFlag(name: String) { + store.remove(name) + } + + override fun clearFeatureFlags() { + store.clear() + } + + @Throws(IOException::class) + override fun toStream(stream: JsonStream) { + stream.beginArray() + store.forEach { (name, variant) -> + stream.beginObject() + stream.name("featureFlag").value(name) + if (variant != emptyVariant) { + stream.name("variant").value(variant) + } + stream.endObject() + } + stream.endArray() + } + + fun toList(): List = store.entries.map { (name, variant) -> + FeatureFlag(name, variant.takeUnless { it == emptyVariant }) + } + + fun copy() = FeatureFlags(store.toMutableMap()) +} diff --git a/app/src/main/java/com/bugsnag/android/ForegroundDetector.java b/app/src/main/java/com/bugsnag/android/ForegroundDetector.java index 5205f7e491..b3475f766c 100644 --- a/app/src/main/java/com/bugsnag/android/ForegroundDetector.java +++ b/app/src/main/java/com/bugsnag/android/ForegroundDetector.java @@ -13,6 +13,8 @@ import java.util.List; class ForegroundDetector { + private static final int IMPORTANCE_FOREGROUND_SERVICE = 125; + @Nullable private final ActivityManager activityManager; @@ -36,8 +38,7 @@ class ForegroundDetector { ActivityManager.RunningAppProcessInfo info = getProcessInfo(); if (info != null) { - return info.importance - <= ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; + return info.importance <= IMPORTANCE_FOREGROUND_SERVICE; } else { return null; } diff --git a/app/src/main/java/com/bugsnag/android/JsonStream.java b/app/src/main/java/com/bugsnag/android/JsonStream.java index 1ef62a2a88..57072f22b6 100644 --- a/app/src/main/java/com/bugsnag/android/JsonStream.java +++ b/app/src/main/java/com/bugsnag/android/JsonStream.java @@ -33,6 +33,13 @@ public class JsonStream extends JsonWriter { objectJsonStreamer = new ObjectJsonStreamer(); } + JsonStream(@NonNull JsonStream stream, @NonNull ObjectJsonStreamer streamer) { + super(stream.out); + setSerializeNulls(stream.getSerializeNulls()); + this.out = stream.out; + this.objectJsonStreamer = streamer; + } + // Allow chaining name().value() @NonNull public JsonStream name(@Nullable String name) throws IOException { diff --git a/app/src/main/java/com/bugsnag/android/LastRunInfoStore.kt b/app/src/main/java/com/bugsnag/android/LastRunInfoStore.kt index 4e6f125f7f..6309bbdc93 100644 --- a/app/src/main/java/com/bugsnag/android/LastRunInfoStore.kt +++ b/app/src/main/java/com/bugsnag/android/LastRunInfoStore.kt @@ -89,7 +89,8 @@ private class KeyValueWriter { private val sb = StringBuilder() fun add(key: String, value: Any) { - sb.appendln("$key$KEY_VALUE_DELIMITER$value") + sb.append("$key$KEY_VALUE_DELIMITER$value") + sb.append("\n") } override fun toString() = sb.toString() diff --git a/app/src/main/java/com/bugsnag/android/MarshalledEventSource.kt b/app/src/main/java/com/bugsnag/android/MarshalledEventSource.kt new file mode 100644 index 0000000000..cd5fbf69e0 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/MarshalledEventSource.kt @@ -0,0 +1,42 @@ +package com.bugsnag.android + +import com.bugsnag.android.internal.JsonHelper +import java.io.File + +internal class MarshalledEventSource( + private val eventFile: File, + private val apiKey: String, + private val logger: Logger +) : () -> Event { + + /** + * The parsed and possibly processed event. This field remains `null` if the `EventSource` + * is not used, and may not reflect the same data as is stored in `eventFile` (as the `Event` + * is mutable, and may have been modified after loading). + */ + var event: Event? = null + private set + + override fun invoke(): Event { + var unmarshalledEvent = event + if (unmarshalledEvent == null) { + unmarshalledEvent = unmarshall() + event = unmarshalledEvent + } + + return unmarshalledEvent + } + + fun clear() { + event = null + } + + private fun unmarshall(): Event { + val eventMapper = BugsnagEventMapper(logger) + val jsonMap = JsonHelper.deserialize(eventFile) + return Event( + eventMapper.convertToEventImpl(jsonMap, apiKey), + logger + ) + } +} diff --git a/app/src/main/java/com/bugsnag/android/MetadataState.kt b/app/src/main/java/com/bugsnag/android/MetadataState.kt index d95177a5a4..d305b6b6e4 100644 --- a/app/src/main/java/com/bugsnag/android/MetadataState.kt +++ b/app/src/main/java/com/bugsnag/android/MetadataState.kt @@ -61,7 +61,7 @@ internal data class MetadataState(val metadata: Metadata = Metadata()) : private fun notifyMetadataAdded(section: String, value: Map) { value.entries.forEach { - updateState { AddMetadata(section, it.key, metadata.getMetadata(it.key)) } + updateState { AddMetadata(section, it.key, metadata.getMetadata(section, it.key)) } } } } diff --git a/app/src/main/java/com/bugsnag/android/NativeInterface.java b/app/src/main/java/com/bugsnag/android/NativeInterface.java index d6d2528542..e7b90736fa 100644 --- a/app/src/main/java/com/bugsnag/android/NativeInterface.java +++ b/app/src/main/java/com/bugsnag/android/NativeInterface.java @@ -397,12 +397,22 @@ public class NativeInterface { }); } + /** + * Create an {@code Event} object + * + * @param exc the Throwable object that caused the event + * @param client the Client object that the event is associated with + * @param severityReason the severity of the Event + * @return a new {@code Event} object + */ @NonNull public static Event createEvent(@Nullable Throwable exc, @NonNull Client client, @NonNull SeverityReason severityReason) { Metadata metadata = client.getMetadataState().getMetadata(); - return new Event(exc, client.getConfig(), severityReason, metadata, client.logger); + FeatureFlags featureFlags = client.getFeatureFlagState().getFeatureFlags(); + return new Event(exc, client.getConfig(), severityReason, metadata, featureFlags, + client.logger); } @NonNull diff --git a/app/src/main/java/com/bugsnag/android/NativeStackframe.kt b/app/src/main/java/com/bugsnag/android/NativeStackframe.kt index d58650202d..5c47e1633c 100644 --- a/app/src/main/java/com/bugsnag/android/NativeStackframe.kt +++ b/app/src/main/java/com/bugsnag/android/NativeStackframe.kt @@ -35,13 +35,18 @@ class NativeStackframe internal constructor( /** * The address of the library where the event occurred. */ - var loadAddress: Long? -) : JsonStream.Streamable { + var loadAddress: Long?, + + /** + * Whether this frame identifies the program counter + */ + var isPC: Boolean?, /** * The type of the error */ - var type: ErrorType? = ErrorType.C + var type: ErrorType? = null +) : JsonStream.Streamable { @Throws(IOException::class) override fun toStream(writer: JsonStream) { @@ -52,6 +57,7 @@ class NativeStackframe internal constructor( writer.name("frameAddress").value(frameAddress) writer.name("symbolAddress").value(symbolAddress) writer.name("loadAddress").value(loadAddress) + writer.name("isPC").value(isPC) type?.let { writer.name("type").value(it.desc) diff --git a/app/src/main/java/com/bugsnag/android/Notifier.kt b/app/src/main/java/com/bugsnag/android/Notifier.kt index 150a2bbee4..b2f08dbab0 100644 --- a/app/src/main/java/com/bugsnag/android/Notifier.kt +++ b/app/src/main/java/com/bugsnag/android/Notifier.kt @@ -7,7 +7,7 @@ import java.io.IOException */ class Notifier @JvmOverloads constructor( var name: String = "Android Bugsnag Notifier", - var version: String = "5.14.0", + var version: String = "5.19.2", var url: String = "https://bugsnag.com" ) : JsonStream.Streamable { diff --git a/app/src/main/java/com/bugsnag/android/ObjectJsonStreamer.kt b/app/src/main/java/com/bugsnag/android/ObjectJsonStreamer.kt index 1e817b22ec..9316067b81 100644 --- a/app/src/main/java/com/bugsnag/android/ObjectJsonStreamer.kt +++ b/app/src/main/java/com/bugsnag/android/ObjectJsonStreamer.kt @@ -8,8 +8,8 @@ import java.util.Date internal class ObjectJsonStreamer { companion object { - private const val REDACTED_PLACEHOLDER = "[REDACTED]" - private const val OBJECT_PLACEHOLDER = "[OBJECT]" + internal const val REDACTED_PLACEHOLDER = "[REDACTED]" + internal const val OBJECT_PLACEHOLDER = "[OBJECT]" } var redactedKeys = setOf("password") diff --git a/app/src/main/java/com/bugsnag/android/OnSendCallback.java b/app/src/main/java/com/bugsnag/android/OnSendCallback.java new file mode 100644 index 0000000000..1856a328a3 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/OnSendCallback.java @@ -0,0 +1,12 @@ +package com.bugsnag.android; + +import androidx.annotation.NonNull; + +/** + * A callback to be invoked before an {@link Event} is uploaded to a server. Similar to + * {@link OnErrorCallback}, an {@code OnSendCallback} may modify the {@code Event} + * contents or even reject the entire payload by returning {@code false}. + */ +public interface OnSendCallback { + boolean onSend(@NonNull Event event); +} diff --git a/app/src/main/java/com/bugsnag/android/Session.java b/app/src/main/java/com/bugsnag/android/Session.java index fd3515680b..23dc782e2c 100644 --- a/app/src/main/java/com/bugsnag/android/Session.java +++ b/app/src/main/java/com/bugsnag/android/Session.java @@ -1,5 +1,7 @@ package com.bugsnag.android; +import com.bugsnag.android.internal.DateUtils; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -7,6 +9,7 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Date; +import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -40,6 +43,23 @@ public final class Session implements JsonStream.Streamable, UserAware { return copy; } + Session(Map map, Logger logger) { + this(null, null, logger); + setId((String) map.get("id")); + + String timestamp = (String) map.get("startedAt"); + setStartedAt(DateUtils.fromIso8601(timestamp)); + + @SuppressWarnings("unchecked") + Map events = (Map) map.get("events"); + + Number handled = (Number) events.get("handled"); + handledCount.set(handled.intValue()); + + Number unhandled = (Number) events.get("unhandled"); + unhandledCount.set(unhandled.intValue()); + } + Session(String id, Date startedAt, User user, boolean autoCaptured, Notifier notifier, Logger logger) { this(null, notifier, logger); @@ -60,9 +80,14 @@ public final class Session implements JsonStream.Streamable, UserAware { Session(File file, Notifier notifier, Logger logger) { this.file = file; this.logger = logger; - Notifier copy = new Notifier(notifier.getName(), notifier.getVersion(), notifier.getUrl()); - copy.setDependencies(new ArrayList<>(notifier.getDependencies())); - this.notifier = copy; + if (notifier != null) { + Notifier copy = new Notifier(notifier.getName(), + notifier.getVersion(), notifier.getUrl()); + copy.setDependencies(new ArrayList<>(notifier.getDependencies())); + this.notifier = copy; + } else { + this.notifier = null; + } } private void logNull(String property) { diff --git a/app/src/main/java/com/bugsnag/android/SessionTracker.java b/app/src/main/java/com/bugsnag/android/SessionTracker.java index 345d635b0a..2a5e95c7f1 100644 --- a/app/src/main/java/com/bugsnag/android/SessionTracker.java +++ b/app/src/main/java/com/bugsnag/android/SessionTracker.java @@ -38,7 +38,7 @@ class SessionTracker extends BaseObservable { // The first Activity in this 'session' was started at this time. private final AtomicLong lastEnteredForegroundMs = new AtomicLong(0); - private final AtomicReference currentSession = new AtomicReference<>(); + private volatile Session currentSession = null; private final ForegroundDetector foregroundDetector; final BackgroundTaskService backgroundTaskService; final Logger logger; @@ -89,9 +89,11 @@ class SessionTracker extends BaseObservable { } String id = UUID.randomUUID().toString(); Session session = new Session(id, date, user, autoCaptured, client.getNotifier(), logger); - currentSession.set(session); - trackSessionIfNeeded(session); - return session; + if (trackSessionIfNeeded(session)) { + return session; + } else { + return null; + } } Session startSession(boolean autoCaptured) { @@ -102,7 +104,7 @@ class SessionTracker extends BaseObservable { } void pauseSession() { - Session session = currentSession.get(); + Session session = currentSession; if (session != null) { session.isPaused.set(true); @@ -111,7 +113,7 @@ class SessionTracker extends BaseObservable { } boolean resumeSession() { - Session session = currentSession.get(); + Session session = currentSession; boolean resumed; if (session == null) { @@ -159,7 +161,7 @@ class SessionTracker extends BaseObservable { } else { updateState(StateEvent.PauseSession.INSTANCE); } - currentSession.set(session); + currentSession = session; return session; } @@ -168,23 +170,27 @@ class SessionTracker extends BaseObservable { * stored and sent to the Bugsnag API, otherwise no action will occur in this method. * * @param session the session + * @return true if the Session should be tracked */ - private void trackSessionIfNeeded(final Session session) { + private boolean trackSessionIfNeeded(final Session session) { logger.d("SessionTracker#trackSessionIfNeeded() - session captured by Client"); session.setApp(client.getAppDataCollector().generateApp()); session.setDevice(client.getDeviceDataCollector().generateDevice()); boolean deliverSession = callbackState.runOnSessionTasks(session, logger); if (deliverSession && session.isTracked().compareAndSet(false, true)) { + currentSession = session; notifySessionStartObserver(session); - flushAsync(); flushInMemorySession(session); + flushAsync(); + return true; } + return false; } @Nullable Session getCurrentSession() { - Session session = currentSession.get(); + Session session = currentSession; if (session != null && !session.isPaused.get()) { return session; diff --git a/app/src/main/java/com/bugsnag/android/Severity.kt b/app/src/main/java/com/bugsnag/android/Severity.kt index d32349867b..24b31c34e8 100644 --- a/app/src/main/java/com/bugsnag/android/Severity.kt +++ b/app/src/main/java/com/bugsnag/android/Severity.kt @@ -17,4 +17,8 @@ enum class Severity(private val str: String) : JsonStream.Streamable { override fun toStream(writer: JsonStream) { writer.value(str) } + + internal companion object { + internal fun fromDescriptor(desc: String) = values().find { it.str == desc } + } } diff --git a/app/src/main/java/com/bugsnag/android/SeverityReason.java b/app/src/main/java/com/bugsnag/android/SeverityReason.java index d445e6fd95..54be78ae1a 100644 --- a/app/src/main/java/com/bugsnag/android/SeverityReason.java +++ b/app/src/main/java/com/bugsnag/android/SeverityReason.java @@ -1,5 +1,8 @@ package com.bugsnag.android; +import static com.bugsnag.android.Severity.ERROR; +import static com.bugsnag.android.Severity.WARNING; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringDef; @@ -11,8 +14,8 @@ import java.lang.annotation.RetentionPolicy; final class SeverityReason implements JsonStream.Streamable { @StringDef({REASON_UNHANDLED_EXCEPTION, REASON_STRICT_MODE, REASON_HANDLED_EXCEPTION, - REASON_USER_SPECIFIED, REASON_CALLBACK_SPECIFIED, REASON_PROMISE_REJECTION, - REASON_LOG, REASON_SIGNAL, REASON_ANR}) + REASON_HANDLED_ERROR, REASON_USER_SPECIFIED, REASON_CALLBACK_SPECIFIED, + REASON_PROMISE_REJECTION, REASON_LOG, REASON_SIGNAL, REASON_ANR }) @Retention(RetentionPolicy.SOURCE) @interface SeverityReasonType { } @@ -20,6 +23,7 @@ final class SeverityReason implements JsonStream.Streamable { static final String REASON_UNHANDLED_EXCEPTION = "unhandledException"; static final String REASON_STRICT_MODE = "strictMode"; static final String REASON_HANDLED_EXCEPTION = "handledException"; + static final String REASON_HANDLED_ERROR = "handledError"; static final String REASON_USER_SPECIFIED = "userSpecifiedSeverity"; static final String REASON_CALLBACK_SPECIFIED = "userCallbackSetSeverity"; static final String REASON_PROMISE_REJECTION = "unhandledPromiseRejection"; @@ -30,6 +34,9 @@ final class SeverityReason implements JsonStream.Streamable { @SeverityReasonType private final String severityReasonType; + @Nullable + private final String attributeKey; + @Nullable private final String attributeValue; @@ -42,51 +49,52 @@ final class SeverityReason implements JsonStream.Streamable { return newInstance(severityReasonType, null, null); } - static SeverityReason newInstance(@SeverityReasonType String severityReasonType, + static SeverityReason newInstance(@SeverityReasonType String reason, @Nullable Severity severity, @Nullable String attrVal) { - if (severityReasonType.equals(REASON_STRICT_MODE) && Intrinsics.isEmpty(attrVal)) { + if (reason.equals(REASON_STRICT_MODE) && Intrinsics.isEmpty(attrVal)) { throw new IllegalArgumentException("No reason supplied for strictmode"); } - if (!(severityReasonType.equals(REASON_STRICT_MODE) - || severityReasonType.equals(REASON_LOG)) && !Intrinsics.isEmpty(attrVal)) { + if (!(reason.equals(REASON_STRICT_MODE) + || reason.equals(REASON_LOG)) && !Intrinsics.isEmpty(attrVal)) { throw new IllegalArgumentException("attributeValue should not be supplied"); } - switch (severityReasonType) { + switch (reason) { case REASON_UNHANDLED_EXCEPTION: case REASON_PROMISE_REJECTION: case REASON_ANR: - return new SeverityReason(severityReasonType, Severity.ERROR, true, null); + return new SeverityReason(reason, ERROR, true, true, null, null); case REASON_STRICT_MODE: - return new SeverityReason(severityReasonType, Severity.WARNING, true, attrVal); + return new SeverityReason(reason, WARNING, true, true, attrVal, "violationType"); + case REASON_HANDLED_ERROR: case REASON_HANDLED_EXCEPTION: - return new SeverityReason(severityReasonType, Severity.WARNING, false, null); + return new SeverityReason(reason, WARNING, false, false, null, null); case REASON_USER_SPECIFIED: case REASON_CALLBACK_SPECIFIED: - return new SeverityReason(severityReasonType, severity, false, null); + return new SeverityReason(reason, severity, false, false, null, null); case REASON_LOG: - return new SeverityReason(severityReasonType, severity, false, attrVal); + return new SeverityReason(reason, severity, false, false, attrVal, "level"); default: - String msg = "Invalid argument for severityReason: '" + severityReasonType + '\''; + String msg = "Invalid argument for severityReason: '" + reason + '\''; throw new IllegalArgumentException(msg); } } - SeverityReason(String severityReasonType, Severity currentSeverity, boolean unhandled, - @Nullable String attributeValue) { - this(severityReasonType, currentSeverity, unhandled, unhandled, attributeValue); - } - - SeverityReason(String severityReasonType, Severity currentSeverity, boolean unhandled, - boolean originalUnhandled, @Nullable String attributeValue) { + SeverityReason(String severityReasonType, + Severity currentSeverity, + boolean unhandled, + boolean originalUnhandled, + @Nullable String attributeValue, + @Nullable String attributeKey) { this.severityReasonType = severityReasonType; this.unhandled = unhandled; this.originalUnhandled = originalUnhandled; this.defaultSeverity = currentSeverity; this.currentSeverity = currentSeverity; this.attributeValue = attributeValue; + this.attributeKey = attributeKey; } String calculateSeverityReasonType() { @@ -118,6 +126,10 @@ final class SeverityReason implements JsonStream.Streamable { return attributeValue; } + String getAttributeKey() { + return attributeKey; + } + void setCurrentSeverity(Severity severity) { this.currentSeverity = severity; } @@ -132,25 +144,11 @@ final class SeverityReason implements JsonStream.Streamable { .name("type").value(calculateSeverityReasonType()) .name("unhandledOverridden").value(getUnhandledOverridden()); - if (attributeValue != null) { - String attributeKey = null; - switch (severityReasonType) { - case REASON_LOG: - attributeKey = "level"; - break; - case REASON_STRICT_MODE: - attributeKey = "violationType"; - break; - default: - break; - } - if (attributeKey != null) { - writer.name("attributes").beginObject() + if (attributeKey != null && attributeValue != null) { + writer.name("attributes").beginObject() .name(attributeKey).value(attributeValue) .endObject(); - } } writer.endObject(); } - -} +} \ No newline at end of file diff --git a/app/src/main/java/com/bugsnag/android/Stackframe.kt b/app/src/main/java/com/bugsnag/android/Stackframe.kt index 8db78bf67b..3e4e063a76 100644 --- a/app/src/main/java/com/bugsnag/android/Stackframe.kt +++ b/app/src/main/java/com/bugsnag/android/Stackframe.kt @@ -10,28 +10,16 @@ class Stackframe : JsonStream.Streamable { * The name of the method that was being executed */ var method: String? - set(value) { - nativeFrame?.method = value - field = value - } /** * The location of the source file */ var file: String? - set(value) { - nativeFrame?.file = value - field = value - } /** * The line number within the source file this stackframe refers to */ var lineNumber: Number? - set(value) { - nativeFrame?.lineNumber = value - field = value - } /** * Whether the package is considered to be in your project for the purposes of grouping and @@ -50,14 +38,30 @@ class Stackframe : JsonStream.Streamable { */ var columnNumber: Number? + /** + * The address of the instruction where the event occurred. + */ + var frameAddress: Long? = null + + /** + * The address of the function where the event occurred. + */ + var symbolAddress: Long? = null + + /** + * The address of the library where the event occurred. + */ + var loadAddress: Long? = null + + /** + * Whether this frame identifies the program counter + */ + var isPC: Boolean? = null + /** * The type of the error */ var type: ErrorType? = null - set(value) { - nativeFrame?.type = value - field = value - } @JvmOverloads internal constructor( @@ -76,34 +80,52 @@ class Stackframe : JsonStream.Streamable { this.columnNumber = columnNumber } - private var nativeFrame: NativeStackframe? = null - constructor(nativeFrame: NativeStackframe) : this( nativeFrame.method, nativeFrame.file, nativeFrame.lineNumber, - false, + null, null ) { - this.nativeFrame = nativeFrame + this.frameAddress = nativeFrame.frameAddress + this.symbolAddress = nativeFrame.symbolAddress + this.loadAddress = nativeFrame.loadAddress + this.isPC = nativeFrame.isPC this.type = nativeFrame.type } + internal constructor(json: Map) { + method = json["method"] as? String + file = json["file"] as? String + lineNumber = json["lineNumber"] as? Number + inProject = json["inProject"] as? Boolean + columnNumber = json["columnNumber"] as? Number + frameAddress = (json["frameAddress"] as? Number)?.toLong() + symbolAddress = (json["symbolAddress"] as? Number)?.toLong() + loadAddress = (json["loadAddress"] as? Number)?.toLong() + isPC = json["isPC"] as? Boolean + + @Suppress("UNCHECKED_CAST") + code = json["code"] as? Map + type = (json["type"] as? String)?.let { ErrorType.fromDescriptor(it) } + } + @Throws(IOException::class) override fun toStream(writer: JsonStream) { - val ndkFrame = nativeFrame - if (ndkFrame != null) { - ndkFrame.toStream(writer) - return - } - writer.beginObject() writer.name("method").value(method) writer.name("file").value(file) writer.name("lineNumber").value(lineNumber) - writer.name("inProject").value(inProject) + + inProject?.let { writer.name("inProject").value(it) } + writer.name("columnNumber").value(columnNumber) + frameAddress?.let { writer.name("frameAddress").value(it) } + symbolAddress?.let { writer.name("symbolAddress").value(it) } + loadAddress?.let { writer.name("loadAddress").value(it) } + isPC?.let { writer.name("isPC").value(it) } + type?.let { writer.name("type").value(it.desc) } diff --git a/app/src/main/java/com/bugsnag/android/StateEvent.kt b/app/src/main/java/com/bugsnag/android/StateEvent.kt index 83ba82108e..729c9b9fb7 100644 --- a/app/src/main/java/com/bugsnag/android/StateEvent.kt +++ b/app/src/main/java/com/bugsnag/android/StateEvent.kt @@ -68,4 +68,15 @@ sealed class StateEvent { // JvmField allows direct field access optimizations @JvmField val memoryTrimLevel: Int? = null, @JvmField val memoryTrimLevelDescription: String = "None" ) : StateEvent() + + class AddFeatureFlag( + @JvmField val name: String, + @JvmField val variant: String? = null + ) : StateEvent() + + class ClearFeatureFlag( + @JvmField val name: String + ) : StateEvent() + + object ClearFeatureFlags : StateEvent() } diff --git a/app/src/main/java/com/bugsnag/android/Thread.java b/app/src/main/java/com/bugsnag/android/Thread.java index d6d5ee73fe..c93745e613 100644 --- a/app/src/main/java/com/bugsnag/android/Thread.java +++ b/app/src/main/java/com/bugsnag/android/Thread.java @@ -23,7 +23,13 @@ public class Thread implements JsonStream.Streamable { @NonNull Thread.State state, @NonNull Stacktrace stacktrace, @NonNull Logger logger) { - this.impl = new ThreadInternal(id, name, type, errorReportingThread, state, stacktrace); + this.impl = new ThreadInternal( + id, name, type, errorReportingThread, state.getDescriptor(), stacktrace); + this.logger = logger; + } + + Thread(@NonNull ThreadInternal impl, @NonNull Logger logger) { + this.impl = impl; this.logger = logger; } @@ -88,7 +94,7 @@ public class Thread implements JsonStream.Streamable { */ public void setState(@NonNull Thread.State threadState) { if (threadState != null) { - impl.setState(threadState); + impl.setState(threadState.getDescriptor()); } else { logNull("state"); } @@ -99,7 +105,7 @@ public class Thread implements JsonStream.Streamable { */ @NonNull public Thread.State getState() { - return impl.getState(); + return Thread.State.byDescriptor(impl.getState()); } /** diff --git a/app/src/main/java/com/bugsnag/android/ThreadInternal.kt b/app/src/main/java/com/bugsnag/android/ThreadInternal.kt index efacfb5644..fbb9457b87 100644 --- a/app/src/main/java/com/bugsnag/android/ThreadInternal.kt +++ b/app/src/main/java/com/bugsnag/android/ThreadInternal.kt @@ -7,7 +7,7 @@ class ThreadInternal internal constructor( var name: String, var type: ThreadType, val isErrorReportingThread: Boolean, - var state: Thread.State, + var state: String, stacktrace: Stacktrace ) : JsonStream.Streamable { @@ -19,7 +19,7 @@ class ThreadInternal internal constructor( writer.name("id").value(id) writer.name("name").value(name) writer.name("type").value(type.desc) - writer.name("state").value(state.descriptor) + writer.name("state").value(state) writer.name("stacktrace") writer.beginArray() diff --git a/app/src/main/java/com/bugsnag/android/ThreadType.kt b/app/src/main/java/com/bugsnag/android/ThreadType.kt index 2769447f4d..c1c3cbb5d7 100644 --- a/app/src/main/java/com/bugsnag/android/ThreadType.kt +++ b/app/src/main/java/com/bugsnag/android/ThreadType.kt @@ -18,5 +18,9 @@ enum class ThreadType(internal val desc: String) { /** * A thread captured from JavaScript */ - REACTNATIVEJS("reactnativejs") + REACTNATIVEJS("reactnativejs"); + + internal companion object { + internal fun fromDescriptor(desc: String) = ThreadType.values().find { it.desc == desc } + } } diff --git a/app/src/main/java/com/bugsnag/android/internal/FallbackWriter.kt b/app/src/main/java/com/bugsnag/android/internal/FallbackWriter.kt new file mode 100644 index 0000000000..fd32beeb33 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/internal/FallbackWriter.kt @@ -0,0 +1,29 @@ +package com.bugsnag.android.internal + +import com.bugsnag.android.ObjectJsonStreamer +import com.bugsnag.android.repackaged.dslplatform.json.DslJson +import java.io.InputStream +import java.io.OutputStream +import java.lang.reflect.Type + +internal class FallbackWriter : DslJson.Fallback> { + + private val placeholder = "\"${ObjectJsonStreamer.OBJECT_PLACEHOLDER}\"".toByteArray() + + override fun serialize(instance: Any?, stream: OutputStream) { + stream.write(placeholder) + } + + override fun deserialize( + context: MutableMap?, + manifest: Type, + body: ByteArray, + size: Int + ): Any = throw UnsupportedOperationException() + + override fun deserialize( + context: MutableMap?, + manifest: Type, + stream: InputStream + ): Any = throw UnsupportedOperationException() +} diff --git a/app/src/main/java/com/bugsnag/android/internal/JsonHelper.kt b/app/src/main/java/com/bugsnag/android/internal/JsonHelper.kt new file mode 100644 index 0000000000..8493d47895 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/internal/JsonHelper.kt @@ -0,0 +1,79 @@ +package com.bugsnag.android.internal + +import com.bugsnag.android.repackaged.dslplatform.json.DslJson +import com.bugsnag.android.repackaged.dslplatform.json.JsonWriter +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.util.Date + +internal object JsonHelper { + + // ignore deprecation warnings as there is no other API that allows + // serializing a placeholder for a type and all its subtypes + @Suppress("deprecation") + private val settings = DslJson.Settings>().fallbackTo(FallbackWriter()) + + // Only one global DslJson is needed, and is thread-safe + // Note: dsl-json adds about 150k to the final binary size. + private val dslJson = DslJson(settings) + + init { + dslJson.registerWriter(Date::class.java) { writer: JsonWriter, value: Date? -> + value?.let { + val timestamp = DateUtils.toIso8601(it) + writer.writeString(timestamp) + } + } + } + + fun serialize(value: Any, stream: OutputStream) { + dslJson.serialize(value, stream) + } + + fun serialize(value: Any, file: File) { + val parentFile = file.parentFile + if (parentFile != null && !parentFile.exists()) { + if (!parentFile.mkdirs()) { + throw FileSystemException(file, null, "Could not create parent dirs of file") + } + } + try { + FileOutputStream(file).use { stream -> dslJson.serialize(value, stream) } + } catch (ex: IOException) { + throw IOException("Could not serialize JSON document to $file", ex) + } + } + + fun deserialize(bytes: ByteArray): MutableMap { + val document = dslJson.deserialize( + MutableMap::class.java, + bytes, + bytes.size + ) + requireNotNull(document) { "JSON document is invalid" } + @Suppress("UNCHECKED_CAST") + return document as MutableMap + } + + fun deserialize(stream: InputStream): MutableMap { + val document = dslJson.deserialize(MutableMap::class.java, stream) + requireNotNull(document) { "JSON document is invalid" } + @Suppress("UNCHECKED_CAST") + return document as MutableMap + } + + fun deserialize(file: File): MutableMap { + try { + FileInputStream(file).use { stream -> return deserialize(stream) } + } catch (ex: FileNotFoundException) { + throw ex + } catch (ex: IOException) { + throw IOException("Could not deserialize from $file", ex) + } + } +} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/Base64.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/Base64.java new file mode 100644 index 0000000000..64b9e084c9 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/Base64.java @@ -0,0 +1,190 @@ +package com.bugsnag.android.repackaged.dslplatform.json; + +import java.util.Arrays; + +/** A very fast and memory efficient class to encode and decode to and from BASE64 in full accordance + * with RFC 2045.

+ * On Windows XP sp1 with 1.4.2_04 and later ;), this encoder and decoder is about 10 times faster + * on small arrays (10 - 1000 bytes) and 2-3 times as fast on larger arrays (10000 - 1000000 bytes) + * compared to sun.misc.Encoder()/Decoder().

+ * + * On byte arrays the encoder is about 20% faster than Jakarta Commons Base64 Codec for encode and + * about 50% faster for decoding large arrays. This implementation is about twice as fast on very small + * arrays (< 30 bytes). If source/destination is a String this + * version is about three times as fast due to the fact that the Commons Codec result has to be recoded + * to a String from byte[], which is very expensive.

+ * + * This encode/decode algorithm doesn't create any temporary arrays as many other codecs do, it only + * allocates the resulting array. This produces less garbage and it is possible to handle arrays twice + * as large as algorithms that create a temporary array. (E.g. Jakarta Commons Codec). It is unknown + * whether Sun's sun.misc.Encoder()/Decoder() produce temporary arrays but since performance + * is quite low it probably does.

+ * + * The encoder produces the same output as the Sun one except that the Sun's encoder appends + * a trailing line separator if the last character isn't a pad. Unclear why but it only adds to the + * length and is probably a side effect. Both are in conformance with RFC 2045 though.
+ * Commons codec seem to always att a trailing line separator.

+ * + * Note! + * The encode/decode method pairs (types) come in three versions with the exact same algorithm and + * thus a lot of code redundancy. This is to not create any temporary arrays for transcoding to/from different + * format types. The methods not used can simply be commented out.

+ * + * There is also a "fast" version of all decode methods that works the same way as the normal ones, but + * har a few demands on the decoded input. Normally though, these fast verions should be used if the source if + * the input is known and it hasn't bee tampered with.

+ * + * If you find the code useful or you find a bug, please send me a note at base64 @ miginfocom . com. + * + * Licence (BSD): + * ============== + * + * Copyright (c) 2004, Mikael Grev, MiG InfoCom AB. (base64 @ miginfocom . com) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * Neither the name of the MiG InfoCom AB nor the names of its contributors may be + * used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + * + * @version 2.2 + * @author Mikael Grev + * Date: 2004-aug-02 + * Time: 11:31:11 + */ + +abstract class Base64 { + private static final char[] CA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray(); + private static final byte[] BA; + private static final int[] IA = new int[256]; + static { + Arrays.fill(IA, -1); + for (int i = 0, iS = CA.length; i < iS; i++) { + IA[CA[i]] = i; + } + IA['='] = 0; + BA = new byte[CA.length]; + for (int i = 0; i < CA.length; i++) { + BA[i] = (byte)CA[i]; + } + } + + static int encodeToBytes(byte[] sArr, byte[] dArr, final int start) { + final int sLen = sArr.length; + + final int eLen = (sLen / 3) * 3; // Length of even 24-bits. + final int dLen = ((sLen - 1) / 3 + 1) << 2; // Returned character count + + // Encode even 24-bits + for (int s = 0, d = start; s < eLen;) { + // Copy next three bytes into lower 24 bits of int, paying attension to sign. + int i = (sArr[s++] & 0xff) << 16 | (sArr[s++] & 0xff) << 8 | (sArr[s++] & 0xff); + + // Encode the int into four chars + dArr[d++] = BA[(i >>> 18) & 0x3f]; + dArr[d++] = BA[(i >>> 12) & 0x3f]; + dArr[d++] = BA[(i >>> 6) & 0x3f]; + dArr[d++] = BA[i & 0x3f]; + } + + // Pad and encode last bits if source isn't even 24 bits. + int left = sLen - eLen; // 0 - 2. + if (left > 0) { + // Prepare the int + int i = ((sArr[eLen] & 0xff) << 10) | (left == 2 ? ((sArr[sLen - 1] & 0xff) << 2) : 0); + + // Set last four chars + dArr[start + dLen - 4] = BA[i >> 12]; + dArr[start + dLen - 3] = BA[(i >>> 6) & 0x3f]; + dArr[start + dLen - 2] = left == 2 ? BA[i & 0x3f] : (byte)'='; + dArr[start + dLen - 1] = '='; + } + + return dLen; + } + + static int findEnd(final byte[] sArr, final int start) { + for (int i = start; i < sArr.length; i++) + if (IA[sArr[i] & 0xff] < 0) + return i; + return sArr.length; + } + + private final static byte[] EMPTY_ARRAY = new byte[0]; + + static byte[] decodeFast(final byte[] sArr, final int start, final int end) { + // Check special case + int sLen = end - start; + if (sLen == 0) + return EMPTY_ARRAY; + + int sIx = start, eIx = end - 1; // Start and end index after trimming. + + // Trim illegal chars from start + while (sIx < eIx && IA[sArr[sIx] & 0xff] < 0) { + sIx++; + } + + // Trim illegal chars from end + while (eIx > 0 && IA[sArr[eIx] & 0xff] < 0) { + eIx--; + } + + // get the padding count (=) (0, 1 or 2) + final int pad = sArr[eIx] == '=' ? (sArr[eIx - 1] == '=' ? 2 : 1) : 0; // Count '=' at end. + final int cCnt = eIx - sIx + 1; // Content count including possible separators + final int sepCnt = sLen > 76 ? (sArr[76] == '\r' ? cCnt / 78 : 0) << 1 : 0; + + final int len = ((cCnt - sepCnt) * 6 >> 3) - pad; // The number of decoded bytes + final byte[] dArr = new byte[len]; // Preallocate byte[] of exact length + + // Decode all but the last 0 - 2 bytes. + int d = 0; + for (int cc = 0, eLen = (len / 3) * 3; d < eLen;) { + // Assemble three bytes into an int from four "valid" characters. + int i = IA[sArr[sIx++]] << 18 | IA[sArr[sIx++]] << 12 | IA[sArr[sIx++]] << 6 | IA[sArr[sIx++]]; + + // Add the bytes + dArr[d++] = (byte) (i >> 16); + dArr[d++] = (byte) (i >> 8); + dArr[d++] = (byte) i; + + // If line separator, jump over it. + if (sepCnt > 0 && ++cc == 19) { + sIx += 2; + cc = 0; + } + } + + if (d < len) { + // Decode last 1-3 bytes (incl '=') into 1-3 bytes + int i = 0; + for (int j = 0; sIx <= eIx - pad; j++) { + i |= IA[sArr[sIx++]] << (18 - j * 6); + } + + for (int r = 16; d < len; r -= 8) { + dArr[d++] = (byte) (i >> r); + } + } + + return dArr; + } +} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/BinaryConverter.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/BinaryConverter.java new file mode 100644 index 0000000000..34fbc2949f --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/BinaryConverter.java @@ -0,0 +1,57 @@ +package com.bugsnag.android.repackaged.dslplatform.json; + +import androidx.annotation.Nullable; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; + +@SuppressWarnings({"rawtypes", "unchecked"}) // suppress pre-existing warnings +public abstract class BinaryConverter { + + static final JsonReader.ReadObject Base64Reader = new JsonReader.ReadObject() { + @Nullable + @Override + public byte[] read(JsonReader reader) throws IOException { + return reader.wasNull() ? null : deserialize(reader); + } + }; + static final JsonWriter.WriteObject Base64Writer = new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable byte[] value) { + serialize(value, writer); + } + }; + + public static void serialize(@Nullable final byte[] value, final JsonWriter sw) { + if (value == null) { + sw.writeNull(); + } else if (value.length == 0) { + sw.writeAscii("\"\""); + } else { + sw.writeBinary(value); + } + } + + public static byte[] deserialize(final JsonReader reader) throws IOException { + return reader.readBase64(); + } + + @SuppressWarnings("unchecked") + public static ArrayList deserializeCollection(final JsonReader reader) throws IOException { + return reader.deserializeCollection(Base64Reader); + } + + public static void deserializeCollection(final JsonReader reader, final Collection res) throws IOException { + reader.deserializeCollection(Base64Reader, res); + } + + @SuppressWarnings("unchecked") + public static ArrayList deserializeNullableCollection(final JsonReader reader) throws IOException { + return reader.deserializeNullableCollection(Base64Reader); + } + + public static void deserializeNullableCollection(final JsonReader reader, final Collection res) throws IOException { + reader.deserializeNullableCollection(Base64Reader, res); + } +} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/BoolConverter.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/BoolConverter.java new file mode 100644 index 0000000000..cd8b7e41cc --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/BoolConverter.java @@ -0,0 +1,128 @@ +package com.bugsnag.android.repackaged.dslplatform.json; + +import androidx.annotation.Nullable; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +@SuppressWarnings({"rawtypes", "unchecked"}) // suppress pre-existing warnings +public abstract class BoolConverter { + + public final static boolean[] EMPTY_ARRAY = new boolean[0]; + + public static final JsonReader.ReadObject READER = new JsonReader.ReadObject() { + @Override + public Boolean read(JsonReader reader) throws IOException { + return deserialize(reader); + } + }; + public static final JsonReader.ReadObject NULLABLE_READER = new JsonReader.ReadObject() { + @Nullable + @Override + public Boolean read(JsonReader reader) throws IOException { + return reader.wasNull() ? null : deserialize(reader); + } + }; + public static final JsonWriter.WriteObject WRITER = new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable Boolean value) { + serializeNullable(value, writer); + } + }; + public static final JsonReader.ReadObject ARRAY_READER = new JsonReader.ReadObject() { + @Nullable + @Override + public boolean[] read(JsonReader reader) throws IOException { + if (reader.wasNull()) return null; + if (reader.last() != '[') throw reader.newParseError("Expecting '[' for boolean array start"); + reader.getNextToken(); + return deserializeBoolArray(reader); + } + }; + public static final JsonWriter.WriteObject ARRAY_WRITER = new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable boolean[] value) { + serialize(value, writer); + } + }; + + public static void serializeNullable(@Nullable final Boolean value, final JsonWriter sw) { + if (value == null) { + sw.writeNull(); + } else if (value) { + sw.writeAscii("true"); + } else { + sw.writeAscii("false"); + } + } + + public static void serialize(final boolean value, final JsonWriter sw) { + if (value) { + sw.writeAscii("true"); + } else { + sw.writeAscii("false"); + } + } + + public static void serialize(@Nullable final boolean[] value, final JsonWriter sw) { + if (value == null) { + sw.writeNull(); + } else if (value.length == 0) { + sw.writeAscii("[]"); + } else { + sw.writeByte(JsonWriter.ARRAY_START); + sw.writeAscii(value[0] ? "true" : "false"); + for(int i = 1; i < value.length; i++) { + sw.writeAscii(value[i] ? ",true" : ",false"); + } + sw.writeByte(JsonWriter.ARRAY_END); + } + } + + public static boolean deserialize(final JsonReader reader) throws IOException { + if (reader.wasTrue()) { + return true; + } else if (reader.wasFalse()) { + return false; + } + throw reader.newParseErrorAt("Found invalid boolean value", 0); + } + + public static boolean[] deserializeBoolArray(final JsonReader reader) throws IOException { + if (reader.last() == ']') { + return EMPTY_ARRAY; + } + boolean[] buffer = new boolean[4]; + buffer[0] = deserialize(reader); + int i = 1; + while (reader.getNextToken() == ',') { + reader.getNextToken(); + if (i == buffer.length) { + buffer = Arrays.copyOf(buffer, buffer.length << 1); + } + buffer[i++] = deserialize(reader); + } + reader.checkArrayEnd(); + return Arrays.copyOf(buffer, i); + } + + @SuppressWarnings("unchecked") + public static ArrayList deserializeCollection(final JsonReader reader) throws IOException { + return reader.deserializeCollection(READER); + } + + public static void deserializeCollection(final JsonReader reader, final Collection res) throws IOException { + reader.deserializeCollection(READER, res); + } + + @SuppressWarnings("unchecked") + public static ArrayList deserializeNullableCollection(final JsonReader reader) throws IOException { + return reader.deserializeNullableCollection(READER); + } + + public static void deserializeNullableCollection(final JsonReader reader, final Collection res) throws IOException { + reader.deserializeNullableCollection(READER, res); + } +} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/Configuration.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/Configuration.java new file mode 100644 index 0000000000..a5ff40f99a --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/Configuration.java @@ -0,0 +1,16 @@ +package com.bugsnag.android.repackaged.dslplatform.json; + +/** + * Configuration API for setting up readers/writers during library initialization. + * DslJson will use ServiceLoader.load(Configuration.class) in default constructor. + * This will load services registered in META-INF/services/com.bugsnag.dslplatform.json.Configuration file. + */ +@SuppressWarnings("rawtypes") // suppress pre-existing warnings +public interface Configuration { + /** + * Configure library instance with appropriate readers/writers/etc... + * + * @param json library instance + */ + void configure(DslJson json); +} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/ConfigurationException.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/ConfigurationException.java new file mode 100644 index 0000000000..ae10ac9207 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/ConfigurationException.java @@ -0,0 +1,16 @@ +package com.bugsnag.android.repackaged.dslplatform.json; + +@SuppressWarnings("serial") // suppress pre-existing warnings +public class ConfigurationException extends RuntimeException { + public ConfigurationException(String reason) { + super(reason); + } + + public ConfigurationException(Throwable cause) { + super(cause); + } + + public ConfigurationException(String reason, Throwable cause) { + super(reason, cause); + } +} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/DslJson.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/DslJson.java new file mode 100644 index 0000000000..a12a6f92a5 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/DslJson.java @@ -0,0 +1,2908 @@ +package com.bugsnag.android.repackaged.dslplatform.json; + +import androidx.annotation.Nullable; + +import org.w3c.dom.Element; + +import java.io.*; +import java.lang.annotation.Annotation; +import java.lang.reflect.*; +import java.math.BigDecimal; +import java.net.InetAddress; +import java.net.URI; +import java.nio.charset.Charset; +import java.util.*; +import java.util.concurrent.*; + +/** + * Main DSL-JSON class. + * Easiest way to use the library is to create an DslJson<Object> instance and reuse it within application. + * DslJson has optional constructor for specifying default readers/writers. + *

+ * During initialization DslJson will use ServiceLoader API to load registered services. + * This is done through `META-INF/services/com.dslplatform.json.CompiledJson` file. + *

+ * DslJson can fallback to another serializer in case when it doesn't know how to handle specific type. + * This can be specified by Fallback interface during initialization. + *

+ * If you wish to use compile time databinding @CompiledJson annotation must be specified on the target class + * or implicit reference to target class must exists from a class with @CompiledJson annotation. + *

+ * Usage example: + *

+ *     DslJson<Object> dsl = new DslJson<>();
+ *     dsl.serialize(instance, OutputStream);
+ *     POJO pojo = dsl.deserialize(POJO.class, InputStream);
+ * 
+ *

+ * For best performance use serialization API with JsonWriter and byte[] as target. + * JsonWriter is reused via thread local variable. When custom JsonWriter's are used, reusing them will yield maximum performance. + * JsonWriter can be reused via reset methods. + * For best deserialization performance prefer byte[] API instead of InputStream API. + * JsonReader is reused via thread local variable. When custom JsonReaders are used, reusing them will yield maximum performance. + * JsonReader can be reused via process methods. + *

+ * During deserialization TContext can be used to pass data into deserialized classes. + * This is useful when deserializing domain objects which require state or service provider. + * For example DSL Platform entities require service locator to be able to perform lazy load. + *

+ * DslJson doesn't have a String or Reader API since it's optimized for processing bytes and streams. + * If you wish to process String, use String.getBytes("UTF-8") as argument for DslJson. + * Only UTF-8 is supported for encoding and decoding JSON. + *

+ *     DslJson<Object> dsl = new DslJson<>();
+ *     JsonWriter writer = dsl.newWriter();
+ *     dsl.serialize(writer, instance);
+ *     String json = writer.toString(); //JSON as string - avoid using JSON as Strings whenever possible
+ *     byte[] input = json.getBytes("UTF-8");
+ *     POJO pojo = dsl.deserialize(POJO.class, input, input.length);
+ * 
+ * + * @param used for library specialization. If unsure, use Object + */ +@SuppressWarnings({"rawtypes", "unchecked"}) // suppress pre-existing warnings +public class DslJson implements UnknownSerializer, TypeLookup { + + private static final Charset UTF8 = Charset.forName("UTF-8"); + private static final Object unknownValue = new Object(); + + /** + * The context of this instance. + * Can be used for library specialization + */ + @Nullable + public final TContext context; + @Nullable + protected final Fallback fallback; + /** + * Should properties with default values be omitted from the resulting JSON? + * This will leave out nulls, empty collections, zeros and other attributes with default values + * which can be reconstructed from schema information + */ + public final boolean omitDefaults; + /** + * When object supports array format, eg. [prop1, prop2, prop3] this value must be enabled before + * object will be serialized in such a way. Regardless of this value deserialization will support all formats. + */ + public final boolean allowArrayFormat; + + protected final StringCache keyCache; + protected final StringCache valuesCache; + protected final List> writerFactories = new CopyOnWriteArrayList>(); + private final int settingsWriters; + protected final List> readerFactories = new CopyOnWriteArrayList>(); + private final int settingsReaders; + protected final List> binderFactories = new CopyOnWriteArrayList>(); + private final int settingsBinders; + private final JsonReader.ErrorInfo errorInfo; + private final JsonReader.DoublePrecision doublePrecision; + private final JsonReader.UnknownNumberParsing unknownNumbers; + private final int maxNumberDigits; + private final int maxStringSize; + protected final ThreadLocal localWriter; + protected final ThreadLocal localReader; + private final ExternalConverterAnalyzer externalConverterAnalyzer; + private final Map, Boolean> creatorMarkers; + + public interface Fallback { + void serialize(@Nullable Object instance, OutputStream stream) throws IOException; + + @Nullable + Object deserialize(@Nullable TContext context, Type manifest, byte[] body, int size) throws IOException; + + @Nullable + Object deserialize(@Nullable TContext context, Type manifest, InputStream stream) throws IOException; + } + + public interface ConverterFactory { + @Nullable + T tryCreate(Type manifest, DslJson dslJson); + } + + /** + * Configuration for DslJson options. + * By default key cache is enabled. Everything else is not configured. + * To load `META-INF/services` call `includeServiceLoader()` + * + * @param DslJson context + */ + public static class Settings { + private TContext context; + private boolean javaSpecifics; + private Fallback fallback; + private boolean omitDefaults; + private boolean allowArrayFormat; + private StringCache keyCache = new SimpleStringCache(); + private StringCache valuesCache; + private int fromServiceLoader; + private JsonReader.ErrorInfo errorInfo = JsonReader.ErrorInfo.WITH_STACK_TRACE; + private JsonReader.DoublePrecision doublePrecision = JsonReader.DoublePrecision.DEFAULT; + private JsonReader.UnknownNumberParsing unknownNumbers = JsonReader.UnknownNumberParsing.LONG_AND_BIGDECIMAL; + private int maxNumberDigits = 512; + private int maxStringBuffer = 128 * 1024 * 1024; + private final List configurations = new ArrayList(); + private final List> writerFactories = new ArrayList>(); + private final List> readerFactories = new ArrayList>(); + private final List> binderFactories = new ArrayList>(); + private final Set classLoaders = new HashSet(); + private final Map, Boolean> creatorMarkers = new HashMap, Boolean>(); + + /** + * Pass in context for DslJson. + * Context will be available in JsonReader for objects which needs it. + * + * @param context context propagated to JsonReaders + * @return itself + */ + public Settings withContext(@Nullable TContext context) { + this.context = context; + return this; + } + + /** + * Enable converters for Java specific types (Graphics API) not available on Android. + * + * @param javaSpecifics should register Java specific converters + * @return itself + */ + public Settings withJavaConverters(boolean javaSpecifics) { + this.javaSpecifics = javaSpecifics; + return this; + } + + /** + * Will be eventually replaced with writer/reader factories. + * Used by DslJson to call into when trying to serialize/deserialize object which is not supported. + * + * @param fallback how to handle unsupported type + * @return which fallback to use in case of unsupported type + */ + @Deprecated + public Settings fallbackTo(@Nullable Fallback fallback) { + this.fallback = fallback; + return this; + } + + /** + * DslJson can exclude some properties from resulting JSON which it can reconstruct fully from schema information. + * Eg. int with value 0 can be omitted since that is default value for the type. + * Null values can be excluded since they are handled the same way as missing property. + * + * @param omitDefaults should exclude default values from resulting JSON + * @return itself + */ + public Settings skipDefaultValues(boolean omitDefaults) { + this.omitDefaults = omitDefaults; + return this; + } + + /** + * Some encoders/decoders support writing objects in array format. + * For encoder to write objects in such format, Array format must be defined before the Default and minified formats + * and array format must be allowed via this setting. + * If objects support multiple formats decoding will work regardless of this setting. + * + * @param allowArrayFormat allow serialization via array format + * @return itself + */ + public Settings allowArrayFormat(boolean allowArrayFormat) { + this.allowArrayFormat = allowArrayFormat; + return this; + } + + /** + * Use specific key cache implementation. + * Key cache is enabled by default and it's used when deserializing unstructured objects such as Map<String, Object> + * to avoid allocating new String key instance. Instead StringCache will provide a new or an old instance. + * This improves memory usage and performance since there is usually small number of keys. + * It does have some performance overhead, but this is dependant on the implementation. + *

+ * To disable key cache, provide null for it. + * + * @param keyCache which key cache to use + * @return itself + */ + public Settings useKeyCache(@Nullable StringCache keyCache) { + this.keyCache = keyCache; + return this; + } + + /** + * Use specific string values cache implementation. + * By default string values cache is disabled. + *

+ * To support memory restricted scenarios where there is limited number of string values, + * values cache can be used. + *

+ * Not every "JSON string" will use this cache... eg UUID, LocalDate don't create an instance of string + * and therefore don't use this cache. + * + * @param valuesCache which values cache to use + * @return itself + */ + public Settings useStringValuesCache(@Nullable StringCache valuesCache) { + this.valuesCache = valuesCache; + return this; + } + + /** + * DslJson will iterate over converter factories when requested type is unknown. + * Registering writer converter factory allows for constructing JSON converter lazily. + * + * @param writer registered writer factory + * @return itself + */ + @SuppressWarnings("unchecked") + public Settings resolveWriter(ConverterFactory writer) { + if (writer == null) throw new IllegalArgumentException("writer can't be null"); + if (writerFactories.contains(writer)) { + throw new IllegalArgumentException("writer already registered"); + } + writerFactories.add((ConverterFactory) writer); + return this; + } + + /** + * DslJson will iterate over converter factories when requested type is unknown. + * Registering reader converter factory allows for constructing JSON converter lazily. + * + * @param reader registered reader factory + * @return itself + */ + @SuppressWarnings("unchecked") + public Settings resolveReader(ConverterFactory reader) { + if (reader == null) throw new IllegalArgumentException("reader can't be null"); + if (readerFactories.contains(reader)) { + throw new IllegalArgumentException("reader already registered"); + } + readerFactories.add((ConverterFactory) reader); + return this; + } + + /** + * DslJson will iterate over converter factories when requested type is unknown. + * Registering binder converter factory allows for constructing JSON converter lazily. + * + * @param binder registered binder factory + * @return itself + */ + @SuppressWarnings("unchecked") + public Settings resolveBinder(ConverterFactory binder) { + if (binder == null) throw new IllegalArgumentException("binder can't be null"); + if (binderFactories.contains(binder)) { + throw new IllegalArgumentException("binder already registered"); + } + binderFactories.add((ConverterFactory) binder); + return this; + } + + /** + * Load converters using thread local ClassLoader. + * Will scan through `META-INF/services/com.bugsnag.dslplatform.json.Configuration` file and register implementation during startup. + * This will pick up compile time databindings if they are available in specific folder. + *

+ * Note that gradle on Android has issues with preserving that file, in which case it can be provided manually. + * DslJson will fall back to "expected" class name if it doesn't find anything during scanning. + * + * @return itself + */ + public Settings includeServiceLoader() { + return includeServiceLoader(Thread.currentThread().getContextClassLoader()); + } + + /** + * Load converters using provided `ClassLoader` instance + * Will scan through `META-INF/services/com.bugsnag.dslplatform.json.Configuration` file and register implementation during startup. + * This will pick up compile time databindings if they are available in specific folder. + *

+ * Note that gradle on Android has issues with preserving that file, in which case it can be provided manually. + * DslJson will fall back to "expected" class name if it doesn't find anything during scanning. + * + * @param loader ClassLoader to use + * @return itself + */ + public Settings includeServiceLoader(ClassLoader loader) { + if (loader == null) throw new IllegalArgumentException("loader can't be null"); + classLoaders.add(loader); + for (Configuration c : ServiceLoader.load(Configuration.class, loader)) { + boolean hasConfiguration = false; + Class manifest = c.getClass(); + for (Configuration cur : configurations) { + if (cur.getClass() == manifest) { + hasConfiguration = true; + break; + } + } + if (!hasConfiguration) { + fromServiceLoader++; + configurations.add(c); + } + } + return this; + } + + /** + * By default doubles are not deserialized into an exact value in some rare edge cases. + * + * @param errorInfo information about error in parsing exception + * @return itself + */ + public Settings errorInfo(JsonReader.ErrorInfo errorInfo) { + if (errorInfo == null) throw new IllegalArgumentException("errorInfo can't be null"); + this.errorInfo = errorInfo; + return this; + } + + /** + * By default doubles are not deserialized into an exact value in some rare edge cases. + * + * @param precision type of double deserialization + * @return itself + */ + public Settings doublePrecision(JsonReader.DoublePrecision precision) { + if (precision == null) throw new IllegalArgumentException("precision can't be null"); + this.doublePrecision = precision; + return this; + } + + /** + * When processing JSON without a schema numbers can be deserialized in various ways: + * + * - as longs and decimals + * - as longs and doubles + * - as decimals only + * - as doubles only + * + * Default is as long and BigDecimal + * + * @param unknownNumbers how to deserialize numbers without a schema + * @return itself + */ + public Settings unknownNumbers(JsonReader.UnknownNumberParsing unknownNumbers) { + if (unknownNumbers == null) throw new IllegalArgumentException("unknownNumbers can't be null"); + this.unknownNumbers = unknownNumbers; + return this; + } + + /** + * Specify maximum allowed size for digits buffer. Default is 512. + * Digits buffer is used when processing strange/large input numbers. + * + * @param size maximum allowed size for digit buffer + * @return itself + */ + public Settings limitDigitsBuffer(int size) { + if (size < 1) throw new IllegalArgumentException("size can't be smaller than 1"); + this.maxNumberDigits = size; + return this; + } + + /** + * Specify maximum allowed size for string buffer. Default is 128MB + * To protect against malicious inputs, maximum allowed string buffer can be reduced. + * + * @param size maximum size of buffer in bytes + * @return itself + */ + public Settings limitStringBuffer(int size) { + if (size < 1) throw new IllegalArgumentException("size can't be smaller than 1"); + this.maxStringBuffer = size; + return this; + } + + /** + * When there are multiple constructors, pick the one marked with annotation. + * When markers is allowed on non public targets, attempt at visibility change will be done in runtime. + * + * @param marker annotation used for marking constructor or static method factory + * @param expandVisibility should consider annotation declared on non public accessor + * @return itself + */ + public Settings creatorMarker(Class marker, boolean expandVisibility) { + if (marker == null) throw new IllegalArgumentException("marker can't be null"); + this.creatorMarkers.put(marker, expandVisibility); + return this; + } + + /** + * Configure DslJson with custom Configuration during startup. + * Configurations are extension points for setting up readers/writers during DslJson initialization. + * + * @param conf custom extensibility point + * @return itself + */ + public Settings with(Configuration conf) { + if (conf == null) throw new IllegalArgumentException("conf can't be null"); + configurations.add(conf); + return this; + } + + private Settings with(Iterable confs) { + if (confs != null) { + for (Configuration c : confs) + configurations.add(c); + } + return this; + } + } + + /** + * Simple initialization entry point. + * Will provide null for TContext + * Java graphics readers/writers will not be registered. + * Fallback will not be configured. + * Key cache will be enables, values cache will be disabled. + * Default ServiceLoader.load method will be used to setup services from META-INF + */ + public DslJson() { + this(new Settings().includeServiceLoader()); + } + + /** + * Will be removed. Use DslJson(Settings) instead. + * Fully configurable entry point. + * + * @param context context instance which can be provided to deserialized objects. Use null if not sure + * @param javaSpecifics register Java graphics specific classes such as java.awt.Point, Image, ... + * @param fallback in case of unsupported type, try serialization/deserialization through external API + * @param omitDefaults should serialization produce minified JSON (omit nulls and default values) + * @param keyCache parsed keys can be cached (this is only used in small subset of parsing) + * @param serializers additional serializers/deserializers which will be immediately registered into readers/writers + */ + @Deprecated + public DslJson( + @Nullable final TContext context, + final boolean javaSpecifics, + @Nullable final Fallback fallback, + final boolean omitDefaults, + @Nullable final StringCache keyCache, + final Iterable serializers) { + this(new Settings() + .withContext(context) + .withJavaConverters(javaSpecifics) + .fallbackTo(fallback) + .skipDefaultValues(omitDefaults) + .useKeyCache(keyCache) + .with(serializers) + ); + } + + /** + * Fully configurable entry point. + * Provide settings for DSL-JSON initialization. + * + * @param settings DSL-JSON configuration + */ + public DslJson(final Settings settings) { + if (settings == null) throw new IllegalArgumentException("settings can't be null"); + final DslJson self = this; + this.localWriter = new ThreadLocal() { + @Override + protected JsonWriter initialValue() { + return new JsonWriter(4096, self); + } + }; + this.localReader = new ThreadLocal() { + @Override + protected JsonReader initialValue() { + return new JsonReader(new byte[4096], 4096, self.context, new char[64], self.keyCache, self.valuesCache, self, self.errorInfo, self.doublePrecision, self.unknownNumbers, self.maxNumberDigits, self.maxStringSize); + } + }; + this.context = settings.context; + this.fallback = settings.fallback; + this.omitDefaults = settings.omitDefaults; + this.allowArrayFormat = settings.allowArrayFormat; + this.keyCache = settings.keyCache; + this.valuesCache = settings.valuesCache; + this.unknownNumbers = settings.unknownNumbers; + this.errorInfo = settings.errorInfo; + this.doublePrecision = settings.doublePrecision; + this.maxNumberDigits = settings.maxNumberDigits; + this.maxStringSize = settings.maxStringBuffer; + this.writerFactories.addAll(settings.writerFactories); + this.settingsWriters = settings.writerFactories.size(); + this.readerFactories.addAll(settings.readerFactories); + this.settingsReaders = settings.readerFactories.size(); + this.binderFactories.addAll(settings.binderFactories); + this.settingsBinders = settings.binderFactories.size(); + this.externalConverterAnalyzer = new ExternalConverterAnalyzer(settings.classLoaders); + this.creatorMarkers = new HashMap, Boolean>(settings.creatorMarkers); + + registerReader(byte[].class, BinaryConverter.Base64Reader); + registerWriter(byte[].class, BinaryConverter.Base64Writer); + registerReader(boolean.class, BoolConverter.READER); + registerWriter(boolean.class, BoolConverter.WRITER); + registerDefault(boolean.class, false); + registerReader(boolean[].class, BoolConverter.ARRAY_READER); + registerWriter(boolean[].class, BoolConverter.ARRAY_WRITER); + registerReader(Boolean.class, BoolConverter.NULLABLE_READER); + registerWriter(Boolean.class, BoolConverter.WRITER); + if (settings.javaSpecifics) { + registerJavaSpecifics(this); + } + registerReader(LinkedHashMap.class, ObjectConverter.MapReader); + registerReader(HashMap.class, ObjectConverter.MapReader); + registerReader(Map.class, ObjectConverter.MapReader); + registerWriter(Map.class, new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable Map value) { + if (value == null) { + writer.writeNull(); + } else { + try { + serializeMap(value, writer); + } catch (IOException ex) { + throw new SerializationException(ex); + } + } + } + }); + registerReader(URI.class, NetConverter.UriReader); + registerWriter(URI.class, NetConverter.UriWriter); + registerReader(InetAddress.class, NetConverter.AddressReader); + registerWriter(InetAddress.class, NetConverter.AddressWriter); + registerReader(double.class, NumberConverter.DOUBLE_READER); + registerWriter(double.class, NumberConverter.DOUBLE_WRITER); + registerDefault(double.class, 0.0); + registerReader(double[].class, NumberConverter.DOUBLE_ARRAY_READER); + registerWriter(double[].class, NumberConverter.DOUBLE_ARRAY_WRITER); + registerReader(Double.class, NumberConverter.NULLABLE_DOUBLE_READER); + registerWriter(Double.class, NumberConverter.DOUBLE_WRITER); + registerReader(float.class, NumberConverter.FLOAT_READER); + registerWriter(float.class, NumberConverter.FLOAT_WRITER); + registerDefault(float.class, 0.0f); + registerReader(float[].class, NumberConverter.FLOAT_ARRAY_READER); + registerWriter(float[].class, NumberConverter.FLOAT_ARRAY_WRITER); + registerReader(Float.class, NumberConverter.NULLABLE_FLOAT_READER); + registerWriter(Float.class, NumberConverter.FLOAT_WRITER); + registerReader(int.class, NumberConverter.INT_READER); + registerWriter(int.class, NumberConverter.INT_WRITER); + registerDefault(int.class, 0); + registerReader(int[].class, NumberConverter.INT_ARRAY_READER); + registerWriter(int[].class, NumberConverter.INT_ARRAY_WRITER); + registerReader(Integer.class, NumberConverter.NULLABLE_INT_READER); + registerWriter(Integer.class, NumberConverter.INT_WRITER); + registerReader(short.class, NumberConverter.SHORT_READER); + registerWriter(short.class, NumberConverter.SHORT_WRITER); + registerDefault(short.class, (short)0); + registerReader(short[].class, NumberConverter.SHORT_ARRAY_READER); + registerWriter(short[].class, NumberConverter.SHORT_ARRAY_WRITER); + registerReader(Short.class, NumberConverter.NULLABLE_SHORT_READER); + registerWriter(Short.class, NumberConverter.SHORT_WRITER); + registerReader(long.class, NumberConverter.LONG_READER); + registerWriter(long.class, NumberConverter.LONG_WRITER); + registerDefault(long.class, 0L); + registerReader(long[].class, NumberConverter.LONG_ARRAY_READER); + registerWriter(long[].class, NumberConverter.LONG_ARRAY_WRITER); + registerReader(Long.class, NumberConverter.NULLABLE_LONG_READER); + registerWriter(Long.class, NumberConverter.LONG_WRITER); + registerReader(BigDecimal.class, NumberConverter.DecimalReader); + registerWriter(BigDecimal.class, NumberConverter.DecimalWriter); + registerReader(String.class, StringConverter.READER); + registerWriter(String.class, StringConverter.WRITER); + registerReader(UUID.class, UUIDConverter.READER); + registerWriter(UUID.class, UUIDConverter.WRITER); + registerReader(Number.class, NumberConverter.NumberReader); + registerWriter(CharSequence.class, StringConverter.WRITER_CHARS); + registerReader(StringBuilder.class, StringConverter.READER_BUILDER); + registerReader(StringBuffer.class, StringConverter.READER_BUFFER); + + for (Configuration serializer : settings.configurations) { + serializer.configure(this); + } + if (!settings.classLoaders.isEmpty() && settings.fromServiceLoader == 0) { + //TODO: workaround common issue with failed services registration. try to load common external name if exists + loadDefaultConverters(this, settings.classLoaders, "dsl_json_Annotation_Processor_External_Serialization"); + loadDefaultConverters(this, settings.classLoaders, "dsl_json.json.ExternalSerialization"); + loadDefaultConverters(this, settings.classLoaders, "dsl_json_ExternalSerialization"); + } + } + + /** + * Simplistic string cache implementation. + * It uses a fixed String[] structure in which it caches string value based on it's hash. + * Eg, hash & mask provide index into the structure. Different string with same hash will overwrite the previous one. + */ + public static class SimpleStringCache implements StringCache { + + private final int mask; + private final String[] cache; + + /** + * Will use String[] with 1024 elements. + */ + public SimpleStringCache() { + this(10); + } + + public SimpleStringCache(int log2Size) { + int size = 2; + for (int i = 1; i < log2Size; i++) { + size *= 2; + } + mask = size - 1; + cache = new String[size]; + } + + /** + * Calculates hash of the provided "string" and looks it up from the String[] + * It it doesn't exists of a different string is already there a new String instance is created + * and saved into the String[] + * + * @param chars buffer into which string was parsed + * @param len the string length inside the buffer + * @return String instance matching the char[]/int pair + */ + @Override + public String get(char[] chars, int len) { + long hash = 0x811c9dc5; + for (int i = 0; i < len; i++) { + hash ^= (byte) chars[i]; + hash *= 0x1000193; + } + final int index = (int) hash & mask; + final String value = cache[index]; + if (value == null) return createAndPut(index, chars, len); + if (value.length() != len) return createAndPut(index, chars, len); + for (int i = 0; i < value.length(); i++) { + if (value.charAt(i) != chars[i]) return createAndPut(index, chars, len); + } + return value; + } + + private String createAndPut(int index, char[] chars, int len) { + final String value = new String(chars, 0, len); + cache[index] = value; + return value; + } + } + + /** + * Create a writer bound to this DSL-JSON. + * Ideally it should be reused. + * Bound writer can use lookups to find custom writers. + * This can be used to serialize unknown types such as Object.class + * + * @return bound writer + */ + public JsonWriter newWriter() { + return new JsonWriter(this); + } + + /** + * Create a writer bound to this DSL-JSON. + * Ideally it should be reused. + * Bound writer can use lookups to find custom writers. + * This can be used to serialize unknown types such as Object.class + * + * @param size initial buffer size + * @return bound writer + */ + public JsonWriter newWriter(int size) { + return new JsonWriter(size, this); + } + + /** + * Create a writer bound to this DSL-JSON. + * Ideally it should be reused. + * Bound writer can use lookups to find custom writers. + * This can be used to serialize unknown types such as Object.class + * + * @param buffer initial buffer + * @return bound writer + */ + public JsonWriter newWriter(byte[] buffer) { + if (buffer == null) throw new IllegalArgumentException("null value provided for buffer"); + return new JsonWriter(buffer, this); + } + + /** + * Create a reader bound to this DSL-JSON. + * Bound reader can reuse key cache (which is used during Map deserialization) + * This reader can be reused via process method. + * + * @return bound reader + */ + public JsonReader newReader() { + return new JsonReader(new byte[4096], 4096, context, new char[64], keyCache, valuesCache, this, errorInfo, doublePrecision, unknownNumbers, maxNumberDigits, maxStringSize); + } + + /** + * Create a reader bound to this DSL-JSON. + * Bound reader can reuse key cache (which is used during Map deserialization) + * This reader can be reused via process method. + * + * @param bytes input bytes + * @return bound reader + */ + public JsonReader newReader(byte[] bytes) { + return new JsonReader(bytes, bytes.length, context, new char[64], keyCache, valuesCache, this, errorInfo, doublePrecision, unknownNumbers, maxNumberDigits, maxStringSize); + } + + /** + * Create a reader bound to this DSL-JSON. + * Bound reader can reuse key cache (which is used during Map deserialization) + * This reader can be reused via process method. + * + * @param bytes input bytes + * @param length use input bytes up to specified length + * @return bound reader + */ + public JsonReader newReader(byte[] bytes, int length) { + return new JsonReader(bytes, length, context, new char[64], keyCache, valuesCache, this, errorInfo, doublePrecision, unknownNumbers, maxNumberDigits, maxStringSize); + } + + + /** + * Create a reader bound to this DSL-JSON. + * Bound reader can reuse key cache (which is used during Map deserialization) + * Pass in initial string buffer. + * This reader can be reused via process method. + * + * @param bytes input bytes + * @param length use input bytes up to specified length + * @param tmp string parsing buffer + * @return bound reader + */ + public JsonReader newReader(byte[] bytes, int length, char[] tmp) { + return new JsonReader(bytes, length, context, tmp, keyCache, valuesCache, this, errorInfo, doublePrecision, unknownNumbers, maxNumberDigits, maxStringSize); + } + + /** + * Create a reader bound to this DSL-JSON. + * Bound reader can reuse key cache (which is used during Map deserialization) + * Created reader can be reused (using process method). + * This is convenience method for creating a new reader and binding it to stream. + * + * @param stream input stream + * @param buffer temporary buffer + * @return bound reader + * @throws java.io.IOException unable to read from stream + */ + public JsonReader newReader(InputStream stream, byte[] buffer) throws IOException { + final JsonReader reader = newReader(buffer); + reader.process(stream); + return reader; + } + + /** + * Create a reader bound to this DSL-JSON. + * Bound reader can reuse key cache (which is used during Map deserialization) + * This method id Deprecated since it should be avoided. + * It's better to use byte[] or InputStream based readers + * + * @param input JSON string + * @return bound reader + */ + @Deprecated + public JsonReader newReader(String input) { + final byte[] bytes = input.getBytes(UTF8); + return new JsonReader(bytes, bytes.length, context, new char[64], keyCache, valuesCache, this, errorInfo, doublePrecision, unknownNumbers, maxNumberDigits, maxStringSize); + } + + private static void loadDefaultConverters(final DslJson json, Set loaders, final String name) { + for (ClassLoader loader : loaders) { + try { + Class external = loader.loadClass(name); + Configuration instance = (Configuration) external.newInstance(); + instance.configure(json); + } catch (NoClassDefFoundError ignore) { + } catch (Exception ignore) { + } + } + } + + static void registerJavaSpecifics(final DslJson json) { + json.registerReader(Element.class, XmlConverter.Reader); + json.registerWriter(Element.class, XmlConverter.Writer); + } + + private final Map defaults = new ConcurrentHashMap(); + + public void registerDefault(Class manifest, T instance) { + defaults.put(manifest, instance); + } + + @SuppressWarnings("unchecked") + public boolean registerWriterFactory(ConverterFactory factory) { + if (factory == null) throw new IllegalArgumentException("factory can't be null"); + if (writerFactories.contains(factory)) return false; + writerFactories.add(writerFactories.size() - settingsWriters, (ConverterFactory) factory); + return true; + } + + @SuppressWarnings("unchecked") + public boolean registerReaderFactory(ConverterFactory factory) { + if (factory == null) throw new IllegalArgumentException("factory can't be null"); + if (readerFactories.contains(factory)) return false; + readerFactories.add(readerFactories.size() - settingsReaders, (ConverterFactory) factory); + return true; + } + + @SuppressWarnings("unchecked") + public boolean registerBinderFactory(ConverterFactory factory) { + if (factory == null) throw new IllegalArgumentException("factory can't be null"); + if (binderFactories.contains(factory)) return false; + binderFactories.add(binderFactories.size() - settingsBinders, (ConverterFactory) factory); + return true; + } + + @Nullable + public final Object getDefault(@Nullable Type manifest) { + if (manifest == null) return null; + Object instance = defaults.get(manifest); + if (instance != null) return instance; + final Class rawType; + if (manifest instanceof Class) { + rawType = (Class) manifest; + } else if (manifest instanceof ParameterizedType) { + ParameterizedType pt = (ParameterizedType) manifest; + rawType = (Class) pt.getRawType(); + } else return null; + if (rawType.isPrimitive()) { + return Array.get(Array.newInstance(rawType, 1), 0); + } + return defaults.get(rawType); + } + + private final ConcurrentMap, JsonReader.ReadJsonObject> objectReaders = + new ConcurrentHashMap, JsonReader.ReadJsonObject>(); + + private final ConcurrentMap readers = new ConcurrentHashMap(); + private final ConcurrentMap binders = new ConcurrentHashMap(); + private final ConcurrentMap writers = new ConcurrentHashMap(); + + + public final Set getRegisteredDecoders() { + return readers.keySet(); + } + + public final Set getRegisteredBinders() { + return binders.keySet(); + } + + public final Set getRegisteredEncoders() { + return writers.keySet(); + } + + public final Map, Boolean> getRegisteredCreatorMarkers() { + return creatorMarkers; + } + + /** + * Register custom reader for specific type (JSON -> instance conversion). + * Reader is used for conversion from input byte[] -> target object instance + *

+ * Types registered through @CompiledJson annotation should be registered automatically through + * ServiceLoader.load method and you should not be registering them manually. + *

+ * If null is registered for a reader this will disable deserialization of specified type + * + * @param manifest specified type + * @param reader provide custom implementation for reading JSON into an object instance + * @param type + * @param type or subtype + */ + public void registerReader(final Class manifest, @Nullable final JsonReader.ReadObject reader) { + if (reader == null) readers.remove(manifest); + else readers.put(manifest, reader); + } + + /** + * Register custom reader for specific type (JSON -> instance conversion). + * Reader is used for conversion from input byte[] -> target object instance + *

+ * Types registered through @CompiledJson annotation should be registered automatically through + * ServiceLoader.load method and you should not be registering them manually. + *

+ * If null is registered for a reader this will disable deserialization of specified type + * + * @param manifest specified type + * @param reader provide custom implementation for reading JSON into an object instance + * @return old registered value + */ + @Nullable + public JsonReader.ReadObject registerReader(final Type manifest, @Nullable final JsonReader.ReadObject reader) { + if (reader == null) return readers.remove(manifest); + try { + return readers.get(manifest); + } finally { + readers.put(manifest, reader); + } + } + + /** + * Register custom binder for specific type (JSON -> instance conversion). + * Binder is used for conversion from input byte[] -> existing target object instance. + * It's similar to reader, with the difference that it accepts target instance. + *

+ * Types registered through @CompiledJson annotation should be registered automatically through + * ServiceLoader.load method and you should not be registering them manually. + *

+ * If null is registered for a binder this will disable binding of specified type + * + * @param manifest specified type + * @param binder provide custom implementation for binding JSON to an object instance + * @param type + * @param type or subtype + */ + public void registerBinder(final Class manifest, @Nullable final JsonReader.BindObject binder) { + if (binder == null) binders.remove(manifest); + else binders.put(manifest, binder); + } + + /** + * Register custom binder for specific type (JSON -> instance conversion). + * Binder is used for conversion from input byte[] -> existing target object instance. + * It's similar to reader, with the difference that it accepts target instance. + *

+ * Types registered through @CompiledJson annotation should be registered automatically through + * ServiceLoader.load method and you should not be registering them manually. + *

+ * If null is registered for a binder this will disable binding of specified type + * + * @param manifest specified type + * @param binder provide custom implementation for binding JSON to an object instance + */ + public void registerBinder(final Type manifest, @Nullable final JsonReader.BindObject binder) { + if (binder == null) binders.remove(manifest); + else binders.put(manifest, binder); + } + + /** + * Register custom writer for specific type (instance -> JSON conversion). + * Writer is used for conversion from object instance -> output byte[] + *

+ * Types registered through @CompiledJson annotation should be registered automatically through + * ServiceLoader.load method and you should not be registering them manually. + *

+ * If null is registered for a writer this will disable serialization of specified type + * + * @param manifest specified type + * @param writer provide custom implementation for writing JSON from object instance + * @param type + */ + public void registerWriter(final Class manifest, @Nullable final JsonWriter.WriteObject writer) { + if (writer == null) { + writerMap.remove(manifest); + writers.remove(manifest); + } else { + writerMap.put(manifest, manifest); + writers.put(manifest, writer); + } + } + + /** + * Register custom writer for specific type (instance -> JSON conversion). + * Writer is used for conversion from object instance -> output byte[] + *

+ * Types registered through @CompiledJson annotation should be registered automatically through + * ServiceLoader.load method and you should not be registering them manually. + *

+ * If null is registered for a writer this will disable serialization of specified type + * + * @param manifest specified type + * @param writer provide custom implementation for writing JSON from object instance + * @return old registered value + */ + @Nullable + public JsonWriter.WriteObject registerWriter(final Type manifest, @Nullable final JsonWriter.WriteObject writer) { + if (writer == null) return writers.remove(manifest); + try { + return writers.get(manifest); + } finally { + writers.put(manifest, writer); + } + } + + private final ConcurrentMap, Class> writerMap = new ConcurrentHashMap, Class>(); + + /** + * Try to find registered writer for provided type. + * If writer is not found, null will be returned. + * If writer for exact type is not found, type hierarchy will be scanned for base writer. + *

+ * Writer is used for conversion from object instance into JSON representation. + * + * @param manifest specified type + * @return writer for specified type if found + */ + @Nullable + public JsonWriter.WriteObject tryFindWriter(final Type manifest) { + JsonWriter.WriteObject writer = writers.get(manifest); + if (writer != null) return writer; + final Type actualType = extractActualType(manifest); + if (actualType != manifest) { + writer = writers.get(actualType); + if (writer != null) { + writers.putIfAbsent(manifest, writer); + return writer; + } + } + if (actualType instanceof Class) { + final Class signature = (Class) actualType; + if (JsonObject.class.isAssignableFrom(signature)) { + writers.putIfAbsent(manifest, OBJECT_WRITER); + return OBJECT_WRITER; + } + } + writer = lookupFromFactories(manifest, actualType, writerFactories, writers); + if (writer != null) return writer; + if (!(actualType instanceof Class)) return null; + Class found = writerMap.get(actualType); + if (found != null) { + return writers.get(found); + } + Class container = (Class) actualType; + final ArrayList> signatures = new ArrayList>(); + findAllSignatures(container, signatures); + for (final Class sig : signatures) { + writer = writers.get(sig); + if (writer == null) { + writer = lookupFromFactories(manifest, sig, writerFactories, writers); + } + if (writer != null) { + writerMap.putIfAbsent(container, sig); + return writer; + } + } + return null; + } + + private static Type extractActualType(final Type manifest) { + if (manifest instanceof WildcardType) { + WildcardType wt = (WildcardType) manifest; + if (wt.getUpperBounds().length == 1 && wt.getLowerBounds().length == 0) { + return wt.getUpperBounds()[0]; + } + } + return manifest; + } + + private void checkExternal(final Type manifest, final ConcurrentMap cache) { + if (manifest instanceof Class) { + externalConverterAnalyzer.tryFindConverter((Class) manifest, this); + } else if (manifest instanceof ParameterizedType) { + ParameterizedType pt = (ParameterizedType) manifest; + Type container = pt.getRawType(); + externalConverterAnalyzer.tryFindConverter((Class) container, this); + for (Type arg : pt.getActualTypeArguments()) { + if (!cache.containsKey(arg)) { + Type actualType = extractActualType(arg); + if (actualType != arg && !cache.containsKey(actualType)) { + checkExternal(actualType, cache); + } + } + } + } + } + + @Nullable + private T lookupFromFactories( + final Type signature, + final Type manifest, + final List> factories, + final ConcurrentMap cache) { + if (manifest instanceof Class) { + externalConverterAnalyzer.tryFindConverter((Class) manifest, this); + T found = cache.get(manifest); + if (found != null) return found; + } else if (manifest instanceof ParameterizedType) { + checkExternal(manifest, cache); + } + + for (ConverterFactory wrt : factories) { + final T converter = wrt.tryCreate(manifest, this); + if (converter != null) { + cache.putIfAbsent(signature, converter); + return converter; + } + } + return null; + } + + /** + * Try to find registered reader for provided type. + * If reader is not found, null will be returned. + * Exact match must be found, type hierarchy will not be scanned for alternative readers. + *

+ * If you wish to use alternative reader for specific type, register it manually with something along the lines of + *

+	 *     DslJson dslJson = ...
+	 *     dslJson.registerReader(Interface.class, dslJson.tryFindReader(Implementation.class));
+	 * 
+ * + * @param manifest specified type + * @return found reader for specified type + */ + @Nullable + public JsonReader.ReadObject tryFindReader(final Type manifest) { + JsonReader.ReadObject found = readers.get(manifest); + if (found != null) return found; + final Type actualType = extractActualType(manifest); + if (actualType != manifest) { + found = readers.get(actualType); + if (found != null) { + readers.putIfAbsent(manifest, found); + return found; + } + } + if (actualType instanceof Class) { + final Class signature = (Class) actualType; + if (JsonObject.class.isAssignableFrom(signature)) { + final JsonReader.ReadJsonObject decoder = getObjectReader(signature); + if (decoder != null) { + found = convertToReader(decoder); + readers.putIfAbsent(manifest, found); + return found; + } + } + } + return lookupFromFactories(manifest, actualType, readerFactories, readers); + } + + /** + * Try to find registered binder for provided type. + * If binder is not found, null will be returned. + * Exact match must be found, type hierarchy will not be scanned for alternative binders. + *

+ * If you wish to use alternative binder for specific type, register it manually with something along the lines of + *

+	 *     DslJson dslJson = ...
+	 *     dslJson.registerBinder(Interface.class, dslJson.tryFindBinder(Implementation.class));
+	 * 
+ * + * @param manifest specified type + * @return found reader for specified type + */ + @Nullable + public JsonReader.BindObject tryFindBinder(final Type manifest) { + JsonReader.BindObject found = binders.get(manifest); + if (found != null) return found; + final Type actualType = extractActualType(manifest); + if (actualType != manifest) { + found = binders.get(actualType); + if (found != null) { + binders.putIfAbsent(manifest, found); + return found; + } + } + return lookupFromFactories(manifest, actualType, binderFactories, binders); + } + + /** + * Try to find registered writer for provided type. + * If writer is not found, null will be returned. + * If writer for exact type is not found, type hierarchy will be scanned for base writer. + *

+ * Writer is used for conversion from object instance into JSON representation. + * + * @param manifest specified class + * @param specified type + * @return found writer for specified class or null + */ + @SuppressWarnings("unchecked") + @Nullable + public JsonWriter.WriteObject tryFindWriter(final Class manifest) { + return (JsonWriter.WriteObject) tryFindWriter((Type) manifest); + } + + /** + * Try to find registered reader for provided type. + * If reader is not found, null will be returned. + * Exact match must be found, type hierarchy will not be scanned for alternative reader. + *

+ * If you wish to use alternative reader for specific type, register it manually with something along the lines of + *

+	 *     DslJson dslJson = ...
+	 *     dslJson.registerReader(Interface.class, dslJson.tryFindReader(Implementation.class));
+	 * 
+ * + * @param manifest specified class + * @param specified type + * @return found reader for specified class or null + */ + @SuppressWarnings("unchecked") + @Nullable + public JsonReader.ReadObject tryFindReader(final Class manifest) { + return (JsonReader.ReadObject) tryFindReader((Type) manifest); + } + + /** + * Try to find registered binder for provided type. + * If binder is not found, null will be returned. + * Exact match must be found, type hierarchy will not be scanned for alternative binder. + *

+ * If you wish to use alternative binder for specific type, register it manually with something along the lines of + *

+	 *     DslJson dslJson = ...
+	 *     dslJson.registerBinder(Interface.class, dslJson.tryFindBinder(Implementation.class));
+	 * 
+ * + * @param manifest specified class + * @param specified type + * @return found reader for specified class or null + */ + @SuppressWarnings("unchecked") + @Nullable + public JsonReader.BindObject tryFindBinder(final Class manifest) { + return (JsonReader.BindObject) tryFindBinder((Type) manifest); + } + + private static void findAllSignatures(final Class manifest, final ArrayList> found) { + if (found.contains(manifest)) { + return; + } + found.add(manifest); + final Class superClass = manifest.getSuperclass(); + if (superClass != null && superClass != Object.class) { + findAllSignatures(superClass, found); + } + for (final Class iface : manifest.getInterfaces()) { + findAllSignatures(iface, found); + } + } + + @SuppressWarnings("unchecked") + @Nullable + private JsonReader.ReadJsonObject probeForObjectReader(Class manifest, Object instance) { + Object found; + try { + found = manifest.getField("JSON_READER").get(instance); + } catch (Exception ignore) { + try { + found = manifest.getMethod("JSON_READER").invoke(instance); + } catch (Exception ignore2) { + try { + found = manifest.getMethod("getJSON_READER").invoke(instance); + } catch (Exception ignore3) { + return null; + } + } + } + return found instanceof JsonReader.ReadJsonObject + ? (JsonReader.ReadJsonObject)found + : null; + } + + @SuppressWarnings("unchecked") + @Nullable + protected final JsonReader.ReadJsonObject getObjectReader(final Class manifest) { + try { + JsonReader.ReadJsonObject reader = objectReaders.get(manifest); + if (reader == null) { + reader = probeForObjectReader(manifest, null); + if (reader == null) { + //probe in few special places + try { + Object companion = manifest.getField("Companion").get(null); + reader = probeForObjectReader(companion.getClass(), companion); + } catch (Exception ignore) { + return null; + } + } + if (reader != null) { + objectReaders.putIfAbsent(manifest, reader); + } + } + return reader; + } catch (final Exception ignore) { + return null; + } + } + + public void serializeMap(final Map value, final JsonWriter sw) throws IOException { + sw.writeByte(JsonWriter.OBJECT_START); + final int size = value.size(); + if (size > 0) { + final Iterator> iterator = value.entrySet().iterator(); + Map.Entry kv = iterator.next(); + sw.writeString(kv.getKey()); + sw.writeByte(JsonWriter.SEMI); + serialize(sw, kv.getValue()); + for (int i = 1; i < size; i++) { + sw.writeByte(JsonWriter.COMMA); + kv = iterator.next(); + sw.writeString(kv.getKey()); + sw.writeByte(JsonWriter.SEMI); + serialize(sw, kv.getValue()); + } + } + sw.writeByte(JsonWriter.OBJECT_END); + } + + @Deprecated + @Nullable + public static Object deserializeObject(final JsonReader reader) throws IOException { + return ObjectConverter.deserializeObject(reader); + } + + /** + * Will be removed + * @param reader JSON reader + * @return deseralized list + * @throws IOException error during parsing + */ + @Deprecated + public static ArrayList deserializeList(final JsonReader reader) throws IOException { + return ObjectConverter.deserializeList(reader); + } + + /** + * Will be removed + * @param reader JSON reader + * @return deserialized map + * @throws IOException error during parsing + */ + @Deprecated + public static LinkedHashMap deserializeMap(final JsonReader reader) throws IOException { + return ObjectConverter.deserializeMap(reader); + } + + private static Object convertResultToArray(Class elementType, List result) { + if (elementType.isPrimitive()) { + if (boolean.class.equals(elementType)) { + boolean[] array = new boolean[result.size()]; + for (int i = 0; i < result.size(); i++) { + array[i] = (Boolean) result.get(i); + } + return array; + } else if (int.class.equals(elementType)) { + int[] array = new int[result.size()]; + for (int i = 0; i < result.size(); i++) { + array[i] = (Integer) result.get(i); + } + return array; + } else if (long.class.equals(elementType)) { + long[] array = new long[result.size()]; + for (int i = 0; i < result.size(); i++) { + array[i] = (Long) result.get(i); + } + return array; + } else if (short.class.equals(elementType)) { + short[] array = new short[result.size()]; + for (int i = 0; i < result.size(); i++) { + array[i] = (Short) result.get(i); + } + return array; + } else if (byte.class.equals(elementType)) { + byte[] array = new byte[result.size()]; + for (int i = 0; i < result.size(); i++) { + array[i] = (Byte) result.get(i); + } + return array; + } else if (float.class.equals(elementType)) { + float[] array = new float[result.size()]; + for (int i = 0; i < result.size(); i++) { + array[i] = (Float) result.get(i); + } + return array; + } else if (double.class.equals(elementType)) { + double[] array = new double[result.size()]; + for (int i = 0; i < result.size(); i++) { + array[i] = (Double) result.get(i); + } + return array; + } else if (char.class.equals(elementType)) { + char[] array = new char[result.size()]; + for (int i = 0; i < result.size(); i++) { + array[i] = (Character) result.get(i); + } + return array; + } + } + return result.toArray((Object[]) Array.newInstance(elementType, 0)); + } + + /** + * Check if DslJson knows how to serialize a type. + * It will check if a writer for such type exists or can be used. + * + * @param manifest type to check + * @return can serialize this type into JSON + */ + public final boolean canSerialize(final Type manifest) { + JsonWriter.WriteObject writer = writers.get(manifest); + if (writer != null) return true; + if (manifest instanceof Class) { + final Class content = (Class) manifest; + if (JsonObject.class.isAssignableFrom(content)) { + return true; + } + if (JsonObject[].class.isAssignableFrom(content)) { + return true; + } + if (tryFindWriter(manifest) != null) { + return true; + } + if (content.isArray()) { + return !content.getComponentType().isArray() + && !Collection.class.isAssignableFrom(content.getComponentType()) + && canSerialize(content.getComponentType()); + } + } + if (manifest instanceof ParameterizedType) { + final ParameterizedType pt = (ParameterizedType) manifest; + if (pt.getActualTypeArguments().length == 1) { + final Class container = (Class) pt.getRawType(); + if (container.isArray() || Collection.class.isAssignableFrom(container)) { + final Type content = pt.getActualTypeArguments()[0]; + return content instanceof Class && JsonObject.class.isAssignableFrom((Class) content) + || tryFindWriter(content) != null; + } + } + } else if (manifest instanceof GenericArrayType) { + final GenericArrayType gat = (GenericArrayType) manifest; + return gat.getGenericComponentType() instanceof Class + && JsonObject.class.isAssignableFrom((Class) gat.getGenericComponentType()) + || tryFindWriter(gat.getGenericComponentType()) != null; + } + for (ConverterFactory wrt : writerFactories) { + if (wrt.tryCreate(manifest, this) != null) { + return true; + } + } + return false; + } + + /** + * Check if DslJson knows how to deserialize a type. + * It will check if a reader for such type exists or can be used. + * + * @param manifest type to check + * @return can read this type from JSON + */ + public final boolean canDeserialize(final Type manifest) { + if (tryFindReader(manifest) != null) { + return true; + } + if (manifest instanceof Class) { + final Class objectType = (Class) manifest; + if (objectType.isArray()) { + return !objectType.getComponentType().isArray() + && !Collection.class.isAssignableFrom(objectType.getComponentType()) + && canDeserialize(objectType.getComponentType()); + } + } + if (manifest instanceof ParameterizedType) { + final ParameterizedType pt = (ParameterizedType) manifest; + if (pt.getActualTypeArguments().length == 1) { + final Class container = (Class) pt.getRawType(); + if (container.isArray() || Collection.class.isAssignableFrom(container)) { + final Type content = pt.getActualTypeArguments()[0]; + if (tryFindReader(content) != null) { + return true; + } + } + } + } else if (manifest instanceof GenericArrayType) { + final Type content = ((GenericArrayType) manifest).getGenericComponentType(); + return tryFindReader(content) != null; + } + return false; + } + + /** + * Reusable deserialize API. + * For maximum performance `JsonReader` should be reused (otherwise small buffer will be allocated for processing) + * and `JsonReader.ReadObject` should be prepared (otherwise a lookup will be required). + *

+ * This is mostly convenience API since it starts the processing of the JSON by calling getNextToken on JsonReader, + * checks for null and calls converter.read(input). + * + * @param specified type + * @param converter target reader + * @param input input JSON + * @return deserialized instance + * @throws IOException error during deserialization + */ + @Nullable + public T deserialize( + final JsonReader.ReadObject converter, + final JsonReader input) throws IOException { + if (converter == null) { + throw new IllegalArgumentException("converter can't be null"); + } + if (input == null) { + throw new IllegalArgumentException("input can't be null"); + } + input.getNextToken(); + return converter.read(input); + } + + /** + * Convenient deserialize API for working with bytes. + * Deserialize provided byte input into target object. + *

+ * Since JSON is often though of as a series of char, + * most libraries will convert inputs into a sequence of chars and do processing on them. + * DslJson will treat input as a sequence of bytes which allows for various optimizations. + * + * @param manifest target type + * @param body input JSON + * @param size length + * @param target type + * @return deserialized instance + * @throws IOException error during deserialization + */ + @SuppressWarnings("unchecked") + @Nullable + public TResult deserialize( + final Class manifest, + final byte[] body, + final int size) throws IOException { + if (manifest == null) { + throw new IllegalArgumentException("manifest can't be null"); + } + if (body == null) { + throw new IllegalArgumentException("body can't be null"); + } + final JsonReader json = localReader.get().process(body, size); + try { + json.getNextToken(); + final JsonReader.ReadObject simpleReader = tryFindReader(manifest); + if (simpleReader != null) { + return (TResult) simpleReader.read(json); + } + if (manifest.isArray()) { + if (json.wasNull()) { + return null; + } else if (json.last() != '[') { + throw json.newParseError("Expecting '[' for array start"); + } + final Class elementManifest = manifest.getComponentType(); + final List list = deserializeList(elementManifest, body, size); + if (list == null) { + return null; + } + return (TResult) convertResultToArray(elementManifest, list); + } + if (fallback != null) { + return (TResult) fallback.deserialize(context, manifest, body, size); + } + throw createErrorMessage(manifest); + } finally { + json.reset(); + } + } + + /** + * Deserialize API for working with bytes. + * Deserialize provided byte input into target object. + *

+ * Since JSON is often though of as a series of char, + * most libraries will convert inputs into a sequence of chars and do processing on them. + * DslJson will treat input as a sequence of bytes which allows for various optimizations. + * + * @param manifest target type + * @param body input JSON + * @param size length + * @return deserialized instance + * @throws IOException error during deserialization + */ + @Nullable + public Object deserialize( + final Type manifest, + final byte[] body, + final int size) throws IOException { + if (manifest instanceof Class) { + return deserialize((Class) manifest, body, size); + } + if (manifest == null) { + throw new IllegalArgumentException("manifest can't be null"); + } + if (body == null) { + throw new IllegalArgumentException("body can't be null"); + } + final JsonReader json = localReader.get().process(body, size); + try { + json.getNextToken(); + final Object result = deserializeWith(manifest, json); + if (result != unknownValue) return result; + if (fallback != null) { + return fallback.deserialize(context, manifest, body, size); + } + throw new ConfigurationException("Unable to find reader for provided type: " + manifest + " and fallback serialization is not registered.\n" + + "Try initializing DslJson with custom fallback in case of unsupported objects or register specified type using registerReader into " + getClass()); + } finally { + json.reset(); + } + } + + @SuppressWarnings("unchecked") + @Nullable + protected Object deserializeWith(Type manifest, JsonReader json) throws IOException { + final JsonReader.ReadObject simpleReader = tryFindReader(manifest); + if (simpleReader != null) { + return simpleReader.read(json); + } + if (manifest instanceof ParameterizedType) { + final ParameterizedType pt = (ParameterizedType) manifest; + if (pt.getActualTypeArguments().length == 1) { + final Type content = pt.getActualTypeArguments()[0]; + final Class container = (Class) pt.getRawType(); + if (container.isArray() || Collection.class.isAssignableFrom(container)) { + if (json.wasNull()) { + return null; + } else if (json.last() != '[') { + throw json.newParseError("Expecting '[' for array start"); + } + if (json.getNextToken() == ']') { + if (container.isArray()) { + returnEmptyArray(content); + } + return new ArrayList(0); + } + final JsonReader.ReadObject contentReader = tryFindReader(content); + if (contentReader != null) { + final ArrayList result = json.deserializeNullableCollection(contentReader); + if (container.isArray()) { + return returnAsArray(content, result); + } + return result; + } + } + } + } else if (manifest instanceof GenericArrayType) { + if (json.wasNull()) { + return null; + } else if (json.last() != '[') { + throw json.newParseError("Expecting '[' for array start"); + } + final Type content = ((GenericArrayType) manifest).getGenericComponentType(); + if (json.getNextToken() == ']') { + return returnEmptyArray(content); + } + final JsonReader.ReadObject contentReader = tryFindReader(content); + if (contentReader != null) { + final ArrayList result = json.deserializeNullableCollection(contentReader); + return returnAsArray(content, result); + } + } + return unknownValue; + } + + private static Object returnAsArray(final Type content, final ArrayList result) { + if (content instanceof Class) { + return convertResultToArray((Class) content, result); + } + if (content instanceof ParameterizedType) { + final ParameterizedType cpt = (ParameterizedType) content; + return result.toArray((Object[]) Array.newInstance((Class) cpt.getRawType(), 0)); + } + return result.toArray(); + } + + private static Object returnEmptyArray(Type content) { + if (content instanceof Class) { + return Array.newInstance((Class) content, 0); + } + if (content instanceof ParameterizedType) { + final ParameterizedType pt = (ParameterizedType) content; + return Array.newInstance((Class) pt.getRawType(), 0); + } + return new Object[0]; + } + + protected IOException createErrorMessage(final Class manifest) { + final ArrayList> signatures = new ArrayList>(); + findAllSignatures(manifest, signatures); + for (final Class sig : signatures) { + if (readers.containsKey(sig)) { + if (sig.equals(manifest)) { + return new IOException("Reader for provided type: " + manifest + " is disabled and fallback serialization is not registered (converter is registered as null).\n" + + "Try initializing system with custom fallback or don't register null for " + manifest); + } + return new IOException("Unable to find reader for provided type: " + manifest + " and fallback serialization is not registered.\n" + + "Found reader for: " + sig + " so try deserializing into that instead?\n" + + "Alternatively, try initializing system with custom fallback or register specified type using registerReader into " + getClass()); + } + } + return new IOException("Unable to find reader for provided type: " + manifest + " and fallback serialization is not registered.\n" + + "Try initializing DslJson with custom fallback in case of unsupported objects or register specified type using registerReader into " + getClass()); + } + + /** + * Convenient deserialize list API for working with bytes. + * Deserialize provided byte input into target object. + *

+ * Since JSON is often though of as a series of char, + * most libraries will convert inputs into a sequence of chars and do processing on them. + * DslJson will treat input as a sequence of bytes which allows for various optimizations. + * + * @param manifest target type + * @param body input JSON + * @param size length + * @param target element type + * @return deserialized list instance + * @throws IOException error during deserialization + */ + @SuppressWarnings("unchecked") + @Nullable + public List deserializeList( + final Class manifest, + final byte[] body, + final int size) throws IOException { + if (manifest == null) throw new IllegalArgumentException("manifest can't be null"); + if (body == null) throw new IllegalArgumentException("body can't be null"); + if (size == 4 && body[0] == 'n' && body[1] == 'u' && body[2] == 'l' && body[3] == 'l') { + return null; + } else if (size == 2 && body[0] == '[' && body[1] == ']') { + return new ArrayList(0); + } + final JsonReader json = localReader.get().process(body, size); + try { + if (json.getNextToken() != '[') { + if (json.wasNull()) { + return null; + } + throw json.newParseError("Expecting '[' for list start"); + } + if (json.getNextToken() == ']') { + return new ArrayList(0); + } + //leave for now in to avoid overhead of going through redirection via generic tryFindReader + if (JsonObject.class.isAssignableFrom(manifest)) { + final JsonReader.ReadJsonObject reader = getObjectReader(manifest); + if (reader != null) { + return (List) json.deserializeNullableCollection(reader); + } + } + final JsonReader.ReadObject simpleReader = tryFindReader(manifest); + if (simpleReader != null) { + return json.deserializeNullableCollection(simpleReader); + } + if (fallback != null) { + final Object array = Array.newInstance(manifest, 0); + final TResult[] result = (TResult[]) fallback.deserialize(context, array.getClass(), body, size); + if (result == null) { + return null; + } + final ArrayList list = new ArrayList(result.length); + for (TResult aResult : result) { + list.add(aResult); + } + return list; + } + throw createErrorMessage(manifest); + } finally { + json.reset(); + } + } + + /** + * This is deprecated to avoid using it. + * Use deserializeList method without the buffer argument instead. + * + * Convenient deserialize list API for working with streams. + * Deserialize provided stream input into target object. + * Use buffer for internal conversion from stream into byte[] for partial processing. + * This method creates a new instance of JsonReader. + * There is also deserializeList without the buffer which reuses thread local reader. + *

+ * Since JSON is often though of as a series of char, + * most libraries will convert inputs into a sequence of chars and do processing on them. + * DslJson will treat input as a sequence of bytes which allows for various optimizations. + *

+ * When working on InputStream DslJson will process JSON in chunks of byte[] inputs. + * Provided buffer will be used as input for partial processing. + *

+ * For best performance buffer should be reused. + * + * @param manifest target type + * @param stream input JSON + * @param buffer buffer used for InputStream -> byte[] conversion + * @param target element type + * @return deserialized list + * @throws IOException error during deserialization + */ + @SuppressWarnings("unchecked") + @Nullable + public List deserializeList( + final Class manifest, + final InputStream stream, + final byte[] buffer) throws IOException { + if (manifest == null) throw new IllegalArgumentException("manifest can't be null"); + if (stream == null) throw new IllegalArgumentException("stream can't be null"); + if (buffer == null) throw new IllegalArgumentException("buffer can't be null"); + return deserializeList(manifest, newReader(stream, buffer), stream); + } + + /** + * Convenient deserialize list API for working with streams. + * Deserialize provided stream input into target object. + *

+ * Since JSON is often though of as a series of char, + * most libraries will convert inputs into a sequence of chars and do processing on them. + * DslJson will treat input as a sequence of bytes which allows for various optimizations. + *

+ * When working on InputStream DslJson will process JSON in chunks of byte[] inputs. + *

+ * + * @param manifest target type + * @param stream input JSON + * @param target element type + * @return deserialized list + * @throws IOException error during deserialization + */ + @SuppressWarnings("unchecked") + @Nullable + public List deserializeList( + final Class manifest, + final InputStream stream) throws IOException { + if (manifest == null) throw new IllegalArgumentException("manifest can't be null"); + if (stream == null) throw new IllegalArgumentException("stream can't be null"); + + final JsonReader json = localReader.get().process(stream); + try { + return deserializeList(manifest, json, stream); + } finally { + json.reset(); + } + } + + @SuppressWarnings("unchecked") + @Nullable + protected List deserializeList( + final Class manifest, + JsonReader json, + InputStream stream) throws IOException { + if (json.getNextToken() != '[') { + if (json.wasNull()) { + return null; + } + throw json.newParseError("Expecting '[' for list start"); + } + if (json.getNextToken() == ']') { + return new ArrayList(0); + } + //leave for now in to avoid overhead of going through redirection via generic tryFindReader + if (JsonObject.class.isAssignableFrom(manifest)) { + final JsonReader.ReadJsonObject reader = getObjectReader(manifest); + if (reader != null) { + return (List) json.deserializeNullableCollection(reader); + } + } + final JsonReader.ReadObject simpleReader = tryFindReader(manifest); + if (simpleReader != null) { + return json.deserializeNullableCollection(simpleReader); + } + if (fallback != null) { + final Object array = Array.newInstance(manifest, 0); + final TResult[] result = (TResult[]) fallback.deserialize(context, array.getClass(), new RereadStream(json.buffer, stream)); + if (result == null) { + return null; + } + final ArrayList list = new ArrayList(result.length); + for (TResult aResult : result) { + list.add(aResult); + } + return list; + } + throw createErrorMessage(manifest); + } + + /** + * Convenient deserialize API for working with streams. + * Deserialize provided stream input into target object. + * This method accepts a buffer and will create a new reader using provided buffer. + * This buffer is used for internal conversion from stream into byte[] for partial processing. + * There is also method without the buffer which reuses local thread reader for processing. + *

+ * Since JSON is often though of as a series of char, + * most libraries will convert inputs into a sequence of chars and do processing on them. + * DslJson will treat input as a sequence of bytes which allows for various optimizations. + *

+ * When working on InputStream DslJson will process JSON in chunks of byte[] inputs. + * Provided buffer will be used as input for partial processing. + *

+ * For best performance buffer should be reused. + * + * @param manifest target type + * @param stream input JSON + * @param buffer buffer used for InputStream -> byte[] conversion + * @param target type + * @return deserialized instance + * @throws IOException error during deserialization + */ + @SuppressWarnings("unchecked") + @Nullable + public TResult deserialize( + final Class manifest, + final InputStream stream, + final byte[] buffer) throws IOException { + if (manifest == null) { + throw new IllegalArgumentException("manifest can't be null"); + } + if (stream == null) { + throw new IllegalArgumentException("stream can't be null"); + } + if (buffer == null) { + throw new IllegalArgumentException("buffer can't be null"); + } + return deserialize(manifest, newReader(stream, buffer), stream); + } + + /** + * Convenient deserialize API for working with streams. + * Deserialize provided stream input into target object. + * This method reuses thread local reader for processing input stream. + *

+ * Since JSON is often though of as a series of char, + * most libraries will convert inputs into a sequence of chars and do processing on them. + * DslJson will treat input as a sequence of bytes which allows for various optimizations. + *

+ * When working on InputStream DslJson will process JSON in chunks of byte[] inputs. + *

+ * + * @param manifest target type + * @param stream input JSON + * @param target type + * @return deserialized instance + * @throws IOException error during deserialization + */ + @SuppressWarnings("unchecked") + @Nullable + public TResult deserialize( + final Class manifest, + final InputStream stream) throws IOException { + if (manifest == null) { + throw new IllegalArgumentException("manifest can't be null"); + } + if (stream == null) { + throw new IllegalArgumentException("stream can't be null"); + } + final JsonReader json = localReader.get().process(stream); + try { + return deserialize(manifest, json, stream); + } finally { + json.reset(); + } + } + + @SuppressWarnings("unchecked") + @Nullable + protected TResult deserialize( + final Class manifest, + final JsonReader json, + final InputStream stream) throws IOException { + json.getNextToken(); + final JsonReader.ReadObject simpleReader = tryFindReader(manifest); + if (simpleReader != null) { + return (TResult) simpleReader.read(json); + } + if (manifest.isArray()) { + if (json.wasNull()) { + return null; + } else if (json.last() != '[') { + throw json.newParseError("Expecting '[' for array start"); + } + final Class elementManifest = manifest.getComponentType(); + if (json.getNextToken() == ']') { + return (TResult) Array.newInstance(elementManifest, 0); + } + //leave for now in to avoid overhead of going through redirection via generic tryFindReader + if (JsonObject.class.isAssignableFrom(elementManifest)) { + final JsonReader.ReadJsonObject objectReader = getObjectReader(elementManifest); + if (objectReader != null) { + List list = json.deserializeNullableCollection(objectReader); + return (TResult) convertResultToArray(elementManifest, list); + } + } + final JsonReader.ReadObject simpleElementReader = tryFindReader(elementManifest); + if (simpleElementReader != null) { + List list = json.deserializeNullableCollection(simpleElementReader); + return (TResult) convertResultToArray(elementManifest, list); + } + } + if (fallback != null) { + return (TResult) fallback.deserialize(context, manifest, new RereadStream(json.buffer, stream)); + } + throw createErrorMessage(manifest); + } + + /** + * Deserialize API for working with streams. + * Deserialize provided stream input into target object. + * Use buffer for internal conversion from stream into byte[] for partial processing. + * This method creates a new instance of JsonReader for processing the stream. + * There is also a method without the byte[] buffer which reuses thread local reader. + *

+ * Since JSON is often though of as a series of char, + * most libraries will convert inputs into a sequence of chars and do processing on them. + * DslJson will treat input as a sequence of bytes which allows for various optimizations. + *

+ * When working on InputStream DslJson will process JSON in chunks of byte[] inputs. + * Provided buffer will be used as input for partial processing. + *

+ * For best performance buffer should be reused. + * + * @param manifest target type + * @param stream input JSON + * @param buffer buffer used for InputStream -> byte[] conversion + * @return deserialized instance + * @throws IOException error during deserialization + */ + @Nullable + public Object deserialize( + final Type manifest, + final InputStream stream, + final byte[] buffer) throws IOException { + if (manifest instanceof Class) { + return deserialize((Class) manifest, stream, buffer); + } + if (manifest == null) { + throw new IllegalArgumentException("manifest can't be null"); + } + if (stream == null) { + throw new IllegalArgumentException("stream can't be null"); + } + if (buffer == null) { + throw new IllegalArgumentException("buffer can't be null"); + } + final JsonReader json = newReader(stream, buffer); + json.getNextToken(); + final Object result = deserializeWith(manifest, json); + if (result != unknownValue) return result; + if (fallback != null) { + return fallback.deserialize(context, manifest, new RereadStream(buffer, stream)); + } + throw new ConfigurationException("Unable to find reader for provided type: " + manifest + " and fallback serialization is not registered.\n" + + "Try initializing DslJson with custom fallback in case of unsupported objects or register specified type using registerReader into " + getClass()); + } + + /** + * Deserialize API for working with streams. + * Deserialize provided stream input into target object. + * This method reuses thread local reader for processing JSON input. + *

+ * Since JSON is often though of as a series of char, + * most libraries will convert inputs into a sequence of chars and do processing on them. + *

+ * When working on InputStream DslJson will process JSON in chunks of byte[] inputs. + *

+ * + * @param manifest target type + * @param stream input JSON + * @return deserialized instance + * @throws IOException error during deserialization + */ + @Nullable + public Object deserialize( + final Type manifest, + final InputStream stream) throws IOException { + if (manifest instanceof Class) { + return deserialize((Class) manifest, stream); + } + if (manifest == null) { + throw new IllegalArgumentException("manifest can't be null"); + } + if (stream == null) { + throw new IllegalArgumentException("stream can't be null"); + } + final JsonReader json = localReader.get().process(stream); + try { + json.getNextToken(); + final Object result = deserializeWith(manifest, json); + if (result != unknownValue) return result; + if (fallback != null) { + return fallback.deserialize(context, manifest, new RereadStream(json.buffer, stream)); + } + throw new ConfigurationException("Unable to find reader for provided type: " + manifest + " and fallback serialization is not registered.\n" + + "Try initializing DslJson with custom fallback in case of unsupported objects or register specified type using registerReader into " + getClass()); + } finally { + json.reset(); + } + } + + static class RereadStream extends InputStream { + private final byte[] buffer; + private final InputStream stream; + private boolean usingBuffer; + private int position; + + RereadStream(byte[] buffer, InputStream stream) { + this.buffer = buffer; + this.stream = stream; + usingBuffer = true; + } + + @Override + public int read() throws IOException { + if (usingBuffer) { + if (position < buffer.length) { + return buffer[position++]; + } else usingBuffer = false; + } + return stream.read(); + } + + @Override + public int read(byte[] buf) throws IOException { + if (usingBuffer) { + return super.read(buf); + } + return stream.read(buf); + } + + @Override + public int read(byte[] buf, int off, int len) throws IOException { + if (usingBuffer) { + return super.read(buf, off, len); + } + return stream.read(buf, off, len); + } + } + + private static final Iterator EMPTY_ITERATOR = new Iterator() { + @Override + public boolean hasNext() { + return false; + } + + @Override + public void remove() { + } + + @Nullable + @Override + public Object next() { + return null; + } + }; + + /** + * Streaming API for collection deserialization. + * DslJson will create iterator based on provided manifest info. + * It will attempt to deserialize from stream on each next() invocation. + * This method requires buffer instance for partial stream processing. + * It will create a new instance of JsonReader. + * There is also a method without the buffer which will reuse thread local reader. + *

+ * Useful for processing very large streams if only one instance from collection is required at once. + *

+ * Stream will be processed in chunks of specified buffer byte[]. + * It will block on reading until buffer is full or end of stream is detected. + * + * @param manifest type info + * @param stream JSON data stream + * @param type info + * @return Iterator to instances deserialized from input JSON + * @throws IOException if reader is not found or there is an error processing input stream + */ + @SuppressWarnings("unchecked") + @Nullable + public Iterator iterateOver( + final Class manifest, + final InputStream stream) throws IOException { + if (manifest == null) { + throw new IllegalArgumentException("manifest can't be null"); + } + if (stream == null) { + throw new IllegalArgumentException("stream can't be null"); + } + final JsonReader json = localReader.get(); + json.process(stream); + return iterateOver(manifest, json, stream); + } + + + /** + * Streaming API for collection deserialization. + * DslJson will create iterator based on provided manifest info. + * It will attempt to deserialize from stream on each next() invocation. + * This method requires buffer instance for partial stream processing. + * It will create a new instance of JsonReader. + * There is also a method without the buffer which will reuse thread local reader. + *

+ * Useful for processing very large streams if only one instance from collection is required at once. + *

+ * Stream will be processed in chunks of specified buffer byte[]. + * It will block on reading until buffer is full or end of stream is detected. + * + * @param manifest type info + * @param stream JSON data stream + * @param buffer size of processing chunk + * @param type info + * @return Iterator to instances deserialized from input JSON + * @throws IOException if reader is not found or there is an error processing input stream + */ + @SuppressWarnings("unchecked") + @Nullable + public Iterator iterateOver( + final Class manifest, + final InputStream stream, + final byte[] buffer) throws IOException { + if (manifest == null) { + throw new IllegalArgumentException("manifest can't be null"); + } + if (stream == null) { + throw new IllegalArgumentException("stream can't be null"); + } + if (buffer == null) { + throw new IllegalArgumentException("buffer can't be null"); + } + return iterateOver(manifest, newReader(stream, buffer), stream); + } + + @SuppressWarnings("unchecked") + @Nullable + protected Iterator iterateOver( + final Class manifest, + final JsonReader json, + final InputStream stream) throws IOException { + if (json.getNextToken() != '[') { + if (json.wasNull()) { + return null; + } + throw json.newParseError("Expecting '[' for iterator start"); + } + if (json.getNextToken() == ']') { + return EMPTY_ITERATOR; + } + //leave for now in to avoid overhead of going through redirection via generic tryFindReader + if (JsonObject.class.isAssignableFrom(manifest)) { + final JsonReader.ReadJsonObject reader = getObjectReader(manifest); + if (reader != null) { + return json.iterateOver(reader); + } + } + final JsonReader.ReadObject simpleReader = tryFindReader(manifest); + if (simpleReader != null) { + return json.iterateOver(simpleReader); + } + if (fallback != null) { + final Object array = Array.newInstance(manifest, 0); + final TResult[] result = (TResult[]) fallback.deserialize(context, array.getClass(), new RereadStream(json.buffer, stream)); + if (result == null) { + return null; + } + final ArrayList list = new ArrayList(result.length); + for (TResult aResult : result) { + list.add(aResult); + } + return list.iterator(); + } + throw createErrorMessage(manifest); + } + + private final JsonWriter.WriteObject OBJECT_WRITER = new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable JsonObject value) { + if (value == null) writer.writeNull(); + else value.serialize(writer, omitDefaults); + } + }; + private JsonReader.ReadObject convertToReader(final JsonReader.ReadJsonObject decoder) { + return new JsonReader.ReadObject() { + @Override + public T read(JsonReader reader) throws IOException { + if (reader.wasNull()) return null; + else if (reader.last() != '{') throw reader.newParseError("Expecting '{' for object start"); + reader.getNextToken(); + return decoder.deserialize(reader); + } + }; + } + + private final JsonWriter.WriteObject OBJECT_ARRAY_WRITER = new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable Object value) { + serialize(writer, (JsonObject[]) value); + } + }; + + private static final JsonWriter.WriteObject CHAR_ARRAY_WRITER = new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable Object value) { + StringConverter.serialize(new String((char[]) value), writer); + } + }; + + private final JsonWriter.WriteObject NULL_WRITER = new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable Object value) { + writer.writeNull(); + } + }; + + @SuppressWarnings("unchecked") + private JsonWriter.WriteObject getOrCreateWriter(@Nullable final Object instance, final Class instanceManifest) throws IOException { + if (instance instanceof JsonObject) { + return OBJECT_WRITER; + } + if (instance instanceof JsonObject[]) { + return OBJECT_ARRAY_WRITER; + } + final Class manifest = instanceManifest != null ? instanceManifest : instance.getClass(); + if (instanceManifest != null) { + if (JsonObject.class.isAssignableFrom(manifest)) { + return OBJECT_WRITER; + } + } + final JsonWriter.WriteObject simpleWriter = tryFindWriter(manifest); + if (simpleWriter != null) { + return simpleWriter; + } + if (manifest.isArray()) { + final Class elementManifest = manifest.getComponentType(); + if (char.class == elementManifest) { + return CHAR_ARRAY_WRITER; + } else { + final JsonWriter.WriteObject elementWriter = tryFindWriter(elementManifest); + if (elementWriter != null) { + //TODO: cache writer for next lookup + return new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable Object value) { + writer.serialize((Object[]) value, elementWriter); + } + }; + } + } + } + if (instance instanceof Collection || Collection.class.isAssignableFrom(manifest)) { + return new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable final Object value) { + final Collection items = (Collection) value; + Class baseType = null; + final Iterator iterator = items.iterator(); + //TODO: pick lowest common denominator!? + do { + final Object item = iterator.next(); + if (item != null) { + Class elementType = item.getClass(); + if (elementType != baseType) { + if (baseType == null || elementType.isAssignableFrom(baseType)) { + baseType = elementType; + } + } + } + } while (iterator.hasNext()); + if (baseType == null) { + writer.writeByte(JsonWriter.ARRAY_START); + writer.writeNull(); + for (int i = 1; i < items.size(); i++) { + writer.writeAscii(",null"); + } + writer.writeByte(JsonWriter.ARRAY_END); + } else if (JsonObject.class.isAssignableFrom(baseType)) { + serialize(writer, (Collection) items); + } else { + final JsonWriter.WriteObject elementWriter = tryFindWriter(baseType); + if (elementWriter != null) { + writer.serialize(items, elementWriter); + } else if (fallback != null) { + final ByteArrayOutputStream stream = new ByteArrayOutputStream(); + stream.reset(); + try { + fallback.serialize(value, stream); + } catch (IOException ex) { + throw new SerializationException(ex); + } + writer.writeAscii(stream.toByteArray()); + } else { + throw new ConfigurationException("Unable to serialize provided object. Failed to find serializer for: " + items.getClass()); + } + } + } + }; + } + throw new ConfigurationException("Unable to serialize provided object. Failed to find serializer for: " + manifest); + } + + /** + * Streaming API for collection serialization. + *

+ * It will iterate over entire iterator and serialize each instance into target output stream. + * After each instance serialization it will copy JSON into target output stream. + * During each serialization reader will be looked up based on next() instance which allows + * serializing collection with different types. + * If collection contains all instances of the same type, prefer the other streaming API. + *

+ * If reader is not found an IOException will be thrown + *

+ * If JsonWriter is provided it will be used, otherwise a new instance will be internally created. + * + * @param iterator input data + * @param stream target JSON stream + * @param writer temporary buffer for serializing a single item. Can be null + * @param input data type + * @throws IOException reader is not found, there is an error during serialization or problem with writing to target stream + */ + @SuppressWarnings("unchecked") + public void iterateOver( + final Iterator iterator, + final OutputStream stream, + @Nullable final JsonWriter writer) throws IOException { + if (iterator == null) { + throw new IllegalArgumentException("iterator can't be null"); + } + if (stream == null) { + throw new IllegalArgumentException("stream can't be null"); + } + stream.write(JsonWriter.ARRAY_START); + if (!iterator.hasNext()) { + stream.write(JsonWriter.ARRAY_END); + return; + } + final JsonWriter buffer = writer == null ? new JsonWriter(this) : writer; + T item = iterator.next(); + Class lastManifest = null; + JsonWriter.WriteObject lastWriter = null; + if (item != null) { + lastManifest = item.getClass(); + lastWriter = getOrCreateWriter(item, lastManifest); + buffer.reset(); + try { + lastWriter.write(buffer, item); + } catch (ConfigurationException e) { + throw e; + } catch (Exception e) { + throw new IOException(e); + } + buffer.toStream(stream); + } else { + stream.write(NULL); + } + while (iterator.hasNext()) { + stream.write(JsonWriter.COMMA); + item = iterator.next(); + if (item != null) { + final Class currentManifest = item.getClass(); + if (lastWriter == null || lastManifest == null || !lastManifest.equals(currentManifest)) { + lastManifest = currentManifest; + lastWriter = getOrCreateWriter(item, lastManifest); + } + buffer.reset(); + try { + lastWriter.write(buffer, item); + } catch (ConfigurationException e) { + throw e; + } catch (Exception e) { + throw new IOException(e); + } + buffer.toStream(stream); + } else { + stream.write(NULL); + } + } + stream.write(JsonWriter.ARRAY_END); + } + + /** + * Streaming API for collection serialization. + *

+ * It will iterate over entire iterator and serialize each instance into target output stream. + * After each instance serialization it will copy JSON into target output stream. + *

+ * If reader is not found an IOException will be thrown + *

+ * If JsonWriter is provided it will be used, otherwise a new instance will be internally created. + * + * @param iterator input data + * @param manifest type of elements in collection + * @param stream target JSON stream + * @param writer temporary buffer for serializing a single item. Can be null + * @param input data type + * @throws IOException reader is not found, there is an error during serialization or problem with writing to target stream + */ + @SuppressWarnings("unchecked") + public void iterateOver( + final Iterator iterator, + final Class manifest, + final OutputStream stream, + @Nullable final JsonWriter writer) throws IOException { + if (iterator == null) { + throw new IllegalArgumentException("iterator can't be null"); + } + if (manifest == null) { + throw new IllegalArgumentException("manifest can't be null"); + } + if (stream == null) { + throw new IllegalArgumentException("stream can't be null"); + } + final JsonWriter buffer = writer == null ? new JsonWriter(this) : writer; + final JsonWriter.WriteObject instanceWriter = getOrCreateWriter(null, manifest); + stream.write(JsonWriter.ARRAY_START); + T item = iterator.next(); + if (item != null) { + buffer.reset(); + try { + instanceWriter.write(buffer, item); + } catch (ConfigurationException e) { + throw e; + } catch (Exception e) { + throw new IOException(e); + } + buffer.toStream(stream); + } else { + stream.write(NULL); + } + while (iterator.hasNext()) { + stream.write(JsonWriter.COMMA); + item = iterator.next(); + if (item != null) { + buffer.reset(); + try { + instanceWriter.write(buffer, item); + } catch (ConfigurationException e) { + throw e; + } catch (Exception e) { + throw new IOException(e); + } + buffer.toStream(stream); + } else { + stream.write(NULL); + } + } + stream.write(JsonWriter.ARRAY_END); + } + + /** + * Use writer.serialize instead + * + * @param writer writer + * @param array items + * @param type + */ + @Deprecated + public void serialize(final JsonWriter writer, @Nullable final T[] array) { + if (array == null) { + writer.writeNull(); + return; + } + writer.writeByte(JsonWriter.ARRAY_START); + if (array.length != 0) { + T item = array[0]; + if (item != null) { + item.serialize(writer, omitDefaults); + } else { + writer.writeNull(); + } + for (int i = 1; i < array.length; i++) { + writer.writeByte(JsonWriter.COMMA); + item = array[i]; + if (item != null) { + item.serialize(writer, omitDefaults); + } else { + writer.writeNull(); + } + } + } + writer.writeByte(JsonWriter.ARRAY_END); + } + + /** + * Use writer.serialize instead + * + * @param writer writer + * @param array items + * @param len part of array + * @param type + */ + @Deprecated + public void serialize(final JsonWriter writer, final T[] array, final int len) { + if (writer == null) { + throw new IllegalArgumentException("writer can't be null"); + } + if (array == null) { + writer.writeNull(); + return; + } + writer.writeByte(JsonWriter.ARRAY_START); + if (len != 0) { + T item = array[0]; + if (item != null) { + item.serialize(writer, omitDefaults); + } else { + writer.writeNull(); + } + for (int i = 1; i < len; i++) { + writer.writeByte(JsonWriter.COMMA); + item = array[i]; + if (item != null) { + item.serialize(writer, omitDefaults); + } else { + writer.writeNull(); + } + } + } + writer.writeByte(JsonWriter.ARRAY_END); + } + + /** + * Use writer.serialize instead + * + * @param writer writer + * @param list items + * @param type + */ + @Deprecated + public void serialize(final JsonWriter writer, @Nullable final List list) { + if (writer == null) { + throw new IllegalArgumentException("writer can't be null"); + } + if (list == null) { + writer.writeNull(); + return; + } + writer.writeByte(JsonWriter.ARRAY_START); + if (list.size() != 0) { + T item = list.get(0); + if (item != null) { + item.serialize(writer, omitDefaults); + } else { + writer.writeNull(); + } + for (int i = 1; i < list.size(); i++) { + writer.writeByte(JsonWriter.COMMA); + item = list.get(i); + if (item != null) { + item.serialize(writer, omitDefaults); + } else { + writer.writeNull(); + } + } + } + writer.writeByte(JsonWriter.ARRAY_END); + } + + /** + * Use writer.serialize instead + * + * @param writer writer + * @param collection items + * @param type + */ + @Deprecated + public void serialize(final JsonWriter writer, @Nullable final Collection collection) { + if (writer == null) { + throw new IllegalArgumentException("writer can't be null"); + } + if (collection == null) { + writer.writeNull(); + return; + } + writer.writeByte(JsonWriter.ARRAY_START); + if (!collection.isEmpty()) { + final Iterator it = collection.iterator(); + T item = it.next(); + if (item != null) { + item.serialize(writer, omitDefaults); + } else { + writer.writeNull(); + } + while (it.hasNext()) { + writer.writeByte(JsonWriter.COMMA); + item = it.next(); + if (item != null) { + item.serialize(writer, omitDefaults); + } else { + writer.writeNull(); + } + } + } + writer.writeByte(JsonWriter.ARRAY_END); + } + + /** + * Generic serialize API. + * Based on provided type manifest converter will be chosen. + * If converter is not found method will return false. + *

+ * Resulting JSON will be written into provided writer argument. + * In case of successful serialization true will be returned. + *

+ * For best performance writer argument should be reused. + * + * @param writer where to write resulting JSON + * @param manifest type manifest + * @param value instance to serialize + * @return successful serialization + */ + @SuppressWarnings("unchecked") + public boolean serialize(final JsonWriter writer, final Type manifest, @Nullable final Object value) { + try { + if (writer == null) { + throw new IllegalArgumentException("writer can't be null"); + } + if (value == null) { + writer.writeNull(); + return true; + } else if (value instanceof JsonObject) { + ((JsonObject) value).serialize(writer, omitDefaults); + return true; + } else if (value instanceof JsonObject[]) { + serialize(writer, (JsonObject[]) value); + return true; + } + final JsonWriter.WriteObject simpleWriter = tryFindWriter(manifest); + if (simpleWriter != null) { + simpleWriter.write(writer, value); + return true; + } + Class container = null; + if (manifest instanceof Class) { + container = (Class) manifest; + } + if (container != null && container.isArray()) { + if (Array.getLength(value) == 0) { + writer.writeAscii("[]"); + return true; + } + final Class elementManifest = container.getComponentType(); + if (char.class == elementManifest) { + //TODO? char[] !? + StringConverter.serialize(new String((char[]) value), writer); + return true; + } else { + final JsonWriter.WriteObject elementWriter = (JsonWriter.WriteObject) tryFindWriter(elementManifest); + if (elementWriter != null) { + writer.serialize((Object[]) value, elementWriter); + return true; + } + } + } + if (value instanceof Collection) { + final Collection items = (Collection) value; + if (items.isEmpty()) { + writer.writeAscii("[]"); + return true; + } + Class baseType = null; + final Iterator iterator = items.iterator(); + final boolean isList = items instanceof List; + final List values = isList ? (List) items : new ArrayList(); + final ArrayList itemWriters = new ArrayList(); + Class lastElementType = null; + JsonWriter.WriteObject lastWriter = null; + boolean hasUnknownWriter = false; + //TODO: pick lowest common denominator!? + do { + final Object item = iterator.next(); + if (!isList) { + values.add(item); + } + if (item != null) { + final Class elementType = item.getClass(); + if (elementType != baseType) { + if (baseType == null || elementType.isAssignableFrom(baseType)) { + baseType = elementType; + } + } + if (lastElementType != elementType) { + lastElementType = elementType; + lastWriter = tryFindWriter(elementType); + } + itemWriters.add(lastWriter); + hasUnknownWriter = hasUnknownWriter || lastWriter == null; + } else { + itemWriters.add(NULL_WRITER); + } + } while (iterator.hasNext()); + if (baseType != null && JsonObject.class.isAssignableFrom(baseType)) { + writer.writeByte(JsonWriter.ARRAY_START); + final Iterator iter = values.iterator(); + final JsonObject first = (JsonObject) iter.next(); + if (first != null) first.serialize(writer, omitDefaults); + else writer.writeNull(); + while (iter.hasNext()) { + writer.writeByte(JsonWriter.COMMA); + final JsonObject next = (JsonObject) iter.next(); + if (next != null) next.serialize(writer, omitDefaults); + else writer.writeNull(); + } + writer.writeByte(JsonWriter.ARRAY_END); + return true; + } + if (!hasUnknownWriter) { + writer.writeByte(JsonWriter.ARRAY_START); + final Iterator iter = values.iterator(); + int cur = 1; + itemWriters.get(0).write(writer, iter.next()); + while (iter.hasNext()) { + writer.writeByte(JsonWriter.COMMA); + itemWriters.get(cur++).write(writer, iter.next()); + } + writer.writeByte(JsonWriter.ARRAY_END); + return true; + } + final JsonWriter.WriteObject elementWriter = (JsonWriter.WriteObject) tryFindWriter(baseType); + if (elementWriter != null) { + writer.serialize(items, elementWriter); + return true; + } + } + return false; + } catch (ClassCastException exc) { // workaround for mixed primitive arrays (PLAT-7551) + return false; + } + } + + private static final byte[] NULL = new byte[]{'n', 'u', 'l', 'l'}; + + /** + * Convenient serialize API. + * In most cases JSON is serialized into target `OutputStream`. + * This method will reuse thread local instance of `JsonWriter` and serialize JSON into it. + * + * @param value instance to serialize + * @param stream where to write resulting JSON + * @throws IOException error when unable to serialize instance + */ + public final void serialize(@Nullable final Object value, final OutputStream stream) throws IOException { + if (stream == null) { + throw new IllegalArgumentException("stream can't be null"); + } + if (value == null) { + stream.write(NULL); + return; + } + final JsonWriter jw = localWriter.get(); + jw.reset(stream); + final Class manifest = value.getClass(); + if (!serialize(jw, manifest, value)) { + if (fallback == null) { + throw new ConfigurationException("Unable to serialize provided object. Failed to find serializer for: " + manifest); + } + fallback.serialize(value, stream); + } else { + jw.flush(); + jw.reset(null); + } + } + + /** + * Main serialization API. + * Convert object instance into JSON. + *

+ * JsonWriter contains a growable byte[] where JSON will be serialized. + * After serialization JsonWriter can be copied into OutputStream or it's byte[] can be obtained + *

+ * For best performance reuse `JsonWriter` or even better call `JsonWriter.WriteObject` directly + * + * @param writer where to write resulting JSON + * @param value object instance to serialize + * @throws IOException error when unable to serialize instance + */ + public final void serialize(final JsonWriter writer, @Nullable final Object value) throws IOException { + if (writer == null) { + throw new IllegalArgumentException("writer can't be null"); + } + if (value == null) { + writer.writeNull(); + return; + } + final Class manifest = value.getClass(); + if (!serialize(writer, manifest, value)) { + if (fallback == null) { + throw new ConfigurationException("Unable to serialize provided object. Failed to find serializer for: " + manifest); + } + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + fallback.serialize(value, stream); + writer.writeAscii(stream.toByteArray()); + } + } +} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/ExternalConverterAnalyzer.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/ExternalConverterAnalyzer.java new file mode 100644 index 0000000000..4364a91e8a --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/ExternalConverterAnalyzer.java @@ -0,0 +1,46 @@ +package com.bugsnag.android.repackaged.dslplatform.json; + +import java.util.*; + +class ExternalConverterAnalyzer { + private final Set lookedUpClasses = new HashSet(); + private final ClassLoader[] classLoaders; + + ExternalConverterAnalyzer(Collection classLoaders) { + this.classLoaders = classLoaders.toArray(new ClassLoader[0]); + } + + synchronized boolean tryFindConverter(Class manifest, DslJson dslJson) { + final String className = manifest.getName(); + if (!lookedUpClasses.add(className)) return false; + String[] converterClassNames = resolveExternalConverterClassNames(className); + for (ClassLoader cl : classLoaders) { + for (String ccn : converterClassNames) { + try { + Class converterClass = cl.loadClass(ccn); + if (!Configuration.class.isAssignableFrom(converterClass)) continue; + Configuration converter = (Configuration) converterClass.newInstance(); + converter.configure(dslJson); + return true; + } catch (ClassNotFoundException ignored) { + } catch (IllegalAccessException ignored) { + } catch (InstantiationException ignored) { + } + } + } + return false; + } + + private String[] resolveExternalConverterClassNames(final String fullClassName) { + int dotIndex = fullClassName.lastIndexOf('.'); + if (dotIndex == -1) { + return new String[]{String.format("_%s_DslJsonConverter", fullClassName)}; + } + String packageName = fullClassName.substring(0, dotIndex); + String className = fullClassName.substring(dotIndex + 1); + return new String[]{ + String.format("%s._%s_DslJsonConverter", packageName, className), + String.format("dsl_json.%s._%s_DslJsonConverter", packageName, className), + String.format("dsl_json.%s.%sDslJsonConverter", packageName, className)}; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/Grisu3.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/Grisu3.java new file mode 100644 index 0000000000..0b4f8d2957 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/Grisu3.java @@ -0,0 +1,924 @@ +// Copyright 2010 the V8 project authors. All rights reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Ported to Java from Mozilla's version of V8-dtoa by Hannes Wallnoefer. +// The original revision was 67d1049b0bf9 from the mozilla-central tree. + +// Modified by Rikard Pavelic do avoid allocations +// and unused code paths due to external checks + +package com.bugsnag.android.repackaged.dslplatform.json; + +@SuppressWarnings("fallthrough") // suppress pre-existing warnings +abstract class Grisu3 { + + // FastDtoa will produce at most kFastDtoaMaximalLength digits. + static final int kFastDtoaMaximalLength = 17; + + + // The minimal and maximal target exponent define the range of w's binary + // exponent, where 'w' is the result of multiplying the input by a cached power + // of ten. + // + // A different range might be chosen on a different platform, to optimize digit + // generation, but a smaller range requires more powers of ten to be cached. + static final int minimal_target_exponent = -60; + + private static final class DiyFp { + + long f; + int e; + + static final int kSignificandSize = 64; + static final long kUint64MSB = 0x8000000000000000L; + private static final long kM32 = 0xFFFFFFFFL; + private static final long k10MSBits = 0xFFC00000L << 32; + + DiyFp() { + this.f = 0; + this.e = 0; + } + + // this = this - other. + // The exponents of both numbers must be the same and the significand of this + // must be bigger than the significand of other. + // The result will not be normalized. + void subtract(DiyFp other) { + f -= other.f; + } + + // this = this * other. + void multiply(DiyFp other) { + // Simply "emulates" a 128 bit multiplication. + // However: the resulting number only contains 64 bits. The least + // significant 64 bits are only used for rounding the most significant 64 + // bits. + long a = f >>> 32; + long b = f & kM32; + long c = other.f >>> 32; + long d = other.f & kM32; + long ac = a * c; + long bc = b * c; + long ad = a * d; + long bd = b * d; + long tmp = (bd >>> 32) + (ad & kM32) + (bc & kM32); + // By adding 1U << 31 to tmp we round the final result. + // Halfway cases will be round up. + tmp += 1L << 31; + long result_f = ac + (ad >>> 32) + (bc >>> 32) + (tmp >>> 32); + e += other.e + 64; + f = result_f; + } + + void normalize() { + long f = this.f; + int e = this.e; + + // This method is mainly called for normalizing boundaries. In general + // boundaries need to be shifted by 10 bits. We thus optimize for this case. + while ((f & k10MSBits) == 0) { + f <<= 10; + e -= 10; + } + while ((f & kUint64MSB) == 0) { + f <<= 1; + e--; + } + this.f = f; + this.e = e; + } + + void reset() { + e = 0; + f = 0; + } + + @Override + public String toString() { + return "[DiyFp f:" + f + ", e:" + e + "]"; + } + + } + + private static class CachedPowers { + + static final double kD_1_LOG2_10 = 0.30102999566398114; // 1 / lg(10) + + static class CachedPower { + final long significand; + final short binaryExponent; + final short decimalExponent; + + CachedPower(long significand, short binaryExponent, short decimalExponent) { + this.significand = significand; + this.binaryExponent = binaryExponent; + this.decimalExponent = decimalExponent; + } + } + + static int getCachedPower(int e, int alpha, DiyFp c_mk) { + final int kQ = DiyFp.kSignificandSize; + final double k = Math.ceil((alpha - e + kQ - 1) * kD_1_LOG2_10); + final int index = (GRISU_CACHE_OFFSET + (int) k - 1) / CACHED_POWERS_SPACING + 1; + final CachedPower cachedPower = CACHED_POWERS[index]; + + c_mk.f = cachedPower.significand; + c_mk.e = cachedPower.binaryExponent; + return cachedPower.decimalExponent; + } + + // Code below is converted from GRISU_CACHE_NAME(8) in file "powers-ten.h" + // Regexp to convert this from original C++ source: + // \{GRISU_UINT64_C\((\w+), (\w+)\), (\-?\d+), (\-?\d+)\} + + // interval between entries of the powers cache below + static final int CACHED_POWERS_SPACING = 8; + + static final CachedPower[] CACHED_POWERS = { + new CachedPower(0xe61acf033d1a45dfL, (short) -1087, (short) -308), + new CachedPower(0xab70fe17c79ac6caL, (short) -1060, (short) -300), + new CachedPower(0xff77b1fcbebcdc4fL, (short) -1034, (short) -292), + new CachedPower(0xbe5691ef416bd60cL, (short) -1007, (short) -284), + new CachedPower(0x8dd01fad907ffc3cL, (short) -980, (short) -276), + new CachedPower(0xd3515c2831559a83L, (short) -954, (short) -268), + new CachedPower(0x9d71ac8fada6c9b5L, (short) -927, (short) -260), + new CachedPower(0xea9c227723ee8bcbL, (short) -901, (short) -252), + new CachedPower(0xaecc49914078536dL, (short) -874, (short) -244), + new CachedPower(0x823c12795db6ce57L, (short) -847, (short) -236), + new CachedPower(0xc21094364dfb5637L, (short) -821, (short) -228), + new CachedPower(0x9096ea6f3848984fL, (short) -794, (short) -220), + new CachedPower(0xd77485cb25823ac7L, (short) -768, (short) -212), + new CachedPower(0xa086cfcd97bf97f4L, (short) -741, (short) -204), + new CachedPower(0xef340a98172aace5L, (short) -715, (short) -196), + new CachedPower(0xb23867fb2a35b28eL, (short) -688, (short) -188), + new CachedPower(0x84c8d4dfd2c63f3bL, (short) -661, (short) -180), + new CachedPower(0xc5dd44271ad3cdbaL, (short) -635, (short) -172), + new CachedPower(0x936b9fcebb25c996L, (short) -608, (short) -164), + new CachedPower(0xdbac6c247d62a584L, (short) -582, (short) -156), + new CachedPower(0xa3ab66580d5fdaf6L, (short) -555, (short) -148), + new CachedPower(0xf3e2f893dec3f126L, (short) -529, (short) -140), + new CachedPower(0xb5b5ada8aaff80b8L, (short) -502, (short) -132), + new CachedPower(0x87625f056c7c4a8bL, (short) -475, (short) -124), + new CachedPower(0xc9bcff6034c13053L, (short) -449, (short) -116), + new CachedPower(0x964e858c91ba2655L, (short) -422, (short) -108), + new CachedPower(0xdff9772470297ebdL, (short) -396, (short) -100), + new CachedPower(0xa6dfbd9fb8e5b88fL, (short) -369, (short) -92), + new CachedPower(0xf8a95fcf88747d94L, (short) -343, (short) -84), + new CachedPower(0xb94470938fa89bcfL, (short) -316, (short) -76), + new CachedPower(0x8a08f0f8bf0f156bL, (short) -289, (short) -68), + new CachedPower(0xcdb02555653131b6L, (short) -263, (short) -60), + new CachedPower(0x993fe2c6d07b7facL, (short) -236, (short) -52), + new CachedPower(0xe45c10c42a2b3b06L, (short) -210, (short) -44), + new CachedPower(0xaa242499697392d3L, (short) -183, (short) -36), + new CachedPower(0xfd87b5f28300ca0eL, (short) -157, (short) -28), + new CachedPower(0xbce5086492111aebL, (short) -130, (short) -20), + new CachedPower(0x8cbccc096f5088ccL, (short) -103, (short) -12), + new CachedPower(0xd1b71758e219652cL, (short) -77, (short) -4), + new CachedPower(0x9c40000000000000L, (short) -50, (short) 4), + new CachedPower(0xe8d4a51000000000L, (short) -24, (short) 12), + new CachedPower(0xad78ebc5ac620000L, (short) 3, (short) 20), + new CachedPower(0x813f3978f8940984L, (short) 30, (short) 28), + new CachedPower(0xc097ce7bc90715b3L, (short) 56, (short) 36), + new CachedPower(0x8f7e32ce7bea5c70L, (short) 83, (short) 44), + new CachedPower(0xd5d238a4abe98068L, (short) 109, (short) 52), + new CachedPower(0x9f4f2726179a2245L, (short) 136, (short) 60), + new CachedPower(0xed63a231d4c4fb27L, (short) 162, (short) 68), + new CachedPower(0xb0de65388cc8ada8L, (short) 189, (short) 76), + new CachedPower(0x83c7088e1aab65dbL, (short) 216, (short) 84), + new CachedPower(0xc45d1df942711d9aL, (short) 242, (short) 92), + new CachedPower(0x924d692ca61be758L, (short) 269, (short) 100), + new CachedPower(0xda01ee641a708deaL, (short) 295, (short) 108), + new CachedPower(0xa26da3999aef774aL, (short) 322, (short) 116), + new CachedPower(0xf209787bb47d6b85L, (short) 348, (short) 124), + new CachedPower(0xb454e4a179dd1877L, (short) 375, (short) 132), + new CachedPower(0x865b86925b9bc5c2L, (short) 402, (short) 140), + new CachedPower(0xc83553c5c8965d3dL, (short) 428, (short) 148), + new CachedPower(0x952ab45cfa97a0b3L, (short) 455, (short) 156), + new CachedPower(0xde469fbd99a05fe3L, (short) 481, (short) 164), + new CachedPower(0xa59bc234db398c25L, (short) 508, (short) 172), + new CachedPower(0xf6c69a72a3989f5cL, (short) 534, (short) 180), + new CachedPower(0xb7dcbf5354e9beceL, (short) 561, (short) 188), + new CachedPower(0x88fcf317f22241e2L, (short) 588, (short) 196), + new CachedPower(0xcc20ce9bd35c78a5L, (short) 614, (short) 204), + new CachedPower(0x98165af37b2153dfL, (short) 641, (short) 212), + new CachedPower(0xe2a0b5dc971f303aL, (short) 667, (short) 220), + new CachedPower(0xa8d9d1535ce3b396L, (short) 694, (short) 228), + new CachedPower(0xfb9b7cd9a4a7443cL, (short) 720, (short) 236), + new CachedPower(0xbb764c4ca7a44410L, (short) 747, (short) 244), + new CachedPower(0x8bab8eefb6409c1aL, (short) 774, (short) 252), + new CachedPower(0xd01fef10a657842cL, (short) 800, (short) 260), + new CachedPower(0x9b10a4e5e9913129L, (short) 827, (short) 268), + new CachedPower(0xe7109bfba19c0c9dL, (short) 853, (short) 276), + new CachedPower(0xac2820d9623bf429L, (short) 880, (short) 284), + new CachedPower(0x80444b5e7aa7cf85L, (short) 907, (short) 292), + new CachedPower(0xbf21e44003acdd2dL, (short) 933, (short) 300), + new CachedPower(0x8e679c2f5e44ff8fL, (short) 960, (short) 308), + new CachedPower(0xd433179d9c8cb841L, (short) 986, (short) 316), + new CachedPower(0x9e19db92b4e31ba9L, (short) 1013, (short) 324), + new CachedPower(0xeb96bf6ebadf77d9L, (short) 1039, (short) 332), + new CachedPower(0xaf87023b9bf0ee6bL, (short) 1066, (short) 340) + }; + + // nb elements (8): 82 + + static final int GRISU_CACHE_OFFSET = 308; + } + + private static class DoubleHelper { + + static final long kExponentMask = 0x7FF0000000000000L; + static final long kSignificandMask = 0x000FFFFFFFFFFFFFL; + static final long kHiddenBit = 0x0010000000000000L; + + static void asDiyFp(long d64, DiyFp v) { + v.f = significand(d64); + v.e = exponent(d64); + } + + // this->Significand() must not be 0. + static void asNormalizedDiyFp(long d64, DiyFp w) { + long f = significand(d64); + int e = exponent(d64); + + // The current double could be a denormal. + while ((f & kHiddenBit) == 0) { + f <<= 1; + e--; + } + // Do the final shifts in one go. Don't forget the hidden bit (the '-1'). + f <<= DiyFp.kSignificandSize - kSignificandSize - 1; + e -= DiyFp.kSignificandSize - kSignificandSize - 1; + w.f = f; + w.e = e; + } + + static int exponent(long d64) { + if (isDenormal(d64)) return kDenormalExponent; + + int biased_e = (int) (((d64 & kExponentMask) >>> kSignificandSize) & 0xffffffffL); + return biased_e - kExponentBias; + } + + static long significand(long d64) { + long significand = d64 & kSignificandMask; + if (!isDenormal(d64)) { + return significand + kHiddenBit; + } else { + return significand; + } + } + + // Returns true if the double is a denormal. + private static boolean isDenormal(long d64) { + return (d64 & kExponentMask) == 0L; + } + + // Returns the two boundaries of first argument. + // The bigger boundary (m_plus) is normalized. The lower boundary has the same + // exponent as m_plus. + static void normalizedBoundaries(DiyFp v, long d64, DiyFp m_minus, DiyFp m_plus) { + asDiyFp(d64, v); + final boolean significand_is_zero = (v.f == kHiddenBit); + m_plus.f = (v.f << 1) + 1; + m_plus.e = v.e - 1; + m_plus.normalize(); + if (significand_is_zero && v.e != kDenormalExponent) { + // The boundary is closer. Think of v = 1000e10 and v- = 9999e9. + // Then the boundary (== (v - v-)/2) is not just at a distance of 1e9 but + // at a distance of 1e8. + // The only exception is for the smallest normal: the largest denormal is + // at the same distance as its successor. + // Note: denormals have the same exponent as the smallest normals. + m_minus.f = (v.f << 2) - 1; + m_minus.e = v.e - 2; + } else { + m_minus.f = (v.f << 1) - 1; + m_minus.e = v.e - 1; + } + m_minus.f = m_minus.f << (m_minus.e - m_plus.e); + m_minus.e = m_plus.e; + } + + private static final int kSignificandSize = 52; // Excludes the hidden bit. + private static final int kExponentBias = 0x3FF + kSignificandSize; + private static final int kDenormalExponent = -kExponentBias + 1; + + } + + static class FastDtoa { + + // Adjusts the last digit of the generated number, and screens out generated + // solutions that may be inaccurate. A solution may be inaccurate if it is + // outside the safe interval, or if we ctannot prove that it is closer to the + // input than a neighboring representation of the same length. + // + // Input: * buffer containing the digits of too_high / 10^kappa + // * distance_too_high_w == (too_high - w).f() * unit + // * unsafe_interval == (too_high - too_low).f() * unit + // * rest = (too_high - buffer * 10^kappa).f() * unit + // * ten_kappa = 10^kappa * unit + // * unit = the common multiplier + // Output: returns true if the buffer is guaranteed to contain the closest + // representable number to the input. + // Modifies the generated digits in the buffer to approach (round towards) w. + static boolean roundWeed( + final FastDtoaBuilder buffer, + final long distance_too_high_w, + final long unsafe_interval, + long rest, + final long ten_kappa, + final long unit) { + final long small_distance = distance_too_high_w - unit; + final long big_distance = distance_too_high_w + unit; + // Let w_low = too_high - big_distance, and + // w_high = too_high - small_distance. + // Note: w_low < w < w_high + // + // The real w (* unit) must lie somewhere inside the interval + // ]w_low; w_low[ (often written as "(w_low; w_low)") + + // Basically the buffer currently contains a number in the unsafe interval + // ]too_low; too_high[ with too_low < w < too_high + // + // too_high - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // ^v 1 unit ^ ^ ^ ^ + // boundary_high --------------------- . . . . + // ^v 1 unit . . . . + // - - - - - - - - - - - - - - - - - - - + - - + - - - - - - . . + // . . ^ . . + // . big_distance . . . + // . . . . rest + // small_distance . . . . + // v . . . . + // w_high - - - - - - - - - - - - - - - - - - . . . . + // ^v 1 unit . . . . + // w ---------------------------------------- . . . . + // ^v 1 unit v . . . + // w_low - - - - - - - - - - - - - - - - - - - - - . . . + // . . v + // buffer --------------------------------------------------+-------+-------- + // . . + // safe_interval . + // v . + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - . + // ^v 1 unit . + // boundary_low ------------------------- unsafe_interval + // ^v 1 unit v + // too_low - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // + // + // Note that the value of buffer could lie anywhere inside the range too_low + // to too_high. + // + // boundary_low, boundary_high and w are approximations of the real boundaries + // and v (the input number). They are guaranteed to be precise up to one unit. + // In fact the error is guaranteed to be strictly less than one unit. + // + // Anything that lies outside the unsafe interval is guaranteed not to round + // to v when read again. + // Anything that lies inside the safe interval is guaranteed to round to v + // when read again. + // If the number inside the buffer lies inside the unsafe interval but not + // inside the safe interval then we simply do not know and bail out (returning + // false). + // + // Similarly we have to take into account the imprecision of 'w' when rounding + // the buffer. If we have two potential representations we need to make sure + // that the chosen one is closer to w_low and w_high since v can be anywhere + // between them. + // + // By generating the digits of too_high we got the largest (closest to + // too_high) buffer that is still in the unsafe interval. In the case where + // w_high < buffer < too_high we try to decrement the buffer. + // This way the buffer approaches (rounds towards) w. + // There are 3 conditions that stop the decrementation process: + // 1) the buffer is already below w_high + // 2) decrementing the buffer would make it leave the unsafe interval + // 3) decrementing the buffer would yield a number below w_high and farther + // away than the current number. In other words: + // (buffer{-1} < w_high) && w_high - buffer{-1} > buffer - w_high + // Instead of using the buffer directly we use its distance to too_high. + // Conceptually rest ~= too_high - buffer + while (rest < small_distance && // Negated condition 1 + unsafe_interval - rest >= ten_kappa && // Negated condition 2 + (rest + ten_kappa < small_distance || // buffer{-1} > w_high + small_distance - rest >= rest + ten_kappa - small_distance)) { + buffer.decreaseLast(); + rest += ten_kappa; + } + + // We have approached w+ as much as possible. We now test if approaching w- + // would require changing the buffer. If yes, then we have two possible + // representations close to w, but we cannot decide which one is closer. + if (rest < big_distance && + unsafe_interval - rest >= ten_kappa && + (rest + ten_kappa < big_distance || + big_distance - rest > rest + ten_kappa - big_distance)) { + return false; + } + + // Weeding test. + // The safe interval is [too_low + 2 ulp; too_high - 2 ulp] + // Since too_low = too_high - unsafe_interval this is equivalent to + // [too_high - unsafe_interval + 4 ulp; too_high - 2 ulp] + // Conceptually we have: rest ~= too_high - buffer + return (2 * unit <= rest) && (rest <= unsafe_interval - 4 * unit); + } + + static final int kTen4 = 10000; + static final int kTen5 = 100000; + static final int kTen6 = 1000000; + static final int kTen7 = 10000000; + static final int kTen8 = 100000000; + static final int kTen9 = 1000000000; + + // Returns the biggest power of ten that is less than or equal than the given + // number. We furthermore receive the maximum number of bits 'number' has. + // If number_bits == 0 then 0^-1 is returned + // The number of bits must be <= 32. + // Precondition: (1 << number_bits) <= number < (1 << (number_bits + 1)). + static long biggestPowerTen(int number, int number_bits) { + int power, exponent; + switch (number_bits) { + case 32: + case 31: + case 30: + if (kTen9 <= number) { + power = kTen9; + exponent = 9; + break; + } // else fallthrough + case 29: + case 28: + case 27: + if (kTen8 <= number) { + power = kTen8; + exponent = 8; + break; + } // else fallthrough + case 26: + case 25: + case 24: + if (kTen7 <= number) { + power = kTen7; + exponent = 7; + break; + } // else fallthrough + case 23: + case 22: + case 21: + case 20: + if (kTen6 <= number) { + power = kTen6; + exponent = 6; + break; + } // else fallthrough + case 19: + case 18: + case 17: + if (kTen5 <= number) { + power = kTen5; + exponent = 5; + break; + } // else fallthrough + case 16: + case 15: + case 14: + if (kTen4 <= number) { + power = kTen4; + exponent = 4; + break; + } // else fallthrough + case 13: + case 12: + case 11: + case 10: + if (1000 <= number) { + power = 1000; + exponent = 3; + break; + } // else fallthrough + case 9: + case 8: + case 7: + if (100 <= number) { + power = 100; + exponent = 2; + break; + } // else fallthrough + case 6: + case 5: + case 4: + if (10 <= number) { + power = 10; + exponent = 1; + break; + } // else fallthrough + case 3: + case 2: + case 1: + if (1 <= number) { + power = 1; + exponent = 0; + break; + } // else fallthrough + case 0: + power = 0; + exponent = -1; + break; + default: + // Following assignments are here to silence compiler warnings. + power = 0; + exponent = 0; + // UNREACHABLE(); + } + return ((long) power << 32) | (0xffffffffL & exponent); + } + + // Generates the digits of input number w. + // w is a floating-point number (DiyFp), consisting of a significand and an + // exponent. Its exponent is bounded by minimal_target_exponent and + // maximal_target_exponent. + // Hence -60 <= w.e() <= -32. + // + // Returns false if it fails, in which case the generated digits in the buffer + // should not be used. + // Preconditions: + // * low, w and high are correct up to 1 ulp (unit in the last place). That + // is, their error must be less that a unit of their last digits. + // * low.e() == w.e() == high.e() + // * low < w < high, and taking into account their error: low~ <= high~ + // * minimal_target_exponent <= w.e() <= maximal_target_exponent + // Postconditions: returns false if procedure fails. + // otherwise: + // * buffer is not null-terminated, but len contains the number of digits. + // * buffer contains the shortest possible decimal digit-sequence + // such that LOW < buffer * 10^kappa < HIGH, where LOW and HIGH are the + // correct values of low and high (without their error). + // * if more than one decimal representation gives the minimal number of + // decimal digits then the one closest to W (where W is the correct value + // of w) is chosen. + // Remark: this procedure takes into account the imprecision of its input + // numbers. If the precision is not enough to guarantee all the postconditions + // then false is returned. This usually happens rarely (~0.5%). + // + // Say, for the sake of example, that + // w.e() == -48, and w.f() == 0x1234567890abcdef + // w's value can be computed by w.f() * 2^w.e() + // We can obtain w's integral digits by simply shifting w.f() by -w.e(). + // -> w's integral part is 0x1234 + // w's fractional part is therefore 0x567890abcdef. + // Printing w's integral part is easy (simply print 0x1234 in decimal). + // In order to print its fraction we repeatedly multiply the fraction by 10 and + // get each digit. Example the first digit after the point would be computed by + // (0x567890abcdef * 10) >> 48. -> 3 + // The whole thing becomes slightly more complicated because we want to stop + // once we have enough digits. That is, once the digits inside the buffer + // represent 'w' we can stop. Everything inside the interval low - high + // represents w. However we have to pay attention to low, high and w's + // imprecision. + static boolean digitGen(FastDtoaBuilder buffer, int mk) { + final DiyFp low = buffer.scaled_boundary_minus; + final DiyFp w = buffer.scaled_w; + final DiyFp high = buffer.scaled_boundary_plus; + + // low, w and high are imprecise, but by less than one ulp (unit in the last + // place). + // If we remove (resp. add) 1 ulp from low (resp. high) we are certain that + // the new numbers are outside of the interval we want the final + // representation to lie in. + // Inversely adding (resp. removing) 1 ulp from low (resp. high) would yield + // numbers that are certain to lie in the interval. We will use this fact + // later on. + // We will now start by generating the digits within the uncertain + // interval. Later we will weed out representations that lie outside the safe + // interval and thus _might_ lie outside the correct interval. + long unit = 1; + final DiyFp too_low = buffer.too_low; + too_low.f = low.f - unit; + too_low.e = low.e; + final DiyFp too_high = buffer.too_high; + too_high.f = high.f + unit; + too_high.e = high.e; + // too_low and too_high are guaranteed to lie outside the interval we want the + // generated number in. + final DiyFp unsafe_interval = buffer.unsafe_interval; + unsafe_interval.f = too_high.f; + unsafe_interval.e = too_high.e; + unsafe_interval.subtract(too_low); + // We now cut the input number into two parts: the integral digits and the + // fractionals. We will not write any decimal separator though, but adapt + // kappa instead. + // Reminder: we are currently computing the digits (stored inside the buffer) + // such that: too_low < buffer * 10^kappa < too_high + // We use too_high for the digit_generation and stop as soon as possible. + // If we stop early we effectively round down. + final DiyFp one = buffer.one; + one.f = 1L << -w.e; + one.e = w.e; + // Division by one is a shift. + int integrals = (int) ((too_high.f >>> -one.e) & 0xffffffffL); + // Modulo by one is an and. + long fractionals = too_high.f & (one.f - 1); + long result = biggestPowerTen(integrals, DiyFp.kSignificandSize - (-one.e)); + int divider = (int) ((result >>> 32) & 0xffffffffL); + int divider_exponent = (int) (result & 0xffffffffL); + int kappa = divider_exponent + 1; + // Loop invariant: buffer = too_high / 10^kappa (integer division) + // The invariant holds for the first iteration: kappa has been initialized + // with the divider exponent + 1. And the divider is the biggest power of ten + // that is smaller than integrals. + while (kappa > 0) { + int digit = integrals / divider; + buffer.append((byte) ('0' + digit)); + integrals %= divider; + kappa--; + // Note that kappa now equals the exponent of the divider and that the + // invariant thus holds again. + final long rest = ((long) integrals << -one.e) + fractionals; + // Invariant: too_high = buffer * 10^kappa + DiyFp(rest, one.e()) + // Reminder: unsafe_interval.e() == one.e() + if (rest < unsafe_interval.f) { + // Rounding down (by not emitting the remaining digits) yields a number + // that lies within the unsafe interval. + buffer.point = buffer.end - mk + kappa; + final DiyFp minus_round = buffer.minus_round; + minus_round.f = too_high.f; + minus_round.e = too_high.e; + minus_round.subtract(w); + return roundWeed(buffer, minus_round.f, + unsafe_interval.f, rest, + (long) divider << -one.e, unit); + } + divider /= 10; + } + + // The integrals have been generated. We are at the point of the decimal + // separator. In the following loop we simply multiply the remaining digits by + // 10 and divide by one. We just need to pay attention to multiply associated + // data (like the interval or 'unit'), too. + // Instead of multiplying by 10 we multiply by 5 (cheaper operation) and + // increase its (imaginary) exponent. At the same time we decrease the + // divider's (one's) exponent and shift its significand. + // Basically, if fractionals was a DiyFp (with fractionals.e == one.e): + // fractionals.f *= 10; + // fractionals.f >>= 1; fractionals.e++; // value remains unchanged. + // one.f >>= 1; one.e++; // value remains unchanged. + // and we have again fractionals.e == one.e which allows us to divide + // fractionals.f() by one.f() + // We simply combine the *= 10 and the >>= 1. + while (true) { + fractionals *= 5; + unit *= 5; + unsafe_interval.f = unsafe_interval.f * 5; + unsafe_interval.e = unsafe_interval.e + 1; // Will be optimized out. + one.f = one.f >>> 1; + one.e = one.e + 1; + // Integer division by one. + final int digit = (int) ((fractionals >>> -one.e) & 0xffffffffL); + buffer.append((byte) ('0' + digit)); + fractionals &= one.f - 1; // Modulo by one. + kappa--; + if (fractionals < unsafe_interval.f) { + buffer.point = buffer.end - mk + kappa; + final DiyFp minus_round = buffer.minus_round; + minus_round.f = too_high.f; + minus_round.e = too_high.e; + minus_round.subtract(w); + return roundWeed(buffer, minus_round.f * unit, + unsafe_interval.f, fractionals, one.f, unit); + } + } + } + } + + public static boolean tryConvert(final double value, final FastDtoaBuilder buffer) { + final long bits; + final int firstDigit; + buffer.reset(); + if (value < 0) { + buffer.append((byte) '-'); + bits = Double.doubleToLongBits(-value); + firstDigit = 1; + } else { + bits = Double.doubleToLongBits(value); + firstDigit = 0; + } + + // Provides a decimal representation of v. + // Returns true if it succeeds, otherwise the result cannot be trusted. + // There will be *length digits inside the buffer (not null-terminated). + // If the function returns true then + // v == (double) (buffer * 10^decimal_exponent). + // The digits in the buffer are the shortest representation possible: no + // 0.09999999999999999 instead of 0.1. The shorter representation will even be + // chosen even if the longer one would be closer to v. + // The last digit will be closest to the actual v. That is, even if several + // digits might correctly yield 'v' when read again, the closest will be + // computed. + final int mk = buffer.initialize(bits); + + // DigitGen will generate the digits of scaled_w. Therefore we have + // v == (double) (scaled_w * 10^-mk). + // Set decimal_exponent == -mk and pass it to DigitGen. If scaled_w is not an + // integer than it will be updated. For instance if scaled_w == 1.23 then + // the buffer will be filled with "123" und the decimal_exponent will be + // decreased by 2. + if (FastDtoa.digitGen(buffer, mk)) { + buffer.write(firstDigit); + return true; + } else { + return false; + } + } + + static class FastDtoaBuilder { + + private final DiyFp v = new DiyFp(); + private final DiyFp w = new DiyFp(); + private final DiyFp boundary_minus = new DiyFp(); + private final DiyFp boundary_plus = new DiyFp(); + private final DiyFp ten_mk = new DiyFp(); + private final DiyFp scaled_w = new DiyFp(); + private final DiyFp scaled_boundary_minus = new DiyFp(); + private final DiyFp scaled_boundary_plus = new DiyFp(); + + private final DiyFp too_low = new DiyFp(); + private final DiyFp too_high = new DiyFp(); + private final DiyFp unsafe_interval = new DiyFp(); + private final DiyFp one = new DiyFp(); + private final DiyFp minus_round = new DiyFp(); + + int initialize(final long bits) { + DoubleHelper.asNormalizedDiyFp(bits, w); + // boundary_minus and boundary_plus are the boundaries between v and its + // closest floating-point neighbors. Any number strictly between + // boundary_minus and boundary_plus will round to v when convert to a double. + // Grisu3 will never output representations that lie exactly on a boundary. + boundary_minus.reset(); + boundary_plus.reset(); + DoubleHelper.normalizedBoundaries(v, bits, boundary_minus, boundary_plus); + ten_mk.reset(); // Cached power of ten: 10^-k + final int mk = CachedPowers.getCachedPower(w.e + DiyFp.kSignificandSize, minimal_target_exponent, ten_mk); + // Note that ten_mk is only an approximation of 10^-k. A DiyFp only contains a + // 64 bit significand and ten_mk is thus only precise up to 64 bits. + + // The DiyFp::Times procedure rounds its result, and ten_mk is approximated + // too. The variable scaled_w (as well as scaled_boundary_minus/plus) are now + // off by a small amount. + // In fact: scaled_w - w*10^k < 1ulp (unit in the last place) of scaled_w. + // In other words: let f = scaled_w.f() and e = scaled_w.e(), then + // (f-1) * 2^e < w*10^k < (f+1) * 2^e + scaled_w.f = w.f; + scaled_w.e = w.e; + scaled_w.multiply(ten_mk); + // In theory it would be possible to avoid some recomputations by computing + // the difference between w and boundary_minus/plus (a power of 2) and to + // compute scaled_boundary_minus/plus by subtracting/adding from + // scaled_w. However the code becomes much less readable and the speed + // enhancements are not terriffic. + scaled_boundary_minus.f = boundary_minus.f; + scaled_boundary_minus.e = boundary_minus.e; + scaled_boundary_minus.multiply(ten_mk); + scaled_boundary_plus.f = boundary_plus.f; + scaled_boundary_plus.e = boundary_plus.e; + scaled_boundary_plus.multiply(ten_mk); + + return mk; + } + + // allocate buffer for generated digits + extra notation + padding zeroes + private final byte[] chars = new byte[kFastDtoaMaximalLength + 10]; + private int end = 0; + private int point; + + void reset() { + end = 0; + } + + void append(byte c) { + chars[end++] = c; + } + + void decreaseLast() { + chars[end - 1]--; + } + + @Override + public String toString() { + return "[chars:" + new String(chars, 0, end) + ", point:" + point + "]"; + } + + int copyTo(final byte[] target, final int position) { + for (int i = 0; i < end; i++) { + target[i + position] = chars[i]; + } + return end; + } + + public void write(int firstDigit) { + // check for minus sign + int decPoint = point - firstDigit; + if (decPoint < -5 || decPoint > 21) { + toExponentialFormat(firstDigit, decPoint); + } else { + toFixedFormat(firstDigit, decPoint); + } + } + + private void toFixedFormat(int firstDigit, int decPoint) { + if (point < end) { + // insert decimal point + if (decPoint > 0) { + // >= 1, split decimals and insert point + for (int i = end; i >= point; i--) { + chars[i + 1] = chars[i]; + } + chars[point] = '.'; + end++; + } else { + // < 1, + final int offset = 2 - decPoint; + for (int i = end + firstDigit; i >= firstDigit; i--) { + chars[i + offset] = chars[i]; + } + chars[firstDigit] = '0'; + chars[firstDigit + 1] = '.'; + if (decPoint < 0) { + int target = firstDigit + 2 - decPoint; + for (int i = firstDigit + 2; i < target; i++) { + chars[i] = '0'; + } + } + end += 2 - decPoint; + } + } else if (point > end) { + // large integer, add trailing zeroes + for (int i = end; i < point; i++) { + chars[i] = '0'; + } + end += point - end; + chars[end] = '.'; + chars[end + 1] = '0'; + end += 2; + } else { + chars[end] = '.'; + chars[end + 1] = '0'; + end += 2; + } + } + + private void toExponentialFormat(int firstDigit, int decPoint) { + if (end - firstDigit > 1) { + // insert decimal point if more than one digit was produced + int dot = firstDigit + 1; + System.arraycopy(chars, dot, chars, dot + 1, end - dot); + chars[dot] = '.'; + end++; + } + chars[end++] = 'E'; + byte sign = '+'; + int exp = decPoint - 1; + if (exp < 0) { + sign = '-'; + exp = -exp; + } + chars[end++] = sign; + + int charPos = exp > 99 ? end + 2 : exp > 9 ? end + 1 : end; + end = charPos + 1; + + do { + int r = exp % 10; + chars[charPos--] = digits[r]; + exp = exp / 10; + } while (exp != 0); + } + + final static byte[] digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; + } +} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/JsonObject.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/JsonObject.java new file mode 100644 index 0000000000..f773e2e6b6 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/JsonObject.java @@ -0,0 +1,35 @@ +package com.bugsnag.android.repackaged.dslplatform.json; + +/** + * Objects which implement this interface are supported for serialization in DslJson. + * This is used by DSL Platform POJO objects. + * Annotation processor uses a different method, since it can't modify existing objects to add such signature into them. + * + * Objects which implement JsonObject support convention based deserialization in form of public static JSON_READER + * An example: + * + *

+ *     public class MyCustomPojo implements JsonObject {
+ *       public void serialize(JsonWriter writer, boolean minimal) {
+ *         //implement serialization logic, eg: writer.writeAscii("{\"my\":\"object\"}");
+ *       }
+ *       public static final JsonReader.ReadJsonObject<MyCustomPojo> JSON_READER = new JsonReader.ReadJsonObject<MyCustomPojo>() {
+ *         public MyCustomPojo deserialize(JsonReader reader) throws IOException {
+ *           //implement deserialization logic, eg: return new MyCustomPojo();
+ *         }
+ *       }
+ *     }
+ * 
+ * + */ +public interface JsonObject { + /** + * Serialize object instance into JsonWriter. + * In DslJson minimal serialization stands for serialization which omits unnecessary information from JSON. + * An example of such data is false for boolean or null for Integer which can be reconstructed from type definition. + * + * @param writer write JSON into target writer + * @param minimal is minimal serialization requested + */ + void serialize(JsonWriter writer, boolean minimal); +} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/JsonReader.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/JsonReader.java new file mode 100644 index 0000000000..addb14a6c2 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/JsonReader.java @@ -0,0 +1,1783 @@ +package com.bugsnag.android.repackaged.dslplatform.json; + +import androidx.annotation.Nullable; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.*; + +/** + * Object for processing JSON from byte[] and InputStream. + * DSL-JSON works on byte level (instead of char level). + * Deserialized instances can obtain TContext information provided with this reader. + *

+ * JsonReader can be reused by calling process methods. + * + * @param context passed to deserialized object instances + */ +@SuppressWarnings({"rawtypes", "serial"}) // suppress pre-existing warnings +public final class JsonReader { + + private static final boolean[] WHITESPACE = new boolean[256]; + private static final Charset utf8 = Charset.forName("UTF-8"); + + static { + WHITESPACE[9 + 128] = true; + WHITESPACE[10 + 128] = true; + WHITESPACE[11 + 128] = true; + WHITESPACE[12 + 128] = true; + WHITESPACE[13 + 128] = true; + WHITESPACE[32 + 128] = true; + WHITESPACE[-96 + 128] = true; + WHITESPACE[-31 + 128] = true; + WHITESPACE[-30 + 128] = true; + WHITESPACE[-29 + 128] = true; + } + + private int tokenStart; + private int nameEnd; + private int currentIndex = 0; + private long currentPosition = 0; + private byte last = ' '; + + private int length; + private final char[] tmp; + + public final TContext context; + protected byte[] buffer; + protected char[] chars; + + private InputStream stream; + private int readLimit; + //always leave some room for reading special stuff, so that buffer contains enough padding for such optimizations + private int bufferLenWithExtraSpace; + + private final StringCache keyCache; + private final StringCache valuesCache; + private final TypeLookup typeLookup; + + private final byte[] originalBuffer; + private final int originalBufferLenWithExtraSpace; + + public enum ErrorInfo { + WITH_STACK_TRACE, + DESCRIPTION_AND_POSITION, + DESCRIPTION_ONLY, + MINIMAL + } + + public enum DoublePrecision { + EXACT(0), + HIGH(1), + DEFAULT(3), + LOW(4); + + final int level; + + DoublePrecision(int level) { + this.level = level; + } + } + + public enum UnknownNumberParsing { + LONG_AND_BIGDECIMAL, + LONG_AND_DOUBLE, + BIGDECIMAL, + DOUBLE + } + + protected final ErrorInfo errorInfo; + protected final DoublePrecision doublePrecision; + protected final int doubleLengthLimit; + protected final UnknownNumberParsing unknownNumbers; + protected final int maxNumberDigits; + private final int maxStringBuffer; + + private JsonReader( + final char[] tmp, + final byte[] buffer, + final int length, + @Nullable final TContext context, + @Nullable final StringCache keyCache, + @Nullable final StringCache valuesCache, + @Nullable final TypeLookup typeLookup, + final ErrorInfo errorInfo, + final DoublePrecision doublePrecision, + final UnknownNumberParsing unknownNumbers, + final int maxNumberDigits, + final int maxStringBuffer) { + this.tmp = tmp; + this.buffer = buffer; + this.length = length; + this.bufferLenWithExtraSpace = buffer.length - 38; //currently maximum padding is for uuid + this.context = context; + this.chars = tmp; + this.keyCache = keyCache; + this.valuesCache = valuesCache; + this.typeLookup = typeLookup; + this.errorInfo = errorInfo; + this.doublePrecision = doublePrecision; + this.unknownNumbers = unknownNumbers; + this.maxNumberDigits = maxNumberDigits; + this.maxStringBuffer = maxStringBuffer; + this.doubleLengthLimit = 15 + doublePrecision.level; + this.originalBuffer = buffer; + this.originalBufferLenWithExtraSpace = bufferLenWithExtraSpace; + } + + /** + * Prefer creating reader through DslJson#newReader since it will pass several arguments (such as key/string value cache) + * First byte will not be read. + * It will allocate new char[64] for string buffer. + * Key and string vales cache will be null. + * + * @param buffer input JSON + * @param context context + */ + @Deprecated + public JsonReader(final byte[] buffer, @Nullable final TContext context) { + this(buffer, context, null, null); + } + + @Deprecated + public JsonReader(final byte[] buffer, @Nullable final TContext context, @Nullable StringCache keyCache, @Nullable StringCache valuesCache) { + this(buffer, buffer.length, context, new char[64], keyCache, valuesCache); + } + + @Deprecated + public JsonReader(final byte[] buffer, final TContext context, final char[] tmp) { + this(buffer, buffer.length, context, tmp); + if (tmp == null) { + throw new IllegalArgumentException("tmp buffer provided as null."); + } + } + + @Deprecated + public JsonReader(final byte[] buffer, final int length, final TContext context) { + this(buffer, length, context, new char[64]); + } + + @Deprecated + public JsonReader(final byte[] buffer, final int length, final TContext context, final char[] tmp) { + this(buffer, length, context, tmp, null, null); + } + + @Deprecated + public JsonReader(final byte[] buffer, final int length, @Nullable final TContext context, final char[] tmp, @Nullable final StringCache keyCache, @Nullable final StringCache valuesCache) { + this(tmp, buffer, length, context, keyCache, valuesCache, null, ErrorInfo.WITH_STACK_TRACE, DoublePrecision.DEFAULT, UnknownNumberParsing.LONG_AND_BIGDECIMAL, 512, 256 * 1024 * 1024); + if (tmp == null) { + throw new IllegalArgumentException("tmp buffer provided as null."); + } + if (length > buffer.length) { + throw new IllegalArgumentException("length can't be longer than buffer.length"); + } else if (length < buffer.length) { + buffer[length] = '\0'; + } + } + + JsonReader( + final byte[] buffer, + final int length, + @Nullable final TContext context, + final char[] tmp, + @Nullable final StringCache keyCache, + @Nullable final StringCache valuesCache, + @Nullable final TypeLookup typeLookup, + final ErrorInfo errorInfo, + final DoublePrecision doublePrecision, + final UnknownNumberParsing unknownNumbers, + final int maxNumberDigits, + final int maxStringBuffer) { + this(tmp, buffer, length, context, keyCache, valuesCache, typeLookup, errorInfo, doublePrecision, unknownNumbers, maxNumberDigits, maxStringBuffer); + if (tmp == null) { + throw new IllegalArgumentException("tmp buffer provided as null."); + } + if (length > buffer.length) { + throw new IllegalArgumentException("length can't be longer than buffer.length"); + } else if (length < buffer.length) { + buffer[length] = '\0'; + } + } + + + /** + * Will be removed. Exists only for backward compatibility + * @param stream process stream + * @throws IOException error reading from stream + */ + @Deprecated + public final void reset(final InputStream stream) throws IOException { + process(stream); + } + + /** + * Will be removed. Exists only for backward compatibility + * @param size size of byte[] input to use + */ + @Deprecated + final void reset(final int size) { + process(null, size); + } + + /** + * Reset reader after processing input + * It will release reference to provided byte[] or InputStream input + */ + final void reset() { + this.buffer = this.originalBuffer; + this.bufferLenWithExtraSpace = this.originalBufferLenWithExtraSpace; + currentIndex = 0; + this.length = 0; + this.readLimit = 0; + this.stream = null; + } + + /** + * Bind input stream for processing. + * Stream will be processed in byte[] chunks. + * If stream is null, reference to stream will be released. + * + * @param stream set input stream + * @return itself + * @throws IOException unable to read from stream + */ + public final JsonReader process(@Nullable final InputStream stream) throws IOException { + this.currentPosition = 0; + this.currentIndex = 0; + this.stream = stream; + if (stream != null) { + this.readLimit = this.length < bufferLenWithExtraSpace ? this.length : bufferLenWithExtraSpace; + final int available = readFully(buffer, stream, 0); + readLimit = available < bufferLenWithExtraSpace ? available : bufferLenWithExtraSpace; + this.length = available; + } + return this; + } + + /** + * Bind byte[] buffer for processing. + * If this method is used in combination with process(InputStream) this buffer will be used for processing chunks of stream. + * If null is sent for byte[] buffer, new length for valid input will be set for existing buffer. + * + * @param newBuffer new buffer to use for processing + * @param newLength length of buffer which can be used + * @return itself + */ + public final JsonReader process(@Nullable final byte[] newBuffer, final int newLength) { + if (newBuffer != null) { + this.buffer = newBuffer; + this.bufferLenWithExtraSpace = buffer.length - 38; //currently maximum padding is for uuid + } + if (newLength > buffer.length) { + throw new IllegalArgumentException("length can't be longer than buffer.length"); + } + currentIndex = 0; + this.length = newLength; + this.stream = null; + this.readLimit = newLength; + return this; + } + + /** + * Valid length of the input buffer. + * + * @return size of JSON input + */ + public final int length() { + return length; + } + + @Override + public String toString() { + return new String(buffer, 0, length, utf8); + } + + private static int readFully(final byte[] buffer, final InputStream stream, final int offset) throws IOException { + int read; + int position = offset; + while (position < buffer.length + && (read = stream.read(buffer, position, buffer.length - position)) != -1) { + position += read; + } + return position; + } + + private static class EmptyEOFException extends EOFException { + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } + } + private static final EOFException eof = new EmptyEOFException(); + + boolean withStackTrace() { + return errorInfo == ErrorInfo.WITH_STACK_TRACE; + } + + /** + * Read next byte from the JSON input. + * If buffer has been read in full IOException will be thrown + * + * @return next byte + * @throws IOException when end of JSON input + */ + public final byte read() throws IOException { + if (stream != null && currentIndex > readLimit) { + prepareNextBlock(); + } + if (currentIndex >= length) { + throw ParsingException.create("Unexpected end of JSON input", eof, withStackTrace()); + } + return last = buffer[currentIndex++]; + } + + private int prepareNextBlock() throws IOException { + final int len = length - currentIndex; + System.arraycopy(buffer, currentIndex, buffer, 0, len); + final int available = readFully(buffer, stream, len); + currentPosition += currentIndex; + if (available == len) { + readLimit = length - currentIndex; + length = readLimit; + currentIndex = 0; + } else { + readLimit = available < bufferLenWithExtraSpace ? available : bufferLenWithExtraSpace; + this.length = available; + currentIndex = 0; + } + return available; + } + + final boolean isEndOfStream() throws IOException { + if (stream == null) { + return length == currentIndex; + } + if (length != currentIndex) { + return false; + } + return prepareNextBlock() == 0; + } + + /** + * Which was last byte read from the JSON input. + * JsonReader doesn't allow to go back, but it remembers previously read byte + * + * @return which was the last byte read + */ + public final byte last() { + return last; + } + + public String positionDescription() { + return positionDescription(0); + } + + public String positionDescription(int offset) { + final StringBuilder error = new StringBuilder(60); + positionDescription(offset, error); + return error.toString(); + } + + private void positionDescription(int offset, StringBuilder error) { + error.append("at position: ").append(positionInStream(offset)); + if (currentIndex > offset) { + try { + int maxLen = Math.min(currentIndex - offset, 20); + String prefix = new String(buffer, currentIndex - offset - maxLen, maxLen, utf8); + error.append(", following: `"); + error.append(prefix); + error.append('`'); + } catch (Exception ignore) { + } + } + if (currentIndex - offset < readLimit) { + try { + int maxLen = Math.min(readLimit - currentIndex + offset, 20); + String suffix = new String(buffer, currentIndex - offset, maxLen, utf8); + error.append(", before: `"); + error.append(suffix); + error.append('`'); + } catch (Exception ignore) { + } + } + } + + private final StringBuilder error = new StringBuilder(0); + private final Formatter errorFormatter = new Formatter(error); + + public final ParsingException newParseError(final String description) { + return newParseError(description, 0); + } + + public final ParsingException newParseError(final String description, final int positionOffset) { + if (errorInfo == ErrorInfo.MINIMAL) return ParsingException.create(description, false); + error.setLength(0); + error.append(description); + error.append(". Found "); + error.append((char)last); + if (errorInfo == ErrorInfo.DESCRIPTION_ONLY) return ParsingException.create(error.toString(), false); + error.append(" "); + positionDescription(positionOffset, error); + return ParsingException.create(error.toString(), withStackTrace()); + } + + public final ParsingException newParseErrorAt(final String description, final int positionOffset) { + if (errorInfo == ErrorInfo.MINIMAL || errorInfo == ErrorInfo.DESCRIPTION_ONLY) { + return ParsingException.create(description, false); + } + error.setLength(0); + error.append(description); + error.append(" "); + positionDescription(positionOffset, error); + return ParsingException.create(error.toString(), withStackTrace()); + } + + public final ParsingException newParseErrorAt(final String description, final int positionOffset, final Exception cause) { + if (cause == null) throw new IllegalArgumentException("cause can't be null"); + if (errorInfo == ErrorInfo.MINIMAL) return ParsingException.create(description, cause, false); + error.setLength(0); + final String msg = cause.getMessage(); + if (msg != null && msg.length() > 0) { + error.append(msg); + if (!msg.endsWith(".")) { + error.append("."); + } + error.append(" "); + } + error.append(description); + if (errorInfo == ErrorInfo.DESCRIPTION_ONLY) return ParsingException.create(error.toString(), cause, false); + error.append(" "); + positionDescription(positionOffset, error); + return ParsingException.create(error.toString(), withStackTrace()); + } + + public final ParsingException newParseErrorFormat(final String shortDescription, final int positionOffset, final String longDescriptionFormat, Object... arguments) { + if (errorInfo == ErrorInfo.MINIMAL) return ParsingException.create(shortDescription, false); + error.setLength(0); + errorFormatter.format(longDescriptionFormat, arguments); + if (errorInfo == ErrorInfo.DESCRIPTION_ONLY) return ParsingException.create(error.toString(), false); + error.append(" "); + positionDescription(positionOffset, error); + return ParsingException.create(error.toString(), withStackTrace()); + } + + public final ParsingException newParseErrorWith( + final String description, @Nullable Object argument) { + return newParseErrorWith(description, 0, "", description, argument, ""); + } + + public final ParsingException newParseErrorWith( + final String shortDescription, + final int positionOffset, + final String longDescriptionPrefix, + final String longDescriptionMessage, @Nullable Object argument, + final String longDescriptionSuffix) { + if (errorInfo == ErrorInfo.MINIMAL) return ParsingException.create(shortDescription, false); + error.setLength(0); + error.append(longDescriptionPrefix); + error.append(longDescriptionMessage); + if (argument != null) { + error.append(": '"); + error.append(argument.toString()); + error.append("'"); + } + error.append(longDescriptionSuffix); + if (errorInfo == ErrorInfo.DESCRIPTION_ONLY) return ParsingException.create(error.toString(), false); + error.append(" "); + positionDescription(positionOffset, error); + return ParsingException.create(error.toString(), withStackTrace()); + } + + public final int getTokenStart() { + return tokenStart; + } + + public final int getCurrentIndex() { + return currentIndex; + } + + /** + * will be removed. not used anymore + * + * @return parsed chars from a number + */ + @Deprecated + public final char[] readNumber() { + tokenStart = currentIndex - 1; + tmp[0] = (char) last; + int i = 1; + int ci = currentIndex; + byte bb = last; + while (i < tmp.length && ci < length) { + bb = buffer[ci++]; + if (bb == ',' || bb == '}' || bb == ']') break; + tmp[i++] = (char) bb; + } + currentIndex += i - 1; + last = bb; + return tmp; + } + + public final int scanNumber() { + tokenStart = currentIndex - 1; + int i = 1; + int ci = currentIndex; + byte bb = last; + while (ci < length) { + bb = buffer[ci++]; + if (bb == ',' || bb == '}' || bb == ']') break; + i++; + } + currentIndex += i - 1; + last = bb; + return tokenStart; + } + + final char[] prepareBuffer(final int start, final int len) throws ParsingException { + if (len > maxNumberDigits) { + throw newParseErrorWith("Too many digits detected in number", len, "", "Too many digits detected in number", len, ""); + } + while (chars.length < len) { + chars = Arrays.copyOf(chars, chars.length * 2); + } + final char[] _tmp = chars; + final byte[] _buf = buffer; + for (int i = 0; i < len; i++) { + _tmp[i] = (char) _buf[start + i]; + } + return _tmp; + } + + final boolean allWhitespace(final int start, final int end) { + final byte[] _buf = buffer; + for (int i = start; i < end; i++) { + if (!WHITESPACE[_buf[i] + 128]) return false; + } + return true; + } + + final int findNonWhitespace(final int end) { + final byte[] _buf = buffer; + for (int i = end - 1; i > 0; i--) { + if (!WHITESPACE[_buf[i] + 128]) return i + 1; + } + return 0; + } + + /** + * Read simple ascii string. Will not use values cache to create instance. + * + * @return parsed string + * @throws ParsingException unable to parse string + */ + public final String readSimpleString() throws ParsingException { + if (last != '"') throw newParseError("Expecting '\"' for string start"); + int i = 0; + int ci = currentIndex; + try { + while (i < tmp.length) { + final byte bb = buffer[ci++]; + if (bb == '"') break; + tmp[i++] = (char) bb; + } + } catch (ArrayIndexOutOfBoundsException ignore) { + throw newParseErrorAt("JSON string was not closed with a double quote", 0); + } + if (ci > length) throw newParseErrorAt("JSON string was not closed with a double quote", 0); + currentIndex = ci; + return new String(tmp, 0, i); + } + + /** + * Read simple "ascii string" into temporary buffer. + * String length must be obtained through getTokenStart and getCurrentToken + * + * @return temporary buffer + * @throws ParsingException unable to parse string + */ + public final char[] readSimpleQuote() throws ParsingException { + if (last != '"') throw newParseError("Expecting '\"' for string start"); + int ci = tokenStart = currentIndex; + try { + for (int i = 0; i < tmp.length; i++) { + final byte bb = buffer[ci++]; + if (bb == '"') break; + tmp[i] = (char) bb; + } + } catch (ArrayIndexOutOfBoundsException ignore) { + throw newParseErrorAt("JSON string was not closed with a double quote", 0); + } + if (ci > length) throw newParseErrorAt("JSON string was not closed with a double quote", 0); + currentIndex = ci; + return tmp; + } + + /** + * Read string from JSON input. + * If values cache is used, string will be looked up from the cache. + *

+ * String value must start and end with a double quote ("). + * + * @return parsed string + * @throws IOException error reading string input + */ + public final String readString() throws IOException { + final int len = parseString(); + return valuesCache == null ? new String(chars, 0, len) : valuesCache.get(chars, len); + } + + public final StringBuilder appendString(StringBuilder builder) throws IOException { + final int len = parseString(); + builder.append(chars, 0, len); + return builder; + } + + public final StringBuffer appendString(StringBuffer buffer) throws IOException { + final int len = parseString(); + buffer.append(chars, 0, len); + return buffer; + } + + final int parseString() throws IOException { + final int startIndex = currentIndex; + if (last != '"') throw newParseError("Expecting '\"' for string start"); + else if (currentIndex == length) throw newParseErrorAt("Premature end of JSON string", 0); + + byte bb; + int ci = currentIndex; + char[] _tmp = chars; + final int remaining = length - currentIndex; + int _tmpLen = _tmp.length < remaining ? _tmp.length : remaining; + int i = 0; + while (i < _tmpLen) { + bb = buffer[ci++]; + if (bb == '"') { + currentIndex = ci; + return i; + } + // If we encounter a backslash, which is a beginning of an escape sequence + // or a high bit was set - indicating an UTF-8 encoded multibyte character, + // there is no chance that we can decode the string without instantiating + // a temporary buffer, so quit this loop + if ((bb ^ '\\') < 1) break; + _tmp[i++] = (char) bb; + } + if (i == _tmp.length) { + final int newSize = chars.length * 2; + if (newSize > maxStringBuffer) { + throw newParseErrorWith("Maximum string buffer limit exceeded", maxStringBuffer); + } + _tmp = chars = Arrays.copyOf(chars, newSize); + } + _tmpLen = _tmp.length; + currentIndex = ci; + int soFar = --currentIndex - startIndex; + + while (!isEndOfStream()) { + int bc = read(); + if (bc == '"') { + return soFar; + } + + if (bc == '\\') { + if (soFar >= _tmpLen - 6) { + final int newSize = chars.length * 2; + if (newSize > maxStringBuffer) { + throw newParseErrorWith("Maximum string buffer limit exceeded", maxStringBuffer); + } + _tmp = chars = Arrays.copyOf(chars, newSize); + _tmpLen = _tmp.length; + } + bc = buffer[currentIndex++]; + + switch (bc) { + case 'b': + bc = '\b'; + break; + case 't': + bc = '\t'; + break; + case 'n': + bc = '\n'; + break; + case 'f': + bc = '\f'; + break; + case 'r': + bc = '\r'; + break; + case '"': + case '/': + case '\\': + break; + case 'u': + bc = (hexToInt(buffer[currentIndex++]) << 12) + + (hexToInt(buffer[currentIndex++]) << 8) + + (hexToInt(buffer[currentIndex++]) << 4) + + hexToInt(buffer[currentIndex++]); + break; + + default: + throw newParseErrorWith("Invalid escape combination detected", bc); + } + } else if ((bc & 0x80) != 0) { + if (soFar >= _tmpLen - 4) { + final int newSize = chars.length * 2; + if (newSize > maxStringBuffer) { + throw newParseErrorWith("Maximum string buffer limit exceeded", maxStringBuffer); + } + _tmp = chars = Arrays.copyOf(chars, newSize); + _tmpLen = _tmp.length; + } + final int u2 = buffer[currentIndex++]; + if ((bc & 0xE0) == 0xC0) { + bc = ((bc & 0x1F) << 6) + (u2 & 0x3F); + } else { + final int u3 = buffer[currentIndex++]; + if ((bc & 0xF0) == 0xE0) { + bc = ((bc & 0x0F) << 12) + ((u2 & 0x3F) << 6) + (u3 & 0x3F); + } else { + final int u4 = buffer[currentIndex++]; + if ((bc & 0xF8) == 0xF0) { + bc = ((bc & 0x07) << 18) + ((u2 & 0x3F) << 12) + ((u3 & 0x3F) << 6) + (u4 & 0x3F); + } else { + // there are legal 5 & 6 byte combinations, but none are _valid_ + throw newParseErrorAt("Invalid unicode character detected", 0); + } + + if (bc >= 0x10000) { + // check if valid unicode + if (bc >= 0x110000) { + throw newParseErrorAt("Invalid unicode character detected", 0); + } + + // split surrogates + final int sup = bc - 0x10000; + _tmp[soFar++] = (char) ((sup >>> 10) + 0xd800); + _tmp[soFar++] = (char) ((sup & 0x3ff) + 0xdc00); + continue; + } + } + } + } else if (soFar >= _tmpLen) { + final int newSize = chars.length * 2; + if (newSize > maxStringBuffer) { + throw newParseErrorWith("Maximum string buffer limit exceeded", maxStringBuffer); + } + _tmp = chars = Arrays.copyOf(chars, newSize); + _tmpLen = _tmp.length; + } + + _tmp[soFar++] = (char) bc; + } + throw newParseErrorAt("JSON string was not closed with a double quote", 0); + } + + private int hexToInt(final byte value) throws ParsingException { + if (value >= '0' && value <= '9') return value - 0x30; + if (value >= 'A' && value <= 'F') return value - 0x37; + if (value >= 'a' && value <= 'f') return value - 0x57; + throw newParseErrorWith("Could not parse unicode escape, expected a hexadecimal digit", value); + } + + private boolean wasWhiteSpace() { + switch (last) { + case 9: + case 10: + case 11: + case 12: + case 13: + case 32: + case -96: + return true; + case -31: + if (currentIndex + 1 < length && buffer[currentIndex] == -102 && buffer[currentIndex + 1] == -128) { + currentIndex += 2; + last = ' '; + return true; + } + return false; + case -30: + if (currentIndex + 1 < length) { + final byte b1 = buffer[currentIndex]; + final byte b2 = buffer[currentIndex + 1]; + if (b1 == -127 && b2 == -97) { + currentIndex += 2; + last = ' '; + return true; + } + if (b1 != -128) return false; + switch (b2) { + case -128: + case -127: + case -126: + case -125: + case -124: + case -123: + case -122: + case -121: + case -120: + case -119: + case -118: + case -88: + case -87: + case -81: + currentIndex += 2; + last = ' '; + return true; + default: + return false; + } + } else { + return false; + } + case -29: + if (currentIndex + 1 < length && buffer[currentIndex] == -128 && buffer[currentIndex + 1] == -128) { + currentIndex += 2; + last = ' '; + return true; + } + return false; + default: + return false; + } + } + + /** + * Read next token (byte) from input JSON. + * Whitespace will be skipped and next non-whitespace byte will be returned. + * + * @return next non-whitespace byte in the JSON input + * @throws IOException unable to get next byte (end of stream, ...) + */ + public final byte getNextToken() throws IOException { + read(); + if (WHITESPACE[last + 128]) { + while (wasWhiteSpace()) { + read(); + } + } + return last; + } + + public final long positionInStream() { + return currentPosition + currentIndex; + } + + public final long positionInStream(final int offset) { + return currentPosition + currentIndex - offset; + } + + public final int fillName() throws IOException { + final int hash = calcHash(); + if (read() != ':') { + if (!wasWhiteSpace() || getNextToken() != ':') { + throw newParseError("Expecting ':' after attribute name"); + } + } + return hash; + } + + public final int fillNameWeakHash() throws IOException { + final int hash = calcWeakHash(); + if (read() != ':') { + if (!wasWhiteSpace() || getNextToken() != ':') { + throw newParseError("Expecting ':' after attribute name"); + } + } + return hash; + } + + public final int calcHash() throws IOException { + if (last != '"') throw newParseError("Expecting '\"' for attribute name start"); + tokenStart = currentIndex; + int ci = currentIndex; + long hash = 0x811c9dc5; + if (stream != null) { + while (ci < readLimit) { + byte b = buffer[ci]; + if (b == '\\') { + if (ci == readLimit - 1) { + return calcHashAndCopyName(hash, ci); + } + b = buffer[++ci]; + } else if (b == '"') { + break; + } + ci++; + hash ^= b; + hash *= 0x1000193; + } + if (ci >= readLimit) { + return calcHashAndCopyName(hash, ci); + } + nameEnd = currentIndex = ci + 1; + } else { + //TODO: use length instead!? this will read data after used buffer size + while (ci < buffer.length) { + byte b = buffer[ci++]; + if (b == '\\') { + if (ci == buffer.length) throw newParseError("Expecting '\"' for attribute name end"); + b = buffer[ci++]; + } else if (b == '"') { + break; + } + hash ^= b; + hash *= 0x1000193; + } + nameEnd = currentIndex = ci; + } + return (int) hash; + } + + public final int calcWeakHash() throws IOException { + if (last != '"') throw newParseError("Expecting '\"' for attribute name start"); + tokenStart = currentIndex; + int ci = currentIndex; + int hash = 0; + if (stream != null) { + while (ci < readLimit) { + byte b = buffer[ci]; + if (b == '\\') { + if (ci == readLimit - 1) { + return calcWeakHashAndCopyName(hash, ci); + } + b = buffer[++ci]; + } else if (b == '"') { + break; + } + ci++; + hash += b; + } + if (ci >= readLimit) { + return calcWeakHashAndCopyName(hash, ci); + } + nameEnd = currentIndex = ci + 1; + } else { + //TODO: use length instead!? this will read data after used buffer size + while (ci < buffer.length) { + byte b = buffer[ci++]; + if (b == '\\') { + if (ci == buffer.length) throw newParseError("Expecting '\"' for attribute name end"); + b = buffer[ci++]; + } else if (b == '"') { + break; + } + hash += b; + } + nameEnd = currentIndex = ci; + } + return hash; + } + + public final int getLastHash() { + long hash = 0x811c9dc5; + if (stream != null && nameEnd == -1) { + int i = 0; + while (i < lastNameLen) { + final byte b = (byte)chars[i++]; + hash ^= b; + hash *= 0x1000193; + } + } else { + int i = tokenStart; + int end = nameEnd - 1; + while (i < end) { + final byte b = buffer[i++]; + hash ^= b; + hash *= 0x1000193; + } + } + return (int)hash; + } + + private int lastNameLen; + + private int calcHashAndCopyName(long hash, int ci) throws IOException { + int soFar = ci - tokenStart; + long startPosition = currentPosition - soFar; + while (chars.length < soFar) { + chars = Arrays.copyOf(chars, chars.length * 2); + } + int i = 0; + for (; i < soFar; i++) { + chars[i] = (char) buffer[i + tokenStart]; + } + currentIndex = ci; + do { + byte b = read(); + if (b == '\\') { + b = read(); + } else if (b == '"') { + nameEnd = -1; + lastNameLen = i; + return (int) hash; + } + if (i == chars.length) { + chars = Arrays.copyOf(chars, chars.length * 2); + } + chars[i++] = (char) b; + hash ^= b; + hash *= 0x1000193; + } while (!isEndOfStream()); + //TODO: check offset + throw newParseErrorAt("JSON string was not closed with a double quote", (int)startPosition); + } + + private int calcWeakHashAndCopyName(int hash, int ci) throws IOException { + int soFar = ci - tokenStart; + long startPosition = currentPosition - soFar; + while (chars.length < soFar) { + chars = Arrays.copyOf(chars, chars.length * 2); + } + int i = 0; + for (; i < soFar; i++) { + chars[i] = (char) buffer[i + tokenStart]; + } + currentIndex = ci; + do { + byte b = read(); + if (b == '\\') { + b = read(); + } else if (b == '"') { + nameEnd = -1; + lastNameLen = i; + return hash; + } + if (i == chars.length) { + chars = Arrays.copyOf(chars, chars.length * 2); + } + chars[i++] = (char) b; + hash += b; + } while (!isEndOfStream()); + //TODO: check offset + throw newParseErrorAt("JSON string was not closed with a double quote", (int)startPosition); + } + + public final boolean wasLastName(final String name) { + if (stream != null && nameEnd == -1) { + if (name.length() != lastNameLen) { + return false; + } + for (int i = 0; i < name.length(); i++) { + if (name.charAt(i) != chars[i]) { + return false; + } + } + return true; + } + if (name.length() != nameEnd - tokenStart - 1) { + return false; + } + //TODO: not correct with escaping + for (int i = 0; i < name.length(); i++) { + if (name.charAt(i) != buffer[tokenStart + i]) { + return false; + } + } + return true; + } + + public final boolean wasLastName(final byte[] name) { + if (stream != null && nameEnd == -1) { + if (name.length != lastNameLen) { + return false; + } + for (int i = 0; i < name.length; i++) { + if (name[i] != chars[i]) { + return false; + } + } + return true; + } + if (name.length != nameEnd - tokenStart - 1) { + return false; + } + for (int i = 0; i < name.length; i++) { + if (name[i] != buffer[tokenStart + i]) { + return false; + } + } + return true; + } + + public final String getLastName() throws IOException { + if (stream != null && nameEnd == -1) { + return new String(chars, 0, lastNameLen); + } + return new String(buffer, tokenStart, nameEnd - tokenStart - 1, "UTF-8"); + } + + private byte skipString() throws IOException { + byte c = read(); + boolean inEscape = false; + while (c != '"' || inEscape) { + inEscape = !inEscape && c == '\\'; + c = read(); + } + return getNextToken(); + } + + /** + * Skip to next non-whitespace token (byte) + * Will not allocate memory while skipping over JSON input. + * + * @return next non-whitespace byte + * @throws IOException unable to read next byte (end of stream, invalid JSON, ...) + */ + public final byte skip() throws IOException { + if (last == '"') return skipString(); + if (last == '{') { + byte nextToken = getNextToken(); + if (nextToken == '}') return getNextToken(); + if (nextToken == '"') { + nextToken = skipString(); + } else { + throw newParseError("Expecting '\"' for attribute name"); + } + if (nextToken != ':') throw newParseError("Expecting ':' after attribute name"); + getNextToken(); + nextToken = skip(); + while (nextToken == ',') { + nextToken = getNextToken(); + if (nextToken == '"') { + nextToken = skipString(); + } else { + throw newParseError("Expecting '\"' for attribute name"); + } + if (nextToken != ':') throw newParseError("Expecting ':' after attribute name"); + getNextToken(); + nextToken = skip(); + } + if (nextToken != '}') throw newParseError("Expecting '}' for object end"); + return getNextToken(); + } + if (last == '[') { + getNextToken(); + byte nextToken = skip(); + while (nextToken == ',') { + getNextToken(); + nextToken = skip(); + } + if (nextToken != ']') throw newParseError("Expecting ']' for array end"); + return getNextToken(); + } + if (last == 'n') { + if (!wasNull()) throw newParseErrorAt("Expecting 'null' for null constant", 0); + return getNextToken(); + } + if (last == 't') { + if (!wasTrue()) throw newParseErrorAt("Expecting 'true' for true constant", 0); + return getNextToken(); + } + if (last == 'f') { + if (!wasFalse()) throw newParseErrorAt("Expecting 'false' for false constant", 0); + return getNextToken(); + } + while (last != ',' && last != '}' && last != ']') { + read(); + } + return last; + } + + /** + * will be removed + * + * @return not used anymore + * @throws IOException throws if invalid JSON detected + */ + @Deprecated + public String readNext() throws IOException { + final int start = currentIndex - 1; + skip(); + return new String(buffer, start, currentIndex - start - 1, "UTF-8"); + } + + public final byte[] readBase64() throws IOException { + if (stream != null && Base64.findEnd(buffer, currentIndex) == buffer.length) { + final int len = parseString(); + final byte[] input = new byte[len]; + for (int i = 0; i < input.length; i++) { + input[i] = (byte) chars[i]; + } + return Base64.decodeFast(input, 0, len); + } + if (last != '"') throw newParseError("Expecting '\"' for base64 start"); + final int start = currentIndex; + currentIndex = Base64.findEnd(buffer, start); + last = buffer[currentIndex++]; + if (last != '"') throw newParseError("Expecting '\"' for base64 end"); + return Base64.decodeFast(buffer, start, currentIndex - 1); + } + + /** + * Read key value of JSON input. + * If key cache is used, it will be looked up from there. + * + * @return parsed key value + * @throws IOException unable to parse string input + */ + public final String readKey() throws IOException { + final int len = parseString(); + final String key = keyCache != null ? keyCache.get(chars, len) : new String(chars, 0, len); + if (getNextToken() != ':') throw newParseError("Expecting ':' after attribute name"); + getNextToken(); + return key; + } + + /** + * Custom objects can be deserialized based on the implementation specified through this interface. + * Annotation processor creates custom deserializers at compile time and registers them into DslJson. + * + * @param type + */ + public interface ReadObject { + @Nullable + T read(JsonReader reader) throws IOException; + } + + /** + * Existing instances can be provided as target for deserialization. + * Annotation processor creates custom deserializers at compile time and registers them into DslJson. + * + * @param type + */ + public interface BindObject { + T bind(JsonReader reader, T instance) throws IOException; + } + + public interface ReadJsonObject { + @Nullable + T deserialize(JsonReader reader) throws IOException; + } + + /** + * Checks if 'null' value is at current position. + * This means last read byte was 'n' and 'ull' are next three bytes. + * If last byte was n but next three are not 'ull' it will throw since that is not a valid JSON construct. + * + * @return true if 'null' value is at current position + * @throws ParsingException invalid 'null' value detected + */ + public final boolean wasNull() throws ParsingException { + if (last == 'n') { + if (currentIndex + 2 < length && buffer[currentIndex] == 'u' + && buffer[currentIndex + 1] == 'l' && buffer[currentIndex + 2] == 'l') { + currentIndex += 3; + last = 'l'; + return true; + } + throw newParseErrorAt("Invalid null constant found", 0); + } + return false; + } + + /** + * Checks if 'true' value is at current position. + * This means last read byte was 't' and 'rue' are next three bytes. + * If last byte was t but next three are not 'rue' it will throw since that is not a valid JSON construct. + * + * @return true if 'true' value is at current position + * @throws ParsingException invalid 'true' value detected + */ + public final boolean wasTrue() throws ParsingException { + if (last == 't') { + if (currentIndex + 2 < length && buffer[currentIndex] == 'r' + && buffer[currentIndex + 1] == 'u' && buffer[currentIndex + 2] == 'e') { + currentIndex += 3; + last = 'e'; + return true; + } + throw newParseErrorAt("Invalid true constant found", 0); + } + return false; + } + + /** + * Checks if 'false' value is at current position. + * This means last read byte was 'f' and 'alse' are next four bytes. + * If last byte was f but next four are not 'alse' it will throw since that is not a valid JSON construct. + * + * @return true if 'false' value is at current position + * @throws ParsingException invalid 'false' value detected + */ + public final boolean wasFalse() throws ParsingException { + if (last == 'f') { + if (currentIndex + 3 < length && buffer[currentIndex] == 'a' + && buffer[currentIndex + 1] == 'l' && buffer[currentIndex + 2] == 's' + && buffer[currentIndex + 3] == 'e') { + currentIndex += 4; + last = 'e'; + return true; + } + throw newParseErrorAt("Invalid false constant found", 0); + } + return false; + } + + /** + * Will advance to next token and check if it's comma + * + * @throws IOException it's not comma + */ + public final void comma() throws IOException { + if (getNextToken() != ',') { + if (currentIndex >= length) throw newParseErrorAt("Unexpected end in JSON", 0, eof); + throw newParseError("Expecting ','"); + } + } + + /** + * Will advance to next token and check if it's semicolon + * + * @throws IOException it's not semicolon + */ + public final void semicolon() throws IOException { + if (getNextToken() != ':') { + if (currentIndex >= length) throw newParseErrorAt("Unexpected end in JSON", 0, eof); + throw newParseError("Expecting ':'"); + } + } + + /** + * Will advance to next token and check if it's array start + * + * @throws IOException it's not array start + */ + public final void startArray() throws IOException { + if (getNextToken() != '[') { + if (currentIndex >= length) throw newParseErrorAt("Unexpected end in JSON", 0, eof); + throw newParseError("Expecting '[' as array start"); + } + } + + /** + * Will advance to next token and check if it's array end + * + * @throws IOException it's not array end + */ + public final void endArray() throws IOException { + if (getNextToken() != ']') { + if (currentIndex >= length) throw newParseErrorAt("Unexpected end in JSON", 0, eof); + throw newParseError("Expecting ']' as array end"); + } + } + + /** + * Will advance to next token and check if it's object start + * + * @throws IOException it's not object start + */ + public final void startObject() throws IOException { + if (getNextToken() != '{') { + if (currentIndex >= length) throw newParseErrorAt("Unexpected end in JSON", 0, eof); + throw newParseError("Expecting '{' as object start"); + } + } + + /** + * Will advance to next token and check it it's object end + * + * @throws IOException it's not object end + */ + public final void endObject() throws IOException { + if (getNextToken() != '}') { + if (currentIndex >= length) throw newParseErrorAt("Unexpected end in JSON", 0, eof); + throw newParseError("Expecting '}' as object end"); + } + } + + public final void startAttribute(final String name) throws IOException { + do { + if (getNextToken() != '"') throw newParseError("Expecting '\"' as attribute start"); + fillNameWeakHash(); + if (wasLastName(name)) return; + getNextToken(); + } while (skip() == ','); + throw newParseErrorWith("Unable to find attribute", name); + } + + /** + * Check if the last read token is an array end + * + * @throws IOException it's not array end + */ + public final void checkArrayEnd() throws IOException { + if (last != ']') { + if (currentIndex >= length) throw newParseErrorAt("Unexpected end of JSON in collection", 0, eof); + throw newParseError("Expecting ']' as array end"); + } + } + + /** + * Check if the last read token is an object end + * + * @throws IOException it's not object end + */ + public final void checkObjectEnd() throws IOException { + if (last != '}') { + if (currentIndex >= length) throw newParseErrorAt("Unexpected end of JSON in object", 0, eof); + throw newParseError("Expecting '}' as object end"); + } + } + + @Nullable + private Object readNull(final Class manifest) throws IOException { + if (!wasNull()) throw newParseErrorAt("Expecting 'null' as null constant", 0); + if (manifest.isPrimitive()) { + if (manifest == int.class) return 0; + else if (manifest == long.class) return 0L; + else if (manifest == short.class) return (short)0; + else if (manifest == byte.class) return (byte)0; + else if (manifest == float.class) return 0f; + else if (manifest == double.class) return 0d; + else if (manifest == boolean.class) return false; + else if (manifest == char.class) return '\0'; + } + return null; + } + + /** + * Will advance to next token and read the JSON into specified type + * + * @param manifest type to read into + * @param type + * @return new instance from input JSON + * @throws IOException unable to process JSON + */ + @SuppressWarnings("unchecked") + @Nullable + public final T next(final Class manifest) throws IOException { + if (manifest == null) throw new IllegalArgumentException("manifest can't be null"); + if (typeLookup == null) throw new ConfigurationException("typeLookup is not defined for this JsonReader. Unable to lookup specified type " + manifest); + if (this.getNextToken() == 'n') { + return (T)readNull(manifest); + } + final ReadObject reader = typeLookup.tryFindReader(manifest); + if (reader == null) { + throw new ConfigurationException("Reader not found for " + manifest + ". Check if reader was registered"); + } + return reader.read(this); + } + + /** + * Will advance to next token and read the JSON into specified type + * + * @param reader reader to use + * @param type + * @return new instance from input JSON + * @throws IOException unable to process JSON + */ + @Nullable + public final T next(final ReadObject reader) throws IOException { + if (reader == null) throw new IllegalArgumentException("reader can't be null"); + if (this.getNextToken() == 'n') { + if (!wasNull()) throw newParseErrorAt("Expecting 'null' as null constant", 0); + return null; + } + return reader.read(this); + } + + /** + * Will advance to next token and bind the JSON to provided instance + * + * @param manifest type to read into + * @param instance instance to bind + * @param type + * @return bound instance + * @throws IOException unable to process JSON + */ + @SuppressWarnings("unchecked") + @Nullable + public final T next(final Class manifest, final T instance) throws IOException { + if (manifest == null) throw new IllegalArgumentException("manifest can't be null"); + if (instance == null) throw new IllegalArgumentException("instance can't be null"); + if (typeLookup == null) throw new ConfigurationException("typeLookup is not defined for this JsonReader. Unable to lookup specified type " + manifest); + if (this.getNextToken() == 'n') { + return (T)readNull(manifest); + } + final BindObject binder = typeLookup.tryFindBinder(manifest); + if (binder == null) throw new ConfigurationException("Binder not found for " + manifest + ". Check if binder was registered"); + return binder.bind(this, instance); + } + + /** + * Will advance to next token and bind the JSON to provided instance + * + * @param binder binder to use + * @param instance instance to bind + * @param type + * @return bound instance + * @throws IOException unable to process JSON + */ + @SuppressWarnings("unchecked") + @Nullable + public final T next(final BindObject binder, final T instance) throws IOException { + if (binder == null) throw new IllegalArgumentException("binder can't be null"); + if (instance == null) throw new IllegalArgumentException("instance can't be null"); + if (this.getNextToken() == 'n') { + if (!wasNull()) throw newParseErrorAt("Expecting 'null' as null constant", 0); + return null; + } + return binder.bind(this, instance); + } + + @Nullable + public final ArrayList readCollection(final ReadObject readObject) throws IOException { + if (wasNull()) return null; + if (last != '[') throw newParseError("Expecting '[' as collection start"); + if (getNextToken() == ']') return new ArrayList(0); + final ArrayList res = new ArrayList(4); + res.add(readObject.read(this)); + while (getNextToken() == ',') { + getNextToken(); + res.add(readObject.read(this)); + } + checkArrayEnd(); + return res; + } + + @Nullable + public final LinkedHashSet readSet(final ReadObject readObject) throws IOException { + if (wasNull()) return null; + if (last != '[') throw newParseError("Expecting '[' as set start"); + if (getNextToken() == ']') return new LinkedHashSet(0); + final LinkedHashSet res = new LinkedHashSet(4); + res.add(readObject.read(this)); + while (getNextToken() == ',') { + getNextToken(); + res.add(readObject.read(this)); + } + checkArrayEnd(); + return res; + } + + @Nullable + public final LinkedHashMap readMap(final ReadObject readKey, final ReadObject readValue) throws IOException { + if (wasNull()) return null; + if (last != '{') throw newParseError("Expecting '{' as map start"); + if (getNextToken() == '}') return new LinkedHashMap(0); + final LinkedHashMap res = new LinkedHashMap(4); + K key = readKey.read(this); + if (key == null) throw newParseErrorAt("Null detected as key", 0); + if (getNextToken() != ':') throw newParseError("Expecting ':' after key attribute"); + getNextToken(); + V value = readValue.read(this); + res.put(key, value); + while (getNextToken() == ',') { + getNextToken(); + key = readKey.read(this); + if (key == null) throw newParseErrorAt("Null detected as key", 0); + if (getNextToken() != ':') throw newParseError("Expecting ':' after key attribute"); + getNextToken(); + value = readValue.read(this); + res.put(key, value); + } + checkObjectEnd(); + return res; + } + + @Nullable + public final T[] readArray(final ReadObject readObject, final T[] emptyArray) throws IOException { + if (wasNull()) return null; + if (last != '[') throw newParseError("Expecting '[' as array start"); + if (getNextToken() == ']') return emptyArray; + final ArrayList res = new ArrayList(4); + res.add(readObject.read(this)); + while (getNextToken() == ',') { + getNextToken(); + res.add(readObject.read(this)); + } + checkArrayEnd(); + return res.toArray(emptyArray); + } + + public final ArrayList deserializeCollection(final ReadObject readObject) throws IOException { + final ArrayList res = new ArrayList(4); + deserializeCollection(readObject, res); + return res; + } + + public final void deserializeCollection(final ReadObject readObject, final Collection res) throws IOException { + res.add(readObject.read(this)); + while (getNextToken() == ',') { + getNextToken(); + res.add(readObject.read(this)); + } + checkArrayEnd(); + } + + public final ArrayList deserializeNullableCollection(final ReadObject readObject) throws IOException { + final ArrayList res = new ArrayList(4); + deserializeNullableCollection(readObject, res); + return res; + } + + public final void deserializeNullableCollection(final ReadObject readObject, final Collection res) throws IOException { + if (wasNull()) { + res.add(null); + } else { + res.add(readObject.read(this)); + } + while (getNextToken() == ',') { + getNextToken(); + if (wasNull()) { + res.add(null); + } else { + res.add(readObject.read(this)); + } + } + checkArrayEnd(); + } + + public final ArrayList deserializeCollection(final ReadJsonObject readObject) throws IOException { + final ArrayList res = new ArrayList(4); + deserializeCollection(readObject, res); + return res; + } + + public final void deserializeCollection(final ReadJsonObject readObject, final Collection res) throws IOException { + if (last == '{') { + getNextToken(); + res.add(readObject.deserialize(this)); + } else throw newParseError("Expecting '{' as collection start"); + while (getNextToken() == ',') { + if (getNextToken() == '{') { + getNextToken(); + res.add(readObject.deserialize(this)); + } else throw newParseError("Expecting '{' as object start within a collection"); + } + checkArrayEnd(); + } + + public final ArrayList deserializeNullableCollection(final ReadJsonObject readObject) throws IOException { + final ArrayList res = new ArrayList(4); + deserializeNullableCollection(readObject, res); + return res; + } + + public final void deserializeNullableCollection(final ReadJsonObject readObject, final Collection res) throws IOException { + if (last == '{') { + getNextToken(); + res.add(readObject.deserialize(this)); + } else if (wasNull()) { + res.add(null); + } else throw newParseError("Expecting '{' as collection start"); + while (getNextToken() == ',') { + if (getNextToken() == '{') { + getNextToken(); + res.add(readObject.deserialize(this)); + } else if (wasNull()) { + res.add(null); + } else throw newParseError("Expecting '{' as object start within a collection"); + } + checkArrayEnd(); + } + + public final Iterator iterateOver(final JsonReader.ReadObject reader) { + return new WithReader(reader, this); + } + + public final Iterator iterateOver(final JsonReader.ReadJsonObject reader) { + return new WithObjectReader(reader, this); + } + + private static class WithReader implements Iterator { + private final JsonReader.ReadObject reader; + private final JsonReader json; + + private boolean hasNext; + + WithReader(JsonReader.ReadObject reader, JsonReader json) { + this.reader = reader; + this.json = json; + hasNext = true; + } + + @Override + public boolean hasNext() { + return hasNext; + } + + @Override + public void remove() { + } + + @Nullable + @Override + public T next() { + try { + byte nextToken = json.last(); + final T instance; + if (nextToken == 'n') { + if (!json.wasNull()) throw json.newParseErrorAt("Expecting 'null' as null constant", 0); + instance = null; + } else { + instance = reader.read(json); + } + hasNext = json.getNextToken() == ','; + if (hasNext) { + json.getNextToken(); + } else { + if (json.last() != ']') throw json.newParseError("Expecting ']' for iteration end"); + //TODO: ideally we should release stream bound to reader + } + return instance; + } catch (IOException e) { + throw new SerializationException(e); + } + } + } + + private static class WithObjectReader implements Iterator { + private final JsonReader.ReadJsonObject reader; + private final JsonReader json; + + private boolean hasNext; + + WithObjectReader(JsonReader.ReadJsonObject reader, JsonReader json) { + this.reader = reader; + this.json = json; + hasNext = true; + } + + @Override + public boolean hasNext() { + return hasNext; + } + + @Override + public void remove() { + } + + @Nullable + @Override + public T next() { + try { + byte nextToken = json.last(); + final T instance; + if (nextToken == 'n') { + if (!json.wasNull()) throw json.newParseErrorAt("Expecting 'null' as null constant", 0); + instance = null; + } else if (nextToken == '{') { + json.getNextToken(); + instance = reader.deserialize(json); + } else { + throw json.newParseError("Expecting '{' for object start in iteration"); + } + hasNext = json.getNextToken() == ','; + if (hasNext) { + json.getNextToken(); + } else { + if (json.last() != ']') throw json.newParseError("Expecting ']' for iteration end"); + //TODO: ideally we should release stream bound to reader + } + return instance; + } catch (IOException e) { + throw new SerializationException(e); + } + } + } +} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/JsonWriter.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/JsonWriter.java new file mode 100644 index 0000000000..b423c85701 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/JsonWriter.java @@ -0,0 +1,909 @@ +package com.bugsnag.android.repackaged.dslplatform.json; + +import androidx.annotation.Nullable; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.*; + +/** + * DslJson writes JSON into JsonWriter which has two primary modes of operation: + * + * * targeting specific output stream + * * buffering the entire response in memory + * + * In both cases JsonWriter writes into an byte[] buffer. + * If stream is used as target, it will copy buffer into the stream whenever there is no more room in buffer for new data. + * If stream is not used as target, it will grow the buffer to hold the encoded result. + * To use stream as target reset(OutputStream) must be called before processing. + * This class provides low level methods for JSON serialization. + *

+ * After the processing is done, + * in case then stream was used as target, flush() must be called to copy the remaining of the buffer into stream. + * When entire response was buffered in memory, buffer can be copied to stream or resulting byte[] can be used directly. + *

+ * For maximum performance JsonWriter instances should be reused (to avoid allocation of new byte[] buffer instances). + * They should not be shared across threads (concurrently) so for Thread reuse it's best to use patterns such as ThreadLocal. + */ +@SuppressWarnings({"rawtypes", "unchecked"}) // suppress pre-existing warnings +public final class JsonWriter { + + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + final byte[] ensureCapacity(final int free) { + if (position + free >= buffer.length) { + enlargeOrFlush(position, free); + } + return buffer; + } + + void advance(int size) { + position += size; + } + + private int position; + private long flushed; + private OutputStream target; + private byte[] buffer; + + private final UnknownSerializer unknownSerializer; + private final Grisu3.FastDtoaBuilder doubleBuilder = new Grisu3.FastDtoaBuilder(); + + /** + * Prefer creating JsonWriter through DslJson#newWriter + * This instance is safe to use when all type information is known and lookups to custom writers is not required. + */ + @Deprecated + public JsonWriter() { + this(512, null); + } + + JsonWriter(@Nullable final UnknownSerializer unknownSerializer) { + this(512, unknownSerializer); + } + + JsonWriter(final int size, @Nullable final UnknownSerializer unknownSerializer) { + this(new byte[size], unknownSerializer); + } + + JsonWriter(final byte[] buffer, @Nullable final UnknownSerializer unknownSerializer) { + this.buffer = buffer; + this.unknownSerializer = unknownSerializer; + } + + /** + * Helper for writing JSON object start: { + */ + public static final byte OBJECT_START = '{'; + /** + * Helper for writing JSON object end: } + */ + public static final byte OBJECT_END = '}'; + /** + * Helper for writing JSON array start: [ + */ + public static final byte ARRAY_START = '['; + /** + * Helper for writing JSON array end: ] + */ + public static final byte ARRAY_END = ']'; + /** + * Helper for writing comma separator: , + */ + public static final byte COMMA = ','; + /** + * Helper for writing semicolon: : + */ + public static final byte SEMI = ':'; + /** + * Helper for writing JSON quote: " + */ + public static final byte QUOTE = '"'; + /** + * Helper for writing JSON escape: \\ + */ + public static final byte ESCAPE = '\\'; + + private void enlargeOrFlush(final int size, final int padding) { + if (target != null) { + try { + target.write(buffer, 0, size); + } catch (IOException ex) { + throw new SerializationException("Unable to write to target stream.", ex); + } + position = 0; + flushed += size; + if (padding > buffer.length) { + buffer = Arrays.copyOf(buffer, buffer.length + buffer.length / 2 + padding); + } + } else { + buffer = Arrays.copyOf(buffer, buffer.length + buffer.length / 2 + padding); + } + } + + /** + * Optimized method for writing 'null' into the JSON. + */ + public final void writeNull() { + if ((position + 4) >= buffer.length) { + enlargeOrFlush(position, 0); + } + final int s = position; + final byte[] _result = buffer; + _result[s] = 'n'; + _result[s + 1] = 'u'; + _result[s + 2] = 'l'; + _result[s + 3] = 'l'; + position += 4; + } + + /** + * Write a single byte into the JSON. + * + * @param value byte to write into the JSON + */ + public final void writeByte(final byte value) { + if (position == buffer.length) { + enlargeOrFlush(position, 0); + } + buffer[position++] = value; + } + + /** + * Write a quoted string into the JSON. + * String will be appropriately escaped according to JSON escaping rules. + * + * @param value string to write + */ + public final void writeString(final String value) { + final int len = value.length(); + if (position + (len << 2) + (len << 1) + 2 >= buffer.length) { + enlargeOrFlush(position, (len << 2) + (len << 1) + 2); + } + final byte[] _result = buffer; + _result[position] = QUOTE; + int cur = position + 1; + for (int i = 0; i < len; i++) { + final char c = value.charAt(i); + if (c > 31 && c != '"' && c != '\\' && c < 126) { + _result[cur++] = (byte) c; + } else { + writeQuotedString(value, i, cur, len); + return; + } + } + _result[cur] = QUOTE; + position = cur + 1; + } + + /** + * Write a quoted string into the JSON. + * Char sequence will be appropriately escaped according to JSON escaping rules. + * + * @param value char sequence to write + */ + public final void writeString(final CharSequence value) { + final int len = value.length(); + if (position + (len << 2) + (len << 1) + 2 >= buffer.length) { + enlargeOrFlush(position, (len << 2) + (len << 1) + 2); + } + final byte[] _result = buffer; + _result[position] = QUOTE; + int cur = position + 1; + for (int i = 0; i < len; i++) { + final char c = value.charAt(i); + if (c > 31 && c != '"' && c != '\\' && c < 126) { + _result[cur++] = (byte) c; + } else { + writeQuotedString(value, i, cur, len); + return; + } + } + _result[cur] = QUOTE; + position = cur + 1; + } + + private void writeQuotedString(final CharSequence str, int i, int cur, final int len) { + final byte[] _result = this.buffer; + for (; i < len; i++) { + final char c = str.charAt(i); + if (c == '"') { + _result[cur++] = ESCAPE; + _result[cur++] = QUOTE; + } else if (c == '\\') { + _result[cur++] = ESCAPE; + _result[cur++] = ESCAPE; + } else if (c < 32) { + if (c == 8) { + _result[cur++] = ESCAPE; + _result[cur++] = 'b'; + } else if (c == 9) { + _result[cur++] = ESCAPE; + _result[cur++] = 't'; + } else if (c == 10) { + _result[cur++] = ESCAPE; + _result[cur++] = 'n'; + } else if (c == 12) { + _result[cur++] = ESCAPE; + _result[cur++] = 'f'; + } else if (c == 13) { + _result[cur++] = ESCAPE; + _result[cur++] = 'r'; + } else { + _result[cur] = ESCAPE; + _result[cur + 1] = 'u'; + _result[cur + 2] = '0'; + _result[cur + 3] = '0'; + switch (c) { + case 0: + _result[cur + 4] = '0'; + _result[cur + 5] = '0'; + break; + case 1: + _result[cur + 4] = '0'; + _result[cur + 5] = '1'; + break; + case 2: + _result[cur + 4] = '0'; + _result[cur + 5] = '2'; + break; + case 3: + _result[cur + 4] = '0'; + _result[cur + 5] = '3'; + break; + case 4: + _result[cur + 4] = '0'; + _result[cur + 5] = '4'; + break; + case 5: + _result[cur + 4] = '0'; + _result[cur + 5] = '5'; + break; + case 6: + _result[cur + 4] = '0'; + _result[cur + 5] = '6'; + break; + case 7: + _result[cur + 4] = '0'; + _result[cur + 5] = '7'; + break; + case 11: + _result[cur + 4] = '0'; + _result[cur + 5] = 'B'; + break; + case 14: + _result[cur + 4] = '0'; + _result[cur + 5] = 'E'; + break; + case 15: + _result[cur + 4] = '0'; + _result[cur + 5] = 'F'; + break; + case 16: + _result[cur + 4] = '1'; + _result[cur + 5] = '0'; + break; + case 17: + _result[cur + 4] = '1'; + _result[cur + 5] = '1'; + break; + case 18: + _result[cur + 4] = '1'; + _result[cur + 5] = '2'; + break; + case 19: + _result[cur + 4] = '1'; + _result[cur + 5] = '3'; + break; + case 20: + _result[cur + 4] = '1'; + _result[cur + 5] = '4'; + break; + case 21: + _result[cur + 4] = '1'; + _result[cur + 5] = '5'; + break; + case 22: + _result[cur + 4] = '1'; + _result[cur + 5] = '6'; + break; + case 23: + _result[cur + 4] = '1'; + _result[cur + 5] = '7'; + break; + case 24: + _result[cur + 4] = '1'; + _result[cur + 5] = '8'; + break; + case 25: + _result[cur + 4] = '1'; + _result[cur + 5] = '9'; + break; + case 26: + _result[cur + 4] = '1'; + _result[cur + 5] = 'A'; + break; + case 27: + _result[cur + 4] = '1'; + _result[cur + 5] = 'B'; + break; + case 28: + _result[cur + 4] = '1'; + _result[cur + 5] = 'C'; + break; + case 29: + _result[cur + 4] = '1'; + _result[cur + 5] = 'D'; + break; + case 30: + _result[cur + 4] = '1'; + _result[cur + 5] = 'E'; + break; + default: + _result[cur + 4] = '1'; + _result[cur + 5] = 'F'; + break; + } + cur += 6; + } + } else if (c < 0x007F) { + _result[cur++] = (byte) c; + } else { + final int cp = Character.codePointAt(str, i); + if (Character.isSupplementaryCodePoint(cp)) { + i++; + } + if (cp == 0x007F) { + _result[cur++] = (byte) cp; + } else if (cp <= 0x7FF) { + _result[cur++] = (byte) (0xC0 | ((cp >> 6) & 0x1F)); + _result[cur++] = (byte) (0x80 | (cp & 0x3F)); + } else if ((cp < 0xD800) || (cp > 0xDFFF && cp <= 0xFFFF)) { + _result[cur++] = (byte) (0xE0 | ((cp >> 12) & 0x0F)); + _result[cur++] = (byte) (0x80 | ((cp >> 6) & 0x3F)); + _result[cur++] = (byte) (0x80 | (cp & 0x3F)); + } else if (cp >= 0x10000 && cp <= 0x10FFFF) { + _result[cur++] = (byte) (0xF0 | ((cp >> 18) & 0x07)); + _result[cur++] = (byte) (0x80 | ((cp >> 12) & 0x3F)); + _result[cur++] = (byte) (0x80 | ((cp >> 6) & 0x3F)); + _result[cur++] = (byte) (0x80 | (cp & 0x3F)); + } else { + throw new SerializationException("Unknown unicode codepoint in string! " + Integer.toHexString(cp)); + } + } + } + _result[cur] = QUOTE; + position = cur + 1; + } + + /** + * Write string consisting of only ascii characters. + * String will not be escaped according to JSON escaping rules. + * + * @param value ascii string + */ + @SuppressWarnings("deprecation") + public final void writeAscii(final String value) { + final int len = value.length(); + if (position + len >= buffer.length) { + enlargeOrFlush(position, len); + } + value.getBytes(0, len, buffer, position); + position += len; + } + + /** + * Write part of string consisting of only ascii characters. + * String will not be escaped according to JSON escaping rules. + * + * @param value ascii string + * @param len part of the provided string to use + */ + @SuppressWarnings("deprecation") + public final void writeAscii(final String value, final int len) { + if (position + len >= buffer.length) { + enlargeOrFlush(position, len); + } + value.getBytes(0, len, buffer, position); + position += len; + } + + /** + * Copy bytes into JSON as is. + * Provided buffer can't be null. + * + * @param buf byte buffer to copy + */ + public final void writeAscii(final byte[] buf) { + final int len = buf.length; + if (position + len >= buffer.length) { + enlargeOrFlush(position, len); + } + final int p = position; + final byte[] _result = buffer; + for (int i = 0; i < buf.length; i++) { + _result[p + i] = buf[i]; + } + position += len; + } + + /** + * Copy part of byte buffer into JSON as is. + * Provided buffer can't be null. + * + * @param buf byte buffer to copy + * @param len part of buffer to copy + */ + public final void writeAscii(final byte[] buf, final int len) { + if (position + len >= buffer.length) { + enlargeOrFlush(position, len); + } + final int p = position; + final byte[] _result = buffer; + for (int i = 0; i < len; i++) { + _result[p + i] = buf[i]; + } + position += len; + } + + /** + * Copy part of byte buffer into JSON as is. + * Provided buffer can't be null. + * + * @param buf byte buffer to copy + * @param offset in buffer to start from + * @param len part of buffer to copy + */ + public final void writeRaw(final byte[] buf, final int offset, final int len) { + if (position + len >= buffer.length) { + enlargeOrFlush(position, len); + } + System.arraycopy(buf, offset, buffer, position, len); + position += len; + } + + /** + * Encode bytes as Base 64. + * Provided value can't be null. + * + * @param value bytes to encode + */ + public final void writeBinary(final byte[] value) { + if (position + (value.length << 1) + 2 >= buffer.length) { + enlargeOrFlush(position, (value.length << 1) + 2); + } + buffer[position++] = '"'; + position += Base64.encodeToBytes(value, buffer, position); + buffer[position++] = '"'; + } + + final void writeDouble(final double value) { + if (value == Double.POSITIVE_INFINITY) { + writeAscii("\"Infinity\""); + } else if (value == Double.NEGATIVE_INFINITY) { + writeAscii("\"-Infinity\""); + } else if (value != value) { + writeAscii("\"NaN\""); + } else if (value == 0.0) { + writeAscii("0.0"); + } else { + if (Grisu3.tryConvert(value, doubleBuilder)) { + if (position + 24 >= buffer.length) { + enlargeOrFlush(position, 24); + } + final int len = doubleBuilder.copyTo(buffer, position); + position += len; + } else { + writeAscii(Double.toString(value)); + } + } + } + + @Override + public String toString() { + return new String(buffer, 0, position, UTF_8); + } + + /** + * Content of buffer can be copied to another array of appropriate size. + * This method can't be used when targeting output stream. + * Ideally it should be avoided if possible, since it will create an array copy. + * It's better to use getByteBuffer and size instead. + * + * @return copy of the buffer up to the current position + */ + public final byte[] toByteArray() { + if (target != null) { + throw new ConfigurationException("Method is not available when targeting stream"); + } + return Arrays.copyOf(buffer, position); + } + + /** + * When JsonWriter does not target stream, this method should be used to copy content of the buffer into target stream. + * It will also reset the buffer position to 0 so writer can be continued to be used even without a call to reset(). + * + * @param stream target stream + * @throws IOException propagates from stream.write + */ + public final void toStream(final OutputStream stream) throws IOException { + if (target != null) { + throw new ConfigurationException("Method should not be used when targeting streams. Instead use flush() to copy what's remaining in the buffer"); + } + stream.write(buffer, 0, position); + flushed += position; + position = 0; + } + + /** + * Current buffer. + * If buffer grows, a new instance will be created and old one will not be used anymore. + * + * @return current buffer + */ + public final byte[] getByteBuffer() { + return buffer; + } + + /** + * Current position in the buffer. When stream is not used, this is also equivalent + * to the size of the resulting JSON in bytes + * + * @return position in the populated buffer + */ + public final int size() { + return position; + } + + /** + * Total bytes currently flushed to stream + * + * @return bytes flushed + */ + public final long flushed() { + return flushed; + } + + /** + * Resets the writer - same as calling reset(OutputStream = null) + */ + public final void reset() { + reset(null); + } + + /** + * Resets the writer - specifies the target stream and sets the position in buffer to 0. + * If stream is set to null, JsonWriter will work in growing byte[] buffer mode (entire response will be buffered in memory). + * + * @param stream sets/clears the target stream + */ + public final void reset(@Nullable OutputStream stream) { + position = 0; + target = stream; + flushed = 0; + } + + /** + * If stream was used, copies the buffer to stream and resets the position in buffer to 0. + * It will not reset the stream as target, + * meaning new usages of the JsonWriter will try to use the already provided stream. + * It will not do anything if stream was not used + *

+ * To reset the stream to null use reset() or reset(OutputStream) methods. + */ + public final void flush() { + if (target != null && position != 0) { + try { + target.write(buffer, 0, position); + } catch (IOException ex) { + throw new SerializationException("Unable to write to target stream.", ex); + } + flushed += position; + position = 0; + } + } + + /** + * This is deprecated method which exists only for backward compatibility + * + * @throws java.io.IOException unable to write to target stream + */ + @Deprecated + public void close() throws IOException { + if (target != null && position != 0) { + target.write(buffer, 0, position); + position = 0; + flushed = 0; + } + } + + /** + * Custom objects can be serialized based on the implementation specified through this interface. + * Annotation processor creates custom deserializers at compile time and registers them into DslJson. + * + * @param type + */ + public interface WriteObject { + void write(JsonWriter writer, @Nullable T value); + } + + /** + * Convenience method for serializing array of JsonObject's. + * Array can't be null nor can't contain null values (it will result in NullPointerException). + * + * @param array input objects + * @param type of objects + */ + public void serialize(final T[] array) { + writeByte(ARRAY_START); + if (array.length != 0) { + array[0].serialize(this, false); + for (int i = 1; i < array.length; i++) { + writeByte(COMMA); + array[i].serialize(this, false); + } + } + writeByte(ARRAY_END); + } + + /** + * Convenience method for serializing only part of JsonObject's array. + * Useful when array is reused and only part of it needs to be serialized. + * Array can't be null nor can't contain null values (it will result in NullPointerException). + * + * @param array input objects + * @param len size of array which should be serialized + * @param type of objects + */ + public void serialize(final T[] array, final int len) { + writeByte(ARRAY_START); + if (array.length != 0 && len != 0) { + array[0].serialize(this, false); + for (int i = 1; i < len; i++) { + writeByte(COMMA); + array[i].serialize(this, false); + } + } + writeByte(ARRAY_END); + } + + /** + * Convenience method for serializing list of JsonObject's. + * List can't be null nor can't contain null values (it will result in NullPointerException). + * It will use list .get(index) method to access the object. + * When using .get(index) is not appropriate, + * it's better to call the serialize(Collection<JsonObject>) method instead. + * + * @param list input objects + * @param type of objects + */ + public void serialize(final List list) { + writeByte(ARRAY_START); + if (list.size() != 0) { + list.get(0).serialize(this, false); + for (int i = 1; i < list.size(); i++) { + writeByte(COMMA); + list.get(i).serialize(this, false); + } + } + writeByte(ARRAY_END); + } + + /** + * Convenience method for serializing array through instance serializer (WriteObject). + * Array can be null and can contain null values. + * Instance serializer will not be invoked for null values + * + * @param array array to serialize + * @param encoder instance serializer + * @param type of object + */ + public void serialize(@Nullable final T[] array, final WriteObject encoder) { + if (array == null) { + writeNull(); + return; + } + writeByte(ARRAY_START); + if (array.length != 0) { + T item = array[0]; + if (item != null) { + encoder.write(this, item); + } else { + writeNull(); + } + for (int i = 1; i < array.length; i++) { + writeByte(COMMA); + item = array[i]; + if (item != null) { + encoder.write(this, item); + } else { + writeNull(); + } + } + } + writeByte(ARRAY_END); + } + + /** + * Convenience method for serializing list through instance serializer (WriteObject). + * List can be null and can contain null values. + * Instance serializer will not be invoked for null values + * It will use list .get(index) method to access the object. + * When using .get(index) is not appropriate, + * it's better to call the serialize(Collection<JsonObject>, WriteObject) method instead. + * + * @param list list to serialize + * @param encoder instance serializer + * @param type of object + */ + public void serialize(@Nullable final List list, final WriteObject encoder) { + if (list == null) { + writeNull(); + return; + } + writeByte(ARRAY_START); + if (!list.isEmpty()) { + if (list instanceof RandomAccess) { + T item = list.get(0); + if (item != null) { + encoder.write(this, item); + } else { + writeNull(); + } + for (int i = 1; i < list.size(); i++) { + writeByte(COMMA); + item = list.get(i); + if (item != null) { + encoder.write(this, item); + } else { + writeNull(); + } + } + } else { + Iterator iter = list.iterator(); + T item = iter.next(); + if (item != null) { + encoder.write(this, item); + } else { + writeNull(); + } + while (iter.hasNext()) { + writeByte(COMMA); + item = iter.next(); + if (item != null) { + encoder.write(this, item); + } else { + writeNull(); + } + } + } + } + writeByte(ARRAY_END); + } + + public void serializeRaw(@Nullable final List list, final WriteObject encoder) { + serialize(list, encoder); + } + + /** + * Convenience method for serializing collection through instance serializer (WriteObject). + * Collection can be null and can contain null values. + * Instance serializer will not be invoked for null values + * + * @param collection collection to serialize + * @param encoder instance serializer + * @param type of object + */ + public void serialize(@Nullable final Collection collection, final WriteObject encoder) { + if (collection == null) { + writeNull(); + return; + } + writeByte(ARRAY_START); + if (!collection.isEmpty()) { + final Iterator it = collection.iterator(); + T item = it.next(); + if (item != null) { + encoder.write(this, item); + } else { + writeNull(); + } + while (it.hasNext()) { + writeByte(COMMA); + item = it.next(); + if (item != null) { + encoder.write(this, item); + } else { + writeNull(); + } + } + } + writeByte(ARRAY_END); + } + + public void serializeRaw(@Nullable final Collection collection, final WriteObject encoder) { + serialize(collection, encoder); + } + + public void serialize(@Nullable final Map map, final WriteObject keyEncoder, final WriteObject valueEncoder) { + if (map == null) { + writeNull(); + return; + } + writeByte(OBJECT_START); + final int size = map.size(); + if (size > 0) { + final Iterator> iterator = map.entrySet().iterator(); + Map.Entry kv = iterator.next(); + writeQuoted(keyEncoder, kv.getKey()); + writeByte(SEMI); + valueEncoder.write(this, kv.getValue()); + for (int i = 1; i < size; i++) { + writeByte(COMMA); + kv = iterator.next(); + writeQuoted(keyEncoder, kv.getKey()); + writeByte(SEMI); + valueEncoder.write(this, kv.getValue()); + } + } + writeByte(OBJECT_END); + } + + public void serializeRaw(@Nullable final Map map, final WriteObject keyEncoder, final WriteObject valueEncoder) { + serialize(map, keyEncoder, valueEncoder); + } + + public void writeQuoted(final JsonWriter.WriteObject keyWriter, final T key) { + if (key instanceof Double) { + final double value = (Double) key; + if (Double.isNaN(value)) writeAscii("\"NaN\""); + else if (value == Double.POSITIVE_INFINITY) writeAscii("\"Infinity\""); + else if (value == Double.NEGATIVE_INFINITY) writeAscii("\"-Infinity\""); + else { + writeByte(QUOTE); + NumberConverter.serialize(value, this); + writeByte(QUOTE); + } + } else if (key instanceof Float) { + final float value = (Float) key; + if (Float.isNaN(value)) writeAscii("\"NaN\""); + else if (value == Float.POSITIVE_INFINITY) writeAscii("\"Infinity\""); + else if (value == Float.NEGATIVE_INFINITY) writeAscii("\"-Infinity\""); + else { + writeByte(QUOTE); + NumberConverter.serialize(value, this); + writeByte(QUOTE); + } + } else if (key instanceof Number) { + writeByte(QUOTE); + keyWriter.write(this, key); + writeByte(QUOTE); + } else { + keyWriter.write(this, key); + } + } + + /** + * Generic object serializer which is used for "unknown schema" objects. + * It will throw SerializationException in case if it doesn't know how to serialize provided instance. + * Will delegate the serialization to UnknownSerializer, which in most cases is the DslJson instance from which the writer was created. + * This enables it to use DslJson configuration and serialize using custom serializers (when they are provided). + * + * @param value instance to serialize + */ + public void serializeObject(@Nullable final Object value) { + if (value == null) { + writeNull(); + } else if (unknownSerializer != null) { + try { + unknownSerializer.serialize(this, value); + } catch (IOException ex) { //serializing unknown stuff can fail in various ways ;( + throw new SerializationException(ex); + } + } else { + throw new ConfigurationException("Unable to serialize: " + value.getClass() + ".\n" + + "Check that JsonWriter was created through DslJson#newWriter."); + } + } +} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/MapConverter.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/MapConverter.java new file mode 100644 index 0000000000..5de4221a0c --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/MapConverter.java @@ -0,0 +1,88 @@ +package com.bugsnag.android.repackaged.dslplatform.json; + +import androidx.annotation.Nullable; + +import java.io.IOException; +import java.util.*; + +@SuppressWarnings({"rawtypes", "unchecked"}) // suppress pre-existing warnings +public abstract class MapConverter { + + private static final JsonReader.ReadObject> TypedMapReader = new JsonReader.ReadObject>() { + @Nullable + @Override + public Map read(JsonReader reader) throws IOException { + return reader.wasNull() ? null : deserialize(reader); + } + }; + + public static void serializeNullable(@Nullable final Map value, final JsonWriter sw) { + if (value == null) { + sw.writeNull(); + } else { + serialize(value, sw); + } + } + + public static void serialize(final Map value, final JsonWriter sw) { + sw.writeByte(JsonWriter.OBJECT_START); + final int size = value.size(); + if (size > 0) { + final Iterator> iterator = value.entrySet().iterator(); + Map.Entry kv = iterator.next(); + StringConverter.serializeShort(kv.getKey(), sw); + sw.writeByte(JsonWriter.SEMI); + StringConverter.serializeNullable(kv.getValue(), sw); + for (int i = 1; i < size; i++) { + sw.writeByte(JsonWriter.COMMA); + kv = iterator.next(); + StringConverter.serializeShort(kv.getKey(), sw); + sw.writeByte(JsonWriter.SEMI); + StringConverter.serializeNullable(kv.getValue(), sw); + } + } + sw.writeByte(JsonWriter.OBJECT_END); + } + + public static Map deserialize(final JsonReader reader) throws IOException { + if (reader.last() != '{') throw reader.newParseError("Expecting '{' for map start"); + byte nextToken = reader.getNextToken(); + if (nextToken == '}') return new LinkedHashMap(0); + final LinkedHashMap res = new LinkedHashMap(); + String key = StringConverter.deserialize(reader); + nextToken = reader.getNextToken(); + if (nextToken != ':') throw reader.newParseError("Expecting ':' after attribute name"); + reader.getNextToken(); + String value = StringConverter.deserializeNullable(reader); + res.put(key, value); + while ((nextToken = reader.getNextToken()) == ',') { + reader.getNextToken(); + key = StringConverter.deserialize(reader); + nextToken = reader.getNextToken(); + if (nextToken != ':') throw reader.newParseError("Expecting ':' after attribute name"); + reader.getNextToken(); + value = StringConverter.deserializeNullable(reader); + res.put(key, value); + } + if (nextToken != '}') throw reader.newParseError("Expecting '}' for map end"); + return res; + } + + @SuppressWarnings("unchecked") + public static ArrayList> deserializeCollection(final JsonReader reader) throws IOException { + return reader.deserializeCollection(TypedMapReader); + } + + public static void deserializeCollection(final JsonReader reader, final Collection> res) throws IOException { + reader.deserializeCollection(TypedMapReader, res); + } + + @SuppressWarnings("unchecked") + public static ArrayList> deserializeNullableCollection(final JsonReader reader) throws IOException { + return reader.deserializeNullableCollection(TypedMapReader); + } + + public static void deserializeNullableCollection(final JsonReader reader, final Collection> res) throws IOException { + reader.deserializeNullableCollection(TypedMapReader, res); + } +} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/NetConverter.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/NetConverter.java new file mode 100644 index 0000000000..f496f9ca0b --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/NetConverter.java @@ -0,0 +1,110 @@ +package com.bugsnag.android.repackaged.dslplatform.json; + +import androidx.annotation.Nullable; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; + +@SuppressWarnings({"rawtypes", "unchecked"}) // suppress pre-existing warnings +public abstract class NetConverter { + + static final JsonReader.ReadObject UriReader = new JsonReader.ReadObject() { + @Nullable + @Override + public URI read(JsonReader reader) throws IOException { + return reader.wasNull() ? null : deserializeUri(reader); + } + }; + static final JsonWriter.WriteObject UriWriter = new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable URI value) { + serializeNullable(value, writer); + } + }; + static final JsonReader.ReadObject AddressReader = new JsonReader.ReadObject() { + @Nullable + @Override + public InetAddress read(JsonReader reader) throws IOException { + return reader.wasNull() ? null : deserializeIp(reader); + } + }; + static final JsonWriter.WriteObject AddressWriter = new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable InetAddress value) { + serializeNullable(value, writer); + } + }; + + public static void serializeNullable(@Nullable final URI value, final JsonWriter sw) { + if (value == null) { + sw.writeNull(); + } else { + serialize(value, sw); + } + } + + public static void serialize(final URI value, final JsonWriter sw) { + StringConverter.serializeShort(value.toString(), sw); + } + + public static URI deserializeUri(final JsonReader reader) throws IOException { + return URI.create(reader.readString()); + } + + @SuppressWarnings("unchecked") + public static ArrayList deserializeUriCollection(final JsonReader reader) throws IOException { + return reader.deserializeCollection(UriReader); + } + + public static void deserializeUriCollection(final JsonReader reader, final Collection res) throws IOException { + reader.deserializeCollection(UriReader, res); + } + + @SuppressWarnings("unchecked") + public static ArrayList deserializeUriNullableCollection(final JsonReader reader) throws IOException { + return reader.deserializeNullableCollection(UriReader); + } + + public static void deserializeUriNullableCollection(final JsonReader reader, final Collection res) throws IOException { + reader.deserializeNullableCollection(UriReader, res); + } + + public static void serializeNullable(@Nullable final InetAddress value, final JsonWriter sw) { + if (value == null) { + sw.writeNull(); + } else { + serialize(value, sw); + } + } + + public static void serialize(final InetAddress value, final JsonWriter sw) { + sw.writeByte(JsonWriter.QUOTE); + sw.writeAscii(value.getHostAddress()); + sw.writeByte(JsonWriter.QUOTE); + } + + public static InetAddress deserializeIp(final JsonReader reader) throws IOException { + return InetAddress.getByName(reader.readSimpleString()); + } + + @SuppressWarnings("unchecked") + public static ArrayList deserializeIpCollection(final JsonReader reader) throws IOException { + return reader.deserializeCollection(AddressReader); + } + + public static void deserializeIpCollection(final JsonReader reader, final Collection res) throws IOException { + reader.deserializeCollection(AddressReader, res); + } + + @SuppressWarnings("unchecked") + public static ArrayList deserializeIpNullableCollection(final JsonReader reader) throws IOException { + return reader.deserializeNullableCollection(AddressReader); + } + + public static void deserializeIpNullableCollection(final JsonReader reader, final Collection res) throws IOException { + reader.deserializeNullableCollection(AddressReader, res); + } +} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/NumberConverter.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/NumberConverter.java new file mode 100644 index 0000000000..3369c44775 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/NumberConverter.java @@ -0,0 +1,1700 @@ +package com.bugsnag.android.repackaged.dslplatform.json; + +import androidx.annotation.Nullable; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +@SuppressWarnings({"rawtypes", "unchecked"}) // suppress pre-existing warnings +public abstract class NumberConverter { + + public final static short[] SHORT_EMPTY_ARRAY = new short[0]; + public final static int[] INT_EMPTY_ARRAY = new int[0]; + public final static long[] LONG_EMPTY_ARRAY = new long[0]; + public final static float[] FLOAT_EMPTY_ARRAY = new float[0]; + public final static double[] DOUBLE_EMPTY_ARRAY = new double[0]; + public final static Short SHORT_ZERO = 0; + public final static Integer INT_ZERO = 0; + public final static Long LONG_ZERO = 0L; + public final static Float FLOAT_ZERO = 0f; + public final static Double DOUBLE_ZERO = 0.0; + + private final static int[] DIGITS = new int[1000]; + private final static int[] DIFF = {111, 222, 444, 888, 1776}; + private final static int[] ERROR = {50, 100, 200, 400, 800}; + private final static int[] SCALE_10 = {10000, 1000, 100, 10, 1}; + private final static double[] POW_10 = { + 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, + 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, + 1e20, 1e21, 1e22, 1e23, 1e24, 1e25, 1e26, 1e27, 1e28, 1e29, + 1e30, 1e31, 1e32, 1e33, 1e34, 1e35, 1e36, 1e37, 1e38, 1e39, + 1e40, 1e41, 1e42, 1e43, 1e44, 1e45, 1e46, 1e47, 1e48, 1e49, + 1e50, 1e51, 1e52, 1e53, 1e54, 1e55, 1e56, 1e57, 1e58, 1e59, + 1e60, 1e61, 1e62, 1e63, 1e64, 1e65 + }; + public static final JsonReader.ReadObject DOUBLE_READER = new JsonReader.ReadObject() { + @Nullable + @Override + public Double read(JsonReader reader) throws IOException { + return deserializeDouble(reader); + } + }; + public static final JsonReader.ReadObject NULLABLE_DOUBLE_READER = new JsonReader.ReadObject() { + @Nullable + @Override + public Double read(JsonReader reader) throws IOException { + return reader.wasNull() ? null : deserializeDouble(reader); + } + }; + public static final JsonWriter.WriteObject DOUBLE_WRITER = new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable Double value) { + serializeNullable(value, writer); + } + }; + public static final JsonReader.ReadObject DOUBLE_ARRAY_READER = new JsonReader.ReadObject() { + @Nullable + @Override + public double[] read(JsonReader reader) throws IOException { + if (reader.wasNull()) return null; + if (reader.last() != '[') throw reader.newParseError("Expecting '[' for double array start"); + reader.getNextToken(); + return deserializeDoubleArray(reader); + } + }; + public static final JsonWriter.WriteObject DOUBLE_ARRAY_WRITER = new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable double[] value) { + serialize(value, writer); + } + }; + + public static final JsonReader.ReadObject FLOAT_READER = new JsonReader.ReadObject() { + @Override + public Float read(JsonReader reader) throws IOException { + return deserializeFloat(reader); + } + }; + public static final JsonReader.ReadObject NULLABLE_FLOAT_READER = new JsonReader.ReadObject() { + @Nullable + @Override + public Float read(JsonReader reader) throws IOException { + return reader.wasNull() ? null : deserializeFloat(reader); + } + }; + public static final JsonWriter.WriteObject FLOAT_WRITER = new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable Float value) { + serializeNullable(value, writer); + } + }; + public static final JsonReader.ReadObject FLOAT_ARRAY_READER = new JsonReader.ReadObject() { + @Nullable + @Override + public float[] read(JsonReader reader) throws IOException { + if (reader.wasNull()) return null; + if (reader.last() != '[') throw reader.newParseError("Expecting '[' for float array start"); + reader.getNextToken(); + return deserializeFloatArray(reader); + } + }; + public static final JsonWriter.WriteObject FLOAT_ARRAY_WRITER = new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable float[] value) { + serialize(value, writer); + } + }; + public static final JsonReader.ReadObject INT_READER = new JsonReader.ReadObject() { + @Override + public Integer read(JsonReader reader) throws IOException { + return deserializeInt(reader); + } + }; + public static final JsonReader.ReadObject NULLABLE_INT_READER = new JsonReader.ReadObject() { + @Nullable + @Override + public Integer read(JsonReader reader) throws IOException { + return reader.wasNull() ? null : deserializeInt(reader); + } + }; + public static final JsonWriter.WriteObject INT_WRITER = new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable Integer value) { + serializeNullable(value, writer); + } + }; + public static final JsonReader.ReadObject INT_ARRAY_READER = new JsonReader.ReadObject() { + @Nullable + @Override + public int[] read(JsonReader reader) throws IOException { + if (reader.wasNull()) return null; + if (reader.last() != '[') throw reader.newParseError("Expecting '[' for int array start"); + reader.getNextToken(); + return deserializeIntArray(reader); + } + }; + public static final JsonWriter.WriteObject INT_ARRAY_WRITER = new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable int[] value) { + serialize(value, writer); + } + }; + public static final JsonReader.ReadObject SHORT_READER = new JsonReader.ReadObject() { + @Override + public Short read(JsonReader reader) throws IOException { + return deserializeShort(reader); + } + }; + public static final JsonReader.ReadObject NULLABLE_SHORT_READER = new JsonReader.ReadObject() { + @Nullable + @Override + public Short read(JsonReader reader) throws IOException { + return reader.wasNull() ? null : deserializeShort(reader); + } + }; + public static final JsonWriter.WriteObject SHORT_WRITER = new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable Short value) { + if (value == null) writer.writeNull(); + else serialize(value.intValue(), writer); + } + }; + public static final JsonReader.ReadObject SHORT_ARRAY_READER = new JsonReader.ReadObject() { + @Nullable + @Override + public short[] read(JsonReader reader) throws IOException { + if (reader.wasNull()) return null; + if (reader.last() != '[') throw reader.newParseError("Expecting '[' for short array start"); + reader.getNextToken(); + return deserializeShortArray(reader); + } + }; + public static final JsonWriter.WriteObject SHORT_ARRAY_WRITER = new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable short[] value) { + serialize(value, writer); + } + }; + + public static final JsonReader.ReadObject LONG_READER = new JsonReader.ReadObject() { + @Override + public Long read(JsonReader reader) throws IOException { + return deserializeLong(reader); + } + }; + public static final JsonReader.ReadObject NULLABLE_LONG_READER = new JsonReader.ReadObject() { + @Nullable + @Override + public Long read(JsonReader reader) throws IOException { + return reader.wasNull() ? null : deserializeLong(reader); + } + }; + public static final JsonWriter.WriteObject LONG_WRITER = new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable Long value) { + serializeNullable(value, writer); + } + }; + public static final JsonReader.ReadObject LONG_ARRAY_READER = new JsonReader.ReadObject() { + @Nullable + @Override + public long[] read(JsonReader reader) throws IOException { + if (reader.wasNull()) return null; + if (reader.last() != '[') throw reader.newParseError("Expecting '[' for long array start"); + reader.getNextToken(); + return deserializeLongArray(reader); + } + }; + public static final JsonWriter.WriteObject LONG_ARRAY_WRITER = new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable long[] value) { + serialize(value, writer); + } + }; + + public static final JsonReader.ReadObject DecimalReader = new JsonReader.ReadObject() { + @Nullable + @Override + public BigDecimal read(JsonReader reader) throws IOException { + return reader.wasNull() ? null : deserializeDecimal(reader); + } + }; + public static final JsonWriter.WriteObject DecimalWriter = new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable BigDecimal value) { + serializeNullable(value, writer); + } + }; + static final JsonReader.ReadObject NumberReader = new JsonReader.ReadObject() { + @Nullable + @Override + public Number read(JsonReader reader) throws IOException { + return reader.wasNull() ? null : deserializeNumber(reader); + } + }; + + static { + for (int i = 0; i < DIGITS.length; i++) { + DIGITS[i] = (i < 10 ? (2 << 24) : i < 100 ? (1 << 24) : 0) + + (((i / 100) + '0') << 16) + + ((((i / 10) % 10) + '0') << 8) + + i % 10 + '0'; + } + } + + static void write4(final int value, final byte[] buf, final int pos) { + if (value > 9999) { + throw new IllegalArgumentException("Only 4 digits numbers are supported. Provided: " + value); + } + final int q = value / 1000; + final int v = DIGITS[value - q * 1000]; + buf[pos] = (byte) (q + '0'); + buf[pos + 1] = (byte) (v >> 16); + buf[pos + 2] = (byte) (v >> 8); + buf[pos + 3] = (byte) v; + } + + static void write3(final int number, final byte[] buf, int pos) { + final int v = DIGITS[number]; + buf[pos] = (byte) (v >> 16); + buf[pos + 1] = (byte) (v >> 8); + buf[pos + 2] = (byte) v; + } + + static void write2(final int value, final byte[] buf, final int pos) { + final int v = DIGITS[value]; + buf[pos] = (byte) (v >> 8); + buf[pos + 1] = (byte) v; + } + + static int read2(final char[] buf, final int pos) { + final int v1 = buf[pos] - 48; + return (v1 << 3) + (v1 << 1) + buf[pos + 1] - 48; + } + + static int read4(final char[] buf, final int pos) { + final int v2 = buf[pos + 1] - 48; + final int v3 = buf[pos + 2] - 48; + return (buf[pos] - 48) * 1000 + + (v2 << 6) + (v2 << 5) + (v2 << 2) + + (v3 << 3) + (v3 << 1) + + buf[pos + 3] - 48; + } + + static void numberException(final JsonReader reader, final int start, final int end, String message) throws ParsingException { + final int len = end - start; + if (len > reader.maxNumberDigits) { + throw reader.newParseErrorWith("Too many digits detected in number", len, "", "Too many digits detected in number", end, ""); + } + throw reader.newParseErrorWith("Error parsing number", len, "", message, null, ". Error parsing number"); + } + + static void numberException(final JsonReader reader, final int start, final int end, String message, Object messageArgument) throws ParsingException { + final int len = end - start; + if (len > reader.maxNumberDigits) { + throw reader.newParseErrorWith("Too many digits detected in number", len, "", "Too many digits detected in number", end, ""); + } + throw reader.newParseErrorWith("Error parsing number", len, "", message, messageArgument, ". Error parsing number"); + } + + public static void serializeNullable(@Nullable final Double value, final JsonWriter sw) { + if (value == null) { + sw.writeNull(); + } else { + serialize(value, sw); + } + } + + private static BigDecimal parseNumberGeneric(final char[] buf, final int len, final JsonReader reader, final boolean withQuotes) throws ParsingException { + int end = len; + while (end > 0 && Character.isWhitespace(buf[end - 1])) { + end--; + } + if (end > reader.maxNumberDigits) { + throw reader.newParseErrorWith("Too many digits detected in number", len, "", "Too many digits detected in number", end, ""); + } + final int offset = buf[0] == '-' ? 1 : 0; + if (buf[offset] == '0' && end > offset + 1 && buf[offset + 1] >= '0' && buf[offset + 1] <= '9') { + throw reader.newParseErrorAt("Leading zero is not allowed. Error parsing number", len + (withQuotes ? 2 : 0)); + } + try { + return new BigDecimal(buf, 0, end); + } catch (NumberFormatException nfe) { + throw reader.newParseErrorAt("Error parsing number", len + (withQuotes ? 2 : 0), nfe); + } + } + + public static void serialize(final double value, final JsonWriter sw) { + sw.writeDouble(value); + } + + public static void serialize(@Nullable final double[] value, final JsonWriter sw) { + if (value == null) { + sw.writeNull(); + } else if (value.length == 0) { + sw.writeAscii("[]"); + } else { + sw.writeByte(JsonWriter.ARRAY_START); + serialize(value[0], sw); + for (int i = 1; i < value.length; i++) { + sw.writeByte(JsonWriter.COMMA); + serialize(value[i], sw); + } + sw.writeByte(JsonWriter.ARRAY_END); + } + } + + private static class NumberInfo { + final char[] buffer; + final int length; + + NumberInfo(final char[] buffer, final int length) { + this.buffer = buffer; + this.length = length; + } + } + + private static NumberInfo readLongNumber(final JsonReader reader, final int start) throws IOException { + int len = reader.length() - start; + char[] result = reader.prepareBuffer(start, len); + while (reader.length() == reader.getCurrentIndex()) { + if (reader.isEndOfStream()) break; + reader.scanNumber(); // peek, do not read + int end = reader.getCurrentIndex(); + int oldLen = len; + len += end; + if (len > reader.maxNumberDigits) { + throw reader.newParseErrorFormat("Too many digits detected in number", len, "Number of digits larger than %d. Unable to read number", reader.maxNumberDigits); + } + char[] tmp = result; + result = new char[len]; + System.arraycopy(tmp, 0, result, 0, oldLen); + System.arraycopy(reader.prepareBuffer(0, end), 0, result, oldLen, end); + } + return new NumberInfo(result, len); + } + + public static double deserializeDouble(final JsonReader reader) throws IOException { + if (reader.last() == '"') { + final int position = reader.getCurrentIndex(); + final char[] buf = reader.readSimpleQuote(); + return parseDoubleGeneric(buf, reader.getCurrentIndex() - position - 1, reader, true); + } + final int start = reader.scanNumber(); + final int end = reader.getCurrentIndex(); + final byte[] buf = reader.buffer; + final byte ch = buf[start]; + if (ch == '-') { + return -parseDouble(buf, reader, start, end, 1); + } + return parseDouble(buf, reader, start, end, 0); + } + + private static double parseDouble(final byte[] buf, final JsonReader reader, final int start, final int end, final int offset) throws IOException { + if (end - start - offset > reader.doubleLengthLimit) { + if (end == reader.length()) { + final NumberInfo tmp = readLongNumber(reader, start + offset); + return parseDoubleGeneric(tmp.buffer, tmp.length, reader, false); + } + return parseDoubleGeneric(reader.prepareBuffer(start + offset, end - start - offset), end - start - offset, reader, false); + } + long value = 0; + byte ch = ' '; + int i = start + offset; + final boolean leadingZero = buf[start + offset] == 48; + for (; i < end; i++) { + ch = buf[i]; + if (ch == '.' || ch == 'e' || ch == 'E') break; + final int ind = buf[i] - 48; + if (ind < 0 || ind > 9) { + if (leadingZero && i > start + offset + 1) { + numberException(reader, start, end, "Leading zero is not allowed"); + } + if (i > start + offset && reader.allWhitespace(i, end)) return value; + numberException(reader, start, end, "Unknown digit", (char)ch); + } + value = (value << 3) + (value << 1) + ind; + } + if (i == start + offset) numberException(reader, start, end, "Digit not found"); + else if (leadingZero && ch != '.' && i > start + offset + 1) numberException(reader, start, end, "Leading zero is not allowed"); + else if (i == end) return value; + else if (ch == '.') { + i++; + if (i == end) numberException(reader, start, end, "Number ends with a dot"); + final int maxLen; + final double preciseDividor; + final int expDiff; + final int decPos = i; + final int decOffset; + if (value == 0) { + maxLen = i + 15; + ch = buf[i]; + if (ch == '0' && end > maxLen) { + return parseDoubleGeneric(reader.prepareBuffer(start + offset, end - start - offset), end - start - offset, reader, false); + } else if (ch < '8') { + preciseDividor = 1e14; + expDiff = -1; + decOffset = 1; + } else { + preciseDividor = 1e15; + expDiff = 0; + decOffset = 0; + } + } else { + maxLen = start + offset + 16; + if (buf[start + offset] < '8') { + preciseDividor = 1e14; + expDiff = i - maxLen + 14; + decOffset = 1; + } else { + preciseDividor = 1e15; + expDiff = i - maxLen + 15; + decOffset = 0; + } + } + final int numLimit = maxLen < end ? maxLen : end; + //TODO zeros + for (; i < numLimit; i++) { + ch = buf[i]; + if (ch == 'e' || ch == 'E') break; + final int ind = ch - 48; + if (ind < 0 || ind > 9) { + if (reader.allWhitespace(i, end)) return value / POW_10[i - decPos - 1]; + numberException(reader, start, end, "Unknown digit", (char)buf[i]); + } + value = (value << 3) + (value << 1) + ind; + } + if (i == end) return value / POW_10[i - decPos - 1]; + else if (ch == 'e' || ch == 'E') { + return doubleExponent(reader, value, i - decPos,0, buf, start, end, offset, i); + } + if (reader.doublePrecision == JsonReader.DoublePrecision.HIGH) { + return parseDoubleGeneric(reader.prepareBuffer(start + offset, end - start - offset), end - start - offset, reader, false); + } + int decimals = 0; + final int decLimit = start + offset + 18 < end ? start + offset + 18 : end; + final int remPos = i; + for(;i < decLimit; i++) { + ch = buf[i]; + if (ch == 'e' || ch == 'E') break; + final int ind = ch - 48; + if (ind < 0 || ind > 9) { + if (reader.allWhitespace(i, end)) { + return approximateDouble(decimals, value / preciseDividor, i - remPos - decOffset); + } + numberException(reader, start, end, "Unknown digit", (char)buf[i]); + } + decimals = (decimals << 3) + (decimals << 1) + ind; + } + final double number = approximateDouble(decimals, value / preciseDividor, i - remPos - decOffset); + while (i < end && ch >= '0' && ch <= '9') { + ch = buf[i++]; + } + if (ch == 'e' || ch == 'E') { + return doubleExponent(reader, 0, expDiff, number, buf, start, end, offset, i); + } else if (expDiff > 0) { + return number * POW_10[expDiff - 1]; + } else if (expDiff < 0) { + return number / POW_10[-expDiff - 1]; + } else { + return number; + } + } else if (ch == 'e' || ch == 'E') { + return doubleExponent(reader, value, 0, 0, buf, start, end, offset, i); + } + return value; + } + + private static double approximateDouble(final int decimals, final double precise, final int digits) { + final long bits = Double.doubleToRawLongBits(precise); + final int exp = (int)(bits >> 52) - 1022; + final int missing = (decimals * SCALE_10[digits + 1] + ERROR[exp]) / DIFF[exp]; + return Double.longBitsToDouble(bits + missing); + } + + private static double doubleExponent(JsonReader reader, final long whole, final int decimals, double fraction, byte[] buf, int start, int end, int offset, int i) throws IOException { + if (reader.doublePrecision == JsonReader.DoublePrecision.EXACT) { + return parseDoubleGeneric(reader.prepareBuffer(start + offset, end - start - offset), end - start - offset, reader, false); + } + byte ch; + ch = buf[++i]; + final int exp; + if (ch == '-') { + exp = parseNegativeInt(buf, reader, i, end) - decimals; + } else if (ch == '+') { + exp = parsePositiveInt(buf, reader, i, end, 1) - decimals; + } else { + exp = parsePositiveInt(buf, reader, i, end, 0) - decimals; + } + if (fraction == 0) { + if (exp == 0 || whole == 0) return whole; + else if (exp > 0 && exp < POW_10.length) return whole * POW_10[exp - 1]; + else if (exp < 0 && -exp < POW_10.length) return whole / POW_10[-exp - 1]; + else if (reader.doublePrecision != JsonReader.DoublePrecision.HIGH) { + if (exp > 0 && exp < 300) return whole * Math.pow(10, exp); + else if (exp > -300 && exp < 0) return whole / Math.pow(10, exp); + } + } else { + if (exp == 0) return whole + fraction; + else if (exp > 0 && exp < POW_10.length) return fraction * POW_10[exp - 1] + whole * POW_10[exp - 1]; + else if (exp < 0 && -exp < POW_10.length) return fraction / POW_10[-exp - 1] + whole / POW_10[-exp - 1]; + else if (reader.doublePrecision != JsonReader.DoublePrecision.HIGH) { + if (exp > 0 && exp < 300) return whole * Math.pow(10, exp); + else if (exp > -300 && exp < 0) return whole / Math.pow(10, exp); + } + } + return parseDoubleGeneric(reader.prepareBuffer(start + offset, end - start - offset), end - start - offset, reader, false); + } + + private static double parseDoubleGeneric(final char[] buf, final int len, final JsonReader reader, final boolean withQuotes) throws IOException { + int end = len; + while (end > 0 && Character.isWhitespace(buf[end - 1])) { + end--; + } + if (end > reader.maxNumberDigits) { + throw reader.newParseErrorWith("Too many digits detected in number", len, "", "Too many digits detected in number", end, ""); + } + final int offset = buf[0] == '-' ? 1 : 0; + if (buf[offset] == '0' && end > offset + 1 && buf[offset + 1] >= '0' && buf[offset + 1] <= '9') { + throw reader.newParseErrorAt("Leading zero is not allowed. Error parsing number", len + (withQuotes ? 2 : 0)); + } + try { + return Double.parseDouble(new String(buf, 0, end)); + } catch (NumberFormatException nfe) { + throw reader.newParseErrorAt("Error parsing number", len + (withQuotes ? 2 : 0), nfe); + } + } + + @SuppressWarnings("unchecked") + public static ArrayList deserializeDoubleCollection(final JsonReader reader) throws IOException { + return reader.deserializeCollection(DOUBLE_READER); + } + + public static void deserializeDoubleCollection(final JsonReader reader, final Collection res) throws IOException { + reader.deserializeCollection(DOUBLE_READER, res); + } + + @SuppressWarnings("unchecked") + public static ArrayList deserializeDoubleNullableCollection(final JsonReader reader) throws IOException { + return reader.deserializeNullableCollection(DOUBLE_READER); + } + + public static void deserializeDoubleNullableCollection(final JsonReader reader, final Collection res) throws IOException { + reader.deserializeNullableCollection(DOUBLE_READER, res); + } + + public static void serializeNullable(@Nullable final Float value, final JsonWriter sw) { + if (value == null) { + sw.writeNull(); + } else { + serialize(value, sw); + } + } + + public static void serialize(final float value, final JsonWriter sw) { + if (value == Float.POSITIVE_INFINITY) { + sw.writeAscii("\"Infinity\""); + } else if (value == Float.NEGATIVE_INFINITY) { + sw.writeAscii("\"-Infinity\""); + } else if (value != value) { + sw.writeAscii("\"NaN\""); + } else { + sw.writeAscii(Float.toString(value));//TODO: better implementation required + } + } + + public static void serialize(@Nullable final float[] value, final JsonWriter sw) { + if (value == null) { + sw.writeNull(); + } else if (value.length == 0) { + sw.writeAscii("[]"); + } else { + sw.writeByte(JsonWriter.ARRAY_START); + serialize(value[0], sw); + for (int i = 1; i < value.length; i++) { + sw.writeByte(JsonWriter.COMMA); + serialize(value[i], sw); + } + sw.writeByte(JsonWriter.ARRAY_END); + } + } + + public static float deserializeFloat(final JsonReader reader) throws IOException { + if (reader.last() == '"') { + final int position = reader.getCurrentIndex(); + final char[] buf = reader.readSimpleQuote(); + return parseFloatGeneric(buf, reader.getCurrentIndex() - position - 1, reader, true); + } + final int start = reader.scanNumber(); + final int end = reader.getCurrentIndex(); + if (end == reader.length()) { + final NumberInfo tmp = readLongNumber(reader, start); + return parseFloatGeneric(tmp.buffer, tmp.length, reader, false); + } + final byte[] buf = reader.buffer; + final byte ch = buf[start]; + if (ch == '-') { + return -parseFloat(buf, reader, start, end, 1); + } + return parseFloat(buf, reader, start, end, 0); + } + + private static float parseFloat(byte[] buf, final JsonReader reader, final int start, int end, int offset) throws IOException { + long value = 0; + byte ch = ' '; + int i = start + offset; + final int digitStart = i; + final boolean leadingZero = buf[start + offset] == 48; + for (; i < end; i++) { + ch = buf[i]; + if (ch == '.' || ch == 'e' || ch == 'E') break; + final int ind = ch - 48; + if (ind < 0 || ind > 9) { + if (leadingZero && i > start + offset + 1) { + numberException(reader, start, end, "Leading zero is not allowed"); + } + if (i > start + offset && reader.allWhitespace(i, end)) return value; + numberException(reader, start, end, "Unknown digit", (char)ch); + } + value = (value << 3) + (value << 1) + ind; + } + if (i == digitStart) numberException(reader, start, end, "Digit not found"); + else if (leadingZero && ch != '.' && i > start + offset + 1) { + numberException(reader, start, end, "Leading zero is not allowed"); + } else if (i > 18 + digitStart) { + return parseFloatGeneric(reader.prepareBuffer(start + offset, end - start - offset), end - start - offset, reader, false); + } else if (i == end) { + return value; + } else if (ch == '.') { + i++; + if (i == end) numberException(reader, start, end, "Number ends with a dot"); + final int decPos; + final int maxLen; + final int pointOffset; + if (value == 0) { + pointOffset = 0; + decPos = i + 1; + while (i < end && buf[i] == '0') { + i++; + } + maxLen = i + 17; + } else { + pointOffset = 1; + maxLen = digitStart + 17; + decPos = i; + } + final int numLimit = maxLen < end ? maxLen : end; + boolean foundE = false; + for (; i < numLimit; i++) { + ch = buf[i]; + if (ch == 'e' || ch == 'E') { + foundE = true; + ++i; + break; + } + final int ind = ch - 48; + if (ind < 0 || ind > 9) { + if (reader.allWhitespace(i, end)) return (float) (value / POW_10[i - decPos - pointOffset]); + numberException(reader, start, end, "Unknown digit", (char) ch); + } + value = (value << 3) + (value << 1) + ind; + } + final int endPos; + if (i == numLimit && !foundE) { + endPos = i + 1 - pointOffset; + while (i < end && ch >= '0' && ch <= '9') { + ch = buf[i++]; + } + } else endPos = i - pointOffset; + while (i == end && reader.length() == end) { + i = reader.scanNumber(); + end = reader.getCurrentIndex(); + buf = reader.buffer; + while (i < end && ch >= '0' && ch <= '9') { + ch = buf[i++]; + } + } + if (ch == 'e' || ch == 'E') { + return floatExponent(reader, value, endPos - decPos, buf, end, i); + } + final int expDiff = endPos - decPos; + if (expDiff > 0) { + return (float)(value / POW_10[expDiff - 1]); + } else if (expDiff < 0) { + return (float)(value * POW_10[-expDiff - 1]); + } else { + return value; + } + } else if (ch == 'e' || ch == 'E') { + return floatExponent(reader, value, 0, buf, end, i + 1); + } + return value; + } + + private static float floatExponent(JsonReader reader, final long whole, final int decimals, byte[] buf, int end, int i) throws IOException { + byte ch; + ch = buf[i]; + final int exp; + if (ch == '-') { + exp = parseNegativeInt(buf, reader, i, end) - decimals; + } else if (ch == '+') { + exp = parsePositiveInt(buf, reader, i, end, 1) - decimals; + } else { + exp = parsePositiveInt(buf, reader, i, end, 0) - decimals; + } + if (exp == 0 || whole == 0) return whole; + else if (exp > 0 && exp < POW_10.length) return (float) (whole * POW_10[exp - 1]); + else if (exp < 0 && -exp < POW_10.length) return (float) (whole / POW_10[-exp - 1]); + else return exp > 0 ? Float.POSITIVE_INFINITY : 0f; + } + + private static float parseFloatGeneric(final char[] buf, final int len, final JsonReader reader, final boolean withQuotes) throws ParsingException { + int end = len; + while (end > 0 && Character.isWhitespace(buf[end - 1])) { + end--; + } + if (end > reader.maxNumberDigits) { + throw reader.newParseErrorWith("Too many digits detected in number", len, "", "Too many digits detected in number", end, ""); + } + final int offset = buf[0] == '-' ? 1 : 0; + if (buf[offset] == '0' && end > offset + 1 && buf[offset + 1] >= '0' && buf[offset + 1] <= '9') { + throw reader.newParseErrorAt("Leading zero is not allowed. Error parsing number", len + (withQuotes ? 2 : 0)); + } + try { + return Float.parseFloat(new String(buf, 0, end)); + } catch (NumberFormatException nfe) { + throw reader.newParseErrorAt("Error parsing number", len + (withQuotes ? 2 : 0), nfe); + } + } + + @SuppressWarnings("unchecked") + public static ArrayList deserializeFloatCollection(final JsonReader reader) throws IOException { + return reader.deserializeCollection(FLOAT_READER); + } + + public static void deserializeFloatCollection(final JsonReader reader, Collection res) throws IOException { + reader.deserializeCollection(FLOAT_READER, res); + } + + @SuppressWarnings("unchecked") + public static ArrayList deserializeFloatNullableCollection(final JsonReader reader) throws IOException { + return reader.deserializeNullableCollection(FLOAT_READER); + } + + public static void deserializeFloatNullableCollection(final JsonReader reader, final Collection res) throws IOException { + reader.deserializeNullableCollection(FLOAT_READER, res); + } + + public static void serializeNullable(@Nullable final Integer value, final JsonWriter sw) { + if (value == null) { + sw.writeNull(); + } else { + serialize(value, sw); + } + } + + private static final byte MINUS = '-'; + private static final byte[] MIN_INT = "-2147483648".getBytes(); + + public static void serialize(final int value, final JsonWriter sw) { + final byte[] buf = sw.ensureCapacity(11); + final int position = sw.size(); + int current = serialize(buf, position, value); + sw.advance(current - position); + } + + private static int serialize(final byte[] buf, int pos, final int value) { + int i; + if (value < 0) { + if (value == Integer.MIN_VALUE) { + for (int x = 0; x < MIN_INT.length; x++) { + buf[pos + x] = MIN_INT[x]; + } + return pos + MIN_INT.length; + } + i = -value; + buf[pos++] = MINUS; + } else { + i = value; + } + final int q1 = i / 1000; + if (q1 == 0) { + pos += writeFirstBuf(buf, DIGITS[i], pos); + return pos; + } + final int r1 = i - q1 * 1000; + final int q2 = q1 / 1000; + if (q2 == 0) { + final int v1 = DIGITS[r1]; + final int v2 = DIGITS[q1]; + int off = writeFirstBuf(buf, v2, pos); + writeBuf(buf, v1, pos + off); + return pos + 3 + off; + } + final int r2 = q1 - q2 * 1000; + final int q3 = q2 / 1000; + final int v1 = DIGITS[r1]; + final int v2 = DIGITS[r2]; + if (q3 == 0) { + pos += writeFirstBuf(buf, DIGITS[q2], pos); + } else { + final int r3 = q2 - q3 * 1000; + buf[pos++] = (byte) (q3 + '0'); + writeBuf(buf, DIGITS[r3], pos); + pos += 3; + } + writeBuf(buf, v2, pos); + writeBuf(buf, v1, pos + 3); + return pos + 6; + } + + public static void serialize(@Nullable final int[] values, final JsonWriter sw) { + if (values == null) { + sw.writeNull(); + } else if (values.length == 0) { + sw.writeAscii("[]"); + } else { + final byte[] buf = sw.ensureCapacity(values.length * 11 + 2); + int position = sw.size(); + buf[position++] = '['; + position = serialize(buf, position, values[0]); + for (int i = 1; i < values.length; i++) { + buf[position++] = ','; + position = serialize(buf, position, values[i]); + } + buf[position++] = ']'; + sw.advance(position - sw.size()); + } + } + + public static void serialize(@Nullable final short[] value, final JsonWriter sw) { + if (value == null) { + sw.writeNull(); + } else if (value.length == 0) { + sw.writeAscii("[]"); + } else { + sw.writeByte(JsonWriter.ARRAY_START); + serialize(value[0], sw); + for (int i = 1; i < value.length; i++) { + sw.writeByte(JsonWriter.COMMA); + serialize(value[i], sw); + } + sw.writeByte(JsonWriter.ARRAY_END); + } + } + + public static short deserializeShort(final JsonReader reader) throws IOException { + if (reader.last() == '"') { + final int position = reader.getCurrentIndex(); + final char[] buf = reader.readSimpleQuote(); + try { + return parseNumberGeneric(buf, reader.getCurrentIndex() - position - 1, reader, true).shortValueExact(); + } catch (ArithmeticException ignore) { + throw reader.newParseErrorAt("Short overflow detected", reader.getCurrentIndex() - position); + } + } + final int start = reader.scanNumber(); + final int end = reader.getCurrentIndex(); + final byte[] buf = reader.buffer; + final byte ch = buf[start]; + final int value = ch == '-' + ? parseNegativeInt(buf, reader, start, end) + : parsePositiveInt(buf, reader, start, end, 0); + if (value < Short.MIN_VALUE || value > Short.MAX_VALUE) { + throw reader.newParseErrorAt("Short overflow detected", reader.getCurrentIndex()); + } + return (short)value; + } + + public static int deserializeInt(final JsonReader reader) throws IOException { + if (reader.last() == '"') { + final int position = reader.getCurrentIndex(); + final char[] buf = reader.readSimpleQuote(); + try { + return parseNumberGeneric(buf, reader.getCurrentIndex() - position - 1, reader, true).intValueExact(); + } catch (ArithmeticException ignore) { + throw reader.newParseErrorAt("Integer overflow detected", reader.getCurrentIndex() - position); + } + } + final int start = reader.scanNumber(); + final int end = reader.getCurrentIndex(); + final byte[] buf = reader.buffer; + final byte ch = buf[start]; + if (ch == '-') { + if (end > start + 2 && buf[start + 1] == '0' && buf[start + 2] >= '0' && buf[start + 2] <= '9') { + numberException(reader, start, end, "Leading zero is not allowed"); + } + return parseNegativeInt(buf, reader, start, end); + } else { + if (ch == '0' && end > start + 1 && buf[start + 1] >= '0' && buf[start + 1] <= '9') { + numberException(reader, start, end, "Leading zero is not allowed"); + } + return parsePositiveInt(buf, reader, start, end, 0); + } + } + + private static int parsePositiveInt(final byte[] buf, final JsonReader reader, final int start, final int end, final int offset) throws IOException { + int value = 0; + int i = start + offset; + if (i == end) numberException(reader, start, end, "Digit not found"); + for (; i < end; i++) { + final int ind = buf[i] - 48; + if (ind < 0 || ind > 9) { + if (i > start + offset && reader.allWhitespace(i, end)) return value; + else if (i == end - 1 && buf[i] == '.') numberException(reader, start, end, "Number ends with a dot"); + final BigDecimal v = parseNumberGeneric(reader.prepareBuffer(start, end - start), end - start, reader, false); + if (v.scale() > 0) numberException(reader, start, end, "Expecting int but found decimal value", v); + return v.intValue(); + + } + value = (value << 3) + (value << 1) + ind; + if (value < 0) { + numberException(reader, start, end, "Integer overflow detected"); + } + } + return value; + } + + private static int parseNegativeInt(final byte[] buf, final JsonReader reader, final int start, final int end) throws IOException { + int value = 0; + int i = start + 1; + if (i == end) numberException(reader, start, end, "Digit not found"); + for (; i < end; i++) { + final int ind = buf[i] - 48; + if (ind < 0 || ind > 9) { + if (i > start + 1 && reader.allWhitespace(i, end)) return value; + else if (i == end - 1 && buf[i] == '.') numberException(reader, start, end, "Number ends with a dot"); + final BigDecimal v = parseNumberGeneric(reader.prepareBuffer(start, end - start), end - start, reader, false); + if (v.scale() > 0) numberException(reader, start, end, "Expecting int but found decimal value", v); + return v.intValue(); + } + value = (value << 3) + (value << 1) - ind; + if (value > 0) { + numberException(reader, start, end, "Integer overflow detected"); + } + } + return value; + } + + @SuppressWarnings("unchecked") + public static ArrayList deserializeIntCollection(final JsonReader reader) throws IOException { + return reader.deserializeCollection(INT_READER); + } + + public static int[] deserializeIntArray(final JsonReader reader) throws IOException { + if (reader.last() == ']') { + return INT_EMPTY_ARRAY; + } + int[] buffer = new int[4]; + buffer[0] = deserializeInt(reader); + int i = 1; + while (reader.getNextToken() == ',') { + reader.getNextToken(); + if (i == buffer.length) { + buffer = Arrays.copyOf(buffer, buffer.length << 1); + } + buffer[i++] = deserializeInt(reader); + } + reader.checkArrayEnd(); + return Arrays.copyOf(buffer, i); + } + + public static short[] deserializeShortArray(final JsonReader reader) throws IOException { + if (reader.last() == ']') { + return SHORT_EMPTY_ARRAY; + } + short[] buffer = new short[4]; + buffer[0] = (short)deserializeInt(reader); + int i = 1; + while (reader.getNextToken() == ',') { + reader.getNextToken(); + if (i == buffer.length) { + buffer = Arrays.copyOf(buffer, buffer.length << 1); + } + buffer[i++] = (short)deserializeInt(reader); + } + reader.checkArrayEnd(); + return Arrays.copyOf(buffer, i); + } + + public static long[] deserializeLongArray(final JsonReader reader) throws IOException { + if (reader.last() == ']') { + return LONG_EMPTY_ARRAY; + } + long[] buffer = new long[4]; + buffer[0] = deserializeLong(reader); + int i = 1; + while (reader.getNextToken() == ',') { + reader.getNextToken(); + if (i == buffer.length) { + buffer = Arrays.copyOf(buffer, buffer.length << 1); + } + buffer[i++] = deserializeLong(reader); + } + reader.checkArrayEnd(); + return Arrays.copyOf(buffer, i); + } + + public static float[] deserializeFloatArray(final JsonReader reader) throws IOException { + if (reader.last() == ']') { + return FLOAT_EMPTY_ARRAY; + } + float[] buffer = new float[4]; + buffer[0] = deserializeFloat(reader); + int i = 1; + while (reader.getNextToken() == ',') { + reader.getNextToken(); + if (i == buffer.length) { + buffer = Arrays.copyOf(buffer, buffer.length << 1); + } + buffer[i++] = deserializeFloat(reader); + } + reader.checkArrayEnd(); + return Arrays.copyOf(buffer, i); + } + + public static double[] deserializeDoubleArray(final JsonReader reader) throws IOException { + if (reader.last() == ']') { + return DOUBLE_EMPTY_ARRAY; + } + double[] buffer = new double[4]; + buffer[0] = deserializeDouble(reader); + int i = 1; + while (reader.getNextToken() == ',') { + reader.getNextToken(); + if (i == buffer.length) { + buffer = Arrays.copyOf(buffer, buffer.length << 1); + } + buffer[i++] = deserializeDouble(reader); + } + reader.checkArrayEnd(); + return Arrays.copyOf(buffer, i); + } + + public static void deserializeShortCollection(final JsonReader reader, final Collection res) throws IOException { + reader.deserializeCollection(SHORT_READER, res); + } + + @SuppressWarnings("unchecked") + public static ArrayList deserializeShortNullableCollection(final JsonReader reader) throws IOException { + return reader.deserializeNullableCollection(SHORT_READER); + } + + public static void deserializeShortNullableCollection(final JsonReader reader, final Collection res) throws IOException { + reader.deserializeNullableCollection(SHORT_READER, res); + } + + public static void deserializeIntCollection(final JsonReader reader, final Collection res) throws IOException { + reader.deserializeCollection(INT_READER, res); + } + + @SuppressWarnings("unchecked") + public static ArrayList deserializeIntNullableCollection(final JsonReader reader) throws IOException { + return reader.deserializeNullableCollection(INT_READER); + } + + public static void deserializeIntNullableCollection(final JsonReader reader, final Collection res) throws IOException { + reader.deserializeNullableCollection(INT_READER, res); + } + + public static void serializeNullable(@Nullable final Long value, final JsonWriter sw) { + if (value == null) { + sw.writeNull(); + } else { + serialize(value, sw); + } + } + + private static int writeFirstBuf(final byte[] buf, final int v, int pos) { + final int start = v >> 24; + if (start == 0) { + buf[pos++] = (byte) (v >> 16); + buf[pos++] = (byte) (v >> 8); + } else if (start == 1) { + buf[pos++] = (byte) (v >> 8); + } + buf[pos] = (byte) v; + return 3 - start; + } + + private static void writeBuf(final byte[] buf, final int v, int pos) { + buf[pos] = (byte) (v >> 16); + buf[pos + 1] = (byte) (v >> 8); + buf[pos + 2] = (byte) v; + } + + private static final byte[] MIN_LONG = "-9223372036854775808".getBytes(); + + public static void serialize(final long value, final JsonWriter sw) { + final byte[] buf = sw.ensureCapacity(21); + final int position = sw.size(); + int current = serialize(buf, position, value); + sw.advance(current - position); + } + + private static int serialize(final byte[] buf, int pos, final long value) { + long i; + if (value < 0) { + if (value == Long.MIN_VALUE) { + for (int x = 0; x < MIN_LONG.length; x++) { + buf[pos + x] = MIN_LONG[x]; + } + return pos + MIN_LONG.length; + } + i = -value; + buf[pos++] = MINUS; + } else { + i = value; + } + final long q1 = i / 1000; + if (q1 == 0) { + pos += writeFirstBuf(buf, DIGITS[(int) i], pos); + return pos; + } + final int r1 = (int) (i - q1 * 1000); + final long q2 = q1 / 1000; + if (q2 == 0) { + final int v1 = DIGITS[r1]; + final int v2 = DIGITS[(int) q1]; + int off = writeFirstBuf(buf, v2, pos); + writeBuf(buf, v1, pos + off); + return pos + 3 + off; + } + final int r2 = (int) (q1 - q2 * 1000); + final long q3 = q2 / 1000; + if (q3 == 0) { + final int v1 = DIGITS[r1]; + final int v2 = DIGITS[r2]; + final int v3 = DIGITS[(int) q2]; + pos += writeFirstBuf(buf, v3, pos); + writeBuf(buf, v2, pos); + writeBuf(buf, v1, pos + 3); + return pos + 6; + } + final int r3 = (int) (q2 - q3 * 1000); + final int q4 = (int) (q3 / 1000); + if (q4 == 0) { + final int v1 = DIGITS[r1]; + final int v2 = DIGITS[r2]; + final int v3 = DIGITS[r3]; + final int v4 = DIGITS[(int) q3]; + pos += writeFirstBuf(buf, v4, pos); + writeBuf(buf, v3, pos); + writeBuf(buf, v2, pos + 3); + writeBuf(buf, v1, pos + 6); + return pos + 9; + } + final int r4 = (int) (q3 - q4 * 1000); + final int q5 = q4 / 1000; + if (q5 == 0) { + final int v1 = DIGITS[r1]; + final int v2 = DIGITS[r2]; + final int v3 = DIGITS[r3]; + final int v4 = DIGITS[r4]; + final int v5 = DIGITS[q4]; + pos += writeFirstBuf(buf, v5, pos); + writeBuf(buf, v4, pos); + writeBuf(buf, v3, pos + 3); + writeBuf(buf, v2, pos + 6); + writeBuf(buf, v1, pos + 9); + return pos + 12; + } + final int r5 = q4 - q5 * 1000; + final int q6 = q5 / 1000; + final int v1 = DIGITS[r1]; + final int v2 = DIGITS[r2]; + final int v3 = DIGITS[r3]; + final int v4 = DIGITS[r4]; + final int v5 = DIGITS[r5]; + if (q6 == 0) { + pos += writeFirstBuf(buf, DIGITS[q5], pos); + } else { + final int r6 = q5 - q6 * 1000; + buf[pos++] = (byte) (q6 + '0'); + writeBuf(buf, DIGITS[r6], pos); + pos += 3; + } + writeBuf(buf, v5, pos); + writeBuf(buf, v4, pos + 3); + writeBuf(buf, v3, pos + 6); + writeBuf(buf, v2, pos + 9); + writeBuf(buf, v1, pos + 12); + return pos + 15; + } + + public static void serialize(@Nullable final long[] values, final JsonWriter sw) { + if (values == null) { + sw.writeNull(); + } else if (values.length == 0) { + sw.writeAscii("[]"); + } else { + final byte[] buf = sw.ensureCapacity(values.length * 21 + 2); + int position = sw.size(); + buf[position++] = '['; + position = serialize(buf, position, values[0]); + for (int i = 1; i < values.length; i++) { + buf[position++] = ','; + position = serialize(buf, position, values[i]); + } + buf[position++] = ']'; + sw.advance(position - sw.size()); + } + } + + public static long deserializeLong(final JsonReader reader) throws IOException { + if (reader.last() == '"') { + final int position = reader.getCurrentIndex(); + final char[] buf = reader.readSimpleQuote(); + try { + return parseNumberGeneric(buf, reader.getCurrentIndex() - position - 1, reader, true).longValueExact(); + } catch (ArithmeticException ignore) { + throw reader.newParseErrorAt("Long overflow detected", reader.getCurrentIndex() - position); + } + } + final int start = reader.scanNumber(); + final int end = reader.getCurrentIndex(); + final byte[] buf = reader.buffer; + final byte ch = buf[start]; + int i = start; + long value = 0; + if (ch == '-') { + i = start + 1; + if (i == end) numberException(reader, start, end, "Digit not found"); + final boolean leadingZero = buf[i] == 48; + for (; i < end; i++) { + final int ind = buf[i] - 48; + if (ind < 0 || ind > 9) { + if (leadingZero && i > start + 2) { + numberException(reader, start, end, "Leading zero is not allowed"); + } + if (i > start + 1 && reader.allWhitespace(i, end)) return value; + return parseLongGeneric(reader, start, end); + } + value = (value << 3) + (value << 1) - ind; + if (value > 0) { + numberException(reader, start, end, "Long overflow detected"); + } + } + if (leadingZero && i > start + 2) { + numberException(reader, start, end, "Leading zero is not allowed"); + } + return value; + } + if (i == end) numberException(reader, start, end, "Digit not found"); + final boolean leadingZero = buf[i] == 48; + for (; i < end; i++) { + final int ind = buf[i] - 48; + if (ind < 0 || ind > 9) { + if (leadingZero && i > start + 1) { + numberException(reader, start, end, "Leading zero is not allowed"); + } + if (ch == '+' && i > start + 1 && reader.allWhitespace(i, end)) return value; + else if (ch != '+' && i > start && reader.allWhitespace(i, end)) return value; + return parseLongGeneric(reader, start, end); + } + value = (value << 3) + (value << 1) + ind; + if (value < 0) { + numberException(reader, start, end, "Long overflow detected"); + } + } + if (leadingZero && i > start + 1) { + numberException(reader, start, end, "Leading zero is not allowed"); + } + return value; + } + + private static long parseLongGeneric(final JsonReader reader, final int start, final int end) throws IOException { + final int len = end - start; + final char[] buf = reader.prepareBuffer(start, len); + if (len > 0 && buf[len - 1] == '.') numberException(reader, start, end, "Number ends with a dot"); + final BigDecimal v = parseNumberGeneric(buf, len, reader, false); + if (v.scale() > 0) numberException(reader, start, end, "Expecting long, but found decimal value ", v); + return v.longValue(); + } + + @SuppressWarnings("unchecked") + public static ArrayList deserializeLongCollection(final JsonReader reader) throws IOException { + return reader.deserializeCollection(LONG_READER); + } + + public static void deserializeLongCollection(final JsonReader reader, final Collection res) throws IOException { + reader.deserializeCollection(LONG_READER, res); + } + + @SuppressWarnings("unchecked") + public static ArrayList deserializeLongNullableCollection(final JsonReader reader) throws IOException { + return reader.deserializeNullableCollection(LONG_READER); + } + + public static void deserializeLongNullableCollection(final JsonReader reader, final Collection res) throws IOException { + reader.deserializeNullableCollection(LONG_READER, res); + } + + public static void serializeNullable(@Nullable final BigDecimal value, final JsonWriter sw) { + if (value == null) { + sw.writeNull(); + } else { + sw.writeAscii(value.toString()); + } + } + + public static void serialize(final BigDecimal value, final JsonWriter sw) { + sw.writeAscii(value.toString()); + } + + public static BigDecimal deserializeDecimal(final JsonReader reader) throws IOException { + if (reader.last() == '"') { + final int len = reader.parseString(); + return parseNumberGeneric(reader.chars, len, reader, true); + } + final int start = reader.scanNumber(); + int end = reader.getCurrentIndex(); + if (end == reader.length()) { + NumberInfo info = readLongNumber(reader, start); + return parseNumberGeneric(info.buffer, info.length, reader, false); + } + int len = end - start; + if (len > 18) { + return parseNumberGeneric(reader.prepareBuffer(start, len), len, reader, false); + } + final byte[] buf = reader.buffer; + final byte ch = buf[start]; + if (ch == '-') { + return parseNegativeDecimal(buf, reader, start, end); + } + return parsePositiveDecimal(buf, reader, start, end); + } + + private static BigDecimal parsePositiveDecimal(final byte[] buf, final JsonReader reader, final int start, final int end) throws IOException { + long value = 0; + byte ch = ' '; + int i = start; + final boolean leadingZero = buf[start] == 48; + for (; i < end; i++) { + ch = buf[i]; + if (ch == '.' || ch == 'e' || ch == 'E') break; + final int ind = ch - 48; + if (ind < 0 || ind > 9) { + if (leadingZero && i > start + 1) { + numberException(reader, start, end, "Leading zero is not allowed"); + } + if (i > start && reader.allWhitespace(i, end)) return BigDecimal.valueOf(value); + numberException(reader, start, end, "Unknown digit", (char)ch); + } + value = (value << 3) + (value << 1) + ind; + } + if (i == start) numberException(reader, start, end, "Digit not found"); + else if (leadingZero && ch != '.' && i > start + 1) numberException(reader, start, end, "Leading zero is not allowed"); + else if (i == end) return BigDecimal.valueOf(value); + else if (ch == '.') { + i++; + if (i == end) numberException(reader, start, end, "Number ends with a dot"); + int dp = i; + for (; i < end; i++) { + ch = buf[i]; + if (ch == 'e' || ch == 'E') break; + final int ind = ch - 48; + if (ind < 0 || ind > 9) { + if (reader.allWhitespace(i, end)) return BigDecimal.valueOf(value, i - dp); + numberException(reader, start, end, "Unknown digit", (char)ch); + } + value = (value << 3) + (value << 1) + ind; + } + if (i == end) return BigDecimal.valueOf(value, end - dp); + else if (ch == 'e' || ch == 'E') { + final int ep = i; + i++; + ch = buf[i]; + final int exp; + if (ch == '-') { + exp = parseNegativeInt(buf, reader, i, end); + } else if (ch == '+') { + exp = parsePositiveInt(buf, reader, i, end, 1); + } else { + exp = parsePositiveInt(buf, reader, i, end, 0); + } + return BigDecimal.valueOf(value, ep - dp - exp); + } + return BigDecimal.valueOf(value, end - dp); + } else if (ch == 'e' || ch == 'E') { + i++; + ch = buf[i]; + final int exp; + if (ch == '-') { + exp = parseNegativeInt(buf, reader, i, end); + } else if (ch == '+') { + exp = parsePositiveInt(buf, reader, i, end, 1); + } else { + exp = parsePositiveInt(buf, reader, i, end, 0); + } + return BigDecimal.valueOf(value, -exp); + } + return BigDecimal.valueOf(value); + } + + private static BigDecimal parseNegativeDecimal(final byte[] buf, final JsonReader reader, final int start, final int end) throws IOException { + long value = 0; + byte ch = ' '; + int i = start + 1; + final boolean leadingZero = buf[start + 1] == 48; + for (; i < end; i++) { + ch = buf[i]; + if (ch == '.' || ch == 'e' || ch == 'E') break; + final int ind = ch - 48; + if (ind < 0 || ind > 9) { + if (leadingZero && i > start + 2) { + numberException(reader, start, end, "Leading zero is not allowed"); + } + if (i > start + 1 && reader.allWhitespace(i, end)) return BigDecimal.valueOf(value); + numberException(reader, start, end, "Unknown digit", (char)ch); + } + value = (value << 3) + (value << 1) - ind; + } + if (i == start + 1) numberException(reader, start, end, "Digit not found"); + else if (leadingZero && ch != '.' && i > start + 2) numberException(reader, start, end, "Leading zero is not allowed"); + else if (i == end) return BigDecimal.valueOf(value); + else if (ch == '.') { + i++; + if (i == end) numberException(reader, start, end, "Number ends with a dot"); + int dp = i; + for (; i < end; i++) { + ch = buf[i]; + if (ch == 'e' || ch == 'E') break; + final int ind = ch - 48; + if (ind < 0 || ind > 9) { + if (reader.allWhitespace(i, end)) return BigDecimal.valueOf(value, i - dp); + numberException(reader, start, end, "Unknown digit", (char)ch); + } + value = (value << 3) + (value << 1) - ind; + } + if (i == end) return BigDecimal.valueOf(value, end - dp); + else if (ch == 'e' || ch == 'E') { + final int ep = i; + i++; + ch = buf[i]; + final int exp; + if (ch == '-') { + exp = parseNegativeInt(buf, reader, i, end); + } else if (ch == '+') { + exp = parsePositiveInt(buf, reader, i, end, 1); + } else { + exp = parsePositiveInt(buf, reader, i, end, 0); + } + return BigDecimal.valueOf(value, ep - dp - exp); + } + return BigDecimal.valueOf(value, end - dp); + } else if (ch == 'e' || ch == 'E') { + i++; + ch = buf[i]; + final int exp; + if (ch == '-') { + exp = parseNegativeInt(buf, reader, i, end); + } else if (ch == '+') { + exp = parsePositiveInt(buf, reader, i, end, 1); + } else { + exp = parsePositiveInt(buf, reader, i, end, 0); + } + return BigDecimal.valueOf(value, -exp); + } + return BigDecimal.valueOf(value); + } + + private static final BigDecimal BD_MAX_LONG = BigDecimal.valueOf(Long.MAX_VALUE); + private static final BigDecimal BD_MIN_LONG = BigDecimal.valueOf(Long.MIN_VALUE); + + private static Number bigDecimalOrDouble(BigDecimal num, JsonReader.UnknownNumberParsing unknownNumbers) { + return unknownNumbers == JsonReader.UnknownNumberParsing.LONG_AND_BIGDECIMAL + ? num + : num.doubleValue(); + } + + private static Number tryLongFromBigDecimal(final char[] buf, final int len, JsonReader reader) throws IOException { + final BigDecimal num = parseNumberGeneric(buf, len, reader, false); + if (num.scale() == 0 && num.precision() <= 19) { + if (num.signum() == 1) { + if (num.compareTo(BD_MAX_LONG) <= 0) { + return num.longValue(); + } + } else if (num.compareTo(BD_MIN_LONG) >= 0) { + return num.longValue(); + } + } + return bigDecimalOrDouble(num, reader.unknownNumbers); + } + + public static Number deserializeNumber(final JsonReader reader) throws IOException { + if (reader.unknownNumbers == JsonReader.UnknownNumberParsing.BIGDECIMAL) return deserializeDecimal(reader); + else if (reader.unknownNumbers == JsonReader.UnknownNumberParsing.DOUBLE) return deserializeDouble(reader); + final int start = reader.scanNumber(); + int end = reader.getCurrentIndex(); + if (end == reader.length()) { + NumberInfo info = readLongNumber(reader, start); + return tryLongFromBigDecimal(info.buffer, info.length, reader); + } + int len = end - start; + if (len > 18) { + return tryLongFromBigDecimal(reader.prepareBuffer(start, len), len, reader); + } + final byte[] buf = reader.buffer; + final byte ch = buf[start]; + if (ch == '-') { + return parseNegativeNumber(buf, reader, start, end); + } + return parsePositiveNumber(buf, reader, start, end); + } + + private static Number parsePositiveNumber(final byte[] buf, final JsonReader reader, final int start, final int end) throws IOException { + long value = 0; + byte ch = ' '; + int i = start; + final boolean leadingZero = buf[start] == 48; + for (; i < end; i++) { + ch = buf[i]; + if (ch == '.' || ch == 'e' || ch == 'E') break; + final int ind = ch - 48; + if (ind < 0 || ind > 9) { + if (leadingZero && i > start + 1) { + numberException(reader, start, end, "Leading zero is not allowed"); + } + if (i > start && reader.allWhitespace(i, end)) return value; + return tryLongFromBigDecimal(reader.prepareBuffer(start, end - start), end - start, reader); + } + value = (value << 3) + (value << 1) + ind; + } + if (i == start) numberException(reader, start, end, "Digit not found"); + else if (leadingZero && ch != '.' && i > start + 1) numberException(reader, start, end, "Leading zero is not allowed"); + else if (i == end) return value; + else if (ch == '.') { + i++; + if (i == end) numberException(reader, start, end, "Number ends with a dot"); + int dp = i; + for (; i < end; i++) { + ch = buf[i]; + if (ch == 'e' || ch == 'E') break; + final int ind = ch - 48; + if (ind < 0 || ind > 9) { + if (reader.allWhitespace(i, end)) return BigDecimal.valueOf(value, i - dp); + return tryLongFromBigDecimal(reader.prepareBuffer(start, end - start), end - start, reader); + } + value = (value << 3) + (value << 1) + ind; + } + if (i == end) return bigDecimalOrDouble(BigDecimal.valueOf(value, end - dp), reader.unknownNumbers); + else if (ch == 'e' || ch == 'E') { + final int ep = i; + i++; + ch = buf[i]; + final int exp; + if (ch == '-') { + exp = parseNegativeInt(buf, reader, i, end); + } else if (ch == '+') { + exp = parsePositiveInt(buf, reader, i, end, 1); + } else { + exp = parsePositiveInt(buf, reader, i, end, 0); + } + return bigDecimalOrDouble(BigDecimal.valueOf(value, ep - dp - exp), reader.unknownNumbers); + } + return BigDecimal.valueOf(value, end - dp); + } else if (ch == 'e' || ch == 'E') { + i++; + ch = buf[i]; + final int exp; + if (ch == '-') { + exp = parseNegativeInt(buf, reader, i, end); + } else if (ch == '+') { + exp = parsePositiveInt(buf, reader, i, end, 1); + } else { + exp = parsePositiveInt(buf, reader, i, end, 0); + } + return bigDecimalOrDouble(BigDecimal.valueOf(value, -exp), reader.unknownNumbers); + } + return bigDecimalOrDouble(BigDecimal.valueOf(value), reader.unknownNumbers); + } + + private static Number parseNegativeNumber(final byte[] buf, final JsonReader reader, final int start, final int end) throws IOException { + long value = 0; + byte ch = ' '; + int i = start + 1; + final boolean leadingZero = buf[start + 1] == 48; + for (; i < end; i++) { + ch = buf[i]; + if (ch == '.' || ch == 'e' || ch == 'E') break; + final int ind = ch - 48; + if (ind < 0 || ind > 9) { + if (leadingZero && i > start + 2) { + numberException(reader, start, end, "Leading zero is not allowed"); + } + if (i > start + 1 && reader.allWhitespace(i, end)) return value; + return tryLongFromBigDecimal(reader.prepareBuffer(start, end - start), end - start, reader); + } + value = (value << 3) + (value << 1) - ind; + } + if (i == start + 1) numberException(reader, start, end, "Digit not found"); + else if (leadingZero && ch != '.' && i > start + 2) numberException(reader, start, end, "Leading zero is not allowed"); + else if (i == end) return value; + else if (ch == '.') { + i++; + if (i == end) numberException(reader, start, end, "Number ends with a dot"); + int dp = i; + for (; i < end; i++) { + ch = buf[i]; + if (ch == 'e' || ch == 'E') break; + final int ind = ch - 48; + if (ind < 0 || ind > 9) { + if (reader.allWhitespace(i, end)) return BigDecimal.valueOf(value, i - dp); + return tryLongFromBigDecimal(reader.prepareBuffer(start, end - start), end - start, reader); + } + value = (value << 3) + (value << 1) - ind; + } + if (i == end) return bigDecimalOrDouble(BigDecimal.valueOf(value, end - dp), reader.unknownNumbers); + else if (ch == 'e' || ch == 'E') { + final int ep = i; + i++; + ch = buf[i]; + final int exp; + if (ch == '-') { + exp = parseNegativeInt(buf, reader, i, end); + } else if (ch == '+') { + exp = parsePositiveInt(buf, reader, i, end, 1); + } else { + exp = parsePositiveInt(buf, reader, i, end, 0); + } + return bigDecimalOrDouble(BigDecimal.valueOf(value, ep - dp - exp), reader.unknownNumbers); + } + return bigDecimalOrDouble(BigDecimal.valueOf(value, end - dp), reader.unknownNumbers); + } else if (ch == 'e' || ch == 'E') { + i++; + ch = buf[i]; + final int exp; + if (ch == '-') { + exp = parseNegativeInt(buf, reader, i, end); + } else if (ch == '+') { + exp = parsePositiveInt(buf, reader, i, end, 1); + } else { + exp = parsePositiveInt(buf, reader, i, end, 0); + } + return bigDecimalOrDouble(BigDecimal.valueOf(value, -exp), reader.unknownNumbers); + } + return bigDecimalOrDouble(BigDecimal.valueOf(value), reader.unknownNumbers); + } + + @SuppressWarnings("unchecked") + public static ArrayList deserializeDecimalCollection(final JsonReader reader) throws IOException { + return reader.deserializeCollection(DecimalReader); + } + + public static void deserializeDecimalCollection(final JsonReader reader, final Collection res) throws IOException { + reader.deserializeCollection(DecimalReader, res); + } + + @SuppressWarnings("unchecked") + public static ArrayList deserializeDecimalNullableCollection(final JsonReader reader) throws IOException { + return reader.deserializeNullableCollection(DecimalReader); + } + + public static void deserializeDecimalNullableCollection(final JsonReader reader, final Collection res) throws IOException { + reader.deserializeNullableCollection(DecimalReader, res); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/ObjectConverter.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/ObjectConverter.java new file mode 100644 index 0000000000..ab37064fe0 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/ObjectConverter.java @@ -0,0 +1,135 @@ +package com.bugsnag.android.repackaged.dslplatform.json; + +import androidx.annotation.Nullable; + +import java.io.IOException; +import java.util.*; + +@SuppressWarnings({"rawtypes", "unchecked"}) // suppress pre-existing warnings +public abstract class ObjectConverter { + + private static final JsonReader.ReadObject> TypedMapReader = new JsonReader.ReadObject>() { + @Nullable + @Override + public Map read(JsonReader reader) throws IOException { + return reader.wasNull() ? null : deserializeMap(reader); + } + }; + @SuppressWarnings("rawtypes") + static final JsonReader.ReadObject MapReader = new JsonReader.ReadObject() { + @Nullable + @Override + public LinkedHashMap read(JsonReader reader) throws IOException { + return reader.wasNull() ? null : deserializeMap(reader); + } + }; + + public static void serializeNullableMap(@Nullable final Map value, final JsonWriter sw) { + if (value == null) { + sw.writeNull(); + } else { + serializeMap(value, sw); + } + } + + public static void serializeMap(final Map value, final JsonWriter sw) { + sw.writeByte(JsonWriter.OBJECT_START); + final int size = value.size(); + if (size > 0) { + final Iterator> iterator = value.entrySet().iterator(); + Map.Entry kv = iterator.next(); + sw.writeString(kv.getKey()); + sw.writeByte(JsonWriter.SEMI); + sw.serializeObject(kv.getValue()); + for (int i = 1; i < size; i++) { + sw.writeByte(JsonWriter.COMMA); + kv = iterator.next(); + sw.writeString(kv.getKey()); + sw.writeByte(JsonWriter.SEMI); + sw.serializeObject(kv.getValue()); + } + } + sw.writeByte(JsonWriter.OBJECT_END); + } + + public static void serializeObject(@Nullable final Object value, final JsonWriter sw) throws IOException { + sw.serializeObject(value); + } + + @Nullable + public static Object deserializeObject(final JsonReader reader) throws IOException { + switch (reader.last()) { + case 'n': + if (!reader.wasNull()) { + throw reader.newParseErrorAt("Expecting 'null' for null constant", 0); + } + return null; + case 't': + if (!reader.wasTrue()) { + throw reader.newParseErrorAt("Expecting 'true' for true constant", 0); + } + return true; + case 'f': + if (!reader.wasFalse()) { + throw reader.newParseErrorAt("Expecting 'false' for false constant", 0); + } + return false; + case '"': + return reader.readString(); + case '{': + return deserializeMap(reader); + case '[': + return deserializeList(reader); + default: + return NumberConverter.deserializeNumber(reader); + } + } + + public static ArrayList deserializeList(final JsonReader reader) throws IOException { + if (reader.last() != '[') throw reader.newParseError("Expecting '[' for list start"); + byte nextToken = reader.getNextToken(); + if (nextToken == ']') return new ArrayList(0); + final ArrayList res = new ArrayList(4); + res.add(deserializeObject(reader)); + while ((nextToken = reader.getNextToken()) == ',') { + reader.getNextToken(); + res.add(deserializeObject(reader)); + } + if (nextToken != ']') throw reader.newParseError("Expecting ']' for list end"); + return res; + } + + public static LinkedHashMap deserializeMap(final JsonReader reader) throws IOException { + if (reader.last() != '{') throw reader.newParseError("Expecting '{' for map start"); + byte nextToken = reader.getNextToken(); + if (nextToken == '}') return new LinkedHashMap(0); + final LinkedHashMap res = new LinkedHashMap(); + String key = reader.readKey(); + res.put(key, deserializeObject(reader)); + while ((nextToken = reader.getNextToken()) == ',') { + reader.getNextToken(); + key = reader.readKey(); + res.put(key, deserializeObject(reader)); + } + if (nextToken != '}') throw reader.newParseError("Expecting '}' for map end"); + return res; + } + + @SuppressWarnings("unchecked") + public static ArrayList> deserializeMapCollection(final JsonReader reader) throws IOException { + return reader.deserializeCollection(TypedMapReader); + } + + public static void deserializeMapCollection(final JsonReader reader, final Collection> res) throws IOException { + reader.deserializeCollection(TypedMapReader, res); + } + + @SuppressWarnings("unchecked") + public static ArrayList> deserializeNullableMapCollection(final JsonReader reader) throws IOException { + return reader.deserializeNullableCollection(TypedMapReader); + } + + public static void deserializeNullableMapCollection(final JsonReader reader, final Collection> res) throws IOException { + reader.deserializeNullableCollection(TypedMapReader, res); + } +} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/ParsingException.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/ParsingException.java new file mode 100644 index 0000000000..21ee52de59 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/ParsingException.java @@ -0,0 +1,44 @@ +package com.bugsnag.android.repackaged.dslplatform.json; + +import java.io.IOException; + +@SuppressWarnings("serial") // suppress pre-existing warnings +public class ParsingException extends IOException { + + private ParsingException(String reason) { + super(reason); + } + + private ParsingException(String reason, Throwable cause) { + super(reason, cause); + } + + public static ParsingException create(String reason, boolean withStackTrace) { + return withStackTrace + ? new ParsingException(reason) + : new ParsingStacklessException(reason); + } + + + public static ParsingException create(String reason, Throwable cause, boolean withStackTrace) { + return withStackTrace + ? new ParsingException(reason, cause) + : new ParsingStacklessException(reason, cause); + } + + private static class ParsingStacklessException extends ParsingException { + + private ParsingStacklessException(String reason) { + super(reason); + } + + private ParsingStacklessException(String reason, Throwable cause) { + super(reason, cause); + } + + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } + } +} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/SerializationException.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/SerializationException.java new file mode 100644 index 0000000000..1c9a2f2e5b --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/SerializationException.java @@ -0,0 +1,18 @@ +package com.bugsnag.android.repackaged.dslplatform.json; + +import androidx.annotation.Nullable; + +@SuppressWarnings("serial") // suppress pre-existing warnings +public class SerializationException extends RuntimeException { + public SerializationException(@Nullable String reason) { + super(reason); + } + + public SerializationException(@Nullable Throwable cause) { + super(cause); + } + + public SerializationException(@Nullable String reason, @Nullable Throwable cause) { + super(reason, cause); + } +} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/StringCache.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/StringCache.java new file mode 100644 index 0000000000..390ac190ba --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/StringCache.java @@ -0,0 +1,5 @@ +package com.bugsnag.android.repackaged.dslplatform.json; + +public interface StringCache { + String get(char[] chars, int len); +} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/StringConverter.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/StringConverter.java new file mode 100644 index 0000000000..06fe923f5e --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/StringConverter.java @@ -0,0 +1,119 @@ +package com.bugsnag.android.repackaged.dslplatform.json; + +import androidx.annotation.Nullable; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@SuppressWarnings({"rawtypes", "unchecked"}) // suppress pre-existing warnings +public abstract class StringConverter { + + public static final JsonReader.ReadObject READER = new JsonReader.ReadObject() { + @Nullable + @Override + public String read(JsonReader reader) throws IOException { + if (reader.wasNull()) return null; + return reader.readString(); + } + }; + public static final JsonWriter.WriteObject WRITER = new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable String value) { + serializeNullable(value, writer); + } + }; + public static final JsonWriter.WriteObject WRITER_CHARS = new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable CharSequence value) { + if (value == null) writer.writeNull(); + else writer.writeString(value); + } + }; + public static final JsonReader.ReadObject READER_BUILDER = new JsonReader.ReadObject() { + @Nullable + @Override + public StringBuilder read(JsonReader reader) throws IOException { + if (reader.wasNull()) return null; + StringBuilder builder = new StringBuilder(); + return reader.appendString(builder); + } + }; + public static final JsonReader.ReadObject READER_BUFFER = new JsonReader.ReadObject() { + @Nullable + @Override + public StringBuffer read(JsonReader reader) throws IOException { + if (reader.wasNull()) return null; + StringBuffer builder = new StringBuffer(); + return reader.appendString(builder); + } + }; + + public static void serializeShortNullable(@Nullable final String value, final JsonWriter sw) { + if (value == null) { + sw.writeNull(); + } else { + sw.writeString(value); + } + } + + public static void serializeShort(final String value, final JsonWriter sw) { + sw.writeString(value); + } + + public static void serializeNullable(@Nullable final String value, final JsonWriter sw) { + if (value == null) { + sw.writeNull(); + } else { + sw.writeString(value); + } + } + + public static void serialize(final String value, final JsonWriter sw) { + sw.writeString(value); + } + + public static String deserialize(final JsonReader reader) throws IOException { + return reader.readString(); + } + + @Nullable + public static String deserializeNullable(final JsonReader reader) throws IOException { + if (reader.last() == 'n') { + if (!reader.wasNull()) throw reader.newParseErrorAt("Expecting 'null' for null constant", 0); + return null; + } + return reader.readString(); + } + + @SuppressWarnings("unchecked") + public static ArrayList deserializeCollection(final JsonReader reader) throws IOException { + return reader.deserializeCollection(READER); + } + + public static void deserializeCollection(final JsonReader reader, final Collection res) throws IOException { + reader.deserializeCollection(READER, res); + } + + @SuppressWarnings("unchecked") + public static ArrayList deserializeNullableCollection(final JsonReader reader) throws IOException { + return reader.deserializeNullableCollection(READER); + } + + public static void deserializeNullableCollection(final JsonReader reader, final Collection res) throws IOException { + reader.deserializeNullableCollection(READER, res); + } + + public static void serialize(final List list, final JsonWriter writer) { + writer.writeByte(JsonWriter.ARRAY_START); + if (list.size() != 0) { + writer.writeString(list.get(0)); + for (int i = 1; i < list.size(); i++) { + writer.writeByte(JsonWriter.COMMA); + writer.writeString(list.get(i)); + } + } + writer.writeByte(JsonWriter.ARRAY_END); + } +} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/TypeLookup.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/TypeLookup.java new file mode 100644 index 0000000000..f8d9f4b7fd --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/TypeLookup.java @@ -0,0 +1,10 @@ +package com.bugsnag.android.repackaged.dslplatform.json; + +import androidx.annotation.Nullable; + +interface TypeLookup { + @Nullable + JsonReader.ReadObject tryFindReader(Class manifest); + @Nullable + JsonReader.BindObject tryFindBinder(Class manifest); +} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/UUIDConverter.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/UUIDConverter.java new file mode 100644 index 0000000000..92ef69e925 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/UUIDConverter.java @@ -0,0 +1,198 @@ +package com.bugsnag.android.repackaged.dslplatform.json; + +import androidx.annotation.Nullable; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.UUID; + +@SuppressWarnings({"rawtypes", "unchecked"}) // suppress pre-existing warnings +public abstract class UUIDConverter { + + public static final UUID MIN_UUID = new java.util.UUID(0L, 0L); + public static final JsonReader.ReadObject READER = new JsonReader.ReadObject() { + @Nullable + @Override + public UUID read(JsonReader reader) throws IOException { + return reader.wasNull() ? null : deserialize(reader); + } + }; + public static final JsonWriter.WriteObject WRITER = new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable UUID value) { + serializeNullable(value, writer); + } + }; + + private static final char[] Lookup; + private static final byte[] Values; + + static { + Lookup = new char[256]; + Values = new byte['f' + 1 - '0']; + for (int i = 0; i < 256; i++) { + int hi = (i >> 4) & 15; + int lo = i & 15; + Lookup[i] = (char) (((hi < 10 ? '0' + hi : 'a' + hi - 10) << 8) + (lo < 10 ? '0' + lo : 'a' + lo - 10)); + } + for (char c = '0'; c <= '9'; c++) { + Values[c - '0'] = (byte) (c - '0'); + } + for (char c = 'a'; c <= 'f'; c++) { + Values[c - '0'] = (byte) (c - 'a' + 10); + } + for (char c = 'A'; c <= 'F'; c++) { + Values[c - '0'] = (byte) (c - 'A' + 10); + } + } + + + public static void serializeNullable(@Nullable final UUID value, final JsonWriter sw) { + if (value == null) { + sw.writeNull(); + } else { + serialize(value, sw); + } + } + + public static void serialize(final UUID value, final JsonWriter sw) { + serialize(value.getMostSignificantBits(), value.getLeastSignificantBits(), sw); + } + + public static void serialize(final long hi, final long lo, final JsonWriter sw) { + final int hi1 = (int) (hi >> 32); + final int hi2 = (int) hi; + final int lo1 = (int) (lo >> 32); + final int lo2 = (int) lo; + final byte[] buf = sw.ensureCapacity(38); + final int pos = sw.size(); + buf[pos] = '"'; + int v = (hi1 >> 24) & 255; + int l = Lookup[v]; + buf[pos + 1] = (byte) (l >> 8); + buf[pos + 2] = (byte) l; + v = (hi1 >> 16) & 255; + l = Lookup[v]; + buf[pos + 3] = (byte) (l >> 8); + buf[pos + 4] = (byte) l; + v = (hi1 >> 8) & 255; + l = Lookup[v]; + buf[pos + 5] = (byte) (l >> 8); + buf[pos + 6] = (byte) l; + v = hi1 & 255; + l = Lookup[v]; + buf[pos + 7] = (byte) (l >> 8); + buf[pos + 8] = (byte) l; + buf[pos + 9] = '-'; + v = (hi2 >> 24) & 255; + l = Lookup[v]; + buf[pos + 10] = (byte) (l >> 8); + buf[pos + 11] = (byte) l; + v = (hi2 >> 16) & 255; + l = Lookup[v]; + buf[pos + 12] = (byte) (l >> 8); + buf[pos + 13] = (byte) l; + buf[pos + 14] = '-'; + v = (hi2 >> 8) & 255; + l = Lookup[v]; + buf[pos + 15] = (byte) (l >> 8); + buf[pos + 16] = (byte) l; + v = hi2 & 255; + l = Lookup[v]; + buf[pos + 17] = (byte) (l >> 8); + buf[pos + 18] = (byte) l; + buf[pos + 19] = '-'; + v = (lo1 >> 24) & 255; + l = Lookup[v]; + buf[pos + 20] = (byte) (l >> 8); + buf[pos + 21] = (byte) l; + v = (lo1 >> 16) & 255; + l = Lookup[v]; + buf[pos + 22] = (byte) (l >> 8); + buf[pos + 23] = (byte) l; + buf[pos + 24] = '-'; + v = (lo1 >> 8) & 255; + l = Lookup[v]; + buf[pos + 25] = (byte) (l >> 8); + buf[pos + 26] = (byte) l; + v = lo1 & 255; + l = Lookup[v]; + buf[pos + 27] = (byte) (l >> 8); + buf[pos + 28] = (byte) l; + v = (lo2 >> 24) & 255; + l = Lookup[v]; + buf[pos + 29] = (byte) (l >> 8); + buf[pos + 30] = (byte) l; + v = (lo2 >> 16) & 255; + l = Lookup[v]; + buf[pos + 31] = (byte) (l >> 8); + buf[pos + 32] = (byte) l; + v = (lo2 >> 8) & 255; + l = Lookup[v]; + buf[pos + 33] = (byte) (l >> 8); + buf[pos + 34] = (byte) l; + v = lo2 & 255; + l = Lookup[v]; + buf[pos + 35] = (byte) (l >> 8); + buf[pos + 36] = (byte) l; + buf[pos + 37] = '"'; + sw.advance(38); + } + + public static UUID deserialize(final JsonReader reader) throws IOException { + final char[] buf = reader.readSimpleQuote(); + final int len = reader.getCurrentIndex() - reader.getTokenStart(); + if (len == 37 && buf[8] == '-' && buf[13] == '-' && buf[18] == '-' && buf[23] == '-') { + try { + long hi = 0; + for (int i = 0; i < 8; i++) + hi = (hi << 4) + Values[buf[i] - '0']; + for (int i = 9; i < 13; i++) + hi = (hi << 4) + Values[buf[i] - '0']; + for (int i = 14; i < 18; i++) + hi = (hi << 4) + Values[buf[i] - '0']; + long lo = 0; + for (int i = 19; i < 23; i++) + lo = (lo << 4) + Values[buf[i] - '0']; + for (int i = 24; i < 36; i++) + lo = (lo << 4) + Values[buf[i] - '0']; + return new UUID(hi, lo); + } catch (ArrayIndexOutOfBoundsException ex) { + return UUID.fromString(new String(buf, 0, 36)); + } + } else if (len == 33) { + try { + long hi = 0; + for (int i = 0; i < 16; i++) + hi = (hi << 4) + Values[buf[i] - '0']; + long lo = 0; + for (int i = 16; i < 32; i++) + lo = (lo << 4) + Values[buf[i] - '0']; + return new UUID(hi, lo); + } catch (ArrayIndexOutOfBoundsException ex) { + return UUID.fromString(new String(buf, 0, 32)); + } + } else { + return UUID.fromString(new String(buf, 0, len - 1)); + } + } + + @SuppressWarnings("unchecked") + public static ArrayList deserializeCollection(final JsonReader reader) throws IOException { + return reader.deserializeCollection(READER); + } + + public static void deserializeCollection(final JsonReader reader, final Collection res) throws IOException { + reader.deserializeCollection(READER, res); + } + + @SuppressWarnings("unchecked") + public static ArrayList deserializeNullableCollection(final JsonReader reader) throws IOException { + return reader.deserializeNullableCollection(READER); + } + + public static void deserializeNullableCollection(final JsonReader reader, final Collection res) throws IOException { + reader.deserializeNullableCollection(READER, res); + } +} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/UnknownSerializer.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/UnknownSerializer.java new file mode 100644 index 0000000000..405d9f8eb9 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/UnknownSerializer.java @@ -0,0 +1,9 @@ +package com.bugsnag.android.repackaged.dslplatform.json; + +import androidx.annotation.Nullable; + +import java.io.IOException; + +interface UnknownSerializer { + void serialize(JsonWriter writer, @Nullable Object unknown) throws IOException; +} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/XmlConverter.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/XmlConverter.java new file mode 100644 index 0000000000..ddd88e99af --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/XmlConverter.java @@ -0,0 +1,217 @@ +package com.bugsnag.android.repackaged.dslplatform.json; + +import androidx.annotation.Nullable; + +import org.w3c.dom.*; +import org.w3c.dom.ls.DOMImplementationLS; +import org.w3c.dom.ls.LSOutput; +import org.w3c.dom.ls.LSSerializer; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.*; + +@SuppressWarnings({"rawtypes", "unchecked"}) // suppress pre-existing warnings +public abstract class XmlConverter { + + static final JsonReader.ReadObject Reader = new JsonReader.ReadObject() { + @Nullable + @Override + public Element read(JsonReader reader) throws IOException { + return reader.wasNull() ? null : deserialize(reader); + } + }; + static final JsonWriter.WriteObject Writer = new JsonWriter.WriteObject() { + @Override + public void write(JsonWriter writer, @Nullable Element value) { + serializeNullable(value, writer); + } + }; + + private static final DocumentBuilder documentBuilder; + + static { + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + try { + documentBuilder = dbFactory.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + throw new RuntimeException(e); + } + } + + public static void serializeNullable(@Nullable final Element value, final JsonWriter sw) { + if (value == null) + sw.writeNull(); + else + serialize(value, sw); + } + + public static void serialize(final Element value, final JsonWriter sw) { + Document document = value.getOwnerDocument(); + DOMImplementationLS domImplLS = (DOMImplementationLS) document.getImplementation(); + LSSerializer serializer = domImplLS.createLSSerializer(); + LSOutput lsOutput = domImplLS.createLSOutput(); + lsOutput.setEncoding("UTF-8"); + StringWriter writer = new StringWriter(); + lsOutput.setCharacterStream(writer); + serializer.write(document, lsOutput); + StringConverter.serialize(writer.toString(), sw); + } + + public static Element deserialize(final JsonReader reader) throws IOException { + if (reader.last() == '"') { + try { + InputSource source = new InputSource(new StringReader(reader.readString())); + return documentBuilder.parse(source).getDocumentElement(); + } catch (SAXException ex) { + throw reader.newParseErrorAt("Invalid XML value", 0, ex); + } + } else { + final Map map = ObjectConverter.deserializeMap(reader); + return mapToXml(map); + } + } + + public static Element mapToXml(final Map map) throws IOException { + final Set xmlRootElementNames = map.keySet(); + if (xmlRootElementNames.size() > 1) { + throw ParsingException.create("Invalid XML. Expecting root element", true); + } + final String rootName = xmlRootElementNames.iterator().next(); + final Document document = createDocument(); + final Element rootElement = document.createElement(rootName); + document.appendChild(rootElement); + buildXmlFromHashMap(document, rootElement, map.get(rootName)); + return rootElement; + } + + private static synchronized Document createDocument() { + try { + final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + final DocumentBuilder builder = factory.newDocumentBuilder(); + return builder.newDocument(); + } catch (ParserConfigurationException e) { + throw new ConfigurationException(e); + } + } + + private static final String TEXT_NODE_TAG = "#text"; + private static final String COMMENT_NODE_TAG = "#comment"; + private static final String CDATA_NODE_TAG = "#cdata-section"; + + @SuppressWarnings("unchecked") + private static void buildXmlFromHashMap( + final Document doc, + final Element subtreeRootElement, + @Nullable final Object elementContent) { + if (elementContent instanceof HashMap) { + final HashMap elementContentMap = (HashMap) elementContent; + for (final Map.Entry childEntry : elementContentMap.entrySet()) { + final String key = childEntry.getKey(); + if (key.startsWith("@")) { + subtreeRootElement.setAttribute(key.substring(1), childEntry.getValue().toString()); + } else if (key.startsWith("#")) { + if (key.equals(TEXT_NODE_TAG)) { + if (childEntry.getValue() instanceof List) { + buildTextNodeList(doc, subtreeRootElement, (List) childEntry.getValue()); + } else { + final Node textNode = doc.createTextNode(childEntry.getValue().toString()); + subtreeRootElement.appendChild(textNode); + } + } else if (key.equals(CDATA_NODE_TAG)) { + if (childEntry.getValue() instanceof List) { + buildCDataList(doc, subtreeRootElement, (List) childEntry.getValue()); + } else { + final Node cDataNode = doc.createCDATASection(childEntry.getValue().toString()); + subtreeRootElement.appendChild(cDataNode); + } + } else if (key.equals(COMMENT_NODE_TAG)) { + if (childEntry.getValue() instanceof List) { + buildCommentList(doc, subtreeRootElement, (List) childEntry.getValue()); + } else { + final Node commentNode = doc.createComment(childEntry.getValue().toString()); + subtreeRootElement.appendChild(commentNode); + } + } //else if (key.equals(WHITESPACE_NODE_TAG) + // || key.equals(SIGNIFICANT_WHITESPACE_NODE_TAG)) { + // Ignore + //} else { + /* + * All other nodes whose name starts with a '#' are invalid XML + * nodes, and thus ignored: + */ + //} + } else { + final Element newElement = doc.createElement(key); + subtreeRootElement.appendChild(newElement); + buildXmlFromHashMap(doc, newElement, childEntry.getValue()); + } + } + } else if (elementContent instanceof List) { + buildXmlFromJsonArray(doc, subtreeRootElement, (List) elementContent); + } else { + if (elementContent != null) { + subtreeRootElement.setTextContent(elementContent.toString()); + } + } + } + + private static void buildTextNodeList(final Document doc, final Node subtreeRoot, final List nodeValues) { + final StringBuilder sb = new StringBuilder(); + for (final String nodeValue : nodeValues) { + sb.append(nodeValue); + } + subtreeRoot.appendChild(doc.createTextNode(sb.toString())); + } + + private static void buildCDataList(final Document doc, final Node subtreeRoot, final List nodeValues) { + for (final String nodeValue : nodeValues) { + subtreeRoot.appendChild(doc.createCDATASection(nodeValue)); + } + } + + private static void buildCommentList(final Document doc, final Node subtreeRoot, final List nodeValues) { + for (final String nodeValue : nodeValues) { + subtreeRoot.appendChild(doc.createComment(nodeValue)); + } + } + + private static void buildXmlFromJsonArray( + final Document doc, + final Node listHeadNode, + final List elementContentList) { + final Node subtreeRootNode = listHeadNode.getParentNode(); + /* The head node (already exists) */ + buildXmlFromHashMap(doc, (Element) listHeadNode, elementContentList.get(0)); + /* The rest of the list */ + for (final Object elementContent : elementContentList.subList(1, elementContentList.size())) { + final Element newElement = doc.createElement(listHeadNode.getNodeName()); + subtreeRootNode.appendChild(newElement); + buildXmlFromHashMap(doc, newElement, elementContent); + } + } + + @SuppressWarnings("unchecked") + public static ArrayList deserializeCollection(final JsonReader reader) throws IOException { + return reader.deserializeCollection(Reader); + } + + public static void deserializeCollection(final JsonReader reader, final Collection res) throws IOException { + reader.deserializeCollection(Reader, res); + } + + @SuppressWarnings("unchecked") + public static ArrayList deserializeNullableCollection(final JsonReader reader) throws IOException { + return reader.deserializeNullableCollection(Reader); + } + + public static void deserializeNullableCollection(final JsonReader reader, final Collection res) throws IOException { + reader.deserializeNullableCollection(Reader, res); + } +} diff --git a/patches/Bugsnag.patch b/patches/Bugsnag.patch index 25c19fd4c7..c762d488a1 100644 --- a/patches/Bugsnag.patch +++ b/patches/Bugsnag.patch @@ -1,17 +1,8 @@ -From 3270faf44aea11754c940ba43ee6db72b7462f14 Mon Sep 17 00:00:00 2001 -From: M66B -Date: Sat, 15 May 2021 22:07:24 +0200 -Subject: [PATCH] Bugsnag failure on I/O error - ---- - app/src/main/java/com/bugsnag/android/DefaultDelivery.kt | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - diff --git a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt -index a7995164cb4e..5620f0bacd80 100644 +index 0ce2eec8c..e1bac196e 100644 --- a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt +++ b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt -@@ -64,7 +64,7 @@ internal class DefaultDelivery( +@@ -66,7 +66,7 @@ internal class DefaultDelivery( return DeliveryStatus.UNDELIVERED } catch (exception: IOException) { logger.w("IOException encountered in request", exception) @@ -20,3 +11,30 @@ index a7995164cb4e..5620f0bacd80 100644 } catch (exception: Exception) { logger.w("Unexpected error delivering payload", exception) return DeliveryStatus.FAILURE +diff --git a/patches/Bugsnag.patch b/patches/Bugsnag.patch +index 25c19fd4c..e69de29bb 100644 +--- a/patches/Bugsnag.patch ++++ b/patches/Bugsnag.patch +@@ -1,22 +0,0 @@ +-From 3270faf44aea11754c940ba43ee6db72b7462f14 Mon Sep 17 00:00:00 2001 +-From: M66B +-Date: Sat, 15 May 2021 22:07:24 +0200 +-Subject: [PATCH] Bugsnag failure on I/O error +- +---- +- app/src/main/java/com/bugsnag/android/DefaultDelivery.kt | 2 +- +- 1 file changed, 1 insertion(+), 1 deletion(-) +- +-diff --git a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt +-index a7995164cb4e..5620f0bacd80 100644 +---- a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt +-+++ b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt +-@@ -64,7 +64,7 @@ internal class DefaultDelivery( +- return DeliveryStatus.UNDELIVERED +- } catch (exception: IOException) { +- logger.w("IOException encountered in request", exception) +-- return DeliveryStatus.UNDELIVERED +-+ return DeliveryStatus.FAILURE +- } catch (exception: Exception) { +- logger.w("Unexpected error delivering payload", exception) +- return DeliveryStatus.FAILURE