blob: 7e41e841ce286cd75e4eb42e7e08931488da1a32 [file] [log] [blame]
/*
* 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 static com.android.manifmerger.MergingReport.Record.Severity.ERROR;
import static com.android.manifmerger.MergingReport.Record.Severity.WARNING;
import static com.android.manifmerger.XmlNode.NodeKey;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.xml.AndroidManifest;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import org.w3c.dom.Attr;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Validates a loaded {@link XmlDocument} and check for potential inconsistencies in the model due
* to user error or omission.
*
* This is implemented as a separate class so it can be invoked by tools independently from the
* merging process.
*
* This validator will check the state of the loaded xml document before any merging activity is
* attempted. It verifies things like a "tools:replace="foo" attribute has a "android:foo"
* attribute also declared on the same element (since we want to replace its value).
*/
public class PreValidator {
private PreValidator(){
}
/**
* Validates a loaded {@link com.android.manifmerger.XmlDocument} and return a status of the
* merging model.
*
* Will return one the following status :
* <ul>
* <li>{@link com.android.manifmerger.MergingReport.Result#SUCCESS} : the merging model is
* correct, merging should be attempted</li>
* <li>{@link com.android.manifmerger.MergingReport.Result#WARNING} : the merging model
* contains non fatal error, user should be notified, merging can be attempted</li>
* <li>{@link com.android.manifmerger.MergingReport.Result#ERROR} : the merging model
* contains errors, user must be notified, merging should not be attempted</li>
* </ul>
*
* A successful validation does not mean that the merging will be successful, it only means
* that the {@link com.android.SdkConstants#TOOLS_URI} instructions are correct and consistent.
*
* @param mergingReport report to log warnings and errors.
* @param xmlDocument the loaded xml part.
* @return one the {@link com.android.manifmerger.MergingReport.Result} value.
*/
@NonNull
public static MergingReport.Result validate(
@NonNull MergingReport.Builder mergingReport,
@NonNull XmlDocument xmlDocument) {
validateManifestAttribute(
mergingReport, xmlDocument.getRootNode(), xmlDocument.getFileType());
return validate(mergingReport, xmlDocument.getRootNode());
}
private static MergingReport.Result validate(MergingReport.Builder mergingReport,
XmlElement xmlElement) {
validateAttributeInstructions(mergingReport, xmlElement);
validateAndroidAttributes(mergingReport, xmlElement);
checkSelectorPresence(mergingReport, xmlElement);
// create a temporary hash map of children indexed by key to ensure key uniqueness.
Map<NodeKey, XmlElement> childrenKeys = new HashMap<NodeKey, XmlElement>();
for (XmlElement childElement : xmlElement.getMergeableElements()) {
// if this element is tagged with 'tools:node=removeAll', ensure it has no other
// attributes.
if (childElement.getOperationType() == NodeOperationType.REMOVE_ALL) {
validateRemoveAllOperation(mergingReport, childElement);
} else {
if (checkKeyPresence(mergingReport, childElement)) {
XmlElement twin = childrenKeys.get(childElement.getId());
if (twin != null && !childElement.getType().areMultipleDeclarationAllowed()) {
// we have 2 elements with the same identity, if they are equals,
// issue a warning, if not, issue an error.
String message = String.format(
"Element %1$s at %2$s duplicated with element declared at %3$s",
childElement.getId(),
childElement.printPosition(),
childrenKeys.get(childElement.getId()).printPosition());
if (twin.compareTo(childElement).isPresent()) {
childElement.addMessage(mergingReport, ERROR, message);
} else {
childElement.addMessage(mergingReport, WARNING, message);
}
}
childrenKeys.put(childElement.getId(), childElement);
}
validate(mergingReport, childElement);
}
}
return mergingReport.hasErrors()
? MergingReport.Result.ERROR : MergingReport.Result.SUCCESS;
}
/**
* Validate an xml declaration with 'tools:node="removeAll" annotation. There should not
* be any other attribute declaration on this element.
*/
private static void validateRemoveAllOperation(MergingReport.Builder mergingReport,
XmlElement element) {
NamedNodeMap attributes = element.getXml().getAttributes();
if (attributes.getLength() > 1) {
List<String> extraAttributeNames = new ArrayList<String>();
for (int i = 0; i < attributes.getLength(); i++) {
Node item = attributes.item(i);
if (!(SdkConstants.TOOLS_URI.equals(item.getNamespaceURI()) &&
NodeOperationType.NODE_LOCAL_NAME.equals(item.getLocalName()))) {
extraAttributeNames.add(item.getNodeName());
}
}
String message = String.format(
"Element %1$s at %2$s annotated with 'tools:node=\"removeAll\"' cannot "
+ "have other attributes : %3$s",
element.getId(),
element.printPosition(),
Joiner.on(',').join(extraAttributeNames)
);
element.addMessage(mergingReport, ERROR, message);
}
}
private static void checkSelectorPresence(MergingReport.Builder mergingReport,
XmlElement element) {
Attr selectorAttribute =
element.getXml().getAttributeNodeNS(SdkConstants.TOOLS_URI, Selector.SELECTOR_LOCAL_NAME);
if (selectorAttribute!=null && !element.supportsSelector()) {
String message = String.format(
"Unsupported tools:selector=\"%1$s\" found on node %2$s at %3$s",
selectorAttribute.getValue(),
element.getId(),
element.printPosition());
element.addMessage(mergingReport, ERROR, message);
}
}
private static void validateManifestAttribute(
MergingReport.Builder mergingReport, XmlElement manifest, XmlDocument.Type fileType) {
Attr attributeNode = manifest.getXml().getAttributeNode(AndroidManifest.ATTRIBUTE_PACKAGE);
// it's ok for an overlay to not have a package name, it's not ok for a main manifest
// and it's a warning for a library.
if (attributeNode == null && fileType != XmlDocument.Type.OVERLAY) {
manifest.addMessage(mergingReport,
fileType == XmlDocument.Type.MAIN ? ERROR : WARNING,
String.format(
"Missing 'package' declaration in manifest at %1$s",
manifest.printPosition()));
}
}
/**
* Checks that an element which is supposed to have a key does have one.
* @param mergingReport report to log warnings and errors.
* @param xmlElement xml element to check for key presence.
* @return true if the element has a valid key or false it does not need one or it is invalid.
*/
private static boolean checkKeyPresence(
MergingReport.Builder mergingReport,
XmlElement xmlElement) {
ManifestModel.NodeKeyResolver nodeKeyResolver = xmlElement.getType().getNodeKeyResolver();
ImmutableList<String> keyAttributesNames = nodeKeyResolver.getKeyAttributesNames();
if (keyAttributesNames.isEmpty()) {
return false;
}
if (Strings.isNullOrEmpty(xmlElement.getKey())) {
// we should have a key but we don't.
String message = keyAttributesNames.size() > 1
? String.format(
"Missing one of the key attributes '%1$s' on element %2$s at %3$s",
Joiner.on(',').join(keyAttributesNames),
xmlElement.getId(),
xmlElement.printPosition())
: String.format(
"Missing '%1$s' key attribute on element %2$s at %3$s",
keyAttributesNames.get(0),
xmlElement.getId(),
xmlElement.printPosition());
xmlElement.addMessage(mergingReport, ERROR, message);
return false;
}
return true;
}
/**
* Validate attributes part of the {@link com.android.SdkConstants#ANDROID_URI}
* @param mergingReport report to log warnings and errors.
* @param xmlElement xml element to check its attributes.
*/
private static void validateAndroidAttributes(MergingReport.Builder mergingReport,
XmlElement xmlElement) {
for (XmlAttribute xmlAttribute : xmlElement.getAttributes()) {
AttributeModel model = xmlAttribute.getModel();
if (model != null && model.getOnReadValidator() != null) {
model.getOnReadValidator().validates(
mergingReport, xmlAttribute, xmlAttribute.getValue());
}
}
}
/**
* Validates attributes part of the {@link com.android.SdkConstants#TOOLS_URI}
* @param mergingReport report to log warnings and errors.
* @param xmlElement xml element to check its attributes.
*/
private static void validateAttributeInstructions(
MergingReport.Builder mergingReport,
XmlElement xmlElement) {
for (Map.Entry<XmlNode.NodeName, AttributeOperationType> attributeOperationTypeEntry :
xmlElement.getAttributeOperations()) {
Optional<XmlAttribute> attribute = xmlElement
.getAttribute(attributeOperationTypeEntry.getKey());
switch(attributeOperationTypeEntry.getValue()) {
case STRICT:
break;
case REMOVE:
// check we are not provided a new value.
if (attribute.isPresent()) {
xmlElement.addMessage(mergingReport, ERROR, String.format(
"tools:remove specified at line:%d for attribute %s, but "
+ "attribute also declared at line:%d, "
+ "do you want to use tools:replace instead ?",
xmlElement.getLine(),
attributeOperationTypeEntry.getKey(),
attribute.get().getPosition().getLine()
));
}
break;
case REPLACE:
// check we are provided a new value
if (!attribute.isPresent()) {
xmlElement.addMessage(mergingReport, ERROR, String.format(
"tools:replace specified at line:%d for attribute %s, but "
+ "no new value specified",
xmlElement.getLine(),
attributeOperationTypeEntry.getKey()
));
}
break;
default:
throw new IllegalStateException("Unhandled AttributeOperationType " +
attributeOperationTypeEntry.getValue());
}
}
}
}