com.android.manifmerger.AttributeModel Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of manifest-merger Show documentation
Show all versions of manifest-merger Show documentation
A Library to merge Android manifests.
/*
* Copyright (C) 2014 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 com.android.manifmerger;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.base.Joiner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Describes an attribute characteristics like if it supports smart package name replacement, has
* a default value and a validator for its values.
*/
class AttributeModel {
@NonNull private final XmlNode.NodeName mName;
private final boolean mIsPackageDependent;
@Nullable private final String mDefaultValue;
@Nullable private final Validator mOnReadValidator;
@Nullable private final Validator mOnWriteValidator;
@NonNull private final MergingPolicy mMergingPolicy;
/**
* Define a new attribute with specific characteristics.
*
* @param name name of the attribute, so far assumed to be in the
* {@link com.android.SdkConstants#ANDROID_URI} namespace.
* @param isPackageDependent true if the attribute support smart substitution of package name.
* @param defaultValue an optional default value.
* @param onReadValidator an optional validator to validate values against.
*/
private AttributeModel(@NonNull XmlNode.NodeName name,
boolean isPackageDependent,
@Nullable String defaultValue,
@Nullable Validator onReadValidator,
@Nullable Validator onWriteValidator,
@NonNull MergingPolicy mergingPolicy) {
mName = name;
mIsPackageDependent = isPackageDependent;
mDefaultValue = defaultValue;
mOnReadValidator = onReadValidator;
mOnWriteValidator = onWriteValidator;
mMergingPolicy = mergingPolicy;
}
@NonNull
XmlNode.NodeName getName() {
return mName;
}
/**
* Return true if the attribute support smart substitution of partially fully qualified
* class names with package settings as provided by the manifest node's package attribute
* {@link }
*
* @return true if this attribute supports smart substitution or false if not.
*/
boolean isPackageDependent() {
return mIsPackageDependent;
}
/**
* Returns the attribute's default value or null if none.
*/
@Nullable
String getDefaultValue() {
return mDefaultValue;
}
/**
* Returns the attribute's {@link com.android.manifmerger.AttributeModel.Validator} to
* validate its value when read from xml files or null if no validation is necessary.
*/
@Nullable
public Validator getOnReadValidator() {
return mOnReadValidator;
}
/**
* Returns the attribute's {@link com.android.manifmerger.AttributeModel.Validator} to
* validate its value when the merged file is about to be persisted.
*/
@Nullable
public Validator getOnWriteValidator() {
return mOnWriteValidator;
}
/**
* Returns the {@link com.android.manifmerger.AttributeModel.MergingPolicy} for this
* attribute.
*/
@NonNull
public MergingPolicy getMergingPolicy() {
return mMergingPolicy;
}
/**
* Creates a new {@link Builder} to describe an attribute.
* @param attributeName the to be described attribute name
*/
static Builder newModel(String attributeName) {
return new Builder(attributeName);
}
static class Builder {
private final String mName;
private boolean mIsPackageDependent = false;
private String mDefaultValue;
private Validator mOnReadValidator;
private Validator mOnWriteValidator;
private MergingPolicy mMergingPolicy = STRICT_MERGING_POLICY;
Builder(String name) {
this.mName = name;
}
/**
* Sets the attribute support for smart substitution of partially fully qualified
* class names with package settings as provided by the manifest node's package attribute
* {@link }
*/
Builder setIsPackageDependent() {
mIsPackageDependent = true;
return this;
}
/**
* Sets the attribute default value.
*/
Builder setDefaultValue(String value) {
mDefaultValue = value;
return this;
}
/**
* Sets a {@link com.android.manifmerger.AttributeModel.Validator} to validate the
* attribute's values coming from xml files.
*/
Builder setOnReadValidator(Validator validator) {
mOnReadValidator = validator;
return this;
}
/**
* Sets a {@link com.android.manifmerger.AttributeModel.Validator} to validate values
* before they are written to the final merged document.
*/
Builder setOnWriteValidator(Validator validator) {
mOnWriteValidator = validator;
return this;
}
Builder setMergingPolicy(MergingPolicy mergingPolicy) {
mMergingPolicy = mergingPolicy;
return this;
}
/**
* Build an immutable {@link com.android.manifmerger.AttributeModel}
*/
AttributeModel build() {
return new AttributeModel(
XmlNode.fromXmlName("android:" + mName),
mIsPackageDependent,
mDefaultValue,
mOnReadValidator,
mOnWriteValidator,
mMergingPolicy);
}
}
/**
* Defines a merging policy between two attribute values. Example of merging policies can be
* strict when it is illegal to try to merge or override a value by another. Another example
* is a OR merging policy on boolean attribute values.
*/
interface MergingPolicy {
/**
* Returns true if it should be attempted to merge this attribute value with
* the attribute default value when merging with a node that does not contain
* the attribute declaration.
*/
boolean shouldMergeDefaultValues();
/**
* Merges the two attributes values and returns the merged value. If the values cannot be
* merged, return null.
*/
@Nullable
String merge(@NonNull String higherPriority, @NonNull String lowerPriority);
}
/**
* Standard attribute value merging policy, generates an error unless both values are equal.
*/
static final MergingPolicy STRICT_MERGING_POLICY = new MergingPolicy() {
@Override
public boolean shouldMergeDefaultValues() {
return false;
}
@Nullable
@Override
public String merge(@NonNull String higherPriority, @NonNull String lowerPriority) {
// it's ok if the values are equal, otherwise it's not.
return higherPriority.equals(lowerPriority)
? higherPriority
: null;
}
};
/**
* Boolean OR merging policy.
*/
static final MergingPolicy OR_MERGING_POLICY = new MergingPolicy() {
@Override
public boolean shouldMergeDefaultValues() {
return true;
}
@Nullable
@Override
public String merge(@NonNull String higherPriority, @NonNull String lowerPriority) {
return Boolean.toString(BooleanValidator.isTrue(higherPriority) ||
BooleanValidator.isTrue(lowerPriority));
}
};
/**
* Merging policy that will return the higher priority value regardless of the lower priority
* value
*/
static final MergingPolicy NO_MERGING_POLICY = new MergingPolicy() {
@Override
public boolean shouldMergeDefaultValues() {
return true;
}
@Nullable
@Override
public String merge(@NonNull String higherPriority, @NonNull String lowerPriority) {
return higherPriority;
}
};
/**
* Decode a decimal or hexadecimal {@link String} into an {@link Integer}.
* String starting with 0 will be considered decimal, not octal.
*/
private static int decodeDecOrHexString(String s) {
long decodedValue = s.startsWith("0x") || s.startsWith("0X")
? Long.decode(s)
: Long.parseLong(s);
if (decodedValue < 0xFFFFFFFFL) {
return (int) decodedValue;
} else {
throw new IllegalArgumentException("Value " + s + " too big for 32 bits.");
}
}
/**
* Validates an attribute value.
*
* The validator can be called when xml documents are read to ensure the xml file contains
* valid statements.
*
* This is a poor-mans replacement for not having a proper XML Schema do perform such
* validations.
*/
interface Validator {
/**
* Validates a value, issuing a warning or error in case of failure through the passed
* merging report.
* @param mergingReport to report validation warnings or error
* @param attribute the attribute to validate.
* @param value the proposed or existing attribute value.
* @return true if the value is legal for this attribute.
*/
boolean validates(@NonNull MergingReport.Builder mergingReport,
@NonNull XmlAttribute attribute,
@NonNull String value);
}
/**
* Validates a boolean attribute type.
*/
static class BooleanValidator implements Validator {
// TODO: check with @xav where to find the acceptable values by runtime.
private static final Pattern TRUE_PATTERN = Pattern.compile("true|True|TRUE");
private static final Pattern FALSE_PATTERN = Pattern.compile("false|False|FALSE");
private static boolean isTrue(String value) {
return TRUE_PATTERN.matcher(value).matches();
}
@Override
public boolean validates(@NonNull MergingReport.Builder mergingReport,
@NonNull XmlAttribute attribute,
@NonNull String value) {
boolean matches = TRUE_PATTERN.matcher(value).matches() ||
FALSE_PATTERN.matcher(value).matches();
if (!matches) {
attribute.addMessage(mergingReport, MergingReport.Record.Severity.ERROR,
String.format(
"Attribute %1$s at %2$s has an illegal value=(%3$s), "
+ "expected 'true' or 'false'",
attribute.getId(),
attribute.printPosition(),
value
)
);
}
return matches;
}
}
/**
* A {@link com.android.manifmerger.AttributeModel.Validator} for verifying that a proposed
* value is part of the acceptable list of possible values.
*/
static class MultiValueValidator implements Validator {
private final String[] multiValues;
private final String allValues;
MultiValueValidator(String... multiValues) {
this.multiValues = multiValues;
allValues = Joiner.on(',').join(multiValues);
}
@Override
public boolean validates(@NonNull MergingReport.Builder mergingReport,
@NonNull XmlAttribute attribute, @NonNull String value) {
for (String multiValue : multiValues) {
if (multiValue.equals(value)) {
return true;
}
}
attribute.addMessage(mergingReport, MergingReport.Record.Severity.ERROR,
String.format(
"Invalid value for attribute %1$s at %2$s, value=(%3$s), "
+ "acceptable values are (%4$s)",
attribute.getId(),
attribute.printPosition(),
value,
allValues
)
);
return false;
}
}
/**
* A {@link com.android.manifmerger.AttributeModel.Validator} for verifying that a proposed
* value is a numerical integer value.
*/
static class IntegerValueValidator implements Validator {
@Override
public boolean validates(@NonNull MergingReport.Builder mergingReport,
@NonNull XmlAttribute attribute, @NonNull String value) {
try {
return Integer.parseInt(value) > 0;
} catch (NumberFormatException e) {
attribute.addMessage(mergingReport, MergingReport.Record.Severity.ERROR,
String.format(
"Attribute %1$s at %2$s must be an integer, found %3$s",
attribute.getId(),
attribute.printPosition(),
value)
);
return false;
}
}
}
/**
* A {@link com.android.manifmerger.AttributeModel.Validator} to validate that a string is
* a valid 32 bits hexadecimal representation.
*/
static class Hexadecimal32Bits implements Validator {
protected static final Pattern PATTERN = Pattern.compile("0[xX]([0-9a-fA-F]+)");
@Override
public boolean validates(@NonNull MergingReport.Builder mergingReport,
@NonNull XmlAttribute attribute, @NonNull String value) {
Matcher matcher = PATTERN.matcher(value);
boolean valid = matcher.matches() && matcher.group(1).length() <= 8;
if (!valid) {
attribute.addMessage(mergingReport, MergingReport.Record.Severity.ERROR,
String.format(
"Attribute %1$s at %2$s is not a valid hexadecimal 32 bit value,"
+ " found %3$s",
attribute.getId(),
attribute.printPosition(),
value
));
}
return valid;
}
}
/**
* A {@link com.android.manifmerger.AttributeModel.Validator} to validate that a string is
* a valid 32 positive hexadecimal representation with a minimum value requirement.
*/
static class Hexadecimal32BitsWithMinimumValue extends Hexadecimal32Bits {
private final int mMinimumValue;
Hexadecimal32BitsWithMinimumValue(int minimumValue) {
mMinimumValue = minimumValue;
}
@Override
public boolean validates(@NonNull MergingReport.Builder mergingReport,
@NonNull XmlAttribute attribute, @NonNull String value) {
boolean valid = super.validates(mergingReport, attribute, value);
if (valid) {
try {
Long decodedValue = Long.decode(value);
valid = decodedValue >= mMinimumValue && decodedValue < 0xFFFFFFFFL;
} catch(NumberFormatException e) {
valid = false;
}
if (!valid) {
attribute.addMessage(mergingReport, MergingReport.Record.Severity.ERROR,
String.format(
"Attribute %1$s at %2$s is not a valid hexadecimal value,"
+ " minimum is 0x%3$08X, maximum is 0x%4$08X, found %5$s",
attribute.getId(),
attribute.printPosition(),
mMinimumValue,
Integer.MAX_VALUE,
value
));
}
return valid;
}
return false;
}
}
}