blob: 48f2aa22e1c5cfd34e6cbfe4b1e52a18a5dc20ef [file] [log] [blame]
/*
* Copyright (C) 2022 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 android.platform.spectatio.configs.validators;
import android.platform.spectatio.configs.UiElement;
import android.platform.spectatio.constants.JsonConfigConstants;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* {@link ValidateUiElement} is a deserializer that validates Ui Elements in Spectatio JSON Config
* while deserializing it to a Java Object.
*
* <p>It checks for - 1. If TYPE is valid ( TEXT, TEXT_CONTAINS, DESCRIPTION, RESOURCE_ID, CLASS )
* 2. If values are correctly provided ( PACKAGE is required if TYPE is RESOURCE_ID )
*/
public class ValidateUiElement implements JsonDeserializer<UiElement> {
private Set<String> mSupportedTypes =
Set.of(
JsonConfigConstants.CLICKABLE,
JsonConfigConstants.SCROLLABLE,
JsonConfigConstants.TEXT,
JsonConfigConstants.TEXT_CONTAINS,
JsonConfigConstants.DESCRIPTION,
JsonConfigConstants.CLASS,
JsonConfigConstants.HAS_ANCESTOR,
JsonConfigConstants.HAS_DESCENDANT,
JsonConfigConstants.MULTIPLE,
JsonConfigConstants.RESOURCE_ID);
private Set<String> mSupportedProperties =
Set.of(
JsonConfigConstants.TYPE,
JsonConfigConstants.VALUE,
JsonConfigConstants.PACKAGE,
JsonConfigConstants.FLAG,
JsonConfigConstants.MAX_DEPTH,
JsonConfigConstants.ANCESTOR,
JsonConfigConstants.DESCENDANT,
JsonConfigConstants.SPECIFIERS);
@Override
public UiElement deserialize(
JsonElement jsonElement, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
JsonObject jsonObject = jsonElement.getAsJsonObject();
validateProperties(jsonObject);
String type =
validateAndGetValue(JsonConfigConstants.TYPE, jsonObject, /*isOptional*/ false);
validateType(type);
if (JsonConfigConstants.CLICKABLE.equals(type)
|| JsonConfigConstants.SCROLLABLE.equals(type)) {
boolean flag = validateAndGetBoolean(JsonConfigConstants.FLAG, jsonObject, true);
return new UiElement(type, flag);
}
if (JsonConfigConstants.MULTIPLE.equals(type)) {
JsonArray specifiersJson =
validateAndGetArray(JsonConfigConstants.SPECIFIERS, jsonObject);
List<UiElement> specifiers =
specifiersJson.asList().stream()
.map(element -> context.<UiElement>deserialize(element, typeOfT))
.collect(Collectors.toList());
int ancestorSpecifiers = 0;
for (UiElement specifier : specifiers) {
if (JsonConfigConstants.MULTIPLE.equals(specifier.getType())) {
throw new RuntimeException(
"Multiple-specifier can't contain a multiple-specifier.");
}
if (JsonConfigConstants.HAS_ANCESTOR.equals(specifier.getType())) {
ancestorSpecifiers++;
if (ancestorSpecifiers > 1) {
throw new RuntimeException(
"Multiple-specifier can't contain more than one ancestor "
+ "specifier.");
}
}
}
return new UiElement(specifiers);
}
if (JsonConfigConstants.HAS_ANCESTOR.equals(type)) {
JsonObject parent = validateAndGetObject(JsonConfigConstants.ANCESTOR, jsonObject);
int maxDepth = validateAndGetInteger(JsonConfigConstants.MAX_DEPTH, jsonObject, 1);
return new UiElement(
JsonConfigConstants.HAS_ANCESTOR,
context.deserialize(parent, typeOfT),
maxDepth);
}
if (JsonConfigConstants.HAS_DESCENDANT.equals(type)) {
JsonObject childJson = validateAndGetObject(JsonConfigConstants.DESCENDANT, jsonObject);
int maxDepth = validateAndGetInteger(JsonConfigConstants.MAX_DEPTH, jsonObject, 1);
return new UiElement(
JsonConfigConstants.HAS_DESCENDANT,
context.deserialize(childJson, typeOfT),
maxDepth);
}
String value =
validateAndGetValue(JsonConfigConstants.VALUE, jsonObject, /*isOptional*/ false);
// Package is not required for SCROLLABLE, CLICKABLE, TEXT, TEXT_CONTAINS and DESCRIPTION
// Package is optional for CLASS and RESOURCE_ID
String pkg =
validateAndGetValue(JsonConfigConstants.PACKAGE, jsonObject, /*isOptional*/ true);
// For RESOURCE_ID, unspecified package should be treated as the empty string
if (JsonConfigConstants.RESOURCE_ID.equals(type)) {
pkg = Objects.requireNonNullElse(pkg, "");
}
return new UiElement(type, value, pkg);
}
/**
* Validate and return the value of given property from Json Object
*
* @param key : Property name to get the value from Json Object
* @param jsonObject : Json Object to read the property
* @param isOptional : If the property is optional, it will be true else false.
* <p>If isOptional is false, then throw an exception if the property is missing If
* isOptional is true, then ignore if the property is missing
*/
private String validateAndGetValue(String key, JsonObject jsonObject, boolean isOptional) {
JsonElement value = jsonObject.get(key);
if (value != null
&& value.isJsonPrimitive()
&& value.getAsJsonPrimitive().isString()
&& !value.getAsString().trim().isEmpty()) {
return value.getAsString().trim();
}
if (!isOptional) {
throw new RuntimeException(
String.format(
"Non-optional string property %s for %s in Spectatio JSON Config "
+ "is either missing or not a valid string.",
key, jsonObject));
}
return null;
}
private boolean validateAndGetBoolean(String key, JsonObject jsonObject, Boolean defaultValue) {
JsonElement value = jsonObject.get(key);
if (value == null) {
if (defaultValue == null) {
throw new RuntimeException(
String.format(
"Non-optional boolean property %s for %s in Spectatio JSON Config "
+ "is missing.",
key, jsonObject));
}
return defaultValue;
}
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isBoolean()) {
throw new RuntimeException(
String.format(
"Non-optional boolean property %s for %s in Spectatio JSON Config "
+ "is not a valid boolean.",
key, jsonObject));
}
return value.getAsBoolean();
}
private int validateAndGetInteger(String key, JsonObject jsonObject, Integer defaultValue) {
JsonElement value = jsonObject.get(key);
if (value == null) {
if (defaultValue == null) {
throw new RuntimeException(
String.format(
"Non-optional integer property %s for %s in Spectatio JSON Config "
+ "is missing.",
key, jsonObject));
}
return defaultValue;
}
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isNumber()) {
throw new RuntimeException(
String.format(
"Non-optional integer property %s for %s in Spectatio JSON Config "
+ "is not a valid integer.",
key, jsonObject));
}
try {
return value.getAsInt();
} catch (NumberFormatException e) {
throw new RuntimeException(
String.format(
"Non-optional integer property %s for %s in Spectatio JSON Config "
+ "is not a valid integer.",
key, jsonObject));
}
}
private JsonObject validateAndGetObject(String key, JsonObject jsonObject) {
JsonElement value = jsonObject.get(key);
if (value != null && value.isJsonObject()) {
return value.getAsJsonObject();
}
throw new RuntimeException(
String.format(
"Non-optional object property %s for %s in Spectatio JSON Config "
+ "is either missing or not a valid JSON object.",
key, jsonObject));
}
private JsonArray validateAndGetArray(String key, JsonObject jsonObject) {
JsonElement value = jsonObject.get(key);
if (value != null && value.isJsonArray()) {
return value.getAsJsonArray();
}
throw new RuntimeException(
String.format(
"Non-optional array property %s for %s in Spectatio JSON Config "
+ "is either missing or not a valid JSON array.",
key, jsonObject));
}
private void validateType(String type) {
if (!mSupportedTypes.contains(type)) {
throw new RuntimeException(
String.format(
"UI Element TYPE %s in Spectatio JSON Config is invalid. Supported"
+ " Types: [ RESOURCE_ID, TEXT, TEXT_CONTAINS, DESCRIPTION, CLASS,"
+ " MULTIPLE, HAS_ANCESTOR, HAS_DESCENDANT ]",
type));
}
}
private void validateProperties(JsonObject jsonObject) {
List<String> unknownProperties =
jsonObject.entrySet().stream()
.map(Entry::getKey)
.map(String::trim)
.filter(key -> !mSupportedProperties.contains(key))
.collect(Collectors.toList());
if (!unknownProperties.isEmpty()) {
throw new RuntimeException(
String.format(
"Unknown properties: [ %s ] for %s in Spectatio JSON Config",
String.join(", ", unknownProperties), jsonObject));
}
}
}