1 /*
<lambda>null2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.statementservice.network.retriever
18 
19 import android.util.JsonReader
20 import com.android.statementservice.retriever.AbstractAsset
21 import com.android.statementservice.retriever.AssetFactory
22 import com.android.statementservice.retriever.JsonParser
23 import com.android.statementservice.retriever.Relation
24 import com.android.statementservice.retriever.Statement
25 import com.android.statementservice.utils.Result
26 import com.android.statementservice.utils.StatementUtils
27 import java.io.StringReader
28 import java.util.ArrayList
29 import com.android.statementservice.retriever.WebAsset
30 import com.android.statementservice.retriever.AndroidAppAsset
31 import com.android.statementservice.retriever.DynamicAppLinkComponent
32 import org.json.JSONObject
33 
34 /**
35  * Parses JSON from the Digital Asset Links specification. For examples, see [WebAsset],
36  * [AndroidAppAsset], and [Statement].
37  */
38 object StatementParser {
39 
40     private const val FIELD_NOT_STRING_FORMAT_STRING = "Expected %s to be string."
41     private const val FIELD_NOT_ARRAY_FORMAT_STRING = "Expected %s to be array."
42     private const val COMMENTS_NAME = "comments"
43     private const val EXCLUDE_NAME = "exclude"
44     private const val FRAGMENT_NAME = "#"
45     private const val QUERY_NAME = "?"
46     private const val PATH_NAME = "/"
47 
48     /**
49      * Parses a JSON array of statements.
50      */
51     fun parseStatementList(statementList: String, source: AbstractAsset): Result<ParsedStatement> {
52         val statements: MutableList<Statement> = ArrayList()
53         val delegates: MutableList<String> = ArrayList()
54         StringReader(statementList).use { stringReader ->
55             JsonReader(stringReader).use { reader ->
56                 reader.isLenient = false
57                 reader.beginArray()
58                 while (reader.hasNext()) {
59                     val result = parseOneStatement(reader, source)
60                     if (result is Result.Failure) {
61                         continue
62                     }
63                     result as Result.Success
64                     statements.addAll(result.value.statements)
65                     delegates.addAll(result.value.delegates)
66                 }
67                 reader.endArray()
68             }
69         }
70         return Result.Success(ParsedStatement(statements, delegates))
71     }
72 
73     /**
74      * Parses a single JSON statement.
75      */
76     fun parseStatement(statementString: String, source: AbstractAsset) =
77         StringReader(statementString).use { stringReader ->
78             JsonReader(stringReader).use { reader ->
79                 reader.isLenient = false
80                 parseOneStatement(reader, source)
81             }
82         }
83 
84     /**
85      * Parses a single JSON statement. This method guarantees that exactly one JSON object
86      * will be consumed.
87      */
88     private fun parseOneStatement(
89         reader: JsonReader,
90         source: AbstractAsset
91     ): Result<ParsedStatement> {
92         val statement = JsonParser.parse(reader)
93         val delegate = statement.optString(StatementUtils.DELEGATE_FIELD_DELEGATE)
94         if (!delegate.isNullOrEmpty()) {
95             return Result.Success(ParsedStatement(emptyList(), listOfNotNull(delegate)))
96         }
97 
98         val targetObject = statement.optJSONObject(StatementUtils.ASSET_DESCRIPTOR_FIELD_TARGET)
99             ?: return Result.Failure(
100                 FIELD_NOT_STRING_FORMAT_STRING.format(StatementUtils.ASSET_DESCRIPTOR_FIELD_TARGET)
101             )
102         val relations = statement.optJSONArray(StatementUtils.ASSET_DESCRIPTOR_FIELD_RELATION)
103             ?: return Result.Failure(
104                 FIELD_NOT_ARRAY_FORMAT_STRING.format(StatementUtils.ASSET_DESCRIPTOR_FIELD_RELATION)
105             )
106         val target = AssetFactory.create(targetObject)
107         val dynamicAppLinkComponents = parseDynamicAppLinkComponents(statement)
108 
109         val statements = (0 until relations.length())
110             .map { relations.getString(it) }
111             .map(Relation::create)
112             .map { Statement.create(source, target, it, dynamicAppLinkComponents) }
113         return Result.Success(ParsedStatement(statements, listOfNotNull(delegate)))
114     }
115 
116     private fun parseDynamicAppLinkComponents(
117         statement: JSONObject?
118     ): List<DynamicAppLinkComponent> {
119         val relationExtensions = statement?.optJSONObject(
120             StatementUtils.ASSET_DESCRIPTOR_FIELD_RELATION_EXTENSIONS
121         ) ?: return emptyList()
122         val handleAllUrlsRelationExtension = relationExtensions.optJSONObject(
123             StatementUtils.RELATION.toString()
124         ) ?: return emptyList()
125         val components = handleAllUrlsRelationExtension.optJSONArray(
126             StatementUtils.RELATION_EXTENSION_FIELD_DAL_COMPONENTS
127         ) ?: return emptyList()
128 
129         return (0 until components.length())
130             .map { components.getJSONObject(it) }
131             .map { parseComponent(it) }
132     }
133 
134     private fun parseComponent(component: JSONObject): DynamicAppLinkComponent {
135         val query = component.optJSONObject(QUERY_NAME)
136         return DynamicAppLinkComponent.create(
137             component.optBoolean(EXCLUDE_NAME, false),
138             if (component.has(FRAGMENT_NAME)) component.getString(FRAGMENT_NAME) else null,
139             if (component.has(PATH_NAME)) component.getString(PATH_NAME) else null,
140             query?.keys()?.asSequence()?.associateWith { query.getString(it) },
141             component.optString(COMMENTS_NAME)
142         )
143     }
144 
145     data class ParsedStatement(val statements: List<Statement>, val delegates: List<String>)
146 }
147