ArrayCreatingInputMerger.kt

/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.work

import java.lang.IllegalArgumentException
import java.lang.reflect.Array
import java.util.HashMap

/**
 * An [InputMerger] that attempts to merge the inputs, creating arrays when necessary.  For
 * each input, we look at each key:
 *
 *  * If this is the first time we encountered the key:
 *      * If it's an array, put it in the output
 *      * If it's a primitive, turn it into a size 1 array and put it in the output
 *  * Else (we have encountered the key before):
 *      * If the value type matches the old value type:
 *         * If they are arrays, concatenate them
 *         * If they are primitives, turn them into a size 2 array
 *      * Else if one is an array and the other is a primitive of that type:
 *          * Make a longer array and concatenate them
 *      * Else throw an [IllegalArgumentException] because the types don't match.
 *
 *  If a value by a key is `null`, it is considered to have type `String`, because it is the only
 *  nullable typed allowed in [Data].
 */
class ArrayCreatingInputMerger : InputMerger() {
    @Suppress("DocumentExceptions")
    override fun merge(inputs: List<Data>): Data {
        val output = Data.Builder()
        // values are always arrays
        val mergedValues: MutableMap<String, Any> = HashMap()
        for (input in inputs) {
            for ((key, value) in input.keyValueMap) {
                val valueClass: Class<*> = value?.javaClass ?: String::class.java
                val existingValue = mergedValues[key]
                mergedValues[key] = if (existingValue == null) {
                    // First time encountering this key.
                    if (valueClass.isArray) {
                        // Arrays carry over as-is.
                        value
                    } else {
                        // Primitives get turned into size 1 arrays.
                        createArrayFor(value, valueClass)
                    }
                } else {
                    // We've encountered this key before.
                    val existingValueClass: Class<*> = existingValue.javaClass
                    when {
                        existingValueClass == valueClass -> {
                            // The classes match; we can merge.
                            concatenateArrays(existingValue, value)
                        }
                        existingValueClass.componentType == valueClass -> {
                            // We have an existing array of the same type.
                            concatenateArrayAndNonArray(existingValue, value, valueClass)
                        }
                        else -> throw IllegalArgumentException()
                    }
                }
            }
        }
        output.putAll(mergedValues)
        return output.build()
    }

    private fun concatenateArrays(array1: Any, array2: Any): Any {
        val length1 = Array.getLength(array1)
        val length2 = Array.getLength(array2)
        val newArray = Array.newInstance(
            array1.javaClass.componentType!!,
            length1 + length2
        )
        System.arraycopy(array1, 0, newArray, 0, length1)
        System.arraycopy(array2, 0, newArray, length1, length2)
        return newArray
    }

    private fun concatenateArrayAndNonArray(array: Any, obj: Any?, valueClass: Class<*>): Any {
        val arrayLength = Array.getLength(array)
        val newArray = Array.newInstance(valueClass, arrayLength + 1)
        System.arraycopy(array, 0, newArray, 0, arrayLength)
        Array.set(newArray, arrayLength, obj)
        return newArray
    }

    private fun createArrayFor(obj: Any?, valueClass: Class<*>): Any {
        val newArray = Array.newInstance(valueClass, 1)
        Array.set(newArray, 0, obj)
        return newArray
    }
}